FreeRTOS 学习笔记:从入门到内核机制

FreeRTOS 全流程笔记:程序设计模式、移植、任务创建/删除/挂起/恢复、中断管理、列表与列表项、调度机制、消息队列、信号量、事件标志组、任务通知、软件定时器与内存管理。

序、FreeRTOS函数名前缀

image-20250804213757624

一、单片机程序设计模式

1.轮询模式

image-20250802143036241

2.前后台

后台:执行在main

前台:执行在中断,但不能执行太多

可以在中断中用标志位,将任务放在主函数

3.定时器驱动

函数执行不能太长

image-20250802143221867

4.状态机

基于轮询,将单个任务分解成多个任务,减少每个任务需要的时间,降低造成的相互影响

5.多任务系统

交替执行,基于时间片和任务优先级调度

对于重要的任务可以提高优先级,在高优先级处于延时中执行低优先级任务

image-20250802144931000

image-20250802143633146

伪代码实现

void main()
{
// 创建 2 个任务
create_task(task1);
create_task(task2);
// 启动调度器
start_scheduler();
}
task1()
{
while(1)
{
	do_task1();
}
}
task2()
{
while(1)
{
	do_task2();
}
}

二、FreeRTOS基础知识

1.任务调度简介(熟悉)

抢占式:每个任务有优先级,优先级高抢占优先级低

FreeRTOS数值越大,优先级越高

Task1 、 Task2 、 Task3 的优先级分别为 1 、 2 、 3 ;在FreeRTOS 中任务设置的数值越大,优先级越高,所以 TASK3 的优先级最高

image-20250802145631062
时间片:同优先级,每个时钟节拍切换任务

同等优先级任务轮流地享有相同的 CPU 时间 ( 可设置 ) , 叫时间片,在 FreeRTOS 中,一个时间片就等于 SysTick 中断周期

没用完的时间片抛弃

image-20250802145838370

协程式:当前执行任务一直执行,高优先级不抢占低优先级(不常用)

2.任务状态(熟悉)

1.运行态

正在执行,同一时间仅一个

2.就绪态

能够被执行,但还未执行

3.阻塞态

任务在延时或等待外部事件发生

4.挂起态

类似暂停,不参与调度,需要解挂才能进就绪vTaskResume();

image-20250802150415156

注:只有就绪态可以转运行态,挂起要解挂,阻塞要变成就绪

任务状态列表

1.就绪列表:pxReadyTasksLists[x],其中 x 代表任务优先级数值。0~31,31最高

32位变量,当某个位置一说明对应优先级有任务存在

image-20250802151219493

2.阻塞列表:pxDelayedTaskList

3.挂起列表:xSuspendedTaskList

调度器取就绪列表最高优先级任务来执行

相同优先级的任务会连接在同一个就绪列表

三、FreeRTOS移植

源码:https://www.freertos.org/

image-20250802152204321

FreeRTOS:内核有用

image-20250802152338698

Demo和Source

Source文件夹

image-20250802152630841

include,portable,list,queue,task必须

portable文件夹

与硬件连接桥梁

由于使用MDK开发,只要

image-20250802152913833

手把手移植

image-20250802153157922

1.基础工程的Middlewares文件夹中新建一个FreeRTOS子文件夹

image-20250802153620409

将FreeRTOS内核源码的Source文件夹下的所有文件添加到工程的FreeRTOS文件夹中

image-20250802153705681

CORE中是源码

heap_4是内存管理算法4

port是软硬件连接桥梁

image-20250802153948337

port根据芯片选择

image-20250802154239833

头文件哪有就加什么路径

2.添加 FreeRTOSConfig.h 文件

这个可裁剪,用不到的功能可以裁剪,Demo里有,或者例程

3.修改SYSTEM

详阅《FreeRTOS开发指南》

系统配置详解

FreeRTOSConfig.h 配置文件作用:对 FreeRTOS 的功能进行配置和裁剪,以及 API 函数的使能

image-20250802163812620

详看注释

四、FreeRTOS任务创建和删除

1.任务创建和删除的 API 函数

API描述
xTaskCreate()动态创建
xTaskCreateStatic()静态创建
vTaskDelete()删除

动态创建:任务控制块以及任务栈空间所需内存,有FreeRTOS自动分配

静态创建:用户分配

动态创建

image-20250802165520234

返回pdPass:创建成功

返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:创建失败

流程:

1、将宏 configSUPPORT_DYNAMIC_ALLOCATION 配置为 1

2 、定义函数入口参数

3 、编写任务函数

创建后进入就绪态,然后启动任务调度器运行

内部实现:

image-20250802170201003

任务控制块就是任务的身份证,保留任务的信息

image-20250802170649880

静态创建

image-20250802170921182

返回NULL:创建成功

其他值:任务句柄,任务创建成功

流程:

1 、需将宏 configSUPPORT_STATIC_ALLOCATION 配置为 1

2 、定义空闲任务 & 定时器任务的任务堆栈及TCB

3 、实现两个接口函数

vApplicationGetIdleTaskMemory( )必须

vApplicationGetTimerTaskMemory ()可选

4 、定义函数入口参数

5 、编写任务函数

任务删除

xTaskDelete(TaskHandle_t xTaskToDelete);

实际是通过传入的任务句柄获取任务控制块,从四个任务列表中移除

注意:1.当传入NULL,删除任务自身

​ 2.空闲任务会释放被删除任务中有系统分配的内存,但是用户在任务 删除前申请的内存,需要手动释放,否则内存泄漏

五、FreeRTOS任务挂起与恢复

1.任务挂起与恢复的API函数

image-20250802212436605

挂起:暂停

恢复:继续

FromISR:从中断中恢复

任务挂起

将宏 INCLUDE_vTaskSuspend 配置为 1

image-20250802212603585

任务恢复

将宏 INCLUDE_vTaskSuspend 必须定义为 1

image-20250802212618152

中断中恢复

INCLUDE_vTaskSuspend 和 INCLUDE_xTaskResumeFromISR 必须定义为 1

image-20250802212911425

注意:中断服务程序中要调用FreeRTOS的API,中断优先级不能高于FreeRTOS所管理的优先级

因为FreeRTOS管理的515的优先级,所有中断优先级不能在04,因为中断优先级数字越低优先级越高

六、FreeRTOS中断管理

1.中断

中断请求,响应中断(转中断处理ISR),退出中断

2.中断优先级

8位宽寄存器来配置中断优先级

ARM Cortex-M支持2^8=256个中断优先级

STM32只用高四位[7:4],所以STM32提供2^4=16个中断优先级

image-20250804141722815

STM32中断分为抢占优先级和子优先级

抢占优先级: 抢占优先级高的中断可以打断正在执行但抢占优先级低的中断

子优先级:当同时发生具有相同抢占优先级的两个中断时,子优先级数值小的优先执行

(中断优先级数字越小越优先)

分配方式

