Skip to content

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 为例

ch11_1.png

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
    • 这里的串行表示按位传输数据,并行表示按大于位(比如字节)传输数据

ch11_2.png

在设计接口的过程中,我们尝试明确以下问题:

  • <1> 怎么将 I/O 设备连接到 CPU?
    • 对于输出接口,使用 Latches
    • 对于输入接口,使用 Three State Buffers
  • <2> 怎么为 I/O 设备提供 Address Space?
    • 有两种 Schemes:Isolated I/OMemory 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 使用 INOUT 指令来做数据的传输,这里的 isolated 意指 I/O 地址空间是和内存地址空间隔离的,称为 port

  • Advantage
    • port 是隔离的,不需要为设备占用 Memory Address
  • Disadvantage
    • 只能通过 INOUT 指令进行数据交换
    • 需要对 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 由于消耗更少能量、速度更快而得到了更广泛的应用。对于它们的输入和输出的逻辑电平,有如下规定标准:

ch11_3.png

TTL 通常是 5V,但是为了和 CMOS 兼容也有 3.3V 的版本

ch11_4.png

CMOS 的 VCC 可以是 5V, 3.3V, 1.8V

总结得到下表:

ch11_5.png

上表下方有两个例子,其中左例 TTL 的输出电平不满足 CMOS 的输入电平要求,即 \(VOHmin > VIHmin, VOLmax < VILmax\)

对于输入设备,我们只需要保证电平匹配、信号稳定即可得到对应的逻辑信号,通常使用三态门作为中介;而输出设备还需要保证电流大小足够驱动输出设备。

实际控制中,我们要通过设置合理的电阻值来获得额定电流,我们以一个 LED 灯为例:

ch11_6.png

已知该三极管基极和集电极的电流大小比例为 1:100。对于 Logic 1,TTL 提供的输出电压幅度在 2.4V - 5V 范围内,因此我们要保证对于 2.4V 的输入也能驱动该 LED 灯,因此对基极和集电极的电阻设置计算如上。

17K 不是标准电阻,因此最终选用相近的 18K 电阻(为什么可以选用更大的电阻?)

更一般的感性负载电机设备中存在很多电感,在我们断电瞬间会产生反向电动势。如果不对该反向电动势进行处理,高额电压可能会损坏三极管。为此,我们需要在三极管两端增加稳压管:

ch11_7.png

该电机的额定电流为 1A,电阻的具体计算方式见图上

对于输出设备,输出的信号还需要进行锁存,因为输出的信号是持续性的。控制信号此时不再接入使能信号,而是接入时钟信号;当我们使用指令 OUT 38H, AL 时,时钟信号就会发生一次跳变,将我们输出信号进行锁存保存。

对于打印机等更“智能”的设备,我们还要求 CPU 能够和外部设备进行异步数据传输,常见有如下两种方法:

  • Strobing 选通,单向控制信号
  • Handshaking 握手,双向控制信号

Strobing

Strobing 分为 source-initiated transfer 和 destination-initiated transfer 两种:

ch11_8.png

  • source 先将数据写入 Data Bus,然后设置控制信号 Strobe 为 1
  • destination 读取数据到寄存器中
  • source 设置控制信号 Strobe 为 0
  • source 从 Data Bus 中移除数据

ch11_9.png

  • destination 设置控制信号 Strobe 为 1
  • source 将数据写入 Data Bus
  • destination 捕获到数据并写入寄存器,然后设置控制信号 Strobe 为 0
  • source 从 Data Bus 中移除数据

选通方式好处在于简单,坏处在于难以控制连接时间,源并不知道目标何时接收完数据。

Handshaking

为了更好地控制时序,握手方式引入了第二条控制信号。

现在稍微复杂点的设备都采用双向信号,两条控制线分别用于 RequestReply

ch11_10.png

  • source 先将数据写入 Data Bus,然后设置 request
  • destination 读取数据到寄存器中,然后设置 reply
  • source 从 Data Bus 中移除数据,并且重置 request
  • destination 重置 reply

ch11_11.png

  • destination 设置 request
  • source 将数据写入 Data Bus,然后设置 reply
  • destination 捕获到数据并写入寄存器,然后重置 request
  • source 从 Data Bus 中移除数据,并且重置 reply

如下例子展示了 PC 和一个 Printer 之间的数据交换过程,ASCII 数据被放置在 \(D_7- D_0\) 中,向 port PRINTER 发送数据就相当于 request;打印机收到数据后设置 port BUSY 为 1 就相当于 reply:

ch11_12.png

因此轮询 Polling 也是双向握手

I/O Port Address Decoding

往年没考,今年必考

ch11_13.png

其中最重要的是左侧部分对 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,其结构如本节开始的图中左侧部分所示。

