【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; //重装初值,不能忘。
}
- 首页
- 第一节:我的价值观
- 第二节:初学者的疑惑
- 第三节:单片机最重要的一个特性
- 第四节:平台软件和编译器软件的简介
- 第五节:用Keil2软件关闭,新建,打开一个工程的操作流程
- 第六节:把.c源代码编译成.hex机器码的操作流程
- 第七节:本节预留
- 第八节:把.hex机器码程序烧录到单片机的操作流程
- 第九节:本节预留
- 第十节:程序从哪里开始,要到哪里去?
- 第十一节:一个在单片机上练习C语言的模板程序
- 第十二节:变量的定义和赋值
- 【TODO】第十三节:赋值语句的覆盖性
- 【TODO】第十四节:二进制与字节单位,以及常用三种变量的取值范围
- 【TODO】第十五节:二进制与十六进制
- 【TODO】第十六节:十进制与十六进制
- 【TODO】第十七节:加法运算的5种常用组合
- 【TODO】第十八节:连加、自加、自加简写、自加1
- 【TODO】第十九节:加法运算的溢出
- 【TODO】第二十节:隐藏中间变量为何物?
- 【TODO】第二十一节:减法运算的5种常用组合。
- 【TODO】第二十二节:连减、自减、自减简写、自减1
- 【TODO】第二十三节:减法溢出与假想借位
- 【TODO】第二十四节:借用unsigned long类型的中间变量可以减少溢出现象
- 【TODO】第二十五节:乘法运算中的5种常用组合
- 【TODO】第二十六节:连乘、自乘、自乘简写,溢出
- 【TODO】第二十七节:整除求商
- 【TODO】第二十八节:整除求余
- 【TODO】第二十九节:“先余后商”和“先商后余”提取数据某位,哪家强?
- 【TODO】第三十节:逻辑运算符的“与”运算
- 【TODO】第三十一节:逻辑运算符的“或”运算
- 【TODO】第三十二节:逻辑运算符的“异或”运算
- 【TODO】第三十三节:逻辑运算符的“按位取反”和“非”运算
- 【TODO】第三十四节:移位运算的左移
- 【TODO】第三十五节:移位运算的右移
- 【TODO】第三十六节:括号的强制功能---改变运算优先级
- 【TODO】第三十七节:单字节变量赋值给多字节变量的疑惑
- 【TODO】第三十八节:第二种解决“运算过程中意外溢出”的便捷方法
- 【TODO】第三十九节:if判断语句以及常量变量的真假判断
- 【TODO】第四十节:关系符的等于“==”和不等于“!=”
- 【TODO】第四十一节:关系符的大于“>”和大于等于“>=”
- 【TODO】第四十二节:关系符的小于“<”和小于等于“<=”
- 【TODO】第四十三节:关系符中的关系符:与“&&”,或“||”
- 【TODO】第四十四节:小括号改变判断优先级
- 【TODO】第四十五节: 组合判断if...else if...else
- 【TODO】第四十六节: 一维数组
- 【TODO】第四十七节: 二维数组
- 【TODO】第四十八节: while循环语句
- 【TODO】第四十九节: 循环语句do while和for
- 【TODO】第五十节: 循环体内的continue和break语句
- 【TODO】第五十一节: for和while的循环嵌套
- 【TODO】第五十二节: 支撑程序框架的switch语句
- 【TODO】第五十三节: 使用函数的三要素和执行顺序
- 【TODO】第五十四节: 从全局变量和局部变量中感悟“栈”为何物
- 【TODO】第五十五节: 函数的作用和四种常见书写类型
- 【TODO】第五十六节: return在函数中的作用以及四个容易被忽略的功能
- 【TODO】第五十七节: static的重要作用
- 【TODO】第五十八节: const(./book/或code)在定义数据时的作用
- 【TODO】第五十九节: 全局“一键替换”功能的#define
- 【TODO】第六十节: 指针在变量(./book/或常量)中的基础知识
- 【TODO】第六十一节: 指针的中转站作用,地址自加法,地址偏移法
- 【TODO】第六十二节: 指针,大小端,化整为零,化零为整
- 【TODO】第六十三节: 指针“化整为零”和“化零为整”的“灵活”应用
- 【TODO】第六十四节: 指针让函数具备了多个相当于return的输出口
- 【TODO】第六十五节: 指针作为数组在函数中的入口作用
- 【TODO】第六十六节: 指针作为数组在函数中的出口作用
- 【TODO】第六十七节: 指针作为数组在函数中既“入口”又“出口”的作用
- 【TODO】第六十八节: 为函数接口指针“定向”的const关键词
- 【TODO】第六十九节: 宏函数sizeof(./book/)
- 【TODO】第七十节: “万能数组”的结构体
- 【TODO】第七十一节: 结构体的内存和赋值
- 【TODO】第七十二节: 结构体的指针
- 【TODO】第七十三节: 结构体数据的传输存储和还原
- 【TODO】第七十四节: 结构体指针在函数接口处的频繁应用
- 【TODO】第七十五节: 指针的名义(例:一维指针操作二维数组)
- 【TODO】第七十六节: 二维数组的指针
- 【TODO】第七十七节: 指针唯一的“单向输出”通道return
- 【TODO】第七十八节: typedef和#define和enum
- 【TODO】第七十九节: 各种变量常量的命名规范
- 【TODO】第八十节: 单片机IO口驱动LED
- 【TODO】第八十一节: 时间和速度的起源(指令周期和晶振频率)
- 【TODO】第八十二节: Delay“阻塞”延时控制LED闪烁
- 【TODO】第八十三节: 累计主循环的“非阻塞”延时控制LED闪烁
- 【TODO】第八十四节: 中断与中断函数
- 【TODO】第八十五节: 定时中断的寄存器配置
- 【TODO】第八十六节: 定时中断的“非阻塞”延时控制LED闪烁
- 【TODO】第八十七节: 一个定时中断产生N个软件定时器
- 【TODO】第八十八节: 两大核心框架理论(四区一线,switch外加定时中断)
- 【TODO】第八十九节: 跑马灯的三种境界
- 【TODO】第九十节: 多任务并行处理两路跑马灯
- 【TODO】第九十一节: 蜂鸣器的“非阻塞”驱动
- 【TODO】第九十二节: 独立按键的四大要素(自锁,消抖,非阻塞,清零式滤波)
- 【TODO】第九十三节: 独立按键鼠标式的单击与双击
- 【TODO】第九十四节: 两个独立按键构成的组合按键
- 【TODO】第九十五节: 两个独立按键的“电脑键盘式”组合按键
- 【TODO】第九十六节: 独立按键“一键两用”的短按与长按
- 【TODO】第九十七节: 独立按键按住不松手的连续均匀触发
- 【TODO】第九十八节: 独立按键按住不松手的“先加速后匀速”的触发
- 【TODO】第九十九节: “行列扫描式”矩阵按键的单个触发(原始版)
- 【TODO】第一百节: “行列扫描式”矩阵按键的单个触发(优化版)
- 【TODO】第一百零一节: 矩阵按键鼠标式的单击与双击
- 【TODO】第一百零二节: 两个“任意行输入”矩阵按键的“有序”组合触发
- 【TODO】第一百零三节: 两个“任意行输入”矩阵按键的“无序”组合触发
- 【TODO】第一百零四节: 矩阵按键“一键两用”的短按与长按
- 【TODO】第一百零五节: 矩阵按键按住不松手的连续均匀触发
- 【TODO】第一百零六节: 矩阵按键按住不松手的“先加速后匀速”触发
- 【TODO】第一百零七节: 开关感应器的识别与软件滤波
- 【TODO】第一百零八节: 按键控制跑马灯的启动和暂停和停止
- 【TODO】第一百零九节: 按键控制跑马灯的方向
- 【TODO】第一百一十节: 按键控制跑马灯的速度
- 第一百一十一节: 工业自动化设备的开关信号的运动控制
- 【TODO】第一百一十二节: 数码管显示的基础知识
- 【TODO】第一百一十三节: 动态扫描的数码管显示数字
- 【TODO】第一百一十四节: 动态扫描的数码管显示小数点
- 【TODO】第一百一十五节: 按键控制数码管的秒表
- 【TODO】第一百一十六节: 按键控制数码管的倒计时
- 【TODO】第一百一十七节: 按键切换数码管窗口来设置参数
- 【TODO】第一百一十八节: 按键让某位数码管闪烁跳动来设置参数
- 【TODO】第一百一十九节: 一个完整的人机界面的程序框架的脉络
- 【TODO】第一百二十节: 按键切换窗口切换局部来设置参数
- 【TODO】第一百二十一节: 可调参数的数码管倒计时
- 【TODO】第一百二十二节: 利用定时中断做的“时分秒”数显时钟
- 【TODO】第一百二十三节: 一种能省去一个lock自锁变量的按键驱动程序
- 【TODO】第一百二十四节: 数显仪表盘显示“速度、方向、计数器”的跑马灯
- 【TODO】第一百二十五节: “双线”的肢体接触通信
- 【TODO】第一百二十六节: “单线”的肢体接触通信
- 【TODO】第一百二十七节: 单片机串口接收数据的机制
- 【TODO】第一百二十八节: 接收“固定协议”的串口程序框架
- 【TODO】第一百二十九节: 接收带“动态密匙”与“累加和”校验数据的串口程序框架
- 【TODO】第一百三十节: 接收带“动态密匙”与“异或”校验数据的串口程序框架
- 【TODO】第一百三十一节: 灵活切换各种不同大小“接收内存”的串口程序框架
- 【TODO】第一百三十二节:“转发、透传、多种协议并存”的双缓存串口程序框架
- 【TODO】第一百三十三节:常用的三种串口发送函数
- 【TODO】第一百三十四节:“应用层半双工”双机串口通讯的程序框架