Funpack5-3 - 基于 MAX32690EVKIT 实现 SW2 按键控制 LED 闪烁
本报告为 Funpack S5 #3 活动 T1 基础题(按键控制 LED)的项目总结,用于提交至电子森林项目页面。报告正文超过 1500 字,涵盖项目介绍、硬件、方案框图、软件流程、关键代码、功能展示、问题解决与心得。
一、项目介绍
本项目基于 Analog Devices(ADI)MAX32690EVKIT 评估套件,使用官方 MaximSDK(MSDK)裸机开发环境,完成 Funpack S5 #3 活动的 T1 基础题:SW2 按键控制红色 LED 闪烁。
实现的核心功能为:系统上电后红色 LED 保持熄灭;第一次有效按下按键 SW2,红色 LED 开始以约 1 Hz 的频率闪烁;再次有效按下 SW2,闪烁停止且 LED 保持熄灭。如此往复,每次按压只触发一次状态切换。
MAX32690 是 ADI 面向可穿戴设备、工业和物联网应用的高性能微控制器,采用 Arm Cortex-M4F 内核,最高主频 120 MHz,集成 3 MB 闪存、1 MB SRAM,并内置 BLE 5.2 无线电。MAX32690EVKIT 评估板将 MCU 的丰富外设以板载外设形式呈现,包括两个用户按键(SW2、SW3)、多颗 LED、USB 转串口、CMSIS-DAP 调试器、音频编解码 MAX9867 及多种扩展接口,非常适合进行嵌入式系统开发学习与原型验证。
本项目选用 T1 基础题并采用 MSDK 裸机路线,不使用操作系统,直接通过寄存器驱动封装完成 GPIO 按键检测、定时器中断和 LED 控制,有助于深入理解 MCU 的中断、定时器与状态机设计。
二、硬件介绍
2.1 硬件平台
主控 : ADI MAX32690(Arm Cortex-M4F @120 MHz,3 MB Flash / 1 MB SRAM)
评估板 : MAX32690EVKIT 评估套件
用户按键 : SW2(P4.0,上拉,按下为低电平)
红色 LED : LED1(P0.14,低电平点亮 active-low)
调试/烧录 : 板载 CMSIS-DAP 调试器 + OpenOCD
串口 : USB 转串口(COM8,115200 8-N-1)
2.2 关键硬件特性
SW2 的特殊性:MAX32690 的 SW2 连接在专用 GPIO4 唤醒端口(P4.0)。该端口在硬件层面只支持双边沿(上升沿与下降沿)唤醒中断,无法直接请求单个下降沿中断。这与普通 GPIO 端口不同,是本项目软件设计需要重点适配的硬件约束。
LED 为低电平有效:LED1 连接在 P0.14,输出低电平时点亮,高电平时熄灭。代码中对应使用 `LED_On`/`LED_Off`/`LED_Toggle` 驱动 API,无需关心电平极性细节。
JP5 跳线:官方 TMR 示例说明,使用 LED1 时需要闭合 JP5(LED1_EN)跳线,确保 LED 供电使能。
三、方案框图
┌─────────────────────────────────────────────┐
│ MAX32690EVKIT │
│ │
┌──────────┐ │ ┌──────────────┐ ┌──────────────────┐ │
│ SW2 │──────┼─▶│ GPIO4 (P4.0)│───▶│ PB 驱动(按键回调) │ │
│ 按键输入 │ │ │ 双边沿中断 │ │ 仅置中断挂起标志 │ │
└──────────┘ │ └──────────────┘ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ 主循环按键状态机 │ │
│ │ (50ms 消抖, 确认 │ │
│ │ 稳定按下/松开) │ │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ 切换闪烁使能状态 │ │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ TMR0 定时器 │───▶│ TMR0 中断处理 │ │
│ │ 连续模式 2Hz │ │ LED_Toggle(LED1) │ │
│ └──────────────┘ └────────┬─────────┘ │
│ ▼ │
┌──────────┐ │ ┌──────────────────┐ │
│ LED1 │◀─────┼────────────────────│ P0.14 (active-low)│ │
│ 红色LED │ │ └──────────────────┘ │
└──────────┘ │ │
└─────────────────────────────────────────────┘
系统由三个中断源驱动:SysTick 提供 1 ms 毫秒计数时基;GPIO4 双边沿中断唤醒按键处理;TMR0 定时器以 2 Hz 频率产生中断用于翻转 LED。所有实时要求不高的逻辑(按键消抖、状态切换)都放在主循环中处理。
四、软件流程
4.1 总体流程
系统上电
│
▼
SystemInit() → Board_Init() 初始化板级资源(MSDK 自动调用)
│
▼
main():
├─ PB_Init() 按键驱动初始化(检查返回值)
├─ LED_Init() LED 驱动初始化(检查返回值)
├─ LED_Off() 初始熄灭 LED
├─ SysTick_Config() 配置 1 ms 系统时基
├─ blink_timer_init() 配置 TMR0 连续模式 2 Hz 中断
├─ PB_Set_Polarity(SW2, 低电平有效)
├─ PB_RegisterCallbackRiseFall() 注册按键回调
└─ 使能按键中断
│
▼
主循环(死循环):
├─ process_button() 按键状态机(消抖处理)
└─ __WFI() 无事件时休眠省电
4.2 按键消抖状态机
由于 GPIO4 只能产生双边沿中断,中断回调无法直接区分按下与松开。本项目将中断回调简化为"仅置位挂起标志",而在主循环中通过四态状态机完成消抖与语义过滤:
状态 | 含义 | 转移条件 |
|---|---|---|
IDLE(空闲) | 等待按键事件 | 收到中断挂起标志 → 禁用按键中断,进入 VERIFY_PRESS |
VERIFY_PRESS(验证按下) | 等待 50 ms 消抖 | 消抖后仍为低电平 → 视为有效按下,切换闪烁,进入 WAIT_RELEASE;否则重新武装中断 |
WAIT_RELEASE(等待松开) | 等待按键释放 | 检测到高电平 → 记录截止时刻,进入 VERIFY_RELEASE |
VERIFY_RELEASE(验证松开) | 等待松开消抖 50 ms | 稳定松开 → 重新武装中断,回到 IDLE;再次变低 → 回到 WAIT_RELEASE |
该设计确保一次完整的"按下-松开"过程只触发一次闪烁状态切换,有效过滤机械抖动与 GPIO4 双沿中断带来的干扰。
五、关键代码
5.1 定时器周期计算与初始化
定时器以 2 Hz 产生中断,每次中断翻转一次 LED 电平,占空比 50%,因此 LED 的完整亮灭周期约为 1 秒,即约 1 Hz 闪烁。
period_ticks = MXC_TMR_GetPeriod(BLINK_TIMER, BLINK_TIMER_CLOCK,
BLINK_TIMER_PRESCALER, BLINK_TOGGLE_HZ);
if (period_ticks == 0U) {
return E_BAD_PARAM;
}
timer_cfg.pres = TMR_PRES_32; /* 预分频 32 */
timer_cfg.mode = TMR_MODE_CONTINUOUS; /* 连续模式 */
timer_cfg.bitMode = TMR_BIT_MODE_32; /* 32 位计数 */
timer_cfg.clock = BLINK_TIMER_CLOCK; /* APB 时钟 */
timer_cfg.cmp_cnt = period_ticks; /* 计数周期 */
error = MXC_TMR_Init(BLINK_TIMER, &timer_cfg, false);
5.2 定时器中断处理
中断函数保持短小,只做清标志和条件翻转;是否翻转由主循环设置的 `g_blink_enabled` 标志控制。
static void blink_timer_handler(void)
{
MXC_TMR_ClearFlags(BLINK_TIMER); /* 先清中断标志 */
if (g_blink_enabled) { /* 仅在使能时翻转 */
LED_Toggle(BLINK_LED);
}
}
5.3 按键回调与状态切换
`toggle_blink_state` 通过关中断保护"读取-修改-写入"临界区,避免与定时器中断并发导致状态不一致。
static void button_callback(void *callback_data)
{
(void)callback_data;
g_button_irq_pending = 1U; /* 中断中只置标志 */
}
static void toggle_blink_state(void)
{
__disable_irq(); /* 进入临界区 */
enable = !g_blink_enabled;
MXC_TMR_Stop(BLINK_TIMER); /* 停表清零 */
MXC_TMR_SetCount(BLINK_TIMER, 0U);
MXC_TMR_ClearFlags(BLINK_TIMER);
if (enable) {
LED_On(BLINK_LED);
g_blink_enabled = true;
MXC_TMR_Start(BLINK_TIMER);
} else {
g_blink_enabled = false;
LED_Off(BLINK_LED);
}
__enable_irq();
}
5.4 溢出安全的截止时间比较
毫秒计数器为 32 位无符号数,运行约 49.7 天后会回绕。该函数利用无符号减法回绕与有符号解释,在计数器溢出时依然能正确判断截止时间是否到达。
static bool deadline_reached(uint32_t now, uint32_t deadline)
{
return (int32_t)(now - deadline) >= 0;
}
六、功能展示