根据图识别该设备地址译码的范围

只有译码器使能全为 1,才能启用该地址译码器;例如下图要求 \(A_7 - A_3\)11110(注意非门是一个圈)

ch11_16.png

对于 16-bit 的地址,使用硬件 PLD

下图是一个可编程硬件,因此 \(A_{11}- A_{15}\) 也是作为译码器使能输入进入的

ch11_14.png

我们的数据总线通常为 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)
OUT  AL, 40H   ; 写低位 bank,BLE = 0,写入  D7-D0
OUT  AL, 41H   ; 写高位 bank,BHE = 0,写入 D15-D8

\(\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 中选取对应的单字节

ch11_15.png

为什么 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 即可

ch11_17.png

对于 16-bit port(2-byte aligned),一次写入 16-bit 数据,不再需要 \(\overline{BHE}\)\(A_0\)

ch11_18.png

Programmable Peripheral

本节介绍三个经典的外部可编程器件。

82C55

Quote

ch11_19.png

芯片 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\)

ch11_21.png

\(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 整字节写

ch11_22.png

三种模式的交互模式

ch11_20.png

Mode=0

对于只需要数据输出或者只需要数据输入的设备(如投影仪),我们只需要运行在 Mode 0 即可。

我们以点亮 8-digit 七段数码管为例,使用 Port B 作为 8 个数字的使能与刷新信号,使用 Port A 作为每个七段数码管的数据,接线如下:

ch11_23.png

观察我们可以得知,Port A 和 Port B 此时都运行在 Mode 0,输出模式下,因此我们对 Command Register 进行设置:

1
2
3
4
; programming the 82C55 PIA
MOV AL, 10000000B    ; command (A) 此时 Port C 相关可以随意设置
MOV DX, 703H         ; address port 703H
OUT DX, AL           ; send command to port 703H

接下来,我们循环扫描 8 个数字,以达成所有数字同时亮着的视觉效果:

DISP PROC NEAR USES AX BX DX SI

    PUSHF                   ; 在循环移位中会影响 Flag,因此要保存恢复
    MOV   BX, 8             ; 计数器 Counter
    MOV   AH, 7FH           ; 选择模式,7F 表示第七个 LED 被选中
    MOV   SI, OFFSET MEM - 1; Display Data 的地址(偏移)
    MOV   DX, 701H          ; Port B 的地址 

; DISPLAY ALL 8 DIGITS

    .REPEAT
        MOV   AL, AH        ;  
        OUT   DX, AL        ; 发送选择模式给 Port B
        DEC   DX            ; Port A 的地址
        MOV   AL, [BX+SI]   ;
        OUT   DX, AL        ; 发送数据给 Port A
        CALL  DELAY         ; 延迟等待 1ms
        ROR   AH, 1         ; 调整选择模式
        INC   DX            ; Port B 的地址
        DEC   BX            ; 计数器-1
    .UNTIL BX == 0

    POPF                    ; 恢复 Flag
    RET

DISP ENDP

第二个例子使用 82C55 作为步进电机的驱动。步进电机通过线圈通电来吸引 Motor 旋转,按照激励模式,有 full-step, half-step, micro-step 或者 one-phase, two-phase 两种分类方式。

ch11_24.png

ch11_25.png

例如,我们通过 Port 7 来控制步进电机的线圈使能,使用三个线圈来控制 Motor 移动,因此只有低三位是在使用的:

ch11_26.png

Mode=1

模式 1 开始,外部设备也具有一定状态,需要同步,我们使用 Port C 作为握手信号。

ch11_27.png

  • 对于 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 口不够用

ch11_28.png

  • 对于 Group A,CPU 主动向 82C55 发送 \(\overline{WR}\) 信号,让 82C55 锁存 CPU 数据总线上的数据;82C55 向外部设备发送 \(\overline{OBF}\) 来指示 CPU 有数据要写入
  • 外部设备读入数据后响应 \(\overline{ACK}\),82C55 此时向 CPU 发送中断信号 \(INTR\),告知 CPU 可以继续写入其它数据

Group B 不会工作在 Mode2,因此它完全可以复用 PC0-PC2 三条信号线

当然,在输入过程中,我们也可以不等 82C55 给 CPU 发送中断请求,而直接让 CPU 轮询 Port C 的状态。对于 Input,如果查询到 \(IBF\) 已经为 1 就可以开始读;对于 Output,如果查询到 \(\overline{OBF}\) 为 1 就可以开始写入新的数据(外部设备已经取走锁存的数据)。

下图展示了对于输入设备 Keyboard 的轮询过程,其反复检测直到 Port C 的第五位为 1,才开始读入锁存的 ASCII 数据:

ch11_29.png

实际上,Mode 1 允许 Group A 和 Group B 分别工作在输入输出两种不同模式下,此时 Command Byte A 中关于 Port C 的控制只影响 Group A 中没有使用的两个位:

ch11_30.png

ch11_31.png

Mode=2

Mode 2 支持 Group A 工作在双工方式下,Port A 的 8 位引脚既可以发送数据,也可以接收数据,且不需要在程序运行中频繁更改模式控制字。

ch11_32.png

其中 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}\)

