3.解决STM32工程混乱:一套可复用六层架构,GPIO 、LED 实战演示

1. 本篇目标

上一篇文章(基础工程框架搭建)完成了 v0.1 的工程骨架------六层目录建立、初始化编排、公共基础设施就绪。但那个骨架是空的,没有控制任何外设。

本篇的目标是用最简单的 GPIO 外设 (2 个 LED + 2 个按键)把分层开发的完整闭环跑通。你将看到同一份 GPIO 硬件,如何经过 bsp → device → service → app 四层封装,最终变成业务代码中一行 dev_led_on()dev_key_read()

读完本篇后,你将掌握:

  • 如何在 bsp 层用结构体数组管理多个 GPIO 的引脚定义和有效电平
  • 如何在 device 层提供多 ID 的 LED 和按键语义化接口
  • 如何在 service 层用状态机处理多个按键的消抖和事件分类
  • 如何在 app 层区分不同按键事件、编排业务逻辑
  • 如何用消息队列在层之间传递带按键编号的事件

本篇硬件依赖: 板载 LED 2 个、板载按键 2 个。几乎所有 STM32 开发板都自带,无需外接。

2. 本篇完成后的工程效果

跑通之后,你的板子会这样工作:

  • 上电启动:LED0 开始慢速心跳闪烁(亮 500ms,灭 500ms),系统进入正常模式。
  • 短按 KEY0:在正常模式和配置模式之间切换------切到配置模式 LED0 变快闪(100ms),切回正常恢复慢闪。
  • 长按 KEY0(>2s):触发告警,LED0 常亮 3 秒后自动恢复。
  • 按 KEY1:串口打印按键事件日志(预留扩展,后续可绑定其他功能)。

整条链路贯穿四层:app → service → device → bsp,无论按键还是 LED 都走同一套分层。

分层调用链路

复制代码
按键按下
    ↓
bsp_gpio_read()               ← bsp 层:读取 GPIO 电平
    ↓
dev_key_read()                 ← device 层:有效电平反转
    ↓
service 层按键扫描线程          ← service 层:消抖 + 事件判断
    ↓
消息队列发送按键事件
    ↓
app 任务接收事件               ← app 层:执行业务逻辑
    ↓
srv_led_indicator_set_mode()        ← service 层:切换指示灯策略
    ↓
dev_led_on()                  ← device 层:设置 LED 开关
    ↓
bsp_gpio_write()               ← bsp 层:操作 GPIO 寄存器

串口输出示例

复制代码
[I] 检测到短按,切换到配置模式
[I] 检测到短按,切换到正常模式
[W] 检测到长按(>2s),触发告警事件
[I] 检测到 KEY1 短按

CMD> cmd_status
系统状态:
模式: 正常
运行时间: 125s

3. 前置条件与硬件连接

硬件

  • 开发板:STM32F429 正点原子开发板(或其他 STM32 板)。
  • 板载 LED:LED0 接 PB1,LED1 接 PB0(不同板子引脚可能不同,以原理图为准)。
  • 板载按键:KEY0 接 PH3,KEY1 接 PH2(需确认有效电平)。

如果你的开发板 LED/按键引脚不同,只需修改 bsp 层的引脚定义,上层代码不需要改动------这正是分层的价值之一。

软件

  • 基础工程:上一篇完成的 v0.1 工程骨架(基于 RT-Thread)。
  • 操作系统:RT-Thread 3.1.3(FreeRTOS / 裸机也可,分层思路一致)。
  • IDE / 构建工具:Keil MDK(或 IAR / STM32CubeIDE / GCC + Makefile)。
  • 串口工具:PuTTY / MobaXterm / SSCOM,波特率 115200。

GPIO 配置(CubeMX)

外设 引脚 模式
LED0 PB1 GPIO_Output
LED1 PB0 GPIO_Output
KEY0 PH3 GPIO_Input(上拉)
KEY1 PH2 GPIO_Input(上拉)

按键必须配置为 Input 模式并启用上拉(按下接地,低电平有效)。如果 CubeMX 里配成 Output,按键将无法读取------这是最容易踩的坑。


4. 本篇涉及的架构与模块关系

新增文件

