一、基本配置
1.1 数据类型
FreeRTOS 使用的数据类型主要分为 stdint.h 文件中定义的和自己定义的两种
FreeRTOS 主要自定义了以下四种数据类型:
TickType_t----32 位无符号数( 32位MCU,配置configUSE_16_BIT_TICKS = 0)
BaseType_t----32 位有符号数(32位MCU)
UBaseType_t---32 位无符号数(BaseType_t类型无符号版本)
StackType_t----32 位变量(栈变量数据类型定义,32位MCU)
1.2 代码风格
变量
uint32_t 定义的变量都加上前缀 ul。u 代表 unsigned 无符号,l 代表 long 长整型。
uint16_t 定义的变量都加上前缀 us。u 代表 unsigned 无符号,s 代表 short 短整型。
uint8_t 定义的变量都加上前缀 uc。u 代表 unsigned 无符号,c 代表 char 字符型。
stdint.h 文件中未定义的变量类型,在定义变量时需要加上前缀 x,比如 BaseType_t 和
TickType_t 定义的变量。
stdint.h 文件中未定义的无符号变量类型,在定义变量时要加上前缀 u,比如 UBaseType_t 定义
的变量要加上前缀 ux。
size_t 定义的变量也要加上前缀 ux。
枚举变量会加上前缀 e。
指针变量会加上前缀 p,比如 uint16_t 定义的指针变量会加上前缀 pus。
根据 MISRA 代码规则,char 定义的变量只能用于 ASCII 字符,前缀使用 c。
根据 MISRA 代码规则,char *定义的指针变量只能用于 ASCII 字符串,前缀使用 pc。
函数
加上了 static 声明的函数,定义时要加上前缀 prv,这个是单词 private 的缩写。
带有返回值的函数,根据返回值的数据类型,加上相应的前缀,如果没有返回值,即 void 类型
,函数的前缀加上字母 v。
根据文件名,文件中相应的函数定义时也将文件名加到函数命名中,比如 tasks.c 文件中函数
vTaskDelete,函数中的 task 就是文件名中的 task。
宏定义
根据宏定义所在的文件,文件中的宏定义声明时也将文件名加到宏定义中,比如宏定义
configUSE_PREEMPTION 是定义在文件 FreeRTOSConfig.h 里面。宏定义中的 config 就是文
件名中的 config。另外注意,前缀要小写。
除了前缀,其余部分全部大写,同时用下划线分开。
排版和注释
缩进
Tab 制表符用于缩进,Tab 一次缩进 4 个字符空间。
注释
FreeRTOS 中注释不会超过 80 个字符宽度,除非对函数的参数进行注释时。源码中主要是采用/* */
的形式进行注释,不采用 C++中的双斜杠风格来注释。
1.3 关键配置
configCPU_CLOCK_HZ---配置MCU主频,单位Hz
configTICK_RATE_HZ---配置系统时钟节拍数,单位 Hz
configMAX_PRIORITIES---配置用户可用最大优先级数,0~configMAX_PRIORITIES-1
configTOTAL_HEAP_SIZE---配置堆大小
configUSE_TASK_NOTIFICATIONS---配置任务通知
configUSE_TIME_SLICING---配置时间片轮转调度
configLIBRARY_LOWEST_INTERRUPT_PRIORITY---受RTOS管理的最小中断优先级
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY---受RTOS管理的最大中断优先级
二、任务管理
2.1 任务栈
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) )
1,局部变量,函数调用时的现场保护和返回地址,函数的形参,进入中断函数前和中断嵌套等都需要栈空间,栈空间定义小了会造成系统崩溃
2,实际分配的栈大小可以在最小栈需求的基础上乘以一个安全系数,一般取 1.5-2
3,栈生长方向从高地址向低地址生长(M4 和 M3 是这种方式)
2.2 系统栈
任务栈不使用系统栈控件,中断函数和中断嵌套使用
Cortex-M3 和 M4 内核具有双堆栈指针,MSP 主堆栈指针和 PSP 进程堆栈指针
在 FreeRTOS 操作系统中,主堆栈指针 MSP 是给系统栈空间使用的,进
程堆栈指针 PSP 是给任务栈使用的
对于 Cortex-M3 内核和未使用 FPU(浮点运算单元)功能的 Cortex-M4 内核,需要64字节
对于具有 FPU(浮点运算单元)功能的 Cortex-M4 内核,需要200字节
2.3 任务优先级
FreeRTOS 中任务的最高优先级是通过 FreeRTOSConfig.h 文件中的 configMAX_PRIORITIES 进行配置的,用户实际可以使用的优先级范围是 0 到 configMAX_PRIORITIES – 1
用户配置任务的优先级数值越小,那么此任务的优先级越低,空闲任务的优先级是 0
建议用户配置宏定义 configMAX_PRIORITIES 的最大值不要超过 32
FreeRTOS 中处于运行状态的任务永远是当前能够运行的最高优先级任务
2.3.1 任务优先级分配
IRQ 任务:IRQ 任务是指通过中断服务程序进行触发的任务,此类任务应该设置为所有任务里面优先级最高的
高优先级后台任务:比如按键检测,触摸检测,USB 消息处理,串口消息处理等,都可以归为这一类任务 低优先级的时间片调度任务:比如 emWin 的界面显示,LED 数码管的显示等不需要实时执行的都可以归为这一类任务
实际应用中用户不必拘泥于将这些任务都设置为优先级 1 的同优先级任务,可以设置多个优先级,只需注意这类任务不需要高实时性
空闲任务:空闲任务是系统任务
特别注意:IRQ 任务和高优先级任务必须设置为阻塞式(调用消息等待或者延迟等函数即可), 只有这样,高优先级任务才会释放 CPU 的使用权,,从而低优先级任务才有机会得到执行
2.3.2 中断优先级与任务优先级
中断的优先级永远高于任何任务的优先级,即任务在执行的过程中,中断来了就开始执行中断服务程序
对于 STM32F103,F407 和 F429 来说,中断优先级的数值越小,优先级越高。而 FreeRTOS的任务优先级是,任务优先级数值越小,任务优先级越低
2.3.2 任务调度
2.2.3 相关API
vTaskStartScheduler-任务启动
空闲任务和可选的定时器任务是在调用这个函数后自动创建
正常情况下这个函数是不会返回的,运行到这里极有可能是用于定时器任务或者空闲任务的 heap 空 间不足造成创建失败,此时需要加大 FreeRTOSConfig.h 文件中定义的 heap 大小
如果在此类任务函数里面填的任务 ID 是 NULL,即数值 0 的话,那么执行生效的就是当前正在执行的任务
系统框架写法
int main(void)
{
/* 初始化外设 */
xTaskCreate(); /* 创建开始任务 */
vTaskStartScheduler(); /* 开启任务调度 */
}
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); /* */进入临界区
xTaskCreate(); /* 创建任务 1 */
xTaskCreate(); /* 创建任务 2 */
xTaskCreate(); /* 创建任务 3 */
vTaskDelete(StartTask_Handler); /* 删除开始任务 */
taskEXIT_CRITICAL(); /* 退出临界区 */
}
任务创建、挂起、删除
/* 相关配置 */
#define configSUPPORT_DYNAMIC_ALLOCATION 1 /* 支持动态内存申请 */
//#define configSUPPORT_STATIC_ALLOCATION 1 /* 支持静态内存申请 */
#define configTOTAL_HEAP_SIZE ((size_t)(20*1024)) /* 系统所有总的堆大小,heap_x.h需要,动态申请 */
/* 任务创建 */
/* 任务创建宏定义,便于修改 */
#define START_TASK_PRIO 1 /* 任务优先级 */
#define START_STK_SIZE 256 /* 任务堆栈大小 */
TaskHandle_t StartTask_Handler; /* 任务句柄 */
void start_task(void *pvParameters); /* 任务函数 */
/* 任务创建函数 */
xTaskCreate((TaskFunction_t )start_task, /* 任务函数 */
(const char* )"start_task", /* 任务名称 */
(uint16_t )START_STK_SIZE, /* 任务堆栈大小 */
(void* )NULL, /* 传递给任务函数的参数 */
(UBaseType_t )START_TASK_PRIO, /* 任务优先级 */
(TaskHandle_t* )&StartTask_Handler); /* 任务句柄 */
/* 任务挂起 */
vTaskSuspend(Task1Task_Handler);
/* 任务内解挂 */
vTaskResume(Task1Task_Handler);
/* 中断内解挂 */
BaseType_t YieldRequired;
YieldRequired=xTaskResumeFromISR(Task1Task_Handler);
portYIELD_FROM_ISR(YieldRequired); /* 判断是否需要调度到恢复的任务 */
/* 任务删除 */
vTaskDelete(Task1Task_Handler);
任务调度
嵌入式实时操作系统的核心就是调度器和任务切换,调度器的核心就是调度算法
抢占式务调度
每个任务都有不同的优先级,任务会一直运行直到被高优先级任务抢占或者遇到阻塞式的 API 函数,抢占式调度器会获得就绪列表中优先级最高的任务,并运行这个任
根据抢占式调度器,当前的任务要么被高优先级任务抢占,
要么通过调用阻塞式 API 来释放 CPU 使用权让低优先级任务执行,没有用户任务执行时就执行空闲任务
时间片调度
实现 Round-robin 调度算法需要给同优先级的任务分配一个专门的列表,用于记录当前就绪的任务,
并为每个任务分配一个时间片
每个任务都有相同的优先级,任务会运行固定的时间片个数或者遇到阻塞式的 API 函数
在 FreeRTOS 操作系统中只有同优先级任务才会使用时间片调度
默认情况下,此宏定义已经在 FreeRTOS.h 文件里面使能
FreeRTOS的时间片仅支持一个时钟周期,比如你的系统时钟节拍是1000Hz,那么时间片的大小就是1ms,时间片调度仅存在于同优先级任务
在 RTOS 中,最小的时间单位为一个 tick,即 SysTick 的中断周期,RT-Thread 和 μC/OS可以指定时间片的大小为多个 tick,但是 FreeRTOS不一样,时间片只能是一个 tick。与其说 FreeRTOS 支持时间片,倒不如说它的时间片就是正常的任务调度
/* 相关配置,时间片长度即1/Tick中断频率 */
#define configUSE_PREEMPTION 1 /* 1使用抢占式内核,0使用协程 */
#define configUSE_TIME_SLICING 1 /* 1使能时间片调度(默认式使能的) */
#define configTICK_RATE_HZ (20) /* 时钟节拍频率,20HZ = 50ms */
/* 两个任务优先级相等 */
#define TASK1_TASK_PRIO 2
#define TASK1_STK_SIZE 128
TaskHandle_t Task1Task_Handler;
void task1_task(void *pvParameters);
#define TASK2_TASK_PRIO 2
#define TASK2_STK_SIZE 128
TaskHandle_t Task2Task_Handler;
void task2_task(void *pvParameters);
三、系统延时
const TickType_t xDelayTime = pdMS_TO_TICKS(300);/* 将延时ms转换为系统节拍 */
const TickType_t xDelayTime = 300 / portTICK_RATE_MS;/* 将延时ms转换为系统节拍 */
/* TickType_t 等价于 portTickType */
/* 系统延时函数,单位:系统节拍,阻塞延时 portMAX_DELAY-最大延时等待 */
/* 相对延时 */
const TickType_t xDelayTime = pdMS_TO_TICKS(300);
vTaskDelay(xDelayTime);
/* 绝对延时 */
static portTickType xLastWakeTime;
const portTickType xFrequency = pdMS_TO_TICKS(500);
vTaskDelayUntil(&xLastWakeTime,xFrequency);
如果想精确周期性执行某个任务,可以调用系统节拍钩子函数vApplicationTickHook(),它在系统节拍中断服务函数中被调用,因此这个函数中的代码必须简洁
四、软件定时器
/* 定时器结构体、函数声明 */
TimerHandle_t MyTimer_Handle; /* 定时器句柄 */
void ReloadCallback(TimerHandle_t xTimer); /* 定时器回调函数 */
/* 创建软件定时器 */
MyTimer_Handle=xTimerCreate((const char* )"ReloadTimer", /* 定时器名称 */
(TickType_t )1000, /* 周期1s(1000个时钟节拍) */
(UBaseType_t )pdTRUE, /* 周期模式 */
(void* )1, /* 定时器ID */
(TimerCallbackFunction_t)ReloadCallback); /* 定时器回调函数 */
/* 定时器回调函数 */
void ReloadCallback(TimerHandle_t xTimer)
{
//do something
}
xTimerStart(MyTimer_Handle,0); /* 开启定时器 */
xTimerStop(MyTimer_Handle,0); /* 定时器停止 */
xTimerReset(MyTimer_Handle, 0); /* 定时器复位 */
五、消息队列
/* 消息队列宏定义 */
#define MESSAGE_Q_NUM 4 /* 发送数据的消息队列的数量 */
#define MESSAGE_Q_ITEM_NUM 200 /* 每个消息的空间大小 */
QueueHandle_t Message_Queue; /* 信息队列句柄 */
/* 创建消息队列 */
Message_Queue=xQueueCreate(MESSAGE_Q_NUM,MESSAGE_Q_ITEM_NUM);
/* 任务内消息发送 */
u8 sendData[MESSAGE_Q_ITEM_NUM];
BaseType_t err;
err=xQueueSend(Message_Queue,&senddata,10); /* 10为发送等待时间,有可能队列已满,err = errQUEUE_FULL 或 err = pdPASS */
/* 中断内消息发送 */
8 sendData[MESSAGE_Q_ITEM_NUM];
BaseType_t xHigherPriorityTaskWoken;
xQueueSendFromISR(Message_Queue,sendData,&xHigherPriorityTaskWoken); /* 向队列中发送数据,返回值,依然是 满了或Pass,第三个参数是判断高优先级 */接受到队列后,退出中断,是否需要调度
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);/*如果需要的话进行一次任务切换 */
/* 任务内消息接收 */
u8 *receiveData;
xQueueReceive(Message_Queue,receiveData,portMAX_DELAY) /* 返回值为pdPASS 或 errQUEUE_EMPTY,这里等待时间用portMAX_DELAY阻塞,所以不用再判断 */
/* 中断内消息接收 */
u8 *receiveData;
err=xQueueReceiveFromISR(Message_Queue,receiveData,&xTaskWokenByReceive); /* 向队列中接受数据,返回值, FAIL或Pass,第三个参数是判断高优先级接受到队列后,退出中断,是否需要调度 */
portYIELD_FROM_ISR(xTaskWokenByReceive);/* 如果需要的话进行一次任务切换 */
u8 remain_size; /* 消息队列剩余大小 */
remain_size=uxQueueSpacesAvailable(Message_Queue); /* 得到队列剩余大小 */
u8 used_size; /* 消息队列使用大小 */
used_size=uxQueueMessagesWaiting(Message_Queue); /* 得到队列使用大小 */
六、信号量
FreeRTOS的信号量包括二进制信号量、计数信号量、互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量
信号量API函数实际上都是宏,它使用现有的队列机制。这些宏定义在semphr.h文件中。如果使用信号量或者互斥量,需要包含semphr.h头文件。二进制信号量、计数信号量和互斥量信号量的创建API函数是独立的,但是获取和释放API函数都是相同的;递归互斥信号量的创建、获取和释放API函数都是独立的
6.1 二值信号量
/* 二值信号量创建 */
SemaphoreHandle_t BinarySemaphore; /* 二值信号量句柄 */
BinarySemaphore=xSemaphoreCreateBinary(); /* 创建二值信号量 */
/* 二值信号量等待 */
BaseType_t err;
err = xSemaphoreTake(BinarySemaphore,portMAX_DELAY); /* 获取信号量 */
/* 二值信号量发送 */
BaseType_t err;
err = xSemaphoreGive(BinarySemaphore); /* 释放二值信号量 */
6.2 计数信号量
/* 计数信号量创建 */
SemaphoreHandle_t CountSemaphore;/* 计数型信号量句柄 */
CountSemaphore=xSemaphoreCreateCounting(255,0); /* 创建计数型信号量,最大计数和初始化计数,参数没改动的话,为long,所以最大值可以设计为不止255 */
/* 计数信号量等待 */
UBaseType_t semavalue;
xSemaphoreTake(CountSemaphore,portMAX_DELAY); /* 等待数值信号量,阻塞 */
semavalue=uxSemaphoreGetCount(CountSemaphore); /* 获取数值信号量值 */
/* 计数信号量发送 */
BaseType_t err;
err=xSemaphoreGive(CountSemaphore);/* 释放计数型信号量 */
6.3 互斥信号量
/* 创建互斥信号量 */
SemaphoreHandle_t MutexSemaphore; /* 互斥信号量句柄 */
MutexSemaphore=xSemaphoreCreateMutex(); /* 创建互斥信号量 */
/* 互斥信号量等待 */
xSemaphoreTake(MutexSemaphore,portMAX_DELAY); /* 获取互斥信号量,因为是阻塞,也就不需要查看什么返回值 */
/* 互斥信号量发送 */
xSemaphoreGive(MutexSemaphore); /* 释放互斥信号量 */
6.4 递归信号量
SemaphoreHandle_t RecursiveMutex;/* 递归互斥信号量句柄 */
RecursiveMutex = xSemaphoreCreateRecursiveMutex(); /* 创建递归互斥信号量 */
/* 递归互斥信号量等待 */
xSemaphoreTakeRecursive(RecursiveMutex,10); /* 10为等待节拍 */
/* 递归互斥信号量发送 */
xSemaphoreGiveRecursive(RecursiveMutex); /* 发送递归互斥信号量 */
七、事件标志组
/* 例子,3个事件 */
#define EVENTBIT_0 (1<<0)
#define EVENTBIT_1 (1<<1)
#define EVENTBIT_2 (1<<2)
#define EVENTBIT_ALL (EVENTBIT_0|EVENTBIT_1|EVENTBIT_2)
EventGroupHandle_t EventGroupHandler; /* 事件标志组句柄 */
EventGroupHandler=xEventGroupCreate(); /* 创建事件标志组 */
/* 事件标志组置位 */
xEventGroupSetBits(EventGroupHandler,EVENTBIT_1); /* 事件1置位 */
/* 事件标志组清除 */
xEventGroupClearBits(EventGroupHandler,EVENTBIT_1); /* 事件1清除 */
/* 事件标志组获取 */
EventBits_t NewValue;
NewValue = xEventGroupGetBits(EventGroupHandler);
/* 事件标志组等待 */
EventValue=xEventGroupWaitBits((EventGroupHandle_t )EventGroupHandler, /* 句柄 */
(EventBits_t )EVENTBIT_ALL, /* 标志位 */
(BaseType_t )pdTRUE, /* 获取成功后 清除 */
(BaseType_t )pdTRUE, /* 等待所有标志位 置位 */
(TickType_t )portMAX_DELAY); /* 阻塞 */
八、任务通知
任务通知可完成消息队列、信号量、事件标志组的功能,不过任务通知是只能实现一对一的,也就是一个任务对一个任务
/* 通知值发送,设置通知值,可发送一个数据 */
u8 data;
BaseType_t err;
err=xTaskNotify((TaskHandle_t )Task_Handler, /* 接收任务通知的任务句柄 */
(uint32_t )data, /* 任务通知值 */
(eNotifyAction )eSetValueWithOverwrite); /* 覆写的方式发送任务通知 */
/* 通知值发送,设置通知值,可做标记位组 */
#define EVENTBIT_1 (1<<1)
xTaskNotify((TaskHandle_t )Task_Handler, /* 接收任务通知的任务句柄 */
(uint32_t )EVENTBIT_1, /* 要更新的bit */
(eNotifyAction )eSetBits); /* 更新指定的bit */
/* 通知值获取,获取通知值,并判断是否需要清零 */
BaseType_t err;
uint32_t NotifyValue;
err=xTaskNotifyWait((uint32_t )0x00, /* 进入函数,没有接受到通知,不清除任何bit */
(uint32_t )ULONG_MAX, /* 退出函数,接受到通知,清除所有(0xffffffffUL)位的bit, */
(uint32_t* )&NotifyValue, /* 保存任务通知值 */
(TickType_t )portMAX_DELAY); /* 阻塞时间 */
九、临界段
/* 任务内临界段处理 */
taskEXIT_CRITICAL();
/* 任务处理 */
taskEXIT_CRITICAL();
/* 中断内临界段处理 */
taskENTER_CRITICAL_FROM_ISR();
/* 中断内处理 */
taskEXIT_CRITICAL_FROM_ISR();
十、内存管理
/* 内存申请 */
u8 *buffer;
buffer=pvPortMalloc(30); /* 申请内存,30个字节 */
/* 释放内存 */
vPortFree(buffer);
/* 获取内存剩余空间 */
u32 freeSize;
freeSize = xPortGetFreeHeapSize();