Microcontroller Driving LCD Programming Approach

Source | Roof Ridge Sparrow | strongerHuang

There are many methods for microcontrollers to drive LCDs, and many accompanying examples are available online. However, among the thousands of examples online, which one is your “no.1”?

Today, I would like to share an approach to driving LCDs with microcontrollers in an object-oriented manner.

Overview of LCD Types

Before discussing how to write an LCD driver, let’s briefly understand the commonly used embedded LCDs. We will outline some concepts related to driver architecture design without going into the principles and details, which will be covered in dedicated articles or referenced in online documentation.

TFT LCD

TFT LCD, commonly known as color screens, typically have high pixel counts, such as the common 2.8-inch screen with a resolution of 320×240 pixels, or a 4.0-inch screen with a resolution of 800×400 pixels. These screens usually use parallel interfaces, such as 8080 or 6800 interfaces (STM32’s FSMC interface); or RGB interfaces, supported by chips like STM32F429. Others, like those used in mobile phones, utilize MIPI interfaces.

In summary, there are many types of interfaces. Some also support SPI interfaces. Unless it is a relatively small screen, using SPI interfaces is not recommended due to slow speeds and screen flickering. The commonly used TFT LCD driver ICs for STM32 include ILI9341/ILI9325, etc.

tft lcd:

Microcontroller Driving LCD Programming Approach

IPS:

Microcontroller Driving LCD Programming Approach

COG LCD

Many may not know what COG LCD is; I believe this relates to the current sales direction of development boards. Everyone is focusing on large screens and flashy interfaces, neglecting deeper technologies, such as software architecture design. In products using microcontrollers, COG LCDs actually account for a significant portion. COG stands for Chip On Glass, meaning the driver chip is directly bonded to the glass, making it transparent. The physical object is shown below:

Microcontroller Driving LCD Programming Approach

This type of LCD usually has low pixel counts, commonly 128×64 or 128×32. Generally, it only supports black and white displays, although there are grayscale screens.

The interfaces usually include SPI and I2C. Some claim to support 8-bit parallel interfaces, but they are rarely used; 3 IO lines can solve the problem without needing 8. Common driver ICs include STR7565.

OLED LCD

If you’ve purchased a development board, you’ve likely used one. This new technology is perceived as high-end and is commonly used in products like wristbands. Currently, OLED screens are relatively small, and larger ones are quite expensive. In terms of control, it is similar to COG LCD, with the main difference being their display methods. From a programming perspective, the biggest difference is that OLED LCDs do not require backlight control. The physical object is shown below:

Microcontroller Driving LCD Programming Approach

The common interfaces are SPI and I2C. Common driver ICs include SSD1615.

Hardware Scenarios

The following discussions are based on the hardware information below:

1. There is a TFT screen connected to the hardware’s FSMC interface, but the model of the screen is unknown.

2. There is a COG LCD connected to several ordinary IO ports, with a driver IC of STR7565 and a resolution of 128×32 pixels.

3. There is a COG LCD connected to hardware SPI3 and several IO ports, with a driver IC of STR7565 and a resolution of 128×64 pixels.

4. There is an OLED LCD connected to SPI3, using CS2 to control the chip selection, with a driver IC of SSD1315.

Microcontroller Driving LCD Programming Approach

Prerequisites

Before we dive into the discussion, let’s briefly mention the following concepts. If you wish to delve deeper into these concepts, please GOOGLE.

Object-Oriented Programming

Object-oriented programming is a concept in the programming world. What does object-oriented mean? Programming has two elements: program (methods) and data (attributes). For example, for an LED, we can turn it on or off, which is called a method. What is the state of the LED? Is it on or off? That is the attribute. We typically program like this:

u8 ledsta = 0;
void ledset(u8 sta)
{
}

However, there is a problem with this programming approach: if we have 10 such LEDs, how do we write it? At this point, we can introduce object-oriented programming, encapsulating each LED as an object. We can do it like this:

/*
Define a structure that encapsulates the properties and methods of the LED object.
This structure is an object.
However, this is not a real entity, but an abstraction of an object.
*/
typedef struct{
u8 sta;
void (*setsta)(u8 sta);
}LedObj;

