【笔记】C51程序学习笔记

前言

Windows上标准51单片机的C51学习笔记
注意:本文所有代码均按照sdcc编译器代码规范编写,如果使用的Keli编译器,需要自行修改代码

存储空间的访问

  • 单片机有两个存储器,分别是:只读存储器ROM、随机存储器RAM

    • ROM:通常用于存程序(code)
    • RAM:通常用于存数据(data)
  • 51单片机存储器的空间

    • 标准51单片机
      • ROM空间为4KB
      • RAM空间为128B
    • STC12C5A60S2单片机
      • ROM空间(4KB)+外扩内存(56KB)总共为60KB
      • RAM空间为1280B
  • C语言中的变量值将会默认存入RAM

1
int a = 0;
  • 因为RAM容量较小,所以如果有大量数据需要存储(例如数组),可以通过code关键字强制将变量的值存入ROM
1
__code int a = 0;
  • 如果使用的是增强型51单片机(例如STC12C5A60S2单片机),可以通过xdata关键字强制将变量的值存入外扩内存
1
__xdata int a = 0;

操作2进制位

  • 在操作2进制位时,如果需要改变2进制位,通常不使用算术运算符,通常使用按位运算符

指定位为0

  • 任意位与0相,结果不变
  • 任意位与1相,结果为0

举例:将末位置0

1
num = num & 0b00000001;

指定位为1

  • 任意位与0相,结果不变
  • 任意位与1相,结果为1

举例:将末位置1

1
num = num | 0b00000001;

指定位取反

  • 任意位与0异或,结果不变
  • 任意位与1异或,结果取反

举例:将末位置取反

1
num = num ^ 0b00000001;

固定格式

1
2
3
4
5
6
7
8
9
#include<reg52.h>

void main(void)
{
while (1)
{
...
}
}

引入头文件

1
#include<8052.h>

为引脚赋值

  • I/O端口都是可以位寻址的,所以既可以按字节赋值,也可以按位赋值

