实验三 步进电机原理及应用 汇编实现

单片机:MCS-51系列 STC12C5A32S2

本人学习单片机也没多久,基本上都是现学现卖,有不懂或不对的地方->评论区留言

实验内容:

  • 编制MCS-51程序使步进电机按照规定的转速和方向进行旋转,并将已转动的步数显示在数码管上。
  • 步进电机的转速分为两档,当按下S1开关时,进行快速旋转,速度为60转/分。当松开开关时,进行慢速旋转,速度为10转/分。当按下S2开关时,按照顺时针旋转;当松开时,按照逆时针旋转。
  • 本程序要求使用定时器中断来实现,不准使用程序延时的方式。

    文章目录

        • 1. 中断部分
          • 1.1设置 “定时器T0溢出” 中断的中断向量
            • 1.1.1 为啥要这么写?
            • 1.1.2 中断向量是啥?
            • 1.1.3 ORG 是啥?
            • 1.1.4 为什么是000BH?
            • 1.1.5 LJMP T0IN 是?
          • 1.2设置允许“定时器T0溢出” 中断
            • 1.2.1法一
              • 1.2.1.1 中断允许寄存器IE
            • 1.2.2法二
        • 2. 定时器部分
          • 2.1 设置“定时器T0”工作方式
          • 2.2 计算定时器初值
        • 3. 数码管部分
          • 3.1 设置P4SW
          • 3.2 设置P4
          • 3.3 设置共阳极数码管编码表
            • 3.3.1怎么看是共阴还是共阳
          • 3.4 设置数据指针 DPTR
            • 3.4.1 为啥要设置DPTR?
        • 4. 步进电机部分
          • 4.1 将CE1和CE2分别置为高
            • 4.1.1 CE1和CE2口得先置低再置高
            • 4.1.1 为啥要CE1和CE2要置高?
        • 5. 完整代码
        • 6. 思考题

1. 中断部分

1.1设置 “定时器T0溢出” 中断的中断向量

(当然你也可以使用定时器T1)
(要实现这个实验的功能的话用查询的方式也能做,但是实验要求要用中断)

ORG  0000H
LJMP  START
ORG   000BH ;定时器T0的中断向量
LJMP  T0IN  ;T0IN对应于实际的中断服务程序的入口地址
ORG   0040H
START: ;主程序
1.1.1 为啥要这么写?

因为这是MCS51单片机中断系统的主程序结构
实验三 步进电机原理及应用 汇编实现_第1张图片

( 来源)

1.1.2 中断向量是啥?

中断向量:是指中断服务程序入口地址的偏移量与段基值,一个中断向量占据4字节空间。中断向量表是8088系统内存中最低端1K字节空间,它的作用就是按照中断类型号从小到大的顺序存储对应的中断向量,总共存储256个中断向量。在中断响应过程中,CPU通过从接口电路获取的中断类型号(中断向量号)计算对应中断向量在表中的位置,并从中断向量表中获取中断向量,将程序流程转向中断服务程序的入口地址。

80x86系统是把所有的中断向量集中起来,按中断类型号从小到大的顺序存放到存储器的某一区域内,这个存放中断向量的存储区叫做中断向量表,即中断服务程序入口地址表。(来源)

虽然这里说的是8088的中断向量但和MCS-51中的意思上是差不多的。

1.1.3 ORG 是啥?

ORG是个用于定位的伪指令。
简单的说就是把从这句话开始直到下一个ORG指令或者END指令前的程序语句都顺序放在它指定的地址里。比如说你的程序里ORG只管了一个语句(AJMP MAIN),则从0000H这个地址开始放语句。放多少,看下面有几条语句(直到org或end 指令为止)。 同样ORG 0030H是把它后面的所有到下一个ORG或END命令前的所有代码都顺序放到从0030H开始的程序单元。(来源)

1.1.4 为什么是000BH?

MCS-51就这么规定的,其他的中断向量也都是确定的

