🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
【85.1 寄存器配置的本质。】 单片机内部可供我们选择的资源非常丰富,有定时器,有串口,有外部中断,等等。这些丰富的资源,就像你进入一家超市,你只需选择你所需要的东西就可以了,所以配置寄存器的关键在于选择,所谓选择就是往寄存器里面做填空题,单片机系统内部再根据你的“选择清单”,去启动对应的资源。那么我们怎么知道某个型号的单片机内部有哪些资源呢?看该型号“单片机的说明书”呀,“单片机的说明书”就是我们通常所说的“芯片的datasheet”,或者说是“芯片的数据手册”,这些资料单片机厂家会提供的。 跟单片机打交道,其实跟人打交道没什么区别,你要让单片机按照你的“意愿”走,你首先要把你的“意愿”表达清楚,这个“意愿”就是信息,信息要具备准确性和唯一性,不能模凌两可。比如,现在要让单片机“每1ms产生一次中断”,你想想你可能需要给单片机提供哪些信息? (1)51单片机有2个定时器,一个是0号定时器,一个是1号定时器,我们要面临“二选一”的选择,本例子中用的是“0号定时器”。 (2)0号定时器内部又有4种工作方式:方式0,方式1,方式2,方式3,本例子中用的是“方式1”。 (3)定时器到底多长时间中断一次,这就涉及到填充与中断时间有关的寄存器的数值,该数值是跟时间成比例关系,本例子中配置的是1ms中断,就要填充对应的数值。 (4)默认状态下,定时器是不会被开启的,如果要开启,这里就是涉及到定时器的“开关”,本例子要开启此开关。 (5)定时器时间到了就要产生中断,中断也有“总开关”和“定时器的局部开关”,这两个开关都必须同时打开,中断才会有效。 要配置定时器“每1ms产生一次中断”,大概就上述这些信息,根据这些信息提示,下面开始讲解一下寄存器的具体内容。 【85.2 定时器/计数器的模式控制寄存器TMOD。】 寄存器TMOD是一个8位的特殊变量,里面每一位都代表了不同的功能选择。根据芯片的说明书,TMOD的8位从左到右依次对应从D7到D0(左高位,右低位),定义如下: GATE C/T M1 M0 GATE C/T M1 M0 仔细观察,发现左4位与右4位是对称的,分别都是“GATE,C/T , M1 , M0”,左4位控制的是“定时器1”,右4位控制的是“定时器0”,因为本例子用的是“定时器0”,因此“定时器1”的左4位都设置为0的默认数值,我们只需重点关注右4位的“定时器0”即可。 GATE:定时器是否受“其它外部开关”的影响的标志位。定时器的开启或者停止,受到两个开关的影响,第一个开关是“自身原配开关”,第二个开关是“其它外部开关”。GATE取1代表定时器受“其它外部开关”的影响,取0代表定时器不受“其它外部开关”的影响。本例子中,定时器只受到“自身原配开关”的影响,而不受到“其它外部开关”的影响,因此,GATE取0。 C/T:定时器有两种模式,该位取1代表“计数器模式”,取0代表“定时器模式”。本例子是“定时器模式”,因此,C/T取0。 M1与M0:工作方式的选择。M1与M0这两位的01搭配,可以有4种组合(00,01,10,11),每一种组合就代表一种工作方式。本例子选用“方式1”,因此M1与M0取“01”的组合。 综上所述,TMOD的配置代码是:TMOD=0x01; 【85.3 决定时间长度的寄存器TH0与TL0。】 TH0与TL0,T代表定时器英文单词TIME的T,H代表高位,L代表低位,0代表定时器0。 TH0是一个8位宽度的寄存器,TL0也是一个8位宽度的寄存器,两者合并起来成为一个整体,实际上就是一个16位宽度的寄存器,TH0是高8位,TL0是低8位,它们合并后的数值范围是:0到65535。该16位寄存器取值越大,定时中断一次的时间反倒越小,为什么?TH0与TL0的初始值,就像一个水桶里装的水。如果这个桶是空桶(取值为0),“雨水”想把这个桶“滴满溢出”所需要的时间就很大。如果里面已经装了大半的水(取值为大于32767),“雨水”想把这个桶“滴满溢出”所需要的时间就很小。这里的关键词“滴满溢出”的“滴”与“满溢出”,“滴”的速度是由单片机晶振决定的,而“满溢出”一次就代表产生一次中断,执行完中断函数在即将返回主函数之前,我们重新装入特定容量的水(重装初值),为下一次的“滴满溢出”做准备,依次循环,从而连续不断地产生间歇的定时中断。 配置中断时间的大小是需要经验的,因为,每次定时中断的时间太长,就意味着时间的可分度太粗,而如果每次定时中断的时间太短,则会产生很频繁的中断,势必会影响主函数main()的执行效率,而且累记中断次数的时间误差也会增大。因此,配置中断时间是需要经验的,根据经验,定时中断取1ms一次,是几乎所有单片机项目的最佳选择,按我的理解,“1ms定时中断一次”已经是单片机界公认的一种“标配”。 要配置1ms定时中断,TH0与TL0如何取值?刚才提到一个形象的例子“桶,滴,满溢出”。TH0与TL0的最大取值范围是65535,可以理解成为最大65535“滴”,如果超过65535“滴”(比如加1“滴”后变成65536“滴”)就会“满溢出”,从而产生一次中断(65536是中断发生的临界值)。而“滴一次的时间”就刚好是单片机执行“一次单指令的时间”,“一次单指令的时间”等于12个晶振周期,比如12MHz的晶振,晶振周期是(1/12000000)秒,而“一次单指令的时间”就等于12乘以(1/12000000)秒,等于0.000001秒,也就是1us。1us“滴”一次,要产生1ms的时间就需要“滴”1000次。“满溢出”的前提条件是“桶里”一共需要装入65536滴才溢出,因此,在12MHz的晶振下要产生1ms的定时中断,TH0与TL0的初值应该是64536(65536减去1000等于64536),而64536变成十六进制0xfc17,再分解到高8位TH0为0xfc,低8位TL0为0x17。 刚才的例子是假如晶振在12MHz的情况下所计算出来的结果,而本教程所用的晶振是11.0592MHz,根据11.0592MHz产生1ms的定时中断,TH0与TL0应该取值多少?根据刚才的计算方式: 初值=\[溢出值\]-(\[0.001秒\]/(\[晶振周期的12个\]\*(\[1秒\]/\[晶振频率\]))) 初值=65536-(0.001/(12\*(1/11059200))) 初值=65536-922 (注:922是921.6的四舍五入) 初值=64614 初值=0xfc66 初值TH0=0xfc 初值TL0=0x66 【85.4 中断的总开关EA与局部开关ET0。】 EA:中断的总开关。宽度是1位的位变量。此开关如果取0,就会强行屏蔽所有的中断,因此,只要用到中断,此开关必须取1。 ET0:专门针对定时器0中断的局部开关。宽度是1位的位变量。此开关如果取0,则会屏蔽定时器0的中断,如果取1则允许定时器0中断。如果要定时器0能产生中断,那么总开关EA与ET0必须同时都打开(都取1),两者缺一不可。 【85.5 定时器0的“自身原配开关”TR0。】 TR0:定时器的“自身原配开关”。宽度是1位的位变量。很多初学者会把EA,ET0,TR0三者搞不清。定时器可以工作在“查询标志位”和“中断”这两种状态,也就是说在没有中断的情况下定时器也可以单独使用的。TR0是定时器0自身的发动引擎,要不要把这个发动引擎所产生的能量传输到中断的渠道,则取决于中断开关EA和ET0。TR0是源头开关,EA是中断总渠道开关,ET0是中断分支渠道的定时器0开关。TR0取1表示启动定时器0,取0表示关闭定时器0。 【85.6 定时器0的中断函数的书写格式。】 void 函数名() interrupt 1 { ...中断程序内容; ...此处省去若干代码 ...中断程序内容; ...最后面的代码,要记得重装TH0与TL0的初值; } 函数名可以随便取,只要不是编译器已经征用的关键字。这里的1是定时器0的中断号。不同的中断号代表不同类型的中断,至于哪类中断对应哪个中断号,大家可以查找相关书籍和资料。本节用的定时器0处于工作方式1的情况下,在即将退出中断之前,需要重装TH0与TL0的初始值。 【85.7 寄存器的名字来源。】 前面讲的寄存器都有固定的名字,而且这些名字都是唯一的,拼写的时候少一个字母或者多一个字母,C编译器都会报错不让你通过,因此问题来了,初学者刚接触一款单片机的时候,如何知道某个寄存器它特定的唯一的名字?有两个来源。 第一个来源,可以打开C编译器的某个头文件(.h格式)查看这些寄存器的名字。比如51单片机可以查看REG52.H这个头文件。如何打开REG52.H这个文件?在Keil源代码编辑器界面下,选中上面REG52.H这几个字符,在右键弹出的菜单下点击Open ducument“REG52.H”即可。 第二个来源是直接参考一些现成的范例程序,这些范例程序网上很多,有的是原厂提供的,有的是热心网友的分享,有的是技术书籍或者学习板开发板厂家提供的。 【85.8 如何快速配置寄存器。】 建议一边阅读芯片的数据手册,一边参考一些现成的范例程序,这些范例程序网上很多,有的是原厂提供的,有的是热心网友的分享,有的是技术书籍或者学习板开发板厂家提供的。 【85.9 练习例程。】 现在编写一个定时中断程序,让两个LED灯闪烁,一个是在主函数里用累计主循环次数的方式实现(P0.0控制),另一个是在定时中断函数里用累计定时中断次数的方式实现(P0.1控制)。这两个闪烁的LED灯,一个在main函数,一个是在中断函数,两路任务互不干涉独立运行,并行处理的“雏形”略显出来。 ![](https://img.kancloud.cn/0f/49/0f49cc4e7b34f0e8c13dd7e514906c88_214x279.png) 图85.9.1 灌入式驱动8个LED \#include "REG52.H" \#define CYCLE\_SUM 5000 //主循环的次数 \#define INTERRUPT\_SUM 500 //中断的次数 sbit P0\_0=P0^0; //在主循环里的LED灯 sbit P0\_1=P0^1; //在定时中断里的LED灯 unsigned char Gu8CycleStep=0; unsigned long Gu32CycleCnt=0; //累计主循环的计数器 unsigned char Gu8InterruptStep=0; unsigned long Gu32InterruptCnt=0; //累计定时中断次数的计数器 void main() { TMOD=0x01; //设置定时器0为工作方式1 TH0=0xfc; //产生1ms中断的TH0初始值 TL0=0x66; //产生1ms中断的TL0初始值 EA=1; //开总中断 ET0=1; //允许定时0的中断 TR0=1; //启动定时0的中断 while(1) //主循环 { switch(Gu8CycleStep) { case 0: Gu32CycleCnt++; if(Gu32CycleCnt>=CYCLE\_SUM) { Gu32CycleCnt=0; P0\_0=0; //主循环的LED灯亮。 Gu8CycleStep=1; } break; case 1: Gu32CycleCnt++; if(Gu32CycleCnt>=CYCLE\_SUM) { Gu32CycleCnt=0; P0\_0=1; //主循环的LED灯灭。 Gu8CycleStep=0; } break; } } } void T0\_time() interrupt 1 //定时器0的中断函数,每1ms单片机自动执行一次此函数 { switch(Gu8InterruptStep) { case 0: Gu32InterruptCnt++; //累计中断次数的次数 if(Gu32InterruptCnt>=INTERRUPT\_SUM) //次数达到设定值就跳到下一步骤 { Gu32InterruptCnt=0; //及时清零计数器,为下一步骤的新一轮计数准备 P0\_1=0; //定时中断的LED灯亮。 Gu8InterruptStep=1; //跳到下一步骤 } break; case 1: Gu32InterruptCnt++; //累计中断次数的次数 if(Gu32InterruptCnt>=INTERRUPT\_SUM) //次数达到设定值就返回上一步骤 { Gu32InterruptCnt=0; //及时清零计数器,为返回上一步骤的新一轮计数准备 P0\_1=1; //定时中断的LED灯灭。 Gu8InterruptStep=0; //返回到上一个步骤 } break; } TH0=0xfc; //重装初值,不能忘。 TL0=0x66; //重装初值,不能忘。 }