# FreeRTOS-学习 **Repository Path**: he_wenxiang/freertos-learning ## Basic Information - **Project Name**: FreeRTOS-学习 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-12-15 - **Last Updated**: 2024-12-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # FreeRTOS-学习 ​ 2024年12月16日 ## 3-1创建一个多任务程序 创建任务的函数 ```c 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 ); // 任务句柄, 以后使用它来操作这个任务 ``` 先写一个任务 ```c /* Private function prototypes -----------------------------------------------*/ /* USER CODE BEGIN FunctionPrototypes */ void MyTask(void *argument) { while(1) { OLED_Test(); } } /* USER CODE END FunctionPrototypes */ ``` 默认生成的任务(osThreadNew是重新封装的接口方便不同的操作系统 FreeRTOS RT-s...) ```c /* creation of defaultTask */ defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); ``` 照猫画虎 osPriorityNormal 优先级从默认任务抄的 ```c /* USER CODE BEGIN RTOS_THREADS */ /* add threads, ... */ xTaskCreate(MyTask,"myfisttask",128,NULL,osPriorityNormal,NULL); /* USER CODE END RTOS_THREADS */ ``` ## 3-2 ARM架构简明教程 ### 1、硬件架构与汇编指令 汇编指令 - 读内存Load ```assembly # 示例 LDR R0, [R1,#4]; 读地址“R1+4”,得到4字节数据存入R0 ``` - 写内存 ```assembly # 示例 STR R0, [R1,#4]; 把R0的四字节写入地址“R1+4” ``` - 加减 ```assembly ADD R0, R1 ,R2 ;R0=R1+R2 ADD R0, R1 ,#1 ;R0=R1+1 SUB R0, R1 ,R2 ;R0=R1-R2 SUB R0, R1 ,#1 ;R0=R1-1 ``` - 比较 ```assembly CMP R0, R1 ; 结果保存在PSR(程序状态寄存器) ``` - 跳转 ```assembly B main ; Branch 直接跳转 BL main ; Branch and Link 先把返回地址保存在LR寄存器再跳转 ``` ### 2、汇编实例 C函数: ```c int add(volatile int a, volatile int b) { volatile int sum; sum = a + b; return sum; } ``` 让Keil生成反汇编: 为例方便复制,制作反汇编的指令如下: ```shell fromelf --text -a -c --output=xxx.dis xxx.axf ``` C函数add的反汇编代码如下: ```shell i.add add 0x08002f34: b503 .. PUSH {r0,r1,lr} 0x08002f36: b081 .. SUB sp,sp,#4 0x08002f38: e9dd0101 .... LDRD r0,r1,[sp,#4] 0x08002f3c: 4408 .D ADD r0,r0,r1 0x08002f3e: 9000 .. STR r0,[sp,#0] 0x08002f40: bd0e .. POP {r1-r3,pc} ``` ## 3-3 堆和栈 ### 1、堆的概念 所谓堆就是一块空闲的内存,你可以来管理这一块内存,从这块内存中取出一部分,用完之后再把它释放回去 #### 2、栈的概念_局部变量 栈也是一块内存空间,CPU的SP寄存器指向它,它可以用于函数调用,局部变量,多任务系统里保存现场。 #### 3、栈的概念_RTOS如何使用栈 为什么每个任务都要有自己的栈? 每个任务都有自己的调用关系 每个任务都有自己的局部变量 保护现场 恢复现场 ## 4-1 FreeROTS源码 详见PDF第七章 ## 4-2内存管理 对几个heap的理解见 详见PDF ## 5-1创建任务 ### 任务 1. 做事情:函数 2. 栈 和 TCB: malloc/静态分配 3. 优先级 使用链表来管理任务 任务控制块 TCB结构体 Task Control Block ### 创建任务函数 #### 动态分配内存的函数 ```c 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 ); // 任务句柄, 以后使用它来操作这个任务 ``` | 参数 | 描述 | | ------------- | ------------------------------------------------------------ | | pvTaskCode | 函数指针, 可以简单地认为任务就是一个 C 函数。 它稍微特殊一点: 永远不退出, 或者退出时要调用"vTaskDelete(NULL)" | | pcName | 任务的名字, FreeRTOS 内部不使用它, 仅仅起调试作用。 长度为: configMAX_TASK_NAME_LEN | | usStackDepth | 每个任务都有自己的栈, 这里指定栈大小。 单位是 word, 比如传入 100, 表示栈大小为 100 word, 也就是 400 字节。 最大值为 uint16_t 的最大值。 怎么确定栈的大小, 并不容易, 很多时候是估计。 精确的办法是看反汇编码。 | | pvParameters | 调用 pvTaskCode 函数指针时用到: pvTaskCode(pvParameters) | | uxPriority | 优先级范围: 0~(configMAX_PRIORITIES – 1) 数值越小优先级越低, 如果传入过大的值, xTaskCreate 会把它调整为(configMAX_PRIORITIES – 1) | | pxCreatedTask | 用来保存 xTaskCreate 的输出结果: task handle。 以后如果想操作这个任务, 比如修改它的优先级, 就需要这个 handle。 如果不想使用该 handle, 可以传入 NULL。 | | 返回值 | 成功: pdPASS; 失败: errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足) 注意: 文档里都说失败时返回值是 pdFAIL, 这不对。 pdFAIL 是 0, errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 是-1。 | #### 静态分配内存的函数 ```c 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, // 静态分配的栈, 就是一个buffer StaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针, 用它来操作这个任务 ); ``` | 参数 | 描述 | | -------------- | ------------------------------------------------------------ | | pvTaskCode | 函数指针, 可以简单地认为任务就是一个 C 函数。 它稍微特殊一点: 永远不退出, 或者退出时要调用"vTaskDelete(NULL)" | | pcName | 任务的名字, FreeRTOS 内部不使用它, 仅仅起调试作用。 长度为: configMAX_TASK_NAME_LEN | | usStackDepth | 每个任务都有自己的栈, 这里指定栈大小。 单位是 word, 比如传入 100, 表示栈大小为 100 word, 也就是 400 字节。 最大值为 uint16_t 的最大值。 怎么确定栈的大小, 并不容易, 很多时候是估计。 精确的办法是看反汇编码。 | | pvParameters | 调用 pvTaskCode 函数指针时用到: pvTaskCode(pvParameters) | | uxPriority | 优先级范围: 0~(configMAX_PRIORITIES – 1) 数值越小优先级越低, 如果传入过大的值, xTaskCreate 会把它调整为(configMAX_PRIORITIES – 1) | | puxStackBuffer | 静态分配的栈内存, 比如可以传入一个数组, 它的大小是 usStackDepth*4。 | | pxTaskBuffer | 静态分配的 StaticTask_t 结构体的指针 | | 返回值 | 成功: 返回任务句柄; 失败: NULL | ### 1、声光色影 ```c //动态分配内存的函数 TaskHandle_t pxmusicTask;//句柄 BaseType_t ret;//返回值 //静态分配内存的函数 static StackType_t g_pucStackOfLightTask[128];//栈 static StaticTask_t g_TCBofLightTask;//任务控制块 TCB结构体 static TaskHandle_t g_LightHandle;//句柄 static StackType_t g_pucStackOfcolorledTask[128]; static StaticTask_t g_TCBofcolorledTask; static TaskHandle_t g_colorledHandle; //1.创建任务 声 extern void Playmusic(void *params); ret = xTaskCreate(Playmusic, "musicTask", 128, NULL, osPriorityNormal, pxmusicTask); //2.创建任务 光 g_LightHandle = xTaskCreateStatic(Led_Test, "LightTask", 128, NULL, osPriorityNormal, g_pucStackOfLightTask,&g_TCBofLightTask); //3.创建任务 色 g_colorledHandle = xTaskCreateStatic(ColorLED_Test, "colorledTask", 128, NULL, osPriorityNormal, g_pucStackOfcolorledTask,&g_TCBofcolorledTask); ``` ### 2、估算栈大小 栈 1. 返回地址 2. 局部变量 3. 保存现场 ## 5-2创建任务 使用任务参数 首先创建一个LCD打印函数 ```c //创建一个结构体来传输参数 struct PrintTaskInfo { uint8_t x; uint8_t y; char name[16]; }; struct PrintTaskInfo g_TaskInfo1={0, 0, "Task1"}; struct PrintTaskInfo g_TaskInfo2={0, 3, "Task2"}; struct PrintTaskInfo g_TaskInfo3={0, 6, "Task3"}; //创建一个全局变量来判断lcd是否可以被使用 uint8_t LCDCanUse = 1; //不可靠的 //创建Lcd打印函数 void LcdPrint(void *params) { struct PrintTaskInfo *pInfo = params; uint8_t cnt=0; int len; while(1) { if(LCDCanUse==1) { LCDCanUse=0; //打印信息 len = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name); len += LCD_PrintString(len,pInfo->y,":"); LCD_PrintSignedVal(len,pInfo->y,cnt++); LCDCanUse=1; } mdelay(10); //添加一定的延时增加其他任务使用lcd的概率 } } ``` 然后创建三个任务 ```c //使用同一个函数创建不同任务 xTaskCreate(LcdPrint,"Task1", 128, &g_TaskInfo1, osPriorityNormal, NULL); xTaskCreate(LcdPrint,"Task1", 128, &g_TaskInfo2, osPriorityNormal, NULL); xTaskCreate(LcdPrint,"Task1", 128, &g_TaskInfo3, osPriorityNormal, NULL); ``` 其中 "&g_TaskInfo" 就是传给 "LcdPrint" 的参数 ## 5-3 删除任务 频繁的删除创建任务不好 频繁的申请内存释放内层容易造成内存碎片 删除任务后任务就停止了 例子:蜂鸣器播放音乐删除任务后会保持删掉时的音调一直响 ### 删除任务时使用的函数 ```c void vTaskDelete( TaskHandle_t xTaskToDelete ); ``` | 参数 | 描述 | | ---------- | ------------------------------------------------------------ | | pvTaskCode | 任务句柄, 使用 xTaskCreate 创建任务时可以得到一个句柄。 也可传入 NULL, 这表示删除自己。 | 怎么删除任务? 举个不好的例子:  自杀: vTaskDelete(NULL)  被杀: 别的任务执行 vTaskDelete(pvTaskCode), pvTaskCode 是自己的句柄  杀人: 执行 vTaskDelete(pvTaskCode), pvTaskCode 是别的任务的句柄 ### 程序 ```c TaskHandle_t pxmusicTaskHand = NULL; BaseType_t ret; void IRReceiver_Test111(void) { //1:0x30 //2:0x18 uint8_t dev, data; int len; IRReceiver_Init(); while (1) { //读取红外遥控器 if (0==IRReceiver_Read(&dev, &data)) { if(data==0x30) { //创建任务 extern void Playmusic(void *params); if(pxmusicTaskHand == NULL) { LCD_PrintString(0,0,"yes music"); ret = xTaskCreate(Playmusic, "musicTask", 128, NULL, osPriorityNormal, &pxmusicTaskHand);//创建任务 } } else if(data==0x18) { //删除任务 if(pxmusicTaskHand != NULL) { LCD_PrintString(0,0,"no music"); vTaskDelete(pxmusicTaskHand);//删除任务函数 pxmusicTaskHand = NULL; PassiveBuzzer_Control(0);//关闭蜂鸣器 解决任务停止蜂鸣器还响 } } } } } ``` ## 5-4 改善播放效果-优先级与阻塞 修改地方 提升任务的优先级 使用vTaskDelay函数进行延时(主动放弃cpu资源) osPriorityNormal-->osPriorityNormal+1 ```c ret = xTaskCreate(Playmusic, "musicTask", 128, NULL, osPriorityNormal+1, &pxmusicTaskHand); ``` HAL_Delay-->vTaskDelay ```c void MUSIC_Analysis(void){ //切换数组即可更改音乐 uint16_t MusicBeatNum = ((((sizeof(Music_Lone_Brave))/2)/3)-1); uint16_t MusicSpeed = Music_Lone_Brave[0][2]; for(uint16_t i = 1;i<=MusicBeatNum;i++){ //BSP_Buzzer_SetFrequency(Tone_Index[Music_Lone_Brave[i][0]][Music_Lone_Brave[i][1]]); PassiveBuzzer_Set_Freq_Duty(Tone_Index[Music_Lone_Brave[i][0]][Music_Lone_Brave[i][1]],50); //HAL_Delay(MusicSpeed/Music_Lone_Brave[i][2]); vTaskDelay(MusicSpeed/Music_Lone_Brave[i][2]); } } ``` ## 5-5 任务状态与调度理论的讲解 (最重要的一环) 重点:使用链表讲解内部原理 - 任务状态:添加音乐暂停/恢复功能 - 优先级、链表管理 - 任务切换、tick ### 在FreeRTOS中任务只有四种状态 ready 就绪状态 runing 运行状态 Blocked 阻塞状态(等待某些事件) Suspend 暂停 #### **任务状态转换图** ![](pic/任务状态转换图.png) ### 1、任务状态-改进播放控制 添加暂停恢复功能 vTaskSuspend(pxmusicTaskHand);//暂停 vTaskResume(pxmusicTaskHand);//恢复 ```c void IRReceiver_Test111(void) { //1:0x30 //2:0x18 uint8_t dev, data; int len; uint8_t bRuning;//运行状态标志 IRReceiver_Init(); LCD_PrintString(0,0,"ready music");//任务准备就绪 while (1) { //读取红外遥控器 if (0==IRReceiver_Read(&dev, &data)) { if(data==0x30) { //创建任务 extern void Playmusic(void *params); if(pxmusicTaskHand == NULL) { LCD_Clear(); LCD_PrintString(0,0,"start music"); ret = xTaskCreate(Playmusic, "musicTask", 128, NULL, osPriorityNormal+1, &pxmusicTaskHand); bRuning=1; } else { if(bRuning)//暂停 { LCD_Clear(); LCD_PrintString(0,0,"suspend music"); vTaskSuspend(pxmusicTaskHand); PassiveBuzzer_Control(0); bRuning=0; } else//恢复 { LCD_Clear(); LCD_PrintString(0,0,"Resume music"); vTaskResume(pxmusicTaskHand); bRuning=1; } } } else if(data==0x18) { //删除任务 if(pxmusicTaskHand != NULL) { LCD_Clear(); LCD_PrintString(0,0,"stop music"); vTaskDelete(pxmusicTaskHand); pxmusicTaskHand = NULL; PassiveBuzzer_Control(0); } } } } } ``` ### 2.、任务调度管理 调度 1. 相同优先级的任务轮流运行 2. 最高优先级的任务先运行 得到 a.高优先级的任务未执行完 优先级低的任务无法运行 b.一旦高优先级任务就绪 马上运行 c.最高优先级的任务有多个他们轮流运行 #### 链表介绍 task.c中有很多链表 ```c /* Lists for ready and blocked tasks. --------------------*/ PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ] = {0}; /*< Prioritised ready tasks. */ PRIVILEGED_DATA static List_t xDelayedTaskList1 = {0}; /*< Delayed tasks. */ PRIVILEGED_DATA static List_t xDelayedTaskList2 = {0}; /*< Delayed tasks (two lists are used - one for delays that have overflowed the current tick count. */ PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList = NULL; /*< Points to the delayed task list currently being used. */ PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList = NULL; /*< Points to the delayed task list currently being used to hold tasks that have overflowed the current tick count. */ PRIVILEGED_DATA static List_t xPendingReadyList = {0}; /*< Tasks that have been readied while the scheduler was suspended. They will be moved to the ready list when the scheduler is resumed. */ #if( INCLUDE_vTaskDelete == 1 ) PRIVILEGED_DATA static List_t xTasksWaitingTermination = {0}; /*< Tasks that have been deleted - but their memory not yet freed. */ PRIVILEGED_DATA static volatile UBaseType_t uxDeletedTasksWaitingCleanUp = ( UBaseType_t ) 0U; #endif #if ( INCLUDE_vTaskSuspend == 1 ) PRIVILEGED_DATA static List_t xSuspendedTaskList = {0}; /*< Tasks that are currently suspended. */ #endif ``` task.c 351 ```c PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ] = {0}; /*< Prioritised ready tasks. */ ``` configMAX_PRIORITIES=56 ```c //就有56个链表 pxReadyTasksLists[55] pxReadyTasksLists[54] pxReadyTasksLists[53] . . pxReadyTasksLists[N]//这里面放的就是优先级为N的处于runing/ready的任务 . . pxReadyTasksLists[24]//osPriorityNormal=24 这里面就放了 ①StartDefaultTask ②Led_Test ③ColorLED_Test . pxReadyTasksLists[0]//启动调度器会创建一个优先级为0的空闲任务 ④prvIdleTask ``` #### 创建任务 创建任务的时候会把任务添加到链表(根据优先级选择链表) 在创建任务函数xTaskCreateStatic()中prvAddNewTaskToReadyList()中有个全局变量pxCurrentTCB(是当前任务的TCB) 当创建**StartDefaultTask**任务时pxCurrentTCB指向**StartDefaultTask** 创建**Led_Test**任务时pxCurrentTCB指向**Led_Test** 创建**ColorLED_Test**任务时pxCurrentTCB指向**ColorLED_Test** 当启动调度器的时候创建**prvIdleTask**但是他的**优先级比较低 ** pxCurrentTCB还是指向**ColorLED_Test** #### 同等级任务运行 ③(1ms)——①(1ms)——②(1ms)——③(1ms)——①(1ms) 在RTOS初始化的时候Systick定时器中断频率1000hz 在中断函数里面 1.cnt++ 作为时间基准 ​ 2.并且 发起一次调度 ​ 从上到下遍历 pxReadyTasksLists[ ] 找到不空的 pxReadyTasksLists[24] pxCurrentTCB在③ColorLED_Test 此时让pxCurrentTCB指向下一个要运行的任务①StartDefaultTask ​ 然后再进入中断重复运行 所以当启动调度器任务开始最先开始的是pxCurrentTCB指向**ColorLED_Test**的最后一个任务 #### 运行时创建更高优先级任务 运行①StartDefaultTask 0.1ms时如果创建了更高优先级的⑤Playmusic 那么⑤任务就会立马开始运行 ⑤号任务运行中设置蜂鸣器频率之后马上调用**vTaskDelay()** 假设参数是2 打算阻塞两个tick (这个函数单位是tick每个中断称为tick)这个时候属于Blocked 会把这个任务从pxReadyTasksLists[]删除放入 pxDelayedTaskList[] vTaskDelay()主动放弃运行 会触发调度 :遍历pxReadyTasksLists[]链表找到任务 链表里面有一个记录项index会指向上一个运行的任务 ① 取出下一个任务来运行 ​ 两个tick过后的tick中断a.cnt++ b.判断DealyTasklist[]里任务是否可以恢复 c.发起调度 这个时候⑤号任务恢复了DealyTasklist[]恢复到pxReadyTasksLists[],发起调度运行下一个任务① 假如①任务暂停了⑤任务 ⑤任务就会从DealyTasklist[]移到xSuspendedTaskList[],通过tick无法恢复 只能vTaskResume();把⑤任务xSuspendedTaskList[]移到pxReadyTasksLists[]恢复 ### 3、空闲任务 如果一个任务不是一个死循环 不经处理 运行完会返回到这个函数 port.c 194 ```c static void prvTaskExitError( void ) { /* A function that implements a task must not exit or attempt to return to its caller as there is nothing to return to. If a task wants to exit it should instead call vTaskDelete( NULL ). Artificially force an assert() to be triggered if configASSERT() is defined, then stop here so application writers can catch the error. */ configASSERT( uxCriticalNesting == ~0UL ); portDISABLE_INTERRUPTS(); for( ;; ); } ``` 这个函数会关闭中断然后死循环 所有任务都没法运行了(tick中断没了) 那么任务如何退出? 1、自杀 2、他杀 A杀B:A给B收尸 B自杀:空闲任务收尸 收尸:释放TCB结构体 释放栈 如果其他任务没有主动放弃CPU的话,空闲任务没法运行,如果一直创建任务自杀就会导致内存不足 使用 良好的编程习惯 1,事件驱动 2,延时函数,不要使用死循环 mdealy()就是死循环查询定时器时间到了没 可以更换为vTaskDelay()函数 ## 5-6两个Delay函数 有两个 Delay 函数: vTaskDelay: 至少等待指定个数的 Tick Interrupt 才能变为就绪状态 vTaskDelayUntil: 等待到指定的绝对时刻, 才能变为就绪态。 ```c void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给Tick */ /* pxPreviousWakeTime: 上一次被唤醒的时间 * xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement) * 单位都是Tick Count */ BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement ); ``` ## 6 同步与互斥 1、同步与互斥的概念 一句话理解同步与互斥: 我等你用完厕所, 我再用厕所。 什么叫同步? 就是: 哎哎哎, 我正在用厕所, 你等会。 什么叫互斥? 就是: 哎哎哎, 我正在用厕所, 你不能进来。 同步与互斥经常放在一起讲, 是因为它们之的关系很大, “互斥”操作可以使用“同步”来 实现。 我“等”你用完厕所, 我再用厕所。 这不就是用“同步”来实现“互斥”吗? 再举一个例子。 在团队活动里, 同事A先写完报表, 经理B才能拿去向领导汇报。 经理 B必须等同事A完成报表, AB之间有依赖, B必须放慢脚步, 被称为同步。 在团队活动中, 同事A已经使用会议室了, 经理B也想使用, 即使经理B是领导, 他也得等着, 这就叫互斥。 经理B跟同事A说: 你用完会议室就提醒我。 这就是使用"同步"来实现"互斥" ### 1、有缺陷的同步示例 ```c int g_sum=0;//全局变量计算结果 volatile int g_calc_end=0;//标志计算完成 防止被优化 int g_time=0;//全局变量时间 //任务一 void CalcTask(void *params) { int cnt=0; int len; int sum; g_time = system_get_ns(); for(cnt=0;cnt<1000000;cnt++) { g_sum +=cnt; } g_calc_end=1; g_time = system_get_ns() - g_time; vTaskDelete(NULL); } //任务一 void LcdPrint(void *params) { LCD_PrintString(0,0,"Wating"); while(g_calc_end==0);//等待计算完 LCD_PrintHex(0,0,g_sum,1); LCD_PrintSignedVal(0,2,g_time/1000000); vTaskDelete(NULL); } ``` ### 2、有缺陷的互斥示例 ```c uint8_t LCDCanUse = 1; //lcd是否能被使用 void LcdPrint(void *params) { struct PrintTaskInfo *pInfo = params; uint8_t cnt=0; int len; while(1) { if(LCDCanUse==1) { LCDCanUse=0; //打印信息 len = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name); len += LCD_PrintString(len,pInfo->y,":"); LCD_PrintSignedVal(len,pInfo->y,cnt++); LCDCanUse=1; } mdelay(10); } } ``` ### 3. FreeRTOS的解决方案 * 正确性 * 效率:等待者要进入阻塞状态 * 多种解决方案 ![](pic/13_compare_sync_objects.png) ## 8-1数据的传输方式——环形缓冲 ![](pic/环形缓冲.png)