/*  Declare an LED object named LED1 and implement its method drv_led1_setsta */
void drv_led1_setsta(u8 sta)
{
}

LedObj LED1={
        .sta = 0,
        .setsta = drv_led1_setsta,
    };

/*  Declare an LED object named LED2 and implement its method drv_led2_setsta */
void drv_led2_setsta(u8 sta)
{
}

LedObj LED2={
        .sta = 0,
        .setsta = drv_led2_setsta,
    };
    
/*  Function to operate the LED, with parameters specifying which LED */
void ledset(LedObj *led, u8 sta)
{
    led->setsta(sta);
}

Indeed, in C language, the means to achieve object-oriented programming is through the use of structures. The above code is very friendly for APIs. To operate all LEDs, we use the same interface and only need to specify which LED. Think about the previous LCD hardware scenarios. For 4 LCDs, if we do not use object-oriented programming, “do we need to implement the interface for displaying Chinese characters 4 times?” Each screen one?

Separation of Driver and Device

If you want to understand the separation of driver and device in depth, please refer to books on LINUX drivers.

What is a device? I believe a device consists of “attributes”, “parameters”, and “data and hardware interface information used by the driver program”. Thus, the driver is the “code process that controls these data and interfaces”.

Generally, if the LCD driver ICs are the same, the same driver can be used. Some different ICs can also use the same driver, such as SSD1315 and STR7565; aside from initialization, the rest can use the same driver. For example, a COG LCD:

The driver IC is STR7565 with a resolution of 128 * 64 pixels using SPI3, backlight using PF5, command line using PF4, reset pin using PF3

All the above information combined represents a device. The driver is the driver code for STR7565.

Why separate drivers and devices? To solve the following problems:

There is a new product, a cash register device. The system has two LCDs, both OLED, with the same driver IC, but one is 128×64, and the other is 128×32 pixels; one is called the main display for the cashier; the other is called the customer display for showing the amount.

This problem can be best solved by “using the same program to control two devices”. The means of separating the driver from the device is:

By adding device parameters to the driver program interface function parameters, all resources needed by the driver are passed from the device parameters.

How does the driver bind to the device? Through the model of the device’s driver IC.

Modularization

I believe modularization is about encapsulating a segment of code and providing a stable interface for different drivers. Non-modularization means implementing this segment of code in different drivers. For example, in character library processing, when displaying Chinese characters, we need to find the dot matrix. When printing Chinese characters on a printer, we also need to find the dot matrix. How do you think the program should be written? Making the dot matrix processing into a module is modularization. A typical feature of non-modularization is that “a wire connects all the way through without any sense of hierarchy”.

What is LCD?

Earlier, we talked about object orientation; now we need to abstract LCD and derive an object. To do this, we need to know what LCD really is. Ask yourself the following questions:

  • What can LCD do?
  • What do you want LCD to do?
  • Who wants LCD to do what?

Friends who are new to embedded systems may not understand this and may find it hard to think through. Let’s simulate the data flow of LCD’s functional operations. The APP wants to display a Chinese character on the LCD.

1. First, an interface for displaying Chinese characters is needed, which the APP can call to display Chinese characters; let’s assume the interface is called lcd_display_hz.

2. Where do the Chinese characters come from? From the dot matrix font library, so within the lcd_display_hz function, a function called find_font must be called to retrieve the dot matrix.

3. After obtaining the dot matrix, it must be displayed on the LCD, so we call an interface called ILL9341_dis to refresh the dot matrix onto the LCD with a driver IC model of ILI9341.

4. How does ILI9341_dis display the dot matrix? By calling an interface called 8080_WRITE.

Okay, this is the general process from which we can abstract the LCD functional interface. Are Chinese characters related to the LCD object? No. In the eyes of the LCD, whether it is a Chinese character or an image, it is all just dots. Therefore, the answers to the previous questions are:

  • LCD can display content dot by dot.
  • To make the LCD display Chinese characters or images—it’s just displaying a bunch of dots.
  • The APP wants the LCD to display images or text.

