设计并实现基础DMA(Direct Memory Access)控制器,允许外设直接读写内存而不需要CPU介入。DMA是高速数据传输的关键——VGA显示刷新、批量数据搬运都依赖它。
没有DMA时,所有数据传输都需要CPU参与。例如,将VGA缓冲区的2000字节发送到显示器,CPU需要执行2000次LDA+STA,占用几千个周期。有了DMA,这个过程完全由硬件自动完成,CPU可以去做别的事情。
// ========================================
// dma_ctrl.v - DMA控制器(基础版)
// Retro8 复古电脑 存储子系统
// ✅Verilator验证通过
// ========================================
module dma_ctrl (
input wire clk,
input wire rst_n,
// CPU配置接口
input wire [15:0] src_addr, // 源地址
input wire [15:0] dst_addr, // 目标地址
input wire [15:0] xfer_count, // 传输字节数
input wire xfer_start, // 启动传输
input wire xfer_mode, // 0=单次, 1=自动重装
output wire xfer_done, // 传输完成
output wire xfer_busy, // 传输中
// 总线接口(与CPU共享)
output reg [15:0] dma_addr,
output reg [7:0] dma_wdata,
output reg dma_rd,
output reg dma_wr,
input wire [7:0] dma_rdata,
output wire dma_request, // 请求总线
input wire dma_grant // 总线授权
);
// DMA状态机
localparam IDLE = 3'd0;
localparam REQ = 3'd1;
localparam READ = 3'd2;
localparam WRITE = 3'd3;
localparam NEXT = 3'd4;
localparam DONE_ST = 3'd5;
reg [2:0] state;
reg [15:0] src_ptr, dst_ptr, count;
reg [7:0] hold_data; // 读到的暂存数据
assign dma_request = (state == REQ);
assign xfer_busy = (state != IDLE);
assign xfer_done = (state == DONE_ST);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
src_ptr <= 16'd0;
dst_ptr <= 16'd0;
count <= 16'd0;
end else begin
case (state)
IDLE: if (xfer_start) begin
src_ptr <= src_addr;
dst_ptr <= dst_addr;
count <= xfer_count;
state <= REQ;
end
REQ: if (dma_grant) state <= READ;
READ: begin
dma_addr <= src_ptr;
dma_rd <= 1'b1;
hold_data <= dma_rdata;
state <= WRITE;
end
WRITE: begin
dma_rd <= 1'b0;
dma_addr <= dst_ptr;
dma_wdata <= hold_data;
dma_wr <= 1'b1;
state <= NEXT;
end
NEXT: begin
dma_wr <= 1'b0;
src_ptr <= src_ptr + 16'd1;
dst_ptr <= dst_ptr + 16'd1;
count <= count - 16'd1;
if (count == 16'd1)
state <= DONE_ST;
else
state <= READ;
end
DONE_ST: begin
if (xfer_mode) begin
// 自动重装模式:重新开始
src_ptr <= src_addr;
dst_ptr <= dst_addr;
count <= xfer_count;
state <= REQ;
end else
state <= IDLE;
end
default: state <= IDLE;
endcase
end
end
endmodule
CPU和DMA不能同时访问内存。总线仲裁器决定谁获得控制权:
我们的实现采用CPU优先策略:DMA请求总线后,等待CPU当前内存操作完成才获得控制权。这保证了CPU的响应延迟不受DMA影响。
VGA控制器需要每帧从缓冲区读取2000字节。如果用DMA自动完成:
// 配置DMA用于VGA刷新
// 源: VGA缓冲区 $7800
// 目标: VGA控制器数据口
// 计数: 2000
// 模式: 自动重装(每帧重复)
LDI R1, #$78
STA R1, $FF10 // DMA源地址高
LDI R1, #$00
STA R1, $FF11 // DMA源地址低
LDI R1, #$FF
STA R1, $FF12 // DMA目标地址高
LDI R1, #$20
STA R1, $FF13 // DMA目标地址低
LDI R1, #$D0
STA R1, $FF14 // 计数高8位(2000=$07D0)
LDI R1, #$07
STA R1, $FF15 // 计数低8位
LDI R1, #$01
STA R1, $FF16 // 模式=自动重装
LDI R1, #$01
STA R1, $FF17 // 启动DMA
计算DMA传输2000字节的吞吐量:每次传输需要几个周期?在10MHz时钟下,传输速率是多少字节/秒?与CPU轮询方式相比快了多少?
设计链式DMA:当一次传输完成后,自动加载下一组参数开始新传输。这在多缓冲区切换(双缓冲)中非常有用。
当多个DMA通道同时请求时怎么办?设计一个4通道DMA控制器,实现优先级仲裁和通道调度。
达成条件:
奖励:内存系统阶段完成!你的8位电脑现在有了完整的存储层次:ROM、RAM、MMIO、VGA缓冲、键盘FIFO和DMA。接下来是I/O外设阶段!
双缓冲是DMA最常见的优化模式——当一个缓冲区被DMA传输时,CPU可以处理另一个缓冲区:
// 双缓冲DMA实现
reg current_buf; // 0=A, 1=B
always @(posedge clk) begin
if (xfer_done) begin
current_buf <= ~current_buf; // 切换缓冲区
// 重新配置DMA源地址
if (current_buf)
src_ptr <= BUF_A_ADDR;
else
src_ptr <= BUF_B_ADDR;
end
end
| 模式 | 描述 | 总线占用 | 延迟 |
|---|---|---|---|
| 突发传输 | 连续占用总线直到完成 | 高(CPU被冻结) | 最低 |
| 周期窃取 | 在CPU不访问时偷用周期 | 低 | 中等 |
| 透明DMA | 只在CPU的空闲周期传输 | 零(对CPU透明) | 最高 |
| 分时复用 | CPU和DMA严格交替 | 50% | 可预测 |
经典8位电脑中的DMA应用:
Retro8的DMA主要用于VGA缓冲区刷新和批量内存搬移。