基于MAX32690EVKIT评估套件上手测试——非阻塞状态机按键控制实践报告
一、项目介绍
本次实践基于 ADI 公司的 MAX32690EVKIT 评估套件,摒弃传统延时或简单轮询架构,采用”事件驱动 + 有限状态机(FSM)“的非阻塞设计模式,构建一套可靠、可扩展的按键控制逻辑,避免CPU 资源浪费、系统实时性差,且极易受机械按键抖动等的影响。
核心功能需求:1. 单次短按 SW2 按键,红色 LED(LED0,即 P0.14 / D1 红色)开始以 1Hz 频率闪烁。 2. 再次短按 SW2 按键,LED 停止闪烁并保持熄灭。 3. 全程采用非阻塞设计,确保系统在处理按键消抖和 LED 闪烁时,主循环依然保持全速运转。
1.1 硬件介绍
本项目基于MAX32690 评估套件实现。MAX32690 评估套件提供了一个平台,用于评估 MAX32690 微控制器的功能,该微控制器是一款先进的片内系统 (SoC)。它采用了 Arm Cortex-M4F CPU,可高效计算复杂的功能和算法,并采用了最新一代的 Bluetooth 5 低功耗(蓝牙 LE)射频,专为可穿戴和便携式健身设备、便携式和可穿戴式医疗设备、工业传感器/网络、物联网 (IoT) 和资产追踪而设计。
1.2 板卡功能优势和特点
MAX32690EVKIT评估套件基于Arm Cortex-M4F与BLE 5.2 SoC,板载HyperRAM、立体声音频编解码器及1.45英寸TFT彩屏,提供SMA蓝牙天线、CAN 2.0及全引出GPIO等丰富接口,并配备完善的多级电源管理(3.3V、1.8V 和 1.1V LDO)与功耗测量跳线。
二、项目功能实现
本项目实现任务1按键控制LED闪烁:使用 MAX32690EVKIT 的板载按键功能,实现以下逻辑:单次按键 → LED开始闪烁;再次按键 → LED停止闪烁
功能要求:正确初始化 GPIO 中断;LED 控制逻辑清晰,不出现误触发;闪烁频率约 1Hz。
2.1 任务分析
根据板上资源,这个任务按键选择使用SW2(connected to GPIO P4.0),LED选择使用LED0(D1 (red) is connected to GPIO P0.14),则需要:
GPIO P4.0为输入模式,默认上拉。
GPIO P0.14为输出模式。


