3. SPI中断配置实战:使能TXE/RXNE/ERR中断,配置NVIC优先级,编写中断服务函数
好,咱们直接进入正题。上一章我们把SPI的轮询模式跑通了,但说实话,在真正的项目里,我很少只用轮询。为什么?因为CPU大部分时间都在空转等待,浪费啊。中断模式才是嵌入式开发的常态。
这一章,我们就来亲手配置SPI的中断。说白了,就是让SPI硬件在“有事”的时候主动通知CPU,而不是CPU一遍遍去问“你好了没”。
3.1 中断源:TXE、RXNE、ERR 分别是什么?
在配置之前,你得先搞清楚这三个中断标志位各自代表什么。我刚开始学的时候,就搞混过TXE和TXIS,结果调试了半天。
| 中断标志 | 全称 | 触发条件 | 典型用途 |
|---|---|---|---|
| TXE | Transmit Buffer Empty | 发送缓冲区为空,可以写入新数据 | 连续发送多字节数据 |
| RXNE | Receive Buffer Not Empty | 接收缓冲区非空,有数据可读 | 读取接收到的数据 |
| ERR | Error Interrupt | 发生溢出、模式错误或CRC错误 | 异常处理与恢复 |
3.2 使能SPI中断:寄存器操作
使能中断其实就两步:设置SPI的CR2寄存器,把对应的位拉高。以STM32为例,我们操作的是 SPIx->CR2。
// 使能SPI1的TXE、RXNE、ERR中断
SPI1->CR2 |= SPI_CR2_TXEIE; // 使能发送缓冲区空中断
SPI1->CR2 |= SPI_CR2_RXNEIE; // 使能接收缓冲区非空中断
SPI1->CR2 |= SPI_CR2_ERRIE; // 使能错误中断
// 或者一次性写
SPI1->CR2 |= (SPI_CR2_TXEIE | SPI_CR2_RXNEIE | SPI_CR2_ERRIE);
嗯,这里要注意:使能中断之前,最好先确认SPI外设已经处于关闭或复位状态,避免在配置过程中产生误中断。我习惯的做法是:先关SPI(SPI->CR1 &= ~SPI_CR1_SPE),配完中断再打开。
3.3 NVIC优先级配置:抢占优先级与子优先级
中断使能了,但CPU怎么知道该先响应哪个中断?这就轮到NVIC出场了。NVIC是Cortex-M内核的中断控制器,它决定了中断的优先级。
你想想看,如果SPI中断和定时器中断同时发生,谁先执行?这就取决于优先级配置。
3.3.1 优先级分组
STM32的NVIC支持4位优先级,可以拆分成抢占优先级和子优先级。通过 NVIC_PriorityGroupConfig() 来设置分组。
| 分组 | 抢占优先级位数 | 子优先级位数 | 典型场景 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 所有中断同级,按子优先级排队 |
| NVIC_PriorityGroup_1 | 1 | 3 | 2级抢占,8级子优先级 |
| NVIC_PriorityGroup_2 | 2 | 2 | 4级抢占,4级子优先级 |
| NVIC_PriorityGroup_3 | 3 | 1 | 8级抢占,2级子优先级 |
| NVIC_PriorityGroup_4 | 4 | 0 | 16级抢占,无子优先级(最常用) |
3.3.2 配置SPI中断优先级
假设我们使用Group_4,给SPI1中断分配抢占优先级2。
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = SPI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级(Group_4下无效)
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
这里有个坑:优先级数值越小,优先级越高。0是最高,15是最低。我曾经见过有人把SPI中断优先级配成15,结果被其他中断疯狂打断,SPI通信一直超时。
3.4 编写中断服务函数
中断服务函数(ISR)是中断模式的灵魂。你需要根据中断标志位来判断发生了什么事件,然后做对应的处理。
3.4.1 标准ISR框架
void SPI1_IRQHandler(void)
{
// 1. 检查错误中断
if (SPI1->SR & SPI_SR_OVR)
{
// 溢出错误处理
uint8_t tmp = SPI1->DR; // 读DR清OVR标志
tmp = SPI1->DR; // 再读一次(某些系列需要)
// 记录错误计数或复位SPI
Error_Handler();
}
// 2. 检查接收中断(RXNE)
if (SPI1->SR & SPI_SR_RXNE)
{
// 读取接收到的数据
uint8_t rx_data = (uint8_t)SPI1->DR;
// 存入接收缓冲区
if (rx_buffer_index < RX_BUFFER_SIZE)
{
rx_buffer[rx_buffer_index++] = rx_data;
}
}
// 3. 检查发送中断(TXE)
if (SPI1->SR & SPI_SR_TXE)
{
// 如果还有数据要发送
if (tx_buffer_index < tx_length)
{
SPI1->DR = tx_buffer[tx_buffer_index++];
}
else
{
// 数据发送完毕,关闭TXE中断防止频繁触发
SPI1->CR2 &= ~SPI_CR2_TXEIE;
}
}
}
3.4.2 中断服务函数的执行效率
ISR里不要做耗时操作。比如不要在ISR里调用printf、malloc或者复杂的数学运算。你想想看,如果ISR执行时间过长,其他中断就会被阻塞,系统实时性就没了。
我一般只在ISR里做三件事:
- 读取/写入数据到缓冲区
- 更新标志位或计数器
- 清除中断标志
剩下的数据处理,交给主循环或任务去完成。
3.5 整体配置流程
把上面几步串起来,就是一个完整的SPI中断初始化流程。我画了一张图,帮你理清思路。
3.6 避坑指南:我踩过的几个坑
- 中断标志没清干净: 有些标志位是硬件自动清除的,有些需要软件写1清除。比如TXE是硬件自动置位和清除,但ERR中的OVR需要读SR再读DR才能清除。搞混了就会导致ISR反复进入。
- 优先级配得太低: 我曾经把SPI中断优先级配成15,结果系统里一个高优先级的定时器中断每100us触发一次,SPI的ISR一直被抢占,导致接收数据丢失。后来我把SPI优先级提到3,问题解决。
- 在ISR里调用HAL_Delay: HAL_Delay依赖SysTick中断,如果SysTick优先级比SPI低,就会死锁。我建议ISR里只用寄存器操作,别用HAL库的阻塞函数。