51开发板样例代码合集
学习51单片机开发板的代码样例备份一下。
LED相关控制
点亮一个LED
先来看一下LED部分和51单片机的电路连接,这个影响到我们代码应该怎么写,比如LED是共阴极,那你LED控制亮的话,要输出高电平,如果你LED共阳极,则需要输出低电平,
1 | #include<8052.h> |
单片机LED轮流闪烁
1 | #include<8052.h> |
按键控制LED
单个独立按键控制LED,按下点亮,松开熄灭
这里主要是检测IO口的电平高低,上电后寄存器可以操作IO引脚电平高低,比如P1_0 = 1 输出高电平,那么同理,反过来 如果外围电路按键,参考电路图,
这里的电路图,K1按键按下,P3_1 和 GND导通,那么P3_1 自然就是低电平,此时如果去检测P3_1 IO口的电平,那他就是低电平,IO口除了可以用来输出高低电平,还可以用作检测输入,那么这里,是把按键绑定到了LED亮灭来指示效果。
1 | #include<8052.h> |
两个按键一起绑定,控制LED亮灭,K1 & K2 一起按下LED点亮,松开其中一个熄灭
同样,中间的表达式改成 || 的话,就两个按键 都可以控制LED亮灭。再同样,左移,右移操作,可以玩LED灯的按位与,按位或,可以实现一个逻辑门电路的结果展示,以及LED左右移,来实现流水灯。
1 | #include<8052.h> |
按键控制LED长亮,或长灭
这里包含了按键消抖,按键因为是机械部件,设计不太会很精密,按下和松开的时候,不会直接就变高电平或低电平,他可能会有一部分抖动的时间,大概20ms左右才变稳定,所以要加按键消抖。
而代码上,P2_0 的取反,江科大他写的是 P2_0 = ~P2_0; 但是在我们这里不起作用,问了AI,建议是用 逻辑取反,因为AI说 具体运行的时候,P2_0会被当作int来处理,int是16位,也不知真假,估计是编译器的问题,可能用Keil就正常了,这个差异,AI说是我们编译器的问题,我们用的是 sdcc,这个本身没有什么问题,能够学到就ok了。
1 | #include<8052.h> |
LED展示二进制加法操作
P2++ 这个是核心的操作,最开始是1111 1111 8个全亮,++ 第一次全部熄灭,然后按一次D1亮,按第二次,D1灭,D2亮,第三次D1再亮,D2也亮,
1 | #include<8052.h> |
LED显示二进制加法操作的另一种方式
1 | #include<8052.h> |
使用位移来实现LED流水灯
1 | #include<8052.h> |
一个按键控制LED往左移,一个按键控制LED往右移
1 | #include<8052.h> |
单片机控制数码管的操作
背景资料
我们使用的数码管,有单个位的,只能显示一个数字,一般是有共阴极数码管和共阳极数码管,每个段其实可以看成是一个LED,那么数码管的每一位其实都是很多LED组成,单个段的话,ABCDEFG DP 从数码管顶那个横杠开始,,这两个编程方式不一样,
示意图如图所示,abcdefgDP 这几个 是开发的时候每个数码管的段选顺序,按照这个特定的顺序,给ABCDEFG DP几个引脚 给对应的亮灭,就可以让这个数码管出现对应的数字,而共阴极和共阳极的数码管,体现在段选的差异上,段选就是说要哪个数码管显示数字, 
比如上面的图,上面的是共阴极,那如果想要第一位 数码管显示,就要12引脚给低电平,其他9 8 6三个引脚给高电平,而ABCDEFG DP 这几个位,可以看到这几个是统一接到了 1 2 3 4 5 7 10 11 这几个引脚上,可以提高单片机IO口的复用率,用一套8个引脚,控制所有数码管的位选,而用另外4个引脚,控制4个段选,而我们这个单片机上一共有2组4位一体数码管,所以位选是统一绑定到P00-P07,也就是P0口上,而段选,可以看到旁边有个 74LS138芯片,他可以通过3个引脚,控制8个引脚输出高低电平,因为2^3=8,而每个引脚可以有2个状态,高低电平,通过这个,可以进一步减少IO口的占用,提高IO口的使用效率。可以看下接线:
,可以看到,P22,P23,P24这三个引脚通过38译码器,控制LED1-LED8,而LED1-LED8正好是8个LED的段选引脚,再然后,可以看一下 又有一个 74HC245,他是一个单向的输出功能,可以理解成是驱动电路,数码管需要VCC来驱动电路,而单片机的IO口引脚输出的电平太低,需要有一个类似三极管的芯片,这里可以不用管这个 74HC245,对编程无感。
第一个例程,数码管的静态显示
主要用到段选,和位选,位选负责控制数码管显示什么数字,段选控制哪个数码管亮,P22-P24 3个引脚,负责控制8个数码管。
1 | #include<8052.h> |
通过内置的数组和 switch case 来实现数码管单个的延时显示
主要内容是循环每个数码管的位,在每个数码管位上轮流显示0-F,显示间隔为500ms
1 | #include<8052.h> |
数码管的动态显示与消影
上面的基础上,要在第一位显示1,第二位显示2,第三位显示3,需要注意一下digital_led_segment 这个函数的最后两行,delay(1);P0 = 0x00,这个就是去消影,因为数码管的段选和位选是同时操作的,所以会有概率段选和位选串位,导致下一个段显示了上一个段的内容,所以说 在输出之后立马把P0口置0,就不会受到上一位的影响。这个是因为我们这个74LS138这个芯片,被设置为单向输出。
1 | #include<8052.h> |
模块化编程
模块化编程,其实就是 把原本写到一个 main.c文件里的内容,分散到多个 .h 和 .c 组成的 文件中,可以代码结构比较合理,同C语言的分散编程。
然后他教程里,直接出了个 LCD1602的debug演示,所以我们就从 LCD1602这个地方开始学,学完之后再去debug LCD1602之后的课程继续学。
LCD1602的控制与输出
LCD1602的概念,也就是一个 16列,2行的古早显示器,每行每列都是一个5*7的点阵组成的字符显示输出设备,LCD1602的原理就和现代LCD屏幕比较像了,所以学一下,后面可以控制更现代化的LCD屏幕。
先看一下LCD1602的电路图,相关的代码,得结合LCD 1602的数据手册来分析,主要是RS,RW,和E这几个引脚,以及1是控制写命令,还是写数据,写数据得往P0 这个组合引脚写,因为我们这几个LCD的数据引脚,在D0-D7,A/K引脚,VCC,VSS,GND这几个也可以不管,模块也都解决了。
那么主要就是看LCD1602的数据手册了,数据手册上有给一些demo样例参考,可以看一下。
代码部分,由于用了分布模块的方式,每个文件都在自己的那一小块里面,所以一起贴上来吧。
main.c,主函数,
1 | #include<8052.h> |
delay.h,这个就不贴了,是个延时函数,可以参考上面的 delay函数抄一个
lcd_1602.h,这个里面主要是 LCD1602的控制和显示部分了。
1 | #include<8052.h> |
这个代码上,还是有很多小巧思,易读,模块化,以及抽出冗余的地方,这些还是真的挺牛逼的。
矩阵键盘
矩阵键盘输入检测以及结合LCD1602做显示
电路图,
同理,delay.h,lcd1602.h,这几个都不继续贴了,主要贴一下 核心的 metrixkeys.h,还有另外的几个方法,叫做线反转法,或者是行检测,列检测等多种方法,等后面有空了再给独立补全一下这些方法。实现的效果,就是按 S1-S16,LCD上显示对应的标号。
1 | #include<8052.h> |
还有一个main.c里面的调用的地方:
1 | #include<8052.h> |
矩阵键盘密码锁
在矩阵键盘的基础上,衍生出来的密码锁应用,
1 | #include<8052.h> |
定时器
定时器的基础知识
STC89C52RC里面有三个定时器,T0/T1/T2,一般不用T2,只用T0,T1。详细的内容要参考数据手册来写,不过有个问题是 定时器和晶振的频率还有区别。
我的开发板 晶振是11.0592Mhz,所以在AI的辅助下,我和江科大的定时器参数有些许的不一样,如果你的晶振也是12Mhz,可以按照他的来。
他的思路
他的思路是:他的晶振是12Mhz,用16位计数器,计数器每累加一次 代表一个时钟周期,单片机内部采用12T分频,也就是1us,得到 12Mhz / 12 = 1us,就是1微秒,刚好能被整除,然后16位计数器累加最大的就是 2^16=65536,我们用无符号整数就可以刚好拿到 0-65535这个值,那么这个累加器每次超过65535以后,这个值就要溢出归零,溢出归零以后,我们通过寄存器开一个中断,溢出以后定时器就会调用一次中断函数,来达到我们的目的。
那么可以给TH0和 TL0这两个寄存器,这两个就是存那个16位累加器的数字的,给他俩一个初始值,这样子,限定到65535 只有 1000的位置,假如说是64535,那么初始化上去,他第一次就从64535开始累加,累加1000次,也就是1000us=1ms,自动触发中断函数,中断函数里再次把这个寄存器给到这个初始,然后就一直能够得到稳定的1ms,这样子我们累计1000次,得到1000ms=1s,这样子1s 以后 就可以控制 P2_0口反转,获得稳定的时间。
我们的思路
我们的晶振是11.0592Mhz,被1ms除不尽,所以永远会有误差,我们如果想要模拟1ms的话, T = 12 / 11.0592 = 1.08507us,定时1ms = 1000us / 1.08507us = 921.6次累加,那所以是 近似922次,65536-922=64614,所以我们的TH0和 TL0初始值是 64614,但是我试了,LED闪烁可能刚开始还好,后来就慢慢不准。但,按照我们这个晶振频率,给出来50ms是准确的,能够被算法除尽,所以我们就改成了50ms中断一次。
main.c 代码:
1 | #include<8052.h> |
timer0.h代码:
1 | #include<8052.h> |
结合定时器+独立按键+LED流水灯的左移,右移
独立按键,实现控制LED左移右移的变量;另外是这个方法里,多了很多其他的东西,有循环移位,之类的操作比较高级了;
库函数:intrins.h,因为这个是keil的库函数,所以我们自己开发的话,AI给我们提供了一个自己实现的库函数,放到 include目录里 引入就好。
在这里,我们用的是 crol 循环左移 和 cror 循环右移这两个函数。
1 | #ifndef __INTRINS_H__ |
timer0.h 的内容,
1 | #include<8052.h> |
keys.h 的内容:
1 | #include<8052.h> |
主函数 main.c的代码:
1 | #include<8052.h> |
结合LCD1602,和定时器,做一个简易的24小时时钟
先来main.c的:
1 | #include<8052.h> |
其他的参考一下之前的就可以,可以在LCD1602上显示一个 时分秒的时钟,到了24小时还会归零那种,学到这里 成就感满满,可以睡了。
串口协议
简单发送信息
用串口可以方便的和一些外部设备沟通,是对外常见的一个通信协议,类似的 还有 I^2C,USB,SPI总线协议等,这些都是物联网或单片机MCU开发常用的设备通信协议。
串口通信的代码,实现每隔1s往外发送一个数字:main.c:
1 | #include<8052.h> |
uart.h:
1 | #ifndef __UART_H__ |
主要还是要看数据手册上的定义,以及使用模式,这里主要是使用 8位可重装计数器来用串口,而串口主要是SBUF,所以可以看到发送的主要操作也就是往SBUF这个寄存器写数据。这里的参数,要看你看的教程不一样,而不一样了,我这里看的是4800波特率,11.0592Mhz晶振频率,关闭中断得到的参数,如果你的有些区别,则按你的来动态变化。
还有个事是,因为我们用的是 PlatformIO IDE,所以串口的显示 有些不太好用,别个都用STC-ISP这个软件,我们不行,我们有内置的一个脚本来使用,
platformio device monitor --port /dev/cu.usbserial-120 -b 4800 --encoding UTF-8 就是这个脚本,port就是你的设备路径,b就是你的波特率,encoding 就是字符编码集。
同时收发数据
还有个更高级的工具,就是VSCode 有个 微软的工具,叫做 Serial Monitor,串口监视器,他可以直接监听串口,选择波特率,显示模式为Hex,这样子就可以直接观察输出,而发送也可以直接在发送端口输入字符,来输出,同时也可以选择按文本模式或Hex格式输入。
同时收发的代码:
1 | #include<8052.h> |
这里的头文件里,要开一个串口的中断,EA =1, ES =1; 这两句 就是这样子来做的。
1 | #ifndef __UART_H__ |
LED点阵屏
74HC595芯片
这里讲到了一个特殊的芯片,74HC595,这是一个可以用3个引脚控制8位甚至更多引脚的一个比较牛逼的芯片,可以多个芯片串联,共同控制多个8位引脚,来达到驱动点阵LED的目的。
用LED点阵屏 显示一个笑脸图案
点阵屏驱动函数,matrix_led.h
1 | #ifndef __MATRIX_LED_H__ |
代码main.c:
1 | #include<8052.h> |
用LED点阵屏 显示一个流动的Hello!
这里开始有点体会到了 这个时钟线路的意义,他可以把IO口的时序设计一下,因为之前只有很少的几个电路,IO引脚也不是很多,所以说IO通过时钟信号来作为泵,来传入0/1.
上代码,matrix_led.h
1 | #ifndef __MATRIX_LED_H__ |
主函数main.c:
1 | #include<8052.h> |
ani这个实际上是一个数组,固定了”Hello!”这个字符的字模,也就是LED哪个地方亮,哪个地方不亮,并且是一个比较大的字模,足足有328这么大,而且加上了头尾加的为了这个LED的效果显示流畅故意加的空白行,所以这个数组大概是488这么大,动态给他显示出来就ok了。
DS1302时钟芯片
用DS1302时钟芯片做一个不靠中断的时间表
这篇代码比较长,所以全给贴上来一份吧,
delay.h
1 | #ifndef __DELAY_H__ |
lcd1602.h,
1 | #include<8052.h> |
ds1302.h代码:
1 | #ifndef __DS_1302_H__ |
主函数代码main.c:
1 | #include<8052.h> |
这个代码结合1602,DS1302,代码量感觉比较大,深入学习了时钟引脚带动电路运转的逻辑。
结合定时器,LCD1602,DS1302这几个,做一个综合时钟模块
这块他说比较难,但是 对我们来说比较简单了已经,既然已经封装成了函数,无非也就是调用一下而已。
确实比较麻烦,而且很简单,就是程序逻辑调用,日期的越界判断之类的,把代码贴上来吧,这个有点复杂的程序还是,另外我写的也不定能按照他的完全,还有bug,等有心情了再慢慢修吧,或者借助ai帮你修复吧。
delay.h的代码:
1 | #ifndef __DELAY_H__ |
lcd1602.h代码:
1 | #include<8052.h> |
ds1302.h代码:这里有个时间的格式,本来原来写的是 unsigned char,但是为了越界判断好判断,改成了 char,这样子范围就变成 -128~127,因为日期这个程序所有位都不会超过100,所以导师也还好的,而且方便减的时候做判断。
1 | #ifndef __DS_1302_H__ |
keys.h代码:
1 | #include<8052.h> |
timer0.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 #include<8052.h>
static unsigned int T0TimerCount = 0;
void Timer0_Init() {
// 工作模式寄存器,0x01 代表:M0=1 M1=0 代表定时器工作在16位累加器,TH0 和 TL0 两个8位寄存器
TMOD = 0x01;
TF0 = 0; // 中断标志位
TR0 = 1; //
TH0 = 19456/256; // 这里是 因为我的单片机是 11.0592Mhz的晶振,改为按50ms 触发一次中断
TL0 = 19456%256;
ET0 = 1;
EA = 1;
PT0 = 0;
}
/*
定时器中断函数
void Timer0_Routine() __interrupt(1) {
unsigned int n = ((unsigned int)TH0 << 8) | TL0;
n += 19456;
TH0 = n >> 8;
TL0 = n;
T0TimerCount++;
// 50ms 中断一次 20*50 = 1000ms = 1s
if(T0TimerCount >= 20) {
T0TimerCount = 0;
// TODO 这里是你想实现的控制位
}
}
*/
最后,最重要的主函数:
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141 #include<8052.h>
#include "delay.h"
#include "intrins.h"
#include "lcd_1602.h"
#include "ds1302.h"
#include "keys.h"
#include "timer0.h"
unsigned char keyNum = 0;
unsigned char MODE = 0;
unsigned char SetTimeSelect = 0;
unsigned char TimeFlag = 0;
void ShowTime() {
DS1302_ReadTime();
LCD_ShowNum(1,1, DS1302_Time[0], 2);
LCD_ShowNum(1,4, DS1302_Time[1], 2);
LCD_ShowNum(1,7, DS1302_Time[2], 2);
LCD_ShowNum(2,1, DS1302_Time[3], 2);
LCD_ShowNum(2,4, DS1302_Time[4], 2);
LCD_ShowNum(2,7, DS1302_Time[5], 2);
}
void SetTime() {
switch (keyNum) {
case 2:
SetTimeSelect++;
SetTimeSelect %= 6;
break;
case 3:
DS1302_Time[SetTimeSelect]++;
if(DS1302_Time[0]>99) { DS1302_Time[0] =0;}
if(DS1302_Time[1]>12) { DS1302_Time[1] =1;}
if(DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 || DS1302_Time[1]==8 || DS1302_Time[1]==10 || DS1302_Time[1]==12) {
if(DS1302_Time[2]>31) { DS1302_Time[2] =1;}
} else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11) {
if(DS1302_Time[2]>30) { DS1302_Time[2] =1;}
} else if(DS1302_Time[1]==2) {
if(DS1302_Time[0]%4 ==0) {
if(DS1302_Time[2]>29) {
DS1302_Time[2] = 1;
}
} else {
if(DS1302_Time[2]>28) {
DS1302_Time[2] = 1;
}
}
}
if(DS1302_Time[3]>24) { DS1302_Time[3] =0;}
if(DS1302_Time[4]>60) { DS1302_Time[4] =0;}
if(DS1302_Time[5]>60) { DS1302_Time[5] =0;}
break;
case 4:
DS1302_Time[SetTimeSelect]--;
if(DS1302_Time[0]<0) { DS1302_Time[0] =99;}
if(DS1302_Time[1]<1) { DS1302_Time[1] =12;}
if(DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 || DS1302_Time[1]==8 || DS1302_Time[1]==10 || DS1302_Time[1]==12) {
if(DS1302_Time[2]<1) { DS1302_Time[2] =31;}
} else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11) {
if(DS1302_Time[2]<1) { DS1302_Time[2] =30;}
} else if(DS1302_Time[1]==2) {
if(DS1302_Time[0]%4 ==0) {
if(DS1302_Time[2]<1) {
DS1302_Time[2] = 29;
}
} else {
if(DS1302_Time[2]<1) {
DS1302_Time[2] = 28;
}
}
}
if(DS1302_Time[3]<0) { DS1302_Time[3] =23;}
if(DS1302_Time[4]<0) { DS1302_Time[4] =59;}
if(DS1302_Time[5]<0) { DS1302_Time[5] =59;}
break;
}
if(SetTimeSelect == 0 && TimeFlag == 1) { LCD_ShowString(1,1, " "); } else {
LCD_ShowNum(1,1, DS1302_Time[0], 2);
}
if(SetTimeSelect == 1 && TimeFlag == 1) { LCD_ShowString(1,4, " "); } else {
LCD_ShowNum(1,4, DS1302_Time[1], 2);
}
if(SetTimeSelect == 2 && TimeFlag == 1) { LCD_ShowString(1,7, " "); } else {
LCD_ShowNum(1,7, DS1302_Time[2], 2);
}
if(SetTimeSelect == 3 && TimeFlag == 1) { LCD_ShowString(2,1, " "); } else {
LCD_ShowNum(2,1, DS1302_Time[3], 2);
}
if(SetTimeSelect == 4 && TimeFlag == 1) { LCD_ShowString(2,4, " "); } else {
LCD_ShowNum(2,4, DS1302_Time[4], 2);
}
if(SetTimeSelect == 5 && TimeFlag == 1) { LCD_ShowString(2,7, " "); } else {
LCD_ShowNum(2,7, DS1302_Time[5], 2);
}
}
void main() {
LCD_Init();
DS1302_Init();
Timer0_Init();
LCD_ShowString(1,1, " - - ");
LCD_ShowString(2,1, " : : ");
DS1302_SetTime();
while(1) {
keyNum = Key();
if(keyNum==1) {
if(MODE ==0) { MODE = 1;}
else if(MODE ==1) { MODE = 0; DS1302_SetTime();}
}
switch(MODE) {
case 0:
ShowTime();
break;
case 1:
SetTime();
break;
}
}
}
void Timer0_Routine() __interrupt(1) {
unsigned int n = ((unsigned int)TH0 << 8) | TL0;
n += 19456;
TH0 = n >> 8;
TL0 = n;
T0TimerCount++;
// 50ms 中断一次 20*50 = 1000ms = 1s
if(T0TimerCount >= 10) {
T0TimerCount = 0;
// TODO 这里是你想实现的控制位
TimeFlag = !TimeFlag;
}
}
再说一遍,这个代码还有bug,如果抄的话 不要照抄,逻辑上的问题要自己修一下。
蜂鸣器
让蜂鸣器响
这里是实现蜂鸣器+按键+LED数码管的一个例程,这里数码管是静态的了,要把 之前看过的数码管代码,清空的要删除掉。所以主函数可以这么写:
1 | #include<8052.h> |
实现的功能大概是 按独立按键,LED数码管上显示什么数字,顺带中间,蜂鸣器以i hz 响一下。
实现任意一个乐谱
这里是天空之城的乐谱,其实乐谱有让ai帮我抓了一个出来,但是道理其实懂了就很好写谱子。
还有个东西,是一些做铺子的乐理知识,大概是12平均律,他的每个音调的频率,比如中央C的音调频率定为440Hz,那按 低中高,三个音调,平均每个相差一倍,那么低音就是220hz,高音就是440*2=880hz,然后每个音阶,他在钢琴上有7个白键,5个黑键,按照经典排布,音阶关系大概是全全半,全全全半的关系,同时再结合他的频率,按照 1是比440高的频率,按照 上一个音符 * 2^(1/12) 的关系,比440低的音符,按照440/ 2^(1/12) 的关系,推出来的音符频率表,再取整,得出来这个表:
1 | 1 262 |
这里只算了12*3 = 36个音符,实际可以写的更全一点。
beep.h的代码:
1 | #ifndef __BEEP_H__ |
然后上面是定义了音符的位置,在main.c 中有音符和他对应频率的关系,
1 | #include<8052.h> |
FreqTable 这个定义了每个音符频率,对应的定时器的初始装载值,超过65535-这个值,定时器就会溢出重置进入中断,而我们要做的就是使用这个中断函数,当超过这个值之后,定时器中断里,不断的反转蜂鸣器的IO,为蜂鸣器提供一个PWM信号,这样子不断调整他的占空比,来达到输出不同音高的目的,这就是调整定时器装载值的目的。
而至于谱子,只是一个灵魂,可以移植,如果移植的话,只要把 频率对应的表 修改一下,其他定义完全不需要变。里面还有个小星星的谱子。