2.2 设计思路
正确初始化 GPIO 中断,就要求使用中断功能。注意不能在中断中使用print输出,否则会有延迟不能快进快出。
LED 控制逻辑清晰,不出现误触发,就要采取按键防抖措施,最常用使用时间戳,使用心跳,在中断中记录时间戳。
闪烁频率约 1Hz,可使用计数器完成频率设定。
本系统设计将硬件底层与业务逻辑彻底解耦,严格遵循”中断极简,时基统一,主循环状态机非阻塞”架构:
- 硬件中断极简原则:GPIO 中断回调函数中严禁执行任何耗时操作,仅负责将事件标志位(btn_flag)清零,实现”快进快出”,避免阻塞其他中断。
- 全局时基与心跳:利用 ARM Cortex-M4 内核自带的 SysTick 定时器,配置为 10ms 触发一次中断,提供全局毫秒级时间戳(sys_ms)。
- 非阻塞状态机驱动:主循环(while(1))全速运行,通过比对时间戳完成所有时序逻辑。系统被划分为4个工作状态:
◦ BTN_IDLE(空闲态):等待按键事件触发。
◦ BTN_DEBOUNCE(消抖态):过滤机械抖动,确认有效按压并翻转 LED 意图。
◦ BTN_WAIT_RELEASE(等待松开态):锁定按键,防止长按或松手抖动引发状态死锁。
◦ BTN_RUN(运行态):执行 LED 1Hz 闪烁及关闭逻辑。
2.3 上手测试过程
2.3.1 准备硬件和软件环境
2.3.1.1 硬件连接:
根据套件说明,开发板只需一根microUSB数据线就可以快速上手了:
MAX32690 芯片内部已经预先烧录了演示代码。只需将附带的 USB 线插入 CN2 接口为开发板供电(将 USB 线的另一端连接到电脑或电源适配器)即可运行这个演示程序。检查 4V5 蓝色 LED 指示灯(D5)以及 3V3 (DS1)、1V8 (DS3)、1V1 (DS4) 和 BLE LDO (DS5) 这几个绿色 LED 指示灯是否均已点亮,如果亮起,说明这些电压供电轨(voltage rails)都已正常上电。这也说明板子上不同的电压域(比如 4.5V、3.3V、1.8V 和 1.1V)都已正常供电。
如果要烧录示例程序,还需要使用套件自带的MAX32625PICO 调试器。要连接该功能的带状线MAX32625PICO 调试器至 MAX32690 EV 套件的 J3 接口,通过SWD 接口进行烧录和调试。此外,MAX32625PICO还用作UART桥,无需额外USB即可实现。
2.3.1.2 软件环境搭建:
安装CodeFusion Studio (CFS)
在 VS Code 中安装codefusion studio扩展插件
CodeFusion Studio 扩展依赖于其他 VS Code 扩展(包括 Microsoft C/C++、Cortex-Debug 以及 Zephelin Trace Viewer 等)来实现调试和开发功能。如果缺少某些依赖扩展,这些扩展将会自动被安装。
如果首次安装 CodeFusion Studio,或者刚升级到新版本,可能会收到确认 CFS SDK 的路径是否正确设置的提示。 如果还没有安装SDK,就按照提示下载并安装SDK,当文件夹缺失、被移动或位置错误时,就会出现这些提示信息,那么需要设置CFS SDK的安装路径。

探索 CodeFusion Studio 所提供的功能。

使用工作区创建向导创建新的工作区。创建后,工作区文件将显示在文件资源管理器中,并且系统规划器配置工具仪表板将打开。


System Planner Configuration Tools提供了一个仪表板,轻松配置引脚多路复用、时钟设置和片上系统(SoC)资源。可以打开配置文件.cfsconfig以打开配置工具。
在设置所有配置(如引脚多路复用、时钟设置和内存分配)之后,可以为应用程序生成必要的源文件。

