2023/12/25重启韦东山老师RTOS
韦东山老师开源万岁!!!
累了困了看哥哥给大家跳舞助兴^_(_^
韦东山freeRTOS快速入门视频教程学习笔记
目录
1. 任务状态及实验对应程序: 08_freertos_example_task_status
6-3_VTaskDelay和vTaskDelayUntil
2.xTaskNotifyGive/ulTaskNotifyTake
1单片机_RTOS_架构的概念
2-1堆的概念
可参考,配套理解 ①②③④⑤⑥⑦⑧⑨⑩ ■ ●
堆:
char heap_buf[1024];
int pos = 0;
void *my_malloc(int size)
{
int old_pos = pos;
pos += size;
return &heap_buf[old_pos];
}
void my_free(void *buf)
{
/* err */
}
int main(void)
{
char ch = 65; // char ch = 'A';
char *buf = my_malloc(100);
unsigned char uch = 200;
for (i = 0; i < 26; i++)
buf[i] = 'A' + i;
}
这里void *my_malloc(int size)是一个指针函数,返回的是一个地址。
这段代码在调试器中调试后,经过几次循环buf[i]的值会有改变,底下附图。
疑惑及学习
1、2、 断点,按下3程序运行到1就会暂停,按下4,程序往下执行一行,进入for循环多点几次4,查看buf的值。
问
gpt解答
问
后面那个问题不回答了。 gpt答解微瑕,自己来: 参考CSDN这个例
typedef struct _Data{
int a;
int b;
}Data;
//指针函数
Data* f(int a,int b){
Data * data = new Data;
data->a = a;
data->b = b;
return data;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//调用指针函数
Data * myData = f(4,5);
qDebug() << "f(4,5) = " << myData->a << myData->b;
return a.exec();
}
输出如下:
f(4,5) = 4 5
Data * myData = f(4,5); 这行代码将f函数里面赋值4和5,然后将其地址返回给myData,所以myDatat,就相当于是f函数(自己的理解,因为myData这个函数的地址就是这个,),之后输出打印当中相当于myData这个地址里边有a和b两个变量,然后他们的值已经在f函数中给过了。
再看上面韦东山老师代码,buf就相当于my_malloc这个函数,buf存的这个地址拥有my_malloc这个函数里边的东西,注意看my_malloc这个函数,它最终返回了一个数组的地址,所以为什么,gpt当中说buf是指向字符数组的针。 要在英文的路径下安放文件,不然调试器关闭的时候出问题。或者取消断点。^--^
2-2栈的概念
栈
char heap_buf[1024];
int pos = 0;
void *my_malloc(int size)
{
int old_pos = pos;
pos += size;
return &heap_buf[old_pos];
}
void my_free(void *buf)
{
/* err */
}
void c_fun(void)
{
}
void b_fun(void)
{
}
int a_fun(int val)
{
int a = 8;
a += val;
b_fun();
c_fun();
return a;
}
int main(void)
{
char ch = 65; // char ch = 'A';
int i;
char *buf = my_malloc(100);
unsigned char uch = 200;
for (i = 0; i < 26; i++)
buf[i] = 'A' + i;
a_fun(46);
return 0;
}
a 函数返回给return 0,返回地址保存在哪里?
LR (Link registev),暂且把它看作一个寄存器/ 看下面的流程
使用栈的过程
C函数开头:
1、划分栈
2、LR…局部变量存入栈中
3、执行代码(比如 a=8; 在划分的栈里面把8写进去)
返回的地址就是下一句代码的位置
BL main会做两个事情
第一,将返回地址存在LR中
第二,执行main函数
main函数会划分出自己的栈,假设他划分出SP=sp-n,这里面会保存lr等寄存器,会保存局部变量。
第二步,执行a函数,第一 lr=a的返回地址,第二执行函数a,函数a执行的事情和main类似,划分出SP=sp-m的栈,在这一块空间里面会放lr的值以及a的局部变量
第二步,调用到函数b,操作同上两步差不多。在执行b的时候,他会将返回值付给lr,也就是c(下一句代码的位置),然后执行b函数 ,将lr的值放到保存划出来栈当中,这里的lr相当于是保存一个值,然后将值再放入别的函数画出来的栈当中,然后这个lr就可以被别的函数地址覆盖掉了,因为前面那一个函数的地址已经被保存到自己的栈里面了,
当b函数执行完之后,注意是执行完之后,他就会返回到他原本自己存的lr地址的位置,也就是下一句代码,就开始执行c函数了
然后当函数a执行到return a的时候,他已经完成了他的工作,他要返回去,他去自己的栈里把自己的lr当时存的值取出来,跳过去执行这个值就是return 0,也就是main中a函数下面的return 0
从这个过程中,我们可以看到,栈在保存返回地址的时候起的作用,(应该是l2那个寄存器反复被利用,然后存到各自相应的栈中)
划分出来的地址是下一行代码的位置
相当于lr保存的是下一条语句的地址
HAL库_栈的估算(学习RTOS_32HAL库开发理解笔记)
栈 (通过以下三点,你大概可以估算出你这个程序用的栈大小)
①返回地址LR,其他Reg (并不是深度越深,用到的占最多,可能在前面定义一个巨大的buffer 1000,所以得去看你的代 码,找到局部变量最多的函数。)
②局部变量 char buf[1000]
③现场 16*4 = 64 byte
例如:
3-1从官方源码精简出第1个FreeRTO
源码从官网下载
删掉
tools也删掉
D:\RTOS_study\FreeRTOSv202107.00\FreeRTOS\Demo
Demo中只保留下面这个和Comm…文件
还有一部分不需要不做展示,按照如下目录选择
下载的文件是keil4编写的,我们更改成5
更新后关闭重新代打开、
3-2修改官方源码增加串口打印
复制上一个代码,打开软件阅读源码,软件下载方式csdn搜索。
复制路径
所有文件都添加进去
添加串口打印功能
打开keil工程,删除无用的文件——
lcd, ParTest,TimerTest,spi_flashm,semest,blockQ,blockdim,comtest,deaath,flashinteger,pollQ
解决精简程序后的报错。
!!!!!!!!!!!!跟不上老师!!!!!!直接用老师精简后的代码!!!!!
就这几个文件加一个汇编,是很好学的。。。。。
4-1自己的第1个FreeRTOS程序
1什么是任务
在FreeRTOS中,任务就是一个函数,原型如下:
void ATaskFunction( void *pvParameters );
要注意的是:
● 这个函数不能返回
● 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
● 函数内部,尽量使用局部变量:
୦ 每个任务都有自己的栈
୦ 每个任务运行这个函数时
■ 任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里
■ 不同任务的局部变量,有自己的副本
୦ 函数使用全局变量、静态变量的话
■ 只有一个副本:多个任务使用的是同一个副本
■ 要防止冲突(后续会讲)
2创建任务
创建任务时使用的函数如下:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位
为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用
它来操作这个任务
参数说明:
1.自己的第1个FreeRTOS程序
/*-----------------------------------------------------------*/
void Task1Function (void * param)
{
while(1)
{
printf("1");
}
}
void Task2Function (void * param)
{
while(1)
{
printf("2");
}
}
/*-----------------------------------------------------------*/
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task1Function,"Tsak1",100,NULL,1,&xHandleTask1);
xTaskCreate(Task2Function,"Tsak2",100,NULL,1,NULL);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
/*-----------------------------------------------------------*/
两个任务,2和1交替打印,为什么不是打印一个一,再打印一个二呢?
因为每个时间片执行一个任务,而不是一个任务执行完再执行另一个任务。哦,我们根据连续打印1或者连续打印2的个数,可以大致计算出每个任务所占据的调度时间。观察两个函数,他们分别有两个循环,如果放在逻辑系统中,在一个will死循环里面,就不会跳出来,但是加上RTOS的,就会连续的切换任务,在我们人类感觉就是在同时进行。
HAL库_创建任务(学习RTOS_32HAL库开发理解笔记)
创建任务
我们创建2个任务,使用同一个函数,但是在LCD上打印不一样的信息。
提问:如何互斥地访问LCD? 使用全局变量,大概率可以,但是不是万无一失。
解答:添加一个delay(500)
提问:为何是后面创建的task3先运行?
解答:后面再说
22课12:48有解释
HAL库_任务的删除(学习RTOS_32HAL库开发理解笔记)任
删除任务时使用的函数如下:
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数说明:
● 自杀:vTaskDelete(NULL) ● 被杀:别的任务执行vTaskDelete(pvTaskCode),pvTaskCode是自己的句柄 ● 杀人:执行vTaskDelete(pvTaskCode),pvTaskCode是别的任务的句柄
示例3: 删除任务
功能为:当监测到遥控器的Power按键被按下后,删除音乐播放任务。
代码如下:
还有蜂鸣器在响
提问:频繁地创建、删除任务,好吗?有什么坏处?如何解决?
解答:频繁的创建和删除会造成内存的碎片化,多次执行后可能分配不到内存了。
HAL库_任务优先级(学习RTOS_32HAL库开发理解笔记)
HAL库_任务状态讲解(学习RTOS_32HAL库开发理解笔记)
■一个任务被创建出来之后,就必定处于ready状态。
他有机会能运行的时候就处于runing状态。
它被切换出去之后,又回到ready状态。
■代码中的音乐任务在执行到vTask Delay的时候,进入到block状态,等待某些event。也就是图中 的blocking API funcation called。当时间到了之后,它又会被唤醒,又会进入ready状态。
■进入暂停状态,只有一种方法,要么自己调用vTaskSuspend函数,把自己从running状态放入 Suspended暂停状态或者说我现在正处于ready状态和或者我正处于block状态,等别的任务来执 行vTaskSuspend函数,指定说要我把你放入暂停状态。
代码太长了截屏不下,贴代码
while (1)
{
/* 读取红外遥控器 */
if (0 == IRReceiver_Read(&dev, &data))
{
if (data == 0xa8) /* play */
{
/* 创建播放音乐的任务 */
extern void PlayMusic(void *params);
if (xSoundTaskHandle == NULL)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Create Task");
ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal+1, &xSoundTaskHandle);
bRunning = 1;
}
else
{
/* 要么suspend要么resume */
if (bRunning)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Suspend Task");
vTaskSuspend(xSoundTaskHandle);
PassiveBuzzer_Control(0); /* 停止蜂鸣器 */
bRunning = 0;
}
else
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Resume Task");
vTaskResume(xSoundTaskHandle);
bRunning = 1;
}
}
}
else if (data == 0xa2) /* power */
{
/* 删除播放音乐的任务 */
if (xSoundTaskHandle != NULL)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Delete Task");
vTaskDelete(xSoundTaskHandle);
PassiveBuzzer_Control(0); /* 停止蜂鸣器 */
xSoundTaskHandle = NULL;
}
}
}
}
HAL库_1任务优先级和2调度理论讲解(学习RTOS_32HAL库开发理解笔记)
● 调度: ①相同优先级的任务轮流运行 ②最高优先级任务优先执行
■高优先级的任务未执行完,低优先级的任务无法运行。
■一旦高优先级任务就绪,马上运行。
■最高优先级的任务有多个则轮流交叉执行。
!!!知道这三个就可以解决所有的任务调度问题!!!
讲的有点内部机制有点深入:
一共有56链表,示例代码中创建的任务的优先级是24,有一个pxCurrenTCB指针。开始创建①默认任务,然后是②LEDtest任务,最后是③颜色LED任务,最后创建了一个空闲任务就是④,但是空闲任务优先级是零,所以指针就指向第③个颜色LED任务了,所以也就可以解决之前的问题,为什么是从第三个任务开始执行?因为指针指向了这里。
调度的过程是从上到下遍历这些Readylist,发现列表是空的,就找下一个,还是空的那就继续找下一个,当找到24的时候,发现你不空,发现不空之后,让px指针指向下一个任务,也就是第一个任务,然后启动它。再发生一个Tick中断的时候,就会重复这个调度过程,然后执行任务。
………………………………………………还有,看视频再总结一下,(看视频!!)
HAL库_空闲任务(学习RTOS_32HAL库开发理解笔记)
如果一个任务不是well死循环的话,而是一个比如for循环十次,然后结束任务,这时候退出任务会发生什么事情呢?
会直接进入到如图所示,它会关闭所有中断,然后陷入一个for无限死循环,然后整个系统会卡死。解决方法加个自杀VtaskDelete()
任务退出,有自杀和他杀两种,通过传入句柄就可以,自杀的话传入NULL。A杀B,A给B收尸,B自杀空闲任务收尸。收哪些东西呢 创建任务时的tcb结构体以及栈。
但是空闲任务我们知道它的优先级是最低的,如果其余的任务一直保持就绪状态,就无法执行这个空闲任务,那怎么解决呢? 这需要我们有良好的编程习惯。①事件驱动②延时时不要使用死循环,比如用VTaskDelay()。
1空闲任务
空闲任务(Idle任务)的作用之一:释放被删除的任务的内存。
除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务:
- 空闲任务优先级为0:它不能阻碍用户任务运行
- 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
空闲任务的优先级为0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。
要注意的是:如果使用vTaskDelete()来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。
1.1空闲任务的钩子函数
我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:
- 执行一些低优先级的、后台的、需要连续执行的函数
- 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
- 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。
- 空闲任务的钩子函数的限制:
- 不能导致空闲任务进入阻塞状态、暂停状态
- 如果你会使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。
1.1.1使用钩子函数的前提
在FreeRTOS\Source\tasks.c中,可以看到如下代码,所以前提就是:
- 把这个宏定义为1:configUSE_IDLE_HOOK
- 实现vApplicationIdleHook函数
4-2_源码结构与编程规范
!!!!!!!这节课很重要!!!!!!!
1.数据类型和编程规范
2.变量名
变量名有前缀:
3.函数名
函数名的前缀有2部分:返回值类型、在哪个文件定义
4.宏的名
宏的名字是大小,可以添加小写的前缀。前缀是用来表示:宏在哪个文件中定义。
5.举例
5-1_动态_静态创建任务
添加学生的信息,像这样子每增加一种信息就得添加一类。
我们要学会面向对象的编程
用一个结构体把上述信息包含完之后再定义一个学生的指针(next)用链表管理起来
void Task3Function (void * param)
{
while(1)
{
printf("3");
}
}
/*-----------------------------------------------------------*/
StackType_t xTask3Stack[100];
StaticTask_t xTask3TCB;
StackType_t xITdleTaskStack[100];
StaticTask_t xIdleTaskTCB;
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
StackType_t ** ppxIdleTaskStackBuffer,
uint32_t * pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
*ppxIdleTaskStackBuffer = xITdleTaskStack;
*pulIdleTaskStackSize = 100;
}
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task1Function,"Tsak1",100,NULL,1,&xHandleTask1);
xTaskCreate(Task2Function,"Tsak2",100,NULL,1,NULL);
xTaskCreateStatic(Task3Function,"Tsak3",100,NULL,1,xTask3Stack,&xTask3TCB);
1.chatgpt的代码解释
这段代码看起来是使用 FreeRTOS 创建了一个静态任务 Task3。让我逐行解释一下这段代码的功能:
void Task3Function(void *param) { while(1) { printf("3"); } }
这是一个函数
Task3Function
,它是任务 Task3 的执行函数。它是一个无限循环,不断地向输出流打印字符 "3"。
StackType_t xTask3Stack[100];
这行代码定义了一个名为
xTask3Stack
的数组,用于存储 Task3 任务的栈空间,大小为 100 个StackType_t
类型的元素。
StaticTask_t xTask3TCB;
这行代码定义了一个名为
xTask3TCB
的静态任务控制块(TCB),用于存储 Task3 任务的任务控制信息。
StackType_t xITdleTaskStack[100];
这行代码定义了一个名为
xIdleTaskStack
的数组,用于存储空闲任务的栈空间,大小也为 100 个StackType_t
类型的元素。
StaticTask_t xIdleTaskTCB;
这行代码定义了一个名为
xIdleTaskTCB
的静态任务控制块(TCB),用于存储空闲任务的任务控制信息。
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize) { ... }
这是 FreeRTOS 的一个回调函数
vApplicationGetIdleTaskMemory
,用于配置空闲任务(Idle Task)的内存。在这个函数中,空闲任务的任务控制块地址被赋值给ppxIdleTaskTCBBuffer
,空闲任务的栈地址被赋值给ppxIdleTaskStackBuffer
,空闲任务的栈大小被赋值给pulIdleTaskStackSize
。
xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB);
这行代码创建了一个静态任务 Task3。具体参数解释如下:
Task3Function
: 任务函数指针,指向 Task3Function 函数。"Task3"
: 任务的名称,用于标识任务。100
: 任务栈的大小,这里为 100 个StackType_t
元素。NULL
: 任务参数,这里没有传递参数给任务函数。1
: 任务的优先级,数值越高,优先级越高。xTask3Stack
: 任务栈的起始地址。&xTask3TCB
: 任务控制块的地址。综上所述,这段代码的主要功能是创建了一个名为 Task3 的静态任务,并配置了空闲任务的内存。
5-2_进一步实验
1.优先级实验
2.删除任务
static int task1flagrun = 0;
static int task2flagrun = 0;
static int task3flagrun = 0;
TaskHandle_t xHandleTask1;
void Task1Function (void * param)
{
while(1)
{
task1flagrun = 1;
task2flagrun = 0;
task3flagrun = 0;
printf("1");
}
}
void Task2Function (void * param)
{
int i = 0;
while(1)
{
task1flagrun = 0;
task2flagrun = 1;
task3flagrun = 0;
printf("2");
if(i++ == 100)
{
vTaskDelete(xHandleTask1);
}
if(i == 200)
{
vTaskDelete(NULL);
}
}
}
vTaskDelete(xHandleTask1); 和
vTaskDelete(NULl);
一个自杀,一个删除别人
使用同一个任务函数创建多个任务
为什么同一个函数可以创建不同的任务,他们的效果不一样,因为不同的任务,他们的——栈——是不一样的。引用不同的任务传入不同的参数param,局部变量是保存在栈里边的每一个任务都有不同的栈,他们的运行互不影响。
void TaskGenericFunction (void * param)
{
int val = (int)param;
while(1)
{
printf("%d",param);
}
}
xTaskCreate(TaskGenericFunction,"Tsak4",100,(void *)4,1,NULL);
xTaskCreate(TaskGenericFunction,"Tsak5",100,(void *)5,1,NULL);
3.栈大小实验
void Task1Function (void * param)
{
volatile char buf[500];
int i ;
while(1)
{
task1flagrun = 1;
task2flagrun = 0;
task3flagrun = 0;
printf("1");
for( i = 0; i < 500; i++)
buf[i] = 0;
}
}
程序崩掉了
6-1_任务状态理论讲解
1. 任务状态及实验对应程序: 08_freertos_example_task_status
任务切换的基础:tick中断
每隔一毫秒产生中断,从零开始中断一次,累加一,这个计数值被称为tick count。
一开始任务3先运行,然后发生一次中断。中断的时候,Tick Isr中断函数会调用判断是否需要切换任务。每个任务运行的最基本时间是一毫秒(从逻辑分析一中看出来,700多us接近一毫秒)。我们可以配置定时器产生中断的周期。下图
我们可以规定每个任务执行多少个tick。
有哪些任务状态? 状态切换图
2,官方文档解释(图片下面有解释)
1.阻塞状态
2.暂停状态
3.就绪状态
3.完整的状态转换图
下面
看视频老师讲解,自己理解(建议看这个)
1正在执行的任务处于running状态。
2还未执行的任务处于ready状态。
3(母亲给孩子喂饭,但是孩子嘴里有饭,不能强行给他喂)这个任务就处于blocked阻塞状态,阻塞就是在等待某事。
4他正在跟同事工作,突然领导给他说你休息一下吧,这个是suspended暂停状态(他并不是在等待某个事情发生,他是主动或者被动了被命令去休息).
在freertos当中,一个任务他就只有这四种状态之一。
要弄清阻塞状态和暂停状态,阻塞状态就是在等待某个事情,而暂停状态是主动或被动的休息。(怎样结束他的暂停状态呢?必须由正在running的任务启用vtiskresume函数传入它的handle,让他从暂停状态中回到ready状态)
(怎么从block的阻塞状态变为ready状态?比如按键中断,让它发生这个事件或者其他任务结束了,告诉他你可以运行了)
这些任务是怎么被管理起来的呢?最简单的方法就是用链表。
在发生tick中断的时候,他会起ready list链表里边寻找下一个运行的任务,比如上面的图在t1t2t3t4的tick中断的时候,他会找到相应的3123任务
6-2_任务状态实验
比如我们要在tick1+10的时候进行suspend task3,我们就需要得到tick的值,怎么得到这个值呢?在tick.c里边,有一个获得基数值的函数xtask gettickcount。
vTaskSuspend(xHandleTask3);
vTaskResume(xHandleTask3);
vTaskDelay(10);
三个函数注意//有了xHandleTask3 这个句柄别人就可以控制他了
TaskHandle_t xHandleTask3;
void Task1Function (void * param)
{
TickType_t tStart = xTaskGetTickCount();
TickType_t t;
int flag = 0;
while(1)
{
t = xTaskGetTickCount();
task1flagrun = 1;
task2flagrun = 0;
task3flagrun = 0;
printf("1");
if(!flag && (t > tStart + 10))
{
vTaskSuspend(xHandleTask3);
flag = 1;
}
if(t > tStart + 20)
{
vTaskResume(xHandleTask3);
}
}
}
void Task2Function (void * param)
{
int i = 0;
while(1)
{
task1flagrun = 0;
task2flagrun = 1;
task3flagrun = 0;
printf("2");
vTaskDelay(10);
}
}
xHandleTask3 = xTaskCreateStatic(Task3Function,"Tsak3",100,NULL,1,xTask3Stack,&xTask3TCB);
//有了xHandleTask3 这个句柄别人就可以控制他了
6-3_VTaskDelay和vTaskDelayUntil
同优先级因为是交错执行且每个任务执行一个中断时间,若有一任务执行时间过长则会经过多个中断时间执行完
delay就是让任务执行完延迟20个tick,delayuntil就是每隔20个tick都会执行一次(周期执行)
看底下的逻辑分析仪,每两个上升沿之间的时间相同。
static int rands[] = {3,56,23,5,99};
void Task1Function (void * param)
{
TickType_t tStart = xTaskGetTickCount();
int i = 0;
int j = 0;
int flag = 0;
while(1)
{
task1flagrun = 1;
task2flagrun = 0;
task3flagrun = 0;
for(i = 0; i < rands[j]; i++)
printf("1");
j++;
if(j == 5)
j=0;
#if 0
vTaskDelay(20);
#else
vTaskDelayUntil(&tStart, 20);
#endif
}
}
HAL库_两个Delay函数(学习RTOS_32HAL库开发理解笔记)
有两个Delay函数:
- vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
/* xTicksToDelay: 等待多少给Tick */
void vTaskDelay( const TickType_t xTicksToDelay );
/* pxPreviousWakeTime: 上一次被唤醒的时间
* xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
* 单位都是Tick Count
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement );
下面画图说明:
- 使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断
- 使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断
- 退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会
- 所以可以使用xTaskDelayUntil来让任务周期性地运行
代码示例
本节代码为:11_taskdelay。 本程序会比较vTaskDelay和vTaskDelayUntil实际阻塞的时间,并在LCD上打印出来。
void LcdPrintTask(void *params)
{
struct TaskPrintInfo *pInfo = params;
uint32_t cnt = 0;
int len;
BaseType_t preTime;
uint64_t t1, t2;
preTime = xTaskGetTickCount();
while (1)
{
/* 打印信息 */
if (g_LCDCanUse)
{
g_LCDCanUse = 0;
len = LCD_PrintString(pInfo->x, pInfo->y, pInfo->name);
len += LCD_PrintString(len, pInfo->y, ":");
LCD_PrintSignedVal(len, pInfo->y, cnt++);
g_LCDCanUse = 1;
mdelay(cnt & 0x3);//随机延时
}
t1 = system_get_ns();
//vTaskDelay(500); // 500000000
vTaskDelayUntil(&preTime, 500);
t2 = system_get_ns();
LCD_ClearLine(pInfo->x, pInfo->y+2);
LCD_PrintSignedVal(pInfo->x, pInfo->y+2, t2-t1);
}
}
效果
1:使用vTaskDelay(500);
2:使用vTaskDelayUntil(&preTime, 500);
因为延时随机,所以T1-T2不一样。
6-4_空闲任务及其钩子函数
1任务的删除
删除任务时使用的函数如下:
void vTaskDelete( TaskHandle_t xTaskToDelete );
怎么删除任务?举个不好的例子:
自杀: vTaskDelete(NULL)
被杀:别的任务执行 vTaskDelete(pvTaskCode) ,pvTaskCode是自己的句柄
杀人:执行 vTaskDelete(pvTaskCode) ,pvTaskCode是别的任务的句柄
2空闲任务及其钩子函数
介绍
在 FreeRTOS_03_delete_task 的实验里,我们体验过空闲任务(Idle任务)的作用:释放被删除的任务的内存。
除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部 分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运 行的任务:所以,我们要提供空闲任务。用vTaskStartScheduler() 函数来创建、启动调度器 时,这个函数内部会创建空闲任务:
1空闲任务优先级为0:它不能阻碍用户任务运行
2空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
空闲任务的优先级为0,这以为着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。
要注意的是:如果使用 vTaskDelete() 来删除任务,那么你就要确保空闲任务有机会执行,否则就无 法释放被删除任务的内存。
我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环没执行一次,就会 调用一次钩子函数。钩子函数的作用有这些:
执行一些低优先级的、后台的、需要连续执行的函数
1测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量
2空闲任 务占据的时间,就可以算出处理器占用率。
3让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式 了。
空闲任务的钩子函数的限制:
1 不能导致空闲任务进入阻塞状态、暂停状态
2如果你会使用 vTaskDelete() 来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植 卡在钩子函数里的话,它就无法释放内存。
代码注意,首先要定义这个宏,让他为1,下面有对这个宏的解释
定义那个红,并且将红的值变为一,那你就一定要有配套的这个勾子函数,就算你将这个函数写为空函数,你也要有,不然会报错。
在 FreeRTOS 的配置文件(通常是 FreeRTOSConfig.h)中,
configUSE_IDLE_HOOK
是一个用于配置是否使用空闲任务钩子函数的宏定义。当configUSE_IDLE_HOOK
被定义为 1 时,意味着您打开了空闲任务钩子函数的使用。如果您将
configUSE_IDLE_HOOK
宏定义为 1,FreeRTOS 将会期望您提供一个叫做vApplicationIdleHook
的函数来作为空闲任务的钩子函数。这个函数会在系统空闲时被调用,允许您执行一些特定的任务或操作。因此,当您将
configUSE_IDLE_HOOK
宏定义为 1 后,链接器就会期待找到vApplicationIdleHook
函数的定义。如果没有定义这个函数,或者定义不正确,链接器就会报错。通过将
configUSE_IDLE_HOOK
宏定义为 1,您告诉 FreeRTOS 您要使用空闲任务钩子函数,并且链接器就会期待找到相应的符号。这解释了为什么修改了configUSE_IDLE_HOOK
后链接错误得到了解决。
在main中,任务1的优先级要设置为0,因为在任务2中将会执行任务2的自杀,这时候将会有一个空闲函数过来清理任务2的内存这个空闲函数的优先级是零如果你将任务的优先级设置为一那么他将无法执行这个空闲函数将将导致任务2的内存无法清理将后结果就是逻辑分析仪中你会看到只有三四个波形之后就任务卡死了,然后在串口上面会发现打印出任务1当中的内容。
这个钩子函数的作用底下有gpt的解释
3空闲任务的钩子函数与任务删除
空闲任务可以通过钩子函数进行定制。其中一个钩子函数是
vApplicationIdleHook
,它允许用户在空闲任务执行时执行特定的操作。这包括检查任务状态、清理资源等。但是,钩子函数与任务删除之间并没有直接的关系。4空闲任务钩子函数的意义和用途
空闲任务钩子函数的主要作用是在系统空闲时执行一些特定的操作。这可以包括系统性能分析、资源检查、低功耗模式的启用等。钩子函数允许您在系统空闲时执行自定义的代码,以满足特定的需求和要求。
总结来说,空闲任务的优先级和钩子函数在任务删除时起到一定的作用,但与任务删除的关系不是直接的因果关系。空闲任务的优先级和钩子函数的主要目的是在系统空闲时执行额外的操作,而任务删除是 FreeRTOS 内核的一部分,通过调用
vTaskDelete()
函数来实现。
1代码
#define configUSE_IDLE_HOOK 1
void Task1Function (void * param)
{
TaskHandle_t xHandleTask2;
BaseType_t xReturn;
while(1)
{
task1flagrun = 1;
task2flagrun = 0;
taskidleflagrun = 0;
printf("1");
xReturn = xTaskCreate(Task2Function,"Tsak2",1024,NULL,2,&xHandleTask2);
if(xReturn != pdPASS)
printf("xTaskCreat err\r\n");
//vTaskDelete(xHandleTask2);
}
}
void Task2Function (void * param)
{
while(1)
{
task1flagrun = 0;
task2flagrun = 1;
taskidleflagrun = 0;
printf("2");
//vTaskDelay(2);
vTaskDelete(NULL);
}
}
void vApplicationIdleHook(void)
{
task1flagrun = 0;
task2flagrun = 0;
taskidleflagrun = 1;
printf("0");
}
/*-----------------------------------------------------------*/
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task1Function,"Tsak1",100,NULL,1,&xHandleTask1);
/* Start the scheduler. */
vTaskStartScheduler();
6-5_任务调度算法
重要概念
正在运行的任务,被称为"正在使用处理器",它处于运行状态。在单处理系统中,任何时间里只能有一 个任务处于运行状态。
非运行状态的任务,它处于这3中状态之一:阻塞(Blocked)、暂停(Suspended)、就绪(Ready)。
就绪态的任务,可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态任务并让它进入运行状态。
阻塞状态的任务,它在等待"事件",当事件发生时任务就会进入就绪状态。事件分为两类:时间相关的 事件、同步事件。所谓时间相关的事件,就是设置超时时间:在指定时间内阻塞,时间到了就进入就绪 状态。使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。同步事件就是:某个任务在 等待某些信息,别的任务或者中断服务程序会给它发送信息。怎么"发送信息"?方法很多,有:任务通 知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)等。 这些方法用来发送同步信息,比如表示某个外设得到了数据。
配置调度算法
所谓调度算法,就是怎么确定哪个就绪态的任务可以切换为运行状态。
通过配置文件FreeRTOSConfig.h的两个配置项来配置调度算法:configUSE_PREEMPTION、 configUSE_TIME_SLICING。
还有第三个配置项:configUSE_TICKLESS_IDLE,它是一个高级选项,用于关闭Tick中断来实现省电, 后续单独讲解。现在我们假设configUSE_TICKLESS_IDLE被设为0,先不使用这个功能。
调度算法的行为主要体现在两方面:高优先级的任务先运行、同优先级的就绪态任务如何被选中。调度 算法要确保同优先级的就绪态任务,能"轮流"运行,策略是"轮转调度"(Round Robin Scheduling)。轮 转调度并不保证任务的运行时间是公平分配的,我们还可以细化时间的分配方法。
---从3个角度统一理解多种调度算法:
--可否抢占?高优先级的任务能否优先执行(配置项: configUSE_PREEMPTION)
--可以:被称作"可抢占调度"(Pre-emptive),高优先级的就绪任务马上执行,下面再细化。
-当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让 出CPU资源。
-其他同优先级的任务也只能等待:更高优先级的任务都不能抢占,平级的更应该老实点
---可抢占的前提下,同优先级的任务是否轮流执行(配置项:configUSE_TIME_SLICING)
--轮流执行:被称为"时间片轮转"(Time Slicing),同优先级的任务轮流执行,你执行一个时间 片、我再执行一个时间片
--不轮流执行:英文为"without Time Slicing",当前任务会一直执行,直到主动放弃、或者被 高优先级任务抢占
----在"可抢占"+"时间片轮转"的前提下,进一步细化:空闲任务是否让步于用户任务(配置项: configIDLE_SHOULD_YIELD)
--空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务
--空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊
效果太多不贴了,老师B站快速入门之后一个视频就是展示。
7_1同步互斥与通信概述
把数据写到队列头部,会覆盖原来头部的数据吗???
这个问题也是对参考书的勘误。不会覆盖原来的数据。队列中的数据使用环形缓冲区管理数据,把数据放到头部时,会先移动头部位置,并不会覆盖原来数据。
同步互斥举例:任务1任务2是同步 任务3和4是互斥
void Task1Function(void * param)
{
volatile int i = 0;
while (1)
{
for (i = 0; i < 10000000; i++)
sum++;
//printf("1");
flagCalcEnd = 1;
vTaskDelete(NULL);
}
}
void Task2Function(void * param)
{
while (1)
{
if (flagCalcEnd)
printf("sum = %d\r\n", sum);
}
}
void TaskGenericFunction(void * param)
{
while (1)
{
if (!flagUARTused)
{
flagUARTused = 1;
printf("%s\r\n", (char *)param);
flagUARTused = 0;
vTaskDelay(1);
}
}
}
/*-----------------------------------------------------------*/
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
//xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
xTaskCreate(TaskGenericFunction, "Task3", 100, "Task 3 is running", 1, NULL);
xTaskCreate(TaskGenericFunction, "Task4", 100, "Task 4 is running", 1, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
HAL库_同步互斥与通信(学习RTOS_32HAL库开发理解笔记)
1.同步互斥的概念
一句话理解同步与互斥:我等你用完厕所,我再用厕所。
什么叫同步?就是:哎哎哎,我正在用厕所,你等会。 什么叫互斥?就是:哎哎哎,我正在用厕所,你不能进来。
同步与互斥经常放在一起讲,是因为它们之的关系很大,“互斥”操作可以使用“同步”来实现。我“等”你用完厕所,我再用厕所。这不就是用“同步”来实现“互斥”吗?
再举一个例子。在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。经理B必须等同事A完成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同事A已经使用会议室了,经理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经理B跟同事A说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。
有时候看代码更容易理解,伪代码如下:
void 抢厕所(void)
{
if (有人在用) 我眯一会;
用厕所;
喂,醒醒,有人要用厕所吗;
}
假设有A、B两人早起抢厕所,A先行一步占用了;B慢了一步,于是就眯一会;当A用完后叫醒B,B也就愉快地上厕所了。
在这个过程中,A、B是互斥地访问“厕所”,“厕所”被称之为临界资源。我们使用了“休眠-唤醒”的同步机制实现了“临界资源”的“互斥访问”。
同一时间只能有一个人使用的资源,被称为临界资源。比如任务A、B都要使用串口来打印,串口就是临界资源。如果A、B同时使用串口,那么打印出来的信息就是A、B混杂,无法分辨。所以使用串口时,应该是这样:A用完,B再用;B用完,A再用。
这里的厕所是被保护的资源,叫临界资源,同一时刻只能有一个任务来使用他。
2.同步的例子:
有缺陷
用一个全局变量同步两个任务,带来效率问题。一般的时间在执行B里面的死循环。
while (g_calc_end == 0)这个死循环,完全判断完全可以有其他方法实现,等任务A完成后唤醒B就行。让那些等待的任务进入阻塞,不让他们参与CP的调度。
3. 互斥的例子:
有缺陷
在裸机程序里,可以使用一个全局变量或静态变量实现互斥操作,比如要互斥地使用LCD,可以使用如下代码:
判断变量和设置变量被打断了,A跟B都能运行这个lcd任务了。虽然概率非常非常非常低,但是假设成千上万次的运行程序之后,会不会出现bug?(doge)
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;
if (bCanUse)
{
bCanUse = 0;
/* 使用LCD */
bCanUse = 1;
return 0;
}
return -1;
}
但是在RTOS里,使用上述代码实现互斥操作时,大概率是没问题的,但是无法确保万无一失。
假设如下场景:有两个任务A、B都想调用LCD_PrintString,任务A执行到第4行代码时发现bCanUse为1,可以进入if语句块,它还没执行第6句指令就被切换出去了;然后任务B也调用LCD_PrintString,任务B执行到第4行代码时也发现bCanUse为1,也可以进入if语句块使用LCD。在这种情况下,使用静态变量并不能实现互斥操作。
上述例子中,是因为第4、第6两条指令被打断了,那么如下改进:在函数入口处先然让bCanUse减一。这能否实现万无一失的互斥操作呢?
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;
bCanUse--;
if (bCanUse == 0)
{
/* 使用LCD */
bCanUse++;
return 0;
}
else
{
bCanUse++;
return -1;
}
}
把第4行的代码使用汇编指令表示如下:
04.1 LDR R0, [bCanUse] // 读取bCanUse的值,存入寄存器R0
04.2 DEC R0, #1 // 把R0的值减一
04.3 STR R0, [bCanUse] // 把R0写入变量bCanUse
假设如下场景:有两个任务A、B都想调用LCD_PrintString,任务A执行到第04.1行代码时读到的bCanUse为1,存入寄存器R0就被切换出去了;然后任务B也调用LCD_PrintString,任务B执行到第4行时发现bCanUse为1并把它减为0,执行到第5行代码时发现条件成立可以进入if语句块使用LCD,然后任务B也被切换出去了;现在任务A继续运行第04.2行代码时R0为1,运行到第04.3行代码时把bCanUse设置为0,后续也能成功进入if的语句块。在这种情况下,任务A、B都能使用LCD。
上述方法不能保证万无一失的原因在于:在判断过程中,被打断了。如果能保证这个过程不被打断,就可以了:通过关闭中断来实现。
示例1的代码改进如下:在第5~7行前关闭中断。
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;
disable_irq();
if (bCanUse)
{
bCanUse = 0;
enable_irq();
/* 使用LCD */
bCanUse = 1;
return 0;
}
enable_irq();
return -1;
}
示例2的代码改进如下:在第5行前关闭中断。
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;
disable_irq();
bCanUse--;
enable_irq();
if (bCanUse == 0)
{
/* 使用LCD */
bCanUse++;
return 0;
}
else
{
disable_irq();
bCanUse++;
enable_irq();
return -1;
}
}
HAL库_FreeRTOS提供的方法(学习RTOS_32HAL库开发理解笔记)
1.数据传输的方法
1.1在两个任务之间传输数据
①使用全局变量 ■没有互斥措施则可能导致数据的错误。 ■没有阻塞唤醒功能则浪费CPU资源降低CPU效率。
②使用环形缓冲区 ■环形缓冲区 定义一个八位的数组,刚开始读的位置是零,写的位置也是零读。空的时候读的位置和写的位置相同 ■
1.2队列的本质
队列中,数据的读写本质就是环形缓冲区Buffer,在这个基础上增加了互斥措施、阻塞-唤醉机制。
如果这个队列不传输数据,只调整“数据个数”,它就是信号量(semaphore)。
如果信号量中,限定"数据个数"最大值为1,它就是互斥量(mutex)。
从内部机制的实现深入理解队列:
如下图所示
对于B有三种情况:
①B看流水线,相当于读队列,无产品就眯一会儿(需要定一个闹钟)闹钟就相当于超时时间 timeout 比如30分钟。
②同时,A放入产品叫醒B,假设15分钟。相当于写队列,并且唤醒B。
③如果一直没有,那就等到30分钟后到了take中断把你叫醒。
对于A有三种情况:
①写队列。
②队列满了。进入阻塞。/
③1)任务b读队列唤醒a
2)tick中断超时唤醒a
任务a任务b怎么唤醒互相呢?他们并不知道,只是敲一下流水线,然后会有一个链表,又开始下面的操作,如下图所示
情况一
刚开始创建任务B,那么它会进入到(假设优先级是十)就绪链表ready list里边也就是就绪状态里边。任务b运行进入到循环里边之后,他去读数据,但是队列里面现在没有数据,那么b就进入阻塞。等待这个队列有数据,他会把自己放到队列的链表里边,然后把自己从就绪链表里面删除掉,然后放到两个地方去,一个是把自己放到要读的这个队列的receive list里边,这个队列里面存放着一些想读数据,但是读不到数据的任务。你指定了一个超时间,因此你还会被放到第二个链表里边delaylist这个链表里面记录有任务B。
总结就是一个任务,他想去读队列,但是读不到数据,并且他愿意等待的话,那么他将会从就绪链表里边移除,会把它放入这个要读的队列的接收链表里边,并且还会放入一个Delaylist链表里。
对任务a来说,他写队列就是把数据放入环形缓冲区,且去唤醒谁呢?他也不知道唤醒谁,但是他会去今入的这个队列的链表QueueReserve list里面看一看,里面是不是空的,如果不空的话,它就会取出第一个任务来唤醒,也就是咱们上面的任务B。认为A把任务b从reserve list里边删除然后把任务B重新放入ready list就绪链表。这就是任务b被任务a唤醒的流程。
情况二
在任务B阻塞的过程中,没有人来写这个队列,那么任务B就会在Tick中断里面去判断一下是否达到了超时时间,如果到了超时时间,就会把你从reserve list里边删除,放入ready list里面。然后继续执行函数并返回一个错误,也就是知道了没有读到数据。
1.3 队列的函数
下面实验_游戏会用到函数 (配合下面的游戏看)
1.3.1 创建
队列的创建有两种方法:动态分配内存、静态分配内存,
- 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配
函数原型如下:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
- 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好
函数原型如下:
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer
);
1.3.2 读队列
使用xQueueReceive()
函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
1.3.3 写队列
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
/* 等同于xQueueSendToBack
* 往队列部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
注意等待时间,如果在中断里则不要等待。中断里面不允许等待。
1.4队列集
有多个硬件设备输入,那么你每个硬件设备创建一个任务,这样会极大的浪费系统资源
改进代码:
硬件驱动程序也就是中断函数里的,写队列,写队列1,队列2,队列3。有一个input task,这个任务干嘛呢?读这多个队列。把多有的数据处理之后去写挡球板队列。这样做不管你有多少个队列,我这里只有一个任务,就不会浪费CPU资源。
现在的问题是,如何让这个inputtask准确的及时的读到三个队列的数据?
第一可以使用轮询方式,
第二种使用队列集。
①轮询方式:
在一个循环里边,依次读这三个队列,有数据去处理,没数据就去读下一个对列,要注意的是,不能指定超时时间。因为指定超时时间读后面的队列会被推迟,处理就会不及时。布置的超时时间就意味着会使这个任务不断的运行,不断的运行,他不会阻塞,极大浪费CPU资源。
②队列集
a. 创建队列A,它的长度是n1
b. 创建队列B,它的长度是n2
c. 创建队列集S,它的长度是“n1+n2”
d. 把队列A、B加入队列集S
e. 这样,写队列A的时候,他会判断是否为某一个队列集的队列,在把数据写入队列A的同时会顺便把队列A的句柄写入队列集S
f. 这样,写队列B的时候,他会判断是否为某一个队列集的队列,在把数据写入队列B的同时会顺便把队列B的句柄写入队列集S 每写一个数据,就写一次句柄
g. InputTask先读取队列集S,它的返回值是一个队列句柄,这样就可以知道哪个队列有有数据了 然后InputTask再读取这个队列句柄得到数据。
使用队列集后的流程:(硬件驱动程序里边不需要对数据进行处理了,只需要在Input task任务里面处理就行)
1.41创建
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
1.4.2把队列加入队列集
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet );
1.4.3读取队列集
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
TickType_t const xTicksToWait );
****忽略****//
struct input_data {
uint32_t dev;
uint32_t val
};
QueueHandle_t g_xQueuePlatform; /* 挡球板队列 */
/* 创建队列:平台任务从里面读到红外数据,... */
g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data));
****忽略****//
1.5 队列实验_多设备玩游戏
代码看Keil工程,太多了///
1.5.1普通方法
实验目的:使用红外遥控器、旋转编码器玩游戏。
实现方案:
●游戏任务:读取队列A获得控制信息,用来控制游戏
●红外遥控器驱动:在中断函数里解析出按键后,写队列A
●旋转编码器:
○它的中断函数里解析出旋转编码器的状态,写队列B;
○它的任务函数里,读取队列B,构造好数据后写队列A
过程如图所示,红外在中断函数里边解析出按键,然后写队列A。(数据是在中断函数里边处理之后再写入队列的)
旋转编码器,在中断函数里面,解析,出状态之后,先写一个队列,然后在它的任务函数里边再读队列,将读到的数据构造好之后再写队列A。
这样有多个硬件任务的时候,你就需要在他们的各自中断函数里边把数据处理好之后,然后写到统一的一个队列A当中。这样你的底层函数得到的数据只能被做个任务他自己使用,而我们不想改变它获取的原数据,那么就可以对它进行一些封装,也就是队列级让他读到数据之后,在之后继续往上封装进行数据处理,然后写队列。也就是上面提到的队列集
1.5.2改进程序框架--第15个程序使用队列集完成游戏
再次把上面的图拉过来,对这个图的解释,复简略代码
红外硬件写自己的队列:
static int IRReceiver_IRQTimes_Parse(void)
{
·
· /*省略*/
·
idata.dev = datas[0];
idata.val = datas[2];
xQueueSendFromISR(g_xQueueIR, &idata, NULL);
return 0;
}
编码器硬件写自己的队列:
void RotaryEncoder_IRQ_Callback(void)
{
·
· /*省略*/
·
/* 写队列 */
rdata.cnt = g_count;
rdata.speed = g_speed;
xQueueSendFromISR(g_xQueueRotary, &rdata, NULL);
}
队列集,创建队列集
void game1_task(void *params)
{
·
· /*省略*/
·
/* 创建队列,队列集,创建输入任务InputTask */
g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data));
g_xQueueSetInput = xQueueCreateSet(IR_QUEUE_LEN + ROTARY_QUEUE_LEN);
g_xQueueIR = GetQueueIR();
g_xQueueRotary = GetQueueRotary();
xQueueAddToSet(g_xQueueIR, g_xQueueSetInput);
xQueueAddToSet(g_xQueueRotary, g_xQueueSetInput);
xTaskCreate(InputTask, "InputTask", 128, NULL, osPriorityNormal, NULL);
·
· /*省略*/
·
xTaskCreate(platform_task, "platform_task", 128, NULL, osPriorityNormal, NULL);
while (1)
{
game1_draw();
//draw_end();
vTaskDelay(50);
}
}
读队列集
static void InputTask(void *params)
{
QueueSetMemberHandle_t xQueueHandle;
while (1)
{
/* 读队列集, 得到有数据的队列句柄 */
xQueueHandle = xQueueSelectFromSet(g_xQueueSetInput, portMAX_DELAY);
if (xQueueHandle)
{
/* 读队列句柄得到数据,处理数据 */
if (xQueueHandle == g_xQueueIR)
{
ProcessIRData();
}
else if (xQueueHandle == g_xQueueRotary)
{
ProcessRotaryData();
}
}
}
读句柄得到数据,然后处理数据(将原始的硬件数据转化为游戏的控制数据)
红外:
static void ProcessIRData(void)
{
struct ir_data idata;
static struct input_data input;
xQueueReceive(g_xQueueIR, &idata, 0);
if (idata.val == IR_KEY_LEFT)
{
input.dev = idata.dev;
input.val = UPT_MOVE_LEFT;
}
else if (idata.val == IR_KEY_RIGHT)
{
input.dev = idata.dev;
input.val = UPT_MOVE_RIGHT;
}
else if (idata.val == IR_KEY_REPEAT)
{
/* 保持不变 */;
}
else
{
input.dev = idata.dev;
input.val = UPT_MOVE_NONE;
}
/* 写挡球板队列 */
xQueueSend(g_xQueuePlatform, &input, 0);
}
编码器:
static void ProcessRotaryData(void)
{
struct rotary_data rdata;
struct input_data idata;
int left;
int i, cnt;
/* 读旋转编码器队列 */
xQueueReceive(g_xQueueRotary, &rdata, 0);
/* 处理数据 */
/* 判断速度: 负数表示向左转动, 正数表示向右转动 */
if (rdata.speed < 0)
{
left = 1;
rdata.speed = 0 - rdata.speed;
}
else
{
left = 0;
}
//cnt = rdata.speed / 10;
//if (!cnt)
// cnt = 1;
if (rdata.speed > 100)
cnt = 4;
else if (rdata.speed > 50)
cnt = 2;
else
cnt = 1;
/* 写挡球板队列 */
idata.dev = 1;
idata.val = left ? UPT_MOVE_LEFT : UPT_MOVE_RIGHT;
for (i = 0; i < cnt; i++)
{
xQueueSend(g_xQueuePlatform, &idata, 0);
}
}
挡球板任务读数据然后控制游戏:
/* 挡球板任务 */
static void platform_task(void *params)
{
byte platformXtmp = platformX;
uint8_t dev, data, last_data;
struct input_data idata;
// Draw platform
draw_bitmap(platformXtmp, g_yres - 8, platform, 12, 8, NOINVERT, 0);
draw_flushArea(platformXtmp, g_yres - 8, 12, 8);
while (1)
{
//if (0 == IRReceiver_Read(&dev, &data))
xQueueReceive(g_xQueuePlatform, &idata, portMAX_DELAY);
uptMove = idata.val;
// Hide platform
draw_bitmap(platformXtmp, g_yres - 8, clearImg, 12, 8, NOINVERT, 0);
draw_flushArea(platformXtmp, g_yres - 8, 12, 8);
// Move platform
if(uptMove == UPT_MOVE_RIGHT)
platformXtmp += 3;
else if(uptMove == UPT_MOVE_LEFT)
platformXtmp -= 3;
uptMove = UPT_MOVE_NONE;
// Make sure platform stays on screen
if(platformXtmp > 250)
platformXtmp = 0;
else if(platformXtmp > g_xres - PLATFORM_WIDTH)
platformXtmp = g_xres - PLATFORM_WIDTH;
// Draw platform
draw_bitmap(platformXtmp, g_yres - 8, platform, 12, 8, NOINVERT, 0);
draw_flushArea(platformXtmp, g_yres - 8, 12, 8);
platformX = platformXtmp;
}
}
后面报错其中之一:
添加队列集相关的配置
这个文件里面搜索配置下,没有找着那么直接在freeRROS.h文件里面将它改为1,这样做是可以用,但是如果你重新配置的话,这个配置项又会被变为0。
那我们直接在刚才那个路径下打开的文件中,我们的这个配置写为1可以编译通过。
1.5.3队列集实验_增加姿态控制
这里可能出现的一些bug以及问题,如果一开始在freertos.c文件里边连续创建这样三个任务,由于同优先级任务是从最后一个创建的任务开始执行,那么最后一个任务mpu6050会先执行,然后写队列,队列被写满之后等执行第一个gametask的时候,由于一开始6050任务已经把队列写满,他无法再写队列句柄,就无法被写入队列集当中,队列级就无法获取6050的数据,6050硬件就无法使用。
有一个bug就是OLED屏幕与6050都使用的是iic,他们两个会冲突,使用如图所示,一个全局变量让他们互斥的访问iic就可以,但是之前讲过这样做也不是最保险的,但是现在先用这个将就一下。
8_1队列的原理②①③④⑤⑥⑦⑧⑨⑩ ■ ●
队列(queue)可以用于"任务到任务"、"任务到中断"、"中断到任务"直接传输信息。
本章涉及如下内容:
怎么创建、清除、删除队列
队列中消息如何保存
怎么向队列发送数据、怎么从队列读取数据、怎么覆盖队列的数据
在队列上阻塞是什么意思
怎么在多个队列上阻塞
读写队列时如何影响任务的优先级
使用队列传输数据时有两种方法:
拷贝:把数据、把变量的值复制进队列里
引用:把数据、把变量的地址复制进队列里
8_2队列的使用
question:使用全局变量也可以在任务之间实现同步,为什么要加volatile?
A.任务A写变量,任务B读变量,不加volatile的话,任务
B读到的值可能不是任务A写的值B.在while循环中判断一个变量时,不加volatile的话,有可 能把变 量读进来后就循环判断,不会多次读变量
C.volatile的意思是"易变",就是告诉编译器:别轻易来优化我,老老实实去读写内存上的变量
使用队列的流程:创建队列、写队列、读队列、删除队列。
创建队列:
xQueueCalcHandle = xQueueCreate(2, sizeof(int));
写队列:
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
这些函数用到的参数是类似的,统一说明如下:
读队列:
使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版 本:在任务中使用、在ISR中使用。函数原型如下:
参数说明如下:
举例
static QueueHandle_t xQueueCalcHandle;
static QueueHandle_t xQueueUARTcHandle;
int InitUARTLock(void)
{
int val;
xQueueUARTcHandle = xQueueCreate(1, sizeof(int));
if (xQueueUARTcHandle == NULL)
{
printf("can not create queue\r\n");
return -1;
}
xQueueSend(xQueueUARTcHandle, &val, portMAX_DELAY);
return 0;
}
void GetUARTLock(void)
{
int val;
xQueueReceive(xQueueUARTcHandle, &val, portMAX_DELAY);
}
void PutUARTLock(void)
{
int val;
xQueueSend(xQueueUARTcHandle, &val, portMAX_DELAY);
}
void TaskGenericFunction(void * param)
{
while (1)
{
GetUARTLock();
printf("%s\r\n", (char *)param);
PutUARTLock();
}
}
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xQueueCalcHandle = xQueueCreate(2, sizeof(int));
if (xQueueCalcHandle == NULL)
{
printf("can not create queue\r\n");
}
InitUARTLock();
xTaskCreate(TaskGenericFunction, "Task3", 100, "Task 3 is running", 1, NULL);
xTaskCreate(TaskGenericFunction, "Task4", 100, "Task 4 is running", 1, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
return 0;
}
对代码的一些解释:
1
2
3
4
08_3队列集和邮箱
邮箱自己看手册
队列集:
代码目标:
一开始任务3先运行,但是一开始两个队列里面都没有数据,所以他会进入阻塞状态。
然后开始运行任务1,往队列里面写入一个数据,根据上面那个流程图,第三步可以看出,在写入一个数据的同时,会将它的hand放入queue set里边。任务3一直在等待queue set,于是任务3就可以执行,任务3返回有数据的那个队列进行读取并打印。第一次打印的数据是0数据来自于队列1。然后任务2同上。
static volatile int flagCalcEnd = 0;
static volatile int flagUARTused = 0;
static QueueHandle_t xQueueHandle1;
static QueueHandle_t xQueueHandle2;
static QueueSetHandle_t xQueueSet;
void Task1Function(void * param)
{
int i = 0;
while (1)
{
xQueueSend(xQueueHandle1, &i, portMAX_DELAY);
i++;
vTaskDelay(10);
}
}
void Task2Function(void * param)
{
int i = -1;
while (1)
{
xQueueSend(xQueueHandle2, &i, portMAX_DELAY);
i--;
vTaskDelay(20);
}
}
void Task3Function(void * param)
{
QueueSetMemberHandle_t handle;
int i;
while (1)
{
/* 1. 读队列集合.判断哪个队列有数据? */
handle = xQueueSelectFromSet(xQueueSet,portMAX_DELAY);
/* 2.读数据 */
xQueueReceive(handle,&i,0);
/* 3. print */
printf("get data : %d\r\n", i);
}
}
/*-----------------------------------------------------------*/
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
/* 1. 创建2个queue */
xQueueHandle1 = xQueueCreate(2, sizeof(int));
if (xQueueHandle1 == NULL)
{
printf("can not create queue\r\n");
}
xQueueHandle2 = xQueueCreate(2, sizeof(int));
if (xQueueHandle2 == NULL)
{
printf("can not create queue\r\n");
}
/* 2. 创建队列集合queue set */
xQueueSet = xQueueCreateSet(4);
/* 3. 把2个queue添加进queue set */
xQueueAddToSet(xQueueHandle1,xQueueSet);
xQueueAddToSet(xQueueHandle2,xQueueSet);
/* 4. 创建3个任务 */
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
xTaskCreate(Task3Function, "Task3", 100, NULL, 1, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
return 0;
}
09-1_信号量的理论讲解
使用信号量时,先创建、然后去添加资源、获得资源。使用句柄来表示一个信号量。
信号量不能用来传输数据,它可以记录数据的数量
09-2_信号量的常规使用
代码对12个代码的改造,使用信号量。
10-1 互斥量的理论讲解
1.互斥量(mutex)
2.互斥量的使用场合
在多任务系统中,任务A正在使用某个资源,还没用完的情况下任务B也来使用的话,就可能导致问题。
比如对于串口,任务A正使用它来打印,在打印过程中任务B也来打印,客户看到的结果就是A、B的信 息混杂在一起。
这种现象很常见:
● 访问外设:刚举的串口例子
● 读、修改、写操作导致的问题 对于同一个变量,比如 int a ,如果有两个任务同时写它就有可能导致问题。 对于变量的修改,C代码只有一条语句,比如: a=a+8; ,它的内部实现分为3步:读出原 值、修改、写入。
我们想让任务A、B都执行add_a函数,a的最终结果是 1+8+8=17 。 假设任务A运行完代码①,在执行代码②之前被任务B抢占了:现在任务A的R0等于1。 任务B执行完add_a函数,a等于9。 任务A继续运行,在代码②处R0仍然是被抢占前的数值1,执行完②③的代码,a等于9,这 跟预期的17不符合。 ● 对变量的非原子化访问 修改变量、设置结构体、在16位的机器上写32位的变量,这些操作都是非原子的。也就是它们的 操作过程都可能被打断,如果被打断的过程有其他任务来操作这些变量,就可能导致冲突。 ● 函数重入 "可重入的函数"是指:多个任务同时调用它、任务和中断同时调用它,函数的运行也是安全 的。可重入的函数也被称为"线程安全"(thread safe)。 每个任务都维持自己的栈、自己的CPU寄存器,如果一个函数只使用局部变量,那么它就是 线程安全的。 函数中一旦使用了全局变量、静态变量、其他外设,它就不是"可重入的",如果改函数正在 被调用,就必须阻止其他任务、中断再次调用它。
上述问题的解决方法是:任务A访问这些全局变量、函数代码时,独占它,就是上个锁。这些全局变 量、函数代码必须被独占地使用,它们被称为临界资源。
互斥量也被称为互斥锁,使用过程如下:
● 互斥量初始值为1 ● 任务A想访问临界资源,先获得并占有互斥量,然后开始访问 ● 任务B也想访问临界资源,也要先获得互斥量:被别人占有了,于是阻塞 ● 任务A使用完毕,释放互斥量;任务B被唤醒、得到并占有互斥量,然后开始访问临界资源 ● 任务B使用完毕,释放互斥量
正常来说:在任务A占有互斥量的过程中,任务B、任务C等等,都无法释放互斥量。 但是FreeRTOS未实现这点:任务A占有互斥量的情况下,任务B也可释放互斥量。
3.优先级反转
引入互斥量是要解决谁上锁谁才能解锁的问题。
解决优先级反转的问题ABC的优先级分别为123,但是c不能执行b反而执行了,这就是优先级反转。
优先级继承:C想运行,但是lock函数被a占用着,这时候c把它的优先级3让给a让a去执行。当a执行完后a执行unlock去释放那个互斥量,释放完之后让c执行。
假设任务A、B都想使用串口,A优先级比较低:
● 任务A获得了串口的互斥量 ● 任务B也想使用串口,它将会阻塞、等待A释放互斥量 ● 高优先级的任务,被低优先级的任务延迟,这被称为"优先级反转"(priority inversion)
如果涉及3个任务,可以让"优先级反转"的后果更加恶劣。
互斥量可以通过"优先级继承",可以很大程度解决"优先级反转"的问题,这也是FreeRTOS中互斥量和二 级制信号量的差别。
本节程序使用二级制信号量来演示"优先级反转"的恶劣后果。
main函数创建了3个任务:LPTask/MPTask/HPTask(低/中/高优先级任务),代码如下:
/* 互斥量/二进制信号量句柄 */
SemaphoreHandle_t xLock;
int main( void )
{
prvSetupHardware();
/* 创建互斥量/二进制信号量 */
xLock = xSemaphoreCreateBinary( );
if( xLock != NULL )
{
/* 创建3个任务: LP,MP,HP(低/中/高优先级任务)
*/
xTaskCreate( vLPTask, "LPTask", 1000, NULL, 1, NULL );
xTaskCreate( vMPTask, "MPTask", 1000, NULL, 2, NULL );
xTaskCreate( vHPTask, "HPTask", 1000, NULL, 3, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建互斥量/二进制信号量 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
LPTask/MPTask/HPTask三个任务的代码和运行过程如下图所示:
● A:HPTask优先级最高,它最先运行。在这里故意打印,这样才可以观察到flagHPTaskRun的脉 冲。 ● HP Delay:HPTask阻塞 ● B:MPTask开始运行。在这里故意打印,这样才可以观察到flagMPTaskRun的脉冲。 ● MP Delay:MPTask阻塞 ● C:LPTask开始运行,获得二进制信号量,然后故意打印很多字符 ● D:HP Delay时间到,HPTask恢复运行,它无法获得二进制信号量,一直阻塞等待 ● E:MP Delay时间到,MPTask恢复运行,它比LPTask优先级高,一直运行。导致LPTask无法 运行,自然无法释放二进制信号量,于是HPTask用于无法运行。
总结:
● LPTask先持有二进制信号量, ● 但是MPTask抢占LPTask,是的LPTask一直无法运行也就无法释放信号量, ● 导致HPTask任务无法运行 ● 优先级最高的HPTask竟然一直无法运行!
4.优先级继承
看 10-2_互斥量的常规使用的 举例代码
5.递归锁
1.死锁的概念
日常生活的死锁:我们只招有工作经验的人!我没有工作经验怎么办?那你就去找工作啊!
假设有2个互斥量M1、M2,2个任务A、B:
● A获得了互斥量M1 ● B获得了互斥量M2 ● A还要获得互斥量M2才能运行,结果A阻塞 ● B还要获得互斥量M1才能运行,结果B阻塞 ● A、B都阻塞,再无法释放它们持有的互斥量 ● 死锁发生!
2.自我死锁
假设这样的场景:
●任务A获得了互斥锁M ●它调用一个库函数 ●库函数要去获取同一个互斥锁M,于是它阻塞:任务A休眠,等待任务A来释放互斥锁! ●死锁发生!
3.函数
怎么解决这类问题?可以使用递归锁(Recursive Mutexes),它的特性如下:
●任务A获得递归锁M后,它还可以多次去获得这个锁 ●"take"了N次,要"give"N次,这个锁才会被释放
递归锁的函数根一般互斥量的函数名不一样,参数类型一样,列表如下:
函数原型如下:
/* 创建一个递归锁,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );
/* 释放 */
BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xSemaphore );
/* 获得 */
BaseType_t xSemaphoreTakeRecursive(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
示例: 递归锁
本节代码为: FreeRTOS_19_mutex_recursive 。
递归锁实现了:谁上锁就由谁解锁。
本程序从 FreeRTOS_16_mutex_who_give 修改得来,它的main函数里创建了2个任务
●任务1:高优先级,一开始就获得递归锁,然后故意等待很长时间,让任务2运行 ●任务2:低优先级,看看能否操作别人持有的锁
main函数代码如下:
/* 递归锁句柄 */
SemaphoreHandle_t xMutex;
int main( void )
{
prvSetupHardware();
/* 创建递归锁 */
xMutex = xSemaphoreCreateRecursiveMutex( );
if( xMutex != NULL )
{
/* 创建2个任务: 一个上锁, 另一个自己监守自盗(看看能否开别人的锁自己用)
*/
xTaskCreate( vTakeTask, "Task1", 1000, NULL, 2, NULL );
xTaskCreate( vGiveAndTakeTask, "Task2", 1000, NULL, 1, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建递归锁 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
两个任务经过精细设计,代码和运行流程如下图所示:
●A:任务1优先级最高,先运行,获得递归锁 ●B:任务1阻塞,让任务2得以运行 ●C:任务2运行,看看能否获得别人持有的递归锁:不能 ●D:任务2故意执行"give"操作,看看能否释放别人持有的递归锁:不能 ●E:任务2等待递归锁 ●F:任务1阻塞时间到后继续运行,使用循环多次获得、释放递归锁 ●递归锁在代码上实现了:谁持有递归锁,必须由谁释放。
程序运行结果如下图所示:
10-2_互斥量的常规使用
1.创建
互斥量是一种特殊的二进制信号量。
使用互斥量时,先创建、然后去获得、释放它。使用句柄来表示一个互斥量。
创建互斥量的函数有2种:动态分配内存,静态分配内存,函数原型如下:
/* 创建一个互斥量,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutex( void );
/* 创建一个互斥量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer
);
要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:
##define configUSE_MUTEXES 1
2.其他函数
要注意的是,互斥量不能在ISR中使用。
各类操作函数,比如删除、give/take,跟一般是信号量是一样的。
/*
* xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
/* 释放 */
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
/* 获得 */
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
3.举例代码
static volatile uint8_t flagLPTaskRun = 0;
static volatile uint8_t flagMPTaskRun = 0;
static volatile uint8_t flagHPTaskRun = 0;
static void vLPTask( void *pvParameters )
{
const TickType_t xTicksToWait = pdMS_TO_TICKS( 10UL );
uint32_t i;
char c = 'A';
printf("LPTask start\r\n");
/* 无限循环 */
for( ;; )
{
flagLPTaskRun = 1;
flagMPTaskRun = 0;
flagHPTaskRun = 0;
/* 获得互斥量/二进制信号量 */
xSemaphoreTake(xLock, portMAX_DELAY);
/* 耗时很久 */
printf("LPTask take the Lock for long time");
for (i = 0; i < 500; i++)
{
flagLPTaskRun = 1;
flagMPTaskRun = 0;
flagHPTaskRun = 0;
printf("%d\r\n", i);
}
printf("\r\n");
/* 释放互斥量/二进制信号量 */
xSemaphoreGive(xLock);
vTaskDelay(xTicksToWait);
}
}
static void vMPTask( void *pvParameters )
{
volatile int j;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 30UL );
flagLPTaskRun = 0;
flagMPTaskRun = 1;
flagHPTaskRun = 0;
printf("MPTask start\r\n");
/* 让LPTask、HPTask先运行 */
vTaskDelay(xTicksToWait);
/* 无限循环 */
for( ;; )
{
flagLPTaskRun = 0;
flagMPTaskRun = 1;
flagHPTaskRun = 0;
j++;
printf("%d\r\n",j);
}
}
static void vHPTask( void *pvParameters )
{
const TickType_t xTicksToWait = pdMS_TO_TICKS( 10UL );
flagLPTaskRun = 0;
flagMPTaskRun = 0;
flagHPTaskRun = 1;
printf("HPTask start\r\n");
/* 让LPTask先运行 */
vTaskDelay(xTicksToWait);
/* 无限循环 */
for( ;; )
{
flagLPTaskRun = 0;
flagMPTaskRun = 0;
flagHPTaskRun = 1;
printf("HPTask wait for Lock\r\n");
/* 获得互斥量/二进制信号量 */
xSemaphoreTake(xLock, portMAX_DELAY);
flagLPTaskRun = 0;
flagMPTaskRun = 0;
flagHPTaskRun = 1;
/* 释放互斥量/二进制信号量 */
xSemaphoreGive(xLock);
}
}
int main( void )
{
prvSetupHardware();
/* 创建互斥量/二进制信号量 */
xLock = xSemaphoreCreateMutex();
xSemaphoreGive(xLock);
if( xLock != NULL )
{
/* 创建3个任务: LP,MP,HP(低/中/高优先级任务)
*/
xTaskCreate( vLPTask, "LPTask", 1000, NULL, 1, NULL );
xTaskCreate( vMPTask, "MPTask", 1000, NULL, 2, NULL );
xTaskCreate( vHPTask, "HPTask", 1000, NULL, 3, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建互斥量/二进制信号量 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
效果
4.问题
10-3_互斥量的缺陷和递归锁
11-1_事件组的理论讲解
前面学过的队列信号量,互斥量都解决不了一个事情,就是多个事件,也就是事件组。
1.概念
2.事件组函数
1.创建
使用事件组之前,要先创建,得到一个句柄;使用事件组时,要使用句柄来表明使用哪个事件组。
有两种创建方法:动态分配内存、静态分配内存。函数原型如下:
/* 创建一个事件组,返回它的句柄。
* 此函数内部会分配事件组结构体
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreate( void );
/* 创建一个事件组,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticEventGroup_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t *
pxEventGroupBuffer
);
2.删除
对于动态创建的事件组,不再需要它们时,可以删除它们以回收内存。
vEventGroupDelete可以用来删除事件组,函数原型如下:
/*
* xEventGroup: 事件组句柄,你要删除哪个事件组
*/
void vEventGroupDelete( EventGroupHandle_t xEventGroup )
3.设置事件
可以设置事件组的某个位、某些位,使用的函数有2个:
● 在任务中使用 xEventGroupSetBits() ● 在ISR中使用 xEventGroupSetBitsFromISR()
有一个或多个任务在等待事件,如果这些事件符合这些任务的期望,那么任务还会被唤醒。
函数原型如下:
/* 设置事件组中的位
* xEventGroup: 哪个事件组
* uxBitsToSet: 设置哪些位?
* 如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1
* 可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0
* 返回值: 返回原来的事件值(没什么意义, 因为很可能已经被其他任务修改了)
*/
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet
);
4.等待事件
使用 xEventGroupWaitBits 来等待事件,可以等待某一位、某些位中的任意一个,也可以等待多位; 等到期望的事件后,还可以清除某些位。
函数原型如下:
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait
);
先引入一个概念:unblock condition。一个任务在等待事件发生时,它处于阻塞状态;当期望的时间 发生时,这个状态就叫"unblock condition",非阻塞条件,或称为"非阻塞条件成立";当"非阻塞条件成 立"后,该任务就可以变为就绪态。
函数参数说明列表如下:
举例如下:
你可以使用 xEventGroupWaitBits() 等待期望的事件,它发生之后再使用 xEventGroupClearBits() 来清除。但是这两个函数之间,有可能被其他任务或中断抢占,它们可能会修改事件组。
可以使用设置 xClearOnExit 为pdTRUE,使得对事件组的测试、清零都在 xEventGroupWaitBits() 函数内部完成,这是一个原子操作。
3.同步点
4.问题
11-2_事件组的使用_等待事件
1.代码
static int sum = 0;
static int dec = 0;
static volatile int flagCalcEnd = 0;
static volatile int flagUARTused = 0;
static QueueHandle_t xQueueCalcHandle;
static QueueHandle_t xQueueUARTcHandle;
static EventGroupHandle_t xEventGroupCalc;
void Task1Function(void * param)
{
volatile int i = 0;
while (1)
{
for (i = 0; i < 100000; i++)
sum++;
xQueueSend(xQueueCalcHandle, &sum, 0);
/* 设置事件0 */
xEventGroupSetBits(xEventGroupCalc,(1<<0));
}
}
void Task2Function(void * param)
{
volatile int i = 0;
while (1)
{
for (i = 0; i < 100000; i++)
dec--;
xQueueSend(xQueueCalcHandle, &dec, 0);
/* 设置事件1 */
xEventGroupSetBits(xEventGroupCalc,(1<<1));
}
}
void Task3Function(void * param)
{
int val1,val2;
while (1)
{
/* 等待事件 */
xEventGroupWaitBits(xEventGroupCalc,(1<<0)|(1<<1),pdTRUE,pdTRUE,portMAX_DELAY);
xQueueReceive(xQueueCalcHandle,&val1,0);
xQueueReceive(xQueueCalcHandle,&val2,0);
printf("val1 = %d,val2 = %d\r\n", val1,val2);
}
}
/*-----------------------------------------------------------*/
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
/* 创建事件组 */
xEventGroupCalc = xEventGroupCreate();
xQueueCalcHandle = xQueueCreate(2, sizeof(int));
if (xQueueCalcHandle == NULL)
{
printf("can not create queue\r\n");
}
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
xTaskCreate(Task3Function, "Task3", 100, NULL, 1, NULL);
vTaskStartScheduler();
return 0;
}
2.问题
A
B
C
xEventGroupSetBits(xEventGroupCalc,(1<<1));函数参数设置的问题解决:
1)
2)
4
11-3_事件组的使用_同步点
1.代码
EventGroupHandle_t xEventGroup;
/* bit0: 洗菜
* bit1: 生火
* bit2: 炒菜
*/
#define TABLE (1<<0)
#define BUYING (1<<1)
#define COOKING (1<<2)
#define ALL (TABLE | BUYING | COOKING)
static void vCookingTask( void *pvParameters )
{
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
int i = 0;
/* 无限循环 */
for( ;; )
{
/* 做自己的事 */
printf("%s is cooking %d time....\r\n", (char *)pvParameters, i);
/* 表示我做好了, 还要等别人都做好 */
xEventGroupSync(xEventGroup, COOKING, ALL, portMAX_DELAY);
/* 别人也做好了, 开饭 */
printf("%s is eating %d time....\r\n", (char *)pvParameters, i++);
vTaskDelay(xTicksToWait);
}
}
static void vBuyingTask( void *pvParameters )
{
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
int i = 0;
/* 无限循环 */
for( ;; )
{
/* 做自己的事 */
printf("%s is buying %d time....\r\n", (char *)pvParameters, i);
/* 表示我做好了, 还要等别人都做好 */
xEventGroupSync(xEventGroup, BUYING, ALL, portMAX_DELAY);
/* 别人也做好了, 开饭 */
printf("%s is eating %d time....\r\n", (char *)pvParameters, i++);
vTaskDelay(xTicksToWait);
}
}
static void vTableTask( void *pvParameters )
{
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
int i = 0;
/* 无限循环 */
for( ;; )
{
/* 做自己的事 */
printf("%s is do the table %d time....\r\n", (char *)pvParameters, i);
/* 表示我做好了, 还要等别人都做好 */
xEventGroupSync(xEventGroup, TABLE, ALL, portMAX_DELAY);
/* 别人也做好了, 开饭 */
printf("%s is eating %d time....\r\n", (char *)pvParameters, i++);
vTaskDelay(xTicksToWait);
}
}
int main( void )
{
prvSetupHardware();
/* 创建递归锁 */
xEventGroup = xEventGroupCreate( );
if( xEventGroup != NULL )
{
/* 创建3个任务: 洗菜/生火/炒菜
*/
xTaskCreate( vCookingTask, "task1", 1000, "A", 1, NULL );
xTaskCreate( vBuyingTask, "task2", 1000, "B", 2, NULL );
xTaskCreate( vTableTask, "task3", 1000, "C", 3, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建事件组 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
12-1任务通知理论讲解
1.任务通知理论讲解
2.任务通知的特性
1.优势及限制
2.通知状态和通知值
3.任务通知的使用
使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、计数型信号量、二进制信号量、 事件组。
1.两类函数
2.xTaskNotifyGive/ulTaskNotifyTake
xTaskNotifyGive函数的参数说明如下:
12-2任务通知使用_轻量级信号量
本节源码: 22_freertos_example_tasknotify_semaphore,来自 15_freertos_example_semaphore .
static int sum = 0;
static volatile int flagCalcEnd = 0;
static volatile int flagUARTused = 0;
static SemaphoreHandle_t xSemCalc;
static SemaphoreHandle_t xSemUART;
static TaskHandle_t XHandleTask2;
void Task1Function(void * param)
{
volatile int i = 0;
while (1)
{
for (i = 0; i < 10000; i++)
sum++;
//printf("1");
for(i = 0; i < 10; i++)
{
//xSemaphoreGive(xSemCalc);
xTaskNotifyGive(XHandleTask2);
}
vTaskDelete(NULL); }
}
void Task2Function(void * param)
{
int i = 0;
int val;
while (1)
{
//if (flagCalcEnd)
//flagCalcEnd = 0;
val = ulTaskNotifyTake(pdFALSE,portMAX_DELAY);
//xSemaphoreTake(xSemCalc, portMAX_DELAY);
flagCalcEnd = 1;
printf("sum = %d, NotifyVal = %d\r\n", sum,val);
}
}
void TaskGenericFunction(void * param)
{
while (1)
{
xSemaphoreTake(xSemUART, portMAX_DELAY);
printf("%s\r\n", (char *)param);
xSemaphoreGive(xSemUART);
vTaskDelay(1);
}
}
/*-----------------------------------------------------------*/
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xSemCalc = xSemaphoreCreateCounting(10, 0);
xSemUART = xSemaphoreCreateBinary();
xSemaphoreGive(xSemUART);
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, &XHandleTask2);
//xTaskCreate(TaskGenericFunction, "Task3", 100, "Task 3 is running", 1, NULL);
//xTaskCreate(TaskGenericFunction, "Task4", 100, "Task 4 is running", 1, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
return 0;
}
12-3_任务通知使用_轻量级队列
本节源码: 23_freertos_example_tasknotify_queue,来自 13_freertos_example_queue
void Task1Function(void * param)
{
volatile int i = 0;
while (1)
{
for (i = 0; i < 10000; i++)
sum++;
//printf("1");
//flagCalcEnd = 1;
//vTaskDelete(NULL);
for(i = 0; i < 10; i++)
{
//xQueueSend(xQueueCalcHandle, &sum, portMAX_DELAY);
//xTaskNotify(xHandleTask2,sum,eSetValueWithoutOverwrite);
xTaskNotify(xHandleTask2,sum,eSetValueWithOverwrite);
sum++;
}
vTaskDelete(NULL);
}
}
void Task2Function(void * param)
{
int val;
int i =0;
while (1)
{
//if (flagCalcEnd)
flagCalcEnd = 0;
//xQueueReceive(xQueueCalcHandle, &val, portMAX_DELAY);
xTaskNotifyWait(0,0,&val,portMAX_DELAY);
flagCalcEnd = 1;
printf("sum = %d , i = %d\r\n", val , i++);
}
}
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xQueueCalcHandle = xQueueCreate(10,sizeof(int));
if (xQueueCalcHandle == NULL)
{
printf("can not create queue\r\n");
}
InitUARTLock();
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, &xHandleTask2);
}
12-4_任务通知使用_轻量级事件组
本节源码: 24_freertos_example_tasknotify_event_group,来自 20_frkertos_example_event_group
任务通知里面给任意一个位都会唤醒,所以得在函数中自己判断一下是否是自己要的那一位
static QueueHandle_t xQueueCalcHandle;
static TaskHandle_t xHandLeTask3;
static EventGroupHandle_t xEventGroupCalc;
void Task1Function(void * param)
{
volatile int i = 0;
while (1)
{
for (i = 0; i < 100000; i++)
sum++;
xQueueSend(xQueueCalcHandle, &sum, 0);
/* 设置事件0 */
//xEventGroupSetBits(xEventGroupCalc, (1<<0));
xTaskNotify(xHandLeTask3,(1<<0),eSetBits);
printf("Task 1 set bit 0 \r\n");
vTaskDelete(NULL);
}
}
void Task2Function(void * param)
{
volatile int i = 0;
while (1)
{
for (i = 0; i < 1000000; i++)
dec--;
xQueueSend(xQueueCalcHandle, &dec, 0);
/* 设置事件1 */
//xEventGroupSetBits(xEventGroupCalc, (1<<1));
xTaskNotify(xHandLeTask3,(1<<1),eSetBits);
printf("Task 2 set bit 1 \r\n");
vTaskDelete(NULL);
}
}
void Task3Function(void * param)
{
int val1, val2;
int bits;
while (1)
{
/*等待事件 */
//xEventGroupWaitBits(xEventGroupCalc, (1<<0)|(1<<1), pdTRUE, pdTRUE, portMAX_DELAY);
xTaskNotifyWait(0,0,&bits,portMAX_DELAY);
if( (bits & 0X3) == 0X3)
{
vTaskDelay(20);
xQueueReceive(xQueueCalcHandle, &val1, 0);
xQueueReceive(xQueueCalcHandle, &val2, 0);
printf("val1 = %d, val2 = %d\r\n", val1, val2);
}
else
{
vTaskDelay(20);
printf("have not get all bits, get only ox%x\r\n", bits);
}
}
}
/*-----------------------------------------------------------*/
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
/* 创建事件组 */
xEventGroupCalc = xEventGroupCreate();
xQueueCalcHandle = xQueueCreate(2, sizeof(int));
if (xQueueCalcHandle == NULL)
{
printf("can not create queue\r\n");
}
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
xTaskCreate(Task3Function, "Task3", 100, NULL, 1, &xHandLeTask3);
/* Start the scheduler. */
vTaskStartScheduler();
return 0;
}
13-1_定时器的理论介绍
1. 定时器的理论讲解
定时器三
·超时时间
·函数
·单次触发还是周期性触发
13-2_定时器的一般使用
1创建
要使用定时器,需要先创建它,得到它的句柄。
有两种方法创建定时器:动态分配内存、静态分配内存。函数原型如下:
/* 使用动态分配内存的方法创建定时器
* pcTimerName:定时器名字, 用处不大, 尽在调试时用到
* xTimerPeriodInTicks: 周期, 以Tick为单位
* uxAutoReload: 类型, pdTRUE表示自动加载, pdFALSE表示一次性
* pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器
* pxCallbackFunction: 回调函数
* 返回值: 成功则返回TimerHandle_t, 否则返回NULL
*/
TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
回调函数的类型是:
void ATimerCallback( TimerHandle_t xTimer );
typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );
3启动/停止
启动定时器就是设置它的状态为运行态(Running、Active)。
停止定时器就是设置它的状态为冬眠(Dormant),让它不能运行。
涉及的函数原型如下:
/* 启动定时器
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL表示"启动命令"在xTicksToWait个Tick内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
注意,这些函数的 xTicksToWait 表示的是,把命令写入命令队列的超时时间。命令队列可能已经满 了,无法马上把命令写入队列里,可以等待一会。
例子
static TimerHandle_t xMyTimerHandle;
static int flagTimer = 0;
void Task1Function(void * param)
{
volatile int i = 0;
// 注意!MyTimerCallbackFunction_t!函数是由守护任务执行而不是任务一//
xTimerStart(xMyTimerHandle,0);
while (1)
{
printf("Task1Function...\r\n");
}
}
void Task2Function(void * param)
{
volatile int i = 0;
while (1)
{
}
}
void MyTimerCallbackFunction_t (TimerHandle_t xTimer)
{
static int cnt = 0;
flagTimer = !flagTimer;
printf("MyTimerCallbackFunction_t cnt = %d\r\n",cnt++);
}
/*-----------------------------------------------------------*/
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xMyTimerHandle = xTimerCreate("mytimier",100,pdTRUE,NULL,MyTimerCallbackFunction_t);
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1); /* Start the scheduler. */
vTaskStartScheduler();
return 0;
}
13-3_使用定时器消除抖动
本节程序为 FreeRTOS_25_software_timer_readkey 。
例子
static TimerHandle_t xMyTimerHandle;
static int flagTimer = 0;
void Task1Function(void * param)
{
volatile int i = 0;
//xTimerStart(xMyTimerHandle, 0);
while (1)
{
//printf("Task1Function ...\r\n");
}
}
void Task2Function(void * param)
{
volatile int i = 0;
while (1)
{
}
}
void MyTimerCallbackFunction( TimerHandle_t xTimer )
{
static int cnt = 0;
flagTimer = !flagTimer;
printf("Get GPIO Key cnt = %d\r\n", cnt++);
}
/*-----------------------------------------------------------*/
void KeyInit(void)
{
GPIO_InitTypeDef GPIO_InitStructure;//定义结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);// 使能时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 选择IO口 PA0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置成上拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure); // 使用结构体信息进行初始化IO口}
}
void KeyIntInit(void)
{
EXTI_InitTypeDef EXTI_InitStructure;//定义初始化结构体
NVIC_InitTypeDef NVIC_InitStructure;//定义结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); /* 使能AFIO复用时钟 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); /* 将GPIO口与中断线映射起来 */
EXTI_InitStructure.EXTI_Line=EXTI_Line0; // 中断线
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; // 双边沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure); // 初始化
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQChannel; //使能外部中断所在的通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能外部中断通道
NVIC_Init(&NVIC_InitStructure); // 初始化
}
void EXTI0_IRQHandler(void)
{
static int cnt = 0;
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
printf("EXTI0_IRQHandler cnt = %d\r\n", cnt++);
/* 使用定时器消除抖动 */
xTimerReset(xMyTimerHandle, 0); /* Tcur + 2000 */
EXTI_ClearITPendingBit(EXTI_Line0); //清除中断
printf("okokokr\n");
}
}
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
KeyInit();
KeyIntInit();
xMyTimerHandle = xTimerCreate("mytimer", 2000, pdFALSE, NULL, MyTimerCallbackFunction);
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
//xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
return 0;
}
14_中断管理
15_资源管理
1 屏蔽中断
16_调试与优化
16.1 调试FreeRTOS提供了很多调试手段
本节视频源码为:28_freertos_example_stats
16.1.1 打印
printf: FreeRTOS工程里使用了microlib,里面实现了printf函数。
我们只需实现一下函数即可使用printf:
int fputc( int ch, FILE *f );
16.1.2 断言
一般的C库里面,断言就是一个函数:
void assertGcalar exprespion);
它的作用是:确认expression必须为真,如果expression为假的话就中止程序。
在FreeRTOS里,使用 configASSERT(),比如:
#define configASSERT(x) if (Ix) while(1);
我们可以让它提供更多信息,比如:
#define configASSERT(×) \
if (Ix) \
[
printf("%s %s %d\r\n", _FILE_, FUN CTION_, LINE_); \
while(1); \
}
16.1.3 Trace
FreeRTOS中定义了很多trace开头的宏,这些宏被放在系统个关键置。
它们一般都是空的宏,这不会影响代码:不影响编程处理的程序大小、不影响运行时间。
我们要调试某些功能时,可以修改宏:修改某些标记变量、打印信息等待。
16.1.4 Malloc Hook函数
编程时,一般的逻辑错误都容易解决。难以处理的是内存越界、栈溢出等。
内存越界经常发生在堆的使用过程总:堆,就是使用malloc得到的内存。
并没有很好的方法检测内存越界,但是可以提供一些回调函数:
使用pvPortMalloc失败时,如果在FreeRTOSConfig.h里配置configUSE_MALLOC_FAILED_HOOK 为1,会调用:
void vApplicationMallocFailedHook( void );
16.1.5 栈溢出Hook函数
在切换任务(vTaskSwitchContext)时调用taskCHECK_FOR_STACK_OVERFLOW来检测栈是否溢出,如果溢出会调用:
void vApplicationstackoverflowHook( TaskHandle_t xTask, char * pcTaskName );
怎么判断栈溢出?有两种方法:
方法1:
· 当前任务被切换出去之前,它的整个运行现场都被保存在栈里,这时很可能就是它对栈的 使用到达了峰值。
·这方法很高效,但是并不精确
· 比如:任务在运行过程中调用了函数A大量地使用了栈,调用完函数A后才被调度。
方法2:
·创建任务时,它的栈被填入固定的值,比如:0xa5
·检测栈里最后16字节的数据,呆不是0xa5的话表示栈即将、或者已经被用完了
·没有方法1快速,但是也足够快
·能捕获几乎所有的栈溢出
·为什么是几乎所有?可能有些函数使用栈时,非常凑巧地把栈设置为0xa5:几乎不可能
16-2 统计任务信息的原理
16.2 优化
在Windows中,当系统卡顿时我们可以查看任务管理器找到最消耗CPU资源的程序。
在FreeRTOS中,我们也可以查看任务使用CPU的情况、使用栈的情况,然后针对性地进行优化。这就是查看"任务的统计"信息。
16.2.1 栈使用情况
在创建任务时分配了栈,可以填入固定的数值比如0xa5,以后可以使用以下函数查看"栈的高水位",也就是还有多少空余的栈空间:
UBaseType_t uxTaskGetstackHighwaterMark( TaskHandle_t xTask );
原理是:从栈底往栈顶逐个字节地判断,它们的值持续是0xa5就表示它是空闲的。
函数说明:
16.2.2 任务运行时间统计
对于同优先级的任务,它们按照时间片轮流运行:你执行一个Tick,我执行一个Tick。
是否可以在Tick中断函数中,统计当前任务的累计运行时间?
不行!很不精确,因为有更高优先级的任务就绪时,当前任务还没运行一个完整的Tick就被抢占了。
我们需要比Tick更快的时钟,比如Tick周期时1m我们可以使用另一公个定时器,让它发生中断的周期时0.1ms甚至更短。
使用这个定时器来衡量一个任务的运行时间,原理如下图所示:
·切换到Task1时,使用更快的定时器记录当前时间T1
·Task1被切换出去时,使用更快的定时器记录当前时间T4
·(T4-T1)就是它运行的时间,累加起来
·关键点:在 vTaskSwitchContext 函数中,使用更快的定时器统计运行时间
16.2.3 涉及的代码
·配置
#define configGENERATE_RUN_TIME_STATS 1
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
·实现宏portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(),它用来初始化更快的定时器
·实现这两个宏之一,它们用来返回当前时钟值(更快的定时器) ·portGET_RUN_TIME_COUNTER_VALUE():直接返回时钟值。 ·portALT_GET_RUN_TIME_COUNTER_VALUE(Time):设置Time变量等于时钟值
·在任务切换时统计运行时间
·获得统计信息,可以使用下列函数
·uxTaskGetSystemState:对于每个任务它的统计信息都放在一个TaskStatus_t结构体里
·vTaskList:得到釣信息是可读的字符串
·vTaskGetRunTimeStats:得到的信息是可读的字符串
16.2.4 函数说明
·uxTaskGetSystemState:
获得任务的统计信息
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray,
const UBaseType_t uxArraySize,
uint32_t * const pulTotalRunTime );
·vTaskList:
获得任务的统计信息,形式为可读的字符串。注意,pcWriteBuffer必须足够大。
void vTaskList( signed char *pcwriteBuffer );
可读信息格式如下:
·vTaskGetRunTimeStats:
获得任务的运行信息,形式为可读的字符串。注意,pcWriteBuffer必须足够大。
void vTaskGetRunTimestats( signed char *pcwriteBuffer );
可读信息格式如下:
16-3 编写程序获取统计信息
16.2.5 上机实验
这样可以获得更加精确的时间
17-1 FreeRTOS入门结课总结
我们在创建一个任务的时候,有三个点最核心 a函数、b栈、cTCB结构体。
我们创建一个任务的时候,首先他会创建一个TCP结构体,第二他会去创建一个栈,在这个TCP结构体当中,他会保存这个栈,保存这个函数的信息。
假设我现在有两个任务,又有另外一个任务
①他们之间怎么通信呢?
②他们之间怎么同步?(同步的意思是,当我a做完某个事情任务b才可以做某个事情)。
③他们还有怎么互斥?(比如说我认为在使用某个全局变量的时候,我任务b不能使用这个变量)
①②③④⑤⑥⑦⑧⑨⑩⑪⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳
怎么使用队列?
①第一,你得创建一个生产者任务。
②第二,创建一个消费者任务。
③第三,得创建一个队列。
④第四,得写队列。
⑤第五,消费者读队列。
那么对列的实质是什么?实质是一个环形缓冲区。
一开始环形缓冲区里面没有数据,刚开始写的时候把数据拷贝进来写第一次。假设写第一个数据,再写第二个数据。这里边必定有一个指示,指示到你写到了哪里!队列前面肯定有一个队列头。
加上队列写满了,那他会休眠,等下一次使用的时候他会,把自己放入一个链表当中。读队列也是类似。
互斥
假设有两个优先级相同的任务a和任务b,他们都想写一个队列,怎样才能让他们互斥的访问这个队列?
谁跟我抢,我就先禁止掉谁:
●中断、任务:关中断 ●任务:关调度器
这个函数可以关中断,