How to drive a segment LCD glass with GPIO

4-digit segment LCD glass

A while back when browsing Aliexpress, I saw this 4-digit segment LCD glass that selling for slightly more than USD1.00, so I bought it out of curiosity and this article describes on how to drive the LCD glass with GPIO, no dedicated LCD controller used.

Segment LCD (a.k.a. LCD glass)

Most of the popular LCD displays, such as LCD 1602, it has a LCD controller bonded and exposy-sealed on the back of LCD display. The controller make the interface with the display much simpler and easy to interface with an MCU.

But for the 4-digit seven-segment LCD glass that I purchase, it is a so-called segment LCD or LCD glass, meaning that other the display glass itself, there is no additional electronics parts attached to it. Microchip PIC product line has many MCUs that haave build-in LCD controller as part of the MCU offers, and could be used to drive the segment LCD glass. But I never use PIC microcontrollers before, and has no intension to use it just for this segment LCD, so I'm interested to see how I can "bit-banging" with GPIO pins to control the segment LCD.

The Aliexpress seller's product page provided a a jpeg image which is sort of the "specification" of the display. The "specification" provides two crutial infomration for understanding how the segment LCD should be driven. First, it shows that the display requires 3.3v with a driving condition of 1/4 duty and 1/3 bias. Secondly, the table at the bottom of the image shows that all the segment A and B are connected together and control by the com1, all the segment F and G are connected together and control by the com2, all segment E and C are with com3, and last all decimal point dots, semi-column and segment D are with com4.

"Connection between ATtiny3227 and segment LCD

This is a little bit like charlieplexing, take com1 as an example, depend on how the com1 pin is polarized, either segment A or segment B will be turned on, flipping the polarity, the segment that was previously on, will be off, and the other segment that was previously off, now will be on, so to display a digit, it needs to multiplex each of the com pins with twice with different polarity and go through all the four com pins just to display one digit. This is quite different from seven-segment LED, where all the segment of one digit are connected to one common-anode or common-cathode.

The segment LCD has a 20-pin pin header with 2.0 pitch, so it is not breadboard friendly. As a testing setup, I don't want to directly solder wire to the LCD, I end-up plug solder a 2.00 pitch header socket direct to my ATtiny3227 board, and plug the segment LCD into the header socket. You can see it in the demo video below. If I need to use the segment LCD in my future project, it will definitely need a custom PCB for the project, or to create a carrier board to have a 20-pin 2.54mm pitch header pin for plug into breadboard.

Through my test, I noticed that there is a mistake about the "specification", the table on the "specification" shown that com1 is connected to segment A and B, but through my test, it shows that it is actually in the reverse order, that is com4 is related to segment A and B, and com1 is related to segment D and all the decimal points and semi-column as shown in the following table. I use this table in my program instead of the one shown on the "specification".

LCD 1 2 3 4 12 11 10 9 8 7 6 5
PIN PC3 PC2 PC1 PC0 PB7 PB6 PB5 PB4 PB3 PB2 PB1 PB0
COM COM4 4B 4A 3B 3A 2B 2A 1B 1A
COM3 4G 4F 3G 3F 2G 2F 1G 1F
COM2 4C 4E 3C 3E 2C 2E 1C 1E
COM1 4D 4P 3D 3P 2D 2P 1D P1

Connection with MCU

I use an ATtiny3227 as the MCU for driving the segment LCD using GPIOs. This table also shown the GPIO Port and Pins that I used for connecting to the segment LCD.

The R9 on the following schematic provides the contrast control, the value of R1 should be adjusted based on personal preference and expected viewing angle. I noticed that if the value of the resistor is higher than 470-ohm, the display might have difficulty to start-up. The R1 - R8 are 4 voltage dividers that provides the 1/3 bias voltage specified for each com pin. GPIO pins on Port B are used to drive the 8 segmenets, the first 4 GPIO pins on Port C are used to drive the com1 to com4. Since the LCD is a 3.3V device, so the ATtiny3227 should run at 3.3v as well. That's all we need in hardware configuration as shown in the following schematic. The rest will be done in software.