按字节赋值

  • 将P0的所有引脚都通电(00000000
1
P0 = 0x00;
  • 将P0的所有引脚都断电(11111111
1
P0 = 0xFF;

按位赋值

  • 将P0的第1个引脚通电(11111110
1
P0_0 = 0;
  • 将P0的第1个引脚断电(11111111
1
P0_0 = 1;

循环语句提升效率

  • 循环要以递减的方式迭代。在编译成汇编代码后,递减的方式迭代要比递增的方式迭代执行效率更高
1
2
3
4
5
6
int i = 100;
while (i > 0)
{
...
i--;
}
1
2
3
4
for (int i = 100; i > 0; i--)
{
...
}

发光二极管

  • 发光二极管是一个输出外设

拉电流和灌电流

拉电流

  • 拉电流时
    • 输入为1时,发光二极管亮灯
    • 输入为0时,发光二极管不亮灯
  • 拉电流为200微安
1
2
单片机引脚 <- 发光二极管正级
发光二极管负级 -> 限流电阻 -> 电源负级

灌电流

  • 灌电流时
    • 输入为0时,发光二极管亮灯
    • 输入为1时,发光二极管不亮灯
  • 灌电流为20毫安,所以通常使用灌电流,才能让发光二极管达到正常亮度
1
2
单片机引脚 <- 限流电阻 <- 发光二极管负级
发光二极管正级 -> 电源正级

软件延迟函数

  • 通过600~700次的空循环,可以实现延迟1ms左右
1
2
3
4
5
6
void delay(unsigned int ms)
{
unsigned int j;
while (ms--)
for (j = 600; j > 0; j--);
}

通过延迟实现闪烁

  • 频率越高闪烁越快
  • 每1s内,实现1次亮和1次灭,这样的频率为1Hz
  • 因为人眼有余晖时间,所以人眼可识别的最大闪烁频率为50Hz
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<8052.h>

void delay(unsigned int ms)
{
unsigned int j;
while (ms--)
for (j = 600; j > 0; j--);
}

void main(void)
{
while (1)
{
// 闪烁频率为50Hz(1秒钟闪烁50次)
unsigned int time = 1;
unsigned int count = 50;
unsigned int delay_time = 1000 * time / 2 / count;
P1_0 = 0;
delay(delay_time);
P1_0 = 1;
delay(delay_time);
}
}

通过调整占空比改变亮度

  • 亮度的决定因素:发光二极管闪烁时,亮的时间越大,灭的时间越小,亮度越亮
  • 占空比:灭的时间 / 亮的时间 + 灭的时间
  • 当亮和灭的时间间隔发生改变时,占空比就会改变,发光二极管亮度就会改变
  • 50Hz在1秒内的的两次间隔之和为20ms,所以能产生19级亮度
    • 当亮1ms,灭19ms,亮度最暗
    • 当亮19ms,灭1ms,亮度最亮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<8052.h>

void delay(unsigned int ms)
{
unsigned int j;
while (ms--)
for (j = 600; j > 0; j--);
}

// 亮度最亮
void max() {
P1_0 = 0;
delay(19);
P1_0 = 1;
delay(1);
}

// 亮度最暗
void mim() {
P1_0 = 0;
delay(1);
P1_0 = 1;
delay(19);
}

STC12C5A60S2的特殊功能寄存器

引入头文件

传送门

1
#include<STC12C5A60S2.h>

切换端口工作模式

  • 以修改P0端口的第0个位置为例

工作模式

工作模式 M1寄存器的值 M0寄存器的值 备注
标准双向口模式(缺省值) 0 0 灌电流为20毫安,拉电流为200微安
强推挽输出模式(强推模式) 0 1 灌电流为20毫安,拉电流提升为20毫安
开漏模式 1 0 -
高阻模式 1 1 -

设置方法

  • 每个位可以单独设置工作模式,由2个寄存器对应位的值决定
1
2
P0M1 = 0x00;
P0M0 = 0x00;

按键

  • 按键是一个输入外设

  • 按键按下时,输入为0

  • 按键没有按下时,输入为1

软件消抖

  • 按键按下和抬起时,会有抖动现象产生,抖动现象通常会在按下和抬起的10ms~20ms之间发生
  • 软件消抖通过利用软件延迟函数实现
  • 软件消抖的效果
    • 按下消抖效果:按下时进入循环,此时通过延迟10ms,解决10ms之内按下瞬间出现反复按的情况。
    • 抬起消抖效果:如果按住并且一直没抬起,此时会进入无限循环,也就是不会做处理。抬起时通过延迟10ms,解决10ms之内按下瞬间出现反复按的情况

按下后执行业务

P0_0:按键的连接点

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
#include<8052.h>

void delay(unsigned int ms)
{
unsigned int j;
while (ms--)
for (j = 600; j > 0; j--);
}

void main(void)
{
while (1)
{
while (!P0_0)
{
// 按下消抖
delay(10);
if (!P0_0)
{
// 业务代码
...
}
// 抬起消抖
while (!P0_0);
delay(10);
}

}
}

抬起后执行业务

P0_0:按键的连接点

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
#include<8052.h>

void delay(unsigned int ms)
{
unsigned int j;
while (ms--)
for (j = 600; j > 0; j--);
}

void main(void)
{
while (1)
{
while (!P0_0)
{
delay(10);
while (!P0_0);
delay(10);

// 业务代码
...
}

}
}

按键检测函数

  • 为每一个按键按下赋予键值,在主函数中通过判断减值,实现指定业务
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
#include<8052.h>

void delay(unsigned int ms)
{
unsigned int j;
while (ms--)
for (j = 600; j > 0; j--);
}

unsigned char key_scan(void)
{
unsigned char key_num = 0;
if (!P0_0 || !P0_1)
{
delay(10);
if (!P0_0) key_num = 1;
if (!P0_1) key_num = 2;
while (!P0_0 || !P0_1);
delay(10);
}
return key_num;
}

void main(void)
{
while (1)
{
switch (key_scan())
{
case 1:
// 业务代码
break;
case 2:
// 业务代码
break;
}
}
}

七段数码管

  • 数码管的引脚一般都在上下,共阴和共阳是由内部决定的

  • 共阳数码管(灌电流)点亮:位信号(com端)接高电平,段信号(a、b、c、d、e、f、g、dp端)接低电平

    • 本案例接线方式
      • 位信号1接P0.3、…、位信号4接P0.0
      • 段信号a接P1.0、…、段信号g接P1.6、段信号dep接P1.7
  • 共阴数码管(拉电流)点亮:位信号(com端)接低电平,段信号(a、b、c、d、e、f、g、dp端)接高电平

  • 不应该将位信号同时点亮,如果需要全部点亮每个位,可以采用动态刷新技术

  • 不同位数的七段数码管

    • 1位七段数码管一共有10个引脚
      • 2个com端(这两个引脚内部是想通的)
      • a(上)、b(右上)、c(右下)、d(下)、e(左下)、f(左上)、g(中)、dp(小数点)端
    • 2位七段数码管一共有10个引脚
      • com1端、com2端
      • a(上)、b(右上)、c(右下)、d(下)、e(左下)、f(左上)、g(中)、dp(小数点)端
    • N位七段数码管一共有(8+N)个引脚
      • com1端、…、comN端
      • a(上)、b(右上)、c(右下)、d(下)、e(左下)、f(左上)、g(中)、dp(小数点)端

全部点亮

  • 通过动态扫描(刷新),实现全部点亮检查坏点:通过快速切换不同位上的数字,实现现实多个位上的数字扫描
    • 因为人眼能识别的最大闪烁频率为50Hz,所以为了骗过人眼,需要将所有数字在20ms内完成闪烁
  • 七段码:每个数字位用于显示数字的16进制值,16进制数的每一位决定哪一个笔划亮灯
    • 从com端到a~dep端如果是灌电流,则com端输出0时,数码管指定位亮灯
  1. 选位:选位阶段需要指定哪个位亮
  2. 送段:送出段信号阶段需要指定亮哪个数字
  3. 延迟:如果是4位数码管,每个位的延迟应设置为5ms
  4. 灭段:为了解决拖影(拖尾)现象,将亮灯的数字熄灭
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
#include<8052.h>

__sfr __at (0x94) P0M0;
__sfr __at (0x93) P0M1;
__sfr __at (0x92) P1M0;
__sfr __at (0x91) P1M1;

void delay(unsigned int ms)
{
unsigned int j;
while (ms--)
for (j = 600; j > 0; j--);
}

void main(void)
{
// 强推挽输出模式
P0M1 = 0x00;
P0M0 = 0xFF;

// 定义七段码
unsigned char num[] = {0xC0, 0xEC, 0x92, 0x98,0xCC, 0x89, 0x81, 0xDC, 0x80, 0x88};
// 定义位
unsigned char w[] = {0x01, 0x02, 0x04, 0x08};

while (1)
{
for (unsigned char i = 0; i < 4; i++)
{
P0 = w[i]; // 选位
P1 = num[8]; // 送段
delay(5); // 延迟
P1 = 0xFF; // 灭段
}
}
}

封装为函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void display(unsigned char *arr)
{
// 定义计数器
static unsigned char i = 0;
// 定义七段码
unsigned char num[] = {0xC0, 0xEC, 0x92, 0x98,0xCC, 0x89, 0x81, 0xDC, 0x80, 0x88};
P0 = 0x01 << i;
P1 = num[arr[i]];
delay(5);
P0 = 0x00;
i += 1;
i %= 4;
}

void main(void)
{
while (1)
{
unsigned char arr[4] = {0, 0, 0, 0};
display(arr);
delay(2);
}
}

点阵屏

  • 8(列)*8(行)的点阵屏的操作逻辑与8位数码管理论上相同

  • 分辨率:8(列)*8(行)

  • 像素:64

  • 点阵屏的引脚是由方向决定的,共阴和共阳也是由方向决定的

  • 共阳(灌电流)接线方式(引脚在左右)点亮:列信号接高电平(高选列),行信号接低电平(低送行)

  • 共阴(拉电流)接线方式(引脚在上下)点亮:列信号接低电平(低选列),行信号接高电平(高送行)

    • 本案例接线方式(单屏)
      • 列信号1接P0.0、…、位信号8接P0.7
      • 行信号1接P1.0、…、段信号8接P1.7
    • 本案例接线方式(多屏)
      • 列信号1接P0.0、…、位信号8接P0.7
      • 列信号9接P2.0、…、位信号16接P2.7
      • 行信号1接P1.0、…、段信号8接P1.7
  • 不应该将列信号同时点亮,如果需要全部点亮每个列,可以采用动态刷新技术

  • 取模:将文字转化成点阵

    • 汉字的字模需要最少12*12的分辨率才能正常显示
  • 切屏:每次刷新8列

  • 滚屏:每次刷新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
#include<8052.h>

__sfr __at (0x94) P0M0;
__sfr __at (0x93) P0M1;
__sfr __at (0x92) P1M0;
__sfr __at (0x91) P1M1;

void delay(unsigned int ms)
{
unsigned int j;
while (ms--)
for (j = 600; j > 0; j--);
}

void main(void)
{
// 强推挽输出模式
P0M1 = 0x00;
P0M0 = 0xFF;

P1M1 = 0x00;
P1M0 = 0xFF;

// 定义字模值
unsigned char num[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
// 定义列
unsigned char l[] = {0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F};

while (1)
{
for (unsigned char i = 0; i < 8; i++)
{
P0 = l[i]; // 选列
P1 = num[i]; // 送行
delay(2); // 延迟
P1 = 0xFF; // 灭行
}
}
}

双屏

全部点亮

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
#include<8052.h>

__sfr __at (0x94) P0M0;
__sfr __at (0x93) P0M1;
__sfr __at (0x92) P1M0;
__sfr __at (0x91) P1M1;

void delay(unsigned int ms)
{
unsigned int j;
while (ms--)
for (j = 600; j > 0; j--);
}

void main(void)
{
// 强推挽输出模式
P0M1 = 0x00;
P0M0 = 0xFF;

P1M1 = 0x00;
P1M0 = 0xFF;

// 定义字模值
unsigned char num[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
// 定义列
unsigned char l[] = {0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F};

while (1)
{
for (unsigned char i = 0; i < 8; i++)
{
if (i < 8)
{
P2 = 0xFF;
P0 = l[i]; // 选列
}
else
{
P0 = 0xFF;
P2 = l[i]; // 选列
}
P1 = num[i]; // 送行
delay(1); // 延迟
P1 = 0xFF; // 灭行
}
}
}

蜂鸣器(Buzz)

  • 有源蜂鸣器:内置了震荡源芯片的蜂鸣器,接上电点就能响,但是不能改变振动频率所以只能发出一种音调

  • 无源蜂鸣器:没有内置震荡源芯片的蜂鸣器,接上电后,需要给脉冲信号才能响,可以通过改变震动频发出不同的音调

  • 拉电流和灌电流都可以使蜂鸣器发声

    • 本案例接线方式
      • 灌电流。蜂鸣器一端接P1.0
  • 因为人耳识别声音的范围为5Hz~20kHz,所以延迟最大为100ms

软件延迟实现蜂鸣器5Hz声音

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<8052.h>

void delay(unsigned int ms)
{
unsigned int j;
while (ms--)
for (j = 600; j > 0; j--);
}

void main(void)
{
P0_0 = 1;
while (1)
{
P0_0 = ~P0_0;
delay(100);
}
}

定时/计数器实现蜂鸣器500Hz声音

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
#include<8052.h>

void init()
{
// 指定开启定时计数器0中断
IE = 0x82;
// 为定时/计数器0指定模式
// 指定门控信号为软件开启
// 指定工作在定时模式
// 指定工作方式3
TMOD = 0x01;
// 为定时/计数器0指定时间初值(1ms=1000次)
TH0 = (65536 - 1000) / 256;
TL0 = (65536 - 1000) % 256;
// 为定时/计数器0开启计数
TR0 = 1;
}

void main(void)
{
init();
// 初始蜂鸣器
P1_0 = 1;
while (1)
{

}
}

void timer0(void) __interrupt 1
{
// 阻止计数
TR0 = 0;

// 业务代码
if (P1_0) P1_0 = 0;
else P1_0 = 1;

// 重新指定计数初值
TH0 = (65536 - 1000) / 256;
TL0 = (65536 - 1000) % 256;
// 恢复计数
TR0 = 1;
}

定时/计数器实现蜂鸣器指定频率声音

<frequency>:频率

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
#include<8052.h>

void init()
{
// 指定开启定时计数器0中断
IE = 0x82;
// 为定时/计数器0指定模式
// 指定门控信号为软件开启
// 指定工作在定时模式
// 指定工作方式3
TMOD = 0x01;
// 为定时/计数器0指定时间初值(1ms=1000次)
TH0 = (65536 - 1000000/<frequency>/2) / 256;
TL0 = (65536 - 1000000/<frequency>/2) % 256;
// 为定时/计数器0开启计数
TR0 = 1;
}

void main(void)
{
init();
// 初始蜂鸣器
P1_0 = 1;
while (1)
{

}
}

void timer0(void) __interrupt 1
{
// 阻止计数
TR0 = 0;

// 业务代码
if (P1_0) P1_0 = 0;
else P1_0 = 1;

// 重新指定计数初值
TH0 = (65536 - 1000000/<frequency>/2) / 256;
TL0 = (65536 - 1000000/<frequency>/2) % 256;
// 恢复计数
TR0 = 1;
}

定时/计数器实现蜂鸣器音阶

  • 全音符的音长大约在1600ms
  • 2分音符的音长大约在800ms
  • 4分音符的音长大约在400ms(~600ms)
  • 8分音符的音长大约在200ms
  • 16分音符的音长大约在100ms
  • 32分音符的音长大约在50ms
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
#include<8052.h>

// 存放寄存器高位
unsigned int h;
// 存放寄存器低位
unsigned int l;

void init()
{
// 指定开启定时计数器0中断
IE = 0x82;
// 为定时/计数器0指定模式
// 指定门控信号为软件开启
// 指定工作在定时模式
// 指定工作方式3
TMOD = 0x01;
}

void delay(unsigned int ms)
{
unsigned int j;
while (ms--)
for (j = 600; j > 0; j--);
}

void main(void)
{
init();
// 初始蜂鸣器
P1_0 = 1;

// C大调中音7音阶数组
unsigned int pitch[] = {523, 587, 659, 698, 784, 880, 988};
// 每次蜂鸣器发声的音调(C大调中音7音阶数组的索引)
unsigned int tone[] = {1, 1, 5, 5, 6, 6, 5};
// 每次蜂鸣器发声的音长(4分音符的倍数)
unsigned int rhythm[] = {1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2};
// 累加器
unsigned char i = 0;

while (1)
{
h = (65536 - 1000000/pitch[tone[i] - 1]/2) / 256;
l = (65536 - 1000000/pitch[tone[i] - 1]/2) % 256;
// 为定时/计数器0指定时间初值(1ms=1000次)
TH0 = h;
TL0 = l;
// 为定时/计数器0开启计数
TR0 = 1;
// 延迟总音长(4分音符的倍数 * 4分音符的音长)
delay(rhythm[i] * 600);
// 为定时/计数器0关闭计数
TR0 = 0;
// 蜂鸣器断电
P1_0 = 0;
// 累加器增加
i += 1;
i %= sizeof(tone)/sizeof(tone[0]);
}
}

void timer0(void) __interrupt 1
{
// 阻止计数
TR0 = 0;

// 业务代码
if (P1_0) P1_0 = 0;
else P1_0 = 1;

// 重新指定计数初值
TH0 = h;
TL0 = l;
// 恢复计数
TR0 = 1;
}

C大调音符频率对照表

  • 中音的频率约定于2倍的低音
  • 高音的频率约定于2倍的中音
  • 以此类推
序号 音符 频率
1 低音1 262
2 低音2 294
3 低音3 330
4 低音4 349
5 低音5 392
6 低音6 440
7 低音7 494
8 中音1 523
9 中音2 587
10 中音3 659
11 中音4 698
12 中音5 784
13 中音6 880
14 中音7 988
15 高音1 1046
16 高音2 1175
17 高音3 1318
18 高音4 1397
19 高音5 1568
20 高音6 1760
21 高音7 1976

完成

参考文献

哔哩哔哩——江科大自化协