1. STM32并行驱动CH12864B液晶屏方案概述
第一次在STM32上驱动CH12864B液晶屏时,我被它复杂的时序要求折腾得够呛。这款基于ST7920控制器的128x64点阵液晶屏,虽然文档上写着支持并行8位接口,但实际调试时会遇到各种时序匹配问题。经过三个项目的实战积累,我总结出一套稳定可靠的驱动方案,实测在STM32F103C8T6和STM32F407VET6上都能稳定运行。
CH12864B属于常见的低成本图形液晶模块,内置中文字库,适合显示中文菜单和简单图形。与SPI接口相比,并行驱动虽然多占用IO口,但刷新速度更快,特别是在需要频繁更新显示内容的场景下优势明显。ST7920控制器采用独特的"8080"时序,与常见的6800时序有所不同,这也是很多开发者首次接触时容易踩坑的地方。
关键提示:ST7920控制器对建立时间和保持时间要求严格,直接使用GPIO模拟时序时,必须插入适当的延时,否则会出现显示错位或雪花噪点现象。
2. 硬件连接与引脚定义
2.1 液晶屏引脚功能解析
CH12864B通常采用20Pin排针接口,关键引脚定义如下:
| 引脚编号 | 符号 | 功能说明 | 连接STM32引脚 |
|---|---|---|---|
| 1 | VSS | 电源地 | GND |
| 2 | VDD | 逻辑电源(+5V或+3.3V) | 3.3V |
| 3 | VO | 对比度调节 | 接10K电位器 |
| 4 | RS(CS) | 数据/指令选择 | PC0 |
| 5 | R/W(SID) | 读写选择 | PC1 |
| 6 | E(SCLK) | 使能信号 | PC2 |
| 7-14 | DB0-DB7 | 数据总线 | PB0-PB7 |
| 15 | PSB | 并行/串行模式选择 | 接VDD(并行) |
| 16 | NC | 空脚 | - |
| 17 | /RESET | 复位信号(低有效) | 接VDD或MCU控制 |
| 18 | VOUT | LCD驱动电压输出 | 不接 |
| 19 | BLA | 背光阳极(+5V) | 通过限流电阻 |
| 20 | BLK | 背光阴极 | GND |
实际项目中,我将控制信号(RS,R/W,E)连接到GPIOC端口,数据总线使用GPIOB的8个引脚,这样编程时可以直接用寄存器操作提高速度。特别注意PSB引脚必须接高电平选择并行模式,否则控制器会默认进入串行模式。
2.2 硬件设计注意事项
- 电源滤波:在VDD引脚附近放置0.1μF去耦电容,能有效减少电源噪声导致的显示异常。
- 对比度调节:VO引脚建议接10K电位器中间抽头,两端分别接VDD和GND,方便调整显示对比度。
- 背光限流:BLA引脚需串联适当电阻(通常100Ω-1KΩ),避免电流过大烧毁LED背光。
- 复位处理:/RESET引脚可接MCU控制,上电时拉低至少10ms完成硬复位,也可直接接VDD省去复位电路。
3. 底层驱动实现
3.1 GPIO初始化配置
首先配置STM32的GPIO端口,以下是基于HAL库的初始化代码:
c复制void LCD_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOB和GPIOC时钟
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
// 配置数据端口PB0-PB7为输出
GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|
GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 配置控制信号PC0(RS), PC1(RW), PC2(E)
GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// 初始状态设置
LCD_E_LOW(); // 使能信号低电平
LCD_RW_WRITE(); // 设置为写模式
}
3.2 基本时序函数实现
ST7920控制器的并行接口时序要求如下:
- 建立时间(tAS):RS/RW信号变化后,至少需要140ns才能触发E信号
- 脉冲宽度(tPW):E信号高电平至少需要230ns
- 保持时间(tAH):E信号下降后,RS/RW信号至少需要10ns保持不变
基于STM32F103的72MHz主频,一个NOP指令约13.8ns,我们实现如下延时函数:
c复制#define LCD_Delay() __asm__ volatile("nop; nop; nop; nop; nop; nop;")
void LCD_WriteByte(uint8_t data, uint8_t cmd)
{
LCD_RW_WRITE(); // 设置为写模式
if(cmd)
LCD_RS_CMD(); // 命令模式
else
LCD_RS_DATA(); // 数据模式
// 设置数据总线
GPIOB->ODR = (GPIOB->ODR & 0xFF00) | data;
LCD_Delay(); // 建立时间延时(约83ns)
LCD_E_HIGH(); // 使能信号高电平
LCD_Delay(); // 脉冲宽度延时(约83ns)
LCD_Delay(); // 再加6个NOP确保满足230ns
LCD_E_LOW(); // 使能信号下降沿
LCD_Delay(); // 保持时间延时(约55ns)
}
3.3 控制器初始化序列
ST7920需要特定的初始化序列才能进入图形显示模式:
c复制void LCD_Init(void)
{
HAL_Delay(50); // 上电延时等待稳定
// 功能设置: 8位接口, 基本指令集
LCD_WriteByte(0x30, LCD_CMD);
HAL_Delay(1);
// 功能设置: 8位接口, 基本指令集
LCD_WriteByte(0x30, LCD_CMD);
HAL_Delay(1);
// 显示开关控制: 开显示, 关光标, 不闪烁
LCD_WriteByte(0x0C, LCD_CMD);
HAL_Delay(1);
// 清除显示
LCD_WriteByte(0x01, LCD_CMD);
HAL_Delay(2); // 清屏需要较长时间
// 进入点设置: 地址递增, 不移位
LCD_WriteByte(0x06, LCD_CMD);
HAL_Delay(1);
// 扩展指令集设置
LCD_WriteByte(0x34, LCD_CMD);
HAL_Delay(1);
// 进入图形显示模式
LCD_WriteByte(0x36, LCD_CMD);
HAL_Delay(1);
}
4. 图形显示实现
4.1 显存结构与寻址方式
ST7920的图形显示区分为上下两半屏,各对应32×16字节的显存:
- 上半屏:地址80H~87H, 90H~97H, A0H~A7H, B0H~B7H
- 下半屏:地址88H~8FH, 98H~9FH, A8H~AFH, B8H~BFH
每个地址对应16个水平像素点(2字节),垂直方向32行。写入数据时需要注意:
- 先设置垂直地址(Y坐标),再设置水平地址(X坐标)
- 每个地址写入2字节数据,对应16个水平像素
- 地址自动递增方向由Entry Mode指令设置
4.2 基本绘图函数实现
c复制// 设置图形显示位置
void LCD_SetPosition(uint8_t x, uint8_t y)
{
uint8_t address;
if(y < 32) // 上半屏
address = 0x80 + (y % 16) + (y/16)*0x10 + (x/16);
else // 下半屏
address = 0x88 + (y % 16) + ((y-32)/16)*0x10 + (x/16);
LCD_WriteByte(address, LCD_CMD);
}
// 写入图形数据(16像素)
void LCD_WriteData(uint16_t data)
{
LCD_WriteByte(data >> 8, LCD_DATA); // 高字节
LCD_WriteByte(data & 0xFF, LCD_DATA); // 低字节
}
// 清屏函数
void LCD_Clear(void)
{
uint8_t x,y;
for(y=0; y<64; y++)
{
LCD_SetPosition(0, y);
for(x=0; x<8; x++) // 128像素/16=8列
{
LCD_WriteData(0x0000); // 写入全0
}
}
}
4.3 高级图形功能实现
基于上述基本函数,可以实现更复杂的图形操作:
c复制// 绘制单个像素
void LCD_DrawPixel(uint8_t x, uint8_t y, uint8_t color)
{
uint16_t mask = 0x8000 >> (x % 16);
uint16_t data;
LCD_SetPosition(x, y);
// 读取当前显示数据(需要临时切换为读模式)
LCD_RW_READ();
LCD_RS_CMD();
data = GPIOB->IDR & 0x00FF; // 读取高字节
data <<= 8;
LCD_E_HIGH();
LCD_E_LOW();
data |= (GPIOB->IDR & 0x00FF); // 读取低字节
// 修改像素值
if(color)
data |= mask;
else
data &= ~mask;
// 写回修改后的数据
LCD_RW_WRITE();
LCD_WriteData(data);
}
// 绘制水平线
void LCD_DrawHLine(uint8_t x0, uint8_t x1, uint8_t y, uint8_t color)
{
uint8_t x;
for(x=x0; x<=x1; x++)
{
LCD_DrawPixel(x, y, color);
}
}
// 绘制垂直线
void LCD_DrawVLine(uint8_t x, uint8_t y0, uint8_t y1, uint8_t color)
{
uint8_t y;
for(y=y0; y<=y1; y++)
{
LCD_DrawPixel(x, y, color);
}
}
5. 性能优化技巧
5.1 批量写入优化
单像素操作虽然灵活但效率低下,实际应用中应尽量采用批量写入:
c复制// 快速填充矩形区域
void LCD_FillRect(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t color)
{
uint8_t x,y;
uint16_t pattern = color ? 0xFFFF : 0x0000;
for(y=y0; y<=y1; y++)
{
LCD_SetPosition(x0, y);
for(x=x0/16; x<=(x1/16); x++)
{
LCD_WriteData(pattern);
}
}
}
5.2 直接操作寄存器提速
HAL库的函数调用开销较大,直接操作寄存器可显著提高速度:
c复制void LCD_WriteByte_Fast(uint8_t data, uint8_t cmd)
{
// 设置RS和RW状态
if(cmd)
GPIOC->BSRR = GPIO_BSRR_BR0; // RS=0
else
GPIOC->BSRR = GPIO_BSRR_BS0; // RS=1
GPIOC->BSRR = GPIO_BSRR_BR1; // RW=0(写)
// 设置数据总线
GPIOB->ODR = (GPIOB->ODR & 0xFF00) | data;
// 产生E脉冲
GPIOC->BSRR = GPIO_BSRR_BS2; // E=1
__asm__ volatile("nop; nop; nop; nop; nop; nop;");
GPIOC->BSRR = GPIO_BSRR_BR2; // E=0
}
5.3 双缓冲技术
对于动画显示,可以采用双缓冲技术避免闪烁:
- 在RAM中开辟两块显存缓冲区
- 所有绘图操作在后台缓冲区进行
- 完成一帧后,一次性将后台缓冲区内容刷入液晶屏
c复制uint16_t lcd_buffer[2][32][8]; // 双缓冲显存
void LCD_Update(uint8_t buf_idx)
{
uint8_t x,y;
for(y=0; y<32; y++) // 上半屏
{
LCD_WriteByte(0x80 | (y & 0x0F) | ((y & 0x10)<<1), LCD_CMD);
for(x=0; x<8; x++)
{
LCD_WriteByte(lcd_buffer[buf_idx][y][x] >> 8, LCD_DATA);
LCD_WriteByte(lcd_buffer[buf_idx][y][x] & 0xFF, LCD_DATA);
}
}
for(y=32; y<64; y++) // 下半屏
{
LCD_WriteByte(0x80 | (y & 0x0F) | (((y-32) & 0x10)<<1), LCD_CMD);
for(x=0; x<8; x++)
{
LCD_WriteByte(lcd_buffer[buf_idx][y][x] >> 8, LCD_DATA);
LCD_WriteByte(lcd_buffer[buf_idx][y][x] & 0xFF, LCD_DATA);
}
}
}
6. 常见问题与解决方案
6.1 显示内容错位
现象:文字或图形显示位置不正确,出现偏移
可能原因:
- 地址设置错误,未正确区分上下半屏
- 水平地址计算错误,每个地址对应16像素
- 未正确清除地址计数器
解决方案: - 检查SetPosition函数中的地址计算逻辑
- 在每次重新设置位置前,先发送地址重置命令(0x02)
- 确认Entry Mode设置是否正确(0x06表示地址递增)
6.2 显示雪花噪点
现象:屏幕上随机出现闪烁的像素点
可能原因:
- 时序不符合要求,特别是E信号脉冲宽度不足
- 电源噪声干扰
- 未正确初始化控制器
解决方案: - 增加时序延时,确保满足ST7920的最小时序要求
- 在电源引脚添加0.1μF去耦电容
- 严格按照初始化序列操作,确保进入图形模式(0x36)
6.3 显示对比度不佳
现象:显示内容过淡或过深,调节电位器效果不明显
可能原因:
- VO引脚电压不合适(最佳范围通常为0-2V)
- 背光亮度影响视觉对比度
- 电源电压不稳定
解决方案: - 检查电位器连接,确保VO引脚电压可调
- 调整背光限流电阻,避免过亮影响显示效果
- 确保供电电压稳定在5V±10%或3.3V±5%
6.4 读写操作冲突
现象:读取数据时返回错误值
可能原因:
- 未正确设置RW引脚状态
- 读取时序不符合要求
- 数据端口方向未切换
解决方案: - 读取操作前确保RW=1,RS=0(状态读取)或RS=1(数据读取)
- 按照手册要求,E信号高电平期间读取数据
- 临时将数据端口配置为输入模式:
c复制uint8_t LCD_ReadStatus(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
uint8_t status;
// 临时配置数据端口为输入
GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|
GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 设置读模式
LCD_RW_READ();
LCD_RS_CMD();
// 产生E脉冲读取状态
LCD_E_HIGH();
status = GPIOB->IDR & 0x00FF;
LCD_E_LOW();
// 恢复数据端口为输出
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
return status;
}
