4、SPI与显示驱动:SPI控制命令与数据发送
SPI接口在显示驱动芯片里,可以说是最常用的通信方式之一。我刚开始做显示驱动时,用的就是ILI9341这颗经典芯片。说实话,SPI的时序看起来简单,但真正调通、调稳,还是有不少门道的。
4.1 SPI通信的基本概念
SPI全称是Serial Peripheral Interface,串行外设接口。它用四根线完成通信:
- SCLK:时钟线,由主机产生
- MOSI:主机输出,从机输入
- MISO:主机输入,从机输出
- CS:片选信号,低电平有效
显示驱动芯片通常只需要三根线——SCLK、MOSI和CS。MISO一般不用,因为数据是单向流向屏幕的。嗯,这里要注意:有些驱动IC支持读操作,比如读取寄存器状态,这时候MISO就有用了。
4.2 SPI控制命令与数据的区别
显示驱动芯片怎么区分你发的是命令还是数据?答案是通过DCX引脚(也叫D/CX、RS)。
| DCX电平 | 含义 | 示例 |
|---|---|---|
| 低电平(0) | 发送命令 | 0x36(设置扫描方向) |
| 高电平(1) | 发送数据 | 颜色值、参数等 |
我遇到过不少新手,上来就把命令和数据混着发,结果屏幕死活不亮。后来一查,DCX引脚没控制对。说白了,DCX就是告诉芯片:我现在要给你下指令,还是给你喂数据。
4.3 ILI9341的SPI时序要求
ILI9341这颗芯片,我用了不下五年。它的SPI时序有几个关键点:
- 时钟极性(CPOL):通常设为0,即空闲时时钟为低电平
- 时钟相位(CPHA):通常设为0,即第一个时钟沿采样数据
- 最大时钟频率:一般不超过10MHz,实际建议跑6-8MHz比较稳
- CS建立时间:CS拉低后,至少等待5ns再发时钟
- CS保持时间:最后一个时钟结束后,CS至少保持15ns再拉高
核心要点:ILI9341的SPI模式是Mode 0(CPOL=0, CPHA=0)。数据在时钟上升沿被采样,下降沿切换。
4.4 初始化序列编写
显示驱动芯片上电后,必须按照数据手册的初始化序列进行配置。我习惯把初始化序列写成结构体数组,方便管理和调试。
// 初始化序列结构体
typedef struct {
uint8_t cmd; // 命令
uint8_t len; // 参数长度
uint8_t *params; // 参数指针
} lcd_init_cmd_t;
// ILI9341初始化序列示例
static const lcd_init_cmd_t ili9341_init[] = {
// 软件复位
{0x01, 0, NULL},
// 延时120ms
{0xFF, 0, NULL}, // 自定义延时命令
// 电源控制A
{0xCB, 5, (uint8_t[]){0x39, 0x2C, 0x00, 0x34, 0x02}},
// 电源控制B
{0xCF, 3, (uint8_t[]){0x00, 0xC1, 0x30}},
// 驱动时序控制A
{0xE8, 3, (uint8_t[]){0x85, 0x00, 0x78}},
// 驱动时序控制B
{0xEA, 2, (uint8_t[]){0x00, 0x00}},
// 电源控制1
{0xC0, 1, (uint8_t[]){0x23}},
// 电源控制2
{0xC1, 1, (uint8_t[]){0x10}},
// VCOM控制1
{0xC5, 2, (uint8_t[]){0x3E, 0x28}},
// VCOM控制2
{0xC7, 1, (uint8_t[]){0x86}},
// 内存访问控制
{0x36, 1, (uint8_t[]){0x48}},
// 像素格式设置
{0x3A, 1, (uint8_t[]){0x55}}, // 16位色
// 帧率控制
{0xB1, 2, (uint8_t[]){0x00, 0x18}},
// 显示功能控制
{0xB6, 3, (uint8_t[]){0x08, 0x82, 0x27}},
// 伽马设置
{0xF2, 1, (uint8_t[]){0x00}},
// 正伽马校正
{0x26, 1, (uint8_t[]){0x01}},
// 退出睡眠
{0x11, 0, NULL},
// 延时120ms
{0xFF, 0, NULL},
// 显示开启
{0x29, 0, NULL},
};
我的经验:初始化序列里,延时非常关键。ILI9341从睡眠模式唤醒后,需要至少120ms的稳定时间。我曾经因为延时不够,屏幕出现花屏,查了两天才发现是复位后等待时间太短。
4.5 SPI发送函数实现
写SPI发送函数时,我习惯把命令发送和数据发送分开,这样代码更清晰。
// SPI发送命令
void LCD_WriteCmd(uint8_t cmd) {
DCX_LOW(); // DCX拉低,表示命令
CS_LOW(); // 片选使能
SPI_WriteByte(cmd); // 发送命令字节
CS_HIGH(); // 片选禁止
}
// SPI发送数据
void LCD_WriteData(uint8_t data) {
DCX_HIGH(); // DCX拉高,表示数据
CS_LOW(); // 片选使能
SPI_WriteByte(data);// 发送数据字节
CS_HIGH(); // 片选禁止
}
// 批量发送数据(用于填充颜色)
void LCD_WriteDataBuf(uint8_t *buf, uint32_t len) {
DCX_HIGH();
CS_LOW();
for(uint32_t i = 0; i < len; i++) {
SPI_WriteByte(buf[i]);
}
CS_HIGH();
}
避坑指南:我曾经在批量发送数据时,忘记在每次发送后检查SPI忙标志。结果数据发送太快,芯片来不及处理,导致显示错位。建议在SPI_WriteByte函数里加入忙等待,或者用DMA方式发送。
4.6 初始化流程的执行
初始化序列的执行逻辑其实很简单:遍历数组,依次发送命令和参数。我一般这样写:
void LCD_Init(void) {
uint32_t i;
// 硬件复位
RST_LOW();
delay_ms(10);
RST_HIGH();
delay_ms(120);
// 执行初始化序列
for(i = 0; i < sizeof(ili9341_init)/sizeof(ili9341_init[0]); i++) {
LCD_WriteCmd(ili9341_init[i].cmd);
// 自定义延时命令
if(ili9341_init[i].cmd == 0xFF) {
delay_ms(120);
continue;
}
// 发送参数
for(uint8_t j = 0; j < ili9341_init[i].len; j++) {
LCD_WriteData(ili9341_init[i].params[j]);
}
}
// 清屏为黑色
LCD_Clear(0x0000);
}
4.7 常见问题与调试技巧
做显示驱动这么多年,我总结了几条调试经验:
- 屏幕不亮:先检查电源和复位,再用逻辑分析仪看SPI波形
- 花屏:多半是初始化序列不对,或者时序参数没配好
- 颜色不对:检查像素格式设置,ILI9341支持12位、16位、18位色
- 显示偏移:检查内存访问控制寄存器(0x36)的设置
调试利器:逻辑分析仪是调试SPI时序的最佳工具。我每次调新屏,都会先抓一波波形,确认命令和数据发送正确,再去看显示效果。
4.8 本章知识体系
下面这张图,是我对SPI与显示驱动核心逻辑的总结:
这张图把SPI驱动显示芯片的整个流程串起来了。从最底层的SPI协议,到命令数据的区分,再到时序要求、初始化序列、发送函数,最后是调试验证。每一步都环环相扣。
个人建议:刚开始学的时候,别急着写复杂的初始化序列。先点亮背光,再发一个清屏命令,看看屏幕能不能正常显示颜色。一步一步来,出问题了也好定位。
SPI驱动显示芯片,说白了就是控制好DCX引脚,按照时序要求把命令和数据发出去。ILI9341的初始化序列虽然长,但每个寄存器都有明确的用途。我建议你把数据手册打印出来,对照着看,理解每个寄存器的作用,而不是盲目复制代码。
公众号:蓝海资料掘金营,微信deep3321