代码改变世界

Freescale MC9S08AW60汇编学习笔记(整理)

2015-05-01 11:56  赤骥~  阅读(2483)  评论(0编辑  收藏  举报

  MC9S08AW60是HCS08系列的MCU,它是8位的MCU,由HCS08核加上存储器和外围模块构成。HCS08系列的MCU除了MC9S08AW系列之外还有MC9S08GB系列、MC9S08GT系列、MC9S08AC系列等。不同型号的MCU应用领域的侧重点不同。

  HCS08系列的MCU就只有5个寄存器:A、H:X、SP、PC、CCR。分别是8位、16位、16位、16位、8位。具体功能要在使用中掌握。MC9S08AW60的主频为4MHz,一个总线周期为T=0.25us。MC9S08AW60存放数据的方式是大端方式:低地址存放最高有效字节,此外需要知道的是存储器中RAM地址:$0070~$086F,FLASH地址:$0870~$17FF、$1860~$FFAF。接着我们就可以使用Codewarrior来编写代码了。

  首先,给出一个最基础的框架:

 org $0070        ;变量定义
x1 ds.b 1

 org $1860         ;常量定义并写出主程序

x2 dc.b 1

main:                 ;主函数
  clra
  clrx

  nop

again:                ;主循环
  nop
  jmp again

 org $fffe           ;复位向量
 dc.w main

  编译后,可以在调试面板查看一下效果。其中复位向量的使用实质是在地址$FFFE~$FFFF中写入main函数的地址,当复位时就直接执行$FFFE中的代码,于是转入main函数中,程序就回到了最初的固定状态,复位的使用可以应对程序中出现的异常,就好比手机的重启、初始化之类的总是回到一个可以预知的固定状态。其他程序代码(如:nop表示空操作占用一个总线周期T)可以翻阅数据手册,也可以看看《Freescale-HCS08单片机原理及应用-指令集汇总》——我已经上传了。

 

  路要一步步走,饭要一口口吃,有了一个主体框架后,就来实现一个简单的程序:实现两数相加,并在存储器中查看变化。

 org $0070
x1 ds.b 1
x2 ds.b 1
s1 ds.b 1

 org $1860
main:
  clra
  clrx

  mov #$04,x1    ;4T,将#$04送给x1
  mov #$28,x2    ;4T,将#$28送给x2

  lda x1               ;3T,将x1中的数送给寄存器a
  add x2              ;3T,寄存器a中的数加上x2后再送入寄存器a
  sta s1               ;3T,将寄存器a中的数传送给s1,s1中就是两数相加的结果了

again:
nop
jmp again

org $fffe
dc.w main

  可以知道这个小程序的工作时间是17T,也就是17*0.25us=4.25us,只需要这么短的时间,MC9S08AW60这个MCU就帮我们实现了两数相加。我们还可以在调试面板中查看存储器、寄存器中的变化,主要是$0070之后的变量值变化和寄存器a中的变化。

 

  前面给出了一个实现加法的小程序,但是如果要求变得更复杂、步骤变得更繁琐,这时又该怎么办呢??我们可以使用子程序来解决这个问题,这里给个例子:若字变量Data1和Data2分别存放着两个16位无符号数,编写求两个数之和的子程序,并将和存放到Sum字节存储空间中。代码如下:

 org $0070
data1 ds.b 2
data2 ds.b 2
sum ds.b 3

 org $1860
Add_Pro: clc        ;清空进位标志位
  clrx
  clrh
  clra

  mov #$00,data1
  mov #$04,data1+1
  mov #$00,data2
  mov #$28,data2+1

  lda data1+1
  add data2+1
  sta sum+2

  lda data1
  adc data2
  sta sum+1
  rola                    ;得到第17为,主要操作是:将寄存器a中数据向做移一位并把进位标志位放入寄存器a中的第一位

  and #01H
  sta sum

  rts                     ;子程序返回指令

main: 
  clra
  clrx
  bsr Add_Pro    ;子程序调用,直接进入子程序中,执行子程序中的程序。

again:
  nop
  jmp again

 org $fffe
 dc.w main
  分析:两个16位无符号数相加,和为17位,至少需要3B的存储空间。又因为MC9S08AW60的存储方式是大端方式:低地址存放最高有效字节。所以对两个数的低位数相加时是使用Data1+1、Data2+1。

 

  面对复杂的程序,我们不能都放入main主程序之中,那样会让人思路不够清晰,于是我们加入了子程序,这样程序就有了条理。主程序决定我们的思路,子程序则实现具体的功能。如果程序没有达到预想中的效果,就可以有条有理的进行分析,这在编写汇编程序时是很重要的,毕竟汇编语言不像c语言、c++、java等高级语言贴近人类语言。汇编语言的优势在于执行效率,可以用于程序的优化。为了让程序更加强大,我们给程序加入查表的功能,利用查表可以实现转换、计算等各种功能。

  例子:将单字节变量Hex_Byte中存放的十六进制数转换为两位ASCII码,并将结果存至以Result开首的字节存储单元中,可使用子程序。例如4AH的ASCII码为3441H,$F9的ASCII码为4639H。代码如下:

 org $0070
Hex_Byte ds.b 1
Result ds.b 2 

 org $0870
ASCII_Table dc.b '0123456789ABCDEFG'

Hex_To_Ascii: 
  clra
  clrx
  clrh

  lda Hex_Byte
  and #$f0
  nsa                         ;作用是:将寄存器a中的高4为与低4位交换:即(A[3:0]:A[7:4])-->A
  tax                         ;A-->X
  clrh                        ;00H-->H,查表时使用的寻址使用H:X中的值
  lda ASCII_Table,x    ;查表
  sta Result

  lda Hex_Byte 
  and #$0f
  tax
  lda ASCII_Table,x
  sta Result+1
  rts

main:
  clra
  clrx
  clrh

  mov #4AH,Hex_Byte

  bsr Hex_To_Ascii

again:
  nop
  jmp again 


 org $fffe
 dc.w main

  请时刻记住MC9S08AW60使用的是大端方式:低地址存放最高有效字节。这就是为什么高4位的ASCII码存放入Result中,二低4位的ASCII码存放入Result+1中。查表时是这样执行的:lda ASCII_Table,x    ;查表    可以理解为:找到表中的第x位并放入A中。

  我们再来试试查立方表:设字节变量Num存放在RAM的$0070单元,取值范围为0T~9T,编写查表子程序,查出变量的立方值,并存入Res_Cube字节变量中。代码如下:

 org $0070
Num ds.b 1
Res_Cube ds.b 2 

 org $1860
Cube_Tab: dc.w 0000,0001,0008,0027,0064,0125,0216,0343,0512,0729


Cube: 
  clra
  clrx
  clrh

  ldx Num            ;Num-->X
  lslx                    ;X*2-->X
  clrh                    ;查表使用H:X中的值寻址,所以不要忘记清零
  lda Cube_Tab,x  ;查表得立方值高位
  sta Res_Cube    ;大端方式:低地址存放最高有效字节
  lda Cube_Tab+1,x ;查表得立方值地位
  sta Res_Cube+1
  rts

main:
  clra
  clrx
  clrh

  mov #$09,Num

  bsr Cube

again:
  nop
  jmp again


 org $fffe
 dc.w main

  分析:由于9的立方值为729T=2D9H,故表中的每一个立方值需用2B来存储,则数值Num的立方值的存储地址Addr_n和立方表的首地址Cube_Tab的关系为:Addr_n=Cube_Tab+Num*2。

  查表技术是汇编语言程序设计的一个重要技术,通过查表可避免复杂的计算和编程,如查平方表、立方表、函数表、数码管显示的段码表等。表格常数一定要定位到flash区域才能正确实现查表功能,不能将其定位到RAM区域;此外要注意清零H寄存器。

 

  前面学习了查表的功能,可以用查表的功能实现不少强大的功能,如查立方表、平方表、函数表、数码管显示的段码表等。这里还有一种由查表和转移指令配合使用而实现的一种强大的功能:散转。散转是指根据输入数据的不同来跳转到不同的程序入口进行处理,也就是说如果有多个子程序,我们不仅可以让它们按顺序一个个执行,还可以根据做判断用的数据的值来决定跳转到哪一个子程序。实现散转的具体做法是:在程序中定义一块连续存储单元作为跳转表,表中顺序存放各分支处理程序的跳转地址。各跳转地址在表中的偏移地址等于跳转表首地址加上它们各自的序号所占字节数的乘积。感觉关于跳转地址这一块有点不好理解,就用一个例子来说明一下:

  设字节变量Num的取值范围为0~3,另有程序入口地址Pro0~Pro3,编写子程序,要求根据Num的值转向不同的Pro程序。如Num为2则转向Pro2。代码如下:

 org $0070
Num ds.b 1 

 org $1860

Pro0:nop                    ;子程序群,这里都让他们空操作。
   rts 
Pro1:nop
   rts
Pro2:nop
   rts
Pro3:nop
   rts

Sbranch:                   ;分支程序,有点像c语言中switch
  ldx Num              ;Num-->X
  lda #05h        
  mul                     ;Num*5-->X:A
  tax                      ;A-->X  
  clrh                     ;查表之前别忘清零
  jmp Pro_Tab,x

Pro_Tab:                   ;跳转表
  jsr Pro0               ;跳转到Pro0,3B
  bra Sb_End          ;调用Pro0完毕,返回,2B

  jsr Pro1
  bra Sb_End

  jsr Pro2
  bra Sb_End

  jsr Pro3
Sb_End: rts

main:
  clra
  clrx
  clrh

  mov #$01,Num     ;设定Num的值,用来作为选择子程序的判断数

  bsr Sbranch


again:
  nop
  jmp again 


 org $fffe
 dc.w main

  细心的人肯定会发现Num是先乘以5之后才用来做散转的,其原因在于Pro_Tab标号后面的JSR指令是3字节指令,BRA指令值2字节指令,故数值Num、标号Pro_Tab的地址Addr_Pro和指令语句JSR  Pro_n的首地址Addr_Pro_n之间的关系为:

                         Address_Pro_n=Addr_Pro+Num*5

  是不是突然发现汇编的强大了,从一开始汇编给人的感觉是在寄存器之间跳来跳去变成了汇编可以实现这个功能那个功能,只要设计足够精巧、心思足够细腻,就算是汇编这种呆板的机器语言也可以焕发勃勃生机,举个例子:MenuetOS是一款完全用汇编语言写成的操作系统。 因此它运行起来超快,体积也非常小,它甚至可以放在一张软盘内。MenuetOS提供了抢占式多任务处理,一个引人注目的漂亮GUI,用于网络接入的 TCP/IP堆栈等等。由此可见:强大的不是工具而是人。当然,如果强大的工具加强大的人必然可以爆发出惊人的力量。

 

  延时,汇编中经常要用到的功能,也就是MCU什么也不做,仅仅是拖延一段时间而已。MCU本身就有定时器、计数器,用来实现延时当然不在话下,但是一个编程的人自然更希望要实现的功能更加易于掌控,我们就用代码、用程序来实现延时,也就是采用软件进行延时。具体做法是:通过A、H:X增减指令、空操作指令nop和brn以及相应的转移指令,再利用循环结构就可以实现延时功能。既然是延时,能知道延时多长时间最好了,我们知道的是:MCU总线时钟频率为4MHz,所以一个总线周期占用的时间为0.25us,这样,只要知道每条指令所占用的总线周期就可以计算我们的程序运行了多久。实现延时自然也是这样,这些当然要用到数学知识了,放心的是还不至于用到微积分,只要耐心一点精确地计算出延时的时间并不是难事。

  例子:设计一个延时10ms的延时子程序,已知MCU总线时钟频率为4MHz。

分析:由于总线时钟频率为4MHz,故一个总线周期占用的时间为0.25us,10ms延时需要执行相当于40000个总线周期的指令。我们可以先设计一个实现较小延时的子程序Re_cycle,然后多次循环调用该子程序来实现较长的时延。代码如下:

 org $0070
num ds.b 1
count1 ds.b 1

 org $1860
re_cycle:                              ;4+7*70+6=500T=125us
  mov #70T,num              ;4T
  dbnz num,*                    ;7T
  rts                                  ;6T
delay_10ms:                        ;[4+78*(5+7+500)]+4+7*7+6=39999T
  mov #78T,count1          ;4T
re_call:
  bsr re_cycle                    ;5T
  dbnz count1,re_call         ;7T
  mov #07T,count1           ;4T
  dbnz count1,*                 ;7T
  rts                                   ;6T

main:
  bsr delay_10ms               ;5~6T
again:
  nop
  jmp again


 org $fffe
 dc.w main

  每一条指令所占用的时间皆已标出,需要的就是精巧的设计和精准的计算,比如Re_cycle子程序的设计三条指令刚好500T,这里要解释一下DBNZ这个指令,它所实现的功能就是前面变量中的数自减1与0比较,不等就转移到后面的地址并执行,相等就结束该指令(也就是减1不为0转移指令)。这里肯定要问*代表什么,它代表的就是它本身所在指令的地址,dbnz num,*   指令就可以解释为num自减1不等于0就回来再执行该语句,知道num自减1等于0后结束。由注释看出,delay_10ms子程序执行完所占用的总时间为39999T,再加上在main主程序中对delay_10ms子程序的调用占用5~6T,这样没调用delay_10ms一次,便可以实现40004~40005个总线周期约10ms的延时。当然,如果设计足够好,还可以更精确,越靠近40000个T越好。

  上面实现了延时10ms,那如果要实现100ms,500ms,1ms,0.1ms呢,同样的办法,全靠精巧的设计和精准的计算。下面给出延时1ms和500ms的子程序:

 org $0070
num ds.b 1
count1 ds.b 1
count2 ds.b 1

 org $1860
re_cycle:                          ;4+7*70+6=500T=125us
  mov #70T,num
  dbnz num,*
  rts

delay_1ms:                      ;[4+(5+7+500)*8+6]=4106T
  mov #08t,count
  re_call:
  bsr re_cycle                
  dbnz count,re_call
  rts

delay_10ms:                    ;39999T
  mov #78T,count1
re_call:
  bsr re_cycle
  dbnz count1,re_call
  mov #07T,count1
  dbnz count1,*
  rts

delay_500ms:                      ;想法是将delay_10ms重复执行50次,这样肯定有不精确的地方,还请见谅。
  mov #50T,count2
re:
  bsr delay_10ms
  dbnz count2,re
  rts


main:

  bsr delay_1ms
  bsr delay_10ms

  bsr delay_500ms
again:
  nop
  jmp again


 org $fffe
 dc.w main