image-20250804141852953

如:HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4 )设置优先级分组4

注意

1 、低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 优先级的中断里才允许调FreeRTOS 的 API 函数

2 、建议将所有优先级位指定为抢占优先级位,方便 FreeRTOS 管理

HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4 )

3 、中断优先级数值越小越优先,任务优先级数值越大越优先

七、临界段代码保护 任务调度器挂起和恢复

1.临界段代码保护

临界段:指那些必须完整运行,不能被(中断、任务调度PendSV)打断的代码段

需要保护:

1.严格时序初始化的外设

2.系统自身需求

3.用户需求

2.临界段代码保护函数

image-20250804145449284

临界区屏蔽中断,系统任务调度和ISR都靠中断,因此临界区不能长

3.任务调度器挂起和恢复

挂起任务调度器,就是不许任务切换,不需要关闭中断

image-20250804151438414

1.挂起任务调度器未关闭中断

2.防止任务间资源争夺,中断照样响应

八、FreeRTOS列表和列表项

1.列表和列表项

列表是FreeRTOS的一个数据结构,类似链表,用于跟踪FreeRTOS的任务

列表项是在列表中的项目

FreeRTOS中列表是双向环形链表

image-20250804155007010

非连续地址,人为连接到一起,数目后期添加,随时可以改变

列表相关结构体

image-20250804155613541

1.校验值:用于调试,默认不开启

2.uxNumberOfltems,记录列表项的个数

3.pxIndex:指向列表中的某个列表项,用于遍历列表项

4.xListEnd是迷你列表项,排在末尾

image-20250804160002038

列表项结构体

image-20250804160223478

1.xItemValue为列表项的值,对列表项进行排序(升序)

2.pxNext 和 pxPrevious 分别用于指向列表中列表项的下一个列表项和上一个列表项

3.pxOwner指向包含列表项的对象(通常是任务控制块)

3.pxContainer指向列表项所在列表

image-20250804160559934

迷你列表项结构体

用于标记列表的末尾和挂载其他插入列表中的列表项

image-20250804160919917

少了pxOwner和pxContainer

列表和列表项的关系

列表初始化就有末尾列表项,而无列表项

image-20250804161344141

初始末尾列表项的xItemValue=0xFFFFFFFF

*pxPrevious,*pxNext都是指向的列表项的头指针(首地址)

pxIndex不遍历时不指向

image-20250804162946703

2.列表及列表项API

image-20250804170048461

九、任务调度机制*

1.开启任务调度器

vTaskStartScheduler()中

启动任务调度器

1.创建空闲任务

先判断创建方式:静态创建、动态创建;根据宏configSUPPORT_STATIC_ALLOCATION选择

2.定时器任务

如果使能软件定时器,创建定时器任务

根据宏configUSE_TIMERS;xTimerCreateTimerTask()

若创建成功

3.关闭中断

防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断

portDISABLE_INTERRUPTS();

4.初始化全局变量

并将任务调度器设置为已运行

//阻塞时间默认最大值
xNextTaskUnblockTime = portMAX_DELAY;
//调度器运行状态
xSchedulerRunning = pdTRUE;
//初始节拍 0
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

5.调用xPortStartScheduler();

作用:该函数用于完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务

5.1检测配置是否有误

用户在 FreeRTOSConfig.h 文件中对中断的相关配置

5.2配置 PendSV 和 SysTick

配置 PendSV 和 SysTick 的中断优先级为最低优先级

portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

5.3调用vPortSetupTimerInterrupt();

函数中在配置SysTick

SysTick控制及状态寄存器(地址:0xE000_E010)

SysTick当前数值寄存器(地址:0xE000_E018)

1
2
3
4
5
6
7
/*
配置寄存器
#define portNVIC_SYSTICK_CTRL_REG          ( *( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_CURRENT_VALUE_REG ( *( ( volatile uint32_t * ) 0xe000e018 ) )
*/
portNVIC_SYSTICK_CTRL_REG = 0UL;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
image-20250804202454119
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/*
#define portNVIC_SYSTICK_CTRL_REG   ( *( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG   ( *( ( volatile uint32_t * ) 0xe000e014 ) )
*/
/*
F103C8中
uint32_t SystemCoreClock = 16000000;
#define configCPU_CLOCK_HZ    	SystemCoreClock     //定义CPU主频, 单位: Hz, 无默认需定义
#define configSYSTICK_CLOCK_HZ  (configCPU_CLOCK_HZ / 8)
#define configTICK_RATE_HZ      20					/* 定义系统时钟节拍频率, 单位: Hz, 无默认需定义 */
*/
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT_CONFIG | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );

5.4初始化临界区嵌套计数器为 0

uxCriticalNesting = 0;

5.5调用函数 prvEnableVFP() 使能 FPU

Cortex M3没有FPU

5.6调用函数 prvStartFirstTask() 启动第一个任务

2.启动第一个任务

要启动的第一个任务是任务 A,那么就需要将任务A的寄存器值恢复到CPU寄存器

任务 A 的寄存器值,在一开始创建任务时就保存在任务堆栈里

注意:

1、中断产生时,硬件自动将 xPSR,PC(R15) ,LR(R14) ,R12 ,R3-R0出/入栈;而 R4~R11需要手动出/入栈

2 、进入中断后硬件会强制使用 MSP 指针 ,此时 LR(R14 )的值将会被自动被更新为特殊的 EXC_RETURN

prvStartFirstTask()

….

/* Call SVC to start the first task. */ svc 0

触发SVC中断,启动第一个任务

#define vPortSVCHandler SVC_Handler

SVC中断

只在启动第一次任务时会调用一次,以后不调用

image-20250804212919268

出栈/压栈汇编指令详解

image-20250804214029244

3.任务切换

任务切换本质:CPU寄存器的切换

就是把不同任务加载到CPU寄存器里

假设要把任务A切换到B:

第一步:暂停任务A,将此时任务A的寄存器保存(压栈)到任务堆栈,即保存现场

第二步:将任务B的寄存器值(存在任务堆栈中)恢复到CPU寄存器,即恢复现场

切换过程:保存A现场,恢复B现场,即上下文切换

需要手动的部分:R4-R11

切换:任务切换在PendSV中断服务函数里完成

image-20250804215919484

如何触发PendSV中断

1.滴答定时器中断调用

2.FreeRTOS相关API:portYIELD();//底层函数

本质:向中断控制和状态寄存器ICSR的bit28写1挂起PendSV来启动PendSV中断

流程

1:滴答定时器中断处理函数SysTick_Handler()

启动了任务调度器,则进入xPortSysTickHandler()

判断xTaskIncrementTick()

则执行

1
2
3
4
#define portNVIC_INT_CTRL_REG     ( *( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT    ( 1UL << 28UL )
//即向地址0xe000ed04 第28位写入1
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;

image-20250805141250773

2:

#define portYIELD()                                 
{                                                   
    portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; 
    __dsb( portSY_FULL_READ_WRITE );                           
    __isb( portSY_FULL_READ_WRITE );                           
}

PendSV中断服务函数

FreeRTOSConfig.h中

#define xPortPendSVHandler PendSV_Handler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;		//任务控制块地址
    extern vTaskSwitchContext;
	//八字节对齐
    PRESERVE8

    mrs r0, psp
    isb

    ldr r3, =pxCurrentTCB 	
    ldr r2, [ r3 ]				//r2存放任务控制块地址

    stmdb r0 !, { r4 - r11 } 	//r4到r11压入cpu任务栈
    str r0, [ r2 ] 

    stmdb sp !, { r3, r14 }
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0
    dsb
    isb
    bl vTaskSwitchContext
    mov r0, #0
    msr basepri, r0
    ldmia sp !, { r3, r14 }

    ldr r1, [ r3 ]
    ldr r0, [ r1 ] 
    ldmia r0 !, { r4 - r11 } 
    msr psp, r0
    isb
    bx r14
    nop
}

十、时间片调度

当多个任务的任务优先级相同时,CPU 会轮流的处理每一个任务,每个任务享有 CPU 处理的时间都是相同的,这个时间被称为时间片。

一个时间片就等于 SysTick 中断周期(可以由用户自行设置),CPU 给与了一个时间片运行一个任务,不管这个任务是否运行完成,CPU 都会跑到下一个任务。

img

流程:

1.task1 运行完一个时间片切换到 task2

2.task2 运行完一个时间片切换至 task3

3.task3 运行阻塞(系统延时或等待信号量等)切换至下一个任务 task1

没有用完的时间片不会再使用,下次任务 task3 得到执行还是按照一个时间片的时钟节拍运行

4.task1 运行完一个时间片之后,切换至 task2 运行

十一、任务相关API

image-20250805151211347

不多介绍

十二、时间管理

image-20250805153017599

· 相对延时:指每次延时都是从任务执行函数vTaskDelay()开始,延时指定的时间结束。

· 绝对延时:指每隔指定的时间,执行一次调用vTaskDelayUntil()函数的任务。

即:vTaskDelay()是从调用其本身开始计时,vTaskDelayUntil()是从调用任务开始计时

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
BaseType_t xTaskDelayUntil( TickType_t *pxPreviousWakeTime,                          						  const TickType_t xTimeIncrement );

// Perform an action every 10 ticks.
void vTaskFunction( void * pvParameters )
{
    TickType_t xLastWakeTime;
    const TickType_t xFrequency = 10;
    xLastWakeTime = xTaskGetTickCount();
    for( ;; )
    {
        // Wait for the next cycle.
        // Perform action here.
        vTaskDelayUntil( &xLastWakeTime, xFrequency );
    }
}

十三、FreeRTOS消息队列

1.队列简介

任务到任务,任务到中断,中断到任务数据交流的一种机制

为什么不用全局变量?

假设有一个全局变量a=0,现在俩个认为在写变量a

image-20250805174038545

任务1执行到第二步,任务2打断,执行a++,此时a才是1,回到任务1的第三步,a为1,但执行两次a++。

因此全局变量数据无保护,当多任务操作数据时,数据受损

使用队列

image-20250805174331194

为什么会安全?因为封装了临界区,即关闭中断,读写队列都有保护,防止多任务冲突

img

队列存储数量有限、大小固定的数据。队列的每个数据叫队列项目

image-20250805174900810

队列要点

1.数据入队出队:通常FIFO(先入先出),也可以LIFO(后入先出)

2.数据传递方式:freertos采用实际值,将数据拷贝至队列,在传递较大数据时可以传递指针

3.多任务访问:队列不属于某任务,任何任务、中断都可以读写

4.出入阻塞:当任务向队列读写时,可以指定阻塞时间,队列已满或空则无法入队、出队

阻塞时间是:0:直接返回不等待

​ 0~max:超时不入队则不等待

​ max:死等

入/出队阻塞

入队阻塞,队列满了,此时写不进去数据

image-20250805180519958
  • 将该任务的状态列表项挂载再 pxDelayedTaskLisk;
  • 将该任务的事件列表项挂载在 xTasksWaitingToSend;

出队阻塞,队列为空,此时读取不了数据:

image-20250805180611951
  • 将该任务的状态列表项挂载再 pxDelayedTaskLisk;
  • 将该任务的事件列表项挂载在 xTasksWaitingToReceive;

多任务都在等待同一个队列入队,都在阻塞,那当队列有空间,谁先进入就绪态:

那优先级最高的入队,若优先级相同,等最久的入队

2.队列结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typedef struct QueueDefinition 
{ 
    int8_t * pcHead;           /* 存储区域的起始地址 */ 
    int8_t * pcWriteTo;        /* 下一个写入的位置 */ 
    union 
    { 
      QueuePointers_t xQueue; 		//当用于队列时
      SemaphoreData_t xSemaphore; 	//当用于信号量时
    } u; 
    List_t xTasksWaitingToSend;        /* 写入阻塞任务列表 */ 
    List_t xTasksWaitingToReceive;       /* 读取阻塞任务列表 */ 
     
    volatile UBaseType_t uxMessagesWaiting;     /* 非空闲项目的数量 */ 
    UBaseType_t uxLength;         /* 队列的长度 */ 
    UBaseType_t uxItemSize;         /* 队列项目的大小 */ 
     
    /* 锁用于在任务因队列操作被阻塞前,防止中断或其他任务操作队列。 
     * 上锁期间,队列可以写入和读取消息,但不会操作队列阻塞任务列表, 
     * 当有消息写入时,cTxLock加1,当有消息被读取时,cRxLock加1, 
     * 在解锁时,会统一处理队列的阻塞任务列表 
     */ 
    volatile int8_t cRxLock;        /* 读取上锁计数器 */ 
    volatile int8_t cTxLock;        /* 写入上锁计数器 */ 
	//以下省略一些条件编译
} xQUEUE; 

3.队列相关API函数

主要流程:创建队列->写队列->读队列

image-20250805192810878

1
2
3
#define xQueueCreate( uxQueueLength, uxItemSize )           
		xQueueGenericCreate(( uxQueueLength ),( uxItemSize ),
                            (queueQUEUE_TYPE_BASE )) 

其中queueQUEUE_TYPE_BASE代表不同创建类型的功能,实际都是基于一个函数实现的

#define queueQUEUE_TYPE_BASE    ( ( uint8_t ) 0U ) 				/* 队列 */ 
#define queueQUEUE_TYPE_SET     ( ( uint8_t ) 0U ) 				/* 队列集 */ 
#define queueQUEUE_TYPE_MUTEX    ( ( uint8_t ) 1U ) 			/* 互斥信号量 */ 
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE ( ( uint8_t ) 2U ) 	/* 计数型信号量 */ 
#define queueQUEUE_TYPE_BINARY_SEMAPHORE ( ( uint8_t ) 3U )		/* 二值信号量 */ 
#define queueQUEUE_TYPE_RECURSIVE_MUTEX ( ( uint8_t ) 4U ) 		/* 递归互斥信号量 */

