﻿# Intel 8237 DMA 控制器 —— 工作原理、编程接口与实例

> 现代 DMA 早已从独立控制器芯片演化为外设自带的 Bus Mastering 引擎（见 [dma.md](/concepts/io/dma.md) §二），但理解 8237 是理解所有 DMA 的基础——它定义了 DMA 的核心抽象：**通道、地址寄存器、计数寄存器、传输模式、总线仲裁**。即使今天的 NVMe 驱动在初始化描述符环时，骨子里还是那套"告诉硬件从哪搬、搬多少、搬完通知我"的逻辑。本篇细致拆解 8237 的硬件架构、寄存器编程模型、时序、以及三个可跑的汇编级 demo。

---

## 零、8237 在历史上的位置

8237 是 Intel 在 1980 年代推出的可编程 DMA 控制器，几乎是所有 IBM PC/AT 及兼容机的标配。一块 8237 提供 **4 个独立 DMA 通道**；PC/AT 用两片级联（主片+从片）提供 7 个通道（通道 4 用于级联）。

```bash
IBM PC/AT DMA 架构:
主 8237 (端口 0xC0~0xDE):
  通道 0 ─── DRAM 刷新 (未用,保留)
  通道 1 ─── (空闲,或 Sound Blaster 8-bit)
  通道 2 ─── 软盘控制器
  通道 3 ─── (空闲,或 ECP 并口)
  通道 4 ─── 级联到从 8237
              ↓
从 8237 (端口 0x00~0x0F, 只支持 16-bit 传输):
  通道 5 ─── (空闲,或 Sound Blaster 16-bit)
  通道 6 ─── (空闲)
  通道 7 ─── (空闲)
```

> 8237 的局限很明确：16 位地址总线（加上页面寄存器才凑够 24 位→16MB）、单次传输最大 64KB（16 位计数器）、最高传输速率约 1.6MB/s，在 PCIe 时代早已消失。但它的**编程模型**——通道×4、地址寄存器+计数寄存器+模式寄存器——是所有现代 DMA 引擎的"祖宗"。

---

## 一、硬件架构

### 1.1 内部框图

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<reg>> #E3F2FD
  BorderColor<<reg>> #1976D2
  BackgroundColor<<ctrl>> #FFF9C4
  BorderColor<<ctrl>> #F9A825
  BackgroundColor<<arb>> #FFCCBC
  BorderColor<<arb>> #E64A19
  BackgroundColor<<bus>> #C8E6C9
  BorderColor<<bus>> #388E3C
}
rectangle "数据总线缓冲器\n(DB0~DB7)" <<bus>> as DB
rectangle "I/O 缓冲器\n(IOR#/IOW#/MEMR#/MEMW#)" <<bus>> as IO
rectangle "控制逻辑\n定时与控制\n命令寄存器(8bit)\n状态寄存器(8bit)" <<ctrl>> as CTRL
rectangle "优先级仲裁器\n固定优先级/循环优先级" <<arb>> as ARB
rectangle "通道 0\n━━━━━━━━\n地址寄存器(16bit)\n计数寄存器(16bit)\n模式寄存器(6bit)" <<reg>> as CH0
rectangle "通道 1\n━━━━━━━━\n地址寄存器(16bit)\n计数寄存器(16bit)\n模式寄存器(6bit)" <<reg>> as CH1
rectangle "通道 2\n━━━━━━━━\n地址寄存器(16bit)\n计数寄存器(16bit)\n模式寄存器(6bit)" <<reg>> as CH2
rectangle "通道 3\n━━━━━━━━\n地址寄存器(16bit)\n计数寄存器(16bit)\n模式寄存器(6bit)" <<reg>> as CH3
DB -down- CTRL
DB -down- ARB
IO -down- CTRL
CTRL -right- ARB
ARB -right- CH0
CH0 -[hidden]right- CH1
CH1 -[hidden]right- CH2
CH2 -[hidden]right- CH3
@enduml
```

**关键部件说明**：

| 部件 | 功能 |
|------|------|
| **定时与控制** | 产生 MEMR#/MEMW#/IOR#/IOW# 总线控制信号，控制传输时序 |
| **优先级仲裁器** | 多个通道同时请求 DMA 时决定谁先拿到总线，支持固定和循环两种策略 |
| **命令寄存器** | 全局配置：DREQ/DACK 极性、是否允许内存到内存、是否允许固定优先级等 |
| **模式寄存器** | 每通道独立：传输方向/自动初始化/地址增减/传输模式 |
| **地址寄存器**（每通道×16bit）| 存放当前传输的源/目标地址。注意只有 16 位——高 8 位由外部**页面寄存器**(74LS612)提供 |
| **计数寄存器**（每通道×16bit）| 存放要传输的字节数，每传一个字节减 1，减到 0xFFFF→0x0000 时触发 TC(Terminal Count) |

### 1.2 关键引脚

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<in>> #C8E6C9
  BorderColor<<in>> #388E3C
  BackgroundColor<<out>> #FFCCBC
  BorderColor<<out>> #E64A19
}
rectangle "8237\nDMA 控制器" as DMAC
rectangle "DREQ0~3\n(外设→8237)" <<in>> as DREQ
rectangle "HRQ\n(8237→CPU)" <<out>> as HRQ
rectangle "HLDA\n(CPU→8237)" <<in>> as HLDA
rectangle "DACK0~3\n(8237→外设)" <<out>> as DACK
rectangle "A0~A7\n(地址线)" <<out>> as A
rectangle "DB0~DB7\n(数据/高8位地址)" <<out>> as DB
rectangle "MEMR#/MEMW#\nIOR#/IOW#" <<out>> as RW
rectangle "EOP#\n(过程结束)" <<out>> as EOP
DREQ -left- DMAC
HRQ -up- DMAC
HLDA -up- DMAC
DACK -right- DMAC
A -down- DMAC
DB -down- DMAC
RW -down- DMAC
EOP -down- DMAC
@enduml
```

