(指南者)(一)51单片机学习系列文章

这个系列里面的代码是基于 STC12C5A60S2型号的芯片写的,这个系列的文章的目的是带领小白们入门51单片机。

关于51单片机

在学51单片机之前,我们总要先知道什么是51单片机?为什么要学它?学了能做什么?

什么是51单片机

其实我们可以将51单片机看作是一台特别特别古老的电脑现在的电脑动轧16G内存,1T外部储存,单片机就只有256Byte RAM(内存)和1024KB FLASH(外部储存)。

为什么要学51单片机

为什么大多数学电子的人都要选择 51 单片机作为入门的呢?我们可以使用 ESP32 或者 ESP8266 来入门,存在大量的库,只需要简单的调用函数就可以实现一些相对复杂的功能,也可以使用 STM32 单片机来入门,相比较 51 单片机它具有更强的功能,初学也更加了解底层,找工作也比学 51 单片机更容易找,但是我还是推荐小白以 51 单片机作为入门是最好的选择。
51 单片机对于电子小白来说我认为最好的选择,对 51 单片机的操作不会难度太大,各种概念也不会像 STM32 单片机那样难以理解,却又不像 ESP32 那样简单的调用函数,在可以提高自己对单片机理解的同时又不会因为过高的难度而放弃学习。

学了能做什么

51 单片机它本身其实并不具有太强的功能,不过用它做个小车,自动门锁,心形灯等还是能够满足的。

如何使用51单片机

在这里插入图片描述

图中,我们可以看到一张 STC12C5A60S2 这个型号的 51 单片机的引脚图,可以看到每个引脚有个数字,称之为引脚序号,这个和实际芯片上的引脚顺序是一样的,除此之外,我们还可以看到每个引脚都有其对应的名称,所以再实际中,引脚的序号和功能是严格对应的,这个是在芯片生产就规定了的。

VCC和GND:电源引脚(给芯片提供电)
RST    :复位引脚(将芯片内部程序重新运行,理解为重启)
XTAL1/2:晶振接入引脚
P0/P1/P2/P3:普通的用户IO口,我们可以直接控制,后面会讲到控制方法

我们可以通过编写程序的方式来控制单片机,但是从我们编写程序到单片机运行是一个怎样的过程呢?

首先我们要有一个认知,就是我们是在使用 C 语言这种人能看懂的编程语言来实现变成。
但单片机是看不懂编程语言的,它只看得懂由 0 和 1 组成的机械语言,所以这个时候往往需要一个编译器来实现将我们写的编程语言转变成机械语言,我们可以使用 keil 等编译器。
编译器将我们写的代码打包成 hex 后缀的文件,在通过串口(之后的文章会讲都一种通信方式)将 hex 文件发送给单片机。
单片机会把程序储存在内部的储存空间中,在上电的时候就会运行程序,内部完成运算并且操作 IO 口实现具体的操作(例如点灯或者通信)。

控制IO输出

#include "STC12C5A60S2"    //这个我们平常包含的头文件不一样,平常我们在写C语言的程序的时候,总是需要包含一个头文件,例如:#include<stdio.h>,这是因为我们常常用到的printf这种函数在这个头文件里面。
                        //写51单片机的时候也一样,不过不同的是,我们并不常常使用printf这种函数,我们需要对51单片机内部的寄存器进行操作(什么是寄存器,寄存器的概率在这个系列文章下一篇会说到),这个寄存器的定义就是在现在所看到的STC12C5A60S2.H这个头文件里面,不同的芯片所使用的头文件也不相同。
//我们怎么去控制IO口输出呢?其实很简单,直接对IO口的赋值就行。
P01 = 1;        //这就让P01这个口输出高电平。
//其实除了上面的写法,有些还有其他的赋值方法。
P0^1 = 1;
P0_1 = 1;
//这三种方法一般情况下是通用的,可能由于不同的头文件的限制,有个别在某些头文件下不能使用,可以三种写法都尝试一下,以便分清我们目前使用的单片机的头文件支持哪种写法。

//上面是单独对P0^1这个IO口进行操作,我们还可以看到,其实所有引脚分了不同的类,例如P0^0到P0^7是属于P0的,那我们可不可以直接一句话操作P0的所有IO口呢?
P0 = 0x12;    //可以看到,我们直接对P0赋值了,这样就可以直接改变P0的8个IO口,0x代表16进制的含义,将P0的8个IO口赋成00010010,其中最低位代表了P00,最高位代表了P07,这样就很清晰每个IO口的状态了。

P0M0 = 0xff;    //P0M0是一个寄存器,暂时不用管太多,只需要记住作用就行,这句话的含义是将P0口输出模式变成推挽输出(推挽输出的概念在之后的文章中会讲解),它的作用就是增大P0口的电流。

读取IO口状态

//我们已经可以控制每个IO口的状态了,但是当我们的IO口并不想作为输出,而是由外界传输高电平或者低电平给单片机IO口,单片机IO口负责读取这个电平应该怎么做呢?
//做法其实很简单,我们可以直接读取
if (P1^0 == 1)    {    //可以通过if条件语句来判断此时IO口的状态
    ;//语句
}
if (P0 == 0x01) {}    //一次判断P0的8个IO也是可以的
//除了用if判断,我们还可以直接读取
sbit        state;    //sbit在C语言中没有,是51单片机特有的变量类型,代表了定义一个一位宽的变量来存储一个IO口的值
state = P0^1;    //将P0^1口的状态直接赋值给state变量,就读取到了IO口的状态
//也可以读取8个IO的状态
char    state;    //这时我们就设置一个8位宽的变量来存储IO口的值
state = P0;    //直接将P0上8个IO口的值存放到变量state上

