第15章 FreeRTOS操作系统版本二代示波器实现
本章教程为大家讲解FreeRTOS操作系统版本的二代示波器实现。主要讲解RTOS设计框架,即各个任务实现的功能,任务间的通信方案选择,任务栈,系统栈以及全局变量共享问题。同时,工程调试方法也专门做了说明。
15.1 注意事项(重要必读)
15.2 任务功能划分
15.3 用户任务优先级设置
15.4 全局变量分配,系统堆栈和任务堆栈
15.5 任务间通信和全局变量共享问题
15.6 FreeRTOS系统调试
15.7 MDK优化等级
15.8 总结
15.1 注意事项(重要必读)
1、学习本章节前,务必保证已经学习完毕前面章节。另外,工程代码注释已经比较详细,了解了框架后,直接看源码即可。
2、仅支持800*480分辨率显示屏,如果是电容屏,无需校准。如果是电阻屏,需要校准,按下按键K1即可进入校准界面。
3、由于按键不够用,K1按键的消息处理做了三个条件编译,详情见本章15.6小节。默认K1按键执行触摸校准,也可以选择执行截图或者串口打印任务执行情况。另外,不管当前处于任何界面都可以进行触摸校准,仅电阻屏需要校准,电容屏无需校准。
4、STemWin5.40版本的截图功能有bug,详情看此贴:
。
当前用的5.32版本,也是来自STemWin软件包。
5、FreeRTOS工程的文件系统是采用的FatFS,当前开启了MDK最高等级优化和时间优化。如果大家要使用FatFS功能,请务必关闭时间优化,因为FatFS在时间优化下会工作异常。详情见本章15.7小节。
6、工程编译支持MDK4.7X和MDK5。另外不支持MDK发布的MDK5.24及其以上版本,因为这个版本不支持MDK4创建的工程转换为MDK5了,所以要使用这个最新的版本,需要给MDK5安装MDK4的兼容包。
15.2 任务功能划分
前面第三章已经将任务功能划分好:
根据这个功能划分,创建所需要的任务。
15.2.1 主函数创建
在main.c文件实现:
/*********************************************************************************************************** 函 数 名: main* 功能说明: 标准c程序入口。* 形 参: 无* 返 回 值: 无**********************************************************************************************************/int main(void){ /* 在启动调度前,为了防止初始化STM32外设时有中断服务程序执行,这里禁止全局中断(除了NMI和HardFault)。 这样做的好处是: 1. 防止执行的中断服务程序中有FreeRTOS的API函数。 2. 保证系统正常启动,不受别的中断影响。 3. 关于是否关闭全局中断,大家根据自己的实际情况设置即可。 在移植文件port.c中的函数prvStartFirstTask中会重新开启全局中断。通过指令cpsie i开启,__set_PRIMASK(1) 和cpsie i是等效的。 */ __set_PRIMASK(1); /* 硬件初始化 */ bsp_Init(); /* 1. 初始化一个定时器中断,精度高于滴答定时器中断,这样才可以获得准确的系统信息 仅供调试目的,实际项 目中不要使用,因为这个功能比较影响系统实时性。 2. 为了正确获取FreeRTOS的调试信息,可以考虑将上面的关闭中断指令__set_PRIMASK(1); 注释掉。 */ vSetupSysInfoTest(); /* 创建任务通信机制 */ AppObjCreate(); /* 创建任务 */ AppTaskCreate(); /* 启动调度,开始执行任务 */ vTaskStartScheduler(); /* 如果系统正常启动是不会运行到这里的,运行到这里极有可能是用于定时器任务或者空闲任务的 heap空间不足造成创建失败,此要加大FreeRTOSConfig.h文件中定义的heap大小: #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) */ while(1);}
主函数中做的工作比较多,下面依次为大家做个说明:
调用函数bsp_Init()做硬件初始化
硬件外设的初始化函数bsp_Init是在 bsp.c 文件实现:
/*********************************************************************************************************** 函 数 名: bsp_Init* 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次* 形 参:无* 返 回 值: 无**********************************************************************************************************/void bsp_Init(void){ /* 由于ST固件库的启动文件已经执行了CPU系统时钟的初始化,所以不必再次重复配置系统时钟。 启动文件配置了CPU主时钟频率、内部Flash访问速度和可选的外部SRAM FSMC初始化。 系统时钟缺省配置为168MHz,如果需要更改,可以修改 system_stm32f4xx.c 文件 */ /* 使能CRC 因为使用STemWin前必须要使能 */ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_CRC, ENABLE); /* 优先级分组设置为4,可配置0-15级抢占式优先级,0级子优先级,即不存在子优先级。*/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); SystemCoreClockUpdate(); /* 根据PLL配置更新系统时钟频率变量 SystemCoreClock */ bsp_InitDWT(); /* 初始DWT */ bsp_InitUart(); /* 初始化串口 */ bsp_InitKey(); /* 初始化按键变量(必须在 bsp_InitTimer() 之前调用) */ bsp_InitI2C(); /* 配置I2C总线 */ bsp_InitExtSDRAM(); bsp_DetectLcdType(); /* 检测触摸板和LCD面板型号, 结果存在全局变量 g_TouchType, g_LcdType */ TOUCH_InitHard(); /* 初始化配置触摸芯片 */ LCD_ConfigLTDC(); /* 初始化配置LTDC */ DSO_ConfigCtrlGPIO(); /* 初始化示波器模块的引脚配置 */ bsp_InitADC(); /* 初始化ADC1,ADC2和ADC3 */ bsp_InitDAC1(); /* 初始化DAC1 */ g_DAC1.ucDuty = 50; /* 初始化DAC配置,用于信号发生器 */ g_DAC1.ucWaveType = 0; g_DAC1.ulAMP = 4095; g_DAC1.ulFreq = 10000; dac1_SetSinWave(g_DAC1.ulAMP, g_DAC1.ulFreq); MountSD(); /* 挂载SD卡 */ TIM8_MeasureTrigConfig(); /* 初始化TIM8用于记录一段波形 */}
调用函数vSetupSysInfoTest()初始化系统信息调试功能
这个函数涉及到的内容比较多,需要大家专门看我们FreeRTOS教程的第8章,有详细讲解:
。
调用函数AppObjCreate()创建任务通信机制
实现代码如下,其中事件标志的创建比较重要,emWin任务和数字信号处理任务之间通信要使用。
/*********************************************************************************************************** 函 数 名: AppObjCreate* 功能说明: 创建任务通信机制* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void AppObjCreate (void){ /* 创建互斥信号量 */ xMutex = xSemaphoreCreateMutex(); if(xMutex == NULL) { /* 没有创建成功,用户可以在这里加入创建失败的处理机制 */ } /* 创建事件标志组 */ xCreatedEventGroup = xEventGroupCreate(); if(xCreatedEventGroup == NULL) { /* 没有创建成功,用户可以在这里加入创建失败的处理机制 */ } /* 动态内存申请部分,省略未贴 */}
调用函数AppTaskCreate()创建任务
创建了如下五个任务:
/*********************************************************************************************************** 函 数 名: AppTaskCreate* 功能说明: 创建应用任务* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void AppTaskCreate (void){ xTaskCreate( vTaskGUI, /* 任务函数 */ "vTaskGUI", /* 任务名 */ 1024, /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ 1, /* 任务优先级*/ xHandleTaskGUI ); /* 任务句柄 */ xTaskCreate( vTaskTaskUserIF, /* 任务函数 */ "vTaskUserIF", /* 任务名 */ 512, /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ 2, /* 任务优先级*/ &xHandleTaskUserIF ); /* 任务句柄 */ xTaskCreate( vTaskMsgPro, /* 任务函数 */ "vTaskMsgPro", /* 任务名 */ 512, /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ 3, /* 任务优先级*/ &xHandleTaskMsgPro ); /* 任务句柄 */ xTaskCreate( vTaskStart, /* 任务函数 */ "vTaskStart", /* 任务名 */ 512, /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ 4, /* 任务优先级*/ &xHandleTaskStart ); /* 任务句柄 */ xTaskCreate( vTaskDSO, /* 任务函数 */ "vTaskDSO", /* 任务名 */ 1024, /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ 5, /* 任务优先级*/ &xHandleTaskStart ); /* 任务句柄 */}
15.2.2 启动任务(触摸和按键)
启动任务实现的功能比较简单,主要是按键扫描和触摸扫描:
/*********************************************************************************************************** 函 数 名: vTaskStart* 功能说明: 启动任务,主要实现按键检测和触摸检测。* 形 参: pvParameters 是在创建该任务时传递的形参* 返 回 值: 无* 优 先 级: 5 **********************************************************************************************************/static void vTaskStart(void *pvParameters){ uint8_t ucCount = 0; while(1) { /* 1ms一次触摸扫描,电阻触摸屏 */ if(g_tTP.Enable == 1) { TOUCH_Scan(); /* 按键扫描 */ ucCount++; if(ucCount == 10) { ucCount = 0; bsp_KeyScan(); } vTaskDelay(1); } /* 10ms一次触摸扫描,电容触摸屏GT811 */ if(g_GT811.Enable == 1) { bsp_KeyScan(); GT811_OnePiontScan(); vTaskDelay(10); } /* 10ms一次触摸扫描,电容触摸屏FT5X06 */ if(g_tFT5X06.Enable == 1) { bsp_KeyScan(); FT5X06_OnePiontScan(); vTaskDelay(10); } }}
知识点拓展
新版emWin教程第4章或者第5章,对触摸的实现做了详细讲解:
。
15.2.3 信号处理任务
信号处理任务的实现如下:
/*********************************************************************************************************** 函 数 名: AppTaskDSO* 功能说明: 双通道示波器数据处理任务。* 形 参: pvParameters 是在创建该任务时传递的形参* 返 回 值: 无* 优 先 级: 6 **********************************************************************************************************/static void vTaskDSO(void *pvParameters){ EventBits_t uxBits; uint8_t pcWriteBuffer[512]; /* 实数序列FFT长度 */ fftSize = 2048; /* 正变换 */ ifftFlag = 0; /* 初始化结构体S中的参数 */ arm_rfft_fast_init_f32(&S, fftSize); while(1) { /* 等待所有任务发来事件标志 */ uxBits = xEventGroupWaitBits(xCreatedEventGroup, /* 事件标志组句柄 */ 0xFFFF, /* 等待0xFFFF某一位被设置 */ pdTRUE, /* 退出前0xFFFF位被清除,这里是任意0xFFFF位被设置就“退出”*/ pdFALSE, /* 设置为pdTRUE表示等待0xFFFF任意位被设置*/ portMAX_DELAY); /* 等待延迟时间 */ switch (uxBits) { /* 双通道波形处理 */ case DspFFT2048Pro_15: /* 读取的是ADC3的位置 */ g_DSO1->usCurPos = 10240 - DMA2_Stream1->NDTR; /* 读取的是ADC1的位置 */ g_DSO2->usCurPos = 10240 - DMA2_Stream0->NDTR; DSO2_WaveTrig(g_DSO2->usCurPos); DSO1_WaveTrig(g_DSO1->usCurPos); DSO2_WaveProcess(); DSO1_WaveProcess(); break; /* 用于简单的ADC数据采集 */ case DspMultiMeterPro_14: g_uiAdcAvgSample = ADC_GetSampleAvgN(); break; /* 仅用于调试目的,打印任务的执行情况,默认不使用 */ case DspTaskInfo_13: App_Printf("=================================================\r\n"); App_Printf("任务名 任务状态 优先级 剩余栈 任务序号\r\n"); vTaskList((char *)&pcWriteBuffer); App_Printf("%s\r\n", pcWriteBuffer); App_Printf("\r\n任务名 运行计数 使用率\r\n"); vTaskGetRunTimeStats((char *)&pcWriteBuffer); App_Printf("%s\r\n", pcWriteBuffer); App_Printf("当前动态内存剩余大小 = %d字节\r\n", xPortGetFreeHeapSize()); break; /* 其它位暂未使用 */ default: App_Printf("*ucReceive = %x\r\n", uxBits); break; } }}
根据接收到的不同任务消息来处理不同的功能,要处理的消息分为三类:
1、双通道波形数据处理
主要实现软件触发,计算FFT ,FIR ,RMS,最大值,最小值,平均值和峰峰值。两个通道都进行了处理。具体实现方法已经在前面章节为大家做了讲解。
2、简单电压测量处理
这个功能比较简单,就是获取一组ADC数值,然后求平均。
3、打印任务执行情况
通过串口打印任务栈的使用情况和各个任务的CPU利用率。
15.2.4 GUI任务
emWin任务的实现代码如下:
/*********************************************************************************************************** 函 数 名: vTaskGUI* 功能说明: emWin任务* 形 参: pvParameters 是在创建该任务时传递的形参* 返 回 值: 无* 优 先 级: 1 (数值越小优先级越低,这个跟uCOS相反)**********************************************************************************************************/static void vTaskGUI(void *pvParameters){ while (1) { MainTask(); }}
emWin的代码都是在函数MainTask里面实现,这样做是方便在main.c文件里面统一管理任务。关于GUI部分最重要的界面优化,波形刷新优化,波形浏览等,在前面章节已经都做了讲解,我们这里不再赘述。更详细的实现,需要结合前面章节的讲解去看源码。
15.2.5 用户接口任务
这个任务暂时未执行任何功能,保留供以后升级使用。代码如下:
/*********************************************************************************************************** 函 数 名: vTaskTaskUserIF* 功能说明: 保留,暂时未用到。* 形 参: pvParameters 是在创建该任务时传递的形参* 返 回 值: 无* 优 先 级: 2**********************************************************************************************************/static void vTaskTaskUserIF(void *pvParameters){ while(1) { vTaskDelay(1000); }}
15.2.6 文件系统处理任务
当前文件系统处理任务主要用来做截图功能,将GUI界面以BMP格式存储到SD卡里面:
/*********************************************************************************************************** 函 数 名: vTaskMsgPro* 功能说明: 实现截图功能,将图片以BMP格式保存到SD卡中* 形 参: pvParameters 是在创建该任务时传递的形参* 返 回 值: 无* 优 先 级: 4 **********************************************************************************************************/static void vTaskMsgPro(void *pvParameters){ uint8_t Pic_Name = 0; uint32_t ulStart, ulEnd; char buf[20]; while(1) { ulTaskNotifyTake( pdTRUE, /* 此参数设置为pdTRUE,接收到的notification value清零 */ portMAX_DELAY ); /* 无限等待 */ sprintf(buf,"0:/PicSave/%d.bmp",Pic_Name); /* 记录截图前起始时间 */ ulStart = xTaskGetTickCount(); /* 开启调度锁 */ //vTaskSuspendAll(); /* 如果SD卡中没有PicSave文件,会进行创建 */ result = f_mkdir("0:/PicSave"); /* 创建截图 */ result = f_open(&file, buf, FA_WRITE|FA_CREATE_ALWAYS); /* 向SD卡绘制BMP图片 */ GUI_BMP_Serialize(_WriteByte2File, &file); /* 创建完成后关闭file */ result = f_close(&file); /* 关闭调度锁 */ //xTaskResumeAll (); /* 记录截图后时间并获取截图过程耗时 */ ulEnd = xTaskGetTickCount(); ulEnd -= ulStart; App_Printf("截图完成,耗时 = %dms\r\n", ulEnd); Pic_Name++; }}
后期这个任务将被升级,用于将波形数据以CSV文件格式存储到SD卡里面。
15.3 用户任务优先级设置
当前任务的优先级安排如下(数值越小,优先级越低):
vTaskDSO任务 : 优先级5。
DSP任务一定要是优先级最高的,因为采集的数据要实时处理。
vTaskStart 任务 : 优先级4。
vTaskMsgPro任务 : 优先级3。
启动任务(触摸和按键扫描)以及MsgPro(文件系统处理)任务的优先级谁高谁低都没有关系。
vTaskUserIF任务 :优先级2。
保留,未使用任务,暂且安排为这个优先级。
vTaskGUI任务 :优先级1。
emWin任务是除了空闲任务,统计任务以外最低优先级的,因为emWin极其占用系统资源,而且时间长,如果这个任务设置为高优先级,会直接影响低优先级任务的执行。
知识点拓展
关于任务优先级的安排,在我们RTX操作系统教程第8章的8.2小节有些拓展:
。
在我们FreeRTOS操作系统教程的第13章的13.2小节有些拓展:
。
15.4 全局变量分配,系统堆栈和任务堆栈
示波器的设计需要很多变量进行逻辑管理,从设计之初就需要将变量分类进行结构体封装,方便以后的维护升级。这一步至关重要,实际中差不多要定义上百个变量,如果不进行分类管理,以后的升级维护将非常麻烦。
这种方式还有一个好处是方便我们将F429的CCM RAM空间分配给这些变量使用。使用CCM RAM的好处是速度比通用RAM要快些,缺点是这部分空间不支持DMA操作。初次使用的用户比较容易在这个地方犯错误。所以在使用局部变量时,切勿将局部变量用于DMA传输。
1、任务栈
因为直接将FreeRTOS的动态内存管理文件heap_4.c中的数组重定向到CCM RAM空间了,那么任务栈以及所有组件需要的内存空间都是来自CCM RAM。
/*********************************************************************************************************** FreeRTOS的任务栈空间使用CCM RAM区***********************************************************************************************************/#if defined ( __CC_ARM ) /* MDK编译器 */ uint8_t ucHeap[64*1024] __attribute__((at(0x10000000)));#elif defined ( __ICCARM__ ) /* IAR编译器 */ #pragma location=0x10000000 uint8_t ucHeap[64*1024];#endif
知识点拓展1
关于任务栈大小应该分配多大的问题,可以看FreeRTOS教程第11章:
。
知识点拓展2
FreeRTOS教程第28章:动态内存管理。
。
2、全局变量分配
当前需要频繁调用的变量也通过动态内存管理分配给各个结构体变量,使用的CCM RAM空间。
/*********************************************************************************************************** 函 数 名: AppObjCreate* 功能说明: 创建任务通信机制* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void AppObjCreate (void){ /* 任务消息机制的创建,省略未写 */ /* 申请示波器通道1动态内存 */ g_DSO1 = (DSO_T *)pvPortMalloc(sizeof(DSO_T)); /* 申请示波器通道2动态内存 */ g_DSO2 = (DSO_T *)pvPortMalloc(sizeof(DSO_T)); /* 申请游标测量结构体变量动态内存 */ g_Cursors = (CURSORS_T *)pvPortMalloc(sizeof(CURSORS_T)); /* 申请标志位结构体变量动态内存 */ g_Flag = (FLAG_T *)pvPortMalloc(sizeof(FLAG_T)); /* 申请触发结构体变量动态内存 */ g_TrigVol = (TRIVOLTAGE_T *)pvPortMalloc(sizeof(TRIVOLTAGE_T)); /* 申请FFT动态内存 */ testInput_fft_2048 = (float32_t *)pvPortMalloc(sizeof(float32_t)*2048); testOutput_fft_2048 = (float32_t *)pvPortMalloc(sizeof(float32_t)*2048); /* 申请RMS动态内存 */ g_RMSBUF = (float32_t *)pvPortMalloc(sizeof(float32_t)*600); /* 申请FIR动态内存 */ FirDataInput = (float32_t *)pvPortMalloc(sizeof(float32_t)*FIR_LENGTH_SAMPLES); FirDataOutput = (float32_t *)pvPortMalloc(sizeof(float32_t)*FIR_LENGTH_SAMPLES); firStateF32 = (float32_t *)pvPortMalloc(sizeof(float32_t)*FIR_StateBufSize);}
3、系统栈分配
系统栈分配的大小如下:
15.5 任务间通信机制和全局变量共享问题
二代示波器的双通道ADC通过DMA方式在实时的采集数据,每个通道的缓冲大小是1024*20字节,采集的数据经过信号处理后送给GUI任务进行波形显示和测量值显示。为了实现这个功能,专门测试了两种方案。
(1)方案一
采用DMA双缓冲,一路缓冲采集波形的时候,另一路已经采集的波形数据发给数字信号处理任务,信号处理任务再将整理好的波形数据和测量值发给emWin任务做刷新。这种方式的优点是ADC采集的数据可以实时处理。缺点是F429处理不过来,比如我们一个通道的采样率是2Msps,缓冲大小设置为2048,将缓冲填满需要1ms左右的时间,而我们仅做一个2048点的实数FFT就需要0.862ms,其它的FIR,RMS等都还没有做,而且已经没有时间发消息给emWin任务做界面刷新了。如果我们降低FFT,FIR等信号处理的点数,也就失去了实时处理的意义。也许读者会说,加大缓冲不就好了,其实不然。如果我们加大了缓冲,我们要处理的数据也增加了,还是处理不过来,而且我们现在要处理的是双通道。
除了F429的性能问题,这种方式还有一个比较棘手的问题需要解决,就是用户操作界面的时候,GUI任务基本已经没有时间去处理数字信号处理任务发来的数据,为了解决这个问题,大大增加了软件设计的复杂度,特别是波形暂停和运行的切换,窗口的切换以及其它操作时,都要注意这个问题。
如果没有复杂的界面操作,而且采样率较低的话,方案一还是比较合适的。由于我们需要滑动操作波形,而且要实现双通道,每个通道最高采样率是2.8Msps,所以放弃这种方案。
(2)方案二
与方案一恰恰相反,ADC数据依然是通过DMA方式实时采集,而任务间的通信反过来进行,emWin任务需要波形数据刷新时给数字信号处理任务发消息获取,这样就有效地解决了方案一中F429性能不够的问题,而且方案一中棘手的软件问题得到了很好的解决,随时都可以操作界面。
并且这种方式无形中解决了emWin任务和数字信号处理任务之间共同操作全局变量的问题,因为emWin是低优先级任务,而数字信号处理任务在emWin任务发消息后才会执行,这样就不存在抢占问题了,有效地解决了全局变量共享问题。
但是这种方式也有一个缺陷,无法实时刷新波形和测量值了,不过可以通过普通触发来解决了,普通触发方式实时采集了触发值前后各1024字节的数据,并且可以滑动浏览。不过工程中未对这种方式做FFT和FIR的支持。
总结,二代示波器中最终选择了方案二。
15.6 FreeRTOS系统调试
FreeRTOS的调试比较简单,采用串口打印,按下按键K1即可。不过由于按键不够用,在MainTask.c文件的MainTask函数里面对按键K1的消息处理做了三个条件编译,大家可以根据需要选择执行触摸校准功能,截图功能还是串口打印功能。#if 0取消执行,#if 1表示执行。
case KEY_1_DOWN: #if 1 hTouchWin = WM_CreateWindowAsChild(0, 0, 800, 480, WM_HBKWIN, WM_CF_SHOW, _cbTouchCalibration, 0); WM_Exec(); WM_SelectWindow(hTouchWin); /* 执行触摸校准 */ TOUCH_Calibration(); WM_SelectWindow(0); WM_DeleteWindow(hTouchWin); WM_Exec(); /* 自动触发暂停状态 */ if(g_Flag->hWinRunStop == 1) { g_Flag->ucWaveRefresh = 1; } /* 普通触发暂停状态 */ if(TriggerFlag == 1) { TriggerFlag = 2; } #endif /* 用于截图 */ #if 0 xTaskNotifyGive(xHandleTaskMsgPro); #endif /* 打印任务的执行情况 */ #if 0 g_Flag->ucDsoMsg = DspTaskInfo_13; xEventGroupSetBits(xCreatedEventGroup, g_Flag->ucDsoMsg); #endif
如果使能了串口打印的条件编译,串口打印任务执行情况如下:
15.7 MDK优化等级
为了发挥STM32F429的最高性能,需要大家开启最高等级优化和时间优化,即下面两个选项:
FreeRTOS工程的文件系统是采用的FatFS,当前开启了最高等级的三级优化和时间优化。如果大家要使用FatFS功能,请务必关闭时间优化,即Optimize for Time,取消勾选即可。因为FatFS在时间优化下会工作异常。
知识点拓展
MDK曾经做的专题:如何做MDK编译器的代码最小优化和性能最佳优化。
。
15.8 总结
FreeRTOS系统设计二代示波器的关键问题在本章节都做了阐释,建议大家学习完本章节后,直接看源码做实战演练,这样理解的更透彻,而且这时再做改进拓展也容易些。