[TOC]
FreeRTOS内核应用开发学习手记
移植
1、解压并添加官方源码到工程中,包括:
FreeRTOS\Source\*.c
FreeRTOS\Source\include\*.h
FreeRTOS\Source\portable\MemMang\heap_4.c
FreeRTOS\Source\portable\RVDS\ARM_CM4F\*
FreeRTOS\Demo\CORTEX_STM32F103_Keil\FreeRTOSConfig.h
2、修改FreeRTOSConfig.h:
#include "stm32f103.h"
改为你的,如
#include "stm32f4xx.h"
3、注释原工程中:
void SVC_Handler(void) {}
void PendSV_Handler(void) {}
4、修改SysTick中断服务函数
extern void xPortSysTickHandler(void);
//systick中断服务函数
void SysTick_Handler(void)
{
TimingDelay_Decrement();
#if (INCLUDE_xTaskGetSchedulerState == 1 )
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
#endif /* INCLUDE_xTaskGetSchedulerState */
xPortSysTickHandler();
#if (INCLUDE_xTaskGetSchedulerState == 1 )
}
#endif /* INCLUDE_xTaskGetSchedulerState */
}
5、完成
注意
freertos+cjson时,需要改一下内存分配函数:
cJSON_Hooks hooks;
hooks.malloc_fn = pvPortMalloc;
hooks.free_fn = vPortFree;
cJSON_InitHooks(&hooks);
这样,只要把当前任务的stacksz(注意它*4才是实际占用的字节数哦)设置到足够大就OK了。
任务状态迁移
任务创建与删除
// 返回值
BaseType_t xReturn = pdPASS;
// 任务句柄
TaskHandle_t testHandle = NULL;
xReturn = xTaskCreate((TaskFunction_t) TaskTest, // 任务入口函数
(const char*) "TaskTest"), // 任务名字
(uint16_t) 512, // 任务栈大小
(void*) NULL, // 任务入口函数参数
(UBaseType_t)3, // 任务优先级
(TaskHandle_t*)&testHandle); // 控制块指针
if(xReturn == pdPASS){
// 启动任务调度
vTaskStartScheduler();
}
// 删除指定任务
vTaskDelete(testHandle);
// 删除任务自身
vTaskDelete(NULL);
优先级说明
用户配置的任务优先级数值越小,那么此任务的优先级越低,空闲任务的优先级是0
。
中断优先级的数值越小,优先级越高。
用户实际可以使用的任务优先级范围是 0
到 configMAX_PRIORITIES – 1
。
建议用户配置宏定义configMAX_PRIORITIES
的最大值不要超过32
,即用户任务可以使用的优先级范围是0到31。
任务优先级设置推荐方式:
任务挂起与恢复
// 挂起任务
vTaskSuspend(taskHandle);
// 挂起全部任务
vTaskSuspendAll();
// 恢复任务
vTaskResume(taskHandle);
// 恢复全部任务
vTaskResumeAll();
// 从中断恢复任务
BaseType_t xYieldRequired = xTaskResumeFromISR(taskHandle);
if(xYieldRequired == pdTRUE){
// 执行上下文切换
portYIELD_FROM_ISR();
}
任务延时
/***********************相对延时**************************/
// 相对延时,延时100个tick,不排除调用前后任务被抢占
vTaskDelay(100);
/* ...code... */
/***********************绝对延时*************************/
// 绝对延时,固定频率运行
// 保存上一次时间,系统自动更新
static portTickType previousWakeTime;
// 延时时间
const portTickType timeIncrement = pdMS_TO_TICKS(100);
// 当前系统时间
previousWakeTime = xTaskGetTickCount();
while(1){
// 间隔100个tick
vTaskDelayUntil(&previousWakeTime, timeIncrement);
/* ...code... */
}
消息队列
configSUPPORT_DYNAMIC_ALLOCATION = 1
/************************创建队列***********************/
// 队列长度,最大消息数
#define QUEUE_LEN 4
// 每个消息的大小(字节)
#define QUEUE_SIZE 4
// 返回值
BaseType_t xReturn = pdPASS;
// 队列句柄
QueueHandle_t queueHandle = NULL;
// 进入临界区
taskENTER_CRITICAL();
queueHandle = xQueueCreate((UBaseType_t) QUEUE_LEN, // 长度
(UBaseType_t) QUEUE_SIZE); // 大小
// 创建成功
if(NULL != queueHandle){
printf("创建成功\r\n");
}
// 退出临界区
taskEXIT_CRITICAL();
/*********************任务中发到队尾***********************/
// 删除队列
vQueueDelete(queueHandle);
// 待发送内容
uint32_t sendData = 1;
xReturn = xQueueSend(queueHandle, // 队列句柄
&sendData, // 发送内容
0); // 等待时间
// 发送成功
if(pdPASS == xReturn){
printf("发送成功\r\n");
}
/*********************中断中发到队尾**********************/
// 从中断发送队列消息
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(queueHandle,&sendData,&xHigherPriorityTaskWoken);
if(xHigherPriorityTaskWoken){
// 上下文切换
taskYIELD_FROM_ISR();
}
/**********************其他发到队首***********************/
// 发到队首
xQueueSendToFront(...);
// 中断中发到队首
xQueueSendToFrontFromISR(...);
/*********************任务中接收消息**********************/
// 待接收内容
uint32_t getData;
xReturn = xQueueRecevie(queueHandle, // 队列句柄(会删除队列消息)
&getData, // 接收内容
portMAX_DELAY); // 等待时间,一直等
// 获取成功
if(pdTRUE == xReturn){
print("接收到:%d\r\n", getData);
}
// 获取队列消息,但不删除队列中的内容,用法一样
xQueuePeek(...)
/********************中断中接收消息**********************/
BaseType_t xTaskWokenByReceive = pdFALSE;
xQueueReceiveFromISR(queueHandle, &getData, xTaskWokenByReceive);
if(xTaskWokenByReceive != pdFALSE){
// 上下文切换
taskYIELD();
}
信号量
信号量可以由其他任务删除,互斥量只能由当前任务删除。
互斥量可减小“优先级翻转”现象的影响。
configUSE_MUTEXES = 1
configUSE_RECURSIVE_MUTEXES = 1
configQUEUE_REGISTRY_SIZE = 10
/********************二值信号量**********************/
// 信号量句柄
SemaphoreHandle_t semaphoreHandle = NULL;
// 创建二值信号量
semaphoreHandle = xSemaphoreCreateBinary();
// 创建成功
if(semaphoreHandle != NULL){
print("创建成功\r\n");
}
/********************计数信号量**********************/
semaphoreHandle = xSemaphoreCreateCounting(5, // 最大计数到5
5); // 初始当前计数值为5
// 创建成功
if(semaphoreHandle != NULL){
print("创建成功\r\n");
}
/********************互斥信号量**********************/
// 创建单次互斥量
semaphoreHandle = xSemaphoreCreateMutex(); .
// 创建成功
if(semaphoreHandle != NULL){
print("创建成功\r\n");
}
// 创建递归互斥量
semaphoreHandle = xSemaphoreCreateRecursiveMutex();
// 创建成功
if(semaphoreHandle != NULL){
print("创建成功\r\n");
}
/********************删除信号量**********************/
vSemaphoreDelete(semaphoreHandle);
/******************任务中释放信号量*******************/
xReturn = xSemaphoreGive(semaphoreHandle);
if(pdTRUE == xReturn){
print("释放成功\r\n");
}
// 释放递归互斥量,其他一样
xSemaphoreGiveRecursive(...);
/******************中断中释放信号量*******************/
BaseType_t pxHigherPriorityTaskWoken;
xSemaphoreGiveFromISR(semaphoreHandle, &pxHigherPriorityTaskWoken);
// 上下文切换
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
/*****************任务中获取信号量********************/
xReturn = xSemaphoreTake(semaphoreHandle, // 信号量句柄
portMAX_DELAY) // 等待时间,一直等
if(pdTRUE == xReturn){
print("获取成功\r\n");
}
// 获取递归互斥量,其他一样
xSemaphoreTakeRecursive(...);
/*****************中断中获取信号量********************/
很少用到
事件
configUSE_16_BIT_TICKS = 0
/*****************任务中创建事件*********************/
// 事件句柄
EventGroupHandle_t eventHandle = NULL;
// 创建事件
eventHandle = xEventGroupCreate();
// 创建成功
if(NULL != eventHandle){
print("创建成功\r\n");
}
// 删除事件
xEventGroupDelete(eventHandle);
#define BIT0 (0x01 << 0) //
#define BIT1 (0x01 << 1) //
/*****************任务中设置事件********************/
// 返回值,返回的置位前的值
EventBits_t rReturn;
rReturn = xEventGroupSetBits(eventHandle, // 事件句柄
BIT0 | BIT1); // 置位事件组
/*****************中断中设置事件********************/
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
BaseType_t xResult;
xResult = xEventGroupSetBitsFromISR(eventHandle,
BIT0 | BIT1,
&xHigherPriorityTaskWoken);
if(pdFAIL != xResult){
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
/*****************任务中等待事件********************/
EventBits_t rReturn;
rReturn = xEventGroupWaitBits(eventHandle, // 事件句柄
BIT0|BIT1, // 事件组
pdTRUE, // 退出时清除事件
pdTRUE, // 等待所有事件
portMAX_DELAY) // 超时等待时间
if((rReturn&(BIT0|BIT1)) == (BIT0|BIT1)){ // 两个事件都到达
print("BIT0和BIT1都到达\r\n");
}
/*****************任务中清除事件********************/
// 返回值,返回的清除前的值
EventBits_t uxBits;
uxBits = xEventGroupClearBits(eventHandle, BITO|BIT1);
各功能特点
-
二值信号量
就像一个标志位,事件产生置一,事件处理后置零。对于二值信号量,对存在优先级反转的问题。比如任务3、2、1的优先级从高到低,任务3和1通过二值信号量控制访问某个资源,若任务1先锁定该资源,则任务3访问该资源时,会因为得不到资源而阻塞。此时,若任务2运行条件具备,任务2会打断任务1而执行,从而呈现低优先级的任务2优先于高优先级的任务3运行的情景,即优先级反转了。
-
计数信号量
事件产生加一,事件处理减一,减到零表示事件处理完毕。计数型信号量通常用于如下两个场合:a. 事件计数:每次事件发生的时候就在事件处理函数中释放信号量(增加信号量计数值),其他任务会获取信号量(信号量计数值减一)来处理事件。
b. 资源管理:信号量的值代表当前资源的可用数量,一个任务要想获取资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减一。当信号量值为0的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量就会加一。
-
互斥信号量
当一个资源不能被同时使用时使用互斥,比如打印机。互斥信号量使用和二值信号量具有相同的API操作函数,所以互斥信号量也可以设置阻塞时间。互斥信号量与二值信号量的区别在于,互斥信号量具有优先级继承的特性。即在任务3获取互斥信号量的时候,若无法获取互斥信号量,则会判断一下当前获取互斥信号量的任务优先级是否比自己低,若是,则将该任务的优先级提高到和自己一样。
二值信号量更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适合用于简单的互斥访问。
互斥信号量不能用于中断服务函数中,原因如下:
a. 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
b. 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间而进入阻塞态
-
任务通知
任务通知一定程度上可以替代二值信号量、计数信号量、事件组或队列。任务通知优点:更快、占用RAM少
任务通知缺点:数据不能从任务发送到ISR(也就是ISR中不能读取任务通知);接收处理任务通知只能在本任务中;任务通知只能通过32位无符号整数传递数据;当任务为“pending”,发送任务通知API不会等待任务变为“not-pending”而阻塞,也就是数据可能丢失
-
队列
携带信息进行任务间通信。队列的原理是FIFO,可以存放数据。
可以创建队列集合,队列集合里面可以放队列、计数信号量或二值信号量,使用队列集合适用于多个任务向某个任务发送数据,而数据类型不相同,不同数据类型的数据存放在不同的队列集合成员里。
队列可以实现邮箱功能,即多个任务可以读取长度为1的队列里的数据,但不会清除数据,即使队列有数据但没有更新,调用xQueuePeek()也不会阻塞;任务往此队列写数据是覆盖里面的数据。适用于向多个任务发送数据,即一对多通信,即广播。
-
事件组
任务间、任务与中断等多个事件的同步。
事件组可以用于某个任务等待几个条件都满足或某个条件满足才解除阻塞的情景(多个发送,一个接收,即多对一通信),或者几个任务互相等待条件满足才进一步执行任务,比如task A,B,C需要进一步执行各自的任务,需要task A,B,C都满足条件(多个发送,多个接收,即多对多通信)。 -
优先级翻转
在使用二值信号量的时候会存在优先级翻转的问题。
(1)任务H和任务M处于挂起状态,等待某一事件的发生,任务L正在运行。
(2)某一时刻任务L想要访问共享资源,在此之前它必须先获得对应资源的信号量。
(3)任务L获得信号量并开始使用该共享资源。
(4)由于任务H优先级高,它等待的时间发生后便剥夺了任务L的CPU使用权。
(5)任务H开始运行。
(6)任务H运行过程中也要使用任务L正在使用着的资源,由于该资源的信号量还被L占用着,任务H只能进入挂起状态,等待任务L释放该信号量。
(7)任务L继续运行。
(8)由于任务M优先级高于任务L,当任务M等待的事件发生后,任务M剥夺了任务L的CPU使用权。
(9)任务M处理该处理的事。
(10)任务M执行完毕后,将CPU使用权归还给任务L。
(11)任务L继续执行。
(12)最终任务L完成所有的工作并释放了信号量,到此为止,由于实时内核知道有个高优先级的任务正在等待这个信号量,故内核做任务切换。
(13)任务H得到该信号量并接着运行。
任务H的优先级实际上降到了任务L的优先级水平。因为任务H要一直等待任务L释放其占用的那个共享资源。由于任务M剥夺了任务L的CPU使用权,使得任务H的情况更加恶化,这样就相当于任务M的优先级高于任务H,导致优先级翻转。
软件定时器
configUSE_TIMERS = 1
configTIMER_TASK_PRIORITY = (configMAX_PRIORITIES-1)
configTIMER_QUEUE_LENGTH = 10
/*****************任务中创建定时器********************/
// 定时器句柄
TimerHandle_t swtmrHandle = NULL;
BaseType_t xReturn;
// 自定义回调函数
void SwtFun(void* parameter){}
swtmrHandle = xTimerCreate((const char*)"time1", // 定时器名字
(TickType_t)1000, // 周期1000tick
(UBaseType_t)pdTRUE, // 循环模式
(void*)1, // 唯一ID
(TimerCallbackFunction_t)SwtFun); //回调函数
/*****************任务中启动定时器********************/
// 创建成功
if(NULL != swtmrHandle){
xTimerStart(swtmrHandle, // 启动定时器
0); // 等待时间0
}
// 停止软件定时器
xReturn = vTimerStop(swtmrHandle, 0);
// 删除软件定时器
xReturn = xTimerDelete(swtmrHandle, 0);
/*****************中断中启动定时器********************/
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xReturn = xTimerStartFromISR(swtmrHandle,&xHigherPriorityTaskWoken);
if(pdPASS == xReturn){
print("启动成功\r\n");
}
if(xHigherPriorityTaskWoken){
// 上下文切换
}
xReturn = vTimerStopFromISR(swtmrHandle, &xHigherPriorityTaskWoken);
if(pdPASS == xReturn){
print("停止成功\r\n");
}
if(xHigherPriorityTaskWoken){
// 上下文切换
}
任务通知
configUSE_TASK_NOTIFICATIONS = 1
// 发送通知部分
/*****************xTaskNotifyGive()********************/
TaskHandle_t taskHandle;
// 向taskHandle发送通知
xTaskNotifyGive(taskHandle);
ulTaskNotifyTake(pdTRUE, // 退出时清空任务计数
portMAX_DELAY); // 阻塞等待通知
/*************vTaskNotifyGiveFromISR()*****************/
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(taskHandle, &xHigherPriorityTaskWoken);
// 上下文切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
/******************xTaskNotify()***********************/
xTaskNotify(taskHandle, 0, eNoAction);
“eAction取值:eNoAction、eSetBits、eIncrement、eSetValueWithOverwrite、eSetValueWithoutOverwrite”
/***************xTaskNotifyFromISR()*******************/
xTaskNotifyFromISR(taskHandle, 0, eNoAction, &xHigherPriTaskWoken);
// 上下文切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
/***************xTaskNotifyAndQuery()******************/
// 存对象任务的上一个任务通知值,为NULL则不用回传
uint32_t ulPreviousValue;
xTaskNotifyAndQuery(taskHandle, 0, eSetBits, &ulPreviousValue)
/************xTaskNotifyAndQueryFromISR()**************/
xTaskNotifyAndQueryFromISR(taskHandle,
0,
eSetBits,
&ulPreviousValue,
&xHigherPriorityTaskWoken);
// 上下文切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
// 获取通知部分
/****************ulTaskNotifyTake()*******************/
// 返回任务的当前通知值(减1或清0之前的值)
uint32_t res;
res = ulTaskNotifyTake(pdTRUE, // 退出时清零(pdFALSE则减一)
portMAX_DELAY); // 等待时间
/****************xTaskNotifyWait()********************/
// 存接收到的任务通知值,为NULL则不需要
uint32_t ulNotifiedValue;
BaseType_t res;
res = xTaskNotifyWait(0x00, // 使用通知前,任务通知值的哪些位清0
ULONG_MAX, // 结束本函数前,接收到的通知值的哪些位清0
&ulNotifiedValue, // 保存接收到的任务通知值
portMAX_DELAY); // 等待时间
if((ulNotifiedValue & 0x01) != 0){
/* 位0被置1 */
}
内存管理
/****************heap_4.c*******************/
//系统所有总的堆大小
#define configTOTAL_HEAP_SIZE ((size_t)(36*1024))
// 获取剩余内存
uint32_t g_memsize = xPortGetFreeHeapSize();
// 申请1024字节内存
uint8_t* ptr = pvPortMalloc(1024);
// 获取成功
if(NULL != ptr) {
printf("\r\n");
}
// 释放内存
vPortFree(ptr);
程序的内存分配
一个由C/C++编译的程序占用的内存分为:
栈区stack:由编译器自动分配释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构的栈。
堆区heap:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式类似于链表。
全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
文字常量区:常量字符串就是放在这里的,程序结束后由系统释放。
程序代码区:存放函数体的二进制代码。
int a = 0; // 全局初始化区
char *p1; // 全局未初始化区
main()
{
int b; // 栈
char s[] = "abc"; // 栈
char *p2; // 栈
char *p3 = "123456"; // 123456\0在常量区,p3在栈上。
static int c =0; // 全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20); //分配得来的10和20个字节的区域就在堆区。
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
MDK中RAM占用大小分析
-
ROM
—— 存储固化程序的(存放指令代码和一些固定数值,程序运行后不可改动)c
文件及h
文件中所有代码、全局变量、局部变量、const
限定符定义的常量数据、startup.asm
文件中的代码(类似ARM
中的bootloader
或者X86
中的BIOS
,一些低端的单片机是没有这个的)通通都存储在ROM
中。 -
RAM
—— 程序运行中数据的随机存取(掉电后数据消失)整个程序中,所用到的需要被改写的量,都存储在RAM
中,“被改变的量”包括全局变量、局部变量、堆栈段。 -
FLASH
—— 存储用户程序和需要永久保存的数据。 -
Code
指存储到flash【Rom】
中的程序代码。 ZI (zero initial)
: 就是程序中用到的变量并且被系统初始化为0
的变量的字节数,keil 编译器默认是把你没有初始化的变量都赋值一个0
,这些变量在程序运行时是保存在RAM
中的。RW
:是可读可写变量,就是初始化时候就已经赋值了的,RW + ZI
就是你的程序总共使用的RAM
字节数。RO
:是程序中的指令和常量,这些值是被保存到Rom
中的。Total ROM Size (Code + RO Data + RW Data)
:这样所写的程序占用的ROM
的字节总数,也就是说程序所下载到ROM flash
中的大小。-
为什么
Rom
中还要存RW
,因为掉电后RAM
中所有数据都丢失了,每次上电RAM
中的数据是被重新赋值的, 每次这些固定的值就是存储在Rom
中的,为什么不包含ZI
段呢,是因为ZI
数据都是0
,没必要包含,只要程序运行之前将ZI
数据所在的区域一律清零即可,包含进去反而浪费存储空间。 -
所有的全局变量、静态变量之类的,全部存储在静态存储区,紧跟静态存储区之后的,是堆区(如没用到
malloc
,则没有该区),之后是栈区,栈在程序中存储局部变量。 - 在程序里面,所有的内存分为:堆+栈,只是他们各自的起始地址和增长方向不同,他们没有一个固定的界限。
- 堆:向高地址增长,
- 栈:向低地址增长。