引言
对于嵌入式系统的单片机程序设计,其目标代码的运行效率是很令人关注的[1-2]。开关语句switch是C51程序设计中的一个重要语句,其逻辑清晰、层次结构分明、易读性和可维护性都很好,因此受到程序设计者的青睐。本文分析目前流行的Keil C51编译,分析其对switch的编译方法及其编译效果。
Keil C51编译switch-case语句,对于分支少于8的switchcase语句,其转移控制程序一般情况是将候选值(即case之常量)逐一比较,即编译成比较跳转指令形式,代码率很高;但对分支超过8个的较为复杂的switchcase语句,考虑到代码的执行效率,其转移控制程序要调用不同的系统库函数进行处理,其运行效率差强人意。
1复杂式switch控制转移编译方法
分支大于8的情形,如果其Δx(即case值之间的差)小于255,则采用复杂式编译。因为51单片机是8位单片机,执行字符型运算只要一条指令,执行整型运算至少要两条指令,长型运算则要4条指令。基于代码的执行效率考量,Keil编译对于选择因子是字符型(8 位)、整型(16 位)或长型(32 位),所提供的编译略有不同,对应的函数库分别是CCASE 、 ICCASE或LCASE。 编译的目标代码虽然不同,但算法基本相同。
本编译方式所产生的开关语句switch结构分4层:开关头码、转移控制代码、转移表、开关体,其结构如图1所示。

图1 开关语句编译后代码结构
转移表由地址域和值域构成,按照case值上升排序,其结构如表1所列。

控制转移算法描述如下:
① 扫描开关转移表;
② 判断地址域是否为0;
③ 不为0,则跳转至⑤;
④ 表终止标志,计算switch出口地址在表中位置,跳转至⑥;
⑤ 判断值域是否与A相等,若不等,则跳转至①;
⑥ 从表中取出相应的入口,间址转移至分支处理程序。详细算法,参见图2。

图2 转移控制算法流程
下面选择字符型为例,说明复杂式switch编译的目标代码及其代码结构。
例:复杂式switch源程序段如下:
switch(x){
case 3: y=2*x;break;
case 7: y=5+x;break;
case 32: y=7/x;break;
case 21: y=x; break;
case 30: y=25;break;
case 40: y=x*3;break;
case 47: y=x*4;break;
case 50: y=x/2;break;
case 43: y=x/3;break;
}
编译后的目标代码分解如下:
(1) 转移控制库函数CCASE
C:0x0381D083 POPDPH(0x83) ;转移表首址≥DPTR
C:0x0383D082 POPDPL(0x82)
C:0x0385F8 MOVR0,A;选择因子送R0
C:0x0386E4 CLRA
C:0x038793 MOVCA,@A+DPTR;取入口地址码高字节
C:0x03887012 JNZ C:039C;如果非空转移39C
C:0x038A7401 MOVA,#0x01;取入口地址码低字节
C:0x038C93 MOVCA,@A+DPTR
C:0x038D700D JNZC:039C;为0,表终止,否则转39C
C:0x038FA3 INCDPTR;本层结束,计算switch出口地址
C:0x0390A3 INCDPTR
C:0x039193 MOVCA,@A+DPTR;取转移地址
C:0x0392F8 MOV R0,A
C:0x03937401 MOV A,#0x01
C:0x039593 MOVCA,@A+DPTR
C:0x0396F582 MOVDPL(0x82),A;转移地址送DPTR
C:0x03988883 MOVDPH(0x83),R0
C:0x039AE4 CLR A;转移到相应的处理程序
C:0x039B73 JMP @A+DPTR
C:0x039C7402 MOVA,#0x02;取case的值
C:0x039E93 MOVCA,@A+DPTR
C:0x039F68 XRL A,R0;判断是否与选择因子相同
C:0x03A060EF JZC:0391;相同,跳转
C:0x03A2A3 INC DPTR;推进表指针,指向下一条记录
C:0x03A3A3 INC DPTR
C:0x03A4A3 INC DPTR
C:0x03A580DF SJMPC:0386;下一轮判定
(2) switch的开关头码
40: switch(x)
C:0x041FEFMOVA,R7;switch的开关头码
C:0x0420120381LCALLCCASE(C:0381);转移控制函数CCASE
(3)转移表
C:0x04230442;转移表,分支地址即地址域
C:0x042503;3 ,分支值域
C:0x04260445;分支地址0x0445
C:0x042707;7 ,分支值域
C:0x04290452
C:0x042A15;21
C:0x042C0456
C:0x042E1E;30
C:0x042F044C
C:0x043120;32
C:0x0432045B
C:0x043428;40
C:0x04350475
C:0x04372B;43
C:0x04380464
C:0x04392F;47
C:0x0440046D;分支地址即地址域
C:0x043D32;50,分支值域
C:0x043E0000;表终止标志
C:0x0440047E;本层switch结束出口
(4) 开关体
开关体代码物理排序没有变化。
41: {
42:case 3: y=2*x;break;
C:0x0442EFMOVA,R7;值域为3的处理程序
C:0x04438022SJMPC:0467
……
50: case 43: y=x/3;break;
C:0x0475EFMOVA,R7;值域为43的处理程序
C:0x047675F003MOVB(0xF0),#0x03
C:0x047912035FLCALLC?SCDIV(C:035F)
C:0x047CF508MOV0x08,A
}
(5) 本层的出口地址
C:0x047E;
2简式switch转移控制编译方法
对于分支不大于8的情形,且Δx小于255,则采用简单形式编译转移控制代码。方法如下:
设xn为n个case值,且xn>xn-1,则有
Δxn=xn-∑h-1i=1xi(n=2,3,…,8)
简式的算法是:将选择因子逐一与Δx相减,若结果为0,则转移到相应的分支处理程序。
例:简式switch源程序如下:
switch(x){
case 3: y=2*x;break;
case 7: y=5+x;break;
case 32: y=7/x;break;
case 21: y=x; break;
case 30: y=25;break;
case 40: y=x*3;break;
case 47: y=x*4;break;
case 50: y=x/2;break;
}
表2为case值的编排结果,相应的Δx如表3所列。


