Intel 8237 DMA 控制器 —— 工作原理、编程接口与实例
现代 DMA 早已从独立控制器芯片演化为外设自带的 Bus Mastering 引擎(见 dma §二),但理解 8237 是理解所有 DMA 的基础——它定义了 DMA 的核心抽象:通道、地址寄存器、计数寄存器、传输模式、总线仲裁。即使今天的 NVMe 驱动在初始化描述符环时,骨子里还是那套"告诉硬件从哪搬、搬多少、搬完通知我"的逻辑。本篇细致拆解 8237 的硬件架构、寄存器编程模型、时序、以及三个可跑的汇编级 demo。
零、8237 在历史上的位置
8237 是 Intel 在 1980 年代推出的可编程 DMA 控制器,几乎是所有 IBM PC/AT 及兼容机的标配。一块 8237 提供 4 个独立 DMA 通道;PC/AT 用两片级联(主片+从片)提供 7 个通道(通道 4 用于级联)。
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 内部框图
关键部件说明:
| 部件 | 功能 |
|---|---|
| 定时与控制 | 产生 MEMR#/MEMW#/IOR#/IOW# 总线控制信号,控制传输时序 |
| 优先级仲裁器 | 多个通道同时请求 DMA 时决定谁先拿到总线,支持固定和循环两种策略 |
| 命令寄存器 | 全局配置:DREQ/DACK 极性、是否允许内存到内存、是否允许固定优先级等 |
| 模式寄存器 | 每通道独立:传输方向/自动初始化/地址增减/传输模式 |
| 地址寄存器(每通道×16bit) | 存放当前传输的源/目标地址。注意只有 16 位——高 8 位由外部页面寄存器(74LS612)提供 |
| 计数寄存器(每通道×16bit) | 存放要传输的字节数,每传一个字节减 1,减到 0xFFFF→0x0000 时触发 TC(Terminal Count) |
1.2 关键引脚
| 引脚 | 方向 | 含义 | 为什么重要 |
|---|---|---|---|
| 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)地址:
完整 20 位物理地址 = [页面寄存器 4bit] [8237 地址寄存器 16bit]
= 高位 低位
例:要传数据到物理地址 0x8A000:
页面寄存器 = 0x8
8237 地址寄存器 = 0xA000
完整地址 = 0x8A000这就是为什么老 PC 的 DMA 缓冲不能跨 64KB 边界——因为 8237 的 16 位地址自增时到 0xFFFF 会回绕到 0x0000,但页面寄存器不会自动进位。如果缓冲区跨了 64KB 边界,后半部分的数据会被写到一个完全错误的物理地址。
二、工作模式
每个通道可通过模式寄存器独立配置为以下几种模式之一:
2.1 四种传输模式

| 模式 | 传输行为 | 总线释放 | 适用场景 |
|---|---|---|---|
| 单次(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):
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):
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):
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 传输的标准步骤
步骤 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 写传输(外设→内存)的完整总线周期:

逐步分解:
| 阶段 | 外设 | 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。
; ==========================================
; 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
; ... 后续处理 ...内存到内存传输的内部机制:
传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。
; ==========================================
; 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 完成时序分解(外设→内存,单字节):

七、Demo 3:级联模式(主片通道 4 级联从片)
PC/AT 用两片 8237 提供 7 个可用通道。主片通道 4 设为级联模式(不实际传输数据),从片的 HRQ/HLDA 接到主片的 DREQ4/DACK4。
; ==========================================
; 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 → 传输级联模式的握手链:
外设 → 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,现代 Bus Mastering DMA 见 dma §二,SG-DMA 描述符模型见 dma §四,DMA Cache 一致性见 dma §三,CPU 与 DMA 控制器的交互细节见 dma-cpu-interaction。