STM32-FreeRTOS快速学习之总结1
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
STM32-FreeRTOS快速学习之总结1
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函数.⽐如喂看门狗中断函数就需要设置为0~4
如下图所⽰(来⾃原⼦⼿册):
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 中断发送/读取消息队列时,要注意的事情
使⽤中断相关的读写队列相关的API时,第3个参数是不⼀样的,⽐如xQueueSendFromISR():
PxHigherPriorityTaskWoken
⽤来标记退出该函数后是否需要进⾏任务切换,因为我们发送队列时,有可能会将某个阻塞任务退出阻塞态,⽽此时⼜在中断中,所以
当PxHigherPriorityTaskWoken为pdTRUE时,我们则必须进⾏⼀次任务切换.
可以通过portYIELD_FROM_ISR()来进⾏任务切换,并且我们不需要去判断PxHigherPriorityTaskWoken是否为pdTRUE,因为该函数内部有判断的,如下图所⽰:
来个中断函数发送队列⽰例:
extern QueueHandle_t Message_Queue; //信息队列句柄
void USART1_IRQHandler(void) //串⼝1中断服务程序
{
BaseType_t xHigherPriorityTaskWoken; //定义任务切换标志位
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
//处理中断接收数据
}
if (Message_Queue!=NULL) //判断Message_Queue是否已创建
{
xQueueSendFromISR(Message_Queue, RX_BUF,&xHigherPriorityTaskWoken);
//向队列Message_Queue中发送RX_BUF
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
//通过portYIELD_FROM_ISR()判断是否需要切换任务
}
}
PS:尽量将portYIELD_FROM_ISR()写在中断函数末尾处
6.6⽰例-任务之间的伪代码
按键任务向打印任务发送按键消息队列,代码如下:
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 ()
PS:在中断中使⽤定时器API时,同样和队列⼀样,也需要在函数末尾通过portYIELD_FROM_ISR()进⾏⼀次任务切换判断
8. 信号量
在项⽬中我们⼀般⽤⼆值信号量,⽤来同步数据的.
⽐如任务A要向任务B发送⼀个很⼤的数据buf,⽽⽤队列的话会进⾏复制拷贝,从⽽占⽤⼤量时间.
此时我们不妨定义⼀个全局数据buf,任务A修改这个buf,发送⼀个信号量给任务B,任务B就去读取这个全局数据buf即可.从⽽省去了队列复制拷贝的时间. 8.1定义信号量举例
SemaphoreHandle_t BinarySemaphore; //⼆值信号量句柄
BinarySemaphore=xSemaphoreCreateBinary(); //创建⼆值信号量
8.2在中断中发送信号量过程
BaseType_t xHigherPriorityTaskWoken;
xSemaphoreGiveFromISR(BinarySemaphore,&xHigherPriorityTaskWoken);
//发送⼆值信号量
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);//如果需要的话进⾏⼀次任务切换
8.3在任务中发送信号量过程
xSemaphoreGive(BinarySemaphore);
//返回值: pdPASS(0, 表⽰发送成功,如果信号量⼀直未处理,则会返回值失败FULL)
8.4 在任务中接收信号量过程
err=xSemaphoreTake(BinarySemaphore,portMAX_DELAY); //获取信号量
// portMAX_DELAY:进⼊阻塞态⼀直等待获取
//返回值为pdTRUE(OK) pdFALSE(err)。