注意:如果现有项目尚未配置为CodeFusion Studio工作区,需要等待“配置”对话框完成后再继续。
三、软件设计及功能实现
3.1 代码架构说明
整体采用 三层(中断极简、全局时基、状态机驱动) 架构,结合了硬件SysTick 心跳 + GPIO 中断分层与软件状态机,实现了按键防抖、LED闪烁控制以及串口日志的安全输出。代码严格遵循了中断快进快出原则,构建了轻量的底层支撑:
- SysTick 心跳(纯时基):SysTick_Handler 中断服务函数中,仅有一行 sys_ms += TICK_MS。它不处理任何业务,只为整个系统提供全局、无阻塞的时间基准。
- GPIO 中断(纯通知):button_isr 回调函数中,仅执行 btn_flag = 0。当按键按下触发硬件中断时,仅负责向主循环通知,避免了在 ISR 中执行耗时操作(如printf 或延时)导致的系统崩溃风险。
按下(中断清零flag)
│
┌──────────▼──────────┐
│ BTN_DEBOUNCE │ 等待 30ms 消抖
│ 确认按下后翻转 │ led_blinking ^= 1
│ LED 状态 │
└──────────┬──────────┘
│ 30ms 到
┌──────────▼──────────┐
│ BTN_WAIT_RELEASE │ 等待物理松手
│ 松手后恢复 flag │ 防止长按重复触发
└──────────┬──────────┘
│ 松手
┌──────────▼──────────┐
│ BTN_RUN │◄──┐
│ ·检测新按键→DEBOUNCE│ │ 每 500ms
│ ·非阻塞 LED 翻转 │───┘ LED_Toggle
└─────────────────────┘
┌─────────────────────┐
│ BTN_IDLE │ 初始/异常恢复
│ 等待按下事件 │
└─────────────────────┘
3.2 状态流转详解
主循环中的 switch(st) 将按键与 LED 的控制逻辑划分为四个互斥的业务状态,形成了一个严密的逻辑闭环:
- BTN_IDLE(空闲态):系统处于待命状态。当检测到按键被按下(pressed && !btn_flag)时,记录当前时间戳(t0 = now),并无缝切入消抖状态。
- BTN_DEBOUNCE(消抖态):通过 if ((now - t0) >= DEBOUNCE_MS) 进行非阻塞时间差比对。当 30ms 消抖期满,系统立即翻转 LED 意图标志(blinking = !blinking),并根据意图执行开/关灯动作,随后切入等待松手状态。
- BTN_WAIT_RELEASE(等待松手态):这是防死锁的关键。系统在此状态死等按键物理释放(!pressed)。只有确认用户手指离开按键,才会重置事件标志(btn_flag = 1)并进入运行态。这彻底杜绝了”长按不松手”或”松手弹片抖动”引发的状态机卡死。
- BTN_RUN(运行态):系统在此状态并行处理两件事。一是监听新的按键事件以准备关闭 LED;二是通过 (now - t_led) >= BLINK_HALF_PERIOD_MS 实现 1Hz 的非阻塞 LED 翻转。
3.3 关键变量与函数解析
核心变量
• static volatile uint32_t sys_ms = 0;
作用:系统的全局毫秒级时间戳(心跳)。
解析:volatile 关键字告诉编译器这个变量随时会被硬件中断修改,必须每次都从内存中读取,防止被优化掉。它是整个系统非阻塞设计的基石,所有的延时(消抖、闪烁)都依赖它。
• static volatile uint8_t btn_flag = 1;
作用:按键事件的”门铃”标志位。
解析:初始化为 1(表示无事件)。当按键按下触发中断时,ISR 将其清零(0);当主循环处理完按键并确认松手后,再将其置为 1。它实现了中断与主循环之间的异步通信。
• btn_state_t st
作用:记录当前系统处于哪个业务阶段。
解析:它的取值被严格限制在 BTN_IDLE、BTN_DEBOUNCE、BTN_WAIT_RELEASE、BTN_RUN 四个状态中。主循环的 switch(st) 就是根据这个变量的值来决定当前该干什么。
• uint32_t t0
作用:记录”按键刚被按下”那一刻的系统时间。
解析:在进入 BTN_DEBOUNCE 状态时,t0 被赋值为当前的 sys_ms。随后通过 (now - t0) >= DEBOUNCE_MS 来判断 30ms 的消抖时间是否过去。
• uint32_t t_led
作用:记录”LED 上一次翻转”那一刻的系统时间。
解析:在 BTN_RUN 状态下,通过 (now - t_led) >= BLINK_HALF_PERIOD_MS 来判断是否该翻转 LED 了。每次翻转后,t_led 会被更新为当前时间,从而实现非阻塞的 1Hz 闪烁。
• uint8_t blinking
作用:LED 的”意图”标志(0=灭,1=闪烁)。
解析:这是解耦设计的核心。在 BTN_DEBOUNCE 阶段,系统只负责执行 blinking = !blinking;(翻转意图),而不管 LED 到底怎么闪。LED 的实际翻转动作完全交给 BTN_RUN 状态根据 blinking 的值和时间戳来驱动。
核心函数
• void SysTick_Handler(void)
作用:系统心跳中断服务函数。
解析:每 10ms 触发一次,内部仅执行 sys_ms += TICK_MS;。它绝对不包含任何业务逻辑,保证了系统时基的绝对精准和 ISR 的”快进快出”。
• static void button_isr(void *pb)
作用:GPIO 硬件中断回调函数。
解析:当 SW2 被按下(产生下降沿)时由硬件触发。内部仅执行 btn_flag = 0;。它的作用仅仅是”打铃”,通知主循环”有按键动作,请去检查”。
• uint8_t pressed = PB_Get(0);
作用:实时读取 SW2 的物理电平状态。
解析:在主循环的每一次迭代中都会执行。它不依赖中断,而是主动去”看”按键现在的真实状态。这是解决”按键死锁”和实现”松手检测”的最关键依据。
4、变量与函数的协同工作流程
- 按下按键:button_isr 将 btn_flag 清零。
- 主循环捕获:pressed 读到 1,且 btn_flag 为 0,状态机从 BTN_IDLE 跳入 BTN_DEBOUNCE,并记录 t0。
- 消抖与翻转:经过 30ms,blinking 被翻转,状态机跳入 BTN_WAIT_RELEASE。
- 等待松手:主循环持续读取 pressed,直到用户松手(pressed 变为 0),btn_flag 被重新置 1,状态机跳入 BTN_RUN。
- 非阻塞闪烁:在 BTN_RUN 中,状态机一边通过 t_led 控制 LED 以 1Hz 闪烁,一边继续监听 pressed 和 btn_flag,随时准备响应下一次按键。
5、核心机制亮点
• 非阻塞时差算法:代码全程没有使用 delay() 等挂起 CPU 的指令。所有的时间控制(30ms 消抖、500ms 闪烁)均通过”当前时间 - 记录时间 >= 目标时间”来实现。这意味着 CPU 在主循环中以最高频率全速空转,随时可以响应外部突发事件。
• 意图与动作分离:在 BTN_DEBOUNCE 阶段,代码只负责翻转逻辑变量 blinking,而不直接控制 LED 的闪烁频率。LED 的闪烁完全交由 BTN_RUN 状态根据 blinking 的值和时间戳来驱动。这种设计使得状态机逻辑极其清晰,未来若要扩展”呼吸灯”或”双击”功能,只需增加新的状态分支即可。
• 双重检测保护机制:代码不仅在 BTN_WAIT_RELEASE 状态中检测松手,还在 while(1) 主循环的最末尾加入了 if (!pressed) { btn_flag = 1; } 的全局逻辑。这确保了无论状态机处于何种异常状态,只要按键松开,事件标志都会被正确复位,为下一次有效按压做好了准备。
6、完整代码如下:
/*
* 按键控制 LED 闪烁 — 状态机非阻塞实现
*
* 硬件:
* SW2 = P4.0, 低电平有效, 内部上拉 (BSP Pushbutton 0)
* LED0 = P0.14 (D1 红色), 低电平点亮
*
* 功能:
* 短按 SW2 → LED 开始 1Hz 闪烁
* 再按 SW2 → LED 停止闪烁
*
* 架构:
* GPIO 中断 (下降沿) → 仅设置事件标志, 不做业务逻辑
* SysTick (10ms) → 仅提供 ms 时基, 不做业务逻辑
* 主循环 → 状态机处理按键 + 非阻塞 LED 闪烁
*/
#include <stdio.h>
#include <stdint.h>
#include "mxc_device.h"
#include "led.h"
#include "pb.h"
#include "board.h"
#include "soc_init.h"
#include "core_cm4.h"
/* ================================================================
* 宏定义
* ================================================================ */
#define TICK_MS 10
#define DEBOUNCE_MS 30
#define BLINK_HALF_PERIOD_MS 500
/* ================================================================
* 状态机定义
* ================================================================ */
typedef enum {
BTN_IDLE,
BTN_DEBOUNCE,
BTN_WAIT_RELEASE,
BTN_RUN
} btn_state_t;
/* ================================================================
* 全局变量
* ================================================================ */
static volatile uint32_t sys_ms = 0;
static volatile uint8_t btn_flag = 1;
/* ================================================================
* SysTick ISR — 仅累加毫秒计数
* ================================================================ */
void SysTick_Handler(void)
{
sys_ms += TICK_MS;
}
/* ================================================================
* GPIO 中断回调 — 仅清零标志, 通知主循环
* ================================================================ */
static void button_isr(void *pb)
{
btn_flag = 0;
}
/* ================================================================
* 主函数
* ================================================================ */
int main(void)
{
PinInit();
PeripheralInit();
LED_Off(LED_RED);
PB_Init();
PB_RegisterCallback(0, button_isr);
SysTick_Config(SystemCoreClock / (1000 / TICK_MS));
printf("\r\n=== LED Blink Toggle (State Machine) ===\r\n");
printf("Press SW2 to start/stop. Status: LED OFF\r\n");
btn_state_t st = BTN_IDLE;
uint32_t t0 = 0;
uint32_t t_led = 0;
uint8_t blinking = 0;
while (1) {
uint32_t now = sys_ms;
uint8_t pressed = PB_Get(0);
switch (st) {
case BTN_IDLE:
if (pressed && !btn_flag) {
st = BTN_DEBOUNCE;
t0 = now;
}
break;
case BTN_DEBOUNCE:
if ((now - t0) >= DEBOUNCE_MS) {
blinking = !blinking;
if (blinking) {
LED_On(LED_RED);
t_led = now;
printf("Status: LED BLINKING (1Hz)\r\n");
} else {
LED_Off(LED_RED);
printf("Status: LED OFF\r\n");
}
st = BTN_WAIT_RELEASE;
}
break;
case BTN_WAIT_RELEASE:
if (!pressed) {
btn_flag = 1;
st = BTN_RUN;
}
break;
case BTN_RUN:
if (pressed && !btn_flag) {
st = BTN_DEBOUNCE;
t0 = now;
} else if (blinking && (now - t_led) >= BLINK_HALF_PERIOD_MS) {
LED_Toggle(LED_RED);
t_led = now;
}
break;
default:
st = BTN_IDLE;
break;
}
if (!pressed) {
btn_flag = 1;
}
}
}
四、功能展示
软件设计好后,需要通过附带的MAX32625PICO 调试器接到SWD 接口进行烧录和调试。烧录成功后就可看到效果。经过多轮编译与真机烧录测试,系统表现稳定: - 响应零延迟:由于主循环无任何阻塞延时,按键响应极其迅速。 - 逻辑零误触:在连续快速按压、长按不松手等操作下,LED 均能按照”按一次闪烁,再按一次熄灭”的逻辑运行,串口终端准确打印状态切换信息。

