????????本章介绍定时器中断的使用。
一 定时器中断的使用
? ? ? ? 由【操作系统】30天自制操作系统--(5)分割编译与中断处理中的表格可以看到,对于定时器中断的使用就用到了IRQ0这个中断请求。
中断请求 | 硬件设备 | IRQ0 | 系统计时器 | IRQ1 | 键盘 | IRQ2 | 可设置中断控制卡 | IRQ3 | COM2(串行接口2) | IRQ4 | COM1(串行接口1) | IRQ5 | 空 | IRQ6 | 磁盘机 | IRQ7 | 并行接口 | IRQ8 | CMOS/时钟 | IRQ9 | 空 | IRQ10 | 空 | IRQ11 | 空 | IRQ12 | PS/2鼠标 | IRQ13 | 算术处理器(Arithmetic Processor) | IRQ14 | Primary(主)IDE控制器 | IRQ15 | Secondary(从)IDE控制器 |
? ? ? ? 其中断周期的变更步骤如下:
? ? ? ? 这边用的是8254定时器芯片 ,其主频为1.19318MHz(参考8254芯片详解)
????????就是说,如果想要IRQ0中断时间为10ms,就需要往寄存器中写值11932(0x2e9c):
#include "bootpack.h"
#define PIT_CTRL 0x0043
#define PIT_CNT0 0x0040
void init_pit(void) {
io_out8(PIT_CTRL, 0x34);
io_out8(PIT_CNT0, 0x9c);
io_out8(PIT_CNT0, 0x2e);
return;
}
? ? ? ? 在主函数中调用(下面关于中断屏蔽寄存器IMR的使用可以参考【操作系统】30天自制操作系统--(5)分割编译与中断处理):
void HariMain(void)
{
// ...
init_pit();
// ...
io_out8(PIC0_IMR, 0xf8); /* PIT和PIC1和键盘设置为许可(11111000) */
io_out8(PIC1_IMR, 0xef); /* 鼠标设置为许可(11101111) */
// ...
}
? ? ? ? 当然,编写中断服务程序---->汇编实现中断服务程序的返回---->将编写好的中断处理程序(包含中断服务程序和汇编处理两部分)注册到IDT中断记录表中?这几个步骤也与鼠标和键盘中断的处理如出一辙,这边不再赘述,只简单将这几处程序贴在一起:
void inthandler20(int *esp) {
io_out8(PIC0_OCW2, 0x60); /* 把IRQ-00信号接收完了的信息通知给PIC */
/* 暂时什么都不做 */
return;
}
_asm_inthandler20:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler20
POP EAX
POPAD
POP DS
POP ES
IRETD
void init_gdtidt(void)
{
/* 中略 */
/* 中断注册表IDT注册 */
set_gatedesc(idt + 0x20, (int)asm_inthandler20, 2*8, AR_INTGATE32); //IRQ0定时中断
set_gatedesc(idt + 0x21, (int)asm_inthandler21, 2*8, AR_INTGATE32); //IRQ1键盘中断
set_gatedesc(idt + 0x27, (int)asm_inthandler27, 2*8, AR_INTGATE32); //IRQ7并行接口
set_gatedesc(idt + 0x2c, (int)asm_inthandler2c, 2*8, AR_INTGATE32); //IR12鼠标中断
}
? ? ? ? 至此,完成了定时器中断处理程序的编写和注册。接下来就是往服务程序里面填写逻辑了。
二 添加定时器功能(计数器、超时返回、设置多个定时器)
? ? ? ? 这边列出了三种功能的使用。
【1】计数器:很简单,定时器中断服务程序中整个变量自加,完了在窗口中显示出来。
【2】超时返回:
? ? ? ? (1)首先向结构体struct TIMERCTL中添加代码记录超时的信息:
struct TIMERCTL {
unsigned int count;
unsigned int timeout; /* 距离暂停还有多少时间 */
struct FIFO8 *fifo; /* 接收到时间后程序发送数据的缓冲区 */
unsigned char data;
};
? ? ? ? (2)随后在中断服务程序里面实现的暂停功能,在settimer中 ,为了防止IRQ0中断乱入,采取的是禁止中断 + 处理 + 开放 的策略(这一点之前鼠标键盘也是这么处理的)。
struct TIMERCTL timerctl;
void init_pit(void) {
io_out8(PIT_CTRL, 0x34);
io_out8(PIT_CNT0, 0x9c);
io_out8(PIT_CNT0, 0x2e);
timerctl.count = 0;
timerctl.timeout = 0;
return;
}
void inthandler20(int *esp) {
io_out8(PIC0_OCW2, 0x60); /* 把IRQ-00信号接收完了的信息通知给PIC */
timerctl.count++;
if (timerctl.timeout > 0) { /* 如果设定了超时 */
timerctl.timeout--;
if (timerctl.timeout == 0) {
fifo8_put(timerctl.fifo, timerctl.data);
}
}
return;
}
void settimmer(unsigned int timeout, struct FIFO8 *fifo, unsigned char data){
int eflags;
eflags = io_load_eflags();
timerctl.timeout = timeout;
timerctl.fifo = fifo;
timerctl.data = data;
io_store_eflags(eflags);
return;
}
? ? ? ? (3)最后,在主函数中读取定时器缓冲数组中的数据(如果有的话),并在屏幕上返回一个定时器超时的信息“10[sec]”:
void HariMain(void)
{
//(中略)
struct FIFO8 timerfifo;
char s[40], keybuf[32], mousebuf[128], timerbuf[8];
//(中略)
fifo8_init(&timerfifo, 8, timerbuf);
settimer(1000, &timerfifo, 1);
(中略)
for (;;) {
//(中略)
io_cli();
if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) + fifo8_status(&timerfifo) == 0) {
io_sti();
} else {
if (fifo8_status(&keyfifo) != 0) {
//(中略)
} else if (fifo8_status(&mousefifo) != 0) {
//(中略)
} else if (fifo8_status(&timerfifo) != 0) {
i = fifo8_get(&timerfifo); /* 首先读入(为了设定起始点) */
io_sti();
putfonts8_asc(buf_back, binfo-
>scrnx, 0, 64, COL8_FFFFFF, "10[sec]");
sheet_refresh(sht_back, 0, 64, 56, 80);
}
}
}
}
【3】设置多个定时器:这边类似于之前【操作系统】30天自制操作系统--(8)内存管理中“内存管理表”的方法,添加一个flags标志位,用来标识定时器的状态(包括未使用为0,已配置为1,运行中为2):
????????(1)首先修改结构体struct TIMERCTL(这边最大可以配置500个定时器):
#define MAX_TIMER 500
struct TIMER {
unsigned int timeout, flags;
struct FIFO8 *fifo;
unsigned char data;
};
struct TIMERCTL {
unsigned int count;
struct TIMER timer[MAX_TIMER];
};
? ? ? ? (2)添加一些定时器的配置和释放等功能函数(即对flags标志位进行操作):
#include "bootpack.h"
#define PIT_CTRL 0x0043
#define PIT_CNT0 0x0040
struct TIMERCTL timerctl;
#define TIMER_FLAGS_ALLOC 1 /* 已配置状态 */
#define TIMER_FLAGS_USING 2 /* 定时器运行中 */
void init_pit(void) {
io_out8(PIT_CTRL, 0x34);
io_out8(PIT_CNT0, 0x9c);
io_out8(PIT_CNT0, 0x2e);
timerctl.count = 0;
int i;
for (i = 0; i < MAX_TIMER; i++) {
timerctl.timer[i].flags = 0; /* 未使用 */
}
return;
}
/**
* @brief 返回未在使用的 timer
*/
struct TIMER *timer_alloc(void){
int i;
for (i = 0; i < MAX_TIMER; i++) {
if (timerctl.timer[i].flags == 0) {
timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;
return &timerctl.timer[i];
}
}
return 0;
}
void timer_free(struct TIMER *timer){
timer->flags = 0;
return;
}
void timer_init(struct TIMER *timer, struct FIFO8 *fifo, unsigned char data){
timer->fifo = fifo;
timer->data = data;
return;
}
void timer_settime(struct TIMER *timer, unsigned int timeout){
timer->timeout = timeout;
timer->flags = TIMER_FLAGS_USING;
return;
}
void inthandler20(int *esp){
io_out8(PIC0_OCW2, 0x60); /* 把IRQ-00信号接收结束的信息通知给PIC */
timerctl.count++;
int i;
for (i = 0; i < MAX_TIMER; i++) {
if (timerctl.timer[i].flags == TIMER_FLAGS_USING) {
timerctl.timer[i].timeout--;
if (timerctl.timer[i].timeout == 0) {
timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;
fifo8_put(timerctl.timer[i].fifo, timerctl.timer[i].data);
}
}
}
return;
}
? ? ? ? (3)在主函数中调用(这边作者配置了三个定时器,一个是10s超时显示字符串;一个是3s超时显示字符串;一个是每隔0.5s通过切换定时器的使用状态来控制屏幕上小矩形光标的亮灭,以实现光标闪烁的效果)。这边主函数的代码比较简单但冗长,就不贴出来了。
三 加快定时器中断处理
? ? ? ? 这边列出了三种方法。
【1】原本是timeout参数自减,根据其是否到0来判断超时,现在修改为count参数直接跟timeout参数相比较,减少一行timeout参数的运算。算是一个小优化吧,具体程序略。
【2】在结构体中新增next参数用来记住下一个时间,先和 timeout 最小的作比较。这样就不需要每次都要比较判断所有的定时器。具体程序略。
【3】参考【操作系统】30天自制操作系统--(9)叠加处理中对于图层的处理,专门开辟一块内存用来存放某种顺序排列好的定时器地址struct TIMER *timer[MAX_TIMER],以减少到达next时刻的处理时间(具体程序略。个人感觉方法三,与增加的代码复杂度相比,带来的性能提升感觉不大):
struct TIMERCTL {
unsigned int count, next, using;
struct TIMER *timers[MAX_TIMER];
struct TIMER timers0[MAX_TIMER];
};
|