QueueHandle_t xQueueCreate( uxQueueLength, uxItemSize )

uxQueueLength:队列长度

uxItemSize:队列项大小

返回值QueueHandle_t句柄

NULL:创建失败,其他值:创建成功,返回句柄

3.1写入消息API

image-20250805193852464

这四个函数都是对xQueueGenericSend()的宏定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#define xQueueSend(xQueue, pvItemToQueue, xTicksToWait)    
        xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK) 
 
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait)      
        xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK) 
 
#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait)    
        xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT) 
 
#define xQueueOverwrite( xQueue, pvItemToQueue) 
        xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE)

xQueueGenericSend为

BaseType_t xQueueGenericSend( QueueHandle_t  	 xQueue, 
           					  const void * const pvItemToQueue, 
           					  TickType_t 	     xTicksToWait, 
           					  const BaseType_t   xCopyPosition); 

image-20250805194502113

3.2读取消息API

image-20250805194543783

xQueueReceive

从队列中读取消息,并且消息读取成功后,会将消息从队列中移除

image-20250805194845838
BaseType_t xQueueReceive( QueueHandle_t xQueue,
                          void * const pvBuffer,
                          TickType_t xTicksToWait );

xQueuePeek

不会删除的函数,参数一致

4.队列相关API函数解析

4.1xQueueCreate

#define xQueueCreate( uxQueueLength, uxItemSize )

​ xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )

对队列而言xQueueGenericCreate前两个入口参数有效

首先长度判断

if( ( uxQueueLength > ( UBaseType_t ) 0 ) &&
            ( ( SIZE_MAX / uxQueueLength ) >= uxItemSize ) &&
            ( ( SIZE_MAX - sizeof( Queue_t ) ) >= ( uxQueueLength * uxItemSize ) ) )

队列项存储区大小

xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );

申请内存是队列项存储区+结构体存储区,并把首地址给到pxNewQueue

pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );

再将地址指向队列项存储区

pucQueueStorage = ( uint8_t * ) pxNewQueue; pucQueueStorage += sizeof( Queue_t );

image-20250805203138966

执行

QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )

除一个是存储区首地址,一个是队列结构体首地址pucQueueStorage,pxNewQueue外,由外部传入

prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );

进入prvInitialiseNewQueue后

if( uxItemSize == ( UBaseType_t ) 0 )是信号量

否则是队列,执行以下行,将队列项存储区首地址给到队列首地址

pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;

再进入

( void ) xQueueGenericReset( pxNewQueue, pdTRUE );

4.2xQueueSend

#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait )

	    xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), 		queueSEND_TO_BACK )

进入

xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

如果尾部插入

image-20250805210830882

十四、信号量

1.简介

队列是发送数据,信号是发送状态,信号量是队列的衍生

信号量结局同步机制(我做完一件事告诉你),任务间的通信以及事件通知,对共享资源的有序访问

举例:

1.停车场车位是否还有(判断信号量是否还有资源)

2.停车场有空位(有资源),就可以将车开入(获取信号量)

3.停车场无空位(无资源),不可以将车开入(获取信号量失败),可以等待其他人开出(释放)

image-20250805214538146

如果最大值被限定为 1 ,那么它就是二值信号量; 如果最大值不是 1 ,它就是计数型信号量。

image-20250805215045090

1.二值信号量

本质:长度为1的队列,该队列只有空和满

通常用于互斥访问(一个任务持有锁,其他任务无法执行)和任务同步(一个任务等另一个任务完成才能执行下去),互斥信号量相似,但二值信号量可能导致优先级反转

image-20250805215532525

二值信号量相关API函数

过程:创建-释放-获取

image-20250805215908052

创建

SemaphoreHandle_t xSemaphoreCreateBinary( void )

image-20250805220323164

释放

传入句柄xSemaphore

BaseType_t xSemaphoreGive( xSemaphore )

返回值pdPASS:成功

errQUEUE_FULL:失败

获取

传入句柄xSemaphore,阻塞时间xBlockTime ,死等为portMAX_DELAY

BaseType_t xSemaphoreTake( xSemaphore,xBlockTime )

返回值:pdTRUE 获取信号量成功 pdFALSE 超时,获取信号量失败

2.计数型信号量

计数型信号量相当于队列长度大于 1 的队列,因此计数型信号量能够容纳多个资源,这在计数型信号量被创建的时候确定的

适用场合:

  1. 事件计数:每次事件发生,事件处理函数中释放计数型信号量,计数值+1,其他任务获取计数型信号量,计数值-1,初始计数值一般设为0
  2. 资源管理:信号量表示有效资源数量,任务必须先获取信号量,计数值-1,才能获取资源控制权,当计数值减到0时表示没有资源,当任务使用完资源后,必须释放,计数值+1,。信号量创建时一般等于最大资源数

计数型信号量相关API

创建-释放-获取

创建:

image-20250806145915838
1
2
3
4
5
6
//uxMaxCount计数值的最大值限定
//uxInitialCount计数值的初始值
#define xSemaphoreCreateCounting(uxMaxCount, uxInitialCount)
   		xQueueCreateCountingSemaphore((uxMaxCount),(uxInitialCount)) 
//返回值NULL创建失败
//其他值返回句柄          

获取和释放与二值相同

3.优先级翻转

优先级翻转:高优先级任务反而慢执行,低优先级任务反而先执行

优先级翻转在抢占式内核中是很常见的问题,但是在实时操作系统中是不允许出现优先级翻转的,因为优先级翻转会破环任务的预期顺序,可能会导致未知的严重后果。在使用二值信号量的时候,经常会遇到优先级翻转问题。

角色行为优先级
H(高)想用共享资源(如 UART、SPI)3(最高)
L(低)已经拿到资源,正在用1(最低)
M(中)纯计算任务,不碰资源2

image-20250806154108779

  1. L 先运行,拿到互斥量 → 进入临界区。
  2. H 就绪,优先级最高,抢占 L,但发现互斥量被占 → 阻塞。
  3. M 就绪,优先级介于 H 和 L 之间 → 抢占 L,开始跑长计算。
  4. 结果: • H 必须等 M 跑完 → M 跑完 L 才能继续 → L 释放后 H 才能拿到。 • H 实际被 优先级远低于它的 L 间接阻塞。

4.互斥信号量

互斥信号量是一个拥有优先级继承的二值信号量,在同步的应用中二值信号量最适合,互斥信号量适用与需要互斥访问的应用中。

优先级继承:当一个互斥信号量正在被一个低优先级的任务持有时, 如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级。

image-20250806155928222

