2024年4月7日发(作者:)

Keil uVision4编译器代码空间优化指南

目 录

摘 要 .................................................................................................................................................... 1

1. Keil链接定位器(Code Linker)设置 ...................................................................................... 2

2. 未调用(UNCALLED)函数的处理 ........................................................................................ 5

3. 编译优化等级设置 ...................................................................................................................... 7

3.1 全局代码优化 ....................................................................................................................... 7

3.2 局部代码优化 ....................................................................................................................... 8

3.3 优化设置中的注意事项 ....................................................................................................... 9

4.

5.

8位机与16位机编译差异 ....................................................................................................... 12

Keil C编程与调试技巧 ............................................................................................................ 15

5.1 存储器类型 ......................................................................................................................... 15

5.2 C语言中嵌入汇编 .............................................................................................................. 15

5.3 volatile修饰声明 ................................................................................................................. 15

5.4 静态局部变量 ..................................................................................................................... 16

5.5 静态全局变量 ..................................................................................................................... 16

5.6 static 函数 ........................................................................................................................... 16

5.7 位域 ..................................................................................................................................... 17

5.8 C51 intrins.h库文件 ............................................................................................................ 17

5.9 指针 ..................................................................................................................................... 17

5.10 C程序优化 ........................................................................................................................ 18

(1) 程序结构的优化 ................................................................................................ 18

(2) 代码的优化 ........................................................................................................ 19

附录 ............................................................................................................................................ 21

版本更新 .................................................................................................................................... 24

6.

7.

摘 要

随着家电产品的功能日益丰富、应用方案的平台化兼容趋势以及IoT概念下Wi-Fi通信

处理的引入,对单片机的ROM及RAM空间大小提出了更高的要求。但随着ROM空间增大,

应用方案的成本也相应提高,在能够保证量产可靠性的前提下,优化程序代码是更为合理的

选择。

然而,相对于16位单片机,8 bit MCU的编译效率存在无法避免的劣势(如高位运算指

令、拓展指令集等)。在实际应用中,考虑到应用程序指令密度以及计算复杂度不同,在不应

用Keil编译器优化设置的情况下,8位机所编译生成的代码体积比16位机可能增大30%以上。

故本文针对中颖8 bit单片机所使用的Keil uVision4仿真平台,提出了5项代码空间优化

方式。需要注意,由于不同应用程序及编程语法在Keil编译器中的处理方式存在差异,故在

使用文章中所涉及的优化方法时,应当设计适当验证实验,以保证软件在量产测试下的可靠

性。

根据经验,采用本文的优化方式后,在将瑞萨16位单片机软件移植到中颖8位机上时,

软件空间增大量可控制在10%左右。

关键词: Keil仿真环境 编译优化 8位单片机 Keil C编程与调试技巧

-1-

本文针对在Keil uVision4仿真环境下,说明了如何通过调整编译选项等多种方式,实现

对C语言源程序的空间占用进行优化处理。

1. Keil链接定位器(Code Linker)设置

在Keil C5l环境下,源程序在经过C5l编译器编译后,首先生成地址浮动的目标代码文

件。这种浮动地址的目标代码必须经过Linker(链接定位器)BL5l或LX51的链接和定位,

进而生成具有绝对地址的目标代码,最终才能够写入芯片的ROM或SRAM中正常运行。最

终生成的目标代码中,各函数所对应的片上地址可以通过M51(选择BL51 Linker生成)或

MAP(选择LX51 Linker生成)文件【双击Project下的Target】查看,如图1所示:

图1 链接器定位后各函数及对应绝对地址一览

在Keil uVision4及后续版本,均有两种链接器可供选择:默认的BL51 Linker以及功能

增强的LX51 Linker,可通过在:Options for Target->Device下进行勾选,如图2所示。

图2 Linker链接器选择标签页

BL51为早期Keil版本延续下来的链接器,可支持代码空间上限为64KB;LX51链接器