此时一个可能的时序图如下:

ch11_33.png

输入输出共用了 PC3 作为中断线,CPU 该如何判断该中断来自什么事件?

通过读取 Port C 来获知当前到底发生了什么。

8254

Quote

ch11_35.png

芯片 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 控制寄存器 用于设定上述三个计数器的工作模式和读写格式。

ch11_34.png

每个计数器都有关键的三条引脚:

  1. CLK (输入):时钟源。每来一个脉冲,计数器减 1。
  2. GATE (输入):门控信号。用于控制计数的开始、暂停或重置(类似于秒表的开始键)。门控信号为 0 代表停止计数;门控信号上升沿代表计数开始
  3. OUT (输出):结果信号。当计数减到 0(或达到特定条件)时,OUT 引脚会改变电平,触发中断或驱动外设。

单个计数器的内部结构如下图所示:

ch11_36.png

上图除了左上角标绿的控制寄存器,其它都是三个计数器独有的资源

  • 每个计数器中都有单独的控制逻辑,有单独的状态寄存器以及状态锁存器
    • 这允许我们先批量设控制字,再批量赋初值
  • 8254 只能接收 8-bit 数据,在 Counter 内部计数要使用 16-bit 单元,则需要额外使用两个 8-bit 的 Count-Registers(CR) 来进行数据读入以及两个 8-bit 的 Output Latches 来进行数据输出
    • 两个 CR 装载到 CE 中只会发生在时钟信号 CLK 的下降沿
    • CE 自减这一事件也只会发生在始终信号 CLK 的下降沿

我们通过修改 Control Word Register 来对三个计数器进行编程控制,每设置一个 Counter 都需要重新设置控制字。

ch11_37.png

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 位

先写控制字,再为计数器赋初值;写初值时要满足控制字中的约束

锁存命令的具体汇编例子如下:

1
2
3
4
5
6
MOV   AL, 10000000B  ; Counter 2 锁存 
OUT   43H, AL        ; 设置控制字
IN    AL, 42H        ; 读取 Counter 2 LSB
MOV   AH, AL         ; 暂存到 AH
IN    AL, 42H        ; 读取 Counter 2 MSB
XCHAG AH, AL         ; 恢复 Byte Order

具体的读取顺序视初始化顺序而定,如果只初始化了 MSB,那么只能读 1B,且返回 MSB

SC=11 表示该控制字是一个回读命令,其用来批量锁存 Counter 以及其状态寄存器的值,格式如下:

ch11_50.png

如果一个 Counter 的计数器和状态都被锁存,此时读取该端口时先返回 Latched Status,再返回 1-2 Byte 的 Latch Count。

状态寄存器的各个位如下:

1
2
3
4
5
6
# Status Register

    D7         D6       D5    D4    D3   D2   D1   D0
+--------+------------+-----+-----+----+----+----+-----+
| Output | Null Count | RW1 | RW0 | M2 | M1 | M0 | BCD | 
+--------+------------+-----+-----+----+----+----+-----+
  • \(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)

ch11_38.png

ch11_39.png

ch11_40.png

Mode=1 hardware retriggerable one-shot

  • 控制字写完后 OUT 就被初始化为 1,在计数开始(Initial Count 被装载)时下降为 0;当计数器为 0 时,OUT 立即置 1
  • 当门控信号 GATE 出现上升沿时,下一个时钟下降沿会加载初值重新开始计数
  • 因为是上升沿敏感的模式,因此计数过程中就算 GATE 变为 0,也仍然继续计数

ch11_41.png

ch11_42.png

ch11_43.png

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 同时对电平和上升沿敏感

ch11_44.png

ch11_45.png

Mode=3 square wave generator

  • Mode 3 和 Mode 2 类似,但是它的占空比为 \(\frac{1}{2}\)\(\frac{N+1}{2N}\)
  • 例如如果 Initial Count 为 6,那么三个周期为高,三个周期为低,duty cycle 等于 \(\frac{3}{6}\)

ch11_46.png

Mode=4 software-triggered strobe

  • Mode 4,5 与 Mode 0, 1 对应,区别在于它们输出初值为 1,并且计数为 0 时会发出一个负脉冲

ch11_47.png

