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 编译与下载
- 打开 Keil 工程(
template.uvprojx),确认User/下所有 .c 文件已加入工程。 - 点击 Build (或 F7),确保 0 Error 。若报
ERR_NONE未定义,检查component_def.h是否被各文件 include。 - 点击 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/toggle、dev_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 收发与命令处理