1、计算机(单片机)汇编语言程序设计1. 程序设计的难点我们曾经把计算机比喻为博弈(下棋) 。计算机的硬件相当于棋盘与棋子,计算机的指令就相当于是规则,而计算机的程序则是完成指定任务的指令序列。下棋是需要动脑子的,计算机的程序设计更需要仔细思考。因为下棋时可以看着棋盘,而计算机的程序运行我们一般是看不到的,除非借助特殊的调试工具。因此计算机的程序设计相当于下盲棋,即不看棋盘下棋。这就要求大家具有下盲棋的基本技能与训练。具体来说,你所编写的程序中的每一个步骤、每一条指令的执行结果是什么,都要能够记住并在以后的程序中使用。一旦忘记或记错,就会导致结果错误。现在很多计算机的程序开发环境都提供了一定的调试
2、工具,可以在一定程度上看到计算机内部的情况。但是在程序设计的编写阶段(不是调试阶段) ,仍然是处于下盲棋的环境。“失之毫厘,差之千里”这句话用来形容计算机的程序设计是再恰当不过的了。要从事计算机的程序设计的工作,必须具备三“心”:兢兢业业、一丝不苟的细心;孜孜不倦、反复琢磨的耐心;坚持不懈、持之以恒的恒心。2. 程序设计与调试的基本方法第一步:画程序流程框图。程序流程框图是描述用计算机语言解决问题的思路,必须先考虑清楚如何下手以及具体的步骤。初学程序设计的人往往忽视这一步,实际上这是非常重要的一步。高级程序设计人员基本上都有画框图的习惯,因为框图才是程序设计员进行交流的语言和工具。框图画出来后
3、,编写程序就已经不是问题了。第二步:框图的细化。程序设计一般遵循从上到下的步骤,即先构思总体结构,再逐步细化直至每一个环节。总体框图往往只有对于解题过程的笼统的描述,而要编写程序还需要比较细致的描述,因此需要对总体框图进行细化,直到能够编写程序为止。第三步:将框图转化为程序。程序流程框图细化到一定程度时,就可以将框图的每个环节转化为若干指令了。将这些指令组合连接起来,就形成了计算机的程序。第四步:用调试工具进行调试。程序编写完成后,一般都会有这样那样的问题和错误。其中的语法错误可以通过编译(Build )解决。而逻辑错误(即程序不能满足设计要求的功能)只能通过调试来解决。程序调试需要借助一定的
4、调试工具(调试器、仿真器) ,使程序在受控状态下运行。程序的受控运行,指的是用户能够以单步和断点方式运行程序,同时能够随时查看 CPU 和存储器中的状态和信息。这样用户就可以将计算机的运行中间结果和自己预期的结果进行比较,从而发现问题和错误所在。现在的软件开发系统都提供了一定的调试手段,我们所使用的 Keil 软件就提供了软件仿真和硬件仿真功能。3. 典型算法程序分析下面通过具体的实例来说明程序设计和调试的过程。3.1 编写 4 字节加法的子程序。加数与被加数由指针 R0 和 R1 定位。结果存放于被加数的存储器单元及随后的一个单元(原有内容被冲掉) 。解题步骤如下:程序流程框图及其细化(能够
5、转化为指令即可):MOV R0,#30HMOV R1,#40HMOV A,R0ADD A,R1MOV R1, A中间过程省略SJMP $(调试过程略)3.2 编写 2 字节乘 2 字节的子程序,乘数位于 R2R3,被乘数位于 R4R5。结果存放于R4R5R6R7 中。程序流程框图如下:中间过程省略为了进行编程,初学者需要进一步将框图细化,直至每一个方框可以转化为一条指令。熟练以后就可以省去细化框图的过程,不用过分细化,直接编写程序。开始初始化加第 1 字节停机存放第 1 字节结果加第 2 字节做 R3*R5直接存储结果停机做 R3*R4加前次结果开始细化后的程序流程框图如下:MOV A,R3M
6、OV B,R5MUL ABMOV R7,AMOV R6,BMOV A,R2MOV B,R5MUL ABADD A,R6MOV R6,AMOV A,BADDC A,#0MOV R5,A中间过程与此类似SJMP $在上面的程序中,第二次的乘法既可以做 R2*R5,也可以做 R3*R4。但是先做 R2*R5 可以将 R5 解放出来,用于存放结果。这样可以尽量少地占用存储器资源。这是程序设计的技巧。下面用一个带有缺陷的乘法程序说明调试过程。MUL16: MOV A,R3MOV B,R5MUL ABMOV R7,AMOV R6,BMOV A,R2MOV B,R5MUL ABADD A,R6MOV R6,
7、ACLR AADDC A,BMOV R5,A做 R3*R5 准备工作直接存储结果停机做 R2*R5 准备工作加前次结果并回存开始做 R3*R5做 R2*R5做 R3*R4MOV A,R3MOV B,R4MUL ABADD A,R6MOV R6,AMOV A,R5ADDC A,BMOV R5,AMOV A,R2MOV B,R4MUL ABADD A,R5MOV R5,ACLR AADDC A,BMOV R4,ARET;MOV R2,#12HMOV R3,#34HMOV R4,#12HMOV R5,#34HCALL MUL16SJMP $上面的程序运行结果是正确的。用不同的数据进行验算,大多数也是
8、正确的。但是有些数据(例如 FFFF*FFFF=FFFE0001)结果是错误的。用调试工具单步执行程序,并与预期的结果进行比较,就能逐步找到错误所在。上面程序的错误之处在于第三次乘法后的加法没有考虑到进位,而大多数验算数据没有产生进位,所以对应的结果是正确的。3.3 编写 2 字节除以 2 字节的子程序,除数位于 R2R3,被除数位于 R4R5。结果存放于R4R5,余数存放于 R6R7 中。解题:除法程序不同于乘法,无法用 8 位除法解决 16 位除法的问题。除法程序实际上并不难,只要回顾一下十进制的除法的操作过程,并认真理解,就很容易弄懂二进制的除法运算过程。例如:1234/56=22 余
9、2 的计算过程如下(注意处理方法是移出再用余数减除数):除 数 | 被 除 数5 6 | 1 2 3 41 | 2 3 4 第一次移位,不够减1 2 | 3 4 第二次移位,不够减1 2 3 | 4 第三次移位,够减,商 21 1 | 4 减去 2*除数后的余数1 1 4 | 第四次移位,够减,商 22 | 减去 2*除数后的余数0 0 2 2 商与余数一起移位在计算机的处理过程中我们将开设存储余数和商的存储单元。为了便于计算机处理,并尽量减少所占用的寄存器/存储器数量,将商、余数与被除数一起移位,并将商紧跟在被除数的后面。这样当除法完成后,被除数所在的存储单元就完全转化变成了商。如下图所示:
10、除 数 | 被 除 数5 6 | 1 2 3 41 | 2 3 4 0 第一次移位,不够减,商 01 2 | 3 4 0 0 第二次移位,不够减,商 01 2 3 | 4 0 0 2 第三次移位,够减,商 21 1 | 4 - - - 减去 2*除数后的余数1 1 4 | 0 0 2 2 第四次移位,够减,商 22 | 减去 2*除数后的余数根据上面思路画出的程序流程框图如下:根据上面的框图编写的程序如下:注意在上面的除法过程中,减法是双字节的。因此必须先暂存第一次的减法结果,如果不够减,则丢弃结果,如果够减,则将暂存的结果取出回存即可。其次二进制除法的商非 0 即 1,因此商 1 的处理非常
11、简单,用 INC 指令即可实现。前提是,每次移位后商的最低位一定是 0。余数-除数 余数除数?除法初始化综合移位处理结束开始移位结束?除数=0?4.1 软件延时程序的编写方法由于在单片机中,每条指令的执行时间是确定的,且没有流水线部件,因此程序的执行时间就是程序中每条指令的执行时间之和。软件延时就是利用这一点设计给定的程序执行时间的程序。例如,指令 NOP 的执行时间为 1 个机器周期。则 10 条 NOP 指令的执行时间就是 10个机器周期。对于 12MHz 的时钟频率,一个机器周期就是 12 个时钟周期,也就是 1 微秒。因此 10 条 NOP 指令的执行时间就是 10 微秒。但是要延时很
12、长时间,例如 1 秒,这需要1000000 条 NOP 指令,是不现实的。显然我们可以利用循环来实现软件延时的功能,如下程序:DELAY: MOV A, #10 ;1TDLY: NOP ;1TDJNZ A, DLY ;2T每条指令的后面已经注明了执行周期数。由于第 2、3 条指令要执行 10 次,因此总的执行周期数为 3*10+1=31。显然,要得到更长的延时时间,只要将 #10 改成尽量大的数即可。但是,要得到最大的延时时间,该数值是多少?最大延时时间又是多少?为了得到更长的延时时间,需要编写多重循环程序。书中 96 页的例子说明了两重循环延时子程序的编写方法:DELAY: MOV 30H,
13、 #50 ;2TDL1: MOV 31H, #49 ;2TDL2: NOP ;1TNOP ;1TDJNZ 31H, DL2 ;2TDJNZ 30H, DL1 ;2TRET ;2T其中的内圈循环是 3 条指令共计 4T,外圈循环又增加了 2 条指令共计 4T,最前面和最后面的指令不参加循环,因此总的执行周期数为(49*4)+4)*50+4=10004 。对于 12M 的时钟频率,执行时间就是大约 10 毫秒。多重循环软件延时的参数有多种选择,需要进行试凑。一般情况下软件延时用在对于时间精度要求不高的场合。由于中断会影响程序的执行,所以在有中断发生的情况下,软件延时时间是不准确的。4.2 查表程序
14、的编写方法对于类似彩灯控制这样的程序,一种方法是利用运算功能进行计算的到所需要的数据,例如循环移位。但是当数据比较复杂时,计算会很麻烦。在计算机的程序设计中经常使用查表的方法得到所需要的数据,例如三角函数、显示字模等。对于彩灯控制程序,我们可以事先建立一张循环数据表:ADDR+0: 0FEHADDR+1: 0FDHADDR+2: 0FBHADDR+3: 0F7HADDR+4: 0EFHADDR+5: 0DFHADDR+6: 0BFHADDR+7: 07FH将上面的数据存放在内部 RAM 中 ADDR 开始的单元中,然后再利用间接寻址的方法得到所需要的数据。这样程序可以简化很多。ADDR EQU
15、 30H;MOV R0,#ADDRMOV A,#0FEHMOV R0, AINC R0MOV A,#0FDHMOV R0, AINC R0MOV A,#0FBHMOV R0, AINC R0MOV A,#0F7HMOV R0, AINC R0INIT: CLR AMOV R0,#ADDRLOOP: ADD A,R0MOV R0,AMOV P1,R0LCALL DELAY1SINC ACJNE A,#8,LOOPSJMP INIT程序流程框图如下:结束?建立数据表取表中数据调整指针开始结束上面的程序是将表建立在 RAM 中。在实际应用中,大多数的表是建立在 ROM 中的。建立在 ROM 中的表是
16、只读的,因此需要用特殊的方法建立和读出数据。4.3 子程序及其调用在程序设计中使用子程序的目的有两个:一是对于在程序中多次使用的程序段,将其作为子程序处理后能够减少总的程序存储空间,提高存储器使用效率。二是将具有独立功能的程序段作为子程序处理,可以使程序的结构更加清楚,增加程序的可读性。主程序调用子程序的过程如下图所示:主程序子程序入口调用指令 CALL 子程序语句下一条指令子程序结束返回 RET再次调用 CALL下一条指令调用子程序的指令是 ACALL 或 LCALL (后跟用标号表示的子程序入口地址) 。子程序的返回指令是 RET。主程序中的 CALL 与子程序中的 RET 是相互对应的,
17、缺少任何一方都是严重的逻辑错误(语法上倒是没有错) 。因为调用时会自动将返回地址压入堆栈,没有返回将倒是堆栈指针错误。而没有调用指令子程序返回时将会返回到无法预知的地址。堆栈指针 SP 在复位时为 07H,第一个压入堆栈的地址为 08H。用户在进行程序设计时应该根据程序的需要预留足够的堆栈空间,并设置相应的堆栈指针。由于堆栈是间接寻址,因此可以把堆栈设置在内部 RAM 的高 128 字节空间。4.4 中断及其程序设计中断是计算机的一个非常重要的功能,因此不管高端还是低端产品,所有的 CPU 都有这个功能,只是处理的方法不同。中断也是计算机的一种资源,合理使用中断将会使计算机的处理能力大为增强;
18、而不合理的使用中断将会使计算机的处理能力降低,且容易发生很难解决的冲突问题。中断的过程与子程序调用非常类似,如下图所示:主程序 中断服务程序入口保护现场此时发生中断 中断服务程序语句下一条指令恢复现场中断返回 RETI再次发生中断下一条指令与子程序不同的是,中断的发生是不能事先预知的。例如用户键盘中断,无法预知用户何时按下按键。再如掉电中断的发生,也是无法预知的。正因为如此,中断服务程序中一般都有保护现场与恢复现场的操作。MCS-51 系列的单片机至少有 5 个以上的中断。即两个外部事件中断、两个定时器中断,以及一个串行通信中断,我们称之为中断源。对于中断的处理也是比较简单的。每个中断发生时,
19、如果允许中断,则程序会转向与中断源相对应的地址执行程序。这些地址依次排列如下:03H、0BH、13H、1BH、23H 、2BH、 。这就是为什么在进行程序设计时要把前面的地址让开,从 100H 单元开始编写程序的原因。带有中断的程序设计分为两个部分,即主程序和中断服务程序。在主程序中,除了要设置中断源的工作方式等工作外,还要设置相应的中断优先级、开放中断源以及开放总中断允许位。中断服务程序的编写与子程序类似,只是要注意:不同的中断源其入口地址也不同,不能搞错。另外,中断返回的指令是 RETI,与子程序返回的指令是不同的,两者不能混淆。下面以硬件实验一为例,说明中断应用程序的编写方法。主程序如下
20、:ORG 0AJMP START;ORG 0BHAJMP T0INT;ORG 100HSTART:MOV TMOD,#01HMOV TH0,#4CHMOV TL0,#00MOV R0,#0SETB TR0SETB ET0SETB EALOOP: INC A ;保护现场的必要性NOP ;主程序循环SJMP LOOP;中断服务程序如下:T0INT:PUSH ACC ;保护现场MOV TH0,#4CH ;重新设置计数器初值MOV TL0,#00INC R0 ;软件计数器CJNE R0,#20,OVERMOV R0,#0CLR A ;保护现场的必要性CPL P3.5OVER:POP ACC ;恢复现场
21、RETI理论上来说,大多数的程序功能都有两种实现方法,即单纯主程序实现与利用中断实现。因此实验指导书中的大多数应用程序都可以尝试采用中断实现。大家可以比较不同的设计方法有什么优缺点。由于中断的处理需要一定的时间,在中断服务程序结束前一般 CPU 无法响应再次中断和其它的中断(即中断嵌套) 。因此中断服务程序要尽量短,尽量把需要做的工作放在主程序中完成。以下是利用中断完成实验二的程序。在中断服务程序中仅仅是设置了一个标志便返回,而把需要做的工作放在主程序中完成。ORG 0AJMP START;ORG 0BHAJMP T0INT;ORG 100HSTART:MOV TMOD,#01HMOV TH0
22、,#4CHMOV TL0,#00MOV R0,#0SETB TR0SETB ET0SETB EAMOV A,#0FEHLOOP: JBC F0,$ ;检测标志RL AMOV P1,A ;主程序实现循环SJMP LOOP;中断服务程序如下:T0INT:MOV TH0,#4CH ;重新设置计数器初值MOV TL0,#00INC R0 ;软件计数器CJNE R0,#20,OVERMOV R0,#0SETB F0 ;设置标志OVER:RETI5. 键盘程序设计在单片机的应用系统中,一般都有一定数量的键盘以便用户进行某些操作(例如数字时钟、空调控制器,等等) 。键盘也是人机接口的重要组成部分。完整的键盘
23、程序是非常复杂的,需要具有以下功能: 能够消除机械按键的抖动。 能够适应不同人群的操作习惯。 具有快速调整功能。 具有一键多能的功能。 调整与设置参数时要有上限和下限。 要能够实现屏幕保护功能(检测出用户在给定时间内未按键) 。大家只要仔细看看自己的手机按键功能就可以体会到键盘处理的难度了。在单片机实验箱中设置了 8 个按键,通过 74HC245 连接到单片机的数据总线上。由电路图可见,当用户没有按键时,端口由上拉电阻上拉至高电平,读入的数据为 1。当用户按下按键时,端口为低电平。这样,就可以通过读入端口的数据(用 MOVX A,DPTR指令)来判断用户按下了哪个键。现在来编写一个程序,通过按
24、键来设置一个参数的数值。该参数的调整范围为 10-200。程序的流程框图如下:根据框图可以编写出如下的程序,其中消除抖动采用了简单的延时法(即延时一定时间再次读键盘):KINC EQU 0FEHKDEC EQU 0FDH有按键?程序初始化消除抖动开始参数+1 操作增量?参数-1 操作减量?上限?下限?ORG 0AJMP START;ORG 100HSTART:MOV R0,#10 ;设置参数保存在 R0 中MOV DPTR,#8000HLOOP:MOVX A,DPTR ;读入键盘数据CJNE A,#0FFH,NEXTSJMP LOOPNEXT:ACALL DELAY ;延时 5mSMOVX A
25、,DPTR ;再次读入键盘数据CJNE A,#0FFH,KEYPSJMP LOOPKEYP:CJNE A,#KINC,KEYNCJNE R0,#200, PINC ;检查上限SJMP LOOPPINC: INC R0SJMP LOOPKEYN:CJNE R0,#10, PDEC ;检查下限SJMP LOOPPINC: DEC R0SJMP LOOP;END现在逐步增加程序的难度,将参数设置范围增加到 30000。这一变化将增加很大的难度,首先判断上下限需要做双字节的操作。其次,如果用户将参数从最小调整到最大,将要按键 29990 次。这是用户无法接受的,必须采用快速调整的方法。即当用户按下按键并保持超过一定时间时(一般为 2 秒) ,参数可以连续快速变化。再次增加程序的难度,要能够适应不同用户的操作习惯,在 2 秒内每次按键只能有一次操作(增量或减量) 。如果 30 秒用户未按键,则报警并禁止用户操作(银行的柜员机就是如此) 。