Reading Raw Clock LCD Signals

The picture above shows an arduino interpreting the drive signals of an LCD clock and feeding them to the serial port. A truly silly way to tell the time but an interesting exercise.

It’s a follow-on to this http://arduino.cc/forum/index.php/topic,51464.0.html project where I was driving the same LCD directly from the arduino and someone on hack-a-day posed the question of whether you could READ the LCD signals.

These clocks were remaindered at Staples after Christmas. They have motors and wheels to run around and, more importantly for this project, they have LCDs with relatively huge, easy to solder, connectors.


LCDs like this are multiplexed with four common connectors called backplanes that go sort-of horizontally across the display and eight segment connectors that address separate vertical slices. To activate a particular segment/backplane you put a square wave across the two with at least a couple of volts potential. To keep the other segments from lighting up you keep them no more than 1.5 volts from the backplanes. The segments can’t stand ANY DC voltage so you have to weave a complex pattern to light up the ones you want at any given time. My driver circuit used only three voltages in a simple pattern but commercial drivers use more levels, probabl to get more separation between signal levels. See Smart | Connected | Secure | Microchip Technology and the picture below.

When I was developing my LCD driver proof of concept I had to map the segment and backplane connectors of the LCD. Each of the three full digits occupies two of the segment drivers across the four backplanes as shown below for the second digit. The first digit is a special case using only one segment connector.

The picture below shows the four backplanes waveforms and one pair of segment waveforms lighting up the segments for the digit “2”. The time for each part of the cycle is about 4 ms.

If you look for example at state 0, BP1 is at 0 volts, S2 is at 1 volt, and S3 is at 3 volts. With a 1 volt difference S2 will not be activated but with a 3 volt difference, S3 will be active. So the topmost segment which is where S3 and BP1 intersect will be active. Similarly if you look at the other states you can trace which segments are active. Generally the second four states are a mirror image of the first four.

In principle then, you can tell if a segment is lit by continuously reading the backplane and segment waveforms and watching for voltage differences. There are two things wrong with that, both have to do with the arduino’s analog inputs.

The first problem is that the arduino’s analog pins present about a 10K impedance and that is low enough to interfere with the LCD driver. Once I hooked up more than one or two segments I got the whole LCD fading on me – oops. A simple op-amp buffer circuit worked a treat to beef up the signal and keep from loading down the clock’s LCD drivers.

The second problem is that to read all seven digit signals and the four backplanes would take 11 analog inputs and the arduino has only six. I worked with the four backplanes and a single digit while I struggled with this and gradually came to a solution. Because the backplane signals always repeat in a cycle of eight transitions, I only had to read one of them to synchronize with the cycle then I would know the other three automatically. Also, I realized that I could use the op-amps as more than buffers: I made a second op-amp circuit that multiplied the input voltage by 10 so anything except a zero would look high to the arduino. This worked perfectly to help me synchronize with one backplane and to interpret the signal on the first segment connector.

The picture below shows the whole setup.


The ribbon connector on the right carries the backplane and segment signals from the clock. It also connects the grounds and carries power back to the clock. The diode at the extreme right drops the 5v of the arduino a bit for the clock which runs normally on 4 Aas.

The first op-amp amplifies the signals of the first backplane and the first segment pins and connect to digital pins 2 and three respectively. The other three are the buffers for the six segment pins driving the 3 other digits and connect to the analog pins.

The code below uses the backplane on digital pin 2 to synchronize itself with the clock. It establishes a state variable and increases the variable every four ms. In each state it samples each of the segments and compares it with the known backplane voltages for that state. If a segment shows a difference of two volts from a backplane it records the segment and backplane in the seglit array. It doesn’t care which state or how many times the segment is lit – just that it is. After the eight cycles are done, it assembles the segments for each digit and looks up in a table to see what digit is represented by that pattern. It sends the digits out on the serial port where it’s displayed by the pc.

The version below just runs once. I’ve had it rigged to run continuously and watch for changes in the digits to send back but it tends to get flakey results doing that and I’m not sure why.

Conclusion.
So, this is doable but not all that practical. You have to know the details of the waveform your circuit uses and be prepared to solder into it. I can see some special case where knowing the state of a particular segment you would need only two connection points but it might be easier to either watch that spot with an optical sensor or trap an output like an alarm.

Here's the code for the clock lcd interpreter, my post was getting too long:

//program to read a clock lcd
//backplane and segment pins are on a combination of analog and digital pins

