tips:这是一篇系列文章,总目录在这里哟~
1. 计算机基本组成
冯·诺依曼体系架构下的计算机,是由运算器、控制器和存储器组成的。现代计算机通常还会有外部存储器和各种外围的I/O设备组成。限于篇幅,我们只能简单的聊一聊运算器、控制器和存储器,不敢深入原理,否则就是另外一个专题了。大家如果有兴趣,我们后面可以再搞一个专题专门来聊一聊。
我以前上学的时候,听过一种很奇怪的说法,说是冯·诺依曼体系就是二进制计算机。其实冯·诺依曼跟二进制没啥关系,真要说二进制体系,那是香农的功劳。
(1)运算器和控制器
大家都知道现在这俩东西合起来被称为了CPU。因为他们被发明之初就密不可分了。
运算器通过一定的逻辑电路可以完成算术运算,最简单的就是加法运算。可以去了解一下半加器、全加器这些概念。现在,我只能说,运算器就是用来做二进制加法的机器。
控制器是另外一组逻辑电路,它可以控制计算机什么时候该读取指令,什么时候该进行运算了。而驱动控制器的装置就叫做振荡器。下文会有介绍。
(2)存储器
存储器就是存储bit的地方。根据类型不同,他们的实现方式也不同。
- 寄存器:严格来说,寄存器也是一种存储器,因为它也能存储bit。寄存器是由一组触发器串联组成的。它的访问速度最快,但由于触发器的制造成本比较高,所以也只能在CPU内部用一用了。
- 静态存储器SRAM:一般用作高速缓存。它也是由触发器组成的。当然高速缓存也有其他的实现方案。
- 动态存储器DRAM:使用电容器来存储数据,电容器充电表示1,没充电表示0。电容器可以做到很小,就可以集成到芯片上了。只是电容器会放电,所以需要不断地充电来刷新。
- 只读存储器ROM:一开始的ROM使用的是一种一次性写入的材料,所以只读;后来也有可擦写的ROM出现,也就是闪存。好处是掉电数据不丢失。
- 其他:磁带、光盘、软盘、硬盘等
2. 晶振和时钟
振荡器,虽然其貌不扬,但却是计算机的心脏。利用继电器可以设计出机械振荡器,但机械振荡器的振动频率不稳定。我们现代电子计算机中使用的都是电子振荡器。电子振荡器也有很多种实现,计算机中使用的是晶体振荡器,简称晶振。
振荡器产生周期性的振荡信号(计算机中通常为方波)。控制器如果为上升沿触发,则每个周期都会被触发一次。从而驱动控制器工作。所以振荡器也就是我说的时间驱动力在物理层面最本质的体现。
时钟,计算机中有时钟的概念。我们经常听到的时钟主频的概念,就是指CPU中的控制器能稳定工作的频率。但振荡器不是位于CPU中的,而是位于主板中的。所以买不同CPU得搭配不同型号的主板,组装电脑也不能瞎组的。另一个时钟的概念,叫做RTC,即实时时钟,用来记录现实世界的时间。它通常都有多个电源,即使计算机断电,它也能用备用电源工作。主板上那个放纽扣电池的家伙,就是RTC。在各种嵌入式设备中,你也能看到RTC的影子。
3. 中断
计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。这种电路设计叫做中断。
所以,通过一定的逻辑电路进行定时,到时间后触发产生的中断就叫做定时器中断。定时器每次触发中断,都可以执行中断处理逻辑(中断函数)。所以,我认为定时器中断是时间驱动力在物理层面的第二层体现。
① 什么?定时器怎么计时?那还是用晶振啊,知道了振荡周期,还算不出需要定时的时间吗?
② 中断是怎么实现的?中断是一种纯硬件电路的实现,逻辑电路的设计其实跟软件设计很像,也可以通过电路表示出与、或、非的关系。换句话说,软件能实现的功能,逻辑电路也一样能实现。这也是视频解码的时候,我们为什么会经常听到软解硬解了。关于逻辑电路怎么设计,这个又是另一个领域了,有兴趣可以一起探究下。
我们先以简单的8051单片机为例,说明定时器中断的概念和使用。然后再来看看在x86架构下,计算机如何实现定时器中断的。
4. 8051的定时器实现
51单片机是一种8位的微处理器,实现原理较为简单,比较适合我们用来理解一些硬件原理和操作。
(1)中断系统简介
典型的C51实现,中断系统有5个中断源(8052有 6个) ,2个优先级,可实现二级中断嵌套
中断编号 | 中断名 | 中断源 |
---|---|---|
0 | 外部中断0 | IE0(P3.2) |
1 | 定时器0溢出中断 | TF0 |
2 | 外部中断1 | IE1(P3.3) |
3 | 定时器1溢出中断 | TF1 |
4 | 串行口中断 | RI |
可以看到,C51中断一共有三种类型:外部中断、定时器中断、串行口中断。
由于C51内部就有定时器逻辑电路和相关的寄存器,所以不借助外部芯片就可以实现定时器中断。但C51内部的定时器并不是实时时钟,也就是说断电后就会归零,所以每次使用时,都需要先进行初始化。我们也可以使用外部的RTC芯片作为中断源,这样我们将RTC芯片的对应输出端口连接到单片机的IE0或者IE1管脚上即可。
(2)中断控制寄存器
在C51内部还有一组用于中断控制的寄存器:
- 中断允许控制寄存器IE
- 定时器控制寄存器TCON
- 串口控制寄存器SCON
- 中断优先控制寄存器IP
- 定时器工作方式控制寄存器TMOD
- 定时器初值赋予寄存器(TH0/TH1,TL0/TL1)
我们一个一个来看一下。
IE寄存器
- EX0:外部中断0允许位;
- ET0:定时/计数器T0中断允许位;
- EX1:外部中断1允许位;
- ET1:定时/计数器T1中断允许位;
- ES :串行口中断允许位;
- EA :CPU中断允许(总允许)位。
可以看出,EA是总开关,所以必须为1;我们需要使用ET0定时器中断,那么ET0要为1。
TCON寄存器
- IT0:外部中断0触发方式控制位
- 当IT0=0时,为电平触发方式(低电平有效)
- 当IT0=1时,为边沿触发方式(下降沿有效)
- IE0:外部中断0中断请求标志位
- IT1:外部中断1触发方式控制位
- IE1:外部中断1中断请求标志位
- TF0:定时/计数器T0溢出中断请求标志位
- TF1:定时/计数器T1溢出中断请求标志位
- TR0:定时/计数器T0工作控制标志位,1–开始计时,0–停止
- TR1:定时/计数器T1工作控制标志位,1–开始计时,0–停止
SCON寄存器用于控制串口中断,不在这里展开。
IP控制器
- PX0:外部中断0优先级设定位
- PT0:定时/计数器T0优先级设定位
- PX1:外部中断0优先级设定位
- PT1:定时/计数器T1优先级设定位
- PS :串行口优先级设定位
TMOD寄存器
TMOD是用来指定定时器工作方式的寄存器
(3)计数寄存器
TX0和TX1,当前T0和T1计数的值将存放在计数寄存器中
TX0又分为TH0和TL0,TX1又分为TH1和TL1
定时器初值的计算方式:
时钟周期:假设我们使用12MHz的晶振,也就是说每秒震荡12M次;那么一个时钟周期就是12M的倒数。时钟周期就是最小的时间单位。
机器周期:1个时钟周期没办法让单片机干完所有事情,而12个时钟周期才能够完成基本的操作。
即:每计一次数,就会消耗掉一个机器周期=1/12M*12=1/1M=0.000001s=1μs
如果我想要每隔50ms定时器中断一次,我应该设置定时器的初值为:65536-50000=15536
换算为二进制就是:0011 1100 1011 0000
(4)定时器中断
我们先来看看如何使用内部的定时器中断的操作。
首先根据上面的介绍,我们知道要使用定时器中断,首先要进行初始化设置:
- 给TMOD赋值,确定工作方式
- 给定时器赋初值,写入TH0,TL0或TH1,TL1
- 中断允许控制,EA和ET0 或ET1的置位为1
- TR0或TR1置位,启动定时器
// 定时器初始化,我们使用T0定时器
void timerInit()
{
// 0000 0001
// 高四位用于T1,所以全部为0;低四位用于T0,第四位GATE为0即可,第三位选用定时模式为0,第二位和第一位设置工作方式为16位定时器
TMOD = 0x01;
// 0011 1100 1011 0000 = 15536(十进制)
TH0 = 0x3C;
TL0 = 0xB0;
// 总开关打开
EA = 1;
// T0开关打开
ET0 = 1;
// T0开始工作
TR0 = 1;
}
完整程序如下:
/*********************************************************************************
*FileName: DynamicDigitalTube
*Author: wjc133
*Version: 1.0.0
**********************************************************************************/
#include <8051.h>
#define A P2_4
#define B P2_3
#define C P2_2
#define SEGMENT P0
__code unsigned char NUMBER[] = {
0xFC, // 0: 0000 0011 1111 1100
0x60, // 1: 1001 1111 0110 0000
0xDA, // 2: 0010 0101 1101 1010
0xF2, // 3: 0000 1101 1111 0010
0x66, // 4: 1001 1001 0110 0110
0xB6, // 5: 0100 1001 1011 0110
0xBE, // 6: 0100 0001 1011 1110
0xE0, // 7: 0001 1111 1110 0000
0xFE, // 8: 0000 0001 1111 1110
0xF6, // 9: 0000 1001 1111 0110
};
/* 定义全局变量 */
unsigned char time;
unsigned int currentNum = 0;
/* 函数声明 */
void timerInit();
void timerInterrupt() __interrupt 1;
unsigned int getNumber(unsigned int n);
void display(unsigned int a);
void main()
{
time = 0;
timerInit();
while (1)
{
display(currentNum);
}
}
void timerInit()
{
TMOD = 0x01;
TH0 = 0x3C;
TL0 = 0xB0;
EA = 1;
ET0 = 1;
TR0 = 1;
}
/* 响应中断号为1的中断函数 */
void timerInterrupt() __interrupt 1
{
TH0 = 0x3C;
TL0 = 0xB0;
if (++time == 20)
{
currentNum = getNumber(currentNum);
time = 0;
}
}
unsigned int getNumber(unsigned int n)
{
if (n + 1 >= 1000)
{
return 0;
}
return n + 1;
}
void display(unsigned int a)
{
// 拆解分析a的位数
unsigned char i = 0;
unsigned int d[8];
do
{
d[i++] = a % 10;
a = a / 10;
} while (a > 0);
// 显示每一位
unsigned char ni = 0;
unsigned int nj = 0;
for (ni = 7; ni > 7 - i; ni--)
{
switch (ni)
{
case 0:
A = 0;
B = 0;
C = 0;
break;
case 1:
A = 1;
B = 0;
C = 0;
break;
case 2:
A = 0;
B = 1;
C = 0;
break;
case 3:
A = 1;
B = 1;
C = 0;
break;
case 4:
A = 0;
B = 0;
C = 1;
break;
case 5:
A = 1;
B = 0;
C = 1;
break;
case 6:
A = 0;
B = 1;
C = 1;
break;
case 7:
A = 1;
B = 1;
C = 1;
break;
default:
break;
}
SEGMENT = NUMBER[d[7 - ni]];
nj = 10;
while (nj--)
;
SEGMENT = 0x00;
}
}
参考资料
- C51 定时器/计数器个人笔记,by YuQiao0303
- 51单片机中断详解(上),by Line
- C51单片机晶振频率