4、SPI接口驱动适配:从协议到波形
SPI 接口,说白了就是嵌入式 GUI 里最常用的「四线制」通信方式。我这些年经手的 LCD 驱动,十有八九都是 SPI 接口。为什么?因为它简单、可靠,而且大多数 MCU 都自带硬件 SPI 外设。
但简单归简单,真要把 SPI LCD 驱动写好、跑顺、还不占 CPU,里面还是有不少门道的。这一章,我就带你从协议回顾开始,一步步把驱动写出来,再上 DMA 优化,最后用示波器看看真实波形——嗯,这才是工程师该有的调试方式。
4.1 SPI 协议快速回顾
SPI 全称 Serial Peripheral Interface,摩托罗拉发明的。四根线:
- SCLK — 时钟,由主机产生
- MOSI — 主机输出,从机输入
- MISO — 主机输入,从机输出
- CS — 片选,低电平有效
对于 LCD 驱动,我们通常只需要 SCLK、MOSI、CS 三根线就够了。为什么?因为 LCD 基本不回传数据,MISO 可以省掉。我早期做的一个项目,为了省一个引脚,还把 CS 直接接地了——结果发现多个 SPI 设备共用总线时,片选必须单独控制,否则数据会串。嗯,这个坑我踩过。
SPI 有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定:
| 模式 | CPOL | CPHA | 数据采样边沿 |
|---|---|---|---|
| 模式 0 | 0 | 0 | 上升沿采样 |
| 模式 1 | 0 | 1 | 下降沿采样 |
| 模式 2 | 1 | 0 | 上升沿采样 |
| 模式 3 | 1 | 1 | 下降沿采样 |
LCD 驱动芯片,比如 ILI9341、ST7789,绝大多数默认使用 模式 0。你想想看,如果模式配错了,屏幕要么不亮,要么花屏。我建议你拿到 LCD 数据手册后,第一件事就是确认 SPI 模式,别急着写代码。
4.2 SPI LCD 驱动编写
驱动 SPI LCD,核心就两个操作:写命令 和 写数据。区别在于,写命令时 DC 引脚拉低,写数据时 DC 引脚拉高。
先看一个最基础的实现:
// 硬件抽象层 — 发送一个字节
static void spi_write_byte(uint8_t data)
{
// 等待发送缓冲区空
while (!(SPI->SR & SPI_SR_TXE));
// 写入数据寄存器
SPI->DR = data;
// 等待接收完成(确保发送完毕)
while (!(SPI->SR & SPI_SR_RXNE));
// 读走接收寄存器(清标志)
(void)SPI->DR;
}
// 写命令
void lcd_write_cmd(uint8_t cmd)
{
LCD_DC_LOW(); // DC 拉低,表示命令
LCD_CS_LOW(); // 片选使能
spi_write_byte(cmd);
LCD_CS_HIGH(); // 片选释放
}
// 写数据
void lcd_write_data(uint8_t data)
{
LCD_DC_HIGH(); // DC 拉高,表示数据
LCD_CS_LOW();
spi_write_byte(data);
LCD_CS_HIGH();
}
这个代码能跑,但效率很低。为什么?因为每个字节都要等待硬件标志位。如果你要刷一帧 320×240 的图片,每个像素 2 字节,那就是 153600 次 SPI 传输——光等标志位就能把 CPU 拖死。
4.3 DMA 传输优化
要解决 CPU 被 SPI 拖死的问题,就得请出 DMA(直接存储器访问)。DMA 可以在不经过 CPU 的情况下,把数据从内存搬到 SPI 发送寄存器。
配置 DMA 的步骤大致如下:
- 使能 DMA 时钟
- 配置 DMA 通道:源地址(内存)、目的地址(SPI->DR)、传输方向(内存到外设)
- 设置数据宽度(8 位或 16 位,看 LCD 要求)
- 使能传输完成中断(可选)
- 启动 DMA 传输
来看一个实际配置片段(以 STM32F4 为例):
void lcd_dma_send(uint8_t *buf, uint32_t len)
{
// 等待上一次 DMA 传输完成
while (DMA2->LISR & DMA_LISR_TCIF0);
// 关闭 DMA 流,清配置
DMA2_Stream0->CR &= ~DMA_SxCR_EN;
// 配置源地址、目的地址、传输长度
DMA2_Stream0->PAR = (uint32_t)&SPI1->DR;
DMA2_Stream0->M0AR = (uint32_t)buf;
DMA2_Stream0->NDTR = len;
// 使能 DMA 流
DMA2_Stream0->CR |= DMA_SxCR_EN;
// 使能 SPI 的 DMA 发送请求
SPI1->CR2 |= SPI_CR2_TXDMAEN;
}
这里有个关键点:DMA 传输完成后,一定要等 SPI 把最后一个字节发完。我曾经遇到过一个问题:DMA 说传完了,但 SPI 移位寄存器里还有数据没送出去,结果屏幕最后一行数据丢了。解决方案是在 DMA 传输完成中断里,等待 SPI 忙标志位清除:
void DMA2_Stream0_IRQHandler(void)
{
if (DMA2->LISR & DMA_LISR_TCIF0) {
// 清除标志
DMA2->LIFCR = DMA_LIFCR_CTCIF0;
// 等待 SPI 发送完毕
while (SPI1->SR & SPI_SR_BSY);
// 关闭 DMA 请求
SPI1->CR2 &= ~SPI_CR2_TXDMAEN;
}
}
4.4 实测波形调试
代码写完了,跑起来屏幕亮了,但总觉得刷新速度不够快?或者偶尔花屏?这时候别瞎猜,上示波器看波形。
我调试 SPI 时,一般会看这几个关键点:
- SCLK 频率:是不是和配置的一致?如果示波器显示频率只有配置的一半,检查时钟分频系数。
- CS 时序:CS 拉低后,SCLK 第一个边沿之前有没有足够的建立时间?我见过一个案例,CS 和 SCLK 几乎同时变化,导致 LCD 误判第一个时钟边沿。
- MOSI 数据:数据是否在 SCLK 的采样边沿稳定?如果数据在边沿附近跳变,说明时序裕量不足。
- DMA 传输间隔:两次 DMA 传输之间有没有不必要的延迟?
下面这张图是我总结的 SPI LCD 驱动调试流程:
实际调试时,我习惯把 SCLK 和 CS 放在示波器的两个通道上,触发条件设为 CS 下降沿。这样每次刷屏时,都能看到完整的片选周期和时钟波形。如果发现 CS 拉低后 SCLK 迟迟不来,说明软件里可能有额外的延迟——检查一下是不是在 CS 操作前后插了不必要的延时函数。
一个真实案例: 我之前调试一块 320×480 的 SPI LCD,发现刷新率只有 15fps,远低于预期的 30fps。用示波器一看,SCLK 频率只有 8MHz,而配置的是 20MHz。查了半天,发现是 SPI 时钟分频系数配错了——代码里写的是 SPI_BAUDRATEPRESCALER_8,但实际硬件分频器是 16 分频。改过来之后,刷新率直接翻倍。
最后说一句:SPI 驱动调试,不要只盯着代码看。示波器才是你的眼睛。波形对了,代码基本没问题;波形不对,代码再漂亮也没用。