1. STM32驱动ST7789V显示屏项目概述
在嵌入式开发中,显示模块的人机交互功能至关重要。最近我在一个项目中使用了STM32F103C8T6单片机驱动2.4寸ST7789V驱动的TFT显示屏,实现了多种尺寸汉字和字符的显示功能。这个方案特别适合需要中文显示的嵌入式设备,如工业控制面板、智能家居终端等场景。
ST7789V是一款主流的TFT驱动芯片,支持262K色显示,分辨率可达240x320。与STM32的结合使用,既保证了性能又控制了成本。项目中我采用了硬件SPI和模拟SPI两种通信方式,通过实际测试发现,硬件SPI在刷新率上能达到模拟SPI的3倍左右,但在引脚资源紧张时,模拟SPI方案也不失为一种选择。
2. 硬件设计与连接方案
2.1 核心硬件选型
主控芯片选用STM32F103C8T6,这款Cortex-M3内核的MCU具有72MHz主频,内置64KB Flash和20KB RAM,完全满足显示驱动的需求。显示屏选用2.4寸IPS屏,驱动芯片为ST7789V,分辨率为240x320,支持16位RGB565色彩格式。
在实际采购时需要注意,市面上有些兼容ST7789V的屏幕初始化序列可能略有不同。我使用的这款屏幕需要发送特定的初始化命令序列才能正常工作,后文会详细介绍。
2.2 引脚连接方案
硬件连接采用SPI接口,具体引脚分配如下:
| STM32引脚 | 显示屏引脚 | 功能说明 |
|---|---|---|
| PB13 | SCL | SPI时钟线 |
| PB14 | SDA | SPI数据线(MOSI) |
| PB15 | RES | 复位信号 |
| PB12 | DC | 数据/命令选择 |
| PB9 | CS | 片选信号 |
| 3.3V | VCC | 电源正极 |
| GND | GND | 电源地 |
提示:如果使用硬件SPI,SCK和MOSI必须连接到STM32的SPI硬件引脚。其他信号线可以灵活分配。
2.3 电源设计考虑
显示屏的背光通常需要较大电流,建议:
- 单独为背光LED供电,不要直接从STM32的GPIO取电
- 添加PWM控制电路实现亮度调节
- 在电源输入端添加100μF电解电容和0.1μF陶瓷电容滤波
3. 软件架构与核心代码解析
3.1 系统软件架构
整个显示驱动分为三个层次:
- 硬件抽象层:实现GPIO和SPI的底层操作
- 驱动层:ST7789V的初始化、基本绘图功能
- 应用层:字体显示、UI绘制等高级功能
c复制// 硬件抽象层示例:GPIO初始化
void LCD_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_12|GPIO_Pin_8|GPIO_Pin_7|GPIO_Pin_6|GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
3.2 SPI通信实现
项目支持硬件SPI和模拟SPI两种方式,通过宏定义切换:
c复制#define USE_HARDWARE_SPI 1 // 1使用硬件SPI,0使用模拟SPI
// 硬件SPI写数据函数
u8 SPI_WriteByte(SPI_TypeDef* SPIx, u8 Byte)
{
while((SPIx->SR & SPI_I2S_FLAG_TXE) == RESET);
SPIx->DR = Byte;
while((SPIx->SR & SPI_I2S_FLAG_RXNE) == RESET);
return SPIx->DR;
}
// 模拟SPI写数据函数
void SPIv_WriteData(u8 Data)
{
unsigned char i=0;
for(i=8; i>0; i--) {
if(Data & 0x80) LCD_SDA_SET;
else LCD_SDA_CLR;
LCD_SCL_CLR;
LCD_SCL_SET;
Data <<= 1;
}
}
3.3 ST7789V初始化序列
ST7789V需要正确的初始化序列才能正常工作。以下是关键初始化代码:
c复制void Lcd_Init(void)
{
Lcd_Reset(); // 硬件复位
// 发送初始化命令序列
Lcd_WriteIndex(0x11); // Sleep out
delay_ms(120);
Lcd_WriteIndex(0x3A); // 颜色格式设置
Lcd_WriteData(0x55); // 16位/pixel (RGB565)
Lcd_WriteIndex(0x36); // 屏幕方向设置
#if USE_HORIZONTAL
Lcd_WriteData(0xE8); // 横屏参数
#else
Lcd_WriteData(0x48); // 竖屏参数
#endif
// 更多初始化命令...
Lcd_WriteIndex(0x29); // 开启显示
}
4. 字模生成与显示实现
4.1 PCtoLCD2002工具配置
使用PCtoLCD2002完美版生成字模数据,关键配置参数:
- 取模方式:逐行式
- 取模走向:顺向
- 输出格式:C51格式
- 字体大小:32x32像素
- 字符模式:选择"字符模式"而非"图形模式"
工具设置截图:

