第3章 51单片机编程语言 3.1单片机的编程语言概述 51单片机的编程语言可以是汇编语言,也可以是高级语言,如由C语言演变而成的C51语言等。汇编语言产生的目标代码短,占用的存储空间小,执行速度快,能充分发挥单片机的硬件功能。但对于复杂的应用来讲,使用汇编语言编程复杂,程序的可读性和可移植性不强。高级语言产生的目标代码长,占用的存储空间大,执行速度慢。但这是相对于汇编语言来讲的,其实C语言在大多数情况下的机器代码生成效率和汇编语言相当,但可读性和可移植性却远远超过汇编语言,编程效率也大大高于汇编语言。 可见,汇编语言和高级语言各有优缺点,在应用中应根据实际情况选用。如果应用系统的存储空间比较小,且对实时性的要求很高,则应选用汇编语言编程; 如果系统的存储空间比较大,且对实时性的要求不是很高,则C51语言是理想的编程语言。如果系统中有部分模块对实时性的要求很高,而其他模块对实时性的要求不是很高,则可以将两种语言结合,程序的主体部分使用C51编程,对实时性要求高的模块用汇编语言编程,然后将汇编语言程序模块嵌入C51语言程序中。 用高级语言和汇编语言编写的源程序都必须转换为目标程序(机器语言),单片机才能执行。目前很多公司都将编辑器、汇编器、编译器、连接/定位器、符号转换程序做成了软件包,称为集成开发环境(Integrated Developing Environment,IDE),如Keil μVision、Silicon Laboratories IDE等。用户进入集成开发环境,编辑好程序后,只需单击相应的菜单或快捷工具按钮就可以完成汇编/编译、连接/定位、程序下载等功能,还可以在线跟踪调试。 可见,汇编语言和高级语言都是开发单片机应用系统必须掌握的编程语言。本书以目前流行的C51为主要编程语言,同时兼顾传统的汇编语言。本章首先简要介绍51单片机的寻址方式和指令系统,然后通过几个编程实例介绍汇编语言程序设计中的几种基本结构程序的设计方法和技巧,最后重点讲述C51语言的基础知识及编程方法。在集成开发环境中调试汇编语言程序和C51语言程序的方法见本书第8章。 3.2CIP51指令介绍 CIP51系统控制器的指令集与标准MCS51TM指令集完全兼容,可以使用标准8051的开发工具开发CIP51的软件。所有的CIP51指令共111条,在二进制机器码和功能上与MCS51 TM 产品完全等价(如操作码、寻址方式、对PSW标志的影响等),但是指令时序与标准8051不同。 在很多的8051产品中,机器周期和时钟周期是不同的,机器周期的长度在2~12个时钟周期之间,如Intel公司的MSC51单片机的机器周期的长度为12个时钟周期。但是CIP51只基于时钟周期,所有指令时序都以时钟周期计算,也就是说,CIP51的机器周期与时钟周期相等。由于CIP51采用了流水线结构,大多数指令执行所需的时钟周期数与指令的字节数一致。条件转移指令在不发生转移时的执行周期数比发生转移时少一个。附录A给出了CIP51指令一览表,包括每条指令的助记符、字节数和时钟周期数。 3.2.1寻址方式 寻址方式就是根据指令中给出的地址寻找真实操作数地址的方式。8051单片机的寻址方式有以下7种。 1. 寄存器寻址 寄存器寻址时,指令中地址码给出的是某一通用寄存器的编号,寄存器的内容为操作数。例如指令 MOVA,R0;A←(R0) 8051可用寄存器寻址的空间是R0~R7,ACC,DPTR,B。 2. 直接寻址 直接寻址时,指令中地址码部分直接给出了操作数的有效地址。例如指令 MOVA,4FH;A←(0x4F) 可用于直接寻址的空间是内部RAM低128B(包括其中的可位寻址区)、特殊功能寄存器。 3. 寄存器间接寻址 寄存器间接寻址时,指令中给出的寄存器的内容为操作数的地址,而不是操作数本身,即寄存器为地址指针。例如指令 MOVA,@R1;A←((R1)) 8051中可以用R0或R1间接寻址片内或片外RAM的256B范围,可以用DPTR或PC间接寻址64KB外部RAM或ROM。 4. 立即寻址 立即寻址时,指令中地址码部分给出的就是操作数。即取出指令的同时立即得到了操作数。例如指令 MOVA,#6FH;A←0x6F 5. 变址寻址 变址寻址时,指定的变址寄存器的内容与指令中给出的偏移量相加,所得的结果作为操作数的地址。例如指令 MOVCA,@A+DPTR;A←((A)+(DPTR)) 不论是用DPTR还是PC来提供基址指针,变址寻址方式都只适用于8051的程序存储器,通常用于读取数据表。 6. 相对寻址 相对寻址时,由程序计数器PC提供的基地址与指令中提供的偏移量rel相加,得到操作数的地址。这时指出的地址是操作数与现行指令的相对位置。例如指令 SJMPrel;PC←(PC)+2+rel 7. 位寻址 位寻址时,操作数是二进制数的某一位,指令中使用位地址指明要操作的位,例如指令 SETBbit;(bit)←1 8051可用于位寻址的空间是内部RAM的可位寻址区和SFR区中的字节地址可以被8整除(即地址以0或8结尾)的寄存器所占空间。 3.2.251指令集 8051单片机的指令按其功能可分为以下5大类。 数据传送指令。 算术运算指令。 逻辑运算指令。 布尔运算指令。 程序分支指令。 1. 数据传送指令 数据传送指令是将数据由来源地址传送至目的地址,除XCH与XCHD外,数据传送指令均不会改变来源地址数据内容。8051的数据传送指令共分以下7种,下面分别给予叙述。 MOV(一般传送指令): 将来源地址所指定的数据复制到目的地址所指定的单元。此种操作不影响任何标志,这种指令主要用于内部RAM及寄存器之间的数据传送。共有以下16种寻址格式: MOVA,Rn;A←(Rn) MOVA,direct;A←(direct) MOVA,@Ri;A←((Ri)) MOVA,#data;A← data MOVRn,A;Rn←(A) MOVRn,direct;Rn←(direct) MOVRn,#data;Rn← data MOVdirect, A;direct←(A) MOVdirect, Rn;direct←(Rn) MOVdirect,direct;direct←(direct) MOVdirect,@Ri;direct←((Ri)) MOVdirect,#data;direct← data MOV@Ri,A;(Ri)←(A) MOV@Ri,direct;(Ri)←(direct) MOV@Ri,#data;(Ri)← data MOVDPTR,#data16;DPTR← data16 MOVC(查表指令): 在程序区段复制一字节数据到A累加器,此种指令经常用于查表程序中。共有以下2种寻址格式: MOVCA,@A+DPTR;A←((A)+(DPTR)) MOVCA,@A+PC;PC←(PC)+1; A←((A)+(PC)) MOVX(外部数据存储器读写指令): 在外部数据存储器区段复制一字节数据到A累加器,或将A累加器中的数据复制到被寻址的外部数据存储器区段。共有以下4种寻址格式: MOVXA,@DPTR;A←((DPTR)) MOVXA,@Ri;A←((Ri)) MOVX@DPTR,A;(DPTR)←(A) MOVX@Ri,A;(Ri)←(A) 注意,MOVX指令的寻址地址有8位和16位两种,16位时是通过DPTR寄存器中的16位地址来寻找64KB存储器范围的单元。8位时是通过Ri中的内容作为地址的低8位,特殊功能寄存器EMIOCN的内容作为地址的高8位来寻找64KB存储器范围的单元。具体见2.2.5节。 PUSH(压栈指令): 将数据推进堆栈,执行指令时先将SP加1,再将数据压入堆栈。指令格式: PUSHdirect;SP←(SP)+1; (SP)←(direct) POP(出栈指令): 将数据由堆栈顶端取出,执行指令时先将数据取出,再将SP减1。 POPdirect;direct←(SP); SP←(SP)-1 XCH(字节交换指令): 将累加器内数据与指定寻址的内容数据互换。有如下3种寻址格式: XCHA,Rn;(A)(Rn) XCHA,direct;(A)(direct) XCHA,@Ri;(A)((Ri)) XCHD(半字节交换指令): 将累加器内低4位数据与指定寻址的内容低4位数据互换。指令格式: XCHDA,@Ri;(A)3~0((Ri))3~0 2. 算术运算指令 算术运算指令包括四则运算(加减乘除)、加1、减1以及BCD码十进制加法调整指令等,共有8种指令。分别叙述如下。 ADD(加指令): 将指定寻址的内容加上累加器内容,并将结果存入累加器中,此种运算会影响特殊功能寄存器PSW。有如下4种寻址格式: ADDA,Rn;A←(A)+(Rn) (n: 0~7) ADDA,direct;A←(A)+(direct) ADDA,@Ri;A←(A)+((Ri)) ADDA,#data;A←(A)+data ADDC(带进位加指令): 将指定寻址的内容加上累加器内容,再加上进位标志CY,并将结果存入累加器中。有如下4种寻址格式: ADDCA,Rn;A←(A)+(Rn)+(CY) ADDCA,direct;A←(A)+(direct)+(CY) ADDCA,@Ri;A←(A)+((Ri))+(CY) ADDCA,#data;A←(A)+data+(CY) 以上两个加运算后,如果位7有进位,则特殊功能寄存器PSW的进位标志CY被设定为1,否则为0; 如果位3有进位,则特殊功能寄存器的辅助进位标志AC被设定为1,否则为0。此种运算也会影响OV(溢出标志),当溢出标志为1时,表示两个带符号正数相加变为负数(溢出)或两个带符号负数相加变为正数(溢出)。 DA(十进制调整指令): 将A累加器内容作BCD码调整。指令格式: DAA;若AC=1或A3~0>9,则A←(A)+0x06 ;若CY=1或A7~4>9,则A←(A)+0x60 DEC(减1指令): 将指定寻址的内容减1。注意,这种运算不影响任何标志,且当递减至0x0时再递减变为0xFF,共有以下4种寻址格式: DECA;A←(A)-1 DECdirect;direct←(direct)-1 DEC@Ri;(Ri)←((Ri))-1 DECRn;Rn←(Rn)-1 DIV(除法指令): 将累加器A和寄存器B作无符号数相除。指令格式: DIVAB;(A)/(B),A←商、B←余数 INC(加1指令): 将指定寻址的内容加1。注意,此种运算不影响任何标志,且当递增至0xFF(8b)或0xFFFF(DPTR)时,再递增变为0。共有以下5种寻址格式: INCA;A←(A)+1 INCdirect;direct←(direct)+1 INC@Ri;(Ri)←((Ri))+1 INCRn;Rn←(Rn)+1 INCDPTR;DPTR←(DPTR)+1 MUL(乘法指令): 将累加器A和寄存器B 作无符号数相乘。此结果产生一个16位数据。注意,若运算后B寄存器不为0,则溢出标志被设为1。指令格式: MULAB;(A)×(B),A←乘积低字节、B←乘积高字节 SUBB(带借位减法指令): 将累加器A的内容减去指定寻址的内容,再减去进位标志CY,并将结果存入累加器中。共有4种寻址格式: SUBBA,Rn;A←(A)-(Rn)-(CY) SUBBA,direct;A←(A)-(direct)-(CY) SUBBA,@Ri;A←(A)-((Ri))-(CY) SUBBA,#data;A←(A)-data-(CY) 如果位7有借位,则进位标志CY被设定为1; 如果位3有借位,则辅助进位标志AC被设定为1。此种运算会影响溢出标志,当溢出标志为1时,表示一个带符号正数减一个带符号负数变为负数(溢出),或一个带符号负数减一个带符号正数变为正数(溢出)。 3. 逻辑运算指令 8051的逻辑运算指令包括AND、OR、XOR、NOT、左/右移位、清除、高/低4位对调等指令。分别叙述如下。 ANL(逻辑与指令): 将两个被指定寻址的内容相对应的位分别做逻辑与运算,并将结果存入目的地址中。共有以下6种寻址格式: ANLA,Rn;A←(A)∧(Rn) ANLA,direct;A←(A)∧(direct) ANLA,@Ri;A←(A)∧((Ri)) ANLA,#data;A←(A)∧data ANLdirect,A;direct←(direct)∧(A) ANLdirect,#data;direct←(direct)∧data 逻辑与指令常用于清0字节的某些位。欲清0的位用“0”去“与”,欲保留的位用“1”去“与”。 ORL(逻辑或指令): 将两个被指定寻址的内容相对应的位分别做逻辑或运算,并将结果存入目的地址中。共有以下6种寻址格式: ORLA,Rn;A←(A)∨(Rn) ORLA,direct;A←(A)∨(direct) ORLA,@Ri;A←(A)∨((Ri)) ORLA,#data;A←(A)∨data ORLdirect,A;direct←(direct)∨(A) ORLdirect,#data;direct←(direct)∨data 逻辑或指令常用于置1字节的某些位。欲置1的位用“1”去“或”,欲保留的位用“0”去“或”。 XRL(逻辑异或指令): 将两个被指定寻址的内容相对应的位分别做逻辑异或运算,并将结果存入目的地址中。共有以下6种寻址格式: XRLA,Rn;A←(A)(Rn) XRLA,direct;A←(A)(direct) XRLA,@Ri;A←(A)((Ri)) XRLA,#data;A←(A)data XRLdirect,A;direct←(direct)(A) XRLdirect,#data;direct←(direct)data 逻辑异或指令常用于去取反字节的某些位。欲取反的位用“1”去“异或”,欲保留的位用“0”去“异或”。 CLR(累加器清0指令): 将累加器A内容清除为0。指令格式: CLRA;A←0 CPL(累加器取反指令): 将累加器A内容每位求反。指令格式: CPLA;A←(A)的每位求反,即0→1,1→0 RL(累加器左移指令): 将累加器A的内容左环移一位。指令格式: RLA; RLC(累加器带进位左移指令): 将累加器A的内容与进位左环移一位。指令格式: RLCA; RR(累加器右移指令): 将累加器A的内容右环移一位。指令格式: RRA; RRC(累加器带进位右移指令): 将累加器A的内容与进位右环移一位。指令格式: RRCA; SWAP(4位互换指令): 将累加器A的高4位与低4位互换。指令格式: SWAPA; 4. 布尔运算指令 8051的布尔运算指令是比较特殊的一种运算,它可针对可位寻址的内部RAM、寄存器进行操作。进位标志CY就是这种操作运算的累加器。布尔运算指令包括清除、置位、取反、与、或及位传送等指令,共有6种。分别叙述如下。 CLR(清除): 可清除CY标志或任一可位寻址的位。指令格式: CLRC;CY←0 CLRbit;bit←0 SETB(置位): 可设定CY标志或任一可位寻址的位,使其为1。指令格式: SETBC;CY←1 SETBbit;bit←1 CPL(取反): 可取反CY标志或任一可位寻址的位,使其1变0、0变1。指令格式: CPLC;CY←(CY)的反,即0→1,1→0 CPLbit;bit←(bit)的反,即0→1,1→0 ANL(与): 两个位作逻辑与。指令格式: ANLC,bit;CY←(CY)∧(bit) ANLC,/bit;CY←(CY)∧(bit)的反 ORL(或): 两个位作逻辑或。 ORLC,bit;CY←(CY)∨(bit) ORLC,/bit;CY←(CY)∨(bit)的反 MOV(位传送指令): 两个操作数中要有一个为CY标志,另一个为可位寻址的直接位地址。指令格式: MOVC,bit;CY←(bit) MOVbit,C;bit←(CY) 5. 程序分支指令 单片机在执行程序时,程序指令一般是依顺序执行的,而程序分支指令则是用来改变程序指令执行的顺序,使程序设计更加方便。程序分支指令有无条件跳转、有条件跳转、调用子程序、子程序返回、中断返回等指令。分别叙述如下。 JC(有进位跳): 判断CY标志等于1然后跳转。指令格式: JCrel;若CY=1则PC←(PC)+2+rel ;若CY=0则PC←(PC)+2 JNC(无进位跳): 判断CY标志等于0然后跳转。指令格式: JNCrel;若CY=0则PC←(PC)+2+rel ;若CY=1则PC←(PC)+2 JB(位1跳): 判断给出位等于1然后跳转。给出的位应该是可位寻址。指令格式: JBbit,rel;若bit=1则PC←(PC)+3+rel ;若bit=0则PC←(PC)+3 JNB(位0跳): 判断给出位等于0然后跳转。给出的位应该是可位寻址。指令格式: JNBbit,rel;若bit=0则PC←(PC)+3+rel ;若bit=1则PC←(PC)+3 JBC(位1跳并清除): 判断给出位等于1然后跳转,并清除此位为0。给出的位应该是可位寻址。指令格式: JBCbit,rel;若bit=1则PC←(PC)+3+rel 且bit←0 ;若bit=0则PC←(PC)+3 JZ(A为0跳): 判断累加器等于0,然后跳转。指令格式: JZrel;若(A)=0则PC←(PC)+2+rel ;若(A)≠0则PC←(PC)+2 JNZ(A为非0跳): 判断累加器不等于0,然后跳转。指令格式: JNZrel;若(A)≠0则PC←(PC)+2+rel ;若(A)=0则PC←(PC)+2 CJNE(比较不相等跳): 比较两个操作数,若不相等就跳转。共有4种寻址格式: CJNEA,direct,rel;若(A)≠(direct)则PC←(PC)+3+rel,CY按规则形成 ;若(A)=(direct)则PC←(PC)+3,CY=0 CJNEA,#data,rel;若(A)≠ data则PC←(PC)+3+rel,CY按规则形成 ;若(A)= data则PC←(PC)+3,CY=0 CJNERn,#data,rel;若(Rn)≠ data则PC←(PC)+3+rel,CY按规则形成 ;若(Rn)= data则PC←(PC)+3,CY=0 CJNE@Rn,#data,rel;若((Rn))≠ data则PC←(PC)+3+rel,CY按规则形成 ;若((Rn))= data则PC←(PC)+3,CY=0 DJNZ(减1不为0跳): 若寻址内容不等于0则跳转。指令格式: DJNZRn,rel;Rn←(Rn)-1 ;若(Rn)≠ 0则PC←(PC)+2+rel ;若(Rn)= 0则PC←(PC)+2 DJNZdirect,rel;direct←(direct)-1 ;若(direct)≠ 0则PC←(PC)+3+rel ;若(direct)= 0则PC←(PC)+3 LJMP(长跳转指令): 64KB内存范围内无条件跳转。指令格式: LJMPaddr16;PC← addr16 AJMP(短跳转指令): 2KB内存范围内无条件跳转。指令格式: AJMPaddr11;PC←(PC)+2,PC10~0← addr11 SJMP(相对跳转指令): 在此指令前128B或后128B范围内跳转。指令格式: SJMPrel;PC←(PC)+2,PC←(PC)+rel JMP(散转指令): 跳转至@A+DPTR所指定的地址。指令格式: JMP@A+DPTR;PC←(A)+(DPTR) LCALL(长调用指令): 64KB内存范围内子程序调用。指令格式: LCALLaddr16;PC←(PC)+3 ;SP←(SP)+1 ,(SP)←PC7~0 ;SP←(SP)+1 ,(SP)←PC15~8 ;PC← addr16 ACALL(短调用指令): 2KB内存范围内子程序调用。指令格式: ACALLaddr16;PC←(PC)+2 ;SP←(SP)+1 ,(SP)←PC7~0 ;SP←(SP)+1 ,(SP)←PC15~8 ;PC10~0← addr11 RET(子程序返回): 指令格式: RET;PC15~8←((SP)),SP←(SP)-1 ;PC7~0←((SP)),SP←(SP)-1 RETI(中断返回): 指令格式: RETI;PC15~8←((SP)),SP←(SP)-1 ;PC7~0←((SP)),SP←(SP)-1 ;清除相应中断优先级状态位 最后还有一条空操作指令: NOP;PC←(PC)+1 该指令仅产生一个机器周期的延时,不进行任何操作。 3.3汇 编 语 言 3.3.1伪指令 汇编语言也称符号语言,是使用助记符表示的机器指令进行编程的一种语言。通常把用汇编语言编写的程序称为汇编语言源程序,但机器不能直接识别和执行汇编语言源程序,必须将其翻译成机器语言程序(目标程序),计算机才能执行。这个翻译过程称为汇编。汇编有手工汇编和机器汇编两种方式。手工汇编是通过人工查找指令代码表,得到每条指令的机器代码; 机器汇编是通过计算机执行一种系统软件(汇编程序)自动完成的。 当使用机器汇编时,必须在源程序中为汇编程序提供一些辅助信息,以便帮助其完成源程序的翻译并生成目标代码,如源程序中哪些是指令、哪些是数据,数据是字节还是字,代码存放的目的地址在哪里以及程序翻译到哪里结束等。这些为汇编程序提供必要的辅助信息,控制其汇编过程的命令称为伪指令。这里要注意,伪指令不是控制计算机执行某种操作的指令,仅仅是在机器汇编时为汇编程序提供必要的信息。因此,汇编时伪指令并不产生供机器直接执行的机器码,也不会直接影响存储器中代码和数据的分布。 不同的51汇编程序对伪指令的定义有所不同,但基本的用法是相似的,下面介绍一些常用的伪指令及其基本用法。 1. 定位伪指令ORG 格式: ORGm 其中,m一般为以十进制或十六进制数表示的16位地址,用来指定该伪指令后面的指令的汇编地址,即生成机器指令的起始存储地址,也可以用来指定其后的数据定义伪指令所定义的数据的起始存储地址。 在一个汇编语言源程序中允许使用多条定位伪指令,但其值不应和前面生成的机器指令存放地址重叠。在实际应用中,一般仅设置中断服务子程序和主程序的起始存放地址,其他程序或常数依次存放即可。 例3.1定位伪指令的用法举例。 ORG0000H START: AJMPMAIN ︙ ORG0100H MAIN: MOVSP,# 30H ︙ 以START开始的程序汇编为机器代码后从0000H单元开始存放,以MAIN开始的程序机器代码则从0100H单元开始连续存放。 从前面的介绍可知,单片机复位后程序计数器PC的值为0000H,即从地址0开始执行程序,所以汇编语言源程序一般都以伪指令ORG 0000H开始。如果在源程序开始处没有ORG伪指令,则汇编程序将从自动从0000H单元开始存放目标程序。但是我们还知道,程序存储器的0003H、000BH、0013H等单元为中断服务程序的入口地址,一般不应该被覆盖,否则相应的中断功能无法实现,所以可以在程序存储器的0000H处安排一条转移指令,将主程序转移到程序存储器的其他地方,本例中将程序的真正入口设置在程序存储器的0100H处。 2. 汇编结束伪指令END 格式: END END是汇编语言源程序的结束标志,表示汇编结束。机器汇编时遇到END就认为源程序已经结束,对END后面的指令都不再汇编。因此一个源程序只能有一个END伪指令,并且必须放在汇编语言源程序的末尾。 3. 定义字节伪指令DB 格式: [标号:] DBx1, x2, …, xn 定义字节伪指令DB(Define Byte)将其右边的数据依次存放到以左边标号为起始地址的存储单元中,xi为字节数据,可以采用二进制、十进制、十六进制和ASCII码等多种表示形式。DB通常用于定义一个常数表。 例3.2定义字节伪指令的用法举例。 ORG7F00H TAB: DB01110010B,16H,45 DB'8','MCS-51' 汇编后存储单元内容为: (7F00H)= 72H(7F01H)= 16H(7F02H)= 2DH(7F03H)= 38H (7F04H)= 4DH(7F05H)= 43H(7F06H)= 53H(7F07H)= 2DH (7F08H)= 35H(7F09H)= 31H 4. 定义字伪指令DW 格式: [标号:] DWY1,Y2,…,Yn 定义字伪指令DW(Define Word)的功能与DB类似,但DW定义的是一个字(2字节),主要用于定义16位地址表。汇编时,机器自动按高8位在前(低地址)、低8位在后(高地址)的格式存入存储器,这与80x86系列的微处理器正好相反。 例3.3定义字伪指令的用法举例。 ORG6000H TAB: DW1254H,32H,161 DW'AB',TAB 汇编后存储单元内容为: (6000H)= 12H(6001H)= 54H(6002H)= 00H(6003H)= 32H (6004H)= 00H(6005H)= 0A1H(6006H)= 41H(6007H)= 42H (6008H)= 60H(6009H)= 00H 伪指令DB和DW均是根据源程序的需要,用来定义程序中用到的数据(地址)或数据块的,一般应放在源程序之后,汇编后的数据将紧挨着目标程序的末尾地址开始存放。 5. 定义预留存储空间伪指令DS 格式: [标号:] DS数值表达式 定义预留存储空间伪指令DS从指定的地址开始,保留若干字节的内存空间作为备用。汇编时,将根据表达式的值决定从指定地址开始留出多少字节的存储空间,表达式也可以是一个指定的数值。 例3.4定义预留存储空间伪指令的用法举例。 ORG0F00H DS10H DB20H,40H 汇编后,从0F00H开始,保留16(10H)字节的内存单元,然后从0F10H开始,按照下一条DB伪指令给内存单元赋值,即(0F10H)=20H,(0F11H)=40H。保留的空间将由程序的其他部分决定其用处。 DB、DW和DS伪指令都只对程序存储器起作用,不能用来对数据存储器的内容进行赋值或进行其他初始化工作。 6. 等值伪指令EQU 格式: 字符名称EQU数据或汇编符号 等值伪指令EQU将其右边的数据或汇编符赋给左边的字符名称。“字符名称”被赋值后,在程序中就可以作为一个8位或16位的数据、地址或汇编符来使用了。 使用EQU伪指令时应注意,字符名称必须先定义后使用,通常将等值伪指令放在源程序的开头; 在同一程序中,用EQU伪指令对标号赋值后,该标号的值在整个程序中不能再改变,即不能用EQU对同一个标号进行两次或两次以上的赋值。 例3.5等值伪指令的用法举例。 ORG8500H AAEQUR1 A10EQU10H DELAYEQU87E6H MOV R0,A10;R0←(10H) MOV A,AA ;A←(R1) LCALL DELAY ;调用起始地址为87E6H的子程序 END EQU赋值后,AA为寄存器R1,A10为8位直接地址10H,DELAY子程序的入口地址为87E6H。 7. 数据地址赋值伪指令DATA 格式: 字符名称DATA表达式 数据地址赋值伪指令DATA的功能与EQU类似,是将其右边“表达式”的值赋给左边的“字符名称”。表达式可以是一个8位或16位的数据或地址,也可以是包含已定义“字符名称”在内的表达式,但不可以是一个汇编符号(如R0、R7等)。 DATA伪指令定义的“字符名称”没有先定义后使用的限制,可以放在源程序的开头或末尾。 8. 位地址定义伪指令BIT 格式: 字符名称BIT位地址 位地址定义伪指令BIT将其右边位地址赋给左边的字符名称。 例3.6位地址定义伪指令的用法举例。 A1BITACC.1 USER1BITPSW.5 USER2BIT20H 这样就把位地址ACC.1(累加器ACC的第1位)赋给了变量A1,把位地址PSW.5赋给了变量USER1,把位地址20H赋给了变量USER2,在编程中A1、USER1和USER2就可以作为位地址使用了。 3.3.2顺序程序设计 顺序程序是指执行顺序与源程序的书写顺序完全一致的一种程序结构,程序中不存在可引起程序流程发生改变的转移类指令。这种结构是所有结构中最简单、最基本的一种,也称为简单程序结构。 例3.7编写一个实现两个双字节无符号十进制数相加的程序。 设两个双字节无符号十进制数采用压缩的BCD码表示,分别存放在片内RAM的40H、41H和50H、51H单元,结果仍为压缩的BCD码,存放在片内RAM的52H、51H和50H单元中。程序如下: DATA0EQU12H DATA1EQU34H DATA2 EQU56H DATA3EQU78H ORG0000H AJMP0100H ORG0100H MOV40H, #DATA0 MOV 41H, #DATA1;被加数送41H,40H MOV 50H, #DATA2 MOV51H, #DATA3 ;加数送51H,50H MOVA, 40H ADD A, 50H ;(40H)+(50H) →A DA A MOV 50H, A ;保存结果 MOV A, 41H ADDC A, 51H ;(41H)+(51H)+CY→A DA A MOV 51H, A ;保存结果 MOV 52H, #0 MOV A, #0 ADDC A, 52H MOV 52H, A ;进位52H LOOP0: SJMP LOOP0 END 程序中首先用EQU伪指令定义了4个常数(压缩的BCD码),这里要注意,常数后面的H一定不要漏掉,否则就不是BCD码了。如12H在计算机中的存储形式为00010010B,可以表示BCD码的12,而12在计算机中的存储形式为00001100B(0CH),就不是BCD码。接着用4条MOV指令把4个常数分别存放到相应的存储单元中。最后进行相加运算,进行相加运算时应注意: (1) 十进制的加法是在普通加法指令之后用DA A指令对结果进行调整实现的; (2) 低字节相加用ADD指令,高字节相加要用ADDC指令; (3) 两个双字节无符号十进制数的和有可能超出双字节的表示范围,要用3字节存储,第3字节为两数相加产生的进位,即0或1。 本例实现的是十进制数3412和7856相加,结果为11268。 程序的最后一条指令LOOP0: SJMP LOOP0是让程序原地踏步,不再往前执行。这条指令也可以用SJMP $代替,$在指令中表示本条指令的地址。 3.3.3分支程序设计 分支程序对程序中给定的条件进行判断,然后根据条件的成立与否决定程序的走向。 例3.8编写计算函数Y=f(X)的程序。 Y=2,X>0 0,X=0 -2,X<0 该函数有3个分支,要做两次判断: 第1次将X取到累加器中并用JZ指令判断其是否为0; 第2次用位条件转移指令判断X>0还是X<0。设X和Y分别存放在片内RAM的30H和31H单元,程序如下: $INCLUDE(C8051F020.INC);包含文件 XEQU30H YEQU31H ORG0000H AJMP0100H ORG0100H MOVA, X JZDONE ;若X=0,转DONE JNB ACC.7, POSI ;若X>0,则转POSI MOV A, #0FEH ;若X<0,则A=-2(补码为0FEH) LJMP DONE POSI: MOV A, #02H ;保存结果 DONE: MOV Y, A SJMP $ END .INC包含文件类似于C语言中的.h头文件,C8051F020.INC中有对特殊功能寄存器的定义,如开头不加上语句$INCLUDE(C8051F020.INC),汇编时因为找不到累加器ACC的定义而报ACC.7是非法位地址的错误信息。文件C8051F020.INC要和源文件放在同一个文件夹内。如果程序中只用到个别特殊功能寄存器,或者C8051F020.INC缺少所用特殊功能寄存器的定义,也可以在源程序中用EQU(或DATA)伪指令直接定义相关特殊功能寄存器,如本例中也可以用ACC EQU 0E0H语句替换$INCLUDE(C8051F020.INC)语句。 3.3.4循环程序设计 1. 循环程序的结构 顺序结构和分支结构中的指令一般只执行一次。而在一些实际应用系统中,往往同一组操作需要重复执行多次,这种有规律可循又需反复处理的操作可采用循环结构的程序来实现。这样结构可使程序简短,占用内存少,重复次数越多,运行效率越高。 循环程序一般包括以下几个部分: (1) 初始化部分。程序在进入循环之前,应对各循环变量、其他变量和常量赋初值,为循环操作做必要的准备工作。 (2) 循环体部分。这一部分由重复执行部分和循环控制部分组成。这是循环程序的主体,又称为循环体。值得注意的是,每执行一次循环体后,必须为下一次循环创造条件。如对数据地址指针、循环计数器等循环变量进行修改,还要检查判断循环条件, 若符合循环条件,则继续重复循环,不符合则退出循环,以实现对循环的判断与控制。 (3) 结束部分。用于保存和分析循环程序的处理结果。 循环程序设计的一个主要问题是循环次数的控制,一般有两种控制方法: 第一种方法是先判断再处理,即先判断是否满足循环条件,如不满足,则不循环。这种结构的循环也称为当型循环,其流程图如图31(a)所示。第二种方法是先处理再判断,即循环先执行一次后,再判断是否还需要下一次循环。这种结构的循环也称为直到型循环,其流程图如图31(b)所示。 图31两种循环结构的流程图 例3.9片外RAM的BLOCK单元开始有一个无符号数据块,数据块长度存放在片内RAM的LEN单元中,编程找出数据块中的最大数存入片内RAM的MAX单元。 这是基本搜索问题。可以采用两两比较的方法,取两者较大的数再与下一个数进行比较,若数据块长度(LEN)=n,则应比较n-1次,最后较大的数就是数据块中的最大数。 程序中使用减法指令和借位标志CY来判断两数的大小,用B寄存器作比较与交换的暂存寄存器,使用DPTR作外部RAM的地址指针。流程图如图32所示,程序如下: 图32例3.9的程序流程图 $INCLUDE(C8051F020.INC) ;包含文件 BLOCKDATA 0100H;定义数据块首址 MAXDATA 31H ;定义最大数存储单元 LEN DATA 30H ;定义长度计数单元 ORG 0000H AJMP FMAX ORG 0100H FMAX: MOV DPTR, #BLOCK ;数据块首址送DPTR DEC LEN ;长度减1 MOVX A, @DPTR ;取数至A LOOP: CLRC ;0→CY MOV B, A ;暂存于B INC DPTR ;修改指针 MOVXA, @DPTR ;取数 SUBB A, B ;比较 JNC NEXT MOV A, B ;大者送A SJMP NEXT1 NEXT: ADD A, B ;(A)>(B),则恢复A NEXT1: DJNZ LEN, LOOP ;未完继续比较 MOV MAX, A ;存最大数 SJMP $ ;程序踏步,若用RET指 ;令结尾则可作为子程 ;序调用 END 程序中因为用减法指令SUBB A, B比较两数的大小,执行完后A中为两数的差,所以当A中的数据大于B中的数据时,需要对A中的数据用ADD A,B指令进行恢复。数据的比较也可以用CJNE指令实现,该指令可同时完成数据的比较与不相等时的转移功能,效率更高,作为练习请读者自己完成。 例3.10片外RAM的BLOCK单元开始有一个带符号数据块,数据块的长度存放在片内RAM的LEN单元。试编写程序统计其中正数、负数和零的个数,分别存入片内RAM的PCOUNT、MCOUNT和ZCOUNT单元。 这是一个包含多重分支的单循环程序。数据块中的数据是用补码表示的带符号数,因而首先用JB ACC.7,REL指令判断其符号位。若ACC.7=1,则该数一定是负数,MCOUNT单元加1; 若ACC.7=0,则该数可能为正数,也可能为零,再用JNZ REL指令进一步判断之,若A≠0,则一定是正数,PCOUNT加1; 否则该数为零,ZCOUNT加1。当数据块中的所有数据都按顺序判断一次之后,PCOUNT、MCOUNT和ZCOUNT单元中就分别对应正数、负数和零的个数。流程图如图33所示,程序如下: 图33例3.10的程序流程图 $INCLUDE(C8051F020.INC) BLOCKDATA 2000H;定义数据块首址 LENDATA 30H ;定义长度计数单元 PCOUNTDATA 31H ;正数计数单元 MCOUNTDATA 32H ;负数计数单元 ZCOUNTDATA 33H ;零计数单元 ORG0000H AJMPSTART ORG 0100H START: MOVDPTR, #BLOCK MOVPCOUNT, #0 ;计数单元清0 MOV MCOUNT, #0 MOV ZCOUNT, #0 LOOP:MOVXA, @DPTR ;取数 JBACC.7, MCONT ;若ACC.7=1,转负计数 JNZPCONT ;若(A)≠0,转正计数 INCZCOUNT ;若(A)=0,则零的个数加l AJMPNEXT MCONT: INCMCOUNT ;负数计数单元加1 AJMPNEXT PCONT: INC PCOUNT ;正计数单元加1 NEXT: INC DPTR ;修正指针 DJNZ LEN, LOOP ;未完继续 SJMP $ END 2. 多重循环 在前面介绍的两个例子中,程序中都只包含一个循环,这种程序称为单重循环程序。而遇到复杂问题时,采用单重循环往往不够,必须采用多重循环才能解决。所谓多重循环(也称循环嵌套),就是在循环程序中还套有其他的循环程序。应注意,在多重循环中内外循环不能交叉,也不允许从外循环跳入到内循环,只能将内循环完整地包含在外循环当中。图34(a)、图34(b)所示是正确的循环嵌套形式,图34(c)是应避免的不正确的循环嵌套形式。下面通过两个实例说明多重循环程序的设计方法。 图34多重循环的嵌套形式 例3.11片外RAM的BLOCK开始的单元中有一个无符号数据块,其长度存放在片内RAM的LEN单元中(为方便编程,假设长度<256)。编程将这些无符号数按递增顺序重新排列,并存入原存储区。 处理这个问题要利用双重循环结构,在内循环中将相邻两单元的数进行比较,若符合从小到大的次序则不做任何操作,否则应将两数交换。这样两两比较下去,n-1次比较之后所有的数都比较并交换完毕,最大数沉底,在下一个内循环中将减少一次比较与交换。为提高程序的执行效率,进行下一次内循环前, 先检查上次内循环中有无数据交换发生,若从未交换过,则说明这些数据已按递增顺序排列,程序可提前结束(未执行完的外循环次数不需要再执行); 否则将再进行下一次内循环,如此反复比较与交换,每次内循环的最大数都沉底,而较小的数一个个冒上来,因此这种排序方法称为“冒泡排序”。 图35例3.11的程序流程图 程序中用R0、R1存放相邻两单元地址的低字节,采用8位MOVX指令访问XRAM中的数据,此时需要由外部存储器接口控制寄存器EMIOCN提供地址的高字节。因为数据的个数小于256,所以整个程序中EMIOCN的值不需要改变。用R7、R6作为外循环与内循环的循环计数器; 用程序状态字PSW中的用户标志位F0作为交换标志位,内循环中有交换发生时则将F0置1,进入外循环时将F0清0。因为包含文件C8051F020.INC中没有EMIOCN的定义,所以需要用DATA或EQU伪指令定义EMIOCN的地址,也可以通过修改C8051F020.INC文件,加入EMIOCN的定义。 流程图如图35所示,程序如下: $INCLUDE(C8051F020.INC) BLOCKDATA 2200H LENDATA 51H TEM DATA 50H EMIOCN DATA 0AFH;外部存储器接口控制寄存器的地址 ORG 0000H AJMP START ORG 0100H START: MOV DPTR, #BLOCK ;置数据块地址指针 MOV EMIOCN, DPH ;EMIOCN存放地址的高字节 MOV R7, LEN ;置外循环计数初值 DEC R7 ;外循环最多执行n-1次 LOOP0: CLR F0 ;交换标志清0 MOV R0, DPL MOV R1,DPL ;置相邻两数地址指针低字节 INC R1 MOV A, R7 MOV R6, A ;置内循环计数器初值 LOOP1: MOVX A, @R0 ;取数 MOV TEM, A ;暂存 MOVX A, @ R1 ;取下一个数 CJNE A, TEM, NEXT ;两相邻数比较,不等则转 SJMP NOCHA ;相等不交换 NEXT: JNC NOCHA ;CY=0,不交换 SETB F0 ;置位交换标志 MOVX @R0, A XCH A, TEM MOVX @R1, A ;两数交换 NOCHA: INC R0 INC R1 ;修改指针 DJNZ R6, LOOP1 ;内循环未完,则继续 JNB F0, HALT ;若上次内循环中没有交换,则结束 DJNZ R7, LOOP0 ;未完,继续 HALT: SJMP $ END 例3.12设系统时钟频率为20MHz,试编写延时2ms的延时子程序。 软件延时是靠处理器执行一段程序达到的,通常称为延时子程序。延时子程序通常采用双重或多重循环结构。在系统时钟频率确定之后,延时时间主要与两个因素有关: 其一是循环体(内循环)中指令的执行时间; 其二是外循环变量(时间常数)的设置。 系统时钟频率为20MHz,则一个时钟周期为0.05μs,执行一条DJNZ Rn,rel指令需2个时钟周期,即0.1μs。 延时2ms的子程序如下: DELAY: MOV R7, #200 ;1个时钟周期 DLY0: MOV R6, #100 ;1个时钟周期 DLY1: DJNZ R6, DLY1 ;200×(100×2×0.05)=200×10μs=2ms DJNZ R7, DLY0 ;2个时钟周期 RET ;2个时钟周期 程序的注释部分给出了延时时间的计算,但这种计算不太精确,只计算了内循环一条指令(程序中的第3条指令)的执行时间,没有把其他指令的执行时间计算进去。若把所有指令的执行时间计算在内,则延时时间为 (10μs+0.15μs)×200+0.15μs=2030.15μs=2.03015ms 其中,括号内的10μs为内循环执行时间,0.15μs为第2、4条指令的执行时间,两者之和乘以200为外循环执行时间,括号外的0.15μs为第1、5条指令的执行时间。 如果要求延时时间比较精确,可通过修改循环次数和在循环体中增加NOP指令的方式实现。本例程序可修改如下: DELAY: MOVR7, #200 DLY0: MOVR6, #98 NOP DLY1: DJNZR6, DLY1 ;98×2×0.05=9.8μs DJNZR7, DLY0 RET 此程序的实际延时时间为 (9.8μs +0.2μs)×200+0.15μs =2000.15μs =2.00015ms 如果需要延时更长时间,可以采用多重循环实现。注意,用软件实现延时时,应禁止中断,否则延时过程中若有中断发生,则会严重影响延时时间的准确性。 3.3.5子程序设计 在大型程序或是多个不同的程序中,往往有许多地方需要执行同样的运算和操作。例如,求三角函数和各种加减乘除运算、代码转换以及延时程序等。如果编程过程中每遇到这样的操作都单独编写一段程序,会使编程工作十分烦琐,也会占用大量程序存储空间。通常将这些能完成某种基本操作的程序段单独编制成子程序,以供不同程序或同一程序的不同地方反复调用。在程序中需要执行这种操作的地方执行一条调用指令,转到子程序中完成规定操作后再返回到原来的程序中继续执行。这就是所谓的子程序结构。 为使子程序能在不同程序或同一程序中反复被调用,子程序应具备以下特征: (1) 通用性。子程序应设计成能由各种应用程序调用的通用程序,这主要是通过可变参数实现的。 (2) 可浮动性。子程序可以不加任何修改地存放在存储器的任何区域。这要求在子程序中应避免使用绝对转移指令,子程序的首地址应该使用符号地址(即子程序名)。 (3) 可递归性和可重入性。可递归性是指子程序可以自己调用自己,可重入性是指一个子程序可以同时被多个程序调用。这两个特性主要是对大规模复杂系统程序的要求,对一般程序可以不做要求。 1. 子程序的设计要点 1) 子程序的结构 用汇编语言编制子程序时,要注意以下两个问题: (1) 子程序的第一条指令必须有一个简单明了、见名识义的标号,此标号即为子程序名,代表该子程序的入口地址。在主程序中使用短调用指令ACALL或长调用指令LCALL和子程序名即可调用子程序。例如调用延时子程序可用: LCALLDELAY 或 ACALLDELAY 其中,DELAY就是子程序名,即延时子程序第一条指令的标号。这两条调用指令属于程序分支(转子)指令,不仅具有寻址子程序入口地址的功能,而且在转入子程序之前,硬件能自动将主程序的断点(调用指令的下一条指令地址)入栈保存,以使返回指令RET能正确返回。 (2) 子程序结尾必须有一条子程序返回指令RET。该指令具有恢复主程序断点的功能,即从堆栈弹出断点至程序计数器PC,以便继续执行主程序。一般来说,子程序调用指令和子程序返回指令要成对使用。注意,中断服务子程序的返回指令是RETI,该指令除返回断点外,还会清除相应中断优先级状态位。 2) 参数传递 要使子程序具备通用性,子程序内部就不能使用固定的数据完成相关计算,子程序中使用的数据一般要由主程序以参数的形式提供,子程序的计算结果也应以参数的形式传给主程序。在调用一个子程序时,主程序应先把有关参数(也称入口条件)放到某些约定的位置,子程序在运行时,可以从约定的位置得到有关参数。同样在子程序结束前,也应把处理结果(也称出口条件)送到约定位置。返回后,主程序便可从这些位置中得到需要的结果,这就是参数传递。参数传递的方法有多种,下面将结合具体例子介绍。 3) 现场保护和恢复 进入子程序后,特别是进入中断服务子程序时,要特别注意现场保护和恢复问题。现场保护是指主程序中使用的RAM内容、各工作寄存器的内容、累加器A的内容和DPTR以及PSW等寄存器内容,都不应因转入子程序而改变。如果子程序所使用的寄存器和存储单元与主程序中使用的寄存器和存储单元有冲突,则在转入子程序后首先要采取保护措施。现场保护的一般方法是将要保护的单元和寄存器的内容压入堆栈,从而空出这些单元和寄存器供子程序使用。子程序返回主程序之前再将这些内容弹出到原工作单元,恢复主程序原来的状态,此即现场恢复。压栈与出栈应按相反的顺序进行,这样才能保证现场的正确恢复。例如: 子程序名: PUSHACC;保护现场 PUSHPSW PUSHDPL PUSHDPH … ;子程序体 POPDPH ;恢复现场 POPDPL POPPSW POPACC RET ;子程序返回 对于一个具体的子程序是否要进行现场保护,以及哪些寄存器和存储单元需要保护,要视具体情况而定,不能一概而论。 通过前面的学习,我们知道工作寄存器有4个区,每区8个(R0~R7),每个程序一般只使用其中的一个区。那么对工作寄存器的保护就可以通过简单的寄存器区切换来实现。如,通过以下两条指令就可以将工作寄存器切换到第2个区。 SETRS1 CLRRS0 4) 堆栈设置 调用子程序时,主程序的断点将自动入栈; 转入子程序后,现场的保护也要占用堆栈单元,尤其是多重子程序嵌套调用,要求堆栈有更多的空间。因此,恰当地设置堆栈指针SP的初值是十分重要的。 C8051F系列单片机堆栈指针SP的复位值为07H,程序中可将其设置在内部RAM的任意单元,但考虑00H~1FH为工作寄存器区,20H~2FH为可位寻址的区域,因此,一般将SP设置在30H以上的单元。 2. 参数传递方法 1) 无须参数传递 这类子程序所需的参数是由子程序本身赋予的,不需要主程序给出。例如,例3.12的最后一句为RET指令,所以该例是子程序的结构形式。但该子程序延时2ms所需要的参数(内、外循环的次数)完全是在子程序内部直接赋值的,调用时只需在主程序中适当位置写入LCALL DELAY或ACALL DELAY指令即可。 2) 用累加器和工作寄存器传递参数 这种方法要求在转入子程序之前把所需的入口参数存入累加器A和工作寄存器R0~R7中。在子程序中对这些数据进行相关操作,返回时,出口参数也保存在累加器和工作寄存器中。这种参数传递方法最直接、最简单,运算速度也最快。但由于工作寄存器的数量有限,不能传递更多的参数。 例3.13编写计算c=a2+b2的程序,设a、b均小于10。a、b分别存放在片内RAM的31H、32H单元,结果c存入片内RAM的34H和33H单元(要求和为BCD码)。 因该算式两次用到平方值,所以可将求平方运算编写为子程序,主程序中两次调用,再求和即可。求平方值采用查表法实现,主程序和子程序编写如下: 主程序: $INCLUDE(C8051F020.INC) ORG0000H AJMPSTART ORG0100H START:MOV SP, #3FH MOV A, 31H ;取a LCALL SQR ;求a2 MOV R1, A MOV A, 32H ;取b LCALLSQR ;求b2 ADD A, R1 ;求和 DA A ;调整 MOV 33H, A MOV A, #0 ADDC A, #0 ;计算和高位 MOV 34H, A SJMP $ 子程序: ORG0030H SQR: INC A ;累加器A增加1B变址调整值 MOVC A, @A+PC RET ;1B TAB: DB 00H, 01H, 04H, 09H, 16H, 25H, 36H, 49H, 64H, 81H END 主程序和子程序之间使用累加器A传递参数。查表指令MOVC A,@A+PC使用A+PC作为地址访问程序存储器,取出其内容送给累加器A,执行该指令前,A中存放的是待查表项数(由主程序设定)。但执行MOVC A,@A+PC指令时,PC指向的是其下一条指令RET的首地址,而非表格首地址,因此要能正确查找到表格中的内容,必须使累加器A再加上一个变址调整值。这里的变址调整值即为MOVC A,@A+PC指令的下一条指令(RET指令)到表首的间隔,即两处地址之间其他指令所占字节数,这里仅RET一条指令,占1字节,所以使用INC A指令。每条指令所占的字节数都可以在附录A中查到。若查表指令与表首之间指令较多,也可以通过指令标号相减的方式让汇编程序自动完成计算,从而省去人工查表的麻烦,如本例的查表子程序也可以按如下方式编写。 SQR:ADD A, #(TAB-XX) ;累加器A变址调整 MOVCA, @A+PC XX:RET TAB:DB00H, 01H, 04H, 09H, 16H, 25H, 36H, 49H, 64H, 81H END 用PC内容作基址查表只能查距本指令256B以内的表格数据,称页内查表指令或短查表指令。查表子程序也可以用指令MOVC A, @A+DPTR实现,只要让DPTR指向表首,A中存放待查数据即可,该指令可在64KB程序存储器范围内查表,称为长查表指令。本例用MOVC A, @A+DPTR实现的查表子程序如下: ORG0030H SQR:MOVDPTR, #TAB MOVCA,@A+DPTR RET TAB:DB00H, 01H, 04H, 09H, 16H, 25H, 36H, 49H, 64H, 81H END 3) 通过操作数地址传递参数 该方法中主程序将子程序所需的操作数存入数据存储器中,调用子程序之前将操作数的地址作为入口参数存入R0、R1或DPTR中。子程序以R0、R1或DPTR间接寻址访问数据存储器即可取出所需数据,结束前将结果仍存入数据存储器中,并将其地址作为出口参数存入R0、R1或DPTR中。主程序再以R0、R1或DPTR间接寻址访问数据存储器即可取得运算结果。一般内部RAM由R0、R1作地址指针,外部RAM由DPTR作地址指针。这种参数传递方法可以节省传递数据的工作量,可实现变字长运算。 例3.14n字节求补子程序。 入口参数: (R0)=待求补数低字节指针,(R7)=n-1 出口参数: (R0)=求补后的高字节指针 求补运算就是对数据(含符号位)变反加1,程序如下: CPLN:MOVA, @R0 CPLA ;最低字节取反 ADDA, #1 ;加1 MOV@R0, A NEXT:INCR0 MOV A, @R0 CPL A ;高字节取反 ADDC A, #0 ;传递进位 MOV@R0, A DJNZ R7, NEXT RET 4) 通过堆栈传递参数 堆栈可用于参数传递,在调用子程序前,主程序先把参与运算的操作数用PUSH指令压入堆栈。转入子程序后,用POP指令取出操作数进行相应运算,并把运算结果压入堆栈。返回主程序后,可用POP指令获取运算结果。值得注意的是,转向子程序时,主程序的返回地址也要压入堆栈,占用堆栈2字节,弹出参数时要用两条DEC SP指令修改SP指针,以便使SP指向操作数。另外在子程序返回指令RET之前要增加两条INC SP指令,以便使SP指向返回地址,保证能正确返回主程序。 例3.15在片内RAM的HEX单元存放两个十六进制数,编程将它们分别转换为ASCII码并存入片内RAM的ASC和ASC+1单元。 由于要进行两次转换,故可调用查表子程序完成。 主程序: $INCLUDE(C8051F020.INC) ORG0000H HEXDATA 20H ASCDATA 30H AJMPMAIN ORG 0100H MAIN:MOVSP, #3FH PUSHHEX ;取被转换数 LCALLHASC ;调用子程序 *PC→ POPASC ;ASCL→ASC MOVA, HEX ;取被转换数 SWAPA ;处理高4位 PUSHACC LCALLHASC ;调用子程序 POP ASC+1 ;ASCH→ASC+1 AJMP $ 子程序: HASC:DECSP ;修改SP指向HEX DECSP POPACC ;弹出HEX ANLA, #0FH ;屏蔽高4位 ADD A, #7 ;变址调整 MOVCA, @A+PC;查表 PUSHACC ;结果入栈 (2B) INC SP ;修改SP指向断点位置(2B) INC SP ;(2B) RET ;(1B) ASCTAB:DB '0123456789ACBDEF' END 在主程序中将入口参数HEX入栈,即HEX被推入堆栈的40H单元,当执行LCALL HASC指令之后,主程序的返回地址PC也被压入堆栈,即*PCL被推入41H单元,*PCH被推入42H单元,此时SP=42H,如图36(a)所示。进入子程序HASC后,两条DEC SP指令使SP指向参数HEX,如图36(b)所示,然后用POP指令将其弹出。查表变换的结果通过PUSH指令压入到原来HEX所在的堆栈单元。返回子程序前用两条INC指令使SP指向存放返回地址的单元处,如图36(c)所示,以便由RET指令正确返回。 图36例3.15堆栈变化 使用堆栈传递参数,方法简单,能传递大量参数,不必为特定参数分配存储单元。 3.4C51语言 目前的嵌入式系统硬件性能和软件规模都有了很大的提高,为了提高程序开发效率和程序质量,开发人员更多采用C语言进行嵌入式软件程序设计。 使用C语言有以下优点: C语言具有结构性和模块化特点,便于程序的阅读和维护。 C语言可移植性好,功能模块可以在不同项目中使用,从而减少了开发时间。 C语言程序设计更加简明、清晰,可以减少编程错误,从而提高开发效率。 C语言和微控制器是相对独立的,开发者无须详细了解微控制器的内部结构和处理过程,因此可以很快上手。 尽管C语言有以上优点,但有时必须采用C和汇编语言混合编程才能实现特定功能。在对实时响应时间有很严格要求的应用系统中,使用汇编语言是开发者的唯一选择。 C51语言是为8051单片机编程而设计的一种专用C语言,它完全兼容ANSI标准C语言规范,并针对8051单片机的体系结构的特点对ANSI C的关键字做了一些扩展。 3.4.1C51关键字 关键字(Key Word)是一种具有固定名称和特定含义的标识符,又称为保留字(Reserved Word)。用户自定义的标识符不能和关键字同名。 ANSI C语言定义了32个关键字,还为预处理功能保留了关键字。C51语言除了支持ANSI C标准的关键字以外,还增加了若干关键字,按照其功能划分如下。 (1) 存储器类型相关: 用来声明变量存储的内存区域(参见图25)。 code: 用来定义位于8051程序代码存储区的只读变量。 bdata: 用来定义位于8051可位寻址的内部数据存储区的变量。 data: 用来定义位于8051可直接寻址的内部数据存储区的变量。 idata: 用来定义位于8051可间接寻址的内部数据存储区的变量。 pdata: 用来定义位于分页寻址的8051外部数据存储区的变量,页大小一般为256B。 xdata: 用来定义位于8051外部数据存储区的变量,一般用于外部数据存储区不大于64KB的情况。 far: 用来定义位于8051外部数据存储区的变量,一般用于外部数据存储区大于64B的情况。 bit: 用来定义位于8051可位寻址的内部数据存储区的位变量。 _at_: 用来对变量进行存储器绝对空间地址定位。 (2) 特殊功能寄存器相关: 用来声明特殊功能寄存器。 sbit: 声明可位寻址的特殊功能寄存器的特殊功能位。 sfr: 声明8位的特殊功能寄存器。 sfr16: 声明16位的特殊功能寄存器。 (3) 存储模式相关: 用来声明没有显式指定存储类型的变量的存储区域。 small: 小模式,变量默认存放在内部数据存储区。 compact: 紧凑模式,变量默认存放在分页寻址的外部数据存储区。 large: 大模式,变量默认存放在外部数据存储区(该区域一般不大于64KB)。 (4) 函数相关: 用来声明函数的实现方法。 interrupt: 专门用于中断服务函数的定义与声明。 reentrant: 专门用于可重入函数的定义与声明。 using: 专门用于指定函数内部使用的8051的工作寄存器组。 (5) 其他。 alien: 用以声明与PL/M51兼容的函数。 _priority_: 规定RTX51或RTX51 Tiny的任务优先级。 _task_: 定义实时多任务函数。 3.4.2C51变量定义 1. 存储器类型 C51编译器允许在变量声明时使用存储器类型(Memory Type)来指定变量所希望占用的存储区域类型。C51中的存储器类型修饰符如表31所示。 表31C51中的存储器类型修饰符 类型存储器类型存 储 区 域区域大小对应的汇编语句描述 代码区code程序存储区64KBMOVC XX,@A+DPTR用来存储只读变量 内部 数据 存储区 bdata可位寻址的内部数据存储区16BMOV XX,ADDR还可使用位寻址来访问的区域 data直接寻址的内部数据存储区128BMOV XX,ADDR访问速度快(包含bdata区) idata间接寻址的内部数据区256BMOV XX,@Rn可访问整个内部数据区域(包含data区) 外部 数据 存储区 xdata外部数据存储区64KBMOVX XX,@DPTR使用DPTR来访问外部数据 far扩充的RAM和ROM使用用户定义的专用例程或特殊芯片专用指令来访问 pdata分页的外部数据存储区256BMOVX XX,@Rn利用R0,R1来访问分页存储的外部数据 注意: idata可用来访问整个内部数据存储区,即256B,并不是仅局限于内部数据存储区的后128B。 声明变量时可以说明变量的存储器类型,如下所示: char data var1;//内部数据区的字符类型变量 char code text[] = "Hello world!"; //程序区的只读字串变量 unsigned long xdata array[100];//外部数据区的无符号整型数组变量 float idata x,y,z; //间接寻址内部数据区的浮点变量 unsigned int pdata dimension;//分页寻址外部数据区的无符号整型变量 unsigned char xdata vector[10][4][4];//外部数据区的无符号字符类型的三维数组变量 char bdata flags; //可位寻址内部数据区的字符变量 说明: 声明变量时存储区修饰符和数据类型修饰符的位置可以互换,即“char data x;”和“data char x;”是完全等效的。本书从一致性考虑,使用前一种格式。 2. 存储模式 如果在变量定义时未显式声明变量的存储器类型,则该变量的存储器类型由程序的存储模式 (memory model) 来决定。常见的存储模式有以下3种。 1) 小模式 在小模式(small model)下,所有未显式声明存储器类型的变量,使用内部数据区来存放,即这种方式和用data进行显式声明一样。在这种存储模式下,变量的访问是最有效的。但是所有的数据对象(包括堆栈)都必须放在内部数据存储区中,可使用的数据存储区最少。 2) 紧凑模式 在紧凑模式(compact model)下,所有未显式声明存储器类型的变量都使用分页寻址外部数据区来存放,即这种方式和用pdata显式声明一样。该模式利用R0和R1寄存器来进行间接寻址(@R0,@R1),此时最大可寻址256B的存储区域。这种方式的存取速度比小模式慢,但比大模式快。 使用紧凑模式时,C51编译器使用@R0和@R1来访问外部数据区。R0和R1寄存器的大小为1字节,因此只能存放所要访问单元的低8位地址。在紧凑模式下如果使用了超过256B的外部数据存储区,那么访问单元的高8位地址(即页地址)必须由端口P2来输出。开发人员必须为分页寻址设置合适的开始地址,编译器会使用这个开始地址在启动代码中对P2端口进行设置。 3) 大模式 在大模式(large model)下,所有未显式声明存储器类型的变量都使用外部数据区来存放,即和用xdata显式说明一样。此时最大可寻址64KB的存储区域。此时会使用数据指针寄存器(DPTR)来进行间接寻址来访问相关数据。使用这种寻址方式效率低,生成的代码比小模式和紧凑模式下生成的代码都要长。 注意: 小模式可以提供最快和最有效的代码,所以一般选择小模式。当所需数据存储区域较大时,可选择紧凑模式或者大模式。 3. 设置存储模式 有以下几种方式设置存储模式: 1) 在IDE中的配置选项中设置 在KEIL IDE中,可以通过选择Project→Options for Target命令进入配置窗口。单击Target选项卡,更改Memory Model选择框的设定,如图37所示。 图37在IDE中设置存储模式 该选项的变更实际是通过C51的编译参数实现的,可单击C51选项卡查看。 小模式是默认模式,无额外选项; 紧凑模式使用COMPACT选项; 大模式使用LARGE选项。 2) 在代码中用编译选项设定 在文件开头位置,使用#pragma[SMALL|COMPACT|LARGE]设置。如: #pragma LARGE 3) 在函数声明中指定 在定义函数时,使用large、compact、small来指定函数内部默认的存储模式。如: void add(int x,int y) large 4. 指定变量的绝对地址 开发者有时候希望把变量存储在指定的地址单元中,可用_at_关键词来将变量定位在一个绝对的内存地址单元。使用方法如下: 数据类型 存储器类型 变量名_at_变量所在绝对地址; 在_at_后面的绝对地址必须符合存储器类型的物理边界限制,即不超过存储区域的最大可寻址范围,该地址必须为常数。 绝对定位的变量遵循以下约束: (1) 声明绝对定位的变量时,不能同时进行初始化赋值。 (2) 类型为bit的变量不能进行绝对地址定位。 (3) 只能对全局变量进行绝对定位,不能对局部变量进行。 下面的例子演示了如何用_at_关键字来定位不同类型的变量。 例3.16_at_关键字的使用。 int xdata xval _at_ 0x8000;//全局变量 xval 存储在xdata区的地址为0x8000和0x8001单元 void main(void) { xval = 0x1234; //赋值,不能在声明时进行 } 可使用下列语句来在另一个源文件中引用例3.16中用_at_修饰的变量。 例3.17_at_关键字修饰变量的引用。 extern int xdata xval; //引入例3.16中声明的变量 void func(void){ xval =0x1000; //重新赋值 } 注意: 如果使用_at_关键字声明变量访问定义在XDATA区的外设,必须使用volatile关键字,以强制生成的代码去内存取硬件数据的实际值,而不是使用以前从硬件读入的保存在寄存器中的旧数据值。原因是外设数据的值可能会被硬件更改,必须确保C编译器不会将内存访问语句优化掉。 3.4.3C51数据类型 表32列出了C51语言支持的数据类型。 表32C51语言支持的数据类型 数 据 类 型C51专用长度取 值 范 围 signed char单字节-128~+127 unsigned char单字节0~255 signed short2字节-32768~+32767 unsigned short2字节0~65535 signed int2字节-32768~+32767 unsigned int2字节0~65535 signed long4字节-2147483648~+214746483647 unsigned long4字节0~4294967295 float4字节 -3.402823E+38~-1.175494E-38或 1.175494E-38~3.402823E+38 *1~3字节对象的地址 enum1字节或2字节-128~+127或-32768~+32767 bit专用1位0或1 sbit专用1位0或1 sfr专用1字节0~255 sfr16专用2字节0~65535 注意: 在未使用signed/unsigned关键字定义整数型变量时,变量是有符号数,还是无符号数由使用的编译器和相关设置共同决定。 C51中有几种ANSI C所没有的特殊数据类型,这些数据类型是和存储区域和存储器类型的概念密切相关的。 1. 特殊功能寄存器 8051系列的微控制器提供了一个独立的内存区,用来存放特殊功能寄存器(Special Function Register, SFR)。 SFR用来进行定时器、计数器、串行I/O、I/O端口等内部资源的工作控制。SFR驻留在0X80到0XFF内部地址空间,可按字节寻址,某些寄存器还可以按位寻址或按字寻址。 由于8051系列的微控制器所拥有的SFR的数量、名称和类型是不完全相同的,因此C51使用sfr、sfr16和sbit关键字对SFR进行声明。 C51编译器为常用的微控制器提供预先定义的SFR头文件(.h文件)。编程时用户可以通过引用对应的头文件,来获取SFR定义信息。例如,对标准的8052芯片,可使用#include <reg52.h>语句。当然用户也可自行定义SFR头文件,甚至为SFR进行不同的命名。 2. 8位特殊功能寄存器(sfr) sfr关键字可以用来定义8051单片机的8位特殊功能寄存器。格式如下: sfr特殊功能寄存器名=特殊功能寄存器的地址; SFR的声明和C变量的声明格式是一样的,只不过使用的修饰符不是char或int,而是sfr。 例如: sfr P0 = 0x80; //Port-0,对应地址为 80H sfr P1 = 0x90; //Port-1,对应地址为 90H sfr P2 = 0xA0; //Port-2,对应地址为 0A0H sfr P3 = 0xB0; //Port-3,对应地址为 0B0H P0、P1、P2、P3是sfr声明的特殊功能寄存器的名称。等号后的常量表示SFR所在的内存地址。地址必须是数值常量,不允许使用带运算符的表达式。 特殊功能寄存器名称只要是一个合法的C标识符即可,但一般使用大写名称,并和芯片手册中的SFR的名称一致。 3. 16位特殊功能寄存器(sfr16) 8051芯片可以将两个8位SFR作为一个16位寄存器来访问。条件是这两个SFR必须处在相邻地址上,并且是低字节在高字节地址的前面。 C51提供了sfr16关键字来进行16位特殊功能寄存器的声明,声明时低字节地址被用来作为16位特殊功能寄存器的地址。定义格式如下: sfr16特殊功能寄存器名=特殊功能寄存器的低字节地址; 例如: sfr16 T2 = 0xCC; //TL2 0CCH, TH2 0CDH sfr16 RCAP2 = 0xCA; //RCAP2L 0CAH, RCAP2H 0CBH 在这个例子中,T2和RCAP2被声明为16位的特殊功能寄存器。 sfr16声明和sfr声明的规则相同,等号后的地址是低字节所对应的地址。 4. 普通位变量(bit) 位变量(Bit Type)是指用一个二进制位表示的变量。位数据类型可以用来声明变量、参数表、函数返回值等。位数据变量声明和基本的数据类型声明一样,格式如下: [存储种类] bit 变量名表; 所有的位变量都存储在内部数据区的可位寻址段中。因为该段只有16字节,所以在一个作用域内最多只能声明128个位变量。 位变量定义或声明时必须遵循以下规则: (1) 禁止中断的函数(#pragma disable)和显式指定寄存器组(using n)的函数不能使用位变量返回值,否则编译器将产生一个错误信息。 (2) 不能将指针声明为指向一个位类型,同样也不能获取位变量的地址。 bit *ptr; //非法语句 (3) 不能声明位变量类型的数组。 bit ware [5]; //非法语句 例3.18位变量使用示例。 bit done_flag = 0; //全局位变量 bit testfunc ( //函数返回值为位类型 bit flag1, //位类型参数 bit flag2) { bit ret; //局部位变量 ret =flag1&flag2; //位变量运算 return (ret); //返回位类型值 } 5. 特殊位变量(sbit) sbit关键字有如下两种使用方式: (1) 用来引用已经声明的可位寻址的对象的某一位。 sbit位变量名=可位寻址变量名 ^ 指定的可寻址位的序号; int bdata ibase; //可位寻址的整型变量 char bdata cbase; //可位寻址的字符型变量 long bdata lbase; sbit mybit0 = ibase ^ 0; //和ibase 变量的位 0 (最低位)实现关联 sbit mybit15 = ibase ^ 15; //和ibase 变量的位 15 (最高位)实现关联 sbit bitc7 = cbase^ 7; //和cbase 变量的位 7 (最高位)实现关联 sbit bitl31 = lbase^ 31; //和lbase 变量的位 31 (最高位)实现关联 在上面的例子中的语句不是赋值语句,而是对ibase、cbase、lbase变量的特定位进行声明。表达式中在“^”符号后的表达式定义了位的位置。该表达式必须是一个常量。 注意: 表达式的取值范围由变量声明中的基变量的数据类型来决定。对char和unsigned char类型,范围为0~7; 对int、unsigned int、short、unsigned short类型,为0~15; 对long和unsigned long为0~31。 可以使用bdata来定义全局可位寻址变量和局部可位寻址变量。但由于sbit声明的变量必须为全局变量,因此sbit声明所使用的可位寻址变量必须为全局变量。 声明可位寻址对象的可寻址位时,必须使用sbit关键字,而不能使用bit关键字。 例3.19sbit与bit的区别。 int bdata iData=1; sbit sbTest=iData ^0;//位寻址变量必须为全局变量 //sbTest和iData的末位绑定, 此时sbTest=1,即iData末位的值 void main(void) { bit bTest=iData^0; //运行结果: iData=1(不变), sbTest=1(不变),bTest=1 //bTest的值是iData的值和0按位异或,并取最后一位的值 iData=32; //运行结果: iData=32,sbTest=0, bTest=1(不变) bTest=0;//运行结果: iData=32(不变), sbTest=0(不变), bTest=0 sbTest=1; //运行结果: iData=33,sbTest=1, bTest=0(不变) } (2) 用来引用已经声明的特殊功能寄存器对象的某一位。 在8051应用中,经常需要对SFR中的可寻址位(特殊功能位)进行独立访问。可以用sbit数据类型将SFR中的可寻址位声明为特殊功能位。 sbit位变量名=可寻址位的位地址; 注意: 不是所有的SFR都是可位寻址的。只有那些SFR地址能被8整除的特殊功能寄存器的是可位寻址的,即SFR二进制地址表示的低3位应全为0。例如,在地址为0XA8(IE)和0XD0(PSW)的SFR是可以位寻址的,而地址为0X81(SP)和0X89(TMOD)的SFR不能位寻址。 任何合法标识符均可用在sbit声明中。等号右边的表达式定义了标识符的绝对位地址。有3种方法来声明位地址: 方法一: sfr_name ^ int_constant,即SFR寄存器名^整型常量。 这种方法使用已经定义的SFR作为sbit的基地址。该SFR必须可位寻址,^符号后的表达式定义了可寻址位的位编号。位编号必须是0~7的数。 sfr PSW = 0xD0; //声明寄存器名 sbit OV = PSW ^ 2; //声明特殊功能位OV sbit CY = PSW ^ 7; //声明特殊功能位CY sfr IE = 0xA8; //声明寄存器名 sbit EA = IE ^ 7; //声明特殊功能位EA 为了计算SFR中位的地址,用位的编号加上SFR寄存器的字节地址。上例中sbit EA=IE ^ 7等效于sbit EA=0xAF,EA的位地址等于IE的SFR寄存器地址0xA8加上位编号7,等于0XAF。 注意: 由于sfr16所对应的寄存器是两个相邻寄存器,因此不可能两个地址均是8的倍数,从而sfr16定义的寄存器一般不能位寻址。 方法二: int_constant^int_constant,即整型常量^整型常量。 这种方法使用整型常数作为基地址。该地址必须地址值在0X80~0XFF之间,并且可以被8整除。^符号后的表达式定义了可寻址位的位编号。位编号必须是0~7的数。 sbit OV = 0xD0 ^ 2; sbit CY = 0xD0 ^ 7; sbit EA = 0xA8 ^ 7; 方法三: int_constant,即用绝对位地址来声明sbit。 sbit OV = 0xD2; sbit CY = 0xD7; sbit EA = 0xAF; 注意: sbit、bit和ANSI C语言中的位域(Bit Field)是3种不同的数据类型。使用sbit声明时,基对象必须可位寻址变量或者是可以位寻址的特殊功能寄存器。 3.4.4C51指针类型 C51的指针和标准C中的指针功能相同。但是由于8051体系结构的不同存储区域的地址有重叠(如data区和xdata区均从地址0开始编址),因此必须要在指针中保存额外的存储区域信息。 根据存储区域信息保存方式的不同,C51提供了两种不同类型的指针: 通用指针(Generic Pointer)和具体指针(Memoryspecific Pointer)。 1. 通用指针 通用指针可以用来保存位于不同存储区域中的相同数据类型变量的地址。通用指针的声明和标准C中的指针声明是相同的,例如: char*s; //指向字符类型的指针 int*numptr; //指向整型类型的指针 long*state; //指向长整型类型的指针 由于一般情况下,8051存储区域的最大寻址范围不大于64KB,因此通用指针总是占用3字节。第1字节保存存储器类型编码值(见表33),第2字节保存地址的高字节,第3字节保存地址的低字节。许多C51的库例程使用这种指针类型,通用指针类型可以访问任何存储区域内的变量。 表33存储器类型编码值 存储器类型idata/data/bdataxdatapdatacode 编码值0x000x010xFE0xFF 下列代码表示了不同存储区的通用指针变量的赋值过程。 例3.20通用指针的使用。 void main(void) { char *c_ptr; //通用字符指针 char data dj; //data 区字符变量 char xdata xj; //xdata 区字符变量 char code cj = 9; //code 区字符变量 c_ptr = &dj; c_ptr = &xj; c_ptr = &cj; } 2. 具体指针 具体指针是在声明时指定了存储器类型的指针,仅用于保存指定存储区域中的指定数据类型变量的地址。 char data *str; //指向data区的字符变量的指针 intxdata*numtab; //指向xdata区的整型变量的指针 longcode*powtab; //指向code区的长整型变量的指针 因为存储器类型在编译时就已经指定,所以和通用指针不同,具体指针不需要保存存储器类型字节。具体指针可以保存在1字节(idata、data、bdata、pdata类型指针,这些区域的最大寻址范围不大于256B)或2字节(code和xdata类型指针,这些区域的最大寻址范围不大于64KB)中。 例3.21具体指针的使用。 void main(void) { char data *c_ptr; //指向data区的字符变量的指针 char data dj; //data 区字符变量 char xdata xj; //xdata 区字符变量 c_ptr = &dj; c_ptr = &xj; //非法语句 } 定义具体指针变量时可以使用两个存储器类型,“*”前的存储器类型修饰指针指向的数据,“*”后的存储器类型修饰指针本身,即指针所占据的存储区域类型。例如: char data * xdata str; //指向data区的字符变量的指针,指针变量本身存储在xdata区 int xdata * data numtab; //指向xdata区的整型变量的指针,指针变量本身存储在data区 注意: 使用通用指针类型的代码和具体指针类型的代码相比,完成相同的功能代码的运行速度要慢很多。这是因为通用指针类型只有在程序运行时才能知道实际的变量存储区类型,所以编译器就不能对内存访问进行优化,从而只能生成可以访问任意存储区的通用代码。如果必须优先考虑程序的运行速度,那么只要有可能就应该使用具体指针来替代通用指针。 3.4.5C51函数定义 1. C51函数完整声明 综上所述,完整的函数声明如下: [return_type] funcname([args]) [{small|compact|large}] [reentrant][interrupt x][using y] 2. 指定存储模式 C51定义函数时可使用small、compact或large这3个C51关键字来指明函数内部使用的存储模式。 3. 可重入函数 一个可重入函数可以在同一个时刻由多个进程共享。即当一个进程正在执行一个可重入函数,另一个进程可以中断该进程,然后执行同一个可重入函数,而不会影响函数的运行结果。 ANSI C调用函数时会把函数的调用参数和函数中使用的局部变量存入堆栈。而C51使用固定的存储空间(称为局部数据区)来存放相关数据。所以在递归调用仅使用局部变量的函数时,ANSI C函数总是可重入的,C51中的函数是不能重入的(局部数据区存储的数据会被覆盖)。 为此必须使用reentrant函数属性来声明函数是可重入的,以便C51编译器对函数进行特殊处理。格式如下: 函数类型 函数名(形式参数列表) reentrant C51编译器为可重入函数创建一个模拟堆栈(软件方式实现)来完成参数传递和局部变量存储,从而解决数据信息覆盖问题。可重入函数一般占用较大的内存空间,运行起来也比较慢,并且不允许传递bit类型的变量,也不能定义bit类型的局变量。 可重入函数经常在实时应用系统中应用,也可在中断响应函数和非中断响应函数同时调用同一个函数时使用。 下面以斐波那契数列(Fibonacci Sequence)为例看一下可重入函数的应用。斐波那契数列定义: F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)(n≥2,n∈N*)。 例3.22可重入函数的使用。 int fib(int num) reentrant { int ret=0; if(num==0) ret=0; else if(num==1) ret=1; else ret=fib(num-1)+fib(num-2); return ret; } void main(void) { int x; x=fib(3); return; } 使用reentrant关键字,变量x的值为2,正确。 不使用reentrant关键字,编译时有警告,变量x的值为1,错误。 4. 中断响应函数 传统的8051处理器的中断有5种,某些8051兼容类型可以有更多的中断,C51最大支持32个中断。 中断响应函数的定义如下: 函数类型 函数名(形式参数列表) interrupt n 它将函数定义为中断响应函数。中断属性带一个值为0~31的整型参数,用来表示中断响应函数所对应的中断号,该参数不能是带运算符的表达式。 注意: 仅能在函数定义时使用interrupt函数属性,不能在函数声明时使用interrupt函数属性。 中断响应过程如下: (1) 当中断产生时,首先由硬件实现返回地址(PC值)压入堆栈操作。 (2) 然后中断响应函数被调用,用软件方式实现以下处理: 使用语句将ACC、B、DPH、DPL、PSW这些特殊功能寄存器的值将保存在堆栈中(如果对应寄存器在中断处理函数中未被使用则不保存,由编译器自动判断)。 如果中断响应函数未使用using属性进行修饰,中断响应函数中所使用的通用寄存器的值保存到堆栈中。 对中断进行处理。 恢复保存寄存器的值,退出中断响应函数(其对应的汇编代码使用RETI指令退出,普通函数使用RET指令退出)。 (3) 执行RETI语句,硬件实现返回地址的载入,并跳转到对应语句执行。 例3.23中断响应函数定义。 int alarm_count =0; //中断计数 void falarm(void) interrupt 1 //中断号1 对应中断源T0 { alarm_count++; //每产生一次T0中断,计数值增加1 } 中断响应函数应遵循以下规则: 中断响应函数不能进行参数传递。 中断响应函数没有返回值。 不能在其他函数中直接调用中断响应函数。 如果在中断中调用了其他函数,则必须保证这些函数和中断响应函数使用了相同的寄存器组,并且这些函数应为可重入函数。 C51编译器将在绝对地址8n+3中存放一个绝对跳转指令,实现对中断响应函数的调用,其中n为中断号。 5. 指定寄存器组 可使用using函数说明属性来规定函数所使用的寄存器组。格式如下: 函数类型 函数名(形式参数列表)using n using属性使用一个值为0~3的整型参数,这个参数表示使用的寄存器组的编号,这个参数不能使用带运算符的表达式。using属性只能在函数定义中使用,不能在函数原型声明中使用。 使用using属性的函数将完成以下操作: 进入函数前,将当前使用的寄存器组的标号保存在堆栈中。 更改PSW的寄存器组选择位,选择设定的寄存器组作为当前的寄存器组。 函数退出时,将寄存器组恢复成进入函数前的寄存器组。 例3.24寄存器组的使用。 void test1(void) {char idata x=0x10;} void test2(void) using 1 {char idata x=0x11;} void main(void) { test1(); test2(); } 反汇编代码: … ; FUNCTION test1(BEGIN) MOVR0,#LOW x MOV@R0,#010H RET ; FUNCTION test1(END) … ; FUNCTION test2(BEGIN) PUSHPSW MOVPSW,#08H MOVR0,#LOW x MOV@R0,#011H POPPSW RET ; FUNCTION test2(END) 可以看出,test2使用using关键字,会在函数内部对PSW寄存器的RS1、RS0位进行更改,上例中改成了01,对应using 1,从而使用BNAK 1的相关寄存器。 注意: 使用using属性就不能通过寄存器来返回值了。必须很小心地使用using属性,以避免出现错误。另外,即使用相同的寄存器组,使用using属性声明的函数也不能返回bit值(bit值是通过CF标志来返回的,使用using属性的函数在退出时,将恢复PSW字,CF是PSW字中的一位)。 使用寄存器组切换技术可以提高程序的运行速度,若使用不当则会导致参数传递错误,代码维护也比较麻烦,建议不要使用。 3.4.6C51程序设计的注意事项 C51编译器能对C程序源代码进行处理,产生高度优化的代码。注意下面一些问题,可以获取性能更好的代码。 采用短变量。减小变量的数据宽度提高代码效率的最基本的方法。使用C语言编程时,用户习惯于对循环控制变量使用int类型,这对8位的单片机来说是一种极大的浪费,应该仔细考虑变量值可能的范围,然后选择合适的变量类型。很明显,经常使用的变量应该是unsigned char,只占用1字节。 避免使用浮点运算。在8位操作系统上进行32位浮点数运算速度是很慢的,所以如果需要使用浮点数,可以考虑是否使用整型运算来替代浮点运算。整型(长整型)的运算速度要比浮点数的运算速度要快得多。另外两个浮点数比较是否相等时,一般不使用==运算符,而是采用两个数的差小于一个极小值来判断。 使用位变量。对于逻辑值应使用位变量,这将节省内存的使用,提高程序的运行速度。 用局部变量代替全局变量。全局变量始终占用内存空间,因此使用全局变量会占用更多的内存空间。而且在中断系统和多任务系统中,可能会出现几个过程同时使用全局变量的情况,因而必须对全局变量进行保护,才能确保不会出现错误的运行结果。 尽量使用内部数据存储区。应把经常使用的变量放在内部数据存储区中,这可使程序的运行速度得到提高,缩短代码长度。考虑存储速度,应按下面的顺序使用存储器: DATA、IDATA、PDATA、XDATA。 使用具体指针。程序中使用指针时,应指定指针的类型,确定它们指向的存储区域,这样程序代码会更加紧凑,运行速度更快。 使用库函数。常用的和汇编指令对应的库函数有循环左移和循环右移(字符类型) _crol_、_cror_,(int类型)_irol_、_iror_,(long类型)_lrol_、_lror_ 以及空操作_nop_。这些例程直接对应着汇编指令,因而速度更快。 使用宏替代函数。对于小段代码,如使能某些电路或从锁存器中读取数据,可通过使用宏来替代函数。这使得程序有更好的可读性。编译器在碰到宏时,用事先定义的代码去替代宏。当需要改变宏时,只要修改宏的定义。这可以提高程序的可维护性。 存储器模式。C51提供了3种存储器模式,应该尽量使用小模式。小模式下编译出的代码运行速度较快,但可以使用的内存空间较小。如果既希望可以使用较大的内存空间,又希望部分函数有较快的运行速度,此时可以使用混合的存储模式。例如,将项目设置为大模式,将部分经常执行的函数显式声明为小模式。这样编译器将该函数的局部变量存储在内部数据区中,因而可以较快地执行。 习题3 1. 片外RAM 1000H~10FFH单元有一个数据块,用汇编语言编写程序将其传送到片外RAM的2500H单元开始的区域中。 2. 用汇编语言编写将片内RAM的31H、30H单元中的16位二进制数(31H中为高位)求补码后放回原单元的程序。 3. 用汇编语言编写将累加器A中的一位十六进制数(A的高4位为0)转换为ASCII码的程序,转换结果仍存放在累加器A中,要求用查表和非查表两种方式实现。 4. 用汇编语言编程实现函数 y=x+1,x>100,5≤x≤0-1,x<5 设x的值存放在片内RAM的35H单元,y的值存放在片内RAM的36H单元。 5. 假设累加器A中的内容为0~5,编写根据累加器A的不同内容,转向不同分支进行处理的汇编语言程序。 6. 用汇编语言编写程序,将R0中的8位二进制数的各位用其ASCII码表示,结果保存放到片内RAM的30H开始的单元中。 7. 片内RAM的HEXR开始的单元中存放着一组十六进制数(一个单元放两位),数据的个数放在片内RAM的LEN单元中,用汇编语言编写程序将这些十六进制数转换为ASCII码,并存入片内RAM中ASCR开始的单元。 8. 程序存储器中有一个5行×8列的表格,用汇编语言编程把行下标为I、列下标为J的元素读入到累加器A中。 9. 用汇编语言编写程序,将累加器A中的8位二进制数转换为十进制数存放在片内RAM的21H(百位)和20H(十位和个位)单元中。 10. 用汇编语言编写程序实现图38所示的硬件逻辑功能。其中P1.1、P1.2和P1.3分别是端口线上的信息,IE0、IE1为外部中断请求标志,25H和26H为两个位地址。 图38习题10电路图 11. 用汇编语言编程求两个无符号数据块中最大值的乘积。数据块的首地址分别为片内RAM的60H和70H,每个数据块的第1字节用来存放数据块的长度。结果存入片内RAM的5FH和5EH单元中,要求求数据块中的最大值,用子程序实现。 12. 编写多字节无符号数加法子程序,入口参数为R0为被加数低位地址指针、R1为加数低位地址指针、R2为字节数。出口参数为R0为和的高位地址指针。 13. C51有哪几种存储区域?如何将变量定义在这些区域中?如何进行绝对定位? 14. 存储种类、存储器类型各指什么?各自分为哪几类? 15. C51的存储模式有哪几种?各有什么特点? 16. C51有哪些特殊数据类型? 17. 什么是通用指针?什么是具体指针?两者各有什么优缺点? 18. C51语言对函数定义进行了哪些扩展?