yanfengzhu的笔记 https://bbs.21ic.com/?207699 [收藏] [复制] [RSS]

日志

单片机另类技术

已有 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的情况出现。这就要看各位读者根据不同的情况量体裁衣
咯。



路过

鸡蛋

鲜花

握手

雷人

发表评论 评论 (5 个评论)

2007-2-13 11:38
你太利害了5154
回复 hello 2007-3-20 20:10
好文章,深入浅出。 比国内大多数教材强多了
鹏星 2007-9-28 15:49
大哥,你太强了!看了你的文章,非常有见地,真的让我收获很多!!希望你能出更好的文章!!! 也祝你工作成功!
very good!! 2007-12-12 14:58
very good ! very 历害!! ^_^
回复 苗爱祥 2009-12-8 04:08
太牛了!