依旧L阻塞H,但M就不会浪费时间执行,将优先级翻转危害降低。

就是不能完全消除影响,但尽可能把影响降到最低。

注意:互斥信号量不能用于中断服务函数中。原因:

  • 互斥信号量有任务优先级继承的机制,但中断不是任务,没有任务优先级,所以互斥信号量只能用于任务中,不能用于中断服务函数
  • 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态

互斥信号量相关API

宏 configUSE_MUTEXES 置一

创建互斥信号量 -( task )获取信号量 -( give )释放信号量

image-20250806160514786

无传入参数

xSemaphoreCreateMutex()

返回NULL创建失败,其他值返回句柄

获取与释放与二值信号量相同,只是不支持中断调用

十五、队列集

1.简介

一个队列只允许任务间传递同一种数据类型,如果要传递不同数据类型,可以使用队列集。

作用:

对多个队列或信号量进行监听,不管哪个消息到来,都可以让任务退出阻塞状态

原始思路:

image-20250806164333227

队列集思路:

image-20250806164353656

2.队列集相关API

image-20250806164541592

2.1创建xQueueCreateSet

QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength );

参数:uxEventQueueLength 队列集可容纳的队列数量

返回值:NULL:队列集创建失败 其他值:创建成功,返回句柄

2.2添加xQueueAddToSet

往队列集添加队列时,队列不能有消息

BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore , QueueSetHandle_t xQueueSet );

image-20250806164917603

2.3移除

移除前不能有有效消息

BaseType_t xQueueRemoveFromSet( QueueSetMemberHandle_t xQueueOrSemaphore , QueueSetHandle_t xQueueSet );

image-20250806165023992

2.4获取有效消息xQueueSelectFromSet

1
2
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
										TickType_t const xTicksToWait )
image-20250806165250506

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 member_handle = xQueueSelectFromSet(queueset_handle,portMAX_DELAY);
        if(member_handle == queue_handle)
        {
            if(xQueueReceive(queue_handle,&key,portMAX_DELAY)==pdPASS)
            {
                printf("队列queue_handle读取数据成功\r\n");
            }
        }
        else if(member_handle == semphr_handle)
        {
            if(xSemaphoreTake(semphr_handle,portMAX_DELAY)==pdPASS)
            {
                printf("获取信号量成功\r\n");
            }
        }

十六、事件标志组

1.简介

事件标志组:用一个位表示时间是否发生

是一组事件标志位的集合,类似多个标志位

特点:

  • 每一个位表示一个事件(高八位不算)
  • 每位事件的含义由用户决定如bit0表示按键,bit1表示消息是否接受
  • 1:事件发生,0:事件未发生
  • 任意任务和中断可读写这些位
  • 可等待某一位,也可等待多位

一个事件组包含,EventBits_t是数据类型变量

image-20250806175133210

一个事件组最多可以存储 24 个事件标志

image-20250806175201301

与队列信号量区别

image-20250806175250430

2.事件标志组相关API

image-20250806175445375

2.1创建xEventGroupCreate

EventGroupHandle_t xEventGroupCreate ( void) ;

返回NULL:失败

​ 其他值:成功返回句柄

2.2清零标志位xEventGroupClearBits

EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear )

参数:

xEventGroup 待操作的事件标志组句柄 uxBitsToSet 待清零的事件标志位

返回:整数=清零事件标志位之前事件组中事件标志位的值

2.3设置标志位xEventGroupSetBits

EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet )

参数:

xEventGroup 待操作的事件标志组句柄 uxBitsToSet 待设置的事件标志位

返回:整数=函数返回时,事件组中的事件标志位值

2.4等待标志位xEventGroupWaitBits

1
2
3
4
5
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
								 const EventBits_t 	uxBitsToWaitFor,
								 const BaseType_t 	xClearOnExit,
								 const BaseType_t  	xWaitForAllBits,
								 TickType_t 		xTicksToWait )
image-20250806180148945

2.5同步函数

1
2
3
4
EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup, 
							const EventBits_t  uxBitsToSet, 
							const EventBits_t  uxBitsToWaitFor, 
							TickType_t  	   xTicksToWait) 
image-20250806191201673

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
EventGroupHandle_t eventgroup_handle;
#define EVENTBIT_0 1<<0
#define EVENTBIT_1 1<<1

eventgroup_handle = xEventGroupCreate();

if(key==KEY0_PRES)
        {
            xEventGroupSetBits(eventgroup_handle,EVENTBIT_0);
        }else if(key==KEY1_PRES)
        {
            xEventGroupSetBits(eventgroup_handle,EVENTBIT_1);
        }

event_bit = xEventGroupWaitBits(eventgroup_handle,EVENTBIT_0|EVENTBIT_1,
                                pdTRUE,pdFALSE,portMAX_DELAY);
        printf("等待到的事件标志位值:%#x\r\n",event_bit);

十七、任务通知

1.简介

任务通知:用来通知任务,任务控制块中的结构体成员变量 ulNotifiedValue 就是这个通

知值

为什么还要这个?

  • 使用任务通知时,任务结构体就包含了内部对象,可以直接接收别人发过来的通知 在这里插入图片描述
  • 使用队列、信号量、事件标志组时都需另外创建一个结构体,通过中间的结构体进行简介通信 在这里插入图片描述

发送端修改接收端任务控制块中结构体变量:ulNotifiedValue

任务通知值的更新方式

  • 覆盖或不覆盖接受任务的通知值(队列)
  • 更新接收任务通知值的一个过多个 bit(事件标志组)
  • 增加接收任务的通知值(信号量)

但只能在一些场合替代

优劣势

优势:

  • 效率高:使用任务通知向任务发送事件或数据比使用队列、事件标志组或信号量快
  • 使用内存更小:使用其他方法时都要先创建对应的结构体,使用任务通知时无需

劣势:

  • 无法发送数据给 ISR(中断服务函数):ISR 没有任务结构体,因此无法给 ISR 发送数据,但 ISR可以使用任务通知的功能发送数据给任务
  • 无法广播给多个任务:任务通知只能是被指定的一个任务接收并处理
  • 无法缓存多个数据:任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保持一个数据
  • 发送受阻不支持阻塞:发送方无法进入阻塞状态等待

2.任务通知值和通知状态

任务的任务控制块中,有两个结构体成员变量就是任务通知相关的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef struct tskTaskControlBlock
{
	...
	#if(configUSE_TASK_NOTIFICATIONS == 1)	//打开这个宏
	volatile uint32_t ulNotifiedValue[configTASK_NOTIFICATION_ARRAY_ENTRIES];	//通知值
	volatile uint8_t ucNotifyState[configTASK_NOTIFICATION_ARRAY_ENTRIES];		//通知状态
	endif
	...
}tskTCB;
#define  configTASK_NOTIFICATION_ARRAY_ENTRIES  1	//定义任务通知数组的大小,默认为1

任务通知值ulNotifiedValue