The conclusion is: the function of all LCD objects is to display dots. “Thus, the driver only needs to provide an interface for displaying dots, whether displaying one dot or a bunch of dots.” The abstract interface is as follows:

/*
    LCD driver definition
*/
typedef struct  
{
u16 id;

    s32 (*init)(DevLcd *lcd);
    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);
    s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);
    s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);
    s32 (*onoff)(DevLcd *lcd, u8 sta);
    s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);
    void (*set_dir)(DevLcd *lcd, u8 scan_dir);
    void (*backlight)(DevLcd *lcd, u8 sta);
}_lcd_drv;

The above interfaces correspond to the driver and include a driver ID number.

  • id, driver model
  • initialization
  • draw point
  • display a region of dots in a certain color
  • display a region of dots in certain colors
  • display switch
  • prepare the refresh area (mainly for color screens using direct DMA for refresh)
  • set scan direction
  • backlight control

Displaying characters, drawing lines, and other functions do not belong to the LCD driver. They should be classified into the GUI layer.

LCD Driver Framework

We designed the following driver framework:

Microcontroller Driving LCD Programming Approach

Design Ideas:

1. The intermediate display driver IC driver program provides a unified interface, in the form of the _lcd_drv structure mentioned earlier.

2. Each display IC driver calls different interface drivers based on device parameters. For example, TFT uses the 8080 driver, while others use SPI drivers. The SPI driver has only one version, and we also simulate SPI for those controlled by IO ports.

3. The LCD driver layer manages LCDs, such as identifying TFT LCDs, and encapsulates all LCD interfaces into a single set of interfaces.

4. The simple GUI layer encapsulates some display functions, such as drawing lines and displaying characters.

5. The font dot matrix module provides interfaces for obtaining and processing dot matrices.

Since it is not as complicated in practice, we put the GUI and LCD driver layers together in the example. The two drivers for TFT LCDs are also placed in one file, but the logic is separated. The OLED, except for initialization, has interfaces that are basically the same as the COG LCD, so these two drivers are also placed in one file.

Code Analysis

The code is divided into three layers:

1. GUI and LCD driver layer: dev_lcd.c dev_lcd.h

2. Display driver IC layer: dev_str7565.c & dev_str7565.h dev_ILI9341.c & dev_ILI9341.h

3. Interface layer: mcu_spi.c & mcu_spi.h stm324xg_eval_fsmc_sram.c & stm324xg_eval_fsmc_sram.h

GUI and LCD Layer

This layer has three main functions:

1. Device Management

First, a variety of LCD parameter structures are defined, containing ID and pixel dimensions. These structures are then combined into a list array.

/*  Various LCD specifications */
_lcd_pra LCD_IIL9341 ={
        .id   = 0x9341,
        .width = 240,   //LCD width
        .height = 320,  //LCD height
};
...
/* Various LCD list */
_lcd_pra *LcdPraList[5]=
            {
                &LCD_IIL9341,       
                &LCD_IIL9325,
                &LCD_R61408,
                &LCD_Cog12864,
                &LCD_Oled12864,
            };

Then, all driver list arrays are defined, with the array contents being the drivers implemented in the corresponding driver files.

/*  All driver lists
    Driver list */
_lcd_drv *LcdDrvList[] = {
                    &TftLcdILI9341Drv,
                    &TftLcdILI9325Drv,
                    &CogLcdST7565Drv,
                    &OledLcdSSD1615rv,

A device tree is defined, indicating how many LCDs the system has, which interfaces they are connected to, and which driver ICs they use. In a complete system, this could be structured like a LINUX device tree.

/*Device tree definition*/
#define DEV_LCD_C 3//Three LCD devices in the system
LcdObj LcdObjList[DEV_LCD_C]=
{
    {"oledlcd", LCD_BUS_VSPI, 0X1315},
    {"coglcd", LCD_BUS_SPI,  0X7565},
    {"tftlcd", LCD_BUS_8080, NULL},
};

2. Interface Encapsulation

void dev_lcd_setdir(DevLcd *obj, u8 dir, u8 scan_dir)
s32 dev_lcd_init(void)
DevLcd *dev_lcd_open(char *name)
s32 dev_lcd_close(DevLcd *dev)
s32 dev_lcd_drawpoint(DevLcd *lcd, u16 x, u16 y, u16 color)
s32 dev_lcd_prepare_display(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey)
s32 dev_lcd_display_onoff(DevLcd *lcd, u8 sta)
s32 dev_lcd_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color)
s32 dev_lcd_color_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 color)
s32 dev_lcd_backlight(DevLcd *lcd, u8 sta)

