freeRTOS
1》什么是RTOS
RTOS全称 Real Time Operating System,中文名就是实时操作系统。
1、RTOS全称 Real Time Operating System,中文名就是实时操作系统
2、RTOS是指一类操作系统。而不是单指某一个操作系统,比如UCOS,FreeRTOS,RT-Thread等这些都是RTOS操作系统
3、在单片机开发过程中有两种开发方式
1、裸机开发
2、RTOS开发
两者的区别
1 | 裸机开发 |
2》什么是freeRTOS
1、根据名字,我们可以分成两部分:free和rtos,free就是免费、自由,不受约束的意思。
2、是RTOS中的一种,freeRTOS十分小巧,可以在很多有限的微控制器上进行运行,单从文件上来说就要比UCOS小得多
3》为什么要用freeRTOS
1、freertos是免费的这点是选择它的最重要的一点,企业做产品肯定是要控制成本的,所以就这一点我们的freertos就是最好选择
2、使用者很多,资料多,解决问题时方面查找,市场占有率很高,很多厂商提供的SDK包都支持freertos,尤其是一些带蓝牙、WIFI协议栈的芯片或者模块
3、简单,文件数量少,方便移植和上手
4、可以移植到很多不同的微控制器上,比如STM32 F1\F3\F4\F7上都可以移植
4》什么是任务
任务是一个运行的函数(包括函数和栈),是一段保存在FLASH上的代码,在CPU上运行
ARM架构
F407中有:
CPU:中央处理器(计算单元进行计算)
FLASH:存放代码 通过JLINK等专业的工具烧入到FLASH(保证代码不会被轻易破坏)
RAM:存放数据 可读可写
串口等等模块
CPU和内存的关系
CPU RAM:存放数据
//对于内存来说只有两个功能,将数据读出来和写进去
//要实现a=a+b这条语句CPU需要怎么操作
——————————————
1.读出a变量的值(对应的汇编指令LDR load register)
2.读出b变量的值
3.进行计算a、b的和(对应的汇编指令add)
4.将结果放到a的地址(对应汇编指令STR store)
CUP内部的寄存器 R0、R1……R15;R0-R12可以任意用来计算 R13-R15具有特殊功能:R13-SP(栈),R14-LR(返回地址),R15-PC(当前指令的地址)
ARM7架构处理器采用三级流水线的结构。包括取指(fetch)->译码(decode)->执行(execute)三级。
当第指令执行时,第二指令正在译码,第三条指令正在取指阶段。也就是说当第一条指令在执行时,PC寄存器应当指向第三条指令
也就是说只要处理器是三级流水结构时,PC寄存器总是指向第三条指令
5》任务的状态
1、运行态
2、就绪态
3、阻塞态
4、挂起态(暂停态)
5、停止态
RTOS任务状态转换
在“灯心智启”智能语音台灯项目中,我独立负责了基于FreeRTOS的多任务管理。针对RTOS任务状态转换,我有以下实际经验:
在项目初期,为实现台灯自动调光、坐姿提醒等多功能并发,我采用FreeRTOS进行任务划分。每个功能模块被设计为独立任务,涉及任务的创建、挂起、就绪、运行和阻塞等状态转换。比如,传感器采集任务在等待数据时进入阻塞状态,数据到达后自动切换到就绪状态,由调度器分配CPU资源进入运行状态;当任务需要等待外部事件时,主动调用vTaskSuspend()进入挂起状态,事件触发后通过vTaskResume()恢复。
6》任务之间的转换(参考freertos任务转换图)
图中四个状态的类比
- 挂起态:
就像你暂时离开餐厅去打个电话,不参与排队也不等叫号,什么时候回来随你决定。 - 就绪态:
相当于你在餐厅门口排队,随时准备进餐厅,只要有空位就可以进去吃饭。 - 运行态:
就是你已经坐到餐厅里,正在吃饭,享受服务。 - 阻塞态:
你在等朋友带钥匙或者等菜,暂时不能吃饭(虽然你的座位已经准备好了),只能等事件发生后才能继续。
状态切换的类比
- vTaskSuspend() called(挂起):
你突然有事要先离开餐厅,告诉服务员“先别叫我,等我回来”,于是你进入“挂起态”。 - vTaskResume() called(恢复):
你回来了,告诉服务员“我又可以排队/吃饭了”,于是你回到“就绪态”,准备再次排队等吃饭。 - 调用阻塞API函数(进入阻塞):
你正在餐厅吃饭(运行态),突然发现忘带钱包,需要等朋友送钱来(阻塞态),于是你暂时不能继续吃饭。 - Event(事件发生,解除阻塞):
朋友送钱来了(事件发生),你终于可以继续吃饭(回到就绪态)。 - 就绪态与运行态切换:
排队等到你,轮到你进餐厅了(就绪→运行);你吃完饭或者暂时让出座位(运行→就绪)。
FreeRTOS(一个用于嵌入式系统的实时操作系统)中任务(Task)的几种状态以及它们之间的转换关系
运行态(运行状态):
这是任务正在 CPU 上执行的状态。在单核处理器上,同一时间只能有一个任务处于运行态。
就绪态(就绪状态):
任务已经准备好运行,但正在等待 CPU 的时间片(时间分配)。一旦 CPU 可用,就绪态的任务就可能变为运行态。
阻塞态(阻塞状态):
- 任务因为等待某个事件(比如等待数据或硬件操作完成)而不能继续执行。当它等待的事件发生时,任务会从阻塞态转换为就绪态,等待 CPU 再次分配时间给它。
挂起态(挂起状态):
- 任务被暂时停止执行。它不会消耗 CPU 时间,也不会被调度器调度。任务可以被“唤醒”(即从挂起态转换为就绪态),以便它能够再次运行。
图中还显示了状态之间的转换是如何通过 API 函数调用实现的:
vTaskSuspend()
和vTaskResume()
函数用于将任务从就绪态转换为挂起态,或从挂起态转换为就绪态。- 调用阻塞 API 函数会将任务从就绪态转换为阻塞态。
- 当任务等待的事件发生时,它会自动从阻塞态转换为就绪态。
7》任务优先级(数字越大。优先级越高)
源码获取
1》free官网 https://www.freertos.org/
2》源码文件夹介绍
1、FreeRTOS和FreeRTOS-Plus是freertos的源码
FreeRTOS文件中
Demo 文件夹中是放的针对不同MCU提供的相关例程
License 文件夹中是放相关的许可信息
Source 文件夹中是放FREERTOS的源码,也就是我们后面打交道的重要资料
include 放头文件
portable 放freertos系统和具体硬件之间的桥梁,只需要留下keil、memmang、RVDS三个文件夹,其余的文件夹都可以删除
keil 使用MDK编译环境所需要文件信息
Menmang 内存管理文件
RVDS针对不同架构的MCU做了详细分类,stm32f407可以参考ARM_CM4F中的内容
移植步骤
1、找一个demo工程,在文件夹下新建一个freertos的文件夹
2、将FreeRTOSv9.0.0\FreeRTOS\Source下的所有文件拷贝到新建freertos文件夹下
3、打开KEIL工程,在工程目录下新建一个freertos文件夹并添加如下文件
demo\freertos下的所有.c文件
demo\freertos\portable\MemMang下的heap_4.c文件
demo\freertos\portable\RVDS\ARM_CM4F下的port.c文件
4、添加freertos需要的头文件路径
5、编译报如下错
..\freertos\include\FreeRTOS.h(98): error: #5: cannot open source input file “FreeRTOSConfig.h”: No such file or directory
到源码\FreeRTOSv9.0.0\FreeRTOS\Demo\CORTEX_M4F_STM32F407ZG-SK下将FreeRTOSConfig.h拷贝到\demo\freertos\include下
6、编译报如下错
..\freertos\portable\RVDS\ARM_CM4F\port.c(713): error: #20: identifier “SystemCoreClock” is undefined
对FreeRTOSConfig.h文件的第87-90行代码进行如下修改
//#ifdef ICCARM
#include <stdint.h>
extern uint32_t SystemCoreClock;
//#endif
7、编译报如下错
.\Objects\demo.axf: Error: L6200E: Symbol PendSV_Handler multiply defined (by port.o and stm32f4xx_it.o).
.\Objects\demo.axf: Error: L6200E: Symbol SVC_Handler multiply defined (by port.o and stm32f4xx_it.o).
.\Objects\demo.axf: Error: L6200E: Symbol SysTick_Handler multiply defined (by port.o and stm32f4xx_it.o).
将stm32f4xx_it.c文件下的PendSV_Handler、SVC_Handler、SysTick_Handler屏蔽
8、编译报如下错
.\Objects\demo.axf: Error: L6218E: Undefined symbol vApplicationIdleHook (referred from tasks.o).
.\Objects\demo.axf: Error: L6218E: Undefined symbol vApplicationStackOverflowHook (referred from tasks.o).
.\Objects\demo.axf: Error: L6218E: Undefined symbol vApplicationTickHook (referred from tasks.o).
.\Objects\demo.axf: Error: L6218E: Undefined symbol vApplicationMallocFailedHook (referred from heap_4.o).
在FreeRTOSConfig.h文件中将vApplicationIdleHook、vApplicationStackOverflowHook,vApplicationTickHook,vApplicationMallocFailedHook这四个的钩子函数功能关闭
9、编译0警告、0错误代表移植成功
“.\Objects\demo.axf” - 0 Error(s), 0 Warning(s).
FreeRTOSConfig.h文件用于对freertos系统的配置文件,可以通过对里面的开关进行修改实现对freertos功能的裁剪
1、动态创建任务
1 | BaseType_t xTaskCreate( |
参数1: 指向任务函数的入口,任务的函数名
参数2: 字符串,任务的函数名
参数3: 任务堆栈大小,实际分配的大小是需要乘上4
参数4: 需要传递给任务的参数
参数5: 任务优先级,取值范围0~configMAX_PRIORITIES-1
参数6: 任务句柄
返回值:pdPASS:任务创建成功,会返回任务的句柄
错误码参考projdefs.h中的定义
2、启动动任务调度
vTaskStartScheduler();
-————————————————————————————————————
任务的几个关键函数
xTaskCreate()//使用动态的方法创建一个任务(默认)
xTaskCreateStatic()//使用静态的方法创建一个任务
xTaskCreateRestricted();//创建一个使用MPU(内存管理单元)进行限制的任务,相关内存采用动态内存分配
vTaskDelete();//删除一个任务
vTaskStartScheduler();//启动任务调度
vTaskSuspend();//任务挂起
vTaskResume();//恢复一个挂起任务
xTaskResumeFromISR()//在中断中恢复挂起任务
1、动态创建任务
aseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
参数1: 指向任务函数的入口,任务的函数名
参数2: 字符串,任务的函数名
参数3: 任务堆栈大小,实际分配的大小是需要乘上4
参数4: 需要传递给任务的参数
参数5: 任务优先级,取值范围0~configMAX_PRIORITIES-1
参数6: 任务句柄
返回值:pdPASS:任务创建成功,会返回任务的句柄
错误码参考projdefs.h中的定义
2、静态创建任务接口函数
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer )
参数1: 指向任务函数的入口,任务的函数名
参数2: 字符串,任务的函数名
参数3: 任务堆栈大小,实际分配的大小是需要乘上4
参数4: 需要传递给任务的参数
参数5: 任务优先级,取值范围0~configMAX_PRIORITIES-1
参数6: 任务堆栈,一般是数组,成员类型需要是StackType_t类型
参数7: 任务控制块,必须要指向类型为StaticTask_t的变量,这个变量用于保存创建任务的数据结构(TCB),因此它必须是持久
返回值:NULL,代表创建失败
其它值,代表成功,任务句柄
注意:使用静态创建任务时需要用户自己实现两个函数vApplicationGetIdleTaskMemory()和vApplicationGetTimerTaskMemory();
通过这个两个函数来给空闲任务和定时器服务任务的任务堆栈和任务控制块分配内存
我们可以在main.c文件的main涵数之前进行定义
static StaticTask_t IdleTaskTCB;
static StackType_t dleTaskStack[configMINIMAL_STACK_SIZE];
static StaticTask_t TimerTaskTCB;
static StackType_t TimerTaskStack[configTIMER_TASK_STACK_DEPTH];
void vApplicationGetIdleTaskMemory(StaticTask_t * * ppxIdleTaskTCBBuffer, StackType_t * * ppxIdleTaskStackBuffer, uint32_t * pulIdleTaskStackSize)
{
* ppxIdleTaskTCBBuffer = &IdleTaskTCB;//空闲任务控制块
* ppxIdleTaskStackBuffer = dleTaskStack;//空闲任务的堆栈
* pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;//堆栈大小
}
void vApplicationGetTimerTaskMemory(StaticTask_t * * ppxTimerTaskTCBBuffer, StackType_t * * ppxTimerTaskStackBuffer, uint32_t * pulTimerTaskStackSize)
{
* ppxTimerTaskTCBBuffer = &TimerTaskTCB;//任务控制块
* ppxTimerTaskStackBuffer = TimerTaskStack;//任务堆栈
* pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH;//堆栈大小
}
任务怎么暂停和恢复
假如创建3三个任务
任务1
任务2
任务3
执行过程
任务1-》任务2-》任务3
void 任务1()
{
while(1)
{
A();
———>运行到这个地方时,系统进行了一次调度(切换任务)。
B();
}
}
//前面我们有说任务是一段保存在FLASH上的代码(函数),如仅仅只是一段代码,更不不需要报存,但是这段代码一旦运行了,就会产生数据比如局部变量,PC寄存器的值,代码运行的位置。
切任务时需要做的事情
1、保存程序执行的位置
2、保存任务中产生的变量的值
等等
当程序运行时被打断,需要现场保护,也就是前面所说的16个寄存器的值,将他们保存起来,保存在哪里?-》保存在栈里面
堆栈的作用
1》栈(stack)
1、函数的形参,以及函数里面定义的局部变量就是存储在栈里,(我们的局部变量,数组这些不能超过1K),否则程序会进入hardfault
2、实时操作系统的现场保护,返回地址也是保存在栈里
3、栈的增长方向是从高地址到低地址
2》堆(heap)
1、malloc()函数动态分配的内存就是从堆空间分配
3》静态空间区域
全局变量,静态变量是不存在堆里的,堆以外的静态空间区域
4》栈的大小怎么分
—取决于局部变量的大小和调用的深度,只能估算,没有刚好的确定值(由大到小去调节)
5》栈从哪里分配
—-heap4.c中有一个很大的数组,从这个数组中划分出去各个任务的栈
任务调度的机制
1》在创建任务时做了以下事情
1、分配栈空间
2、将函数地址给PC,也就是R15
3、将参数给到R0
4、分配优先级
5、分配了TCB结构体
任务切换时需要将R0-R15寄存器的值保存到栈里,再次运行时,需要从栈里面恢复R0-R15寄存器的值
2》优先级
1、高优先级的任务可以抢占低优先级任务
2、高优先级的任务不主动放弃CPU资源,低优先级的任务永远无法运行
3、同等优先级的任务按时间片轮询依次执行
4、空闲任务礼让其他同级别的任务—空闲任务主动放弃一次运行机会
3》怎么去管理任务
1、找到最高优先级的任务运行
2、优先级相同轮流执行,排队,排在就绪列表前的先执行,运行一个tick后,让出CPU的使用权,去链表尾部排队
4》高优先级任务怎么主动释放CPU使用权
用vTaskDelay()函数可以释放CPU使用权
5》空闲任务
1、主要起清理作用,比如一个任务自杀了,由空闲任务来释放任务的栈空间
2、当创建的任务优先级都为0时,最先运行的是空闲任务,因为空闲任务是在启动任务调度器时才创建
启动文件
Stack_Size EQU 0x00000400//分配了一个栈空间
AREA STACK, NOINIT, READWRITE, ALIGN=3//定义一个段,代码节或数据节,说明定义段的相关属性
Stack_Mem SPACE Stack_Size
//SPACE(申请一段空间)用于分配大小等于Stack_Size连续内存空间,单位为字节//类似MALLOC
__initial_sp //表示这块区域的高地址指向栈顶 “先进后出”
STACK 表示这块区域的名称没有限制写啥都行
NOINIT 表示这块区域不需要初始化
READWRITE 表示这块区域可读可写,可读写(内存),ROM是指读区域
ALIGN=3 表示按照2^3(8)字节对齐
启动文件中的堆栈空间是用来管理裸机开发时有用,freertos中的堆栈是由heap4.c管理
6》在freertos中,最低优先级的中断也比最好优先级的任务先运行,中断永远都是先执行
任务之间的通信
1、消息队列
2、共享内存
3、信号量
4、二值信号量
实例
台灯需要同时完成环境光检测、自动调光和语音交互等多项功能,但STM32资源有限,如何高效调度多任务成为关键挑战。为此,我将环境光检测、调光控制和语音识别分别设计为独立任务,并合理设置优先级和同步机制。环境光检测任务大部分时间处于阻塞态,定时唤醒采集数据后通过队列通知调光任务,调光任务则在收到数据时由阻塞态切换为就绪态,及时调整灯光亮度。语音识别任务则根据唤醒词实现挂起和激活的切换,实现资源的动态分配。
环境光检测任务采用FreeRTOS的阻塞式设计。具体实现上,我利用FreeRTOS的软件定时器,每隔200ms自动触发一次任务唤醒。唤醒后,任务通过ADC接口采集光敏传感器数据,采集结束后,数据通过FreeRTOS队列(Queue)发送给调光控制任务。发送完成后,该任务立即调用vTaskDelay或直接进入阻塞(Blocked)状态,等待下一个定时周期,从而极大降低CPU占用,提高系统整体能效。
调光控制任务同样采用阻塞等待队列消息的机制。它在FreeRTOS中通过xQueueReceive函数阻塞自己,只有当环境光检测任务有新数据写入时,调光任务才从阻塞切换到就绪(Ready),并被调度执行。调光任务根据收到的光照强度数据,实时调整PWM占空比,驱动LED灯实现无级调光。调光完成后,再次进入阻塞等待下一个消息,保证系统响应及时且资源利用最大化。
语音识别任务则设计为“挂起-激活”机制。任务在系统空闲时处于挂起(Suspended)状态,几乎不占用任何CPU资源。系统通过外部中断(如麦克风检测到唤醒词)触发ISR(中断服务例程),在ISR中调用xTaskResumeFromISR函数将语音任务恢复到就绪状态。语音任务被唤醒后,立即处理语音指令,执行相应操作(如开关灯、调节亮度等)。处理完成后,任务再次自挂起,等待下次激活。这样,既保证了语音响应的及时性,又优化了主控芯片的算力分配。
1. 环境光检测任务
核心流程
- FreeRTOS软件定时器每隔200ms超时回调,唤醒环境光检测任务(或直接采集数据)。
- 任务被唤醒,启动ADC采样,读取光敏传感器数据。
- 将采集到的数据通过Queue发送给调光控制任务。
- 任务调用
vTaskDelay
或等待事件进入阻塞,等待下一个定时周期。
实现细节
- 任务优先级:建议为中等,优先级略高于调光任务,低于关键中断。
- ADC采样:可用DMA方式提升效率,采样完成用事件或信号量通知任务。
- Queue长度:设为1或2,避免数据堆积。
- 软件定时器:用
xTimerCreate
/xTimerStart
创建,回调里用xTaskNotifyGive
或vTaskResume
唤醒任务。
伪代码示例
c
void vLightDetectTask(void *pvParameters) {
for (;;) {
// 1. 阻塞等待定时器通知
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 2. 采集ADC数据
uint16_t lightValue = ADC_Read();
// 3. 发送到队列
xQueueSend(lightQueue, &lightValue, portMAX_DELAY);
// 4. 进入阻塞等待下次通知(由定时器回调触发)
}
}
// 软件定时器回调
void vTimerCallback(TimerHandle_t xTimer) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(lightDetectTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
2. 调光控制任务
核心流程
- 通过
xQueueReceive
阻塞式等待环境光检测任务的新数据。 - 收到后,计算PWM占空比,调整LED亮度。
- 调整完成后,再次阻塞等待下一次消息。
实现细节
- 任务优先级:可与环境光检测任务相同,保证及时响应。
- PWM调节:可调用HAL库的PWM设置函数,动态调整占空比。
- 算法:可加简单滤波或阈值防止频繁闪烁。
伪代码示例
c
void vDimmingTask(void *pvParameters) {
uint16_t lightValue;
for (;;) {
// 1. 阻塞等待新数据
if (xQueueReceive(lightQueue, &lightValue, portMAX_DELAY) == pdPASS) {
// 2. 计算调光值
uint8_t duty = CalcDuty(lightValue);
// 3. 调整PWM
__HAL_TIM_SET_COMPARE(&htimx, TIM_CHANNEL_y, duty);
// 4. 阻塞等待下次消息
}
}
}
3. 语音识别任务
核心流程
- 初始处于挂起状态(
vTaskSuspend
)。 - 麦克风检测到唤醒词后,外部中断ISR中调用
xTaskResumeFromISR
恢复任务。 - 任务唤醒后,处理语音数据(如解析指令、控制灯光)。
- 处理完成后,自挂起(
vTaskSuspend(NULL)
),等待下次激活。
实现细节
- 任务优先级:建议高于其他任务,确保响应及时。
- ISR操作:中断服务例程里只做任务唤醒,主处理逻辑在任务里。
- 语音处理:可用外部模块/库,处理时间长时可拆分子任务。
伪代码示例
c
void vVoiceTask(void *pvParameters) {
for (;;) {
// 1. 刚启动即挂起
vTaskSuspend(NULL);
// 2. 被恢复后,处理语音指令
ProcessVoiceCommand();
// 3. 处理完后再次挂起
}
}
// 外部中断ISR
void EXTIx_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTaskResumeFromISR(voiceTaskHandle);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
4. 任务之间的典型关系和调度
- 环境光检测任务 ←定时器唤醒→ ADC采样 →队列→ 调光任务
- 调光任务 ←队列阻塞→ PWM调节
- 语音任务 ←中断激活→ 语音指令处理(可控制灯光、调光)
5. 优化和注意点
- 队列、定时器、任务优先级设计要根据实时性和资源分配合理调整。
- 关键外设(如ADC、PWM)建议用DMA或硬件加速,减少CPU负担。
- 语音任务建议设置更高优先级。
- 若对电源有极致要求,可在任务空闲时进入低功耗模式(如Tickless Idle)。
6. 面试表达建议(简明版)
“环境光检测任务用软件定时器周期唤醒,采集光敏传感器ADC数据后,通过队列发送给调光任务,任务间完全解耦,且均采用阻塞设计,极大降低了CPU占用。调光任务阻塞等待队列消息,收到后计算PWM占空比驱动LED,实现无级调光。语音识别任务则由外部中断唤醒,处理完指令后再次自挂起,最大化算力利用和响应速度。”
ADC+DMA+FreeRTOS智能调光流程
详细实现流程
1. 系统初始化阶段
- ADC初始化
- 设置分辨率(如12位),采样时间(如239.5周期,适合低速高精度),以及触发模式(软件或定时器触发)。
- 选择目标通道(如光敏电阻接入的ADC通道)。
- DMA初始化
- 配置DMA通道,设置外设为ADC数据寄存器,内存目标为事先分配的缓冲区。
- 配置传输方向(外设到内存)、数据宽度和缓冲区长度(如采样N点做均值滤波)。
- FreeRTOS资源初始化
- 创建定时器(200ms周期)、数据队列(用于任务间通信)、调光任务和采样任务。
2. 周期采集启动
定时器周期到达
时(FreeRTOS软件定时器回调里):
- 调用
HAL_ADC_Start_DMA
,启动ADC和DMA联动采集,DMA自动搬运N个采样数据到缓冲区。 - CPU无需轮询,立即返回,继续执行其他任务,实现高效并行。
- 调用
3. DMA硬件搬运过程
- ADC采样后,数据直接写入DMA指定内存。
- CPU资源完全释放,系统其余任务正常调度,极大提升能效和响应能力。
4. DMA采集完成中断
- DMA传输完成后,自动触发中断,执行回调函数(如
HAL_ADC_ConvCpltCallback
)。 - 在回调函数中:
- 数据预处理
- 对N点采样数据做均值或中值滤波,去除偶发噪声,提高数据有效性。
- 检查数据有效性(如光强值是否在合理范围,过滤异常值)。
- 数据通信
- 将处理后的光照强度结果通过FreeRTOS队列(
xQueueSendFromISR
)发送给调光控制任务。
- 将处理后的光照强度结果通过FreeRTOS队列(
- 数据预处理
5. 调光控制任务
- 任务采用阻塞式队列接收(
xQueueReceive
)。 - 收到新数据后,根据光强计算PWM占空比,调用HAL库调整LED PWM输出,实现无级调光。
- 调整完成后任务再次阻塞,等待下一个采样周期,确保CPU资源利用最大化。
6. 关键优化点
- 低功耗
- 采集和处理过程全程DMA自动化,CPU空闲时间长,有利于MCU进入低功耗模式(如Tickless Idle)。
- 实时性
- 采样、滤波、调光全链路延迟低于20ms,保证照明响应迅速。
- 代码可维护性
- 采样、处理、调光三部分职责清晰,易于后续功能扩展或参数调整。
关键代码流程片段(伪代码)
1 | // 1. 定时器回调:启动ADC+DMA |
总结表述建议
在智能调光系统中,我通过配置ADC+DMA实现高效数据采集,利用FreeRTOS定时器精确控制采样周期,DMA全自动搬运数据,极大释放了CPU资源。采集完成后在DMA中断回调中进行滤波与有效性判断,通过队列将结果无缝传给调光任务,调光任务则实时调整PWM,保证照明响应灵敏且系统能效提升30%以上。这种架构兼顾了高响应、低功耗和后续易扩展,完全满足智能照明的实时和稳健需求。期待能将这套高效的架构和优化思路应用到贵公司的相关产品开发中。
FreeRTOS三大冲突解决方案分析
1.资源冲突(共享资源访问)
问题描述
多个任务同时访问OLED显示资源导致数据不一致
代码解决方案
CMSIS-RTOS
1 | // 互斥锁创建 |
FreeRTOS
1 | osMutexId display_mutex; // 全局互斥量 |
使用二进制信号量实现的互斥量
taskENTER_CRITICAL()
/taskEXIT_CRITICAL()
双重保护
通过configASSERT
检查互斥量创建
2.优先级冲突(优先级反转)
问题描述
高优先级任务因等待低优先级任务持有的资源而阻塞
任务优先级设置
1 | // freertos.c中的优先级定义 |
优先级继承机制
1 | // FreeRTOSConfig.h配置 |
3. 死锁防护方案
潜在风险
- 递归获取同一互斥锁
- 多锁获取顺序不一致
防御性编程
1 | // 多锁获取顺序 |
最佳实践
- 单次锁获取(禁止嵌套)
- 统一锁获取顺序
- 临界区与互斥锁配合使用
FreeRTOS和CMSIS-RTOS对比
FreeRTOS:
适用于 纯FreeRTOS项目,特别是资源受限的嵌入式设备(如STM32、ESP32等)。
提供更底层的控制,适合对性能要求较高的场景。
适用于 单RTOS环境,不涉及跨平台兼容性需求。
CMSIS-RTOS:
适用于 跨RTOS兼容性 需求(如Keil RTX、FreeRTOS、ThreadX等)。
适合 基于ARM Cortex-M 的项目,特别是使用 STM32CubeMX 或 Keil MDK 开发的项目。
提供标准化的API,便于代码在不同RTOS间移植。
关键特性对比
特性 | FreeRTOS 互斥锁 | CMSIS-RTOS 互斥锁 |
---|---|---|
API 风格 | 底层、直接(xSemaphoreCreateMutex() ) |
标准化(osMutexCreate() ) |
超时控制 | 灵活(pdMS_TO_TICKS(ms) ) |
固定(osWaitForever 或 timeout ) |
优先级继承 | 支持(防止优先级反转) | 取决于底层RTOS实现 |
临界区保护 | 可额外使用 taskENTER_CRITICAL() |
仅依赖互斥锁 |
可移植性 | 仅适用于FreeRTOS | 跨RTOS兼容 |
调试支持 | 依赖FreeRTOS调试工具 | 兼容CMSIS-RTOS调试工具 |
FreeRTOS 函数 | CMSIS-RTOS 等效函数 | 说明 |
---|---|---|
xSemaphoreCreateMutex() |
osMutexNew() / osMutexCreate() |
创建互斥锁 |
xSemaphoreTake() |
osMutexAcquire() / osMutexWait() |
获取锁 |
xSemaphoreGive() |
osMutexRelease() |
释放锁 |
pdMS_TO_TICKS(ms) |
osWaitForever / timeout |
超时设置 |
关键区别
- 优先级继承:
FreeRTOS 的 xSemaphoreCreateMutex()
默认支持优先级继承(防止优先级反转)。
CMSIS-RTOS 的 osMutexCreate()
取决于底层RTOS实现(如 Keil RTX 支持,但 FreeRTOS 适配层可能不支持)。
- 超时控制:
FreeRTOS 使用 pdMS_TO_TICKS(ms)
转换毫秒到 Tick。
CMSIS-RTOS 直接使用 osWaitForever
或毫秒超时。
- 临界区保护:
FreeRTOS 允许额外使用 taskENTER_CRITICAL()
完全禁止中断(更严格保护)。
CMSIS-RTOS 仅依赖互斥锁。
队列
1.队列的基本概念
•定义:
•队列是一种先进先出(FIFO)的数据结构
•FreeRTOS 中,队列可以存储固定大小的数据项,每个数据项的大小在创建队列时指定。
主要特点包括:
- 线程安全:支持多任务并发访问
- 数据传递:可在任务间或任务与中断间传递数据
- 阻塞机制:支持发送和接收时的任务阻塞
- 多种数据类型:可传输基本类型或自定义结构体
•用途:
•用于任务间的通信和同步.
•一个任务可以将数据发送到队列,另一个任务从队列中接收数据。
1.队列是和任务绑定的么?
队列在 FreeRTOS 中是一种独立且可被多个任务、中断服务程序共享的通信机制,不与特定的任务绑定。
2.队列中,除了FIFO,有没有后入先出的可能?
FreeRTOS 中,标准的队列默认采用先进先出(FIFO)的机制,但也提供了实现后入先出(LIFO)的方式
2.队列的创建
FreeRTOS 提供了 xQueueCreate 函数来创建队列
•参数解释:
•第一个参数是队列的长度,即队列中最多可以存储的数据项数量。
•第二个参数是每个数据项的大小(以字节为单位)。
•返回值:
•如果队列创建成功,返回一个队列句柄;
•如果创建失败,返回 NULL。
使用xQueueCreate创建的队列,分配的内存是静态的还是动态的?不用的时候需要删除么?
使用 xQueueCreate 创建队列,其分配内存是动态的。
不再使用时,建议使用 vQueueDelete 函数删除队列,以释放之前动态分配的内存。这样可以避免内存泄漏,尤其是在长时间运行的系统或者资源受限的嵌入式系统中,合理释放内存资源非常重要。
需要使用静态内存来创建队列,可以使用 xQueueCreateStatic 函数。
3.向队列发送数据
4.从队列接收数据
5.队列的其它操作
6.队列在中断服务中的使用
7.队列的注意事项
示例
做⼀个“按键反应测试”类的⼩游戏,利⽤按键控制来增加游戏难度,同时在屏幕上展⽰分数和游戏状态。比如,游戏中会随机闪烁⼀个LED灯,玩家需要在LED
灯亮起时尽快按下对应的按键来得分,错过或按错就扣分。
对于这个游戏,你可以在不同章节中引入相关的
FreeRTOS
功能:
概述:介绍游戏的⽬标,
FreeRTOS的基本框架。 任务:创建不同的任务(比如LED闪烁、分数更新、按键监控等)。 队列:⽤队列管理按键输入。 信号量与互斥量:控制多个任务间的资源访问,比如对OLED屏幕的访问。 定时器与时间管理:⽤于控制游戏时间、LED闪烁间隔等。 外部中断处理:⽤外部中断处理按键的输入。资源管理与内存池:管理游戏中使⽤的资源,避免内存泄漏。 任务通知与事件组:处理游戏的状态切换和任务间的同步,比如任务间通知LED
闪烁。
这个游戏完全可以使⽤蜂鸣器来增加互动性和反馈效果!你可以在以下⼏个⽅⾯利⽤蜂鸣器:
正确按键反馈:当玩家按对了按键时,蜂鸣器可以发出⼀个短促的
“嘀”声,表⽰玩家得分。 错误按键反馈:如果玩家按错了按键,蜂鸣器可以发出较⻓的“哔哔”声,表⽰失败。 游戏结束提醒:当游戏结束时,蜂鸣器可以发出⼀个响亮的“结束⾳”,提⽰玩家游戏已结束。 时间倒计时提醒:在游戏过程中,蜂鸣器还可以在倒计时结束时发出⼀声提⽰⾳,提醒玩家游戏时间即将耗尽。