FreeRTOS学习笔记
1.中断优先级和任务优先级的区别?
二者之间没有任何关系,中断的优先级永远高于任务优先级,任务执行过程中,中断来了就执行中断服务函数。
2.如果从中断中调用了消息队列函数,退出中断的时候要注意是否有高优先级任务就绪,若有就绪的,需要在退出中断后执行任务切换。
3.什么时候使用二值信号量??
二值信号量本质上是一个长度为一,大小为0的队列;
信号量的另一个重要的应用场合就是任务同步,用于任务与任务或中断与任务之间的同步。
在执行中断服务函数的时候可以通过向任务发送信号量来通知任务它所期待的事件发生了,当退出中断服务函数以后在任务调度器的调度下同步的任务就会执行。
在编写中断服务函数的时候我们都知道一定要快进快出,中断服务函数里面不能放太多的代码,否则的话会影响中断的实时性。
裸机编写中断服务函数的时候一般都只是在中断服务函数中做个标记,然后在后台程序中根据标记来做相应的处理。
这里外部中断触发,KeyNum = 3;作为标记。
使用二值信号量可以将KeyNum = 3替换掉;
void EXTI9_5_IRQHandler(void)
{if (EXTI_GetITStatus(EXTI_Line8) == SET){/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8) == 0){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9) == 0){KeyNum = 3;}}EXTI_ClearITPendingBit(EXTI_Line8);}
}
在使用 RTOS系统的时候我们就可以借助信号量完成此功能,当中断发生的时候就在中断服务程序中释放信号量,中断服务函数不做具体的处理。
具体的处理过程做成一个任务,这个任务会获取信号量,如果获取到信号量就说明中断发生了,那么就开始完成相应的处理,这样做的好处就是中断执行时间非常短。这个例子就是中断与任务之间使用信号量来完成同步。当然了,任务与任务之间也可以使用信号量来完成同步。
4. 优先级翻转-互斥信号量
在这种情况下,任务日的优先级实际上降到了任务工的优先级水平。因为任务 日要一直等待直到任务L释放其占用的那个共享资源。由于任务 M 剥夺了任务L 的 CPU 使用权,使得任务日的情况更加恶化,这样就相当于任务M 的优先级高于任务 H,导致优先级翻转。
不同于二值信号量的是互斥信号量具有优先级继承的特性。当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。
优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。
优先级继承并不能完全的消除优先级翻转,它只是尽可能的降低优先级翻转带来的影响。硬实时应用应该在设计之初就要避免优先级翻转的发生。
互斥信号量不能用于中断服务函数中,原因如下:
互斥信号量有优先级继承的机制,所以只能在任务中,不能在中断服务函数中获取和释放互斥信号量。中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
5. 事件标志组
使用信号量来完成任务之间、任务与中断之间的同步,但是使用信号量来同步的话任务只能与单个的事件或任务进行同步。有时候某个任务可能需要与多个事件或任务进行同步,此时信号量就无能为力了。Freertos为此提供了一个可选的解决方法,那就是事件标志组。
6.任务通知
任务通知特点总结
1. 比队列、信号量、事件标志组更具效率和更省内存,解除任务阻塞状态的速度快了不少;
2. ISR能发通知给任务,但任务不能发送通知给ISR(因为 ISR自身不是在务,有任务
控制块这个数据结构);
3. 数据只能给该任务独享,因为通知值是存放在目标任务的 TCB 中;4. 使用任务通知时,任务结构体中只有一个任务通知值,不像队列有缓冲区,另外接收任务可以因为接收任务通知而进入阻塞状态,但是发送任务不会因为任务通知发送失败而进入阻塞状态,所以需要程序员通过函数参数指明是否要覆盖挂起的通知;
韦东山FreeRTOS学习笔记
三、ARM架构、堆和栈
3-1-1 创建第一个多任务程序
cmsis_os2.c文件作用
freertos的创建任务函数为xTaskCreate,RT-Thread的创建任务函数rt_thread_create,cmsis_os2.c文件抽象出一个统一的接口比如osThreadNew,开发程序的人只需要调用osThreadNew这个函数,这个函数会自动根据不同的底层操作系统,选择调用freertos或者RT-Thread。cmsis_os2.c文件起到一个中转作用。
osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {.name = "defaultTask",.stack_size = 128 * 4,.priority = (osPriority_t) osPriorityNormal,
};
//自己创建的任务函数定义
void MyTask(void *argument)
{while (1){Led_Test();}
}void StartDefaultTask(void *argument);
void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */
//在里面可以创建自己的任务
void MX_FREERTOS_Init(void)
{defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);xTaskCreate(MyTask, "myfirsttask", 128, NULL, osPriorityNormal, NULL);
}
//默认任务
void StartDefaultTask(void *argument)
{LCD_Init();LCD_Clear();for(;;){LCD_Test();}
}
3-2-1 ARM架构
1. soc
电脑主板包括cpu、内存条、硬盘;
单片机对应的部分为soc(片上系统):cpu、内存、flash(相当于硬盘);
数据a、b存放在内存中,代码指令部分存放在flash中;
c语言的存储结构如下所示:
对于单片机而言,代码段相当于flash,其他区域相当于内存;
2. 一些常见的汇编指令
cpu中的寄存器可以暂存数据,另外几个特殊的寄存器有特殊的功能。
3-2-2 ARM架构——汇编实例
查看反汇编代码
int add(volatile int a, volatile int b)
{volatile int sum;sum = a + b;return sum;
}
void OLED_Test(void)
{int cnt = 0;OLED_Init();OLED_Clear();while (1){OLED_PutChar(0, 0, 'A');OLED_PutChar(1, 0, 'Y');OLED_PrintString(0, 2, "Hello World!");OLED_PrintSignedVal(0, 4, cnt);cnt = add(cnt, 1);}
}
3-3-1 堆的概念
1. 堆的概念
堆就是一块空闲的内存,可以管理这块内存,从这块内存中取出一部分,用完后再将其释放出去;
heap_buf就是一块空闲的内存,当我们在这块空闲的内存上实现malloc函数的时候,这块空闲的内存就被成为堆;
char heap_buf[1024];
int pos = 0;void *my_malloc(int size)
{int old_pos = pos;pos += size;return &heap_buf[old_pos];
}
//简略方法实现,并不能真正实现free的功能
void my_free(void *buf)
{/* err */
}
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;return 0;
}
问题:我们上述简单的堆为什么无法实现free功能?
先分配buf开头的100字节,再分配buf2开头的100字节;之后调用my_free(buf),my_free函数只知道这块内存从何处开始,并不知道到哪里停止,不知道要释放多少;
2. 一般堆如何实现free函数?
1.malloc不仅仅分配100字节的内存,还会分配一个在头部的结构体;
2.buf仍然指向分配内存的起始位置,free函数得到buf参数后,再减去前面结构体的大小,就找到了头部,头部中存有size的值,知道要释放的内存为多大;
一个更为详细的例子:
要管理这些空闲内存,就要引入链表,这个链表是一个结构体,包括大小和一个指向下一块内存起始位置的指针;定义一个全局变量g_head。
初始状态下,结构图如下图所示:
要注意头部结构体和自己定义的链表结构体;
这里这两个结构体应该是一样的??感觉g_head是模拟已分配内存的头部结构体;
结构体中有size和next_free;
第一次分配100字节结构图如下图所示:
第一次分配50字节结构图如下图所示:
释放操作:
释放第一块内存,此刻空闲内存中next_free不在指向null,而是指向释放的第一块内存的头部位置;当再有申请内存时,先考虑最后面的空闲内存,若空间不够,在考虑第一次释放的内存空间,若空间再不够,则返回err。
3-3-2 栈的概念:函数调用
栈:也是一块内存空间,cpu的sp寄存器指向他,他可以用于函数调用、局部变量、多任务系统里保存现场;
int g_cnt = 0;
int b_func(volatile int a)
{a+=2;return a;
}
int c_func(volatile int a)
{a+=3;return a;
}
void a_func(volatile int a)
{g_cnt = b_func(a);g_cnt = c_func(g_cnt );
}
int main()
{volatile int i = 99;a_func(i);return 0;
}
对上述代码进行反汇编,调用子函数的本质是调用BL指令,包括2个寄存器:LR和PC,LR保存下一条指令的运行地址,PC保存函数a的地址;
问题1:LR被覆盖了怎么办?
在c入口处,划分出自己的栈;保存LR进入栈中;保存局部变量;
在每个函数的入口,都能看到PUSH[r0, lr]的进栈操作,函数一开始,就会将lr的值保存仅栈中;
1.一开始main函数push[r3-r6, lr];
2.之后调用函数a,push[r0, lr];
3.函数a中又调用函数b,push[r0, lr];
4.函数b运行完调用函数c,函数b最后调用pop[r3,pc],其空间被回收,所以函数c的lr和r0位置如下图所示;即在原来函数b的位置处;
3-3-3 栈的概念:局部变量
问题2:局部变量在栈中如何分配?
1.volatile防止变量被编译器优化,直接放入栈中,如i;
2.没有加volatile的,变量少的时候会被编译器优化,将变量直接放入寄存器中,如ch;
3.当变量足够多的时候,寄存器不够,变量必定会被保存到栈中;
3-3-4 栈的概念:RTOS如何使用栈
有2个任务,任务A和任务B,他们两个中的局部变量不一样;
问题:为何每个rtos任务都有属于自己的栈?
对函数b进行反汇编,假设任务A运行到黑线处被切换;任务B运行到红线处被切换;
在进行任务切换的时候要保存现场;
任务A在被切换的时候,不知道何时被切换,不知道应该保存哪些信息?
保存现场:
1.将所有的寄存器全部保存到自己的栈中比如PC,ro等;
2.在任务结构体中记录指针sp的位置;
3.也保存自己的局部变量和调用关系,如cnt;
恢复现场:
1.找到恢复任务的结构体,得到sp指针,将保存到栈中的信息全部恢复到cpu硬件上;
四、freertos源码、内存管理
4-1 FreeRTOS源码概述
详情请见第7章 FreeRTOS源码概述 | 百问网 (100ask.net)
1. 目录结构
2. 头文件
3. 数据类型
4. 变量名前缀
4-2 内存管理
详情请见:第8章 内存管理 | 百问网 (100ask.net)
五、任务基础操作
5-1-1 创建任务_声光色影
1.动态任务创建
configSTACK_DEPTH_TYPE :uint16_t
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数const char * const pcName, // 任务的名字const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,,10表示20字节void * const pvParameters, // 调用任务函数时传入的参数UBaseType_t uxPriority, // 优先级TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
2.静态任务创建
StackType_t :uint32_t
pxTaskBuffer: 静态分配的StaticTask_t结构体的指针
返回值: 成功:返回任务句柄; 失败:NULL
TaskHandle_t xTaskCreateStatic ( TaskFunction_t pxTaskCode, // 函数指针, 任务函数const char * const pcName, // 任务的名字const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节void * const pvParameters, // 调用任务函数时传入的参数UBaseType_t uxPriority, // 优先级StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个bufferStaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);
static StackType_t g_pucStackOfLightTask[128];
static StaticTask_t g_TCBofLightTask;
static TaskHandle_t xLightTaskHandle;static StackType_t g_pucStackOfColorTask[128]; //一共是128*4字节
static StaticTask_t g_TCBofColorTask;
static TaskHandle_t xColorTaskHandle;osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes =
{.name = "defaultTask",.stack_size = 128 * 4,.priority = (osPriority_t) osPriorityNormal,
};void StartDefaultTask(void *argument);
void MX_FREERTOS_Init(void); void MX_FREERTOS_Init(void)
{TaskHandle_t xSoundTaskHandle;BaseType_t ret;defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);extern void PlayMusic(void *params);/* 创建任务: 音 */ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal, &xSoundTaskHandle);/* 创建任务: 光 */xLightTaskHandle = xTaskCreateStatic(Led_Test, "LightTask", 128, NULL, osPriorityNormal, g_pucStackOfLightTask, &g_TCBofLightTask);/* 创建任务: 色 */xColorTaskHandle = xTaskCreateStatic(ColorLED_Test, "ColorTask", 128, NULL, osPriorityNormal, g_pucStackOfColorTask, &g_TCBofColorTask);
}void StartDefaultTask(void *argument)
{LCD_Init();LCD_Clear();for(;;){IRReceiver_Test(); /* 影 */}
}
5-1-2 创建任务_估算栈的大小
5-2 使用任务参数
当程序中没有包含mdelay函数时,屏幕上只有任务三在运行?
任务切换时,极大概率处在lcd打印信息时,此刻g_LCDCanUse =0;即使切换到其他任务,也无法继续运行下面的打印程序;
加上mydelay函数时,任务切换时,极大概率处在延时函数中,此刻g_LCDCanUse =1,当切换到其他任务时,可以打印信息;
struct TaskPrintInfo {uint8_t x;uint8_t y;char name[16];
};
static struct TaskPrintInfo g_Task1Info = {0, 0, "Task1"};
static struct TaskPrintInfo g_Task2Info = {0, 3, "Task2"};
static struct TaskPrintInfo g_Task3Info = {0, 6, "Task3"};
static int g_LCDCanUse = 1;void LcdPrintTask(void *params)
{struct TaskPrintInfo *pInfo = params;uint32_t cnt = 0;int len;while (1){if (g_LCDCanUse) //使用一个全局变量进行保护,防止iic传输被打断{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(500);}
}
void MX_FREERTOS_Init(void)
{TaskHandle_t xSoundTaskHandle;BaseType_t ret;LCD_Init();LCD_Clear();/* 使用同一个函数创建不同的任务 */xTaskCreate(LcdPrintTask, "task1", 128, &g_Task1Info, osPriorityNormal, NULL);xTaskCreate(LcdPrintTask, "task2", 128, &g_Task2Info, osPriorityNormal, NULL);xTaskCreate(LcdPrintTask, "task3", 128, &g_Task3Info, osPriorityNormal, NULL);
}
问题:如何互斥的访问的lcd,使用全局变量,大概率可以,但不是万无一失?
问题:为什么最后创建的任务三最先运行?
5-5-2 任务管理与调度
1. 高优先级的任务未执行完,低优先级的任务无法运行;
2. 一旦高优先级任务就绪,马上运行;
freertos是通过链表管理任务的调度的;
1. 就绪列表:pxReadyTasksLists[56],已经就绪的任务放入此链表;
2. 堵塞列表:xDelayedTaskList,被堵塞的任务放入此链表;
3. 挂起列表:xSuspendedTaskList,被挂起的任务放入此链表;
void StartDefaultTask(void *argument)
{uint8_t dev, data;int len;int bRunning;TaskHandle_t xSoundTaskHandle = NULL;BaseType_t ret;LCD_Init();LCD_Clear();IRReceiver_Init();LCD_PrintString(0, 0, "Waiting control");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;}}}}
}
开启任务调度时会自己创建一个空闲任务prvIdleTask,优先级为0;
1. 创建任务时,先创建默认任务,pxCurrentTCB指向默认任务;在创建led任务,pxCurrentTCB指向led任务;在创建彩色led任务,pxCurrentTCB指向彩色led任务。所以彩色led任务先开始运行;
2. 默认任务、led任务、彩色led任务优先级都为24,都被放在24这个链表中;
假设Tick为1ms:
1. 任务优先级一样时,tick时间到时,发起任务调度遍历就绪列表,从最高优先级55到最低优先级0开始扫描,若非空,则运行;
2. pxCurrentTCB一开始指向任务三,时间到,遍历就绪列表,发现24项非空,pxCurrentTCB指向任务一;
3. 一直再到任务一运行时,0.1ms后用户按下按键,手动创建一个优先级25的任务五,此刻任务五就绪马上运行,直到执行堵塞函数;
4. 此刻任务五从就绪链表中删除,加入到堵塞链表中,触发任务调度;
5. 任务二运行,任务三运行,耗费2个tick;
6. 此刻任务五堵塞时间到,从堵塞列表中删除,加入到就绪列表中,优先级25,马上运行;
7. 直到运行到堵塞函数时,又发起任务调度,此刻任务一运行;
8. 任务一用户按下按键,手动挂起任务五,任务五从就绪列表中移除,加入到挂起列表中;
Tick中断作用:
1. cnt技数++,判断时间是否到了;
2. 判断堵塞列表中任务堵塞时间是否到达;
3. 发起任务调度(时间到发起调度、调用堵塞函数也发起调度);
5-5-3 空闲任务
如果不是死循环的话,退出的话会调用右侧的这个函数,这个函数会关闭所有中断并运行一个空的死循环,所有的任务都无法执行;
1. 一个任务想要退出的话,必须自杀或者他杀,自杀由空闲任务释放其内存;他杀由他任务进行释放;
2. 空闲任务只有运行和就绪两种状态;
六、同步与互斥
6-1 有缺陷的同步示例
1.有2个任务,计算任务和打印任务,他们的优先级一样;
2.若打印任务中没有vTaskDelay(3000),最后打印结果约2.5s左右,每个任务占1.2s左右;打印任务只是一直在while循环判断,占用资源;
3.若加上vTaskDelay(3000),堵塞打印任务,最后显示打印结果为1.2s左右;
若能一开始只有计算任务,打印任务处于堵塞状态;计算完成后,发送一个通知给打印任务,打印任务开始运行,提高效率;
static int g_LCDCanUse = 1;
static volatile int g_calc_end = 0;
static uint64_t g_time = 0;
static uint32_t g_sum = 0;void CalcTask(void *params)
{uint32_t i = 0;LCD_PrintString(0, 0, "Waiting");g_time = system_get_ns();for (i = 0; i < 10000000; i++){g_sum += i;}g_calc_end = 1;g_time = system_get_ns() - g_time;vTaskDelete(NULL);
}void LcdPrintTask(void *params)
{int len;while (1){LCD_PrintString(0, 0, "Waiting");vTaskDelay(3000);while (g_calc_end == 0);/* 打印信息 */if (g_LCDCanUse){g_LCDCanUse = 0;LCD_ClearLine(0, 0);len = LCD_PrintString(0, 0, "Sum: ");LCD_PrintHex(len, 0, g_sum, 1);LCD_ClearLine(0, 2);len = LCD_PrintString(0, 2, "Time(ms): ");LCD_PrintSignedVal(len, 2, g_time/1000000);g_LCDCanUse = 1;}vTaskDelete(NULL);}
}
6-2 有缺陷的互斥示例
若任务在运行到108行时被切换,会出现打印混乱的情况;
改进: 加上关闭中断的操作,以此达到互斥的功能;
但又带来同步的问题, 任务A可以一直打印,不受任务B的干扰;但任务B在不停的一直判断任务A是否已经打印完成,浪费资源;
我们想要实现的是任务A一直执行打印,任务B访问一次失败后进入堵塞,直到任务A完成后唤醒任务B;
八、队列
8-1-2 队列的本质
队列中,数据的读写本质就是环形缓冲区,在此基础上增加互斥、阻塞-唤醒机制;
队列:环形buffer + 两个链表(发送者链表和接受者链表);
任务A写,任务B读;以任务A写队列为例:
情况一:无堵塞时间
1.任务A写队列,若队列未满,从xQueueSend返回,处理下面的信息;
2.若队列满,从xQueueSend返回一个err;
情况二:指定一个堵塞时间(比如30分钟)
1. 任务A写队列,队列满,任务A进入堵塞列表,并且愿意等待,进入Queue.SendList的列表中;此刻还没到30分钟时,任务B接收数据并唤醒任务A(将任务A从堵塞列表和Queue.SendList的列表中移除),移入就绪列表中,待任务A运行时可从xQueueSend返回,处理下面的信息;
2.任务A写队列,队列满,任务A进入堵塞列表,并且愿意等待,进入Queue.SendList的列表中;时间到30分钟时,堵塞列表唤醒任务A(将任务A从堵塞列表和Queue.SendList的列表中移除),从xQueueSend返回一个err;
情况三:堵塞时间无限
1.队列满,当任务B往队列中读取数据时,同情况二的1,不存在堵塞时间;
2.队列满,任务B没有数据,任务A永远堵塞;
任务A写,任务B读;以任务B读队列为例:
情况一:无堵塞时间
1.任务B立即读队列,若队列中有数据,从xQueueReceive返回,处理下面的信息;
2.若无数据,从xQueueReceive返回一个err;
情况二:指定一个堵塞时间(比如30分钟)
1. 任务B立即读队列,队列无数据,任务B进入堵塞列表,并且愿意等待,进入 Queue.ReceiveList的列表中;此刻还没到30分钟时,任务A发送数据并唤醒任务B(将任务B从堵塞列表和Queue.ReceiveList的列表中移除),移入就绪列表中,待任务B运行时可从xQueueReceive返回,处理下面的信息;
2.任务B立即读队列,队列无数据,任务B进入堵塞列表,并且愿意等待,进入 Queue.ReceiveList的列表中;时间到30分钟时,堵塞列表唤醒任务B(将任务B从堵塞列表和Queue.ReceiveList的列表中移除),从xQueueReceive返回一个err;
情况三:堵塞时间无限
1.当任务A往队列中发送数据时,同情况二的1,不存在堵塞时间;
2.任务A不往队列中发送数据,任务B永远堵塞;
task_B()
{while(1){xQueueReceive(....);}//处理
}
8-2-1 队列实验_红外传感器
队列接收一般堵塞时间设置成无限等待;
xQueueReceive(g_xQueueRotary, &rdata, portMAX_DELAY);
队列发送一般堵塞时间设置成0;
xQueueSendFromISR(g_xQueuePlatform, &data, NULL);
原程序中挡球板任务通过if (0 == IRReceiver_Read(&dev, &data))判断语句一直查询是否接收到数据,极大浪费了资源;若改成红外传感器中断函数向一个队列中写数据,挡球板任务从该队列中读取数据,若队列中无数据时进入堵塞状态;
游戏任务中创建挡球板任务和队列:
void game1_task(void *params)
{ uint8_t dev, data, last_data;/* 创建队列,挡球板任务 */g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data));xTaskCreate(platform_task, "platform_task", 128, NULL, osPriorityNormal, NULL);
}
红外传感器中断函数中的队列发送:
void IRReceiver_IRQ_Callback(void)
{/* ...... */if (isRepeatedKey()){/* device: 0, val: 0, 表示重复码 *///PutKeyToBuf(0);//PutKeyToBuf(0);/* 写队列 */data.dev = 0;data.val = g_last_val;xQueueSendFromISR(g_xQueuePlatform, &data, NULL);g_IRReceiverIRQ_Cnt = 0;}/* ...... */
}
挡球板任务中的队列接收:
/* 挡球板任务 */
static void platform_task(void *params)
{ struct input_data idata;while (1){//if (0 == IRReceiver_Read(&dev, &data))xQueueReceive(g_xQueuePlatform, &idata, portMAX_DELAY);//处理数据....}
}
8-2-2 队列实验_旋转编码器
旋转编码器中断函数产生的数据需要再次处理才能发送到挡球板队列中,这个处理需要花费很长时间,因此在创建一个编码器任务和队列B:
1.编码器任务专门处理旋转编码器中断函数传来的数据,处理好后再发送到队列A;
2.队列B用于旋转编码器中断函数和编码器任务的通信;
游戏任务中创建编码器任务和队列B:
void game1_task(void *params)
{ uint8_t dev, data, last_data;/* 创建队列,任务 */g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data));g_xQueueRotary = xQueueCreateStatic(10, sizeof(struct rotary_data),g_ucQueueRotaryBuf, &g_xQueueRotaryStaticStruct);xTaskCreate(platform_task, "platform_task", 128, NULL, osPriorityNormal, NULL);xTaskCreate(RotaryEncoderTask, "RotaryEncoderTask", 128, NULL, osPriorityNormal, NULL);
}
编码器中断函数中的队列发送:
void RotaryEncoder_IRQ_Callback(void)
{struct rotary_data rdata;//.../* 写队列 */rdata.cnt = g_count;rdata.speed = g_speed;xQueueSendFromISR(g_xQueueRotary, &rdata, NULL);
}
编码器任务中读取编码器队列和向挡球板队列发送:
static void RotaryEncoderTask(void *params)
{struct rotary_data rdata;struct input_data idata;while (1){/* 读旋转编码器队列 */xQueueReceive(g_xQueueRotary, &rdata, portMAX_DELAY);/* 处理数据 *//* 写挡球板队列 */idata.dev = 1;idata.val = left ? UPT_MOVE_LEFT : UPT_MOVE_RIGHT;for (i = 0; i < cnt; i++){xQueueSend(g_xQueuePlatform, &idata, 0);}}
}
8-3-1 队列集
若再添加一个硬件设备mpu6050,需要再创建一个任务mpu6050task,按照这种方式,当硬件多的时候是及其浪费资源的;
换成这种方式:
将上述中每一个硬件就创建一个任务换成:所有任务变成一个inputTask;但inputTask又如何能及时读取到这三个队列中的数据?
使用轮询方式:
若队列一的接收函数中堵塞时间假设设置成30,就会堵塞,影响后续队列的运行;
InputTask()
{while(1){xQueueReceive1();//队列一处理xQueueReceive2();//队列二处理xQueueReceive3();//队列三处理}
}
1.创建任务A、B;
2.创建队列集;
3.将任务A、B的句柄加入到队列集中;
4.InputTask();
队列集中的个数 == 每个队列中的数据;
中断向队列A写数据的时候,顺便将队列A的句柄写入队列集;
中断向队列B写数据的时候,顺便将队列B的句柄写入队列集;
InputTask()
{while(1){句柄 = 读队列集,得到任务句柄;读句柄,得到数据;}
}
8-3-2 队列集实验
创建任务、队列、队列集
void game1_task(void *params)
{ uint8_t dev, data, last_data;/* 创建挡球板队列、队列集 */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);/* 创建input任务和挡球板任务 */xTaskCreate(InputTask, "InputTask", 128, NULL, osPriorityNormal, NULL);xTaskCreate(platform_task, "platform_task", 128, NULL, osPriorityNormal, NULL);
}
红外和编码器中断函数写队列操作省略:
注意:往队列写数据时会将此队列句柄写入到队列集中;
xQueueSelectFromSet:读队列集操作
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();}}}
}
读取到红外队列句柄,执行xQueueReceive(g_xQueueIR, &idata, 0);得到数据;
无需等待,因为队列中一定有数据;
数据处理后,写挡球板队列xQueueSend(g_xQueuePlatform, &input, 0);
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);
}
读取到编码器队列句柄,执行xQueueReceive(g_xQueueRotary, &rdata, 0);得到数据;
无需等待,因为队列中一定有数据;
数据处理后,写挡球板队列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;elsecnt = 1;/* 写挡球板队列 */idata.dev = 1;idata.val = left ? UPT_MOVE_LEFT : UPT_MOVE_RIGHT;for (i = 0; i < cnt; i++){xQueueSend(g_xQueuePlatform, &idata, 0);}
}
8-3-3 队列实验_姿态控制
可能出现的问题:
mpu6050的队列还未放入队列集中,MPU6050_Task已经运行,将队列写满;
game1_task运行时,因为mpu6050队列满无法写队列,导致mpu6050队列无法加入到队列集中;
void MX_FREERTOS_Init(void)
{/* 创建任务 */extern void PlayMusic(void *params);extern void MPU6050_Task(void *params);xTaskCreate(game1_task, "GameTask", 128, NULL, osPriorityNormal, NULL);xTaskCreate(PlayMusic, "MusicTask", 128, NULL, osPriorityNormal, NULL);xTaskCreate(MPU6050_Task, "MPU6050Task", 128, NULL, osPriorityNormal, NULL);}
改正:
先将MPU6050的队列加入到队列集中,在创建MPU6050任务;
void game1_task(void *params)
{ xQueueAddToSet(g_xQueueIR, g_xQueueSetInput);xQueueAddToSet(g_xQueueRotary, g_xQueueSetInput);xQueueAddToSet(g_xQueueMPU6050, g_xQueueSetInput);xTaskCreate(MPU6050_Task, "MPU6050Task", 128, NULL, osPriorityNormal, NULL);xTaskCreate(InputTask, "InputTask", 128, NULL, osPriorityNormal, NULL);
}
各个函数放在哪里?
以红外为例:
红外队列创建放在红外初始化中;
写红外队列放在中断函数中;
队列集创建、队列加入队列集函数、挡球板任务创建放在game1_task中;
读队列集得句柄放在InputTask函数中;
读红外队列、写挡球板队列放在InputTask函数中的子函数ProcessIRData中;