外部中断INT0 0003H
定时/计数器T0溢出 000BH
外部中断INT1 0013H
定时/计数器T1溢出 001BH
串行口 0023H
定时/计数器T2溢出 002BH
(来源)

1.1.5 LJMP T0IN 是?

这里有点套娃的感觉。
当出现T0溢出的中断时,程序自动跑到000BH那,其实你可直接在ORG 000BH后写你的中断服务程序,但是这样写完后主程序的起始地址就不好确定了(当然你可算出来)。所以我们用一个跳转指令跳转到实际的中断服务程序的入口地址那去。

1.2设置允许“定时器T0溢出” 中断

1.2.1法一
MOV IE,#82H
1.2.1.1 中断允许寄存器IE

在这里插入图片描述

( 来源)

高四位的8,二进制下就是1000B,这个1是因为:

EA—中断允许总开关控制位。
EA=0,所有的中断请求被屏蔽。
EA=1,所有的中断请求被开放。

高四位的2,二进制下就是0010B,这个1是因为:

ET0—定时器/计数器T0的溢出中断允许位。
ET0=0,禁止T0溢出中断。
ET0=1,允许T0溢出中断。

其他位是0,也就是说不响应其他类型的中断。

1.2.2法二
SETB EA
SETB ET0 

只操作了IE中与定时器有关的位。

2. 定时器部分

2.1 设置“定时器T0”工作方式

MOV TMOD,#01H 

实验三 步进电机原理及应用 汇编实现_第2张图片

( 来源)

Ⅰ GATE——门控制。

  • 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。

实验三 步进电机原理及应用 汇编实现_第3张图片

( 来源)

所以MOV TMOD,#01H ,就是我们将T0作为定时器并工作在方式一(16位定时器),且不受INT0或INT1输入电平的影响。

2.2 计算定时器初值

公式:
( 2 定 时 器 最 大 位 数 - s ) × 机 器 周 期 = t (2^{定时器最大位数 }- s)× 机器周期 =t 2s×=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} 106s
定时器最大位数 是16
快速旋转,速度为60转/分,即1转/s
步进电机转动一周需要24步。
所以转动一步需要1/24秒

代入上述公式
( 2 16 - s ) × 1 0 − 6 = 1 / 24 , (2^{16} - s)× 10^{-6} =1/24, 216s×106=1/24
计算得s=23870(十进制),转换为十六进制即为5D3EH。

3. 数码管部分

3.1 设置P4SW

P4SW EQU 0BBH
MOV P4SW,#30H

STC12系列因P4与其他NA(空)、ALE、EX_LVD功能复用,故需要配置P4SW才可作IO使用(默认不是)。(来源)

实验三 步进电机原理及应用 汇编实现_第4张图片

(来源:老师给的资料,不过你应该也可以在页眉的那个宏晶stc的网站那找到(浏览器提示网站提示包含有害程序,我不敢进))

我们应该是不能直接MOV P4SW,xxx的,只能往P4SW的地址BBH那送。
30H中的3,二进制下就是0011B,也就是说我们把P4.4,P4.5作为IO口。

3.2 设置P4

P4 EQU 0C0H
CLK EQU P4.4 ;时钟线
DAT EQU P4.5 ;数据线

p4不同于p1,2,3,我们不能直接用p4或p4.4,因为程序不知道p4是啥,所以我们得设置P4 为0C0H,这个地址也是规定好的不能自己随便写一个。
后两条语句可有可无,只是为了表述更加清晰,不写的话,在后面用到的地方直接写p4.4也可以。

3.3 设置共阳极数码管编码表

TABLE:
DB 0C0H,0F9H,0A4H,0B0H,99H,92H,82H,0F8H,80H,90H

分别对应于数字:0-9

3.3.1怎么看是共阴还是共阳

公共端接高电平所以是共阳
实验三 步进电机原理及应用 汇编实现_第5张图片

3.4 设置数据指针 DPTR