复制代码
User/
├── app/
│   ├── inc/
│   │   ├── app_framework.h          ← app_framework_init() 声明
│   │   ├── app_cfg.h                ← 全局线程配置(app + service 统一收口)
│   │   └── app_main_thread.h               ← app_main_thread_entry() 线程入口声明
│   └── src/
│       ├── app_framework.c           ← 初始化编排(创建线程)
│       └── app_main_thread.c               ← 按键处理线程 + Shell 命令
├── service/
│   ├── inc/
│   │   └── srv_led_indicator.h          ← LED 指示策略服务
│   │   └── srv_key.h                ← 按键扫描服务
│   └── src/
│       ├── srv_led_indicator.c
│       └── srv_key.c
├── device/
│   ├── inc/
│   │   └── dev_led.h                ← LED 设备接口
│   │   └── dev_key.h                ← 按键设备接口
│   └── src/
│       ├── dev_led.c
│       └── dev_key.c
├── bsp/
│   ├── inc/
│   │   └── bsp_gpio.h               ← 板级 GPIO 定义 + 读写封装
│   └── src/
│       ├── bsp_gpio.c
└── component/
    └── inc/
        └── component_def.h          ← 已有,无需改动

模块依赖关系

复制代码
app_framework.c
    └── 创建 app_main_thread 线程

app_main_thread.c
    ├── srv_led_indicator.h     设置指示灯模式
    └── srv_key.h           接收按键事件

srv_key.c
    └── dev_key.h           读取按键电平

srv_led_indicator.c
    └── dev_led.h           设置 LED 亮灭

dev_led.c / dev_key.c
    └── bsp_gpio.h          操作 GPIO

bsp_gpio.c
    └── HAL GPIO            操作寄存器

5. 设计思路:为什么这样分层

5.1 有效电平为什么在 bsp 层处理

LED 的有效电平取决于板级电路设计:

  • 有的板子 GPIO 高电平亮、低电平灭
  • 有的板子恰好相反

如果上层代码直接写 HAL_GPIO_WritePin(PB1, GPIO_PIN_SET),换一块板子就要全文搜索修改。

bsp 层的职责就是吸收这种板级差异:

c 复制代码
/* bsp_gpio.h */
#define BSP_LED_ON  GPIO_PIN_RESET
#define BSP_LED_OFF GPIO_PIN_SET

device 层只调用 bsp_gpio_write(BSP_LED0, BSP_LED_ON),不关心电平是高有效还是低有效。

5.2 按键消抖为什么放在 service 层

按键消抖涉及时间判断(延迟 20ms 后再次读取),属于"数据处理"而非"设备访问"或"业务决策":

  • bsp 层不适合------它只做 GPIO 读写,不应引入延时逻辑。
  • device 层可以但不推荐------它能读电平,但消抖本质是数据处理,不该由设备层承担。
  • service 层最合适------数据处理本来就是 service 的职责。
  • app 层不适合------业务层不应关心"按键物理抖动"这种底层细节。

所以消抖放在 service 层(srv_key.c),device 层只提供原始电平读取。

5.3 为什么用消息队列传递按键事件

按键事件需要在 service 层产生、在 app 层消费。如果不经过消息队列,有两种"偷懒"的做法,但都有问题:

做法 A:在 service 层直接调 app 层的函数

service 层判断出按键事件后,直接调用 app_on_key_pressed()。这样 service 层就依赖了 app 层,违反了"依赖方向只能从上到下"的原则。以后 service 层换掉,app 层也要跟着改。

做法 B:app 层直接轮询读取按键电平

app 层每隔 10ms 调用 dev_key_read() 自己判断按键状态。这样 app 层就要自己处理消抖、计时、防连按------这些"数据处理"本该是 service 层的职责,两层又耦合了。

消息队列的解耦方式:

复制代码
service 层(生产者)→ 消息队列 → app 层(消费者)
  • service 层只管往队列里放事件,不管谁消费
  • app 层只管从队列里取事件,不管事件怎么产生的
  • 两方都不需要知道对方的存在

这不是一条绝对规则,而是一个取舍。 对于按键这类低频事件(用户按一次,几秒内不会再按),消息队列的开销完全可以忽略。但对于串口 115200bps 的持续数据流、网络 UDP 报文等高吞吐场景,每帧都走消息队列转发一次,memcpy 和上下文切换成本就不容忽视了。这时候更适合让 app 层直接调用 bsp_uart_read() 读取,跳过消息队列,用直连换取吞吐。核心判断依据:事件频率。低频用队列解耦,高频用直连换性能。

