CubeMax使用FreeRTOS

硬件STM32F103C8T6

创建工程

使用了FreeRTOS,FreeRTOS的时基使用的是Systick,而STM32CubeMX中默认的HAL库时基也是Systick,为了避免可能的冲突,最好将HAL库的时基换做其它的硬件定时器:

中间件,配置freeRTOS

创建任务

工程树以及任务创建位置。

创建任务参数

Task Name :任务的名字,用作一个标志

Priority:任务的优先级,优先级越高先执行 (默认优先级25,宏定义的)

Stack Size: 任务栈的大小,保存变量等信息,填 128 意味着栈空间是 128 * 4 = 512 字节

Entry Functiom:任务函数入口,要按照freeRTOS函数原型来写。函数内要包含死循环,不然执行完任务会自动被删除。

Code Generation Option:(代码生成选项)选择默认值就可以。不做配置。

 Parameter(任务参数):传入的任务参数,类型是void *

Allocation(内存分配方式):Dynamic(动态分配)或 Static(静态分配,需额外配置栈数组)。

Buffer Name(缓冲区名称,部分场景用)

Control Block Name(任务控制块名称)

void StartTask03(void *argument) {
    for(;;) { 
        // 任务逻辑:LED 翻转、数据处理等
        osDelay(100); 
    }
}

在单片机开发中需要经常使用到调试,串口进行调试是一个很好的选择;接下来来配置串口重定向方便使用串口进行发送数据。

打开串口1,配置异步模式,设置波特率。

串口重定向函数

/*   实现串口重新定向*/
//printf  -> fputc :打印一个字节数据

int fputc(int ch,FILE *f)
{
	HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,100);
	return ch;
}

freeRTOS原生创建任务函数

BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数
                        const char * const pcName, // 任务的名字
                        const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
                        void * const pvParameters, // 调用任务函数时传入的参数
                        UBaseType_t uxPriority,    // 优先级
                        TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
参数描述
pvTaskCode函数指针,任务对应的 C 函数。任务应该永远不退出,或者在退出时调用 "vTaskDelete(NULL)"。
pcName任务的名称,仅用于调试目的,FreeRTOS 内部不使用。pcName 的长度为 configMAX_TASK_NAME_LEN。
usStackDepth每个任务都有自己的栈,usStackDepth 指定了栈的大小,单位为 word。例如,如果传入 100,表示栈的大小为 100 word,即 400 字节。最大值为 uint16_t 的最大值。确定栈的大小并不容易,通常是根据估计来设定。精确的办法是查看反汇编代码。
pvParameters调用 pvTaskCode 函数指针时使用的参数:pvTaskCode(pvParameters)。
uxPriority任务的优先级范围为 0~(configMAX_PRIORITIES – 1)。数值越小,优先级越低。如果传入的值过大,xTaskCreate 会将其调整为 (configMAX_PRIORITIES – 1)。
pxCreatedTask用于保存 xTaskCreate 的输出结果,即任务的句柄(task handle)。如果以后需要对该任务进行操作,如修改优先级,则需要使用此句柄。如果不需要使用该句柄,可以传入 NULL。
返回值成功时返回 pdPASS,失败时返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因是内存不足)。请注意,文档中提到的失败返回值是 pdFAIL 是不正确的。pdFAIL 的值为 0,而 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 的值为 -1。

任务调度算法

抢占式调度(任务优先级)

  • 基本概念:在抢占式调度中,FreeRTOS 会为每个任务分配一个优先级。当一个高优先级任务进入就绪状态时,无论当前正在运行的任务处于何种状态(只要不是中断服务程序执行期间),调度器都会立即暂停当前低优先级任务的执行,转而执行高优先级任务。只有当高优先级任务进入阻塞状态(比如等待信号量、延时等),低优先级任务才会有机会继续执行。
  • 优势:能够确保重要的任务(即优先级高的任务)可以及时得到 CPU 资源,满足系统对实时性的要求,比如在工业控制中,紧急故障处理任务优先级高,能被立即执行 。
  • 劣势:如果设计不当,可能会导致低优先级任务长时间得不到执行,出现 “饥饿” 现象 。同时,频繁的任务切换也会带来一定的系统开销。