| 引脚 | 方向 | 含义 | 为什么重要 |
|------|------|------|-----------|
| **DREQ0~3** | 外设→8237 | DMA Request：外设说"我准备好了，帮我搬数据" | 一切 DMA 传输的触发源头 |
| **HRQ** | 8237→CPU | Hold Request：8237 向 CPU 请求总线控制权 | 8237 说"你让开，我来" |
| **HLDA** | CPU→8237 | Hold Acknowledge：CPU 释放总线，交给 8237 | CPU 说"行，给你" |
| **DACK0~3** | 8237→外设 | DMA Acknowledge：8237 告诉外设"轮到你了" | 回应 DREQ，片选外设 |
| **A0~A7** | 输出 | 低 8 位地址 | 寻址内存低 256 字节内 |
| **DB0~DB7** | 双向 | 数据线；在 DMA 周期开始时分时复用输出高 8 位地址(A8~A15) | 16 位地址需要复用数据线 |
| **MEMR#/MEMW#** | 输出 | 内存读/写选通 | 8237 控制内存读写 |
| **IOR#/IOW#** | 双向 | I/O 读/写选通（CPU 编程 8237 时是输入，DMA 传输时是输出）| CPU 写寄存器 vs 8237 读外设，靠这个区分 |
| **EOP#** | 双向 | End Of Process：计数到 0 或外部强制结束 | TC 信号，驱动用它来关断外设 |

### 1.3 地址空间问题：页面寄存器

8237 只有 16 根地址线（A0~A15），只能寻址 64KB。要访问 1MB 地址空间（8086/8088），IBM PC 在 8237 外部加了一个 **74LS612 页面寄存器**——提供高 4 位（A16~A19）地址：

```bash
完整 20 位物理地址 = [页面寄存器 4bit] [8237 地址寄存器 16bit]
                  =       高位             低位
例：要传数据到物理地址 0x8A000:
  页面寄存器 = 0x8
  8237 地址寄存器 = 0xA000
  完整地址 = 0x8A000
```

> 这就是为什么老 PC 的 DMA 缓冲不能跨 64KB 边界——因为 8237 的 16 位地址自增时到 0xFFFF 会回绕到 0x0000，但页面寄存器**不会自动进位**。如果缓冲区跨了 64KB 边界，后半部分的数据会被写到一个完全错误的物理地址。

---

## 二、工作模式

每个通道可通过模式寄存器独立配置为以下几种模式之一：

### 2.1 四种传输模式

```plantuml
@startuml
skinparam shadowing false
state "IDLE\n(空闲)" as IDLE
state "SINGLE\n单次传输" as SINGLE
state "BLOCK\n块传输" as BLOCK
state "DEMAND\n按需传输" as DEMAND
state "TC\n(计数终了)" as TC
IDLE --> SINGLE : DREQ 上升沿
IDLE --> BLOCK : DREQ 上升沿
IDLE --> DEMAND : DREQ 高电平
SINGLE --> SINGLE : 传1字节→释放总线\n→等下一个DREQ
SINGLE --> TC : 计数到0
BLOCK --> BLOCK : 连续传输直到TC或EOP#
BLOCK --> TC : 计数到0
DEMAND --> DEMAND : 只要DREQ高就连续传\nDREQ变低→暂停\nDREQ再升高→继续
DEMAND --> TC : 计数到0
TC --> IDLE
note right of SINGLE : 每次传1字节就释放总线\nCPU可以在期间插入指令\n= CPU 和 DMA 交替
note right of BLOCK : 连续传完所有字节\n期间CPU完全停摆\n= 最高吞吐,最差实时性
note right of DEMAND : 外设控制节奏\nDREQ=高就一直传\nDREQ=低就暂停\n= 适合速度不固定的外设
@enduml
```

