51单片机系列

此文章记录我学单片机进程,以及所写的代码集合,当作笔记和备份吧,以后可以温习用。

目前正在学的是51单片机,普中A2版本。

先上张原理图

开发板原理图

再来张十六进制换算表

十六进制

LED模块

最简单的点灯代码

1
2
3
4
5
6
7
8
9
#include <REGX52.H>

void main()
{
P2 = 0xFE;
while(1)
{}
}

控制点灯时间代码

通过软件内置的时间延迟计算生成函数,来控制时间。

CRnall_20241103_233001031

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 <REGX52.H>
#include <INTRINS.H>
void Delay500ms() //@12.000MHz
{
unsigned char i, j, k;

_nop_();
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}

void main()
{
while(1)
{
P2=0xFE;
Delay500ms();
P2=0xFF;
Delay500ms();
}
}

流水灯代码

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 <REGX52.H>
void Delay1ms(unsigned int xms) //@12MHz
{
unsigned char i, j;
while(xms){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}

}

void main()
{
while(1)
{
P2=0xFE;
Delay1ms(120);
P2=0xFD;
Delay1ms(500);
P2=0xFB;
Delay1ms(100);
P2=0xF7;
Delay1ms(200);
P2=0xEF;
Delay1ms(500);
P2=0xDF;
Delay1ms(400);
P2=0xBF;
Delay1ms(300);
P2=0x7F;
Delay1ms(100);
}
}

通过调整函数变量,可以自由决定每一个灯亮起的时间跟间隔。

按钮控制点灯v1

1
2
3
4
5
6
7
8
9
10
11
12
#include <REGX52.H>
void main()
{ while(1)
{
if(P3_1==0)
{
P2_0=0;
}
else
P2_0=1;
}
}

可以通过按钮的判断来控制小灯的亮灭了,但是这些代码只能需要按钮一直按下才可以把灯点亮,如果松开按钮灯就灭了,而且没有消抖。

按钮控制点灯v2

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

#include <REGX52.H>