MOV DPTR,#TABLE
3.4.1 为啥要设置DPTR?

因为我们要用变址寻址方式来获取不同数数字对应的编码
实验三 步进电机原理及应用 汇编实现_第6张图片

( 来源)

其中:@为接寻址寄存器的前缀标志

4. 步进电机部分

4.1 将CE1和CE2分别置为高

	CE1 EQU P1.1
	CE2 EQU P1.4
	CLR CE1  	;先置低
	CLR CE2 	
	SETB CE1 	;再置高
	SETB CE2 	
4.1.1 CE1和CE2口得先置低再置高

(调了三个小时发现的!)
你可能会想,我直接置高不行吗?直接置高,步进电机有可能会抽搐而不是旋转。而先置低再置高,如果程序正确的话,不会抽搐。这么做的原理我和老师也不太清楚,如果有路过的大侠了解的话还请请教一二。(当然这可能只是我们实验用的机器的bug)

4.1.1 为啥要CE1和CE2要置高?

相当于开关,置高电机才能工作

剩下的就是具体的逻辑处理部分了,看代码吧

5. 完整代码

建议大家看完代码后,自己敲一遍,敲的时候凭自己的记忆和理解,就是不要照着别人的代码敲,那样没意义。实在想不起来,不知道该咋写的时候可以瞄一眼。
即使你看了别人的代码,自己敲完后还是会出现很多bug,而调bug的过程就是你成长的过程。
这里提醒一下程序中需要注意的地方

  • 对于直接拿来用,即用之前未赋值的变量记得初始化
    比如这个实验中的数码管的个位,十位,百位,如果不初始化,直接+1的话,数码管显示就会异常。
  • 区分立即数和地址
    如果你想送立即数但忘了加#,那编译器一般不会报错,编译器会把它当作一个地址来看待。最后程序各种奇怪的情况都有可能出现。
  • 循环左移或右移完的节拍,如果还需要用到的话,记得再存回去
    不然电机不转
  • 注意进制
    10H是十进制下的16不是10

 
烂糟的代码不堪入目
优美的代码赏心悦目
如果你认为下面的代码不优美,我倒是要看看你的[大姨腔]
(本文的代码是用中断实现的,网上有些人的代码只是套了个中断的壳子,看似是中断实则是查询,查询方式的我也写过,没啥意思,这里就不贴啦)
(代码里都是push ACC为啥不是push A呢?如果你想知道为什么的话,移步我的这篇文章)

ORG  0000H
JMP  START
ORG   000BH ;定时器T0的中断向量
JMP  T0IN  ;T0IN对应于实际的中断服务程序的入口地址
ORG   0040H ;这个0040H指的是ROM的地址
START: 
	;初始化堆栈指针
	MOV SP,#50H;这个50H指的是内部RAM的地址。
	;01FH 为 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