更新方式:

  • 计数值(数值累加,类似信号量)
  • 相应位置1(类似事件标志组)
  • 任意数(支持覆写不覆写,类似队列)

任务通知状态ucNotifyState

三种取值:

#define taskNOT_WAITING_NOTIFICATION ((uint8_t)0) //任务未等待通知 #define taskWAITING_NOTIFICATION ((uint8_t)1) //任务在等待通知 #define taskNOTIFICATION_RECEIVED ((uint8_t)2) //任务在等待接收

  • 未等待通知:任务通知默认的初始化状态(事不关己,高高挂起)
  • 等待通知:接收方已经准备好了(调用了接收任务通知函数),等待发送方给个通知
  • 等待接收:发送方已经发送出去(调用了发送任务通知函数),等待接收方接收

3.任务通知API函数

有发送和接收,发送可在任务和中断中,接收只能在任务(因为中断没TCB)

3.1发送

image-20250806194128769

函数原型

image-20250806195030350
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
BaseType_t xTaskGenericNotify(TaskHandle_t   xTaskToNotify, 
            				  UBaseType_t    uxIndexToNotify, 
				              uint32_t  	 ulValue, 
				              eNotifyAction  eAction, 
				              uint32_t *     pulPreviousNotificationValue); 

#define xTaskNotify(xTaskToNotify, ulValue, eAction)            
		xTaskGenericNotify((xTaskToNotify), (tskDEFAULT_INDEX_TO_NOTIFY),       
						   (ulValue), (eAction), NULL) 				  
            
#define xTaskNotifyAndQuery(xTaskToNotify, ulValue, eAction, pulPreviousNotifyValue)     
    	xTaskGenericNotify((xTaskToNotify), (tskDEFAULT_INDEX_TO_NOTIFY),
    					   (ulValue), (eAction), (pulPreviousNotifyValue))     			 
            
#define xTaskNotifyGive(xTaskToNotify)     
   	 	xTaskGenericNotify((xTaskToNotify), (tskDEFAULT_INDEX_TO_NOTIFY),   
           				   (0), eIncrement, NULL) 

其中通知方式(通知值更新方式)eNotifyAction

1
2
3
4
5
6
7
8
typedef enum
{
	eNoAction = 0, 				/* 无操作 */
	eSetBits 					/* 更新指定 bit */
	eIncrement 					/* 通知值加一 */
	eSetValueWithOverwrite 		/* 覆写的方式更新通知值*/
	eSetValueWithoutOverwrite 	/* 不覆写通知值 */
} eNotifyAction;

3.2接收

image-20250806195719064
  • 当任务通知用作与信号量时,使用函数获取信号量:ulTaskNotifyTake( )
  • 当任务通知用作与事件标志组或队列时,使用此函数来获取:xTaskNotifyWait( )

3.2.1ulTaskNotifyTake

ulTaskNotifyTake( xClearCountOnExit , xTicksToWait )

#define ulTaskNotifyTake(xClearCountOnExit, xTicksToWait)         
		ulTaskGenericNotifyTake((tskDEFAULT_INDEX_TO_NOTIFY),     
								(xClearCountOnExit),       
								(xTicksToWait)) 

原型

uint32_t ulTaskGenericNotifyTake(UBaseType_t  uxIndexToWaitOn, 
								 BaseType_t   xClearCountOnExit, 
							 	 TickType_t   xTicksToWait); 
image-20250806201907259

通知值清零模拟二值信号量,通知值减一模拟计数型信号量

3.2.2xTaskNotifyWait

此函数用于等待任务通知通知值中的指定比特位被置 1,此函数可以在等待前和成功等待到任务通知通知值中的指定比特位被置 1 后清零指定比特位,并且还能获取等待超时后任务通知的通知值。

#define xTaskNotifyWait(ulBitsToClearOnEntry,  
			            ulBitsToClearOnExit,    
			            pulNotificationValue,   
			            xTicksToWait)      
   	    xTaskGenericNotifyWait(tskDEFAULT_INDEX_TO_NOTIFY,   
					          (ulBitsToClearOnEntry),      
					          (ulBitsToClearOnExit),     
					          (pulNotificationValue),      
					          (xTicksToWait)) 
形参描述
uxIndexToWaitOn等待的任务通知索引(任务通知数组下标)
ulBitsToClearOnEntry进入等待前,把任务通知值的哪些位清 0(旧值对应位清零)
ulBitsToClearOnExit等待成功后,把任务通知值的哪些位清 0(新值对应位清零)
pulNotificationValue用于取出当时的 32 位通知值;若不需要,可设为 NULL
xTicksToWait最大阻塞等待时间(以系统节拍为单位)
返回值描述
pdTRUE等待任务通知成功(已满足等待条件)
pdFALSE等待任务通知失败(超时或被中断)

4.任务通知模拟信号量

模拟二值信号量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void task1(void* pvParamenters)
{
    uint8_t key  = 0;
    while(1)
    {
        key = key_scan(0);
        if(key==KEY0_PRES)
        {
            printf("任务通知模拟二值信号量释放\r\n");
            xTaskNotifyGive(task2_handle);
        }
    }
}
void task2(void* pvParamenters)
{
    uint32_t rev = 0;
    while(1)
    {
        rev = ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
        if(rev!=0)
        {
            printf("接受任务通知成功,获取模拟二值信号量\r\n");
        }
    }
    
}

模拟计数型信号量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void task1(void* pvParamenters)
{
    uint8_t key  = 0;
    while(1)
    {
        key = key_scan(0);
        if(key==KEY0_PRES)
        {
            printf("任务通知模拟计数型信号量释放\r\n");
            xTaskNotifyGive(task2_handle);
        }
    }
}
void task2(void* pvParamenters)
{
    uint32_t rev = 0;
    while(1)
    {
        rev = ulTaskNotifyTake(pdFALSE,portMAX_DELAY);
        if(rev!=0)
        {
            printf("接受任务通知成功,模拟获取计数型信号量,rev:%d\r\n",rev);
        }
        vTaskDelay(1000);
    }
}

5.任务通知模拟消息邮箱(类似队列)

消息邮箱其实就是长度为1的队列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void task1(void *pvParamenters)
{
    uint8_t key = 0;
    while (1)
    {
        key = key_scan(0);
        if ((key != 0) && (task2_handle != NULL))
        {
            printf("任务通知模拟消息邮箱发送,发送的键值为:%d\r\n", key);
            xTaskNotify(task2_handle, key, eSetValueWithOverwrite);
        }
        vTaskDelay(10);
    }
}
void task2(void *pvParamenters)
{
    uint32_t notify_val = 0;
    while (1)
    {
        xTaskNotifyWait(0, 0xFFFFFFFF, &notify_val, portMAX_DELAY);
        printf("接受到的通知值为:%d\r\n",notify_val);
    }
}

