单片机——蓝桥杯备赛
蓝桥杯比赛指定单片机跟51单片机差不多,可以通过淘宝国信长天购买。
本次学习跟着b站小蜜蜂老师的视频
使用的相关软件为 keil4
和 STC-ISP
相关软件使用
keil4
作用是编写代码和编译
STC-ISP
的作用是将keil4
得到的16进制码下载到我们的板子上
keil4使用
- 选一个位置存代码,新建文件夹
- 打开
keil4
,顶栏选择Project
,再选择第一个New Project
,找到刚才建的文件夹,自己写一个项目名,然后在Atmel
中找到AT89C52
- 新建c文件,保存在刚才建的项目文件夹中
- 在左侧项目栏中右键项目文件夹,找到
add
什么那个,然后把建的c文件加进去 - 改成16进制输出
- 写代码,编译
STC-ISP使用
- 点打开程序文件,打开要下载到板子上的.hex文件
- 点下载/编程
- 按板子上的下载按钮完成下载
下面我们实现各个元件的控制
各个基本元件控制
LED指示灯的控制
在蓝桥杯指定单片机上已经有了诸多外设,传感器等等,板子的内部电路固定。所以需要通过特定的电路来对电路原件做相应的控制,而不像大一的电子设计课一样需要自己连线,可以自己定每条线插在哪个口。
电路部分
板子上共有8个我们需要控制的LED,由一个三八译码器74HC138,一个锁存器74HC573,和或非门构成,而我们就需要通过改变这几个原件的输入输出从而控制LED亮灭,那我们一一来看一看电路图。
- 三八译码器(74HC138)

简单的三八译码器,有三个输入控制对应的八个输出,符合二进制规律
注意的是,该三八译码器八路输出中只有一路是Low,其他都是High
P25, P26, P27连接HC138的 A, B, C输入端,分别对应二进制的第0位,第1位和第2位
本次控制LED我们需要让Y4为Low,所以我们要让CBA为100时,此时二进制对应为4即Y4为Low,其余输出均为High
- 锁存器(74HC573)
有用的只有图中Y4C输入:
Y4C为High,那么八个输出和八个输入保持一致(输出跟着输入随时变)
Y4C为Low,那么八个输出并不改变(输出不跟着输入变,还是原来的输出值)
那么Y4C是啥呢?如下:

图有点糊,但没啥事
WR一般都是0,Y4和Y4C相反
所以在上面我们让CBA是100,Y4为Low,那么Y4C就是High,此时我们就可以顺利通过P0^1到8来控制八个 小灯泡了,0为亮,1为灭,即P0 = 0x00代表8个小灯泡全亮,P0 = 0xff代表8个小灯泡全灭。
至此,有关LED的电路部分介绍完毕,我们开始进行代码的编写。
代码部分
整个51单片机编程使用的都是C语言,很好上手
引入头文件
引入此头文件之后我们可以用特定字母代表指定引脚,比如P2^5代表电路图中的P25
对应引脚定义
sbit HC138_A = P2^5;
sbit HC138_B = P2^6;
sbit HC138_C = P2^7;延迟函数
void Delay(unsigned int t){
while(t--);
while(t--);
while(t--);
}
每个延迟函数都这么写就行
LED控制函数
void LEDRunning(){
unsigned int i;
//Y4 = 0
HC138_C = 1;
HC138_B = 0;
HC138_A = 0;
//跑马灯
for (i = 1; i <= 8; i++){
P0 = 0xff << i;
Delay(60000);
}
for (i = 1; i <= 8; i++){
P0 = ~(0xff << i);
Delay(60000);
}
//8个LED整体闪三下
for(i = 0; i < 3; i++){
P0 = 0x00;
Delay(60000);
P0 = 0xff;
Delay(60000);
}
}主函数
void main(){
while (1){
LEDRunning();
}
}简简单单主函数,不断循环LED控制函数
编写完代码下载到板子上,进行验证以及修改
至此,我们就可以顺利控制LED了
继电器和蜂鸣器
电路部分
如图,N_RELAY
连的是继电器,N_BUZZ
连的是蜂鸣器,相对应的输入分别是P04和P06,输入为High时工作。
和上面LED不同,与锁存器相连的是Y5C,所以要控制继电器和蜂鸣器,我们需要使74HC138的输出Y5为Low,其他输出为High,即CBA为101;
代码部分
代码优化
由于本次涉及到了Y5C,我们可以写一个函数来决定HC138的输出
void InitHC138(unsigned char n){
switch(n){
case 4:
HC138_C = 1;
HC138_B = 0;
HC138_A = 0;
break;
case 5:
HC138_C = 1;
HC138_B = 0;
HC138_A = 1;
break;
case 6:
HC138_C = 1;
HC138_B = 1;
HC138_A = 0;
break;
case 7:
HC138_C = 1;
HC138_B = 1;
HC138_A = 1;
break;
}
}通过这个函数,我们只需一个数字便可以得到我们想要的输出从而选择控制哪部分电路
控制继电器和蜂鸣器
//继电器吸合
InitHC138(5);
P0 = 0x10;
Delay(60000);
P0 = 0x00;
//蜂鸣器叫一下
InitHC138(5);
P0 = 0x40;
Delay(60000);
P0 = 0x00;
共阳数码管
如图,Y6C控制数码管位置,Y7C控制数码管内容
Y6C: P0^0-7与com0-7相连,控制8个数码管的明灭
Y7C: 每个数字或字母由组成数码管的8个LED决定,对应如下:
unsigned char SMG_duanma[18] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, |
静态显示
由于控制数码管内容的只有P0^1-7,所以在静态显示中,如果有多个数码管同时亮,则显示的内容一致
数码管位置及内容控制函数
void ShowSMG_Bit(unsigned char dat, unsigned pos){
//数码管位置
InitHC138(6);
P0 = 0x01 << pos;
//数码管内容
InitHC138(7);
P0 = SMG_duanma[dat];
}8个数码管依次亮
void SMG_Static(){
unsigned int i;
for(i = 0; i < 8; i++){
ShowSMG_Bit(i,i);
Delay(50000);
Delay(50000);
Delay(50000);
}
}
动态显示
动态显示是根据人眼的视觉暂留现象以及发光二极管的余晖效应,只要8个LED闪的够快,那么在看起来就是一直亮的。
动态显示可以大大节省I/O端口,而且功耗低
动态显示代码
void SMG_Dynamic(){
unsigned int j = 0;
ShowSMG_Bit(1,1);
Delay(500);
//延迟也不能太短,LED点亮也需要时间,太短会让数码管看起来很暗
ShowSMG_Bit(1,1);
Delay(500);
ShowSMG_Bit(2,2);
Delay(500);
ShowSMG_Bit(3,3);
Delay(500);
ShowSMG_Bit(4,4);
Delay(500);
ShowSMG_Bit(5,5);
Delay(500);
ShowSMG_Bit(6,6);
Delay(500);
ShowSMG_Bit(7,7);
Delay(500);
}动态显示的延迟函数
动态显示的延迟函数不能像之前的一样,而应该在延迟函数的
while
函数中一直进行动态显示void Delay_Dynamic(unsigned int t){
while(t--){
SMG_Dynamic();
}
}主函数
void main(){
while(1){
SMG_Static();
SMG_Dynamic();
Delay_Dynamic(100);
}
}
这样我们就可以随意控制8个数码管显示任何数字以及实现我们想要完成的变化了
按键
独立按键
单个独立按键内部结构如下图所示,按下按键时输出低电平(接地),松开输出高电平(接VCC电源了)

