实现内存映射I/O(MMIO)桥接模块,将I/O设备映射到内存地址空间$FF00-$FFFF。MMIO是CPU与外设通信的桥梁——通过普通的内存读写指令就能控制外设。
内存映射I/O(Memory-Mapped I/O)将I/O设备的寄存器映射到内存地址空间。CPU不需要特殊的I/O指令,用普通的LDA/STA就能读写设备寄存器。这种设计简化了CPU,也是现代ARM架构的标准做法。
// ========================================
// mmio.v - 内存映射I/O桥接模块
// Retro8 复古电脑 存储子系统
// ✅Verilator验证通过
// ========================================
module mmio (
input wire clk,
input wire rst_n,
input wire [15:0] addr,
input wire [7:0] cpu_data_out,
input wire cpu_rd,
input wire cpu_wr,
// UART接口
input wire [7:0] uart_rx_data,
input wire uart_tx_empty,
input wire uart_rx_ready,
output wire [7:0] uart_tx_data,
output wire uart_tx_start,
// 键盘接口
input wire [7:0] kbd_data,
input wire kbd_valid,
// 定时器接口
input wire [15:0] timer_count,
output wire [7:0] timer_ctrl,
// GPIO接口
output wire [7:0] gpio_out,
input wire [7:0] gpio_in,
output wire [7:0] gpio_dir,
// CPU读回数据
output reg [7:0] cpu_data_in,
output wire mmio_select // 地址在MMIO范围
);
assign mmio_select = (addr >= 16'hFF00);
// I/O寄存器
reg [7:0] gpio_data_reg;
reg [7:0] gpio_dir_reg;
reg [7:0] timer_ctrl_reg;
reg [7:0] vga_cursor_lo;
reg [7:0] vga_attr_reg;
// 写操作解码
wire [7:0] port = addr[7:0];
always @(posedge clk) begin
if (!rst_n) begin
gpio_data_reg <= 8'd0;
gpio_dir_reg <= 8'd0;
timer_ctrl_reg <= 8'd0;
vga_cursor_lo <= 8'd0;
vga_attr_reg <= 8'h0F; // 白色前景,黑色背景
end else if (cpu_wr && mmio_select) begin
case (port)
8'h00: begin // UART TX
end
8'h06: timer_ctrl_reg <= cpu_data_out;
8'h07: gpio_data_reg <= cpu_data_out;
8'h08: gpio_dir_reg <= cpu_data_out;
8'h0B: vga_cursor_lo <= cpu_data_out;
8'h0C: vga_attr_reg <= cpu_data_out;
default: ;
endcase
end
end
// 读操作解码
always @(*) begin
if (cpu_rd && mmio_select) begin
case (port)
8'h00: cpu_data_in = uart_rx_data;
8'h01: cpu_data_in = {6'd0, uart_rx_ready, uart_tx_empty};
8'h02: cpu_data_in = kbd_data;
8'h03: cpu_data_in = {7'd0, kbd_valid};
8'h04: cpu_data_in = timer_count[7:0];
8'h05: cpu_data_in = timer_count[15:8];
8'h06: cpu_data_in = timer_ctrl_reg;
8'h07: cpu_data_in = gpio_in;
8'h08: cpu_data_in = gpio_dir_reg;
8'h0B: cpu_data_in = vga_cursor_lo;
8'h0C: cpu_data_in = vga_attr_reg;
default: cpu_data_in = 8'hFF;
endcase
end else begin
cpu_data_in = 8'hFF;
end
end
// 输出连接
assign uart_tx_data = cpu_data_out;
assign uart_tx_start = cpu_wr && mmio_select && (port == 8'h00);
assign timer_ctrl = timer_ctrl_reg;
assign gpio_out = gpio_data_reg;
assign gpio_dir = gpio_dir_reg;
endmodule
| 特性 | MMIO | 独立I/O (x86) |
|---|---|---|
| 指令 | LDA/STA | IN/OUT (专用) |
| 地址空间 | 统一内存空间 | 独立I/O空间 |
| 优点 | 简单,所有指令可用 | I/O不占内存空间 |
| 缺点 | 占用内存地址 | 需要额外指令和引脚 |
| 典型架构 | ARM, MIPS, 6502 | x86 (但也支持MMIO) |
在MMIO映射中添加一个SPI控制器($FF10-$FF13),包含数据、时钟、片选和控制寄存器。
MMIO寄存器经常需要位操作(设置/清除某一位)。但读-改-写不是原子的。设计一个支持位设置的MMIO寄存器:写入时,bit7=1表示设置,bit7=0表示清除,bit6:0是位掩码。
有些外设比CPU慢,需要插入等待状态。修改MMIO模块,当访问慢速设备时自动插入1-3个等待周期。
达成条件:
奖励:你架起了CPU与外设的桥梁!CPU现在可以"看到"外部世界——键盘、串口、GPIO,一切皆可控制。
MMIO寄存器与普通RAM不同——读写可能有副作用:
这意味着编译器不能对MMIO访问做优化(如合并两次读、消除"无用"读等)。在C语言中,MMIO寄存器必须声明为volatile:
// C语言中的MMIO寄存器声明
volatile uint8_t* uart_data = (uint8_t*)0xFF00;
volatile uint8_t* uart_status = (uint8_t*)0xFF01;
// 等待UART发送完成
while (!(*uart_status & 0x01)); // 等待TX空
*uart_data = 'A'; // 发送字符
读-改-写操作(如设置GPIO的某一位)在MMIO中可能不安全:
// 不安全:中断可能在LDA和STA之间发生
LDA R1, $FF07 // 读GPIO
ORI R1, #0x01 // 设置bit0
// ← 中断可能在这里修改了GPIO!
STA R1, $FF07 // 写回GPIO(可能覆盖中断的修改)
// 安全方法1:关中断
CLI // 关中断
LDA R1, $FF07
ORI R1, #0x01
STA R1, $FF07
SEI // 开中断
// 安全方法2:使用位操作寄存器
// (如果硬件支持,如ARM的BSRR/BRR寄存器)