为Keil扩展型链接器,可支持代码空间上限为8MB,且相对于BL51 Linker增加了更多的链

接定位功能,列举部分增强功能如下:

(1) AJMP/ACALL优化:当使用此项优化、链接器重新调整程序段,以便AJMP

-2-

和ACALL指令的使用;

(2)

(3)

(4)

(5)

(6)

(7)

(8)

链接器代码封装:链接器优化使整体程序大小减少多达8%;

整体汇编代码文件:链接器能产生一个程序范围内混合源程序或者汇编程序的

列表;

段和类别信息:LX51的链接器产生特殊符号,这些符号可以用来为段和类型在

应用程序中使用获取地址和详细信息;

用户提供的存储器类型:userclass的指令(对于C51及CX51编译器)允许为编译

器生成段指定类型名称;

Bank表优化:LX51的链接器允许复位后指定默认的存储代码。这项优化减少

了Inter-Bank跳转表的大小;

FAR型存储器支持:LX51的链接器最多可支持8Mbytes的代码和8Mbytes的

内部及外部空间数据;

详细数据类型检查:LX51链接器在目标模块中进行比较并报告任何不匹配,这

有助于在函数的声明或结构或变量中找到细微的错误。

现以AJMP/ACALL优化处理为例,说明LX51 Linker对减小代码空间的方式。由于选项

“AJMP/ACALL优化”仅在LX51 Linker中被支持,故需要先按照图2所示,勾选LX51 Linker。

然后,在如图3所示标签页下,勾选“Linker Code Packing(max. AJMP/ACALL)”即可开启

ACALL优化功能。

图3 AJMP/ACALL优化选项标签页

在勾选ACALL优化后,当被调用函数在ACALL寻址范围内(2KB)时,编译器会将函

数调用指令反汇编为ACALL;但若ACALL优化关闭或被调用函数超出寻址范围,则编译器

默认将函数调用指令统一反汇编为LCALL,如图4~6所示,本章例程见附件一:

-3-

a. 反汇编程序 b. C语言源程序

图4 不勾选ACALL优化,则反汇编指令为LCALL

a. 反汇编程序 b. C语言源程序

图5 勾选ACALL优化,但函数地址超过2KB寻址范围,则反汇编指令为LCALL

a. 反汇编程序 b. C语言源程序

图6勾选ACALL优化,且函数地址在2KB寻址范围内,则反汇编指令为ACALL

而在中颖8 bit单片机指令集中,LCALL为3字节指令,ACALL为2字节指令,因此当

开启ACALL优化后,可以在调用函数或跳转时节省1个字节的代码空间,从而达到优化编

译效率的目的。

-4-

2. 未调用(UNCALLED)函数的处理

随着客户软件应用平台搭建的需求不断增加,目前的很多应用方案中都会考虑到软件兼

容以及移植问题,故部分工程师在软件工程项目中经常会留有许多未调用函数,以保证在不

同方案下代码的通用性。但此类方法会导致程序代码冗余的新问题,极大降低了芯片FLASH

的利用率。

在Keil编译器中,LX51 Linker提供了“REMOVEUNUSED”功能,通过简单的指令申

明,即可解决该问题:

(1) 在图2所示标签页下,勾选“Use Extended Linker(LX51)instead of BL51”;

(2) 在图7所示标签页中(Options for Target->LX51 Misc->Misc controls)的输入框内

添加“REMOVEUNUSED”指令关键词;

(3) 确认后再编译,则可发现编译器已经在编译时忽略了未调用函数;

图7 LX51 Linker的指令申明标签页

需要注意,当该指令申明有效后,未调用函数处理方式与程序注释效果一样,未调用函

数不参与编译,也不会下载到ROM区。故如果有固件更新代码备份等此类需要烧写但不执

行的程序,建议将其放置在EEPROM内。中颖单片机内建EEPROM烧写文件设置如图8所

示,该烧写文件支持HEX和BIN格式文件:

-5-

图8 中颖单片机内建EEPROM烧写文件配置

