I found an old beaten-up CD player a while back, and decided to take out the human interface card, with the LCD and buttons on it.

In a previous episode, I managed to get the LCD to do something. Now, I will try to let it do exactly what I want it to do!

The first task is to find out what segments is controlled by what segment to the chip(s). In Controlling the card, I found out how to send the chips data — basically I have to send an 80-bit sequence to the two chips, of which 2*4 bits are chip control data. This leaves 72 bits of data to control 57 segments, so some of the chip’s capacity was unused.

My first plan of attack is simple: in the previous episode I learnt how to listen for keypresses, so my idea was to make a simple keypress-driven loop trough all the 72 data bits, to see which segment will light up.

I found an old CD player and am slowly turning it into a kitchen clock/timer, and teaching myself hacking along the way. Instances of this series are:

Controlling individual segments

I decided to make a byte array (size 10 = 80 bits) that holds both the LCD segment data and the control data for the chips. And then a function that will add the necessary control data and send it off to the LCD.

// LCD Variables
byte LCDSegments[10]; 

/**
 * Send out data towards the LCD driver chips, based on a type
 * type = 0: default
 * type = 1: Synchronize
 * type = 2: Initialize master chip (right after initialization)
 */
void sendLCD(uint8_t type) {
    setLCDControlData(type);

    // send data to first chip
    digitalWrite(chip_1_select_pin, LOW);
    if (type == 1) { digitalWrite(chip_2_select_pin, LOW); } // in synchronisation phase, send data to both chips
    // shift out the first 8 bits
    for (int j=0; j < 5; j++) {
        shiftOut(data_pin, clock_pin, LSBFIRST, LCDSegments[j]);
    }
    digitalWrite(chip_1_select_pin, HIGH);
    if (type == 1) { digitalWrite(chip_2_select_pin, HIGH); } // in synchronisation phase, send data to both chips
    
    // only in the default phase: send data to second chip
    if (type==0) {
        digitalWrite(chip_2_select_pin, LOW);
        for (int j=5; j < 10; j++) {
            shiftOut(data_pin, clock_pin, LSBFIRST, LCDSegments[j]);
        }
        digitalWrite(chip_2_select_pin, HIGH);
    }
}

// sendLCD with a type different than 0 will be used only on setup. So to make it easy,
// overload sendLCD with no arguments to mean type=0.
void sendLCD() {
    sendLCD(0);
} 

/**
 * Sets the control data in the right bytes of the chip
 * type = 0: default, 0001 to master, 0110 to slave
 * type = 1: Synchronize, 1110 to both chips.
 * type = 2: Initialize master chip 0000 to master chip (chip 0)
 **/
void setLCDControlData(uint8_t type) {
    const static byte mask = B11110000;
    if (type==0) {
        LCDSegments[4] = LCDSegments[4]&~mask | (B0001<<4)&mask;
        LCDSegments[9] = LCDSegments[9]&~mask | (B0110<<4)&mask;
    } else if (type==1) {
        LCDSegments[4] = LCDSegments[4]&~mask | (B1110<<4)&mask;
        LCDSegments[9] = LCDSegments[9]&~mask | (B1110<<4)&mask;
    } else if (type==2) {
        LCDSegments[4] = LCDSegments[4]&~mask | (B0001<<4)&mask;
    }
}

The byte LCDSegments[10]; line initializes the array.
The upper sendLCD(type) function first sets the chip control data on the chips, using the setLCDControlData(type) function, and then shoves the data to one or both of the chips, depending on the type.

Remember from Controlling the Card that the LCD is driven by two COP472N-3 chips, and that they need 5 bytes of data each, where the last 4 bits of the last byte is control data for that chip.
Upon startup, the two chips need to be synchronized together, by first sending 1110 to both chips, then 0000 to the master chip only. Only then they can work together (1111 and 0001 work as well, more on that later).
This is where the setLCDControlData(type) comes in play: it sets the right data, depending on the type (or moment the function is called), where type=0 is normal operation, and type=1 and 2 are used only in setup.
What is happening in setLCDControlData(type) is that the last four bits for each chip (byte 4 and 9 of the LCDSegments array) are set to their corresponding data.

The two important lines here are:

const static byte mask = B11110000;
LCDSegments[4] = LCDSegments[4]&~mask | (B0001<<4)&mask;

