目录
众所周知,任何一个硬件工程师都是从点亮一个LED开始的,点亮一个LED,相当于软件中的第一个Hello World,都是一个最基础的东西,今天就开始点亮一个LED,探寻单片机的奥秘,
1.配置文件
首先,我们需要使用的开发板是普中的51单片机开发板,其次,我们使用的编写程序的软件是Keil5,使用C语言编程。现在就来开始点亮一个LED吧!
首先我们需要建立一个项目,我们的项目建立之后会有很多的散乱的小文件,所以我们就要建立一个大文件夹,再在里面建立一个子文件夹,把我们的项目放在里面。
首先,打开Keil5
然后把我们的项目建立到刚刚的子文件夹里面就行了。
保存之后,会出现下面的这样的窗口:
我们使用的51单片机型号是STC89C52,这里找不到,但是我们可以找到型号匹配的,就是AT89C52,我们可以使用AT89C52来创建我们的新项目
我们可以像这样点击Atmel里面找到AT89C52,也可以在上面的搜索框里面直接搜索AT89C52,点击OK,会弹出一个页面
点击否就行了。
我们左边的窗口就出现了我们的项目
鼠标右击Source Group1
点击Add New Item
选择C语言,然后命名即可
成功创建之后,我们的左边就会出现这样的东西
这说明我们的文件已经常见成功,可以开始写代码了。
2.点亮一个LED
GPIO(general purpose intput output)是通用输入输出端口的简称,可 以通过软件来控制其输入和输出。
这就是我们芯片的引脚图,一共有40个引脚,这些引脚可以用分为下面几个大类:
- 电源引脚:引脚图中的 VCC、 GND 都属于电源引脚。
- 晶振引脚:引脚图中的 XTAL1、XTAL2 都属于晶振引脚。
- 复位引脚:引脚图中的 RST/VPD 属于复位引脚,不做其他功能使用。
- 下载引脚:51 单片机的串口功能引脚(TXD、RXD)可以作为下载引脚 使用。
- GPIO 引脚:引脚图中带有 Px.x 等字样的均属于 GPIO 引脚。
我们需要使用的就是P2端口
在开发板上,我们的LED就是这样排列的,P20到P27我们可以统称为P2端口,我们只需要给P2端口赋值就可以得到我们想要的LED效果。
我们都知道,LED是一个二级管一样的东西,就上面的图片而言,我们的迪纳留只能从VCC流向P2端口,所以我们如果想要LED发光,我们就需要保证LED左右有电位差,左边是正极,右边是我们控制的P2端口,我们就需要在软件中给它们一个数字0表示低电平,这样我们的LED才会点亮。
现在我们只想要D1点亮,我们就可以把其它的几个P2端口全部设置为高电平,P20端口设置为低电平(也可以只设置P20端口,因为引脚默认都是上拉的,也就是所有端口默认都是高电平)。
这里我们可以有两种方式点亮这个D1,一个是单个操作,一个是整体操作:
2.1单个端口操作点亮单个LED
在所有操作之前,我们要包含一个头文件,我们可以右键单击点击直接插入头文件,也可以自己写一个出来,总之就要包含整个头文件,因为这个头文件里面有我们所有的关于引脚的定义。头文件里的名字也可以写成reg52.h,大小写不会造成影响
然后我们需要使用一个关键词sbit,就可以定义单个引脚,sbit LED1 = P2^0;这样就完成了对一个引脚的重定义,我们就可以直接对LED1赋值间接对P20端口赋值。当然,只要你喜欢,这里的LED1只是一个名字,你可以换成任何非关键字的名字,然后使用。
这里就得到代码:
cpp
#include <REGX52.H>
sbit LED1=P2^0;
void main()
{
LED1 = 0;
}
我们想要把这个代码烧入到我们的单片机上,我们就要安装驱动,连接数据线,下载软件,这里就不演示了,网上有很多的资料和视频,买开发板的时候商家也会赠送这些东西。
这里使用的是普中官方的软件使用它就可以把我们电脑上的编译链接生成的文件烧入到开发板上,在此之前,我们还要先生成这个文件。
左上角有三个按钮,第一个是只编译不生成文件,第二个是只编译最新更改的文件,第三个是编译所有文件,一般来说我们使用第二个用到比较多,因为当我们的代码数量众多的时候,每次全部编译效率就会很慢,所以最好使用中间的。
我们点击一次之后下面就会出现这样的提示,**0 Error(s), 0 Warning(s)**就是让人兴奋的事情,这说明我们的程序没有问题,可以使用了,然后我们需要使用烧入的软件找到生成的hex文件,但是程序默认是不会生成hex文件的,我们就要再配置一下
点击魔术棒,然后再点击Output,再点击生成hex文件,然后我们再点击一次编译
我们发现多了这样的一条提示,这样就说明我们的我呢见成功生成了,我们就可以使用烧入软件开始烧入了
点击"打开文件"
找到hex文件之后开始配置其他的东西,比如选择插入了USB线的串口(需要有CH340驱动的标注),还有芯片类型和波特率
然后就是把板子上电,并点击"程序下载"
出现了这样的程序下载成功就说明我们成功把东西下载到了板子上,你就成功点亮了一个LED!
2.2整体操作点亮LED
整体操作就是直接对P2端口操作,这样我们只要对P2端口就可以点亮一个LED,或者是多个LED,就不需要我们反复使用sbit定义端口了。
具体操作就是直接对P2赋值,假设我们想要让第一个灯亮,其他灯不亮,我们就要确保其他的都是高电平,而第一个灯是低电平,表示出来就是 :0111 1111 但是这里有一个问题就是,我们的软件不会把它当作二进制处理,而是默认当作十进制处理,这样就和我们原本的意愿相悖了。一般我们使用的话,就会把它转化成十六进制:0111当作一位,1111当作一位,处理成为十六进制
不过这里还有个问题:那就是我们的数据是按照类似数据结构中的栈处理的,也就是它按照0111 1111的顺序进入的话,它拿出来的时候是1111 1110,然后会出现一个问题,就是我们的开发板上亮的就不是我们想要的LED1,而是LED8,所以我们想要LED1亮的话,我们就要使用反向推理,也就是我们需要提前把这个我们想要的数据反抓一下,我们单片机得到的就是正确的数据了,比如我们就可以使用1111 1110输入,转换成十六进制就是0xfe(这里的大小写没有影响),这样我们就得到了LED1亮的情况了。
所以我们就可以写出代码:
cpp
#include <REGX52.H>
void main()
{
P2 = 0XFE;
}
然后上电烧入程序就行了
3.LED闪烁
我们想要一个LED闪烁,也就是可以简化成把一个LED在点亮和熄灭之间反复切换,但是单片机操作的速度是很快的,肉眼无法观察到它的闪烁,所以我们就要使用一个暂停函数来让它们之间有一定的时间暂停不动,之后再进行下一个操作,这样我们就可以观察到它的闪烁了。
我们要实现这样的暂停函数,可以借助另外一个工具
这个也是一个软件,可以用来烧入,而且功能丰富,但是我更喜欢用另外一个,因为操作简单点。
这个软件也是板子配套的软件,我们可以使用它生成暂停函数
我们可以在上面找到"软件延时计时器"
选择好相应的系统频率和定时长度,然后点击生成C代码,再点击复制代码,然后我们在Keil里粘贴这个代码。
到这里还不能直接使用,我们还要包含一个头文件#include <INTRINS.H>,这样函数里的_nop_函数才能够使用。
之后,我们就可以把代码放在主函数中使用了。像这样:
cpp
void main()
{
P2 = 0XFE;//点亮LED1
Delay500ms();
P2 = 0XFF;//熄灭LED1
Delay500ms();
}
然后就可以看到板子上LED1在不停闪烁了。
但是这个代码还不完善,在单片机内部,其实是在不断调用main函数的,这并不太规范,我们可以加上一个while(1) 的死循环使它进入了main函数就不退出,并且能一直执行我们的闪烁指令。从某种意义上,提高了一点效率。
完整代码就是这样:
cpp
#include <REGX52.H>
#include <INTRINS.H>
void Delay500ms() //@11.0592MHz
{
unsigned char i, j, k;
_nop_();
_nop_();
i = 22;
j = 3;
k = 227;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main()
{
while(1)
{
P2 = 0XFE;
Delay500ms();
P2 = 0XFF;
Delay500ms();
}
}
但是我们突然想要把闪烁频率加快怎么办呢?再重新生成代码吗?
我们可以生成一个1ms的函数,然后再通过形参调控我们想要的时间。
cpp
#include <REGX52.H>
#include <INTRINS.H>
void Delay1ms(unsigned int xms) //@11.0592MHz
{
while(xms--)
{
unsigned char i, j;
_nop_();
_nop_();
_nop_();
i = 11;
j = 190;
do
{
while (--j);
} while (--i);
}
}
void main()
{
while(1)
{
P2 = 0XFE;
Delay1ms(500);
P2 = 0XFF;
Delay1ms(500);
P2 = 0XFE;
Delay1ms(100);
P2 = 0XFF;
Delay1ms(100);
}
}
这样我们就可以十分灵活使用暂停函数控制闪烁时间和频率了。
4.LED实现流水灯
LED实现流水灯其实就是上面实现闪烁的改版,我们只要计算出每个灯亮的时候它的对应的指令,然后闪烁,延时,再下一个,反复循环就得到了流水灯。
但是这样的写法很是费劲,难道我们每次都要这样生成吗?那样多麻烦!
这个写法很好理解,但是肯定不是最好的写法。
4.1使用for循环和移位实现
4.1.1移位操作符
我们C语言中有一个操作符:<<(左移位)和 >>(右移位),移位左边是一个数,右边是需要移动的位数。
而移位又分为算数移位和逻辑移位,移位是对一个二进制数的操作,这里讲一下基础的知识。
首先:二进制中比如1000 1111 这个数如果是有符号整型的话,它的第一位1就是它的符号位,1表示负,0表示正。
这里的逻辑位移就不考虑符号位,只是单纯的进行移位操作。比如1010 0101进行逻辑左移一位之后,把首位1移除后在最后面补0,得到0100 1010,再左移2位,得到0010 1000,我们发现,它并不会管我们的移除的数字是什么,它只知道移除之后补上0就行了,右移也是同样的道理。
算数位移很相似,但是有一点不一样,算数位移的首位(符号位)不变,其他的遵循逻辑位移的规律,比如1100 1111右移1位,得到的就是1010 0111,符号位不变,其他位就按照逻辑位移的操作。
在C51使用的时候,默认的是逻辑位移,就是没有符号位的位移,而且我们还要把它们转化成十六进制,再使用移位操作符。
4.1.2使用移位操作和for循环实现
这里有一点不好实现的就是,我们使用P2端口定义的时候,比如我们想让它从LED1流水到LED8,这样的话我们初始的就是1111 1110,左移位一次会变成1111 1100,这样就不是只有LED2亮了,所以我们就要换个思路:假如我们的数只有一个1,其他位都是0是不是会更好处理?
我们就可以把1111 1110取反,得到0000 0001,这样我们控制的时候就可以很精确的控制位了,取反操作符是~,加在我们需要取反的数的前面,比如~(0000 0001)得到的就是1111 1110这样,我们就可以使用取反和移位操作符实现流水灯了。
我们把初始设定成P2 =~(0x01<<0)(即P2=~0000 0001)),下一个就是P2 = ~(0x01<<1)(即P2 = ~0000 0010),直到LED1到LED8都进行了一次闪烁,回归原位,就完成了流水灯循环。这里我们使用for循环实现。
cpp
#include <REGX52.H>
#include <INTRINS.H>
void Delay1ms(unsigned int xms) //@11.0592MHz
{
while(xms--)
{
unsigned char i, j;
_nop_();
_nop_();
_nop_();
i = 11;
j = 190;
do
{
while (--j);
} while (--i);
}
}
void main()
{
unsigned int i = 0;
while(1)
{
for(i = 0;i<8;i++)
{
P2 =~(0x01<<i);
Delay1ms(300);
}
}
}
这样我们就完成了对流水灯的实现。
4.2使用移位函数实现LED流水灯
除了使用 for 循环语句实现移位,KEIL C51 软件内还有对应的移位库函数, 左移函数是_crol_(),右移函数是_cror_(),而且这里要注意它们需要包含头文件#include <INTRINS.H>才可以使用,这两个函数和左移右移操作符的区别就是它们移位之后不会让多余的位补上0,而是顺延移出位的数,比如1001 1100左移一位,得到的是0011 1001,相当于我们把整体的数字往前推了一位,把推出的数字放回到末尾,这样的函数使得我们对流水灯的理解更加简便,我们不再需要使用取反符号,只要使用移位操作就好了。
我们原本的数字是1111 1110,只要使用左移函数_crol_()就可以实现流水灯效果,即使用P2接收P2左移一位的返回值,直接改变P2,循环反复即可
cpp
#include <REGX52.H>
#include <INTRINS.H>
void Delay1ms(unsigned int xms) //@11.0592MHz
{
while(xms--)
{
unsigned char i, j;
_nop_();
_nop_();
_nop_();
i = 11;
j = 190;
do
{
while (--j);
} while (--i);
}
}
void main()
{
P2 = 0xfe;
Delay1ms(100);
while(1)
{
P2 = _crol_(P2,1);
Delay1ms(100);
}
}
这样就实现了流水灯,还是比较简单的。