-6-

3. 编译优化等级设置

Keil uVision4及以上版本均支持全局和局部代码优化选项。

3.1 全局代码优化

全局代码优化等级设置如图9所示,Code Optimization的“Level”栏即用于设置C51的

代码全局优化等级,共有9个优化级别,且高优化级别中包含了前面所有的优化级别。

图9 全局代码优化等级设置标签页

事实上,由于实际应用中功能需求或程序编写习惯不同,会存在不同优化等级要求,故

列举如表1所示Keil编译器中各优化等级及优化内容,以供设置代码优化等级作参考:

表1 Keil C51优化等级设置说明表

优化

等级

描 述

常数合并:编译器预先计算结果,尽可能用常数代替表达式,包括运行地址计算。

优化简单访问:编译器优化访问8051系统的内部数据和位地址。

跳转优化:编译器总是扩展跳转到最终目标,多级跳转指令被删除。

死代码删除:删除没用的代码段。

拒绝跳转:检查条件跳转指令,以确定是否可以倒置测试逻辑来改进或删除。

数据覆盖:确定适合静态覆盖的数据和位段,并内部标识。BL51连接/定位器可以通

过全局数据流分析,选择可被覆盖的段。

窥孔优化:清除多余的MOV指令,包括不必要的从存储区加载和常数加载操作。当

存储空间或执行时间可节省时,用简单操作代替复杂操作。

-7-

0

1

2

3

寄存器变量:如有可能,自动变量和函数参数分配到寄存器上,而为这些变量保留的

存储区就可节省。

优化扩展访问:IDATA、XDATA、PDATA和CODE的变量直接包含在操作中,在

多数情况下没必要使用中间寄存器。

局部公共子表达式删除:如果用一个表达式重复进行相同的计算,则保存第一次计算

结果,后面有可能就用这结果,多余的计算被删除。

Case/Switch优化:包含SWITCH和CASE的代码优化为跳转表或跳转队列。

简单循环优化:用一个常数填充存储区的循环程序被修改和优化。

全局公共子表达式删除:一个函数内相同的子表达式有可能就只计算一次。中间结果

保存在寄存器中,在一个新的计算中使用。

循环优化:如果结果程序代码更快和有效则程序对循环进行优化。

扩展索引访问优化:适当时使用DPTR寄存器,对指针和数组访问进行执行速度和代

码大小优化。

公共尾部合并:当一个函数有多个调用,一些配置代码可以复用,因此减少程序大小。

公共块子程序:检测循环指令序列,并转换成子程序。Cx51甚至重排代码以得到更

大的循环序列。

根据表1,用户可自行选择合适的优化等级进行设置。理论上,代码优化等级设置得越

高,编译所得代码体积越小。在无特殊要求的情况下,推荐将程序设置为优化等级9,以减

小反汇编后生成的代码空间。

3.2 局部代码优化

在3.1中,介绍了一种对程序全局优化等级进行设置的方法。但在实际应用中,可能存

在为了保证核心函数的可靠性,需要将某一特定函数优化等级降低,同时又希望不影响其他

函数的优化等级,以减小代码空间。因此需要能够针对单一C文件或单一函数进行优先级设

置,本章节即介绍了2种Keil uVision4中所支持的局部代码优化设置方法。

4

5

6

7

8

9

图10 对单一C文件进行优先级设置

-8-

3.2.1

独立设置单一模块优化等级

在工作区右键选择需要单独设置优先级的模块(

C

文件),然后单击“

Options for File xxx

就会出现如图

10

所示界面,通过更改在

C51

标签页下的“

Level

”下拉框选项,即可实现对

C

文件中所有函数的优先级进行修改。确认修改后,该独立设置的

C

文件图标将出现特殊

标记,从“”图标变更为“”。

3.2.2

独立设置单一函数优化等级

在需要单独指定优化等级的函数前,添加定义:#pragma OT(optimization level, speed/size),

其中:optimization level为该段函数设置的优化等级,数值为0~9;第二个参数为优化倾向,

