摘要:本文讲述了将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作为变量名,修改这些变量名。

ANSIC程序到KeilC51的移植心得的更多相关文章

  1. 低版本GCC程序向高版本移植的兼容性问题

    将低版本gcc编译过的程序移植到高版本GCC时, 可能会出现一些兼容性问题. 原因是, 为了适应新的标准,一些旧的语法规则被废弃了. 关于这方面的一些具体资料可从该处查询. 这里只是自己遇到的其中一个 ...

  2. C++ 自定义控件的移植(将在其它程序中设计的自定义控件,移植到现在的系统中)

    方法很简单就是将需要的代码 复制到 新系统中就可以了,方法就是 把相关文件添加到现有的系统中,并特别注意以下问题 \如果原设计中用到了菜单或是其它资源,相应的资源要在新的菜单中,手动添加. 目前没有发 ...

  3. UCOS移植心得(

    移植UCOS之前,你首先应该做好三件事: 1.弄懂UCOS,这是谁都知道的哦 ^_^ 2. 弄懂你想要移植到的硬件平台 3. 清楚你使用的编译器是如何处理函数的局部变量和怎么样处理函数间的参数传递 这 ...

  4. 程序中使用cocostudio移植到android手机须要的若干配置过程

    首先在解决方式下加入现有项: libCocosStudio.vcxproj E$uVS5Sbv! WL:0n"BExtensions.vcxproj libGUI.vcxproj 然后在pr ...

  5. 记一次程序从x86_64linux平台移植到armv7平台

    前言 最近接了个任务,需要把代码移植到armv7平台,搜寻相关方法,了解到可以利用交叉编译工具如:gcc-linaro-arm-linux-gnueabihf.把自己依赖的第三方库代码和自己代码分别编 ...

  6. 微信小程序开发 --- 小白之路 --- 心得

    1.前言 今天 ,发现我的饭卡不见了....悲催 ,看了一下学校的微信小程序,查了下我这饭卡的流水记录,嗯...最后出现的地方在洗澡房... 好吧,扯远了,虽然没找到,可是突发奇想 ,小程序挺方便的, ...

  7. android4.0.3源码之USB wifi移植心得

    http://blog.csdn.net/eastmoon502136/article/details/7850157 http://forum.cubietech.com/forum.php?mod ...

  8. 一些遇到的Qt程序在Windows平台间移植问题整理

    今天尝试把Qt程序移植到各种虚拟机中测试,由于Qt的依赖库报告往往不能显示出全部依赖库.结果频频出现问题,好不容易全部解决了,这里给出一些套路. 首先对于Qt版本,我用过很多,最终表示现阶段推荐Min ...

  9. Civil 3D 2017本地化中VBA程序移植到2018版中

    中国本地化包简直就是一块鸡肋, 但对于某些朋友来说还真离不了: 可惜中国本地化包的推出一直滞后, 在最新版软件出来后1年多, 本地化还不一定能够出来, 即使出来了, 也只能是购买了速博服务的用户才能得 ...

随机推荐

  1. Delphi 函数指针(函数可以当参数)

    首先学习: 指向非对象(一般的)函数/过程的函数指针 Pascal 中的过程类型与C语言中的函数指针相似,为了统一说法,以下称函数指针.函数指针的声明只需要参数列表:如果是函数,再加个返回值.例如声明 ...

  2. linux 版本家族

    1. 简单的说,在桌面系统上,可分为Debian和RedHat两大分支,然后Debian这一分支到现在比较火的是Ubuntu, RedHat比较火的是Fedora.贴一下它们的版本历史:  fedor ...

  3. HashMap Collision Resolution

    Separate Chaining Use data structure (such as linked list) to store multiple items that hash to the ...

  4. windows puppet manifests 文件维护

    初级 puppet windows agent实现简单的msi格式安装包安装及bat文件创建;

  5. URAL 1036

    题目大意:求前N位与后N位各个位和相等且总和等于S的2N位数的个数. KB     64bit IO Format:%I64d & %I64u 数据规模:1<=N<=50,0< ...

  6. 关于ionic传值

    今天,也是偶然发现有的初学者对ionic的传值还不太清除,这里我说明一下 例如你想在这个页面传递参数a.b过去,传递到"tab.wait"页面 $state.go("ta ...

  7. 关于背景透明,文字不透明的最佳方法,兼容IE

    以背景黑色,透明度0.5举例 非IE:background:rgba(0,0,0,0.5); IE:filter:progid:DXImageTransform.Microsoft.gradient( ...

  8. lseek() 定位一个已经打开的文件

    Lseek lseek()的作用是,设置文件内容的读写位置. 每个打开的文件都有一个"当前文件偏移量",是一个非负整数,用以度量从文件开始处计算的字节数.通常,读写操作都是从当前文 ...

  9. android 新浪微博客户端的表情功能的实现

    这是一篇好文章,我转来收藏,技术的最高境界是分享. 最近在搞android 新浪微博客户端,有一些心得分享弄android客户端表情功能可以用以下思路1.首页把新浪的表情下载到本地一文件夹种,表情图片 ...

  10. Sybase自增字段跳号的解决方法

    Sybase自增字段跳号原因及影响: 在Sybase数据库中如果数据库在开启的情况下,因为非正常的原因(死机.断电)而导致数据库服务进程强制结束. 那么自动增长的字段将会产生跳号的情况,再往数据表里面 ...