0°

使用Rust开发操作系统(可编程中断控制器(PIC)8259A)

可编程中断控制器PIC8259A

  • 可编程中断控制器(PIC)

  • 8259A PIC

    • 8259A编程
  • 8259A初始化

  • ICW1
    * ICW2
    * ICW3
    * ICW4

    
    
    1
    2
    1  * OCW
    2
  • OCW1

    
    
    1
    2
    3
    1  * OCW2
    2  * 结束中断
    3
  • 开始干活

  • 封装端口操作

    • 单个PIC
  • 把它们合在一起

    • 初始PIC
  • 下一步做什么

可编程中断控制器(PIC)

中断的产生有两种原因,一个是外部中断(由硬件产生的中断),另一个是由指令int n产生的中断,指令int n中n位向量号(IDT中定义),外部中断有些复杂些,因为需要建立硬件中断和向量号之间的对应关系,外部中断分为不可屏蔽中断(NMI)和可屏蔽中断两种,分别由CPU的两根引脚NMI和INTR来接受,Intel处理器只有一个外部中断引脚INTR,为了处理器能够同时接收多个硬件设备发送来的中断请求信号,因此将所有外部设备的中断请求汇总到中断控制器,由中断控制器处理后,由选择性的将中断请求一次发往外部中断引脚INTR

在多核处理器之前,8259A(PIC Programmable Interrupt Controller)是PC中最普遍的中断控制器,自多核处理器之后,8259A对多核的支持越来越差,随后出现APIC(Advanced Programmable Interrupt Controller,高级可编程中断寄存器),以及x2APIC(x2apic为Intel提供的xAPIC增强版,针对中断寻址、APIC寄存器访问进行改进优化)在本节中我们介绍PIC和APIC并编写PIC对应的结构(APIC会出一篇单独文章)

8259A PIC

通常情况下PC都会采用两片8259A芯片级联(级联指多个对象之间的映射关系,建立数据之间的级联关系提高管理效率),将外部硬件设备的中断请求与处理器的中断接收引脚关联起来
NMI&INTR

在两个8259A芯片级联过程中,一个8259A作为主芯片(主片),与CPU的INTR引脚相连,另一个8259A作为从芯片(从片)与主8259A的IR2引脚相连,其他中断请求引脚IR将外部设备的中断请求引脚相连,每个8259A有8根中断信号线,两片级联总共可以挂接15个不同的外部设备,我们可以通过对8259A的设置使得设备发出的中断请求与中断向量对应起来。
对8259A的设置主要是通过相应的端口写入特定的ICW(Initialization Command Word,初始化命令字)来实现的,主片对应的端口地址位0x20和0x21从片对应的端口地址为0xA0和0xA1

8259A编程

8259A可接受两种命令字:

  • ICW(初始化命令字):在执行普通操作之前,系统中的每个8259A必须通过2到4个字节的WR定时脉冲序列到达起始点

  • OCW(控制命令字):这些命令字完成8259A不同的模式切换,这些模式为

  • 完全嵌套模式(Fully nested mode)

    • 轮换优先模式(Rotating priority mode)
    • 特殊掩码模式(Special mask mode)
    • 轮询模式(Polled mode)

OCW可以在初始化后的任意时间写入

8259A初始化

8259A的初始化过程如下
在这里插入图片描述
总结一下初始化过程就是这样的:

  1. 往主片(0x20)或从片(0xA0)写入ICW1
  2. 往主片(0x21)或从片(0xA1)写入ICW2
  3. 往主片(0x21)或从片(0xA1)写入ICW3
  4. 往主片(0x21)或从片(0xA1)写入ICW4

这4步不能颠倒的
ICW1-ICW4的结构如下

ICW1

ICW1
8259A的ICW1都固定化为0001_0001B(0x11)

ICW2

ICW2

主片的中断向量号设置为0x20(IRQ0)具体对应关系如下
IRQ中断对应的向量号如下