可选择speed或size,该处推荐选择speed。

以上段程序为例,其表示了在main函数和nop5之间的所有函数,包括nop5函数的优化

等级均设置为4级,而main函数之后的所有函数优化等级设置为0级。

通过

3.1

3.2

中所介绍的设置方法,即可实现灵活配置程序中函数优化等级的需求。

3.3 优化设置中的注意事项

前文提到过,在实际应用中,不同代码优化等级可能会使得程序运行结果存在差异。事

实上,大部分由于优化等级选项导致的程序BUG均可通过查阅C语言编程规范进行修改并

解决。列举实例如下:

图11 优化等级为9时,例程编译结果

-9-

对于外部RAM变量,常见申明语句为“unsigned char xdata XTEMP”,在如图11所示主

程序中调用,采用9级优化后,通过反汇编程序可知,第一次执行“temp=XTEMP”被编译

为:

此时,XTEMP的值存放在R7(0x0007)中,进一步执行“XTEMP=temp+1”运算后,XTEMP

的值被存放在R6(0x0006)中。此时再次读取XTEMP值时,“temp=XTEMP”被编译为:

可以看出,第二次读取XTEMP的操作被替换为了将R6的值重新更新到R7中,即将R7

作为XTEMP的缓存。在该程序中,这样反汇编并不会影响最终执行结果,但是若XTEMP

为外部RAM中的寄存器,其值有可能受到硬件或模块配置的影响而改变,则将会造成对

XTEMP取值错误。此时若将程序优化等级修改为0级,可发现编译结果改变如图12所示:

图12 优化等级为0时,例程编译结果

由图12可知,在代码优化等级为0时,第一次与第二次对XTEMP进行读操作时,均反

汇编为:

即每次读取操作时,都会重复从DPTR赋值开始,从XTEMP所在外部RAM地址取值。

但可以发现,降低优化等级后,XTEMP第二次读取指令反汇编后的代码空间从2 Bytes增加

-10-

到了7 Bytes,明显增大了芯片ROM的消耗。故推荐另一解决方法为:

无需更改代码优化等级,只要在XTEMP变量申明语句中时,增加“volatile”关键字:

“unsigned char volatile xdata XTEMP”,可以得到如图13所示的反汇编代码:

图13 申明volatile变量、优化等级为9时,例程编译结果

由图13可知,XTEMP的第二次取值操作被反汇编为:

由于XTEMP的地址在第一次读取操作执行时,已经在DPTR中更新,故在第二次读取

时,直接从DPTR所指向的地址取值,即从XTEMP所在外部RAM存储单元取值。此时编

译代码空间减少为1Byte,不过执行该MOVX指令需要6个系统时钟,增加了指令执行时间,

但也避免了取值错误的风险。

volatile关键字使用推荐详见“5.3 volatile

修饰声明

”。

-11-

4. 8位机与16位机编译差异

根据1~3章节所描述方法设置Keil优化方式后,能够有效减小程序编译后代码占用空间。

但对于将16位机程序移植到8位机平台的情况,仍有可能发现8位机编译程序后代码空间相

较于16位机会有所增大。

造成该代码编译空间差异的主要原因在于8位机与16位机的指令集不同,特别是对于

16位数据(int型数据)运算指令密集度较高的函数,8位机与16位机两者编译效率差异更

加明显。本章将通过例程(详见附件二),说明8位机与16位机编译效率存在差异。

首先,Keil uVision4(

代码优化等级选择

9

级,且开启

ACALL

优化

)与瑞萨CS+编译器

在代码缺省条件下,其默认编译生成代码所占用空间如图14、15所示,可见瑞萨在函数为空

的情况下,会产生较多初始化冗余代码:

(1)Keil uVision4编译器初始化冗余代码占用空间:16 Bytes;

(2)瑞萨CS+编译器初始化冗余代码占用空间:287 Bytes;

a. C

语言源程序

b.

反汇编程序

