单片机实验讲义

单片机第一次作业

题目要求

  1. 安装 Keil 和 Proteus,熟悉 Keil 编译和可执行文件的生成操作和 Proteus 仿真操作
  2. 使用汇编语言编写代码实现单片机引脚输出 PWM 波形,使用 Proteus 中的示波器查看波形
  3. 使用按键控制 PWM 的延时长度(占空比?)
  4. 录制视频发至 B 站

题目实现

  1. 安装步骤及工程创建省略,具体操作可以类比。
  2. 代码实现思路:首先建立一个延时时间为 1ms(0.999285ms)的延时函数作为时基,然后通过调用该延时函数来控制高低电平的维持时间。主函数中通过每隔一定时间拉高拉低电平来实现 PWM 波形的生成。主函数中还包含按键检测的电路,进入 LOOP 前后都要进行按键检测。PWM 的占空比范围为 0%~100%,可以使用按键来控制每次按下按键时 PWM 的占空比增加 10%。
  3. 仿真电路搭建:在 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

仿真详解

比较简单,略。

单片机第二次作业

题目要求

流水灯实验

  1. 八个LED灯加两个按键
  2. 一个按键用于控制流水灯的样式
  3. 另一个按键用于控制流水灯的延时

题目实现

  1. 创建 Keil 工程和 Proteus 工程
  2. 选择 P2 口连接八个 LED 灯,选择 P1.0 和 P1.1作为按键输入端口
  3. 编写代码

代码详解

首先我们先写一个流水灯代码。由于这次要同时操作八个流水灯,因此我们可以直接操作 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
LED = 0xff << 1;

执行这段代码后,状态变为:

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(); //@11.0592MHz
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() //@11.0592MHz
{
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();
}
}
}

仿真详解

单片机第三次作业(没布置)

题目要求

题目实现

代码详解

仿真详解