4、SPI与并行接口详解:SPI协议时序、4线/3线SPI驱动LCD、8080并行接口驱动、DMA加速传输
好,咱们今天来聊聊LCD驱动里最核心的两个接口——SPI和8080并行接口。说实话,这两个接口我几乎每天都在打交道。你想想看,不管是小尺寸的OLED屏,还是大尺寸的TFT彩屏,总得有个方式让单片机把数据送过去。SPI省引脚,并行接口快,各有各的脾气。
我个人习惯是:128x64以下的单色屏,用SPI就够了;320x240以上的彩色屏,必须上8080并行接口,否则刷新率会让你崩溃。嗯,这里要注意,选错了接口方案,后面调试起来真的很痛苦。
4.1 SPI协议时序——看似简单,坑不少
SPI,全称Serial Peripheral Interface,说白了就是四根线搞定通信。哪四根?SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。驱动LCD时,我们一般只用三根——因为LCD只需要接收数据,不需要回传,所以MISO可以省掉。
时序上,SPI有四种模式,由CPOL(时钟极性)和CPHA(时钟相位)决定。我刚开始学的时候,总觉得这俩参数很抽象。后来我总结了一个笨办法:
- CPOL=0:空闲时时钟是低电平
- CPOL=1:空闲时时钟是高电平
- CPHA=0:在时钟的第一个边沿采样数据
- CPHA=1:在时钟的第二个边沿采样数据
驱动LCD时,绝大多数芯片用的是模式0(CPOL=0, CPHA=0)或模式3(CPOL=1, CPHA=1)。为什么?因为这两种模式都是在时钟的上升沿采样,下降沿变化,时序上最干净。
避坑指南:我曾经在一个项目里,ST7735S的LCD死活不显示。查了两天,最后发现是SPI模式配错了。芯片手册上写的是“Data is latched on the rising edge of SCLK”,我配成了模式1(上升沿变化,下降沿采样)。嗯,从此以后,我拿到任何LCD芯片,第一件事就是翻时序图,确认采样边沿。
4.2 4线SPI与3线SPI驱动LCD
先说说4线SPI。这里的“4线”指的是:CS、SCLK、MOSI、DC(数据/命令选择线)。DC线是LCD驱动特有的,用来区分发送的是命令还是数据。DC=0时发命令,DC=1时发数据。
代码示例(STM32 HAL库):
// 4线SPI写命令
void LCD_WriteCmd(uint8_t cmd) {
LCD_DC_LOW(); // DC拉低,表示命令
LCD_CS_LOW(); // 片选使能
HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
LCD_CS_HIGH(); // 片选释放
}
// 4线SPI写数据
void LCD_WriteData(uint8_t data) {
LCD_DC_HIGH(); // DC拉高,表示数据
LCD_CS_LOW();
HAL_SPI_Transmit(&hspi1, &data, 1, 100);
LCD_CS_HIGH();
}
那3线SPI呢?说白了就是把DC线省掉,通过协议来区分命令和数据。怎么区分?在发送每个字节之前,先发一个额外的bit:0表示命令,1表示数据。这样,原本8bit的数据变成了9bit传输。
我个人不太喜欢3线SPI,为什么?因为很多单片机的SPI硬件不支持9bit模式,你得用软件模拟,速度一下就掉下来了。我建议:只要引脚够用,就用4线SPI,别给自己找麻烦。
小技巧:如果你非要用3线SPI,可以用SPI的8bit模式配合一个额外的GPIO来模拟第9位。具体做法是:先发一个字节(高7位为0,最低位表示命令/数据),再发真正的数据字节。虽然效率低一点,但至少不用改硬件。
4.3 8080并行接口驱动——速度的王者
8080接口,也叫Intel Bus,是驱动彩色LCD最常用的并行接口。它需要多少根线?最少16根数据线(8位或16位),加上WR(写使能)、RD(读使能)、CS(片选)、DC(命令/数据选择)、RESET(复位)。
你想想看,SPI一次只能传1bit,8080一次能传8bit或16bit。同样是16MHz的时钟,SPI的吞吐量是2MB/s,而8080接口可以达到32MB/s以上。这就是为什么大尺寸彩屏必须用8080接口。
时序上,8080接口其实很简单:
- 拉低CS,选中芯片
- 设置DC电平(命令或数据)
- 把数据放到数据线上
- 拉低WR,产生一个写脉冲
- 拉高WR,数据被锁存
代码示例(GPIO模拟8080时序):
// 8080接口写数据(16位)
void LCD_WriteData16(uint16_t data) {
LCD_CS_LOW();
LCD_DC_HIGH(); // 数据模式
// 数据线赋值
GPIO->ODR = (GPIO->ODR & 0xFFFF0000) | data;
// 产生写脉冲
LCD_WR_LOW();
// 这里需要几个nop延时,根据LCD芯片速度调整
__NOP(); __NOP();
LCD_WR_HIGH();
LCD_CS_HIGH();
}
注意:8080接口的时序对延时很敏感。我曾经遇到过一个问题:WR低电平保持时间不够,导致数据写不进去。后来用示波器一量,发现低电平只有10ns,而芯片要求至少20ns。加两个NOP指令就解决了。所以,如果你发现LCD显示花屏或颜色不对,先查WR脉冲宽度。
4.4 DMA加速传输——让CPU喘口气
好了,前面讲的都是怎么把数据发出去。但你想过没有,如果每次发数据都要CPU亲自操作,那CPU啥也别干了。尤其是刷全屏的时候,320x240的屏幕,16位色,一帧就是153600字节。用SPI发,就算2MB/s的速度,也要70多ms。这期间CPU一直在等SPI传输完成。
DMA(Direct Memory Access)就是来解决这个问题的。它可以让数据在内存和外设之间直接传输,不需要CPU干预。你只需要告诉DMA:数据从哪里来,要发到哪里去,发多少字节。然后DMA自己搞定,发完了给你一个中断。
代码示例(STM32 SPI + DMA发送):
// 使用DMA发送一帧数据
void LCD_DrawFrame(uint16_t *frameBuffer) {
// 先设置窗口,准备刷屏
LCD_SetWindow(0, 0, 239, 319);
// 启动DMA传输
// 源地址:frameBuffer,目标地址:SPI数据寄存器
// 传输长度:320*240 = 76800个半字(16位)
HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)frameBuffer, 320*240*2);
// CPU可以去做其他事情了
// DMA传输完成后,会调用回调函数
}
// DMA传输完成回调
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
if(hspi->Instance == SPI1) {
// 刷屏完成,可以更新下一帧或做其他处理
frameReady = 1;
}
}
用了DMA之后,刷一帧的时间从70ms降到了不到20ms(假设SPI时钟18MHz)。而且这20ms里,CPU几乎不参与,可以处理触摸、传感器、通信等任务。
我的经验:DMA虽然好,但有个坑——缓存一致性问题。有些单片机(比如STM32F4)有Cache,DMA直接访问内存,而CPU可能还在Cache里操作数据。结果就是DMA发出去的是旧数据。解决办法很简单:在启动DMA之前,调用SCB_CleanDCache()刷新Cache。我因为这个bug调了整整一个下午,最后发现是Cache没刷。
4.5 接口选型建议
最后,我根据实际项目经验,给一个接口选型的参考表:
| 屏幕类型 | 推荐接口 | 引脚数 | 最大刷新率 | 适用场景 |
|---|---|---|---|---|
| 128x64 单色OLED | 3线/4线SPI | 4-5 | 30fps | 手表、小仪表盘 |
| 160x128 彩屏 | 4线SPI | 5 | 25fps | MP3播放器、遥控器 |
| 320x240 彩屏 | 8080 8位 | 12 | 40fps | 智能家居面板 |
| 480x320 彩屏 | 8080 16位 | 20 | 55fps | 手持设备、工控屏 |
| 800x480 以上 | RGB/MIPI | 24+ | 60fps | HMI、车载 |
嗯,总结一下:SPI省引脚,适合小屏;8080接口速度快,适合中大屏;DMA是必须的,否则CPU会被刷屏任务拖死。下一章我们会讲LVGL的移植和配置,到时候这些接口知识都会用上。