图14 Keil编译器默认占用代码空间

a. C

语言源程序

b.

编译信息

图15 瑞萨CS+编译器默认占用代码空间

由此可知,瑞萨编译器在代码量较小情况下,其产生的与软件无关代码相对于Keil编译

器多出了271 Bytes,故在代码量较小的情况下,瑞萨编译器对于16位单片机的编译效率低

于Keil编译器对8位单片机编译效率。

然而,如图16、17所示,在主函数中重复进行16位int型数据加/减/乘/除运算,此时通

过编译后生成信息可知:

(1)Keil uVision4编译器后有效代码占用空间:365-16 Bytes = 349 Bytes;

(2)瑞萨CS+编译器后有效代码占用空间:629-287 Bytes = 342 Bytes;

由上述可知,此时16位机编译后有效代码量相对于8位机减小了7 Bytes,可见在高位

-12-

运算指令较为密集的函数中,8位机相对于16位机的指令编译效率更低。

a. C

语言源程序

b.

编译信息

图16 Keil编译函数占用代码空间

a. C

语言源程序

b.

编译信息

图17 瑞萨CS+编译函数占用代码空间

进一步,将图16、17中的示例变量更改为8位char型变量,此时可得Keil与CS+编译

结果对比如图18所示,此时:

-13-

(1)Keil uVision4编译器后有效代码占用空间:286-16 Bytes = 270 Bytes;

(2)瑞萨CS+编译器后有效代码占用空间:560-287 Bytes = 273 Bytes;

a. Keil

编译信息

b. CS+

编译信息

图18 Keil与瑞萨CS+编译函数占用代码空间

由图18可知,当进行8位运算时,8位机编译后得到的有效代码反而比16位机小3 Bytes,

可见,16位单片机相对于8位机的指令集优势在8位运算时未能得到体现,甚至16位机的

编译效率略低于8位机。

-14-

5. Keil C编程与调试技巧

5.1 存储器类型

每个变量可准确地赋予不同的存储器类型(data,idata,pdata,xdata,code)。访问内部

数据存储器(data/idata)要比访问扩展数据存储器(xdata)相对要快一些,因此,可将经常

使用的变量置于内部数据存储器中,而将较大及很少使用的数据单元置于扩展数据存储器中。

表2 存储器类型描述

存储器类型

data

bdata

idata

pdata

xdata

code

描述

直接寻址内部数据存储器0~7FH,访问变量速度最快(128 bytes)

可位寻址内部数据存储器20H~2FH,允许位与字节混合访问(16 bytes)

间接寻址内部数据存储器0~FFH,可访问全部地址空间(256 bytes)

分页(256 bytes)外部数据存储器,由操作码MOVX @Ri 访问

外部数据存储器(64K),由MOVX @DPTR访问

代码数据存储器(64K),由MOVC @A+DPTR访问

变量说明举例:

char code msg[]=”ENTER PARAMETER:”;

unsigned long xdata array[100];

char bdata flags;

sbit flag o=flags^0;

如果在变量说明时略去存储器类型标志符,编译器会自动选择默认的存储器类型。默认

的存储器类型进一步由控制指令SMALL、COMPACT和LARGE限制。例如:如果声明char

var,则默认的存储器模式为SMALL,char var放在data存储器;如果使用COMPACT模式,

则char var放入idata存储区;在使用LARGE模式的情况下,char var被放入外部存储区或

xdata存储区。

5.2 C语言中嵌入汇编

(1) 在C文件中要嵌入汇编代码片以如下方式加入汇编代码:

#pragma ASM

//Assembler Code Here

#pragma ENDASM

(2) 在Project窗口中包含汇编代码的C文件上右键,选择“”,点击右边的

“Generate Assembler SRC File”和“Assemble SRC File”,使检查框由灰色变成黑色(有效)

状态;

(3) 根据选择的编译模式,把相应的库文件(如Small模式时,是)

加入工程中,该文件必须作为工程的最后文件;

