There are many LCD examples available online for STM32 development boards, mainly for TFT LCD and OLED. From these examples, everyone can learn how to light up an LCD. However, these codes have the following issues:
-
The layering is unclear, which means the modularization is poor.
-
The interfaces are chaotic. If the interfaces are not chaotic, the layering will be much better.
-
Poor portability.
-
Poor generality.
Why do I say this? If you already understand how to operate an LCD, please consider the following scenarios:
1. There is not enough code space, and you can only keep the driver for the ILI9341, deleting all other LCD drivers. Can you delete it with one key (one macro definition)? How many places need to be changed to compile successfully after deletion?
2. 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. What should be done? How should the example code be modified to support both screens? Copying and pasting all the code and then changing the function names? This can indeed complete the task, but the program will then enter a vicious cycle.
3. An OLED that was originally connected to these IOs is later changed to other IOs. Is it easy to change?
4. Originally only supporting Chinese, now needing to sell to South America, requiring support for Dominican languages. Is it easy to change?
Overview of LCD Types
Before discussing how to write an LCD driver, let’s first understand the commonly used LCDs in embedded systems. We will outline some concepts related to driver architecture design without going into deep discussions on principles and details, which will be covered in dedicated articles or referenced in online documents.
TFT LCD
TFT LCD, commonly referred to as color screens, usually have higher pixel counts, such as the common 2.8-inch, 320X240 pixels, and 4.0-inch, 800X400 pixels. These screens typically use parallel interfaces, such as the 8080 or 6800 interfaces (STM32’s FSMC interface); or RGB interfaces, supported by chips like STM32F429. Others, such as those used in mobile phones, use 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 speed and screen flickering. Common TFT LCD driver ICs used with STM32 include: ILI9341/ILI9325, etc.
TFT LCD:
IPS:
COG LCD
Many people may not know what COG LCD is. I think this is related to the current sales direction of development boards, where everyone is focusing on large screens and flashy interfaces, without involving deeper technologies such as software architecture design. In products using microcontrollers, COG LCD actually accounts for a significant proportion.COG stands for Chip On Glass, meaning the driver chip is directly bonded to the glass, making it transparent. The physical object is shown in the image below:
This type of LCD usually has lower pixel counts, commonly 128X64 and 128X32. Generally, it only supports black and white displays, although there are grayscale screens.
The interfaces are usually SPI and I2C. There are also claims of supporting 8-bit parallel interfaces, but they are rarely used; 3 IOs can solve the problem, so there is no need to use 8. Common driver ICs include: STR7565.
OLED LCD
If you have bought a development board, you should have used it. 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, but the difference is that their display methods are different. From our programming perspective, the biggest difference is that OLED LCD does not require backlight control… The physical object is shown below:
Common interfaces are SPI and I2C. Common driver ICs include: SSD1615.
Hardware Scenarios
The following discussions are based on the following hardware information:
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 the driver IC being STR7565, 128X32 pixels.
3. There is a COG LCD connected to hardware SPI3 and several IO ports, with the driver IC being STR7565, 128×64 pixels.
4. There is an OLED LCD connected to SPI3, using CS2 to control the chip select, with the driver IC being SSD1315.
Prerequisite Knowledge
Before we enter the discussion, let’s briefly mention the following concepts. If you want 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 (method), data (attribute). For example: an LED, we can turn it on or off, which is called a method. What is the state of the LED? On or off? That is the attribute. We usually program like this:
u8 ledsta = 0;
void ledset(u8 sta)
{
}
This programming has a problem: 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 to encapsulate the attributes and methods of the LED object.
This structure is an object.
However, this is not a real existence, 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, parameter specifies which LED*/
void ledset(LedObj *led, u8 sta)
{
led->setsta(sta);
}
Yes, in C language, the means to achieve object-oriented programming is the use of structures. The above code is very friendly for APIs. To operate all LEDs, we use the same interface, just tell the interface which LED. Think about the previously mentioned LCD hardware scenarios. For 4 LCDs, if not object-oriented,“wouldn’t the interface for displaying Chinese characters need to be implemented 4 times”? One for each screen?
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 is“attributes”, which are“parameters”, which are“data and hardware interface information used by the driver program”. The driver is“the code process that controls these data and interfaces”.
Generally speaking, if the LCD driver ICs are the same, the same driver can be used. Some different ICs can also use the same driver, for example, SSD1315 and STR7565, except for initialization, the rest can use the same driver. For example, a COG LCD:
❝
The driver IC is STR7565, 128 * 64 pixels, using SPI3 for backlight, PF5 for command line, PF4 for reset pin.
❞
All the above information combined is a device. The driver is the driver code for STR7565.
Why separate the driver and device? To solve the following problem:
❝
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,“using the same program to control both devices” is the best solution. The means of separating the driver and device:
❝
Add device parameters to the parameters of the driver program interface functions, and all resources used by the driver are passed in 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 encapsulating a piece of code and providing a stable interface for different drivers to use. Non-modularization means implementing this piece of code in different drivers. For example, in font library processing, when displaying Chinese characters, we need to find the dot matrix, and 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 a module is modularization. A typical feature of non-modularization is“a single line runs through without any sense of hierarchy”.
What is LCD?
Earlier we talked about object-oriented programming, and now we need to abstract the LCD to derive an object, which requires knowing what LCD really is. Ask yourself the following questions:
- What can an LCD do?
- What do you want the LCD to do?
- Who wants the LCD to do what?
Friends who are new to embedded systems may not understand this and may find it hard to think. Let’s simulate the data flow of LCD function operations. The APP wants to display a Chinese character on the LCD.
1. First, an interface for displaying Chinese characters is needed, and the APP can call this interface to display Chinese characters, assuming 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 obtain the dot matrix.
3. After obtaining the dot matrix, it needs to be displayed on the LCD, so we call an interface called ILI9341_dis to refresh the dot matrix to the LCD with the driver IC model ILI9341.
4. How does ILI9341_dis display the dot matrix? By calling an interface called 8080_WRITE.
Okay, this is the general process. We abstract the LCD functional interface from this process. 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 just a series of dots. Therefore, the answers to the previous questions are:
- The LCD can display content dot by dot.
- To display Chinese characters or images on the LCD—– it is 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.“Therefore, the driver only needs to provide an interface for displaying dots, whether displaying a single dot or a group 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 interface, which corresponds to the driver, includes a driver ID number.
- id, driver model
- initialization
- draw point
- display a region of points in a certain color
- display a region of points in certain colors
- display on/off switch
- prepare refresh area (mainly for color screens using direct DMA 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:
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 8080 drivers, while others use SPI drivers. There is only one SPI driver, and those controlled by IO ports are also made into simulated SPI.
3. The LCD driver layer manages the LCD, such as completing the identification of TFT LCDs. It also encapsulates all LCD interfaces into a set of interfaces.
4. The simple GUI layer encapsulates some display functions, such as drawing lines and character displays.
5. The font dot matrix module provides interfaces for obtaining and processing dot matrices.
Since it is not as complicated in practice, in the example, we put the GUI and LCD driver layers together. The two drivers for TFT LCD are also placed in one file, but the logic is separated. The OLED, except for initialization, has interfaces that are basically the same as 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 mainly has three functions:
1. Device Management
First, a series of LCD parameter structures are defined, containing ID and pixel information. These structures are then combined into a list array.
/* Various LCD specification parameters*/
_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 contents being the drivers implemented in the corresponding driver files.
/* All driver lists
Driver list*/
_lcd_drv *LcdDrvList[] = {
&TftLcdILI9341Drv,
&TftLcdILI9325Drv,
&CogLcdST7565Drv,
&OledLcdSSD1615rv,
Defines the device tree, which indicates how many LCDs the system has, which interface they are connected to, and what driver ICs they use. In a complete system, it can be made into a device tree similar to LINUX.
/*Device tree definition*/
#define DEV_LCD_C 3//The system has 3 LCD devices
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. Initialization is to find the corresponding driver based on the previously defined device tree, find the corresponding device parameters, and complete the device initialization. The open function searches for the device based on the passed device name, finds it, and returns the device handle; all subsequent operations require this device handle.
3. Simple GUI Layer
The most important function currently is the character display function.
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 later.
Driver IC Layer
The driver IC layer is divided into two parts:
1. Encapsulating LCD Interfaces
Some LCDs use the 8080 bus, while others use SPI or VSPI buses. The functions for these buses are implemented in separate files. However, in addition to these communication signals, LCDs will also have reset signals, command data line signals, backlight signals, etc. We encapsulate these signals along with the communication interfaces into“LCD communication bus”, which is buslcd. BUS_8080 is encapsulated in the dev_ILI9341.c file. BUS_LCD1 and BUS_lcd2 are encapsulated in the dev_str7565.c file.
2. Driver Implementation
Implement the _lcd_drv driver structure. 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 the official interface. 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 be defined according to the number of devices,
and match the driver with parameters, and initialize variables.
When opening, it only 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, black and white screens and OLED screens will allocate display memory during initialization
};
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 the device name, which LCD bus it is connected to, and the device ID can be known.
typedef struct
{
char *name;//device name
LcdBusType bus;//connected to which LCD bus
u16 id;
}LcdObj;
- The member pra points to the LCD parameters, which can provide information about the LCD specifications.
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, and height are common variables used 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 using this pointer. Usually, this pointer points to a structure defined by the driver, and memory space is allocated during device initialization. Currently, it is mainly used for COG LCD and OLED LCD display buffers.
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 opened successfully, it returns a pointer to the structure mentioned above.
3. To display characters, the interface finds the dot matrix and calls the corresponding driver program through the drv of the above structure.
4. The driver program decides which LCD bus to operate based on this structure and uses the variables of 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, "这是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, "屋脊雀工作室", BLACK);
dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 13, "这是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, "屋脊雀工作室", BLACK);
dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,30, "ABC-abc,", RED);
dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,60, "这是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, "屋脊雀工作室", RED);
while(1);
}
Using a single function dev_lcd_open, you can open 3 LCDs and obtain LCD devices. Then, calling dev_lcd_put_string allows you to display on different LCDs. All other GUI operation interfaces have only one. This design is very friendly for the APP layer. The display effect:
- Benefit 2
The current 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, and it can be done. Of course, two devices cannot be connected to one interface.
LcdObj LcdObjList[DEV_LCD_C]=
{
{"oledlcd", LCD_BUS_SPI, 0X1315},
{"tftlcd", LCD_BUS_8080, NULL},
};
Font Library
Currently, the font library is placed on the SD card in the example. You can modify it according to your needs during porting. Please refer to font.c for details.
Declaration
Please use the code according to the copyright agreement. The current source code is just a usable design, and its completeness and robustness have not been tested. It will be uploaded to GitHub later and continuously updated and optimized. For the latest news, please follow www.wujique.com.
Source: Rooftop Sparrow👉A Product-Level MCU Menu Framework Design👉 A sophomore student from Nanjing University of Aeronautics and Astronautics self-studied for 2 years to develop a rocket, successfully launching and recovering it👉 Starting from these knowledge points, learning STM32 microcontrollers becomes very simple👉 Solving RFID noise, a simple and low-cost circuit design