| 模式 | 传输行为 | 总线释放 | 适用场景 |
|------|---------|---------|---------|
| **单次(Single)** | DREQ 每次触发传 1 字节，传完后释放总线 | 每字节后释放 | 低速外设、需要 CPU 保持响应的场景 |
| **块(Block)** | DREQ 触发后连续传完所有字节才释放 | 全部传完后释放 | 软盘扇区读取、高速批量传输 |
| **按需(Demand)** | DREQ=高电平时连续传，变低则暂停（保持现场）| DREQ=低时释放 | 硬盘控制器，数据速率不固定 |
| **级联(Cascade)** | 不从外设传数据，用于 8237 级联 | N/A | 主片通道 4 级联从片 |

### 2.2 传输方向（通道模式字的 bit3-bit2）

| 编码 | 方向 | 信号序列 |
|------|------|---------|
| 00 | **校验(Verify)** | 不产生 MEMR#/MEMW#/IOR#/IOW#——只产生地址和 EOP#，用于"空跑测试" |
| 01 | **写传输(Write)** | IOR# → MEMW#（外设→内存） |
| 10 | **读传输(Read)** | MEMR# → IOW#（内存→外设） |
| 11 | **无效** | — |

### 2.3 优先级策略

| 策略 | 行为 |
|------|------|
| **固定优先级**（命令寄存器 bit4=1）| 通道 0 > 通道 1 > 通道 2 > 通道 3（0 最高）|
| **循环优先级**（命令寄存器 bit4=0）| 刚服务完的通道变成最低优先级，其他通道依次上升——防止某个通道"饿死" |

---

## 三、寄存器编程模型

8237 共有 **16 个端口**（主片 0xC0~0xCF，从片 0x00~0x0F，加上页面寄存器端口 0x80~0x8F）：

### 3.1 寄存器端口映射（主片）

| 端口 | 读 | 写 | 位宽 |
|------|----|----|------|
| 0xC0 | 通道 0 当前地址 | 通道 0 基地址+当前地址 | 16bit |
| 0xC1 | 通道 0 当前计数 | 通道 0 基计数+当前计数 | 16bit |
| 0xC2 | 通道 1 当前地址 | 通道 1 基地址+当前地址 | 16bit |
| 0xC3 | 通道 1 当前计数 | 通道 1 基计数+当前计数 | 16bit |
| 0xC4 | 通道 2 当前地址 | 通道 2 基地址+当前地址 | 16bit |
| 0xC5 | 通道 2 当前计数 | 通道 2 基计数+当前计数 | 16bit |
| 0xC6 | 通道 3 当前地址 | 通道 3 基地址+当前地址 | 16bit |
| 0xC7 | 通道 3 当前计数 | 通道 3 基计数+当前计数 | 16bit |
| 0xC8 | **状态寄存器**（每个通道的 TC 和请求状态）| **命令寄存器** | 8bit |
| 0xC9 | — | **请求寄存器**（软件触发 DREQ）| 8bit |
| 0xCA | — | **单通道屏蔽寄存器** | 8bit |
| 0xCB | — | **模式寄存器** | 8bit |
| 0xCC | — | **清除字节指针触发器** | 8bit |
| 0xCD | — | **主清除(硬件复位)** | 8bit |
| 0xCE | — | **清除屏蔽寄存器(全部通道)** | 8bit |
| 0xCF | — | **写全体屏蔽寄存器** | 8bit |

> **关键**：16 位寄存器（地址/计数）需要分两次写入——先写低字节、再写高字节。内部有一个**字节指针触发器(Byte Pointer Flip-Flop)**在每次写入后翻转。0xCC 端口用来把这个触发器清零，确保"低字节→高字节"的顺序从低字节开始。

### 3.2 关键寄存器位域详解

**命令寄存器（0xC8 写，8bit）**：

```bash
bit7: DACK# 极性   (1=高有效, 0=低有效)
bit6: DREQ 极性    (1=高有效, 0=低有效)
bit5: 写选择       (1=扩展写, 0=滞后写)
bit4: 优先级       (1=固定, 0=循环)
bit3: 时序         (1=压缩时序, 0=正常时序)
bit2: 使能控制器   (1=禁止, 0=允许)
bit1: 通道0地址保持(1=禁止, 0=允许——用于内存到内存传输)
bit0: 内存到内存   (1=允许, 0=禁止——用通道0和通道1)
```

**模式寄存器（0xCB 写，8bit）**：