代码例程

对于单片机的控制已经了解了,我们可以做一些简单的玩意儿了。

在这里插入图片描述

这是之后讲解代码的原理图。

流水灯

首先,我们使用的是 led 灯,也叫发光二极管,它具有单向导通的特性,图中我将 led 的负极接在了 GND 上,所以,当我的 P0 的 IO 口输出高电平的时候,相应的 led 灯就会被点亮。
怎么让 led 出现流水的效果呢?我们让 P00 先变成高电平,其他变成低电平,就可以让第一个灯亮,然后将 P01 变为高电平,其他变成低电平,循环下来,就是一个流水灯了。
#include "STC12C5A60S2"    //首先包含一个51单片机的头文件
#include <intrins.h>    //这个头文件里面包含了后面使用到的_nop_()函数,具体作用后面会说
unsigned int k = 0;    //定义一个全局变量
void Delay_ms(unsigned int time) {    //定义一个延时函数,用于后面的延时,具体作用后面会说
    unsigned char i, j;    //定义一个局部变量,此变量只在当前函数有用
    while (time--) {    //有外部传入参数决定循环次数
        //在51单片机的引脚有两个接入晶振的引脚,晶振在电路中充当了心脏在人体中的作用,他会自动给出一定频率的震动,单片机靠着这个频率完成内部复杂的功能
        _nop_();    //等待一个时钟周期,就是晶振震动一个周期的时间,单片机不做任何事
        _nop_();
        _nop_();
        i = 11;    
        j = 190;
        do {    //两层循环,在这儿只是单纯的空循环,不做任何事情,因为单片机运行一条程序需要时间,这儿就是使时间白白浪费掉,起到延时的作用
            while (j--);
        } while (i--);
    }
}
void main(void) {
    P0M0 = 0xff;    //将P0设置成推挽输出
    while (1) {    //死循环,让程序一直运行
        for (k = 0; k < 8; k++) {
            P0 = 0x01 << k;    //利用for循环,k的值每次就只能在0到7中顺序改变,利用0x01左移k位,就可以实现流水灯了
            Delay_ms(500);    //每次点亮一个灯后都延时500ms,如果不加延时,流水灯的速度就会非常的快,人的眼睛反应不过来,最后你就会看到所有灯都亮的情况,所以为了适应人眼,就主动的延时一段时间,让人眼可以清晰的看清流水灯
        }
    }
}

按键控制

图中可以看出,我们将 IO 口连到了按键上,并将按键的另一端接到了 GND ,在 51 单片机中, IO 口默认是输出高电平,当按键按下, IO 连通到 GND 此时 IO 口的状态变成低电平,我们就可以通过判断 IO 口是否是低电平来进行判断按键是否按下。
我们理想的情况下,按键一按下就立马变成低电平,按键松开就立马恢复成高电平:
在这里插入图片描述

而实际情况下,按键是一个机械结构,按下的瞬间和松开的瞬间会有数次的抖动:

在这里插入图片描述

由于抖动的存在,单片机的 IO 口又只能读取高电平和低电平两种状态,所以最终的波形是:

在这里插入图片描述

实际生活中,按键大概率会由于磕磕碰碰产生一些抖动,可能在你没有按下的情况下,按键也会发生抖动而出现低电平的情况,所以我们不能检测到低电平就判断按键已经按下,一般情况我们需要判断到低电平后再等待一段时间,再次判断按键是否为低电平,如果此时按键为低电平,我们才确定按键按下。
当然,有些情况我们希望长按按键可以连续操作,例如长按音量 + 让音量一直增加,有些情况下我们只希望按键按一次只触发一次。
这儿我们就要用到松键检测,如果连续操作就不使用松键检测,如果按键按一下只生效一次,我们就要使用松键检测。
#include "STC12C5A60S2.H"
#include <intrins.h>
sbit key = P2^1;    //将按键连的引脚定义成key这个名字,方便后面查看
void Delay_ms(unsigned int time) {
    unsigned char i, j;
    while (time--) {
        _nop_();
        _nop_();
        _nop_();
        i = 11;
        j = 190;
        do {
            while (j--);
        } while (i--);
    }
}
void main(void) {
    P0 = 0x00;    //将P0所有IO口输出低电平
    while (1) {
        if (key == 0) {    //判断按键是否有按下的趋势
            Delay_ms(10);    //延时10ms等待抖动消失,按键消抖
            if (key == 0) P00 = !P00;    //再次判断按键是否真的按下,如果按下,就让P00口连接的led亮灭翻转
            while (!key);    //松手检测,当按键按下时,key值为0,!key值为1,所以程序卡在这儿
            //当松手后,按键恢复成1,!key为0,跳出循环
        }
    }
}

逻辑运算

我们可以使用 IO 口输出不同的波形。

#include "STC12C5A60S2"
unsigned char time = 0;
void main(void) {
    while (1) {
        P30 = !P30;    //让P30不断翻转,最终P30不断呈现方波的样子
        if (time++ == 9) {
            time = 0;    //time用于计时,让每次进入这儿的时间相等,所以对time赋初值,或者称为清零(赋初值和清零是两个概念,这儿初值正好是0,不要混了)
            P31 = !P31;    //让P31以P30翻转速度的十分之一速度翻转time从0到9是十次
        }
        P32 = !(P30 & P31);    //将P30和P31的电平做逻运算后的结果让P32显示
        P33 = !P30 | !P31;
    }
}