ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用 FreeRTOS 重建“按键+LED“小系统

用 FreeRTOS 重建“按键+LED“小系统 我们做一个能交互的小系统板子上一个按键、一盏灯串口发命令能控制灯的闪烁速度按键长按能切换模式所有状态都通过串口打印出来。麻雀虽小五脏俱全。这个 demo 会用上前 9 篇的几乎所有知识点任务创建、多任务调度、osDelay/osDelayUntil、参数传递、任务控制挂起/恢复、状态管理、安全 printf。做完它你就真正会用 FreeRTOS 了而不只是认识它。一、需求拆解一个能交互的小系统先把功能列清楚再设计任务。功能需求1.LED 闪烁默认每 500ms 翻转一次。2.串口命令控制串口发送fast/slow/stop/start分别把闪烁速度调到 100ms / 1000ms / 停止 / 恢复。3.按键模式切换按键短按切换慢闪/快闪长按 1 秒切换运行/停止。4.状态上报每次状态变化串口打印当前模式和速度。硬件蓝丸板资源引脚用途板载 LEDPC13输出指示板载按键——蓝丸板没有板载按键我们用串口命令模拟按键或外接一个按键到 PA0下拉按下接 3V3为简化本篇用串口命令 一个虚拟按键标志实现不依赖物理按键所有读者都能复现。二、任务划分三个任务各司其职根据需求划分三个任务任务职责周期/触发优先级LedTask按当前速度翻转 LED周期速度可变osPriorityNormalCommandTask解析串口命令更新模式阻塞等待命令osPriorityAboveNormalStatusTask周期上报当前状态每 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] modeFAST, speed100ms\r\n); } else if (strcmp(cmd, slow) 0) { g_mode MODE_SLOW; g_led_speed 1000; printf([CMD] modeSLOW, speed1000ms\r\n); } else if (strcmp(cmd, stop) 0) { g_mode MODE_STOP; printf([CMD] modeSTOP\r\n); } else if (strcmp(cmd, start) 0) { g_mode (g_led_speed 500U) ? MODE_FAST : MODE_SLOW; printf([CMD] moderesume, 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 不含 printf256 够CommandTask 和 StatusTask 含 printf给 384~512。实际用osThreadGetStackSpace验证过余量充足。七、串口交互实录下载运行后打开串口助手 115200-8-N-1输入命令注意勾选加回车换行[SYS] commands: fast / slow / stop / start / longpress [STATUS] modeSLOW, speed1000ms, tick2000 [STATUS] modeSLOW, speed1000ms, tick4000 slow ← 用户输入 slow板子灯每秒闪一次 [CMD] modeSLOW, speed1000ms [STATUS] modeSLOW, speed1000ms, tick6000 fast ← 用户输入 fast灯变快闪 [CMD] modeFAST, speed100ms [STATUS] modeFAST, speed100ms, tick8000 longpress ← 模拟长按灯停 [KEY] longpress - mode0 [STATUS] modeSTOP, speed100ms, tick10000 start ← 恢复 [CMD] moderesume, speed100ms [STATUS] modeFAST, speed100ms, tick12000 hello ← 未知命令 [CMD] unknown: hello串口设置USART1115200-8-N-1。观察点1. 上电默认 SLOW灯每秒闪一次2. 输入fast后灯明显加快状态打印同步变化3.longpress让灯停start恢复体现模式切换4. 未知命令有明确反馈不崩溃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 让三者真正独立。
返回列表