```bash
bit7-6: 模式选择   00=按需  01=单次  10=块  11=级联
bit5:   地址增减   1=减, 0=增
bit4:   自动初始化  1=允许(TC后自动重装基值), 0=禁止
bit3-2: 传输方向   00=校验  01=写(IOR→MEMW)  10=读(MEMR→IOW)
bit1-0: 通道选择   00=CH0  01=CH1  10=CH2  11=CH3
```

**状态寄存器（0xC8 读，8bit）**：

```bash
bit7: 通道3 请求状态  (1=DREQ3 活跃)
bit6: 通道2 请求状态
bit5: 通道1 请求状态
bit4: 通道0 请求状态
bit3: 通道3 TC 状态   (1=该通道计数到0)
bit2: 通道2 TC 状态
bit1: 通道1 TC 状态
bit0: 通道0 TC 状态
```

### 3.3 编程一个 DMA 传输的标准步骤

```bash
步骤 1: CLI (关中断——配置期间不被打断)
步骤 2: 写 0xCD → 主清除(Master Reset)，复位所有内部状态
步骤 3: 写 0x0A → 屏蔽寄存器：屏蔽该通道(禁止DREQ响应)
步骤 4: 写模式字 → 0xCB (设置传输模式+方向+通道号)
步骤 5: 写 0x0C → 清除字节指针触发器(确保低字节在前)
步骤 6: 写基地址低字节 → 通道端口
步骤 7: 写基地址高字节 → 通道端口
步骤 8: 写页面寄存器 → 0x80+通道号
步骤 9: 写基计数低字节 → 通道端口
步骤10: 写基计数高字节 → 通道端口
步骤11: 写 0x02 → 单通道屏蔽寄存器：解除屏蔽(允许DREQ)
步骤12: STI (开中断)
步骤13: 等待外设拉高 DREQ...(传输开始)
步骤14: 传输完成后，CPU 通过中断或轮询状态寄存器检测 TC，处理数据
```

---

## 四、时序图

一次单字节 DMA 写传输（外设→内存）的完整总线周期：

```plantuml
@startuml
skinparam shadowing false
skinparam defaultFontSize 10
actor "外设" as DEV
participant "8237 DMA" as DMA
participant "CPU 总线" as BUS
== S0: 请求 ==
DEV -> DMA: DREQ↑ 请求DMA传输
DMA -> BUS: HRQ↑ 请求总线控制权
Note over DMA: 仲裁: 多通道同时请求时决定优先级
== S1: 握手机制 ==
BUS -> DMA: HLDA↑ 释放总线
DMA -> DEV: DACK↓ 选中外设
Note over DMA: 输出地址: A0-A7 低位\nDB复用输出 A8-A15 高位
== S2: 读外设 ==
DMA -> DEV: IOR↓ 读外设数据
DEV --> DMA: 数据放到数据总线
== S3: 写内存 ==
DMA -> DMA: MEMW↓ 数据写入内存
Note right: 地址+1, 计数-1\n若TC则触发EOP
== S4: 释放 ==
DEV -> DMA: DREQ↓ (单次模式, 一字节就释放)
DMA -> BUS: HRQ↓ 释放总线, 回到S1
Note over BUS: CPU 收回总线\n继续运行
@enduml
```

**逐步分解**：

| 阶段 | 外设 | 8237 DMA | CPU/总线 | 细节 |
|------|------|----------|---------|------|
| **@0** | 空闲 | S1: 空闲等待 | 正常运行 | — |
| **@1** | 拉高 DREQ | S0: 采样 DREQ，仲裁，拉高 HRQ 向 CPU 要总线 | 正常运行 | `DREQ 建立 → HRQ 有效: ~1 CLK` |
| **@2** | — | S1: 收到 HLDA，A0~A7 输出低地址，DB 复用输出 A8~A15 | 完成当前总线周期，拉高 HLDA 释放总线 | `HRQ → HLDA: 最多几 μs`（等 CPU 交出） |
| **@3** | 等待被选中 | S2: 拉低 DACK# 选中外设，发 IOR# 读外设数据到 DB | 8237 控制 | 读选通阶段 |
| **@4** | 数据放到总线上 | S3: 发 MEMW# 把 DB 上的数据写入内存；地址+1，计数-1 | 8237 控制 | 写选通阶段 |
| **@5** | 拉低 DREQ（单次模式，一字节就释放） | S4: 若 TC 则拉 EOP#；释放 HRQ，总线交回 CPU | 收回总线，继续运行 | 一次传输结束 |
| **@6~7** | 回到空闲 | 回到 S1 空闲，等下次 DREQ | 正常运行 | — |

**DMA 周期 S 状态总览**：