IRQ0
0x20
IRQ1
0x21
IRQ2
0x22
IRQ3
0x23
IRQ4
0x24
IRQ5
0x25
IRQ6
0x26
IRQ7
0x27
IRQ8
0x28
IRQ9
0x29
IRQ10
0x2A
IRQ11
0x2B
IRQ12
0x2C
IRQ13
0x2D
IRQ14
0x2E
IRQ15
0x2F

ICW3

ICW3从片
ICW3

主片的ICW3用于记录各IR引脚与从片的级联状态,从片的ICW3用于记录与主片的级联状态,主片的ICW3的值应该设置为0x04从片的ICW3的值被设置为0x02

ICW4

ICW4
基本位功能如下

  • AEOI(自动EOI): 此模式可使中断控制器接收到CPU发来的第二个INTA中断脉冲后自动复位ISR寄存器的对应位
  • EOI模式:在EOI模式下,处理器执行完中断程序后,必须手动向中断控制器发送中断结束指令,来复位ISR寄存器的对应位
  • FNM(全嵌套模式):请求中断的优先级按引脚名从高到低依次为IRQ0-IRQ7,如果从片的中断请求正在被处理,那么从片将被主片屏蔽至处理结束,即使从片产生更高的优先级中断也不会被执行
  • SFNM(特殊全嵌套模式):基本与FNM相同,不同的地方为从片处理中断时,主片不会屏蔽从片,这样可以主片可以接受从片的更高优先级的中断请求,在中断程序返回前需要项从片发送EOI命令,并检测从片的ISR寄存器值,如果ISR寄存器仍有其他中断,则无需项主片发送EOI

通常情况下主/从片的ICW设置位0x01即可

我们可以看到ICW2的结构中涉及到中断向量号,这就是诀窍所在。

我们对8259A的初始化可以是先配置主片然后配置从片(先后式)或者是先配置主片/从片的ICW1然后在配置设置主片/从片的ICW2以此类推(交替式)。