5.4 为什么线程参数要全局配置

当前系统有两个线程:service 层的按键扫描线程、app 层的业务线程。一种常见写法是把各自的优先级和栈大小直接写在 rt_thread_create() 里------srv_key.c 里写 8,app_framework.c 里写 10。

这种写法在只有两个线程时看不出问题,但线程多了以后会变成:

  • 调优先级时不知道哪些文件里创建了线程,得全局搜索 rt_thread_create
  • 两个线程的优先级调反了(key_scan 比 app_main 还高,按键处理抢占了业务逻辑),排查时得翻两个文件对比
  • 栈溢出时想统一加大栈空间,漏掉一个线程就可能出问题

解决方式很简单:所有线程参数收到一个头文件 app_cfg.h 里,用宏定义。 不管是 app 层还是 service 层的线程,rt_thread_create 的参数都引用这里的宏。以后要调栈大小或优先级,只改这一个文件。

复制代码
app_cfg.h(一个文件)
    ├── THREAD_PRIO_KEY_SCAN    11    ← srv_key.c 引用
    ├── THREAD_PRIO_APP_MAIN   12    ← app_framework.c 引用
    ├── THREAD_STACK_KEY_SCAN  512
    ├── THREAD_STACK_APP_MAIN 1024
    └── THREAD_TICK_MS          10
         ↓
    不  管  哪  层  的  线  程  都  用  这  些  宏

6. 代码实现

bsp 层用结构体数组管理引脚和电平------换板子只改数组,读写代码不动。核心思路:上层传「逻辑电平」(BSP_LED_ON=1),bsp 内部转为「物理电平」(GPIO_PIN_RESET/SET)。

6.1 bsp 层:GPIO 结构体数组驱动

bsp_gpio.h:

c 复制代码
#ifndef __BSP_GPIO_H__
#define __BSP_GPIO_H__

#include <stdint.h>
#include "stm32f4xx_hal.h"

typedef enum
{
    BSP_LED0,
    BSP_LED1,
    BSP_LED_NUM
} bsp_led_id_t;

typedef enum
{
    BSP_KEY0,
    BSP_KEY1,
    BSP_KEY_NUM
} bsp_key_id_t;

#define BSP_LED_ON      1
#define BSP_LED_OFF     0
#define BSP_KEY_PRESS   1
#define BSP_KEY_RELEASE 0

int     bsp_gpio_init(void);
void    bsp_led_write(bsp_led_id_t id, uint8_t level);
void    bsp_led_toggle(bsp_led_id_t id);
uint8_t bsp_key_read(bsp_key_id_t id);

#endif

bsp_gpio.c(typedef 定义配置类型 + 结构体数组):

c 复制代码
#include "bsp_gpio.h"
#include "gpio.h"
#include "component_def.h"

/* LED 配置类型 */
typedef struct
{
    GPIO_TypeDef *port;
    uint16_t      pin;
    GPIO_PinState active_level;     /* 亮灯时的 HAL 电平 */
} bsp_led_cfg_t;

/* 按键配置类型 */
typedef struct
{
    GPIO_TypeDef *port;
    uint16_t      pin;
    GPIO_PinState pressed_level;    /* 按下时的 HAL 电平 */
} bsp_key_cfg_t;

/* LED 配置表 */
static const bsp_led_cfg_t led_cfg[BSP_LED_NUM] =
{
    { GPIOB, GPIO_PIN_1, GPIO_PIN_RESET },    /* BSP_LED0:PB1,低亮 */
    { GPIOB, GPIO_PIN_0, GPIO_PIN_RESET },    /* BSP_LED1:PB0,低亮 */
};

/* 按键配置表 */
static const bsp_key_cfg_t key_cfg[BSP_KEY_NUM] =
{
    { GPIOH, GPIO_PIN_3, GPIO_PIN_RESET },    /* BSP_KEY0:PH3,低按下 */
    { GPIOH, GPIO_PIN_2, GPIO_PIN_RESET },    /* BSP_KEY1:PH2,低按下 */
};

int bsp_gpio_init(void)
{
    MX_GPIO_Init();
    return ERR_NONE;
}

