FreeRTOS 综合实战:串口命令控制 LED 闪烁模式与系统监控 本系列前九篇文章依次深入了工程搭建、任务管理、队列、信号量、互斥量、软件定时器、中断管理以及调试优化。本篇将通过一个完整的实战项目——串口命令控制 LED 闪烁模式与系统状态监控把前面所学知识全部串联起来。你可以把它作为学习 FreeRTOS 的毕业设计也可以当作后续复杂项目的模板。一、项目需求通过串口助手发送文本命令开发板解析命令并执行相应操作。支持的命令LED ON—— 启动 LED 闪烁PC13LED OFF—— 停止 LED 闪烁FREQ 毫秒—— 修改 LED 闪烁周期例如FREQ 200表示 200ms 亮灭切换一次STATS—— 立即打印一次任务列表与 CPU 利用率统计系统持续运行PC13 LED 默认不闪烁。串口回显命令执行结果OK / ERR。保留一个后台统计任务每 10 秒自动输出一次系统状态。二、硬件连接STM32F103C8T6 最小系统板PC13—— 板载 LED低电平点亮PA9—— USART1 TX接串口模块的 RXPA10—— USART1 RX接串口模块的 TX串口模块波特率 1152008N1三、软件架构设计串口中断USART1_IRQHandler │ 每收到一个字节通过队列发送 ▼ 命令解析任务CmdTask │ 拼接命令行以 \r\n 为结束标志 │ 调用命令处理函数 ├─► LED ON/OFF → 控制软件定时器启停 ├─► FREQ ms → 修改软件定时器周期 └─► STATS → 释放二值信号量触发统计任务立即输出使用到的 FreeRTOS 组件队列中断到命令任务的字符传递软件定时器驱动 LED 闪烁可在运行中启停、修改周期二值信号量实现 STATS 命令立即触发统计输出任务命令解析任务、统计任务中断管理串口接收中断优先级 5安全调用 FromISR API四、基础驱动程序沿用前面章节已经实现的部分 BSP根据需要略作补充。4.1 LED 驱动bsp_led.h / .c保留LED_InitAll()和LED3_Toggle()确保bsp_led.h中包含#ifndefBSP_LED_H#defineBSP_LED_H#includestm32f10x.hvoidLED_InitAll(void);voidLED3_Toggle(void);#endif4.2 运行时间统计定时器bsp_timer_stats.h / .c与第九篇完全相同继续使用 TIM2 提供 1 MHz 时基并定义宏#defineportCONFIGURE_TIMER_FOR_RUN_TIME_STATS()vConfigureTimerForRunTimeStats()#defineportGET_RUN_TIME_COUNTER_VALUE()TIM_GetCounter(TIM2)4.3 串口驱动增强bsp_uart.h / .c在前文的基础上为 USART1 增加接收中断支持。bsp_uart.h#ifndefBSP_UART_H#defineBSP_UART_H#includestm32f10x.hvoidUART1_Init(uint32_tbaudrate);voidUART_SendString(char*str);#endifbsp_uart.c增加了中断配置#includebsp_uart.hvoidUART1_Init(uint32_tbaudrate){GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA,ENABLE);// PA9 TXGPIO_InitStructure.GPIO_PinGPIO_Pin_9;GPIO_InitStructure.GPIO_ModeGPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_SpeedGPIO_Speed_50MHz;GPIO_Init(GPIOA,GPIO_InitStructure);// PA10 RXGPIO_InitStructure.GPIO_PinGPIO_Pin_10;GPIO_InitStructure.GPIO_ModeGPIO_Mode_IN_FLOATING;GPIO_Init(GPIOA,GPIO_InitStructure);USART_InitStructure.USART_BaudRatebaudrate;USART_InitStructure.USART_WordLengthUSART_WordLength_8b;USART_InitStructure.USART_StopBitsUSART_StopBits_1;USART_InitStructure.USART_ParityUSART_Parity_No;USART_InitStructure.USART_HardwareFlowControlUSART_HardwareFlowControl_None;USART_InitStructure.USART_ModeUSART_Mode_Tx|USART_Mode_Rx;USART_Init(USART1,USART_InitStructure);// 使能接收中断USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);// 配置 NVIC优先级 5可安全调用 RTOS APINVIC_InitStructure.NVIC_IRQChannelUSART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority5;NVIC_InitStructure.NVIC_IRQChannelSubPriority0;NVIC_InitStructure.NVIC_IRQChannelCmdENABLE;NVIC_Init(NVIC_InitStructure);USART_Cmd(USART1,ENABLE);}voidUART_SendString(char*str){while(*str){while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)RESET);USART_SendData(USART1,*str);}}五、中断服务函数stm32f10x_it.c在stm32f10x_it.c中添加串口接收中断处理并将接收到的字符通过队列发送给命令解析任务。#includestm32f10x_it.h#includeFreeRTOS.h#includequeue.hexternQueueHandle_t xUartRxQueue;// 在 main.c 中定义voidUSART1_IRQHandler(void){BaseType_t xHigherPriorityTaskWokenpdFALSE;if(USART_GetITStatus(USART1,USART_IT_RXNE)!RESET){uint8_tchUSART_ReceiveData(USART1);xQueueSendFromISR(xUartRxQueue,ch,xHigherPriorityTaskWoken);// 读取 DR 后 RXNE 标志自动清除无需额外操作}portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}注意如果stm32f10x_it.c中仍保留了之前的按键中断处理函数且本实验不再使用按键可将其注释或删除只保留USART1_IRQHandler即可其他中断服务函数可留空由启动文件的弱定义接管。六、命令解析与系统控制main.c6.1 命令定义所有命令以回车换行\r\n或仅\n为结束。支持的命令LED ONLED OFFFREQ 毫秒值如FREQ 500STATS6.2 全局对象与任务规划xUartRxQueue队列每个元素为uint8_t长度 128xLedTimer软件定时器句柄负责 LED 翻转xStatsSemaphore二值信号量用于立即触发统计打印CmdTask命令解析任务优先级 4StatsTask统计打印任务优先级 3低于命令任务保证命令优先6.3 完整 main.c#includestm32f10x.h#includeFreeRTOS.h#includetask.h#includequeue.h#includesemphr.h#includetimers.h#includestring.h#includestdio.h#includestdlib.h#includebsp_led.h#includebsp_uart.h#includebsp_timer_stats.h/* -------------------- 栈溢出钩子 -------------------- */voidvApplicationStackOverflowHook(TaskHandle_t xTask,char*pcTaskName){taskDISABLE_INTERRUPTS();while(1);}/* -------------------- 全局句柄 -------------------- */QueueHandle_t xUartRxQueueNULL;// 串口接收字节队列TimerHandle_t xLedTimerNULL;// LED 闪烁定时器SemaphoreHandle_t xStatsSemaphoreNULL;// 触发 STATS 打印/* LED 定时器回调 */voidvLedTimerCallback(TimerHandle_t xTimer){LED3_Toggle();}/* 命令解析任务 */voidvCmdTask(void*pvParameters){charcmd_buf[32];uint8_tidx0;uint8_tch;charresponse[64];while(1){// 阻塞等待一个字符if(xQueueReceive(xUartRxQueue,ch,portMAX_DELAY)pdTRUE){if(ch\r||ch\n){if(idx0){cmd_buf[idx]\0;// 字符串结束// 解析并执行命令if(strcmp(cmd_buf,LED ON)0){xTimerStart(xLedTimer,0);strcpy(response,OK: LED started\r\n);}elseif(strcmp(cmd_buf,LED OFF)0){xTimerStop(xLedTimer,0);strcpy(response,OK: LED stopped\r\n);}elseif(strncmp(cmd_buf,FREQ ,5)0){intperiod_msatoi(cmd_buf5);if(period_ms0){xTimerChangePeriod(xLedTimer,pdMS_TO_TICKS(period_ms),0);sprintf(response,OK: FREQ set to %d ms\r\n,period_ms);}else{strcpy(response,ERR: invalid period\r\n);}}elseif(strcmp(cmd_buf,STATS)0){xSemaphoreGive(xStatsSemaphore);// 触发立即打印strcpy(response,OK: stats triggered\r\n);}else{strcpy(response,ERR: unknown command\r\n);}UART_SendString(response);idx0;// 清缓冲区准备接收下一条命令}}else{if(idxsizeof(cmd_buf)-1){cmd_buf[idx]ch;}}}}}/* 统计打印任务 */voidvStatsTask(void*pvParameters){charbuffer[1024];while(1){// 每 10 秒自动打印一次或被 STATS 命令立即触发if(xSemaphoreTake(xStatsSemaphore,pdMS_TO_TICKS(10000))pdTRUE){// 由命令触发不改变自动打印周期}UART_SendString(\r\n Task List \r\n);vTaskList(buffer);UART_SendString(buffer);UART_SendString(\r\n RunTime Stats \r\n);vTaskGetRunTimeStats(buffer);UART_SendString(buffer);}}intmain(void){NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);LED_InitAll();UART1_Init(115200);// 创建串口接收字节队列长度 128xUartRxQueuexQueueCreate(128,sizeof(uint8_t));if(xUartRxQueueNULL)while(1);// 创建 LED 闪烁软件定时器初始周期 500ms默认不启动xLedTimerxTimerCreate(LedTimer,pdMS_TO_TICKS(500),pdTRUE,// 自动重载(void*)0,vLedTimerCallback);if(xLedTimerNULL)while(1);// 创建二值信号量用于触发统计打印xStatsSemaphorexSemaphoreCreateBinary();if(xStatsSemaphoreNULL)while(1);// 创建命令解析任务xTaskCreate(vCmdTask,Cmd,256,NULL,4,NULL);// 创建统计任务xTaskCreate(vStatsTask,Stats,512,NULL,3,NULL);UART_SendString(System ready.\r\nCommands: LED ON / LED OFF / FREQ ms / STATS\r\n);vTaskStartScheduler();while(1);}说明代码中使用了atoi和sprintf来自 C 标准库需包含stdlib.h和stdio.h。在 Keil 中请勾选Use MicroLIB选项否则可能链接失败。七、FreeRTOSConfig.h补充确保在配置文件中有以下定义前几篇已逐步添加#defineconfigCHECK_FOR_STACK_OVERFLOW2#defineconfigGENERATE_RUN_TIME_STATS1#defineconfigUSE_STATS_FORMATTING_FUNCTIONS1#defineconfigUSE_TIMERS1#includebsp_timer_stats.h#defineportCONFIGURE_TIMER_FOR_RUN_TIME_STATS()vConfigureTimerForRunTimeStats()#defineportGET_RUN_TIME_COUNTER_VALUE()TIM_GetCounter(TIM2)portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()宏会在调度器启动时由内核自动调用无需在main()中手动执行。八、编译下载与验证在 Keil 工程中勾选Use MicroLIB。编译下载至开发板打开串口助手115200, 8N1。开机后串口输出System ready.及命令提示。发送LED ON观察 LED 以默认 500ms 周期闪烁并收到OK: LED started。发送FREQ 100LED 变为快速闪烁周期 200ms收到OK: FREQ set to 100 ms。发送LED OFFLED 停止收到OK: LED stopped。发送STATS立即输出任务列表和 CPU 利用率。如果不发送 STATS系统也会每 10 秒自动输出一次统计信息。九、总结本篇综合运用了 FreeRTOS 的任务管理、队列、软件定时器、二值信号量、中断管理和调试统计功能实现了一个具有实用价值的串口命令控制系统。通过这个项目你应该能够熟练搭建标准库 FreeRTOS 的工程灵活运用队列在中断与任务间传递数据用软件定时器替代硬件定时器实现周期性动作通过信号量实现不同任务间的快速同步利用运行时间统计和栈溢出检测对系统进行监控和优化至此本系列教程已圆满完成。希望这十篇文章能够为你打下坚实的 FreeRTOS 基础让你在后续的实际项目中更加得心应手。如果你有任何疑问或想要探讨更深入的主题欢迎在评论区留言交流。