简介:在嵌入式开发中,微控制器与显示屏的通信往往取决于对底层协议与初始化时序的精准把控。SPI通信作为一种高速同步串行接口,常被用于连接STM32与TFT液晶屏等外设。正确配置时钟极性、分频系数以及显示控制器的初始化序列,是实现清晰稳定显示的关键。ST7735是常见的小尺寸TFT驱动芯片,通过窗口寻址和像素填充即可完成文字、图片与动画渲染。本文以STM32F103RCT6裸机环境为例,提供一套完整的SPI驱动方案,涵盖引脚分配、初始化流程、字库显示及常见问题排查,适合刚接触屏幕驱动的新手快速点亮屏幕,也可作为后续开发调试屏或菜单界面的基础参考。
1. 想点亮一块屏,最折磨你的不是接线而是初始化序列
想用 STM32F103RCT6 驱动一块 1.44 寸 TFT 屏幕,真正劝退新人的往往不是接线,而是那套说不清的 ST7735 初始化命令和字库取模。这个工程是一套可以直接编译烧录的完整裸机 Demo,引脚定义、SPI 初始化、屏幕驱动、ASCII 字库、中文取模、图片显示和动画都封装好了,你只要照着原理图接好 PA5、PA7、PA4 这几个脚,烧录后就能看到清屏、色块、字符切换和百叶窗动画。它适合两类人:第一次接触 TFT、想跳过底层细节直接看到效果的新手;以及想把它当调试屏、二次改造成菜单或仪表盘的老手。代码基于标准外设库,没有跑操作系统,逻辑顺着 main 函数就能读完。
2. 整体设计:从硬件引脚到代码分层的全景
2.1 为什么是 STM32F103RCT6 + 1.44TFT
STM32F103RCT6 在这个场景下算得上"性能富余"。主频 72MHz,Flash 256KB,SRAM 48KB,而驱动 1.44TFT 只需要一路 SPI,屏幕分辨率 128x128,单像素 RGB565 占 2 字节,整屏数据拆到 MCU 里约 32KB,连一半 SRAM 都用不到。这意味着你可以在显示的同时跑 ADC 采样、定时器、甚至塞一个轻量级菜单状态机,不会把资源挤爆。
选屏时有个细节容易踩坑:市面上很多 1.44 寸模组用的驱动 IC 是 ST7735R,但也有改版驱动和带 SPI Flash 的版本,引脚叫法还不统一,有人叫 DC、有人叫 RS,背光默认电平也分高亮和低亮。这套工程按最常见的 ST7735R SPI 模组来设计,接口定义如下:
| 屏幕引脚 | 对应MCU引脚 | 说明 |
|---|---|---|
| SCL/SCK | PA5 | SPI1_SCK,空闲低电平 |
| SDA/MOSI | PA7 | SPI1_MOSI,只写不读 |
| CS | PA4 | 片选,低有效 |
| DC/RS | PA0 | 数据/命令切换 |
| RESET/RST | PA1 | 复位,低有效 |
| BLK/BL | PA2 | 背光控制,高电平亮 |
这套接法只占用 6 个 GPIO,剩下的口都能自由发挥。要注意的是屏幕模组供电是 3.3V,控制引脚电平也必须是 3.3V 逻辑。如果你用的是 5V 单片机或者经过电平转换的板子,务必确认 IO 是否兼容 5V,否则很容易先让屏幕发烫,再烧坏引脚。我在第一次调试时就因为背光脚接反,白屏加屏幕发热折腾了半天。
2.2 工程目录结构
拿到工程后别急着点编译,先看目录。整个工程按标准外设库习惯组织,没有用 RTOS,方便你沿着一条调用链把显示逻辑走通:
STM32F103RCT6_1.44TFT/ ├─ CORE/ # 启动文件、内核寄存器定义 ├─ HARDWARE/ │ ├─ TFT/ │ │ ├─ tft.c # ST7735驱动:初始化、画点、窗口、填充 │ │ ├─ tft.h # 引脚宏、颜色宏、屏幕分辨率 │ │ ├─ text.c # 字符、汉字、数字显示 │ │ ├─ text.h │ │ ├─ picture.c # 全屏图片显示 │ │ └─ font.h # ASCII、16x16汉字字模 ├─ STM32F10x_FWLib/ # 标准外设库源码 ├─ SYSTEM/ # 延时、系统时钟配置 │ ├─ delay/ │ └─ sys/ └─ USER/ ├─ main.c # 演示入口 └─ stm32f10x_it.c # 中断向量把驱动单独放在 HARDWARE/TFT 里,好处是上层代码完全不需要知道 ST7735 内部寄存器是什么。tft.c 负责所有和屏控制器打交道的脏活,text.c 和 picture.c 只拼装数据,main.c 负责演示顺序。以后你想换屏,只要重写 tft.c 的底层接口,上层全部复用。
2.3 软件分层与数据流
整套代码可以分成三层:驱动层、功能层、应用层。main 里调用 LCD_Fill 时,完整链路是:应用层给出坐标和颜色,功能层直接调用驱动层准备好的 LCD_SetWindow 和 SPI 写数据。ST7735 控制器的内部会自动按行列递增地址,只要窗口设置正确,底层的像素填充不会出现错位或漏点。
这次没有在显示路径上启用 DMA,因为屏幕数据量不大,SPI 中断方式足够。但如果后续你要做全屏连续刷新,DMA 是值得优化的方向——一次准备好显存数组,用 DMA 搬运,CPU 就能腾出手处理业务逻辑。工程里没有做这一层抽象,给你留了改造空间。
3. 核心驱动拆解:一条显示数据从发出到上屏的完整链路
3.1 SPI 初始化:时钟极性、分频系数怎么设
屏幕通信靠的是 SPI1,整个初始化函数不长,但几个参数必须和屏控制器匹配。ST7735 手册明确推荐 SPI mode 1:空闲时时钟为低,数据在第二个边沿采样。如果把极性写反,屏幕也能亮,但你会发现清屏时出现均匀暗条纹,颜色不正,那是因为数据 bit 在错误的边沿被锁存。
void SPI1_Init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); gpio.GPIO_Mode = GPIO_Mode_AF_PP; // SCK 和 MOSI 复用推挽 gpio.GPIO_Speed = GPIO_Speed_50MHz; gpio.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_Init(GPIOA, &gpio); // CS、DC、RST、BLK 都用普通推挽 IO gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_Init(GPIOA, &gpio); spi.SPI_Direction = SPI_Direction_2Lines_FullDuplex; spi.SPI_Mode = SPI_Mode_Master; spi.SPI_DataSize = SPI_DataSize_8b; spi.SPI_CPOL = SPI_CPOL_Low; // 空闲低电平 spi.SPI_CPHA = SPI_CPHA_2Edge; // 第二边沿采样 spi.SPI_NSS = SPI_NSS_Soft; spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 72/4=18MHz spi.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &spi); SPI_Cmd(SPI1, ENABLE); }这里把 SCK 和 MOSI 配成复用推挽,控制脚用普通推挽。SPI_BaudRatePrescaler_4 对应 18MHz 时钟,对绝大多数 ST7735 模组是稳定值。想提速可以改成 Prescaler_2 到 36MHz,但信号线一长就容易出现花点,我一般先稳定跑通再往上压。
3.2 ST7735 初始化序列:一屏花屏先查这里
TFT 模组上电后并不是直接就能显示,控制器默认寄存器状态很保守,必须由 MCU 发一串初始化命令。这个工程用的是通用性较好的序列,先把复位时序走完,再逐条配置帧率、像素格式、扫描方向。
void TFT_Init(void) { TFT_RST_LOW; delay_ms(20); TFT_RST_HIGH; delay_ms(20); TFT_Write_Cmd(0x01); // Software Reset delay_ms(10); TFT_Write_Cmd(0x11); // Sleep Out delay_ms(10); TFT_Write_Cmd(0xB1); // 帧率配置:0x05 0x3A TFT_Write_Data(0x05); TFT_Write_Data(0x3A); TFT_Write_Cmd(0x20); // Display Inversion On TFT_Write_Cmd(0x36); // 地址模式:MX、MY、BGR TFT_Write_Data(0x08); TFT_Write_Cmd(0x3A); // 像素格式:16bit TFT_Write_Data(0x05); TFT_Write_Cmd(0x29); // Display ON }注意 0x11(Sleep Out)发出后至少要等 10ms 再发后续命令,否则屏响应不过来。0x36 是扫描方向命令,控制横屏、竖屏和镜像,0x08 表示 BGR 顺序,如果你看到红蓝互换,改这个值就行。整串命令少一条或者顺序颠倒,最直接的现象就是整屏花或者颜色诡异。
3.3 窗口、画点与填充:坐标怎么映射到像素
ST7735 不是显存随机访问,而是先指定一个矩形窗口,然后连续写像素数据,控制器按行列自动递增。窗口函数是驱动层最关键的接口:
void LCD_SetWindow(unsigned int x0, unsigned int y0, unsigned int x1, unsigned int y1) { TFT_DC_LOW; SPI_WriteByte(0x2A); // 列地址设置 TFT_DC_HIGH; SPI_WriteByte(x0 >> 8); SPI_WriteByte(x0 & 0xFF); SPI_WriteByte(x1 >> 8); SPI_WriteByte(x1 & 0xFF); TFT_DC_LOW; SPI_WriteByte(0x2B); // 行地址设置 TFT_DC_HIGH; SPI_WriteByte(y0 >> 8); SPI_WriteByte(y0 & 0xFF); SPI_WriteByte(y1 >> 8); SPI_WriteByte(y1 & 0xFF); TFT_DC_LOW; SPI_WriteByte(0x2C); // 写显存命令 }这里的高字节和低字节必须分开传,很多第一次接触 ST7735 的人以为直接传一个 16 位整数就行,实际控制器是 8 位时序,分两次发。窗口是包含端点的,x0=0、x1=127 才是全屏。一旦窗口设错,画面边缘会出现整行错位的现象,因为控制器在窗口结束后自动 wrap。
画点和填充共用同一套逻辑:画点是设置同样坐标的窗口后写 2 字节颜色;填充是设置整块区域后循环写。工程里的 LCD_Fill 还做了一处边界处理:在矩形边缘补齐数据,规避部分 ST7735 模组在窗口读取越界时产生整行漂移的毛病。
4. 显示进阶:字库、图片与简单动画的实现
4.1 中英文混排:取模数据与坐标自增
显示字符本质上是把点阵字模读出来,按位判断是前景色还是背景色。ASCII 字模放在 font.h,汉字用的是 GB2312 编码、16x16 点阵,取模工具导出格式是逐行式。如果字模格式和这套代码对不上,屏幕上显示的就是一团雪花点。
void LCD_ShowString(unsigned int x, unsigned int y, char *str, unsigned int color, unsigned int bg) { while (*str != 0) { if (*str >= 0x80 && *(str + 1) >= 0x80) // 中文字节大于0x80 { LCD_ShowChinese(x, y, *str, *(str + 1), color, bg); x += 16; str += 2; } else { LCD_ShowChar(x, y, *str, color, bg); x += 8; str++; } } }参数说明:x、y 是字符左上角坐标,屏幕宽 128 高 128,所以一行最多放 8 个汉字或 16 个 ASCII 字符。color 是前景色,bg 是背景色。想显示数字时,可以先把数字拆成字符再调用这个函数,或者自己在 text.c 里封装一个 LCD_ShowNum,把变量转成字符串。
4.2 全屏图片:BMP 转 C 数组再刷
图片显示的做法是把 128x128 的 BMP 位图先用 PC 工具转成 RGB565 格式的常数组,像素数据按从左到右、从上到下排列。
void LCD_ShowPicture(unsigned int x0, unsigned int y0, unsigned int x1, unsigned int y1, const unsigned char *pic) { unsigned int i; unsigned int w = x1 - x0 + 1; unsigned int h = y1 - y0 + 1; LCD_SetWindow(x0, y0, x1, y1); for (i = 0; i < w * h * 2; i++) { SPI_WriteByte(pic[i]); } }整个图片数组大小是 1281282 = 32768 字节,也就是 32KB。RCT6 的 256KB Flash 放两三张全屏图还行,图片多了就必须外挂 SPI Flash 或者做图像压缩。这里最需要注意的是字节顺序:如果取模工具导出的每个像素低字节在前,而屏控制器配置成了 RGB 顺序,颜色就会错乱。出现红蓝对调时,先改初始化序列里的 0x36 参数,而不是去调数组。
4.3 页面切换与动画:基于 fill 的动态效果
动画本质上是不断改变窗口内容和延时。工程里做了一个简单的百叶窗切换效果,逐行扫描填充颜色,模拟擦除过程:
void Demo_PageSwitch(void) { unsigned char row; LCD_Fill(0, 0, 127, 127, BLACK); delay_ms(500); for (row = 0; row < 128; row++) { LCD_Fill(0, row, 127, row, row % 2 ? BLUE : WHITE); delay_ms(1); } LCD_ShowString(16, 56, "Switch OK", RED, WHITE); }row 从 0 到 127 逐行刷新,奇偶行交替蓝白,每行延时 1ms,整屏切换约 128ms,人眼看起来是很流畅的过渡。如果把行扫描改成列扫描,就是左右推进效果。这算是将来做菜单翻页动画的雏形,在 UI 设计里很实用。
5. 避坑指南:TFT 驱动调试中的常见问题与排查思路
5.1 屏幕亮但全是乱码或花屏
现象是背光正常亮起,画面有很多均匀的彩色条纹,字符完全无法辨认。
原因:初始化序列不完整,或者 SPI 极性配置不对。最常见的是把 ST7735 的 mode 1 写成了 mode 0,导致控制器在错误的时钟边沿采样数据,所有命令和像素数据都错位。
解决:先做最小化验证,只清屏填充纯红、纯绿、纯蓝,看三原色是否正确。三种颜色能整屏正常显示,说明 SPI 通信没问题;不能,就对照 ST7735 手册核对 CPOL 和 CPHA。接着补充初始化序列,尤其是 Sleep Out 和 Display ON 之间的延时,不要偷懒。
5.2 颜色错乱,红蓝互换
现象:显示一张红色图片,屏幕显示蓝色,或者白色变成偏黄。
原因:RGB565 的字节序和控制器 RGB/BGR 标志不匹配。不同取模工具导出的数组可能是低位在前,也可能是高位在前,而 0x36 指令里的 RGB/BGR 位决定了控制器如何解释收到的数据。
解决:优先修改 0x36 指令的参数,把 0x08 改成 0xC8 可以翻转 RGB/BGR;再不行就交换图片数组中每个像素的两个字节。注意这种修改会影响所有显示内容,不要在业务代码里单独处理某一处。
5.3 SPI 引脚看不到波形
现象:程序正常跑,但是逻辑分析仪在 SCK 引脚上抓不到任何信号。
原因:GPIO 复用功能没有打开,或者 SPI1 外设时钟没使能。另一个高发原因是把 DC 或 CS 接到了 SPI1 的 MISO 引脚 PA6,PA6 默认被复用功能占用,普通输出配置不生效。
解决:检查 SPI1_Init 函数中 GPIO_Pin_5 和 GPIO_Pin_7 是否都是复用推挽,PA4、PA0 这些控制脚是否错配。硬件连接上,DC 不要接到 PA6,这脚在 SPI 通信里属于读路径,正好和这个工程只写不读的逻辑冲突。
5.4 刷新慢得像 PPT
现象:显示一张图片或者连续填充矩形,整屏刷新肉眼可见地慢,几百毫秒才能出结果。
原因:SPI 分频太大,或者上层代码把一条直线拆成逐点操作,每画一个点都调用一次 SetWindow。这样大部分时间都消耗在函数调用和重复命令发送上,真正传数据的带宽没多少。
解决:把 SPI_BaudRatePrescaler 从 8 调整到 4,SPI 时钟升到 18MHz。连续填充时只设置一次窗口,然后循环发送颜色数据,不要反复发 0x2A、0x2B、0x2C。跑通后可以尝试 36MHz,如果画面对比度出现异常,再降回来。
5.5 背光不亮或上电黑屏
现象:程序烧录成功,但屏幕全程黑屏,背光 LED 不亮。
原因:BLK 引脚默认拉低把背光关了,或者模组的背光硬件上直接接高,不需要软件控制。还有一种情况是上电后没有给足复位稳定时间,屏内部电源还没起来就收到了初始化命令。
解决:先用万用表量 BLK 电平,确认是高电平亮还是低电平亮,然后修改 tft.h 里的背光宏。复位时序别省,上电后至少延时 50ms,复位低脉冲保持 10ms 以上,再拉高。按这个顺序,几乎能解决九成的上电黑屏问题。
6. 二次开发:把它变成一块能看变量、刷帧率的调试屏
这个工程最值得改造的方向,是把 TFT 当成调试终端,直接显示单片机的实时状态。做法不复杂,在 text.c 里增加一个 LCD_ShowNum 接口,把 ADC 采样值、系统运行时间、帧率这些变量转成字符串,然后在 main 的循环里刷新。
while (1) { unsigned short adc_val = Read_ADC(); LCD_ShowString(8, 24, "ADC:", WHITE, BLACK); LCD_ShowNum(48, 24, adc_val, 4, YELLOW, BLACK); LCD_ShowString(8, 40, "FPS:", WHITE, BLACK); LCD_ShowNum(48, 40, fps, 3, GREEN, BLACK); delay_ms(200); }如果你还想画波形,那就再写一个画线函数,用 LCD_SetWindow 和画点填充一条直线路径,配合 ADC 采样就能滚动显示传感器曲线。做这类批量刷新时,关键是减少窗口设置次数,凡是整块变化区域,就一次设置窗口再连续写数据,比逐点刷新的效率高一个量级。
我从这几次调试里学到的习惯是,拿到任何屏幕工程先不急着换屏、换 MCU,第一件事是把上电复位、清屏、三原色显示这三步跑通。这三个基础点稳了,剩下的都是业务层问题,再出 bug 也能很快定位。这套流程让我少熬了不少夜,希望帮到你。
本文还有配套的精品资源,点击获取