(4) 编译,即可生成目标代码。

5.3 volatile修饰声明

如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定

执行)。一般说来,volatile用在如下的几个地方:

(1) 中断服务程序中修改的供其它程序检测的变量需要加volatile;

-15-

(2) 多任务环境下各任务间共享的标志应该加volatile;

(3) 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不

同意义;

5.4 静态局部变量

静态局部变量属于静态存储方式,它具有以下特点:

(1) 静态局部变量在函数内定义它的生存期为整个源程序,但是其作用域仍与自动变量

相同,只能在定义该变量的函数内使用该变量。退出该函数后,尽管该变量还继续存在,

但不能使用它。

(2) 允许对构造类静态局部量赋初值例如数组,若未赋以初值,则由系统自动赋以0值。

(3) 对基本类型的静态局部变量若在说明时未赋以初值,则系统自动赋予0值。而对自

动变量不赋初值,则其值是不定的。根据静态局部变量的特点,可以看出它是一种生存

期为整个源程序的量。虽然离开定义它的函数后不能使用,但如再次调用定义它的函数

时,它又可继续使用,而且保存了前次被调用后留下的值。因此,当多次调用一个函数

且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。虽然用全局变量也

可以达到上述目的,但全局变量有时会造成意外的副作用,因此仍以采用局部静态变量

为宜。

5.5 静态全局变量

全局变量(外部变量)的说明之前再冠以static就构成了静态的全局变量。全局变量本身就

是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这

两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成

时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即

只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局

变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源

文件中引起错误。从以上分析可以看出,把局部变量改变为静态变量后是改变了它的存储方

式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使

用范围。因此static这个说明符在不同的地方所起的作用是不同的。

5.6 static 函数

当一个源程序由多个源文件组成时,C语言根据函数能否被其它源文件中的函数调用,

将函数分为内部函数和外部函数。

(1) 内部函数(又称静态函数)

如果在一个源文件中定义的函数,只能被本文件中的函数调用,而不能被同一程序其它

文件中的函数调用,这种函数称为内部函数。

定义一个内部函数,只需在函数类型前再加一个“static”关键字即可,如下所示:

static 函数类型 函数名(函数参数表)

{……}

关键字“static”,译成中文就是“静态的”,所以内部函数又称静态函数。但此处“static”

的含义不是指存储方式,而是指对函数的作用域仅局限于本文件。

使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否

会与其它文件中的函数同名,因为同名也没有关系。

-16-

(2) 外部函数

外部函数的定义:在定义函数时,如果没有加关键字“static”,或冠以关键字“extern”,

表示此函数是外部函数:

[extern] 函数类型 函数名(函数参数表)

{……}

调用外部函数时,需要对其进行说明:

[extern] 函数类型 函数名(参数类型表)[,函数名2(参数类型表2)……];

5.7 位域

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例

如在存放一个开关量时,只有0和1两种状态,用一位二进位即可。为了节省存储空间,并

使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一

个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允

许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

一、位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:

struct 位域结构名

{ 位域列表 };

其中位域列表的形式为: 类型说明符 位域名:位域长度

例如:

struct bs

{ int a:8; int b:2; int c:6;};

5.8 C51 intrins.h库文件

extern void _nop_ (void); 替代指令 NOP,不产生函数调用。

extern bit _testbit_ (bit); 产生一个 JBC 指令,该函数测试一个位,当置位时返回1,否

则返回0。如果该位置为1,则将该位复位为0。8051的JBC指令即用作此目的。

_testbit_只能用于可直接寻址的位;在表达式中使用是不允许的。

extern unsigned char _cror_ (unsigned char, unsigned char); 字符循环右移

extern unsigned int _iror_ (unsigned int, unsigned char); 整型循环右移

extern unsigned long _lror_ (unsigned long, unsigned char); 长整型循环右移

extern unsigned char _crol_ (unsigned char, unsigned char); 字符循环左移

extern unsigned int _irol_ (unsigned int, unsigned char); 整型循环左移