五、总结
5.1 遇到的问题
- CodeFusion Studio 扩展对SDK兼容性有要求,要选择 CFS 扩展版本相匹配的 SDK。SDK 的较小版本必须与扩展程序的较小版本(版本号第二个小数点前)相同或更高。例如,版本 2.1.x 的扩展程序需要使用 2.1.x 或更高版本的 SDK,而无法与 2.0.x 或任何 1.x.x 版本的 SDK 协同工作。而补丁版本(版本号第二个小数点后)的更新并不会影响软件的兼容性。例如,扩展模块 2.2.1 和开发工具包 2.2.0 被认为是兼容的。如果继续使用旧版本的 SDK,需要先关闭 CFS 扩展功能的自动更新,并选择“安装特定版本”来安装想用的旧版本。
- 首先遇到的一个硬件问题,不知为什么我这个板子复位按键不灵敏,经常没有反应,反复按几次又能用,可能是个体问题。临时解决办法是用金属镊子或针触发按键左上角引脚直接导地。
- 遇到的最大的问题是调试器突然出现无法正常工作,错误 Remote communication error. Target disconnected: could not peek into pipe (error 109)排查了很长,加上复位键异常几乎要放弃了。后来网上查了一下表明 GDB 在与 OpenOCD 调试服务器进行通信时,底层管道(pipe)连接意外断开了。这通常与调试器硬件连接、OpenOCD 配置或系统环境有关。
排除了硬件连接问题后,尝试在终端中手动运行该 OpenOCD 命令,发现是命令路径错误,手动找到正确的命令路径后,成功识别到了目标芯片的 DPIDR (0x2ba01477)。这说明硬件连接和驱动都是正常的。
但是,在尝试读取芯片内部寄存器时,出现了关键错误:Error: Failed to read memory at 0xe000ed04
Error: [max32xxx.cpu] Examination failed
xe000ed04 是 ARM Cortex-M 内核的 AIRCR (Application Interrupt and Reset Control Register) 寄存器。读取这个寄存器失败,通常意味着 OpenOCD 无法与芯片内核进行正常的通信。网上搜索极有可能(最常见)是之前烧录的程序让芯片进入了深度睡眠(Deep Sleep),或者开启了读保护(RDP),调试接口会被禁用,导致无法读取寄存器。这是一个已知的调试器行为/小 Bug:在使用 OpenOCD 时,当 GDB 执行 detach 命令断开连接时,OpenOCD 默认会停止目标芯片的运行,而不是让它继续跑起来。这导致 GDB 和 OpenOCD 之间的管道状态出现异常,最终引发管道断开错误 (error 109)。分析可能是之前烧录的程序让 MAX32690 进入了深度睡眠(Deep Sleep)或低功耗模式,芯片会关闭内核时钟,导致调试接口无法访问 CPU 寄存器。 - 解决办法:在启动 OpenOCD 时,按住目标板上的硬件复位按键(RESET)不要松手,然后执行 OpenOCD 命令。等看到 Listening on port 3333 for gdb connections 后,再松开复位键。这可以强制芯片在启动瞬间被调试器捕获。关键操作——在 5 秒内移除短接:执行完 monitor reset halt 后,必须在 5 秒内移除 RSTN 到 GND 的短接(或松开复位按键)。这样芯片会从复位状态释放,但调试器已经抢先 halt 住了内核,睡眠代码来不及执行。
- 由于我这个板子的复位键有问题,在最开始的时候就尝试使用该办法,但由于无效而转到排查其它原因,绕了一圈最后发现复位键也有问题,用按键左上角引脚直接导地方法复位,再用上面办法才解锁了调试器。
5.2 心得体会
本以为会很容易上手,谁知一开始就选择困难了,开发环境搭建中各种各样问题,尝试先在UOS系统中安装,发现不好用,只能转在win系统中使用了,再加上复位键异常加上调试器突然出现无法正常工作,本以为自身问题打算放弃。最后还是通过AI和实践找到了问题原因,但时间原因只能先简单的将任务简化到初始化外设和点灯。
此次收获主要一是首次使用了CodeFusion Studio还是比较好用的,比如它无缝集成与便捷安装,作为 VS Code 扩展只需搜索并安装 CodeFusion Studio 扩展即可使用;可视化资源分配:提供类似 STM32CubeMX 的图形化配置界面,开发者可以直观地配置引脚、时钟、功耗模式和中间件,减少手动设置误差;代码自动生成:配置工具能够自动生成高质量、可直接用于生产的源代码。二是,利用时间戳替代延时函数,是构建实时可靠嵌入式软件的成熟方法。这套非阻塞状态机架构不仅完美解决了基本任务的按键控制需求,还为高级交互功能打下了坚实的底层基础。后续它的端到端的嵌入式 AI 工作流、异构多核调试与诊断、Zephyr RTOS 模块化框架更值得尝试一下。
再次感谢电子森林《Funpack》 系列活动,接触到新的产品生态体系,拓展了视野,理论结合实践,提高了动手能力。