| 状态 | 动作 | CLK |
|------|------|-----|
| **S0** | 空闲，采样 DREQ，仲裁 | ≥1 |
| **S1** | A0~A7 输出地址，DB 输出 A8~A15 | 1 |
| **S2** | 拉 DACK#，发读选通（IOR# 或 MEMR#） | 1 |
| **S3** | 发写选通（MEMW# 或 IOW#），数据锁入目标 | 1 |
| **S4** | 撤销选通，检测 TC，释放总线 | 1 |

> **正常时序**：S0→S1→S2→S3→S4（每字节 4 CLK）。**压缩时序**（命令寄存器 bit3=1）：跳过 S3，S0→S1→S2→S4（每字节 3 CLK），快 25%，但要求外设能在 2 CLK 内响应。

---

## 五、Demo 1：内存到内存传输

> 这是 8237 的特有功能——通过通道 0 和通道 1 配合实现内存→内存 DMA（现代 DMA 都没有这个能力，因为太慢且用途极窄）。通道 0 的地址寄存器存源地址，通道 1 存目标地址，通道 1 的计数寄存器存传输长度。

**场景**：把物理地址 0x90000 处的 1024 字节搬到物理地址 0xA0000。

```asm
; ==========================================
; Demo 1: 8237 内存到内存 DMA 传输
; 环境: IBM PC/AT, DOS 实模式
; 假设: 8237 主片端口基址 = 0xC0
;       页面寄存器端口 = 0x80+通道号
;       (页面寄存器不在 8237 内部, 在主板 I/O 芯片上)
; ==========================================
    CLI                         ; 关中断
    ; --- 步骤1: 主清除 ---
    MOV  AL, 0x00               ; 写到 0xCD 触发 Master Reset
    OUT  0xCD, AL               ; 任意值都可以，清除所有内部状态
    ; --- 步骤2: 设置命令寄存器 ---
    ; bit0=1: 开启内存到内存模式
    ; bit1=0: 通道0地址允许变化(源地址每次+1)
    ; bit2=1: 使能控制器
    ; bit3=0: 正常时序
    ; bit4=1: 固定优先级
    ; bit5=0: 滞后写
    ; bit6=0: DREQ 高有效(内存到内存不用DREQ)
    ; bit7=0: DACK 低有效
    MOV  AL, 0x05               ; 0000_0101 = MEM2MEM ON + 控制器使能
    OUT  0xC8, AL               ; 命令寄存器
    ; --- 步骤3: 设置通道0 模式寄存器(源) ---
    ; bit7-6=10: 块传输模式
    ; bit5=0:    地址递增
    ; bit4=0:    禁止自动初始化
    ; bit3-2=10: 读传输(MEMR→IOW,  源→8237缓存)
    ; bit1-0=00: 通道0
    MOV  AL, 0x88               ; 1000_1000
    OUT  0xCB, AL
    ; --- 步骤4: 设置通道1 模式寄存器(目标) ---
    ; bit7-6=10: 块传输模式
    ; bit5=0:    地址递增
    ; bit4=0:    禁止自动初始化
    ; bit3-2=01: 写传输(IOR→MEMW,  8237缓存→目标)
    ; bit1-0=01: 通道1
    MOV  AL, 0x85               ; 1000_0101
    OUT  0xCB, AL
    ; --- 步骤5: 清除字节指针 ---
    OUT  0xCC, AL               ; 写入 0xCC 清除字节指针触发器
    ; --- 步骤6: 写通道0 源地址 0x9000 ---
    MOV  AX, 0x9000
    OUT  0xC0, AL               ; 地址低字节
    MOV  AL, AH
    OUT  0xC0, AL               ; 地址高字节
    ; --- 步骤7: 写通道0 页面寄存器 (A16~A19 = 0x9→完整地址0x90000) ---
    MOV  AL, 0x09
    OUT  0x87, AL               ; 页面寄存器 通道0 = 端口 0x87
    ; --- 步骤8: 写通道1 目标地址 0xA000 ---
    MOV  AX, 0xA000
    OUT  0xC2, AL               ; 地址低字节
    MOV  AL, AH
    OUT  0xC2, AL               ; 地址高字节
    ; --- 步骤9: 写通道1 页面寄存器 (→完整地址0xA0000) ---
    MOV  AL, 0x0A
    OUT  0x83, AL               ; 页面寄存器 通道1 = 端口 0x83
    ; --- 步骤10: 写通道1 计数寄存器 = 1024 ---
    ; 注意：计数=N-1——传1024字节应写1023
    MOV  AX, 1023
    OUT  0xC3, AL               ; 计数低字节
    MOV  AL, AH
    OUT  0xC3, AL               ; 计数高字节
    ; --- 步骤11: 解除通道1 屏蔽(软件触发传输) ---
    ; 内存到内存模式：解除通道1屏蔽会自动触发传输，
    ; 不需要外部DREQ。8237 内部把通道0的 DREQ 连到固定逻辑。
    MOV  AL, 0x01               ; bit1-0=01 → 解除通道1屏蔽
    OUT  0xCA, AL
    ; --- 步骤12: 轮询等待完成 ---
    ; 块传输模式下8237 会一直传完所有字节才释放总线。
    ; 然后检查状态寄存器的 TC 位确认传输成功。
.transfer_loop:
    IN   AL, 0xC8               ; 读状态寄存器
    TEST AL, 0x02               ; bit1 = 通道1 TC
    JZ   .transfer_loop         ; TC=0→还没完成，继续等
    ; 传输完成！1024 字节从 0x90000 搬到了 0xA0000
    STI
    ; ... 后续处理 ...
```