void bsp_led_write(bsp_led_id_t id, uint8_t level)
{
    GPIO_PinState hal_level;

    if (id >= BSP_LED_NUM)
    {
        return;
    }

    if (level == BSP_LED_ON)
    {
        hal_level = led_cfg[id].active_level;
    }
    else
    {
        hal_level = (led_cfg[id].active_level == GPIO_PIN_RESET)
                    ? GPIO_PIN_SET : GPIO_PIN_RESET;
    }

    HAL_GPIO_WritePin(led_cfg[id].port, led_cfg[id].pin, hal_level);
}

void bsp_led_toggle(bsp_led_id_t id)
{
    if (id >= BSP_LED_NUM)
    {
        return;
    }

    HAL_GPIO_TogglePin(led_cfg[id].port, led_cfg[id].pin);
}

uint8_t bsp_key_read(bsp_key_id_t id)
{
    GPIO_PinState hal;

    if (id >= BSP_KEY_NUM)
    {
        return 0;
    }

    hal = HAL_GPIO_ReadPin(key_cfg[id].port, key_cfg[id].pin);
    return (hal == key_cfg[id].pressed_level) ? BSP_KEY_PRESS : BSP_KEY_RELEASE;
}

6.2 device 层:多 ID 语义封装

device 层只做转发------LED 传 BSP_LED_ON/OFF,按键传回 BSP_KEY_PRESS/RELEASE,不关心硬件电平。

c 复制代码
/* dev_led.h */
int  dev_led_init(void);
void dev_led_on(bsp_led_id_t id);
void dev_led_off(bsp_led_id_t id);
void dev_led_toggle(bsp_led_id_t id);
c 复制代码
/* dev_led.c */
void dev_led_on(bsp_led_id_t id)
{
    bsp_led_write(id, BSP_LED_ON);
}

void dev_led_off(bsp_led_id_t id)
{
    bsp_led_write(id, BSP_LED_OFF);
}

void dev_led_toggle(bsp_led_id_t id)
{
    bsp_led_toggle(id);
}
c 复制代码
/* dev_key.h */
int     dev_key_init(void);
uint8_t dev_key_read(bsp_key_id_t id);
c 复制代码
/* dev_key.c */
uint8_t dev_key_read(bsp_key_id_t id)
{
    return bsp_key_read(id);
}

6.3 service 层:多按键独立状态机

每个按键独立的状态机上下文,用 key_ctx[BSP_KEY_NUM] 数组管理。线程每次遍历所有按键。

c 复制代码
/* srv_key.h */
typedef enum
{
    KEY_EVENT_NONE,
    KEY_EVENT_SHORT_PRESS,
    KEY_EVENT_LONG_PRESS,
} srv_key_event_t;

typedef struct
{
    bsp_key_id_t    key_id;
    srv_key_event_t event;
} srv_key_msg_t;

int srv_key_init(void);
int srv_key_get_event(srv_key_msg_t *msg);
c 复制代码
/* srv_key.c */
#include "srv_key.h"
#include "dev_key.h"
#include <rtthread.h>

static rt_mq_t key_mq;

typedef enum { SM_IDLE, SM_DEBOUNCE, SM_PRESSED } key_sm_state_t;

typedef struct
{
    key_sm_state_t state;
    uint32_t       enter_tick;
    uint32_t       press_start;
    uint8_t        long_sent;      /* 长按事件是否已触发 */
} key_ctx_t;

static key_ctx_t key_ctx[BSP_KEY_NUM];

#define DEBOUNCE_MS    20
#define LONG_PRESS_MS  2000
#define TIMEOUT_MS     5000

/******************************************************************************
 * @brief  单按键状态机 tick
 * @param  ctx : 该按键的状态机上下文
 * @param  id  : 按键编号
 * @return 产生的按键事件,无事件返回 KEY_EVENT_NONE
 ******************************************************************************/
