单片机另类技术
已有 1478 次阅读2007-2-11 20:33
|个人分类:mcu原创|系统分类:单片机
单片机技术
author yanfeng <szq106@163.com>
update <Sun Feb 11 17:18:28 2007>
* 前言
一直以来有这样一个想法,把自己在实际遇上的问题的一些思考的东东变成文字,然
而这不是一个轻松的活。能想到是一个问题,把所想变成文字又是另一个问题。曾几次动
手却又草草收场。这次硬着头皮来,希望能坚持下来。一来是想把自己的想法留下一些文
字的东西,二是锻炼一下自己的毅力。
首先说说这个标题的由来,由于这篇文章讲述的内容主要是围绕单片机的,但单片机
的什么呢?总不能标题就是单片机吧,首先想到了软件技术,因为基本上所讲述的也都是
软件的内容,但又不全都是软件的,而且很多还称不上是什么技术,如若叫单片机的艺
术,那就忽悠过头了。^_^,因为想不到什么好的名称,因此还是厚颜无耻的叫单片机技术。
此文以51为硬件平台,Keil C51的开发工具为软件平台来讲述。
也许读者会认为市面上51类的书籍已经够多的了,本文为什么还选用51mcu,本文与那
些书籍有什么不一样的地方。选用51是因为很多人都熟悉它,而且本文并不想重复市面上
的书籍里所说的那些东西,而更侧重于它们没有提及的一些软件方面。本文假设读者对
51mcu已有所了解,并具备一些汇编及C51的基础。虽然本文以51为基础,但是本人希望本
文所讲述的思想对大部分的平台来说都是适用的。
软件使用Keil C51 v8.05版,之所以使用Keil的工具是因为在51的开发软件里,keil
的C51算得上是较好的了(我还没见过比它更好的,^_^),使用v8.05版本并不是说低版本
不能用,完全是个人的喜好问题。8.0以下的版本IDE都是uv2,本人觉得界面不如uv3好,
而且在编译速度方面,8.0以上的版本速度会快很多。
uv2及uv3的编辑环境都不好,主要是对中文的支持不好,还有光标跟字符对应不上的
问题。因此本文的示例均使用ntemacs(emacs的windows版)来编辑。
关于emacs,有句广告词比较能说明问题,“谁用谁知道”。^_^,包括本文都是使用
emacs完成。
由于自己在技术及文字功底方面道行尚浅,错漏之处在所难免。如果您有发现错漏之
处,或您有什么好的想法,请您通知我,本人将不胜感激。
* 汇编的技术
如果读者想通过本节来学习51的汇编,可能要让读者失望了。虽然本节的标题叫汇编
技术,但是本节并不是教读者使用汇编指令的。虽然Keil的A51提供了强大的宏的能力,但
是本节也并不想介绍A51的宏,因为这已经是具体到某个型号的mcu的问题了,前面已经说
了,本文只是借51及Keil这个开发环境,希望所提及的东西却对大部分的mcu及其开发环境
都适用。
先来看一个汇编的例子,假如有一个LED接在P1.0上,低电平亮。现要求写一个程序使
LED闪烁。200ms亮,500ms灭。(设晶振12MHz)。例1-1。
;;;
;; file 1_1.asm
;; author yanfeng <szq106@163.com>
;; date Fri Feb 09 11:39:16 2007
;;
;; update <Fri Feb 09 12:58:41 2007>
;;
;; brief 示例1_1
;;
;;
;;
;
;;; code
org 0
ljmp start
org 50h
start:
clr P1.0 ;亮
lcall delay_200ms
setb P1.0 ;灭
lcall delay_500ms
sjmp start
delay_200ms:
mov r1, #200
delay_200ms_2:
mov r0, #250 ;1ms
delay_200ms_1:
nop
nop
djnz r0, delay_200ms_1
djnz r1, delay_200ms_2
ret
delay_500ms:
mov r2, #5
delay_500ms_3:
mov r1, #100 ;100ms
delay_500ms_2:
mov r0, #250 ;1ms
delay_500ms_1:
nop
nop
djnz r0, delay_500ms_1
djnz r1, delay_500ms_2
djnz r2, delay_500ms_3
ret
end
;
;;; end
;;; Local variables:
;;; outline-regexp: ";; @+"
;;; eval: (outline-minor-mode 1)
;;; End:
;;; 1_1.asm ends here
这个程序就这个问题来说已经工作得很不错了。很不幸的是,突然有一天,客户要求
改为360ms亮,600ms灭。这样程序就要做一些改动,delay_200ms和delay_500ms被删
除,delay_360ms及600ms被实现。而且我们重新数了一下手指头来计算指令的个数。又或
者客户要求200ms亮,500ms灭,360ms亮,600ms灭这样循环闪烁。再或者客户要求升级,
加入串口于PC机通信的功能。为了得到准确的波特率,于是换了晶振为11.0952MHz,这时
现在这两个函数又要重新实现了,又有指头数了。^_^
面对这样那样的问题,有没有使程序尽可能少改动的办法呢?答案当然时肯定的,这
就要求在编程的习惯上做一些改变。
看下面的示例1-2, 仍然假定晶振12MHz。
;;;
;; file 1_2.asm
;; author yanfeng <szq106@163.com>
;; date Fri Feb 09 12:39:32 2007
;;
;; update <Fri Feb 09 13:05:00 2007>
;;
;; brief 示例1_2
;;
;;
;;
;
;;; code
org 0
ljmp start
org 50h
start:
clr P1.0 ;亮
mov r7, #200 & 0xff
mov r6, #200 >> 8
lcall delay_nms
setb P1.0 ;灭
mov r7, #500 & 0xff
mov r6, #500 >> 8
lcall delay_nms
sjmp start
delay_nms: ;r6,r7不能同时为0
delay_nms_1:
lcall delay_1ms
djnz r7, delay_nms_1
mov a, r6
jz delay_nms_ret
dec r6
sjmp delay_nms_1
delay_nms_ret:
ret
delay_1ms:
mov r0, #250 ;1ms
delay_200ms_1:
nop
nop
djnz r0, delay_200ms_1
ret
end
;
;;; end
;;; Local variables:
;;; outline-regexp: ";; @+"
;;; eval: (outline-minor-mode 1)
;;; End:
;;; 1_2.asm ends here
读者可能注意到,这个程序并不比上一个示例复杂,上一个例子的code = 118, 这个
例子的code = 119。而且delay_nms函数是通过传入的参数来延时的,这样通过不同的参数
调用delay_nms,就可以得到不同的延时时间。这样当客户的闪烁要求改变时,如:360ms
亮,600ms灭。就可以通过改变传递的参数即可,而不必数手指了。^_^!
start:
clr P1.0 ;亮
mov r7, #360 & 0xff
mov r6, #360 >> 8
lcall delay_nms
setb P1.0 ;灭
mov r7, #600 & 0xff
mov r6, #600 >> 8
lcall delay_nms
sjmp start
而当客户要求200ms亮,500ms灭,360ms亮,600ms灭这样循环闪烁时,则可轻松改变
主循环实现。
start:
clr P1.0 ;亮
mov r7, #200 & 0xff
mov r6, #200 >> 8
lcall delay_nms
setb P1.0 ;灭
mov r7, #500 & 0xff
mov r6, #500 >> 8
lcall delay_nms
clr P1.0 ;亮
mov r7, #360 & 0xff
mov r6, #360 >> 8
lcall delay_nms
setb P1.0 ;灭
mov r7, #600 & 0xff
mov r6, #600 >> 8
lcall delay_nms
sjmp start
只需要copy一下即可,是否方便了很多。而当MCU的振荡时钟改变,只需要改变
delay_1ms函数的实现即可。
在上面所举的例子,又凸现出一个问题, clr P1.0 ;亮 setb P1.0 ;灭,各出现了两
次,如果要求复杂一点,那clr P1.0 ;亮 setb P1.0 ;灭出现的次数将更多。好了,现在
由于某种原因,外围电路做了一些改动,变为LED高点亮。这下郁闷了,要查找修改不少地
方了吧。然而问题还没有完,客户说为了布线方便,把LED改接P1.1 IO了,而且PCB已经投
出去了。(我就遇上过这样的客户),这时读者您的心情是否已经开始极度变坏,国骂已
经出口,早已“问候”客户的母亲若干次。^_^
虽然心情已经很坏,但是工作还是要完成的,于是又开始了重复查找修改的工作。这
时会想要是只修改一个地方多好啊!
虽然不一定能做到只修改一个地方,但是把要修改的范围尽可能的缩小还时有可能的。
如果把灯亮及灯灭设计为两个函数,
led_on:
clr p1.0
ret
led_off:
setb p1.0
ret
这样无论客户的要求怎么变化,都可以只修改这两个函数的实现来达到满足客户的要
求。
也许读者会觉得这样调用会有调用开销,大部分的汇编器都提供宏的功能,可以用宏
来实现这样的功能。
led_on macro
clr p1.0
endm
led_off macro
setb p1.0
endm
有参数的宏,
led_on macro pin_io
clr pin_io
endm
led_off macro pin_io
setb pin_io
endm
这样当有多个LED时,就可以实现很容易通过统一的操作来驱动它们了。
宏还可以提供更强大的功能,如把指令或操作数都当做参数来传递,还可以很容易的
实现可变参数。
led_op macro ins, pin_io
ins pin_io
endm
EQU的功能(有可能写法不一样)是几乎所有的汇编器都提供的,那么可如下操作LED。
LED_PIN equ P1.0
led_op clr LED_PIN
led_op setb LED_PIN
但是本人觉得这样的实现不是很好,会使指令满工程里都有,因此还是到上一步哪里为
止比较好。
但是如果有类似#define预处理指令的支持,通过定义一些符号,操作将更为方便。
(这是因为A51完全支持C51的预处理格式。)如果不喜欢在汇编文件里使用C风格的东东,
也可以使用A51汇编器自己的预处理指令define。
#define LED_PIN P1.0
#define ON_OP clr
#define OFF_OP setb
这样要操作LED的指令如下:
led_op ON_OP LED_PIN
led_op OFF_OP LED_PIN
这样连注释都可以省略了,因为程序上已经很明显,在这里说一个原则是,程序中注
释越少越好,而不是越多越好。程序能自注释才是最好的。
这里可以看到宏的强大,如果好好利用宏,您将遇上很多意想不到的惊喜。有很多厂
家的汇编器不提供宏的功能,或者说很弱。在写程序的时候会使人有一种很难受的感觉。
(宏在语言中应用的经典当数LISP语言,可以通过宏来改写程序本身,^_^,而emacs功能
的强大也得益于它的底层实现了一个elisp(LISP的方言)的解释器,而上层完全用elisp
来实现)。
** 伪指令
伪指令并不产生实际的代码,而是指示汇编器(如A51)具体工作的。也许读者会想,
既然不产生代码,那介绍它又什么用呢。其实写汇编代码,几乎或多或少都要用到伪指令。
伪指令使用得好,可以明显减轻编程者的负担。
在介绍宏指令之前,先来考虑这样一个问题,可以看到前面的例子都很小,当程序大
到几千上万行的时候,我们该怎么办呢?还是把程序放在同一个文件里吗?当需要修改某
个函数的时候,是否满文件的找这个函数?
一个好的办法时把一个大文件分为若干个小文件,每个小文件包含一部分相关的功
能,这样功能将显得很整洁,而且移植到其它工程的时候也很方便,把文件copy过去即可。
然而本人遇上的很多同行,它们都没有把一个工程分为若干个文件来组成的能力(只会
include的不算),虽然他们的.c文件已经接近上万行,当他们试图这样做的时候,都会得到
满屏幕的warnning及error。c语言尚且如此,更何况时汇编。其实只要遵循一下编译器及
汇编器的标准,这是很容易实现的。
例如要把例子1_2中的delay_nms函数及delay_1ms函数,移到delay.asm文件中,需要
解决什么问题呢?首先,移过去的两函数的地址怎么确定,读者也许知道,可以使用org
xxxx来指定函数的地址,但是当指定这个地址时,是否与其它函数冲突呢?有可能其它函
数过长已经占用了这个地址。难道要数手指计算函数的长度吗?另一个问题是start函数怎
样调用delay_nms函数呢?
在回答这些问题之前,先来看看使用的开发工具是怎样工作的。首先A51汇编器汇编文
件后会生成.obj文件,这并不是最终需要的文件。通常还要使用一个叫链接器(如BL51)
的工具,把多个obj文件组合起来,生成最终的二进制代码文件(或其它格式的文件)。虽
然传递给链接器的参数是一个或多个obj文件,但是链接器在链接时却是按段来计算的。每
个obj文件里包含的都是一些绝对地址段或/和可重定位段。链接器必须为可重定位段重新
计算及分配地址。并计算绝对地址段是否重叠。
绝对地址段就是这个段的地址在传入链接器的时候已经固定,链接器不能改变这个段
的地址。
可重定位段就是这个段的地址需要链接器分配,如是代码段的段内的跳转等指令的偏
移也需要重新进算。
段的另一种分法可分为代码段,数据段。数据段又分为以初始化数据段和未初始化数
据段。这里不细说。
通过上面可知,如果一个函数是一个段,而这个段是可重定位的,那么就可以不用考
虑代码的地址问题了,把这个问题留给了链接器。
剩下来的问题就是怎样把一个函数变为一个段的问题,(当然一个段可包括很多函
数,在汇编中,函数只是一个说法而已)。大部分的汇编器都会提供伪指令来定义绝对段
及可重定位段(写法上有可能有区别而已)。
在A51中,定义段的伪指令是
cseg -- 绝对代码段
bseg -- 绝对位段
dseg -- 绝对内部直接寻址(data)数据段
iseg -- 绝对内部间接寻址(idata)数据段
xseg -- 绝对外部(xdata)数据段
segment -- 定义一个可重定位段
rseg -- 选择一个可重定位段
现在先不把工程分做多个文件,使用上面的段伪指令来重实现上节的例子。
;;;
;; file 1_3.asm
;; author yanfeng <szq106@163.com>
;; date Fri Feb 09 16:45:14 2007
;;
;; update <Fri Feb 09 16:50:01 2007>
;;
;; brief 示例1_3
;;
;;
;;
;
;;; code
start_seg segment code
delay_nms_seg segment code
delay_1ms_seg segment code
#define LED_PIN P1.0
#define ON_OP clr
#define OFF_OP setb
led_op macro ins, pin_io
ins pin_io
endm
cseg at 0
ljmp start
rseg start_seg
start:
led_op ON_OP LED_PIN
mov r7, #200 & 0xff
mov r6, #200 >> 8
lcall delay_nms
led_op OFF_OP LED_PIN
mov r7, #500 & 0xff
mov r6, #500 >> 8
lcall delay_nms
sjmp start
rseg delay_nms_seg
delay_nms: ;r6,r7不能同时为0
delay_nms_1:
lcall delay_1ms
djnz r7, delay_nms_1
mov a, r6
jz delay_nms_ret
dec r6
sjmp delay_nms_1
delay_nms_ret:
ret
rseg delay_1ms_seg
delay_1ms:
mov r0, #250 ;1ms
delay_200ms_1:
nop
nop
djnz r0, delay_200ms_1
ret
end
;
;;; end
;;; Local variables:
;;; outline-regexp: ";; @+"
;;; eval: (outline-minor-mode 1)
;;; End:
;;; 1_3.asm ends here
可以看到改动是很小的,且code = 42,程序里没有了org伪指令,org是一个段内指定
偏移的伪指令。当学会上面的段伪指令之后,org伪指令就应该尽可能的不要再使用。(一
些特殊情况除外,如需要对齐的情况。但是多数情况使用的是8位的MCU,总线宽度8bit,
因此这种特殊的情况也微乎其微)
每个函数都包含在一个段内。org 0 改为了 cseg at 0, org 50h则被删除,这样链
接程序会自动的为每个重定位段分配地址,也就相当于为函数分配的地址。这样函数与函
数之间就没有了gas(^_^,这是Keil的术语,也就相当于空隙),而按照以前的例子地址
3 - 4fh这段空间就属于gas,程序是没有使用的,白白浪费了。对于51这样的系统可重定
位段的另一个好处就是空间覆盖处理,这将在下一节细说。
也许读者会有这样的疑问,之前的示例并没有定义任何的段,它们工作得也很好啊,
那汇编器及链接器是怎么工作的呢?链接器的工作仍然是不变的,只是汇编器在扫描文件
时发现文件没有定义段,会为每个空间分配一个绝对段,地址从0开始。以code空间为例,如
果没有定义段,则默认段从0开始,这时文件中的第一条指令的地址就是0开始,而org伪指令
是指定段内偏移的,这里的org 0 有没有也就无所谓了。不信读者可以把这条语句去掉再
汇编一次。
而当工程里有多个文件的时候,每一个都不定义段,那么每个文件的默认段地址都从
地址0开始,这时链接器就会提示地址空间重叠。因此可重定位段的用处就在于此了。
下面将介绍的几个伪指令对于多文档工作就特别的有用了。
extrn -- 指示符号是外部的,并告诉模块是什么类型
extern -- 跟extrn差不多,相当于extrn的一种特例
public -- 指示符号可在其它模块使用
使用方法如下:
EXTRN class (symbol [ , symbol ... ])
EXTRN class:type (symbol [ , symbol ... ])
EXTERN class:type (symbol [ , symbol ... ])
PUBLIC symbol [ , symbol ... ]
中括号内是可选部分。
下面再看例1_4,把delay_nms及delay_1ms分离到1_4_delay.asm函数。
;;;
;; file 1_4.asm
;; author yanfeng <szq106@163.com>
;; date Fri Feb 09 17:30:03 2007
;;
;; update <Fri Feb 09 17:33:00 2007>
;;
;; brief 示例1_4
;;
;;
;;
;
;;; code
extrn code (delay_nms)
start_seg segment code
#define LED_PIN P1.0
#define ON_OP clr
#define OFF_OP setb
led_op macro ins, pin_io
ins pin_io
endm
cseg at 0
ljmp start
rseg start_seg
start:
led_op ON_OP LED_PIN
mov r7, #200 & 0xff
mov r6, #200 >> 8
lcall delay_nms
led_op OFF_OP LED_PIN
mov r7, #500 & 0xff
mov r6, #500 >> 8
lcall delay_nms
sjmp start
end
;
;;; end
;;; Local variables:
;;; outline-regexp: ";; @+"
;;; eval: (outline-minor-mode 1)
;;; End:
;;; 1_4.asm ends here
;;;
;; file 1_4_delay.asm
;; author yanfeng <szq106@163.com>
;; date Fri Feb 09 17:30:22 2007
;;
;; update <Fri Feb 09 17:32:50 2007>
;;
;; brief 示例1_4
;;
;;
;;
;
;;; code
delay_nms_seg segment code
delay_1ms_seg segment code
public delay_nms
rseg delay_nms_seg
delay_nms: ;r6,r7不能同时为0
delay_nms_1:
lcall delay_1ms
djnz r7, delay_nms_1
mov a, r6
jz delay_nms_ret
dec r6
sjmp delay_nms_1
delay_nms_ret:
ret
rseg delay_1ms_seg
delay_1ms:
mov r0, #250 ;1ms
delay_200ms_1:
nop
nop
djnz r0, delay_200ms_1
ret
end
;
;;; end
;;; Local variables:
;;; outline-regexp: ";; @+"
;;; eval: (outline-minor-mode 1)
;;; End:
;;; 1_4_delay.asm ends here
可以看到,code = 48 仍然没有改变,但是当项目很大时,这种优势就体现出来了,
对维护来说会很方便。
上面的例子并非已经尽善尽美了,一个问题是delay_nms调用delay_1ms函数时,它的
临时变量是放在了r6, r7里,假如delay_1ms足够复杂,把a, b, r0-r7, psw, dptr等寄存
器都使用了,这时r6, r7里面的数据将被破坏,程序将出现异常情况,那该如何是好呢?
一个好的办法就是delay_1ms函数使用了什么寄存器,就在函数的开始把使用的寄存器入
栈,在函数结束的地方再把这些寄存器恢复,听起来这是一个不错的主意。但是当使用
的寄存器多时,入栈及出栈的开销可不小呢。还有一个问题是,当函数需要返回值时,它
要把返回值放到哪里去呢?要知道函数破坏的寄存器是要恢复的哦!也许读者会想,既然
是汇编,那么一切都由自己控制,规定一些约定就可以了。是的!但是否这个约定对每一
个函数都适用,如果每个函数一种约定,那工程文件一变大,时间变长时,是否还能记下
每个函数的约定。
这里将介绍一种约定形式,即C51的形式。采用这种形式将使我们只关心函数内部的实
现,而不必理会函数外的任何东西(这将极大减轻脑力负担);且方便汇编写的函数与C写
的函数互相调用。(这可是重点哦!^_^)(这样的约定可以简化C51的实现)
这个约定是(在这里本人做了一些简化,在下一节将有更详细的说明):
1,假定被调用函数会破坏ACC, B, PSW, DPTR, R0-R7等寄存器的值,因此调用函数
在调用其它函数时不在这些寄存器里保存任何有用的数据。(这未必就是最好的方法,^_^)
2,函数的返回值通过R0-R7, C返回。(如果数据只有1bit, 则通过C返回, 1byte -
r7, 2byte - r6-r7, 4byte - r4-r7, 如果数据很大,R0-R7都放不下,则可以把数据
放在固定的数据区里,通过r1-r3返回数据区的起始地址。)
根据上面的两个原理的第一个,在上一个示例程序中,delay_nms在调用
delay_1ms时,r6,r7里不应该再保存有用的数据。因此就需要把数据保存在其它地方。保
存在哪里好呢?当然的想法就是堆栈。但是由于51的特殊结构,堆栈空间小,出入栈花时
间(主要是没有基址寻址的功能),这个办法可不一定是好的。因此C51采用了另一个办
法,把数据放在固定的数据区,这样直接访问数据速度快。这样却又带来了另外一个问
题,每个函数都占用部分数据区,51那么点点大的数据空间能支持写几个函数啊,Keil公
司早想到了这个问题,因此它的链接器(BL51)比其它开发工具的链接器多了一项功能,就
是覆盖处理功能。(这将在下一节介绍)
扯了那么多,希望读者还没有晕!^_^,现在回到正题,既然在固定的数据区分配空
间,那现在就介绍一些分配空间的伪指令。
读者是否会优先想到equ,那本人只能怀疑读者您是否中equ的毒太深。在本人见过的
汇编器里,没有一个分配空间是用equ及与之等效的伪指令(如:set, =)来实现的,滥用
equ可不是件好事,使用equ的一个建议是为一些常量指定一个符号,如果要为地址空间指
定符号,从可维护性考虑,使用bit, data,idata, pdata, xdata, code这些专用的伪指
令要明显优于equ。
每个汇编器分配空间都有其自己的伪指令,下面看看A51的伪指令。
db, dw, dd,这三个伪指令用于分配并初始化空间。
[ label: ] DB expression [ , expression ... ]
[ label: ] DW expression [ , expression ... ]
[ label: ] DD expression [ , expression ... ]
dbit, ds, dsb, dsw, dsd,这几个伪指令用于分配空间却并不初始化。
[ label: ] DBIT expression
[ label: ] DS expression
[ label: ] DSB expression
[ label: ] DSW expression
[ label: ] DSD expression
中括号为可选部分。
下面来看看改写的示例1_5:
;;;
;; file 1_5.asm
;; author yanfeng <szq106@163.com>
;; date Sat Feb 10 10:59:40 2007
;;
;; update <Sat Feb 10 11:00:45 2007>
;;
;; brief 示例1_5
;;
;;
;;
;
;;; code
extrn code (delay_nms)
start_seg segment code
#define LED_PIN P1.0
#define ON_OP clr
#define OFF_OP setb
led_op macro ins, pin_io
ins pin_io
endm
cseg at 0
ljmp start
rseg start_seg
start:
led_op ON_OP LED_PIN
mov r7, #200 & 0xff
mov r6, #200 >> 8
lcall delay_nms
led_op OFF_OP LED_PIN
mov r7, #500 & 0xff
mov r6, #500 >> 8
lcall delay_nms
sjmp start
end
;
;;; end
;;; Local variables:
;;; outline-regexp: ";; @+"
;;; eval: (outline-minor-mode 1)
;;; End:
;;; 1_5.asm ends here
;;;
;; file 1_5_delay.asm
;; author yanfeng <szq106@163.com>
;; date Sat Feb 10 10:59:21 2007
;;
;; update <Sat Feb 10 11:08:28 2007>
;;
;; brief 示例1_5
;;
;;
;;
;
;;; code
delay_nms_seg segment code
delay_1ms_seg segment code
public delay_nms
delay_nms_data segment data
rseg delay_nms_data
delay_reg: ds 2
rseg delay_nms_seg
delay_nms: ;r6,r7不能同时为0
mov a, r6
mov delay_reg, a
mov a, r7
mov delay_reg+1, a
delay_nms_1:
lcall delay_1ms
djnz delay_reg+1, delay_nms_1
mov a, delay_reg
jz delay_nms_ret
dec delay_reg
sjmp delay_nms_1
delay_nms_ret:
ret
rseg delay_1ms_seg
delay_1ms:
mov r0, #250 ;1ms
delay_200ms_1:
nop
nop
djnz r0, delay_200ms_1
ret
end
;
;;; end
;;; Local variables:
;;; outline-regexp: ";; @+"
;;; eval: (outline-minor-mode 1)
;;; End:
;;; 1_5_delay.asm ends here
可以看到,1_5.asm与1_4.asm没有任何变化,delay_1ms函数也没有任何的变化,程序
值改动了delay_nms的实现,而不用考虑调用它的函数,及它调用的函数了。(这就是最终
的结果)。
细心的读者可能会注意到一个问题,delay_nms函数的实现最前面多了几条语句。
mov a, r6
mov delay_reg, a
mov a, r7
mov delay_reg+1, a
也许读者会想,是否可以在调用这个函数的时候就把数据传入delay_reg及
delay_reg+1地址里面呢?这样就免去了调用函数先把数据放到r6,r7,然后被调用的函数
还要把r6,r7的数据移到delay_reg,delay_reg+1中。是的,读者完全可以这么做,只要把
delay_reg说明为外部可用的,然后在要调用的文件里,说明这个delay_reg是外部的即可。
按照上面所说,在1_5_delay.asm文件中删除
mov a, r6
mov delay_reg, a
mov a, r7
mov delay_reg+1, a
4行代码,加入一行下面的代码
public delay_reg
在1_5.asm文件中加入一行代码
extrn data (delay_reg)
然后把start函数
start:
led_op ON_OP LED_PIN
mov r7, #200 & 0xff
mov r6, #200 >> 8
lcall delay_nms
led_op OFF_OP LED_PIN
mov r7, #500 & 0xff
mov r6, #500 >> 8
lcall delay_nms
sjmp start
修改为如下:
start:
led_op ON_OP LED_PIN
mov delay_reg+1, #200 & 0xff
mov delay_reg, #200 >> 8
lcall delay_nms
led_op OFF_OP LED_PIN
mov delay_reg+1, #500 & 0xff
mov delay_reg, #500 >> 8
lcall delay_nms
sjmp start
这里不再列出完整的示例代码。
通过上面一步一步下来,现在的例子已经比较完美了。最后一个问题是,如果有很多
文件都需要调用这个delay_nms,那么就需要在调用这个函数的文件前面声明delay_nms是
外部的。即包含下面这两句代码。
extrn code (delay_nms)
extrn data (delay_reg)
如果函数的接口改变,那就需要在都个文件里修改这两代码。如果无论多少文件要调用它
只修改一次多好啊,读者不禁有此感叹!并不是没有这样的好方法,头文件就是为这样的
要求而设的。把上面两句声明放到一个文件里,在需要调用这个函数的文件里include这
个文件就可以了。这样的文件就叫头文件。
下面来为上面的例子加上头文件。
;;;
;; file 1_6.asm
;; author yanfeng <szq106@163.com>
;; date Sat Feb 10 11:51:48 2007
;;
;; update <Sat Feb 10 11:55:52 2007>
;;
;; brief 示例1_6
;;
;;
;;
;
;;; code
#include "1_6_delay.inc"
start_seg segment code
#define LED_PIN P1.0
#define ON_OP clr
#define OFF_OP setb
led_op macro ins, pin_io
ins pin_io
endm
cseg at 0
ljmp start
rseg start_seg
start:
led_op ON_OP LED_PIN
;; mov r7, #200 & 0xff
;; mov r6, #200 >> 8
mov delay_reg+1, #200 & 0xff
mov delay_reg, #200 >> 8
lcall delay_nms
led_op OFF_OP LED_PIN
;; mov r7, #500 & 0xff
;; mov r6, #500 >> 8
mov delay_reg+1, #500 & 0xff
mov delay_reg, #500 >> 8
lcall delay_nms
sjmp start
end
;
;;; end
;;; Local variables:
;;; outline-regexp: ";; @+"
;;; eval: (outline-minor-mode 1)
;;; End:
;;; 1_6.asm ends here
;;;
;; file 1_6_delay.inc
;; author yanfeng <szq106@163.com>
;; date Sat Feb 10 11:52:32 2007
;;
;; update <Sat Feb 10 12:00:25 2007>
;;
;; brief 示例1_6
;;
;;
;;
#ifndef _1_6_DELAY_INC
#define _1_6_DELAY_INC
extrn code (delay_nms)
extrn data (delay_reg)
#endif
;
;;; end
;;; Local variables:
;;; outline-regexp: ";; @+"
;;; eval: (outline-minor-mode 1)
;;; End:
;;; 1_6_delay.inc ends here
;;;
;; file 1_6_delay.asm
;; author yanfeng <szq106@163.com>
;; date Sat Feb 10 11:52:32 2007
;;
;; update <Sat Feb 10 11:53:09 2007>
;;
;; brief 示例1_6
;;
;;
;;
;
;;; code
delay_nms_seg segment code
delay_1ms_seg segment code
public delay_nms
public delay_reg
delay_nms_data segment data
rseg delay_nms_data
delay_reg: ds 2
rseg delay_nms_seg
delay_nms: ;r6,r7不能同时为0
mov a, r6
mov delay_reg, a
mov a, r7
mov delay_reg+1, a
delay_nms_1:
lcall delay_1ms
djnz delay_reg+1, delay_nms_1
mov a, delay_reg
jz delay_nms_ret
dec delay_reg
sjmp delay_nms_1
delay_nms_ret:
ret
rseg delay_1ms_seg
delay_1ms:
mov r0, #250 ;1ms
delay_200ms_1:
nop
nop
djnz r0, delay_200ms_1
ret
end
;
;;; end
;;; Local variables:
;;; outline-regexp: ";; @+"
;;; eval: (outline-minor-mode 1)
;;; End:
;;; 1_6_delay.asm ends here
统一的一个约定是,汇编文件使用.asm扩展名,汇编头文件使用.inc扩展名,C文件
使用.c扩展名,C头文件使用.h扩展名。
关于参数传递的方法,具体是通过寄存器传递,还是通过固定数据区传递呢?这就是
Keil C51提供的三个参数传递方法中的两个了(另一个方法是通过模拟栈传递),具体哪
一个速度更快呢?请读者不要那么快下结论。当传递参数的数据区在data空间,自然通过
固定数据区的参数传递会有可能会快过通过寄存器传递(只是有可能,并不是绝对的)。
但是当传递参数的数据区在idata, pdata, xdata等空间的时候,它们都需要通过间接寻址
的方法来访问,这时就很难说了,因为对它们的操作需要调入寄存器区,计算完后又保存
回去,开销可不小。因此C51默认的参数传递方法是通过寄存器传递。此后如无特别说明,本
文也将采用Keil C51默认的参数传递方法。(因为如果通过固定的数据区传递参数,如此
函数需要被C函数调用时,还需要遵循C的一些命名习惯)
到这里,这一节的内容已基本说完了,虽说力求所叙述的内容在大部分的汇编器上都
能实现,但仍不免有只针对A51的情况出现。这就要看各位读者根据不同的情况量体裁衣
咯。