国内的技术环境,重开发,轻测试,其实吧,两者都重要,而且我个人是不认同将开发与测试解耦开来,全周期交付最终的产品是所有工程师的职责,不是开发好代码就结束了,测试用例只有最懂这个系统的人设计才最好的,所以开发测试本就是一家。
下面聊聊,嵌入式中的测试,这个跟软件系统最大的不同,就是需要软硬件协同,比如马达控制器,比如锂电池均衡,等等,尤其是涉及到闭环控制的场合,必须要保证测试占用的CPU资源不能影响正常的控制任务执行周期,也就是说,测试消耗的系统资源越少越好。好的测试手段,能够帮助提高整个系统的开发效率。
下面就介绍目前我用到比较便利的三种嵌入式辅助软件测试手段:
IO测试:直接用IO口表示状态,需要用到示波器观察IO电平变化,测试非常快,但是不足就是信息量有限
内存缓冲:内存中开二维数组用于存放测试数据,每个控制周期将测试变量保存到数组,测试结束后串口输出,不足就是受内存限制
串口打印:串口实时printf打印输出测试数据,测试速度会受串口速度限制,速度虽然慢一些,但是可以记录大量信息,数据量不受限制
1.IO测试
比如ch3中,我们要估算每个Task的时间长度,用来去平衡调度周期优化,那就可以用这种IO测试方法来玩,简单直接,不妨来看。
假设我们测Task0的执行时间,首先选一个备用IO口比如PORTA0,在测试前务必将IO口输出模式配置好,然后只要在Task0调用前,将PORTA0拉高,结束后,再将PORTA0拉低,然后上电后,将示波器探头测试PORTA0这个口,查看示波器的波形图,其中高电平的持续时间,就是Task0的执行时间,这个信号的周期时间,就是我们While循环的调度周期。
这种IO测试方法需要示波器配合,简单有效,而且IO翻转的时间一般与主频一致,都在10-100ns级别,对于一些简单的要求精确测量时间的场合,这个妥妥地够用了。
示例代码如下:
int main() { Dis_Interrupt(); System_Init(); En_Interrupt(); while(1) { //IO测试辅助代码 PORTA0=1;//先把IO拉高 Task0_Run(); PORTA0=0;//任务结束后再把IO拉低 Task1_Run(); Task2_Run(); Task3_Run(); Task4_Run(); } } void Task0_Run(void) { Pot1Calc(); //加速器信号计算 Pot2Calc(); //制动器信号计算(保留) TempCalc(); //电机及控制器温度计算 } .....
2.内存缓冲
IO测试可以做简单快速的时间测量,然而我们软件逻辑中,存在大量的控制变量,系统一旦出现问题,我们需要排查整个控制链条上哪个环节的控制变量逻辑或者计算出问题了,这个时候IO测试就玩不转了,因为涉及到软件变量实时记录,尤其是在做电机等闭环控制的场合,还要求测试不能影响正常的闭环控制,也就是说,测试代码最好不要占用CPU时间。其实如果玩MBD-基于模型的设计,也是想把大部分控制上的逻辑或者计算错误直接消灭掉设计阶段。下面就简单介绍这个测试手段的玩法,因为智能车里面的主要测试手段就是这个玩法。
首先我们开辟一个二维数组DataLog,数组的大小要考虑到内存大小(这种方法适合内存比较大的场合),需要记录数据的时候,执行一下DataLog_Add函数即可,在DataLog_Add里我们添加好需要记录的变量个数和具体变量,DataLog_Add的执行时间可以控制到1us左右,等所有二维数组数据填满了之后,我们就可以通过调用DataLog_Print把测试的所有数据串口输出出来。
#include "EIT_Log.h" //header用户应用程序 static int32 DataLog[LOG_COUNT][LOG_NUM_EACH]; static volatile int32 cnt=0; void DataLog_Init(void) { cnt=0; } //记录单条测试数据 void DataLog_Add(void) { if(cnt>=LOG_COUNT) return; else { DataLog[cnt][0]=MotorR_PID.fbValFilter; DataLog[cnt][1]=MotorR_PID.spVal; DataLog[cnt][2]=MotorR_PID.spValRamp; DataLog[cnt][3]=MotorR_PID.I; DataLog[cnt][4]=MotorR_PID.outVal; DataLog[cnt][5]=gVar.InAngle*100; DataLog[cnt][6]=gDir_FarFilter; DataLog[cnt][7]=gDir_MidFilter; DataLog[cnt][8]=angle; cnt++; } } //技术后,串口打印输出 void DataLog_Print(void) { int i,j; for(i=0;i<LOG_COUNT;i++) { for(j=0;j<LOG_NUM_EACH;j++) printf("%d ",DataLog[i][j]); printf(" "); } }
串口输出数据格式的时候要注意一点,最好是一行代表一条数据记录,用空格隔开,像下面这样的格式,好处是什么呢,就是这样的数据直接保存为txt,matlab一拖动就可以成为二维数组,直接就可以plot画图看测试效果了,比如图1所示,整个加减速过程,都可以进行分析。
309 180 5 5 5 1 311 178 5 5 5 1 313 181 5 5 4 1 315 177 4 4 4 1 317 185 4 4 4 1 319 184 4 4 4 1 321 186 4 4 4 1 323 176 3 4 3 1 325 181 3 3 3 0 327 185 4 3 3 0 329 183 4 4 4 1 331 186 4 5 4 1 333 176 4 4 4 1 335 187 3 3 3 0 337 188 3 3 2 0 339 193 4 3 4 0 341 199 3 3 3 0 343 178 2 1 2 0
图1.测试数据Matlab图
3.串口打印
有时候,我们的单片机内存有限,又想分析控制变量,那怎么办,能有啥办法,实时串口打印呗。不过这里要注意,有的串口支持DMA方式,也就是说串口传输不占用CPU资源,这方式比较爽。对于没有DMA方式单片机,只能使用Printf同步打印数据,这里要注意,printf是阻塞的,也就是说在打印数据的时候,CPU啥事都不能干,所以在使用这种玩法的时候,一定要预估一下打印数据占用CPU资源的比例,不要搞得重要任务卡死,一直在打印数据就好。测试的时候,大家尽量是串口波特率设高点。
这种玩法和第二种缓冲的玩法,后面数据处理其实差不多,唯一不足就是串口打印数据会占用CPU资源,这里务必要权衡好。我得建议是,想简单直接一点,就直接将标准C库的printf函数重定向到对应的串口就可以,然后直接调用printf实时打印测试数据,其实也挺简单的。
/*!Vcan山外的代码MK60_conf.c * @brief 重定义printf 到串口 * @param ch 需要打印的字节 * @param stream 数据流 * @since v5.0 * @note 此函数由编译器自带库里的printf所调用 */ int fputc(int ch, FILE *stream) { uart_putchar(VCAN_PORT, (char)ch); return(ch); }
这三种嵌入式辅助测试手段,足够应付山寨还是正规的大部分测试和故障分析玩法了。