"schematic for driving segement lcd with attny3227"

Software

Code is written in bare metal by directly access and control the registers of Port B (for segments) and Port C (for com1 - com4). It however should not be difficult to port it to Arduino framework to run under the megaTinyCore. The demostration code implemented a counter counting from 0 to 9999.

First of all, the clock configuration set the MCU to run at 10MHz instead of 20MHz, this is necessary in order for the MCU to be able to operated at 3.3V or even lower (e.g. coin cell at 3.0V). Periodic Interrupt Timer(PIT - as part of RTC timer) is configured to generate an interrupt in every 2ms (32768Hz/64 = 512Hz or 1/512 = 2ms), which is used to trigger the refresh of the display segment, since there are total 4 com pins, and with two different polarizations, so there are total 2 x 8 = 16 fresh cycles to complete the total display refreshment, or 1/16ms = 62.5Hz refresh rate. This is fast enough that human eyes won't be able to perceive the flicking of the display, and yet not too slow to leave the ghost image between the transistion of the segment from on to off or vice versa.

ISR (RTC_PIT_vect) {
    RTC.PITINTFLAGS = RTC_PI_bm;                      // Clear periodic interrupt flag
    refresh_ready = 1;                                // This is a flag for main loop
}

void pit_init () {
    while (RTC.STATUS > 0);                             // Wait until registers synchronized
    RTC.CLKSEL = RTC_CLKSEL_INT32K_gc;                  // 32.768kHz Internal Oscillator
    RTC.PITCTRLA = RTC_PERIOD_CYC64_gc | RTC_PITEN_bm;  // 32768/64=512, i.e. 1/512=2ms between each interrupt
    RTC.PITINTCTRL = RTC_PI_bm;
}

void main(void) {

    static uint16_t interval_counter =  0;

    _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_2X_gc)); // running at 20/2 = 10MHz
    while (!(CLKCTRL.MCLKSTATUS & CLKCTRL_OSC20MS_bm)) {};
    // _delay_ms(100);

    disableUnusedPin();
    PORTB.DIR = 0xFF;         // set all PORTB(segments) pins as output
    PORTB.OUT = segs_out;     // set all segments to low
    PORTC.DIR = 0x00;         // set all PORTC(COMx) pins as input (high-impedance) for now

    pit_init();

    SLPCTRL.CTRLA = SLPCTRL_SMODE_PDOWN_gc;  // config sleep controller powerdown mode

    sei();

    // LCD refresh
    while (1) {
        if (refresh_ready) {

            refreshSegments();

            interval_counter++;                         // interval_counter increased in every 2ms
            if (interval_counter >= COUNTUP_INTERVAL) { // increment the counter every 2ms * COUNTUP_INTERVAL
                interval_counter = 0;

                LCD_d_1++;                           // 4-digit ripple BCD counter for LCD digits counting from 0000 to 9999
                if (LCD_d_1 >=10) {
                    LCD_d_1 = 0;
                    LCD_d_2++;
                }
                if (LCD_d_2 >=10) {
                    LCD_d_2 = 0;
                    LCD_d_3++;
                }
                if (LCD_d_3 >=10) {
                    LCD_d_3 = 0;
                    LCD_d_4++;
                }
                if (LCD_d_4 >=10) {
                    LCD_d_4 = 0;
                    LCD_d_1++;
                }
            }

            refresh_ready = 0;
        }
        SLPCTRL.CTRLA |= SLPCTRL_SEN_bm;  // sleep enable, this is equivalent to sleep_enable()
        __asm("sleep");                   // this is equivalent to sleep_cpu()
        SLPCTRL.CTRLA &= ~SLPCTRL_SEN_bm; // sleep disable , this is equivalent to sleep_disable()
    }

}

A segment lookup table is used to define what are the segments that need to be turned on or off for each display digit. The segments are not defined in the order of segment A to G like what we normally see in the LED's seven-segment table. It is instead groupped by the segments that are connected to each com pin. With the segment A and B as the two most significant bits, followed by segment F and G, then E and C, and last the Dp(Decimal point) and segment D as the least significant bits.

