本系列前九篇文章依次深入了工程搭建、任务管理、队列、信号量、互斥量、软件定时器、中断管理以及调试优化。本篇将通过一个完整的实战项目------串口命令控制 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 中包含:
c
#ifndef BSP_LED_H
#define BSP_LED_H
#include "stm32f10x.h"
void LED_InitAll(void);
void LED3_Toggle(void);
#endif
4.2 运行时间统计定时器(bsp_timer_stats.h / .c)
与第九篇完全相同,继续使用 TIM2 提供 1 MHz 时基,并定义宏:
c
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() vConfigureTimerForRunTimeStats()
#define portGET_RUN_TIME_COUNTER_VALUE() TIM_GetCounter(TIM2)
4.3 串口驱动增强(bsp_uart.h / .c)
在前文的基础上,为 USART1 增加接收中断支持。
bsp_uart.h
c
#ifndef BSP_UART_H
#define BSP_UART_H
#include "stm32f10x.h"
void UART1_Init(uint32_t baudrate);
void UART_SendString(char *str);
#endif
bsp_uart.c(增加了中断配置)
c
#include "bsp_uart.h"
void UART1_Init(uint32_t baudrate)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// PA9 = TX
GPIO_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 = RX
GPIO_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);
}
void UART_SendString(char *str)
{
while (*str)
{
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, *str++);
}
}
五、中断服务函数(stm32f10x_it.c)
在 stm32f10x_it.c 中添加串口接收中断处理,并将接收到的字符通过队列发送给命令解析任务。
c
#include "stm32f10x_it.h"
#include "FreeRTOS.h"
#include "queue.h"
extern QueueHandle_t xUartRxQueue; // 在 main.c 中定义
void USART1_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t ch = 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 ONLED OFFFREQ <毫秒值>(如FREQ 500)STATS
6.2 全局对象与任务规划
xUartRxQueue:队列,每个元素为uint8_t,长度 128xLedTimer:软件定时器句柄,负责 LED 翻转xStatsSemaphore:二值信号量,用于立即触发统计打印CmdTask:命令解析任务,优先级 4StatsTask:统计打印任务,优先级 3(低于命令任务,保证命令优先)
6.3 完整 main.c
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"
/* -------------------- 栈溢出钩子 -------------------- */
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
taskDISABLE_INTERRUPTS();
while (1);
}
/* -------------------- 全局句柄 -------------------- */
QueueHandle_t xUartRxQueue = NULL; // 串口接收字节队列
TimerHandle_t xLedTimer = NULL; // LED 闪烁定时器
SemaphoreHandle_t xStatsSemaphore = NULL; // 触发 STATS 打印
/* LED 定时器回调 */
void vLedTimerCallback(TimerHandle_t xTimer)
{
LED3_Toggle();
}
/* 命令解析任务 */
void vCmdTask(void *pvParameters)
{
char cmd_buf[32];
uint8_t idx = 0;
uint8_t ch;
char response[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");
}
else if (strcmp(cmd_buf, "LED OFF") == 0)
{
xTimerStop(xLedTimer, 0);
strcpy(response, "OK: LED stopped\r\n");
}
else if (strncmp(cmd_buf, "FREQ ", 5) == 0)
{
int period_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");
}
}
else if (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;
}
}
}
}
}
/* 统计打印任务 */
void vStatsTask(void *pvParameters)
{
char buffer[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);
}
}
int main(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 补充
确保在配置文件中有以下定义(前几篇已逐步添加):
c
#define configCHECK_FOR_STACK_OVERFLOW 2
#define configGENERATE_RUN_TIME_STATS 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
#define configUSE_TIMERS 1
#include "bsp_timer_stats.h"
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() vConfigureTimerForRunTimeStats()
#define portGET_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 100,LED 变为快速闪烁(周期 200ms),收到OK: FREQ set to 100 ms。 - 发送
LED OFF,LED 停止,收到OK: LED stopped。 - 发送
STATS,立即输出任务列表和 CPU 利用率。 - 如果不发送 STATS,系统也会每 10 秒自动输出一次统计信息。
九、总结
本篇综合运用了 FreeRTOS 的任务管理、队列、软件定时器、二值信号量、中断管理和调试统计功能,实现了一个具有实用价值的串口命令控制系统。通过这个项目,你应该能够:
- 熟练搭建标准库 + FreeRTOS 的工程
- 灵活运用队列在中断与任务间传递数据
- 用软件定时器替代硬件定时器实现周期性动作
- 通过信号量实现不同任务间的快速同步
- 利用运行时间统计和栈溢出检测对系统进行监控和优化
至此,本系列教程已圆满完成。希望这十篇文章能够为你打下坚实的 FreeRTOS 基础,让你在后续的实际项目中更加得心应手。如果你有任何疑问或想要探讨更深入的主题,欢迎在评论区留言交流。