笔者认为学习51单片机并不是能通过别人的例子用c语言模仿写出类似的功能即可,必须要对自己的编码意图比较清晰,这样脱离任何例程都是可以自己掌控编写代码。因此学习51单片机其实更准确来说是学习微机的原理以及接口技术。而微机的原理以及接口技术对于51,arm或其它架构的mcu都是通用的,通过51来学习微机原理会涉及到汇编语言,因为只有汇编语言才能直接描述51内部的工作状态。笔者以过来人的身份推荐初学者从51微机原理,汇编学起。C语言只是简化封装了汇编语言的一些处理过程,学完汇编,c语言也自然会达到相应的水平。此外,对于软件出错调试,只能跟踪汇编代码,查看寄存器的状态判断,而想学习arm,从事更深入的嵌入式开发,汇编是必不可少的。

3.1. 硬件原理图

8个LED连接到P0口,当短接CON2后,只要P0口对应位为0(低电平),相应的LED则被点亮。此外说明一下为什么不用P0对应位为1时点亮而用0,因为传统51单片机I/O口是弱上拉的,高电平是输不出大电流的(相对低电平),高电平拉电流估计是ua级,但低电平灌电流几个ma是不成问题的。对于stc系列51单片机,I/O口是可以配置成推挽输出的,这样高低电平都是可以达到20ma(手册数据)的输出/吸收电流。

01_入门汇编 - 图1

图3.1-1

3.2. 工程搭建

打开Keil C51,Project->NewuVersion Project,保存项目后,选择cpu为Atmel的AT89C52的51单片机,这里需要说明的是,Keil没有stc系列的51单片机选择,只要是51内核,在Keil下可选择任一厂家,任一款51单片机进行代码编写,因为代码都是兼容的。而不同厂商芯片之间的差异只是rom大小,ram大小,片内外设以及一些厂家特有的特殊功能寄存器的定义。这些都可以在工程中,代码中重新定义,编译器会老老实实按照要求编译代码。选择了cpu后,会提示是否加入51的启动代码到工程中,由于我们编写的是汇编语言,此处不需要,加入后启动代码会与我们自己的汇编代码定义冲突。这里需要说明的是,启动代码是初始化c环境需要的文件,启动代码会设置c代码运行时的堆栈,清零全局变量,静态变量区等。这就是为什么我们在c文件中定义一个全局变量,默认这个变量的初始值为0(C标准)。

3.3. 代码编写

创建一个新文件,命名为LEDs.ASM,ASM为51汇编文件后缀,保存并加入工程。汇编的一些基本用法在代码注释中有说明,更多的汇编用法请google,百度。这里需要说明的是,51单片机第一条指令位置是在0H,后面相邻的地址是分配给相应的中断进入的,因此第一条指令往往会跳转避开中断向量地址区。以下代码实现8个LED灯轮流点亮,点亮延时1s,这个汇编代码是模仿c语言函数结构化编程的,里面可以类似认识到c编译器大概是如何处理c函数并生成汇编的,当然编译器汇编质量基本是无法达到人工汇编质量的。


ORG 0H        ; 表示后面紧跟的那条指令的地址是 0000H

JMP     Begin ; 无条件跳转到Begin处,以避免中断向量地址

ORG 0BH      ;000BH处为定时器T0的中断处理入口

JMP T0_INT ; 未使用T0定时器中断,只供代码说明



T0_INT:

; 中断发生时会自动把当前程序运行地址PC压入栈sp

; 中断处理完后用RETI中断返回,从栈sp中出栈到PC返回打断程序处

RETI



LED1 EQUP0.0  ; LED1由P0口第0位控制,以下类似

LED2 EQU P0.1

LED3 EQU P0.2

LED4 EQU P0.3

LED5 EQU P0.4

LED6 EQU P0.5

LED7 EQU P0.6

LED8 EQU P0.7



ORG 100H

Begin:

MOV P0, #0xff ;P0口输出全1,所有LED灭



LOOP:

; R6,R7为调用函数的参数传入,参数为16位,需2字节

; _Delay_ms对应c函数原型为void Delay_ms(intnCount)

; 共延时nCount * 1ms(12M普通8051),对于stc指令周期1T的

; 延时nCount * (1/6)ms (12M)

CLR LED1 ;直接位清0指令,清除P0口第0位,LED1亮

MOV R7, #(1000& 0xff) ; 参数为1000,普通8051延时1s

MOV    R6, #((1000>>8) & 0xff)   ; 16位变量需用2字节

CALL _Delay_ms ;延时n个1ms(普通51),延时n个1/6ms(stc 51)

SETB LED1 ; 直接位位置位指令,置位P0口第0位,LED1灭



CLR     LED2

MOV R7, #(1000& 0xff) ; 普通8051延时1s,stc应改1000为1000*6

MOV    R6, #((1000>>8) & 0xff) ; 能让编译器运算的不要自己手动计算

CALL _Delay_ms

SETB LED2



CLR     LED3

MOV R7, #(1000& 0xff)

MOV    R6, #((1000>>8) & 0xff)

CALL _Delay_ms

SETB LED3



CLR     LED4

MOV R7, #(1000& 0xff)

MOV    R6, #((1000>>8) & 0xff)

CALL _Delay_ms

SETB LED4



CLR     LED5

MOV R7, #(1000& 0xff)

MOV    R6, #((1000>>8) & 0xff)

CALL _Delay_ms

SETB LED5



