3、SPI驱动实现:基于STM32的SPI主模式配置,寄存器级初始化代码,DMA传输优化
SPI接口在显示驱动芯片里,可以说是最常用的通信方式之一。我这些年经手的项目,十有八九都在用SPI。为什么?因为它简单、可靠,而且速度够用。今天咱们就聊聊,怎么在STM32上把SPI主模式玩明白。
3.1 SPI通信基础回顾
SPI全称是Serial Peripheral Interface,说白了就是四根线搞定一切:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。
显示驱动芯片通常只需要三根线——因为数据是单向的,我们只管往芯片里写数据,不需要读回来。所以MISO很多时候可以省掉。
我个人习惯把SPI的四种模式记成「时钟极性+时钟相位」的组合。CPOL决定空闲时时钟是高还是低,CPHA决定数据是在第一个边沿采样还是第二个边沿采样。
| 模式 | CPOL | CPHA | 空闲时钟 | 数据采样边沿 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 低电平 | 第一个边沿(上升沿) |
| 模式1 | 0 | 1 | 低电平 | 第二个边沿(下降沿) |
| 模式2 | 1 | 0 | 高电平 | 第一个边沿(下降沿) |
| 模式3 | 1 | 1 | 高电平 | 第二个边沿(上升沿) |
显示驱动芯片大多用模式0或模式3。我建议你拿到芯片手册后,第一件事就是确认SPI模式。我曾经在一个项目里,因为没仔细看手册,默认用了模式0,结果芯片死活不响应。折腾了两天才发现,人家要的是模式3。
3.2 STM32 SPI寄存器级初始化
很多人喜欢用HAL库,但说实话,做显示驱动这种对时序敏感的场景,我更喜欢直接操作寄存器。为什么?因为可控。你知道每一行代码在干什么,出了问题也能快速定位。
下面是我常用的初始化代码,以STM32F4系列为例:
void SPI_Init_Master(void)
{
// 1. 使能SPI1时钟
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
// 2. 配置GPIO:PA5(SCK), PA7(MOSI), PA4(CS)
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// PA5, PA7: 复用功能推挽输出
GPIOA->MODER |= GPIO_MODER_MODER5_1 | GPIO_MODER_MODER7_1;
GPIOA->AFR[0] |= (5 << 20) | (5 << 28); // AF5 = SPI1
// PA4: 普通推挽输出(CS用软件控制)
GPIOA->MODER |= GPIO_MODER_MODER4_0;
GPIOA->OTYPER &= ~GPIO_OTYPER_OT_4;
GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR4;
// 3. 配置SPI控制寄存器
// 主模式,模式0,8位数据,波特率分频8
SPI1->CR1 = 0;
SPI1->CR1 |= SPI_CR1_MSTR; // 主模式
SPI1->CR1 |= SPI_CR1_BR_1; // 分频8,APB2=84MHz,SPI=10.5MHz
SPI1->CR1 |= SPI_CR1_SSM; // 软件管理片选
SPI1->CR1 |= SPI_CR1_SSI; // 内部片选高电平
SPI1->CR1 |= SPI_CR1_SPE; // 使能SPI
}
嗯,这里要注意一点:波特率分频的选择。显示驱动芯片的SPI时钟频率通常有上限,比如10MHz或20MHz。我习惯先查手册确认最大频率,然后选一个接近但不超过的值。
核心要点:SPI时钟频率 = APB时钟 / 分频系数。STM32F4的APB2最高84MHz,分频8就是10.5MHz,这个速度对大多数显示驱动芯片来说足够了。
3.3 数据发送函数实现
寄存器级的发送函数其实很直接。核心逻辑就是:等发送缓冲区空,然后丢数据进去。
void SPI_SendByte(uint8_t data)
{
// 等待发送缓冲区空
while(!(SPI1->SR & SPI_SR_TXE));
// 发送数据
SPI1->DR = data;
// 等待发送完成
while(SPI1->SR & SPI_SR_BSY);
}
void SPI_SendBuffer(uint8_t *buf, uint16_t len)
{
for(uint16_t i = 0; i < len; i++)
{
SPI_SendByte(buf[i]);
}
}
你想想看,如果只是刷个几十字节的配置数据,这个函数完全够用。但如果是刷一帧图像数据,比如320x240的屏幕,一帧就是76800字节。用这种轮询方式,CPU就被死死占住了。
我在项目中遇到过这种情况:用轮询方式刷屏,CPU占用率飙到90%以上,系统其他任务根本跑不动。这就是为什么我们需要DMA。
3.4 DMA传输优化
DMA的全称是Direct Memory Access,说白了就是让数据自己走,不经过CPU。你只需要告诉DMA:数据从哪来、到哪去、传多少字节,然后它自己就搞定了。
STM32的SPI+DMA配置,我一般分三步走:
- 配置DMA通道,设置源地址、目的地址、传输长度
- 使能DMA请求,让SPI触发DMA传输
- 启动传输,然后CPU就可以去干别的事了
void SPI_DMA_Init(void)
{
// 1. 使能DMA2时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
// 2. 配置DMA2 Stream3 Channel3 (SPI1_TX)
DMA2_Stream3->CR = 0; // 先清零
// 配置参数
DMA2_Stream3->CR |= DMA_SxCR_CHSEL_1 | DMA_SxCR_CHSEL_0; // Channel3
DMA2_Stream3->CR |= DMA_SxCR_DIR_0; // 内存到外设
DMA2_Stream3->CR |= DMA_SxCR_MSIZE_0; // 8位数据
DMA2_Stream3->CR |= DMA_SxCR_PSIZE_0; // 8位数据
DMA2_Stream3->CR |= DMA_SxCR_MINC; // 内存地址递增
DMA2_Stream3->CR |= DMA_SxCR_TCIE; // 传输完成中断使能
// 3. 连接SPI1的TX请求
SPI1->CR2 |= SPI_CR2_TXDMAEN;
}
void SPI_DMA_Send(uint8_t *buf, uint16_t len)
{
// 等待上一次传输完成
while(DMA2_Stream3->CR & DMA_SxCR_EN);
// 设置源地址和目的地址
DMA2_Stream3->PAR = (uint32_t)&SPI1->DR; // 外设地址
DMA2_Stream3->M0AR = (uint32_t)buf; // 内存地址
DMA2_Stream3->NDTR = len; // 传输长度
// 使能DMA流
DMA2_Stream3->CR |= DMA_SxCR_EN;
}
小技巧:DMA传输完成后,记得在中断里清除标志位。我习惯在DMA传输完成中断里拉高CS片选,这样数据发送完自动释放总线,非常干净。
3.5 避坑指南
做SPI驱动这么多年,踩过的坑不少。我挑几个典型的说说:
坑1:CS片选时序
我曾经在刷屏时发现第一行数据总是错位。查了半天,原来是CS拉低后没有等足够的时间就发数据。有些显示驱动芯片需要CS拉低后等几个微秒才能开始通信。解决方案是在CS拉低后加一个微秒级的延时。
坑2:DMA缓冲区对齐
STM32的DMA对内存地址有对齐要求。如果缓冲区地址不是4字节对齐,在某些芯片上会导致传输异常。我建议发送缓冲区用__attribute__((aligned(4)))来声明。
坑3:SPI时钟极性搞反
这个前面提过,但值得再说一遍。不同厂家的显示驱动芯片,SPI模式可能不一样。拿到新芯片,先拿示波器看波形,确认时钟和数据的关系对不对。
3.6 性能对比
我实际测试过一组数据,刷一块320x240的屏幕,16位色,一帧数据量是153600字节:
| 传输方式 | 传输时间 | CPU占用率 | 适用场景 |
|---|---|---|---|
| 轮询发送 | 约15ms | 95% | 配置数据、小数据量 |
| 中断发送 | 约15ms | 60% | 中等数据量 |
| DMA发送 | 约15ms | 5% | 大数据量、刷屏 |
看到没?传输时间其实差不多,因为瓶颈在SPI时钟速度上。但CPU占用率天差地别。DMA方式下,CPU几乎可以完全解放出来处理其他任务。
3.7 完整驱动框架
最后,我分享一个我常用的驱动框架结构。说白了就是分层设计:
- 底层:寄存器操作,SPI初始化、发送字节、DMA配置
- 中间层:封装成显示驱动需要的接口,比如写命令、写数据、写缓冲区
- 上层:应用逻辑,比如画点、画线、刷图
这样做的好处是,换芯片时只需要改底层,上层代码基本不用动。我这些年换过好几款MCU,从STM32F1到F4再到H7,底层重写,上层直接复用,省了不少事。
总结一下:SPI驱动实现的核心就三件事——配好寄存器、选对传输方式、注意时序细节。DMA是刷屏场景的必备技能,建议你花点时间把它吃透。
好了,SPI驱动实现这部分就聊到这儿。代码都在上面了,你拿去改改就能用。记住一点:调试时别急,先拿示波器看波形,波形对了,问题就解决了一半。
公众号:蓝海资料掘金营,微信deep3321