黎 建
(广州工商学院,广东 广州 528138)
随着科学技术的进步,嵌入式系统已大量应用于制造工业、过程控制、机器人、通讯、仪器仪表、汽车、船舶、航空航天、军事装备、消费类产品等国民经济的主要行业[1-3]。在嵌入式产品日渐普及和迅速发展的今天,对嵌入式人才的需求越来越大,对嵌入式系统开发人才的素质和能力要求也越来越高。嵌入式技术人才紧缺现象日渐突出,众多公司和科研院所不惜重金招聘嵌入式系统开发方面的高层次人才。高校是培养科研人才的重要基地,是创造高科技产品的重要基地,也是推动社会科技发展的重要力量,培养具备嵌入式系统设计能力的复合型专业人才已是迫在眉睫。然而,嵌入式技术涉及的知识面广、实践性强、技术发展快,跨越了电子、计算机、控制和通信等多个专业,学习难度大[4],对嵌入式系统教学提出了更高要求。嵌入式C语言是对嵌入式系统开发使用最多的语言,主要是由于C语言兼具高低级语言的特性,支持对硬件的直接操作[5],开发速度快、可读性好、工作效率高等优势,加上高级功能的开发必须在操作系统下进行,故逐渐取代了汇编语言成为嵌入式系统的主流开发工具。因此,在嵌入式教学中,都会使用嵌入式C语言来完成实验和实训项目。一般情况下,C语言课程都在大学一年级开设,是学生学习编程语言的入门课程。由于课时限制,学习的课程内容有限,导致学生对C语言一知半解。在嵌入式课程实验中,C语言编写的程序存在较多问题,调试进展缓慢,严重影响学习效率。让学生了解嵌入式C语言的特点,掌握嵌入式开发技巧,设计出良好的软件代码,是教师的重要职责。
嵌入式系统是以应用为中心,以计算机技术为基础,并且软硬件可裁剪,适用于应用系统对功能、可靠性、成本、体积、功耗有严格要求的专用计算机系统。嵌入式系统要求高效率、低成本,在软件设计上,代码的优化尤其重要,还有硬件资源的利用。嵌入式的概念非常广,嵌入式计算机可以从8位的51系列单片机到64位的ARM系处理器。嵌入式计算机有运行内存容量比通用计算机(如PC机)少的特点(手机应用除外),一般在100多字节到100 M字节间,因成本要求,不可能丰富。嵌入式系统课程都在大学三年级和四年级开设,而大学一、二年级的上机实验课程基本上在PC机上进行,对硬件资源几乎没有约束,学生也就“大手大脚”:在程序代码里喜欢定义和申请大数组,其实数据量并不大;可以使用一维数组的,却使用二维数组;可以用char类型数组来存放的数据,为图方便,经常使用int数组类型;本可以用一个for循环解决的问题,却使用多循环结构,只要结果正确就行,不管CPU耗时多少。如果嵌入式设计还是采取这样的形式,可能导致项目失败或成本剧增。为了纠正学生这些不良编程习惯,教师先要讲解嵌入式系统资源的组成,不同芯片有不同资源;不同功能需求,使用不同的芯片;不同资源,有不同成本价格。设定项目需求、CPU类型和存储容量,让学生采用C语言编程实现,通过检查学生的代码,了解他们对资源的使用情况、程序优化情况以及运行情况。通过这样的训练,可以培养学生良好的编程习惯和嵌入式软件编程风格。
在嵌入式应用领域,计算机运行速度是一个非常重要的指标,在满足功能的前提下,希望速度越快越好。在硬件一定的情况下,系统的执行时间取决于编程效率和优化处理等,而学生不一定能理解和掌握。
在嵌入式C语言中,内存分配方式主要是堆和栈。栈是一种数据结构,计算机会在底层对栈提供支持,分配专门的寄存器存放栈的地址,压栈出栈都有专门的执行指令(如ARM系列的STMFD,LDMFD指令),这决定了栈的效率比较高,但栈空间一般都不大,很容易溢出。用malloc()分配的空间就是堆空间,编程者可以自由分配和释放。另外,还有全局与静态数据、常量等存放空间。显然,由于栈空间有硬件的支持,栈内存取数据是最快的,故存放函数的局部变量、返回值等热数据。
下面的程序,破坏了数据访问局部性,运行效率不高,执行速度下降:
在求和过程中,变量sum要调用1 000次,是热数据[6],应该定义在函数内,放到栈空间里,上面程序没有,而是作为全局变量定义。如果把sum作为静态变量放在函数内,其效果类似于全局变量,执行效率都受影响,因为二者都没有把变量定义在栈中,破坏了数据访问的时间局部性。
在实验项目编程中,很多学生喜欢使用C语言数组,觉得简单、方便,但对数组的读写机制并不清楚。看下面这2段程序。
程序1
程序1和2功能相同,时间空间复杂度一样,执行时间一样吗?多数同学会回答是一样的。其实,区别还是很大的,主要涉及2个方面的问题:首先,数组分配内存,一般是按照行优先连续存放的,即存放顺序为a[0][0],a[0][1]…a[0][N-1],然后才是a[1][0],a[1][1]…;对于程序1,数组的读写,符合行优先的规则,下个单元的读写,只需要地址按字增1即可,但对程序2,下个单元的读写,每次地址需要做+N运算,降低了程序运行速度。其次,高档一点的嵌入式CPU,如ARM系列,为提高性能,一般都有cache,但容量不大(个别除外),如Cortex-A8处理器一级cache配置了16 kB;假定不考虑其他条件(如二级cache),对Cortex-A8处理器,使用程序1,每执行2N次循环,只需要从主内存读数据到cache一次(4*N*2=16 kB,int型4字节),如果使用程序2,每2次循环后,chache数据失缺[6],命中失败,要从主内存再读2行数据到cache,如此类推,每执行2N次循环,需要从主内存读数据N次,很不合算,运行速度大为降低。所以,一定要让学生理解数组存放的行优先原则(一般不会设置列优先),还有cache的作用。当然,如果使用指针来代替数组索引,还能加快速度。
很多嵌入式CPU(一些8位单片机)并没有乘法器硬件,做乘法运算只能间接实现,速度慢、效率低。其实,可以通过移位操作和加法运算来完成乘法运算。一个二进制数左移一位,相当于乘2,右移一位,相当于除2。利用这一性质,可以完成一些乘法运算功能,而移位操作是最快的指令之一。
(1)变量X乘以2n,则直接左移n位即可。如8*X=23*X,编程可以写成X=X<<3。
(2)变量X乘以其他数,则需要进行分解处理。如100*X,可以分解为(4+32+64)*X=(22+25+26)*X,编程同a类似,可以写成X=(X<<2)+(X<<5)+(X<<6)。
总而言之,通过移位和加法运算,可以完成一些主要的乘法和一些特殊的除法(右移一位相当于除2),这对于没有硬件乘法器的CPU,能加快运行速度。很多学生不知道对没有乘法器的CPU,应该怎样加快乘法运算。
嵌入式C语言同其他语言一样,有很多关键字,有几个关键字在嵌入式软件开发中是很重要的,但在C语言课程中一般不会学到,这几个关键字跟硬件有关。
interrupt不是标准的关键字,但在使用嵌入式C语言编程时普遍应用,特别是在单片机项目中。用interrupt修饰的函数,应看成一个中断处理函数(ISR)。中断处理函数要求特殊的寄存器保存规则,以及一些特殊的返回序列。当C代码被中断时,ISR必须预先保存所有会被ISR用到的寄存器内容,中断返回时,按逆顺序弹出。中断处理程序需要满足下列要求:中断处理程序不能有返回值,不能给中断处理程序传递参数,中断处理程序应尽量简单精炼[7]。由于学生以前没有学过这些,往往很容易当作普通函数对待,导致在实验中调试失败。
volatile关键字在嵌入式C中频繁使用,作用是告诉编译器该变量是易变的,要编译器去注意该变量的状态,变量是易变的,每次读取该变量的值都重新从内存中读取。也就是说,优化器在用到这个变量时必须每次都重新读取这个变量的值,而不是使用保存在寄存器里的备份[8]。有几种情况需要volatile关键字来修饰变量。
(1)变量值是一个特殊地址,如寄存器地址。
(2)子线程与主线程共享的全局变量。
(3)中断处理函数(ISR)访问到的变量。
如果程序编译时不做优化处理,volatile可能看不出作用,不优化的程序效率低下;如果做优化处理,上面几种情况的变量不使用volatile关键字,可能导致值不一致,如一个定时器内的变量没有用volatile修饰,其定时时间与计算的时间相差很大。
被regester修饰的变量,使用寄存器来存储数据。由于寄存读写速度比内存快一个数量级以上,对于经常反复使用的变量,若在寄存器中读写,程序的执行时间要快不少:
程序中的s、i变量每个循环都要使用,放入寄存器中能大大地缩短运行时间。不过,使用register修饰符有几点限制。
(1)register变量长度应该小于或者等于整型的长度(即机器的字长)。
(2)不能用“&”来获取register变量的地址,寄存器不是内存。
(3)只有局部变量和形参可以作为寄存器变量。
由于CPU中的寄存器数量有限(51系列单片机少的只有几个,而ARM系列则有几十个),当寄存器不够用时,编译器会自动忽略register修饰符。随着编译技术的发展,嵌入式C语言在优化方面可能比程序员做得更好,在决定哪些变量应该被存到寄存器中时,可能编译器会考虑。
在嵌入式软件开发过程中,经常用到位操作,如I/O口控制、端口寄存器的设置等。大部分CISC类CPU,有位操作指令(如51系列单片机的SETB bit指令)。但是,对于RISC这类CPU,由于指令要精简,没有设计位指令。要完成对端口寄存器的设置,只能采用组合操作,不但麻烦,而且可读性极差,学生不能理解,换一下设置要求就不会编程了,很难举一反三。ARM系列S5PV210芯片是应用很广的32位CPU,有多个I/O口,既可以作为输入口,也可以作输出口使用,通过设置端口控制寄存器来复用,如设置GPH3的控制寄存器GPH3CON可以决定对应的8个I/O口是作为输入还是输出使用。由于没有位操作指令,要对变量(或寄存器)的某些位设置,只能通过嵌入式C语言的一些算术运算和移位操作来达到。如果只是使用某个寄存器其中的某一位,其他位的定义不变,则:
这对刚进入嵌入式领域的学生,上面的语句难以理解。
GPH3CON寄存器以4位为一组确定复用功能,0000作为输入,0001作为输出。可见,GPH3需要用4×8=32位来控制8个I/O口。如果借助于C语言的联合位域结构来设置寄存器,学生就很容易理解了。这里假定,要求GPH3的0,2,4,6位作输入,1,3,5,7位作为输出,如设置GPH3CON寄存器的嵌入式C联合体如下。}H3;
位域bit与gpcon共32位存储单元,bit的每个成员占4位,正好作为设置GPH3CON寄存器复用功能的一组数据。
H3.gpcon=GPH3.GPH3CON;//如果H3的每个口都要设置,这一句就多余
显然,这种设置控制寄存器的方法,学生容易理解,很快就会掌握设置方法。经过多次练习,上面寄存器设置,不再需要联合位域,而是直接设置:GPH3.GPH3CON=0×10101010。
也可以举一反三:如果定义输入的4位值不是0000,是0010,则GPH3.GPH3CON=0×12121212。
随着嵌入式技术的迅速发展,嵌入式C语言应用越来越广泛,对嵌入式人才的需求越来越大,开设嵌入式系统设计课程的高校也越来越多。有资料统计,90%以上的嵌入式系统应用代码都是采用C(或者C++)编写的[9],掌握好C语言是学习嵌入式系统对前导课程的要求。由于课时限制,课程中很多内容没有讲到,导致学生的C语言基础不扎实,在嵌入式课程实验中,往往C语言编写的程序问题较多,特别是不会优化处理。笔者根据多年的教学经验,在文章中所提出的问题,都是学生在学习嵌入式系统课程时经常发生的,希望能够起到抛砖引玉的作用,达到希望的教学目标,尽量通过程序优化和编程技巧等方式降低前述问题发生的概率,提升嵌入式软件开发的整体效率与质量[10]。