Basic I/O Interface¶
约 9560 个字 141 行代码 预计阅读时间 50 分钟
Hardware Specifications¶
chapter 9
Intel 8086 和 8088 硬件都包装在 40-pin dual in-line packages(DIPs)中,区别在于 8086 的 Data Bus 宽度为 16-bit(\(AD_0 - AD_{15}\)),8088 的 Data Bus 宽度为 8-bit(\(AD_0- AD_7\))。
因此 8086 能够更有效率地传输 16-bit 数据,接下来我们也都以 8086 为例
8086 可以运行在以下两种模式,两种模式下不同引脚功能不同,通过 33 pin 口 \(MN/\overline{MX}\) 指示,其中低电平表示 Maximum:
- Minimum Mode: 最简单、开销最小的模式
- 所有 Memory 和 I/O 操作的控制信号都由处理器生成
- Maximum Mode:
- 允许系统使用外部的 coprocessor,例如 8087(floating-point coprocessor)
- 一些控制信号只能在外部生成,需要外部数据总线控制
此处我们简要介绍一下部分输入引脚的功能:
- Pin Connections \(AD_{15} - AD_0\)
- 当引脚 ALE 为 0 时,表示该总线上为数据信号
- 当引脚 ALE 为 1 时,表示该总线上为地址信号,包括 Memory Address 和 I/O Port Number
- 具体是内存还是 I/O 由另一个引脚的值进行判断
- 另外注意到作为地址信号使用时,右侧还有 \(A_{16}- A_{19}\)
- Address Latch Enable \(ALE\):只在 Minimum Mode 下有效
- \(M/\overline{IO}\):为 0 时表示总线上为 I/O;为 1 时表示总线上为 Memory
- 在 8088 中正好相反,为 \(IO /\overline {M}\)
- Bus High Enable \(BHE\):指示数据总线上高字节是否有效
- 相对有一个引脚 \(BLE\),但该信号不需要额外产生,默认等于 \(A_0\)
- Read Signal \(\overline{RD}\):是否允许数据总线从 Memory 或 I/O 设备中读取数据
- Write Line \(\overline{WR}\):指示是否正在向 Memory 或 I/O 设备中输出数据
- Interrupt Request \(INTR\):如果 INTR 在 IF = 1 时保持高位,8086/8088 将在当前指令完成执行后进入中断确认循环。
- Non-Maskable Interrupt \(NMI\):与 INTR 类似,但是不需要检查 IF,即不可屏蔽中断
Intro to I/O Interface¶
硬件接口可以根据如下三种依据进行划分:
- Directions of information flow
- Input Interface
- Output Interface
- Types of signal
- Analog Interface
- Digital Interface
- Types of data transmission
- Serial Interface
- Parallel Interface
- 这里的串行表示按位传输数据,并行表示按大于位(比如字节)传输数据
在设计接口的过程中,我们尝试明确以下问题:
- <1> 怎么将 I/O 设备连接到 CPU?
- 对于输出接口,使用 Latches
- 对于输入接口,使用 Three State Buffers
- <2> 怎么为 I/O 设备提供 Address Space?
- 有两种 Schemes:Isolated I/O 和 Memory Mapped I/O
- <3> 怎么执行 I/O Port Decoding?
- 这一过程需要涉及信号 Memory Address, BHE, BLE, IORC, IOWC
- <4> 怎么实现处理器和 I/O 设备的同步?
- unconditional transfer, strobing, handshaking, polling, interrupt-driven, channel-based(e.g. DMA)...
Isolated I/O 使用 IN 和 OUT 指令来做数据的传输,这里的 isolated 意指 I/O 地址空间是和内存地址空间隔离的,称为 port。
- Advantage
- port 是隔离的,不需要为设备占用 Memory Address
- Disadvantage
- 只能通过
IN和OUT指令进行数据交换 - 需要对 I/O Space 额外产生控制信号
- 需要特定的 I/O Instruction,导致编程更加复杂
- 只能通过
而 Memory-Mapped I/O 允许任意指令进行数据交换,I/O 设备将共享内存空间:
- Advantage
- I/O 操作速度与访存操作等价
- 简化了编程难度
- Disadvantage
- 占用了大量 Memory Address,并且可能空间不够寻址所有 I/O 设备
- 如果一个设备响应时间很慢,它会拖累 CPU 访问内存的时间,导致整体性能下降
PC 使用 Isolated I/O,而不使用 Memory-Mapped I/O
在硬件上,TTL 和 CMOS 是最常见的两种逻辑管,其中 CMOS 由于消耗更少能量、速度更快而得到了更广泛的应用。对于它们的输入和输出的逻辑电平,有如下规定标准:
总结得到下表:
上表下方有两个例子,其中左例 TTL 的输出电平不满足 CMOS 的输入电平要求,即 \(VOHmin > VIHmin, VOLmax < VILmax\)。
对于输入设备,我们只需要保证电平匹配、信号稳定即可得到对应的逻辑信号,通常使用三态门作为中介;而输出设备还需要保证电流大小足够驱动输出设备。
实际控制中,我们要通过设置合理的电阻值来获得额定电流,我们以一个 LED 灯为例:
已知该三极管基极和集电极的电流大小比例为 1:100。对于 Logic 1,TTL 提供的输出电压幅度在 2.4V - 5V 范围内,因此我们要保证对于 2.4V 的输入也能驱动该 LED 灯,因此对基极和集电极的电阻设置计算如上。
17K 不是标准电阻,因此最终选用相近的 18K 电阻(为什么可以选用更大的电阻?)
更一般的感性负载电机设备中存在很多电感,在我们断电瞬间会产生反向电动势。如果不对该反向电动势进行处理,高额电压可能会损坏三极管。为此,我们需要在三极管两端增加稳压管:
该电机的额定电流为 1A,电阻的具体计算方式见图上
对于输出设备,输出的信号还需要进行锁存,因为输出的信号是持续性的。控制信号此时不再接入使能信号,而是接入时钟信号;当我们使用指令 OUT 38H, AL 时,时钟信号就会发生一次跳变,将我们输出信号进行锁存保存。
对于打印机等更“智能”的设备,我们还要求 CPU 能够和外部设备进行异步数据传输,常见有如下两种方法:
- Strobing 选通,单向控制信号
- Handshaking 握手,双向控制信号
Strobing
Strobing 分为 source-initiated transfer 和 destination-initiated transfer 两种:
选通方式好处在于简单,坏处在于难以控制连接时间,源并不知道目标何时接收完数据。
Handshaking
为了更好地控制时序,握手方式引入了第二条控制信号。
现在稍微复杂点的设备都采用双向信号,两条控制线分别用于 Request 和 Reply。
如下例子展示了 PC 和一个 Printer 之间的数据交换过程,ASCII 数据被放置在 \(D_7- D_0\) 中,向 port PRINTER 发送数据就相当于 request;打印机收到数据后设置 port BUSY 为 1 就相当于 reply:
因此轮询 Polling 也是双向握手
I/O Port Address Decoding¶
往年没考,今年必考
其中最重要的是左侧部分对 I/O Port 地址的译码。CPU 的 I/O 指令可以使用 8-bit 地址,范围为 00H-FFH,所以这种指令的地址线只会用到 \(A_7 -A_0\)。
不过实际上,PC 实际 I/O 端口都是 16-bit 地址(0-FFFFH),我们通过寄存器 DX 指定端口,理论上会对 \(A_{15}- A_0\) 完整解码。
以 8-bit 地址为例,其中高五位用于选择译码器(译码器使能),低三位用于译码输入,相当于一个 3-to-8 Decoder,其结构如本节开始的图中左侧部分所示。
我们的数据总线通常为 16-bit,但往往只有 8-bit 有效。在 16-bit 微处理器(如 80386SX)里:
- 内存是 16-bit data bus → 由 两个 8-bit memory bank 组成 (High bank + Low bank)
- I/O 空间也是一样! → 由 两个 8-bit I/O bank 组成:
| Bank | 数据线 | 地址例子 |
|---|---|---|
| 低字节 (Low bank) | D7–D0 | 偶数端口 (40h) |
| 高字节 (High bank) | D15–D8 | 奇数端口 (41h) |
\(\overline{BHE}\) 和 \(\overline{BLE}\) 是两个写选通信号,其中 \(\overline {BLE}\) 直接设置为 \(A_0\),对 I/O 设备进行一个 8 位写入必须明确它要写哪一半。而 8-bit I/O Read 不需要这两个信号,CPU 可以直接通过地址最低位 \(A_0\) 来选择读取哪个字节:
- \(\overline{BHE}\) (Bus High Enable): 当该信号为低电平(0)时,表示启用高位数据总线 (\(D_{15}-D_8\))。
- \(A_0\) (Address line 0): 当该信号为低电平(0)时,表示启用低位数据总线 (\(D_7-D_0\))。
- 除了 \(A_0\) 以外的地址总线位 \(A_{19} - A_1\) 就用来从 odd/even bank 中选取对应的单字节
为什么 8-bit 写入需要两个信号?
因为数据通道和地址通道都是 16-bit 的,即 \(AD_{15}- AD_0\),而“写”这一操作具有破坏性,因此不能只靠低位 \(A_0\) 的奇偶来判断是高位还是低位。
Example
- 当访问地址
41H时,译码器的输入为 \(\overline{BHE}=0, \overline{IOWC}=0, A_0=1\) - 当访问地址
40H时,译码器的输入为 \(\overline{BHE}=1, \overline{IOWC}=0, A_0=0\) - 但对于 16L8 这种可编程硬件,\(O_1 -O_8\) 的输出并不一定是根据 3-to-8 译码器那样直接,只需要知道对于该图如果 \(\overline{BHE}\) 置 0 则访问 high bank 即可
Programmable Peripheral¶
本节介绍三个经典的外部可编程器件。
82C55¶
芯片 82C55 提供了可编程接口:
- 它提供 24 条可编程 I/O 引脚,其中
82表示是外部可编程器件,C表示是 CMOS 低功耗版本,55是流水号,通常越大越新 - 分成 A、B、C 三个端口,每个端口都是 8-bit,其中 A、B 口用于数据传输,C 口用于控制信号
- 三个端口资源不平等,其中 Port A 和 Port C 都有 Output Latch/Buffer 以及 Input Buffer,而 Port B 只有 Output/Input Shared Latch/Buffer,因此只有 Group A 可以同时输入输出(运行在 Mode 2 下)
- 实际处理中将三个端口分为两个 Group 设置:
- Group A:Port A(
PA7-PA0) & Half Port C(PC7-PC4) - Group B:Port B(
PB7-PB0) & Half Port C(PC3-PC0)
- 可以设置为 不同模式(Mode 0/1/2)
- Mode 0:Basic I/O Operation(for group A & B)允许设置端口是输入还是输出,但是同时只能做一件事
- Mode 1:Strobe I/O Operation(for group A & B)新增双向选通信号,考虑外部设备状态
- Mode 2:Bidirectional Bus Operation(for group A only)允许同时输入输出
- 即只有 Group A 允许工作在模式 2 下,Port C 宽度不够让 Group B 也工作在双工下
- 在 PC 里常用来连接 键盘、并口、扬声器、定时器 等设备
- 通过 I/O 端口(如 60H、378H)进行读写
- 需要通过 A0/A1 指定访问端口 A/B/C 或控制寄存器
- 控制寄存器大小为 8-bit,第七位决定了该控制字为 Command Byte A 还是 B,分别用于不同的功能
- Command Byte A: 设置端口 A、B、C 的控制信号
- Command Byte B: 设置端口 C 的具体比特位
- 只有片选信号 CS=0 才能访问芯片
将 82C55 接入 80386SX 微处理器的 Low Bank 的一个例子
- 因为只接了 Low Bank,所以每个 Port 相隔 2-Byte,使用 \(A_2 A_1\) 接入到 82C55 的选择信号 \(A_1 A_0\)
- 接入的 \(\overline{CS}\) 信号为 0,才启用 82C55
- 考试可能会给出一批地址,问你哪些位接入 \(\overline{CS}\),哪些位接入 \(A_1 A_0\)
\(A_1 A_0=11\) 地址的 Command Register 既是控制寄存器又是状态寄存器,Bit Position 7 决定了该字节当前工作在 A 模式还是 B 模式:
- \(D_7 = 1\):模式定义模式(Mode Definition),用于配置 A、B、C 端口的工作模式和输入/输出方向。
- 3-6 位为 Group A 服务;0-2 位为 Group B 服务
- \(D_7 = 0\):位置位/复位模式(Bit Set/Reset),专门用于对 Port C 的某一位进行置 1 或清 0 操作。
- Command Byte B 中 4-6 位为自由项,可以随意设置
- 在 Mode 1/2 中,Port C 被用来做握手信号,因此只能按位写;在 Mode 0 中允许对 Port C 整字节写
Mode=0
对于只需要数据输出或者只需要数据输入的设备(如投影仪),我们只需要运行在 Mode 0 即可。
我们以点亮 8-digit 七段数码管为例,使用 Port B 作为 8 个数字的使能与刷新信号,使用 Port A 作为每个七段数码管的数据,接线如下:
观察我们可以得知,Port A 和 Port B 此时都运行在 Mode 0,输出模式下,因此我们对 Command Register 进行设置:
接下来,我们循环扫描 8 个数字,以达成所有数字同时亮着的视觉效果:
第二个例子使用 82C55 作为步进电机的驱动。步进电机通过线圈通电来吸引 Motor 旋转,按照激励模式,有 full-step, half-step, micro-step 或者 one-phase, two-phase 两种分类方式。
例如,我们通过 Port 7 来控制步进电机的线圈使能,使用三个线圈来控制 Motor 移动,因此只有低三位是在使用的:
Mode=1
模式 1 开始,外部设备也具有一定状态,需要同步,我们使用 Port C 作为握手信号。
- 对于 Group A,外部设备主动发送 \(\overline{STB}\) 信号到 PC4 上,这样 82C55 就知道外部设备是否要发送信息;82C55 通过 PC5 发送信号 \(IBF\) 来指示外部设备发送的数据是否已经锁存结束,如果锁存结束,则外部设备可以接着去干自己的事情。
- 数据在 82C55 锁存后,通过 PC3 向 CPU 发送中断请求信号 \(INTR\),CPU 收到后发送读信号 \(\overline{RD}\),将数据读入。
- 82C55 判断条件 \(\overline{STB}=1, IBF=1\) 来设置 \(INTR\),即外部数据已经被锁存住,且被撤掉的情况下,向 CPU 发送请求
- 上图的中断使能 \(INTE\) 其实就是 PC4/2
82C55 相当于中转站,CPU 不再直接向外部设备请求数据,而是从 82C55 锁存的数据读取
相比于 Mode 0,多了个数据锁存的步骤
注意到 Group A 中用到了 PC3,这是因为 Mode2 下 PC6-PC7 被用于数据输出的控制,上 C 口不够用
当然,在输入过程中,我们也可以不等 82C55 给 CPU 发送中断请求,而直接让 CPU 轮询 Port C 的状态。对于 Input,如果查询到 \(IBF\) 已经为 1 就可以开始读;对于 Output,如果查询到 \(\overline{OBF}\) 为 1 就可以开始写入新的数据(外部设备已经取走锁存的数据)。
下图展示了对于输入设备 Keyboard 的轮询过程,其反复检测直到 Port C 的第五位为 1,才开始读入锁存的 ASCII 数据:
实际上,Mode 1 允许 Group A 和 Group B 分别工作在输入输出两种不同模式下,此时 Command Byte A 中关于 Port C 的控制只影响 Group A 中没有使用的两个位:
Mode=2
Mode 2 支持 Group A 工作在双工方式下,Port A 的 8 位引脚既可以发送数据,也可以接收数据,且不需要在程序运行中频繁更改模式控制字。
其中 PC4-5 用于输入控制,PC6-7 用于输出控制,PC3 仍然用于中断信号,PC0-2 保留给 Group 用于 Mode 0/1。
有时存在 Input 和 Output 冲突的情况下,实际的时序控制权在外部设备手中:
- 外设控制 \(\overline{ACK}\) 来决定什么时候让 82C55 把输出数据“倒”出来。
- 当 \(\overline{ACK}\) 有效时,外设正在读取数据,此时外设不应发送 \(\overline{STB}\)。
- 外设控制 \(\overline{STB}\) 来决定什么时候把自己的输入数据“塞”进去。
- 当 \(\overline{STB}\) 有效时,外设正在发送数据,此时外设绝不能拉低 \(\overline{ACK}\)。
此时一个可能的时序图如下:
输入输出共用了 PC3 作为中断线,CPU 该如何判断该中断来自什么事件?
通过读取 Port C 来获知当前到底发生了什么。
8254¶
芯片 8254 是标准的定时器(Timers),只有一个波形信号输出。
8254 内部集成了 3 个独立的 16-bit Counter,每个 Counter 都支持纯二进制计数或 BCD 计数。早期 PC 机中 8254 有如下固定功能:
| 端口地址 | 目标组件 | 功能描述 |
|---|---|---|
| 40H | Counter 0 | 系统时钟滴答:产生约 18.2 Hz 的信号,每秒触发约 18.2 次中断(IRQ0),用于维护系统时间。 |
| 41H | Counter 1 | DRAM 刷新请求:设定为 15 \(\mu\)s,周期性通知 DMA 控制器刷新动态内存,防止数据丢失。 |
| 42H | Counter 2 | 扬声器控制:连接到 PC 扬声器,通过改变频率来产生不同音调的蜂鸣声。 |
| 43H | 控制寄存器 | 用于设定上述三个计数器的工作模式和读写格式。 |
每个计数器都有关键的三条引脚:
- CLK (输入):时钟源。每来一个脉冲,计数器减 1。
- GATE (输入):门控信号。用于控制计数的开始、暂停或重置(类似于秒表的开始键)。门控信号为 0 代表停止计数;门控信号上升沿代表计数开始
- OUT (输出):结果信号。当计数减到 0(或达到特定条件)时,OUT 引脚会改变电平,触发中断或驱动外设。
单个计数器的内部结构如下图所示:
上图除了左上角标绿的控制寄存器,其它都是三个计数器独有的资源
- 每个计数器中都有单独的控制逻辑,有单独的状态寄存器以及状态锁存器
- 这允许我们先批量设控制字,再批量赋初值
- 8254 只能接收 8-bit 数据,在 Counter 内部计数要使用 16-bit 单元,则需要额外使用两个 8-bit 的 Count-Registers(CR) 来进行数据读入以及两个 8-bit 的 Output Latches 来进行数据输出
- 两个 CR 装载到 CE 中只会发生在时钟信号 CLK 的下降沿
- CE 自减这一事件也只会发生在始终信号 CLK 的下降沿
我们通过修改 Control Word Register 来对三个计数器进行编程控制,每设置一个 Counter 都需要重新设置控制字。
Read/Write 位用来解决“如何用 8 位总线读写 16 位数据”的问题,四种组合不同的功能如下:
| RW1 | RW0 | 功能说明 | 动作描述 | 用处 |
|---|---|---|---|---|
| 0 | 0 | Counter Latch (锁存命令) | 将当前计数器的值“锁”在输出缓冲器中,方便 CPU 读取,且不停止计数。 | 计数器运行在高速减法中,直接读取当前计数值,可能出现读完低位准备读高位时,高位已经发生了借位;因此 RW=00 会为当前值保存一个快照到 Latch 中供读取 |
| 0 | 1 | Read/Write Least Significant Byte (LSB) | 只读/写 16 位计数器的 低 8 位 (\(D_7-D_0\))。 | 适用于只需要很小计数值的应用场景;只操作单字节可以减少一条 OUT 指令 |
| 1 | 0 | Read/Write Most Significant Byte (MSB) | 只读/写 16 位计数器的 高 8 位 (\(D_{15}-D_8\))。 | 适用于精度要求不高的应用场景;只操作单字节可以减少一条 OUT 指令 |
| 1 | 1 | Read/Write LSB followed by MSB | 最常用模式。先读/写低 8 位,紧接着读/写高 8 位。 | 连续两个 OUT 指令,自动拼成 16 位 |
先写控制字,再为计数器赋初值;写初值时要满足控制字中的约束
锁存命令的具体汇编例子如下:
具体的读取顺序视初始化顺序而定,如果只初始化了 MSB,那么只能读 1B,且返回 MSB
SC=11 表示该控制字是一个回读命令,其用来批量锁存 Counter 以及其状态寄存器的值,格式如下:
如果一个 Counter 的计数器和状态都被锁存,此时读取该端口时先返回 Latched Status,再返回 1-2 Byte 的 Latch Count。
状态寄存器的各个位如下:
- \(D_7\) 为接口
OUT的值 - \(D_6\) 如果为 1,则该计数器未初始化完全(例如未赋初值);如果为 0,则说明允许被读取
- \(D_5 - D_0\) 为计数器的编程模式
计数器采用减法计数,因此如果 Initial Count 赋值为 0,它实际代表的是该计数方式下最大的循环值:
- Binary: \(2^{16}=65536\)
- BCD: \(10^4 = 10000\)
- 4-bit 对应一个 BCD 码,所以对应十进制总共 \(16/4\) 位
对于最小初始计数值,Mode 2 和 3 下为 2,其余 Mode 中为 1
写入 Initial Count 的原子性
- 随时重写 (Dynamic Reloading): 你可以随时向计数器写入新的初始值,这不会改变已经设定好的工作模式(Mode),但会改变计数器的当前进度。
- 防止中断干扰 (Byte Integrity): 由于 8254 是 16 位计数器,但数据总线通常是 8 位,所以写入一个 16 位计数值需要分两次(先低字节后高字节)。
- 警告: 绝对不能在写完第一个字节后、还没写第二个字节时,让程序跳转到另一个也会操作同一个计数器的子程序。否则会导致计数值被“张冠李戴”,造成严重的逻辑错误。
| 模式 | 计数特点 | OUT 初始状态 | 到达 0 后的行为 | 典型应用场景 |
|---|---|---|---|---|
| Mode 0 计数结束中断 | 软件触发(一次性) | 低 | 变为高并保持,随后回绕 | 事件计数、定时中断 |
| Mode 1 硬件单脉冲 | 硬件触发(retriggerable) | 高 | 计数开始时变为低,计满后再变高 | 产生定时脉冲、宽度调制 |
| Mode 2 分频器/比率计 | 自动重装(periodic) | 高 | 产生一个窄负脉冲后循环 | 系统实时时钟、分频 |
| Mode 3 方波发生器 | 自动重装(periodic) | 高/低切换 | 翻转电平,产生 50% 方波 | 音乐音调、串行波特率 |
| Mode 4 软件触发选通 | 软件触发(一次性) | 高 | 产生一个窄负脉冲后回绕 | 软件控制的精确定时 |
| Mode 5 硬件触发选通 | 硬件触发(retriggerable) | 高 | 产生一个窄负脉冲后回绕 | 硬件同步脉冲输出 |
所谓自动重装,就是计数器到 0 后立即把 Initial Count 重新装载开始下一轮循环
对于其它 Mode(0, 1, 4, 5),计数器归零后只会回绕(Wrap Around),即变为 FFFFH 或者 9999,然后继续无目的地运行下去,输出信号 OUT 不会再变化
我们之前虽然说过,门控信号为 0 代表停止计数,门控信号上升沿代表计数开始。但不同模式下对门控信号的响应方式也有所区别:
- 电平敏感 (Level Sensitive) —— 针对 Mode 0, 2, 3, 4
- 逻辑:GATE 像开关。GATE=1 时准许计数;GATE=0 时暂停计数。
- 采样:在 CLK 的上升沿检查 GATE 的电平。
- 上升沿敏感 (Rising-edge Sensitive) —— 针对 Mode 1, 2, 3, 5
- 逻辑:GATE 的一个从 0 到 1 的跳变(上升沿)会重新启动 (Restart) 计数。
- 机制:门控信号上升沿会触发内部的一个触发器(Flip-flop),在下一个 CLK 上升沿被采样后复位。
- 注意:Mode 2 和 3 既支持电平使能,也支持上升沿重开。
Mode=0 interrupt at the end of count
- 控制字在 \(\overline{WR}\) 上升沿写入,此时 OUT 被初始化为 0
- Initial Count 在 \(\overline{WR}\) 后的第一个时钟下降沿被装载,并在之后每个时钟下降沿减一
- 当计数器为 0 时,输出 OUT 立即置 1,并且产生中断
- 当 GATE 信号为 0 时,停止计数
- 某个时刻时钟和门控信号同时下降时,仍然要对计数器减一(下图 2)
- 因为对于电平敏感的模式,我们在时钟上升沿采样门控信号,来确定下一个时钟下降沿是否暂停计数
- Mode 0 为软件触发,因此软件写入新值时会重新计数(下图 3)
Mode=1 hardware retriggerable one-shot
- 控制字写完后 OUT 就被初始化为 1,在计数开始(Initial Count 被装载)时下降为 0;当计数器为 0 时,OUT 立即置 1
- 当门控信号 GATE 出现上升沿时,下一个时钟下降沿会加载初值重新开始计数
- 因为是上升沿敏感的模式,因此计数过程中就算 GATE 变为 0,也仍然继续计数
Mode=2 rate generator
- 计数到 1 时即产生一个负脉冲,然后下一个时钟下降沿恢复初值
- 因此我们说 Mode 2 和 3 最小初值为 2,因为如果设 1 的话输出 OUT 永远为 0
- 我们称高电平比例为 Duty Cycle,对于 Mode 2 值为 \(\frac{N-1}{N}\),如果 \(N=1\),则占空比为 \(0\)
- Mode 2 同时对电平和上升沿敏感
Mode=3 square wave generator
- Mode 3 和 Mode 2 类似,但是它的占空比为 \(\frac{1}{2}\) 或 \(\frac{N+1}{2N}\)。
- 例如如果 Initial Count 为 6,那么三个周期为高,三个周期为低,duty cycle 等于 \(\frac{3}{6}\)
Mode=4 software-triggered strobe
- Mode 4,5 与 Mode 0, 1 对应,区别在于它们输出初值为 1,并且计数为 0 时会发出一个负脉冲
Mode=5 hardware-triggered strobe
【Example】 例如,我们想要通过向 8254 输入 8MHz 的时钟信号,在端口 0700H 生成 100KHz 的 Square Wave 以及在端口 0702H 生成 200KHz 的 Continuous Pulse,我们的接线如下:
分析要求,我们认为输出需要是连续的、能够自动重装的时钟信号,因此我们只能选用 Mode 2 和 3:
- counter 0 uses mode 3 with count 80 (8M/100K).
- counter 1 uses mode 2 with count 40 (8M/200K).
以下是正确设置计数器的汇编代码:
如果我们希望一个 100Hz 的方波呢?
\(\frac{8M}{100}= 80K > 65536\),因此我们不能直接将 8MHz 的时钟信号接入 Counter 2,而该考虑不同 Counter 之间的串联;例如,可以将 Counter 0 的输出接入 Counter 2 的 CLK,并设置 Initial Count 值为 1000(\(\frac{100K}{100}\))
16550¶
16550 是一个可编程的串行通信接口。
串行通信(Serial Communication)即逐位发送/接收数据的过程,它有如下几个基本概念:
- Three Modes of Transmission
- 单工 simplex;半双工 half duplex;全双工 full duplex
- Steps of Serial Communication
- Clocks & Timing
- 串行通信中,如何让接收方正确采样发送方的每个位?这个问题其实就是要求发送方和输出方的时钟信号相同。
- Synchronous & Asynchronous 主要区别在于异步通信具有明显的起始位和终止位,并且中间存在时延
- 异步时钟: 双方不共享时钟,但是约定一个相同的波特率(baud rate)。需要在数据包组帧时通过 Start Bit 和 Stop Bit 进行时钟同步。
- baud rate 指每秒传输的比特个数,单位为 bps;接收方通过其内部的 baud clock(BCLK) 来对接收的数据采样
- 为了采样到信号强度最高的点(中间),实际波特时钟频率 BCLK 远大于实际波特率,我们对波特率乘上一个过采样参数 baud rate divisor 得到频率:\(\text{BCLK}=\text{baud rate} \times \text{baud rate divisor}\)
- 同步时钟: 双方共享一个全局时钟(dedicated global clock);或者使用锁相环(phase-locked loop)技术,发送方提供一段数据,接收方通过这段数据恢复一致时钟
- 异步时钟: 双方不共享时钟,但是约定一个相同的波特率(baud rate)。需要在数据包组帧时通过 Start Bit 和 Stop Bit 进行时钟同步。
- Signal Encoding
UART 和 USART 是用来将并行数据转换为串行数据的硬件
- Universal Asynchronous Receiver Transmitter: 只支持异步模式
- Universal Synchronous Asynchronous Receiver Transmitter: 同时支持异步模式和同步模式
事实上 16550 就是一个 UART。UART 接收一个输入时钟,并按照 Divisor 比例进行划分,以产生波特时钟 BCLK。通常 \(\text{Divisor}=\frac{\text{input clock frequency}}{\text{BCLK}}\)。
16550 中,异步串行数据会被打包进如下格式进行传输:
- 在没有数据时,接口均输出 Stop Bit = 1;有数据要进入时,先发送 Start Bit = 0
- 然后传输 5-8 bit 的数据,注意数据先传低位
- 之后传输一个校验位 Parity
- 最后持续输出 Stop Bit = 1,与下一个数据进行分隔
传输多少位数据、校验位是什么类型、结束位的产生都是可编程的,后续会讲到
我们所说的 Baud Rate 包含传输的所有比特,即包括 Start、Stop、Data 和 Parity,因此实际的数据传输率要小于这个值。16550 通常工作在 0 - 1.5M 的波特率下,而波特时钟 BLCK 是波特率的 16 倍,不可编程。
16550 采用双列直插封装,共 40 个引脚。芯片内部 Receiver 和 Transmitter 两个模块独立,这也是为什么它能够在单工、半双工、全双工模式下运行:
相比于前代芯片,16550 最大的特点是内部为接收器和发送器都集成了一个长达 16B 的 FIFO Memories(Buffer)。
- 没有 FIFO 时 (如 8250): 每接收 1个 字节,UART 就必须打断 CPU(发送中断),让 CPU 来取走数据。如果波特率很高,CPU 就会频繁被打断,处理效率极低。
- 有 FIFO 时 (16550): 芯片可以先将收到的数据暂存在 FIFO 中。只有当积攒了 16个 字节(或达到设定的触发点)时,才通知 CPU 一次。
- 结果: 大大减少了 CPU 的中断次数,降低了 CPU 的负担,使系统能够支持更高的通信速率(High-speed systems)。
上图中很大一部分引脚用于调制解调器(modem),在本课程中可以不考虑
在 16550 UART 芯片中,地址引脚 \(A_0, A_1, A_2\) 的主要作用是选择内部寄存器。
由于 16550 内部有多个控制和状态寄存器,通过这 3 个引脚的不同高低电平组合(\(2^3 = 8\) 种组合),CPU 可以访问芯片内不同的功能模块。为了完全访问所有寄存器,16550 还需要配合一个特殊的软件标志位——DLAB(Divisor Latch Access Bit,除数锁存访问位,位于 LCR 寄存器的第 7 位)。
这是因为内部寄存器数量超过了 8 个,所以地址 000 和 001 是复用的:
| A2 | A1 | A0 | DLAB 状态 | 读/写 (R/W) | 选中的寄存器 | 功能描述 |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 读 | RBR (Receiver Buffer) | 接收缓冲寄存器:读取接收到的数据 |
| 0 | 0 | 0 | 0 | 写 | THR (Transmitter Holding) | 发送保持寄存器:写入要发送的数据 |
| 0 | 0 | 0 | 1 | 读/写 | DLL (Divisor Latch Low) | 除数锁存器低位:设置波特率 |
| 0 | 0 | 1 | 0 | 读/写 | IER (Interrupt Enable) | 中断使能寄存器:开关各类中断 |
| 0 | 0 | 1 | 1 | 读/写 | DLM (Divisor Latch High) | 除数锁存器高位:设置波特率 |
| 0 | 1 | 0 | X | 读 | IIR (Interrupt Ident.) | 中断标识寄存器:查看当前发生了什么中断 |
| 0 | 1 | 0 | X | 写 | FCR (FIFO Control) | FIFO控制寄存器:启用/重置 FIFO,设置触发点 |
| 0 | 1 | 1 | X | 读/写 | LCR (Line Control) | 线路控制寄存器:设置数据位、停止位、校验位、DLAB |
| 1 | 0 | 0 | X | 读/写 | MCR (Modem Control) | Modem控制寄存器:控制 DTR, RTS 等引脚 |
| 1 | 0 | 1 | X | 读 | LSR (Line Status) | 线路状态寄存器:查询是否收到数据、发送是否空闲 |
| 1 | 1 | 0 | X | 读 | MSR (Modem Status) | Modem状态寄存器:读取 CTS, DSR 等引脚状态 |
| 1 | 1 | 1 | X | 读/写 | SCR (Scratch) | 暂存寄存器:由程序员自由使用,不影响硬件 |
总共 12 个寄存器可用,而我们编程时注重以下两类寄存器
- Control of Communication
- Line Control Register
- Line Status Register
- Divisor LSB Latch
- Divisor MSB Latch
- Data Transmission
- Receiver Buffer Register(read only)
- Transmitter Hoding Register(write only)
- FIFO Control Register(write only)
对于其它引脚,我们尝试按照功能进行划分:
1. 处理器接口与控制信号 (Bus Interface & Control)
这部分引脚负责将 UART 连接到 CPU 的总线上,让 CPU 能读写 UART 的内部寄存器。
- \(\overline {\text{ADS}}\) (Address Strobe / 地址选通):
- 作用: 这是一个锁存信号。当 CPU 总线上的地址信号(\(A_0-A _2\))或片选信号(\(CS\))不稳定时,用 \(\text{ADS}\) 的下降沿将这些信号“锁”住,保存到 UART 内部。
- 架构差异:
- Motorola 体系: 常用 \(\text{ADS}\),因为它们的设计习惯使用地址选通。
- Intel 体系 (如 x86): 通常不需要。Intel 总线在读写周期内地址是保持稳定的。因此,在连接 Intel CPU 时,该引脚通常接地 (Tie Low) 以保持始终有效(透明传输)。
- \(\text{CS}_0, \text{CS}_1, \overline{\text{CS}}_2\) (Chip Selects / 片选):
- 逻辑: 16550 提供了 3 个片选引脚。只有当这三个引脚同时有效(例如 \(CS_0=1, CS_1=1, \overline{CS}_2=0\))时,芯片才会被选中工作。
- \(D_0 - D_7\) (Data Bus / 数据总线):
- 双向 8 位数据线,直接连到 CPU 的数据总线上,用于传送命令、状态和实际的收发数据。
- 读写控制 (\(RD, \overline{RD}, WR, \overline{WR}\)):
- 灵活性: 16550 非常贴心地同时提供了高电平有效(\(RD\))和低电平有效(\(\overline{RD}\))的引脚。
- 用法: 你只需要根据你的 CPU 总线特性连接其中一组即可。例如 x86 系统通常使用低电平有效的 \(\overline{RD}\) 和 \(\overline{WR}\)。
2. 时钟与波特率发生器 (Clock & Timing)
- \(\text{XIN, XOUT}\) (System Clock / 主时钟):
- 这是芯片的“心脏”。你可以接一个晶振(Crystal)在两脚之间,或者直接从 \(\text{XIN}\) 输入外部时钟信号。
- \(\overline {\text{BAUDOUT}}\) (Baud Out / 波特率输出):
- 来源: 这是一个输出信号。它来自发送器部分的波特率发生器(即主时钟经过 DLL/DLM 寄存器分频后的信号)。
- \(\text{RCLK}\) (Receiver Clock / 接收时钟):
- 来源: 这是一个输入信号。接收器通过这个时钟来采样数据。
- 典型接法: 在绝大多数设计中,会将 \(\text{BAUDOUT}\) 直接连回 \(\text{RCLK}\)。这意味着发送和接收使用相同的波特率(这也是我们常见的串口工作方式)。如果你需要发送和接收使用不同的波特率,则可以给 \(\text{RCLK}\) 输入单独的时钟源。
3. 串行数据接口 (Serial Interface)
- \(\text{SOUT}\) (Serial Out): 串行数据输出(发送)。
- \(\text{SIN}\) (Serial In): 串行数据输入(接收)。
- 关键提示: 这里的信号是 TTL 电平(0V~5V)。如果要连接到电脑背后的 COM 口(RS-232 标准,±12V),中间必须加电平转换芯片(如 MAX232)。
4. 系统管理 (System Management)
- \(\text{MR}\) (Master Reset / 主复位):
- 接系统的 RESET 信号。高电平有效,用于初始化芯片,清空 FIFO 和寄存器。
- \(\text{INTR}\) (Interrupt Request / 中断请求):
- 输出到 CPU: 当 16550 需要 CPU 关注时(INTR=1),通知 CPU。
- 触发条件: 出现 Receiver Error,如校验出错;芯片接收器接收到数据;芯片发送器为空
对 16550 的编程分为两个阶段:
- <1> Initialization (setup)
- 设置 Baud Rate Generator,得到要求的波特率
- 设置 Line Control Register,设置合适的传输参数
- <2> Operation
- 清空 Transmitter 和 Receiver 的 FIFOs,通过设置 FIFO Control Register 来完成
- 进行实际 Communication
PC 中,port 0 对应端口 3F8H-3FFH,port 2 对应端口 2F8H-2FFH
对于 Baud Rate Generator,我们知道可以通过设置 Divisor Latch 来设置波特率;而对于 Line Control Register,我们需要了解其寄存器结构如下:
- \(L1L0\):指定传输数据长度
- 00 = 5 bits
- 01 = 6 bits
- 10 = 7 bits
- 11 = 8 bits
- \(S\):表示 Stop Bits
- 0 = 1 stop bit
- 1 = 1.5 or 2 stop bits
- 数据长度为 5 时使用 1.5 stop bits,其余使用 2 stop bits
- 现代电子设备处理速度快,通常 1 stop bit 就足够接收方准备好接收下一个字节了
- \(PE\):表示 Parity Enable
- 0 = no parity
- 1 = parity enabled
- \(P\):表示 Parity Type
- 0 = odd parity
- 1 = even parity
- \(ST\):表示 Stick Bit
- 0 = stick parity off
- 1 = stick parity on
- 仅 \(PE=1\) 时有效,它会覆盖奇偶校验,强制将校验位固定为 \(\overline {P}\)
- \(SB\):表示 Send Break
- 0 = no break sent
- 1 = send break on \(SOUT\)
- 此时,UART 会强制将发送引脚 (\(SOUT\)) 拉低到逻辑 0(Spacing State),并保持直到 \(SB\) 被软件重置为 0。
- 发送数据
0x00包含起始位和停止位(会有短暂的高电平跳变),而 Break 是一条死寂的长低电平。
- \(DL\):表示 Enable Divisor,即我们之前所说的 \(DLAB\)
【Example】 给定 Input Clock 为 18.432 MHz,我们希望得到 9600 波特率。此时计算可得 Divisor 的值为 \(\frac{18432000}{9600\times 16}=120\)。除此之外,我们希望数据为 7-bit,采用 odd parity,1 stop bit:
实际发送 Serial Data 时需要轮询 Line Status Register,以确认 Error Conditions 和接收器、发送器的状态。LSR 的结构如下:
- \(DR\):表示 Data Ready
- 0 = no data to read
- 1 = data in FIFO
- 在从 Receiver 中读取数据之前,需要确保 DR = 1
- \(OE\):表示 Overrun Error
- 0 = no overrun error
- 1 = overrun error
- 新的数据已经传送到了,但是接收端的 FIFO(或缓冲寄存器)已经满了,导致新数据无法存入
- \(PE\):表示 Parity Error
- 0 = no parity error
- 1 = parity error
- \(FE\):表示 Framing Error
- 0 = no framing error
- 1 = framing error
- Receiver 没有在预期的位置检测到 Stop Bit
- \(BI\):表示 Break Indicator
- 0 = no break
- 1 = break being received
- \(TH\):表示 Transmitter Holding Register
- 0 = wait for transmitter
- 1 = transmitter ready for data
- 在向 Transmitter 中写入数据之前,需要确保 TH = 1
- \(ER\):表示 Error
- 0 = no error
- 1 = at least one error in FIFO
例如,以下代码通过轮询 \(TH\) 来将寄存器 \(AH\) 中的数据发送给 16550:
以下代码通过轮询 \(DR\) 来将 Receiver 的数据读入,并检测有无错误:

























































