设备和输入输出
设备和计算机
- 在 nemu 和 npc 中, 程序的结束需要依靠运行的环境.
- 设备: 计算机与物理世界交互的桥梁, 用户通过设备来使用计算机
- 设备 = 电气部分 + 数字部分 (设备控制器)
- 设备控制器: 可以理解为将处理器发出的二进制命令翻译为电气信号的部件
- 一侧连接处理器, 接收来自处理器的命令
- 一侧连接电气部分, 向电气部分发送命令
- 主板和总线:
- 总线的表现形式: 电缆, 插槽, 主板走线, 引脚打线, 芯片内部绕线
- 北桥与南桥芯片: 进行转发
- 北桥: 连接高速设备, 如 DDR, 显卡, PCI-e
- 南桥: 连接低速设备, 如 BIOS, 磁盘, USB
现在由于芯片的集成度大幅提升, 可以在 CPU 中集成高速设备的控制器, 而对于低速设备, 使用 pcie 和 usb 协议就可以连接大部分的设备, 使得现在的使用南北桥芯片的设备越来越少了
- 芯片封装: 将芯片的端口信号引出来
设备模型
- 设备控制器中有什么?
- 数据缓冲寄存器 -> 数据交换
- 控制寄存器 -> 命令控制
- 状态寄存器 -> 状态检测
- 其他部件
- 对于 CPU, 寄存器就是设备功能的抽象, CPU 只需要访问设备寄存器即可控制设备工作, 无须关心其具体实现
设备寄存器的编址
- 独立编址 (port-mapped I/O, PIO): 内存和设备的地址空间不同, 需要新的 I/O 指令来访问设备
- 统一编址 (memory-mapped I/O, MMIO): 内存和设备的地址空间相同, 根据访存地址决定访问什么
- CPU 可以通过普通的访存指令来访问设备
- 将一部分物理内存的访问重定向到 I/O 地址空间中, CPU 尝试访问这部分物理内存的时候, 实际上最终是访问了相应的 I/O 设备, 而 CPU 却浑然不知
- 需要在访问设备时候通过
volatile
标识, 告诉编译器这个行为要严格执行[!tip] 为什么需要
volatile
标识 先来看看没有volatile
标识情况下编译出来的汇编代码 ```cdefine BUSY 0
void uart_putch (char c) { char status = (char *) 0x10000000ul; char *data = (char *) 0x10000004ul; while (status == BUSY); *data = c; }
以上代码编译出来的结果为:
assembly 00000000: 0: 100007b7 lui a5,0x10000 4: 0007c703 lbu a4,0 (a5) # 10000000 <.L5+0xffffff0> 8: 00071463 bnez a4,10 <.L5> 0000000c <.L4>: c: 0000006f j c <.L4> 00000010 <.L5>: 10: 00a78223 sb a0,4 (a5) 14: 00008067 ret 我们可以看到在 `.L4` 标志处是一个死循环, 这是经过了优化的结果, 因为一般来说内存中的某个位置<u></u>在此时是不会改变的, 而实际上, 这是一个访问设备的代码段, 当设备的状态机发生变化时候, 对应的内存位置上的数据是可能发生变化的, 而编译器并不知道这一点, 就对其进行了优化, 这样显然是不符合我们的预期的, 因此我们需要使用 `volatile` 关键字来避免编译器的优化, 告诉编译器这个访问要严格执行. 接下来我们将 `status` 和 `data` 都改为使用 `volatile` 关键字修饰的变量, 再来看看编译出的汇编代码.
assembly 00000000: 0: 10000737 lui a4,0x10000 00000004 <.L2>: 4: 00074783 lbu a5,0 (a4) # 10000000 <.L2+0xffffffc> 8: fe078ee3 beqz a5,4 <.L2> c: 00a70223 sb a0,4 (a4) 10: 00008067 ret `` 此时就可以看到, 每一次都需要重新从
0x10000000` 内存位置出取出最新的数据, 并且进行比较.
输入输出的状态机模型
- 执行普通指令, 按照 TRM 模型转移状态
- 执行设备输出指令:
- 除了更新 PC 外, 其他状态不变
- 设备状态和物理世界发生相应的改变
- 执行设备输入指令, 根据输入进行状态转移
- 取决于设备的输入
AM 中的 IOE
- 不同指令集访问设备的方式有所不同
- 设备型号不同
- 地址不同
常用设备
GPIO
可传输 1bit 的数据, 无须额外的状态和控制
串口
- 双向的字符传输