引言
本篇内容主要根据b站江科大单片机教学视频总结而来,并附上自己的一些感悟
https://www.bilibili.com/video/BV1Mb411e7re?p=1
老师讲的很详细,大家也可以跟着老师一步步学习
首先,我想从告诉大家如何看懂原理图开始,因为只会一种单片机不代表能够使用所有的单片机
真正广泛的原理图应该分成功能块。可能有一个部分用于电源输入和电压调节,或微控制器部分,或专门用于连接器的部分。尝试识别哪个部分是哪个部分,并遵循从输入到输出的电路流程。优秀的原理图工程师甚至可能像电子书一样放置电路,左侧输入,右侧输出。
原理图在哪找?
Ⅰ、立创商城(推荐);Ⅱ、半岛小芯。
这里我们就以我们的51单片机为例

1.我们首先需要找到它的主控芯片

2.接着看它的引脚,进而浏览每一个模块
原理图上的器件类型繁多,但依旧遵循“二八原则”,80%都是常见的器件,因此我们只需熟悉常见器件即可。每一类器件,通常使用英文名称简写标记,比如电阻通常标记为Rn(n为数字),常见元件如下表

有时,为了使原理图更清晰,我们将给网络命名并标记它,而不是在原理图上布线。假设没有连接它们的可见导线,假定连接具有相同名称的网络。名称可以直接写在网络的顶部,也可以是“标签”,悬挂在电线上。
我们以最简单的控制led灯点亮为例

MCU上 P1,P2,P3都为寄存器,是将数据存储下来并发送出去的模块
通过以上网络连接,我们就实现了通过开发MCU来对LED的操控
(网络通常被赋予一个名称,专门说明该线路上信号的用途。例如,电源网可以标记为“VCC”或“5V”,而串行通信网络可以标记为“RX”或“TX”。)
下面,我将从每一个实现的项目来讲起
一、点亮第一个LED灯
在上面的图中,我们可以看到LED最终连上了单片机的管脚。而单片机需要通过CPU控制寄存器的值,进而通过驱动器加大控制力度,由控制电路输出高低电平(对应寄存器1/0)。因此,程序需要在对应的寄存器上写1或0,即可控制LED的亮灭
#include <STC89C5xRC.H>
void main()
{
P2 = 0xFE; //1111 1110
}
注:单片机采用的16进制 1111 1110 到0xFE是二进制到16进制的计算
有时候无法亮灯可能是因为你使用的单片机型号是STC89C52RC而不是STC89C52,在烧录软件中更改型号即可,或者更换环境,重装驱动,万事开头难,不要放弃
这里再介绍一个如何使用vs code 搭建单片机开发环境,因为keil实在太落后了,开发起来对新手很不友好
主要参考另一篇csdn
按照上述方法做了后,若发现无法使用keil的Atmel库可以跟我做一遍

打开下载的EIDE,选择“项目属性”->选择“包含目录”->"+"->打开keil的文件夹->C51->inc->Atmel问题就解决了
二、实现LED的闪烁
这里需要用到delay函数,delay函数的原理是
利用CPU执行空循环所需的时间来引入延迟
那么我们如何准确写出让原件延时的delay呢?
-------》使用stc-isp来生成延时的函数

选择晶振的频率以及延时的时间拷贝即可
#include <STC89C5xRC.H>
#include <INTRINS.H>
void Delay500ms() //@12.000MHz
{
unsigned char i, j, k;
_nop_(); //An empty function, from INTRINS.H
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main()
{
P2=0xFE;
Delay500ms();
P2=0xFF;
Delay500ms();
}
三、LED流水灯
很简单,掌握二进制转化为16进制很容易实现
#include <STC89C5xRC.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; //1111 1110
Delay500ms();
P2=0xFD; //1111 1101
Delay500ms();
P2=0xFB; //1111 1011
Delay500ms();
P2=0xF7; //1111 0111
Delay500ms();
P2=0xEF; //1110 1111
Delay500ms();
P2=0xDF; //1101 1111
Delay500ms();
P2=0xBF; //1011 1111
Delay500ms();
P2=0x7F; //0111 1111
Delay500ms();
}
}
四、独立按键控制LED灯亮灭
首先,我们需要了解独立按键

这几个引脚对应着MCU上的寄存器:

我们可以这样实现按下按键时亮灯,松开按键时熄灯
#include <STC89C5xRC.H>
void main()
{
while(1)
{
if(P30==0)
{
P20=0;
}
else
{
P20=1;
}
}
}
五、通过独立按键来控制LED亮灭状态+消抖
我们为什么要消抖?
按键电路中消除因为接触不良或快速操作导致的误触发现象。当按键被按下或释放时,由于机械接触不良或电气噪声,可能会在短时间内产生多次电信号的快速变化,这会导致单片机错误地识别为多次按键操作
消抖的实现:
void main()
{
while(1)
{
if(P3_1==0)
{
Delay(20);
while(P3_1==0);
Delay(20);
}
P2_0 = ~P2_0;
}
代码的实现
#include <STC89C5xRC.H>
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{
while(1)
{
if(P31==0)
{
Delay(20);
while(P31==0);
Delay(20);
P20=~P20;
}
}
}
六、点亮晶体管

如图,我们需要先指定点亮哪一个晶体管
即指定最上面的那一排:LED8-1
再指定该晶体管需要点亮那些个区域
就比如我想点亮一个5,我就需要点亮a,b,d,e,g五个晶体管
attention:dp代表的是point,即晶体管右下角的小点

需要点亮的记上一再根据(需要从下往上读)二进制与16进制 的转换
我们就可以得到这样一个表:(共阴极类)

那么我们每想用晶体管显示一个数字就必须去进行这么复杂的计算吗?
这就引出了我们的模块化编程,通过引用已经编写好的库中的函数大大缩短编码的时间,且库的调用是非常灵活的,在任何一个电脑上都可以进行使用
模块化编程:
模块化编程 是指程序核心部分定义好功能的接口,而具体的实现留给各个模块去做。举个现实世界的例子:我们可以在电脑的PCI插槽上安装显卡、声卡或者网卡,原因就是这些硬件都按照PCI接口的规范来制造的,就像搭积木一样,建立一个你的武器库。
1)创建.c文件(xxx.c)
2)在.c文件内定义需要的函数
3)创建.h文件,文件名要与.c文件一致
4)在.h文件中声明在.c中定义的函数
5)在main.c文件中包含.h文件,在main函数直接调用自定义的函数
如上我们就可以写一个关于晶体管显示的.c.h文件
#ifndef __NIXIE_H__
#define __NIXIE_H__
void Nixie(unsigned char Location,Number);
#endif
#include <STC89C5xRC.H>
unsigned char NixieTable[]={
0x3f,0x06,0x5b,0x4f,
0x66,0x6d,0x7d,0x07,
0x7f,0x6f,0x77,0x7c,
0x39,0x5e,0x79,0x71, 0x00};
void Delay(unsigned int xms) //@12.000MHz
{
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:
P24=1;P23=1;P22=1;break;
case 2:
P24=1;P23=1;P22=0;break;
case 3:
P24=1;P23=0;P22=1;break;
case 4:
P24=1;P23=0;P22=0;break;
case 5:
P24=0;P23=1;P22=1;break;
case 6:
P24=0;P23=1;P22=0;break;
case 7:
P24=0;P23=0;P22=1;break;
case 8:
P24=0;P23=0;P22=0;break;
}
P0=NixieTable[Number];
Delay(1); // Shadow elimination
P0=0x00; // reset
}
void main()
{
while(1)
{
Nixie(1,1);
Nixie(2,2);
Nixie(3,3);
}
}
(很多人在开始时都会很抗拒直接搬别人的库包括我,但我们要知道,技术的本质就是解决问题,独立开发的都是些嵌入式开发大牛,关我们刚学单片机的小白什么事?独立开发可以等到后期有一定基础才开始进行)
七、LCD1602调试工具
依旧参照我们的模块化编程,调用老师的库函数

库函数https://d.nuelv.com/pc/xzji.com-ODE.rar
https://pan.xunlei.com/s/VO9bitWlVDwXq-oUvSfLHNfyA1?pwd=8nhu#
(包括定时库,延时库,矩阵键盘库)
下面是一段实例:
#include <STC89C5xRC.H>
#include "LCD1602.h"
void main()
{
LCD_Init();//初始化
LCD_ShowString(1,3,"Hello");//从第一行,第三列开始显示Hello
LCD_ShowChar(1,1,'q');
LCD_ShowNum(1,9,123,3);
while(1)
{
}
}
八、矩阵键盘

如图所示此4*4的键盘即为矩阵键盘

我们来看看它的原理图

P1_3到P1_0是控制列数的
P1_7到P1_4是控制行数的
举个例子,如果我想根据第一行第一列来当作判断条件
P1_3=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=1;}
这一块我推荐自己写头文件,比较简单同时也可以让自己更加熟悉自己的库,锻炼代码能力
九、定时器
传统51单片机有3个定时器(T0,T1,T2),51系列单片机的更新迭代都是向下兼容的。定时器的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的定时器个数和操作方式,但一般来说,T0和T1的操作方式是所有51单片机所共有的。STC89C52自带计时模块
在之前的学习中,我们用到了delay()函数来让程序执行过程中出现中断,当计数完成也就代表延时结束,简单点说就是让cpu通过不停的计数来消耗时间,所以这种方式有个很大的弊端,就是当cpu “死跑” 延时的时候,是做不了其他事情的,这个时候就需要一个额外的工具来帮助cpu完成计时,这就是我们定时器存在的意义