独立按键共有四个:S7, S6, S5, S4,电路图如下所示,其中最左侧一列四个为独立按键
将J5
跳帽连接2,3引脚切换至独立按键模式
4个独立按键电路非常简单,P3^0-3依次连接独立按键的S7, S6, S5, S4
在代码中只需将S与P3相对应:
sbit S7 = P3^0; |
在按键操作的代码中,我们需要做去抖动处理,从而识别有效按键,比如下面代码:
if(S7 == 0){ |
我们通过使用Delay(100)
实现了去抖动。
S7按下则L1亮,松开则灭
同时,我们用独立按键控制灯泡明灭会遇到两种情况:第一种情况就是上述的按下灯亮,松开灯灭;第二种就是按下灯亮,松开灯不灭,再次按下该按键灯才灭。第二种情况可以如下实现:
//sta表示当前状态 |
在上面代码if
判断的最后,我们使用了while(S7 == 0);
从而避免按S7的时候程序反复进入该if
判断中,这样可以使得按一下按键只进入一次if
判断语句。
矩阵键盘
电路部分
将J5
跳帽连接1,2引脚切换至矩阵键盘模式

与独立按键不同,矩阵键盘左端不接地,而是连接I/O输入,当左端输入为0且右端输出为0时,说明该按键被按下
所以,我们需要扫描按键,即逐行接低电平,检测每列输出,输出低电平则可以锁定行列坐标
代码部分
首先,我们将行用R1-4表示,列用C1-4表示(由于我们引用的reg52.h
头文件中没有P4端口,所以我们需要提前定义一下)
//定义P4口, 电路图中P36为P42,P37为P44 |
然后,我们开始逐行扫描矩阵键盘
unsigned char key_num; |
通过上述代码,我们便可以确定哪个按键被按下,同时在对应的if判断语句中写下我们要进行的操作即可实现矩阵键盘相关的控制。
中断
在单片机中,内设和外设的主要交互方式有两种:轮询和中断,中断的相关原理跟计组中的中断一样
中断系统
在51单片机中,有5个中断源(要记住每个中断源对应的终端号0-4):
INT0
:外部中断0IF0
:定时/计数器0INT1
:外部中断1IF0
:定时/计数器1R1/T1
:串口中断
中断系统结构如下图所示:
按照该图从左到右的顺序:
- 最左段的表示五个中断源相应的输入
IT0
和IT1
置1时表示响应下降信号,置0表示响应高电平- 在
IE
寄存器中,EA表示中断总开关,如果想要相应中断,EA必须置1;EA左边的那五个则对应五个中断源各自的开关,同样置1相应 IP
寄存器用来设置相应优先级,一般用不到
中断函数
一般情况下,中断的处理函数有两个,一个是中断初始化函数,一个是中断服务函数
中断初始化函数
其实就是对中断相关开关进行赋值,初始化
void Init_INT0() { |
然后再在主函数里最前面引用一下即可
int main() { |
中断服务函数
中断服务函数有特殊格式要求:
void 函数名() interrupt 中断号
比如:
void ServiceINT0() interrupt 0 { |
这样我们就可以实现中断处理了
单片机默认的INT0
中断对应的按键为S5
,要注意将J5
跳帽链接23引脚
这样当我们按下S5
时,我们会发现板子停止原来的工作,开始执行中断服务函数中我们设定的操作,当设定操作完成,板子又会继续原来的工作,即完成了一次中断
计数器和定时器
计数原理
计数器每收到一个脉冲,就会加一,当计数值累计至全为1时(8位255,13位8191,16位65535),再输入一个计数脉冲,计数器就会溢出回零,同时向内核发出中断请求,从而完成计数操作
计时器和定时器的控制寄存器包括:
- 计数初值寄存器
TH0/1,TL0/1
:我们知道,计数器只有在累计至各位全为1时才会发出中断请求。所以当我们要计数特定值时,我们就需要给计数器一个初始值:全为1时计数 - 我们要计的数,这样当计数器加上我们想要计的值时,他就会刚好溢出并发出中断请求 - 模式控制器
TMOD
:
- 中断标志寄存器
TCON
:
在TCON
中,与计数器启动有关的位为TR0
和TR1
,这两位置1时,分别控制计数器0和计数器1的启动
代码部分
计数器/定时器的编程方式也较为固定:
- 配置工作模式,即给
TMOD
寄存器赋一个初值来确定模式 - 计算计数器初值,将高八位赋给
TH0/1
,第八位赋给TL0/1
- 使能定时/计数器中断,即
ET0/1
置1(见上面中断放的那张灰色图) - 打开总中断,即
EA=1
- 启动定时器,即
TR0/1=1
void InitTimer0() { |
再写一个中断服务函数:
- 先初始化计数器,还计那么多数
- 再根据题意完成我们要的操作,比如一个定时器最多也就计65.5ms,当我们需要定的时超过这个时,我们就需要设置一个
count
来记录定时器溢出次数,这样就可以在相应溢出次数时完成我们需要的操作
unsigned char count = 0; |
定时器实现秒表功能
上面介绍了计数器和定时器最基本的使用方法并简单计时,下面我们来通过更复杂一点的代码并结合之前所学的知识来实现秒表功能
该功能涉及到内容:
- 数码管的动态显示
- 独立按键的应用
- 定时器
我们要实现的具体功能如下:
我们利用数码管设计一个秒表,使其具有清零、暂停、启动功能:
- 显示格式为:分(两位)- 秒(两位) - 每0.05s(两位:即满20向前一位秒进1)
- 独立按键控制(按键按下即为有效):
S4
:暂停/启动,S5
:清零
设计思路:
- 实现
HC138
选择函数来选择6(数码管位置)还是7(数码管内容) - 数码管显示函数:通过传入参数位置和内容来使特定一位显示特定内容
- 时间显示函数:调用数码管显示函数,利用动态显示来显示每一位该显示的内容
- 定时器相关函数:启动函数和中断服务函数
- 按键扫描函数:看看
S4
和S5
有没有被按下(做去抖动处理)
代码实现:
这里直接贴上全部代码:
|
好的,通过上面代码,我们就完成了题目中的要求。
秒表功能将我们很多之前学到的知识结合到了一起,算是一个非常好的练习题,它带我们回顾前面学到的一些功能,很推荐去自己写一写。
PWM脉宽调制
调制原理
PWM脉宽调制即在一个周期内,信号一部分为高电平,一部分为低电平
比如,当PWM脉宽信号频率为100Hz,占空比为60%时,此时PWM信号周期为0.01s,也就是100ms。那么在这100ms中,我们让前60ms为高电平,后40ms为低电平,就可以实现占空比60%了
我们通过PWM占空比,可以调节功率,比如控制LED的亮度(由于LED低电平亮,所以占空比越低LED越亮)
代码实现
我们可以利用定时器来实现PWM脉宽调制
比如,我们设定PWM脉宽信号频率为100Hz,占空比可通过按键调节
unsigned char count = 0; |
在上面代码中,我们就实现了PWM占空比为pwm_duty/100
,我们再通过其他函数来改变pwm_duty
的值便可以实现要求
串口通信
相关原理
波特率:串口每秒钟传输的位数(我们用的一般是9600)
我们通常使用定时器1的工作模式2(8位自动重装)来产生波特率,TL1
作为脉冲计数寄存器,TH1
作为自动重装寄存器,当计数到最大值溢出时,TH1
的值会自动装到TL1
中
所以,当我要产生9600BPS的波特率时
SMOD = 0
时,TH1 = TL1 = 0xfd
SMOD = 0
时,TH1 = TL1 = 0xfa
在串行口中有两个缓冲寄存器SBUF
,一个是发送寄存器,一个是接收寄存器。
串行发送时,CPU向SBUF
写入数据,发送完成后标志位TI
会置1,这时候需要我们手动让TI = 0
串行接收时,CPU从SBUF
接收数据,内核接收到一个完整数据后,会将标志位RI
置1,这时候我们从SBUF
读取即可
根据上图,我们在Uart初始化函数
中,需要让SCON = 0x50
。同时因为我们要用到定时器1,我们需要让TMOD = 0x20
,并开启定时器1相关使能
同时由于我们使用的单片机与我们头文件引用的库并不完全相同,我们需要在文件最前面写sfr AXUR = 0x8e
来将AXUR
定义,并在初始化函数中让AXUR = 0x00
代码实现
|
通过上面代码,我们并可以实现在单片机开机后向CPU发送5a和a5,并在接收到CPU发送的数据后加一再发回去
进阶应用
那么接下来,我们对串口通信进行更难一点的应用来加强我们的理解
我们要实现的功能如下:
那么对于略微复杂的问题,我们在敲代码之前一定要先想清楚我们都要干嘛:
- 先初始化系统,关掉所有LED,蜂鸣器和继电器
- 初始化
Uart
(用到定时器1),对相关使能以及模式进行赋值 - 向上位机发送字符串
“Welcome to My System”
- 接受上位机发送的信号,并据此进行相应控制
好的,上面就是我对这道题的全部思路,我的实现代码如下(为了体现整体逻辑,就不将代码拆开分功能实现了,直接全贴上来)
|