在嵌入式开发领域,LCD显示是最基础也最考验功底的技能之一。我至今记得第一次用STM32成功点亮LCD时那种兴奋感——看似简单的字符显示,背后涉及GPIO配置、通信协议、字库处理、刷新优化等一整套技术链。本文将基于STM32F103C8T6开发板和1.44寸SPI接口LCD,拆解显示文字的全流程实现,重点分享那些手册上不会写的实战经验。
选择这个案例有三个原因:首先,文字显示是图形界面的基础,99%的嵌入式项目都需要;其次,SPI接口LCD成本低廉且接线简单,适合初学者;最重要的是,通过这个案例可以掌握嵌入式开发中最核心的"硬件驱动+软件抽象"设计思想。无论后续做更复杂的GUI还是物联网终端,这套方法论都适用。
市面常见的1.44寸LCD通常使用ST7735或ST7789驱动芯片,我选用的是ST7735S驱动的IPS屏,分辨率128x128,支持65K色。相比老款TN屏,IPS的可视角度和色彩表现更好,价格也只贵2-3元。硬件连接需要注意三个关键点:
具体接线示例(以STM32F103C8T6为例):
code复制LCD_RST -> PA1
LCD_DC -> PA2
LCD_CS -> PA3
LCD_SCK -> PA5
LCD_MOSI-> PA7
LCD_BLK -> PA8 (通过2N3906三极管控制)
ST7735支持SPI模式0和模式3,建议使用模式3(CPOL=1, CPHA=1),这是大多数LCD驱动芯片的默认模式。在CubeMX中配置时容易忽略两个参数:
c复制hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; // CPOL=1
hspi1.Init.CLKPase = SPI_PHASE_2EDGE; // CPHA=1
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 18MHz @72MHz主频
关键经验:SPI时钟并非越快越好。超过20MHz可能导致屏幕出现雪花噪点,建议先用8分频测试,稳定后再逐步提高速率。
良好的驱动抽象应该隐藏硬件细节,我习惯按功能分层封装:
c复制// 硬件层
void LCD_WriteCmd(uint8_t cmd);
void LCD_WriteData(uint8_t data);
// 中间层
void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color);
// 应用层
void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t color);
其中LCD_WriteCmd的实现有讲究:ST7735的DC引脚控制命令/数据切换,必须严格遵循时序:
c复制void LCD_WriteCmd(uint8_t cmd) {
HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_RESET); // 命令模式
HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET); // 片选使能
HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_Pin, GPIO_PIN_SET); // 片选关闭
}
显示文字的核心在于字模提取,常见方案有:
内置点阵字库:将ASCII字符的8x16点阵数据直接写在代码中
外挂字库芯片:如GT30L32S4W(包含GB2312字库)
SD卡字库:从存储卡读取字模文件
对于初学者,建议从内置ASCII字库开始。这里分享一个经过优化的8x16英文字模数组(节选):
c复制const uint8_t Font8x16[] = {
// 字符'0'
0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,
0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00,
// 字符'A'
0x00,0x00,0x80,0x60,0x18,0x60,0x80,0x00,
0x04,0x3C,0x47,0x44,0x44,0x47,0x3C,0x04
};
显示单个字符的函数需要处理以下关键逻辑:
优化后的实现代码:
c复制void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t color) {
uint8_t i,j;
uint8_t mask;
uint16_t pixel;
if(x > LCD_WIDTH-8 || y > LCD_HEIGHT-16) return; // 边界检查
uint32_t offset = (ch - ' ') * 16; // 计算字模偏移
for(i=0; i<16; i++) { // 16行扫描
pixel = Font8x16[offset + i];
for(j=0; j<8; j++) { // 8列扫描
mask = 1 << (7-j);
if(pixel & mask) {
LCD_DrawPixel(x+j, y+i, color);
}
}
}
}
性能优化技巧:将连续字符的显示合并为一次窗口设置,可提升3-5倍刷新速度。例如显示字符串时先计算整体区域,调用一次
LCD_SetWindow,再批量发送像素数据。
显示中文需要GB2312/Unicode字库支持。推荐两种实用方案:
方案1:使用外部Flash存储字库
c复制// 汉字字模读取示例
void GetChineseFont(uint8_t *buffer, uint16_t gb_code) {
uint32_t addr = GetGBKOffset(gb_code); // 计算物理地址
W25Qxx_Read(buffer, addr, 32); // 每个汉字占32字节
}
方案2:使用矢量字库引擎
实测数据:3500汉字点阵字库(16x16)约占用112KB,建议优先使用外部存储方案。
直接刷屏会导致肉眼可见的闪烁,双缓冲是专业解决方案:
c复制uint16_t lcd_buffer1[128][128];
uint16_t lcd_buffer2[128][128];
uint16_t *active_buf = lcd_buffer1;
uint16_t *back_buf = lcd_buffer2;
void LCD_Refresh() {
DMA2D->CR = 0; // 复位DMA2D
DMA2D->FGMAR = (uint32_t)back_buf;
DMA2D->OMAR = (uint32_t)&LCD->RAM;
DMA2D->NLR = (128 << 16) | 128; // 行列数
DMA2D->CR = DMA2D_CR_START | (0x02 << 16); // 启动传输
// 交换缓冲区
uint16_t *temp = active_buf;
active_buf = back_buf;
back_buf = temp;
}
全屏刷新耗时长(128x128约32ms),局部刷新可降至1-2ms:
c复制void LCD_UpdateArea(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
LCD_SetWindow(x1, y1, x2, y2);
for(int y=y1; y<=y2; y++) {
for(int x=x1; x<=x2; x++) {
LCD_WriteData(active_buf[y][x] >> 8);
LCD_WriteData(active_buf[y][x] & 0xFF);
}
}
}
基础点阵文字边缘锯齿明显,可通过4级灰度改善:
c复制const uint16_t alpha_blend[16][65536]; // 预计算混合结果
void DrawAntiAliasPixel(uint16_t x, uint16_t y, uint16_t fg, uint8_t alpha) {
uint16_t bg = active_buf[y][x];
active_buf[y][x] = alpha_blend[alpha][(fg<<16)|bg];
}
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 白屏 | 背光未开启 | 检查背光电路电压 |
| 花屏 | SPI相位错误 | 调整CPOL/CPHA配置 |
| 显示偏移 | 初始化参数错误 | 重设X/Y偏移寄存器 |
| 字符残缺 | 字模数据错误 | 校验字模提取工具 |
当SPI通信异常时,建议按以下步骤抓包分析:
LCD是耗电大户,通过以下措施可降低50%以上功耗:
c复制void SetBacklight(uint8_t brightness) {
TIM3->CCR1 = brightness; // 使用TIM3 CH1输出PWM
}
掌握了基础文字显示后,可以尝试以下扩展:
一个实用的技巧是使用LVGL等开源图形库快速构建界面。以LVGL为例,只需实现底层的disp_flush回调,即可获得完整的GUI功能:
c复制void disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) {
LCD_SetWindow(area->x1, area->y1, area->x2, area->y2);
HAL_SPI_Transmit(&hspi1, (uint8_t*)color_p,
(area->x2-area->x1+1)*(area->y2-area->y1+1)*2, 100);
lv_disp_flush_ready(drv); // 通知LVGL刷新完成
}
经过三个版本迭代,我们团队的LCD驱动最终实现了:16级灰度抗锯齿、30fps刷新率、中英文混排、低至2mA的休眠功耗。这些优化使得产品在电池供电场景下也能获得良好的显示效果。