extern unsigned long _lrol_ (unsigned long, unsigned char); 长整型循环左移

extern unsigned char _chkfloat_(float);

extern void _push_ (unsigned char _sfr);对特殊功能寄存器进行压栈

extern void _pop_ (unsigned char _sfr);对特殊功能寄存器进行出栈

5.9 指针

x=*((char xdata *)0x4000); 表示从xdata 0x4000处取一个char赋给 x。

*((char xdata *)0x4000)=X; 表示给xdata 地址为0x4000的空间赋值。

c = *((char code *) 0x8000);

i = *((char idata *) 0xF0);

表示从Code 0x8000处取一个char赋给c.

表示从idata 0xF0处取一个char赋给i.

-17-

函数指针数组

code void (*ArrFn[])(void) =

{&f1, &f2,};

然后就可以像引用数组一样调用函数了:

void main(void)

{ unsigned char i=0;

(*ArrFn[i])();}

5.10 C程序优化

对程序进行优化,通常是指优化程序代码或程序执行速度。优化代码和优化速度实际上

是一个矛盾的统一,一般是优化了代码的尺寸,就会带来执行时间的增加,如果优化了程序

的执行速度,通常会带来代码增加的副作用,很难鱼与熊掌兼得,只能在设计时掌握一个平

衡点。

(1) 程序结构的优化

A. 程序的书写结构

虽然书写格式并不会影响生成的代码质量,但是在实际编写程序时还是应该遵循一定的

书写规则,一个书写清晰、明了的程序,有利于以后的维护。在书写程序时,特别是对于while、

for、do…while、if…elst、switch…case等语句或这些语句嵌套组合时,应采用“缩格”的书

写形式。

B. 标识符

程序中使用的用户标识符除要遵循标识符的命名规则以外,一般不要用代数符号(如a、b、

x1、y1)作为变量名,应选取具有相关含义的英文单词(或缩写)或汉语拼音作为标识符,以增

加程序的可读性,如:count、number1、red、work等。

C. 程序结构

C语言是一种高级程序设计语言,提供了十分完备的规范化流程控制结构。因此在采用

C语言设计单片机应用系统程序时,首先要注意尽可能采用结构化的程序设计方法,这样可

使整个应用系统程序结构清晰,便于调试和维护。于一个较大的应用程序,通常将整个程序

按功能分成若干个模块,不同模块完成不同的功能。各个模块可以分别编写,甚至还可以由

不同的程序员编写,一般单个模块完成的功能较为简单,设计和调试也相对容易一些。在C

语言中,一个函数就可以认为是一个模块。所谓程序模块化,不仅是要将整个程序划分成若

干个功能模块,更重要的是,还应该注意保持各个模块之间变量的相对独立性,即保持模块

的独立性,尽量少使用全局变量等。对于一些常用的功能模块,还可以封装为一个应用程序

库,以便需要时可以直接调用。但是在使用模块化时,如果将模块分成太细太小,又会导致

程序的执行效率变低(进入和退出一个函数时保护和恢复寄存器占用了一些时间)。

D. 定义常数

在程序化设计过程中,对于经常使用的一些常数,如果将它直接写到程序中去,一旦常

数的数值发生变化,就必须逐个找出程序中所有的常数,并逐一进行修改,这样必然会降低

程序的可维护性。因此,应尽量当采用预处理命令方式来定义常数,而且还可以避免输入错

误。

E. 减少判断语句

-18-

能够使用条件编译(ifdef)的地方就使用条件编译而不使用if语句,有利于减少编译生成

的代码的长度。

F. 表达式

对于一个表达式中各种运算执行的优先顺序不太明确或容易混淆的地方,应当采用圆括

号明确指定它们的优先顺序。一个表达式通常不能写得太复杂,如果表达式太复杂,时间久

了以后,自己也不容易看得懂,不利于以后的维护。

G. 函数

对于程序中的函数,在使用之前,应对函数的类型进行说明,对函数类型的说明必须保

