(指南者)(二)寄存器、定时器和中断

寄存器

单片机,具有很多用来储存数据的单元,当我们的代码下载进单片机时,就是将代码转化成二进制的机械码并储存在这种单元中,在这些单元中,有一部分特殊的单元,对里面储存不同的值会导致单片机工作的方式不同,这部分具有特殊功能的单元我们称为寄存器(由多个寄存器组成的一个功能整体我们称之为外设)。(例如 P0 , P0 这个寄存器的作用就是改变 IO 的输出状态。)
链接:这篇文章对寄存器的定义写的更加清晰,可以作为参考

不同的芯片内部具有的寄存器是不完全相同的,所以当我们使用不熟悉的芯片时,需要根据芯片数据手册来编写代码。

接下来我们介绍关于 STC12C5A60S2 这个系列的芯片上常用的寄存器。

定时器

在大多数通信或者实际项目中,对于信息处理的时间是有严格要求的,但是由于不同的芯片在功能等方面的差异,我们不能精准的知道运行一行代码的时间长短,所以大多数芯片都会具有定时器这个外设,来满足我们项目需求中的时间需求。

时钟

已经知道芯片中有定时器了,但是定时器是由什么作为参考来计时呢?
在大多数电路中,芯片附近可以找到晶振存在,晶振的作用就是提供一个固定的时钟信号。

在这里插入图片描述

像上图这样的我们就称之为时钟信号,晶振所发出的时钟信号的频率时固定且精准的,我们就可以通过读取这个时钟信号的周期次数来计时。

定时器寄存器

我们打开芯片手册

在这里插入图片描述

目录中可以直接找到定时器,计数器。

在这里插入图片描述

里面对于定时器的介绍有这一段话,通过记录时钟周期的次数来决定计时脉冲的时间,每一次的时钟周期的时间一定,我们改变一次计时脉冲所需要的时钟周期次数,就可以改变一次计时脉冲的时间,我们使用的芯片的定时器支持一个时钟周期就等同于一个计数脉冲,也支持十二个时钟周期等同于一个计数脉冲,默认情况下,是十二个时钟周期等同于一个计数脉冲。
在这里插入图片描述

接下来我们可以看到定时器外设所含有的寄存器,我们先讲解 TH 和 TL 寄存器(先不管 0 和 1 的区别,后面会讲到)
记录计数脉冲的次数就是使用的这两个寄存器,两个寄存器都是只有 8 位宽,没过一次计数脉冲, TL 的值就会增加 1 ,随着时间增加, TL 加到 255 时再次加 1 就会溢出,此时 TH 的值会加 1 , TL 的值会清零,随后 TH 的值加到 255 后再次加 1 也会溢出,此时 TH 也会清零。所以定时器不能无限记录时间的长短,是有范围的,在使用定时器时我们就要去考虑定时器的计时范围。
在 STC12C5A60S2 中,一共有两个定时器,分别是 T0 和 T1,所以我们可以看到 TH 和 TL 也被分为了 0 和 1 。
在这里插入图片描述

我们再来介绍 TMOD 寄存器的作用,我们可以看到这个寄存器也是八位,被分成了两个四位分别给 T0 和 T1 两个定时器使用。、
每单独的四位里前两位我们不用去深究,有兴趣可以看一下,我们只需要去配置后两位 M0, M1 ,作用时配置定时器的工作方式。
我们可以看到,后两位可以组成四种不同的工作方式。
这儿介绍常用的工作方式。

01

这个工作方式下,TH  TL 两个八位的寄存器全部使用,组成一个十六位的寄存器,计数溢出后计数清零。

10

在这个工作方式下,计数器只用了 TL 的八位,计数 256 次后溢出,但是溢出后的值会把 TH 的值存入 TL TL 就不被清零。

在这里插入图片描述

在这里插入图片描述

我们可以看到,在 TCON 这个寄存器中,每一位都被给予了新的名字,其中 TR 的作用时允许定时器开始计时,我们可以把它当作一个开关的功能,当每次计数溢出后, TF 会被置 1,当产生中断(后面会讲)时,这个位才会被清零。

我们需要配置的寄存器也就这些了,其他的默认就行。

代码例程

//所以,我们要开启定时器计时,就对这些寄存器配置就行
void Init(void) {
    TCOM = 0x01;    //把定时器1配置成01的模式
    TR0 = 1;        //允许定时器1计数
    //定时器计数的初始化就完成了,我们可以通过读取TH和TL的值来判断代码运行时间
}

定时器中断

中断

在这里插入图片描述

当我们正在做一件事情的时候,突然被另一件重要的事情打断,所以我们选择优先完成重要的事情,再回来接着完成之前没做完的事情,这个过程我们就称之为发生了中断,重要的事我们称之为中断事件。

定时器中断

由定时器的计数寄存器溢出,从而产生的中断,我们又称之为定时器中断。
上面我们讲到 TF 寄存器,当定时器溢出,这个寄存器会被置 1 ,此时就会发生中断,并且将此寄存器自动清 0 。

在这里插入图片描述

从 IE 寄存器中可以看到,定时器中断的允许位为 ET ,总中断的允许位为 EA ,我们将两个同时打开就可以打开我们的定时器中断。

代码例程

void Init(void) {
    TCOM = 0x01;
    TR0 = 1;
    ET0 = 1;        //允许定时器中断
    EA = 1;            //打开总中断
}

