前一篇文章居然忘记放测试代码了:
int os_main(void) { init_VBE(); init_console(); init_IDT(); show_logo(); asm volatile ("int $0x1"); asm volatile ("int $0x2"); asm volatile ("int $0x3"); asm volatile ("int $0x4"); /* 进入死循环 */ while(true); return 0;}
接下来我们就来探讨中断应用的事情了,先来介绍一个很重要的芯片:8259A。在早期的IA32架构的PC中,中断控制器都是8259A(现在使用APIC,高级可编程中断控制器,暂不介绍),在中断诞生之前,CPU都是通过轮询来查看外部信息和状态的,效率非常低!中断的出现,大大提高了CPU的工作效率,接下来的内容都是以它为重点。
8259A简介
外部结构
(图片来源于网络)
8259A由两部分组成:主片和从片,其实是intel中断不过拿来级联使用的,掐指一算,一共可以接受15种中断(主片上的IR2不算一个),很少?那就对了,8259A是外部中断用的,硬件软件都搞定了才完整。以下是各个端口的功能:
IRQ0: 时钟 IRQ1 : 键盘 IRQ3 : 串口2 IRQ4 : 串口1 IRQ5 : 并口2 IRQ6 : 软盘 IRQ7 : 并口1 IRQ8 : 实时时钟 IRQ9 : 重定向的IRQ2 IRQ10 : 保留 IRQ11 : 保留 IRQ12 : PS/2 鼠标 IRQ13 : FPU异常 IRQ14 : 硬盘 IRQ15 : 保留 默认情况下,优先级自上而下
这样看来还是时钟、鼠标、键盘中断服务程序好写。一会会涉及到这些东西。
内部结构
(来源:《操作系统真象还原》)
8259 可编程中断控制器有两组寄存器:初始化命令寄存器组,用来保存初始化命令字ICW (Initialization Command Word,共4个,ICW1~ICW4)和操作命令寄存器组,用来保存操作命令字OCW (Operation Command Word,共3个,OCW1~OCW3)。我们使用IN和OUT指令来访问这些寄存器。
8259A初始化
开篇说过了,8259A是可编程芯片,他的功能还不止这篇文章。所以8259A芯片是需要初始化才能使用也不奇怪,想要初始化它,只需要初始化一组ICW寄存器,分别写入主片和从片的ICW1 ~ ICW4,一定要按顺序。这里读者可以不用理解那么深,因为在本个系列结束前,都不会用到很多,而且要是想讲清除,这一篇文章是讲不完的:已经有读者提出文章过长了。接下来就是设置CW1 ~ ICW4,每个线对应一个x(如果没对齐,这是笔者认为最好解释各个寄存器数据的办法,就是垃圾微信排版搞的):
A0线用于选择操作的寄存器,当A0=0时芯片的端口地址是0x20(主)和0xA0(从),当 A0=1时端口就是0x21(主)和0xA1(从)。
/* 详解8259A https://blog.csdn.net/longintchar/article/details/79439466*/
ICW1 (端口 0x20 / 端口 0xA0)
|0|0|0|1|x|0|x|x| | | +--- ICW4 使用(1)、不使用(0) | +----- 单片(1)、级联(0) +--------- 电平触发方式(1)、边缘触发方式(0)
ICW2 (端口 0x21 / 端口 0xA1)
设置ICW1之后,当 A0=1 时表示对 ICW2 进行设置,当A0为1时,主片端口地址是0x21,从片端口地址是0xA1。
|x|x|x|x|x|0|0|0| || | | | +----------------- 中断号的高5位
ICW2 (端口 0x21 / 端口 0xA1)
主:|x|x|x|x|x|x|x|x| || | | | | | | +------------------ 主片是否连接端口:是(1)、否 (0) 从: |0|0|0|0|0|x|x|x| | | | +-------- 从片的ID等于主片端口
ICW4 (端口 0x21 / 端口 0xA1)
工作模式设置
|0|0|0|x|x|x|x|1| || | +------ 自动结束中断方式 (1)、非自动结束方式(0) | | +-------- 缓冲方式下主片(1)、缓冲方式下从片(0) | +---------- 缓冲方式(1)、非缓冲方式(0) +------------ 选择特殊全嵌套方式(1)、一普通全嵌套方式(1)
有点不说人话,翻译成代码就是这样:
// include/interrupt.h#ifndef _INTERRUPT_H#define _INTERRUPT_H#include #include .../* 8259A-Master */#define PIC0_OCW2 0x20#define PIC0_IMR 0x21#define PIC0_ICW2 0x21#define PIC0_ICW3 0x21#define PIC0_ICW4 0x21#define PIC1_ICW1 0xA0/* 8259A-Slave */#define PIC1_OCW2 0xA0#define PIC1_IMR 0xA1#define PIC1_ICW2 0xA1#define PIC1_ICW3 0xA1#define PIC1_ICW4 0xA1// kernel/interrupt.cvoid init_8259A(void) { io_out8(PIC0_IMR, 0xff); io_out8(PIC1_IMR, 0xff); io_out8(PIC0_ICW1, 0x11); // 0001 0001 io_out8(PIC0_ICW2, 0x20); // 0010 0000 io_out8(PIC0_ICW3, 0x04); // 0000 0100 io_out8(PIC0_ICW4, 0x01); // 0000 0001 io_out8(PIC1_ICW1, 0x11); // 0001 0001 io_out8(PIC1_ICW2, 0x28); // 0010 1000 io_out8(PIC1_ICW3, 0x02); // 0000 0010 io_out8(PIC1_ICW4, 0x01); // 0000 0001 io_out8(PIC0_IMR, 0xfe); io_out8(PIC1_IMR, 0xff);}
注意,之前的ISR还不够,还需要IQR(外设中断服务函数),同样需要像之前的ISR那样注册,汇编也要加:
// kernel/interrupt.cvoid IRQ_Handler(Registers_S *Registers) { // 发送重置信号 if (Registers->Interrupt_Number >= 40) { // 主 io_out8(PIC1_ICW1, 0x20); } // 从 io_out8(PIC0_OCW2, 0x20); if (InterruptHandlers[Registers->Interrupt_Number] != NULL) { InterruptHandlers[Registers->Interrupt_Number](Registers); }}void init_IDT(void) { ... init_IDT_Descriptor(0x08, (uint32_t)ISR31, INT_GATE, &IDT[31]); init_IDT_Descriptor(0x08, (uint32_t)IRQ0, INT_GATE, &IDT[32]); init_IDT_Descriptor(0x08, (uint32_t)IRQ1, INT_GATE, &IDT[33]); init_IDT_Descriptor(0x08, (uint32_t)IRQ2, INT_GATE, &IDT[34]); init_IDT_Descriptor(0x08, (uint32_t)IRQ3, INT_GATE, &IDT[35]); init_IDT_Descriptor(0x08, (uint32_t)IRQ4, INT_GATE, &IDT[36]); init_IDT_Descriptor(0x08, (uint32_t)IRQ5, INT_GATE, &IDT[37]); init_IDT_Descriptor(0x08, (uint32_t)IRQ6, INT_GATE, &IDT[38]); init_IDT_Descriptor(0x08, (uint32_t)IRQ7, INT_GATE, &IDT[39]); init_IDT_Descriptor(0x08, (uint32_t)IRQ8, INT_GATE, &IDT[40]); init_IDT_Descriptor(0x08, (uint32_t)IRQ9, INT_GATE, &IDT[41]); init_IDT_Descriptor(0x08, (uint32_t)IRQ10, INT_GATE, &IDT[42]); init_IDT_Descriptor(0x08, (uint32_t)IRQ11, INT_GATE, &IDT[43]); init_IDT_Descriptor(0x08, (uint32_t)IRQ12, INT_GATE, &IDT[44]); init_IDT_Descriptor(0x08, (uint32_t)IRQ13, INT_GATE, &IDT[45]); init_IDT_Descriptor(0x08, (uint32_t)IRQ14, INT_GATE, &IDT[46]); init_IDT_Descriptor(0x08, (uint32_t)IRQ15, INT_GATE, &IDT[47]); // // 加载IDTR // __asm__ ("lidtl (IDTR)");}
; kernel/_Interrupt.asm...%macro IRQ 2[global IRQ%1]IRQ%1: cli push byte 0 push byte %2 jmp IRQ_COMMON_STUB%endmacroIRQ 0,32IRQ 1,33IRQ 2,34IRQ 3,35IRQ 4,36IRQ 5,37IRQ 6,38IRQ 7,39IRQ 8,40IRQ 9,41IRQ 10,42IRQ 11,43IRQ 12,44IRQ 13,45IRQ 14,46IRQ 15,47IRQ_COMMON_STUB: pusha mov ax,ds push eax mov ax,0x10 mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax push esp call IRQ_Handler add esp,4 pop ebx mov ds,bx mov es,bx mov fs,bx mov gs,bx mov ss,bx popa add esp,8 iret
除此之外,我们还需要一个_IO.asm,来操作IO口,笔者把常用的写出来了:
// include/io.h#ifndef _IO_H#define _IO_H#include #define cli() __asm__ ("cli\n\t"::)#define hlt() __asm__ ("hlt\n\t"::)#define sti() __asm__ ("sti\n\t"::)#define nop() __asm__ ("nop\n\t")extern void io_out8(uint32_t port, uint8_t data);extern void io_out16(uint32_t port, uint16_t data);extern void io_out32(uint32_t port, uint32_t data);extern uint8_t io_in8(uint32_t port);extern uint16_t io_in16(uint32_t port);extern uint32_t io_in32(uint32_t port);#endif
; kernel/_IO.asm[bits 32]global io_out8, io_out16, io_out32global io_in8, io_in16, io_in32;void io_out8(uint32_t port, uint8_t data);io_out8: mov edx,[esp+4] ;port mov al,[esp+8] ;data out dx,al ret;void io_out16(uint32_t port, uint16_t data);io_out16: mov edx,[esp+4] ;port mov ax,[esp+8] ;data out dx,ax ret;void io_out32(uint32_t port, uint32_t data);io_out32: mov edx,[esp+4] ;port mov eax,[esp+8] ;data out dx,eax ret;uint8_t io_in8(uint32_t port);io_in8: mov edx,[esp+4] ;port mov eax,0 in al,dx ret;uint16_t io_in16(uint32_t port);io_in16: mov edx,[esp+4] ;port mov eax,0 in ax,dx ret;uint32_t io_in32(uint32_t port);io_in32: mov edx,[esp+4] ;port in eax,dx ret
实现定时器
初始化完毕,当然是要测试啦,很多教材都是先测试定时器,我们也不例外,现在来注册我们第一个中断服务程序:// include/timer.h#ifndef __TIMER__#define __TIMER__#include #include void timer_callback(Registers_S *regs);void init_Timer(uint32_t frequency);#endif
// kernel/timer.c#include #include // 计时器回调函数void timer_callback(Registers_S *regs) { static uint32_t tick = 0; printk(KERN_EMERG "Tick: %d\n", tick++);}// 初始化计时器void init_Timer(uint32_t frequency) { RegisterInterrupt(IRQ_0, timer_callback); // Intel 8253/8254 每秒中断次数 uint32_t divisor = 1193180 / frequency; /* 0011 0110 时钟中断由Intel 8253/8254产生,因此不设置为0(8086模式) 端口地址范围是40h~43h */ io_out8(0x43, 0x36); io_out8(0x40, (uint8_t)(divisor & 0xFF)); // 设置低8位 io_out8(0x40, (uint8_t)((divisor >> 8) & 0xFF)); // 设置高8位}
至于测试,我们得注意8259A得在没有中断的情况下初始化:
/* File: start.c */#include #include #include #include #include ...int os_main(void) { init_VBE(); init_console(); init_8259A(); init_IDT(); show_logo(); asm volatile ("int $0x1"); init_Timer(200); asm volatile ("sti"); /* 进入死循环 */ while(true); return 0;}
实现时钟显示
现在,我们来尝试时钟显示,大家都知道,主板的时间来源于CMOS的RTC。这东西要在实模式下真就白给,现在我们就要弄点“手续”了, 这时就要讲CMOS了,这东西比8259A简单地多:用于 RTC 和 CMOS 的 2 个IO 端口为0x70 和 0x71。端口 0x70 用于指定索引或"寄存器编号",以及禁用 NMI。端口 0x71 用于读取或写入 CMOS 配置空间的该字节。只有三个字节的 CMOS RAM 用于控制 RTC 定期中断功能。它们称为 RTC 状态寄存器 A、B 和 C。它们在 CMOS RAM 中处于偏移 0xA、0xB 和 0xC。要将 0x20 写入状态寄存器 A我们需要初始化 RTC,初始化的时候,需要禁用 NMI(不可屏蔽中断)以防止初始化失败,不过RTC本来就已经是正常状态,只是官方这么要求而已:
void init_RTC() { cli(); // 关中断 io_out8(0x70, 0x8A); // 选择状态寄存器A,并禁用NMI(通过将0x80位置1) io_out8(0x71, 0x20); // 写入CMOS / RTC RAM io_out8(0x70, 0x8B); // 选择寄存器B,并禁用NMI char prev = io_in8(0x71); // 读取寄存器B的当前值 io_out8(0x70, 0x8B); // 再次设置索引(读取将重置索引以进行注册 io_out8(0x71, prev | 0x40); // 写入先前的值与0x40进行“或”运算。 这将打开第6位 sti(); // 开中断}
接下来就是读取RTC时间,这一段不是笔者写的,从一个OS开发文档直接拿的,这东西读取就得这么读,不过就是通过get_RTC_register来读取时间参数,然后进行一些数据的转换,类似linux下处理时间戳。
#define CURRENT_YEAR 2020#define century_register 0x00struct rtc_time OS_RTC_Time;int get_update_in_progress_flag() { io_out8(0x70, 0x0A); return (io_in8(0x71) & 0x80);}unsigned char get_RTC_register(int reg) { io_out8(0x70, reg); return io_in8(0x71);}void read_rtc() { unsigned char century = 20; unsigned char last_second; unsigned char last_minute; unsigned char last_hour; unsigned char last_day; unsigned char last_month; unsigned char last_year; unsigned char last_century; unsigned char registerB; while (get_update_in_progress_flag()); OS_RTC_Time.second = get_RTC_register(0x00); OS_RTC_Time.minute = get_RTC_register(0x02); OS_RTC_Time.hour = get_RTC_register(0x04); OS_RTC_Time.day = get_RTC_register(0x07); OS_RTC_Time.month = get_RTC_register(0x08); OS_RTC_Time.year = get_RTC_register(0x09); if(century_register != 0) { century = get_RTC_register(century_register); } do { last_second = OS_RTC_Time.second; last_minute = OS_RTC_Time.minute; last_hour = OS_RTC_Time.hour; last_day = OS_RTC_Time.day; last_month = OS_RTC_Time.month; last_year = OS_RTC_Time.year; last_century = century; while (get_update_in_progress_flag()); OS_RTC_Time.second = get_RTC_register(0x00); OS_RTC_Time.minute = get_RTC_register(0x02); OS_RTC_Time.hour = get_RTC_register(0x04); OS_RTC_Time.day = get_RTC_register(0x07); OS_RTC_Time.month = get_RTC_register(0x08); OS_RTC_Time.year = get_RTC_register(0x09); if(century_register != 0) { century = get_RTC_register(century_register); } } while( (last_second != OS_RTC_Time.second) || (last_minute != OS_RTC_Time.minute) || (last_hour != OS_RTC_Time.hour) || (last_day != OS_RTC_Time.day) || (last_month != OS_RTC_Time.month) || (last_year != OS_RTC_Time.year) || (last_century != century) ); registerB = get_RTC_register(0x0B); if (!(registerB & 0x04)) { OS_RTC_Time.second = (OS_RTC_Time.second & 0x0F) + ((OS_RTC_Time.second / 16) * 10); OS_RTC_Time.minute = (OS_RTC_Time.minute & 0x0F) + ((OS_RTC_Time.minute / 16) * 10); OS_RTC_Time.hour = ( (OS_RTC_Time.hour & 0x0F) + (((OS_RTC_Time.hour & 0x70) / 16) * 10) ) | (OS_RTC_Time.hour & 0x80); OS_RTC_Time.day = (OS_RTC_Time.day & 0x0F) + ((OS_RTC_Time.day / 16) * 10); OS_RTC_Time.month = (OS_RTC_Time.month & 0x0F) + ((OS_RTC_Time.month / 16) * 10); OS_RTC_Time.year = (OS_RTC_Time.year & 0x0F) + ((OS_RTC_Time.year / 16) * 10); if(century_register != 0) { century = (century & 0x0F) + ((century / 16) * 10); } } if (!(registerB & 0x02) && (OS_RTC_Time.hour & 0x80)) { OS_RTC_Time.hour = ((OS_RTC_Time.hour & 0x7F) + 12) % 24; } if(century_register != 0) { OS_RTC_Time.year += century * 100; } else { OS_RTC_Time.year += (CURRENT_YEAR / 100) * 100; if(OS_RTC_Time.year < CURRENT_YEAR) OS_RTC_Time.year += 100; }}
重新展示一下timer.h:
// include/timer.h#ifndef __TIMER__#define __TIMER__#include #include void timer_callback(Registers_S *regs);void init_Timer(uint32_t frequency);struct rtc_time { unsigned char second; unsigned char minute; unsigned char hour; unsigned char day; unsigned char month; unsigned int year;};extern struct rtc_time OS_RTC_Time;void init_RTC();int get_update_in_progress_flag();unsigned char get_RTC_register(int reg);void read_rtc();#endif
使用起来很简单:
int os_main(void) { init_VBE(); init_console(); init_8259A(); init_IDT(); show_logo(); init_RTC(); // init_Timer(200); asm volatile ("int $0x1"); asm volatile ("sti"); /* 进入死循环 */ while(true) { read_rtc(); // 中国地区小时要加8 printk("%d/%d/%d %d:%d:%d\n", OS_RTC_Time.year, OS_RTC_Time.month, OS_RTC_Time.day, OS_RTC_Time.hour + 8, OS_RTC_Time.minute, (int)OS_RTC_Time.second); } return 0;}
下期讲鼠标和整个OS对时钟频率的实际使用(这才是重点,今天的都是玩具),以及内存管理。
关注"GuEes"公众号,了解更多消息