1、动态创建任务
freeRTOS自动分配内存。
void freertos_demo(void)
{
创建:start_task; 优先级:1
vTaskStartScheduler();
}
void start_task(void* pvParamters)
{
创建:task1; 优先级:2; led亮灭;延时500毫秒;
创建:task2; 优先级:3;led亮灭;延时500毫秒;
创建:task3; 优先级:4; 延时10毫秒;
vTaskDelete(NULL);
}
结果:先运行task1,再运行task2,再运行task3,一直运行task3…
分析任务的执行顺序:
创建完start_task任务后,开启了任务调度vTaskStartScheduler(),然后start_task就开始执行了,创建task1,task1的任务优先级为2,比start_task优先级高,会执行task1,阻塞500毫秒,释放自己的CPU使用权,给start_task执行,创建task2,task2任务优先级比start_yask高,运行task2,task2阻塞,回到start_task,创建任务3,优先级最高,抢占任务,延时10ms,阻塞了,然后回到vTaskDelete(NULL);
规律:创建完一个任务就开始调度。
若想要按照任务优先级高低来运行,就需要在进入start_task()时,关闭任务调度器。任务的切换是在中断中进行的,因此提供了临界区,用来关闭中断函数的。当任务创建完成后,才会开启任务调度器。
void freertos_demo(void)
{
创建:start_task; 优先级:1
vTaskStartScheduler();
}
void start_task(void* pvParamters)
{
taskENTER_CRITICAL();
创建:task1; 优先级:2; led亮灭;延时500毫秒;
创建:task2; 优先级:3;led亮灭;延时500毫秒;
创建:task3; 优先级:4; 延时10毫秒;
vTaskDelete(NULL);
taskEXIT_CRITICAL();
}
运行结果: task3,task2,task1,task3,task3…
规律:所以任务创建完再开始调度。
2、静态创建任务
手动分配内存。
(1)需要将宏configSUPPORT_STATIC_ALLOCATION配置为1。
找到FreeRTOSConfig.h找到宏定义将其改成1。
(2)实现2个函数。定义空闲任务与定时器任务的任务堆栈及TCB。
(3)实现2个接口函数;
定时器任务是可选的。
编译的时候,报错,只是定义了下面这2个函数,而没有实现。
空闲任务内存分配:
StaticTask_t idle_task_tcb;//空闲任务的任务控制块
StackType idle_task_stack[configMINIMAL_STACK_SIZE];//空闲任务的堆栈大小,定义一个数组
void vApplicationGetIdleTaskMemory(StaticTask_t ** ppxIdleTaskTCBBuffer, StackType_r ** ppxIdleTaskStackBuffer, unit32_t* pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer = &idle_task_tcb;
*ppxIdleTaskTCBBuffer = idle_task_stack;
*pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
定时器任务内存分配:
找到任务调度器开启的函数vTaskStartScheduler();创建一个空闲任务,还会创建一个定时器任务xTimerCreateTimerTask();点进去,需要实现下面这个函数:
//软件定时器内存分配
StaticTask_t timer_task_tcb;//空闲任务的任务控制块
StackType timer_task_stack[configMINIMAL_STACK_SIZE];//空闲任务的堆栈大小,定义一个数组
void vApplicationGetTimerTaskMemory(StaticTask_t ** ppxTimerTaskTCBBuffer, StackType_t ** ppxTimerTaskStackBuffer,unit32_t * puiTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer = &timer_task_tcb;
*ppxTimerTaskTCBBuffer = timer_task_stack;
*pulTimerTaskStackSize = configTIMER_STACK_SIZE;
}
(4)定义函数入口参数
void freertos_demo(void)
{
xTaskCreateStatic();
vTaskStartScheduler();
}
函数xTaskCreateStatic(),里面的参数前五个与动态的一样,后两个需要自己手动,
StackType_t start_task_stack[START_TASK_STACK_SIZE];
StaticTask_t start_task_tcb;
xTaskCreateStatic(.........(StackType_t*)start_task_stack,(StaticTask_t*) &start_task_tcb);
void freertos_demo(void)
{
starttask_handler = xTaskCreateStatic(......);
vTaskStartScheduler();
}
void start_task(void* pvParamters)
{
taskENTER_CRITICAL();
task1_handler=创建:task1; //每创建一个任务都要读取它的返回值
task2_handler=创建:task2;
task3_handler=创建:task3;
vTaskDelete(NULL);
taskEXIT_CRITICAL();
}
获取控制块内存(首地址);
获取堆栈内存(首地址);
标记使用的静态的方式申请的TCB和堆栈内存;
调用pvInitialiseNewTask初始化任务块,并将控制块信息返回给任务句柄,以便后续返回句柄信息;
调用prvAddBewTaskToReadyList添加新创建任务到就绪列表中;
3、任务挂起、恢复
挂起:暂停;
删除:无法恢复,堆栈内存空间都被释放掉了;
vTaskSuspend();//挂起任务
vTaskResume();//恢复被挂起的任务
xTaskResumeFromISR();//在中断中恢复被挂起的任务,有返回值
无论优先级如何,被挂起的任务都将不再被执行,直到任务被恢复;
挂起不支持嵌套,无论挂起多少次,只需要在任务中调用vTaskResume()即可使被恢复的任务进入就绪态。
任务恢复函数介绍:
返回值为pdTRUE:任务恢复后需要进行任务切换;要恢复任务的优先级大于当前执行任务的优先级;
返回值为pdFALSE:任务恢复后不需要进行任务切换;
注意:中断服务程序中要调用freeRTOS的API函数,则中断优先级不能高于FreeRTOS所管理的最高优先级(5-15),可在FreeTOSConfig.h中找到。
实验:4个任务:
start_task:用来创建其他的三个任务;
task1:实现LED0每500ms闪烁一次;
task2:实现LED1每500ms闪烁一次;
task3:判断按键按下逻辑,KEY0按下,挂起task1,按下KEY1在任务中恢复task1,按下KEY2,在中断中恢复task1(外部中断线实现);
实现:
使用动态创建任务,前面的任务创建,不需要修改,只需要修改task3。
注意是在任务中恢复的。
void task3(void * pvParameters)
{
uint8_t key = 0;
while(1)
{
//peintf("task3正在运行!\r\n");
key = key_scan(0);
if(key == KEY0_PRESS)
{
printf("挂起task1\r\n");
vTaskSuspend(task1_handler);
}else if(key == KEY1_PRES)
{
printf("在任务中恢复task1\r\n");
vTaskResume(task1_handler);
}
vTaskDelay(10);
}
}
下面代码是在中断中恢复的:
KEY2外部中断,中断优先级是(2,2)。此段代码可以参考freeRTOS官网API函数的使用方法中的例程。
extren TaskHandle_t task1_handler;
void HAL_GPIO_EXTI_Callback(unit16_t GPIO_Pin)
{
delay_ms(20);
switch(GPIO_Pin)
{
BaseType_t xYieldRequired;
case KEY2_INT_GPIO_PIN:
if(KEY2==0)
{
xYieldRequired = xTaskResumeFromISR(task1_handler);
printf("在中断中恢复task1\r\n");
}
if(xYieldRequired == pdTRUE)
{
portYIELD_FROM_ISR(xYieldRequired );//任务切换
}
break;
default:break;
}
}
在串口打印中,当按下KEY2的时候会报错,port.c 807行。
将分组中的2修改为4,编译下载,再次打开串口,报错791
将优先级中的(2,2)修改为(5,0)。
4、中断
4.1、中断优先级分组设置
ARM Cortex-M 使用了8位宽的寄存器来配置中断的优先等级,这个寄存器就是中断优先级配置寄存器,2的8次方=256,最大能支持256个中断优先级。
抢占优先级相同的话,子优先级中,优先级高的、低的都不能打断。
中断优先级数值越小越优先。
freeRTOS为了方便管理,使用了Group4来分组,通过调用函数HAL_NVIC_SetPriorityGrouping(NVIC_PRIOPITYGROUP_4)来完成设置。
FreeRTOS官网关于中断说明:http://www.freertos.org/RTOS-Cortex-M3-M4.html
一个地址你是8位,4个组成32位的。
PendSV和SysTick设置为最低优先级,为了保证系统任务切换不会阻塞系统其他中断的响应。中断可以打断任务,任务不可以打断中断。PendSV处理任务切换,
4.2、实验
两个定时器,一个优先级为4,另一个为6,注意:系统所管理的优先级范围是:5~15,现象:两个定时器每1s,打印一段字符串,当关中断的时候,停止打印,开中断时持续打印。
任务1中测试:
void task1(void * pvParameters)
{
uint8_t task1_num = 0;
while(1)
{
if(++task1_num == 5)
{
task1_num = 0;
printf("关中断!!\r\n");
portDISABLE_INTERRUPTS();
delay_ms(5000);
printf("开中断!!\r\n");
portENABLE_INTERRUPTS();
}
vTaskDelay(1000);
}
}
关中断的时候,优先级为4的正在运行,6的不运行,开中断,4、6都在运行。
portDISABLE_INTERRUPTS();会把低于优先级为5的都给关闭,5秒之后,就开中断,
vTaskDelay(1000)也会调用portDISABLE_INTERRUPTS();只不过参数设置为0了。
5、临界段
临界段代码也叫临界区,是指,那些必须完整运行,不能被打断的代码段。
应用场景:
(1)外设:需要严格按照时序初始化的外设,IIC、spi,IIC中有个几微秒的延时,这个时候不能被打断。
(2)系统:系统自身需求。
(3)用户:用户需求。
什么可以打断当前程序的运行?中断、任务调度(PendSV:切换任务)。
关中断、将PendSV优先级设为最低。
比较强悍!!
因为:临界区是直接屏蔽了中断,系统任务调度靠中断,ISR也靠中断。
6、任务调度器的挂起
任务调度器就是执行任务切换的。任务切换就是PendSV
挂起任务调度器,调用此函数不需要关闭中断。
vTaskSuspendAll();
{
......//内容
}
xTaskResumeAll();
内容就是:不被其他任务打断,但是中断可以打断。防止任务与任务之间的一个抢夺。
挂起调度器的方式,适用于临界区位于任务与任务之间,既不用去延时中断,又可以做到临界区的安全。
挂起任务调度器:vTaskSuspendAll(),调用一次挂起调度器,该变量uxSchedulerSuspended就加1,变量uxSchedulerSuspended的值不为0,将会导致Systick无法触发PendSV中断,即挂起调度器。
恢复任务调度器:
7、列表和列表项
比如:就绪列表,列表项就是各个任务。
列表项:列表中用于存放数据的地方,在list.h文件中,
列表初始化的时候,*pxPrevious指向xListEnd,*pxNext指向xListEnd。
7.2、列表相关API函数介绍(掌握)
vListInitialise();//初始化列表
vListInitialiseItem();//初始化列表项
vListInserEnd();//列表末尾插入列表项
vListInsert();//列表插入列表项
uxListRemove();//列表移除列表项
参考文档:《FreeRTOS开发指南》第七章–“FreeRTOS列表和列表项”。
列表项的插入:将列表项插入列表中,按照升序的方式。
函数vListInsertEnd();是将待插入的列表项插入到列表pxIndex指针指向的列表项前面;无序的插入方法!
7.3、列表项的插入和删除实验
设计三个任务:
start_task:用来创建其他的2个任务;
task1:LED0每500ms闪烁一次;
task2:调用列表和列表项相关API函数,并且通过串口输出相应的信息。
编写任务task2:
初始化列表,列表、列表项的函数在list.c和list.h中,定义的是结构体,因此需要定义一个结构体变量:
List_t TestList;//定义测试列表
ListItem_t ListItem1;//定义测试列表项1
ListItem_t ListItem2;
ListItem_t ListItem3;
在task2中初始化列表和列表项:
vListInitialiseItem(&ListItem1);这个函数里面是NULL,因此要给出一个初始值。
打印列表和列表项的地址。指向的都是地址,
void task2(void * pvParameters)
{
vListInitialise(&TestList);//初始化列表
vListInitialiseItem(&ListItem1);//初始化列表项
vListInitialiseItem(&ListItem2);//初始化列表项
vListInitialiseItem(&ListItem3);//初始化列表项
ListItem1.xItemValue = 40;
ListItem2.xItemValue = 60;
ListItem2.xItemValue = 50;
//打印列表和列表项的地址
printf("TestList\t\t\t0x%p\t\r\n", &TestList);
printf("TestList->pxIndex\t0x%p\t\r\n",TestList.pxIndex);//打印列表项所指向的地址,列表项的末尾插入与这个所指向的有关系
printf("TestList->xListEnd\t0x%p\t\r\n",(&TestList.xListEnd));
printf("ListItem1\t\t\t0x%p\t\r\n", &ListItem1);
printf("ListItem2\t\t\t0x%p\t\r\n", &ListItem2);
printf("ListItem3\t\t\t0x%p\t\r\n", &ListItem3);
//插入列表项1
vListInsert((List_t* )&TestList,)
(ListItem_t*)&ListItem1);
printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));
printf("ListItem1->pxNext\t0x%p\r\n", (ListItem1.pxNext));
printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));
printf("ListItem1->pxPrevious\t0x%p\r\n", (ListItem1.pxPrevious));
while(1)
{
vTaskDelay(1000);
}
}
插入列表项1=40的结果是:
TestList->xListEnd->pxNext=ListItem1;
ListItem1->pxNext=TestList->xListEnd;列表项1的下一个就是末尾列表项
TestList->xListEnd->pxPrevious=ListItem1;末尾列表项的上一个,指向列表项1
ListItem1->pxPrevious=ListItem1;列表项的上一个就是默认列表项
就绪列表、阻塞列表、挂起列表…
当任务由就绪态转成为阻塞态时,就绪列表就删除了一个任务,这个任务插入阻塞列表中,因此就用到了列表的插入与移除。
8、任务调度
滴答定时器的重装载的值为180M/1000=180000,根据Cort-M内核手册中的SysTick控制及状态寄存器的说明,当180000减到0的时候,就会触发滴答定时器中断,滴答定时器的时钟源有2种,外部时钟源180M/8,内核时钟源180M,这里使用的是180M,因此计数一次花费的时间为1/180M,那么1/180*180000(计数次数)=1/1000s=1ms
滴答定时器的中断是1ms1次。
8.1、启动第一个任务
prvStartFirstTask();//开启第一个任务
vPortSVCHandler();//SVC中断服务函数
将任务A的寄存器值加载到CPU寄存器,CPU就会执行任务A。
创建任务A的时候,我们把任务A的寄存器值,放在任务堆栈里面!
此时CPU寄存器就会执行任务A。
1、当中断产生的时候,硬件自动将xPSR,PC(R15),LR(R14),R12,R3-R0保存和恢复;R4-R11需要手动保存和恢复;
出中断,从中断里面会直接恢复到CPU寄存器;
2、进入中断后,硬件会强制使用MSP指针,此时LR(R14)的值将会被自动更新为特殊的EXC_RETURN。
通过地址0xE000ED08找到向量表VTOR储存的地址,通过向量表地址,找到第一个元素,这个元素r0就是MSP的初始值。上电的时候默认MSP就是堆栈指针,经过入栈出栈,msp就已经不是当时的msp了,把r0重新赋值给msp,这样msp就回到了原点初始值。
下面使能全局中断,调用SVC启动第一个任务。SVC_Handler
出栈(恢复现场):将内存里面的值出栈到CPU。出栈的方向:低地址往高地址。
压栈:将CPU的东西保存到内存里。
8.2、任务切换
本质是:CPU寄存器的切换。
假设由任务A切换到任务B时,主要分2种:
(1)暂停任务A的执行,并将此时任务A的寄存器保存到任务堆栈,这个过程叫做保存现场。任务A的寄存器就是CPU的寄存器,因为此时任务A在运行。
(2)将任务B的各个寄存器值(被存放于任务堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场;
对于任务A保存现场,任务B恢复现场,这个整个过程称之为:上下文切换。
PendSV中断是如何触发的?
(1)滴答定时器中断调用;
(2)执行FreeRTOS提供的相关API函数:portYIELD();
本质:通过向中断控制器和状态寄存器ICSR(地址:0xE000_ED04)的bit28写入1挂起PendSV来启动PendSV中断。
查找最高优先级:
vTaskSwitchContext();//查找最高优先级
taskSELECT_HIGHEST_PRIORITY_TASK();//通过这个函数完成
获取最高优先级任务的任务控制块:
listGET_OWNER_OF_NEXT_ENTRY();
任务运行是在中断以外,使用PSP指针寄存器。无论是压栈还是出栈,用的都是PSP指针。
9、时间片调度
9.1、简介:
给出一个时间片,只给出一个时间片去运行Task1,这个时间片过了之后,就开始运行Task2,同样的,也给Task2时间片运行,Task2没有运行完,也会切换到下个时间片运行Task3,Task1/2/3是同等优先级的,只给一个时间片的时间去执行,不管有没有执行完,下次再回到这个任务的时候,就从上次打断的时间点继续运行下去。
假如运行到Task3,这个时候一个时间片还没有运行完,就到了阻塞时间,此时放弃CPU的使用权,这个时候就直接切换到Task1了,此时Task3执行的不到一个时间片的时间,下次再回到Task3怎么办?直接给一个时间片运行。
9.2、实验:
将设计三个任务:start_task、task1、task2,其中task1和task2优先级相同均为2。为了使现象明显,将滴答定时器的中断频率设置为50ms中断一次,即一个时间片50ms。
start_task:用来创建其他的2个任务;
task1:通过串口打印task1的运行次数;
task2:通过串口打印task2的运行次数;
使用时间片调度需把宏 configUSE_TIME_SLICING 和 configUSE_PREEMPTION 置1
在FreeRTOSConfig.h中,#define configSYSTICK_CLOCK_HZ 1000定义系统节拍是1000,是1ms,现在是50ms,所以1000/50=20,
滴答定时器是50ms中断一次,时间片是50ms一次。
void task1(void* pvParameters)
{
uint32_t task_num = 0;
while(1)
{
printf("task1运行次数:\r\n", ++task1_num);
~~vTaskDelay(10);~~
delay_ms(10);
}
}
调用这个函数vTaskDelay(10);会引起任务调度的,会把当前运行的任务挂到阻塞列表,进入阻塞后,就立马进行下一个任务了,这样的话,就观察不到是50ms了,因此要用delay_ms(10)。一个时间片是50ms
为了防止乱码,可在打印的前后加上临界区:
taskENTER_CRITICAL();
taskEXIT_CRITICAL();