输入/输出设备

输入输出设备(I/O Devices)通常有一些模拟元件或非电子元件(比如磁盘驱动器),但与CPU的接口通常是数字逻辑——一组寄存器。

对于IO编程支持,有两种办法:IO指令和内存映射IO:

  • I/O指令为I/O设备提供了单独的地址空间
  • 内存映射I/O是最普遍的方法,即使是提供I/O指令的CPU也不排除使用内存映射I/O。
    • 内存映射I/O是为每个I/O设备的寄存器提供一个地址。
    • 程序使用普通的CPU读写内存命令来与设备通信。

如: 在C语言中定义和使用一个变量时,编译器隐藏了变量地址,可以使用指针来操纵I/O设备的地址。

// 读取任意存储单元的函数peek
int peek(char *location) {
return *location;
}
#define DEV1 0x1000
dev_status = peek(DEV1); /*读取设备寄存器*/

// 写入任意存储单元的函数poke
void poke(char *location, char newval) {
(*location) = newval;
}
poke(DEV1, 8); /*写入设备寄存器*/

数据传送方式

CPU与IO设备间的数据传送方式

  • 程序传送方式
    • 无条件传送
    • 程序查询传送(忙等I/O)
  • 中断传送方式
  • DMA传送方式

忙等I/O

忙等I/O是程序中访问I/O设备的最基本方法。I/O设备一般比CPU慢,需要多个时钟周期来完成一项操作,通过读取I/O设备的状态寄存器来询问I/O设备是否空闲,这样的操作通常被称为轮询(polling)。

然而,忙等I/O的效率非常低:

  • I/O事务未完成时,CPU除测试设备状态什么都不能做。
  • 无法将其他操作与I/O事务并行执行。

因此,我们使用中断机制来提高效率。

中断I/O

中断最初是作为处理器与外部设备交换信息的一种控制方式提出的。由此,最初的中断全部是对外部设备而言的,称为外部中断或硬件中断。随着计算机技术的发展,中断的范围也随之扩大,出现了内部软件中断的概念,它是为解决处理器内部运行时出现的异常以及为编程方便而提出的。

外部中断或硬件中断通常称为中断(Interrupt),软件中断或异常中断通常称为异常(Exception)。

中断机制允许I/O设备发送信号到CPU,并强制执行一段特定代码。它使得CPU能够在不同的上下文之间切换(例如多个I/O设备之间)。

中断发生时,PC指向I/O设备中一个管理设备的中断处理子程序(中断服务程序或设备驱动程序)。中断机制保留了系统被中断时的PC值,以便CPU能够返回到被中断的程序。

中断处理过程

不论哪种中断都遵循同样的中断处理过程。收到中断请求之后,处理器暂停执行当前程序,转而执行中断服务程序,处理完毕后再返回到程序断点处,继续执行原来的程序。

// 输入输出处理程序
void input_handler() {
achar = peek(IN_DATA);
gotchar = TRUE;
poke(IN_STATUS,0);
}
void output_handler() {
}

// 主程序
main() {
while (TRUE) {
if (gotchar) {
poke(OUT_DATA,achar);
poke(OUT_STATUS,1);
while(
peek(OUT_STATUS)!=0);
gotchar = FALSE;
}
}
}

缓冲区

缓冲区示意图
// 弹性缓冲区
#define BUF_SIZE 8
char io_buf[BUF_SIZE];
int buf_head = 0, buf_tail = 0;
int error = 0;
bool empty_buffer() {retun buf_head == buf_tail;}

bool full_buffer() {
return (buf_tail+1)%BUF_SIZE == buf_head;
}

int nchars() {
if(buf_tail>=buf_head) return buf_tail-buf_head;
else return BUF_SIZE + buf_tail – buf_head;
}

void add_char(char achar) {
io_buf[buf_tail++] = achar;
if(buf_tail == BUF_SIZE)
buf_tail = 0;
}

char remove_char() {
char achar;
achar = io_buf[buf_head++];
if(buf_head == BUF_SIZE)
buf_head = 0;
}
// 输入处理程序的代码
#define IN_DATA 0x1000
#define IN_STATUS 0x1001
void input_handler() {
char achar;
if (full_buffer()){
error = 1; //出错
}
else {
achar = peek(IN_DATA);
add_char(achar);
}

poke(IN_STATUS,0);
if (nchars()== 1) {
poke(OUT_DATA,remove_char());
poke(OUT_STATUS,1);
}
}

