
中断是嵌入式系统最重要的机制之一。它使处理器能够在执行主程序的同时,对外部事件作出快速响应。在四旋翼无人机飞控系统中,中断被广泛应用于传感器数据采集、按键检测、定时器任务调度以及通信收发等场景。本章将系统讲解 CH582F 芯片的中断体系,并通过实际代码示例帮助读者掌握中断编程方法。
中断(Interrupt)是指 CPU 在正常执行程序的过程中,由于内部或外部的紧急事件发生,CPU 暂停当前工作,转去处理该事件,处理完成后再返回断点继续执行的过程。这一机制让系统具备了实时响应能力,避免了轮询(Polling)方式带来的 CPU 资源浪费。
以四旋翼飞控为例,MPU-6050 惯性传感器在完成一次采样后,会通过 INT 引脚向 MCU 发出中断信号。MCU 收到中断后立刻读取传感器数据,而不必反复查询传感器状态,从而大幅提高了系统效率。
CH582F 基于 WCH RISC-V 内核,其可编程快速中断控制器(PFIC)支持以下几类中断源:
每个中断源都有唯一的中断向量号(IRQn),CPU 通过该号码在中断向量表中找到对应的中断服务函数(ISR,Interrupt Service Routine)入口地址。CH582F 的中断向量表位于 Flash 起始地址,由启动代码自动填充。
在 SDK 中,中断服务函数以特定命名规则定义。例如,GPIO A 端口的外部中断处理函数命名为 GPIOA_IRQHandler。当 GPIOA 引脚上发生中断事件时,PFIC 自动跳转至该函数执行。
编程提示 中断服务函数应尽量简短,避免耗时计算和阻塞操作。常见的做法是在 ISR 中设置标志位,在主循环中处理具体逻辑。
CH582F 的 PFIC 支持 4 级中断优先级(0-3),其中 0 为最高优先级。当多个中断同时发生时,高优先级中断将抢先执行;若两个中断优先级相同,则按中断向量号由小到大顺序响应。高优先级中断还可以打断正在执行的低优先级中断服务函数,这就是中断嵌套。
ℹ️ 注意 _CH582F 的 PFIC(Programmable Fast Interrupt Controller)与 ARM 的 NVIC 功能类似,但寄存器接口不同。使用 WCH 官方 SDK 时,通过 PFICSetPriority() 函数设置优先级即可,无需直接操作寄存器。
外部中断是由 GPIO 引脚上的电平变化触发的中断。CH582F 的每个 GPIO 引脚均可独立配置为外部中断源,支持以下四种触发模式:
配置 CH582F GPIO 外部中断需要完成以下步骤:
第一步:配置 GPIO 引脚为输入模式
将目标引脚设置为输入模式,并根据外部电路决定是否需要内部上拉电阻。典型的按键电路中,按键一端接地,另一端接 GPIO,因此需要启用内部上拉。
/* 配置 PA4 为输入模式,启用内部上拉 */
GPIOA_ModeCfg(GPIO_Pin_4, GPIO_ModeIN_PU);
第二步:配置中断触发模式
根据应用需求选择触发边沿。对于按键按下事件,通常使用下降沿触发。
/* PA4 配置为下降沿触发 */
GPIOA_ITModeCfg(GPIO_Pin_4, GPIO_ITMode_FallEdge);
第三步:使能 GPIO 中断与 PFIC 中断
分别使能 GPIO 端口中断和 PFIC 全局中断,最后开启全局中断允许。
第四步:编写中断服务函数
/* GPIOA 中断服务函数 */
__INTERRUPT
attribute((interrupt(“WCH-Interrupt-fast”)))
void GPIOA_IRQHandler(void)
{
if (GPIOA_ReadITFlagBit(GPIO_Pin_4))
{
/* 清除中断标志位(必须) */
GPIOA_ClearITFlagBit(GPIO_Pin_4);
/* 在此处理按键事件 */
button_flag = 1;
}
}
ℹ️ 注意 中断服务函数中必须清除中断标志位,否则退出 ISR 后会立刻再次触发中断,导致程序死循环在 ISR 中。
机械按键在按下和释放的瞬间,触点会发生弹跳(Bounce),产生多次快速的电平跳变。如果不加处理,一次按键操作可能被误识别为多次触发。按键抖动持续时间通常为 5-20 毫秒。
在中断方式下,常用的消抖策略有:
① 定时器延迟消抖:在中断触发后启动一个定时器,延迟 15-20ms 后再读取按键状态,确认为有效按键后才处理。
② 软件计数消抖:在中断中记录时间戳,如果两次中断间隔小于消抖时间阈值则忽略。
以下示例实现了一个完整的外部中断按键控制程序:按下按键(PA4,接按键到地)时,LED(PA5)翻转状态,并包含软件消抖逻辑。
主程序文件 main.c:
#include “CH58x_common.h”
/* 全局变量 */
volatile uint8_t key_pressed = 0; /* 按键按下标志 */
volatile uint32_t last_press_tick = 0;/* 上次按键时间戳 */
#define DEBOUNCE_MS 20 /* 消抖时间阈值 */
/* LED 控制引脚: PA5 */
#define LED_PIN GPIO_Pin_5
/* 系统毫秒计数器(由定时器中断维护) */
extern volatile uint32_t sys_tick_ms;
int main(void)
{
/* 系统时钟初始化(60MHz 主频) */
SetSysClock(CLK_SOURCE_PLL_60MHz);
/* 配置 LED 引脚为推挽输出 */
GPIOA_ModeCfg(LED_PIN, GPIO_ModeOut_PP_20mA);
GPIOA_SetBits(LED_PIN); /* 初始 LED 熄灭(高电平灭) */
/* 配置按键引脚 PA4 输入上拉 */
GPIOA_ModeCfg(GPIO_Pin_4, GPIO_ModeIN_PU);
/* 配置 PA4 下降沿中断 */
GPIOA_ITModeCfg(GPIO_Pin_4, GPIO_ITMode_FallEdge);
GPIOA_InterENCfg(GPIO_Pin_4, ENABLE);
PFIC_EnableIRQ(GPIO_A_IRQn);
/* 初始化 1ms 系统定时器(详见 5.4 节) */
SysTick_Init();
PFIC_EnableInterrupt();
while (1)
{
if (key_pressed)
{
key_pressed = 0;
/* 翻转 LED 状态 */
if (GPIOA_ReadOutPin(LED_PIN))
GPIOA_ResetBits(LED_PIN); /* 点亮 */
else
GPIOA_SetBits(LED_PIN); /* 熄灭 */
}
/* 主循环可执行其他任务 */
}
}
中断服务函数:
/* GPIOA 中断服务函数 */
__INTERRUPT
attribute((interrupt(“WCH-Interrupt-fast”)))
void GPIOA_IRQHandler(void)
{
if (GPIOA_ReadITFlagBit(GPIO_Pin_4))
{
GPIOA_ClearITFlagBit(GPIO_Pin_4);
/* 软件消抖:距上次触发超过 DEBOUNCE_MS 才视为有效 */
uint32_t now = sys_tick_ms;
if ((now - last_press_tick) > DEBOUNCE_MS)
{
last_press_tick = now;
key_pressed = 1;
}
}
}
工程技巧 _在中断服务函数中修改的变量(如 key_pressed 和 sys_tickms)必须用 volatile 关键字声明,防止编译器优化导致主循环读取到过时的寄存器缓存值。
实验 _动手实践:将触发模式改为上升沿触发(GPIO_ITModeRiseEdge),观察按键释放时 LED 是否翻转。再改为双沿触发,分析为何按下和释放各翻转一次会导致 LED 状态看似不变。
定时器中断是通过硬件定时器计数到达设定值时产生的周期性中断。它是嵌入式系统时间管理的基础——操作系统的”心跳”(System Tick)即来源于此。在四旋翼飞控中,PID 控制环路、传感器采样、通信超时判断等功能都依赖于精确定时。
CH582F 内置 4 个通用定时器(TIMER0-TIMER3),每个定时器可独立配置预分频系数和计数周期,支持向上计数、向下计数和中心对齐模式。当计数值到达设定周期时,可触发中断。
以下代码将 TIMER0 配置为 1ms 周期中断,作为系统时间基准。假设系统主频为 60MHz:
/* 全局系统毫秒计数器 */
volatile uint32_t sys_tick_ms = 0;
/*
* 定时器0 初始化:1ms 周期中断
* 系统时钟 = 60MHz
* 预分频 = 60, 计数周期 = 1000
* 中断周期 = 60 × 1000 / 60MHz = 1ms
*/
void SysTick_Init(void)
{
/* 关闭定时器0 */
TMR0_TimerInit(0xFFFF); /* 先写入最大值关闭 */
/* 配置预分频:60分频 */
TMR0_TIConfig(1000); /* 计数周期 = 1000 */
/* 配置定时器0:预分频60, 计数1000次 */
/* 60MHz / 60 / 1000 = 1000Hz = 1ms */
R32_TMR0_CNT_END = 1000; /* 计数终止值 */
R16_TMR0_COUNT = 0; /* 清零当前计数 */
/* 使能定时器0中断 */
TMR0_InterEnable();
PFIC_EnableIRQ(TMR0_IRQn);
/* 启动定时器0 */
TMR0_Enable();
}
定时器中断服务函数:
__INTERRUPT
attribute((interrupt(“WCH-Interrupt-fast”)))
void TMR0_IRQHandler(void)
{
/* 清除定时器中断标志 */
TMR0_ClearITFlag();
/* 系统毫秒计数递增 */
sys_tick_ms++;
}
有了 sys_tick_ms,可以轻松实现非阻塞延迟函数:
/* 非阻塞延迟:记录起始时间,在主循环中判断 */
static inline uint32_t millis(void)
{
return sys_tick_ms;
}
/* 阻塞式延迟(仅用于初始化阶段) */
void delay_ms(uint32_t ms)
{
uint32_t start = sys_tick_ms;
while ((sys_tick_ms - start) < ms);
}
性能建议 _在主循环中应尽量避免使用阻塞式 delayms(),而是采用”时间戳比较”方式实现非阻塞定时,以保证系统响应性。
在复杂系统中,多个中断源同时工作的情况十分常见。合理设置中断优先级是保证系统实时性的关键。CH582F 的 PFIC 提供 4 个优先级等级,通过 PFIC_SetPriority() 函数配置:
/* 设置 IMU 数据就绪中断为最高优先级 */
PFIC_SetPriority(GPIO_A_IRQn, 0);
/* 设置定时器0中断为高优先级 */
PFIC_SetPriority(TMR0_IRQn, 1);
/* 设置 UART1 接收中断为中优先级 */
PFIC_SetPriority(UART1_IRQn, 2);
/* 设置按键中断为低优先级 */
PFIC_SetPriority(GPIO_B_IRQn, 3);
中断嵌套是指在执行某个中断服务函数的过程中,如果发生了更高优先级的中断请求,CPU 会暂停当前的 ISR,转去执行高优先级的 ISR。待高优先级 ISR 执行完毕,再返回继续执行被中断的低优先级 ISR。
CH582F 中断嵌套遵循以下规则:
在四旋翼飞控系统中,典型的中断优先级分配如下:
ℹ️ 注意 设计中断嵌套时,务必保证高优先级 ISR 的执行时间足够短,否则会错过下一次高优先级中断事件,导致系统失控。一般要求最高优先级 ISR 执行时间不超过其周期的 1/4。
实验目的
① 掌握 CH582F 定时器中断的配置方法。
② 理解中断周期与预分频、计数值的关系。
③ 学会使用示波器验证定时器精度。
实验器材
CH582F 飞控开发板 ×1,示波器(或逻辑分析仪)×1,杜邦线若干。
实验步骤
① 配置 TIMER0 产生 1ms 周期中断(参见 5.4.2 节代码)。
② 在定时器中断中维护一个计数器,每 500 次中断翻转一次 LED 引脚电平(即 500ms 亮、500ms 灭,周期 1s)。
③ 用示波器测量 LED 引脚输出波形,验证周期是否精确为 1.000s。
实验参考代码:
实验 _拓展实验:修改 led_togglecnt 的比较值为 100(100ms)和 1000(1s),分别用示波器验证。尝试用两个 LED 设置不同闪烁频率(如 1Hz 和 2Hz),验证定时器中断的精确性。