**内存到内存传输的内部机制**：

```bash
传1字节的内部循环(8237 自动执行,无需软件介入):
1. 通道0 读: 发 MEMR# 从 0x90000 读 1 字节 → 8237 内部暂存器
2. 通道1 写: 发 MEMW# 从 8237 内部暂存器写 1 字节 → 0xA0000
3. 通道0 地址+1, 通道1 地址+1, 通道1 计数-1
4. 重复直到计数=0
每个字节需要 8 个 CLK (两次完整 DMA 周期, 每次 S1-S4):
  DMA 周期 #1(S1-S4): MEM读 (通道0做"读传输")
  DMA 周期 #2(S1-S4): MEM写 (通道1做"写传输")
```

---

## 六、Demo 2：外设到内存（软盘控制器读扇区）

**场景**：软盘控制器（FDC，一个典型外设）要从磁盘读出一个 512 字节扇区，通过 DMA 写入物理地址 0x80000 处的缓冲区。软盘控制器的 DREQ 线连接到通道 2。

```asm
; ==========================================
; Demo 2: 8237 外设到内存 DMA (软盘读扇区)
; 通道2: 软盘控制器 → 内存(物理 0x80000)
; ==========================================
    CLI
    ; --- 步骤1: 主清除 ---
    OUT  0xCD, AL
    ; --- 步骤2: 设置命令寄存器 ---
    ; 正常时序、固定优先级、DREQ高有效
    MOV  AL, 0x40               ; 0100_0000=正常时序+固定优先级+控制器使能
    OUT  0xC8, AL
    ; --- 步骤3: 屏蔽通道2(先禁掉,配完再开) ---
    MOV  AL, 0x02               ; bit1-0=10 → 置位通道2屏蔽
    OUT  0xCA, AL
    ; --- 步骤4: 设置通道2 模式寄存器 ---
    ; bit7-6=10: 块传输 (软盘数据连续到达,一次传完)
    ; bit5=0:    地址递增
    ; bit4=0:    不自动初始化
    ; bit3-2=01: 写传输 (外设→内存: IOR#→MEMW#)
    ; bit1-0=10: 通道2
    MOV  AL, 0x86               ; 1000_0110
    OUT  0xCB, AL
    ; --- 步骤5: 清除字节指针 ---
    OUT  0xCC, AL
    ; --- 步骤6: 写目标地址 0x8000 (DMA会在内存侧放到0x80000) ---
    MOV  AX, 0x8000
    OUT  0xC4, AL               ; 地址低字节 (端口0xC4=通道2)
    MOV  AL, AH
    OUT  0xC4, AL               ; 地址高字节
    ; --- 步骤7: 页面寄存器通道2 = 0x08 (→完整地址 0x80000) ---
    MOV  AL, 0x08
    OUT  0x82, AL               ; 页面寄存器 通道2=端口0x82
    ; --- 步骤8: 写传输计数 = 512-1 = 511 ---
    MOV  AX, 511
    OUT  0xC5, AL               ; 计数低字节 (端口0xC5=通道2)
    MOV  AL, AH
    OUT  0xC5, AL               ; 计数高字节
    ; --- 步骤9: 解除通道2屏蔽 ---
    MOV  AL, 0x02               ; bit1=0, bit0=0? 需要清除屏蔽
    OUT  0xCA, AL               ; 单通道解除屏蔽
    ; --- 至此8237 准备好, 等 FDC 拉高 DREQ2 ---
    ; --- 步骤10: 编程软盘控制器(告诉它"读扇区", 它会拉DREQ) ---
    ; (此处省略 FDC 寄存器编程细节——不在 8237 范围内)
    ; 典型 FDC 命令序列:
    ;  - 写 FDC 数据寄存器: 磁头号/柱面号/扇区号
    ;  - 写 FDC 命令寄存器: READ DATA
    ;  - FDC 内部电机启动→寻道→磁头读取
    ;  - 每准备好一个字节: FDC 拉高 DREQ2
    ;
    ;  8237 看到 DREQ2→HRQ→HLDA→DACK2→IOR#→MEMW#
    ;  —每个DMA周期传1字节, 共512次
    STI
    ; --- 实际驱动中会设置中断处理 ---
    ; 当计数到0, 8237 拉低 EOP#, 同时 FDC 检测到 TC
    ; FDC 发 IRQ6 → CPU 进中断 handler → 后续处理
    ; 也可以轮询:
.wait_dma:
    IN   AL, 0xC8               ; 读状态寄存器
    TEST AL, 0x20               ; bit5 = 通道2 TC
    JZ   .wait_dma              ; 等TC
    ; DMA 完成
```

