第13章 RTX操作系统版本二代示波器实现
本章教程为大家讲解RTX操作系统版本的二代示波器实现。主要讲解RTOS设计框架,即各个任务实现的功能,任务间的通信方案选择,任务栈,系统栈以及全局变量共享问题。同时,工程调试方法也专门做了说明。
13.1 注意事项(重要必读)
13.2 任务功能划分
13.3 用户任务优先级设置
13.4 全局变量分配,系统堆栈和任务堆栈
13.5 任务间通信和全局变量共享问题
13.6 RTX配置向导
13.7 RTX系统调试
13.8 MDK优化等级
13.9 总结
13.1 注意事项(重要必读)
1、学习本章节前,务必保证已经学习完毕前面章节。另外,工程代码注释已经比较详细,了解了框架后,直接看源码即可。
2、RTX操作系统版本的限制使用MDK4.74,其它MDK版本不支持。详情看我们RTX教程即可:
http://forum.armfly.com/forum.php?mod=viewthread&tid=14837 。
3、仅支持800*480分辨率显示屏,如果是电容屏,无需校准。如果是电阻屏,需要校准,按下按键K1即可进入校准界面。
4、由于按键不够用,在MainTask.c文件的MainTask函数里面对按键K1的消息处理做了一个条件编译,大家可以根据需要选择执行触摸校准功能还是截图功能。#if 1表示执行触摸校准,#if 0表示执行截图功能。
case KEY_1_DOWN: /************由于按键不够用,将截图功能取消***********/ #if 0 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; } #else os_sem_send (&semaphore); #endif break;
5、文件系统是用的RL-FlashFS,如果大家想学习RL-FlashFS的使用,学习KEIL给的手册即可:
http://forum.armfly.com/forum.php?mod=viewthread&tid=2988 。
6、MDK安装目录里面带的emWin5.4x版本的截图功能有bug,详情看此贴:
http://forum.armfly.com/forum.php?mod=viewthread&tid=82445 。
当前用的5.36版本,也是来自MDK。
13.2 任务功能划分
前面第三章已经将任务功能划分好:
根据这个功能划分,创建所需要的任务。另外,RTX本身是不支持CPU利用率统计的,所以专门创建了一个任务实现CPU利用率统计。
13.2.1 主函数创建
在main.c文件实现:
/* ********************************************************************************************************* * 函 数 名: main * 功能说明: 标准c程序入口。 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ int main (void) { /* 初始化外设 */ bsp_Init(); /* 创建启动任务 */ os_sys_init_user (AppTaskStart, /* 任务函数 */ 6, /* 任务优先级 */ &AppTaskStartStk, /* 任务栈 */ sizeof(AppTaskStartStk)); /* 任务栈大小,单位字节数 */ while(1); }
硬件外设的初始化函数bsp_Init是在 bsp.c 文件实现:
/* ********************************************************************************************************* * 函 数 名: bsp_Init * 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_Init(void) { /* 由于ST固件库的启动文件已经执行了CPU系统时钟的初始化,所以不必再次重复配置系统时钟。 启动文件配置了CPU主时钟频率、内部Flash访问速度和可选的外部SRAM FSMC初始化。 系统时钟缺省配置为168MHz,如果需要更改,可以修改 system_stm32f4xx.c 文件 */ /* 优先级分组设置为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用于记录一段波形 */ }
创建任务的主要功能是硬件外设初始化和启动任务的创建,相对比较简单。
13.2.2 启动任务(信号处理)
启动任务用于二代示波器的信号处理:
/* ********************************************************************************************************* * 函 数 名: AppTaskStart * 功能说明: 启动任务,也是最高优先级任务,用于信号处理。 * 形 参: 无 * 返 回 值: 无 * 优 先 级: 6 ********************************************************************************************************* */ __task void AppTaskStart(void) { OS_RESULT xResult; /* 优先创建统计任务-----------------*/ HandleTaskStat = os_tsk_create_user(AppTaskStatistic, /* 任务函数 */ 1, /* 任务优先级 */ &AppTaskStatStk, /* 任务栈 */ sizeof(AppTaskStatStk)); /* 任务栈大小,单位字节数 */ OSStatInit(); /* 创建任务间通信机制和动态内存分配,此函数要优先调用 */ AppObjCreate(); /* 创建任务 */ AppTaskCreate(); /* 实数序列FFT长度 */ fftSize = 2048; /* 正变换 */ ifftFlag = 0; /* 初始化结构体S中的参数 */ arm_rfft_fast_init_f32(&S, fftSize); HandleTaskStart = os_tsk_self(); while(1) { if(os_evt_wait_or(StartTaskWaitFlag, 0xFFFF) == OS_R_EVT) { xResult = os_evt_get (); switch (xResult) { 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; case DspMultiMeterPro_0: g_uiAdcAvgSample = ADC_GetSampleAvgN(); break; /* 其它位暂未使用 */ default: printf_taskdbg("xResult = %x ", xResult); break; } } } }
除了信号处理,还有一个重要的功能要在启动任务里面优先实现,就是统计任务的创建和执行,用于统计CPU利用率,实现步骤如下:
- 进入到启动任务后,其它任何任务都不要创建,先创建一个统计任务,不让执行。
- 启动任务延迟100ms,延迟的这100ms时间基本都是空闲任务在执行,在空闲任务里面做32位变量加1计算。我们就以这100ms,变量计数的最大值作为CPU利用率的分母。
- 然后开启统计任务的执行,每100ms执行一次,统计即可。空闲任务此时的计数值作为分子。通过这种方式就实现了CPU利用率的统计。
统计任务执行后就是任务间通信机制函数AppObjCreate(动态内存分配也是在这个函数里面实现,在本章13.4小节有说明)和任务创建函数AppTaskCreate,代码比较简单,我们这里就不贴出来了。
说完了前面这些,最重要的还是信号处理。根据不同的事件标志处理不同的功能,任务里面主要是分为了两类:
1、双通道波形数据处理
主要实现软件触发,计算FFT ,FIR ,RMS,最大值,最小值,平均值和峰峰值。两个通道都进行了处理。具体实现方法已经在前面章节为大家做了讲解。
2、另一个是简单电压测量处理
这个功能比较简单,就是获取一组ADC数值,然后求平均。
13.2.3 统计任务
统计任务的实现代码如下:
/* ********************************************************************************************************* * 函 数 名: AppTaskStatistic * 功能说明: 统计任务,用于实现CPU利用率的统计。为了测试更加准确,可以开启注释调用的全局中断开关 * 形 参: 无 * 返 回 值: 无 * 优 先 级: 1 (数值越小优先级越低,这个跟uCOS相反) ********************************************************************************************************* */ void OSStatInit (void) { OSStatRdy = FALSE; os_dly_wait(2u); /* 时钟同步 */ //__disable_irq(); OSIdleCtr = 0uL; /* 清空闲计数 */ //__enable_irq(); os_dly_wait(100); /* 统计100ms内,最大空闲计数 */ //__disable_irq(); OSIdleCtrMax = OSIdleCtr; /* 保存最大空闲计数 */ OSStatRdy = TRUE; //__enable_irq(); } __task void AppTaskStatistic (void) { while (OSStatRdy == FALSE) { os_dly_wait(200); /* 等待统计任务就绪 */ } OSIdleCtrMax /= 100uL; if (OSIdleCtrMax == 0uL) { OSCPUUsage = 0u; } //__disable_irq(); OSIdleCtr = OSIdleCtrMax * 100uL; /* 设置初始CPU利用率 0% */ //__enable_irq(); for (;;) { //__disable_irq(); OSIdleCtrRun = OSIdleCtr; /* 获得100ms内空闲计数 */ OSIdleCtr = 0uL; /* 复位空闲计数 */ //__enable_irq(); /* 计算100ms内的CPU利用率 */ OSCPUUsage = (100uL - (float)OSIdleCtrRun / OSIdleCtrMax); os_dly_wait(100); /* 每100ms统计一次 */ } }
统计任务的实现思路就是前面13.2.2小节中介绍的方法。这个统计任务的实现思路是由uCOS-II修改而来的,如果大家研究过uCOS-II的源码,这里的代码还是比较好理解的。没有研究过也没有关系,直接根据13.2.2小节里面介绍的思路看统计任务的实现代码即可。
13.2.4 GUI任务
emWin任务的实现代码如下:
/* ********************************************************************************************************* * 函 数 名: AppTaskGUI * 功能说明: GUI任务。 * 形 参: 无 * 返 回 值: 无 * 优 先 级: 2 ********************************************************************************************************* */ __task void AppTaskGUI(void) { while(1) { MainTask(); } }
emWin的代码都是在函数MainTask里面实现,这样做是方便在main.c文件里面统一管理任务。关于GUI部分最重要的界面优化,波形刷新优化,波形浏览等,在前面章节已经都做了讲解,我们这里不再赘述。更详细的实现,需要结合前面章节的讲解去看源码。
13.2.5 用户接口任务
这个任务暂时未执行任何功能,保留供以后升级使用。代码如下:
/* ********************************************************************************************************* * 函 数 名: AppTaskUserIF * 功能说明: 保留,暂未使用。 * 形 参: 无 * 返 回 值: 无 * 优 先 级: 3 ********************************************************************************************************* */ __task void AppTaskUserIF(void) { while(1) { os_dly_wait(2000); } }
13.2.6 文件系统处理任务
当前文件系统处理任务主要用来做截图功能,将GUI界面以BMP格式存储到SD卡里面:
/* ********************************************************************************************************* * 函 数 名: AppTaskFsPro * 功能说明: 文件系统处理任务。 * 形 参: 无 * 返 回 值: 无 * 优 先 级: 4 ********************************************************************************************************* */ __task void AppTaskFsPro(void) { OS_RESULT xResult; const uint16_t usMaxBlockTime = 0xFFFF; uint8_t Pic_Name = 0; char buf[40]; while(1) { xResult = os_sem_wait (&semaphore, usMaxBlockTime); switch (xResult) { /* 无需等待接受到信号量同步信号 */ case OS_R_OK: /* 信号量不可用,usMaxBlockTime等待时间内收到信号量同步信号 */ case OS_R_SEM: sprintf((char *)buf,"M0:\PicSave\%d.bmp",Pic_Name); foutbmp = fopen (buf, "w"); if (foutbmp != NULL) { /* 向SD卡绘制BMP图片 */ GUI_BMP_Serialize(_WriteByte2File, foutbmp); /* 关闭文件 */ fclose(foutbmp); } printf_taskdbg("截图完成 "); Pic_Name++; break; /* 超时 */ case OS_R_TMO: break; /* 其他值不处理 */ default: break; } } }
后期这个任务将被升级,用于将波形数据以CSV文件格式存储到SD卡里面。
13.2.7 触摸和按键任务
触摸和按键任务实现的功能比较简单,主要是按键扫描和触摸扫描:
/* ********************************************************************************************************* * 函 数 名: AppTaskMsgPro * 功能说明: 按键和触摸检测 * 形 参: 无 * 返 回 值: 无 * 优 先 级: 5 ********************************************************************************************************* */ __task void AppTaskMsgPro(void) { uint8_t ucCount = 0; while(1) { /* 1ms一次触摸扫描,电阻触摸屏 */ if(g_tTP.Enable == 1) { TOUCH_Scan(); /* 按键扫描 */ ucCount++; if(ucCount == 10) { ucCount = 0; bsp_KeyScan(); } os_dly_wait(1); } /* 10ms一次触摸扫描,电容触摸屏GT811 */ if(g_GT811.Enable == 1) { bsp_KeyScan(); GT811_OnePiontScan(); os_dly_wait(10); } /* 10ms一次触摸扫描,电容触摸屏FT5X06 */ if(g_tFT5X06.Enable == 1) { bsp_KeyScan(); FT5X06_OnePiontScan(); os_dly_wait(10); } } }
知识点拓展:
新版emWin教程第4章或者第5章,对触摸的实现做了详细讲解:
http://forum.armfly.com/forum.php?mod=viewthread&tid=19834 。
13.3 用户任务优先级设置
当前任务的优先级安排如下(数值越小,优先级越低):
AppTaskStatistic任务 : 优先级1。
统计任务的优先级最低,这个毫无争议,因为它要统计CPU利用率。
AppTaskGUI任务 : 优先级2。
emWin任务是除了空闲任务,统计任务以外最低优先级的,因为emWin极其占用系统资源,而且时间长,如果这个任务设置为高优先级,会直接影响低优先级任务的执行。
AppTaskUserIF任务 : 优先级3。
保留,未使用任务,暂且安排为这个优先级。
AppTaskFsPro任务 : 优先级4。
AppTaskMsgPro任务 : 优先级5。
触摸和按键任务,以及文件系统任务的优先级谁高谁低都没有关系。
AppTaskStart任务 : 优先级6。
DSP任务一定要是优先级最高的,因为采集的数据要实时处理。
知识点拓展:
关于任务优先级的安排,在我们RTX操作系统教程第8章的8.2小节有些拓展:
http://forum.armfly.com/forum.php?mod=viewthread&tid=14837。
在我们FreeRTOS操作系统教程的第13章的13.2小节有些拓展:
http://forum.armfly.com/forum.php?mod=viewthread&tid=17658。
13.4 全局变量分配,系统堆栈和任务堆栈
1、全局变量分配
示波器的设计需要很多变量进行逻辑管理,从设计之初就需要将变量分类进行结构体封装,方便以后的维护升级。这一步至关重要,实际中差不多要定义上百个变量,如果不进行分类管理,以后的升级维护将非常麻烦。
这种方式还有一个好处是方便我们将F429的CCM RAM空间分配给这些变量使用。使用CCM RAM的好处是速度比通用RAM要快些,缺点是这部分空间不支持DMA操作。初次使用的用户比较容易在这个地方犯错误。所以在使用局部变量时,切勿将局部变量用于DMA传输。
当前需要频繁调用的变量已经通过动态内存管理分配给各个结构体变量,使用的CCM RAM空间。
uint64_t AppMallocCCM[40*1024/8] __attribute__((at(0x10000000 + 1024*24))); /* 数字信号处理 */ /* ********************************************************************************************************* * 函 数 名: AppObjCreate * 功能说明: 创建任务通信机制和动态内存分配 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ static void AppObjCreate (void) { /* 创建信号量计数值是0, 用于任务同步 */ os_sem_init (&semaphore, 0); /* 将内部CCM SRAM的40KB全部供动态内存使用 */ os_init_mem(AppMallocCCM, 1024*40); /* 申请示波器通道1动态内存 */ g_DSO1 = (DSO_T *)os_alloc_mem(AppMallocCCM, sizeof(DSO_T)); /* 申请示波器通道2动态内存 */ g_DSO2 = (DSO_T *)os_alloc_mem(AppMallocCCM, sizeof(DSO_T)); /* 申请游标测量结构体变量动态内存 */ g_Cursors = (CURSORS_T *)os_alloc_mem(AppMallocCCM, sizeof(CURSORS_T)); /* 申请标志位结构体变量动态内存 */ g_Flag = (FLAG_T *)os_alloc_mem(AppMallocCCM, sizeof(FLAG_T)); /* 申请触发结构体变量动态内存 */ g_TrigVol = (TRIVOLTAGE_T *)os_alloc_mem(AppMallocCCM, sizeof(TRIVOLTAGE_T)); /* 申请FFT动态内存 */ testInput_fft_2048 = (float32_t *)os_alloc_mem(AppMallocCCM, sizeof(float32_t)*2048); testOutput_fft_2048 = (float32_t *)os_alloc_mem(AppMallocCCM, sizeof(float32_t)*2048); /* 申请RMS动态内存 */ g_RMSBUF = (float32_t *)os_alloc_mem(AppMallocCCM, sizeof(float32_t)*600); /* 申请FIR动态内存 */ FirDataInput = (float32_t *)os_alloc_mem(AppMallocCCM, sizeof(float32_t)*FIR_LENGTH_SAMPLES); FirDataOutput = (float32_t *)os_alloc_mem(AppMallocCCM, sizeof(float32_t)*FIR_LENGTH_SAMPLES); firStateF32 = (float32_t *)os_alloc_mem(AppMallocCCM, sizeof(float32_t)*FIR_StateBufSize); }
2、任务栈分配
任务栈也是用的CCM RAM空间,具体分配如下: /* ********************************************************************************************************** 任务栈和任务句柄 ********************************************************************************************************** */ static uint64_t AppTaskStatStk[1024/8] __attribute__((at(0x10000000))); /* 任务栈 */ static uint64_t AppTaskGUIStk[4096/8] __attribute__((at(0x10000000+1024))); /* 任务栈 */ static uint64_t AppTaskUserIFStk[1024/8] __attribute__((at(0x10000000+1024*5))); /* 任务栈 */ static uint64_t AppTaskFsProStk[1024/8] __attribute__((at(0x10000000+1024*6))); /* 任务栈 */ static uint64_t AppTaskMsgProStk[1024/8] __attribute__((at(0x10000000+1024*7))); /* 任务栈 */ static uint64_t AppTaskStartStk[4096/8] __attribute__((at(0x10000000+1024*8))); /* 任务栈 */
将任务栈定义成uint64_t类型可以保证任务栈是8字节对齐的,8字节对齐的含义就是数组的首地址对8求余等于0。如果不做8字节对齐的话,部分C语言库函数、浮点运算和uint64_t类型数据运算会出问题。
知识点拓展:
关于任务栈大小应该分配多大的问题,可以看FreeRTOS教程第11章,对于RTX系统也是适用的。
http://forum.armfly.com/forum.php?mod=viewthread&tid=17658 。
3、系统栈分配
系统栈分配的大小如下:
13.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的支持。
总结,二代示波器中最终选择了方案二。
13.6 RTX配置向导
RTX配置向导详情如下:
Task Configuration
(1)Number of concurrent running tasks
允许创建9个任务,实际创建了如下6个任务:
AppTaskStatistic任务 : 统计任务,获取CPU利用率。
AppTaskGUI任务 : emWin任务。
AppTaskUserIF任务 : 保留,暂未使用。
AppTaskFsPro任务 : 文件系统任务。
AppTaskMsgPro任务 : 按键和触摸检测。
AppTaskStart任务 : 启动任务,也是最高优先级任务,用于信号处理。
(2)Number of tasks with user-provided stack
8个任务可以采用自定义堆栈方式。
(3)Run in privileged mode
设置任务运行在非特权级模式。
13.7 RTX系统调试
MDK自带RTX调试组件,展示系统信息非常方便,本工程的展示效果如下:
调试组件的使用方法请看F429的RTX教程第3章3.4小节,有详细说明:
http://forum.armfly.com/forum.php?mod=viewthread&tid=14837 。
13.8 MDK优化等级
为了发挥STM32F429的最高性能,需要大家开启最高等级优化和时间优化,即下面两个选项:
知识点拓展:
MDK曾经做的专题:如何实现MDK编译器的代码最小优化和性能最佳优化。
http://forum.armfly.com/forum.php?mod=viewthread&tid=1794 。
13.9 总结
RTX系统设计二代示波器的关键问题在本章节都做了阐释,建议大家学习完本章节后,直接看源码做实战演练,这样理解的更透彻,而且这时再做改进拓展也容易些。