static srv_key_event_t key_sm_tick(key_ctx_t *ctx, bsp_key_id_t id)
{
    uint32_t now = rt_tick_get();
    uint8_t  raw = dev_key_read(id);

    switch (ctx->state)
    {
        case SM_IDLE:
        {
            if (raw)
            {
                ctx->enter_tick = now;
                ctx->state = SM_DEBOUNCE;
            }
            break;
        }

        case SM_DEBOUNCE:
        {
            if (now - ctx->enter_tick >= rt_tick_from_millisecond(DEBOUNCE_MS))
            {
                if (dev_key_read(id))
                {
                    ctx->press_start = now;
                    ctx->enter_tick  = now;
                    ctx->state = SM_PRESSED;
                }
                else
                {
                    ctx->state = SM_IDLE;
                }
            }
            break;
        }

        case SM_PRESSED:
        {
            /* 长按:达到阈值立即触发,不等按键释放 */
            if (!ctx->long_sent &&
                now - ctx->press_start >= rt_tick_from_millisecond(LONG_PRESS_MS))
            {
                ctx->long_sent = 1;
                return KEY_EVENT_LONG_PRESS;
            }

            /* 按键释放 */
            if (!dev_key_read(id))
            {
                ctx->state = SM_IDLE;

                if (!ctx->long_sent)
                {
                    return KEY_EVENT_SHORT_PRESS;
                }
                ctx->long_sent = 0;
            }
            break;
        }

        default:
        {
            ctx->state = SM_IDLE;
            break;
        }
    }
    return KEY_EVENT_NONE;
}

static void key_thread(void *param)
{
    int i;

    while (1)
    {
        for (i = 0; i < BSP_KEY_NUM; i++)
        {
            uint32_t now = rt_tick_get();
            key_ctx_t *c = &key_ctx[i];

            /* 超时保护(长按已触发的按键不参与,允许持续按住) */
            if (c->state != SM_IDLE && !c->long_sent &&
                now - c->enter_tick >= rt_tick_from_millisecond(TIMEOUT_MS))
            {
                c->state = SM_IDLE;
                continue;
            }

            srv_key_event_t evt = key_sm_tick(c, (bsp_key_id_t)i);
            if (evt != KEY_EVENT_NONE)
            {
                srv_key_msg_t msg;
                msg.key_id = (bsp_key_id_t)i;
                msg.event  = evt;
                rt_mq_send(key_mq, &msg, sizeof(msg));
            }
        }
        rt_thread_mdelay(10);
    }
}

int srv_key_init(void)
{
    key_mq = rt_mq_create("key_evt", sizeof(srv_key_msg_t), 8, RT_IPC_FLAG_PRIO);
    if (key_mq == RT_NULL)
    {
        return ERR_ERROR;
    }

    rt_thread_t tid = rt_thread_create("key_scan", key_thread, NULL, 512, 8, 10);
    if (tid != RT_NULL)
    {
        rt_thread_startup(tid);
    }
    return ERR_NONE;
}

int srv_key_get_event(srv_key_msg_t *msg)
{
    /* 兼容两种实现:成功返回 RT_EOK(0) 或接收字节数(>0) */
    return rt_mq_recv(key_mq, msg, sizeof(srv_key_msg_t), 0) >= 0 ? 1 : 0;
}

LED 指示灯只控制 LED0,模式由 app 层通过 srv_led_indicator_set_mode() 设置。

c 复制代码
/* srv_led_indicator.h */
typedef enum
{
    IND_MODE_OFF,
    IND_MODE_HEARTBEAT,
    IND_MODE_FAST_BLINK,
    IND_MODE_ALARM,
} srv_led_indicator_mode_t;

void srv_led_indicator_set_mode(srv_led_indicator_mode_t mode);
void srv_led_indicator_tick(void);
c 复制代码
/* srv_led_indicator.c */
static srv_led_indicator_mode_t mode = IND_MODE_HEARTBEAT;
static uint32_t last_tick = 0;

void srv_led_indicator_set_mode(srv_led_indicator_mode_t m)
{
    mode = m;
}

void srv_led_indicator_tick(void)
{
    uint32_t now = rt_tick_get();
    uint32_t interval;

    switch (mode)
    {
        case IND_MODE_HEARTBEAT:
        {
            interval = 500;
            break;
        }
        case IND_MODE_FAST_BLINK:
        {
            interval = 100;
            break;
        }
        case IND_MODE_ALARM:
        {
            dev_led_on(BSP_LED0);
            return;
        }
        default:
        {
            dev_led_off(BSP_LED0);
            return;
        }
    }

    if (now - last_tick >= rt_tick_from_millisecond(interval))
    {
        dev_led_toggle(BSP_LED0);
        last_tick = now;
    }
}

6.4 app 层:区分按键事件