Segment A B F G E C Dp D
Digit COM4-A COM4-B COM3-A COM3-B COM2-A COM2-B COM1-A COM1-B
"0" 1 1 1 0 1 1 0 1
"1" 0 1 0 0 0 1 0 0
"2" 1 1 0 1 1 0 0 1
"3" 1 1 0 1 0 1 0 1
"4" 0 1 1 1 1 1 0 1
"5" 1 0 1 1 0 1 0 1
"6" 1 0 1 1 1 1 0 1
"7" 1 1 0 0 0 1 0 0
"8" 1 1 1 1 1 1 0 1
"9" 1 1 1 1 0 1 0 1

Let's assumed that we'd want to display "0123" on to the display, digit "0" is the left most digit (i.e. digit for 1,000) and digit "3" is the right most digit (i.e. digit for 1). We will first use the lookup table to get the segment values for each display digit, but this value can't be directly used to drive the LCD segments. We need to create a segs_out value that is the combination of all the segment A and B of each of the four digit, then set the com4 to OUTPUT and output the value of segs_out, segment A and B can then be turn on or off by altering the com4 pin to refresh the two segments in two refresh cycles.

On the subsequent fresh cycles, we will create the segs_out value for segment F and G, and repeat the process by setting com3 LOW and HIGH to refresh the segment F and G of all the digits.

The process get repeat for com2, then com1, it takes a total of 16 fresh cycle to fresh the display completely.

void refreshSegments() {
    segment++;

    if (segment > 7) {
        segment = 0;
    }

    // The following segment generates the 4 COM output waveforms via PORTC, each with HI and LOW outputs
    // PORTB for segment pins.
    switch (segment) {
        case 0:
            segs_out  = (segment_table[LCD_d_1] & 0x03);      // get digit_1's B & A bits
            segs_out |= (segment_table[LCD_d_2] & 0x03) << 2; // get digit_10's B & A bits
            segs_out |= (segment_table[LCD_d_3] & 0x03) << 4; // get digit_100's B & A bits
            segs_out |= (segment_table[LCD_d_4] & 0x03) << 6; // get digit_1000's B & A bits
            PORTC.DIR = 0;                                    // set all com pins to input
            PORTC.OUT = 0;
            PORTB.OUT = segs_out;
            PORTB.DIR = 0xFF;                                 // set all segment pin to output
            PORTC.DIRSET = PIN0_bm;                           // COM4 asserted LOW
            break;
        case 1:
            PORTC.OUTSET = PIN0_bm;
            PORTB.OUT = segs_out ^ 0xFF;                      // reverse segment outputs
            PORTB.DIR = 0xFF;                                 // set all segment pin to output
            PORTC.DIRSET = PIN0_bm;                           // COM4 asserted HIGH
            break;
        case 2:
            segs_out  = (segment_table[LCD_d_1] & 0x0c) >> 2; // get digit_1's G & F bits
            segs_out |= (segment_table[LCD_d_2] & 0x0c);      // get digit_10's G & F bits
            segs_out |= (segment_table[LCD_d_3] & 0x0c) << 2; // get digit_100's G & F bits
            segs_out |= (segment_table[LCD_d_4] & 0x0c) << 4; // get digit_1000's G & F bits
            PORTC.DIR = 0;
            PORTC.OUT = 0;
            PORTB.OUT = segs_out;
            PORTB.DIR = 0xFF;                                 // set all segment pin to output
            PORTC.DIRSET = PIN1_bm;                           // COM3 asserted LOW
            break;
        case 3:
            PORTC.OUT = PIN1_bm;
            PORTB.OUT = segs_out ^ 0xFF;                      // reverse segment outputs
            PORTB.DIR - 0xFF;
            PORTC.DIRSET = PIN1_bm;                           // COM3 asserted HIGH
            break;
        case 4:
            segs_out  = (segment_table[LCD_d_1] & 0x30) >> 4; // get digit_1's C & E bits
            segs_out |= (segment_table[LCD_d_2] & 0x30) >> 2; // get digit_10's C & E bits
            segs_out |= (segment_table[LCD_d_3] & 0x30);      // get digit_100's C & E bits
            segs_out |= (segment_table[LCD_d_4] & 0x30) << 2; // get digit_1000's C & E bits
            PORTC.DIR = 0;
            PORTC.OUT = 0;
            PORTB.OUT = segs_out;
            PORTB.DIR = 0xFF;
            PORTC.DIRSET = PIN2_bm;                           // COM2 asserted LOW
            break;
        case 5:
            PORTC.OUT = PIN2_bm;
            PORTB.OUT = segs_out ^ 0xFF;                      // reverse segment outputs
            PORTB.DIR = 0xFF;
            PORTC.DIRSET = PIN2_bm;                           // COM2 asserted HIGH
            break;
        case 6:
            segs_out  = (segment_table[LCD_d_1] & 0xC0) >> 6; // get digit_1000's DP & D bits
            segs_out |= (segment_table[LCD_d_2] & 0xC0) >> 4; // get digit_100's DP & D bits
            segs_out |= (segment_table[LCD_d_3] & 0xC0) >> 2; // get digit_10's DP & D bits
            segs_out |= (segment_table[LCD_d_4] & 0xC0);      // get digit_1's DP & D bits
            PORTC.DIR = 0;
            PORTC.OUT = 0;
            PORTB.OUT = segs_out;
            PORTB.DIR = 0xFF;
            PORTC.DIRSET = PIN3_bm;                           // COM1 asserted LOW
            break;
        case 7:
            PORTC.OUT = PIN3_bm;
            PORTB.OUT = segs_out ^ 0xFF;
            PORTB.DIR = 0xFF;
            PORTC.DIRSET = PIN3_bm;                           // COM1 asserted HIGH
            break;
        default:
            PORTB.DIRCLR = 0xFF;                              // clear all pins
            PORTC.DIRCLR = 0xFF;                              // COM1-COM4 float
    }

}

