简介:STM32驱动1.8英寸TFT彩屏的完整工程实现,基于SPI串口显示屏模块,同时提供标准库与HAL库两套代码,覆盖屏幕初始化、SPI读写、像素与图形显示等关键环节,面向需要快速点亮LCD、学习底层显示驱动,或在既有STM32项目中接入彩屏的开发者。资源以ZIP压缩包形式发布,整体约40.8MB,下载页未标注具体文件数量与类型明细;内容涵盖标准库与HAL库两版驱动源码、显示测试工程及移植注释,便于对照和复用。已有2806人学习下载。作者将1.8寸TFT屏驱动拆解为GPIO/SPI配置、屏幕初始化时序、显示函数封装等模块,标准库与HAL库的差异在代码中均有体现;对于遇到花屏、白屏或SPI时序不稳的初学者,也可顺着工程配置和注释快速定位问题,节省查手册与调试时间,形成一套可复用的彩色LCD驱动框架。
1. 先搞清楚这块屏是什么:1.8寸TFT的控制器、接口和代码坑
刚过点灯关的初学者,压箱底的外设多半有块1.8寸TFT彩屏。多数模块的驱动芯片是ST7735,分辨率128x160,走SPI串口,四根信号线加电源就能亮。真正劝退的不是硬件,而是代码:网上教程标准库和HAL库各占一半,初始化流程和寄存器配置却不完全一样,直接抄十有八九白屏或花屏。这套资源正好解决这个缺口,把标准库和HAL库两套驱动代码分别打通,从GPIO、SPI初始化、寄存器初始化序列、绘点和清屏函数到文字显示都排好了。新手照接线表连好线就能看到彩色画面,熟手也能在这套代码基础上改分辨率、换控制器或接DMA。
2. 硬件接线和SPI参数选型:连线不对,后面全是白屏
2.1 认识ST7735控制器与引脚定义
1.8寸TFT屏的分辨率固定为128x160,绝大多数模块使用ST7735R驱动芯片,少部分用ILI9163。两个芯片的引脚定义几乎一样,但初始化寄存器差异较大,代码不能通用。所以在动手之前,先看屏幕背面丝印或者向卖家确认驱动型号,这决定了后面抄哪一套初始化序列。
典型的模块有8个引脚:VCC、GND、CS、RST、RS(也叫DC/A0)、SDA、SCK、LED。CS是片选,低电平有效;RST是复位,同样低电平有效;RS是数据/命令选择线,高电平表示正在发送像素数据,低电平表示发送寄存器命令;SDA接SPI的MOSI;SCK接时钟。LED是背光引脚,多数模块板上已经放好了限流电阻,直接用3.3V点亮即可。
有几个常见变体要注意:有的模块把LED和VCC合并成一个引脚,那就是7脚版本;有的把CS直接接地,剩下6脚。不管哪种版本,RS、RST、SDA、SCK四根线必须独立引出。另外这个屏没接MISO,所以硬件SPI回读功能不可用,读寄存器ID会得到默认值,属正常现象,别拿着ILI9341的ID表对着这里怀疑屏坏了。
2.2 与STM32的接线表:以SPI1为例
我习惯把屏幕挂在SPI1上,因为STM32F103的SPI1跑在APB2总线上,时钟可以到72MHz,后续要提速也比较方便。下面是这套资源统一使用的接线方式,标准库和HAL库两套代码都遵循这个映射。
| 屏幕引脚 | 功能 | 接STM32 | 说明 |
|---|---|---|---|
| VCC | 电源 | 3.3V | 严禁接5V,模块上没有LDO的话直接烧 |
| GND | 地 | GND | |
| CS | 片选 | PB12 | 软件GPIO控制,低有效 |
| RST | 复位 | PB13 | 低电平复位,上电拉低至少20ms |
| RS/DC | 数据/命令选择 | PB14 | 高=像素数据,低=寄存器命令 |
| SDA | SPI数据输入 | PA7 | SPI1_MOSI |
| SCK | SPI时钟 | PA5 | SPI1_SCK |
| LED | 背光 | 3.3V或PA1 | 需要调亮度时用PWM驱动 |
CS使用软件GPIO控制而不是硬件NSS,原因很实际:硬件NSS在F103上默认由PA4接管,一旦SPI配置成硬件NSS模式而PA4又没有正确驱动,SPI主机可能一直处于禁用状态,屏幕怎么初始化都没反应。软件GPIO控制CS,既灵活又能为以后在同一SPI总线上挂多个从设备留余地。
这里单独提醒一句电源的事。1.8寸屏工作电流其实不大,但很多新手图省事直接从板子的5V引脚取电,又没有稳压,结果就是屏幕发烫或者直接烧掉主控。VCC一定接3.3V,如果你用的是带AMS1117的板子,从3.3V引脚引出,同时在VCC和GND之间并一个10uF电容,上电瞬间的浪涌会小很多。
2.3 SPI模式:为什么默认是模式0,花屏时再试模式3
SPI有四种模式,由时钟极性CPOL和时钟相位CPHA组合而成。CPOL决定空闲时SCK是高还是低,CPHA决定数据在第一个边沿还是第二个边沿被采样。ST7735数据手册写的很清楚:SCK空闲为低电平,数据在SCK上升沿被锁存,这正好对应SPI模式0,也就是CPOL=0、CPHA=0。
实际项目里会碰到一批屏,用模式0显示异常,改成模式3反而正常。原因是不同批次的ST7735内部数字逻辑对时序边沿的要求略有差异,或者模块PCB上的走线引入了额外延迟。经验做法是:先固定模式0,不行就切到模式3,两个方向都试过再定。这块屏的SPI时钟上限大约在15MHz左右,STM32F103的APB2是72MHz,预分频8得到9MHz是安全值。等显示稳定后再尝试预分频4跑到18MHz,我的经验是大多数屏能跑,但个别模块在快速刷新时会出现细雪花。
如果你手头的模块接线长度超过10cm,或者用了杜邦线飞线,建议把预分频直接降到16,也就是4.5MHz,长导线的寄生电容会让高频信号变形,这时候不是驱动代码的问题,是物理链路的问题。
3. 标准库驱动实现:从GPIO配置到第一个像素
3.1 标准库下的SPI1与GPIO初始化
标准库的代码结构比较直接,分三块:开时钟、配GPIO、配SPI。先看引脚和SPI初始化,这是整个驱动的地基。
void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_SPI1, ENABLE); // PA5=SCK, PA7=MOSI,复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // CS=PB12, RST=PB13, DC=PB14,普通推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); } void LCD_SPI_Init(void) { SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // CPOL=0 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // CPHA=0,模式0 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件NSS SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; // 72/8=9MHz SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }代码逻辑不复杂:PA5和PA7配置成复用推挽,硬件SPI外设会自动控制这两个引脚的电平;PB12、PB13、PB14则是普通GPIO,后面用宏定义直接拉高拉低。SPI_Direction选全双工是因为标准库在单发模式下也要走读RXNE标志位,索性配成全双工,MISO不接也不影响。SPI_BaudRatePrescaler_8配合72MHz主频得到9MHz时钟,这是调试期的保守值。
我把CS、RST、DC这三个控制引脚写成了宏,后面所有代码都通过宏操作,方便你改成别的引脚:
#define LCD_CS_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_12) #define LCD_CS_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_12) #define LCD_RST_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_13) #define LCD_RST_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_13) #define LCD_DC_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_14) #define LCD_DC_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_14)3.2 ST7735初始化序列:一份可以直接抄的寄存器清单
ST7735的初始化序列是整个驱动里最玄学的部分。寄存器写错一位,轻则颜色偏,重则整屏花掉。下面这份序列是ST7735官方初始化代码的整理版,常见红板、蓝板1.8寸屏都能用。
void LCD_Init(void) { LCD_RST_LOW(); delay_ms(20); LCD_RST_HIGH(); delay_ms(120); LCD_WriteCmd(0x01); // 软件复位 delay_ms(120); LCD_WriteCmd(0x11); // 退出睡眠模式 delay_ms(80); LCD_WriteCmd(0xB1); // 帧率控制 LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteCmd(0xB2); // 非活动帧率 LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteCmd(0xB3); // 显示反转模式 LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteCmd(0xC0); // 电源控制1 LCD_WriteData(0x28); LCD_WriteData(0x08); LCD_WriteData(0x04); LCD_WriteCmd(0xC1); // 电源控制2 LCD_WriteData(0xC0); LCD_WriteCmd(0xC2); // 电源控制3 LCD_WriteData(0x0D); LCD_WriteData(0x00); LCD_WriteCmd(0x36); // 内存数据访问控制 LCD_WriteData(0xC8); // MX+MY+BGR,方向+颜色 LCD_WriteCmd(0x3A); // 像素格式 LCD_WriteData(0x05); // 16位RGB565 LCD_WriteCmd(0xE0); // 正极性伽马校正 LCD_WriteData(0x0F); LCD_WriteData(0x1A); LCD_WriteData(0x0F); LCD_WriteData(0x18); LCD_WriteData(0x07); LCD_WriteData(0x1A); LCD_WriteData(0x07); LCD_WriteData(0x02); LCD_WriteData(0x07); LCD_WriteData(0x1B); LCD_WriteData(0x00); LCD_WriteData(0x1F); LCD_WriteData(0x14); LCD_WriteData(0x00); LCD_WriteData(0x05); LCD_WriteData(0x0F); LCD_WriteCmd(0xE1); // 负极性伽马校正 LCD_WriteData(0x00); LCD_WriteData(0x1B); LCD_WriteData(0x00); LCD_WriteData(0x18); LCD_WriteData(0x07); LCD_WriteData(0x1B); LCD_WriteData(0x07); LCD_WriteData(0x02); LCD_WriteData(0x07); LCD_WriteData(0x1B); LCD_WriteData(0x00); LCD_WriteData(0x1F); LCD_WriteData(0x14); LCD_WriteData(0x00); LCD_WriteData(0x05); LCD_WriteData(0x0F); LCD_WriteCmd(0x29); // 开启显示 delay_ms(50); LCD_WriteCmd(0x2C); // 准备写入显存 }重点说三个关键寄存器。0x36是方向控制,0xC8的含义是MX+MY同时置位并追加BGR,这样屏幕左上角对应坐标(0,0),同时颜色顺序按BGR排列。如果你的模块显示颜色异常,把这里改成0x08试一次。0x3A设成0x05表示RGB565格式,之后每个像素数据都是两字节,先高字节后低字节,千万别在这上面搞错。0x01软复位后要等够120ms,很多白屏问题就出在这个延时被缩短了。
注意这套序列里我用的是标准库的delay_ms函数,有的工程里叫Delay_Ms。实现方式不限制,但你一定要确认延时是ms级别而不是us级别,否则整个初始化时序全部错位。
3.3 写命令、写数据与绘点画矩形
寄存器序列写好了,接下来就是最底层的三个函数:写命令、写数据、设置窗口。这三个函数决定了所有显示操作能不能成立。
void SPI1_SendByte(uint8_t byte) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, byte); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); SPI_I2S_ReceiveData(SPI1); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET); } void LCD_WriteCmd(uint8_t cmd) { LCD_CS_LOW(); LCD_DC_LOW(); SPI1_SendByte(cmd); LCD_CS_HIGH(); } void LCD_WriteData(uint8_t data) { LCD_CS_LOW(); LCD_DC_HIGH(); SPI1_SendByte(data); LCD_CS_HIGH(); }这里有个细节值得说透。SPI_I2S_SendByte结尾等待BSY标志位清空再拉高CS,非常关键。如果不等BSY就拉高CS,最后一个字节可能还没完全从移位寄存器推出去,导致显示内容缺字节。这个坑在低速SPI下不明显,一旦分频系数调小就会随机出现横纹或残影,根源就在这里。
接下来设置窗口并绘点:
void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); // 列地址 LCD_WriteData(x0 >> 8); LCD_WriteData(x0 & 0xFF); LCD_WriteData(x1 >> 8); LCD_WriteData(x1 & 0xFF); LCD_WriteCmd(0x2B); // 行地址 LCD_WriteData(y0 >> 8); LCD_WriteData(y0 & 0xFF); LCD_WriteData(y1 >> 8); LCD_WriteData(y1 & 0xFF); LCD_WriteCmd(0x2C); // 开始写入显存 } void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if (x >= 128 || y >= 160) return; LCD_SetWindow(x, y, x, y); LCD_WriteData(color >> 8); LCD_WriteData(color & 0xFF); } void LCD_FillRect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint16_t i, j; LCD_SetWindow(x0, y0, x1, y1); for (i = 0; i <= y1 - y0; i++) { for (j = 0; j <= x1 - x0; j++) { LCD_WriteData(color >> 8); LCD_WriteData(color & 0xFF); } } }LCD_FillRect是一次性设置窗口然后连续写像素,比逐个像素调用LCD_SetWindow快得多。窗口范围是闭区间,填充126x158的矩形写的是0到126列、0到158行,这一点和很多人的数组下标习惯不一样,容易多写或少写一行像素。
颜色常量建议直接定义好:
#define WHITE 0xFFFF #define BLACK 0x0000 #define RED 0xF800 #define GREEN 0x07E0 #define BLUE 0x001F #define YELLOW 0xFFE04. HAL库实现:用CubeMX配好SPI再跑通文本显示
4.1 CubeMX里的SPI参数配置
HAL库和标准库最大的区别在于初始化代码由CubeMX生成,你不用手写时钟树和GPIO配置,但参数选错屏幕照样不亮。新建STM32F103C8工程后,按下面这组参数配置SPI1。
SPI1模式选择Transmit Only Master即可,因为我们只往屏幕写数据,不读MISO。参数页里注意这几项:
| 参数 | 值 | 说明 |
|---|---|---|
| Mode | Transmit Only Master | 不需要回读ID |
| Data Size | 8 Bit | 逐字节发送 |
| First Bit | MSB First | ST7735要求高位先发 |
| Prescaler | 8 | 72MHz/8=9MHz |
| CPOL | Low | 模式0 |
| CPHA | 1 Edge | 模式0 |
| NSS | Disable | 使用软件GPIO控制CS |
NSS这里一定要选Disable,对应的就是标准库里的SPI_NSS_Soft。如果选成Hardware,CubeMX会把PA4当作硬件NSS引脚,而屏幕的CS接在PB12上,两边的电平状态完全对不上,SPI主机会一直处于禁用状态。
GPIO方面,PA5、PA7会被CubeMX自动配置成SPI1的复用功能,PB12、PB13、PB14手动配置为GPIO_Output,初始电平都设成High。LED如果要用PWM调背光,那就多配一路定时器PWM输出,这里先用3.3V直连。
4.2 HAL库底层封装:命令/数据写入函数
HAL库的底层写入和标准库思路一致,只是发送函数从SPI_I2S_SendData换成了HAL_SPI_Transmit。注意HAL_SPI_Transmit自带超时参数,如果总线异常,函数会返回HAL_TIMEOUT而不是卡死。
void LCD_WriteCmd(uint8_t cmd) { LCD_CS_LOW(); LCD_DC_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); LCD_CS_HIGH(); } void LCD_WriteData(uint8_t data) { LCD_CS_LOW(); LCD_DC_HIGH(); HAL_SPI_Transmit(&hspi1, &data, 1, 100); LCD_CS_HIGH(); } void LCD_WriteDataBuffer(uint8_t *buf, uint16_t len) { LCD_CS_LOW(); LCD_DC_HIGH(); HAL_SPI_Transmit(&hspi1, buf, len, 1000); LCD_CS_HIGH(); }三个函数的分工很清楚:写命令时DC拉低,写数据时DC拉高,批量写数据时一次性传一个缓冲区。批量写数据函数在填充矩形时特别好用,可以提前准备好一行像素的缓冲区再整个扔给SPI,比逐个字节切换CS快很多。
有一个和标准库不一样的点:HAL_SPI_Transmit是阻塞调用,底层会等到SPI数据传输结束才返回,所以不需要像标准库那样单独等待BSY标志。但这也意味着在传输结束前CPU被占着,如果你在做需要高响应速度的实时系统,后面最好换DMA版本。
4.3 显示文本:8x16字体加上字符串函数
显示文本是整个驱动里最常用也最直观的功能。8x16点阵字体每个字符占16字节,每字节的8个bit对应一行中的8个像素点。这里给出一个精简版显示函数。
const uint8_t font8x16[95][16] = { // 0x20空格到0x7E波浪号,内容用取模软件生成 // 每行16字节,bit为1则点亮对应像素 }; void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t color) { uint8_t i, j; uint16_t index = ch - ' '; for (i = 0; i < 16; i++) { for (j = 0; j < 8; j++) { if (font8x16[index][i] & (0x80 >> j)) { LCD_DrawPoint(x + j, y + i, color); } } } } void LCD_ShowString(uint16_t x, uint16_t y, char *str, uint16_t color) { while (*str) { LCD_ShowChar(x, y, *str++, color); x += 8; if (x > 128 - 8) { x = 0; y += 16; } } }LCD_ShowChar里最关键的是取模方向。font8x16[index][i]里,i是行号,j是列号,用0x80右移j的方式逐位判断。如果你从取模软件导出的字体数据是按列扫描的,这个判断逻辑就要反过来写,否则字符会整个颠倒或错位。
LCD_ShowString在字符超出右边界时自动换行,每次换行y坐标增加16像素。这个函数没有做下边界判断,如果你塞进去一长串字符串,超出屏幕的部分会被LCD_DrawPoint的坐标保护直接丢弃。实际项目中我会在字符串超过屏幕高度时强制返回,避免出现半个字符显示在屏幕外的情况。
5. 避坑指南:白屏、花屏、颜色颠倒的排查与修复
5.1 屏幕亮但全白:复位时序没给够
现象:背光亮,屏幕全白或全黑,初始化代码执行完没有任何反应。
原因:RST引脚拉低时间太短。ST7735硬件复位要求低电平保持至少10us,但实际模块参数离散,很多屏需要几十毫秒才完成内部上电复位。另外,代码里上电后立刻初始化也是个隐患,电源还没稳定,寄存器写入全部无效。
解决:上电后先等100ms,再把RST拉低20ms,拉高后等120ms再发第一条初始化命令。0x11退出睡眠指令之后也要留80ms以上延时。如果你发现白屏问题时有时无,重点查这里。
5.2 花屏乱码:SPI模式或分频系数不对
现象:纯色块能显示,但文字和图片完全花掉,像打满马赛克。
原因:SCK采样沿和屏幕不匹配。模式0和模式3在数据手册上都贴着“推荐”,但实际模块批次差异让同一段代码在不同屏上表现不一致。另一个常见原因是SPI分频调太低,72MHz主频下预分频2就是36MHz,远超ST7735的15MHz上限。
解决:先把分频系数固定成8,排除时钟速度因素。然后分别用模式0和模式3跑纯色测试,确定当前模块该用哪种模式。这条经验在换成ILI9163驱动芯片的屏时同样有效,那些屏对模式的要求可能完全相反。
5.3 颜色全反:BGR位和字节序
现象:红色显示成蓝色,绿色显示成紫色,整个画面像调了滤镜。
原因:0x36寄存器的BGR位设置不对,或者RGB565数据发送时高低字节顺序写反。红色在RGB565里是0xF800,高字节0xF8低字节0x00,如果先发低字节就变成了0x001F,也就是蓝色。
解决:先用0xF800填充全屏测试红色。如果显示蓝色,把0x36寄存器的值在0x08和0xC8之间切换试试。如果切换后还是蓝,那就是字节序问题,把LCD_WriteData(color >> 8)和LCD_WriteData(color & 0xFF)两行颠倒顺序。做这两个操作时一次只改一个变量,不要同时改,否则你永远不知道是哪边修好的。
5.4 标准库能点亮,HAL库死活不亮
现象:同样的屏,标准库代码跑得好好的,换成HAL库工程后完全没有显示。
原因:CubeMX生成的代码里少了某个外设时钟,或者SPI的NSS被配置成了硬件模式。还有一种是PA5和PA7的复用类型没选对,GPIO模式仍然保持了一个analog,导致SPI时钟根本出不来。
解决:打开CubeMX,确认SPI1的NSS选择Disable,PA5和PA7在GPIO配置面板里显示为SPI1_SCK和SPI1_MOSI复用功能。然后在main.c里查MX_GPIO_Init和MX_SPI1_Init两个函数是否都被调用了,缺一个都不行。如果还不行,就用debugger在HAL_SPI_Transmit处下断点,看返回值是不是HAL_BUSY。
5.5 高速刷新出现白条或闪屏
现象:单帧显示正常,一旦连续快速刷新,屏幕随机出现白色横纹或整片闪烁。
原因:SPI分频太激进,模块跟不上;或者用DMA传输时,上一帧还没传完就启动了下一帧的DMA请求,数据队列被冲掉。
解决:分频先回到8,保证9MHz时钟。DMA方案里,必须在HAL_SPI_TxCpltCallback回调函数中确认传输完成后再启动下一帧,两次传输之间加至少1ms间隔。检查CS拉高的时机是不是在DMA彻底结束之前,这个问题用示波器抓CS和SCK波形很容易定位。
6. 验证到进阶:纯色测试、中文显示与DMA刷屏
6.1 测试页函数:先确认底层全通再叠功能
新屏到手,我会先跑一段纯色测试页,把所有底层问题一次性暴露出来。这个测试函数值得保留下来,以后调任何一块屏都用得上。
void LCD_TestPage(void) { LCD_FillRect(0, 0, 127, 159, RED); delay_ms(300); LCD_FillRect(0, 0, 127, 159, GREEN); delay_ms(300); LCD_FillRect(0, 0, 127, 159, BLUE); delay_ms(300); LCD_FillRect(0, 0, 127, 159, WHITE); delay_ms(300); LCD_FillRect(0, 0, 63, 79, RED); LCD_FillRect(64, 80, 127, 159, BLUE); }红绿蓝三屏依次显示,能立刻判断颜色是否正确;最后那个分屏块,如果左上角和右下角颜色位置不对,说明0x36的MX/MY方向位设错了。这一段跑通,后面所有上层代码都安全。
6.2 显示中文:字模提取与存储
英文用8x16字体就够,中文需要16x16点阵。用PCtoLCD2002取模,设置阴码、逐行式、逆向输出,每生成一个汉字的32字节数组。显示的思路和英文一样,只不过一字节只够画半个字宽。
void LCD_ShowChinese(uint16_t x, uint16_t y, const uint8_t *font, uint16_t color) { uint8_t i, j; for (i = 0; i < 16; i++) { for (j = 0; j < 8; j++) { if (font[i * 2] & (0x80 >> j)) LCD_DrawPoint(x + j, y + i, color); if (font[i * 2 + 1] & (0x80 >> j)) LCD_DrawPoint(x + 8 + j, y + i, color); } } }取模方向务必和代码读取顺序保持一致。PCtoLCD2002里常见两种方式:逐行式是按行逐位输出,逐列式是按列扫描。用上面这段代码就必须选逐行式,否则汉字会像倒过来一样。
6.3 用SPI DMA把刷新率再提一档
阻塞式SPI发送一个字节CPU要等好几百个周期,刷一屏数据大部分时间浪费在等待上。DMA可以解决这个问题,让SPI自己从内存搬运数据,CPU去干别的活。CubeMX里给SPI1添加TX DMA请求即可,然后在代码里直接调用HAL_SPI_Transmit_DMA。
uint8_t line_buf[128 * 2]; // 一行像素,RGB565共256字节 void LCD_DrawLineDMA(uint16_t y, uint16_t *pixels) { uint8_t cmd = 0x2C; memcpy(line_buf, pixels, 256); LCD_CS_LOW(); LCD_DC_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); LCD_DC_HIGH(); HAL_SPI_Transmit_DMA(&hspi1, line_buf, 256); } void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { LCD_CS_HIGH(); } }这块屏分辨率128x160,RGB565整帧数据是40960字节,STM32F103C8的20KB SRAM装不下,所以不要试图一次性传完整帧。把屏幕拆成一行或者几行,逐行用DMA推送,效果已经比阻塞式快很多。DMA传输的缓冲区在传输完成前不能被改写,所以上面代码里先用memcpy备份一行,否则下一行数据填充时会破坏当前行内容。
从那以后,我每次接手一块没点亮过的屏,都会先跑一遍6.1的纯色测试页,确认颜色和坐标方向都对了,再继续叠字库、调DMA,这套流程帮我省下了大量重复排错的时间,希望帮到你。
本文还有配套的精品资源,点击获取