摘要:本文讲述了将ANSIC程序移植到KeilC51上应该注意的事项。文章讲述了存储
类型、指针类型、重入函数、根据目标系统RAM的分布的段定位和仿真栈设置、函数
指针、NULL指针问题、字节顺序、交叉汇编等移植时需要注意的事项。对存储类型、
指针类型、重入函数对程序的效率的影响进行了分析。最后文章以将ucosii移植到
KeilC51的小模式下为实例,讲述了移植的一般步骤。

1 引言
C语言是应用很广泛的计算机语言。因为它具有很强的移植性等优点,在编写单片
机程序时,有时系统的可读性、易维护性往往比程序的效率更重要,这时候我们可
以选择C语言作为程序语言。使用C语言的另一个优点是可以利用大量的程序资源,
为X8086等CPU编写的C程序只要稍加修改就可以拿过来用,避免了重复开发。Keil
C51是51系列单片机上的优秀的C编译器,了解KeilC的特点将有利于编写和移植高
效的C51程序。

2 指定存储类型,尽量使用小模式编译
KeilC中的变量除了可以设置数据类型以外还可以设置存储类型(Memory type)。
对于变量常需要在data,idata,pdata和xdata这几个存储类型之间做一个选择,
它们分别将变量放在内部RAM,间接寻址内部RAM,用R0、R1寻址的外部RAM,用DP
TR寻址的外部RAM。KeilC编译器使用的存储模式(memory model)有小模式、紧凑模
式和大模式。在各个模式下,如果变量没有指定存储类型,默认分别对应data、p
data、xdata存储类型。四种存储类型访问速度依次降低,但是可用空间依次增多

稍大的C程序有较多的外部变量,如果从ANSI移植到KeilC不给变量指定存储类型
,那么一般只能使用大模式编译,这样程序速度较慢。为了能在小模式下编译,我
们可以将数据量大、访问量小的变量定义为xdata类型,我的做法是将所有的外部
变量都定义为xdata或者pdata,局部变量不指定存储类型,这样一般能在小模式下
编译。

3 尽量使用指定存储类型的指针(memory-specific pointer)不使用一般指针(gen
eric pointer)
如果程序移植的时候不做修改,所有的指针将都是"一般指针",我们的建议是尽
量修改为"指定存储类型"的指针,因为它的效率要高很多。
首先一般指针使用三个字节,第一个字节指示是什么存储类型,后两个字节是指
针指向的地址。"指定存储类型"的指针则只用一个或者两个字节。可见"一般指针
"占用内存多。
另外,为了取得"一般指针"指向的数据,程序必须调用?C?CLDPTR函数,在?C?
CLDPTR中根据指针第一字节指示的存储类型采取不同的读取RAM的方式。而使用"指
定存储类型"的指针时,采取哪种读取RAM的方式在编译时已经确定,不用在运行时
动态判断。可见"一般指针"运行效率低。
"指定存储类型"的指针指向的变量必须要有明确的存储类型。一般情况下程序中
使用指针是为了指向大块内存,而KeilC中大块内存一般定义为外部变量。依照第
一点移植建议,所有的外部变量都定义为xdata或者pdata类型了,有明确的存储类
型,这说明程序中的指针基本都可以改为"指定存储类型"的指针。