计数器由时钟提供脉冲信号,经过分频器,每来一次脉冲计数+1,我们可以先设置一个计数器的初值 x,当计次超过65535时,下一个脉冲就会产生溢出,触发溢出中断,程序可以捕获到这个中断,就可以得知此次经历了(65535-x+1)us,这样就完成了一次定时。
为什么65536就溢出了呢?
计数器里有两个时基单元,TL0(低8位)和 TH0(高8位)总共16位,所以叫16位定时/计数器,它最多可以计数 2^16=65535次。
那么我们应该怎么告诉单片机要使用定时器了呢?这就需要我们的报备过程:

(TMOD是工作方式寄存器,用于向MPU报备应该怎么计时)
(C/T意为count/time,一个用于计数,一个用于计时)
若想要使用定时器1,则在7号gate给上 高电位,另外M1和M0是控制定时模式的,但我们经常使用的只有模式一,详细如下图:

一般,定时器需要与中断系统搭配起来使用
- 中断请求:当一个中断事件发生时,相应的中断标志位被设置。
- 中断检查:CPU检查中断允许寄存器,确定是否允许当前的中断请求。
- 中断响应:如果中断被允许,CPU会暂停当前程序,保存当前状态(如程序计数器和某些寄存器),然后跳转到中断服务程序。
- 中断服务:执行中断服务程序,处理中断事件。
- 中断返回:中断服务程序执行完毕后,通过中断返回指令(RETI)返回到主程序,恢复之前保存的状态,继续执行。
中断与定时类似,都需要向MPU报备:

中断器中的寄存器ET0、EA、PT0
要初始化中断允许控制寄存器、中断优先级控制寄存器。
为了接收到定时器的中断请求,需要ET0=1和EA=1
若选择低优先级,则需要PT0=0
初始化代码如下:
void Timer0_Init()
{
TMOD = 0x01; // 0000 0001
TF0 = 0;
TR0 = 1;
TH0=64535/256;
TL0=64535%256;
ET0=1;
EA=1;
PT0=0;
}
当然这只是初始化代码,要想真正发挥中断器的作用还需要使用中断服务程序

该如何编写一个中断服务程序呢?
void int0_work() interrupt 0//中断程序主体
{
Delayms(5);
if(KEY3==0)
{
LED1=!LED1;
}
while(!KEY3);
如果满足了中断条件,因为使用了中断程序,就会在当前执行语句保留一个断点(自行执行)进而执行终端服务程序直到走出循环。
(中断函数是特殊的函数,interrupt是<REG52.H>文件中的关键字,1是中断号,中断触发后会根据中断号跳转到相应的中断函数,注意:中断服务程序不能像常规函数一样被外部调用)
此外,中断还具有优先级:
-
中断优先级寄存器(IP):51单片机使用IP寄存器来设置中断的优先级。IP寄存器中的每一位对应一个中断源,用于设置该中断源的优先级。
-
高优先级中断:如果一个中断被设置为高优先级,那么在它发生时,即使当前正在执行低优先级的中断服务程序,它也会被立即处理。
-
低优先级中断:如果一个中断被设置为低优先级,那么在高优先级中断服务程序执行期间,它不会被处理,直到高优先级中断服务程序执行完毕。
-
中断嵌套:51单片机支持中断嵌套,即一个中断服务程序执行期间可以被另一个更高优先级的中断打断
基于此,我们可以实现一个定时器闹钟
代码实现如下:
#include <STC89C5xRC.H>
#include "Timer0.h"
#include "Delay.h"
#include "LCD1602.h"
unsigned char Sec, Min=59, Hour=23;
void main()
{
Timer0Init();
LCD_Init();
LCD_ShowString(1,1,"Clock:");
while(1)
{
LCD_ShowNum(2,1,Hour,2);
LCD_ShowString(2,3,":");
LCD_ShowNum(2,4,Min,2);
LCD_ShowString(2,6,":");
LCD_ShowNum(2,7,Sec,2);
}
}
void Timer0_Routine() interrupt 1 //中断函数标识,含优先级
{
static unsigned int T0Count; //静态变量,拥有局部作用域,全局生命周期
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++; //T0Count计次,对中断频率进行分频
if(T0Count>=1000)//1000ms
{
T0Count=0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
Hour++;
if(Hour>=24)
{
Hour=0;
}
}
}
}
}
1694

被折叠的 条评论
为什么被折叠?



