单片机第一次作业
题目要求
- 安装 Keil 和 Proteus,熟悉 Keil 编译和可执行文件的生成操作和 Proteus
仿真操作
- 使用汇编语言编写代码实现单片机引脚输出 PWM 波形,使用 Proteus
中的示波器查看波形
- 使用按键控制 PWM 的延时长度(占空比?)
- 录制视频发至 B 站
题目实现
- 安装步骤及工程创建省略,具体操作可以类比。
- 代码实现思路:首先建立一个延时时间为
1ms(0.999285ms)的延时函数作为时基,然后通过调用该延时函数来控制高低电平的维持时间。主函数中通过每隔一定时间拉高拉低电平来实现
PWM 波形的生成。主函数中还包含按键检测的电路,进入 LOOP
前后都要进行按键检测。PWM 的占空比范围为
0%~100%,可以使用按键来控制每次按下按键时 PWM 的占空比增加 10%。
- 仿真电路搭建:在 Component Mode 中选择器件 AT89C51 和 button,在
Termianl Mode 中选择 Ground,在 Virtual Instrument Mode 中选择
OSCILLOSCOPE
代码详解
为了实现目标程序,我们首先实现一个目标程序的子集:我们首先设计一个占空比为
50% 的 PWM,这个代码是非常好理解的:
1 2 3 4
| SETB P2.1 ;设置P2^1端口为高电平 LCALL DELAY ;跳转执行延时子函数 CLR P2.1 ;设置P2^1端口为低电平 LCALL DELAY ;设置P2^1端口为高电平
|
这是我们的延时代码
1 2 3 4 5 6 7 8 9 10 11 12
| ;-------------------------------时基,单位msBEGIN------------------------------- DELAY: ;延时:1T+153*(4*1T+2T)+2T = 921T;921T = 921*1.085us = 0.999285ms MOV R2,#153 ;1T HERE: NOP ;1T NOP ;1T NOP ;1T NOP ;1T DJNZ R2,HERE ;2T NOP ;1T RET ;2T ;---------------------------------时基,单位msEND--------------------------------
|
然后我们实现第二个小目标:设置一个占空比可调的占空比
为此我们给高电平一个时间,给低电平另一个时间
1 2 3 4
| SETB P2.1 LCALL HOLDHIGH CLR P2.1 LCALL HOLDLOW
|
这是使用循环嵌套进行高低电平持续时间的设置。我们设置 PWM 的周期为 100
个 DELAY,然后就可以非常方便的表示占空比:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ;--------------------------------高电平维持时间BEGIN---------------------------- HOLDHIGH: ;延时:1T+30*921T+2T+2T = 27635T;27635T = 27635*1.085us = 29.983975ms ;延时:1T+x*921T+2T+2T = 921x+5T;921x+5T = 999.285x+0.000543ms MOV R5,30 ;1T AGAINH: LCALL DELAY ;921T DJNZ R5,AGAINH ;2T RET ;2T ;--------------------------------高电平维持时间END-------------------------------
;------------------------------低电平维持时间BEGIN------------------------------- HOLDLOW: ;延时:1T+70*921T+2T+2T = 64475T;64475T = 64475*1.085us = 69.955375ms ;延时:1T+1T+1T+1T+x*921T+2T+2T = 921x+8T;921x+8T = 999.285x+0.000868ms ;x = 70,69.950ms MOV R5,70 ;1T AGAINL: LCALL DELAY ;921T DJNZ R5,AGAINL ;2T RET ;2T ;-------------------------------低电平维持时间END--------------------------------
|
但是这种情况下如果在源代码中改变占空比的话,我们需要改动两个值,现在我们希望的是,改动一处的值然后就能得到周期
100ms 的占空比正确的 PWM。
于是我们进行了下面的修改:
1 2 3 4 5 6 7 8 9 10 11 12 13
| ;------------------------------低电平维持时间BEGIN------------------------------- HOLDLOW: ;延时:1T+70*921T+2T+2T = 64475T;64475T = 64475*1.085us = 69.955375ms ;延时:1T+1T+1T+1T+x*921T+2T+2T = 921x+8T;921x+8T = 999.285x+0.000868ms ;x = 70,69.950ms MOV R5,30 ;1T MOV A,#100 ;1T SUBB A,R5 ;1T MOV R5,A ;1T AGAINL: LCALL DELAY ;921T DJNZ R5,AGAINL ;2T RET ;2T ;-------------------------------低电平维持时间END--------------------------------
|
最后,我们使用寄存器 B 来存储占空比:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| MOV B,#30 ;--------------------------------高电平维持时间BEGIN---------------------------- HOLDHIGH: ;延时:1T+30*921T+2T+2T = 27635T;27635T = 27635*1.085us = 29.983975ms ;延时:1T+x*921T+2T+2T = 921x+5T;921x+5T = 999.285x+0.000543ms MOV R5,B ;1T AGAINH: LCALL DELAY ;921T DJNZ R5,AGAINH ;2T RET ;2T ;--------------------------------高电平维持时间END-------------------------------
;------------------------------低电平维持时间BEGIN------------------------------- HOLDLOW: ;延时:1T+70*921T+2T+2T = 64475T;64475T = 64475*1.085us = 69.955375ms ;延时:1T+1T+1T+1T+x*921T+2T+2T = 921x+8T;921x+8T = 999.285x+0.000868ms ;x = 70,69.950ms MOV R5,B ;1T MOV A,#100 ;1T SUBB A,R5 ;1T MOV R5,A ;1T AGAINL: LCALL DELAY ;921T DJNZ R5,AGAINL ;2T RET ;2T ;-------------------------------低电平维持时间END--------------------------------
|
接下来的目标是添加按键逻辑。
首先我们来思考一下按键的逻辑是什么样子的:首先我们假设初始时寄存器 B
的值是 10,即占空比为 10%
- 按键按下:寄存器 B 中的值加 10
- 按键松开:执行 IO 口高低电平的切换
依照这种逻辑,我们有代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ;-----------------------------------主函数BEGIN--------------------------------- MAIN: SETB P2.1 ;1T LCALL HOLDHIGH ;2T CLR P2.1 ;1T LCALL HOLDLOW ;2T JB P2.0,MAIN ;P2.1为1的时候循环,按键按下变为0时进入ADD指令 MOV A,B ADD A,#10 MOV B,A SJMP MAIN ;-----------------------------------主函数END-----------------------------------
|
在这种逻辑下,我们会发现我们已经实现了使用按键控制 PWM
的占空比。在仿真工程中我们也可以看到每次按下按键时输出的 PWM
占空比都会发生变化。
但是,如果我们仔细观察占空比的变化,我们会发下每次按下时增长的占空比比预期的
10% 要多。这是为什么呢?
这要来重新思考我们的按键逻辑,我们来重新整理一下:
- 按键按下:寄存器 B 中的值加==一次== 10
- 按键等待:按键等待用户松开按键,确认这是一次按键操作
- 按键松开:一次按键操作后改变了占空比输出 PWM
其实,这里的关键在于,当按键按下时,我们需要等待,等待按键被松开,然后才判断为
1
次按下。如果不进行等待(不进行阻塞),那么当按下的时候,那么代码一直在循环执行按键按下的操作,即寄存器
B 加 10
于是我们得到了新的按键控制逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ;-----------------------------------主函数BEGIN--------------------------------- MAIN: JNB P2.0,MAIN ;使用阻塞的方式检测按键松开 LOOP: SETB P2.1 ;1T LCALL HOLDHIGH ;2T CLR P2.1 ;1T LCALL HOLDLOW ;2T JB P2.0,LOOP ;P2.1为1的时候循环,按键按下变为0时进入ADD指令 MOV A,B ADD A,#10 MOV B,A SJMP MAIN ;-----------------------------------主函数END-----------------------------------
|
此时我们已经能较为完美的进行 PWM 占空比控制了
其实我一开始就是写道这个程度,但是当我整理这份报告的时候,我突然发现其实还有一个重要的改进空间,那就是上述代码在按键按下时在一直等待,而因为我们的
PWM
的周期较小,我们在按下的时候会出现某一段波形为空的状态(波形持续输出一段高电平或者低电平),这是因为我们在按键等待的时候即这个循环内:
1 2
| MAIN: JNB P2.0,MAIN ;使用阻塞的方式检测按键松开
|
什么都没做。这种波形的中断,在一些场合下可能是致命的,因此我们有必要做一些改进。
我这里采用了一个非常简单的方法进行改进,就是在这个循环体内也加入 PWM
生成代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ;-----------------------------------主函数BEGIN-------------------------------- MAIN: SETB P2.1 ;1T LCALL HOLDHIGH ;2T CLR P2.1 ;1T LCALL HOLDLOW ;2T JNB P2.0,MAIN ;使用阻塞的方式检测按键松开 LOOP: SETB P2.1 ;1T LCALL HOLDHIGH ;2T CLR P2.1 ;1T LCALL HOLDLOW ;2T JB P2.0,LOOP ;P2.1为1的时候循环,按键按下变为0时进入ADD指令 MOV A,B ADD A,#10 MOV B,A SJMP MAIN ;-----------------------------------主函数END-----------------------------------
|
这样就比较丝滑的做到了使用按键控制 PWM
仿真详解
比较简单,略。
单片机第二次作业
题目要求
流水灯实验
- 八个LED灯加两个按键
- 一个按键用于控制流水灯的样式
- 另一个按键用于控制流水灯的延时
题目实现
- 创建 Keil 工程和 Proteus 工程
- 选择 P2 口连接八个 LED 灯,选择 P1.0 和 P1.1作为按键输入端口
- 编写代码
代码详解
首先我们先写一个流水灯代码。由于这次要同时操作八个流水灯,因此我们可以直接操作
Port 而不是对其中的单个端口进行操作。一个位的信息可以控制一个
Pin,因此一个 Port 可以使用 8 位二进制数表示。这种情况下想让 LED
灯全亮的代码为:
1 2
| #define LED P2 LED = 0xff;
|
使用移位运算可以改变 Pin 的状态。例如,上两行代码执行过后 P2
口的状态为
1 2 3 4 5 6 7 8
| P20 --> 1 P21 --> 1 P22 --> 1 P23 --> 1 P24 --> 1 P25 --> 1 P26 --> 1 P27 --> 1
|
我们可以使用移位运算来改变 P2 的状态:
执行这段代码后,状态变为:
1 2 3 4 5 6 7 8
| P20 --> 1 P21 --> 1 P22 --> 1 P23 --> 1 P24 --> 1 P25 --> 1 P26 --> 1 P27 --> 0
|
那么流水效果只需要配合循环即可实现:同时因为灯是先全亮然后再从上往下灭,因此我们封装成一个函数起名为
up_to_down()
1 2 3 4 5 6 7 8 9 10
| void up_to_down(unsigned char delay_time) { unsigned char i, j; for (i = 0; i < 9; i++) { LED = 0xff << i; for (j = 0; j < delay_time; j++) Delay10ms(); } }
|
同理我们可以写出第二种流水方式——自下向上亮
1 2 3 4 5 6 7 8 9 10
| void down_to_up(unsigned char delay_time) { unsigned char i, j; for (i = 0; i < 9; i++) { LED = 0xff << 8 - i; for (j = 0; j < delay_time; j++) Delay10ms(); } }
|
封装好之后我们可以在 main()
函数中调用了。方法是设置一个状态变量,然后不停的查询它,每种状态对应一种流水灯流水方式,流水方式的选择可以使用
switch
语句来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| switch (state) { case 0: init_led(); break; case 1: down_to_up(delay_time); break; case 2: up_to_down(delay_time); break; default: break; }
|
最后我们要做的就是将按键控制逻辑加进去,来控制状态变量
state
和延时长度变量 delay_time
发生变化。
一种简单的逻辑是,既然按键也需要单片机轮询扫描,那么我们可以也加入到
while(1)
中,于是我们得到这样的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| void main() { unsigned char state = 0; unsigned char delay_time = 10;
init_led(); init_button();
while (1) { if (button1 == 0) { Delay10ms(); if (button1 == 0) { while(button1 == 0); if (state < 2) state++; else state = 0; } } if (button2 == 0) { Delay10ms(); if (button2 == 0) { while(button2 == 0); if (delay_time < 100) delay_time = delay_time + 10; else delay_time = 10; } } switch (state) { case 0: init_led(); break; case 1: down_to_up(delay_time); break; case 2: up_to_down(delay_time); break; default: break; } } }
|
然后我们可以开始仿真。得到的结果是可以改变流水效果和延时。但是同时也有问题,就是我们的按键在很多情况下是不管用的,只有少数情况下能够起作用。
这是因为我们在 while(1)
中除了扫描按键,还执行了流水灯的代码,而流水灯的代码存在很长的延时,当代码执行到这些延时的时候,即便是按下按键,没有正在执行按键检测逻辑,那么就不能正确的识别按键按下的操作。
解决这个方法最好的办法是,不管我们现在在做什么,只要按键被按下,就让单片机停下手头在做的事情,先帮我们改变
state
变量和 delay_time
,然后再让单片机回到它之前进行的位置比如执行延迟。这样一来就可以实现按键的检测而不影响点灯了。
这种实现思路在单片机上是可以做到的,但是需要使用到我们目前还没有学过的知识,因此我们换一种方式来解决。
这里我们首先将按键检测代码封装成一个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| void check_button() { Delay10ms(); if (button1 == 0) { while (button1 == 0); if (state < 2) state++; else state = 0; } if (button2 == 0) { while (button2 == 0); if (delay_time < 100) delay_time = delay_time + 10; else delay_time = 10; } }
|
使用方法:
1 2
| if (button1 == 0 || button2 == 0); check_button();
|
然后,前面问题的解决方案就是,在每一轮延迟循环中我们都进行按键检测。因为按键检测的代码复杂度不高(只有分支语句,时间复杂度只有
\(O(1)\)
),因此我们将按键检测代码放到流水灯中。又因为流水灯代码中时间复杂度最高的(为
\(O(n²)\)
)是延时函数,因此将按键检测代码放在延迟中去。
最终结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| #include <reg52.h>
#define LED P2 sbit button1 = P1 ^ 0; sbit button2 = P1 ^ 1; sbit error = P1 ^ 3;
unsigned char state = 0; unsigned char delay_time = 10;
void init_led(); void init_button(); void check_button(); void Delay10ms(); void up_to_down(); void down_to_up();
void main() { init_led(); init_button();
while (1) { switch (state) { case 0: init_led(); break; case 1: down_to_up(); break; case 2: up_to_down(); break; default: error = 0; break; } } }
void init_led() { LED = 0x00; if (button1 == 0 || button2 == 0) check_button(); } void init_button() { button1 = 1; button2 = 1; } void Delay10ms() { unsigned char i, j;
i = 18; j = 235; do { while (--j); } while (--i); } void check_button() { Delay10ms(); if (button1 == 0) { while (button1 == 0); if (state < 2) state++; else state = 0; } if (button2 == 0) { while (button2 == 0); if (delay_time < 100) delay_time = delay_time + 10; else delay_time = 10; } } void up_to_down() { unsigned char i, j; for (i = 0; i < 9; i++) { LED = 0xff << i; for (j = 0; j < delay_time; j++) { if (button1 == 0 || button2 == 0) check_button(); else Delay10ms(); } } } void down_to_up() { unsigned char i, j; for (i = 0; i < 9; i++) { LED = 0xff << 8 - i; for (j = 0; j < delay_time; j++) { if (button1 == 0 || button2 == 0) check_button(); else Delay10ms(); } } }
|
仿真详解
单片机第三次作业(没布置)
题目要求
题目实现
代码详解
仿真详解