4 需重入函数增加reentrant关键字
X8086CPU上运行的Dos和Windows程序中的函数都是可重入函数。但是为提高效率
,KeilC默认情况下使用寄存器传递参数,局部变量放在固定的内存空间,这样函
数就不可重入了。如果不加修改的将ANSI程序移植到KeilC,发生不可重入函数被
重入时,程序运行将出错。这时我们需要将可能被重入的函数后增加reentrant关
键字。
但是我们往往对需要移植的程序的流程不太了解,这样也就不清楚哪个函数可能
被重入。这里提供一个方法:首先不添加reentrant,在KeilC下编译连接,将会有
警告。如果提示"recursive call to non-reentrant function",说明此函数被递
归调用而重入;如果提示"multiple call to segment",说明此函数很可能是被中
断函数和非中断函数都调用而重入。然后,在有以上警告的函数后增加reentrant
关键字。但是以上的设置方法并不是万无一失,比如有函数指针存在的程序,函数
调用树(call tree)不能反映真实调用情况;又如程序中改变压入堆栈的程序指针
,使得函数返回时不回到原来的调用点,例如ucosii就是采用这种方式进行任务切
换,这时KeilC编译器无法建立正确的函数调用树,无法判断是否被重入。
既然判断函数是否会被重入较麻烦,为何不将所有的函数都设置为reentrant类型
?为了明白这点,我们首先要了解一下reentrant函数的执行速度和代码量。
为了使函数可重入,KeilC使用了仿真栈(simulated stack),它区别于SP寄存器
指向的硬件栈(hardware stack)。在大模式、紧凑模式和小模式下仿真栈分别被定
义在XDATA、PDATA、IDATA空间中。仿真栈从上向下生长。有一个全局变量(编译
器自动定义的)指向栈顶,对于不同的存储模式该变量分别是:?C_XBP、 ?C_PBP
、 ?C_IBP。仿真栈的作用和Dos操作系统下的堆栈作用是类似的。重入函数和非重
入函数运行时的区别主要有:

-----------------------------------------------------------------------
情况                                非重入函数          重入函数

-----------------------------------------------------------------------
函数参数无法全部通过寄存器传递时    通过局部数据段传递      通过仿真栈传递
需要局部变量时             局部变量放在局部数据段中    局部变量放在仿真栈中
函数返回时                          调整仿真栈顶

-----------------------------------------------------------------------
X0886CPU支持类似于mov eax, dword ptr [esp+20]的汇编语言来读取堆栈的内容
,而51单片机没有读取仿真栈的配套指令,所以仿真栈的额外操作使得速度变慢、
代码量增大。如果你的移植系统对速度和代码量有要求,要避免设置不必要的函数
为reentrant类型。