Most interfaces are secondary encapsulations of the driver IC interfaces. The difference lies in the initialization and opening interfaces. The initialization function searches for the corresponding driver based on the previously defined device tree, finds the corresponding device parameters, and completes the device initialization. The open function searches for the device based on the provided device name, finds it, and returns the device handle; subsequent operations require this device handle.

3. Simple GUI Layer

Currently, the most important function is displaying characters.

s32 dev_lcd_put_string(DevLcd *lcd, FontType font, int x, int y, char *s, unsigned colidx)

Other functions for drawing lines and circles are currently just tests and will be improved in the future.

Driver IC Layer

The driver IC layer is divided into two parts:

1. Encapsulating LCD Interfaces

LCDs use either the 8080 bus or the SPI bus. Functions for these buses are implemented in separate files. However, in addition to these communication signals, LCDs will have reset signals, command data line signals, backlight signals, etc. We encapsulate these signals along with the communication interfaces into the “LCD communication bus”, also referred to as buslcd. BUS_8080 is encapsulated in the dev_ILI9341.c file. BUS_LCD1 and BUS_LCD2 are encapsulated in dev_str7565.c.

2. Driver Implementation

The _lcd_drv driver structure is implemented. Each driver implements one, and some drivers can share functions.

_lcd_drv CogLcdST7565Drv = {
                            .id = 0X7565,

                            .init = drv_ST7565_init,
                            .draw_point = drv_ST7565_drawpoint,
                            .color_fill = drv_ST7565_color_fill,
                            .fill = drv_ST7565_fill,
                            .onoff = drv_ST7565_display_onoff,
                            .prepare_display = drv_ST7565_prepare_display,
                            .set_dir = drv_ST7565_scan_dir,
                            .backlight = drv_ST7565_lcd_bl
                            };

Interface Layer

The 8080 layer is relatively simple, using official interfaces. The SPI interface provides the following operation functions, which can operate SPI and VSPI.

extern s32 mcu_spi_init(void);
extern s32 mcu_spi_open(SPI_DEV dev, SPI_MODE mode, u16 pre);
extern s32 mcu_spi_close(SPI_DEV dev);
extern s32 mcu_spi_transfer(SPI_DEV dev, u8 *snd, u8 *rsv, s32 len);
extern s32 mcu_spi_cs(SPI_DEV dev, u8 sta);

As for why SPI is written this way, there will be a separate file explaining it.

Overall Process

How are the several modules mentioned earlier connected? Please see the structure below:

/*  During initialization, it will define based on the number of devices,
    match the driver with parameters, and initialize variables.
    When opened, it simply retrieves a pointer */
struct _strDevLcd
{
s32 gd;//handle, controls whether it can be opened

    LcdObj   *dev;
    /* LCD parameters, fixed and unchangeable */
    _lcd_pra *pra;

    /* LCD driver */
    _lcd_drv *drv;

    /* Variables needed by the driver */
u8  dir;    //horizontal or vertical control: 0, vertical; 1, horizontal.
u8  scandir;//scan direction
u16 width;  //LCD width
u16 height; //LCD height

    void *pri;//private data, some drivers may need special variables, all recorded with this pointer, usually pointing to a structure defined by the driver, which allocates space during device initialization. Currently mainly used for COG LCD and OLED LCD display caches.
};

Each device will have such a structure, which is initialized when the LCD is initialized.

  • The member dev points to the device tree, from which you can know the device name, which LCD bus it is connected to, and the device ID.
typedef struct
{
    char *name;//device name
    LcdBusType bus;//connected to which LCD bus
    u16 id;
}LcdObj;
  • The member pra points to the LCD parameters, allowing you to know the specifications of the LCD.
