系列回顾:前两篇分别调通了 BLE 通信链路和墨水屏 LVGL 显示。本篇是终篇,聚焦业务逻辑层的实现:设计一个按键驱动的阈值设置状态机,实现 CPU/内存超阈值时的屏幕告警与蜂鸣器联动,并把所有模块串联起来完成项目闭环。
最终项目效果

一、整体业务逻辑回顾
在正式写状态机之前,先梳理清楚整个系统的业务需求:
┌───────────────────────────────┐ │ 业务需求 │ │ │ │ 1. 实时显示 CPU / MEM / GPU 使用率(来自 BLE) │ │ 2. 可设置 CPU 和内存的告警阈值 │ │ 3. 超阈值时屏幕标注告警 + 蜂鸣器响 │ │ 4. 通过板载 4 个按键调整阈值和开关告警 │ └───────────────────────────────┘
四个按键的功能分配:
┌──────────────────────────┐ │ 按键 │ GPIO │ 功能 │ ├──────────────────────────┤ │ BTN0 │ P1.13 │ 告警总开关 (任意状态下有效)│ │ BTN1 │ P1.09 │ 模式切换 (WORKING→CPU→MEM→循环) │ │ BTN2 │ P1.08 │ 阈值 -5% │ │ BTN3 │ P0.04 │ 阈值 +5% │ └──────────────────────────┘
二、状态机设计
2.1 状态定义
系统共有三个状态,用枚举表达:
typedef enum {
FSM_STATE_WORKING = 0, // 正常监控模式
FSM_STATE_SET_CPU, // 调整 CPU 阈值模式
FSM_STATE_SET_MEM, // 调整 MEM 阈值模式
} fsm_state_t;2.2 状态转换图这是整个状态机最核心的设计图:

状态转换规则一句话总结:
BTN1 驱动状态向前切换,SET_MEM 时再按 BTN1 回到 WORKING
BTN2/BTN3 只在 SET_CPU / SET_MEM 状态下调整对应阈值,WORKING 状态下忽略
BTN0 在任何状态下都能切换告警总开关,不受当前状态影响
2.3 整体的按键事件处理流程

