前情提要
前面我们开启了中断,但是这些中断都对应着一个通用的中断处理函数,而且几乎都是处理器触发的中断,没有我们的外设中断,虽然我们提前预留了这些接口。
现在我们实现一个时钟中断
一、可编程计数器8253
计算机中的时钟分为两种:内部时钟,外部时钟。
内部时钟是指处理器中内部元件,如运算器、控制器的工作时序,主要用于控制、同步内部工作过程的步调。内部时钟是由晶体振荡器产生的,简称晶振,它位于主板上,其频率经过分频之后就是主板的外频,处理器和南北桥之间的通信就基于外频。Intel处理器将此外频乘以某个倍数(也称为倍频)之后便称为主频。计算机取指令,执行指令中消耗的时钟周期都是基于主频的。内部时钟由处理器固件结构决定的,在出厂时就设置好了,无法改变,处理器内部原件的工作速度是最快的,所以内部时钟的时间单位粒度比较精细,通常都是ns纳秒级别。
外部时钟是指处理器与外部设备或外部设备之间通信时采用的一种时序,比如两个串口在通信时要首先指定波特率,只有波特率相同的两个串口之间才能通信成功。外部设备的速度对于处理器来讲就很慢了,所以其时钟的时间单位粒度比较大,一般是ms毫秒级别。
如何保证在不同的时钟下的设备能够同步通信呢?解决这个问题的大体思路就是:以处理器的内部时钟为依据来设计外部设备时钟,即要符合处理器内部运行时序,又要满足外部设备工作时序要求。定时计数器就是用来解决时序配合问题的。大家知道,处理器内部时钟信号由晶振产生,所以计时较为精确,但是晶振产生的频率较高,因此必须将其送到定时计数器分频,这样才能产生需要的各种定时信号。
对于外部定时,有两种实现方式,一种是软件实现
int count = 90000;
while(count -- > 0);
假设我们的晶振的频率是12MHZ,12个时钟周期是一个机器周期,也就是一个机器周期的时间是1us,即1微秒,上述的程序执行时间是90000us,即90ms。所以这种方式也是可以的。但是这是在白白消耗计算机的计算时间,是一种浪费的行为。
另一种就是硬件实现,硬件实现需要专门的硬件,这个硬件就是定时器。
计时器的功能就是定时发信号。当到达了所计数的时间,计数器可以自动发一个输出信号,可以用该信号向处理器发出中断,这样处理器可以去执行相应的中断处理程序。和软件定时相比从,硬件定时器不占用CPU资源。定时器分为可编程定时器和不可编程定时器,8253属于可编程定时器PIC,Programmable Interval Timer。
定时器有两种计时方式,一种是正计时,每一个脉冲都将当前的计数值加一,直到与设定的目标终止值相等。一种是倒计时,先设定好计数值,每一次脉冲都将当前值减一,直到为0时提示时间已到。
二、8253入门
8253内部有3个独立的计数器,如下图所示,分别是计数器0,计数器1,计数器2。他们的端口分别是0x40~0x42。计数器又称为通道,每个计数器都完全相同,都是16位大小。既然他们是独立的,也就是这三个计数器的工作是不依赖的,可以各干各的。可以这样做的基础就是他们都有自己的一套寄存器资源,互不干扰,寄存器资源包括一个16位的计数初值寄存器,一个计数器执行部件,和一个输出锁存器,其中计数器执行部件是计数器中真正进行计数工作的元器件,其本质是一个减法计数器。
计数器内部简图
每个计数器都有三个引脚,CLK,GATE,OUT。
CLK
很好理解,一看就是接时钟的输入信号,每收到一个时钟输入信号,计数器就减一,连接到这个引脚的最高频率为10MHZ,8253为2MHZ。
GATE
表示门控输入信号,在某些工作模式下用于控制计数器是否可以开始计数,在不同的工作模式下GATE作用不同。
OUT
表示计数器输出信号,当定时工作结束时,也就是计数值为0时,根据计数器的工作方式,会在OUT上输出相应信号。
计数初值寄存器用来保存计数器的初始值,是一个16位宽的寄存器
,它的作用是为计数器执行部件准备初始计数值,之后的计数过程与它无关。
加法计数器是整个定时器的核心,它从初值寄存器中拿到起始值,载入到自己的寄存器后便开始递减计数。注意,计数过程中不断变化的值称为当前计数值,它保存在执行部件自己的寄存器中,初值寄存器中的值不受影响。
输出锁存器也称为当前计数值锁存器,用于把当前减法计数器中的计数值保存下来,其目的就是为了让外界可以随时获取当前计数值。计数器中的计数值是不断变化的,处理器无法直接从计数器中获取当前计数值。
计数初值寄存器、计数器执行部件和输出锁存器都是16位宽度的寄存器,所以高8位和低8位都可以单独访问。
三个计数器都有自己的作用
计数器0(端口0x40)在个人计算机中,计数器0专用于产生实时时钟信号。它采用工作方式3,往此计数器写入0时则为最大计数值65536
计数器1(端口0x41)在个人计算机中,计数器1专用于DRAM的定时刷新控制。这个刷新是因为DRAM的结构是电容存储,存储的时间短,需要定时刷新,即给电容充电。
计数器2(端口0x42)在个人计算机中,计数器2原用于通过输出方波PWM输出声音,这个我们用不到。
计数器0是我们研究的重点,他就是时钟中断的信号源
三、8253控制字
8253控制字格式,其端口为0x43
SC1和SC0位是选择计数器位,即Select Counter,或者叫选择通道位,即Select Channel。
RW1和RW0位是读/写/锁存操作位,即Read/Write/Latch,用来设置待操作计数器(通道)的读写及锁存方式。计数器是16位宽度,当我们往计数器中写入计数初值时,或者读取计数器中的数值时,可以指定读写低8位,还是高8位。RW1和RW0这两位组合成4种读写方式,具体选择值如图7-47所示。
M2~M0这三位是工作方式(模式)选择位,即Method或Mode。每个计数器有6种不同的工作方式,即方式0~方式5。
BCD码(Binary-Coded Decimal),用4位二进制数来表示1位十进制中的0~9这10个数码,是一种二进制的数字编码形式,用二进制编码的十进制代码。
四、8253工作模式
工作模式 | 描述 |
---|---|
模式0 | 计数结束中断方式(Interrupt on Terminal Count) |
模式1 | 硬件可重触发单稳方式(Hardware Retriggerable One-Shot) |
模式2 | 比率发生器(Rate Generator) |
模式3 | 方波发生器(Square Wave Generator) |
模式4 | 软件触发选通(Software Triggered Strobe) |
模式5 | 硬件触发选通(Hardware Triggered Strobe) |
计数器的计数过程在何时发生呢?将计数初值写入计数器后就开始计数了吗?不完全是。开始计数的时机与工作方式相关,计数器开始计数需要两个条件。
- GATE为高电平,即GATE为1,这是硬件控制的
- 计数初值已经写入利润计数器中的减法计数器,这是由软件out指令控制的。
当这两个条件具备后,计数器将在下一个时钟信号CLK的下降沿开始计数。
软件启动
软件启动是指上面硬件负责的条件1已经完成,也就是GATE已经为1,目前只差软件来完成条件2,即尚未写入计数初值,只要软件负责的条件准备好,计数器就开始启动。当处理器用out指令往计数器写入计数初值,减法器将此初值加载后,计数器便开始计数。工作方式0、2、3、4都是用软件启动计数过程。
硬件启动
硬件启动是指上面软件负责的条件2已经完成,即计数初值已写入计数器。目前只差硬件来完成条件1了,也就是门控信号GATE目前还是低电平,即目前GATE=0,只要硬件负责的条件准备好,计数器就开始启动。GATE引脚是由外部信号来控制的,只有当GATE由0变1的上升沿出现时,计数器才开始启动计数。工作方式1、5都是用硬件启动计数过程。
强制终止
有些工作方式中,计数器是重复计数的,当计时到期(计数值为0)后,减法计数器又会重新把计数初值寄存器中的值重新载入,继续下一轮计数,比如工作方式2和工作方式3都是采用此方式计数,此方式常见于需要周期性发信号的场合。对于采用此类循环计数工作方式的计数器,只能通过外加控制信号来将其计数过程终止,办法是破坏启动计数的条件:将GATE置为0即可。
自动终止
有些工作方式中,计数器是单次计数,只要定时(计数)一到期就停止,不再进行下一轮计数,所以计数过程自然就自动终止了。比如工作方式0、1、4、5都是单次计数,完成后自动终止。如果想在计数过程中将其终止怎么做呢?还是用那个简单粗暴可依赖的方法,将GATE置0
方式0:计数结束中断方式(Interrupt on Terminal Count)
方式0也称为“计数结束输出正跳变信号”方式,其典型应用是作为事件计数器。
在方式0时,对8253任意计数器通道写入控制字,都会使该计数器通道的OUT变为低电平,直到计数值为0。计数值为0时,输出会从低电平跳变到高电平,因此这个信号可以接在8259A的中断引脚IR0上。计数工作会在下一个时钟信号的下降沿开始。
方式0进行计数时,计数器只是单次计数,计数为0时,并不会再将计数初值寄存器中的值重新载入。此方式中,门控信号GATE用于允许或禁止计数,当GATE=1时允许计数,GATE=0时则禁止计数。
方式1: 硬件可重触发单稳方式(Hardware Retriggerable One-Shot)
方式1的典型应用是作为可编程单稳态触发器,其触发信号是GATE,这是由硬件来控制的,故此方式称为硬件可重触发单稳方式。
在方式1下,由处理器将计数初值写入计数器后,OUT引脚变为高电平。不过,无论此时GATE是高电平,还是低电平,计数器都不会启动计数,而是等待外部门控脉冲信号GATE由低到高的上升沿出现,这是由硬件启动的,之后才会在下一个时钟信号CLK的下降沿开始启动计数,同时会将OUT引脚变为低电平。此后,每当CLK引脚收到一个时钟脉冲信号时,在其下降沿,减法计数器便开始对计数值减1。
OUT引脚的低电平状态一直保持到计数为0,当计数为0时,OUT引脚产生由低到高的正跳变信号。
方式2:比率发生器(Rate Generator)
按照比率来分频,其典型应用就是分频器,故也称为分频器方式。
此方式的特点是计数器计数到达后,自动重新载入计数初值,不需要重新写入控制字或计数初值便能连续工作。当计数初值为N时,每N个CLK时钟脉冲,就会在OUT端产生一个输出信号,这样一来,输入信号CLK和输出信号OUT的关系是N:1。
方式2主要用在循环分频的场合。
方式3:方波发生器(Square Wave Generator)
在方式3下工作,就相当于一个方波发生器。当处理器把控制字写入到计数器后,OUT端输出高电平。在GATE为高电平的前提下,在处理器把计数初值写入计数器后的下一个CLK时钟脉冲的下降沿,计数器开始计数。
方式3的特别之处在于每次计时结束都翻转out的电平
方式4:软件触发选通(Software Triggered Strobe)
当处理器把控制字写入到计数器后,OUT端变成高电平。在GATE为高电平的前提下,在处理器把计数初值写入计数器后的下一个CLK时钟脉冲的下降沿,计数器开始计数,所以是软件启动。
当计数值为1时,OUT端由高电平变为低电平,当计数值为0,即持续一个CLK时钟周期后,OUT端又回到高电平,此时计数器停止计数。此方式和方式0类似,都是单次计数,只有在重新写入控制字或重新写入计数初值时才会重新开启计数。
方式5:硬件触发选通(Hardware Triggered Strobe)
此方式与方式4类似,都是一次计数,区别是计数启动的方式不同,方式5是硬件启动。
方式5中,当处理器把控制字写入到计数器后,OUT端变成高电平。处理器把计数初值写入计数器后,计数工作要等到外部门控脉冲信号GATE由低到高的上升沿出现时才开启,这是由硬件启动的。
当计数值为1时,OUT端由高电平变为低电平,保持一个CLK周期,即计数值变为0时,OUT端又变为高电平,同时停止计数。
五、代码
代码就很简单了,就是向相应的端口写控制字就可以了
#define IRQ0_FREQUENCY 100 // IR0需要的频率
#define INPUT_FREQUENCY 1193180 // 8253的输入频率
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
#define CONTRER0_PORT 0x40 // 计数器0的端口
#define CONTRER0_NO 0 // 选择计数器0
#define CONTRER0_MODE 2 // 选择工作模式2,比率发生器,即分频
#define CONTRER0_RWL 3 // 读写方式位先读写低字节再读写高字节
#define COUNTER0_BCD 0 // 采用二进制方式
#define PIT_CONTROL_PORT 0x43 // 控制端口
static void frequency_set(uint8_t counter_port, uint8_t counter_no, uint8_t counter_rwl, \
uint8_t counter_mode,uint8_t counter_bcd, uint16_t counter_value) {
outb(counter_port,(uint8_t)(counter_no << 6 | counter_rwl << 4 | counter_mode << 1 | counter_bcd));
outb(counter_port, (uint8_t)counter_value);
outb(counter_port, (uint8_t)(counter_value >> 8));
}
void timer_init() {
put_str("timer_init start!\n");
frequency_set(CONTRER0_PORT,CONTRER0_NO,CONTRER0_RWL,CONTRER0_MODE,\
COUNTER0_BCD,COUNTER0_VALUE);
put_str("timer_init end!\n");
}
为了验证我们的代码是成功的,我们注册了一个时钟中断的中断处理函数
static void timer_interrupt(void) {
put_str("this is timer interrupt\n");
return;
}
并将其注册
idt_table[0x20] = timer_interrupt;
可以看到下面的仿真结果
结束语
这三节我们讲解了中断以及如何跳转到中断处理函数执行,并且实现了一个时钟中断,下一节我们将实现一个简单的C语言库