这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【nRF54L15-DK测评】低功耗BLE电脑性能监控副屏——阶段三:CPU/内

共1条 1/1 1 跳转至

【nRF54L15-DK测评】低功耗BLE电脑性能监控副屏——阶段三:CPU/内存超阈值告警状态机设计

菜鸟
2026-05-17 16:08:08     打赏

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


最终项目效果

mmexport1778991556348.jpg



一、整体业务逻辑回顾

在正式写状态机之前,先梳理清楚整个系统的业务需求:

┌───────────────────────────────┐
│                    业务需求                │
│                                         │
│  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 状态转换图

这是整个状态机最核心的设计图:

SM.jpg

状态转换规则一句话总结:

  • BTN1 驱动状态向前切换,SET_MEM 时再按 BTN1 回到 WORKING

  • BTN2/BTN3 只在 SET_CPU / SET_MEM 状态下调整对应阈值,WORKING 状态下忽略

  • BTN0 在任何状态下都能切换告警总开关,不受当前状态影响

2.3 整体的按键事件处理流程

btn2.png

2.4 按键事件类型
// 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   5U
3.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.jpg


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电脑性能监控副屏到此全部实现。





关键词: nRF54L15-DK     Zephyr     监控     副屏         

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]