按下开始闪烁

按下结束闪烁

串口打印信息
七、问题解决
7.1 GPIO4 只能双边沿中断
问题:SW2 位于 MAX32690 专用 GPIO4 唤醒端口,硬件只支持双边沿中断,普通 `PB_RegisterCallback()` 尝试请求下降沿但底层驱动未向上层传播 `E_NOT_SUPPORTED` 失败。
处理:改用与硬件能力匹配的 `PB_RegisterCallbackRiseFall()` 注册双边沿回调,回调内仅置位挂起标志;在主循环状态机中通过 50 ms 消抖只接受"稳定低电平按下",并在确认稳定松开后才重新武装中断,实现了等效的"下降沿语义"。
7.2 按键机械抖动
问题:机械按键按下和松开瞬间会产生毫秒级的触点抖动,若不处理会导致一次按压被识别为多次。
处理:按下和释放方向各设置 50 ms 消抖窗口,使用 SysTick 1 ms 时基非阻塞计时,消抖期间不阻塞主循环。
7.3 工具链 PATH 问题
问题:直接以绝对路径启动 `make.exe` 不会自动让其子进程找到 GCC/OpenOCD;若 PATH 未包含 MSDK 工具目录,会出现 `arm-none-eabi-gcc`/`openocd.exe` 找不到的错误。MSDK 自带的 `setenv.bat` 会调用更新器,存在改写 SDK 安装的风险。
处理:不运行 `setenv.bat`,改为显式将 GCC、Make、OpenOCD 目录前置到当前 PowerShell PATH;并添加项目本地 `.vscode/` 一键工作流(构建、烧录运行、Cortex-Debug 调试),固定 SDK 与工具路径,无需手工配置环境。
7.4 闪烁频率与定时器计算
问题:需要产生精确的约 1 Hz 闪烁,直接手算定时器重载值容易出错且不便移植。
处理:使用 `MXC_TMR_GetPeriod()` 由时钟频率、分频系数与目标频率统一计算计数周期,参数化定义(`BLINK_TOGGLE_HZ = 2`、`BLINK_TIMER_PRESCALER = 32`),便于调整。
八、心得与总结
通过本项目,我完整走通了"需求分析 → 硬件资源梳理 → 驱动适配 → 状态机设计 → 构建烧录 → 实机验证"的嵌入式开发全流程。
在软件设计上收获最大的是**中断上下文的职责划分**:中断服务函数保持极短(只置标志、清标志、翻转 LED),把需要耗时的逻辑(消抖、状态转移)全部放到主循环,配合 `__WFI()` 实现无事件时的低功耗休眠,这是嵌入式实时系统设计的通用范式。其次是对**硬件约束驱动的设计**印象深刻。GPIO4 只支持双边沿中断这一硬件事实,直接决定了按键软件不能依赖硬件下降沿,而必须用软件状态机补足语义。这提醒我:好的嵌入式设计一定是先读懂硬件手册,再设计软件,而不是反过来。32 位毫秒计数器回绕问题的处理也让我体会到嵌入式系统里"时间"的严谨性——一个看似简单的截止时间比较,在考虑溢出后需要用无符号减法回绕技巧,否则长时间运行的设备会出现隐蔽的偶发故障。
本项目代码量不大,但涉及按键消抖、定时器中断、状态机、临界区保护、溢出安全等多个嵌入式核心知识点,是一次非常扎实的实战训练。后续如果继续深入,可以在本基础上扩展 T1 进阶题(长按平滑调频),或进一步探索 T3 音频频谱分析等更有挑战性的方向。