typedef struct
{
u16 id;
u16 width;  //LCD width  vertical
    u16 height; //LCD height    vertical
}_lcd_pra;
  • The member drv points to the driver, and all operations are implemented through drv.
typedef struct  
{
u16 id;

    s32 (*init)(DevLcd *lcd);

    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);
s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy, u16 ey, u16 color);
s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy, u16 ey, u16 *color);
s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);
s32 (*onoff)(DevLcd *lcd, u8 sta);
void (*set_dir)(DevLcd *lcd, u8 scan_dir);
void (*backlight)(DevLcd *lcd, u8 sta);
}_lcd_drv;
  • The members dir, scandir, width, height are general variables needed by the driver. Since each LCD has a structure, a set of driver programs can control multiple devices without interference.
  • The member pri is a private pointer; some drivers may need special variables, which are all recorded with this pointer, typically pointing to a structure defined by the driver, which allocates variable space during device initialization. Currently, it is mainly used for COG LCD and OLED LCD display caches.

The entire LCD driver is combined through this structure.

1. During initialization, based on the device tree, find the driver and parameters, and then initialize the structure mentioned above.

2. Before using the LCD, call the dev_lcd_open function. If successful, it returns a pointer to the above structure.

3. To display characters, the interface finds the dot matrix and calls the corresponding driver program through the structure’s drv.

4. The driver program decides which LCD bus to operate based on this structure and uses the variables from this structure.

Usage and Benefits

  • Benefit 1

Please see the test program

void dev_lcd_test(void)
{
    DevLcd *LcdCog;
    DevLcd *LcdOled;
    DevLcd *LcdTft;

    /*  Open three devices */
    LcdCog = dev_lcd_open("coglcd");
    if(LcdCog==NULL)
        uart_printf("open cog lcd err\r\n");

    LcdOled = dev_lcd_open("oledlcd");
    if(LcdOled==NULL)
        uart_printf("open oled lcd err\r\n");

    LcdTft = dev_lcd_open("tftlcd");
    if(LcdTft==NULL)
        uart_printf("open tft lcd err\r\n");

    /* Turn on backlight */
    dev_lcd_backlight(LcdCog, 1);
    dev_lcd_backlight(LcdOled, 1);
    dev_lcd_backlight(LcdTft, 1);

    dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
    dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 13, "This is OLED LCD", BLACK);
    dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);
    dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 47, "Roof Ridge Sparrow Studio", BLACK);

    dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
    dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 13, "This is COG LCD", BLACK);
    dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);
    dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 47, "Roof Ridge Sparrow Studio", BLACK);

    dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,30, "ABC-abc,", RED);
    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,60, "This is TFT LCD", RED);
    dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,100, "www.wujique.com", RED);
    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,150, "Roof Ridge Sparrow Studio", RED);

    while(1);
}

Using a single function dev_lcd_open, three LCDs can be opened and their devices obtained. Then, calling dev_lcd_put_string allows display on different LCDs. All other GUI operation interfaces are unified. This design is very user-friendly for the APP layer. The display effect:

Microcontroller Driving LCD Programming Approach

  • Benefit 2

Currently, the device tree is defined as follows:

LcdObj LcdObjList[DEV_LCD_C]=
{
    {"oledlcd", LCD_BUS_VSPI, 0X1315},
    {"coglcd", LCD_BUS_SPI,  0X7565},
    {"tftlcd", LCD_BUS_8080, NULL},
};

One day, if the OLED LCD needs to be connected to SPI, you only need to change the parameters in the device tree array, of course, two devices cannot be connected to the same interface.

LcdObj LcdObjList[DEV_LCD_C]=
{
    {"oledlcd", LCD_BUS_SPI, 0X1315},
    {"tftlcd", LCD_BUS_8080, NULL},
};

Font Library

The font library is not elaborated upon for now; the font library in the example is stored on an SD card, and you can modify it according to your needs during porting. Please refer to font.c for details.

Note: This method is for learning programming ideas and techniques and may not be suitable for all projects.

END
Evaluation Center Free Application

Microcontroller Driving LCD Programming Approach

Microcontroller Driving LCD Programming Approach

👆Long press the image to scan the code to apply👆

Leave a Comment