4. 中断驱动SPI:中断使能与配置、中断服务函数编写、半双工通信示例
中断驱动,说白了就是让硬件主动通知你,而不是你一直轮询去问。我刚开始做SPI驱动时,总觉得轮询简单可靠,直到有一次项目要求CPU在SPI传输间隙还要处理传感器融合算法……嗯,轮询那套直接卡死。从那以后,我对中断驱动就特别上心了。
4.1 中断使能与配置——别漏了这一步
很多人写SPI中断驱动,上来就写中断服务函数,结果发现根本不进中断。为什么?因为中断使能没配全。SPI中断使能,至少需要配三个地方:
- SPI控制器本身的中断使能寄存器——比如STM32上的SPI_CR2的TXEIE、RXNEIE位
- NVIC(嵌套向量中断控制器)的使能——SPI对应的IRQ通道要打开
- 全局中断使能——cpsie指令或者__enable_irq()
我在项目中遇到过,有人配了前两个,忘了开全局中断,调试了一整天……后来发现是这么低级的错误。
核心配置流程:
- 初始化SPI时钟、GPIO复用
- 配置SPI控制寄存器(模式、波特率、数据格式)
- 使能SPI中断位(TXEIE/RXNEIE/ERRIE)
- 配置NVIC优先级并使能SPI中断通道
- 使能全局中断
- 最后使能SPI外设(SPI_CR1的SPE位)
这里有个细节:SPI使能(SPE)最好放在最后。我曾经先开了SPE再配中断,结果SPI一使能就产生了一个虚假的RXNE中断,导致服务函数读到垃圾数据。你想想看,这种问题排查起来多头疼。
4.2 中断服务函数编写——短小精悍是王道
中断服务函数(ISR)的核心原则就一条:快进快出。不要在ISR里做复杂计算、打印、动态内存分配。我见过有人把printf直接写在ISR里,结果串口中断优先级比SPI低,直接死锁。
一个典型的SPI中断服务函数骨架:
void SPI1_IRQHandler(void)
{
// 检查中断源
if (SPI1->SR & SPI_SR_RXNE) {
// 读DR寄存器自动清除RXNE标志
uint8_t rx_data = SPI1->DR;
// 存入接收缓冲区
if (rx_index < RX_BUF_SIZE) {
rx_buf[rx_index++] = rx_data;
}
// 如果接收完成,置标志位
if (rx_index == expected_len) {
transfer_done = 1;
}
}
if (SPI1->SR & SPI_SR_TXE) {
// 发送缓冲区还有数据就继续写
if (tx_index < TX_BUF_SIZE) {
SPI1->DR = tx_buf[tx_index++];
} else {
// 发送完毕,关闭TXE中断防止反复触发
SPI1->CR2 &= ~SPI_CR2_TXEIE;
}
}
// 错误处理
if (SPI1->SR & (SPI_SR_OVR | SPI_SR_MODF | SPI_SR_CRCERR)) {
// 读SR再读DR清除错误标志
volatile uint32_t tmp = SPI1->DR;
tmp = SPI1->SR;
error_flag = 1;
}
}
个人习惯:我通常会在ISR里只做数据搬运和标志位设置,真正的业务逻辑放在主循环或任务里处理。这样ISR执行时间能控制在几微秒以内。
4.3 半双工通信示例——一条线搞定
SPI半双工模式,说白了就是同一根数据线,分时收发。这在引脚受限的场景下特别有用。比如我做过一个温湿度传感器驱动,MCU只有8个引脚可用,SPI用了3根线(SCK、MOSI、CS),省下的引脚刚好接个按键。
半双工模式下,SPI的数据线(MISO/MOSI)会合并成一根,叫SIMO(Slave In Master Out)或SOMI(Slave Out Master In)。配置时要注意:
| 配置项 | 全双工 | 半双工 |
|---|---|---|
| 数据线数量 | 2根(MOSI+MISO) | 1根(SIMO/SOMI) |
| SPI_CR1的BIDIMODE | 0 | 1 |
| SPI_CR1的BIDIOE | 无效 | 1=输出,0=输入 |
| 收发方向 | 同时收发 | 分时收发 |
下面是一个半双工中断驱动的读写示例:
// 半双工发送
void spi_halfduplex_tx(uint8_t *data, uint16_t len)
{
// 配置为输出模式
SPI1->CR1 |= SPI_CR1_BIDIOE;
tx_buf = data;
tx_len = len;
tx_index = 0;
transfer_done = 0;
// 使能TXE中断
SPI1->CR2 |= SPI_CR2_TXEIE;
// 写入第一个数据触发传输
SPI1->DR = tx_buf[tx_index++];
}
// 半双工接收
void spi_halfduplex_rx(uint8_t *buf, uint16_t len)
{
// 配置为输入模式
SPI1->CR1 &= ~SPI_CR1_BIDIOE;
rx_buf = buf;
rx_len = len;
rx_index = 0;
transfer_done = 0;
// 使能RXNE中断
SPI1->CR2 |= SPI_CR2_RXNEIE;
// 发送空数据产生时钟
SPI1->DR = 0xFF;
}
注意:半双工模式下,切换收发方向时,需要等待当前传输完全停止。我踩过这个坑——发送完立刻切输入,结果最后一个bit还没移出去,数据就乱了。建议在切换方向前加一个小的延时,或者检查SPI_SR的BSY位。
4.4 中断驱动的知识体系
我把中断驱动SPI的核心逻辑画了张图,方便你理解整体流程:
这张图展示了中断驱动的核心思想:主循环发起传输后,CPU可以去做其他事情;硬件在完成一个字节传输后触发中断,ISR负责数据搬运;传输完成后置标志,主循环再回来处理数据。说白了,就是让CPU从轮询的苦力活里解放出来。
4.5 避坑指南
- 中断优先级:SPI中断优先级不要设得太高,否则会饿死其他中断。我一般设成中等优先级,比定时器低,比外部中断高。
- 临界区保护:如果主循环和ISR共享缓冲区,记得在访问缓冲区前关中断或使用原子操作。我吃过这个亏——主循环在读缓冲区时,ISR刚好写进来,数据就错位了。
- 错误中断:别忘了使能错误中断(ERRIE)。SPI通信中过载(OVR)和模式错误(MODF)很常见,不处理的话,通信会莫名其妙卡死。
- 中断标志清除:读SPI_DR寄存器会自动清除RXNE标志,写SPI_DR会自动清除TXE标志。但错误标志需要手动清除——先读SR,再读DR。
总结一下:中断驱动SPI的核心就是配置好中断使能、写好短小精悍的ISR、处理好半双工的方向切换。做到这三点,你的SPI通信就能在中断模式下稳定运行,CPU利用率也能大幅提升。