4. GPIO与基础外设编程
中断,是嵌入式系统的灵魂。没有中断,CPU就得不停地轮询,效率低得可怜。尤其在工业控制里,一个外部信号来了,你得立刻响应,晚一毫秒可能就出事故了。CH32V203 的中断系统,说白了就是一套高效的事件响应机制。今天咱们把它拆开揉碎了讲清楚。
核心要点:中断系统 = 中断源 + 中断控制器(NVIC) + CPU响应 + 中断服务函数。四者缺一不可。
4.1 中断向量表——系统的“紧急联络簿”
中断向量表,你可以把它想象成一本电话簿。CPU收到中断信号后,会立刻翻这本电话簿,找到对应的处理函数地址,然后跳过去执行。CH32V203 的中断向量表默认放在 Flash 起始地址 0x00000000。
我记得第一次做 RISC-V 项目时,自己写 Bootloader,结果中断向量表没重映射,一进中断就跑飞了。查了两天才发现是向量表偏移没设置对。嗯,这个坑我替你们踩过了。
CH32V203 的中断向量表结构如下:
| 偏移地址 | 内容 | 说明 |
|---|---|---|
| 0x00000000 | 栈顶指针 | 复位后 SP 初始值 |
| 0x00000004 | 复位中断入口 | 程序启动地址 |
| 0x00000008 | NMI 入口 | 不可屏蔽中断 |
| 0x0000000C ~ 0x0000002C | 系统异常入口 | HardFault 等 |
| 0x00000030 起 | 外部中断入口 | EXTI0 ~ EXTI15 等 |
个人习惯:我一般会在工程里显式定义中断向量表,而不是完全依赖链接脚本。这样移植时心里有底。
4.2 NVIC 配置——中断的“调度中心”
NVIC(嵌套向量中断控制器)是 RISC-V 内核里专门管中断的硬件模块。它负责中断的使能、挂起、优先级裁决。CH32V203 的 NVIC 支持最多 64 个中断源,每个中断有 4 位优先级(0~15,0 最高)。
配置 NVIC 其实就三步:
- 使能中断源——告诉 NVIC 这个中断我允许它触发。
- 设置优先级——决定哪个中断更重要。
- 清除挂起标志——防止残留的中断信号干扰。
代码示例:
// 使能 USART1 中断,优先级设为 3
NVIC_EnableIRQ(USART1_IRQn);
NVIC_SetPriority(USART1_IRQn, 3);
你想想看,如果两个中断同时来了,NVIC 怎么选?它比较优先级数值,数值小的先执行。如果优先级相同,就看它们在向量表中的位置,位置靠前的先响应。
我曾经踩过的坑:在中断服务函数里调用 printf 打印调试信息。结果 printf 本身可能触发其他中断,导致死锁。工业现场调试,千万别在中断里干重活。
4.3 外部中断 EXTI——芯片的“触角”
EXTI(External Interrupt)是 CH32V203 用来检测外部引脚电平变化的模块。它支持 16 个外部中断线(EXTI0~EXTI15),每个中断线可以映射到多个 GPIO 引脚,但同一时刻只能选一个。
配置 EXTI 的流程:
- 第一步:使能 GPIO 时钟,配置引脚为输入模式。
- 第二步:配置 EXTI 触发方式——上升沿、下降沿、或者双边沿。
- 第三步:配置 NVIC,使能对应的 EXTI 中断。
- 第四步:编写中断服务函数。
代码示例:
// 配置 PA0 为外部中断,下降沿触发
GPIO_InitTypeDef GPIO_InitStruct = {0};
EXTI_InitTypeDef EXTI_InitStruct = {0};
NVIC_InitTypeDef NVIC_InitStruct = {0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
实际项目经验:在电机控制里,我用 EXTI 检测霍尔传感器的跳变沿。注意一定要加硬件去抖,不然一个跳变能触发好几次中断。
4.4 中断优先级与嵌套——谁先谁后?
CH32V203 的中断优先级分为抢占优先级和子优先级。抢占优先级决定能否打断正在执行的中断,子优先级决定同抢占优先级下的响应顺序。
优先级分组由 NVIC_PriorityGroupConfig() 函数设置:
| 分组 | 抢占优先级位数 | 子优先级位数 |
|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 |
| NVIC_PriorityGroup_1 | 1 | 3 |
| NVIC_PriorityGroup_2 | 2 | 2 |
| NVIC_PriorityGroup_3 | 3 | 1 |
| NVIC_PriorityGroup_4 | 4 | 0 |
我建议在工业控制项目里使用 NVIC_PriorityGroup_2,也就是 2 位抢占优先级、2 位子优先级。这样既能保证关键中断(比如定时器中断)可以打断普通中断,又不会让优先级层次太复杂。
嵌套中断的执行流程:
- 低优先级中断 A 正在执行。
- 高优先级中断 B 触发。
- CPU 暂停 A,保存现场,跳转到 B 的服务函数。
- B 执行完毕,恢复现场,继续执行 A。
注意:中断嵌套层数不要太多。我曾经在一个项目里嵌套了 4 层,结果栈空间不够,直接 HardFault。一般嵌套 2~3 层就够了。
4.5 中断系统知识结构图
下面这张图帮你理清中断系统的整体脉络:
4.6 实际项目中的中断设计建议
在工业控制项目里,中断设计直接决定了系统的实时性和稳定性。我总结了几条经验:
- 中断服务函数要短小精悍——只做标志位设置或数据搬运,复杂逻辑放到主循环处理。
- 合理分配优先级——定时器中断、外部紧急信号用高优先级,串口等通信中断用低优先级。
- 注意临界区保护——如果中断和主循环共享变量,记得用关中断或原子操作保护。
- 调试时善用断点——但别在中断里打断点,容易导致看门狗超时复位。
一句话总结:中断系统是 CH32V203 实时响应的基石。向量表是地图,NVIC 是交警,EXTI 是哨兵,优先级是规则。把这套机制玩熟了,工业控制项目就稳了一大半。