第二个例子:
同事驱动一个14094串转并的芯片。串行信号是采用IO模拟的,因为没有专用的硬件。同事就随手写了个驱动,结果调试了3、4天,仍旧是有问题。我实在看不下去了,就去看了看,控制的并行信号有时候正常有时候不正常。我看了看代码,用伪代码大概是:
for (i = 0; i 《 8; i++)
{
SetData((data 》》 i) & 0x1);
SetClockHigh();
for (j = 0; j 《 5; j++);
SetClockLow();
}
将数据的8个bit在每个高电平从bit0到bit7依次发送出去。应该是正常的啊。看不出问题在哪啊?我仔细想了想,有看了14094的datasheet,明白了。原来,14094要求clock的高电平持续10个ns,低电平也要持续10个ns。这段代码之做了高电平时间的延时,没有做低电平的延时。如果中断插在低电平之间工作,那么这段代码是可以的。但是如果CPU没有中断插在低电平时执行,则是不能正常工作的。所以就时好时坏。
修改也比较简单:
for (i = 0; i 《 8; i++)
{
SetData((data 》》 i) & 0x1);
SetClockHigh();
for (j = 0; j 《 5; j++);
SetClockLow();
for (j = 0; j 《 5; j++);
}
这样就完全正常了。但是这个还是不能很好移植的一个代码,因为编译器一优化,就有可能造成这两个延时循环的丢失。丢失了,就不能保证高电平低电平持续10ns的要求,也就不能正常工作了。所以,真正的可以移植的代码,应该把这个循环做成一个纳秒级的DelayNs(10);
像Linux一样,上电时,先测量一下,nop指令执行需要多长时间执行,多少个nop指令执行10ns。执行一定的nop指令就可以了。利用编译器防止优化的编译指令或者特殊的关键字,防止延时循环被编译器优化掉。如GCC中的
__volatile__ __asm__(“nop;n”);
从这个例子中可以清楚的看到,写好一段好代码,是需要很多知识支撑的。你说呢?
嵌入式往往没有支撑,或者因为有操作系统支撑,但因为种种的限制,操作系统提供的功能少得可怜。所以,很多代码不能像PC编程那样天马行空,任意驰骋。今天就聊聊内存分配的问题,内存碎片,可能大家都不陌生。然而在嵌入式系统里,最怕的就是内存碎片,也是系统稳定的头号杀手。我曾经做了一个项目,系统中有很多的malloc和free,尺寸不一,从60多个字节到64KB的不等。使用一款RTOS作为支撑。当时我有两个选择,一个是使用C系统库的malloc和free,另外一个是使用操作系统提供的固定内存分配。我们系统的设计要求要能稳定运行3个月以上。实际上连续运行6天左右就宕机了。各种问题都怀疑过,最后定为在内存分配上,其实就是长时间,大量的内存分配后,系统的内存变得零散而无法连续。虽有大空间,但却无法分配连续的空间。当有大空间申请时,只能是宕机完蛋。为了使系统达到原先的设计需求,我们在PC机上模拟了整个硬件,将嵌入式代码在 PC机上跑起来,并重载了malloc和free,做了个复杂的统计程序。统计系统的内存行为。运行了若干天以后,将数据提取出来分析,虽然申请的内存5花八门,还是有些规律,我们把100个字节以下的归为一类,512B的归为一类,1KB的归为一类,2KB归为一类,64KB一下归为一类。统计出每类的数量,在原先的基础上加上30%的余量。做成固定内存申请,使得系统稳定连续运行的时间大大加长。嵌入式就这样,不怕方法原始,就怕性能不达要求。
内存溢出问题,内存溢出问题嵌入式系统比PC系统更可怕! 往往是没有察觉的就溢出了。都很难想到,尤其是C/C++的初学者,对指针不熟悉,查都没法查。由于PC系统有MMU,内存发生严重的越界时,有MMU的保护,不会产生严重的灾难后果。而嵌入式往往没有MMU,差别很大,系统代码都被破坏了还能跑。只是只有上帝和那个CPU才知道跑得是什么。我们来看看这段代码:
char *strcpy(char *dest, const char * src)
{
assert(dest != NULL && src != NULL);
while (*src != ‘0’)
{
*dest++ = *src++;
}
*dest = ‘0’;
return (dest);
}
这个代码是一个字符串拷贝的代码,PC机这样写,基本上就可以了。但嵌入式要提防一件事情,那就是 src真的以‘0’结束的。要不是得话,那就悲剧了。到什么时候能结束,呵呵,只有上帝老人家才知道。这段代码侥幸能跑完成的话,估计也别想程序能正常的跑了。因为dest指向的内存区域都被破坏的差不多了。为了和标准C/C++的库兼容,还真的没什么好办法,所以这个问题只能留给程序员自己检查。
相同的,
memcpy( dest, src, n);
内存拷贝同样的问题,要提防n传递个负值进去。这个是拷贝多少个字节,负值被强制类型转换成正的。变成一个很大的正数,造成dest之后的内存全部被破坏……
嵌入式里的内存指针必须做严格的检查才能使用,内存的尺寸也必须进行严格的调试。不然的话,悲剧是很难避免的。如一个函数指针,虽然在嵌入式里赋了个NULL,0。若是ARM的话,连个异常错误都没有,直接复位了,因为调用这个函数指针即便是让代码从0开始运行。而0是ARM上电后运行的第一条代码的位置。在ARM7上尤其如此。这种悲剧比PC上悲情多了,MMU 定然给一个无定义指令的错误。引起程序员的重视。在嵌入式里,全部都留给了程序员去寻找了。
内存溢出发生在任何一个不经意的时刻,你给整个前后台的系统(或操作系统)分配了多大的堆?多大的栈?在通常情况下系统的调用深度是多少(最大是多少),占用多少栈?光看程序的功能正确还不够,还需要统计这些参数。不然,只要有一个地方有溢出。对系统都是致命的。嵌入式系统要求系统连续工作时间长,稳定性可靠性要求苛刻。是需要一些时间仔细的磨这些系统的。
嵌入式系统的调试往往很复杂,可用的手段并不像PC编程那么多,开发成本较PC系统也要大很多。嵌入式系统调试主要手段只有JTAG为代表的单步追踪、printf夹杀大法等。
这两种调试方法在嵌入式中也不尽然全部能解决问题。Jtag需要调试者有一个调试设备(有可能很昂贵),和目标系统相连。使用类似GDB Client等软件登录调试设备,跟踪运行程序。说实话,这个方法对嵌入式来讲是终极的调试办法,也是比较好的调试方法。但仍然有几个不足,当断点过多时,超出硬件的限制,某些低档的CPU不支持更多的断点,就需要JTAG利用软件模拟,或采用软件陷阱(软中断或异常)等办法实现断点。机理比较复杂,简单点说,1.不能进行长时间调试,不太稳定; 2.有可能影响程序的运行时刻的行为,通过时序影响。挂接JTAG系统后,利用硬件实现的断点不会影响系统运行的速度,但是软件实现的断点是必定牺牲一些性能的。可靠性也要打折扣的。当断点太多,而系统又进入临界区域,可能会造成断点不起作用。因为嵌入式实现全局临界区域往往需要关闭中断,有些CPU没有非屏蔽中断,当断点超过一定数量,使用软件断点,而软件断点又需要在中断工作的情况下使用……
特别调试时序问题和高速通信类的代码,JTAG帮助并不大。通信过程往往很快,通信包也是接二连三,才能完成一个完整的动作。如果是高速通讯,断点是无法让程序完成工作的。所以只能使用printf夹杀的办法,printf夹杀办法很好。但是也要注意几个问题:嵌入式系统往往没有屏幕,printf输出是通过串口输出。而串口工作模式有两种,一种是查询,另外一种是中断,或DMA。不管哪种,调试输出的printf只能使用查询的办法输出,千万不要使用中断或DMA的办法。不管是前后台程序也好,还是操作系统也好,都有不方便的时候,也许在全局临界内需要打印(关闭了中断),也许需要在中断里打印(不允许嵌套中断),也许要在一些驱动里打印(很多配合的设备没有初始化,内存分配和中断并不能很好的工作)。在这些情况下,利用Uart中断输出字符是不明智的。所以调试输出只能使用查询的办法。不要幻想着使用什么牛叉的办法,不必了。一句话,不可靠!既然做调试,那可靠的输出结果是第一要求。也就是因为如此,printf也会影响代码的工作效率,串口最高的波特率115200bps,越快速的CPU越是浪费时间,因为需要等待上一个字符输出完毕,这段时间完全是通过空转消耗这部分时间。所以使用printf要有一些技巧,在不影响一些关键时序的位置下再打印,而不是随意烂打……淹没了bug。
以上这两种办法并不能很好的解决全部的问题,在实际中如果嵌入式系统有一两个LED灯,尝试用IO口将其在特殊的情况下点亮熄灭的办法,也可表示程序的状态。这种办法适合调试中断、临界区域这些问题。点量LED灯需要的时间是非常短的,基本上是一条内存读写命令,如果IO口寄存器是CPU统一编址的话。基本上造成的影响微乎其微。在调试一些复杂的时序的时候,还可以使用空闲的IO口,将其在特殊的情况下拉低,拔高,然后利用数字示波器或者逻辑分析仪抓取再具体分析。特别是分析一段代码的执行频度,执行时间,优化效果等。对整体的性能提升等,有非常大的意义。对于简单的单片机,厂商开发软件都有个时序统计的功能。但对于有cache和MMU的单片机,时序统计并不准,往往不如用示波器测得的准。如果没有示波器利用CPU内部的时间计数器也可以实现时间的统计,需要结合printf使用。
我一个同事,调试飞利浦的ARM7,由于飞利浦ARM7外扩的RAM全部是静态RAM,即使在CPU死机情况下,只要不断电,SRAM里的数据也不会丢失,由于SRAM和内部的SRAM统一编址,所以,访问起来也就是一条读写指令,速度很快。利用这个特性,他把程序的模块和点全部标记上,当系统运行不正常,将ARM7复位以后,ARM7上电第一个工作就是取出复位前的数据打印出来。由此可调试ARM7的代码,非常巧妙的办法。如果只有SDRAM的朋友们是不能用这种办法的,因为只要系统复位,SDRAM没有刷新,数据即会丢失。
地球人都知道,嵌入式的最大挑战在于硬件和软件同时成熟;出了个问题,不知道是软件问题还是硬件问题。当然,可以通过虚拟的方式解决大部分问题,但虚拟终归是虚拟。不是实际,上了实际的板子,还是有不少问题。嵌入式领域,特别是底层技术,由软件(驱动)和硬件两个部分组成。解决起来,需要两个部分的知识,对人员的素质要求更高。我曾经遇到很多棘手的问题,都是复杂的系统问题。
1.一个系统要求连续不断的24小时工作,即使断电,也要保存断电状态。在电源正常时,就必须恢复断电前的状态,继续工作。
实际中,我们也这样做了软件,但是实际效果并不是所想得那样。一万次断电,总有那么几十次不正常;又没办法重现,只能是猜来猜去。因为系统断电,这个也不好调试,挂着JTAG,系统现在断电了,目标板也就没电了。也就没办法调试跟踪单步了。本来的设计思路是,控制电路利用电容存储的一些些能量在断电后继续工作,保存状态,保存好后,进入待机状态。测试检测断电的信号后,也是没有问题的。后来,这个问题变成悬疑问题了……
这个系统分为两个模块,工作模块和控制模块。控制模块有电容继续供电,而工作模块没有电容工作;所以当发生断电时,全系统不是同一时间断电的。当控制模块检测到断电时,实际上工作模块早都没电了,所以工作模块不能正确的传递相关的数据回来,造成控制模块不能正确的工作。两个断电的时序非常的接近,无法判断其先后。解决的方法也很简单,就是把断电检测模块以工作模块为主进行同步,就没有问题了。
2.还是断电保护的问题,我们用继电器模拟断电的情况上万次正常后,终于上整机实验了,结果经常发现断电无法正常保护的现象。仔细查看电路也没有什么异常,都是一样的。结果工程部指责我们研发部没有仔细测试,发出来的东西都是有问题的东西。哎,伤心啊。后来经过仔细的分析,我们认为,软件异常的可能性很小。主要问题还是在硬件上,硬件上的超级电容可能在频繁的断电下,没有存储够足够的能量,使得系统完成保护过程。那么究竟是什么造成频繁的断电呢?按照设计要求,超级电容在3~5s内就会充满到80%的能量,理论上足够了。又有什么会不到3~5s钟频繁的断电呢?
说出来都匪夷所思,使用数字示波器不间断跟踪控制板的电源,才发现。原来是三相交流电需要接一个相位保护器,相位保护在系统工作时会频繁的开关(可能和系统的状态有关)。解决方法是,简单的把控制器的电源接在相位保护器前面就好了。
这些问题看似都是硬件问题,也是在产品的调试过程中经常碰到的问题。这些问题,需要软件工作人员确认软件中的Bug是否能造成这种情况,然后,还需要硬件工程师确认硬件。当然,硬件的确认过程漫长复杂,并且调试手段非常有限;嵌入式软件的调试相对于硬件来讲,成本和收效都会好一些。所以往往需要嵌入式软件人员花很多时间确认软件问题,最后才怀疑硬件。作为嵌入式开发人员,能了解硬件的基本原理,结合软件的工作原理,和硬件工程师一起配合实验定位错误,是非常有效的办法。
网上有些朋友经常问我一些问题。有关于底层的知识,其中不乏一些多处理器的问题。关于多处理器的问题,我也才疏学浅,说来与大家讨论一下,关于领域的 多CPU的应用。嵌入式说来说去是计算机科学的应用领域之一。既然是计算科学的应用领域之一,那么要做好这个领域,必须有过硬的计算机理论知识。