4.2 字模数据结构解析
生成的字符和汉字数据采用不同的结构体存储:
c复制// ASCII字符数据(32x32)
const unsigned char asc32[]={
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x80,0x03,0x80,
// 更多数据...
};
// 汉字数据结构
struct typFNT_GB322 {
unsigned char Index[2]; // 汉字内码
char Msk[128]; // 点阵数据(32x32=128字节)
};
// 汉字数据示例
const struct typFNT_GB322 hz32[] = {
{"学",0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x01,0x00,0x01,0x02,0x01,0x80,
// 更多数据...
}
};
4.3 字符显示算法实现
字符显示的核心是通过逐像素判断点阵数据来绘制:
c复制void Gui_DrawFont_GBK32(u16 x, u16 y, u16 fc, u16 bc, u8 *s)
{
while(*s) {
if(*s < 0x80) { // ASCII字符
k = *s;
if(k > 65) k -= 65; else k = 0;
for(i=0; i<32; i++) { // 32行
// 每行16像素,对应2字节数据
for(j=0; j<8; j++) { // 第一个字节
if(asc32[k*64+i*2] & (0x80>>j))
Gui_DrawPoint(x+j, y+i, fc);
else if(fc != bc)
Gui_DrawPoint(x+j, y+i, bc);
}
for(j=0; j<8; j++) { // 第二个字节
if(asc32[k*64+i*2+1] & (0x80>>j))
Gui_DrawPoint(x+j+8, y+i, fc);
else if(fc != bc)
Gui_DrawPoint(x+j+8, y+i, bc);
}
}
s++; x += 16;
} else { // 中文字符
// 类似逻辑处理32x32汉字
s += 2; x += 32;
}
}
}
5. 性能优化与问题排查
5.1 显示刷新率优化
通过以下方法可显著提高刷新率:
- 使用硬件SPI而非模拟SPI
- 将SPI时钟分频设置为2分频(36MHz)
- 采用DMA传输显示数据
- 实现局部刷新而非全屏刷新
实测数据对比:
| 优化方式 | 全屏刷新时间(ms) | 提升比例 |
|---|---|---|
| 模拟SPI | 450 | - |
| 硬件SPI | 150 | 300% |
| 硬件SPI+DMA | 90 | 500% |
5.2 常见问题与解决方案
问题1:显示屏初始化后无显示
- 检查复位时序,确保复位脉冲宽度>100ms
- 验证初始化命令序列是否正确
- 测量背光电压是否正常
问题2:显示颜色异常
- 确认颜色格式设置(RGB565)
- 检查SPI数据线是否有干扰
- 验证Gamma校正参数
问题3:字符显示错位
- 确认取模方向与显示代码匹配
- 检查字符和汉字处理逻辑是否正确区分
- 验证坐标计算是否考虑到了像素大小
5.3 实际应用中的经验技巧
-
字库优化:将常用字库存放在内部Flash,非常用字库放在外部SPI Flash,节省内存空间。
-
双缓冲技术:在内存中建立显示缓冲区,完成所有绘制操作后一次性刷新到屏幕,避免闪烁。
-
字体混合显示:通过调整前景色和背景色实现反色、半透明等特效:
c复制// 反色显示示例
Gui_DrawFont_GBK32(x, y, BLACK, WHITE, "反色效果");
- 动态刷新优化:对于变化部分局部刷新,静态部分保持不刷新:
c复制// 只刷新变化的数字区域
Lcd_SetRegion(num_x, num_y, num_x+32, num_y+32);
// ...刷新数字...
这个项目让我深刻体会到,嵌入式显示驱动开发不仅需要理解硬件特性,还需要在资源有限的条件下做出合理的软件设计取舍。特别是在中文字库处理上,如何平衡存储空间和显示效果是需要重点考虑的问题。