手把手解析STM32标准库驱动ST7735S TFT屏:从寄存器配置到图形显示实战
1. STM32与ST7735S的基础认知
第一次接触STM32驱动TFT屏时,看着密密麻麻的引脚和陌生的寄存器名称,我也曾一头雾水。但经过几个项目的实战后,我发现只要掌握几个关键点,就能轻松驾驭这块1.8寸的ST7735S屏幕。让我们先从硬件连接开始,这是所有工作的基础。
开发板与屏幕的接线其实很有规律。以常见的STM32F103C8T6开发板为例,我们需要连接以下引脚:
- GND接电源地
- VCC接3.3V电源
- SCL接PA5(SPI时钟线)
- SDA接PA7(SPI数据线)
- RES接PB0(复位引脚)
- DC接PB1(数据/命令选择)
- CS接PA4(片选引脚)
- BL接PB10(背光控制,可选)
这里有个实用技巧:如果暂时不想控制背光,BL引脚可以直接接3.3V或者悬空。我在早期项目中经常忘记初始化背光引脚,导致屏幕不亮,排查了半天才发现问题所在。
2. 模拟SPI通信的深度解析
2.1 SPI通信的本质
很多初学者会疑惑:为什么不用硬件SPI而选择模拟?其实在资源受限的场景下,模拟SPI更加灵活。SPI通信的核心在于四个信号:
- SCLK - 时钟信号,由主设备产生
- MOSI - 主设备输出,从设备输入
- MISO - 从设备输入,主设备输出
- CS - 片选信号
ST7735S比较特殊,它只需要接收数据而不需要发送,所以我们可以省略MISO线,这简化了我们的接线工作。
2.2 时钟极性与相位
这是SPI最让人困惑的部分,但理解后会发现很简单。CPOL和CPHA两个参数决定了通信时序:
- CPOL=0:时钟空闲时为低电平
- CPOL=1:时钟空闲时为高电平
- CPHA=0:在第一个时钟边沿采样数据
- CPHA=1:在第二个时钟边沿采样数据
对于ST7735S,需要设置为模式0(CPOL=0,CPHA=0)。这意味着:
- 时钟空闲时为低电平
- 数据在上升沿被采样
- 数据在下降沿变化
2.3 模拟SPI的代码实现
直接操作寄存器是最高效的方式。下面是我的常用宏定义:
#define CS_H ((CDS_PORT)->ODR |= (CS_PIN)) #define SCL_H ((CDS_PORT)->ODR |= (SCL_PIN)) #define SDA_H ((CDS_PORT)->ODR |= (SDA_PIN)) #define CS_L ((CDS_PORT)->ODR &= ~(CS_PIN)) #define SCL_L ((CDS_PORT)->ODR &= ~(SCL_PIN)) #define SDA_L ((CDS_PORT)->ODR &= ~(SDA_PIN))发送一个字节的函数实现:
void SPI_SendByte(uint8_t data) { for(uint8_t i=0; i<8; i++) { SCL_L; if(data & 0x80) SDA_H; else SDA_L; SCL_H; data <<= 1; } }3. 寄存器配置实战
3.1 关键寄存器详解
ST7735S有多个重要寄存器,这里重点介绍两个最常用的:
- 0x36 - 显示方向控制寄存器这个寄存器控制屏幕的显示方向、颜色格式等。它的各个位含义如下:
- MY:行地址顺序(1=从上到下)
- MX:列地址顺序(1=从左到右)
- MV:行列交换
- ML:垂直刷新方向
- RGB:颜色格式(0=RGB,1=BGR)
- MH:水平刷新方向
- 0x2A和0x2B - 行列地址设置这两个寄存器用于设置显示区域的范围:
- 0x2A设置列地址(X方向)
- 0x2B设置行地址(Y方向) 使用时需要发送4个参数:起始地址高字节、起始地址低字节、结束地址高字节、结束地址低字节。
3.2 初始化序列
正确的初始化是屏幕正常工作的关键。以下是典型的初始化流程:
void ST7735_Init(void) { // 硬件复位 RST_L; Delay_ms(100); RST_H; Delay_ms(120); // 发送初始化命令序列 ST7735_WriteCmd(0x11); // 退出睡眠模式 Delay_ms(120); ST7735_WriteCmd(0x36); // 设置显示方向 ST7735_WriteData(0xC0); // 我的常用设置 ST7735_WriteCmd(0x3A); // 设置颜色格式 ST7735_WriteData(0x05); // 16位RGB565 // 更多初始化命令... ST7735_WriteCmd(0x29); // 开启显示 }4. 图形显示实战
4.1 基本绘图功能
4.1.1 画点函数
这是所有图形显示的基础:
void ST7735_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { // 检查坐标范围 if(x >= ST7735_WIDTH || y >= ST7735_HEIGHT) return; // 设置显示区域为单个点 ST7735_SetAddressWindow(x, y, x, y); // 发送颜色数据 ST7735_WriteData(color >> 8); ST7735_WriteData(color & 0xFF); }4.1.2 清屏函数
清屏实际上是填充整个屏幕:
void ST7735_FillScreen(uint16_t color) { ST7735_FillRect(0, 0, ST7735_WIDTH, ST7735_HEIGHT, color); }4.1.3 矩形填充
这是更通用的区域填充函数:
void ST7735_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { // 边界检查 if(x >= ST7735_WIDTH || y >= ST7735_HEIGHT) return; if(x + w > ST7735_WIDTH) w = ST7735_WIDTH - x; if(y + h > ST7735_HEIGHT) h = ST7735_HEIGHT - y; // 设置显示区域 ST7735_SetAddressWindow(x, y, x + w - 1, y + h - 1); // 发送颜色数据 for(uint32_t i = 0; i < w * h; i++) { ST7735_WriteData(color >> 8); ST7735_WriteData(color & 0xFF); } }4.2 高级图形功能
4.2.1 画线算法
实现Bresenham画线算法:
void ST7735_DrawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { int dx = abs(x1 - x0); int dy = abs(y1 - y0); int sx = (x0 < x1) ? 1 : -1; int sy = (y0 < y1) ? 1 : -1; int err = dx - dy; while(1) { ST7735_DrawPixel(x0, y0, color); if(x0 == x1 && y0 == y1) break; int e2 = 2 * err; if(e2 > -dy) { err -= dy; x0 += sx; } if(e2 < dx) { err += dx; y0 += sy; } } }4.2.2 圆形绘制
中点圆算法实现:
void ST7735_DrawCircle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color) { int f = 1 - r; int ddF_x = 1; int ddF_y = -2 * r; int x = 0; int y = r; ST7735_DrawPixel(x0, y0 + r, color); ST7735_DrawPixel(x0, y0 - r, color); ST7735_DrawPixel(x0 + r, y0, color); ST7735_DrawPixel(x0 - r, y0, color); while(x < y) { if(f >= 0) { y--; ddF_y += 2; f += ddF_y; } x++; ddF_x += 2; f += ddF_x; ST7735_DrawPixel(x0 + x, y0 + y, color); ST7735_DrawPixel(x0 - x, y0 + y, color); ST7735_DrawPixel(x0 + x, y0 - y, color); ST7735_DrawPixel(x0 - x, y0 - y, color); ST7735_DrawPixel(x0 + y, y0 + x, color); ST7735_DrawPixel(x0 - y, y0 + x, color); ST7735_DrawPixel(x0 + y, y0 - x, color); ST7735_DrawPixel(x0 - y, y0 - x, color); } }5. 文字显示技术
5.1 ASCII字符显示
显示字符需要先获取字模数据。通常我们会使用8x16像素的字模:
void ST7735_DrawChar(uint16_t x, uint16_t y, char c, uint16_t color, uint16_t bg) { // 获取字符在字模数组中的偏移量 uint32_t index = (c - 32) * 16; for(uint8_t i = 0; i < 16; i++) { uint8_t line = font8x16[index + i]; for(uint8_t j = 0; j < 8; j++) { if(line & (0x80 >> j)) { ST7735_DrawPixel(x + j, y + i, color); } else if(bg != color) { ST7735_DrawPixel(x + j, y + i, bg); } } } }5.2 字符串显示
基于字符显示函数,可以轻松实现字符串显示:
void ST7735_DrawString(uint16_t x, uint16_t y, const char *str, uint16_t color, uint16_t bg) { while(*str) { ST7735_DrawChar(x, y, *str++, color, bg); x += 8; // 自动换行处理 if(x >= ST7735_WIDTH - 8) { x = 0; y += 16; } } }5.3 中文显示
中文显示相对复杂,因为每个汉字通常需要16x16像素:
void ST7735_DrawChinese(uint16_t x, uint16_t y, uint16_t index, uint16_t color, uint16_t bg) { for(uint8_t i = 0; i < 16; i++) { uint16_t line = chinese_font[index * 16 + i]; for(uint8_t j = 0; j < 16; j++) { if(line & (0x8000 >> j)) { ST7735_DrawPixel(x + j, y + i, color); } else if(bg != color) { ST7735_DrawPixel(x + j, y + i, bg); } } } }6. 图片显示技术
6.1 图片取模
显示图片前需要将图片转换为数组。常用的取模软件如Image2Lcd可以将图片转换为C语言数组。选择设置时要注意:
- 输出数据类型:C语言数组
- 扫描方式:水平扫描
- 输出灰度:16位真彩色
- 最大宽度和高度:不超过屏幕分辨率
6.2 图片显示实现
有了图片数组后,显示图片就很简单了:
void ST7735_DrawImage(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint16_t *img) { ST7735_SetAddressWindow(x, y, x + w - 1, y + h - 1); for(uint32_t i = 0; i < w * h; i++) { ST7735_WriteData(img[i] >> 8); ST7735_WriteData(img[i] & 0xFF); } }7. 性能优化技巧
7.1 批量写入优化
单像素写入效率很低,我们可以优化为批量写入:
void ST7735_WritePixels(uint16_t *colors, uint32_t len) { for(uint32_t i = 0; i < len; i++) { ST7735_WriteData(colors[i] >> 8); ST7735_WriteData(colors[i] & 0xFF); } }7.2 双缓冲技术
对于动画显示,可以使用双缓冲技术:
- 在内存中创建一个和屏幕大小相同的缓冲区
- 所有绘图操作先在内存缓冲区中进行
- 完成一帧绘制后,一次性将缓冲区内容写入屏幕
这能有效避免屏幕闪烁。
7.3 DMA传输
如果使用硬件SPI,可以结合DMA进一步提高传输效率:
void ST7735_DMA_WritePixels(uint16_t *colors, uint32_t len) { // 配置DMA... // 启动SPI传输... }8. 常见问题排查
8.1 屏幕无显示
- 检查电源和背光
- 确认复位信号正常
- 检查SPI时钟和数据线
- 验证初始化序列是否正确
8.2 显示颜色异常
- 检查颜色格式设置(RGB565)
- 确认0x36寄存器的RGB/BGR位
- 检查伽马校正设置
8.3 显示位置错乱
- 检查0x2A和0x2B寄存器设置
- 确认显示方向寄存器0x36
- 检查坐标范围是否超出屏幕
9. 项目实战建议
在实际项目中,我建议采用分层架构:
- 底层驱动层:处理最基础的SPI通信和寄存器操作
- 中间层:实现基本图形功能(点、线、面等)
- 应用层:实现具体的UI界面和业务逻辑
这种结构便于维护和移植。例如,当更换不同型号的屏幕时,只需修改底层驱动层即可。
10. 进阶功能探索
掌握了基础功能后,可以尝试实现更复杂的功能:
- 触摸屏控制
- 多级菜单系统
- 动画效果
- 自定义字体
- 屏幕截图功能
每个功能的实现都会让你对TFT驱动有更深的理解。我在实际项目中发现,很多看似复杂的功能,拆解后都是基础功能的组合应用。
