Neutrogena light mask part 3: about that LCD…

In Part 1 I hacked it to get 99 lives.
In Part 2 I did an excessively thorough analysis of the current limiting.
In Part 4 I found it keeps resetting on life 83.

The light mask comes with an LCD. I’ve always been curious about driving them, but never really taken the time to look. So, I figured I’d get my scope out and have a go. First off, it’s two seven segment displays. Rather handily, Neutrogena have left nice large test points for me. And if you look closely they even saved me the hassle of counting the number of them! There are 8. So, it must be a multiplexed display. Assuming it’s even vaguely symmetric, it’s got to be something like 5×3 or 4×4 (with some dead spots). So, time to break out the scope! First though, I have to solder some wires on:

Wiring the LCD into a breadboard by using the test points.

Except they don’t fit under the LCD. Plus if you connect the power up backwards, then it appears to stop working. Why knew? Fortunately I have one more spare. The failure mode seems to be that one digit is no longer working (the LCD works though—I rubbed some power supply leads across the zebra strip and they all lit up.). Weird.


Yes OK, so it’s actually working fine (which is weirder) it’s just that it displays “0”, not “00” because it’s made for end users who aren’t expecting everything to be in nicely formatted hex…

So, I’m vaguely aware of some things about LCDs, in no particular order:

  • They activate off DC, but that eventually destroys them so you should use AC.
  • Driving simple (non multiplexed) LCDs is easy.
  • Driving multiplexed ones is harder than you’d expect .
  • There’s a threshold voltage below which they don’t turn on.

And here’s what the waveform for the first 4 pins looks like:


I blame my scope. No way it’s a PEBKAC error. There are like, 4 levels and they go all over the place. It’s crazy.

OK, that is indeed harder than I’d expect. Reading around was a bit boring, confusing and badly written. There’s some information which indicates it’s possible to drive them off only 2 levels. This would make sense: if you want to make a particular row/column pair light up, then you drive those two out of phase. Everything else…

Oh I see.

So, I guess the other rows have to be driven in-phase with the column, and the other columns… hmm. OK, I guess that’s why they have multiple levels. If you have a 5V supply, and you drive the row/column out of phase, the one intersecting segment sees 10v pk-pk. If you drive the other rows and columns with an idle voltage (say 2.5V DC) then the segments see either 0V if neither the row/column is driven or 5V pk-pk if a row/column is driven and the column/row is idle.

Backing up, imagine driving a matrix of light bulbs (not LEDs because I don’t want to get bogged down in Charlieplexing) from switches:


A multiplexed display matrix with one rwo switch and two column switches closed.

Switches are either closed, which on a microcontroller means driving the pin as 1 or 0 depending on which rail the switch is connected to, or open which means tristated. For current driven things, like light emitters, it’s easy: tristated means no current flows. For voltage devices, it simply means “not actively driven”, so something needs to be done with it, i.e. bias it to some neutral voltage.

I have no idea which things are rows and which things are columns on the display. However, I do know that the voltages are 0, 1.6, 3.3 and 5V. I’d hazard a guess that 3v pk-pk (i.e. a neighbouring pair) won’t drive the display but 10V pk-pk will. Not sure about 6.7. Probably?

Well, I’ve got an Arduino and some voltage regulators. For a 5V drive, I can easily get 0, 2.5V and 5V by tristating an output and pulling it to a central value:


Pulling the pins to a 2.5V rail made with a potential divider and a TS358 opamp (costs 6p!)

I used 47k because it’s high ish (won’t load the Arduino or opamp much) and I have plenty of them. Anything in the range of about 1k to several meg would probably work OK.

I could use the 3.3V output as the central value but honestly that seems to be tempting fate and I don’t know if it can sink current. Instead, I’ll use the entertainingly cheap TS358CD op-amp. So, time to cut out the useless remains of the device and wire up the Arduino!

Tracks cut with a stanley knife

I can still fix this!

Also, the test pads aren’t all they’re cracked up to be: the joints need to be filed down. Even now I’m not sure I’m getting perfect contact between the LCD and the board.


I had to file the tops of the joints down very carefully (totally unnecessary as it transpired).

Anyway I’ve wired up pins 1-8 on the arduino (not 0!) to 1-8 on the LCD, more for my own sanity than anything else. And with a simple program:

void setup(){}

