哈喽各位嵌入式新手宝子们!上一期给大家拆解了FreeRTOS的核心知识点,很多宝子留言说“知识点懂了,但一上手写代码就懵”“不知道怎么把任务、队列、信号量用在实际项目里”。
太懂这种感受了!FreeRTOS的难点不在于记概念,而在于“落地实操”——怎么创建任务、怎么用队列传数据、怎么用信号量实现同步,这些只有结合实例才能真正学会。
今天这篇专栏,就给大家带来3个FreeRTOS高频实战实例,全部基于STM32F103C8T6(新手最常用的最小系统板),从简单到复杂,附完整可复制代码+逐行解析+避坑提醒,新手跟着抄就能跑通,学会直接套用在自己的项目里(比如电机控制、串口通信、传感器采集)!
先说明:本文默认你已经掌握FreeRTOS基础知识点(任务、队列、信号量),如果还没看基础篇,可以先回去补补课~ 所有实例均用Keil5编写,代码注释拉满,不用懂底层源码,直接复制运行!
✅ 硬件:STM32F103C8T6最小系统板、LED灯2个、按键1个、USB-TTL模块(用于串口通信)、杜邦线若干;
✅ 软件:Keil5(已安装STM32F103固件库)、FreeRTOS源码(推荐V10.0.0,新手最稳定,文末附获取方式);
✅ 核心前提:已在Keil5中配置好FreeRTOS工程(不懂配置的宝子,评论区扣“FreeRTOS工程”,下期更保姆级配置教程)。
【实例场景】:创建2个独立任务,分别控制2个LED灯,实现“LED1每隔500ms闪烁一次,LED2每隔1000ms闪烁一次”,两个任务互不干扰,体会FreeRTOS多任务调度的核心逻辑。
【核心知识点】:任务创建(xTaskCreate)、任务延时(vTaskDelay)、任务优先级配置。
LED1 → PB0(STM32引脚),LED2 → PB1(STM32引脚),均采用共阴极接法(LED正极接电阻,电阻另一端接3.3V,LED负极接STM32引脚)。
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
// LED引脚定义
#define LED1_PIN GPIO_Pin_0
#define LED2_PIN GPIO_Pin_1
#define LED_PORT GPIOB
// 函数声明
void GPIO_Init_Config(void); // GPIO初始化(控制LED)
void LED1_Task(void *pvParameters); // LED1闪烁任务
void LED2_Task(void *pvParameters); // LED2闪烁任务
int main(void)
{
// 1. 初始化GPIO(控制LED)
GPIO_Init_Config();
// 2. 创建FreeRTOS任务(重点!)
// 参数说明:任务函数、任务名称、任务栈大小、任务参数、任务优先级、任务句柄
xTaskCreate(
LED1_Task, // 任务函数
"LED1_Flash", // 任务名称(仅用于调试)
512, // 任务栈大小(新手默认512字节)
NULL, // 任务参数(无则填NULL)
1, // 任务优先级(1-15,数值越大优先级越高)
NULL // 任务句柄(无则填NULL)
);
xTaskCreate(
LED2_Task,
"LED2_Flash",
512,
NULL,
1, // 两个任务优先级相同,调度器轮流执行
NULL
);
// 3. 启动FreeRTOS调度器(启动后,任务开始执行)
vTaskStartScheduler();
// 以下代码永远不会执行(调度器启动后,CPU被任务占用)
while(1)
{
}
}
// GPIO初始化函数(控制LED引脚)
void GPIO_Init_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置PB0、PB1为推挽输出模式
GPIO_InitStructure.GPIO_Pin = LED1_PIN | LED2_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LED_PORT, &GPIO_InitStructure);
// 初始化LED为熄灭状态(低电平)
GPIO_SetBits(LED_PORT, LED1_PIN | LED2_PIN);
}
// LED1闪烁任务(任务函数必须是void类型,参数为void*)
void LED1_Task(void *pvParameters)
{
while(1) // 任务循环(FreeRTOS任务必须是死循环,否则会报错)
{
GPIO_ResetBits(LED_PORT, LED1_PIN); // LED1点亮(低电平)
vTaskDelay(500); // 延时500ms(FreeRTOS延时,延时期间CPU执行其他任务)
GPIO_SetBits(LED_PORT, LED1_PIN); // LED1熄灭(高电平)
vTaskDelay(500); // 再延时500ms
}
}
// LED2闪烁任务
void LED2_Task(void *pvParameters)
{
while(1)
{
GPIO_ResetBits(LED_PORT, LED2_PIN); // LED2点亮
vTaskDelay(1000); // 延时1000ms
GPIO_SetBits(LED_PORT, LED2_PIN); // LED2熄灭
vTaskDelay(1000); // 再延时1000ms
}
} 1. 任务创建(xTaskCreate):这是FreeRTOS多任务的核心函数,5个关键参数一定要记牢——任务函数(要执行的逻辑)、任务名称(调试用)、栈大小(512字节足够新手使用)、任务参数(无则填NULL)、优先级(同优先级轮流执行);
2. 调度器启动(vTaskStartScheduler):一旦启动,FreeRTOS会自动调度任务,不用我们手动控制,同优先级的LED1_Task和LED2_Task会轮流执行,看起来就像两个LED同时闪烁;
3. 任务函数:必须是void类型、参数为void*,且内部必须有死循环(while(1))——如果任务执行完就退出,会导致FreeRTOS系统崩溃;
4. 任务延时(vTaskDelay):和裸机的delay函数不同,vTaskDelay延时期间,CPU会去执行其他任务(比如LED1延时的时候,CPU执行LED2任务),不会浪费CPU资源。
❌ 任务栈设置太小:比如设为128字节,会导致任务栈溢出,系统死机,新手默认512字节即可;
❌ 任务函数没有死循环:任务执行完后退出,会触发FreeRTOS断言错误,程序卡死;
✅ 同优先级任务:调度器会轮流执行,每个任务执行一段时间后切换,新手可以通过修改延时时间,观察任务切换的效果。
【实例场景】:创建2个任务(串口接收任务、数据处理任务),通过队列实现数据传递——串口接收任务收到电脑发送的数据后,将数据放入队列,数据处理任务从队列中取出数据,解析后通过串口回传,模拟工业级串口通信场景。
【核心知识点】:队列创建(xQueueCreate)、队列发送(xQueueSend)、队列接收(xQueueReceive)、中断中使用队列(xQueueSendFromISR)。
【补充】:该实例用到串口中断,核心逻辑符合工业级标准——中断接收数据,队列传递数据,任务处理数据,能有效避免串口数据丢失、乱码问题。
USB-TTL模块 → STM32串口1:TX(USB-TTL)→ PA10(STM32 USART1_RX),RX(USB-TTL)→ PA9(STM32 USART1_TX),GND共地。
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "stdio.h"
// 串口相关定义
#define USART1_GPIO_PORT GPIOA
#define USART1_TX_PIN GPIO_Pin_9
#define USART1_RX_PIN GPIO_Pin_10
// 队列句柄(全局变量,用于中断和任务之间访问队列)
QueueHandle_t uart_queue;
// 函数声明
void USART1_Init_Config(void); // 串口1初始化(115200波特率)
void USART1_IRQHandler(void); // 串口1中断服务函数
void UART_Receive_Task(void *pvParameters); // 串口接收任务(中断配合队列)
void Data_Process_Task(void *pvParameters); // 数据处理任务
int main(void)
{
// 1. 初始化串口1
USART1_Init_Config();
// 2. 创建队列(重点!)
// 参数说明:队列长度(可存储10个数据)、每个数据的大小(1字节,对应串口接收的1个字节)
uart_queue = xQueueCreate(10, sizeof(uint8_t));
// 3. 创建任务
// 串口接收任务(优先级高,确保及时接收数据)
xTaskCreate(
UART_Receive_Task,
"UART_Receive",
1024, // 串口任务需要较大栈空间,设为1024字节
NULL,
3, // 优先级高于数据处理任务
NULL
);
// 数据处理任务(优先级低,不影响接收)
xTaskCreate(
Data_Process_Task,
"Data_Process",
1024,
NULL,
2,
NULL
);
// 4. 启动调度器
vTaskStartScheduler();
while(1)
{
}
}
// 串口1初始化(115200波特率,8位数据位,1位停止位,无校验)
void USART1_Init_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 使能GPIOA和USART1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
// 配置TX引脚(PA9)为复用推挽输出
GPIO_InitStructure.GPIO_Pin = USART1_TX_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(USART1_GPIO_PORT, &GPIO_InitStructure);
// 配置RX引脚(PA10)为浮空输入
GPIO_InitStructure.GPIO_Pin = USART1_RX_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(USART1_GPIO_PORT, &GPIO_InitStructure);
// 配置串口参数
USART_InitStructure.USART_BaudRate = 115200; // 波特率115200
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位数据位
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 1位停止位
USART_InitStructure.USART_Parity = USART_Parity_No; // 无校验
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
USART_Init(USART1, &USART_InitStructure);
// 配置串口中断(接收中断)
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收中断(收到数据触发)
// 配置NVIC(中断优先级)
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 响应优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 使能串口1
USART_Cmd(USART1, ENABLE);
}
// 串口1中断服务函数(收到数据后,将数据放入队列)
void USART1_IRQHandler(void)
{
uint8_t rec_data; // 存储接收的数据
BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 标记是否需要切换任务
// 判断是否是接收中断(收到数据)
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
rec_data = USART_ReceiveData(USART1); // 读取接收的数据
// 中断中发送数据到队列(重点!中断中必须用FromISR结尾的API)
xQueueSendFromISR(
uart_queue, // 队列句柄
&rec_data, // 要发送的数据地址
&xHigherPriorityTaskWoken // 标记是否需要切换任务
);
USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除中断标志位
}
// 如果需要切换任务,触发任务切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 串口接收任务(从队列中取出数据,这里仅做转发,实际可扩展)
void UART_Receive_Task(void *pvParameters)
{
uint8_t rec_data;
while(1)
{
// 从队列中接收数据(队列空时,任务阻塞等待,不浪费CPU资源)
// 参数:队列句柄、接收数据地址、阻塞时间(portMAX_DELAY表示无限等待)
xQueueReceive(uart_queue, &rec_data, portMAX_DELAY);
// 将接收的数据发送到数据处理任务(这里直接转发,实际可添加过滤逻辑)
xQueueSend(uart_queue, &rec_data, 0); // 发送到同一个队列,供处理任务读取
}
}
// 数据处理任务(从队列中取出数据,解析后回传)
void Data_Process_Task(void *pvParameters)
{
uint8_t process_data;
while(1)
{
// 从队列中取出数据(阻塞等待)
xQueueReceive(uart_queue, &process_data, portMAX_DELAY);
// 数据处理(这里简单回传,实际可扩展为解析指令、控制外设等)
USART_SendData(USART1, process_data); // 将数据回传给电脑
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送完成
// 回传提示信息
char tip[] = " 已收到并回传数据\r\n";
for(uint8_t i=0; i<strlen(tip); i++)
{
USART_SendData(USART1, tip[i]);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
}
} 1. 队列创建(xQueueCreate):创建一个可存储10个字节的队列,用于存储串口接收的数据,队列是“线程安全”的,避免多个任务/中断同时操作数据导致错乱;
2. 中断中使用队列:中断服务函数中,收到数据后必须用xQueueSendFromISR(中断专用API)发送数据到队列,不能用普通的xQueueSend,否则会导致系统崩溃;
3. 队列接收(xQueueReceive):两个任务都调用该函数从队列中取数据,队列空时,任务会阻塞等待(portMAX_DELAY表示无限等待),不会占用CPU资源;
4. 优先级配置:串口接收任务优先级(3)高于数据处理任务(2),确保串口数据能及时接收,不会因为处理任务占用CPU而丢失数据。
❌ 中断中使用普通队列API:比如用xQueueSend代替xQueueSendFromISR,会导致FreeRTOS系统崩溃,中断中必须用FromISR结尾的API;
❌ 队列长度设置太小:比如设为1,当串口连续发送数据时,队列会满,导致数据丢失,新手设为10~20即可;
✅ 队列阻塞等待:xQueueReceive的第三个参数设为portMAX_DELAY,避免任务空循环占用CPU,这是FreeRTOS高效运行的关键。
【实例场景】:创建1个按键中断、1个按键处理任务,用二值信号量实现同步——按下按键后,触发中断,中断释放信号量,按键处理任务收到信号量后,执行LED翻转操作,模拟“按键控制LED”的实际场景,避免中断中执行复杂逻辑。
【核心知识点】:二值信号量创建(xSemaphoreCreateBinary)、信号量释放(xSemaphoreGive/xSemaphoreGiveFromISR)、信号量获取(xSemaphoreTake)。
【补充】:二值信号量是FreeRTOS同步的核心,常用于中断通知任务、任务之间同步,只有“有信号”和“无信号”两种状态,操作简单且高效。
按键 → PA0(STM32引脚),采用上拉输入(按键一端接GND,另一端接PA0,STM32内部上拉);LED → PB0(同实例1)。
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// 引脚定义
#define KEY_PIN GPIO_Pin_0
#define KEY_PORT GPIOA
#define LED_PIN GPIO_Pin_0
#define LED_PORT GPIOB
// 二值信号量句柄(全局变量)
SemaphoreHandle_t key_semaphore;
// 函数声明
void GPIO_Init_Config(void); // GPIO初始化(按键+LED)
void EXTI0_IRQHandler(void); // 按键中断服务函数
void Key_Process_Task(void *pvParameters); // 按键处理任务
int main(void)
{
// 1. 初始化GPIO(按键+LED)
GPIO_Init_Config();
// 2. 创建二值信号量(重点!)
key_semaphore = xSemaphoreCreateBinary();
// 3. 创建按键处理任务
xTaskCreate(
Key_Process_Task,
"Key_Process",
512,
NULL,
2,
NULL
);
// 4. 启动调度器
vTaskStartScheduler();
while(1)
{
}
}
// GPIO初始化(按键:上拉输入;LED:推挽输出)
void GPIO_Init_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 使能GPIOA、GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);
// 配置按键引脚(PA0):上拉输入
GPIO_InitStructure.GPIO_Pin = KEY_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入(默认高电平,按下变低)
GPIO_Init(KEY_PORT, &GPIO_InitStructure);
// 配置LED引脚(PB0):推挽输出
GPIO_InitStructure.GPIO_Pin = LED_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LED_PORT, &GPIO_InitStructure);
GPIO_SetBits(LED_PORT, LED_PIN); // LED初始熄灭
// 配置按键中断(外部中断0,对应PA0)
EXTI_InitTypeDef EXTI_InitStructure;
// 连接PA0到外部中断0
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
// 配置外部中断0
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发(按键按下时)
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 配置NVIC(中断优先级)
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
// 按键中断服务函数(按下按键,释放信号量)
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 判断是否是外部中断0触发
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 消抖(简单延时,实际可优化为多次检测)
for(uint32_t i=0; i<10000; i++);
// 再次判断按键是否按下(消抖后)
if(GPIO_ReadInputDataBit(KEY_PORT, KEY_PIN) == 0)
{
// 中断中释放二值信号量(重点!中断中用FromISR结尾的API)
xSemaphoreGiveFromISR(key_semaphore, &xHigherPriorityTaskWoken);
}
EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志位
}
// 触发任务切换(如果需要)
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 按键处理任务(获取信号量,执行LED翻转)
void Key_Process_Task(void *pvParameters)
{
while(1)
{
// 获取二值信号量(阻塞等待,等待按键中断释放信号量)
// 参数:信号量句柄、阻塞时间(portMAX_DELAY表示无限等待)
if(xSemaphoreTake(key_semaphore, portMAX_DELAY) == pdTRUE)
{
// 收到信号量,执行LED翻转
GPIO_WriteBit(LED_PORT, LED_PIN, !GPIO_ReadOutputDataBit(LED_PORT, LED_PIN));
}
}
} 1. 二值信号量创建(xSemaphoreCreateBinary):创建一个二值信号量,初始状态为“无信号”(需要先释放信号量,任务才能获取);
2. 中断中释放信号量:按键按下触发中断,中断中用xSemaphoreGiveFromISR释放信号量,通知按键处理任务“按键被按下”;
3. 任务中获取信号量:按键处理任务调用xSemaphoreTake获取信号量,信号量未释放时,任务阻塞等待,不会占用CPU资源;信号量释放后,任务立即执行LED翻转操作;
4. 中断消抖:简单的延时消抖,避免按键抖动导致多次触发中断、释放信号量,实际项目中可优化为多次检测按键电平。
❌ 中断中执行复杂逻辑:比如在中断中直接翻转LED,会导致中断执行时间过长,影响其他任务/中断,正确做法是用信号量通知任务,由任务执行复杂逻辑;
❌ 忘记消抖:按键抖动会导致多次触发中断,释放多个信号量,导致LED多次翻转,必须添加消抖逻辑;
✅ 信号量阻塞等待:xSemaphoreTake的阻塞时间设为portMAX_DELAY,让任务在没有信号时阻塞,避免空循环占用CPU。
以上3个实例,覆盖了FreeRTOS最常用的3个场景,新手掌握后,能应对80%的嵌入式项目需求:
1. 多任务LED闪烁:入门必练,掌握任务创建、优先级配置、任务延时的核心用法;
2. 队列串口数据传递:工业级常用,掌握队列的创建、发送、接收,以及中断中使用队列的技巧;
3. 二值信号量按键同步:实用场景,掌握信号量的创建、释放、获取,以及中断与任务的同步方法。
1. 先跑通代码:把上面3个实例的代码复制到Keil5,烧录到STM32,观察运行效果,理解每个函数的作用;
2. 修改参数练习:比如修改任务优先级、队列长度、信号量阻塞时间,观察程序运行变化,加深理解;
3. 结合之前的步进电机:尝试将实例1和步进电机结合,创建“电机调速任务”和“LED闪烁任务”,实现多任务控制;
4. 学习互斥锁:如果多个任务操作同一个外设(比如OLED屏),可以学习互斥锁(xSemaphoreCreateMutex),避免资源竞争。
为了方便新手上手,我整理了:
① 3个实例的完整Keil5工程文件(包含FreeRTOS源码,无需手动配置);
② FreeRTOS核心API手册(新手常用API分类整理,一目了然);
评论区扣“FreeRTOS实例”,自动发送!
后续我会更新更复杂的FreeRTOS实例:比如多任务电机控制、定时器任务、队列+信号量组合使用,关注我,嵌入式进阶不迷路~
如果遇到代码报错、程序跑不通的问题,评论区留言,我会一一回复,帮你避坑!觉得有用的话,点赞+收藏,新手一起进步~