证它与原来定义的函数类型一致,对于没有参数和没有返回值类型的函数应加上“void”说明。

如果需要缩短代码的长度,可以将程序中一些公共的程序段定义为函数,在Keil中的高级别

优化就是这样的。如果需要缩短程序的执行时间,在程序调试结束后,将部分函数用宏定义

来代替。注意,应该在程序调试结束后再定义宏,因为大多数编译系统在宏展开之后才会报

错,这样会增加排错的难度。

H. 尽量少用全局变量,多用局部变量

因为全局变量是放在数据存储器中,定义一个全局变量,MCU就少一个可以利用的数据

存储器空间,如果定义了太多的全局变量,会导致编译器无足够的内存可以分配。而局部变

量大多定位于MCU内部的寄存器中,在绝大多数MCU中,使用寄存器操作速度比数据存储

器快,指令也更多更灵活,有利于生成质量更高的代码,而且局部变量所的占用的寄存器和

数据存储器在不同的模块中可以重复利用。

I. 设定合适的编译程序选项

许多编译程序有几种不同的优化选项,在使用前应理解各优化选项的含义,然后选用最

合适的一种优化方式。通常情况下一旦选用最高级优化,编译程序会近乎病态地追求代码优

化,可能会影响程序的正确性,导致程序运行出错。因此应熟悉所使用的编译器,应知道哪

些参数在优化时会受到影响,哪些参数不会受到影响。

(2) 代码的优化

A. 选择合适的算法和数据结构

应该熟悉算法语言,知道各种算法的优缺点,具体资料请参见相应的参考资料,有很多

计算机书籍上都有介绍。将比较慢的顺序查找法用较快的二分查找或乱序查找法代替,插入

排序或冒泡排序法用快速排序、合并排序或根排序代替,都可以大大提高程序执行的效率。.

选择一种合适的数据结构也很重要,比如你在一堆随机存放的数中使用了大量的插入和删除

指令,那使用链表要快得多。

B. 使用尽量小的数据类型

能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;能够使用整型变量

定义的变量就不要用长整型(long int),能不使用浮点型(float)变量就不要使用浮点型变量。当

然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,C编译器并不报错,

但程序运行结果却错了,而且这样的错误很难发现。

C. 循环

a. 循环语句

对于一些不需要循环变量参加运算的任务可以把它们放到循环外面,这里

-19-

的任务包括表达式、函数的调用、指针运算、数组访问等,应该将没有必要执

行多次的操作全部集合在一起,放到一个init的初始化程序中进行。

b. while循环和do…while循环

用while循环时有以下两种循环形式:

unsigned char i;

i=100;

while (i>0)

{i--;}

或:

unsigned char i;

i=100;

do

{i--;}

while (i>0);

在这两种循环中,使用while循环编译后生成的代码的长度短于do…while

循环。

D. 查表

在程序中一般不进行非常复杂的运算,如浮点数的乘除及开方等,以及一些复杂的数学

模型的插补运算,对这些即消耗时间又消费资源的运算,应尽量使用查表的方式,并且将数

据表置于程序存储区。如果直接生成所需的表比较困难,也尽量在启动时先计算,然后在数

据存储器中生成所需的表,后以在程序运行直接查表就可以了,减少了程序执行过程中重复

计算的工作量。

E. 其它

比如使用在线汇编及将字符串和一些常量保存在程序存储器中,均有利于优化。

根据经验,采用本文的优化方式后,在将瑞萨16位单片机软件移植到中颖8位机上时,

软件空间增大量可控制在10%左右。

-20-

6. 附录

附件1:Keil优化设置例程

-21-

附件2:Keil 8位机和瑞萨CS+ 16位机编译代码空间对比例程

Keil uVision4(选择LX51 Linker,9级优化等级,勾选“AJMP/ACALL优化”):

-22-

瑞萨CS+:

-23-

7. 版本更新

更改版本

1.0

记录

初始版本

日期

2017年4月

-24-