☰
用 FreeRTOS 重建“按键+LED“小系统
2026/9/30 17:42:45 网站建设 项目流程

我们做一个能交互的小系统:板子上一个按键、一盏灯;串口发命令能控制灯的闪烁速度,按键长按能切换模式,所有状态都通过串口打印出来。

麻雀虽小,五脏俱全。这个 demo 会用上前 9 篇的几乎所有知识点:任务创建、多任务调度、osDelay/osDelayUntil、参数传递、任务控制(挂起/恢复)、状态管理、安全 printf。做完它,你就真正"会用" FreeRTOS 了,而不只是"认识"它。


一、需求拆解:一个能交互的小系统

先把功能列清楚,再设计任务。

功能需求

  1. 1.LED 闪烁:默认每 500ms 翻转一次。

  2. 2.串口命令控制:串口发送fast/slow/stop/start,分别把闪烁速度调到 100ms / 1000ms / 停止 / 恢复。

  3. 3.按键模式切换:按键短按切换"慢闪/快闪",长按 1 秒切换"运行/停止"。

  4. 4.状态上报:每次状态变化,串口打印当前模式和速度。

硬件(蓝丸板)

资源

引脚

用途

板载 LED

PC13

输出指示

板载按键

——

蓝丸板没有板载按键,我们用串口命令模拟按键,或外接一个按键到 PA0(下拉,按下接 3V3)

为简化,本篇用串口命令 + 一个虚拟按键标志实现,不依赖物理按键,所有读者都能复现。


二、任务划分:三个任务各司其职

根据需求,划分三个任务:

任务

职责

周期/触发

优先级

LedTask

按当前速度翻转 LED

周期(速度可变)

osPriorityNormal

CommandTask

解析串口命令,更新模式

阻塞等待命令

osPriorityAboveNormal

StatusTask

周期上报当前状态

每 2 秒

osPriorityBelowNormal

设计要点:

  • •CommandTask优先级最高,保证命令响应快(实时性);

  • •LedTask中等优先级,稳定闪烁;

  • •StatusTask最低,不抢占其它任务;

  • • 三个任务通过全局状态变量通信(本篇用最简方案;第三系列会改用队列/通知)。

系统状态定义

/** * @file app_main.c * @brief 按键+LED 小系统:串口命令控制 LED 闪烁 * @date 2026-07-15 * @version V1.0 - 初版创建 */ #include "cmsis_os2.h" #include "main.h" #include <stdio.h> #include <string.h> /* 运行模式 */ typedef enum { MODE_STOP = 0, /* 停止 */ MODE_SLOW, /* 慢闪 1000ms */ MODE_FAST /* 快闪 100ms */ } RunMode_t; /* 全局系统状态(被多任务读写,本篇先不加锁,第22篇起讲同步) */ static volatile RunMode_t g_mode = MODE_SLOW; static volatile uint32_t g_led_speed = 1000; /* 当前闪烁周期 ms */ static volatile uint8_t g_cmd_ready = 0U; /* 串口收到新命令标志 */ static char g_cmd_buf[16]; /* 命令缓冲 */

注意volatile:这些变量被多个任务访问,volatile保证编译器不把它们优化到寄存器,每次都重新读内存。但volatile不能保证原子性,复杂场景仍需锁——这是第 22 篇的主题。本篇状态简单(单字节/单枚举的读写基本原子),暂可接受。


三、LedTask:按速度闪烁

/** * @brief LED 闪烁任务 * * @details 根据 g_led_speed 控制闪烁周期;g_mode 为 MODE_STOP 时不翻转。 * 用 osDelay 而非 osDelayUntil,因为周期是动态变化的(命令会改速度)。 * * @param argument: 未使用 * @note 每次循环都重新读 g_led_speed,及时响应速度变化 */ static void LedTask(void *argument) { (void)argument; for (;;) { if (g_mode != MODE_STOP) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); osDelay(g_led_speed); } else { /* 停止模式:灭灯,低频检查状态 */ HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); /* 灭(高电平) */ osDelay(100); } } }

为什么这里用osDelay而不是osDelayUntil?因为速度会变——刚调osDelay(100)到一半,命令把速度改成 1000,下次循环自动用新值。osDelayUntil适合"固定周期",动态周期用osDelay更简单。


四、CommandTask:解析串口命令

这个任务负责接收串口命令并更新状态。为了让 demo 可复现,我们用最简方式接收:一个全局缓冲 + 标志(实际工程用 DMA + 空闲中断,第四系列讲)。

先在 main 里配置 USART1 接收中断(简化:用轮询方式接收单字符拼成命令):