Mode=5 hardware-triggered strobe

ch11_48.png


【Example】 例如,我们想要通过向 8254 输入 8MHz 的时钟信号,在端口 0700H 生成 100KHz 的 Square Wave 以及在端口 0702H 生成 200KHz 的 Continuous Pulse,我们的接线如下:

ch11_49.png

分析要求,我们认为输出需要是连续的、能够自动重装的时钟信号,因此我们只能选用 Mode 2 和 3:

  • counter 0 uses mode 3 with count 80 (8M/100K).
  • counter 1 uses mode 2 with count 40 (8M/200K).

以下是正确设置计数器的汇编代码:

; A procedure that programs the 8254 timer to function
; as illustrated above

TIME PROC NEAR USES AX DX

    MOV   DX, 706H       ; 控制字端口
    MOV   AL, 00110110B  ; 设置 counter 0 为 Mode 3
    OUT   DX, AL         
    MOV   AL, 01110100B  ; 设置 counter 1 为 Mode 2
    OUT   DX, AL

    MOV   DX, 700H       ; counter 0 端口
    MOV   AL, 80         ; 设置 LSB
    OUT   DX, AL
    MOV   AL, 0          ; 设置 MSB
    OUT   DX, AL

    MOV   DX, 702H       ; counter 1 端口
    MOV   AL, 40         ; 设置 LSB
    OUT   DX, AL
    MOV   AL, 0          ; 设置 MSB
    OUT   DX, AL

    RET

TIME ENDP
如果我们希望一个 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
    • ch11_51.png
  • 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)技术,发送方提供一段数据,接收方通过这段数据恢复一致时钟
  • Signal Encoding
    • Non-Return to Zero 编码是串行通信中最常用的编码
    • ch11_52.png

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}}\)

ch11_53.png

假定 \(\text{divisor}=16, \text{baud rate divisor}=16\)

ch11_54.png

16550 中,异步串行数据会被打包进如下格式进行传输:

ch11_55.png

  • 在没有数据时,接口均输出 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 两个模块独立,这也是为什么它能够在单工、半双工、全双工模式下运行:

ch11_56.png

相比于前代芯片,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 个,所以地址 000001 是复用的:

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,我们需要了解其寄存器结构如下:

1
2
3
4
5
; Line Control Register
  7    6    5    4    3    2    1    0
+----+----+----+----+----+----+----+----+
| DL | SB | ST | P  | PE | S  | L1 | L0 |
+----+----+----+----+----+----+----+----+
  • \(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:

ch11_57.png

LINE EQU 0F3H
LSB  EQU 0F0H
MSB  EQU 0F1H
FIFO EQU 0F2H

INIT PROC NEAR

    MOV   AL, 10001010B  ; set LCR, with DLAB=1
    OUT   LINE, AL

    MOV   AL, 120        ; set DIVISOR LSB
    OUT   LSB, AL

    MOV   AL, 0          ; set DIVISOR MSB
    OUT   MSB, AL

    MOV   AL, 00001010B  ; set LCR, with
    OUT   LINE, AL       ; PE = 1 & L1L0 = 10

    MOV   AL, 00000111B  ; set FCR, with
    OUT   FIFO, AL       ; RST = 11 & EN = 1

    RET

INIT ENDP

FIFO Control Register 通常初始化为 0x7 就行了

ch11_58.png

实际发送 Serial Data 时需要轮询 Line Status Register,以确认 Error Conditions 和接收器、发送器的状态。LSR 的结构如下:

1
2
3
4
5
; Line Status Register
  7    6    5    4    3    2    1    0
+----+----+----+----+----+----+----+----+
| ER | TE | TH | BI | FE | PE | OE | DR |
+----+----+----+----+----+----+----+----+
  • \(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:

LSTAT EQU 0F5H
DATA  EQU 0F0H

SEND PROC NEAR USES AX

    .REPEAT              ; TEST TH BIT
        IN    AL, LSTAT
        TEST  AL, 20H
    .UNITL !ZERO?

    MOV   AL, AH         ; SEND DATA
    OUT   DATA, AL

    RET

SEND ENDP

以下代码通过轮询 \(DR\) 来将 Receiver 的数据读入,并检测有无错误:

LSTAT EQU 0F5H
DATA  EQU 0F0H

RECV PROC NEAR

    .REPEAT              ; TEST DR BIT
        IN   AL, LSTAT
        TEST AL, 1
    .UNTIL !ZERO?

    TEST AL, 0EH         ; TEST FOR ANY ERROR
    .IF ZERO?            ; NO ERROR
        IN   AL, DATA
    .ELSE                ; ENY ERROR
        MOV  AL, '?'
    .ENDIF

    RET

RECV ENDP
Comments: