By adding the above code to the main function, you can easily achieve the following effects such as outputting strings, drawing rectangular blocks, and clearing the screen.
-
The layers are not clear, in layman's terms, the modularization is too poor
-
The interface is messy. As long as the interface is not messy, the layering will be much better.
-
Poor portability
-
Poor versatility
Why do I say this? If you already understand the operation of LCD, please think about the following scenario:
-
There is not enough code space, so I can only keep the 9341 driver and delete all other LCD drivers. Can I delete it with one click (one macro definition)? How many places do I need to change to compile after deleting it?
-
There is a new product, a cash register. The system has two LCDs, both OLED, with the same driver IC, but one is 128x64 and the other is 128x32 pixels. One is called the main display, for the cashier; the other is called the customer display, for the customer to see the amount. What should I do? How should I modify these routine codes to support two screens? Copy and paste all the codes and then change the function names? This can indeed complete the task, but the program has entered a vicious cycle since then.
-
An OLED was originally connected to these IOs, and later changed to other IOs. Is it easy to change?
-
Originally it only supported Chinese, but now we want to sell it to South America and support the Dominican language. Is it easy to change?
LCD Types Overview
Before discussing how to write LCD drivers, let's first take a look at some commonly used embedded LCDs. This article will outline some concepts related to driver architecture design. We will not discuss the principles and details in depth here. There will be special articles to introduce this, or you can refer to online documents.
TFT LCD
TFT LCD, also known as color screen. Usually the pixels are high, for example, the common 2.8-inch, 320X240 pixels. 4.0-inch, 800X400 pixels. These screens usually use parallel ports, that is, 8080 or 6800 interfaces (FSMC interface of STM32); or RGB interfaces, supported by chips such as STM32F429. Others, such as the MIPI interface used in mobile phones.
In short, there are many types of interfaces. Some also support SPI interface. Unless it is a relatively small screen, it is not recommended to use SPI interface, because it is slow and the screen will flash when refreshing. The commonly used TFT LCD screen driver ICs for playing STM32 are usually: ILI9341/ILI9325, etc.
TFT LCD:
IPS:
COG LCD
Many people may not know what COG LCD is. I think it has something to do with the current sales direction of development boards. Everyone is selling large screens and cool interfaces, but they don't get involved in deeper technologies, such as software architecture design. COG LCD actually accounts for a very large proportion of products using single-chip microcomputers. COG is the abbreviation of Chip On Glass, which means that the driver chip is directly bound to the glass, which is transparent. The real thing looks like the picture below:
This type of LCD usually has low pixel count, commonly used are 128X64, 128X32. Generally only supports black and white display, there are also grayscale screens.
The interface is usually SPI, I2C. There are also some that claim to support 8-bit parallel ports, but they are basically not used. If 3 IOs can solve the problem, there is no need to use 8, right? Commonly used driver IC: STR7565.
OLED LCD
Anyone who has bought a development board should have used it. It is a new technology that everyone feels is high-end and is often used in products such as bracelets. OLED currently has a small screen, and larger ones are very expensive. The control is similar to COG LCD, the difference is that the two display methods are different. From our program point of view, the biggest difference is that OLED LCD does not need to control the backlight. . . . . The actual product is as shown below:
The most common interfaces are SPI and I2C. Common driver IC: SSD1615.
Hardware scenario
The following discussion is based on the following hardware information:
1. There is a TFT screen connected to the FSMC interface of the hardware. What type of screen is it? I don't know.
2. There is a COG LCD connected to several ordinary IO ports. The driver IC is STR7565, 128X32 pixels.
3. There is a COG LCD connected to the hardware SPI3 and several IO ports. The driver IC is STR7565, 128x64 pixels.
4. There is an OLED LCD connected to SPI3, using CS2 to control chip select, and the driver IC is SSD1315.
Prerequisites
Before we get into the discussion, let’s briefly talk about the following concepts. If you want to learn more about these concepts, please search them yourself, or you can add WeChat hplwbs to add you to the group for discussion.
Object-oriented
Object-oriented is a concept in the programming world. What is object-oriented? Programming has two elements: program (method) and data (attribute). For example, we can turn on or off an LED, which is called a method. What is the state of the LED? On or off? This is the attribute. We usually program like this:
u8 ledsta = 0;
void ledset(u8 sta)
{
}
There is a problem with this kind of programming. If we have 10 such LEDs, how do we write them? At this time, we can introduce object-oriented programming and encapsulate each LED as an object. We can do this:
/*
定义一个结构体,将LED这个对象的属性跟方法封装。
这个结构体就是一个对象。
但是这个不是一个真实的存在,而是一个对象的抽象。
*/
typedef struct{
u8 sta;
void (*setsta)(u8 sta);
}LedObj;
/* 声明一个LED对象,名称叫做LED1,并且实现它的方法drv_led1_setsta*/
void drv_led1_setsta(u8 sta)
{
}
LedObj LED1={
.sta = 0,
.setsta = drv_led1_setsta,
};
/* 声明一个LED对象,名称叫做LED2,并且实现它的方法drv_led2_setsta*/
void drv_led2_setsta(u8 sta)
{
}
LedObj LED2={
.sta = 0,
.setsta = drv_led2_setsta,
};
/* 操作LED的函数,参数指定哪个led*/
void ledset(LedObj *led, u8 sta)
{
led->setsta(sta);
}
Yes, in C language, the means of implementing object-oriented programming is the use of structures. The above code is very friendly to the API. To operate all LEDs, use the same interface, and just tell the interface which LED to use. Think about the LCD hardware scenario mentioned earlier. If there are 4 LCDs and they are not object-oriented, do we need to implement 4 interfaces to display Chinese characters? One for each screen?
Separation of driver and device
If you want to learn more about the separation of drivers and devices, please read books on LINUX drivers.
What is a device? I think of a device as "attributes", "parameters", and "data and hardware interface information used by the driver". Then the driver is the "code process that controls this data and interface".
Generally speaking, if the LCD driver IC is the same, use the same driver. Some different ICs can also use the same driver, such as SSD1315 and STR7565, except for initialization, everything else can use the same driver. For example, a COG LCD:
The driver IC is STR7565 128 * 64 pixels using SPI3, backlight using PF5, command line using PF4, reset pin using PF3
All the above information is a device. The driver is the driver code of STR7565.
Why do we need to separate drivers from devices? Because we need to solve the following problems:
There is a new product, cash register equipment. The system has two LCDs, both OLED, the same driver IC, but one is 128x64 and the other is 128x32 pixels. One is called the main display, which is used by the cashier; the other is called the customer display, which is used by customers to see the amount.
The best solution to this problem is to use the same program to control both devices.
Add device parameters to the parameters of the driver interface function, and all resources used by the driver are passed in from the device parameters.
How to bind the driver to the device? Through the driver IC model of the device.
Modularity
I think modularization is to encapsulate a program and provide a stable interface for different drivers to use. Non-modularization means that this program is implemented in different drivers. For example, when displaying Chinese characters, we need to find dot matrix. When printing Chinese characters on a printer, we also need to find dot matrix. How do you think the program should be written? Making dot matrix processing into a module is modularization. The typical feature of non-modularization is "a line from beginning to end, without any sense of hierarchy."
What is LCD?
We talked about object-orientation before. Now to abstract LCD and get an object, we need to know what LCD is. Ask yourself the following questions:
-
-
What do you need LCD for?
-
Friends who are new to embedded systems may not understand it well and may not understand it. Let's simulate the function operation data flow of LCD. APP wants to display a Chinese character on LCD.
1. First, an interface for displaying Chinese characters is needed. APP can display Chinese characters by calling this interface. Suppose the interface is called lcd_display_hz.
2. Where do Chinese characters come from? They come from the dot matrix font library, so in the lcd_display_hz function, a function called find_font is called to obtain the dot matrix.
3. After obtaining the dot matrix, we need to display the dot matrix on the LCD, so we call an ILL9341_dis interface to refresh the dot matrix to the LCD whose driver IC model is ILI9341.
4. How to display the dot matrix on ILI9341_dis? Call an 8080_WRITE interface.
OK, this is the general process. We can abstract the LCD functional interface from this process. Are Chinese characters related to LCD objects? No. In the eyes of LCD, both Chinese characters and pictures are dots. So the answer to the previous question is:
-
LCD can display content one dot at a time.
-
To display Chinese characters or pictures on LCD - just display a bunch of dots
-
APP wants LCD to display pictures or text.
The conclusion is: the function of all LCD objects is to display dots. "So the driver only needs to provide an interface for displaying dots, displaying a dot, displaying a piece of dots." The abstract interface is as follows:
/*
LCD驱动定义
*/
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, that is, the corresponding driver, contains a driver ID number.
-
-
-
-
Display a certain color for points in an area
-
Display certain colors for points in an area
-
-
Prepare the refresh area (mainly used for direct DMA refresh of color screen)
-
Set the scanning direction
-
Functions such as displaying characters and drawing lines do not belong to the LCD driver and 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 provides a unified interface in the form of the _lcd_drv structure mentioned above.
2. Each display IC driver calls different interface drivers according to device parameters. For example, TFT uses 8080 driver, and others use SPI driver. There is only one SPI driver, and we also make it into simulated SPI for those controlled by IO port.
3. The LCD driver layer manages the LCD, such as completing the identification of TFT LCDs, and encapsulates all LCD interfaces into a set of interfaces.
4. The simple GUI layer encapsulates some display functions, such as line drawing and character display.
5. The font dot matrix module provides a dot matrix acquisition and processing interface.
Since it is not that complicated in practice, we put the GUI and LCD driver layers together in the example. The two drivers of TFT LCD are also put into one file, but the logic is separate. Except for initialization, the other interfaces of OLED are basically the same as COG LCD, so these two drivers are also put into 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. Equipment management」
First, a bunch of LCD parameter structures are defined, which contain ID and pixel, and these structures are combined into a list array.
/* 各种LCD的规格参数*/
_lcd_pra LCD_IIL9341 ={
.id = 0x9341,
.width = 240, //LCD 宽度
.height = 320, //LCD 高度
};
...
/*各种LCD列表*/
_lcd_pra *LcdPraList[5]=
{
&LCD_IIL9341,
&LCD_IIL9325,
&LCD_R61408,
&LCD_Cog12864,
&LCD_Oled12864,
};
Then all driver list arrays are defined. The array content is the driver, which is implemented in the corresponding driver file.
/* 所有驱动列表
驱动列表*/
_lcd_drv *LcdDrvList[] = {
&TftLcdILI9341Drv,
&TftLcdILI9325Drv,
&CogLcdST7565Drv,
&OledLcdSSD1615rv,
}
The device tree is defined, that is, how many LCDs are in the system, which interface is connected, and what driver IC is used. If it is a complete system, it can be made into a device tree similar to LINUX.
/*设备树定义*/
#define DEV_LCD_C 3//系统存在3个LCD设备
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 encapsulation of driver IC interfaces. The difference is initialization and opening interfaces. Initialization is to find the corresponding driver, find the corresponding device parameters, and complete device initialization according to the previously defined device tree. The opening function searches for the device according to the passed device name, and returns the device handle after finding it. All subsequent operations require this device handle.
「3. Simple GUI layer」
The most important function at present is to display characters.
s32dev_lcd_put_string(DevLcd*lcd,FontTypefont,intx,inty,char*s,unsignedcolidx)
The other functions for drawing lines and circles are currently just for testing and will be improved later.
Driver IC layer
The driver IC layer is divided into two parts:
「1. Encapsulate LCD interface」
LCDs use 8080 bus, SPI bus, and VSPI bus. The functions of these buses are implemented in separate files. However, in addition to these communication signals, LCDs also have reset signals, command data line signals, backlight signals, etc. We use function encapsulation to encapsulate these signals together with the communication interface into "LCD communication bus", that is, buslcd. BUS_8080 is encapsulated in dev_ILI9341.c file. BUS_LCD1 and BUS_lcd2 are encapsulated in dev_str7565.c.
「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 and uses the official interface. The SPI interface provides the following operation functions, which can operate SPI or 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 modules mentioned above connected together? Please see the following structure:
/* 初始化的时候会根据设备数定义,
并且匹配驱动跟参数,并初始化变量。
打开的时候只是获取了一个指针 */
struct _strDevLcd
{
s32 gd;//句柄,控制是否可以打开
LcdObj *dev;
/* LCD参数,固定,不可变*/
_lcd_pra *pra;
/* LCD驱动 */
_lcd_drv *drv;
/*驱动需要的变量*/
u8 dir; //横屏还是竖屏控制:0,竖屏;1,横屏。
u8 scandir;//扫描方向
u16 width; //LCD 宽度
u16 height; //LCD 高度
void *pri;//私有数据,黑白屏跟OLED屏在初始化的时候会开辟显存
};
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;//设备名字
LcdBusType bus;//挂在那条LCD总线上
u16 id;
}LcdObj;
-
The member pra points to the LCD parameters, which can tell the specifications of the LCD.
typedef struct
{
u16 id;
u16 width; //LCD 宽度 竖屏
u16 height; //LCD 高度 竖屏
}_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. Because each LCD has a structure, a set of drivers can control multiple devices without interfering with each other.
-
The member pri is a private pointer. Some drivers may need some special variables, which are all recorded with this pointer. Usually this pointer points to a structure, which is defined by the driver and applies for variable space when the device is initialized. Currently, it is mainly used for COG LCD and OLED LCD display cache.
The entire LCD driver is combined together through this structure.
1. Initialization, find the driver and parameters according to the device tree, and then initialize the structure mentioned above.
2. Before using LCD, call dev_lcd_open function. If it is opened successfully, it will return a pointer to the structure above.
3. Display characters. After the interface finds the dot matrix, it calls the corresponding driver through the drv of the above structure.
4. The driver decides which LCD bus to operate based on this structure and uses the variables of this structure.
Usage and Benefits
Please see the test procedure:
void dev_lcd_test(void)
{
DevLcd *LcdCog;
DevLcd *LcdOled;
DevLcd *LcdTft;
/* 打开三个设备 */
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");
/*打开背光*/
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);
}
Use a function dev_lcd_open to open three LCDs and get the LCD device. Then call dev_lcd_put_string to display on different LCDs. All other GUI operation interfaces have only one. This design is very friendly to the APP layer. Display effect:
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 you want to connect an oled lcd to SPI, you only need to change the parameters in the device tree array. Of course, you cannot connect two devices to one interface.
LcdObj LcdObjList[DEV_LCD_C]=
{
{"oledlcd", LCD_BUS_SPI, 0X1315},
{"tftlcd", LCD_BUS_8080, NULL},
};
Fonts
I won't go into details for now. The example font library is placed in the SD card. You can modify it as needed when porting. For details, refer to font.c.
statement
Please use the code in accordance with the copyright agreement. The current source code is just a usable design, and its integrity and robustness have not been tested. It will be put on GitHub in the future and continuously updated and optimized.