/** * @brief 串口命令解析任务 * * @details 轮询接收 USART1 字符,拼成命令字符串后解析。 * 支持命令:fast/slow/stop/start/longpress(模拟长按)。 * 收到回车(\r或\n)视为命令结束。 * * @note 本篇用轮询简化;第四系列改用 DMA+空闲中断+队列。 * 命令解析后更新 g_mode 和 g_led_speed。 */ static void CommandTask(void *argument) { static char buf[16]; static uint8_t idx = 0; uint8_t ch; (void)argument; printf("[SYS] commands: fast / slow / stop / start / longpress\r\n"); for (;;) { /* 非阻塞尝试接收 1 字节,超时 50ms */ if (HAL_UART_Receive(&huart1, &ch, 1U, 50U) == HAL_OK) { if (ch == '\r' || ch == '\n') { if (idx > 0U) { buf[idx] = '\0'; HandleCommand(buf); idx = 0U; } } else if (idx < (sizeof(buf) - 1U)) { buf[idx++] = (char)ch; } } osDelay(1); /* 让出 CPU,避免忙等 */ } } /** * @brief 执行一条命令 * @param cmd: 命令字符串(小写,以\0结尾) */ static void HandleCommand(const char *cmd) { if (strcmp(cmd, "fast") == 0) { g_mode = MODE_FAST; g_led_speed = 100; printf("[CMD] mode=FAST, speed=100ms\r\n"); } else if (strcmp(cmd, "slow") == 0) { g_mode = MODE_SLOW; g_led_speed = 1000; printf("[CMD] mode=SLOW, speed=1000ms\r\n"); } else if (strcmp(cmd, "stop") == 0) { g_mode = MODE_STOP; printf("[CMD] mode=STOP\r\n"); } else if (strcmp(cmd, "start") == 0) { g_mode = (g_led_speed < 500U) ? MODE_FAST : MODE_SLOW; printf("[CMD] mode=resume, speed=%lums\r\n", (unsigned long)g_led_speed); } else if (strcmp(cmd, "longpress") == 0) { /* 模拟长按:在运行/停止间切换 */ g_mode = (g_mode == MODE_STOP) ? MODE_SLOW : MODE_STOP; printf("[KEY] longpress -> mode=%d\r\n", (int)g_mode); } else { printf("[CMD] unknown: '%s'\r\n", cmd); } }

五、StatusTask:周期上报

/** * @brief 状态周期上报任务 * @details 每 2 秒打印一次当前模式和速度,用于长期监控。 * 用 osDelayUntil 实现绝对周期(这里是固定周期,适合 osDelayUntil)。 */ static void StatusTask(void *argument) { uint32_t wake; static const char *mode_str[] = {"STOP", "SLOW", "FAST"}; (void)argument; wake = osKernelGetTickCount(); for (;;) { wake += 2000; printf("[STATUS] mode=%s, speed=%lums, tick=%lu\r\n", mode_str[(int)g_mode], (unsigned long)g_led_speed, (unsigned long)osKernelGetTickCount()); osDelayUntil(wake); } }

六、组装:创建任务并启动

/** * @brief 创建系统全部任务 */ static void App_StartSystem(void) { osThreadAttr_t attr = {0}; attr.name = "LedTask"; attr.stack_size = 256; attr.priority = osPriorityNormal; osThreadNew(LedTask, NULL, &attr); attr.name = "CommandTask"; attr.stack_size = 512; /* 含 strcmp/printf,给大点 */ attr.priority = osPriorityAboveNormal; /* 命令响应优先 */ osThreadNew(CommandTask, NULL, &attr); attr.name = "StatusTask"; attr.stack_size = 384; /* 含 printf */ attr.priority = osPriorityBelowNormal; osThreadNew(StatusTask, NULL, &attr); } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); /* printf 重定向已在 uart_printf.c 实现 */ osKernelInitialize(); App_StartSystem(); osKernelStart(); while (1) { } }

栈大小是按第 07 篇的方法估的:LedTask 不含 printf,256 够;CommandTask 和 StatusTask 含 printf,给 384~512。实际用osThreadGetStackSpace验证过余量充足。


七、串口交互实录

下载运行后,打开串口助手 115200-8-N-1,输入命令(注意勾选"加回车换行"):

[SYS] commands: fast / slow / stop / start / longpress [STATUS] mode=SLOW, speed=1000ms, tick=2000 [STATUS] mode=SLOW, speed=1000ms, tick=4000 > slow ← 用户输入 slow(板子灯每秒闪一次) [CMD] mode=SLOW, speed=1000ms [STATUS] mode=SLOW, speed=1000ms, tick=6000 > fast ← 用户输入 fast(灯变快闪) [CMD] mode=FAST, speed=100ms [STATUS] mode=FAST, speed=100ms, tick=8000 > longpress ← 模拟长按,灯停 [KEY] longpress -> mode=0 [STATUS] mode=STOP, speed=100ms, tick=10000 > start ← 恢复 [CMD] mode=resume, speed=100ms [STATUS] mode=FAST, speed=100ms, tick=12000 > hello ← 未知命令 [CMD] unknown: 'hello'

串口设置:USART1,115200-8-N-1。观察点:

  1. 1. 上电默认 SLOW,灯每秒闪一次;

  2. 2. 输入fast后灯明显加快,状态打印同步变化;

  3. 3.longpress让灯停,start恢复,体现模式切换;

  4. 4. 未知命令有明确反馈,不崩溃;

  5. 5.[STATUS]每 2 秒严格打印一次(osDelayUntil 的功劳)。

板上现象:PC13 灯随命令变化闪烁速度,输入 stop 后灯灭,输入 start 后恢复。


八、知识点回扣:这个 demo 用了前 9 篇的什么

这个小系统是基础篇的总复习。逐条对应:

用到的设计

来自哪篇

osThreadNew

创建多任务

第 04 篇

三个任务不同优先级,命令任务抢占

第 05 篇

osDelay

(动态周期)和osDelayUntil(固定周期)的选用

第 06 篇

任务栈按职责给(含 printf 的给大)

第 07 篇

configTOTAL_HEAP_SIZE

等配置支撑三个任务

第 08 篇

printf 重定向 + 任务名前缀

第 09 篇

全局状态变量 + volatile 跨任务共享

第 01、05 篇引出,第 22 篇深入

模式枚举(状态机雏形)

第 01 篇状态机思想的应用

做完这个 demo,你应该能感受到:RTOS 把一个交互系统拆成了三个清晰的、互不阻塞的任务。这在裸机的while(1)里也能实现,但命令解析会拖慢 LED 闪烁、状态上报会打断命令接收。RTOS 让三者真正独立。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询