6.任务通知模拟事件标志组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#define EVENTBIT_0 1<<0
#define EVENTBIT_1 1<<1

void task1(void *pvParamenters)
{
    uint8_t key = 0;
    while (1)
    {
        key = key_scan(0);
        if(key==KEY0_PRES)
        {
            printf("更新bit0位置1\r\n");
            xTaskNotify(task2_handle,EVENTBIT_0,eSetBits);
        }
        else if(key==KEY1_PRES)
        {
            printf("更新bit1位置1\r\n");
            xTaskNotify(task2_handle,EVENTBIT_1,eSetBits);
        }
        vTaskDelay(10);
    }
}

void task2(void *pvParamenters)
{
    uint32_t notify_val = 0,event_bit = 0;
    while (1)
    {
        xTaskNotifyWait(0,0xFFFFFFFF,&notify_val,portMAX_DELAY);
        if(notify_val&EVENTBIT_0)
        {
            event_bit|=EVENTBIT_0;
        }
        if(notify_val&EVENTBIT_1)
        {
            event_bit|=EVENTBIT_1;
        }
        if(event_bit == (EVENTBIT_0|EVENTBIT_1))
        {
            printf("任务通知模拟事件标志组接收成功\r\n");
            event_bit = 0;
        }
    }
}

十八、软件定时器

image-20250806205006783

1.简介

定时器:指定时刻开始,经过指定时间,触发超时事件,用户可自定义周期

硬件定时器:芯片自带定时器,精度高,通过定时器中断,在中断服务函数处理

软件定时器:定时功能的软件,可设定定时周期,超时调用回调函数,在回调函数处理

软件定时器优缺点

优点:硬件定时器有限,而软件定时器理论更多;使用简单

缺点:精度低,对于高精度要求,不使用

FreeRTOS软件定时器特点

可裁剪:configUSE_TIMERS 配置项配置成 1

支持单次和周期

注:软件定时器的超时回调函数是由软件定时器服务任务调用的,软件定时器的超时回调函数本身不是任务,因此不能在该回调函数中使用可能会导致任务阻塞的 API 函数

软件定时器服务任务:vTaskStartScheduler()中会创建

软件定时器命令队列

FreeRTOS 提供了许多软件定时器相关的 API 函数,这些 API 函数大多是往定时器的队列中写入消息(发送命令),这个队列叫做软件定时器命令队列。这个队列控制软件定时器

在这里插入图片描述

软件定时器相关配置

configUSE_TIMERS配置为1

启动任务调度器时,会自动创建软件定时器的服务 / 守护任务prvTimerTask( );

软件定时器服务任务的优先级为configTIMER_TASK_PRIORITY = 31;

定时器的命令队列长度为configTIMER_QUEUE_LENGTH = 5;

注:

  • 服务任务服务于多个定时器,因此定时器的回调函数不能影响其他定时器

  • 回调函数要尽快实行,不能进入阻塞状态,既不能调用那些会阻塞任务的 API 函数,如vTaskDelay( );

  • 访问队列或者信号量的非零阻塞时间的 API 函数也不能调用

软件定时器状态

休眠态、运行态

单次定时器和周期定时器

在这里插入图片描述 在这里插入图片描述

2.软件定时器结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
typedef struct
{
	const char * pcTimerName 					/* 软件定时器名字 */
	ListItem_t xTimerListItem 					/* 软件定时器列表项 */
	TickType_t xTimerPeriodInTicks; 			/* 软件定时器的周期*/
	void * pvTimerID 							/* 软件定时器的 ID */
	TimerCallbackFunction_t pxCallbackFunction; /* 软件定时器的回调函数*/
	#if ( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t uxTimerNumber 				/* 软件定时器编号调试用*/
	#endif
	uint8_t ucStatus; 							/* 软件定时器的状态 */
} xTIMER;

3.软件定时器相关API

image-20250806211339288

3.1创建xTimerCreate

1
2
3
4
5
TimerHandle_t xTimerCreate(const char * const    	pcTimerName, 
						   const TickType_t  	    xTimerPeriodInTicks, 
						   const UBaseType_t 		uxAutoReload, 
						   void * const       		pvTimerID, 
						   TimerCallbackFunction_t  pxCallbackFunction);
形参描述
pcTimerName软件定时器名称(字符串)
xTimerPeriodInTicks定时周期,以系统时钟节拍(ticks)为单位
uxAutoReload定时器模式:
pdTRUE = 周期定时器(重复触发)
pdFALSE = 单次定时器(触发一次后停止)
pvTimerID用户自定义 ID,可用于多个定时器共用同一回调函数时区分实例
pxCallbackFunction定时器超时时的回调函数指针
返回值描述
NULL软件定时器创建失败
非 NULL 句柄软件定时器创建成功,返回该定时器的句柄

创建例子

1
2
timer1_handle = xTimerCreate("timer1",1000,pdFALSE,(void *)1,timer1_callback);
    timer2_handle = xTimerCreate("timer2",1000,pdTRUE,(void *)1,timer2_callback);

3.2开启xTimerStart

BaseType_t xTimerStart( TimerHandle_t xTimer, const TickType_t xTicksToWait );

参数:xTimer 待开启的软件定时器的句柄 xTickToWait 发送命令到软件定时器命令队列的最大等待时间

返回pdPASS,pdFAIL

3.3停止xTimerStop

BaseType_t xTimerStop( TimerHandle_t xTimer, const TickType_t xTicksToWait);

参数:xTimer 待停止的软件定时器的句柄 xTickToWait 发送命令到软件定时器命令队列的最大等待时间

返回pdPASS,pdFAIL

3.4复位xTimerReset

使软件定时器的重新开启定时

BaseType_t xTimerReset( TimerHandle_t xTimer, const TickType_t xTicksToWait);

参数:xTimer 待复位的软件定时器的句柄 xTickToWait 发送命令到软件定时器命令队列的最大等待时间

返回pdPASS,pdFAIL

3.5更改超时时间xTimerChangePeriod

BaseType_t xTimerChangePeriod( TimerHandle_t xTimer, const TickType_t xNewPeriod, const TickType_t xTicksToWait);

参数:xTimer 待更新的软件定时器的句柄 xNewPeriod 新的定时超时时间,单位:系统时钟节拍 xTickToWait 发送命令到软件定时器命令队列的最大等待时间

返回pdPASS,pdFAIL

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
TimerHandle_t timer1_handle = 0;/*单次定时器*/
TimerHandle_t timer2_handle = 0;/*周期定时器*/
void timer1_callback(TimerHandle_t pxTimer);
void timer2_callback(TimerHandle_t pxTimer);

timer1_handle = xTimerCreate("timer1",1000,pdFALSE,(void *)1,timer1_callback);
    timer2_handle = xTimerCreate("timer2",1000,pdTRUE,(void *)1,timer2_callback);

xTimerStart(timer2_handle,0);
xTimerStart(timer1_handle,0);