5目标系统的外部RAM起始地址影响段定位和仿真栈设置
例如你的系统的外部RAM为32K,而KeilC默认情况下认为外部RAM为64K,如果移植
程序使用了超过32K的RAM,编译器不会报错,但是程序运行将会出错;又如,你的
系统为了某种需要将RAM范围设置为0x8000-0xFFFF,这时也需要告诉KeilC地址范
围。
设置xdata段定位的方法。例如外部RAM地址分布为0x0000-0x4000和0xC000-0xFF
FF。命令行方式下使用BL51的选项XDATA:BL51 MyProgram.obj XDATA(0x0000
-0x4000,0xC000-0xFFFF)。在KeilC集成开发环境中,找到菜单project-》optio
n for target1-》BL51 location,在Xdata输入框中输入0x0000-0x4000,0xC000
-0xFFFF。
设置pdata段定位的方法。如果让pdata使用0x8000-0x80FF之间的外部RAM,在命
令行方式下使用BL51的选项PDATA:BL51 MyProgram.obj PDATA(0x8000)。在集
成开发环境下,找到菜单project-》option for target1-》BL51 location,在
Pdata输入框中输入0x8000。其中0x8000就是pdata的起始地址。还要修改Startup
.a51,修改如下: ① 增加Startup.a51到工程:将KeilC\C51\LIB\Startup.a51拷
贝一份到你的工作目录下,然后添加到你的工程中。② 找到startup.a51中的
PPAGEENABLE EQU 0   ; set to 1 if pdata object are used.
PPAGE       EQU 0   ; define PPAGE number.
修改为:
PPAGEENABLE EQU 1   ; set to 1 if pdata object are used.
PPAGE       EQU 80H ; define PPAGE number.
初始化时,PPAGE将被赋予单片机P2口寄存器,当程序使用类似MOVX A,@R0时,高
8位地址就是PPAGE的值。使用pdata类型数据时,要特别注意不能随意在程序中修
改P2寄存器的值。
大模式下设置仿真栈顶。在大模式下仿真栈在xdata空间。如果外部RAM地址范围
是0x0000到0x8000。此时需要设置栈顶为0x8000,默认情况下的(0xFFFF+1 )将会
使程序出错。设置方法是:① 增加startup.a51。② 修改startup.a51中的部分代
码为如下代码:
XBPSTACK        EQU 1   ; set to 1 if large reentrant is used.
XBPSTACKTOP EQU 7FFFH+1; set top of stack to highest location+1..
紧凑模式下设置仿真栈顶。默认的情况下为0xFF+1。但是某些时候采用默认值会
出错。比如pdata所有变量占用0x80字节的空间,并且你的程序中有0x80字节的xd
ata类型的数据。那么默认情况下pdata数据放到0-0x007F,xdata放到0x0080-0x0
0FF。这时默认的仿真栈顶在0x00FF,它和xdata数据区冲突。一个解决的办法是将
pdata段定位到xdata段的后面,例如这里将pdata段起始地址定位在0x100。
6 KeilC中的函数指针
如果被移植的程序中使用了函数指针,那么就要注意覆盖分析的出错问题。问题
的产生在于"覆盖分析"(overlay)技术。在小模式下编译的C51程序局部变量都放在
data空间中,为了重复利用data空间,KeilC采用了overlay技术:一个程序中函数
的层层调用会形成一个函数"调用树"(call tree),处于函数调用树的不同树枝上
的函数可以共享一块内存空间(即覆盖),这样就节省了内存空间的使用。KeilC
能够根据函数调用树进行正确的覆盖分析。使用函数指针一般有两种操作:① 将
一个函数名赋给一个函数指针,这时KeilC误认为调用了这个函数名对应的函数。
② 使用函数指针调用函数,这时KeilC不能发现调用了函数。这都使得函数调用树
出错,由此调用树进行的覆盖分析也将出错,致使局部变量冲突,程序出错。对此
有两种措施:① 手动修正调用树:使用BL51的OVERLAY选项增删调用树的树枝。②
将通过函数指针调用的函数都设置为reentrant类型,由于reentrant类型局部变
量在仿真栈中,不会引起局部变量冲突。
ANSIC中,通过函数指针调用的函数的参数的个数没有限制,但是KeilC对此有限
制,至多3个参数。因为,KeilC编译时,无法通过函数指针找到该函数的局部数据
段,也就无法通过局部数据段传递参数,只能通过寄存器传递参数,所以参数个数
是有限制的。碰到这个问题时解决办法是:① 将该函数改为reentarnt类型。② 
修改源程序,将多个参数放在一个结构体中传递。

7 NULL指针问题
C程序一般规定任何变量都不能使用地址为0的内存。但是单片机的xdata空间的0
地址内存在默认的情况下是可以被使用的。现假如有内存分配函数malloc(int si
ze),malloc函数成功分配了一块0地址开始的内存,返回首地址0,当程序发现返
回值等于NULL时误认为内存分配失败。为了防止以上错误,我们移植时要增加以下
一个全局变量:
Char xdata NULLAddr _at_ 0
这里使用了KeilC的_at_关键字将一个变量NULLAddr指定在0地址,从而避免了其它
变量占用0地址。

8 字节顺序(byte order)
X8086等CPU在内存中双字节变量:高字节在高地址,低字节在低地址。KeilC51默
认双字节变量则顺序相反。字节顺序引起修改的一个典型例子:TCP/IP程序中的h
tons()函数将主机字节顺序转化为网络字节顺序,对于X8086和KeilC51这个htons
()函数是不同的。

9 交叉汇编
移植的时候可能还需要编写少量的51汇编程序。汇编和C互相调用应该遵守KeilC
的参数传递和返回值传递规则。为了使汇编程序也能够进行overlay分析,汇编的
书写要有一定的格式。另外需要强调的一点是:被C程序调用的汇编函数可以使用
所有的寄存器,而不用担心会修改C程序中使用的寄存器。

10 关键字
pdata、data等KeilC关键字可能被ANSIC程序中用作变量名,必须修改之。

11 实例:Ucosii到KeilC小模式下的移植
Ucosii已经由杨屹移植到KeilC的大模式下,本文讲述将其修改为小模式的方法。
移植步骤如下:
(1)将所有的外部变量定义为xdata储存类型。
(2)修改指针:查找'*'符号,发现是指针定义的地方在'*'号前加xdata。
(3)在所有的函数申明后增加reentrant关键字。对Ucosii,无法用上文提到的方法
判断哪些函数可能被重入,只好全部设置为可重入函数。
(4)根据你的目标系统的外部RAM起始地址定义xdata段的起始地址。下面具体讲一
下移植到小模式下仿真栈的使用。
在小模式下仿真栈顶默认设置在内部RAM空间的顶端0xFF。硬件栈顶初始值由Keil
C自动分配,实际上在决定栈顶以前KeilC先安排所有的data类型变量,然后设置S
P指向空余data空间的开始。这时两个堆栈上下相对增长。对于堆栈是否会溢出,
KeilC本身不提供编译警告,只能在程序运行时调试。
Ucosii任务栈中是否需要保存堆栈,因移植系统的不同而不同。① 移植到堆栈在
外部RAM中的系统上(例如Dos)时,只要保存当前堆栈的指针就可以了。② 移植
到KeilC大模式下时,需要保存硬件栈的内容和仿真栈的指针。③ 移植到KeilC小
模式下,需要保存硬件栈的内容和仿真栈的内容,它的任务栈的结构如右图所示。

通过?C_IBP可以知道仿真栈所在的内部RAM区间。用以下的方法可以获得初始硬
件栈顶,在汇编程序中增加以下代码:
?STACK SEGMENT IDATA
RSEG ?STACK
StkBottom:
标号StkBottom即为硬件栈的初始栈顶。通过硬件栈大小和初始栈顶可以知道硬件
栈所在内部RAM的区间。图中的寄存器的排列顺序和KeilC在进入中断以后保存寄存
器的顺序是一致的,和中断时寄存器压栈顺序一致是ucosii所要求的。
函数指针问题。Ucosii有任务切换,KeilC得到函数调用树是错误的。另外在m
ain函数中一般将任务函数(例如Task1)作为参数传递给OSTaskCreate函数,KeilC
误认为main函数调用了Task1。由于已经将所有的函数都申明为reentrant类型,所
以没有必要手动修正调用树,实际上也很难修正。
(6)NULL指针问题。使用以上提到的方法,避免NULL指针问题。
(7)交叉汇编。Ucosii移植的需要编译一部分51汇编程序。
(8)关键字。Ucosii中使用pdata、data作为变量名,修改这些变量名。

最新文章

  1. 【转载】lucene中Field.Index,Field.Store详解
  2. python matplotlib 绘图
  3. 获取本地IP和端口号的指令
  4. cobbler客户端重装系统
  5. 同步队列-Queue模块解析
  6. python/SQLAchemy
  7. LVS结合keepalive
  8. Where are your from!!!!!!!!!!!! !Baby! {封装}
  9. [zz] Python 3.7 anaconda environment - import _ssl DLL load fail error
  10. Jenkins构建maven项目跳过测试用例的命令
  11. MHA快速搭建
  12. Spring框架学习04——复杂类型的属性注入
  13. [Android自动化] 在 pip-9.0.1 版本情况下安装 uiautomator2 报错的解决办法
  14. 深入理解Java对象序列化
  15. CodeForces 558B
  16. Android几种layout(布局)的区别
  17. MySQL5.7.20编译安装
  18. springMVC 第一章
  19. UVA - 10780 唯一分解定理
  20. 种类并查集,TOJ(1706)

热门文章

  1. C#中使用SendMessage进行进程通信的实例
  2. 转:使用linq to sql 随机取一行数据的方法
  3. CSS中display:block的使用介绍
  4. Saruman's Army (POJ 3069)
  5. Fiddler 抓取eclipse中的请求
  6. javaweb文件下载
  7. Android--获取当前系统的语言环境
  8. c++11 : range-based for loop
  9. asp.net cookie和session的详细使用
  10. IPMITOOL常用操作指令