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 其实就三步:

  1. 使能中断源——告诉 NVIC 这个中断我允许它触发。
  2. 设置优先级——决定哪个中断更重要。
  3. 清除挂起标志——防止残留的中断信号干扰。

代码示例:

// 使能 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 位子优先级。这样既能保证关键中断(比如定时器中断)可以打断普通中断,又不会让优先级层次太复杂。

嵌套中断的执行流程:

  1. 低优先级中断 A 正在执行。
  2. 高优先级中断 B 触发。
  3. CPU 暂停 A,保存现场,跳转到 B 的服务函数。
  4. B 执行完毕,恢复现场,继续执行 A。

注意:中断嵌套层数不要太多。我曾经在一个项目里嵌套了 4 层,结果栈空间不够,直接 HardFault。一般嵌套 2~3 层就够了。

4.5 中断系统知识结构图

下面这张图帮你理清中断系统的整体脉络:

CH32V203 中断系统结构图 中断源 外部中断 EXTI 外设中断 系统异常 NVIC 中断控制器 优先级裁决与嵌套 CPU 响应 & 中断服务函数 中断向量表位于 Flash 起始地址,NVIC 根据优先级裁决后通知 CPU 跳转到对应服务函数

4.6 实际项目中的中断设计建议

在工业控制项目里,中断设计直接决定了系统的实时性和稳定性。我总结了几条经验:

  • 中断服务函数要短小精悍——只做标志位设置或数据搬运,复杂逻辑放到主循环处理。
  • 合理分配优先级——定时器中断、外部紧急信号用高优先级,串口等通信中断用低优先级。
  • 注意临界区保护——如果中断和主循环共享变量,记得用关中断或原子操作保护。
  • 调试时善用断点——但别在中断里打断点,容易导致看门狗超时复位。

一句话总结:中断系统是 CH32V203 实时响应的基石。向量表是地图,NVIC 是交警,EXTI 是哨兵,优先级是规则。把这套机制玩熟了,工业控制项目就稳了一大半。