#include <Streaming.h>
#define cout Serial
#define endl '\n'
/*lcdreader*/

const int BP0=2; //1st backplane is on digital pin 2
const int spins1=3; //first segment pin is on digital 3 
const int Spins[6]={
  3,2,4,1,5,0}; //analog pins for segments 2,3,4,5,6,7
byte backplanes[4][8]={
  {0,1,1,1,3,2,2,2}, //values of backplanes 1 to 4
  {2,1,1,3,1,2,2,0}, //in each of 8 states
  {2,1,3,1,1,2,0,2},
  {2,3,1,1,1,0,2,2}
};
  

int state=-1; //state is not initialized
const int s1keystate=2; //key state to check segment 1
byte seglits[8]={  0,0,0,0,0,0,0,0}; //each one hold the bits for a single segment
byte segments[4]={0,0,0,0}; //bits combining 2 segments for a single digit
byte digits[4]={11,11,11,11}; //holds final assembled digits - initially set invalid
long unsigned basetime; //will be set to midpoint of first period

void setup(){
  Serial.begin(115200); 
  Serial.println("LCD interpreter version 1.2");
}



void loop(){
  if (millis()<60){
    limitedloop(); //limit how long this can run
  }
}

void limitedloop(){
  if (-1==state){
    synchstate();
  }  //get leading edge of state 0
  processstate();    
}

int ivolts(int analogval){ //convert an analog reading to integer volts with rounding
  int v=(analogval*5+512)/1023;
  return v;
}

void processstate(){
  while (millis()<=basetime+state*4); //spin til next state
  for(int s=0;s<=5;s++){ //read the analog pins for the segments
    int rawval=analogRead(Spins[s]); //save the raw reading
    int vval=ivolts(rawval); //get the voltage on the segment pin
    for (int bp=0;bp<=3; bp++){ //compare the segment to each backplane
      int delta=vval-backplanes[bp][state];
      if (abs(delta)>=2){ //if the voltage delta is 2 or greater
        seglits[s+2]|=1<<bp; //light the appropriate segment (skip 1st 2 spots)
      }
    }
    if((s1keystate==state) &&(0==digitalRead(spins1))){ //special case for segment 1
      seglits[0]=0;seglits[1]=6; //fake up the result of reading seg 1 as "1"
    } 
    else{
      seglits[0]=0x0a; seglits[1]=0x0f; //blank will show as 0
    }
  }
  state++; //prep for the next state
  if (state>7){  //at the end of the cycle
    for (int s=0;s<4;s++){ //for each digit
      segments[s]=seglits[s*2]<<4|seglits[s*2+1]; //combine the bits
      digits[s]=segmentstodigits(segments[s]&0xEF); //translate to a digit (exclude colon bit)
    }
    cout<<_HEX(digits[0])<<_HEX(digits[1])<<":" <<_HEX(digits[2])<<_HEX(digits[3])<<endl; //output the result
    seglits[0]=0; seglits[1]=0; seglits[2]=0; seglits[3]=0; seglits[4]=0; seglits[5]=0; seglits[6]=0; seglits[7]=0; //reset the segments
   state=-1; //reset the state
  }
}

byte segmentstodigits(byte segments){
  const byte patterns[10]={
    0xAF,0x06,0xCB,0x4F,0x66,0x6d,0xED,0x07,0xEF,0x6F  };
  byte whichdigit=10; //return a non-digit if we don't find the pattern
  for (int i=0;i<=9;i++){
    if(patterns[i]==segments){
      whichdigit=i;
    }
  }
  return whichdigit;
}

void synchstate(){ //synchronize to state 0
  while((-1==state) && (0==digitalRead(BP0))); //make sure we're not in state 0
  while((-1==state) && (0!=digitalRead(BP0))); //wait for state 0
  if(-1==state){
    delay(1); //move well into the state period
    basetime=millis();
    state=0;
  }
}

Congratulations. Seems really challenging to me !
And at the same time, as you already know, it is totally useless (so please don't be ofended).
As a combination of both I would say your project is a very strange artwork that few people will be able to appreciate.
I think I do :slight_smile:

Hi Bill2009,

I try to catch and decode my lcd weighing scale in the same way that you did but unfortunately my LCD isn't drived as yours.

I have found a topic that do this job with the same LCD as I have but it's not written for the Arduino platform...too bad :~

The topic is: Google Code Archive - Long-term storage for Google Code Project Hosting.

Could you help me please ?

Regards
Vince

Grundtal 20047 - lcd pins.pdf (140 KB)