前言:在这次笔记中,我打算写一下作为初学者的把一个程序录入单片机中的流程,大概就是“打开冰箱、把大象放进去、关上冰箱门”的一个流程,不会涉及太多的专门的技术知识,特定的元件会在后续的学习中同步到我的文章中。
文章目录
单片机的介绍
一台能够工作的计算机要有这样几个部份构成: CPU( 进行运算、 控制)、RAM(数据存储)、ROM(程序存储) 、输入/输出设备(例如: 串行口、并行输出口等)。 在个人计算机上这些部份被分成若干块芯片,安装一个称之为主板的印刷线路板上。而在单片机中,这些部份全部被做到一块集成电路芯片中了,所以就称为单片机(也称微控制器 MCU),而且有一些单片机中除了上述部份外,还集成了其它部份如 A/D, D/A 等。体积不大,一般用 40 脚封装, 当然功能多一些单片机也有引脚比较多的, 如 68 引脚, 功能少的只有 10 多个或 20多个引脚,有的甚至只 8 只引脚。
51 单片机是对所有兼容 Intel 8031 指令系统的单片机的统称。该系列单片机的始祖是 Intel 的 8004 单片机,后来随着 Flash rom 技术的发展,8004 单片机取得了长足的进展,成为应用最广泛的 8 位单片机之一,其代表型号是 ATMEL公司的 AT89 系列,它广泛应用于工业测控系统之中。很多公司都有 51 系列的兼容机型推出,今后很长的一段时间内将占有大量市场。51 单片机是基础入门的一个单片机,还是应用最广泛的一种。需要注意的是 51 系列的单片机一般不具备自编程能力。
将程序导入单片机的基本操作流程
这个就是“如何把大象放进冰箱”样式的内容了,我将以“点亮一个LED灯的例子”,从“编程工具”、“编写程序”、“将程序放进单片机中”这几个基本流程谈。
编程工具
将程序写进51单片机中需要的工具有两个:
一个是用于编写程序的Keil uVision5
另一个是将程序烧录进单片机的stc-isp
这些均可以在官网进行下载,或者咨询购买51单片机店铺的客服,他们也会提供。
Keil uVision5
Keil5(Keil µVision 5)是一款嵌入式软件开发工具,由德国公司Keil Software开发。它提供了一个集成开发环境(IDE),包括编译器、调试器和仿真器等组件,可用于开发各种基于ARM架构的嵌入式系统。
说白了就是我们写程序的地方。
面板如下:
主要就是这四个板块:
区域①:进行常规操作、基本设置
区域②:查看文件
区域③:编写代码
区域④:报错警告和编译进程
stc-isp
STC-ISP 是一款单片机下载编程烧录软件,是针对STC系列单片机而设计的,可下载STC89系列、12C2052系列和12C5410等系列的STC单片机,使用简便。
这个百度百科就很简洁明了了。
面板如下:
目前我们需要关注的面板无非就是上面所说的三个:
①:选择匹配的型号,一般会在单片机有些,或者编译失败后在面板③处会告诉你应该选什么型号(都能检测型号了还要我们选,浪费时间);选择串口,选错串口会苦苦等待,没有结果。
②:下载程序、操作没什么难度。
③:烧录进程提示,以及官方的宣传。
实现一个“单片机的HelloWorld”——点亮LED灯
创建项目
1.创建文件夹,保存的时候要自己额外建一个,不然文件到处散。
2.新建源文件
做完这两步就可以开始编程了。
编写程序
编写程序使用的是Keil uVision5,我们在此编写一个及其简单的点亮一个LED灯的程序:
#include <REGX52.H>
void main()
{
P2=0x7F;//0111 1111
}
可以看出一些需要注意的基本事项,比如
引入正确的头文件:#include <REGX52.H>
,鼠标放在<REGX52.H>上右键,选择open可以看到这里头文件的内容。
可以看到这里的LED灯在程序中被定义为一个地址,相关的地址信息可以在头文件中看到,也可以在商家给的文件中看到。
0代表亮灯、1代表灭灯,要让处于1号位的D1灯亮,就需要0111 1111的赋值(我用的单片机LED灯是反的,无伤大雅)。但是计算机编译无法直接识别这样的数字,需要把他转换成16进制。有在线转换的网站、问chapgpt都可以解决问题,或者熟记以下表格。
至此,一个简单的点亮LED的程序编好了,接下来就是将程序烧录到单片机中去。
烧录程序
需要提前安装好驱动之类的东西,以防无法检测到单片机。
打开stc-isp,选择好型号和串口。
选择程序,发现找不到程序,这时候需要返回keil5去设置一个东西,keil5没有默认生成可以直接烧录的程序,需要设置一下。
回去发现能找到了,点击即可准备开始下载。
点击下载后,会处于这样的状态。
这时候就需要冷启动(将单片机从关闭的状态开启)。完成后就烧录好了。
至此,单片机中的HelloWorld——点亮一个LED灯就OK了!
以上叙述就是一个打开冰箱放大象的叙述方式,我写的很繁琐,但我觉得这是十分必要的,因为这种基础的操作忘记的后果是最傻逼的,直接导致没办法玩了。
学习进程中的程序记录
这一部分内容我将以“代码”+“运行效果”展示。其余内容不再赘述。
LED灯闪烁
#include <REGX52.H>
#include <INTRINS.H>
void Delay500ms(void) //@11.0592MHz
{
unsigned char data i, j, k;
_nop_();
i = 4;
j = 129;
k = 119;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main(){
while(1){
P2 = 0xFE;
Delay500ms();
P2 = 0xFF;
Delay500ms();
}
}
值得注意的是,软件延时计算器代码种的_nop_();
是一个未定义的函数,他的功能是不执行任何操作,因此需要插入一个新的头文件#include <INTRINS.H>
。
展示:
单片机闪烁演示
LED流水灯
代码:
#include <REGX52.H>
#include <INTRINS.H>
void Delay500ms(void) //@11.0592MHz
{
unsigned char data i, j, k;
_nop_();
i = 4;
j = 129;
k = 119;
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();
P2=0xFF;//1111 1111
Delay500ms();
}
}
后续将会学习用更高级的操作方式来取代复制粘贴。
展示:
led流水灯
还可以稍微优化一下,使得计时更加灵活:
#include <REGX52.H>
#include <INTRINS.H>
void Delay1ms(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms){
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{
int a = 50;
while(1)
{
P2=0xFE;//1111 1110
Delay1ms(a);
P2=0xFD;//1111 1101
Delay1ms(a);
P2=0xFB;//1111 1011
Delay1ms(a);
P2=0xF7;//1111 0111
Delay1ms(a);
P2=0xEF;//1110 1111
Delay1ms(a);
P2=0xDF;//1101 1111
Delay1ms(a);
P2=0xBF;//1011 1111
Delay1ms(a);
P2=0x7F;//0111 1111
Delay1ms(a);
P2=0xFF;//1111 1111
Delay1ms(a);
}
}
这样的话就只用修改a处的赋值就可以使得下面的计时一起变化,在未来通过按键输入的时候还可以使得计时在单片机上得到控制。
独立按键
轻触按键:相当于是一种电子开关,按下时开关接通,松开时开关断开,实现原理是通过轻触按键内部的金属弹片受力弹动来实现接通和断开。
独立按键控制LED灯亮灭
#include <REGX52.H>
void main()
{
P2_0=0;
while(1)
{
if(P3_1==0){
P2_0=0;
}else{
P2_0=1;
}
}
}
独立按键控制LED亮灭
独立按键控制LED状态(消抖)
对于机械开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,所以在开关闭合及断开的瞬间会伴随一连串的抖动。
需要进行消抖处理。
消抖也分为两种消抖:
硬件消抖:断开电路,等抖动片段过了再接上去。
软件消抖:通过程序等待一段时间避开抖动片段。
消抖代码(独立出来以后可以直接复制粘贴啦!)
if(P3_1==0)
{
Delay(20);//跳过抖动
while(P3_1==0);//判断是否还在按,直到不按了再走
Delay(20);//跳过松手的消抖片段
P2_0=~P2_0;//执行功能
}
代码:
#include <REGX52.H>
#include <INTRINS.H>
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms){
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{
while(1)
{
if(P3_1==0)
{
Delay(20);
while(P3_1==0);
Delay(20);
P2_0=~P2_0;
}
}
}
消抖版本展示:
独立按键控制LED状态(消抖版本)
未消抖版本(按键状态出现问题,需要按久一点才能准):
独立按键控制LED状态(未消抖)
独立按键控制LED二进制显示
#include <REGX52.H>
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms)
{
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{
unsigned char LEDNum = 0;
while(1)
{
if(P3_1==0)
{
Delay(20);
while(P3_1==0);
Delay(20);
LEDNum++;
P2=~LEDNum;
}
}
}
展示:
独立按键控制LED二进制显示
独立按键控制LED移位
#include <REGX52.H>
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms)
{
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{
unsigned char LEDNum = 0;
P2=~0X01;
while(1)
{
if(P3_1==0)
{
Delay(20);
while(P3_1==0);
Delay(20);
if(LEDNum>=8)
LEDNum = 0;
LEDNum++;
P2=~(0x01<<LEDNum);
}
if(P3_0==0)
{
Delay(20);
while(P3_0==0);
Delay(20);
if(LEDNum==0)
LEDNum = 7;
else
LEDNum--;
P2=~(0x01<<LEDNum);
}
}
}
展示:
独立按键控制LED移位
数码管显示
多位数码管由两个或更多并列的单个数码管组成,具有独立的公共端(位选线)和相连的段线(段选线)。公共端控制哪个数码管亮起,段线控制显示的数字。不同位数的数码管引脚数不同,例如单位数码管10个,四位数码管12个。引脚配置可通过测量或查询手册得知。开发板上常用的是共阴型的四位数码管,能同时显示8个数字。
如果要想显示数字6,则应该点亮ACDEFG,数据为1011 1111。
静态数码管显示
第一步:通过P2_4/3/2(从下往上数)来确定要显示的数码管,查表找到对应的序号,比如第三个是LED6(在板子上是从右往左数)是Y5,二进制为101。
然后查表找到要亮的晶体管,对应序号赋值为1,不亮的为0(具体原理是根据原理图的二极管方向,高电位用1,低电位用0),想要让第三个晶体管显示6,那么叫要让abcdefg dp,分别为1011 111 0,从下往上读,然后给P0赋值0111 1101(十六进制为0x7D)。
代码如下:
#include <REGX52.H>
void main()
{
P2_2=1;
P2_3=0;
P2_4=1;
P0=0x7D;//0111 1101
}
展示:
反思:可以通过子函数的构建去优化代码结构。
在编程过程种可以把显示数位写成子函数,然后传两个参数(定位+显示)就可以达到目的,灵活又方便。
如下就是将显示位置和数字进行模块化的操作。
首先展示代码:
#include <REGX52.H>
unsigned char NixieTable[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
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(8,1);
while(1){
}
}
展示:
模块化后的操作便捷很多
动态数码管显示
代码:
#include <REGX52.H>
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms){
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
xms--;
}
}
unsigned char NixieTable[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
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()
{
while(1){
Nixie(1,1);
Delay(200);
Nixie(2,2);
Delay(200);
Nixie(3,3);
Delay(200);
}
}
展示:
动态数码管流水灯
将延时注释掉(要留一个设置为0或者全部设置为0),理论上可以达到共同显示123,但实际操作却是有点问题:
动态数码管显示出问题
原因在于:数码管再进行显示的时候,实际上是单线程的,它是一行一行地执行位选、段选,速度又很快,位选的数据和段选的数据可能会混乱,为了避免在进行位选切换期间产生瞬间错误,我们需要消影操作。
常用的方法包括刷新之前关闭所有的段,在改变好位选后再打开段。简单来说,就是把上一个数据清理干净,再迎接下一个客人。
优化的代码如下:
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);//等待1s将显示好再读取下一行
P0 = 0x00;//清空数据
}
我也尝试把延时1ms去掉,发现暗淡了很多,就是因为显示没好就跳下一个代码了。
善用工具
stc-isp种有很多试用的小工具,我将一些学习过程中需要用到的代码记录在这里,因为工具太多太难找了
软件延时计算器
这个可能还要找的,因为需要他根据定时长度去生成代码,或者直接自己改也可以(变化还挺大),以下是500毫秒版本。参数如下:
void Delay500ms(void) //@11.0592MHz
{
unsigned char data i, j, k;
_nop_();
i = 4;
j = 129;
k = 119;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
AL控制组任务
我在这里给出我的代码,希望大家有耐心看到这里的能给我提一些优化的建议:
代码:
#include <REGX52.H>
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms){
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
xms--;
}
}
int DelayReturn(unsigned int xms){
Delay(xms);
return 0;
}
unsigned char NixieTable[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x3E,0x00,0x40};
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){
if(P3_1==0)
{
Delay(20);
while(P3_1==0);
Delay(20);
while(1){
Nixie(2,1);Nixie(1,10);Nixie(7,0);Nixie(8,1);
if(P3_0==0)
break;
}
}
if(P3_0 == 0){
int l = 0;
int o = 0;
Delay(20);
while(P3_0==0);
Delay(20);
for (l = 0;l<10;l++){
for(o = 0;o<=200;o++){
Nixie(2,2);Nixie(1,10);
Nixie(7,0);Nixie(8,l);
Delay(1);
}
}
for(o = 0;o<=200;o++){
Nixie(2,2);Nixie(1,10);
Nixie(7,1);Nixie(8,0);
Delay(1);
}
}
if(P3_2 == 0){
int l = 0;
int o = 0;
Delay(20);
while(P3_2==0);
Delay(20);
while(1){
for(o = 0;o<=100;o++){
Nixie(2,3);Nixie(1,10);
Nixie(5,12);Nixie(6,12);
Nixie(7,0);Nixie(8,3);
Delay(1);
}
for(o = 0;o<=100;o++){
Nixie(2,3);Nixie(1,10);
Nixie(5,11);Nixie(6,11);
Nixie(7,0);Nixie(8,3);
Delay(1);
}
if(P3_3 == 0){
break;
}
}
}
if(P3_3 == 0){
int l = 0;
int o = 0;
Delay(20);
while(P3_3==0);
Delay(20);
while(1){
Nixie(2,4);Nixie(1,10);Nixie(7,o);Nixie(8,l);
if(P3_3 == 0){
Delay(20);
while(P3_3==0);
Delay(20);
l++;
}
if(l >= 10){
l=0;
o++;
}
}
}
}
}