程序中减法采用补码加法运算,最后一项为第一个值域的值差。
(1) 开关转移控制代码
40: switch(x)
C:0x041FEFMOVA,R7
C:0x042024F9ADDA,#0xF9;补码加,即减7
C:0x0422601FJZ C:0443;相等,则转到相应分支程序
C:0x042424F2ADDA,#0xF2;补码加Δx3,即减14
C:0x0426602DJZ C:0455
C:0x042824F7ADDA,#0xF7;补码加Δx4,即减9
C:0x042A602DJZ C:0459
C:0x042C24FEADDA,#0xFE;补码加Δx5,即减2
C:0x042E601AVJZC:044A
C:0x043024F8ADDA,#0xF8;补码加Δx6,即减8
C:0x0432602AJZC:045E
C:0x043424F9ADDA,#0xF9;补码加Δx7,即减7
C:0x0436602FJZC:0467
C:0x043824FDADDA,#0xFD;补码加Δx8,即减3
C:0x043A6034JZC:0470
C:0x043C242FADDA,#0x2F;最后一项,加上与第一项case值之增量
C:0x043E7036JNZC:0476;不相等,switch结束,否则顺序执行第一项case
(2) 开关体
41: {
42: case 3: y=2*x;break;
C:0x0440EFMOVA,R7
C:0x04418027SJMPC:046A
结语
考虑到嵌入式软件运行效率要高的特点,Keil对switch的编译,根据选择因子的数据类型,采用4种方式。其中,简式编译效率最高,其设计方式比较精巧,很值得程序员学习[7,8]。但在简式switch程序中,程序员对case值的排列是毫无用处的,因此,当根据事件发生频度来设计程序时,最好采用if语句。对于复杂式的编译,其代码结构很符合程序设计的风格,即代码与数据分离,建立一个由地址域和值域构成的转移表,然后根据选择因子的数据类型,构造算法相同的扫描转移表完成代码的编译,算法简洁,但由于是顺序扫描,效率较差[3]。对于长型的选择因子,其编译后的代码较长,效率比较低,因此,在程序设计时,要慎用长型。当不得已使用长型选择因子时,如果选择因数(case 值)不是很多,选取if-then-lese的语句效果会更好。
许多参考文献都在讨论嵌入式软件设计的运行效率[4-8]。单片机程序的一个重要设计目标是对硬件的控制,因此,用汇编语言设计程序可以取得较好的效果,但汇编语言程序较难掌握,对于较为抽象的算法,用汇编语言更难,并且其可读性和可维护性较差,因此,用混合编程方法是一个好办法。另外一个途径就是,利用C语言先行设计,然后根据Keil编译的结果进行修改,也可以得到非常满意的效果。