**时序分解（外设→内存，单字节）**：

```plantuml
@startuml
skinparam shadowing false
skinparam defaultFontSize 10
actor "FDC 软驱" as FDC
participant "8237 DMA" as DMA
participant "内存" as MEM
participant "CPU" as CPU
== 第1字节 ==
FDC -> DMA: DREQ2↑ 扇区数据就绪
DMA -> CPU: HRQ↑ 请求总线 (S0)
CPU -> DMA: HLDA↑ 释放总线 (S1: 输出地址A0-A15)
DMA -> FDC: DACK2↓ 选中FDC
DMA -> FDC: IOR↓ 读FDC数据到DB (S2)
FDC --> DMA: 扇区字节
DMA -> MEM: MEMW↓ 数据写内存 (S3)
Note over DMA: 地址+1, 计数-1 (S4)
DMA -> CPU: HRQ↓ 释放总线
== 第2字节 ==
FDC -> DMA: DREQ2↑ 下一字节就绪
Note over DMA, CPU: 重复上述流程\n直到计数=0, TC触发
@enduml
```

---

## 七、Demo 3：级联模式（主片通道 4 级联从片）

PC/AT 用两片 8237 提供 7 个可用通道。主片通道 4 设为**级联模式**（不实际传输数据），从片的 HRQ/HLDA 接到主片的 DREQ4/DACK4。

```asm
; ==========================================
; Demo 3: 级联配置——主片通道4设成级联模式
; 主片: 0xC0~0xCF
; 从片: 0x00~0x0F (AT 架构中从片端口在低位)
;
; 注意: 从片只支持16-bit传输(因为接在AT的16-bit ISA总线上)
;       从片的地址线 A16 对应系统地址线 A17
;       (从片地址左移1位 = 系统地址)
; ==========================================
    CLI
    ; ---------- 主片: 设通道4为级联模式 ----------
    MOV  AL, 0xC0               ; 11xx_xx00 = 级联+通道4
    OUT  0xCB, AL               ; 主片模式寄存器(0xCB)
    ; 级联通道不需要写地址/计数寄存器
    ; 解除屏蔽后, 主片会把 DREQ4→HRQ(从片) 传上来
    MOV  AL, 0x04
    OUT  0xCA, AL               ; 解除通道4屏蔽(实际上级联模式也不check DREQ)
    ; ---------- 从片: 设置通道5(DMA Ch5→16-bit传输) ----------
    ; --- 主清除从片 ---
    OUT  0x0D, AL               ; 从片主清除(端口0x0D = 0xCD-0xC0)
    ; --- 设置从片命令寄存器 ---
    MOV  AL, 0x40               ; 正常时序,固定优先级,使能
    OUT  0x08, AL               ; 从片命令寄存器(0x08 = 0xC8-0xC0)
    ; --- 屏蔽通道5 ---
    MOV  AL, 0x05               ; 屏蔽通道5
    OUT  0x0A, AL
    ; --- 设置通道5模式 ---
    ; 块传输,地址递增,写传输(外设→内存),通道5
    MOV  AL, 0x89               ; 1000_1001
    OUT  0x0B, AL               ; 从片模式寄存器(0x0B=0xCB-0xC0)
    ; --- 清除字节指针 ---
    OUT  0x0C, AL
    ; --- 写地址: 物理地址 0x120000 (注意对齐!) ---
    ; 从片16-bit传输, 地址必须偶对齐
    ; 从片地址寄存器 = 系统地址右移1位
    ; 从片 16-bit 地址 = 0x120000 >> 1 = 0x90000 >> 16bit
    ; → 但从片只有16位地址, 所以物理地址 = 从片地址 << 1
    ; 设从片地址寄存器 = 0x9000 → 物理地址 0x12000
    ;
    ; 实际上 16-bit 从片的地址空间是 0x00000~0x1FFFFE
    ; (17位系统地址 = 从片16位地址 << 1)
    ; 页面寄存器提供更高的地址位
    MOV  AX, 0x9000
    OUT  0x04, AL               ; 从片通道5地址低字节
    MOV  AL, AH
    OUT  0x04, AL               ; 从片通道5地址高字节
    ; --- 页面寄存器通道5 ---
    MOV  AL, 0x01               ; 物理地址 0x12000 + 页面0x01 → 0x12000
    OUT  0x8B, AL               ; 页面寄存器通道5=端口0x8B
    ; --- 写计数 = 1024-1 ---
    MOV  AX, 1023
    OUT  0x05, AL               ; 从片通道5计数低字节
    MOV  AL, AH
    OUT  0x05, AL               ; 从片通道5计数高字节
    ; --- 解除通道5屏蔽 ---
    MOV  AL, 0x05
    OUT  0x0A, AL
    STI
    ; 等待外设拉高 DREQ5 → 从片仲裁 → 从片HRQ
    ; → 主片DREQ4(级联) → 主片仲裁 → 主片HRQ
    ; → CPU HLDA → 主片DACK4 → 从片HLDA → 从片DACK5 → 传输
```