void Delay1ms(unsigned int xms) //@12MHz
{ unsigned char i, j;
while(xms>0){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{P2_0=0;
while(1)
{
if(P3_1==0)
{ Delay1ms(20);
while(P3_1==0);
Delay1ms(20);
P2_0=~P2_0;
}
}
}

这个就相对完善了很多了,已经可以实现按钮按一次就会一直亮,再按就灭的功能,而且也加入了延时来消除抖动。也引入了位运算,可以通过取反来更加便捷地控制小灯亮灭。

按钮控制点灯v3

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
#include <REGX52.H>
char LEDNum=0;
void delay(int xms) //@12MHz
{while(xms)
{
unsigned char i, j;

i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{ P2=~0x01;
while(1)
{
if(P3_1==0) //实现左移
{
delay(30);
while(P3_1==0)
delay(30);
LEDNum++;
if(LEDNum==8)
LEDNum=0;
}
P2 =~(0x01<<LEDNum);
if(P3_0==0) //实现右移
{
delay(30);
while(P3_0==0)
delay(30);
if(LEDNum==0)
LEDNum=7;
else
LEDNum--;
P2 =~(0x01<<LEDNum);
}
}
}

通过if语句和一个中间变量再加上移位运算和取反运算,最终实现了用两个按钮来控制小灯的亮起的左移和右移,并且对小灯在边缘亮起的情况做了处理,让其可以正常运行。

静态数码管显示

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
#include <REGX52.H>
unsigned char nixietable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00};
void nixie(unsigned char location,number)
{
switch(location)
{
case 1:
P2_4=1;P2_3=1;P2_2=1;break;
case 2:
P2_4=1;P2_3=1;P2_2=0;break;
case 3:
P2_4=1;P2_3=0;P2_2=1;break;
case 4:
P2_4=1;P2_3=0;P2_2=0;break;
case 5:
P2_4=0;P2_3=1;P2_2=1;break;
case 6:
P2_4=0;P2_3=1;P2_2=0;break;
case 7:
P2_4=0;P2_3=0;P2_2=1;break;
case 8:
P2_4=0;P2_3=0;P2_2=0;break;
}
P0=nixietable[number];
}
void main()
{ nixie(2,3);
while(1)
{
}
}

定义了一个nixie的函数,用来更为方便的控制静态数码管,数码管的控制主要有两个来决定,一个是位,一个是数,在51单片机中,代码中的switch判断所控制的就是数码管的位置,采用的是38译码器,通过三个数的0和1来表示8个位置的数码管,大限度的减短控制所需要的接口,非常的巧妙,而代码中的nixietable数组则是控制显示的数字,通过十六进制的数字来控制所显示的东西,一切都刚刚好,具体可以去看原理图,如果使用的话,调用现成的库就足够了。

但是,这套控制系统虽然简单,只用了很少的接口,但是也有局限性,就是一次只能表示一个位置的一个数字,所以就有了动态数码管。

动态数码管

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 <REGX52.H>
unsigned char nixietable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00};
void delay(int xms) //@12MHz
{
unsigned char i, j;
while(xms){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);xms--;
}
}
void nixie(unsigned char location,number)
{
switch(location)
{
case 1:
P2_4=1;P2_3=1;P2_2=1;break;
case 2:
P2_4=1;P2_3=1;P2_2=0;break;
case 3:
P2_4=1;P2_3=0;P2_2=1;break;
case 4:
P2_4=1;P2_3=0;P2_2=0;break;
case 5:
P2_4=0;P2_3=1;P2_2=1;break;
case 6:
P2_4=0;P2_3=1;P2_2=0;break;
case 7:
P2_4=0;P2_3=0;P2_2=1;break;
case 8:
P2_4=0;P2_3=0;P2_2=0;break;
}
P0=nixietable[number];
delay(1);
P0=0x00;
}
void main()
{
while(1)
{
nixie(1,1);delay(5);
nixie(2,2);delay(5);
nixie(3,3);delay(5);
}
}

这个就已经比较完善了,通过delay延时,利用人眼的延时,只要变化的够快,人眼就感知不出来(doge)。

同时呢,也加入了消隐,可以避免数码管显示出错,当然,也可以让延时改为1毫秒,也可以避免显示出错。

666显示

模块化编程

这是一种方法,我们可以将一些函数打包成一个.c文件和一个.h文件,只要做好声明并调用,就可以非常方便的使用这些函数了,并且也容易分享和修改。

下面是一个.h文件的示例

1
2
3
4
5
#ifndef __DELAY_H__
#define __DELAY_H__
void delay(int xms);
#endif

.c文件就是类似于自定义函数的写法,放在同一目录下就可以正常使用了。

矩阵键盘

为了简化io口,矩阵键盘也应用了类似于数码管的模式,仅使用了8个io口,便可以控制了16个矩阵按键,相应的,获取矩阵按键的状态,也是需要不断扫描,以时间赢得空间。

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
#include <REGX52.H>
#include "delay.h"
#include "lcd1602.h"
/**
* @brief 矩阵键盘读取按健键码
* @param 无
* @retval 按下按键的键码值
*/
unsigned char Martrixkey()
{
unsigned char KeyNumber = 0;
P1=0xFF;
P1_3=0;
if(P1_7==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=1;}
if(P1_6==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=5;}
if(P1_5==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=9;}
if(P1_4==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=13;}
P1=0xFF;
P1_2=0;
if(P1_7==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=2;}
if(P1_6==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=6;}
if(P1_5==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=10;}
if(P1_4==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=14;}
P1=0xFF;
P1_1=0;
if(P1_7==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=3;}
if(P1_6==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=7;}
if(P1_5==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=11;}
if(P1_4==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=15;}
P1=0xFF;
P1_0=0;
if(P1_7==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=4;}
if(P1_6==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=8;}
if(P1_5==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=12;}
if(P1_4==0)
{delay(20);while(P1_7==0);delay(20);KeyNumber=16;}
return KeyNumber;
}

这是矩阵键盘模块的内部代码,以按列扫描的模式,不断读取每个按键的状态,并返回相对应的结果。

下面是一个简单的矩阵键盘使用案例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <REGX52.H>
#include "delay.h"
#include "lcd1602.h"
#include "Martrixkey.h"
unsigned char KeyNum;
void main()
{
LCD_Init();
LCD_ShowString(1,2,"Hello world");
while(1)
{
KeyNum=MartrixKey();
if(KeyNum)
{
LCD_ShowNum(2,1,KeyNum,2);
}
}
}

这些代码通过调用矩阵键盘的模块可以实现读取键盘的数值,并让其实时打印在LCD屏幕上。

定时器系列

delay函数之所以可以延时,是因为它通过让cpu执行一系列空的事情,来拖延出时间,所以当我们在运行delay函数时,cpu是占满的,我们这个时候再想运行其他函数,cpu是不会响应的,有弊端在,所以,引入了一个重要的模块——定时器

定时器,是芯片内部的资源,通过晶振来推动,51单片机定时器0内部有两个寄存器TH0和TL0,都是一字节的,理解位定时器0高位寄存器(TH0),定时器0低位寄存器(TL0), 我们知道2字节最大能存0到65535一共65536个数字。每过一个指令周期(1us),寄存器的值+1,当加到溢出后发出一个溢出中断,我们程序可以捕获到这个中断,并执行中断函数内容,就可以知道此时经历了(65535+1)us。如果我们要定时1ms,设置寄存器的初值为64536,这样到溢出值65536就正好1ms。如果想要定时1s的话,设置一个计数器,让定时器循环1000次即可。

定时器由于是芯片内部的资源,所以不需要特别占用cpu,比delay好很多。

下面就是一个简单的由定时器驱动的程序。这是一个按键控制流水灯的程序,按下按键,流水灯就会换一个方向亮起,引入了第三方库INTRINS.h。

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
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include <INTRINS.H>
unsigned char KeyNum,LEDmode;
void main()
{ P2=0xFE;
Timer0_Init();
while(1)
{
KeyNum=Key();
if(KeyNum)
{
if(KeyNum==1)
{
LEDmode++;
if(LEDmode>=2)LEDmode=0;
}
}
}
}
void Timer0_Routine() interrupt 1 //中断程序内容
{ static signed int T0Count;
TL0 = 0x66;
TH0 = 0xFC;
T0Count++;
if(T0Count>=1000) //计次1000次以达到1s钟
{ T0Count=0;
if(LEDmode==0)
P2=_crol_(P2,1);
if(LEDmode==1)
P2=_cror_(P2,1);
}
}

像这种程序如果不用定时器而用delay是实现不了的,或者说效果很差,按键根本反应不了。

1
2
3
4
5
6
7
8
9
10
11
12
void Timer0_Init()
{
TMOD &= 0xF0;
TMOD |= 0x01;
TL0 = 0x66;
TH0 = 0xFC;
TF0 = 0;
TR0 = 1;
ET0=1;
EA=1;
PT0=0;
}

这是定时器的内部寄存器配置。

PWM

PWM是一种控制输出电功率的方法,就是控制高低电平的比例,从而控制电力的输出,常用的领域有呼吸灯的制作,还有电机的调速。

呼吸灯

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
#include <REGX52.H>
sbit LED=P2^0;
void delay(unsigned int t) //简易延时函数
{
while(t--);
}
void main()
{ signed char time,t;
while(1)
{

for(time=0;time<100;time++)
{

for(t=0;t<20;t++) //灯泡亮暗的速度太快了,加入这个循环,降低其速度
{
LED=0;
delay(time);
LED=1;
delay(100-time);
}
}
for(time=100;time>0;time--)
{

for(t=0;t<20;t++)
{
LED=0;
delay(time);
LED=1;
delay(100-time);
}

}
}

}

这个有用了delay的方法,有规律的控制小灯暗亮。

电机调速

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
#include <REGX52.H>
#include "delay.h"
#include "Key.h"
#include "LCD1602.h"
#include "Martrixkey.h"
#include "nixie.h"
unsigned char counter,compare;
unsigned char keynum,speed;
sbit Motor = P1^0;
void main()
{ Timer0_Init();
while(1)
{
keynum=Key();
if(keynum==1)
{
speed++;
speed%=4;
if(speed==0)compare=0;
if(speed==1)compare=50;
if(speed==2)compare=75;
if(speed==3)compare=100;
}
nixie(1,speed);
}
}
void Timer0_Routine() interrupt 1
{
TL0 = 0xA4;
TH0 = 0xFF;
counter++;
if(counter>100)
{counter=0; //counter%=100;
}
if(counter<compare)
Motor=1;
else
Motor=0;

}

一共有三档调速度,通过比较counter和compare两个变量的大小,来控制电机的运转与否。

8*8点阵屏

8*8点阵屏是一个由64个小灯组成的小屏幕,类似于矩阵键盘,也是通过时间换io口,总共只需要控制11个io口,就可以在点阵屏上显示内容,,采用的是按列扫描的模式,而控制每一列的内容,则是通过3个io口进行处理,需要74HC595模块,就想填子弹一样,一个一个把数据填进寄存器,再统一发送到点阵屏上。

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
#include <REGX52.H>
#include "delay.h"

sbit RCK=P3^5; //RCLK
sbit SCK=P3^6; //SRCLK
sbit SER=P3^4; //SER

#define MATRIX_LED_PORT P0

/**
* @brief 控制每一列的输入
* @param
* @retval
*/

void _74HC595_WriteByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
SER=Byte&(0x80>>i);
SCK=1;
SCK=0;

}
RCK=1;RCK=0;

}
/**
* @brief 点阵屏显示一列数据
* @param column 要选择的列,范围0-7,date 要选择的列的数据,高位在上,1为亮
* @retval
*/
void MatrixLED_ShowColumn(unsigned char column,date)
{
_74HC595_WriteByte(date);
MATRIX_LED_PORT=~(0x80>>column);
delay(1);
MATRIX_LED_PORT=0xFF;

}
void main()
{ SCK=0;
RCK=0; ///

while(1)
{
MatrixLED_ShowColumn(0,0x3c);
MatrixLED_ShowColumn(1,0x42);
MatrixLED_ShowColumn(2,0xA9);
MatrixLED_ShowColumn(3,0x85);
MatrixLED_ShowColumn(4,0x85);
MatrixLED_ShowColumn(5,0xA9);
MatrixLED_ShowColumn(6,0x42);
MatrixLED_ShowColumn(7,0x3c);
}
}

这是一个简单的在点阵屏显示笑脸的代码,没有进行模块化处理,想要模块化处理就定义一个初始化函数跟一个显示函数就可以了,当然,这个点阵屏也是实时扫描的,所以想进行同时进行其他任务比较困难,当然也可以带上定时器试试。

效果图如下

CRnall_20241123_002500248

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
#include <REGX52.H>
#include "delay.h"
#include "MatrixLED.h"

unsigned char code Animation[]=
{

}; //这个数组存放所有的列的信息
void main()
{
unsigned char i,offset,count=0;

void Martrix_LED_Init();
while(1)
{
for(i=0;i<8;i++)
{
MatrixLED_ShowColumn(0,Animation[i+offset]);
}
count++;
if(count>10) //调整速度用的
{
count=0;
offset++; //如果是显示一帧一帧的动画,而不是流水的图案,则需要改成offset+=8;
offset%=24; //这个要随时调
}

}

}

这些代码可以实现在8*8点阵屏上显示动画,将所有动画的每一行的内容,写进数组Animation中,然后再按照规律进行遍历,就可以显示动画了。