单片机:MCS-51
系列 STC12C5A32S2
本人学习单片机也没多久,基本上都是现学现卖,有不懂或不对的地方->评论区留言
实验内容:
(当然你也可以使用定时器T1)
(要实现这个实验的功能的话用查询的方式也能做,但是实验要求要用中断)
ORG 0000H
LJMP START
ORG 000BH ;定时器T0的中断向量
LJMP T0IN ;T0IN对应于实际的中断服务程序的入口地址
ORG 0040H
START: ;主程序
中断向量:是指中断服务程序入口地址的偏移量与段基值,一个中断向量占据4字节空间。中断向量表是8088系统内存中最低端1K字节空间,它的作用就是按照中断类型号从小到大的顺序存储对应的中断向量,总共存储256个中断向量。在中断响应过程中,CPU通过从接口电路获取的中断类型号(中断向量号)计算对应中断向量在表中的位置,并从中断向量表中获取中断向量,将程序流程转向中断服务程序的入口地址。
80x86系统是把所有的中断向量集中起来,按中断类型号从小到大的顺序存放到存储器的某一区域内,这个存放中断向量的存储区叫做中断向量表,即中断服务程序入口地址表。(来源)
虽然这里说的是8088的中断向量但和MCS-51中的意思上是差不多的。
ORG是个用于定位的伪指令。
简单的说就是把从这句话开始直到下一个ORG指令或者END指令前的程序语句都顺序放在它指定的地址里。比如说你的程序里ORG只管了一个语句(AJMP MAIN),则从0000H这个地址开始放语句。放多少,看下面有几条语句(直到org或end 指令为止)。 同样ORG 0030H是把它后面的所有到下一个ORG或END命令前的所有代码都顺序放到从0030H开始的程序单元。(来源)
MCS-51
就这么规定的,其他的中断向量也都是确定的
外部中断INT0 0003H
定时/计数器T0溢出 000BH
外部中断INT1 0013H
定时/计数器T1溢出 001BH
串行口 0023H
定时/计数器T2溢出 002BH
(来源)
这里有点套娃的感觉。
当出现T0溢出的中断时,程序自动跑到000BH那,其实你可直接在ORG 000BH
后写你的中断服务程序,但是这样写完后主程序的起始地址就不好确定了(当然你可算出来)。所以我们用一个跳转指令跳转到实际的中断服务程序的入口地址那去。
MOV IE,#82H
高四位的8,二进制下就是1000B,这个1是因为:
EA—中断允许总开关控制位。
EA=0,所有的中断请求被屏蔽。
EA=1,所有的中断请求被开放。
高四位的2,二进制下就是0010B,这个1是因为:
ET0—定时器/计数器T0的溢出中断允许位。
ET0=0,禁止T0溢出中断。
ET0=1,允许T0溢出中断。
其他位是0,也就是说不响应其他类型的中断。
SETB EA
SETB ET0
只操作了IE中与定时器有关的位。
MOV TMOD,#01H
- GATE=1时,“与门”的输出信号K由INTx输入电平和TRx位的状态一起决定(即此时K=TRx·INTx),当且仅当TRx=1,INTx=1(高电平)时,计数启动;否则,计数停止。
当INT0引脚为高电平时且TR0置位,TR0=1;启动定时器T0; 当INT1引脚为高电平时且TR1置位,TR1=1;启动定时器T1。- GATE=0时,“或门”输出恒为1,“与门”的输出信号K由TRx决定(即此时K=TRx),定时器不受INTx输入电平的影响,由TRx直接控制定时器的启动和停止。
当TR0=1,启动定时器T0。 当TR1=1,启动定时器T1。
Ⅱ C/T——功能选择位
C/T=0时为定时功能: 加1计数器对脉冲f进行计数,每来一个脉冲,计数器加1,直到计时器TFx满溢出;
C/T=1时为计数功能:加1计数器对来自输入引脚T0(P3.4)和T1(P3.5)的外信号脉冲进行计数,每来一个脉冲,计数器加1,直到计时器TFx满溢出;
Ⅲ,M0、M1——方式选择功能
MCS-51的定时器T0有4种工作方式:方式0,方式1,方式2,方式3。
MCS-51的定时器T1有3种工作方式:方式0,方式1,方式2。
所以MOV TMOD,#01H
,就是我们将T0作为定时器并工作在方式一(16位定时器),且不受INT0或INT1输入电平的影响。
公式:
( 2 定 时 器 最 大 位 数 - s ) × 机 器 周 期 = t (2^{定时器最大位数 }- s)× 机器周期 =t (2定时器最大位数-s)×机器周期=t
其中:
机器周期: 12/CPU晶振频率(8051单片机一个机器周期=12个时钟周期,所以要乘上12)
s: 计数器初值
t: 定时时间
得到的s需要分成高8位和低8位,分别放入计数器THx和TLx中(x为0或1)。如果s为负数,说明需要的定时时间太长,即使定时器的最大时间也无法满足要求。这种情况下,需要加入软件循环才能实现。我们可以将需要的定时时间分成n份,利用定时器达到t/n的时间长度,然后在定时器处理程序中,累计某一变量,如果到达n,说明总的时间t已经达到。
本实验中单片机系统的频率是12M,即CPU晶振频率为 1.2 × 1 0 6 1.2\times10^{6} 1.2×106,所以机器周期为 1 0 − 6 10^{-6} 10−6s
定时器最大位数 是16
快速旋转,速度为60转/分,即1转/s
步进电机转动一周需要24步。
所以转动一步需要1/24秒
代入上述公式
( 2 16 - s ) × 1 0 − 6 = 1 / 24 , (2^{16} - s)× 10^{-6} =1/24, (216-s)×10−6=1/24,
计算得s=23870(十进制),转换为十六进制即为5D3EH。
P4SW EQU 0BBH
MOV P4SW,#30H
STC12系列因P4与其他NA(空)、ALE、EX_LVD功能复用,故需要配置P4SW才可作IO使用(默认不是)。(来源)
(来源:老师给的资料,不过你应该也可以在页眉的那个宏晶stc的网站那找到(浏览器提示网站提示包含有害程序,我不敢进))
我们应该是不能直接MOV P4SW,xxx
的,只能往P4SW的地址BBH那送。
30H中的3,二进制下就是0011B,也就是说我们把P4.4,P4.5作为IO口。
P4 EQU 0C0H
CLK EQU P4.4 ;时钟线
DAT EQU P4.5 ;数据线
p4不同于p1,2,3,我们不能直接用p4或p4.4,因为程序不知道p4是啥,所以我们得设置P4 为0C0H
,这个地址也是规定好的不能自己随便写一个。
后两条语句可有可无,只是为了表述更加清晰,不写的话,在后面用到的地方直接写p4.4也可以。
TABLE:
DB 0C0H,0F9H,0A4H,0B0H,99H,92H,82H,0F8H,80H,90H
分别对应于数字:0-9
MOV DPTR,#TABLE
其中:@为接寻址寄存器的前缀标志
CE1 EQU P1.1
CE2 EQU P1.4
CLR CE1 ;先置低
CLR CE2
SETB CE1 ;再置高
SETB CE2
(调了三个小时发现的!)
你可能会想,我直接置高不行吗?直接置高,步进电机有可能会抽搐而不是旋转。而先置低再置高,如果程序正确的话,不会抽搐。这么做的原理我和老师也不太清楚,如果有路过的大侠了解的话还请请教一二。(当然这可能只是我们实验用的机器的bug)
相当于开关,置高电机才能工作
剩下的就是具体的逻辑处理部分了,看代码吧
建议大家看完代码后,自己敲一遍,敲的时候凭自己的记忆和理解,就是不要照着别人的代码敲,那样没意义。实在想不起来,不知道该咋写的时候可以瞄一眼。
即使你看了别人的代码,自己敲完后还是会出现很多bug,而调bug的过程就是你成长的过程。
这里提醒一下程序中需要注意的地方:
#
,那编译器一般不会报错,编译器会把它当作一个地址来看待。最后程序各种奇怪的情况都有可能出现。
烂糟的代码不堪入目
优美的代码赏心悦目
如果你认为下面的代码不优美,我倒是要看看你的[大姨腔]
(本文的代码是用中断实现的,网上有些人的代码只是套了个中断的壳子,看似是中断实则是查询,查询方式的我也写过,没啥意思,这里就不贴啦)
(代码里都是push ACC为啥不是push A呢?如果你想知道为什么的话,移步我的这篇文章)
ORG 0000H
JMP START
ORG 000BH ;定时器T0的中断向量
JMP T0IN ;T0IN对应于实际的中断服务程序的入口地址
ORG 0040H ;这个0040H指的是ROM的地址
START:
;初始化堆栈指针
MOV SP,#50H;这个50H指的是内部RAM的地址。
;0-1FH 为 4 组工作寄存器区
;20H-2FH 为位寻址区域
;本程序中将30H-50H做为存放临时变量的区域;
;50H往后的作为堆栈区
;临时变量的地址
TEMP_ADDRESS1 EQU 30H
;中断和定时器
MOV IE,#82H ;设置允许“定时器T0溢出” 中断
MOV TMOD,#01H ;选择T0作为定时器并工作在方式一
;数码管
P4SW EQU 0BBH
MOV P4SW,#30H ;将P4.4,P4.5作为IO口。
P4 EQU 0C0H
CLK EQU P4.4 ;时钟线
DAT EQU P4.5 ;数据线
MOV DPTR,#TABLE ;设置数据指针
MOV R0,#0 ;个位初始化
MOV R1,#0 ;十位初始化
MOV R2,#0 ;百位初始化
;步进电机
IN1 EQU P3.2
IN2 EQU P1.0
CE1 EQU P1.1
CE2 EQU P1.4
CLR CE1 ;先置低
CLR CE2
SETB CE1 ;再置高
SETB CE2
;CLOCKWISE EQU 00101101B ;顺时针 ;仅为对照用,方便编码与调试
ANTICLOCKWISE EQU 01111000B ;逆时针
MOV R3,#6 ;R3为多少次时钟中断步进电机转一步,初始默认为慢速状态对应的6次
MOV R4,#ANTICLOCKWISE ;R4为步进电机的4个节拍
;开关
S1 EQU P3.6
S2 EQU P3.7
S1_state EQU 30H
NEXT:
MOV TH0,#5DH ;计数器初值
MOV TL0,#3EH
SETB TR0 ;开始计时
;此后每到一定的时间,步进电机转一步,数码管显示的数字加一
IDLE: JMP IDLE
;T0溢出中断服务程序
T0IN:
MOV TH0,#5DH ;计数器初值
MOV TL0,#3EH
//SETB TR0 ;这里不用重新把TR0置高
DJNZ R3,T0IN_EXIT
;R3减到0时则证明电机应该转动一步了,需要给R3重新赋值
SLOW:
JNB S1,QUICK;如果S1为0则转移,S1按下为0,S1按下代表快速模式
MOV R3,#6 ;慢速状态转一下的时间间隔是快速状态的六倍
JMP LB0
QUICK:
MOV R3,#1
LB0:
CALL COUNT_AND_SHOW_ALL_NUM
CALL ROTATE_ONE_STEP
T0IN_EXIT:
RETI
;步进电机转动一步
ROTATE_ONE_STEP:
PUSH ACC ;保护现场
ROS_S2_OFF: ;S2没按下,电机逆时针转
JNB S2,ROS_S2_ON
MOV A,R4
RLC A ;循环左移
;这里RLC是带C的循环左移,是把A的最左边那一位移到C,把C移到A的最右边那一位
;这不是我们期望得到的A
;为了得到正确的A,我们得在得到C后,把A复原然后RL
;RL是不带C的循环左移,会把A的最左边那一位移到A的最右边那一位
MOV IN1,C
MOV A,R4 ;复原
RL A ;得到正确的A
MOV R4,A ;存正确的A
RLC A
MOV IN2,C
MOV A,R4 ;复原
RL A ;得到正确的A
MOV R4,A;存正确的A
;MOV P0,R4 ;这里只是提醒大家可以用P0口调试
JMP ROS_EXIT
ROS_S2_ON: ;S2按下,电机顺时针转
MOV A,R4
RRC A ;循环右移
MOV IN2,C
;顺时针与逆时针不同的地方就在于节拍的顺序
;注意这里我们把第一个右移得到的C送IN2口而不是IN1
;为什么这么做,说实话不太好描述
;你可以看主程序中对顺逆时针节拍的定义来理解
MOV A,R4 ;复原
RR A ;得到正确的A
MOV R4,A ;存正确的A
RRC A
MOV IN1,C
MOV A,R4 ;复原
RR A ;得到正确的A
MOV R4,A;存正确的A
ROS_EXIT:
POP ACC ;恢复现场
RET
;数字加一并显示三个数码管
COUNT_AND_SHOW_ALL_NUM:
PUSH ACC
MOV A,R0 ;个位 ;一定要先输入个位,因为先最先输入的在最右边
CALL SHOW_ONE_NUM
MOV A,R1 ;十位
CALL SHOW_ONE_NUM
MOV A,R2 ;百位
CALL SHOW_ONE_NUM
INC R0
CJNE R0,#0AH,SA_EXIT ;逢十进一
MOV R0,#0;
INC R1
CJNE R1,#0AH,SA_EXIT
MOV R1,#0;
INC R2
CJNE R2,#0AH,SA_EXIT
MOV R2,#0;
SA_EXIT:
POP ACC
RET
;一个数码管的显示
SHOW_ONE_NUM:
;保护现场
PUSH ACC
PUSH TEMP_ADDRESS1
MOV TEMP_ADDRESS1,R0
PUSH TEMP_ADDRESS1
MOVC A,@A+DPTR ;取A中数字对应的编码到A
MOV R0,#8
;时钟 (CLK) 每次由低变高时,数据左移一位
SON_1:
CLR CLK ;时钟线置低
RLC A
MOV DAT,C
SETB CLK ;时钟线置高
DJNZ R0,SON_1
;恢复现场
POP TEMP_ADDRESS1
MOV R0,TEMP_ADDRESS1
POP TEMP_ADDRESS1
POP ACC
RET
;共阳极数码管编码表
TABLE: DB 0C0H,0F9H,0A4H,0B0H,99H,92H,82H,0F8H,80H,90H
end
MOVC
进行读操作MOVX
进行读写操作MOV
进行读写操作其中位寻址区共有16个字节,128个位,位地址为00H—7FH。位地址分配如表1所示,CPU能直接寻址这些位,执行例如置“1”、清“0”、求“反”、转移,传送和逻辑等操作。(来源)
栈操作:进栈PUSH
弹栈POP
(PUSH/POP能对什么进行操作是有讲究的,详情可以看我的这篇文章)
MOVC:MOVC是累加器与程序存储区之间的数据传送指令。它比MOV指令多了一个字母“C”,这个“C”就是“Code”的意思,翻译过来就是“代码”的意思,就是代码区(程序存储区)与A之间的数据传送指令。它可以用于内部程序存储区(内部ROM)与A之间的数据传送,也可以
用于外部程序存储区(外部ROM)与A之间的数据传送。(来源)
以 16 位的程序计数器 PC 或数据指针 DPTR 作为基寄存器,以 8 位 的累加器 A 作为变址寄存器,基址寄存器和变址寄存器的内容相加作为 16 位的地址来访问程序存储区。
如:
MOVC A,@A+PC
MOVC A, @A+DPTR
时序是用定时单位来描述的,MCS-51的时序单位有四个,它们分别是节拍、状态、机器周期和指令周期,接下来我们分别加以说明。
·节拍与状态:
我们把振荡脉冲的周期定义为节拍(为方便描述,用P表示),振荡脉冲经过二分频后即得到整个单片机工作系统的时钟信号,把时钟信号的周期定义为状态(用S表示),这样一个状态就有两个节拍,前半周期相应的节拍我们定义为1(P1),后半周期对应的节拍定义为2(P2)。
·机器周期:
MCS-51有固定的机器周期,规定一个机器周期有6个状态,分别表示为S1-S6,而一个状态包含两个节拍,那么一个机器周期就有12个节拍,我们可以记着S1P1、S1P2……S6P1、S6P2,一个机器周期共包含12个振荡脉冲,即机器周期就是振荡脉冲的12分频,显然,如果使用6MHz的时钟频率,一个机器周期就是2us,而如使用12MHz的时钟频率,一个机器周期就是1us。
·指令周期:
执行一条指令所需要的时间称为指令周期,MCS-51的指令有单字节、双字节和三字节的,所以它们的指令周期不尽相同,也就是说它们所需的机器周期不相同,可能包括一到四个不等的机器周期。
·MCS-51的指令时序:
MCS-51指令系统中,按它们的长度可分为单字节指令、双字节指令和三字节指令。执行这些指令需要的时间是不同的,也就是它们所需的机器周期是不同的,有下面几种形式:
·单字节指令单机器周期
·单字节指令双机器周期
·双字节指令单机器周期
·双字节指令双机器周期
·三字节指令双机器周期
·单字节指令四机器周期(如单字节的乘除法指令)
(来源)
那些双机器周期和四机器周期的指令执行时间较长。