1、单击此处编辑母版标题样式,单击此处编辑母版文本样式,第二级,第三级,第四级,第五级,*,大学课件,16,位单片机及语音嵌入式系统,-,赵定远,-,第,4,章 程序设计,1,4.1 nSP IDE,的项目组织结构,项目提供用户程序及资源文档的编辑和管理,并提供各项环境要素的设置途径。因此,用户从编程到调程之前实际上都是围绕着项目的操作。,新建项目包括三类文件:源文件,(Source files),、头文件,(Head files),和用来存放文档或项目说明的文件,(External Dependencies),其组织结构如表,4.1,所示。这种项目管理的方式,会把与项目相关的代码模块组织为一个有
2、机的整体,便于开发人员对其代码以及相关文件文档的管理,nSP IDE,新建项目的结果,IDE,项目文件管理的组织结构,4.2,汇编语言程序设计,代码流动过程图,编程示例,/,描述:计算,1 to 100,累加值,.RAM,/,定义预定义,RAM,段,.VAR I_Sum;,/,定义变量,.CODE,/,定义代码段,/=,/,函数:,main(),/,描述:主函数,/=,.PUBLIC _main;,/,对,main,程序段声明,_main:,/,主程序开始,R1=0 x0001;,/r1=1.100,R2=0 x0000;,/,寄存器清零,L_SumLoop:,R2+=R1;,/,累计值存到寄
3、存器,r2,R1+=1;,/,下一个数值,CMP R1,100;,/,判断是否加到,100,JNA L_SumLoop;,/,如果,r1=100,跳到,L_SumLoop,I_Sum=R2;,/,在,I_Sum,中保存最终结果,L_ProgramEndLoop:,/,程序死循环,JMP L_ProgramEndLoop;,/*,编程说明:,1),汇编必须有一个主函数的标号“,_main”,,而且必须声明此“,_main”,为全局型标号:“,.PUBLIC _main”,2),程序代码没有定义实际的物理地址,而是以伪指令“,.CODE”,声明此程序代码可以定位在任何一个程序存储区内。汇编代码在程
4、序存储区中的定位则由,IDE,负责管理。,3),程序用伪指令“,.RAM”,在数据存储区内声明了一个变量“,R_Sum”,,我们无需关心“,R_Sum”,的实际物理地址,,IDE,将负责安排和管理数据变量在数据存储区的地址安排。,4),变量名,R_Sum,实际上代表了变量的地址,在汇编中我们对变量进行读写操作时,则需要用,R_Sum,来表示变量中的实际内容。此部分的详细内容可以阅读第三章中的数据寻址方式一节。,数制、数据类型与参数,nSP,的数制及其后缀规定,nSP,汇编指令中的数据类型,nSP,汇编指令中的连接运算符及修饰符,运算符的优先次序,地址表达式与标号:,修饰符,SEG,与,OFFS
5、ET,常常应用在计算表的地址,修饰符,SEG,:代表,22,位地址,修饰符,OFFSET,:代表,16,位地址,汇编语言程序中所有标号的定义都是字母大小写区分的,程序注释与符号规定:,程序注释行必须用双斜线(,/,)或分号(;)起始,它可与程序指令在同一行,或跟在指令后,亦可在指令的前一行或后一行。,nSP,的汇编器规定伪指令不必区分字母的大小写,亦即书写伪指令时既可全用大写,也可全用小写,甚至可以大小写混用。但所有定义的标号包括宏名、结构名、结构变量名、段名及程序名则一律区分其字母的大小写。,汇编语言的程序结构:,程序最基本的结构形式有顺序、循环、分支、子程序四种结构,分支程序设计:,分支结
6、构可分为双分支结构和多分支结构两种,根据不同的条件执行不同的动作,在某一确定的条件下,只能执行多个分支中的一个分支。,IF()THEN ELSE,双分支结构,CASE,多分支结构图,(a),图,(b),阶跃函数,说明:这是一个典型的双分支结构,输入值大于等于,0,时则返回,1,,输入值小于,0,时返回,0,。,入口参数:,R1;,(有符号数),出口参数:,R1,子程序名:,F_Step,程序的代码如下:,.PUBLIC F_Step;,.CODE,F_Step:.proc,CMP R1,0;/,与,0,比较,JGE?negtive;/,大于等于,0,则跳转到非负数处理,R1=0;/,小于,0,
7、则返回,0,JMP?Step_end;/,跳转到程序结束处,?negtive:,R1=1;/,大于,0,,则返回,1,?Step_end:,RETF;,.ENDP,循环程序设计,汇编语言中没有专用的循环指令,但是可以使用条件转移指令通过条件判断来控制循环是继续还是结束。,(,a,),DO_UNTIL,结构(,b,),WHILE_DO,结构,/,描述:把内存中地址为,0 x0000,0 x0006,中的数据,移到地址为,0 x0010,0 x0016,中。,.IRAM,Label:,.DW 0 x0001,0 x0002,0 x0003,0 x0004,0 x0005,0 x0006,0 x00
8、07;,.VAR C_Move_To_Position=0 x0010;/,定义起始地址;,.CODE,/,函数:,main(),/,描述:主函数,.PUBLIC _main;,_main:,R1=7;/,设置要移动的数据的个数,R2=C_Move_To_Position;,BP=Label;,L_Loop:,R3=BP;/,被移动的数据送入,r3,R2=R3;/,被移动的数据送往目的地址,BP+=1;/,源地址加,1,R2=R2+1;/,目的地址加,1,R1-=1;/,计数减,1,JNZ L_Loop;,MainLoop:,jmp MainLoop;,子程序:,在实际应用中,经常会遇到在同一
9、程序中,需要多次进行一些相同的计算和操作,例如:延时,算术运算等。如果每次使用时都再从头开始编写这些程序,则程序不仅繁琐,而且浪费内存空间,也给程序的调试增加难度。因此,可以采用子程序的概念,将一些重复使用的程序标准化,使之成为一个独立的程序段,需要时调用即可。我们就把这些程序段称作为子程序。一般来说子程序的结构包括三个部分:,1.,子程序的定义声明和开始标号部分;,2.,子程序的实体内容部分,表明程序将进行怎样的操作;,3.,子程序的结束标号部分,子程序结构,程序的调用包括主程序调用子程序,子程序调用子程序,在程序调用的过程中,需要注意到的问题是断点的现场保护。就是说,子程序将占用的资源是否
10、与主程序冲突,子程序将会破坏什么寄存器的内容,而这些寄存器是否是主程序持续使用的等等。通常的做法是用堆栈对现场进行保护,在子程序开始就把子程序要破坏掉的寄存器的内容压栈保护,当子程序结束的时候,再弹栈恢复现场。,程序调用的过程都伴随着参数的传递,正确的参数传递要满足入口和出口条件。入口条件指执行子程序时所必需的有关寄存器内容或源程序的存储器的存储地址等,主程序调用子程序时必须先满足入口条件,换句话说就是满足子程序对输入参数的约定。出口参数就是指子程序执行完了之后运算结果所存放的寄存器或存储器地址等,,参数的传递有以下几种情况:,1),通过寄存器传递,2),通过变量传递,3),通过堆栈传递,下面
11、我们针对每一种情况进行具体讲解,分析,通过寄存器传递参数:,寄存器传递参数,是最常用的一种参数传递的方式。我们常用到的传递参数的寄存器有,4,个,分别为,R1R4,;在程序调用的过程中,寄存器中的值也会被带到被调用的子程序中供子程序使用。以主程序调用子程序为例:在调用子程序前,R1R4,这,4,个寄存器中可能暂存一些值,发生调用子程序以后,这些值仍被带到相应的子程序中继续参加子程序的运算,子程序运算结束后返回主程序,这些寄存器的新值也会被带到主程序中继续参加主程序的运算。这个过程也可以用图来表示。实线表示参数的传递方向是由主程序到子程序,虚线表示参数的传递方向是由子程序到主程序。,范例程序,就
12、是利用寄存器传递变量。,求,32,位有符号数的绝对值,/,函数,:F_Abs_32,(),/,语法:,void F_Abs_32,(,int A,int B,),/,描述:求,32,位有符号数具对值,/,参数:,r3,有符号数低,16,位,,r4,有符号数高,16,位,/,返回:,r1,绝对值结果的低,16,位,,r2,绝对值结果的高,16,位,.CODE,.PUBLIC F_Abs_32,F_Abs_32:,R1=R3;/,传送低,16,位,R2=R4;/,传送高,16,位,JMI?neg;/,如果为负则跳转到负数处理,RETF;/,为正数则无需任何处理,返回,?neg:/,负数处理,R1=
13、0 xFFFF;/,低,16,位去反,R2=0 xFFFF;/,高,16,位取反,R1+=1;/,低,16,位加,1,R2+=0,Carry;/,高,16,位加进位,RETF;,通过变量传递参数:,通过变量进行的参数传递,主要是通过全局型变量实现的。在汇编中,一个变量名,就代表了一个实际的寄存器的物理地址。可以直接对物理地址进行赋值和读取,但这种的方法会带来很多麻烦。用变量名去代表一个实际的物理地址,就涉及到某部分汇编代码是否认识该变量名的问题。,如果在某个汇编文件中定义了一个全局变量(,.PUBLIC,),那么此汇编文件中的所有汇编代码都能够使用这个变量。但是在其他的汇编文件中,仍不能直接使
14、用这个变量。在这种情况下,需要在使用这个变量的汇编文件中将该变量声明成外部变量(,.external,),既可使用这个变量,同时该变量也起到了参数传递的作用。,如图所示。实线表示参数的传递方向是由主程序到子程序,虚线表示参数的传递方向是由子程序到主程序。,通过堆栈传递参数:,在,C,函数与汇编函数的相互调用过程中,主要通过堆栈来传递参数,而在函数返回时,则采用寄存器来传递返回值。在主程序把要传递的参数压入堆栈,然后调用子程序。子程序从堆栈中寻找需要的参数进行处理。当子程序返回后,主程序需要进行弹栈处理,以恢复参数压入堆栈前的堆栈状态,如图所示。事实上,,IDE,开发环境中的,C,语言与汇编语言
15、的相互调用,就是采用堆栈传递参数,寄存器返回参数的方式。,SPCE061A,使用,BP,寄存器,可以实现变址寻址方式,可以简洁地实现堆栈传递参数的过程,。,嵌套与递归,子程序的嵌套,子程序嵌套就是指子程序调用子程序。其中嵌套的层数称为嵌套深度。图表示了三重嵌套的过程。,子程序嵌套要注意以下几个方面:,1),寄存器的保护和恢复,以避免各层子程序之间发生因寄存器冲突而出错的情况。,2),程序中如果使用了堆栈来传递参数,应对堆栈小心操作,避免堆栈使用不当造成子程序不能正确返回的出错情况。,3),子程序的嵌套层数不是无限的。堆栈是在数据存储区内开辟的空间,而由于,SPCE061A,单片机的数据存储的空
16、间为,2k WORD,。,递归子程序,递归调用是指子程序调用自身子程序。,进行递归调用时需注意的地方是,一个递归程序必须有一个能够退出递归调用的测试语句。也就是说,递归调用是有条件的,满足了条件后,才可以进行递归调用;如果无条件地进行递归调用,那么会使堆栈空间溢出,导致严重的错误。下面的一段代码没有退出条件,运行的结果,必然是错误的。,4.3 C,语言程序设计,是否具有对高级语言,HLL,(,High Level Language,)的支持已成为衡量微控制器性能的标准之一。显然,在,HLL,平台上要比在汇编级上编程具有诸多优势:代码清晰易读、易维护,易形成模块化,便于重复使用从而增加代码的开发
17、效率。,HLL,中又因,C,语言的可移植性最佳而成为首选。因此,支持,C,语言几乎是所有微控制器设计的一项基本要求。,nSP,指令结构的设计就着重考虑了对,C,语言的支持。,GCC,是一种针对,nSP,操作平台的,ANSI-C,编译器,,nSP,指令的算逻操作符,nSP,对,ANSI-C,中基本数据类型的支持,程序调用协议,由于,C,编译器产生的所有标号都以下划线(,_,)为前缀,而,C,程序在调用汇编程序时要求汇编程序名也以下划线,(_),为前缀。,模块代码间的调用,是遵循,nSP,体系的调用协议,(Calling Convention),。所谓调用协议,是指用于标准子程序之间一个模块与另一
18、模块的通讯约定;即使两个模块是以不同的语言编写而成,亦是如此。,调用协议是指这样一套法则:它使不同的子程序代码之间形成一种握手通讯接口,并完成由一个子程序到另一个子程序的参数传递与控制,以及定义出子程序调用与子程序返回值的常规规则。,调用协议包括以下一些相关要素:,1),调用子程序间的参数传递;,2),子程序返回值;,3),调用子程序过程中所用堆栈;,4),用于暂存数据的中间寄存器。,nSP,体系的调用协议的内容如下:,1.,参数传递,参数以相反的顺序(从右到左)被压入栈中。必要时所有的参数都被转换成其在函数原型中被声明过的数据类型。但如果函数的调用发生在其声明之前,则传递在调用函数里的参数是
19、不会被进行任何数据类型转换的。,2.,堆栈维护及排列,函数调用者应切记在程序返回时将调用程序压入栈中的参数弹出。,各参数和局部变量在堆栈中的排列如图,4.22,所示。,3.,返回值,16,位的返回值存放在寄存器,R1,中。,32,位的返回值存入寄存器对,R1,、,R2,中;其中低字在,R1,中,高字在,R2,中。若要返回结构则需在,R1,中存放一个指向结构的指针。,4.,寄存器数据暂存方式,编译器会产生,prolog/epilog,过程动作来暂存或恢复,PC,、,SR,及,BP,寄存器。汇编器则通过,CALL,指令可将,PC,和,SR,自动压入栈中,而通过,RETF,或,RETI,指令将其自动
20、弹出栈来。,5.,指针,编译器所认可的指针是,16,位的。函数的指针实际上并非指向函数的入口地址,而是一个段地址向量,_function_entry,,在该向量里由,2,个连续的,word,的数据单元存放的值才是函数的入口地址。,在,C,程序中调用汇编函数:,在,C,中要调用一个汇编编写的函数,需要首先在,C,语言中声明此函数的函数原型。尽管不作声明也能通过编译并能执行代码,但是会带来很多的潜在的,bug,。,下面首先观察最简单的,C,调用汇编的堆栈过程:,无参数传递的,C,语言调用汇编函数,/,描述:无参数传递的,C,语言调用汇编函数,void F_Sub_Asm(void);/,声明要调用
21、的函数的函数原型,此函数没有任何参数的传递,/,函数:,main(),/,描述:主函数,int main(void),while(1),F_Sub_Asm();,return 0;,/void F_Sub_Asm(void);,/main.c,结束,汇编函数如下:,/,函数,:F_Sub_Asm(),/,描述:延时程序,.CODE,.PUBLIC _F_Sub_Asm,_F_Sub_Asm:,NOP;,RETF;,在,IDE,开发环境下运行可以看到调用过程堆栈变化十分简单,局部变量调用示意,oid F_F_Sub_Asm(void);/,声明要调用的函数的函数原型,此函数没有任何 参数的传递,
22、int main(),int i=1,j=2,k=3;,while(1),F_F_Sub_Asm();,i=0;,i+;,j=0;,j+;,k=0;,k+;,return 0;,汇编函数如下:,.CODE,/,函数,:F_F_Sub_Asm,(),/,语法:,void F_F_Sub_Asm(void),/,描述:延时子程序,/,参数:无,/,返回:无,.PUBLIC _F_F_Sub_Asm,_F_F_Sub_Asm:,NOP;,RETF;,的局部变量(,i,j,k,)在堆栈中存放的位置:,C,向汇编函数传递参数:,/,描述:,C,向汇编函数传递参数,void F_Sub_Asm(int a
23、int b,int c);/,声明要调用的函数的函数原型,/,函数:,main(),/,描述:主函数,int main(),int i=1,j=2,k=3;,while(1),F_Sub_Asm(i,j,k);,i=0;,i+;,j=0;,j+;,k=0;,k+;,return 0;,/void F_Sub_Asm(int a,int b,int c);,来自于,asm.asm,。测试传递参数,,a,b,c,所传递的参数,无出口参数。,/main.c,结束,汇编函数如下:,/,函数,:F_Key_Scan,(),/,语法:,void F_Key_Scan,(,int a,int b,int
24、c,),/,描述:测试传递参数,/,参数:,a,b,c,所传递的参数,/,返回:无,.CODE,.PUBLIC _F_Sub_Asm,_F_Sub_Asm:,NOP;,RETF;,C,程序调用时的利用堆栈的,参数传递:,汇编调用,C,的函数:,/,描述:汇编调用,C,的函数,.EXTERNAL _F_Sub_C,.CODE,.PUBLIC _main;,/,函数:,main(),/,描述:主函数,_main:,R1=1;,PUSH R1 TO SP;/,第,3,个参数入栈,R1=2;,PUSH R1 TO SP;/,第,2,个参数入栈,R1=3;,PUSH R1 TO SP;/,第,1,个参数
25、入栈,CALL _F_Sub_C;,POP R1,R3 FROM SP;/,弹出参数回复,SP,指针,GOTO _main;,RETF;,/void F_Sub_C(int i,int j,int k);,来自于,asm.c,。延时程序,入口参数,i,j,k;,返回,i,/main.asm,结束,C,语言子函数如下:,/,函数,:F_Sub_C(),/,语法:,void F_Sub_C(int i,int j,int k),/,描述:延时程序,/,参数:,i,j,k,/,返回:,i,int F_Sub_C(int i,int j,int k),i+;,j+;,k+;,return i;,C,语
26、言的嵌入式汇编:,为了使,C,语言程序具有更高的效率和更多的功能,需在,C,语言程序里嵌入用汇编语言编写的子程序。一方面是为提高子程序的执行速度和效率;另一方面,可解决某些用,C,语言程序无法实现的机器语言操作。而,C,语言代码与汇编语言代码的接口是任何,C,编译器毋庸置疑要解决的问题。,通常,有两种方法可将汇编语言代码与,C,语言代码联合在一起。一种是把独立的汇编语言程序用,C,函数连接起来,通过,API(Application Program Interface),的方式调用;另一种就是我们下面要讲的在线汇编方法,即将直接插入式汇编指令嵌入到,C,函数中。,GCC,的基本数据类型,采用,G
27、CC,规定的在线汇编指令格式进行指令的输入,是,GCC,实现将,nSP,汇编指令嵌入,C,函数中的方法。,GCC,在线汇编指令格式规定如下:,asm(,“,汇编指令模板”:输出参数:输入参数:,clobbers,参数,),;,若无,clobber,参数,则在线汇编指令格式可简化为:,asm(,“,汇编指令模板”:输出参数:输入参数,),;,1),汇编指令模板,模板是在线汇编指令中的主要成分,,GCC,据此可在当前位置产生汇编指令输出。例如,下面一条在线汇编指令:,asm(%0+=%1:+r(foo):r(bar),;,此处,,%0+=%1,就是模板。其中,操作数,%0,、,%1,作为一种形式参
28、数,分别会由第一个冒号后面实际的输出、输入参数取代。带百分号的数字表示的是第一个冒号后参数的序号。,如下例:,asm(%0=%1+%2:=r(foo):r(bar),i(10),;,%0,会由参数,foo,取代,,%1,会由参数,bar,取代,而,%2,则会由数值,10,取代。,在汇编输出中,一个汇编指令模板里可以挂接多条汇编指令。其方法是用换行符,n,来结束每一条指令,并用,Tab,键符,t,将同一模板产生在汇编输出中的各条指令在换行显示时缩进到同一列,以使汇编指令显示清晰。如下例:,asm(%0+=%1nt%0+=%1:+r(foo):r(bar),;,2),操作数,在线汇编指令格式中,第
29、一冒号后的参数为输出操作数,第二冒号后的参数为输入操作数,第三冒号后跟着的则是,clobber,操作数。在各类操作数中,引号里的字符代表的是其存储类型约束符;括弧里面的字符串表示的是实际操作数。,如果输出参数有若干个,可用逗号“,”将每个参数隔开。,同样,该法则适用于输入参数或,clobber,参数。,3),操作数约束符,约束符的作用在于指示,GCC,,使用在汇编指令模板中的操作数的存储类型。表,4.9,列出了一些约束符和它们分别代表的操作数不同的存储类型,也列出了用在操作数约束符之前的两个约束符前缀。,操作数存储类型约束符及约束符前缀,利用嵌入式汇编实现对端口寄存器的操作:,在,C,的嵌入式
30、汇编中,当使用端口寄存器名称时,需要在,C,文件中加入汇编的包含文件,如下所示:,asm(“.include hardware.inc”);,那么,我们就可以使用端口寄存器的名称,而不必去使用端口的实际的地址,1),写端口寄存器,现举例说明:要设定,PortA,端口为输出端口,需要对,P_IOA_Dir,赋值,0 xffff,。那么在,C,中的嵌入式汇编的实现方式如下:,在,C,中有一个,int,型的变量,i,,传送到,P_IOA_Dir,中,则嵌入汇编的实现方式如下:,.,asm(“.include hareware.inc”);,.,int main(void),int i;,.,asm(
31、P_IOA_Dir=%0,:,:r(i),);,如果需要对端口寄存器直接赋值一个立即数(比如对,P_IOA_Dir,赋值,0 x1234,),那么内嵌式汇编为:,.,asm(“.include hareware.inc”);,.,int main(void),.,asm(P_IOA_Dir=%0,:,/,没有输出参数,:r(0 x1234),/,只有输入参数,通过寄存器传递立即数,0 x1234,);,2),读端口寄存器,对端口寄存器进行读操作的方法,与写类似,下面仍然以,P_IOA_Dir,为例,进行说明,如果要实现把端口的寄存器,P_IOA_Dir,的值读出并保存在,C,中的一个,int,变量,j,里,那么可以通过下面的方法来实现。,.,asm(“.include hareware.inc”);,.,int main(void),int j;,.,asm(%0=P_IOA_Dir,:=r(j),/,只有输出参数,而无输入参数,);,本章结束,






