操作系统真象还原_操作系统开发之——中断应用

5615af7b05ff01081ec102f8ff314d01.png

前一篇文章居然忘记放测试代码了:

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简介

外部结构

103e0730039d209cb6bf1dc1492f47bc.png

(图片来源于网络)

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 : 保留 默认情况下,优先级自上而下

这样看来还是时钟、鼠标、键盘中断服务程序好写。一会会涉及到这些东西。

内部结构

5a1d053a65dd46a12624ba8636161d1d.png

(来源:《操作系统真象还原》)

8259 可编程中断控制器有两组寄存器:初始化命令寄存器组,用来保存初始化命令字ICW (Initialization Command Word,共4个,ICW1~ICW4)和操作命令寄存器组,用来保存操作命令字OCW (Operation Command Word,共3个,OCW1~OCW3)。我们使用INOUT指令来访问这些寄存器。

8259A初始化

开篇说过了,8259A是可编程芯片,他的功能还不止这篇文章。所以8259A芯片是需要初始化才能使用也不奇怪,想要初始化它,只需要初始化一组ICW寄存器,分别写入主片和从片的ICW1 ~ ICW4,一定要按顺序。这里读者可以不用理解那么深,因为在本个系列结束前,都不会用到很多,而且要是想讲清除,这一篇文章是讲不完的:已经有读者提出文章过长了9d80750322fc39fcbd9b7f2946ea0654.png。接下来就是设置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;}

8fa7779471845c65458c30e777444d50.png

实现时钟显示

现在,我们来尝试时钟显示,大家都知道,主板的时间来源于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;}

222978af00dddc2f9615e4d7718d286c.png

下期讲鼠标和整个OS对时钟频率的实际使用(这才是重点,今天的都是玩具),以及内存管理。

关注"GuEes"公众号,了解更多消息

17cb74bd93f12e1602adfe5898170da6.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值