Working with a byte array, it is not possible to directly access a single bit and change it, and this is where so-called bit masking comes into play. Basically the mask (the term comes from a mask used in photography, now mainly used in chipmaking) selects the bits an operation has effect on. So here, the mask B11110000 sets the four bits with a 1, which are the four most significant bits (the first four when reading like this, but the last four to be sent off). Oh, and the B in front of what follows means the next is a binary number, not a decimal number just over 11 million.

In the LCDSegments[4]&~mask part, three things happen: the 5th byte of LCDSegment is taken, then it is so-called ANDmasked (the &) with the inverse or NOT (~) of the mask.

mask           = 11110000
                 -------- NOT (denoted by ~)
~mask          = 00001111 (inversion of mask)
LCDSegments[4] = 10101010
                 -------- AND (denoted by &)
result_1         00001010

The NOT-operator (the ~) inverts the bits of a byte. If they were 1 before, they change into a 0, and vice versa.
The AND operator needs two inputs, and sets the output to 1 only if the corresponding input bits are both a 1.

Together the NOT and AND save or select only the rightmost part of LCDSegments[4] (in this example assumed to be 10101010, while the leftmost part is discarded.

Then in the rightmost part of the line, I have (B0001<<4)&mask. Here, the B0001 is shifted 4 bits to the left, basically it is padded with 4 (zero) bits at right. I could just as well have written B00010000, but this code makes it clearer only the 0001 part is important. Then the &mask does the same as previously:

B0001          =     0001
                 -------- Shift 4 bits (denoted by <<4)
                 00010000
mask           = 11110000
                 -------- AND (denoted by &)
result_2         00010000

In this case, the masking was not necessary, because I only need the leftmost 4 bits and the rightmost are already zeroes due to the shifting. But if my mask changed, I would be in trouble. Better to make it safe this way.

Then the center of the line is a lone | — this is the bitwise OR which works a bit like AND but outputs a 1 if either input (the first OR the second) is a 1 (but also if both are a 1!)

result_1       = 00001010 
result_2       = 00010000
                 -------- OR (denoted by |)
end_result       00011010

This all basically is machine code for “set the left 4 bits to 0001 as and the right 4 bit to whatever they were”.

Back to our main code in sendLCD(type): the rest is basically the same code that was already seen in Controlling the card, with first a block to send data to the first chip, and then to send it to the second chip.
Note that during synchronisation I first need to send the same control data to both chips at the same time, which is why there are some extra lines to set chip_2_select_pin to high and later low. Now that I think of it, this code sends the first 5 byte of segment data to both chips, while the first 5 bytes are then later set to only chip 1, which means that I can’t send data uniquely to chip 2 on the initialization. I could change that — and I have, but I posted too much code already, so that I leave up to you.

Oh, and there’s a second function sendLCD(). Whenever I want to send something to the LCD, it will be type=0 except on initialization of the LCD. I am lazy (as any good programmer is) so I don’t want to send that 0 every time. In some programming languages — Arduino (which is C) is one of them — you can overload functions so there are two different function calls with the same name, but different count or type of arguments, with different function bodies. In this case, sendLCD() is just a shortcut to sendLCD(0);, which is literally all the code there is in the sendLCD() function.

Setting the right segment (easy)

I need one more support function to easily set one single bit in the LCDSegment[]-array to on or off.

void setLCDSegment (int segmentNumber, bool value) {
    uint8_t digit = (int)segmentNumber/8;
    LCDSegments[digit] = LCDSegments[digit]&~(B1<<segmentNumber%8) | value<<segmentNumber%8;
}

The setLCDSegment function first makes a variable digit to select which byte of the LCDSegments array I want to select, by dividing the segmentNumber (which is a bit counter) by 8 to turn it into a byte counter (byte) would be a nicer name, but that is a reserved word.

The next line basically follows the same trick as explained above, with bit masking, but a little more complicated, as I don’t have a fixed mask anymore.
The left half, before the OR operator, shifts B1 (a single “1” bit) by the remainder of segmentNumber devided by 8, denoted by the % operator, and then negates that, and uses that on a mask on LCDSegments[digit] to keep every bit but that selected bit.

For example, if LCDSegments[2]=10101010 and segmentNumber=19, then digit is 2 (19/8 rounded down) segmentNumber%8 is 3 (the remainder of 19/8)

B1             = 00000001
B1 << 3        = 00001000 

~(B1 << 3)     = 11110111
LCDSegments[2] = 10101010
                 -------- AND (denoted by &)
result_1         10100010

So I selected all but the 5th bit from the left from LCDSegments[2].

On the right side of the OR, value<<segmentNumber%8 shifts the boolean value. A boolean is just a 0 (when false) or a 1 (when true). We don’t need a mask here, as the boolean is already a single bit and by shifting itself forms both the mask and the data we want to set!

Which bit is what segment?

I now had an easy way to send off all data to the LCD, but I still didn’t know which bit in the LCDSegments[] corresponded to what segment of the LCD being switched on.

I made a little counter segmentNumber that would in- or decrement the segment to be displayed on the press of a button:

void loop()
{
    scanKeys();
    if (keyState[8] == 2 | keyState[8] == 3 | keyState[8] == 5) {
        setLCDSegment(segmentNumber,false);
        segmentNumber++;
        if (segmentNumber>=80) {segmentNumber=0;}
        setLCDSegment(segmentNumber,true);
    }
    if (keyState[0] == 2  | keyState[0] == 3 | keyState[0] == 5) {
        setLCDSegment(segmentNumber,false);
        segmentNumber--;
        if (segmentNumber<0) {segmentNumber=79;}
        setLCDSegment(segmentNumber,true);
    }

    sendLCD();   
    delay(mainLoopDuration);
}

If I press or hold button 8, I will increment the segmentNumber counter by 1, and decrement if I press key 0.
If the segmentNumber goes over 79, I set it to 0, so to make it wrap around. First I set the LCD segment of the old segmentNumber to false, in- or decrementsegmentNumber and then set the new segmentNumber to true.

Yay! I just rolled trough all the segments.

Note that the REPEAT is the very first segment — the segmentNumber started at 0 but the LCD only gets set initially when I in or decrease it.

I stepped trough all the digits again and noted what segment is in what place, written down here in byte/bit number (or only bit for the 7-segment digits)

My scribblings for which segment was enabled by what bit. For the 7-segment digits, the number denotes the byte, and the diagram at right denotes the bits for a 7-segment display, which are equal for all of the 6 7-segment digits.

The digits

I am quite lucky that the 6 numerical digits all have the same layout, being made from bits 1-7 of byte 0-3, 5 and 6. This means I can re-use the code to set a segment to.
I quickly made this table (see image above) for what bits to set to show a number on the digits. Note that I made the table the “wrong” way around, so a 0 is 0111111 in my table but in my program it’s B1111110(0), with the rightmost bit being an entirely different segment (in case of the first byte, that is REPEAT).

I made a function to directly set a whole numeric character on a certain digit.

/** 
 *  Set a certain digit to a certain number. Digts are numbered 0-5 (left to right)
 *  @input digit digit numer 0-5 (left to right)
 *  @input number 0-9 to set number 0-9, 10 for off
 */
void setLCDdigit (uint8_t digit, uint8_t number) {
    const static byte numbers[11] = {
        B1111110, B0110000, B1101101, B1111001, B0110011, // 0,1,2,3,4
        B1011011, B1011111, B1110000, B1111111, B1111011, // 5,6,7,8,9
        B0000000};// add 10 for off
    const static byte mask = B11111110;
    
    if (digit>=4) { digit++; } // digits 4 and 5 go to LCDSegments 5 and 6, resp.
    if (number>9 || number<0) { number = 10; }
    
    LCDSegments[digit] = LCDSegments[digit]&~mask | (numbers[number]<<1)&mask;
}

The const static byte numbers[11] is an array of 11 bytes, which is constant (it can’t be changed) and static (so it gets created only once — without this it would be lost everytime the function ends and recreated on each call of the function).

The bytes are 11, 10 each for the segments to turn digit 0-9 on, and the tenth to completely switch off a digit’s segments. This is useful if you want to display the time as 4:52 instead of 04:52.

The if (digit>=4) { digit++; } line says that if I’m selecting digit 4 or 5, I shoul dset it to LCDSegments[5] or [6] respectively, as LCDSegments[4] does not hold data for a numerical digit data (it holds the control data for the first chip in 4 of its bits, so it can’t fit 7 segments in the remaining 4 bits).

Then line 16 is similar to what I did before: change the left 7 bits of a digit to whatever is needed to make a segment, and keep the rightmost bit at what it was.

This works quite well. I made a very small, hackerish code that prints out the time since the arduino was started on the 6 digits that is so cruedely programmed I don’t want to show it to anyone.

Ok, here is the code:

// demo function
void printTimeSinceBoot() {
    unsigned long time = millis();
    int d1 = (time/=10)%10; //100th sec
    int d2 = (time/=10)%10; //10th sec
    int d3 = (time/=10)%10; //sec
    int d4 = (time/=10)%6; // 10 sec
    int d5 = (time/=6)%10; // minute
    int d6 = (time/=10)%6; // 10 minute
    setLCDdigit (5, d1);
    setLCDdigit (4, d2);
    setLCDdigit (3, d3);
    setLCDdigit (2, d4);
    setLCDdigit (1, d5);
    setLCDdigit (0, d6);
}

The other segments

Next to the 6 numeric 7-segment segments, there are 15 more segments:

I made constants out of all these digits:

// LCD Constants
const uint8_t LCD_REPEAT    =  00;
const uint8_t LCD_PROGR     = 041;
const uint8_t LCD_SCAN      = 010;
const uint8_t LCD_PAUSE     = 042;
const uint8_t LCD_SHUFFLE   = 040;
const uint8_t LCD_N1        = 043;
const uint8_t LCD_N2        =0110;
const uint8_t LCD_N3        = 050;
const uint8_t LCD_N4        =0111;
const uint8_t LCD_N5        = 060;
const uint8_t LCD_N6        =0112;
const uint8_t LCD_COLON     = 030;
const uint8_t LCD_CD1       = 072;
const uint8_t LCD_CD2       = 073;
const uint8_t LCD_BEAM      = 020;

These are the segment numbers in octal. The reason I use octal notation here (number preceded by 0) , is that it conforms nicely with how the LCDSegments[] array is laid out mentally: the first or first two digits are the byte in which a segment is stored, the last digit is the bit number. It would have made no difference at all if I had set the PAUSE button to 34, B00001010 or 0x22 (the latter is hexadecimal). It’s just that right here, the octal comes closest to the map I had made of where the digits go — Just as in masking I could have used a decimal number instead of a binary number, but there using binary is more fitting to the mental model. The compiler compiles it all into binary numbers anyway.

Table with what byt (vertical) / bit (horizontal) controls what segment. Note that for bits 0,1,2,3,5 and 6, I didn’t write out the individual bits for the 7-segment display.

With these constants, I can just use the void setLCDSegment function, e.g. setLCDSegment(PAUSE, false) to switch off the PAUSE digit.

Two little tests

There were two little odds and ends I was curious about controlling this LCD, so I tested them.

Killing the backlight

I thought that I maybe could kill the backlight by pulling the clock signal high with a digitalWrite(clock_pin, HIGH); before going trough the main loop’s delay(mainLoopDuration);. That works in that the backlight goes off, but the display is hardly legible, and if you change the display fast enough after a while it shows spurious digits flickering — so no viable option.
And when I say hardly legible, I can see it with the naked eye when I look at it from the right angle, but I didn’t manage to take an even barely legible photo.

Control bits

The datasheet for the COP472WM-3, on page 5, speaking about the control bits, says “The fifth bit is not used” (note fifth counting from right). However, in the example it shows specific values for these control bits (x110xxxx for slave and x001xxxx for master). I want to try if this is necessary, or whether the datasheet writers picked a value just to save the need to explain “doesn’t matter” or people getting confused on how to write an “X” bit.

I tried a few combinations for the fifth bit in my setLCDControlData-function: all off, all on, inverted from what the datasheet said. I see no difference at all. So it must’ve been a case of “pick one for the user”. But still, it would’ve been easier if they would have picked the 0 (or 1) for all values. Now it seems as if it’s an unexplained magic bit.

Next steps

I have a few things I can do as next steps:

  • Implement the real time clock
  • Start building the case
  • Implement the start of the “main” program which implements the timers.
  • Clean the code

Especially that last bit is in dire need. My code isn’t really dirty, but it’s now one file with 8 functions (not counting the setup() and loop()) and 5 of these functions and >100 lines of code, in the preamble, setup and loop are supporting functions to drive the LCD, and there probably is more to come. It would be best to put that all in a separate file or library.

Join the Conversation

2 Comments

  1. awesome any updates? i have recently found a cop472 attached to an lcd and came across your website.. awesome write up!!

    1. Ha, cool someone finds it useful. It is certainly a project I want to continue, but lately I lack the time due to being very very busy with the covid situation at work.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.