【51单片机】汇编程序与周期性任务设计
一、任务目标
- 深入了解汇编指令和常用程序结构,以周期性点亮LED灯的延时函数为例,掌握:
1)通过软件多重循环软件计数的定时方法 ;
2)通过 循环+ nop 指令的方法; - 掌握汇编语言用查表法完成 求平方数的程序;
- 掌握普通单片机实验开发板的开发使用方法,在板子上完成LED周期性点灯的C程序实验。
*思考题
1)请查阅汇编指令 “MOV R6,#250”和“DJNZ R6,D2”的指令周期数,计算其对应的时钟周期 us值;然后计算 Delay函数的总的循环次数和对应的时钟周期总数us, 说明这个LED灯大约每隔多少毫秒(ms)或秒(s)才变化一次亮灭状态;
2)如果要求实现准确的LED 每隔1s亮灭的周期性变化,程序如何修改?请给出完整代码,并在edsim51中进行实践练习。
二、具体过程
2.1 流水灯延迟函数
2.1.1 通过软件多重循环软件计数
简单来说就是通过设置一个计数器变量,随着其逐渐递减实现延迟的效果,当计数器值为0时循环结束。也就是说,计数器变量的数值决定了延迟时间。
MOV R1,#0FEH ;给R1赋值254
LOOP: ;LOOP英文为循环的意思
MOV P1,#0FEH ;1111 1110,LED1点亮
DJNZ R1,LOOP ;R1--,当R1=0时,跳出循环,执行下一条指令
MOV P1,#0FDH ;1111 1101,LED1灭,LED2点亮
;ps:由于Edsim电路原理图中LED共阳极,所以是低电平点亮
注意看R1的数值变化以及指令的执行情况,可以观察到,当R1的值等于0后,LED2亮,即第五行指令执行。
2.1.2 循环+ nop 指令
NOP(No Operation)指令是一个空操作指令,它不做任何实质性的操作,只是简单地占用一个指令周期的时间。通过在循环中添加一定数量的nop指令,可以创建一个简单的延时。
MOV R1,#0FEH
LOOP: ;LOOP英文为循环的意思
MOV P1,#0FEH
DJNZ R1,LOOP
NOP
NOP
MOV P1,#0FDH
注意看上图中时间变化,一个nop指令1微秒,以上代码加入两个NOP后延长了2微秒的时间。
2.2 查表法求平方数
查表法是一种常用的优化技术,它通过预先计算并存储结果来减少实时计算的时间。对于求平方数这样的操作,我们可以创建一个平方数表,其中存储了0到某个上限的整数的平方值。然后,当我们需要求一个数的平方时,我们只需要查找这个表即可。
ORG 0000H ; 程序起始地址
SQUARE_TABLE: DB 0, 1, 4, 9, 16, 25, 36 ;建立一个表格用于存储平方数,这里存了1~6的平方数表
MOV DPTR, #SQUARE_TABLE ; 设置数据指针DPTR指向平方数表
MOV R1, #6 ; 将6放入R1寄存器
CALL SQUARE_FUNCTION ; 调用求平方函数
SQUARE_FUNCTION:
MOV A, R1 ; 将待求平方的数移动到累加器A中
MOVC A, @A+DPTR ; 使用MOVC指令和DPTR寄存器从SQUARE_TABLE中查表
MOV R2, A ; 将查找到的结果移动到R2寄存器中
RET ; 返回
END
;注意,在仿真过程中发现无法存储超过7个的平方数值,否则程序不往下运行
仿真后我们可以在R2中查找到所求平方数的数值。这里别看R2显示的是0x24便以为哪里出了问题,其实是读取数据的时候将十进制数转化为十六进制数。0x24对应0010 0100,即十进制的36。
查表法的核心思想在于利用数据指针DPTR,通过间接寻址指令MOVC A, @A+DPTR,计算出从中移动字节的代码存储器地址,然后读取相应的值。
2.3 点亮流水灯
掌握普通单片机实验开发板上的开发使用方法。详情可以参考b站江协科技51单片机入门教程的P2,安装好STC-ISP。
主要步骤如下:
值得注意的是,型号别选错了。 根据开发板上主芯片的型号选择。
#include <REGX52.H>
#include<intrins.h>
void Delay_ms(int ms)//延时函数
{
int i,j;
for(i=0;i<50;i++)
{
for(j=0;j<ms;j++)
{
_nop_();//intrins头文件里的函数,无意义指令,占一个指令周期,起到延时的作用
}
}
}
void main()
{
while(1)
{
P2=0xFE;//1111 1110
Delay_ms(500);//想延时多久只需更改括号里的数值
P2=0xFD;//1111 1101
Delay_ms(500);
P2=0xFB;//1111 1011
Delay_ms(500);
P2=0xF7;//1111 0111
Delay_ms(500);
P2=0xEF;//1110 1111
Delay_ms(500);
P2=0xDF;//1101 1111
Delay_ms(500);
P2=0xBF;//1011 1111
Delay_ms(500);
P2=0x7F;//0111 1111
Delay_ms(500);
}
}
//流水灯从LED1开始左往右亮灭
#include <REGX52.H>
#include<intrins.h>
void Delay_ms(int ms)
{
int i,j;
for(i=0;i<50;i++)
{
for(j=0;j<ms;j++)
{
_nop_();
}
}
}
void main()
{
int i,temp=0xFE;
P2=temp;
while(1)
{
for(i=0;i<8;i++)
{
P2=~(0x01<<i);
//0x01即0000 0001,<<是左移1位,即0000 0010,~是取反。即意味着从1111 1110变为1111 1101
Delay_ms(50);
}
}
}
流水灯的几种代码思路可以参考http://t.csdnimg.cn/lPKpg,以上两个分别是我觉得最易懂和最简洁的代码
三、思考拓展
3.1 延时时间计算
MOV R6,#250占一个指令周期,对应1us;
DJNZ R6,D2也占两个指令周期,对应2us。
以以下代码为例,下面是一个嵌套循环。
DEL: MOV R7,#200
DEL1: MOV R6,#125
DEL2: DJNZ R6,DEL2
DJNZ R6,DEL2
RET
类比C语言代码如下:
for(int i=200;i>0;i--)
{
for(int j=125;j>0;j--)
{
}
}
已知一个DJNZ指令对应2us,总共进行200*125次,即50000us即0.5s。从而实现延时0.5ms的效果。
ORG 0000H
LJMP MAIN
ORG 0100H
MAIN:
MOV A,#254
LOOP: ;循环标签
MOV P1,A ;将累加器A的内容移动到端口P0
LCALL DELAY ;长调用指令,它调用DELAY子程序
RL A ;将累加器A的内容左旋一位
LJMP LOOP
DELAY:
DEL: MOV R7,#200
DEL1: MOV R6,#125
DEL2: DJNZ R6,DEL2
DJNZ R7,DEL1
RET
END
通过仿真我们可以观察到这个LED灯大约每隔50毫秒(ms)才变化一次亮灭状态。
3.2 控制延时时长
要求实现准确的LED 每隔1s亮灭的周期性变化,则需要将DELAY函数中的参数变化。由于上面DELAY延时了50ms,想要控制1s亮灭,将嵌套循环整体增大20倍即可,但是值得注意的是!
51内存只有256,存不下大于255的数字,因此我们不能给R7\R6的赋值翻倍,而是再加入一层嵌套。
具体修改如下:
ORG 0000H
LJMP MAIN
ORG 0100H
MAIN:
MOV A,#254
LOOP: ;循环标签
MOV P1,A ;将累加器A的内容移动到端口P0
LCALL DELAY ;长调用指令,它调用DELAY子程序
RL A ;将累加器A的内容左旋一位
LJMP LOOP
DELAY:
DEL: MOV R7,#200
DEL1: MOV R6,#125
DEL2: MOV R5,#20
DEL3: DJNZ R5,DEL3
DJNZ R6,DEL2
DJNZ R7,DEL1
RET
END
以上,基本实现1s控制LED亮灭操作,因为delay函数中mov语句也耗费了一些时钟周期,所以存在一定偏差。
四、总结反思
汇编指令的内核与C语言是相似的。在使用汇编语言的过程中,可以首先尝试用C语言的思考逻辑思考,再转化为汇编语言。
当然,在仿真的过程中会遇到很多疑问,这些主要受限于硬件原因,例如51的内存只有256B,超过255的数无法计入,超出内存的指令也无法执行。
其次就是数值转换的问题,虽然我们输入的是十进制数,但内存存储的是十六进制数,因此我们在观察寄存器数值的时候,不要因为寄存器的值与输入的值不一样就直接否认,而是要思考一下进制转换的问题。
以上是我的个人学习记录,欢迎各位大佬指正其中的错误
相关链接
- 汇编指令大全
链接:https://pan.baidu.com/s/1CvWQBjs0bJxp0j__Nzruvw?pwd=82i5
提取码:82i5