CLR     LED6

MOV R7, #(1000& 0xff)

MOV    R6, #((1000>>8) & 0xff)

CALL _Delay_ms

SETB LED6



CLR     LED7

MOV R7, #(1000& 0xff)

MOV    R6, #((1000>>8) & 0xff)

CALL _Delay_ms

SETB LED7



CLR     LED8

MOV R7, #(1000& 0xff)

MOV    R6, #((1000>>8) & 0xff)

CALL _Delay_ms

SETB LED8



MOV P0, #0    ; 常数存入直接地址,清零P0口,LED全亮

MOV R7, #(1000& 0xff)

MOV    R6, #((1000>>8) & 0xff)

CALL _Delay_ms

MOV P0, #0xff

MOV R7, #(1000& 0xff)

MOV    R6, #((1000>>8) & 0xff)

CALL _Delay_ms



JMP     LOOP ; LOOP循环



; 按照keil c与汇编调用规则命令函数及传参,可先不管

; 用CALL调用函数会硬件把调用处PC地址压栈

; 处理完后用RET函数返回,从栈sp中出栈到PC返回调用程序处

_Delay_ms:

PUSH ACC ; 子函数需用到累加器,需压栈保存以免覆盖调用前值

PUSH PSW ; 用到程序状态寄存器,需压栈

MOV    A, R0 ; 用到R0寄存器,没有直接寄存器名压栈指令

PUSH ACC  ; 通过累加器完成压栈

MOV A, R1 ; 用到R1寄存器,同理压栈

PUSH ACC



; 以下是16位的递减1减法运算,高8位在R6中,低8位在R7中

; 数据运算涉及到进位/借位,只能通过累加器ACC来完成

Delay:

CLR C ; 清除借位标志

MOV A, R7 ; 低8位值给到累加器,只有针对累加器运算的指令

SUBB A, #1 ; 自减1,会改变程序状态标志(进位/借位)

MOV R7, A ; 运算结果返回到原变量中

JNC DelayOnce ; 没有借位说明延时次数未到,跳转延时一次

CLR C ; 产生了借位,需向高8位R6减1

MOV A, R6

SUBB A, #1

MOV R6, A

JNC DelayOnce ; 高8位未减至0,说明延时次数未到

POP ACC   ; 高8位也为0,不能再给低8位借位了,延时到返回

MOV R1, A ; 返回时先出栈,出栈顺序与入栈顺序相反

POP ACC     ; 并且PUSH与POP指令必须一一对应,不然只有让程序飞

MOV R0, A

POP PSW

POP ACC

RET ; 子函数返回,与c函数是一至的



; DelayOnce执行一次机器周期总数为 1+R0*(1+R1*2+2)+2=997个

; 若普通51晶振12M,每个机器周期1us,则DelayOnce一次延时1ms

; 对于stc51,同等晶振下,指令速度快了6倍,DelayOnce延时1/6ms

DelayOnce:

MOV R0, #2 ;普通51机器周期数1(stc这条指令比普通的快6倍)

Delay1:

MOV R1, #247 ;普通51机器周期数1(stc快6倍)

DJNZ R1, $ ; R7减1不为0则跳转到当前地址循环,机器周期数2

DJNZ R0, Delay1; 机器周期数2(stc快6倍)

JMP Delay ; 已延时一次,机器周期数2(stc快6倍)



END            ; 汇编代码结束

3.4. 代码运行

在Keil上选中Create HEX File复选框,编译生成hex文件,可以直接在Keil进行debug,通过查看P0口数据的变化以跟踪代码等,注意设置仿真的时钟为12M。更直观的是用Proteus搭建一个51单片机仿真电路,在P0口连接8个LED,即可看到效果,注意设置仿真的时钟为12M。如果有51开发板,把代码下载进单片机中即可(但对于stc 1T 51单片机需修改一下代码中延时的参数)。

01_入门汇编 - 图2

图3.4-1 Proteus仿真代码效果图

4. 小结

笔者概述性介绍了51单片机(以stc12c5a60s2为例),讲解了其基本的编译,调试工具、环境的搭建。简单给出了采用c函数结构编程的流水灯汇编代码,让读者对汇编,c编译汇编过程有一个初步的认识,由于笔者的认识有限,文章中个人观点有些可能非常片面,以及文章中可能存在不少的错误,恳请大家指正。

由于一门技术是不可能用只言片语就能说清的,笔者也只能在文章中概述性讲述,可能会有初学者觉得例程汇编过难,笔者想说明的是,学习是一个渐近的过程,只要学习了,那么就会有潜移默化的进步。以下资料笔者认为跟本文学习是相关的,推荐大家学习或参考。

  • stc12c5a60s2数据手册,非常有用,里面有很多编程示例代码以及详尽的stc51系列单片机寄存器编程描述。

  • 单片机初学者实验指导书.doc,只对入门者,讲述怎样连接usb转串口线下载代码,keil安装以及工程搭建。

  • Keil软件使用手册.ppt,简单讲解了Keil软件工程的搭建,调试介绍

  • Proteus中文入门教程.doc,讲述了Proteus如何搭建电路以及进行51单片机的仿真

  • 51汇编指令.ppt,较好地介绍了51汇编指令,伪指令等的使用,但细节不够,如指令执行后栈变化没有说明,以51微机原理教科书汇编指令资料为最佳。

  • LEDs.ASM,汇编工程代码,加入到keil工程即可编译。