FreeRTOS 综合实战:串口命令控制 LED 闪烁模式与系统监控

本系列前九篇文章依次深入了工程搭建、任务管理、队列、信号量、互斥量、软件定时器、中断管理以及调试优化。本篇将通过一个完整的实战项目------串口命令控制 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 ON
  • LED OFF
  • FREQ <毫秒值> (如 FREQ 500
  • STATS

6.2 全局对象与任务规划

  • xUartRxQueue:队列,每个元素为 uint8_t,长度 128
  • xLedTimer:软件定时器句柄,负责 LED 翻转
  • xStatsSemaphore:二值信号量,用于立即触发统计打印
  • CmdTask:命令解析任务,优先级 4
  • StatsTask:统计打印任务,优先级 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);
}

说明 :代码中使用了 atoisprintf,来自 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() 中手动执行。


八、编译下载与验证

  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 基础,让你在后续的实际项目中更加得心应手。如果你有任何疑问或想要探讨更深入的主题,欢迎在评论区留言交流。

相关推荐
Szime18 小时前
全球首创10位40GSPS超宽带ADC选型参考:国产超高速ADC深智微科技选型支持
科技·单片机·嵌入式硬件·fpga开发
lularible18 小时前
从沙子到车辙(7.4):《兰亭集序》的启示
开源·嵌入式·汽车电子
(Morgan)19 小时前
51单片机期末复习知识点总结
stm32·单片机·嵌入式硬件
榴莲llll21 小时前
应用于计时器/微波炉等产品的高亮LED数显驱动VK16K33C 数码管屏显驱动芯片
单片机
华一精品Adreamer1 天前
T606 vs 骁龙662/RK3566:主流安卓+4G定制平板芯片横向测评指南
单片机
Zyed1 天前
[STM32]Day9-Part1USART+串口接收+串口收发
stm32·单片机·嵌入式硬件
l'm coming1 天前
[linux]内核启动加载驱动文件的流程
linux·arm开发·驱动开发·嵌入式
小慧10241 天前
手动建立工程模板
stm32·单片机
嵌入式ZYXC1 天前
STM32烧录一次后无法再次烧录的两种原因
stm32·单片机·嵌入式硬件
踏着七彩祥云的小丑1 天前
嵌入式测试学习第33 天:压力测试、反复开关机、反复插拔接口测试
单片机·嵌入式硬件·学习