☰
FreeRTOS 综合实战:串口命令控制 LED 闪烁模式与系统监控
2026/10/10 10:02:23 网站建设 项目流程

本系列前九篇文章依次深入了工程搭建、任务管理、队列、信号量、互斥量、软件定时器、中断管理以及调试优化。本篇将通过一个完整的实战项目——串口命令控制 LED 闪烁模式与系统状态监控,把前面所学知识全部串联起来。你可以把它作为学习 FreeRTOS 的毕业设计,也可以当作后续复杂项目的模板。


一、项目需求

  • 通过串口助手发送文本命令,开发板解析命令并执行相应操作。
  • 支持的命令:
    • LED ON—— 启动 LED 闪烁(PC13)
    • LED OFF—— 停止 LED 闪烁
    • FREQ <毫秒>—— 修改 LED 闪烁周期,例如FREQ 200表示 200ms 亮灭切换一次
    • STATS—— 立即打印一次任务列表与 CPU 利用率统计
  • 系统持续运行,PC13 LED 默认不闪烁。
  • 串口回显命令执行结果(OK / ERR)。
  • 保留一个后台统计任务,每 10 秒自动输出一次系统状态。

二、硬件连接

  • STM32F103C8T6 最小系统板
  • PC13—— 板载 LED(低电平点亮)
  • PA9—— USART1 TX,接串口模块的 RX
  • PA10—— USART1 RX,接串口模块的 TX
  • 串口模块波特率 115200,8N1

三、软件架构设计

串口中断(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#include"stm32f10x.h"voidLED_InitAll(void);voidLED3_Toggle(void);#endif

4.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#include"stm32f10x.h"voidUART1_Init(uint32_tbaudrate);voidUART_SendString(char*str);#endif

bsp_uart.c(增加了中断配置)

#include"bsp_uart.h"voidUART1_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_Pin=GPIO_Pin_9;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);// PA10 = RXGPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;GPIO_Init(GPIOA,&GPIO_InitStructure);USART_InitStructure.USART_BaudRate=baudrate;USART_InitStructure.USART_WordLength=USART_WordLength_8b;USART_InitStructure.USART_StopBits=USART_StopBits_1;USART_InitStructure.USART_Parity=USART_Parity_No;USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;USART_InitStructure.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;USART_Init(USART1,&USART_InitStructure);// 使能接收中断USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);// 配置 NVIC,优先级 5(可安全调用 RTOS API)NVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=5;NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;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中添加串口接收中断处理,并将接收到的字符通过队列发送给命令解析任务。

#include"stm32f10x_it.h"#include"FreeRTOS.h"#include"queue.h"externQueueHandle_t xUartRxQueue;// 在 main.c 中定义voidUSART1_IRQHandler(void){BaseType_t xHigherPriorityTaskWoken=pdFALSE;if(USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET){uint8_tch=USART_ReceiveData(USART1);xQueueSendFromISR(xUartRxQueue,&ch,&xHigherPriorityTaskWoken);// 读取 DR 后 RXNE 标志自动清除,无需额外操作}portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}

注意:如果stm32f10x_it.c中仍保留了之前的按键中断处理函数,且本实验不再使用按键,可将其注释或删除,只保留USART1_IRQHandler即可,其他中断服务函数可留空(由启动文件的弱定义接管)。


六、命令解析与系统控制(main.c)

6.1 命令定义

所有命令以回车换行\r\n(或仅\n)为结束。支持的命令:

  • LED ON
  • LED OFF
  • FREQ <毫秒值>(如FREQ 500)
  • STATS

6.2 全局对象与任务规划

  • xUartRxQueue:队列,每个元素为uint8_t,长度 128
  • xLedTimer:软件定时器句柄,负责 LED 翻转
  • xStatsSemaphore:二值信号量,用于立即触发统计打印
  • CmdTask:命令解析任务,优先级 4
  • StatsTask:统计打印任务,优先级 3(低于命令任务,保证命令优先)

6.3 完整 main.c

#include"stm32f10x.h"#include"FreeRTOS.h"#include"task.h"#include"queue.h"#include"semphr.h"#include"timers.h"#include<string.h>#include<stdio.h>#include<stdlib.h>#include"bsp_led.h"#include"bsp_uart.h"#include"bsp_timer_stats.h"/* -------------------- 栈溢出钩子 -------------------- */voidvApplicationStackOverflowHook(TaskHandle_t xTask,char*pcTaskName){taskDISABLE_INTERRUPTS();while(1);}/* -------------------- 全局句柄 -------------------- */QueueHandle_t xUartRxQueue=NULL;// 串口接收字节队列TimerHandle_t xLedTimer=NULL;// LED 闪烁定时器SemaphoreHandle_t xStatsSemaphore=NULL;// 触发 STATS 打印/* LED 定时器回调 */voidvLedTimerCallback(TimerHandle_t xTimer){LED3_Toggle();}/* 命令解析任务 */voidvCmdTask(void*pvParameters){charcmd_buf[32];uint8_tidx=0;uint8_tch;charresponse[64];while(1){// 阻塞等待一个字符if(xQueueReceive(xUartRxQueue,&ch,portMAX_DELAY)==pdTRUE){if(ch=='\r'||ch=='\n'){if(idx>0){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_ms=atoi(cmd_buf+5);if(period_ms>0){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);idx=0;// 清缓冲区,准备接收下一条命令}}else{if(idx<sizeof(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);// 创建串口接收字节队列(长度 128)xUartRxQueue=xQueueCreate(128,sizeof(uint8_t));if(xUartRxQueue==NULL)while(1);// 创建 LED 闪烁软件定时器,初始周期 500ms,默认不启动xLedTimer=xTimerCreate("LedTimer",pdMS_TO_TICKS(500),pdTRUE,// 自动重载(void*)0,vLedTimerCallback);if(xLedTimer==NULL)while(1);// 创建二值信号量,用于触发统计打印xStatsSemaphore=xSemaphoreCreateBinary();if(xStatsSemaphore==NULL)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#include"bsp_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()中手动执行。


八、编译下载与验证

  1. 在 Keil 工程中勾选Use MicroLIB。
  2. 编译下载至开发板,打开串口助手(115200, 8N1)。
  3. 开机后串口输出System ready.及命令提示。
  4. 发送LED ON,观察 LED 以默认 500ms 周期闪烁,并收到OK: LED started。
  5. 发送FREQ 100,LED 变为快速闪烁(周期 200ms),收到OK: FREQ set to 100 ms。
  6. 发送LED OFF,LED 停止,收到OK: LED stopped。
  7. 发送STATS,立即输出任务列表和 CPU 利用率。
  8. 如果不发送 STATS,系统也会每 10 秒自动输出一次统计信息。

九、总结

本篇综合运用了 FreeRTOS 的任务管理、队列、软件定时器、二值信号量、中断管理和调试统计功能,实现了一个具有实用价值的串口命令控制系统。通过这个项目,你应该能够:

  • 熟练搭建标准库 + FreeRTOS 的工程
  • 灵活运用队列在中断与任务间传递数据
  • 用软件定时器替代硬件定时器实现周期性动作
  • 通过信号量实现不同任务间的快速同步
  • 利用运行时间统计和栈溢出检测对系统进行监控和优化

至此,本系列教程已圆满完成。希望这十篇文章能够为你打下坚实的 FreeRTOS 基础,让你在后续的实际项目中更加得心应手。如果你有任何疑问或想要探讨更深入的主题,欢迎在评论区留言交流。

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

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

立即咨询