The demonstration code implement a 4-digit ripple BCD counter that count up from 0000 to 9999 in every 200ms which is determined by the interval_counter which increment every 2ms as per each freshment cycle. When it reaches to the value set by COUNTUP_INTERVAL (default to 100), the display counter is incremented by 1, that is, the count-up counter on the display is counting up in every 2ms x 100 = 200ms.

Battery Consumption

The demonstration code utilizes ATtiny3227 Power Down sleep mode to keep the overall power consumption as low as possible. All the unused GPIO pins are disabled during the initialization stage. The PIT interrupt is the only one that can be wake-up during Power Down sleep mode, the code basically wakeup every 2ms, fresh a segment and go back to sleep mode.

The battery consumption for both ATtiny3227 and LCD is at about 3.5mA when running at 10MHz clock without deep sleep. The battery consumption drops to approximated 585uA with Power Down deep sleep implemented. As the mojority of the 585uA sleep current is due the four 30k-ohm voltage dividers used to generate the 1/3 bias voltage, which collectively has a total resistance of 7500-ohm across the power source, contributing 3.3v/7500ohm = 440uA to the overall sleep current. The sleep power consumption can be further reduced by using higher resistor values for the voltage divider.

Summary

With limited information available and it tooks a little bit of try-and-error, but I'm glad that I figure out how to bit-banging the segment LCD. It was an impulse buy on Aliexpress out of curiosity, and I don't really has a specific project to use the segment LCD yet, but I'm sure that I will use it in suitable future project with this LCD display.

Despite nowadays OLED and TFT display is popular and LCD display is slowly fading away compare to just 10 years ago, but I like LCD over OLED for one simply reason, that is, they have very low power consumption, making it the idea choice for battery-operated project with small and simple screen. So in many of my projects, if the project requires battery operation, I would tend to use LCD display over OLED. For simple seven-segment display, LCD segment display would be much compact in size and less power hungry than seven-segment LED.

Github

The complete source code is available at my github repository Segment LCD.

Demo Video

Here is a quick Youtube video demostration of the segment LCD driven by an ATtiny3227 running at 3V at internal clock speedd of 10MHz.

Leave a Reply

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.