// settings_fsm.h
typedef enum {
BTN_EVT_MODE = 0, // BTN1:状态切换
BTN_EVT_INC, // BTN3:增加阈值
BTN_EVT_DEC, // BTN2:减少阈值
BTN_EVT_ALERT, // BTN0:告警开关
} btn_evt_t;三、状态机实现
3.1 状态机内部数据
// settings_fsm.c static fsm_state_t s_state; static uint8_t s_cpu_threshold; // CPU 告警阈值 (5~100%) static uint8_t s_mem_threshold; // MEM 告警阈值 (5~100%) static bool s_alert_enabled; // 告警总开关 static alert_toggle_cb_t s_alert_cb; // 告警开关变化回调 #define THRESHOLD_MIN 5U #define THRESHOLD_MAX 100U #define THRESHOLD_STEP 5U3.2 阈值调整辅助函数
// 带边界钳位的阈值步进
static uint8_t clamp_step(uint8_t cur, int8_t delta)
{
int16_t val = (int16_t)cur + delta;
if (val < (int16_t)THRESHOLD_MIN) return THRESHOLD_MIN;
if (val > (int16_t)THRESHOLD_MAX) return THRESHOLD_MAX;
return (uint8_t)val;
}3.3 各状态事件处理函数每个状态对应一个独立的 handler 函数,逻辑清晰,互不干扰:
// WORKING 状态:只响应 BTN1(模式切换),忽略 INC/DEC
static void handle_working(btn_evt_t evt)
{
switch (evt) {
case BTN_EVT_MODE:
transition(FSM_STATE_SET_CPU);
break;
case BTN_EVT_INC:
case BTN_EVT_DEC:
LOG_DBG("[FSM] INC/DEC ignored in WORKING state");
break;
default:
break;
}
}
// SET_CPU 状态:BTN3/BTN2 调整 CPU 阈值,BTN1 进入下一状态
static void handle_set_cpu(btn_evt_t evt)
{
switch (evt) {
case BTN_EVT_INC:
s_cpu_threshold = clamp_step(s_cpu_threshold, +THRESHOLD_STEP);
LOG_INF("[FSM] CPU threshold -> %d%%", s_cpu_threshold);
break;
case BTN_EVT_DEC:
s_cpu_threshold = clamp_step(s_cpu_threshold, -THRESHOLD_STEP);
LOG_INF("[FSM] CPU threshold -> %d%%", s_cpu_threshold);
break;
case BTN_EVT_MODE:
transition(FSM_STATE_SET_MEM);
break;
default:
break;
}
}
// SET_MEM 状态:BTN3/BTN2 调整 MEM 阈值,BTN1 返回 WORKING
static void handle_set_mem(btn_evt_t evt)
{
switch (evt) {
case BTN_EVT_INC:
s_mem_threshold = clamp_step(s_mem_threshold, +THRESHOLD_STEP);
LOG_INF("[FSM] MEM threshold -> %d%%", s_mem_threshold);
break;
case BTN_EVT_DEC:
s_mem_threshold = clamp_step(s_mem_threshold, -THRESHOLD_STEP);
LOG_INF("[FSM] MEM threshold -> %d%%", s_mem_threshold);
break;
case BTN_EVT_MODE:
transition(FSM_STATE_WORKING);
break;
default:
break;
}
}3.4 总入口:事件分发void settings_fsm_handle(btn_evt_t evt)
{
// BTN0 在任意状态下生效,优先处理
if (evt == BTN_EVT_ALERT) {
s_alert_enabled = !s_alert_enabled;
LOG_INF("[FSM] Alert -> %s",
s_alert_enabled ? "ON" : "OFF");
if (s_alert_cb) {
s_alert_cb(s_alert_enabled); // 通知上层(主循环)
}
return;
}
// 其余事件按当前状态路由
switch (s_state) {
case FSM_STATE_WORKING: handle_working(evt); break;
case FSM_STATE_SET_CPU: handle_set_cpu(evt); break;
case FSM_STATE_SET_MEM: handle_set_mem(evt); break;
default: break;
}
}3.5 状态转换函数static void transition(fsm_state_t next)
{
LOG_INF("[FSM] %s -> %s",
state_name(s_state), state_name(next));
s_state = next;
// 进入新状态时打印当前配置,便于调试
switch (next) {
case FSM_STATE_WORKING:
LOG_INF("[FSM] Working | CPU=%d%% MEM=%d%% Alert=%s",
s_cpu_threshold, s_mem_threshold,
s_alert_enabled ? "ON" : "OFF");
break;
case FSM_STATE_SET_CPU:
LOG_INF("[FSM] Set CPU | current=%d%% "
"BTN3=+5 BTN2=-5 BTN1=next",
s_cpu_threshold);
break;
case FSM_STATE_SET_MEM:
LOG_INF("[FSM] Set MEM | current=%d%% "
"BTN3=+5 BTN2=-5 BTN1=confirm",
s_mem_threshold);
break;
}
}四、按键驱动
按键驱动采用 ISR + 延迟工作队列 的两段式消抖方案,不阻塞任何线程:
GPIO 触发 ISR │ │ 记录时间戳,提交延迟工作(50ms 后执行) ▼ k_work_delayable(系统工作队列) │ │ 50ms 后再次确认引脚状态(二次消抖) ▼ 调用 btn_event_cb_t 回调 │ ▼ settings_fsm_handle(evt)
关键代码片段:
// ISR:仅记录时间戳,提交延迟工作(不做任何业务逻辑)
#define DEFINE_BTN_ISR(n) \
static void isr_btn##n(const struct device *dev, \
struct gpio_callback *cb, \
uint32_t pins) \
{ \
int64_t now = k_uptime_get(); \
if ((now - s_ctx[n].last_isr_ms) < (int64_t)DEBOUNCE_MS) { \
return; /* 抖动期内忽略 */ \
} \
s_ctx[n].last_isr_ms = now; \
k_work_schedule(&s_ctx[n].work, K_MSEC(DEBOUNCE_MS)); \
}
// 工作队列回调:二次确认后调用业务回调
static void btn_work_handler(struct k_work *work)
{
btn_ctx_t *ctx = CONTAINER_OF(
k_work_delayable_from_work(work), btn_ctx_t, work);
// 再次读取引脚:GPIO_ACTIVE_LOW,get=1 表示真正按下
if (gpio_pin_get_dt(&s_pins[ctx->idx]) != 1) {
return; // 消抖确认失败,忽略
}
if (s_cb) {
s_cb(s_evt_map[ctx->idx]); // 触发业务回调
}
}五、蜂鸣器告警
蜂鸣器(无源,接 P1.06,PWM20 驱动)使用 可延迟工作队列 异步播放音序,完全不阻塞主循环。
5.1 告警音序列// 3 声短鸣,循环直到调用 buzzer_stop()
static const buzzer_note_t alert_seq[] = {
{ 2000, 200 }, // 2kHz 鸣 200ms
{ 0, 100 }, // 静音 100ms
{ 2000, 200 }, // 2kHz 鸣 200ms
{ 0, 100 }, // 静音 100ms
{ 2000, 200 }, // 2kHz 鸣 200ms
{ 0, 400 }, // 静音 400ms(组间间隔)
};5.2 工作队列驱动的异步播放static void buzzer_work_handler(struct k_work *work)
{
if (!s_alerting) {
pwm_off(); // 停止信号到来时立即静音
return;
}
const buzzer_note_t *note = &alert_seq[s_seq_idx];
pwm_on(note->freq_hz); // 播放当前音符
// 推进到下一个音符(循环)
s_seq_idx = (s_seq_idx + 1U) % ALERT_SEQ_LEN;
// 在 dur_ms 后调度下一步(非阻塞)
k_work_schedule(&s_work, K_MSEC(note->dur_ms));
}
void buzzer_alert(void)
{
if (s_alerting) return; // 防止重复启动
s_alerting = true;
s_seq_idx = 0U;
k_work_schedule(&s_work, K_NO_WAIT); // 立即开始
}
void buzzer_stop(void)
{
s_alerting = false;
k_work_cancel_delayable(&s_work);
pwm_off(); // 立即静音
}六、主循环:各模块串联
整体的程序流程图如下:

main.c 是整个项目的"胶水层",负责把所有模块粘合在一起:
int main(void)
{
// ── 初始化各模块 ──
buzzer_init();
buttons_init(on_btn_event); // 注册按键回调
ble_gatt_init();
settings_fsm_init(90, 80, true, on_alert_toggle);
// ── LVGL 初始化(Zephyr 自动完成)──
lv_display_t *lvgl_disp = lv_display_get_next(NULL);
ui_create();
lv_refr_now(lvgl_disp); // 初始渲染
int64_t last_tick = k_uptime_get();
while (1) {
lv_timer_handler(); // 驱动 LVGL 内部定时器
if ((k_uptime_get() - last_tick) >= 1000LL) {
last_tick = k_uptime_get();
// 1. 读取 BLE 数据
struct pc_stats stats;
bool connected = ble_is_connected();
ble_get_stats(&stats);
// 2. 读取当前阈值配置
uint8_t cpu_thr = settings_fsm_get_cpu_threshold();
uint8_t mem_thr = settings_fsm_get_mem_threshold();
bool alert_en = settings_fsm_get_alert_enabled();
// 3. 告警判断(核心业务逻辑)
if (connected && alert_en) {
if (stats.cpu_usage >= cpu_thr ||
stats.mem_usage >= mem_thr) {
buzzer_alert(); // 超阈值:触发告警
} else {
buzzer_stop(); // 正常:确保蜂鸣器静音
}
} else {
buzzer_stop(); // 未连接或告警关闭
}
// 4. 更新 UI
ui_update_ble_status(connected);
ui_update_stats(&stats, cpu_thr, mem_thr, alert_en);
lv_refr_now(lvgl_disp);
}
k_sleep(K_MSEC(5));
}
}告警判断逻辑流程图:
每秒 Tick │ ├─ BLE 未连接? ──YES──► buzzer_stop() │ ├─ 告警总开关关闭? ──YES──► buzzer_stop() │ └─ CPU >= cpu_thr OR MEM >= mem_thr? │ YES ──► buzzer_alert() + UI 显示阈值标注 │ NO ──► buzzer_stop() + UI 正常显示
按键事件回调(连接状态机与主循环):
// 按键事件 → 状态机处理
static void on_btn_event(btn_evt_t evt)
{
settings_fsm_handle(evt);
// 状态机内部更新阈值,主循环下一个 Tick 自动读取新值
}
// 告警开关变化时的回调(由状态机调用)
static void on_alert_toggle(bool enabled)
{
LOG_INF("Alert -> %s", enabled ? "ON" : "OFF");
if (!enabled) buzzer_stop(); // 关闭告警时立即停止蜂鸣器
}七、UI 告警状态体现
UI 在屏幕上实时反映当前状态和阈值,让用户一眼看清:
void ui_update_stats(const struct pc_stats *stats,
uint8_t cpu_thr, uint8_t mem_thr,
bool alert_en)
{
char buf[32];
// 告警开关状态
lv_label_set_text(lbl_alt,
alert_en ? "[ALT]ON " : "[ALT]OFF");
// CPU:当前值 + 阈值,超阈值时一目了然
// 显示效果: "CPU 75%[90%]" ← 未超阈值
// "CPU 95%[90%]" ← 超阈值(蜂鸣器已响)
snprintf(buf, sizeof(buf), "CPU %3d%%[%2d%%]",
stats->cpu_usage, cpu_thr);
lv_label_set_text(lbl_cpu, buf);
lv_bar_set_value(bar_cpu, stats->cpu_usage, LV_ANIM_OFF);
// MEM
snprintf(buf, sizeof(buf), "MEM %3d%%[%2d%%]",
stats->mem_usage, mem_thr);
lv_label_set_text(lbl_mem, buf);
lv_bar_set_value(bar_mem, stats->mem_usage, LV_ANIM_OFF);
// GPU
snprintf(buf, sizeof(buf), "GPU %3d%%", stats->gpu_usage);
lv_label_set_text(lbl_gpu, buf);
lv_bar_set_value(bar_gpu, stats->gpu_usage, LV_ANIM_OFF);
// 折线图追加最新 CPU 数据点
lv_chart_set_next_value(chart_cpu, ser_cpu, stats->cpu_usage);
}八、状态机运行日志示例
以下是完整操作一遍的 RTT 日志,直观展示状态机的工作过程:
# 系统启动 [00:00:03.521] <inf> settings_fsm: [FSM] Init: CPU=90% MEM=80% Alert=ON [00:00:03.522] <inf> main: LVGL EPD Monitor boot # BLE 连接,开始接收数据 [00:00:08.401] <inf> ble_gatt: Connected: C0:4E:30:11:22:33 [00:00:09.401] <inf> main: Tick! CPU=75% MEM=60% GPU=45% # 按下 BTN1:进入 CPU 阈值设置模式 [00:00:12.301] <inf> buttons: button1 pressed -> MODE [00:00:12.302] <inf> settings_fsm: [FSM] WORKING -> SET_CPU [00:00:12.302] <inf> settings_fsm: [FSM] Set CPU | current=90% BTN3=+5 BTN2=-5 BTN1=next # 按下 BTN2:CPU 阈值 -5% [00:00:14.105] <inf> buttons: button2 pressed -> DEC [00:00:14.106] <inf> settings_fsm: [FSM] CPU threshold -> 85% # 再按 BTN2:CPU 阈值再 -5% [00:00:15.203] <inf> buttons: button2 pressed -> DEC [00:00:15.204] <inf> settings_fsm: [FSM] CPU threshold -> 80% # 按下 BTN1:进入 MEM 阈值设置模式 [00:00:16.801] <inf> buttons: button1 pressed -> MODE [00:00:16.802] <inf> settings_fsm: [FSM] SET_CPU -> SET_MEM [00:00:16.802] <inf> settings_fsm: [FSM] Set MEM | current=80% BTN3=+5 BTN2=-5 BTN1=confirm # 按下 BTN3:MEM 阈值 +5% [00:00:18.402] <inf> buttons: button3 pressed -> INC [00:00:18.403] <inf> settings_fsm: [FSM] MEM threshold -> 85% # 按下 BTN1:返回正常监控模式 [00:00:19.901] <inf> buttons: button1 pressed -> MODE [00:00:19.902] <inf> settings_fsm: [FSM] SET_MEM -> WORKING [00:00:19.902] <inf> settings_fsm: [FSM] Working | CPU=80% MEM=85% Alert=ON # CPU 飙升超阈值,触发告警 [00:00:22.401] <inf> main: Tick! CPU=83% MEM=60% GPU=72% [00:00:22.401] <inf> buzzer: Buzzer alert start # 按下 BTN0:关闭告警总开关 [00:00:25.601] <inf> buttons: button0 pressed -> ALERT [00:00:25.602] <inf> settings_fsm: [FSM] Alert -> OFF [00:00:25.602] <inf> main: Alert -> OFF [00:00:25.602] <inf> buzzer: Buzzer alert stop
九、项目总结
历经三篇,项目终于完整落地,回顾一下三步走的成果:
第一步:BLE 通信 ✅ PC → psutil 采集 → bleak 发送 → nRF54L15 GATT Server 接收 第二步:ePaper 显示 ✅ Zephyr 驱动移植 → 全刷/局刷 → LVGL 适配 → UI 绘制 第三步:告警状态机 ✅ 按键消抖驱动 → FSM 阈值管理 → 蜂鸣器联动 → 主循环串联
低功耗BLE电脑性能监控副屏到此全部实现。
我要赚赏金