**级联模式的握手链**：

```bash
外设 → DREQ5 → 从片8237 仲裁
                  → 从片 HRQ → 主片 DREQ4 (级联) → 主片仲裁
                                                      → 主片 HRQ
                                                      → CPU HLDA
                                                      → 主片 DACK4
                  ← 从片 HLDA ←──────────────────────┘
                  → 从片 DACK5 → 外设
                  → 从片驱动地址/控制信号(IOR#, MEMW#...) → 总线
```

---

## 八、8237 vs 现代 DMA 的对比

| 维度 | 8237 (1980s) | 现代 Bus Mastering DMA (2020s) |
|------|-------------|------------------------------|
| **位置** | 主板上独立芯片，所有外设共用 | 每个外设自带 DMA 引擎（在设备芯片内） |
| **通道数** | 4 个（级联可扩展）| 理论上无限（每个设备自己管自己） |
| **触发方式** | 硬件 DREQ 引脚（电平/边沿）| 软件写描述符 + 门铃寄存器 |
| **地址空间** | 16 位+页面寄存器→最多 16MB | 64 位物理地址（IOMMU 翻译） |
| **传输长度** | 单次最大 64KB（16 位计数器）| 描述符->长度可 32/64 位 |
| **传输模式** | 单次/块/按需/级联 | SG-DMA(分散-聚集描述符链表/环形) |
| **总线** | ISA(8/16bit, ~8MHz) | PCIe(串行, ~16GT/s per lane) |
| **带宽** | ~1.6 MB/s 理论峰值 | PCIe Gen5 x16 = ~64 GB/s |
| **一致性** | 无——CPU 必须手动 flush/invalidate cache | 硬件缓存一致性或内核 DMA API 自动处理 |
| **配置方式** | 写 I/O 端口(out 指令) | 构造描述符→写 MMIO 寄存器→门铃 |
| **完成通知** | EOP# 引脚 + 中断 | MSI/MSI-X 中断，每描述符可选 |
| **编程模型** | 寄存器直写 | 共享内存描述符环 + Head/Tail 指针 |

> **一句话**：8237 是"寄存器直写→外设拉 DREQ→8237 仲裁→总线传输"的硬件同步模型。现代 DMA 是"驱动写描述符环→设备读描述符→设备发起 PCIe 传输→设备写回 completion"的**共享内存异步模型**。两者的共同基因：**告诉硬件"从哪取、搬到哪、搬多少、搬完通知我"——四个参数，四十年没变。**

---

## 九、一句话总结

> **8237 用 4 个独立通道把"源地址、目标地址、传输长度、传输模式"抽象为可编程寄存器：模式寄存器决定怎么搬（单次/块/按需/级联），命令寄存器决定全局行为（优先级/DREQ极性/压缩时序），页面寄存器补足 16 位地址到 20 位。外设拉 DREQ 启动一次传输，8237 经历 S0→S4 五状态完成一次总线周期（IOR#→MEMW# 或反向），地址自增+计数自减直到 TC。它的编程模型——通道×4、地址+计数寄存器对、优先级仲裁——穿透四十年，仍然是现代 DMA 引擎的底层基因。**

---

*相关：DMA 原理总纲见 [dma.md](/concepts/io/dma.md)，现代 Bus Mastering DMA 见 [dma.md §二](/concepts/io/dma.md)，SG-DMA 描述符模型见 [dma.md §四](/concepts/io/dma.md)，DMA Cache 一致性见 [dma.md §三](/concepts/io/dma.md)，CPU 与 DMA 控制器的交互细节见 [dma-cpu-interaction.md](/concepts/io/dma-cpu-interaction.md)。*