时间片轮转

  • 基本概念:当多个任务具有相同的优先级时,FreeRTOS 采用时间片轮转的方式来调度这些任务。系统会给每个任务分配一个固定的时间片(即允许任务连续运行的时间长度),当任务运行完自己的时间片后,即使任务还没有执行完毕,调度器也会暂停该任务的执行,并将 CPU 使用权交给下一个同优先级的任务,让其运行一个时间片,以此类推。当所有同优先级的任务都运行过一个时间片后,又会从第一个任务开始新一轮的时间片轮转 。
  • 优势:保证了同等优先级的任务都能公平地获得 CPU 资源,避免了某个任务长时间占用 CPU 导致其他同优先级任务得不到执行的情况,适用于对公平性要求较高且实时性要求相对不那么严苛的场景,如一些简单的多任务应用程序。
  • 劣势:如果时间片设置过短,会导致任务切换频繁,增加系统开销;如果时间片设置过长,又可能会影响任务的响应速度,降低系统的实时性 。

协作式调度

  • 基本概念:在协作式调度模式下,任务只有在主动放弃 CPU 控制权时,调度器才会切换到其他任务执行。也就是说,当前运行的任务需要主动调用类似 taskdelay() 这样的函数,告诉调度器自己愿意让出 CPU 资源,然后调度器才会选择下一个就绪的任务来运行。即使有更高优先级的任务进入就绪状态,只要当前任务不主动释放 CPU ,高优先级任务也无法得到执行 。
  • 优势:任务切换的时机完全由任务自身控制,因此可以减少任务切换带来的系统开销,在一些对系统开销敏感且实时性要求不高的简单应用中比较适用。
  • 劣势:实时性较差,因为高优先级任务不能及时抢占 CPU 资源,必须等待当前任务主动释放,可能会导致紧急任务无法及时得到处理,影响系统的响应速度 。

Cube创建队列

1. Queue Name(队列名称)

  • 作用:给队列起一个标识名,方便代码里通过名称(或句柄)操作队列(比如 osMessageQueueGet / xQueueSend 等函数)。
  • 示例myQueue02 就是自定义的队列名,代码里可通过 osMessageQueueId_t myQueue02Handle;(CMSIS - RTOS 风格)或 QueueHandle_t myQueue02;(FreeRTOS 原生风格)关联,用于发送 / 接收数据。

2. Queue Size(队列长度)

  • 作用:指定队列最多能存多少个 “数据项(Item)” 。
  • 示例:填 16 表示这个队列最多能缓存 16 个独立的数据项,超出后再发数据会触发 “队列满” 逻辑(比如发送函数阻塞、返回失败等,取决于调用时的参数)。

3. Item Size(数据项大小)

  • 作用:定义队列中单个数据项的字节数,FreeRTOS 会按这个大小分配队列内存、拷贝数据。
  • 示例:填 uint16_t(实际是 2 字节),意味着队列里每个数据项是 2 字节,发送 uint16_t 类型变量(如 uint16_t data = 0x1234; )时,队列会完整拷贝这 2 字节数据。

4. Allocation(内存分配方式)

  • 选项:一般选 Dynamic(动态分配)或 Static(静态分配,需手动定义缓冲区)。
  • Dynamic 特点
    • CubeMX 会让 FreeRTOS 用 pvPortMalloc(基于配置的堆内存)动态分配队列的存储缓冲区。
    • 优点:不用手动算内存,灵活;缺点:如果堆内存不足,队列创建可能失败(需处理错误)。
  • Static 模式:需自己在代码里定义静态数组(作为队列缓冲区),适合对内存布局严格管控的场景(比如内存分区固定、避免动态分配碎片化),CubeMX 会提示你补充静态缓冲区定义。

