3. 开发环境搭建

做嵌入式开发,尤其是工业控制,我最常听到的一句话就是:「别让 CPU 傻等」。轮询方式虽然简单,但 CPU 大部分时间都在做无用功。中断,才是真正让系统高效运转的核心机制。这一章,我们就来啃下 CH32V307 的 GPIO 中断。

3.1 外部中断线配置:把引脚和中断控制器连起来

CH32V307 的 GPIO 中断,说白了就是通过 EXTI(外部中断/事件控制器)来管理的。每个 GPIO 引脚都可以作为中断源,但要注意,同一组引脚(比如 PA0、PB0、PC0)共享一个中断线。也就是说,你不能同时让 PA0 和 PB0 都触发 EXTI0 中断。

⚠️ 我曾经踩过的坑: 项目初期想当然地配置了 PA0 和 PB0 同时作为下降沿中断,结果发现只有 PA0 能触发。查了两天手册才意识到,EXTI 线 0 只能映射一个引脚。后来我养成了习惯,每次配置前先看原理图,确认引脚对应的 EXTI 线是否冲突。

配置步骤其实很清晰,我一般按这个流程走:

  1. 使能时钟:GPIO 时钟和 AFIO 时钟都要打开。
  2. 配置 GPIO 模式:设置为输入模式,带上拉或下拉。
  3. 连接 EXTI 线:通过 AFIO 将 GPIO 引脚映射到对应的 EXTI 线。
  4. 配置 EXTI 触发方式:上升沿、下降沿,或者双边沿。
  5. 使能中断线:在 EXTI 寄存器中使能对应的中断线。

来看一段实际的初始化代码:

void EXTI_Key_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    EXTI_InitTypeDef EXTI_InitStructure = {0};
    NVIC_InitTypeDef NVIC_InitStructure = {0};

    // 1. 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);

    // 2. 配置 PA0 为上拉输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;  // 上拉输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 3. 连接 EXTI 线 0 到 PA0
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

    // 4. 配置 EXTI 线 0
    EXTI_InitStructure.EXTI_Line = EXTI_Line0;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;  // 下降沿触发
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    // 5. 配置 NVIC
    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}
💡 个人习惯: 我通常把 GPIO 引脚和 EXTI 线的映射关系写成一个宏定义,方便后期维护。比如 #define KEY_EXTI_LINE EXTI_Line0,这样换引脚时只需要改一处。

3.2 中断优先级:谁先谁后,得有个规矩

工业控制现场,中断源多得很。定时器、串口、外部信号……如果优先级没配好,高优先级的任务可能被低优先级的打断,系统就乱套了。

CH32V307 用的是 RISC-V 标准的 PLIC(平台级中断控制器)。它支持最多 255 个中断源,每个中断源可以配置独立的优先级。数值越小,优先级越高?不对,这里要注意:数值越大,优先级越高。0 表示中断禁用。

优先级值 含义 典型应用场景
0 中断禁用 不使用的中断源
1 最低优先级 后台数据采集
2-4 中等优先级 按键、显示刷新
5-7 高优先级 通信、故障保护

配置优先级时,我习惯用 NVIC 的 NVIC_SetPriority() 函数。举个例子:

NVIC_SetPriority(EXTI0_IRQn, 2);   // 设置抢占优先级为 2
NVIC_SetPriority(USART1_IRQn, 5);  // 串口优先级更高
🔑 关键点: 中断优先级不是越高越好。你把按键中断优先级设得比电机控制还高,那电机丢步了谁负责?我一般遵循「实时性要求越高,优先级越高」的原则。

3.3 中断服务函数编写:别在里面干重活

中断服务函数(ISR)是中断的「心脏」。但很多人容易犯一个错误——在 ISR 里写太多代码。你想想看,ISR 执行时间越长,其他中断被阻塞的时间就越长。系统响应就慢了。

我总结了一个「三不原则」:

  • 不延时:别用 delay_ms(),那是轮询时代的产物。
  • 不等待:别在 ISR 里等某个标志位,用状态机代替。
  • 不做复杂计算:浮点运算、大数据处理,统统扔到主循环里。

正确的做法是:ISR 里只做最必要的事——清除中断标志位、设置一个全局标志位,然后立即退出。主循环里再根据标志位去处理具体业务。

volatile uint8_t key_pressed = 0;  // 全局标志位

void EXTI0_IRQHandler(void)
{
    if(EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        // 消抖处理:简单延时后再判断
        Delay_Ms(10);
        if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
        {
            key_pressed = 1;  // 设置标志位
        }
        EXTI_ClearITPendingBit(EXTI_Line0);  // 清除中断标志
    }
}
⚠️ 注意: 全局标志位一定要用 volatile 修饰。我之前就因为忘了加,编译器优化后主循环永远读不到更新后的值,排查了整整一个下午。

3.4 按键中断实战:从按下到响应

理论说完了,咱们来实战。用按键中断控制一个 LED 的亮灭。按下按键,LED 状态翻转。

硬件连接很简单:

  • 按键接 PA0,外部下拉电阻到 GND。
  • LED 接 PC0,高电平点亮。

主程序逻辑:

int main(void)
{
    // 初始化 LED 引脚
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    // 初始化按键中断
    EXTI_Key_Init();

    while(1)
    {
        if(key_pressed)
        {
            key_pressed = 0;  // 清除标志
            GPIO_WriteBit(GPIOC, GPIO_Pin_0, 
                (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_0)));
        }
        // 其他任务...
    }
}

这段代码看起来简单,但有几个细节要注意:

  • 消抖:机械按键按下时会有抖动,我用了 10ms 延时做软件消抖。工业现场环境恶劣,有时候需要 20ms 甚至更长。
  • 标志位清零时机:一定要在读取完标志位后立即清零,防止重复触发。
  • LED 状态翻转:用 1 - GPIO_ReadOutputDataBit() 实现,简洁高效。
💡 避坑指南: 我曾经在一个项目中,按键中断里直接调用了 GPIO_WriteBit() 翻转 LED。结果按键按得快了,LED 闪烁不均匀。后来改成标志位方式,主循环里处理,问题就解决了。记住:ISR 里只做「标记」,不做「动作」。

3.5 知识体系:一张图看懂 GPIO 中断

为了让你更直观地理解整个流程,我画了一张结构图。它展示了从按键按下到 CPU 响应中断的完整路径。

GPIO 中断响应流程 按键按下 GPIO 输入检测 EXTI 中断控制器 NVIC 中断仲裁 CPU 执行 ISR 关键配置点 • 触发方式:上升/下降沿 • 优先级:数值越大越高 • 消抖:软件延时 10-20ms ISR 编写原则 • 只设标志位 • 清除中断标志 • 不做复杂运算

从图上你能看到,一个按键中断要经过 GPIO、EXTI、NVIC 三层才能到达 CPU。每一层都有对应的配置项。我刚开始学的时候,总觉得配置项太多记不住。后来发现,只要理解了数据流,配置就是顺理成章的事。

3.6 小结

这一章我们聊了 GPIO 中断的完整链路。从 EXTI 线的配置,到中断优先级的设定,再到 ISR 的编写规范,最后用按键中断实战串了一遍。核心就一句话:中断是手段,不是目的。别为了用中断而用中断,要清楚你的系统到底需要什么样的实时响应。