void loop() {

  pinMode(1, OUTPUT);
  digitalWrite(1, HIGH);
  digitalWrite(1, LOW);
  pinMode(1, INPUT);

I get this scope trace:SCR03

This had me going for quite a while. The pin set to input should be getting 2.5V.  But it’s not; it’s being pulled up. I looked up: the internal pullup is 20k. The voltage is consistent with that: 2.5 * 47 / (47 + 20) + 2.5 ≈ 4 ish.


Well, that had me going for a while. I went back and forth with trying to figure out how to turn the pullup off (it’s off by default), turning it on and seeing what happened, plugging and unplugging wires and all that. Turns out that I was using pin1 which on the Arduino is the TX pin for the serial port if you want it to be. That means it has an LED attached which is doing extra pullup effectively to almost exactly the right value.

Ho ho ho.

So looks like I’ll be using pins 2-9 instead and I won’t get to keep my sanity. But at least that works.  Also I realised after a, er, little debugging that the reason the device has screws next to the LCD is so that they push the board against the zebra strip ensuring good contact. I wonder if I should use those screws…

Anyhow, now I think what remains is to so a somewhat exhaustive search over all pairs of wires driving them in opposition, to see what happens when they’re activated.

static const int A=2, B=3;
void setup(){
  pinMode(B, OUTPUT);
  pinMode(A, OUTPUT);

void loop() {
  digitalWrite(A, 1);
  digitalWrite(B, 0);
  digitalWrite(A, 0);
  digitalWrite(B, 1);

That only took a few minutes: it’s only actually 28 combinations. Here are the results I’ve noted down, along with the matrix that’s implied by the numbers. I’ve written the numbers as pairs indicating the two pins that have been activated:


Oh actually! It’s even better! This proves that not only is 10v pk-pk sufficient to drive the segments (I was sure of this), but 5v pk-pk isn’t, which I wasn’t so sure about. That’s nice: no extra circuitry is required. So, what we have is a 4×4 matrix. What I’m going to do is drive each of the rows in turn, while driving all 4 columns simultaneously.

The mapping is very regular though and we actually have essentially two 4×2 matrices for the two digits. The plan: each digit will be represented by a 7 bit number, one bit for each segment. Then, a 4×2 matrix will be represented as 4 2 bit numbers. The next step is a little tedious and involves designing the segment pattern for each digit and figuring out the mapping of segments to columns for each row. I’ve gone for A-Z (excluding k,m,v,w,x), picking the lowercase letter in general when there’s no other criteria and of course 0-9:


And that’s most of it. All that remains is to write a program which drives the rows in sequence and sets the correct column pattern for each row. Also, I’m going to have a function that maps ASCII to the segment patterns so I can write stuff on the display. My choice of driving is to drive each row low in turn, then repeat with each row high in turn to make it AC. I did try row 1 high then low, then row 2 high then low etc too but it didn’t make much difference. Here’s he code:

void setup(){};

//Decode uppercase ASCII and numbers into
//a list of segments, stored as a bit pattern.
uint8_t ascii_to_code(uint8_t c)
  static const uint8_t letters[26] = {
  static const uint8_t numbers[10]={

  if(c >= 65 && c = 48 && c >4 | (n&64)>>6;
uint8_t r3(uint8_t n){
 return (n&16)>>3 | (n&4)>>2;
uint8_t r2(uint8_t n){
 return (n&8)>>2;

//Set the column outputs as driven or inactive
//according to the input bit pattern. Polarity
//determins whether to drive high or low.
void set_columns(uint8_t n, bool polarity)
  for(uint8_t i=0; i < 4; i++)
      pinMode(i+6, OUTPUT);
      digitalWrite(i+6, polarity);
      pinMode(i+6, INPUT);

void display_digit(uint8_t left, uint8_t right)
  //Columns entries for both digits for
  //the 4 rows.
  uint8_t rows[4];
  rows[3] = r5(left)<<2 | r5(right);
  rows[2] = r4(left)<<2 | r4(right);
  rows[1] = r3(left)<<2 | r3(right);
  rows[0] = r2(left)<<2 | r2(right);  

  //Do positive columns/negative rows first
  //then repeat with the polarity inverted
  //Activate each row in turn and set the
  //column entries for that row.
  for(int p=0; p  100){

There was a lot of fiddling around, most of which did very little. Basically, driving a multiplexed display off 3 levels is pretty marginal. I found that often some digits would ghost on, and others would be very faint. I could increase the contrast by lengthening the amount of time it took to draw the whole display, by driving a row with many cycles then moving on to the next row, but it only got really good when it was far far too slow. I did find putting a slight pause in between driving rows did help. Removing it darkened everything including ghosted on digits, lengthening it lightened everything. The value I got was about the best one I could find.

Here's what it looks like:

It's not perfect, you can see the contrast is a bit poor and so "hELLo" comes out looking a bit like "hECCo". Nonetheless I'm moderately pleased, since it does kind of work. I have to be careful with the choice of symbol though because they're not all that good.

3 thoughts on “Neutrogena light mask part 3: about that LCD…

  1. Pingback: Hacking the Neutrogena visible light therapy system to get 99 lives | Death and the penguin

  2. Pingback: Neutrogena light mask part 2: down the rabbit hole | Death and the penguin

  3. Pingback: Neutrogena light mask: annoying resets | Death and the penguin

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s