6. 思考题

  1. 如采用单四拍工作模式,每次步进角度是多少,程序要如何修改?
    无论你是单四拍还是双四拍,那都是四拍啊,所以单四拍的步进角度等于双四拍的步进角度的: 360 / 24 = 1 5 。 360/24=15^。 360/24=15
    修改输出脉冲为A、B、-A、-B
    程序中对应于 1,1,0,0
  2. 如采用单双八拍工作模式,每次步进角度是多少,程序要如何修改?
    八拍的步进角度是四拍的一半: 7. 5 。 7.5^。 7.5
    修改输出脉冲为:A、AB、B、-AB、-A、-A-B、-B、A-B;
    程序中对应于 1,10,0,01,0,00,0,10
  3. 步进电机的转速取决于那些因素?有没有上、下限?
    如果单就一种电机来说的话,取决于脉冲频率。如果不同电机之间对比的话,还有转子齿数和定子相数有关。由于各种物理因素(包括摩 擦、机械惯性、响应时间等),步进电机的最高转速有限制。上限根据电机的差别而不同,下限则为0。
  4. 如何改变步进电机的转向?
    将脉冲的顺序反向即将01->11->10->00改为:00->10->11->01
  5. 步进电机有那些规格参数,如何根据需要选择型号?
    步进电机的主要参数有最大工作电压、最小启动电压、最大允许功耗、空载启动频率和工作频率等。可以根据定位精度和最大速度进行选择。
  6. MCS51中有哪些可存取的单元,存取方式如何?它们之间的区别和联系有哪些?
  • 程序存储器ROM
    寻址范围:0000H ~ FFFFH 容量64KB
    其中包括内部ROM和外部ROM,但内外统一编址。
    使用MOVC进行读操作
  • 外部数据存储器
    在一般情况下,单片机都不需要很多的数据存储器,这时内部的 128 字节
    RAM 基本是够用的。在有些情况下,需要大量的数据存储器,这时可以外接最 大直至 64K 的外部数据存储器。
    外部数据存储器的寻址空间也可以直接作为扩展 I/O 口的寻址空间使用,对
    此 CPU 使用相同的操作指令来读写。在简单的扩展中,常使用位选的方式来简 单扩展端口。
    使用MOVX进行读写操作
  • 内部 RAM 数据存储器
    MCS-51 单片机的内部 RAM 的寻址空间为 256 字节,实际提供给用户的通用 RAM 空间一般为前 128 字节。内部 RAM的不同地址区域从功能和用途可以区 分为:内部工作寄存器区/通用寄存器区,位寻址区,堆栈和通用数据存储区。
    寄存器之间、寄存器与通用存储区之间使用MOV进行读写操作

其中位寻址区共有16个字节,128个位,位地址为00H—7FH。位地址分配如表1所示,CPU能直接寻址这些位,执行例如置“1”、清“0”、求“反”、转移,传送和逻辑等操作。(来源)

       栈操作:进栈PUSH弹栈POP
       (PUSH/POP能对什么进行操作是有讲究的,详情可以看我的这篇文章)

  • 特殊功能寄存器 SFR
    SFR 离散地分布在 内部RAM 高128位(80H-0FFH )的特殊功能寄存器地址空间中。
    MCS‐51 内部的 I/O 口锁存器及定时器、串行口、中断等各种控制寄存器和 状态寄存器等都称为特殊功能寄存器。它们控制着各种 CPU 周边设备,保存着 它们的状态。不同 型号的单片机内部I/O功能有所不同,实际存在的特殊功能寄存器数量差别较大。 特殊功能寄存器空间中有些单元是空着的,在有些控制寄存器中的某些位也没有 定义。这些单元是为了以后新型的单片机保留的,为了软件与以后的新型号兼容, 用户程序不要对这些空单元进行操作。 特殊功能寄存器中地址为 8 的倍数的寄存器可以进行位寻址,它们的位地址位于 80H 到 0FFH 的地址空间。它们的位地址恰好等于 SFR 的地址加上位的次序 (如 PSW 第 7 位 CY 的位地址是 PSW 的地址 0D0H 加上 7,即 0D7H)。
  1. 说明MOVC指令的使用方法。

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

  1. MCS51的指令时序是什么样的,哪类指令的执行时间较长?

时序是用定时单位来描述的,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指令系统中,按它们的长度可分为单字节指令、双字节指令和三字节指令。执行这些指令需要的时间是不同的,也就是它们所需的机器周期是不同的,有下面几种形式:
·单字节指令单机器周期
·单字节指令双机器周期
·双字节指令单机器周期
·双字节指令双机器周期
·三字节指令双机器周期
·单字节指令四机器周期(如单字节的乘除法指令)
(来源)

      那些双机器周期和四机器周期的指令执行时间较长。

  1. 在本实验环境下,能否控制显示数码的亮度?如何实现?
    能,具体操作是在送完要显示的数字的编码后,送不显示任何数字的编码,通过控制显示与不显示的时间的比例来控制亮度。

你可能感兴趣的:(C51单片机,单片机,嵌入式,汇编,MCS-51)