4. SPI通信协议实现
中断,是嵌入式系统的灵魂。没有中断,CPU就得轮询——说白了就是傻等。我刚开始用GD32V做项目时,第一个栽跟头就在中断优先级配置上。今天咱们把中断系统掰开揉碎,从向量表讲到嵌套抢占。
4.1 中断向量表:CPU的紧急联络簿
中断向量表,本质是一张地址表。每个中断源对应一个入口地址,CPU响应中断时自动跳转。GD32V基于RISC-V内核,向量表基址默认在0x00000000,但可以通过mtvec寄存器重映射。
核心要点:GD32V的向量表每个条目占4字节,存放的是中断服务函数的地址。第0号条目是栈指针,第1号是复位向量,从第2号开始才是各类异常和中断。
我习惯在工程启动文件里这样定义向量表:
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
&_estack, // 0: 栈顶指针
Reset_Handler, // 1: 复位
NMI_Handler, // 2: NMI
HardFault_Handler, // 3: 硬错误
// ... 省略中间条目
EXTI0_IRQHandler, // 外部中断0
EXTI1_IRQHandler, // 外部中断1
// ...
};
嗯,这里要注意:向量表必须4字节对齐。我曾经遇到一个同事,把向量表放在未对齐的地址上,结果一进中断就跑飞,查了两天才发现是对齐问题。
4.2 NVIC与中断优先级配置
GD32V的嵌套向量中断控制器(NVIC)支持最多256个中断,每个中断可配置4位抢占优先级和4位子优先级。但具体用几位,由PRICTL寄存器决定。
| 分组 | 抢占优先级位数 | 子优先级位数 | 应用场景 |
|---|---|---|---|
| 组0 | 0 | 4 | 所有中断同级,按子优先级排队 |
| 组1 | 1 | 3 | 2级抢占,8级子优先级 |
| 组2 | 2 | 2 | 4级抢占,4级子优先级(常用) |
| 组3 | 3 | 1 | 8级抢占,2级子优先级 |
| 组4 | 4 | 0 | 16级抢占,无子优先级 |
配置优先级分两步走。第一步设置分组:
NVIC_PRIGROUP_CMD(NVIC_PRIGROUP_2); // 2位抢占 + 2位子优先级
第二步设置具体中断的优先级:
nvic_irq_enable(EXTI0_IRQn, 2, 1); // 抢占优先级2,子优先级1
我的经验:项目初期就定好优先级分组,中途别改。我见过一个产品,因为后期调整分组导致所有中断优先级重新洗牌,整个系统的实时性全乱了。
4.3 外部中断(EXTI)实战
EXTI是GD32V最常用的外部中断模块。它支持16个外部中断线,每个线可以映射到多个GPIO引脚,但同一时刻只能选一个。
配置EXTI的标准流程:
- 使能GPIO时钟和SYSCFG时钟
- 配置GPIO为输入模式
- 将GPIO连接到EXTI线
- 配置EXTI触发方式(上升沿/下降沿/双边沿)
- 使能EXTI中断
- 配置NVIC优先级
- 编写中断服务函数
来个完整的按键中断示例:
void EXTI_Key_Init(void)
{
// 1. 使能时钟
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_SYSCFG);
// 2. 配置PA0为输入
gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
// 3. 连接EXTI线0到PA0
syscfg_exti_line_config(EXTI_SOURCE_GPIOA, EXTI_SOURCE_PIN0);
// 4. 配置下降沿触发
exti_init(EXTI_0, EXTI_INTERRUPT, EXTI_TRIG_FALLING);
exti_interrupt_flag_clear(EXTI_0);
// 5. 使能EXTI中断
exti_interrupt_enable(EXTI_0);
// 6. 配置NVIC
nvic_irq_enable(EXTI0_IRQn, 1, 0);
}
void EXTI0_IRQHandler(void)
{
if(exti_interrupt_flag_get(EXTI_0) != RESET)
{
// 处理按键事件
// 注意:这里要加消抖,我吃过亏
delay_1ms(20);
if(gpio_input_bit_get(GPIOA, GPIO_PIN_0) == RESET)
{
// 按键确实被按下
key_pressed = 1;
}
exti_interrupt_flag_clear(EXTI_0);
}
}
避坑指南:我曾经在EXTI中断里直接调用printf,结果程序卡死。原因是printf用了轮询方式发送,而串口中断优先级比EXTI低,导致死锁。中断服务函数里别做耗时操作,只设标志位,具体处理放到主循环或任务里。
4.4 中断嵌套与抢占
中断嵌套,说白了就是高优先级中断打断低优先级中断。GD32V的NVIC支持硬件自动完成嵌套,不需要软件干预。
嵌套发生的条件:
- 两个中断的抢占优先级不同
- 高优先级中断在低优先级中断执行期间触发
- 高优先级中断未被屏蔽
来看一个典型的嵌套场景:
// 低优先级中断 - 抢占优先级1
void TIMER2_IRQHandler(void)
{
// 执行到一半...
volatile uint32_t temp = 100;
// 此时EXTI0中断(抢占优先级0)触发
// CPU自动保存上下文,跳转到EXTI0_IRQHandler
// EXTI0执行完毕后,返回这里继续
temp = 200;
}
// 高优先级中断 - 抢占优先级0
void EXTI0_IRQHandler(void)
{
// 这个函数会打断TIMER2的中断
volatile uint32_t data = 0xFF;
// 处理完毕,返回被中断的TIMER2
}
为什么会这样?因为NVIC在硬件层面做了优先级比较。当新中断的抢占优先级高于当前中断时,硬件自动压栈当前上下文,然后执行新中断。新中断退出时,硬件自动恢复上下文。
关键点:子优先级不参与嵌套判断。只有抢占优先级不同才会触发嵌套。子优先级只在多个中断同时挂起时决定谁先执行。
我建议在设计中断系统时,遵循几个原则:
- 时间敏感的中断(如定时器、通信)给高抢占优先级
- 非关键中断(如按键)给低优先级
- 同一抢占优先级内,按紧急程度分配子优先级
- 中断服务函数越短越好,最好只设标志位
实用技巧:在FreeRTOS中,中断嵌套要特别小心。FreeRTOS的临界区保护只对任务级代码有效。如果中断里调用了FreeRTOS API,必须使用FromISR后缀的版本,并且要确保中断优先级不高于configMAX_SYSCALL_INTERRUPT_PRIORITY。
最后说个我踩过的坑。有一次调试电机控制,定时器中断里做了PID计算,结果偶尔出现计算错误。查了半天,发现是串口中断(优先级更高)打断了PID计算中的关键变量读取。解决方案很简单:在读取多字节变量前关中断,读完再开。或者用volatile+原子操作也能解决。
中断系统就这些核心内容。记住:优先级分组定好别改,中断服务函数要短,嵌套时注意临界区保护。做到这三点,中断系统基本不会出大问题。