c 复制代码
/* app/src/app_main_thread.c */
#include "srv_key.h"
#include "srv_led_indicator.h"
#include <rtthread.h>
#include "component_def.h"

typedef enum
{
    SYS_MODE_NORMAL,
    SYS_MODE_CONFIG,
} sys_mode_t;

static sys_mode_t sys_mode       = SYS_MODE_NORMAL;
static uint32_t   alarm_end_tick = 0;
static uint8_t    alarm_active   = 0;

/******************************************************************************
 * @brief  应用主线程入口
 ******************************************************************************/
void app_main_thread_entry(void *param)
{
    srv_key_msg_t msg;

    srv_led_indicator_set_mode(IND_MODE_HEARTBEAT);

    while (1)
    {
        /* 告警超时检查 */
        if (alarm_active && rt_tick_get() >= alarm_end_tick)
        {
            alarm_active = 0;
            srv_led_indicator_set_mode((sys_mode == SYS_MODE_NORMAL)
                                       ? IND_MODE_HEARTBEAT : IND_MODE_FAST_BLINK);
        }

        /* 非阻塞取事件 */
        if (srv_key_get_event(&msg))
        {
            if (msg.key_id == BSP_KEY0)
            {
                switch (msg.event)
                {
                    case KEY_EVENT_SHORT_PRESS:
                    {
                        if (sys_mode == SYS_MODE_NORMAL)
                        {
                            sys_mode = SYS_MODE_CONFIG;
                            srv_led_indicator_set_mode(IND_MODE_FAST_BLINK);
                            LOG_I("检测到短按,切换到配置模式");
                        }
                        else
                        {
                            sys_mode = SYS_MODE_NORMAL;
                            srv_led_indicator_set_mode(IND_MODE_HEARTBEAT);
                            LOG_I("检测到短按,切换到正常模式");
                        }
                        break;
                    }

                    case KEY_EVENT_LONG_PRESS:
                    {
                        LOG_W("检测到长按(>2s),触发告警事件");
                        srv_led_indicator_set_mode(IND_MODE_ALARM);
                        alarm_end_tick = rt_tick_get()
                            + rt_tick_from_millisecond(3000);
                        alarm_active = 1;
                        break;
                    }

                    default:
                    {
                        break;
                    }
                }
            }
            else if (msg.key_id == BSP_KEY1)
            {
                LOG_I("检测到 KEY1 %s",
                    (msg.event == KEY_EVENT_SHORT_PRESS) ? "短按" : "长按");
            }
        }

        rt_thread_mdelay(10);
    }
}

/******************************************************************************
 * @brief  Shell 命令:查看系统状态
 ******************************************************************************/
static void cmd_status(int argc, char **argv)
{
    rt_kprintf("系统状态:\r\n");
    rt_kprintf("  模式: %s\r\n", (sys_mode == SYS_MODE_NORMAL) ? "正常" : "配置");
    rt_kprintf("  运行时间: %ds\r\n", rt_tick_get() / RT_TICK_PER_SECOND);
}
MSH_CMD_EXPORT(cmd_status, show system status);

6.5 初始化编排

各层 framework_init 按 bsp → device → service → app 顺序执行,不能颠倒。

c 复制代码
/* main.c */
int main(void)
{
	HAL_Init();

	SystemClock_Config();

	/* ===== 统一初始化编排 ===== */
	if (bsp_framework_init() != ERR_NONE) 
	{
        LOG_E("bsp init failed, system halted");
        while (1);
    }

    if (device_framework_init() != ERR_NONE) 
	{
        LOG_W("device init failed, continuing...");
    }

    if (service_framework_init() != ERR_NONE) 
	{
        LOG_W("service init failed, continuing...");
    }

    if (app_framework_init() != ERR_NONE) 
	{
        LOG_E("app init failed, system halted");
        while (1);
    }

	show_board_info();
	
	LOG_I("system ready.");

	while (1)
	{
		rt_thread_mdelay(10);
	}
}

7. 编译、下载与功能验证

7.1 编译与下载

  1. 打开 Keil 工程(template.uvprojx),确认 User/ 下所有 .c 文件已加入工程。
  2. 点击 Build (或 F7),确保 0 Error 。若报 ERR_NONE 未定义,检查 component_def.h 是否被各文件 include。
  3. 点击 Download(或 F8)烧录固件,打开串口终端(115200-8N1),复位板子。

7.2 功能验证清单

验证项 预期 通过
上电 LED0 开始慢速心跳闪烁(500ms)
短按 KEY0 LED0 变快闪,串口输出"检测到短按,切换到配置模式"
再短按 KEY0 LED0 恢复慢闪,串口输出"切换到正常模式"
按住 KEY0 约 2s 立即触发告警,LED0 常亮 3s 后恢复(无需等松手)
短按 KEY1 串口输出"检测到 KEY1 短按"
Shell 输入 cmd_status 显示当前模式(正常/配置)和运行时间

8. 常见问题与避坑

Q1:LED 不亮

  • 检查 GPIO 引脚号是否和板级原理图一致
  • 检查有效电平:有的板子是高电平亮,有的是低电平亮
  • 检查 MX_GPIO_Init() 是否已调用

Q2:按键不响应或响应抖动

  • 检查按键有效电平(按下是高还是低)
  • 检查消抖时间 KEY_DEBOUNCE_MS 是否合适(20ms 通常够用)
  • 检查按键是否接了外部上拉/下拉

Q3:消息队列丢失事件

  • 检查 rt_mq_create 创建的队列深度是否够(本例 8 个足够)
  • 检查 srv_key_get_event() 的返回值判断:rt_mq_recv 成功时有些 RT-Thread 移植版返回 RT_EOK(0) 而非接收字节数,所以判断要用 >= 0 而不是 > 0,否则 app 永远收不到事件(这是最容易踩的坑)
  • 检查 app 线程是否及时从队列中取出事件

9. 本篇小结

本篇完成的工作:

维度 成果
bsp 层 GPIO 引脚定义 + 电平屏蔽 + 读写封装
device 层 dev_led_on/off/toggledev_key_read 语义接口
service 层 按键消抖 + 事件分类 + LED 闪烁策略
app 层 模式切换业务 + Shell 命令
文件新增 6 个 .c + 7 个 .h(bsp_gpio、dev_led、dev_key、srv_key、srv_led_indicator、app_main_thread + app_cfg)
版本 v0.2

现在看到的这个 LED 和按键,虽然是最简单的硬件,但经过了完整的四层封装链路

复制代码
bsp 操作 GPIO → device 语义化 → service 数据处理 → app 业务决策

后续每加一个外设,都遵循这个模式。

程序运行真实效果如下:


10. 下一篇预告

下一篇将基于 v0.2 工程,实现 UART 的 DMA 收发封装,包括:

  • bsp 层板级 UART 映射 + DMA 收发驱动(已完成基础代码)
  • device 层将 UART 抽象为"可收可发的通信设备"
  • service 层实现日志服务、命令解析器
  • app 层通过串口命令控制系统模式

你将看到:LED 和按键的封装模式,完全复用到 UART 上。


本篇工程版本:v0.2

下一篇:UART DMA 收发与命令处理

相关推荐
单片机杂货铺1 小时前
基于单片机的自动开窗器设计
stm32·嵌入式硬件·51单片机·硬件工程
凌虚1 小时前
面向 MySQL 用户的 PostgreSQL 快速上手指南
数据库·后端·架构
天天爱吃肉82183 小时前
商用车多体动力学实战笔记|第6篇:动力传动系统(发动机、变速箱、分动器、TCS、LSD限滑差速)
大数据·人工智能·笔记·python·嵌入式硬件·汽车
别催小唐敲代码3 小时前
STM32 I2C协议详解:从原理到实战
stm32·网络协议·协议
lhbweilai3 小时前
STM32 I2C(hal库)
stm32·单片机·嵌入式硬件
老王以为4 小时前
走进 AI Agent
前端·人工智能·架构
SLD_Allen4 小时前
大模型推理引擎架构深度解析:vLLM、SGLang与TensorRT-LLM的技术路线对决
架构·vllm·sglang
cxr8284 小时前
Graphify vs GitNexus vs CodeGraph — 三工具架构深度对比
开发语言·架构·知识图谱
奈斯先生Vector5 小时前
把 Midjourney 二次编辑做成生产系统:customId 能力令牌、Action Graph 与 WebUI 精修工作台
数据库·人工智能·架构·aigc·音视频·midjourney
隔窗听雨眠6 小时前
生产级Kubernetes集群部署完全指南:从kubeadm到高可用架构落地
容器·架构·kubernetes