FreeRTOS学习笔记
- 抢占式、合作式、时间片调度
- 任务,消息队列,信号量,软件定时器。
- 任务与任务,任务与中断之间可以使用任务通知、消息队列、二值信号量、数值型信号量、递归互斥信号量和互斥信号量进行通信和同步。
- 堆栈溢出检测功能(两种都是在任务切换时才进行的):
① #define configCHECK_FOR_STACK_OVERFLOW 1
void vApplicationStackOverflowHook( TaskHandle_t xTask,
signed char *pcTaskName );
任务切换时检测任务栈指针是否过界,过界则触发栈溢出钩子函数。
② #define configCHECK_FOR_STACK_OVERFLOW 2
任务创建的时候将任务栈所有数据初始化为 0xa5,任务切换时进行任务栈检测的时候会检测末
尾的 16 个字节是否都是 0xa5,通过这种方式来检测任务栈是否溢出了
一、FreeRTOS启动流程
- 系统上电时第一个执行的是启动文件中由汇编编写的复位函数Reset_Handle
- 复位函数调用_main,初始化系统的堆和栈,随后调用main函数
- 进入main()函数后,对硬件进行进一步的初始化,包括设置系统时钟、配置串口等。
- 对RTOS系统进行初始化,包括内核初始化,创建任务等
- 创建完所有任务后,调用vTaskStartScheduler() 启动FreeRTOS的任务调度器。
vTaskStartScheduler() 启动流程;
- 创建空闲任务
- 根据宏创建软件定时器服务(即守护进程)
- 关闭中断(在 SVC 中断服务函数 vPortSVCHandler()中会打开中断)
- 设置xSchedulerRunning为pdTRUE,表示调度器开始运行
- 根据宏使能时间统计功能(原理是配置一个高精度定时器)
- 调用xPortStartScheduler()
- 初始化跟调度器启动有关的硬件,比如滴答定时器、FPU 单元和 PendSV 中断等等
二、中断
- 屏蔽中断的特殊寄存器(FreeRTOS使用的是第三种)
①PRIMASK寄存器:关闭除复位、NMI 和 HardFault 以外的其他所有中断
②FAULTMASK寄存器:比 PRIMASK 更狠,它可以连 HardFault 都屏蔽掉
③BASEPRI寄存器:只屏蔽优先级低于某一个阈值的中断
- 对中断的一些猜测:
中断优先级有数字优先级和逻辑优先级,ISR的使用取决于逻辑优先级,数字优先级只赋予数字,如TIM3中断优先级设置为3,TIM4设置为4,设置的则是数字优先级。不同的MCU处理器上,数字优先级和逻辑优先级的关系是不一样的。当这两个中断同时发生,ISR先处理了TIM3的,再处理TIM4的,则说明该MCU处理器架构上,分配给数字优先级越高,逻辑优先级则越低。若先处理TIM4的,则说明该处理器分配的数字优先级越高,则逻辑优先级越低。
- 临界区的使用
configMAX_SYSCALL_INTERRUPT_PRIORITY 配置临界屏蔽中断优先级阈值
任务临界区:
void task(void)
{
while(1)
{
taskENTER_CRITICAL();
代码;
taskEXIT_CRITICAL();
VTaskDelay(1000);
}
}
中断临界区(一般写在中断服务函数):
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
{
status_value=taskENTER_CRITICAL_FROM_ISR();
代码;
taskEXIT_CRITICAL_FROM_ISR(status_value);
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
使用临界区保护代码时,屏蔽掉低于configMAX_SYSCALL_INTERRUPT_PRIORITY优先级的中断,退出临界区时,重新打开他们的中断。例如: #define configMAX_SYSCALL_INTERRUPT_PRIORITY 5
则中断优先级低于5的中断将被屏蔽掉,加入定时器5的中断优先级配置为6,则进入临界区时会将其中断关闭(失能)。
- 软中断的使用
#define mainINTERRUPT_NUMBER ( 5 ) //设置软中断优先级
vPortSetInterruptHandler(mainINTERRUPT_NUMBER, ulExampleInterruptHandler);//注册软中断并设置中断服务函数
vPortGenerateSimulatedInterrupt( mainINTERRUPT_NUMBER ); //启动模拟中断
e.g:
#include "queue.h"
QueueHandle_t xIntegerQueue;
QueueHandle_t xStringQueue;
#define mainINTERRUPT_NUMBER ( 5 )
static void vIntegerGenerator_task( void *pvParameters )
{
TickType_t xLastExecutionTime = xTaskGetTickCount();;
uint32_t ulValueToSend = 0;
for( ;; )
{
vTaskDelayUntil( &xLastExecutionTime, pdMS_TO_TICKS( 200 ) );
for(int i = 0; i < 5; i++ )
{
xQueueSendToBack( xIntegerQueue, &ulValueToSend, 0 );
ulValueToSend++;
}
vPortGenerateSimulatedInterrupt( mainINTERRUPT_NUMBER ); //启动模拟中断
vPrintString( "Generator task - Interrupt generated.\r\n\r\n\r\n" );
}
}
static void vStringPrinter_task( void *pvParameters )
{
char *pcString;
for( ;; )
{
xQueueReceive( xStringQueue, &pcString, portMAX_DELAY );
vPrintString( pcString );
}
}
static uint32_t ulExampleInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
uint32_t ulReceivedNumber;
static const char *pcStrings[] = {
"String 0\r\n",
"String 1\r\n",
"String 2\r\n",
"String 3\r\n"
};
xHigherPriorityTaskWoken = pdFALSE;
while( xQueueReceiveFromISR(xIntegerQueue, &ulReceivedNumber, &xHigherPriorityTaskWoken ) != errQUEUE_EMPTY )
{
ulReceivedNumber &= 0x03;
xQueueSendToBackFromISR( xStringQueue, &pcStrings[ ulReceivedNumber ], &xHigherPriorityTaskWoken );
}
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
int main( void )
{
xIntegerQueue = xQueueCreate( 10, sizeof( uint32_t ) );
xStringQueue = xQueueCreate( 10, sizeof( char * ) );
xTaskCreate( vIntegerGenerator_task, "IntGen", 1000, NULL, 1, NULL );
xTaskCreate( vStringPrinter_task, "String", 1000, NULL, 2, NULL );
vPortSetInterruptHandler( mainINTERRUPT_NUMBER, ulExampleInterruptHandler ); //注册中断函数并设置中断服务函数
vTaskStartScheduler();
for( ;; );
}
三、列表list
列表和列表项是FreeRTOS实时操作系统最核心的内容
1、列表
FreeRTOS中全局列表种类有:
- 就绪列表数组N个(N表示优先级范围,优先级越高,放在数组的下标越小,例如,最高优先级为10,则N=10,任务A的优先级为9,则A就绪状态时放置在第二个就绪列表数组里)
- 延时列表2个(一个是当前,一个是tick溢出时作为切换的列表)
- 挂起待恢复就绪列表(调度器挂起)
- 等待删除任务列表(任务删除自身则会存放在此列表中,空闲任务查询时进行处理)
- 任务挂起列表(挂起等死的任务存放位置)
使用时才申请的列表:
- 消息队列、信号量(实际上都是消息队列)维护两个列表:等待发送列表,等待接收列表
- 事件标志组
- 软件定时器维护两个列表(与延时列表相似)
2、列表项--------------主要是任务使用(TCB控制块)
状态列表项、事件列表项
四、任务
- task—任务
1、任务状态:运行态、就绪态、阻塞态、挂起态(vTaskSuspend()和 xTaskResume())
2、任务优先级:0~configMAX_PRIORITIES-1(FreeRTOSConfig.h中定义)
configUSE_TIME_SLICING 定义为1时,多个任务可以共用一个优先级,默认FreeRTOS.h中均会定义为1
优先级高的可以抢占优先级低的获得cpu使用权限,就绪状态中的同等优先级则通过时间片轮转调度器获取运行时间。
3、任务的创建:动态(xTaskCreate())、静态(xTaskCreateStatic())、使用MPU进行限制的任务创建(xTaskCreateRestricted(),基本不用)
①动态创建
原函数
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, //任务函数
const char * const pcName, //任务名字(长度不超过configMAX_TASK_NAME_LEN)
const uint16_t usStackDepth, //任务堆栈大小(申请到的堆栈大小为usStackDepth*4)
void * const pvParameters, //任务参数(NULL)
UBaseType_t uxPriority, //任务优先级
TaskHandle_t * const pxCreatedTask ) //任务句柄(NULL)
返回值:pdPASS创建成功
②静态创建
静态创建需将configSUPPORT_STATIC_ALLOCATION设置为1,并需要用户实现两个函数vApplicationGetIdleTaskMemory() 和
vApplicationGetTimerTaskMemory(),通过这两个函数来给空闲任务和定时器服务任务的任务堆栈和任务控制块分配内存
原函数
TaskHandle_t xTaskCreateStatic(
TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth, //任务堆栈大小,有用户给出,一般是个数组,此参数就是这个数组的大小
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer, //任务堆栈,一般为数组,数组类型要为 StackType_t 类型
StaticTask_t * const pxTaskBuffer ) //任务控制块
返回值:NULL创建失败 其他:创建成功,返回任务句柄
其中:
任务函数原型
void vATaskFunction(void *pvParameters)
函数类型 void;
参数类型:void *
每个任务循环中都需要有延时函数:比如请求信号量、队列等(进入阻塞态),甚至直接调用任务调度器,最常用的还是相对延时vTaskDelay(pdMS_TO_TICKS(20))和绝对延时vTaskDelayUntil(&last_task_time, pdMS_TO_TICKS(10));
4、任务的删除:vTaskDelete(TaskHandle_t xTaskToDelete);
只有那些由内核分配给任务的内存才会在任务被删除以后自动的释放掉,用户分配给任务的内存需要用户自行释放掉,比如某个任务中用户调用函数 pvPortMalloc()分配了 500 字节的内存,那么在此任务被删除以后用户也必须调用函数 vPortFree()将这 500 字节的内存释放掉,否则会导致内存泄露。
可以在任务中使用vTaskDelete(NULL)表示删除当前任务。
5、任务的挂起和恢复:
vTaskSuspend():挂起一个任务 返回值:无
vTaskResume():恢复一个任务的运行 返回值:无
xTaskResumeFromISR():中断服务函数中恢复一个任务的运行
返回值:
pdTRUE:恢复运行的任务的任务优先级等于或者高于正在运行的任务(被中断打断的任务),这意味着在退出中断服务函数以后必须进行一次上下文切换。
pdFALSE:低于,不需要进行上下文切换。
例如:
BaseType_t YieldRequired=xTaskResumeFromISR(Task2Task_Handler);//恢复任务 2
if(YieldRequired==pdTRUE)
{
portYIELD_FROM_ISR(YieldRequired);
}
如果中断处理的时间特别关键,则可以设置延迟处理任务的优先级,以确保该任务始终优先于系统中的其他任务。然后,可以将ISR实现为包含对portYIELD_FROM_ISR()的调用,以确保ISR直接返回到正在延迟中断处理的任务。这样做可以确保整个事件处理及时地连续执行(没有中断),就像所有的事件处理都是在ISR本身中实现的一样。
五、队列quene
先进先出(FIFO)
1、队列的创建:
①动态创建
原函数:
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSiz)
参数:
uxQueueLength:队列长度,即消息项目的数量
uxItemSiz:队列中每个消息项目的长度,单位---字节
返回值:
创建成功返回句柄,失败则返回空指针NULL
此函数最终调用的是函数 xQueueGenericCreate()
QueueHandle_t xQueueGenericCreate(const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType)
参数:
ucQueueType:指队列类型,枚举,默认为普通消息队列
队列的类型有:
queueQUEUE_TYPE_BASE 普通的消息队列
queueQUEUE_TYPE_SET 队列集
queueQUEUE_TYPE_MUTEX 互斥信号量
queueQUEUE_TYPE_COUNTING_SEMAPHORE 计数型信号量
queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号量
queueQUEUE_TYPE_RECURSIVE_MUTEX 递归互斥信号量
②静态创建
原函数:
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength, UBaseType_t uxItemSiz, uint8_t* pucQueueStorageBuffer, StaticQueue_t* pxQueueBuffer)
参数:
pucQueueStorageBuffer:指向队列消息项目的存储区(自行分配),此参数必须指向一个 uint8_t 类型的数组。这个存储区要大于等于(uxQueueLength * uxItemsSize)字节
pxQueueBuffer:指向一个 StaticQueue_t 类型的变量,用来保存队列结构体
此函数最终调用的是函数 xQueueGenericCreate()
QueueHandle_t xQueueGenericCreateStatic(const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, uint8_t *pucQueueStorage, StaticQueue_t *pxStaticQueue, const uint8_t ucQueueType)
参数:
ucQueueType:队列类型,枚举,默认为普通消息队列
2、向队列发送消息
分类 函数 描述
任务级入队函数 xQueueSend()
xQueueSendToBack() 发送消息到队列尾部(后向入队),这两个函数是一样的。
xQueueSendToFront() 发送消息到队列头(前向入队)。
xQueueOverwrite() 发送消息到队列,带覆写功能,当队列满了以后自动覆盖掉旧的消息。
xQueueSendFromISR() 发送消息到队列尾(后向入队),这两个函数是一样的,用于中断服务函数
中断级入队函数
xQueueSendToBackFromISR()
xQueueSendToFrontFromISR()
xQueueOverwriteFromISR() 用于中断服务函数
①任务中正常发送
原函数:
BaseType_t xQueueSend(QueueHandle_t xQueue, const void* pvItemToQueue, TickType_t xTicksToWait);
参数:
xQueue:队列句柄
pvItemToQueue:要发送的消息,发送时候会将这个消息拷贝到队列中
xTicksToWait:等待时间
返回值:
pdPASS:发送成功
errQUEUE_FULL:队列满了,发送失败
②任务中带覆盖功能的发送
原函数:
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void* pvItemToQueue);
队列满了则覆盖掉旧的消息,无需等待,返回值只有pdPASS
任务中的发送最终都是通过调用xQueueGenericSend()实现。
BaseType_t xQueueGenericSend(QueueHandle_t xQueue, const void * const pvItemToQueue,TickType_t xTicksToWait, const BaseType_t xCopyPosition)
参数:
xCopyPosition:表示入队的方式
入队的方式有:
queueSEND_TO_BACK 后向入队
queueSEND_TO_FRONT 前向入队
queueOVERWRITE 覆写入队
③中断中的消息发送
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue, const void * pvItemToQueue, BaseType_t * pxHigherPriorityTaskWoken);
参数:
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值由函数执行完后来赋值的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
中断级别的消息发送最终都是通过调用xQueueGenericSendFromISR()来实现的
BaseType_t xQueueGenericSendFromISR(QueueHandle_t xQueue, const void* pvItemToQueue, BaseType_t* pxHigherPriorityTaskWoken, BaseType_t xCopyPosition);
3、从队列中读取消息
分类 函数 描述
任务级出队函数 xQueueReceive() 从队列中读取队列项(消息),并且读取完以后删除掉队列项(消息)
xQueuePeek() 从队列中读取队列项(消息),并且读取完以后不删除队列项(消息)
中断级出队函数 xQueueReceiveFromISR()
xQueuePeekFromISR () 用于中断服务函数中。
①任务级出队函数最终都是通过调用xQueueGenericReceive()函数实现的
BaseType_t xQueueGenericReceive(QueueHandle_t xQueue, void* pvBuffer, TickType_t xTicksToWait, BaseType_t xJustPeek)
参数:
xJustPeek:读取成功以后是否删除掉队列项,pdTRUE则表示不用删除
返回值:
pdTRUE:从队列中读取数据成功
pdFALSE:从队列中读取数据失败
六、任务通知
需打开宏开关 configUSE_TASK_NOTIFICATIONS设置为1
每个任务都有一个TCB控制块结构体,里面有两个成员:
① volatile uint8_t ucNotifyState[configTASK_NOTIFICATION_ARRAY_ENTRIES] //用于表示通知状态
② volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; //用于表示通知值
ucNotifyState有三种状态:
#define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 ) //任务处于未等待通知的状态
#define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 ) //任务处于等待通知的状态
#define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 ) //任务收到通知后,切换为已经收到通知的状态。
FreeRTOS 提供以下几种方式发送通知给任务 :
发送消息给任务,如果有通知未读,不覆盖通知值
发送消息给任务,直接覆盖通知值
发送消息给任务,设置通知值的一个或者多个位
发送消息给任务,递增通知值
1、发送通知(xTaskNotifyGive 和 xTaskNotify 两种)
原函数:
① xTaskNotifyGive(TaskHandle_t xTaskToNotify);
向指定任务发送一个通知,不提供具体通知值。
② BaseType_t xTaskGenericNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousValue);
向指定任务发送一个通知,并可以设置通知值和通知行为。
参数:
xTaskToNotify:目标任务的句柄,即接收通知的任务
ulValue:要发送的通知值
eAction:如何修改目标任务当前的通知值(通知值+1/覆盖通知值)
pulPreviousValue:可选参数,返回目标任务在本次更新前的通知值。如果不需要,可以传入NULL
返回值:
①无返回值
②的返回值:
pdPASS:目标任务的状态发生改变(如从阻塞到就绪)
pdFAIL:目标任务的状态未发生改变,或者设置通知值时忽略了新值
2、接收通知(ulTaskNotifyTake 和 xTaskNotifyWait)
①ulTaskNotifyTake---等待接收任务通知
原函数:
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);
参数:
xClearCountOnExit:bool型,接收到通知后,将通知值清零或-1
xTicksToWait:等待时长,常使用portMAX_DELAY无限期等待通知
返回值:
如果成功接收到通知并取得信号量,返回从通知值中获取到的信号量数量(可能大于1)。
如果在xTicksToWait时间内未接收到通知,则返回0。
②xTaskNotifyWait---等待接收任务通知,并且可以设置在进入和退出时要清零的通知比特位。
原函数:
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait
);
参数:
ulBitsToClearOnEntry: 在进入等待前是否需要清除通知值的特定位操作,例如:清零则则设置为0xFFFFFFFF
ulBitsToClearOnExit:表示退出等待时是否需要清除通知值的特定位操作,同上
pulNotificationValue:保存接收到的通知值的指针(存储收到的通知值)
返回值:
pdTRUE
pdFAIL
七、信号量
1、类型:
二进制信号量(二值信号量)、计数信号量、互斥信号量和递归互斥信号量
使用计数信号量时,需要将宏configUSE_COUNTING_SEMAPHORES设置为1,使能计数信号量。
使用互斥信号量时,需要将宏configUSE_MUTEXES设置为1,使能互斥信号量,如若使用递归互斥信号量,还需将宏configUSE_RECURSIVE_MUTEXES也设置为1。
注:二值信号量存在优先级翻转问题
什么是优先级翻转?
例如,有三个任务:H、M、L,优先级H>M>L,其中H任务和L任务都会Take信号量,由于H任务在Take信号量之前有别的事情(较长时间),导致中断释放信号量时,H还未开始Take信号量,L先Take信号量了,所以L任务在中断完后先执行,而L任务获得信号量后,又有一堆别的事情要处理,导致没有释放信号量,信号量被占用,H任务就Take不到导致在阻塞态,而此时,正常运行的M任务优先级高于L,导致其可以抢占L任务资源,出现M任务多次执行而更高级的H任务因为信号量被最低优先级L任务占有而阻塞的问题。
互斥信号量不同于二值信号量,其具有优先级继承的特性。当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承并不能完全的消除优先级翻转,它只是尽可能的降低优先级翻转带来的影响
互斥信号量不能用于中断服务函数,最好是在任务中获取,用完后在当前任务中释放。毕竟互斥信号量本身起到的还是互斥锁作用。
递归互斥信号量,获取几次,最终就要释放几次。
2、创建信号量:
①二值信号量:
----动态创建:
SemaphoreHandle_t xSemaphoreCreateBinary(void);
e.g:
SemaphoreHandle_t pc_binary_handle;
pc_binary_handle = xSemaphoreCreateBinary();
if(pc_binary_handle == NULL)
printf("创建二值信号量失败\r\n");
----静态创建:
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *pxSemaphoreBuffer); //需外部先定义好buff
②计数信号量:
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount)
e.g:
SemaphoreHandle_t pc_counting_handle;
pc_counting_handle = xSemaphoreCreateCounting(100,2); //最大计数为100,当前计数为2
if(pc_counting_handle == NULL)
printf("创建计数信号量失败\r\n");
③互斥信号量:
----动态创建:
SemaphoreHandle_t xSemaphoreCreateMutex(void)
----静态创建
SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t *pxMutexBuffer) //需外部先定义好buff
④递归互斥信号量:
----动态创建:
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void)
----静态创建
SemaphoreHandle_t xSemaphoreCreateRecursiveMutexStatic(StaticSemaphore_t *pxMutexBuffer) //需外部先定义好buff
3、获取信号量(任务级与中断级)
①任务级
原函数
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
参数:
xTicksToWait----等待阻塞时间
返回值:
pdPASS:获取信号量成功
pdFALSE:超时,获取失败
作用范围:任务中获取二值信号量、计数信号量和互斥信号量。
e.g:
SemaphoreHandle_t rev_date_handle;
void pc_command_task(void *argument)
{
rev_date_handle = xSemaphoreCreateBinary();
if(rev_date_handle == NULL)
printf("创建信号量失败\r\n");
while(rev_date_handle != NULL)
{
if(pdPASS = xSemaphoreTake(rev_date_handle,portMAX_DELAY))
{
printf("接收信号量解锁成功,处理接收数据\r\n");
}
}
}
递归互斥信号量的获取:
#define xSemaphoreTakeRecursive(xMutex, xBlockTime) xQueueTakeMutexRecursive((xMutex), (xBlockTime))
②中断级
原函数
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,BaseType_t * pxHigherPriorityTaskWoken)
参数:
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,当此值为pdTRUE的时候,则在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS:获取信号量成功
errQUEUE_FULL:获取信号量失败
4、释放信号量(任务级与中断级)
①任务级
原函数
BaseType_t xSemaphoreGive(xSemaphore)
返回值:
pdPASS:释放信号量成功;
errQUEUE_FULL:释放信号量失败
作用范围:任务中释放二值信号量、计数信号量和互斥信号量。
②中断级
原函数
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
参数:
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,当此值为pdTRUE的时候,则在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS:释放信号量成功
errQUEUE_FULL:释放信号量失败
作用范围:中断中释放二值信号量,计数信号量。不能用于释放互斥信号量。
e.g:
void TIM3_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
{
代码;
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
/* 如果 xHigherPriorityTaskWoken = pdTRUE,则在退出中断前应执行一次上下文切换,以确保退出中断后切到当前最高优先级任务执行 */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
5、删除信号量
原函数
BaseType_t vSemaphoreDelete(SemaphoreHandle_t xSemaphore)
注:如果有任务阻塞在该信号量上,那么不要删除该信号量。
八、事件标志组
1、成员:事件组,事件位。一个事件组就是一组事件标志位
configUSE_16_BIT_TICKS 1 时,一个事件标志组可以存储 8 个事件位
configUSE_16_BIT_TICKS 0 时,一个事件标志组可以存储 24 个事件位,还有高8位做其他用处
2、事件标志组的创建(xEventGroupCreate() 和 xEventGroupCreateStatic())
①动态创建:
EventGroupHandle_t xEventGroupCreate(void)
返回值:
创建成功返回句柄,失败返回NULL空指针
②静态创建:
EventGroupHandle_t xEventGroupCreateStatic(StaticEventGroup_t * pxEventGroupBuffer)
参数:
pxEventGroupBuffer:指向一个 StaticEventGroup_t 类型的变量,用来保存事件标志组结构体
3、事件标志位的设置
①清零
xEventGroupClearBits() 和 xEventGroupClearBitsFromISR()
原函数:
EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear)
参数:
xEventGroup:事件标志组句柄
uxBitsToClear:要清零的事件位,如:清除bit3的话就设置为0x08,同时清除bit3和bit0则设置为0x09
返回值:
任何值,将指定事件位清零之前的事件组值
原函数:
BaseType_t xEventGroupClearBitsFromISR(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet)
参数:
同上
返回值:
pdPASS:事件位清零成功
pdFALSE:事件位清零失败
②置1
xEventGroupSetBits() 和 xEventGroupSetBitsFromISR()
原函数:
EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet)
BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, BaseType_t * pxHigherPriorityTaskWoken);
中断级,事件标志位清零无需考虑上下文切换,置1则需要考虑。返回值中断级为pdPASS和pdFALSE,任务级则为无意义的任何值
4、获取事件标志组的值
EventBits_t xEventGroupGetBits(EventGroupHandle_t xEventGroup)
EventBits_t xEventGroupGetBitsFromISR(EventGroupHandle_t xEventGroup)
5、等待指定事件位
原函数:
EventBits_t xEventGroupWaitBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, const TickType_t xTicksToWait);
参数:
uxBitsToWaitFor:等待的时间位,例如:bit0和bit3则设置为0x09
xClearOnExit: 退出后所等待的事件位是否清零
xWaitForAllBits:等待类型,pdTRUE表示uxBitsToWaitFor所有等待的事件位都满足,pdFALSE表示只要有一个位满足要求即可
九、FreeRTOS软件定时器
软件定时器精度没有MCU自带的硬件定时器高。
软件定时器回调函数:
不能在回调函数中调用任何会阻塞任务的 API 函 数,如:vTaskDelay();
软件定时器实现原理:
定时器服务任务和定时器命令队列
相关宏:
configUSE_TIMERS--------------------------------是否使用定时器服务任务
configTIMER_TASK_PRIORITY-----------------定时器服务任务优先级
configTIMER_TASK_STACK_DEPTH----------定时器服务任务堆栈大小,单位:字,即4个字节
configTIMER_QUEUE_LENGTH---------------定时器命令队列长度
软件定时器分类:
单次定时器 和 周期定时器
1、创建
新创建的软件定时器处于休眠状态,也就是未运行的,需配合其他启动函数进行使用。
①动态创建:
TimerHandle_t xTimerCreate(const char * const pcTimerName, TickType_t xTimerPeriodInTicks, UBaseType_t uxAutoReload, void * pvTimerID, TimerCallbackFunction_t pxCallbackFunction)
参数:
xTimerPeriodInTicks:定时器周期
uxAutoReload:是否自动重装载,即定时器类型是单次还是周期
pvTimerID:定时器ID号,回调中可以根据这个ID号进行不同软件定时器的处理
pxCallbackFunction:绑定回调服务函数
返回值:
创建成功返回句柄,失败返回NULL空指针
②静态创建:
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName, TickType_t xTimerPeriodInTicks, UBaseType_t uxAutoReload, void * pvTimerID, TimerCallbackFunction_t pxCallbackFunction,StaticTimer_t * pxTimerBuffer)
参数:
pxTimerBuffer:指向一个StaticTimer_t类型的变量,用来保存定时器结构体
2、启动
软件定时器未启动则进行启动,软件定时器已运行则为复位作用
①任务级
BaseType_t xTimerStart(TimerHandle_t xTimer, TickType_t xTicksToWait)
②中断级
BaseType_t xTimerStartFromISR(TimerHandle_t xTimer, BaseType_t * pxHigherPriorityTaskWoken);
3、复位
①任务级
BaseType_t xTimerReset(TimerHandle_t xTimer, TickType_t xTicksToWait)
②中断级
BaseType_t xTimerResetFromISR(TimerHandle_t xTimer, BaseType_t * pxHigherPriorityTaskWoken);
4、停止
①任务级
xTimerStop()
②中断级
xTimerStopFromISR()
5、其他
xTimerChangePeriod()
xTimerChangePeriodFromISR()
十、低功耗模式
1、常规裸机:
①待机(standby)模式
功耗最低。
调压器被禁止,1.2V 域断电,PLL、HSI 振荡器和 HSE 振荡器也被关闭,除了备份区域和待机电路相关的寄存器外,SRAM 和其他寄存器的内容都将丢失
进入和退出方法看开发手册
退出待机模式的话会导致 STM32F1重启,所以待机模式的唤醒延时也是最大的
②停止(stop)模式
功耗第二。
内部 SRAM 的数据会被保留,调压器可以工作在正常模式,也可配置为低功耗模式。如果有必要的话可以通过将 PWR_CR 寄存器的FPDS 位置 1 来使 Flash 在停止模式的时候进入掉电状态,当 Flash 处于掉电状态的时候 MCU从停止模式唤醒以后需要更多的启动延时
进入方法看开发手册
退出方法:
当使用WFI进入休眠时,配置任意外部中断线为中断模式,触发中断即可退出;
当使用WFE进入休眠时,配置任意外部中断线为事件模式,触发事件即可退出;
③休眠(sleep)模式
功耗最大
Cortex-M3 内核停止运行,但是其他外设运行正常。
分为等待中断休眠和等待事件休眠,立即休眠和退出时休眠
进入休眠指令:WFI(等待中断)和 WFE(等待事件)
当 SCR 寄存器的 SLEEPONEXIT(bit1)位为 0 的时候使用立即休眠,当为 1 的时候使用退出时休眠
进入方法:
使用 WFI 指令 或 WFE 指令进入。
退出方法:
当使用WFI进入休眠时,配置任意外部中断线为中断模式,触发中断即可退出;
当使用WFE进入休眠时,配置任意外部中断线为事件模式,触发事件即可退出;
2、FreeRTOS新增
Tickless 模式:
当处理器进入空闲任务周期以后就关闭系统节拍中断(滴答定时器中断),只有当其他中断发生或者其他任务需要处理的时候处理器才会被从低功耗模式中唤醒。
实现原理:
①记录下系统节拍中断的关闭时间,当系统节拍中断再次开启运行的时候补上这段时间
②获取到还有多长时间运行下一个任务,配置定时器进行定时唤醒
具体实现:
①开启相关宏:
#define configUSE_TICKLESS_IDLE 1 //1 启用低功耗 tickless 模式
②在函数PreSleepProcessing中配置进入低功耗需要做的处理
③在函数PostSleepProcessing中配置退出低功耗需要恢复的处理
/********************************************************************************/
/*
FreeRTOS 与低功耗管理相关配置
*/
/********************************************************************************/
extern void PreSleepProcessing(uint32_t ulExpectedIdleTime);
extern void PostSleepProcessing(uint32_t ulExpectedIdleTime);
//进入低功耗模式前要做的处理
#define configPRE_SLEEP_PROCESSING PreSleepProcessing
//退出低功耗模式后要做的处理
#define configPOST_SLEEP_PROCESSING PostSleepProcessing
十一、空闲任务与钩子函数
1、空闲任务
创建:vTaskStartScheduler()启动任务调度器时自动创建
作用:系统空闲时运行,保证至少有一个任务在运行,检测是否有任务要删除自身,有的话需要对该任务控制块TCB和任务堆栈等进行释放,如果有定义宏configUSE_IDLE_HOOK,则会进入空闲钩子回调函数,如果有定义configIDLE_SHOULD_YIELD启动同级时间片抢占功能则可让出时间片。
2、钩子函数
宏 钩子函数
configUSE_IDLE_HOOK 空闲钩子函数
configUSE_TICK_HOOK 时间片钩子函数
configUSE_MALLOC_FAILED_HOOK 内存申请失败钩子函数,使用pvPortMalloc()申请内存失败时会进入此回调函数
configUSE_DAEMON_TASK_STARTUP_HOOK 守护(Daemon)任务启动钩子函数,守护任务也就是定时器服务任务
以下是采用空闲任务钩子函数实现的低功耗模式,效果比FreeRTOS系统自带的Tickless模式差,建议还是使用Tickless模式
#define configUSE_TICKLESS_IDLE 0 //关闭低功耗 tickless 模式
#define configUSE_IDLE_HOOK 1 //使能空闲任务钩子函数
//进入低功耗模式前需要处理的事情
void BeforeEnterSleep(void)
{
//关闭某些低功耗模式下不使用的外设时钟,此处只是演示性代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG,DISABLE);
}
//退出低功耗模式以后需要处理的事情
void AfterExitSleep(void)
{
//退出低功耗模式以后打开那些被关闭的外设时钟,此处只是演示性代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG,ENABLE);
}
//空闲任务钩子函数
void vApplicationIdleHook(void)
{
__disable_irq();
__dsb(portSY_FULL_READ_WRITE );
__isb(portSY_FULL_READ_WRITE );
BeforeEnterSleep();//进入睡眠模式之前需要处理的事情
__wfi();//进入睡眠模式
AfterExitSleep();//退出睡眠模式之后需要处理的事情
__dsb(portSY_FULL_READ_WRITE );
__isb(portSY_FULL_READ_WRITE );
__enable_irq();
}
十二、内存管理
FreeRTOS 提供了 5 种内存分配方法,分别在5个文件中,在源码中的路径:FreeRTOS->Source->portable->MemMang
1、heap_1 内存分配方法
先定义一个大数组(内存堆),容量为 configTOTAL_HEAP_SIZE。每次需要使用内存时,则从中分一小块出来。使用函数 xPortGetFreeHeapSize()可以获取内存堆中剩余内存大小。内存释放函数形同虚设,可以认为不支持内存释放功能。
适用范围:适用于一旦创建好任务、信号量和队列就再也不会删除的应用
2、heap_2 内存分配方法
同heap_1一样也是使用内存堆分小块内存的方法,不过它提供了一个更好的分配算法,并提供内存释放功能(引入内存块并用内存块链表结构进行管理)
适用范围:适用于大多数的需要动态分配内存的工程
pvPortMalloc()申请内存时,实际申请的内存大小应加上结构体 BlockLink_t的大小(heapSTRUCT_SIZE),从空闲内存链表中找到满足所需内存大小的内存块进行使用,并将其从空闲内存链表中删除(当然,如果申请到的实际内存减去所需的内存大小(xBlockSize-xWantedSize)大于某个阈值的时候就把多余出来的内存重新组合
成一个新的可用空闲内存块并加到空闲内存链表中)
vPortFree()释放内存时,要释放的内存首地址必须减去 heapSTRUCT_SIZE 才是要释放的内存段所在内存块的首地址。释放完后将其加到空闲内存链表中。
过多的申请内存释放内存会导致内存碎片的增加。
3、heap_3 内存分配方法
只是对 malloc()和 free() 进行简单的封装,并做了线程保护
4、heap_4 内存分配方法
基于heap_2做了优化,将内存碎片合并成一个大的可用内存块,它提供了内存块合并算法。
内存合并算法:heap_4的内存合并算法合并的是相邻内存块,先判断相邻的前面后判断相邻的后面
内存合并算法的使用:
①pvPortMalloc()申请内存时,申请到内存块后,将减去所需大小的内存后剩余的内存块重新插入到空闲内存链表时,进行内存合并算法的使用
②vPortFree()释放内存时,将释放的内存块插入到空闲内存链表时,进行内存合并算法的使用
5、heap_5 内存分配方法
针对外接内存进行合并,对于外接的SRAM或更大容量的SDRAM比较友好,实用heap_4只能合并单个,而使用heap_5则可以对多个不相邻内存段进行合并。
实现原理:
最开始时,调用vPortDefineHeapRegions()对内存堆进行初始化,通过结构体 HeapRegion_t 将不同的内存段链接起来。
e.g:
内部 SRAM、外部 SRAM,起始分别为:0X20000000、0x68000000,大小分别为:64KB、1MB,则:
HeapRegion_t xHeapRegions[] =
{{ ( uint8_t * ) 0X20000000UL, 0x10000 },//内部 SRAM 内存,起始地址 0X20000000,//大小为 64KB
{ ( uint8_t * ) 0X68000000UL, 0x100000},//外部 SRAM 内存,起始地址 0x68000000,//大小为 1MB
{ NULL, 0 }//数组结尾};
十三、辅助打印
- 任务列表打印和任务运行时间打印
1、任务列表
void vTaskList( char * pcWriteBuffer )
需打开下面两个宏开关
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
e.g:
void print_tasklist()
{
char *task_list_buff = (char *)calloc(1,1024);
vTaskList(task_list_buff);
printf("Task List:\n%s\n",task_list_buff);
}
打印表依次是 任务名称, 运行状态,任务优先级,剩余堆栈大小,任务编号
printf("=================================================\r\n");
printf("任务名 任务状态 优先级 剩余栈 任务序号\r\n");
例如:
led task X 2 4 2
IDLE R 0 116 5
usart1 recv tas B 5 162 4
key task B 5 32 3
Tmr Svc B 2 228 6
其中运行状态:
X: running 运行
B: blocked 阻塞
R: ready 就绪
D: deleted 被删除
S: suspended 被挂起
2、任务运行时间
void vTaskGetRunTimeStats( char *pcWriteBuffer )
需打开下面几个宏开关:
#define configGENERATE_RUN_TIME_STATS 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
定义高精度定时器配置函数,以及获取时基时间值的函数:
extern volatile unsigned long long FreeRTOSRunTimeTicks;
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ConfigureTimeForRunTimeStats()
#define portGET_RUN_TIME_COUNTER_VALUE() FreeRTOSRunTimeTicks
选用定时器配置时,这个时基的分辨率一定要比 FreeRTOS的系统时钟高,一般这个时基的时钟精度比系统时钟的高 10~20 倍就可以了
volatile unsigned long long FreeRTOSRunTimeTicks;
void ConfigureTimeForRunTimeStats(void) //在vTaskStartScheduler()启动调度器的时候会判断相关的宏然后调用时基初始化
{
FreeRTOSRunTimeTicks=0;
MX_TIM3_Init();
}
在时基定时器的回调函数,实现runtimeticks的计数
extern volatile unsigned long long FreeRTOSRunTimeTicks;
FreeRTOSRunTimeTicks++;//运行时间统计时基计数器加一
e.g:
char *task_run_time_info = (char *)calloc(1,200);
vTaskGetRunTimeStats(task_run_time_info);
// printf("任务名 运行计数 使用率\r\n");
extern volatile unsigned long long FreeRTOSRunTimeTicks;
printf("runing time:%d\n",(int32_t)FreeRTOSRunTimeTicks);
printf("%s\n",task_run_time_info);