HC32 按键中断 — 按下触发回调

HC32F460 按键外部中断实验:PC13 配置为 EXINT13 下降沿触发,开启硬件滤波消抖,中断回调里置标志位供主循环处理。

按键中断:按下触发回调

源码路径:example/HC32F460JEUA/4.key/source/main.c BSP 实现:bsp/ev_hc32f460_lqfp100_v2/ev_hc32f460_lqfp100_v2.c

1. 功能简介

这个例子让单片机能感知按键被按下,而且用的是最专业的方式——外部中断

「中断」这个词听着高深,其实特别好理解。打个比方:

  • 轮询就像你在等快递,每隔十秒就跑到门口看一眼「来了吗?来了吗?」——什么正事都干不了。
  • 中断就像你留了个门铃,你安心在屋里干活,快递到了会按铃通知你,你放下手里的活去开门,开完门回来继续干。

按键中断就是给引脚装了个「门铃」:平时 CPU 该干啥干啥,一旦你按下按键(引脚电平从高变低),硬件立刻打断 CPU,跳到你写好的回调函数里执行,处理完再回到原来的地方继续。

这个例子的完整链路是:按下 PC13 → 触发 EXINT13 下降沿中断 → 执行 Key_Callback → 置标志位 u8GlobalKey = 1 → 主循环发现标志位,做出反应


2. 硬件准备

用 HC32F460JEUA KIT 开发板,板载按键 K1 接在 PC13,无需接线

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        3.3V
         ┌┴┐  芯片内部上拉电阻(代码里 enPullUp = Enable 打开)
         └┬┘
   PC13 ──┤                 没按:读到高电平 (3.3V)
          │                 按下:读到低电平 (0V)
         ─┴─  按键 K1
         GND

如果要自己外接按键(面包板):

1
2
3
   PC13 ──┬── 按键 ── GND
       (内部上拉已开,不用外接电阻)

只要两根线:引脚 → 按键 → GND。上拉靠芯片内部的,省一个电阻。

常见错误:把按键接到 3.3V(按下拉高)。那样「没按」时引脚是悬空的,读到的电平完全随机,程序会乱触发。按键一定接 GND,配合上拉使用。


3. 完整代码

3.1 main.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "hc32_ddl.h"

/* 按键引脚定义 */
#define  KEYIN_PORT    (PortC)
#define  KEYIN0_PIN    (Pin13)

char del = 0;              // 主循环里随便加加,防止空循环被优化掉

int32_t main(void)
{
    BSP_CLK_Init();        // 系统时钟初始化

    BSP_LED_Init();        // LED 初始化(用来显示按键效果)

    /* 关掉 TDI/TDO 调试脚复用,避免和其他功能引脚冲突 */
    PORT_DebugPortSetting(TDI, Disable);
    PORT_DebugPortSetting(TDO_SWO, Disable);

    /* 初始化串口打印(这样中断里可以 printf 观察)*/
    DDL_PrintfInit(BSP_PRINTF_DEVICE, BSP_PRINTF_BAUDRATE, BSP_PRINTF_PortInit);

    /* 按键初始化 —— 关键的一句,内部完成了 EXINT 全部配置 */
    BSP_KEY_Init();

    /* 启动按键扫描 */
    KEYSCAN_Start();

    while(1)
    {
        /* 主循环:可以在这里检查 u8GlobalKey 标志做出反应 */
        /* 例如:
        if (u8GlobalKey == 1)
        {
            BSP_LED_On(LED_RED);
            u8GlobalKey = 0;      // 处理完记得清零
        }
        */
        del++;
    }
}

main.c 看着很短,因为真正的活都在 BSP_KEY_Init() 里干了。想学明白,必须看它的实现。

3.2 BSP 里的按键中断实现(核心)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
uint8_t u8GlobalKey;       // 全局标志位:按键被按下时置 1

/* ---------- 按键中断回调函数 ---------- */
void Key_Callback(void)
{
    /* 先确认真的是 EXINT 通道 13 触发的 */
    if (Set == EXINT_IrqFlgGet(ExtiCh13))
    {
        /* 死等按键松开:既确认了按下,又天然完成了消抖 */
        while (Reset == PORT_GetBit(KEYIN_PORT, KEYIN0_PIN))
        {
            u8GlobalKey = 1;       // 置标志位,通知主循环
        }

        /* 【必须】清中断标志,不清会反复进中断出不来 */
        EXINT_IrqFlgClr(ExtiCh13);
    }
}

/* ---------- 按键初始化 ---------- */
void BSP_KEY_Init(void)
{
    stc_exint_config_t  stcExtiConfig;     // 外部中断配置
    stc_irq_regi_conf_t stcIrqRegiConf;    // 中断注册配置
    stc_port_init_t     stcPortInit;       // 引脚配置

    /* 三个结构体全部清零 */
    MEM_ZERO_STRUCT(stcExtiConfig);
    MEM_ZERO_STRUCT(stcIrqRegiConf);
    MEM_ZERO_STRUCT(stcPortInit);

    /* ===== ① 配置外部中断通道 13 ===== */
    stcExtiConfig.enExitCh   = ExtiCh13;          // 用通道 13(对应 Pin13)
    stcExtiConfig.enFilterEn = Enable;            // 开硬件滤波(消抖!)
    stcExtiConfig.enFltClk   = Pclk3Div8;         // 滤波时钟 = PCLK3/8
    stcExtiConfig.enExtiLvl   = ExIntFallingEdge; // 下降沿触发 = 按下瞬间
    EXINT_Init(&stcExtiConfig);

    /* ===== ② 配置 PC13 引脚 ===== */
    MEM_ZERO_STRUCT(stcPortInit);
    stcPortInit.enExInt  = Enable;   // 【关键】允许该引脚的电平变化送给 EXINT
    stcPortInit.enPullUp = Enable;   // 开内部上拉(没按时是高电平)
    PORT_Init(KEYIN_PORT, KEYIN0_PIN, &stcPortInit);

    /* ===== ③ 注册中断(HC32 四件套)===== */
    stcIrqRegiConf.enIntSrc    = INT_PORT_EIRQ13;   // 中断源:外部中断13
    stcIrqRegiConf.enIRQn      = Int000_IRQn;       // 绑到向量号 0
    stcIrqRegiConf.pfnCallback = &Key_Callback;     // 绑定回调函数
    enIrqRegistration(&stcIrqRegiConf);             // 注册

    NVIC_ClearPendingIRQ(stcIrqRegiConf.enIRQn);    // 清挂起
    NVIC_SetPriority(stcIrqRegiConf.enIRQn, DDL_IRQ_PRIORITY_DEFAULT); // 优先级
    NVIC_EnableIRQ(stcIrqRegiConf.enIRQn);          // 使能 NVIC

    KEYSCAN_Start();     // 启动按键扫描
}

4. 代码分段讲解

4.1 EXINT 通道号 = 引脚号

1
stcExtiConfig.enExitCh = ExtiCh13;    // 因为按键在 PC13

记住这个绑定关系:ExtiChNN 只能用于任意端口的 PinNN。按键在 PC13,所以必须用 ExtiCh13,用别的通道收不到信号。

副作用是:PA13、PB13、PC13 这些「同号引脚」只能有一个用作外部中断,它们共享通道 13。设计电路时要注意别撞车。

4.2 硬件滤波——一行代码搞定消抖

1
2
stcExtiConfig.enFilterEn = Enable;
stcExtiConfig.enFltClk   = Pclk3Div8;

这两行是 HC32 的贴心之处。开启后,EXINT 模块内部的数字滤波器会把宽度太窄的毛刺直接过滤掉,只有稳定持续的电平变化才算真触发。

按键触点弹跳产生的那些几十微秒的尖峰,就在这里被吃掉了。相比在软件里写 delay(10) 消抖,硬件滤波不占 CPU 时间,更优雅。

Pclk3Div8 是滤波采样时钟,分频越大滤波时间越长、能滤掉更宽的毛刺,但响应也越慢。默认值一般够用。

4.3 enExInt = Enable 是命门

1
2
stcPortInit.enExInt  = Enable;   // 千万不能漏
stcPortInit.enPullUp = Enable;

初学者最容易漏的就是 enExInt。它相当于给引脚和 EXINT 模块之间接上一根线。EXINT 那边配得再完美,这根线没接,中断永远不会来。

enPullUp = Enable 打开内部上拉,保证没按键时引脚是稳定的高电平。这样才有「按下 → 高变低 → 下降沿」这个过程。

注意这里没有设置 enPinMode,因为清零后默认就是输入模式(Pin_Mode_In 对应 0),符合按键需求。

4.4 回调里的「等待释放」写法

1
2
3
4
while (Reset == PORT_GetBit(KEYIN_PORT, KEYIN0_PIN))
{
    u8GlobalKey = 1;
}

这个 while 的意思是:只要引脚还是低电平(按键还按着),就一直循环。松手了才跳出来。

好处是天然把整个抖动过程包住了,只会产生一次「按下」事件。缺点是它是阻塞的——你按着不放,CPU 就一直卡在中断里出不来。

这是可以改进的地方。工程实践中,中断回调应当尽量短,标准做法是:

1
2
3
4
5
6
7
8
void Key_Callback(void)
{
    if (Set == EXINT_IrqFlgGet(ExtiCh13))
    {
        u8GlobalKey = 1;          // 只置个标志,立刻返回
        EXINT_IrqFlgClr(ExtiCh13);
    }
}

然后在主循环里检查标志、做消抖确认、执行具体动作。这样中断响应快、不阻塞。

4.5 中断和主循环的协作:标志位模式

这是嵌入式最经典的设计模式:

1
2
3
4
5
中断回调(快):            主循环(慢,可以慢慢干):
  u8GlobalKey = 1;    ──▶     if (u8GlobalKey == 1) {
  清标志,立刻返回                 干正事(点灯/存数据/printf)
                                   u8GlobalKey = 0;   // 处理完清零
                                }

原则:中断里只做「记录事件」这一件事,具体处理留给主循环。

重要提示:这种被中断修改、被主循环读取的变量,应当声明为 volatile

1
volatile uint8_t u8GlobalKey;

否则编译器可能认为「主循环里没人改这个变量」而把它优化成常量,导致 if 永远不成立。SDK 的 BSP 里没加,属于可以改进的点,实际项目里建议加上。

4.6 EXINT_IrqFlgClr 不能忘

1
EXINT_IrqFlgClr(ExtiCh13);

外部中断的头号 bug 就是忘记清标志。硬件中断标志置位后不清,CPU 一退出中断就发现「标志还在,还有中断要处理」,于是立刻又跳进来——程序死循环在中断里,主循环再也执行不到,看起来就像「按一下按键程序就卡死了」。


5. 实验现象

原版例子里主循环的处理代码是注释掉的,所以直接烧录看不到明显现象(只有 del++ 在空转)。

想看到效果,把主循环改成这样:

1
2
3
4
5
6
7
8
while(1)
{
    if (u8GlobalKey == 1)
    {
        BSP_LED_Toggle(LED_RED);   // 每按一次,红灯翻转
        u8GlobalKey = 0;           // 清标志,等下一次按键
    }
}

改完后的现象:每按一次 K1(PC13),红灯亮灭切换一次。按一下亮,再按一下灭。

如果加上串口打印,还能在串口助手里看到每次按键的记录。


6. 常见问题

Q1:按键完全没反应,中断进不去? 按这个顺序查:

  • stcPortInit.enExInt = Enable 写了吗?(头号原因
  • EXINT 通道号和引脚号匹配吗?(PC13 必须用 ExtiCh13
  • enIntSrc 填的是 INT_PORT_EIRQ13 吗?
  • NVIC_EnableIRQ() 调了吗?
  • 按键真的接在 PC13、真的接到 GND 吗?

Q2:按一下按键程序就卡死了?

  • 十有八九是忘了 EXINT_IrqFlgClr(),程序困在中断里反复进出。
  • 也可能是回调里的 while 等待释放,你按着没松手。

Q3:按一下,程序却响应了好几次?

  • 消抖没生效。检查 enFilterEn = Enable 是否配上。
  • 也可以在主循环处理时再加一层软件确认(延时 10ms 后再读一次电平)。

Q4:主循环里的 if (u8GlobalKey == 1) 永远不成立?

  • 变量没加 volatile,被编译器优化了。改成 volatile uint8_t u8GlobalKey;
  • 或者忘了在处理完后 u8GlobalKey = 0,导致只响应第一次(其实是一直成立,看着像没反应)。

Q5:按住不放,LED 疯狂闪?

  • 说明触发方式配成了 ExIntLowLevel(低电平触发),按着就一直触发。改成 ExIntFallingEdge

Q6:中断里能不能用 printf

  • 能用,但不推荐printf 通过串口一个字符一个字符地发,非常慢(几百微秒到几毫秒),会严重拖累中断响应。调试时临时用可以,正式代码里请置标志位、到主循环去打印。

7. 小结

  • 按键中断链路:按下 → 下降沿 → EXINT 触发 → 回调 → 置标志 → 主循环处理
  • ExtiChNN 通道号必须和 PinNN 引脚号对应。
  • 引脚配置里 enExInt = Enable 是命门,漏了必不通。
  • 硬件滤波一行搞定消抖,比软件延时优雅。
  • 回调里必须 EXINT_IrqFlgClr(),否则卡死在中断。
  • 中断里只置标志,重活交给主循环;共享变量加 volatile

8. 拓展应用

按键 + 中断是人机交互的基础,能玩出很多东西:

  • 多按键菜单系统:三四个按键(上/下/确认/返回)配合 LCD,就是一套完整的菜单界面,工业设备的操作面板都是这么做的。
  • 长按 / 短按 / 双击识别:用 ExIntBothEdge 双边沿触发,配合定时器测量按下的时长——短按 < 500ms 是一个功能,长按 > 2s 是另一个功能(比如长按关机)。
  • 组合键:同时按住两个键触发特殊操作(如恢复出厂设置)。
  • 旋转编码器:两个引脚的相位差信号,用外部中断捕捉,就能做出音量旋钮那样的调节器。
  • 外部传感器触发:把按键换成红外对管、霍尔开关、震动传感器——任何输出开关信号的传感器都能用同样的外部中断方式接入,做门磁报警、转速计数、计步器。
  • 低功耗唤醒:外部中断能把睡眠中的 MCU 唤醒。做电池供电的设备时,平时深度睡眠,按一下按键才醒来工作,能把待机功耗降到微安级。
  • 紧急停止:安全相关的输入必须用中断而不是轮询——因为中断响应是微秒级的,绝不会漏掉信号。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计