我们可以使用汇编来模拟一下该过程(我们采用交替式


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1INIT8259A:
2   ;============ ICW1
3   mov al, 0x11 ; 初始化字段
4   out 0x20,al  ; 主片 ICW1
5   call delay
6   out 0xA0,al ; 从片 ICW1
7   call delay
8   ;=========== ICW2
9   mov al,0x20 ; IRQ0所对应的中断向量0x20
10  out 0x21, al ; 主片 ICW2
11  call delay
12  mov al, 0x28 ; IRQ8所对应的中断向量0x28
13  out 0xA1, al ; 从片 ICW2
14  call delay
15  ;===========ICW3
16  mov al, 0x04 ; IR 2对应的从片
17  out 0x21, al
18  call delay
19  mov al, 0x02 ; 对应主片的IR2
20  out 0xA1, al
21  call delay
22  ;===========ICW4
23  mov al,0x01
24  out 0x21,al ; 主片 ICW4
25  call delay
26  out 0xA1,al ;从片 ICW4
27  call delay
28  ret
29
30

主/从片ICW初始化数据如下


ICW1
0x20
0x11

ICW2
0x21
0x20

ICW3
0x21
0x04

ICW4
0x21
0x01

ICW1
0xA0
0x11

ICW2
0xA1
0x28

iCW3
0xA1
0x02

ICW4
0xA1
0x01

OCW

通常只在两种情况下用到它

  • 屏蔽或打开外部中断
  • 发送EOI给8259A以通知中断处理结束

OCW的结构如下

OCW1

OCW1
尽量屏蔽不使用的中断请求引脚,以防止不必要扥中断请求,导致中断请求拥堵

OCW2

OCW2

对于OCW的第5-7位可以组合多种模式,如下

0
0
0
循环AEOI模式(清除)
0
0
1
非特殊EOI命令(全嵌套方式)
0
1
0

0
1
1
特殊EOI命令(非全嵌套模式)
1
0
0
循环AEOI模式(设置)
1
0
1
循环非特殊EOI命令
1
1
0
设置优先级命令
1
1
1
循环特殊EOI命令

我们根据开头的图片可以知道,时钟中断位于主片的IRQ0,因此我们可以把第0位置0打开即可(1表示屏蔽中断,0表示接受中断),实际上OCW1被写入了中断屏蔽寄存器IMR(Interrupt Mask Register)中,当一个中断到达,IMR会判断此中断是否被应被丢弃

开打时钟中断


1
2
3
4
5
6
7
8
9
10
1; ....初始化后
2mov al,11111110b ; 仅仅开启主片的IRQ0中断(时钟中断)
3out 0x21,al ; 主片 OCW1
4call delay
5
6mov al,11111111b ; 屏蔽从片的所有中断
7out 0xA1,al
8call delay
9
10

结束中断

如果8259A芯片采用AEOI(Automatic End of interrupt,自动结束中断)方式,那么它会在第二个INTA脉冲信号的结尾处复位正在服务的ISR对应为,如果8259A采用非自动中断结束方式,那么CPU必须在中断处理程序结尾向8259A芯片发送EOI(End Of Interrupt,结束中断)命令来复位ISR对应位,如果中断请求组来自级联8259A芯片,则必须向两个芯片都发送EOI命令

使用汇编发送EOI命令例子如下


1
2
3
4
5
6
7
1mov al,0x20
2out 0x20, al
3; 或
4mov al,0x20
5out 0xA0, al
6
7

好了了解完这些内容后可以着手编写我们的代码了

开始干活

我们开始设计一下需要用到那些结构

第一,我们需要使用out 和in指令完成端口的写入和数据读取操作,因此我们需要提供基本的写入读取函数

第二,我们对端口操作进行抽象,分为只读,读写,和可读可写操作,我们分别设计PortRead,PortWrite,PortReadWrite这几个trait

第三,我们需要设计Pic的基本结构,以及ChainedPics结构对应的是主片和从片

设计好后我们开始编写第一步
因为Rust内嵌汇编默认使用的是AT&T语法,而使用的例子是Intel语法,但是基本的内容保持不变的只不过AT&T语法多了几个字母罢了

封装端口操作

首先我们在system/src/ia_32e/instrcutions/port.rs文件中添加以下内容


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1// ------------------- byte 8 bits -------------------------------------
2pub unsafe fn inb(port: u16) -> u8 {
3    let result: u8;
4    asm!("inb %dx, %al" : "={al}"(result) : "{dx}"(port) :: "volatile");
5    result
6}
7
8pub unsafe fn outb(value: u8, port: u16) {
9    asm!("outb %al, %dx" :: "{dx}"(port), "{al}"(value) :: "volatile");
10}
11// ------------------- 2byte 16bits -------------------------------------
12pub unsafe fn inw(port: u16) -> u16 {
13    let result: u16;
14    asm!("inw %dx, %ax" : "={ax}"(result) : "{dx}"(port) :: "volatile");
15    result
16}
17
18pub unsafe fn outw(value: u16, port: u16) {
19    asm!("outw %ax, %dx" :: "{dx}"(port), "{ax}"(value) :: "volatile");
20}
21
22// ------------------- 4byte 32bits -------------------------------------
23pub unsafe fn inl(port: u16) -> u32 {
24    let result: u32;
25    asm!("inl %dx, %eax" : "={eax}"(result) : "{dx}"(port) :: "volatile");
26    result
27}
28
29pub unsafe fn outl(value: u32, port: u16) {
30    asm!("outl %eax, %dx" :: "{dx}"(port), "{eax}"(value) :: "volatile");
31}
32
33

其中的b,w,l对应的是8位,16位,32位,AT&T语法与Intel的语法相反(反正就是不和:-D)

编写完毕后我们在system/src/ia_32e中添加新的模块cpu
在system/src/ia_32e/cpu/port.rs文件中添加以下几个trait


1
2
3
4
5
6
7
8
9
10
11
12
1// system/src/ia_32e/cpu/port.rs
2pub trait PortRead {
3    unsafe fn read(port: u16) -> Self;
4}
5
6pub trait PortWrite {
7    unsafe fn write(port: u16, value: Self);
8}
9
10pub trait PortReadWrite: PortRead + PortWrite {}
11
12

我们的端口一直是16位的,但是写入数值可能时8位,16位或32位,我们需要为这几个类型实现这几个trait

注意我们到现在编写的整数类型都是无符号的!


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
1// system/src/ia_32e/cpu/port.rs
2#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
3pub use crate::ia_32e::instructions::port::{inb, outb, inw, outw, inl, outl};
4// ---------------------- u8 ---------------------
5impl PortRead for u8 {
6    unsafe fn read(port: u16) -> Self {
7        inb(port)
8    }
9}
10
11impl PortWrite for u8 {
12    unsafe fn write(port: u16, value: Self) {
13        outb(value, port);
14    }
15}
16
17impl PortReadWrite for u8 {}
18
19// ---------------------- u16 ---------------------
20impl PortWrite for u16 {
21    unsafe fn write(port: u16, value: Self) {
22        outw(value, port);
23    }
24}
25
26impl PortRead for u16 {
27    unsafe fn read(port: u16) -> Self {
28        inw(port)
29    }
30}
31
32impl PortReadWrite for u16 {}
33// ---------------------- u32 ---------------------
34
35impl PortRead for u32 {
36    unsafe fn read(port: u16) -> Self {
37        inl(port)
38    }
39}
40
41impl PortWrite for u32 {
42    unsafe fn write(port: u16, value: Self) {
43        outl(value, port);
44    }
45}
46
47impl PortReadWrite for u32 {}
48
49

结构比较简单,就不在赘述了最后我们定义一个UnsafePort(主要方法都是unsafe的)结构,用于端口的写入读取操作,实现的是PortReadWrite trait


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1// system/src/ia_32e/cpu/port.rs
2use core::marker::PhantomData;
3
4#[derive(Debug)]
5pub struct UnsafePort<T> {
6    port: u16,
7    phantom: PhantomData<T>,
8}
9impl<T: PortReadWrite> UnsafePort<T> {
10    pub const unsafe fn new(port: u16) -> UnsafePort<T> {
11        UnsafePort { port, phantom: PhantomData }
12    }
13
14    pub unsafe fn write(&mut self, value: T) {
15        T::write(self.port, value);
16    }
17
18    pub unsafe fn read(&mut self) -> T {
19        T::read(self.port)
20    }
21}
22
23

单个PIC

好了我们的端口结构定义完毕,现在开始编写Pic结构如下


1
2
3
4
5
6
7
8
9
1// system/src/ia_32e/cpu/pic.rs
2#[derive(Debug)]
3struct Pic {
4    offset: u8,
5    command: UnsafePort<u8>,
6    data: UnsafePort<u8>,
7}
8
9

在Pic中offset表示中断向量号一般对应着IRQ0(0x20主片)和IRQ8(0x28从片),
command字段表示0x20端口(主片)0xA0(从片),一般用于控制操作,data表示0x21(主片)和0xA1(从片),一般用于数据写入读取操作,
我们开始实现Pic的功能,主要的2个功能是判断当前的芯片能否处理当前的中断向量,每个芯片可接受的范围是0-7共8个(主:IRQ0-IRQ7,从:IRQ8-IRQ15),第二个功能是发送EOI命令的功能,只需要向对应的端口写入0x20即可


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1// system/src/ia_32e/cpu/pic.rs
2impl Pic {
3    /// 判断中断向量是否可接受的范围中
4    /// IRQ0-IRQ7 共8个
5    /// IRQ8 -IRQ15 共8个
6    fn handle_interrupt(&self, interrupt_id: u8) -> bool {
7        self.offset <= interrupt_id && interrupt_id < self.offset + 8
8    }
9    /// 向对应端口写入EOI命令完成中断
10    unsafe fn end_interrupt(&mut self) {
11        self.command.write(EOI);
12    }
13}
14
15

把它们合在一起

单个芯片的结构定义完毕,我们编写一个ChainedPics结构表示组合起来,结构如下


1
2
3
4
5
6
7
8
1// system/src/ia_32e/cpu/pic.rs
2#[derive(Debug)]
3pub struct ChainedPics {
4    main: Pic,
5    slave: Pic,
6}
7
8

初始化函数和中断向量范围判断函数如下


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1// system/src/ia_32e/cpu/pic.rs
2impl ChainedPics {
3    pub const unsafe fn new(offset_1: u8, offset_2: u8) -> ChainedPics {
4        ChainedPics {
5            main: Pic {
6                offset: offset_1,
7                command: UnsafePort::new(0x20),
8                data: UnsafePort::new(0x21),
9            },
10            slave: Pic {
11                offset: offset_2,
12                command: UnsafePort::new(0xA0),
13                data: UnsafePort::new(0xA1),
14            },
15        }
16    }
17    pub fn handles_interrupt(&self, interrupt_id: u8) -> bool {
18        self.main.handle_interrupt(interrupt_id) || self.slave.handle_interrupt(interrupt_id)
19    }
20}
21
22

初始PIC

还记得怎么初始化PIC嘛,我们需要写入ICW1,ICW2,ICW3,ICW4等每次写入后需要一个短暂延迟,这里的延迟的实现是通过将垃圾数据写入端口0x80,各种较旧版本的Linux和其他PC操作系统已通过在端口0x80上写入垃圾数据来解决此问题或者可以使用几个nop指令来完成


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
1// system/src/ia_32e/cpu/pic.rs
2impl ChainedPics {
3   pub unsafe fn initialize(&mut self) {
4      
5        let mut wait_port: Port<u32> = Port::new(0x80);
6        // wait是一个闭包 我们只需要往0x80端口写一些数据即可
7        let mut wait = || { wait_port.write(0) };
8
9        // 写入之前保存掩码
10        let saved_mask1 = self.main.data.read();
11        let saved_mask2 = self.slave.data.read();
12        // 主片写入ICW1
13        self.main.command.write(ICW1);
14        wait();
15        // 从片写入ICW1
16        self.slave.command.write(ICW1);
17        wait();
18
19        // 主片写入ICW2
20        self.main.data.write(self.main.offset);
21        wait();
22        // 从片写入ICW2
23        self.slave.data.write(self.slave.offset);
24        wait();
25
26        // 主片写入ICW3
27        self.main.data.write(ICW3_M);
28        wait();
29        // 从片写入ICW3
30        self.slave.data.write(ICW3_S);
31        wait();
32
33        // 主片写入ICW4
34        self.main.data.write(ICW4);
35        wait();
36        // 从片写入ICW4
37        self.slave.data.write(ICW4);
38        wait();
39
40        // 恢复掩码
41        self.main.data.write(saved_mask1);
42        self.slave.data.write(saved_mask2);
43    }
44}
45
46

最终我们提供一个统一的notify_end_of_interrupt函数来完成中断


1
2
3
4
5
6
7
8
9
10
11
12
13
1/// 8259A采用非自动中断结束方式,
2/// 那么CPU必须在中断处理程序结尾向8259A芯片发送EOI(End Of Interrupt,结束中断)命令
3/// 来复位ISR对应位,如果中断请求来自级联8259A芯片,则必须向两个芯片都发送EOI命令
4pub unsafe fn notify_end_of_interrupt(&mut self, interrupt_id: u8) {
5        if self.handles_interrupt(interrupt_id) {
6            if self.slave.handle_interrupt(interrupt_id) {
7                self.slave.end_interrupt();
8            }
9            self.main.end_interrupt();
10        }
11    }
12
13

如果中断请求来自级联8259A芯片我们需要向两个芯片都发送EOI命令

到此为止我们的PIC中断控制器的结构就编写好了

下一步做什么

在下一篇文章中我们开始使用我们编写的GDT,TSS,IDT,和PIC,为我们的操作系统加载GDT,TSS,IDT并提供中断处理功能

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!