//打开了中断,那我们怎么跳转到中断事件去呢?可以通过中断函数
void Int0(void) interrupt 1 {    //中断函数是特殊的函数,不需要在main函数之中调用,并且需要通过interrupt来标注这是一个中断函数,后面的数字代表了不同的中断函数,后面会讲。
    //这儿就可以写我们需要做的中断事件。
}

在这里插入图片描述

我们可以看到,默认情况下,定时器 0 所对应的就是 interrupt 1 ,这些我们都可以根据芯片手册看到。
现在我们以及有了计时,有了中断,那我们怎么才能使每次进入中断的时间相等呢?
我们可以通过对 TH 和 TL 赋值的方法来控制进入中断的时间。

代码例程

//当使用模式 1 0 时
//我们只赋值一次就可以,每次溢出后TL的值会变成我们设置好的TH的值
void Init(void) {
    TCOM = 0x02;
    TH0 = 10;    //每次TL溢出后讲TH的值装进去
    TL0 = 10;    //TL一开始就从10开始计数人为的控制进入中断的间隔时间
    TR0 = 1;
    ET0 = 1;
    EA = 1;
}

void Int0(void) interrupt 1 {
}

//当使用模式 0 1 时
//我们只赋值一次就可以,每次溢出后TL的值会变成我们设置好的TH的值
void Init(void) {
    TCOM = 0x01;
    TH0 = 10;    //TH一开始就从10开始计数人为的控制进入中断的间隔时间
    TL0 = 10;    //TL一开始就从10开始计数人为的控制进入中断的间隔时间
    TR0 = 1;
    ET0 = 1;
    EA = 1;
}

void Int0(void) interrupt 1 {
    TH0 = 10;    //由于不会自动的赋值,所以我们需要在每次进入中断函数(也就是刚刚好溢出时)重新赋值
    TL0 = 10;
}

时间计算

我们怎么去控制进入中断的时间呢?
我是用的晶振是 11.0592M 的晶振,所以可以知道 1s 有 11059200 个时钟周期,我们对其除 12 就可以得到一个计数周期在 1s 内的次数,此时我们再除定时器的溢出次数就可以得到 1s 内的中断次数,用 1 除这个次数就可以得到时间。在这儿我们是可以把 TH 和 TL 赋值为负数,正数取反加一这个过程反过来就可以将负数转化成正数,最后发现,当我们赋值成正数时,计数到 256 溢出,当赋值为负数时,计数到 0 溢出。

定时器流水灯实现

#include <STC12C5A60S2.h>

unsigned int nTimer = 0;            //定义一个变量作为后面计时使用

void main(void) {
    unsigned char i = 0;        //定义一个变量作为后面流水使用
    
    TMOD = 0x01;                //定时器模式设置成定时器1 01模式
    TH0 = -9;                    //通过计算我们可以得到 115200/12/256/9 = 400,1/400 = 0.0025s定时器溢出,进入一次中断
    ET0 = 1;                    //打开定时器中断
    TR0 = 1;                    //允许中断计数
    EA = 1;                        //打开总中断
    while (1) {
        if(nTimer >= 200) {        //当定时器溢出200次时流水一次,流水灯以200*0.0025 = 0.5s速度流水
            P0 = 0x01 << i++;    //流水灯流水
            i &= 7;                //当i>=8时,将i清零
            nTimer = 0;            //次数清零
        }
    }
}

void Int0(void) interrupt 1 {    //中断函数
    TH0 = -9;                    //重新赋初值
    nTimer++;                    //每次进入中断函数计数加1
}

定时器按键实现

#include <STC12C5A60S2.h>

unsigned int nTimer = 0;
unsigned char cKey, cKeyCode;
unsigned int nDelayKey, nLED_Time = 200;
bit bLoose, bStop;

void DisposeKey(void);    //按键函数

void main(void) {
    unsigned char i = 0;

    TMOD = 0x01;
    TH0 = -9;
    ET0 = 1;
    TR0 = 1;
    EA = 1;
    while (1) {
        if (nTimer >= nLED_Time) {
            if (bStop == 0) P0 = 0x01 << i++;
            i &= 7;
            nTimer = 0;
        }
        if (cKeyCode != 0) DisposeKey();    //判断按键按下后执行按键函数
    }
}

void Int0(void) interrupt 1 {
    TH0 = -9;
    nTimer++;
    if (nDelayKey == 0) {    //当没有按键按下时,此时恒为0
        cKey = P2 & 0x07    //将按键的值保存在cKey中
        if (cKey != 0x07) nDelayKey = 4;    //当按键按下后,值不等于0x70,将nDelayKey赋4用来消抖
        else bLoose = 0;    //松开按键后清零松手标志
    } else {
        if (--nDelayKey == 0) {    //定时器进入四次,相当于10ms按键消抖
            cKeyCode = P2 & 0x07;    //再次存入按键的值
            if (cKeyCode != cKey) cKeyCode = 0;    //如果前后两次按键值不相等,就证明按键没有按下,将按键值清零
        }
    }
}

void DisposeKey(void) {
    if(bLoose == 0) {    //判断是否松手,没有松手就只会执行一次按键的作用
        if (cKeyCode == 6) {    //按键具体值
            if (nLED_Time <= 400) nLED_Time += 40;    //流水速度加快
        } else if (cKeyCode == 5) {
            if (nLED_Time >= 40) nLED_Time -= 40;    //流水速度减慢
        } else if (cKeyCode == 3) bStop = !bStop;    //流水停止
        bLoose = 1;    //表示此次按键按下按键作用已经执行了一次了
    }
    cKeyCode = 0;    //对按键进行清零
}