// 输出处理程序的代码
#define OUT_DATA 0x1100
#define OUT_STATUS 0x1101
void output_handler() {
if(!empty_buffer())
poke(OUT_DATA, remove_char());
poke(OUT_STATUS, 1);
}
  1. BUF_SIZE 是缓冲区的大小,io_buf 是缓冲区本身,buf_headbuf_tail 分别是缓冲区的头部和尾部指针。
  2. empty_buffer() 函数检查缓冲区是否为空,即头部和尾部指针是否相等。
  3. full_buffer() 函数检查缓冲区是否已满,即尾部指针的下一个位置是否是头部指针。
  4. nchars() 函数返回缓冲区中的字符数量。
  5. add_char(char achar) 函数将一个字符添加到缓冲区的尾部,然后更新尾部指针。
  6. remove_char() 函数从缓冲区的头部移除一个字符,然后更新头部指针。
  7. input_handler() 是输入中断处理程序。当缓冲区未满时,它从输入设备读取一个字符并添加到缓冲区。如果缓冲区已满,它将设置错误标志。如果这是缓冲区中的第一个字符,它将触发输出设备。
  8. output_handler() 是输出中断处理程序。当缓冲区不为空时,它从缓冲区移除一个字符并发送到输出设备。

中断与调试

在中断处理程序中,任何一个寄存器在被改写前必须保存原来的值,并且在中断处理程序结束前恢复其原来的值。由中断处理程序造成的前台程序出错很难调试,因为错误的形式取决于中断发生的时间,不具有重复性。

因此,在调试程序时,我们可以选择使用忙等驱动I/O而非中断驱动I/O。

中断的优先级

中断之间,亦有差距。如何让电脑正确区分这些中断呢?有两种解决方法:

  • 中断优先级(interrupt priority):能让CPU辨认出比其他中断更加重要的一些中断。
  • 中断向量(interrupt vector):允许中断设备指定其中断处理程序。

屏蔽(masking):优先级机制必须确保处理高优先级中断时,不发生低优先级中断。换句话说,低优先级的中断会被屏蔽掉。

不可屏蔽中断(non-maskable interrupt, NMI):NMI不能断开,具有最高的优先级。它通常是为由电源故障引发的中断准备的,是一个可以监测危险低电压的简单电路。

中断优先级队列

中断向量

有时候我们会更在乎中断的处理方法而非其本身的重要性。因此,我们引入了中断向量的概念:

在设备的请求或应答之后,通过中断向量线将中断向量发送到CPU,CPU将向量号作为存储在内存中的中断向量表的索引,设备端存储自己的向量号,提供灵活性。

说白了就是用不同的向量代表不同的处理方法。中断向量可以提供中断程序服务的入口地址。

中断机制处理流程,注意中断优先级处理先于中断向量

小练习:下面关于中断的叙述,哪个是不正确的?

  • A、一旦有中断请求出现,CPU立即停止当前指令的执行转而去受理中断请求。
  • B、CPU响应中断时暂停运行当前程序,自动转移到中断服务程序。
  • C、中断方式一般适用于随机事务的出现。
  • D、为了保证中断服务程序执行完毕后,能正确返回到被中断的断点继续执行程序,必须进行现场保存操作。
答案

A。显然只能选A吧?因为有优先级的存在。

管态与陷阱

特权模式又称作管态。 复杂的系统经常由几个相互通信的程序来实现。我们希望能够提供硬件检查来确保程序之间没有互相干扰。可以使用CPU提供的管态来避免内存误操作,MMU控制通常只留给管态使用。 但很多DSP都没有管态,通常也没有MMU。

陷阱(trap),也称为软件中断(software interrupt),是一种显式产生异常状态的指令。

  • 陷阱最通常的用法是进入管态。
  • ARM为软件中断提供了SWI指令:SWI CODE_1,这个指令将使CPU进入管态。
  • 一个能被处理程序读取的操作码CODE_1嵌入到这条指令中。

异常与中断

异常(exception) 是一种内部可以检测到的错误。通常可以将异常当做中断的一种变体。

异常必须有优先级,因为一个操作可能产生不只一个异常。异常的优先级一般由CPU体系结构决定。

中断是异步事件,异常是同步事件!

  • 中断是异步事件
    • 可能随时发生,与处理器正在执行的内容无关。
    • 中断主要由I/O设备产生,可以被启用或禁止。
  • 异常是同步事件
    • 它是某一特定指令执行的结果,在相同条件下,异常可以重现。
    • 内存访问错误、被零除等是典型的异常。

功耗

CMOS VLSI功耗的计算公式:

Ptotal=Pdynamic+PstaticPdynamic=λCVDD2fP_{total}=P_{dynamic}+P_{static}\\ P_{dynamic}=\lambda C{V_{DD}}^2f

其中λ为电平翻转比率。

节省功耗的办法

  • 降低供电电压
  • 以较低的时钟频率运行
    • 降低功率消耗而不是能量消耗
  • 通过控制线关闭某些当前执行不需要的功能单元
    • 时钟门控技术(clock gating)
  • 允许部分CPU完全与电源断开以消除泄漏电流
    • 电源门控技术(power gating)

下一章就该进入MIPS的学习了……好难啊……