1. 基础知识
注意: 在 RTOS 中是优先值越高则优先级越高(和 ucos/Linux 的相反)
在移植的时候, 主要裁剪 FreeRTOS/Source/portable 文件夹, 该文件夹用来针对不同 MCU 做的一些处理, 如下图所示, 我们只需要使用:
1.1 配置工程时, 选择 memMang 时, 一般使用 heap_4.c
heap_4: 优点在于可以有效的利用内存碎片来合并为一个大内存. 缺点在于只能用来一个 ram 里.
heap_5: 一般针对有外部 RAM 才用到, 优点在于可以同时利用内部 ram 和外部 ram 来进行内存碎片合并.
最终添加的库文件有:
然后我们在分配释放内存的时候, 就尽量使用 RTOS 带的函数来实现, 分配 / 释放函数如下所示:
- void *pvPortMalloc( size_t xWantedSize );
- void vPortFree( void *pv );
1.2 添加头文件路径
添加 FreeRTOS\include
添加 FreeRTOS\portable\RVDS\ARM_CM3
并将原子中的 FreeRTOSConfig.h 也复制到我们项目的 FreeRTOS\include 中(用来配置 RTOS 系统)
2. FreeRTOSConfig.h 配置介绍
一般会写 configXXXXX 或者 INCLUDE_XXXX 类似的宏, 这两个宏区别在于:
configXXXXX
用来实现不同功能, 比如定义 configUSE_COUNTING_SEMAPHORES 为 1 时, 表示使用计数信号量
INCLUDE_XXXX
用来是否将某个 API 函数编译进程序中.
比如定义 INCLUDE_xTaskGetSchedulerState 为 1 时, 则将会编译 xTaskGetSchedulerState()函数, 如下图所示:
3. FreeRTOS 任务状态
3.1 运行态
指当前任务正在运行.
3.2 就绪态
指当前任务正在等待调度, 因为有个高优先级 / 同优先级的任务正在运行中
3.3 阻塞态
当前任务处于等待外部事件通知或通过 vTaskDelay()函数进入休眠了, 外部事件通知常见有信号量, 等待队列, 事件标志组, 任务通知.
3.4 挂起态
类似于暂停, 表示不会再参与任务调度了, 通过 vTaskSuspend()实现, 重新恢复调度则使用 xTaskResume()
4. FreeRTOS 中断配置
4.1 回忆 stm32 NVIC 中断
Stm32 可以设置 NVIC 中断组数为 0~4, 其中 0~4 区别在于如下图所示:,
比如我们设置为 NVIC_PriorityGroup_4 时:
表示抢占优先级为 4bit(即为 2^4, 为 0~15 个抢占优先级), 副优先级为 0bit(表示没有).
4.2 抢占优先级和副优先级的区别:
1. 抢占优先级和副优先级的值越低, 则优先级越高
2. 高的抢占优先级的中断可以直接打断低的抢占优先级的中断
3. 高的副优先级的中断不可以打断低的副优先级的中断(只是两个相同抢占优先级的中断同时来的时候, 只会优先选择高的副优先级)
4.3 FreeRTOS 中断配置宏
configKERNEL_INTERRUPT_PRIORITY
用来配置中断最低抢占优先级, 也就是可以 FreeRTOS 可以管理的最小抢占优先级, 所以使用 FreeRTOS 时, 我们尽量设置 stm32 为 NVIC_PriorityGroup_4, 这样就可以管理 16 个优先级了.
configMAX_SYSCALL_INTERRUPT_PRIORITY
用来配置 FreeRTOS 能够安全管理的的最高优先级. 比如原子的 FreeRTOSConfig.h 里就设置为 5, 而 0~4 的优先级中断就不会被 FreeRTOS 因为开关中断而禁止掉(一直都会有), 并且不能调用 RTOS 中的 "FromISR" 结尾的 API 函数.
如下图所示(来自原子手册):
4.3 FreeRTOS 中断开关函数
- portENABLE_INTERRUPTS();
- // 开中断, 将 configMAX_SYSCALL_INTERRUPT_PRIORITY 至 configKERNEL_INTERRUPT_PRIORITY 之间的优先级中断打开
- portDISABLE_INTERRUPTS();
- // 关中断, 将 configMAX_SYSCALL_INTERRUPT_PRIORITY 至 configKERNEL_INTERRUPT_PRIORITY 之间的优先级中断禁止掉
5. 任务常用 API 函数
5.1 xTaskCreate 创建任务函数
定义如下:
- xTaskCreate( TaskFunction_t pxTaskCode, // 任务函数, 用来供给函数指针调用的
- const char * const pcName, // 任务的字符串别名
- const uint16_t usStackDepth, // 任务堆栈深度, 实际申请到的堆栈是该参数的 4 倍
- void * const pvParameters, // 函数参数, 用来供给指针调用的
- UBaseType_t uxPriority, // 优先级, 越高优先级高, 范围为 0~configMAX_PRIORITIES-1
- // 注意优先级 0 会创建为空闲任务, 优先级 configMAX_PRIORITIES-1 会创建一个软件定时器服务任务(管理定时器的)
- TaskHandle_t * const pxCreatedTask ); // 任务句柄, 该句柄可以用于挂起 / 恢复 / 删除对应的任务
- // 返回值 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(-1): 表示创建任务堆空间不足 pdPASS(1): 创建成功
5.2 taskENTER_CRITICAL()和 taskEXIT_CRITICAL()
用于任务中进入 / 退出临界区, 调用 taskENTER_CRITICAL()主要会关闭其他任务调度. 而 taskEXIT_CRITICAL()则会恢复任务调度, 一般用于初始化外设等.
5.3 taskENTER_CRITICAL_FROM_ISR()和 taskEXIT_CRITICAL_FROM_ISR()
用于在中断函数中进入 / 退出临界区, 作用和上面一样
5.4 挂起 / 恢复 / 删除任务函数
- void vTaskSuspend( TaskHandle_t xTaskToSuspend ); // 挂起一个任务, 参数为挂起任务的句柄, 如果为 NULL 则表示挂起自身任务
- void vTaskResume( TaskHandle_t xTaskToResume ); // 恢复一个任务
- BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume);// 从中断函数中恢复一个任务, 返回 1 表示恢复成功
- void vTaskDelete( TaskHandle_t xTaskToDelete ); // 删除一个任务, 如果从任务函数中退出的话, 则需要调用 vTaskDelete(NULL)来删除自身任务
5.5 vTaskDelay()延时函数
void vTaskDelay( const TickType_t xTicksToDelay ); // 参数表示延时的系统滴答数
比如延时 500ms 可以写为: vTaskDelay( 500/portTICK_RATE_MS );
portTICK_RATE_MS 是个宏, 表示当前系统的 1 个滴答需要多少 ms, 而 500/portTICK_RATE_MS 则表示当前 500ms 需要多少个系统滴答数.
6. 队列
6.1 简介
队列用于任务与任务或者任务与中断之间的通信. 比如 key 任务检测到按键按下时, 则可以通过队列向 lcd 显示任务发送信息, 使得 lcd 切换界面.
队列采用先进先出存储机制. 队列发送数据可以有两种方式: 浅拷贝, 深拷贝.
数据量不大的情况下, 都使用深拷贝(会分配新的空间, 并进行数据拷贝, 缺点在于耗时)
数据量大的情况下, 都使用浅拷贝(通过指针方式, 前提是要发送的数据必须不会被释放的)
6.2 队列的优点
队列可以通过任何任务或者中断进行访问, 可以随时存取数据消息.
并且出入队的时候可以进行任务阻塞, 比如某个任务进行读消息出队时, 如果没有消息, 则可以实现进入休眠状态, 直到有消息才唤醒任务.
6.3 队列创建删除相关 API
- QueueHandle_t xQueueCreate( uxQueueLength, uxItemSize );
- // 动态创建队列, 内存会交给 RTOS 自动分配
- // uxQueueLength: 队列长度(表示队列中最大多少条消息),uxItemSize: 每个队列消息的长度(以字节为单位)
- // 返回值: NULL(0, 表示分配失败), 非 0(表示返回该队列分配好的地址)
- // 注意: 使用自动分配时, 需要配置 configSUPPORT_DYNAMIC_ALLOCATION 宏为 1, 否则只能由用户来分配.
- QueueHandle_t xQueueCreateStatic( uxQueueLength, uxItemSize, pucQueueStorage, pxQueueBuffer );
- // 静态创建队列, 内存需要由用户事先分配好
- // uxQueueLength: 队列长度(表示队列中最大多少条消息),uxItemSize: 每个队列消息的长度(以字节为单位)
- // pucQueueStorage: 指向用户事先分配好的存储区内存(必须为 uint8_t 型)
- // pxQueueBuffer: 指向队列结构体, 用来提供给 RTOS 初始化. 然后给用户使用
- // 返回值: NULL(0, 表示分配失败), 非 0(表示返回该队列分配好的地址)
- vQueueDelete( QueueHandle_t xQueue );
- // 删除队列, 并释放空间
- xQueueReset( xQueue );
- // 将队列里的消息清空一次, 也就是恢复初始状态
6.4 队列出入队相关 API
- xQueueSend( xQueue, pvItemToQueue, xTicksToWait );
- // 插入队尾, 和 xQueueSendToBack 函数效果一致
- // xQueue: 队列句柄
- //PvItemToQueue: 消息数据, 会通过数据拷贝到队列中, 如果想使用浅拷贝, 则可以发送一个变量来存储要真正发送的缓冲区地址即可.
- // xTicksToWait: 阻塞时间, 单位为 RTOS 时钟滴答值, 如果 configTICK_RATE_HZ 是 1000, 则填入的值表示阻塞的是多少 ms, 否则的话需要通过 X/portTICK_RATE_MS 来转换一下, 才能实现阻塞 Xms.
- //xTicksToWait==0: 表示入队满了, 则直接退出该函数
- // xTicksToWait==portMAX_DELAY: 表示一直阻塞, 直到队列有空位为止.
- // 注意: INCLUDE_vTaskSuspend 宏必须为 1, 否则任务无法进入休眠状态实现阻塞效果.
- // 返回值: errQUEUE_FULL(队列已满) pdPASS(通过)
- xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait );
- // 插入队头, 参数和上面描述一致
- xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait );
- // 插入队尾, 参数和上面描述一致
- xQueueOverwrite( xQueue, pvItemToQueue );
- // 将之前未出队的旧数据全部清空, 然后再入队, 该函数适用于长度为 1 的队列
- xQueueReceive( xQueue, pvBuffer, xTicksToWait );
- // 从队列头部读出一个消息, 并且这个消息会出队(删除掉)
- xQueuePeek( xQueue, pvBuffer, xTicksToWait );
- // 从队列头部读出一个消息, 但是这个消息不会出队(不会删除)
PS: 这些 API 函数只能用于任务里调用, 如果要在中断服务函数中调用, 则在函数名后添加 FromQueue 即可, 比如 xQueueSendFromQueue()函数
6.5 示例 - 伪代码
按键任务向打印任务发送按键消息队列, 代码如下:
- QueueHandle_t Key_Queue; // 按键值消息队列句柄
- int main()
- {
- //... 省略 N 行代码
- Key_Queue=xQueueCreate(1,sizeof(u8)); // 创建消息 Key_Queue, 长度为 1
- // 创建两个任务: key_task(),print_task()
- //... 省略 N 行代码
- }
- key_task() // 获取按键值
- {
- while(1)
- {
- key=KEY_Scan(0); // 扫描按键
- if((Key_Queue!=NULL)&&(key)) // 消息队列 Key_Queue 创建成功, 并且按键被按下
- {
- err=xQueueSend(Key_Queue,&key,10);
- if(err==errQUEUE_FULL) // 发送按键值
- {
- printf("队列 Key_Queue 已满, 数据发送失败!\r\n");
- }
- }
- vTaskDelay(10); // 延时 10 个时钟节拍
- }
- }
- print_task() // 打印按键值
- {
- u8 key;
- while(1)
- {
- if(Key_Queue!=NULL)
- {
- if(xQueueReceive(Key_Queue,&key,portMAX_DELAY))// 请求消息 Key_Queue
- {
- printf("key=%d\r\n",key);
- }
- }
- vTaskDelay(10); // 延时 10 个时钟节拍
- }
- }
7. RTOS 软件定时器
7.1 简介
在之前的任务创建的时候有讲到过, RTOS 会自动创建一个优先级 configMAX_PRIORITIES-1 的软件定时器服务任务(管理定时器的).
所以我们写一个定时器回调函数时, 则会被该定时器服务任务调用, 所以在我们软件定时器函数中不能使用 vTaskDelay()阻塞之类的 API 函数, 否则会将系统中的定时器服务函数给阻塞掉.
7.2 FreeRTOSConfig.h 相关的定时器配置
- #define configUSE_TIMERS 1 // 为 1 时启用软件定时器
- #define configTIMER_TASK_PRIORITY 31 // 设置软件定时器优先级可设置的值范围为 0~31
- #define configTIMER_QUEUE_LENGTH 5 // 软件定时器队列长度
- #define configTIMER_TASK_STACK_DEPTH 200 // 设置每个软件定时器任务堆栈大小
7.3 定时创建相关 API
- TimerHandle_t xTimerCreateStatic(const char * const pcTimerName, // 定时器字符串别名
- const TickType_t xTimerPeriodInTicks,
- // 需要定时的周期值, 比如通过 200/ portTICK_RATE_MS 来转换实现定时 200 毫秒
- const UBaseType_t uxAutoReload,
- // 是否重载 (周期性 / 单次性), 若为 pdTRUE(1) 表示为周期性, 为 pdFALSE(0)表示为单次
- void * const pvTimerID,
- // 定时器 ID 号, 一般用于多个定时器共用一个定时器回调函数, 否则填 0 即可
- TimerCallbackFunction_t pxCallbackFunction);// 定时器回调函数
- xTimerDelete( xTimer, xTicksToWait );
- // 删除定时器
- //xTicksToWait: 指定该定时器在多少时钟节拍数之前删除掉, 为 0 则立即删除, 一般设为 100(如果设为 0, 则如果在该操作之前还有其它设置定时器操作的话, 则不会进行阻塞等待, 从而返回 false)
7.4 定时器其它常用 API
- xTimerChangePeriod( xTimer, xNewPeriod, xTicksToWait );
- // 修改定时器周期, 在中断中则使用 xTimerChangePeriodFromISR()
- // xNewPeriod: 要修改的周期值
- //xTicksToWait: 指定该定时器在多少时钟节拍数之前修改好, 为 0 则立即删除
- //xTimerReset( xTimer, xTicksToWait );
- // 复位定时器, 让定时器重新计数, 在中断中则使用 xTimerResetFromISR()
- // xTicksToWait: 和上面内容类似
- xTimerStart( xTimer, xTicksToWait );
- // 启动定时器, 如果定时器正在运行的话调用该函数的结果和 xTimerReset()一样, 在中断中则使用 xTimerResetFromISR ()
- xTimerStop( xTimer, xTicksToWait );
- // 停止定时器, 在中断中则使用 xTimerStopFromISR ()
来源: http://www.bubuko.com/infodetail-2974011.html