void timer1_callback(TimerHandle_t pxTimer)
{
    static uint32_t timer1 = 0;
    printf("Timer1的运行次数:%d\r\n",++timer1);
}

void timer2_callback(TimerHandle_t pxTimer)
{
    static uint32_t timer2 = 0;
    printf("Timer2的运行次数:%d\r\n",++timer2);
}

十九、Tickless低功耗模式

1.低功耗模式

STM32提供集中低功耗,睡眠,停止,待机

FreeRTOS也提供Tickless低功耗模式,通过动态调整SysTick 减少不必要的 CPU 唤醒,从而降低功耗

在这里插入图片描述

为了降低功耗,又不影响系统运行,可以在本该空闲任务执行期间,让 MCU 进入相应的低功耗模式;当其他任务准备运行的时候,唤醒 MCU 退出低功耗模式,但是进入低功耗之后,多久唤醒,下一个要运行的任务如何被准确唤醒?任何中断均可唤醒 MCU,若滴答定时器频繁中断则会影响低功耗的效果。这些问题可以这样解决:

将滴答定时器的中断周期修改为低功耗运行时间 推出低功耗之后,补上系统时钟节拍数

2.相关配置

使能低功耗 Tickless 模式:configUSE_TICKLESS_IDLE 定义系统进入相应低功耗模式的最短时长:configEXPECTED_IDLE_TIME_BEFORE_SLEEP 定义需要在系统进入低功耗模式之前执行的事务,例如:进入低功耗之前关闭外设时钟,以降低功耗:configPRE_SLEEP_PROCESSING(x),需要自己在 FreeRTOSconfig.h 里面自己定义 定义需要在系统退出低功耗模式之后执行的事务,例如:退出低功耗之后开启被关闭的外设时钟,使系统能够正常运行:configPOST_SLEEP_PROCESSING(x),需要自己在 FreeRTOSconfig.h 里面自己定义

3.实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#define configUSE_TICKLESS_IDLE                  1  
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP    2

//接着在 FreeRTOSconfig.h 文件中找个位置定义下面函数
#include "freertos_demo.h"
#define configPRE_SLEEP_PROCESSING( x )		PRE_SLEEP_PROCESSING()
#define configPOST_SLEEP_PROCESSING( x )	POSR_SLEEP_PROCESSING()

//freertos_demo.h 文件里声明这两个函数
void PRE_SLEEP_PROCESSING(void);
void POST_SLEEP_PROCESSING(void);

//freertos_demo.c 文件里,入口函数上面定义
//进入低功耗模式前要关闭的事情
void PRE_SLEEP_PROCESSING(void)
{
	__HAL_RCC_GPIOA_CLK_DISABLE();
	__HAL_RCC_GPIOB_CLK_DISABLE();
	__HAL_RCC_GPIOC_CLK_DISABLE();
	__HAL_RCC_GPIOD_CLK_DISABLE();
	__HAL_RCC_GPIOE_CLK_DISABLE();
	__HAL_RCC_GPIOF_CLK_DISABLE();
	__HAL_RCC_GPIOG_CLK_DISABLE();
}
//退出低功耗模式后需要打开的事情
void POST_SLEEP_PROCESSING(void)
{
	__HAL_RCC_GPIOA_CLK_ENABLE();
	__HAL_RCC_GPIOB_CLK_ENABLE();
	__HAL_RCC_GPIOC_CLK_ENABLE();
	__HAL_RCC_GPIOD_CLK_ENABLE();
	__HAL_RCC_GPIOE_CLK_ENABLE();
	__HAL_RCC_GPIOF_CLK_ENABLE();
	__HAL_RCC_GPIOG_CLK_ENABLE();
}

二十、FreeRTOS内存管理

1.FreeRTOS内存管理简介

FreeRTOS创建任务、队列、信号量等时,可用动态方法或者静态方法创建

  • 动态方法:申请时FreeRTOS从内存堆中自动申请,删除对象时自动释放
  • 静态方法:用户提供内存空间,但是占用的空间固定下来了,删除依旧占用

本节讲的是动态内存管理方式(实际是管理算法的区别heap.c)

freertosconfig.h中定义的宏管理内存堆的大小:

1
2
#define configTOTAL_HEAP_SIZE   ((size_t)(10 * 1024))   
/* FreeRTOS堆中可用的RAM总量, 单位: Byte, 无默认需定义 */

2.FreeRTOS内存管理算法

算法优点缺点
heap_1分配简单,时间确定只允许申请内存,不允许释放内存
heap_2允许申请和释放内存不能合并相邻空闲块,产生碎片,时间不定
heap_3直接调用 C 库 malloc()/free(),实现简单(不使用)速度慢、时间不定
heap_4相邻空闲块可合并,减少碎片时间不定
heap_5支持管理多个非连续内存区域,功能同 heap_4时间不定

2.1 heap_1

heap_1只能申请内存无法释放内存,若工程中创建好的任务、队列、信号量等不需要删除,那使用heap_1

heap_1分配图示:

image-20250806221459608

2.2 heap_2

heap_2采用最适应算法,支持释放内存,但是并不能将相临空闲内存块合并,不可避免产生内存碎片。如果需要频繁的创建和删除任务,且所创建的任务堆栈都相同,可以用heap_2

最适应算法:

  1. 假设heap 有 3 块空闲内存:5,25,50
  2. 申请一个总共20字节的任务,先找到最小能满足的内存:25
  3. 删除任务内存释放后,得到20、5两块内存碎片
  4. 最后存在:5,5,20,50

heap_2分配图示:

image-20250806222759001

2.3 heap_4

heap_4 使用了首次适应算法,能够将空闲且相邻的内存进行合并。适合频繁分配释放大小不同内存。

首次适应算法:

  1. 假设heap 有 3 块空闲内存(按内存块地址由低到高排序):5,50,25
  2. 申请一个总共20字节的任务,先找到第一个能满足的内存:50
  3. 删除任务内存释放后,这块内存依旧为50
  4. 最后存在:5,50,25

heap_4分配图示:

image-20250806223412620

2.4 heap_5

heap_5能管理多块不连续内存,但需要用户指定内存区域

使用如下结构体:

1
2
3
4
5
typedef struct HeapRegion
{
	uint8_t * pucStartAddress; /* 内存区域的起始地址 */
	size_t xSizeInBytes; /* 内存区域的大小,单位:字节 */
} HeapRegion_t;

指定多块不连续内存

1
2
3
4
5
6
7
8
Const HeapRegion_t xHeapRegions[] =
{
	{ (uint8_t *)0x80000000, 0x10000 }, /* 内存区域1 */
	{ (uint8_t *)0x90000000, 0xA0000 }, /* 内存区域2 */
	{ NULL, 0 } /* 数组终止标志 */
};

vPortDefineHeapRegions(xHeapRegions);
使用 Hugo 构建
主题 Stack 由 Jimmy 设计