能从PC机器编程去看嵌入式问题,那是第一步;学会用嵌入式编程思想,那是第二步;用PC的思想和嵌入式的思想结合在一起,应用于实际的项目,那是第三步。很多朋友都是从PC编程转向嵌入式编程的。在中国,嵌入式编程的朋友很少是正儿八经从计算机专业毕业的,都是从自动控制啊,电子相关的专业毕业的。这些童鞋们,实践经验雄厚,但是理论知识缺乏;计算机专业毕业的童鞋很大一部分去弄网游、网页这些独立于操作系统的更高层的应用了。也不太愿意从事嵌入式行业,毕竟这条路不好走。他们理论知识雄厚,但缺乏电路等相关的知识,在嵌入式里学习需要再学习一些具体的知识,比较难走。
虽然没有做过产业调查,但从我所见和所招聘人员,从事嵌入式行业的工程师,要么缺乏理论知识,要么缺乏实践经验。很少两者兼备的。究其原因,还是中国的大学教育的问题。这里不探讨这个问题,避免口水战。我想列出我实践中的几个例子。引起大家在嵌入式中做项目时对一些问题的关注。
第一个问题:
同事在uC/OS-II下开发一个串口的驱动程序,驱动和接口在测试中均为发现问题。应用中开发了个通讯程序,串口驱动提供了一个查询驱动缓冲区字符的函数:GetRxBuffCharNum()。 高层需要接受一定数量的字符以后才能对包做解析。一个同事撰写的代码,用伪代码表示如下:
bExit = FALSE;
do {
if (GetRxBuffCharNum() >= 30)
bExit = ReadRxBuff(buff, GetRxBuffCharNum());
} while (!bExit);
这段代码判断当前缓冲区中超过30个字符,就将缓冲区中全部字符读到缓冲区中,直到读取成功为止。逻辑清楚,思路也清楚。但这段代码是不能正常工作。如果是在PC机上,定然是没有任何问题,工作的异常正常。但在嵌入式里真的是不得而知了。同事很郁闷,不知道为什么。来请我解决问题,当时我看到代码,就问了他,GetRxBuffCharNum()是怎么实现的?打开一看:
unsigned GetRxBuffCharNum(void)
{
cpu_register reg;
unsigned num;
reg = interrupt_disable();
num = gRxBuffCharNum;
interrupt_enable(reg);
return (num);
}
很明显,由于在循环中,interruput_disable()和interrupt_enable()之间是个全局临界区域,保证gRxBufCharNum的完整性。但是,由于在外层的do { } while() 循环中,CPU频繁的关闭中断,打开中断,这个时间非常的短。实际上CPU可能不能正常的响应UART的中断。当然这和uart的波特率、硬件缓冲区的大小还有CPU的速度都有关系。我们使用的波特率非常高,大约有3Mbps。uart起始信号和停止信号占一个比特位。一个字节需要消耗10个周期。3Mbps的波特率大约需要3.3us传输一个字节。3.3us能执行多少个CPU指令呢?100MHz的ARM,大约能执行150条指令左右。结果关闭中断的时间是多长呢?一般ARM关闭中断都需要4条以上的指令,打开又有4条以上的指令。接收uart中断的代码实际上是不止20条指令的。所以,这样下来,就有可能出现丢失通信数据的Bug,体现在系统层面上,就是通信不稳定。
修改这段代码其实很简单,最简单的办法是从高层修改。即:
bExit = FALSE;
do {
DelayUs(20); //延时 20us,一般采用空循环指令实现
num = GetRxBuffCharNum();
if (num >= 30)
bExit = ReadRxBuff(buff, num);
} while (!bExit);
这样,让CPU有时间去执行中断的代码,从而避免了频繁关闭中断造成的中断代码执行不及时,产生的信息丢失。在嵌入式系统里,大部分的RTOS应用都是不带串口驱动。自己设计代码时,没有充分考虑代码与内核的结合。造成代码深层次的问题。RTOS之所以称为RTOS,就是因为对事件的快速响应;事件快速的响应依赖于CPU对中断的响应速度。驱动在Linux这种系统中都是与内核高度整合,一起运行在内核态。RTOS虽然不能抄袭linux这种结构,但有一定的借鉴意义。
从上面的例子可以看清楚,嵌入式需要开发人员对代码的各个环节需要了解清楚。
第二个例子:
同事驱动一个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的应用。嵌入式说来说去是计算机科学的应用领域之一。既然是计算科学的应用领域之一,那么要做好这个领域,必须有过硬的计算机理论知识。
首先多处理器分为好几种,
-
处理器是同一型号,大家完全一样,通过一种通讯方式连接,如多口的RAM,rapidIO,千兆级以太网,或者PCI-E等;
-
处理器不同型号,甚至架构都完全不一样。之间通过一种通讯方式连接,同上,如多口RAM、RapidIO等;
-
同一个芯片中集成了多个CPU。这几个CPU什么都共享,属于比多口RAM还要紧耦合的系统。
为什么要用多处理器?
-
大规模的并行运算;
-
想利用多个CPU的特点,如DM642这样的方案,应用于复杂的视频方案。想利用DSP的浮点计算能力,同时也使用ARM的事务计算能力;
-
单纯的提高系统的性能。
对于普通的应用,提高系统性能是基本出发点。但嵌入式系统应用多处理器并不是一个简单的事情。多处理器的软件设计难度很大,调试也是很大的问题。
如果不采用操作系统处理,采用前后台系统。那么自己还要设计一个通信算法,还要设计一个结果整合系统。这样的系统自己设计很多东西,其中总线的可靠和容错设计至关重要。 所以可能的话,利用成熟稳定的操作系统来支持多处理器可以减少不少的开发难度。然而,寻找这样的一个操作系统并非易事。
首先要明确自己的应用,需要线程进程迁移吗?需要处理器平衡吗? 对于多处理器,如果不支持线程进程迁移,那也就谈不上处理器任务的动态平衡,不然只能事前指定好线程进程运行于哪个处理器。对于异构型多处理器,线程迁移和进程迁移并没有多大的实际意义。对于追求利益的公司来说,目前还谈不上实用价值。所以,迁移只限于对称处理器。然而,对称处理器也不是什么进程可迁移。对于对称处理器,操作系统封装好底层,让用户开发起来像是对一个CPU再做开发,当然不可能与单个CPU完全一致,但起码减轻了许多难度。
很多朋友问我RTEMS可以跑在x86这样的CMP的多处理器上吗?当然。但是,设计起来又不同于普通的对称多处理器。因为,CMP处理器上的CPU共享了许多东西,中断,内存,总线,他们的编址空间基本上都是一致的。对于RTEMS这样的RTOS来说,它采用的是异构型的方式支持对称处理器,即有几个CPU就得跑几个RTEMS。那么通讯显得尤为重要,多个RTEMS需要多个系统的TICK,那么TICK从哪里来,CMP共享着很多资源,那么就要求,使用者必须为RTEMS手动的指定中断源,划分内存空间,这就造成了,CMP上的多个CPU虽然都是跑RTEMS,但是想关于CPU的驱动很多都是不一样的。这种紧耦合的系统是非常难办的。
相对于CMP,同 种CPU组成的SMP就要简单一些,因为全部驱动都是一样的,可能会因为通信方式的问题,通信驱动要特殊处理一下,但这会极大的减轻了开发的压力和调试的难度。总好比每个CPU一个Core,那是要崩溃了。特别是调试问题,所以从经济角度的问题考虑,还是比较喜欢这种多个相同的单个CPU组成的多处理器系统。
很多时候,对于那个异构型的处理器,当然用RTEMS也可以轻松摆平,但是还是一个问题,多个核心需要自己的RTEMS支持,开发多有不便。况且,操作系统的调试还是比较复杂的。所以现实版的方案都是,异构型处理器当中负责事务运算的处理器跑操作系统,而负责计算的处理器采用前后台系统,简单的通过共享内存通讯,响应操作系统的计算请求。这样大大的减小了开发难度,反正操作系统把DSP当作了个硬件的寄存器,写几个寄存器就能得到结果,或者是输入一组天文一样的数据,得到一个复杂的结果。Anyway,总之这样的反应式的处理方式是绝大部分工程中采用的方式。就是简单、可靠、实用。
看来,嵌入式系统中的多处理器还是与应用高度的相关