5. Buffer Name(缓冲区名称,动态分配时可忽略)

  • 作用:若用 Static 分配,需填你在代码里定义的静态缓冲区数组名,让 FreeRTOS 关联使用。
  • Dynamic 场景:填 NULL 即可,FreeRTOS 内部会自动管理动态缓冲区,无需手动命名。

6. Buffer size(缓冲区大小,动态分配时无意义)

  • 作用:静态分配时,需手动填缓冲区的总字节数(一般 = Queue Size × Item Size ),让 FreeRTOS 知道可用内存范围。
  • Dynamic 场景:显示 n/a(无意义),因为动态分配由堆管理,无需手动指定总大小。

7. Control Block Name(队列控制块名称)

  • 作用:队列控制块(QueueControlBlock )是 FreeRTOS 管理队列的核心结构体(存队列长度、头 / 尾指针、互斥锁等信息)。
  • 常规配置:填 NULL 即可,FreeRTOS 内部会自动创建、管理控制块,上层逻辑通过队列句柄(osMessageQueueId_t 或 QueueHandle_t )操作,无需直接用控制块名称。

CUbemax封装的函数

信号量的创建

1. Semaphore Name(信号量名称)

  • 作用:给信号量起一个标识性名字,方便代码中通过名称(或句柄)操作信号量。
  • 示例myBinarySem02 是自定义的信号量名,代码中会生成类似 osSemaphoreId_t myBinarySem02Handle;(CMSIS-RTOS 风格)用于 osSemaphoreRelease/xSemaphoreGive(释放)、osSemaphoreAcquire/xSemaphoreTake(获取)等操作。

2. Initial State(初始状态)

  • 选项:一般选 Available(可用)。
  • Available 含义:信号量创建后,初始值为 **“可用”**(相当于信号量被 “释放” 了一次 )。任务调用 xSemaphoreTake 时,能直接获取到信号量(不会阻塞,除非被其他任务抢占)。
  • Not Available 含义:信号量初始值为 **“不可用”**(相当于信号量未被释放)。任务调用 xSemaphoreTake 时会阻塞,直到有任务调用 xSemaphoreGive 释放信号量。

3. Allocation(内存分配方式)

  • 选项Dynamic(动态分配)或 Static(静态分配,需手动定义控制块 / 缓冲区 )。
  • Dynamic 特点
    • CubeMX 会让 FreeRTOS 用 pvPortMalloc(基于配置的堆内存)动态分配信号量控制块(Semaphore Control Block) 的内存。
    • 优点:无需手动管理内存,灵活;缺点:若堆内存不足,信号量创建可能失败(需处理错误)。
  • Static 模式:需自己在代码里定义静态的信号量控制块结构体,适合对内存布局严格管控的场景(比如内存分区固定、避免动态分配碎片化 ),CubeMX 会提示你补充静态控制块的定义。

4. Control Block Name(控制块名称)

  • 作用:信号量控制块(SemaphoreControlBlock )是 FreeRTOS 管理信号量的核心结构体,存信号量的状态(可用 / 不可用)、等待任务列表等信息。
  • 常规配置:填 NULL 即可,FreeRTOS 内部会自动创建、管理控制块。上层逻辑通过信号量句柄操作,无需直接引用控制块名称(高级场景才会用到,比如手动调试、内存诊断 )。

cubemax中封装的创建信号量函数

互斥量的创建

1. Mutex Name(互斥锁名称)

  • 自定义标识,如 myRecursiveMutex01 ,代码中用于关联互斥锁句柄(方便通过名称操作,如 xSemaphoreTakeRecursive/xSemaphoreGiveRecursive )。

2. Allocation(内存分配方式)

  • 选 Dynamic 时,FreeRTOS 用 pvPortMalloc 动态分配互斥锁控制块内存;若需静态管理内存(避免动态分配碎片化),可改 Static(需手动定义控制块变量 )。

3. Control Block Name(控制块名称)

  • 递归互斥锁的控制块(RecursiveMutexControlBlock )存锁状态、等待任务等信息。一般填 NULL ,FreeRTOS 自动管理,无需手动引用(高级调试场景可能用到 )。

优先级继承与优先级反转

优先级继承:容易发生在信号量的使用中。低优先级获取到信号量未释放,高优先级得不到的情况。

优先级反转:使用互斥量来避免优先级继承。低优先级在持有互斥量时,优先级提高。确保低优先级任务快速执行。而后转为高优先级任务执行。

事件组

事件组可以简单地认为就是一个整数:

  • 每一位表示一个事件
  • 每一位事件的含义由程序员决定,比如:Bit0表示用来串口是否就绪,Bit1表示按键是否被按下
  • 这些位,值为1表示事件发生了,值为0表示事件没发生
  • 一个或多个任务、ISR都可以去写这些位;一个或多个任务、ISR都可以去读这些位
  • 可以等待某一位、某些位中的任意一个,也可以等待多位。

事件组用一个整数来表示,其中的高8位留给内核使用,只能用其他的位来表示事件。那么这个整数是多少位的?

  • 如果configUSE_16_BIT_TICKS是1,那么这个整数就是16位的,低8位用来表示事件
  • 如果configUSE_16_BIT_TICKS是0,那么这个整数就是32位的,低24位用来表示事件
  • configUSE_16_BIT_TICKS是用来表示Tick Count的,怎么会影响事件组?这只是基于效率来考虑
    • 如果configUSE_16_BIT_TICKS是1,就表示该处理器使用16位更高效,所以事件组也使用16位
    • 如果configUSE_16_BIT_TICKS是0,就表示该处理器使用32位更高效,所以事件组也使用32位

任务通知

每个任务都有一个结构体:TCB(Task Control Block),里面有2个成员:

  • 一个是uint8_t类型,用来表示通知状态
  • 一个是uint32_t类型,用来表示通知值
typedef struct tskTaskControlBlock
{
    ......
    /* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */
    volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    ......
} tskTCB;

通知状态有3种取值:

  • taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
  • taskWAITING_NOTIFICATION:任务在等待通知
  • taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为pending(有数据了,待处理)
##define taskNOT_WAITING_NOTIFICATION              ( ( uint8_t ) 0 )  /* 也是初始状态 */
##define taskWAITING_NOTIFICATION                  ( ( uint8_t ) 1 )
##define taskNOTIFICATION_RECEIVED                 ( ( uint8_t ) 2 )

通知值可以有很多种类型:

  • 计数值
  • 位(类似事件组)
  • 任意数值

 任务通知的使用

使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、计数型信号量、二进制信号量、事件组。

 两类函数

任务通知有2套函数,简化版、专业版,列表如下:

  • 简化版函数的使用比较简单,它实际上也是使用专业版函数实现的
  • 专业版函数支持很多参数,可以实现很多功能
简化版专业版
发出通知xTaskNotifyGive vTaskNotifyGiveFromISRxTaskNotify xTaskNotifyFromISR
取出通知ulTaskNotifyTakexTaskNotifyWait

 xTaskNotifyGive/ulTaskNotifyTake

在任务中使用xTaskNotifyGive函数,在ISR中使用vTaskNotifyGiveFromISR函数,都是直接给其他任务发送通知:

  • 使得通知值加一
  • 并使得通知状态变为"pending",也就是taskNOTIFICATION_RECEIVED,表示有数据了、待处理

可以使用ulTaskNotifyTake函数来取出通知值:

  • 如果通知值等于0,则阻塞(可以指定超时时间)
  • 当通知值大于0时,任务从阻塞态进入就绪态
  • 在ulTaskNotifyTake返回之前,还可以做些清理工作:把通知值减一,或者把通知值清零

使用ulTaskNotifyTake函数可以实现轻量级的、高效的二进制信号量、计数型信号量。

这几个函数的原型如下:

BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );

void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle, BaseType_t *pxHigherPriorityTaskWoken );

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );

xTaskNotifyGive函数的参数说明如下:

参数说明
xTaskToNotify任务句柄(创建任务时得到),给哪个任务发通知
返回值必定返回pdPASS

vTaskNotifyGiveFromISR函数的参数说明如下:

参数说明
xTaskHandle任务句柄(创建任务时得到),给哪个任务发通知
pxHigherPriorityTaskWoken被通知的任务,可能正处于阻塞状态。 此函数发出通知后,会把它从阻塞状态切换为就绪态。 如果被唤醒的任务的优先级,高于当前任务的优先级, 则"*pxHigherPriorityTaskWoken"被设置为pdTRUE, 这表示在中断返回之前要进行任务切换。

ulTaskNotifyTake函数的参数说明如下:

参数说明
xClearCountOnExit函数返回前是否清零: pdTRUE:把通知值清零 pdFALSE:如果通知值大于0,则把通知值减一
xTicksToWait任务进入阻塞态的超时时间,它在等待通知值大于0。 0:不等待,即刻返回; portMAX_DELAY:一直等待,直到通知值大于0; 其他值:Tick Count,可以用*pdMS_TO_TICKS()*把ms转换为Tick Count
返回值函数返回之前,在清零或减一之前的通知值。 如果xTicksToWait非0,则返回值有2种情况: 1. 大于0:在超时前,通知值被增加了 2. 等于0:一直没有其他任务增加通知值,最后超时返回0

xTaskNotify/xTaskNotifyWait

xTaskNotify 函数功能更强大,可以使用不同参数实现各类功能,比如:

  • 让接收任务的通知值加一:这时 xTaskNotify() 等同于 xTaskNotifyGive()
  • 设置接收任务的通知值的某一位、某些位,这就是一个轻量级的、更高效的事件组
  • 把一个新值写入接收任务的通知值:上一次的通知值被读走后,写入才成功。这就是轻量级的、长度为1的队列
  • 用一个新值覆盖接收任务的通知值:无论上一次的通知值是否被读走,覆盖都成功。类似 xQueueOverwrite() 函数,这就是轻量级的邮箱。

xTaskNotify() 比 xTaskNotifyGive() 更灵活、强大,使用上也就更复杂。xTaskNotifyFromISR() 是它对应的ISR版本。

这两个函数用来发出任务通知,使用哪个函数来取出任务通知呢?

使用 xTaskNotifyWait() 函数!它比 ulTaskNotifyTake() 更复杂:

  • 可以让任务等待(可以加上超时时间),等到任务状态为"pending"(也就是有数据)
  • 还可以在函数进入、退出时,清除通知值的指定位

这几个函数的原型如下:

BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction );

BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
                               uint32_t ulValue, 
                               eNotifyAction eAction, 
                               BaseType_t *pxHigherPriorityTaskWoken );

BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, 
                            uint32_t ulBitsToClearOnExit, 
                            uint32_t *pulNotificationValue, 
                            TickType_t xTicksToWait );

xTaskNotify函数的参数说明如下:

参数说明
xTaskToNotify任务句柄(创建任务时得到),给哪个任务发通知
ulValue怎么使用ulValue,由eAction参数决定
eAction见下表
返回值pdPASS:成功,大部分调用都会成功 pdFAIL:只有一种情况会失败,当eAction为eSetValueWithoutOverwrite, 并且通知状态为"pending"(表示有新数据未读),这时就会失败。

eNotifyAction参数说明:

eNotifyAction取值说明
eNoAction仅仅是更新通知状态为"pending",未使用ulValue。 这个选项相当于轻量级的、更高效的二进制信号量。
eSetBits通知值 = 原来的通知值 | ulValue,按位或。 相当于轻量级的、更高效的事件组。
eIncrement通知值 = 原来的通知值 + 1,未使用ulValue。 相当于轻量级的、更高效的二进制信号量、计数型信号量。 相当于**xTaskNotifyGive()**函数。
eSetValueWithoutOverwrite不覆盖。 如果通知状态为"pending"(表示有数据未读), 则此次调用xTaskNotify不做任何事,返回pdFAIL。 如果通知状态不是"pending"(表示没有新数据), 则:通知值 = ulValue。
eSetValueWithOverwrite覆盖。 无论如何,不管通知状态是否为"pendng", 通知值 = ulValue。

xTaskNotifyFromISR函数跟xTaskNotify很类似,就多了最后一个参数pxHigherPriorityTaskWoken。在很多ISR函数中,这个参数的作用都是类似的,使用场景如下:

  • 被通知的任务,可能正处于阻塞状态
  • xTaskNotifyFromISR函数发出通知后,会把接收任务从阻塞状态切换为就绪态
  • 如果被唤醒的任务的优先级,高于当前任务的优先级,则"*pxHigherPriorityTaskWoken"被设置为pdTRUE,这表示在中断返回之前要进行任务切换。

xTaskNotifyWait函数列表如下:

参数说明
ulBitsToClearOnEntry在xTaskNotifyWait入口处,要清除通知值的哪些位? 通知状态不是"pending"的情况下,才会清除。 它的本意是:我想等待某些事件发生,所以先把"旧数据"的某些位清零。 能清零的话:通知值 = 通知值 & ~(ulBitsToClearOnEntry)。 比如传入0x01,表示清除通知值的bit0; 传入0xffffffff即ULONG_MAX,表示清除所有位,即把值设置为0
ulBitsToClearOnExit在xTaskNotifyWait出口处,如果不是因为超时推出,而是因为得到了数据而退出时: 通知值 = 通知值 & ~(ulBitsToClearOnExit)。 在清除某些位之前,通知值先被赋给"*pulNotificationValue"。 比如入0x03,表示清除通知值的bit0、bit1; 传入0xffffffff即ULONG_MAX,表示清除所有位,即把值设置为0
pulNotificationValue用来取出通知值。 在函数退出时,使用ulBitsToClearOnExit清除之前,把通知值赋给"*pulNotificationValue"。 如果不需要取出通知值,可以设为NULL。
xTicksToWait任务进入阻塞态的超时时间,它在等待通知状态变为"pending"。 0:不等待,即刻返回; portMAX_DELAY:一直等待,直到通知状态变为"pending"; 其他值:Tick Count,可以用*pdMS_TO_TICKS()*把ms转换为Tick Count
返回值1. pdPASS:成功 这表示xTaskNotifyWait成功获得了通知: 可能是调用函数之前,通知状态就是"pending"; 也可能是在阻塞期间,通知状态变为了"pending"。 2. pdFAIL:没有得到通知。

软件定时器

  1. Timer Name:定时器名称,用于代码中标识该定时器,自定义命名(如myTimer01 ),方便识别和调用。
  2. Callback:回调函数名,定时器触发时会执行的函数,需在代码中实现该函数逻辑(如Callback01 ),处理定时到期后的任务。
  3. Type:定时器类型,osTimerPeriodic 表示周期定时器,会按设定周期重复触发;还有 osTimerOnce (单次触发,只执行一次 )等类型。
  4. Code Generation Option:代码生成选项,Default 即默认生成方式,按常规逻辑为定时器生成初始化等代码。
  5. Parameter:传递给回调函数的参数,设为 NULL 表示不传递额外参数,也可填自定义变量,在回调中使用。
  6. Allocation:内存分配方式,Dynamic 表示动态分配内存,运行时按需分配;还有 Static (静态分配,需预先指定内存区域 )。
  7. Control Block Name:控制块名称,NULL 时用默认命名规则,也可自定义,控制块用于 FreeRTOS 管理定时器的运行状态等信息 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值