设计并实现8位通用输入输出(GPIO)控制器,支持独立方向控制、三态缓冲、上拉电阻和边沿中断。GPIO是8位电脑与物理世界交互的最基本接口——LED、按键、传感器、继电器,一切外设的起点。
GPIO(General Purpose Input/Output)看似简单——不过是几个可控的引脚。但真正做好GPIO需要处理大量工程细节。让我们从最基本的三态缓冲说起:
| 模式 | DIR | OUT | 引脚状态 | 典型用途 |
|---|---|---|---|---|
| 推挽输出 | 1 | 0/1 | 强驱动低/高 | LED、继电器、片选 |
| 开漏输出 | 1 | 0/Z | 强低/高阻 | I²C、线或逻辑 |
| 浮空输入 | 0 | — | 高阻态 | 读取传感器输出 |
| 上拉输入 | 0 | — | 弱高+高阻 | 按键检测(低有效) |
// gpio.v - 8位通用IO控制器
module gpio (
input wire clk,
input wire rst_n,
input wire cs,
input wire rw,
input wire [1:0] reg_addr,
input wire [7:0] cpu_data_in,
output reg [7:0] cpu_data_out,
inout wire [7:0] gpio_pins,
output reg gpio_irq
);
// 寄存器
reg [7:0] gpio_dir; // 方向:0=输入 1=输出
reg [7:0] gpio_out; // 输出数据
reg [7:0] gpio_irq_en; // 中断使能
reg [7:0] gpio_irq_edge; // 边沿选择
reg [7:0] gpio_prev; // 上一拍输入(边沿检测)
reg [7:0] gpio_input; // 同步后的输入值
// 三态缓冲输出
genvar i;
generate
for (i = 0; i < 8; i = i + 1) begin : pin_buf
assign gpio_pins[i] = gpio_dir[i] ? gpio_out[i] : 1'bz;
end
endgenerate
// 输入同步(消除亚稳态)
reg [7:0] gpio_sync;
always @(posedge clk) begin
gpio_sync <= gpio_pins;
gpio_input <= gpio_sync;
end
// 边沿检测与中断
wire [7:0] rising_edge = ~gpio_prev & gpio_input;
wire [7:0] falling_edge = gpio_prev & ~gpio_input;
wire [7:0] edge_detected = (gpio_irq_edge & rising_edge) |
(~gpio_irq_edge & falling_edge);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
gpio_dir <= 8'd0;
gpio_out <= 8'd0;
gpio_irq_en <= 8'd0;
gpio_irq_edge <= 8'd0;
gpio_prev <= 8'd0;
gpio_irq <= 1'b0;
cpu_data_out <= 8'd0;
end else begin
gpio_prev <= gpio_input;
gpio_irq <= 1'b0;
// 中断检测
if ((edge_detected & gpio_irq_en) != 8'd0)
gpio_irq <= 1'b1;
// CPU写寄存器
if (cs && !rw) begin
case (reg_addr)
2'b00: gpio_dir <= cpu_data_in;
2'b01: gpio_out <= cpu_data_in;
2'b10: gpio_irq_en <= cpu_data_in;
2'b11: gpio_irq_edge <= cpu_data_in;
endcase
end
// CPU读寄存器
if (cs && rw) begin
case (reg_addr)
2'b00: cpu_data_out <= gpio_dir;
2'b01: cpu_data_out <= gpio_input;
2'b10: cpu_data_out <= gpio_irq_en;
2'b11: cpu_data_out <= gpio_irq_edge;
endcase
end
end
end
endmodule
verilator --lint-only 检查,无错误无警告。
// tb_gpio.v - GPIO控制器测试
module tb_gpio;
reg clk, rst_n, cs, rw;
reg [1:0] reg_addr;
reg [7:0] cpu_data_in;
wire[7:0] cpu_data_out;
wire[7:0] gpio_pins;
wire gpio_irq;
// 外部驱动(模拟按键/传感器)
reg [7:0] ext_drive;
assign gpio_pins = (ext_drive !== 8'hZZ) ? ext_drive : 8'hZZ;
gpio uut (.*);
initial clk = 0;
always #5 clk = ~clk;
initial begin
rst_n = 0; cs = 0; rw = 0;
reg_addr = 0; cpu_data_in = 0;
ext_drive = 8'hZZ;
#20; rst_n = 1;
// 测试1:输出模式 - 点亮LED
$display("--- Test 1: Output mode (LED) ---");
cs = 1; rw = 0; reg_addr = 2'b00;
cpu_data_in = 8'hFF; // 全部设为输出
#10;
reg_addr = 2'b01;
cpu_data_in = 8'hA5; // 输出 10100101
#10;
cs = 0;
#10;
if (gpio_pins === 8'hA5)
$display(" PASS: Output 0xA5 on pins");
else
$display(" FAIL: Expected 0xA5, got 0x%02h", gpio_pins);
// 测试2:输入模式 - 读取按键
$display("--- Test 2: Input mode (Button) ---");
cs = 1; rw = 0; reg_addr = 2'b00;
cpu_data_in = 8'h00; // 全部设为输入
#10;
ext_drive = 8'h3C; // 模拟外部输入
#20;
cs = 1; rw = 1; reg_addr = 2'b01;
#10;
if (cpu_data_out === 8'h3C)
$display(" PASS: Read 0x3C from input");
else
$display(" FAIL: Expected 0x3C, got 0x%02h", cpu_data_out);
cs = 0;
// 测试3:边沿中断
$display("--- Test 3: Edge interrupt ---");
// 使能bit0下降沿中断
cs = 1; rw = 0;
reg_addr = 2'b10; cpu_data_in = 8'h01; // IRQ_EN
#10;
reg_addr = 2'b11; cpu_data_in = 8'h00; // 下降沿
#10;
cs = 0;
ext_drive = 8'hFF; // 初始高
#20;
ext_drive = 8'hFE; // bit0下降沿!
#20;
if (gpio_irq)
$display(" PASS: Falling edge interrupt fired");
else
$display(" FAIL: No interrupt on falling edge");
$display("--- All GPIO tests complete ---");
$finish;
end
endmodule
; GPIO驱动8个LED做流水灯
LDA #$FF ; 全部设为输出
STA $FF10 ; 方向寄存器
LDA #$01 ; 从bit0开始
STA $FF11 ; 输出寄存器
LOOP:
LDA $FF11 ; 读取当前输出
ROL ; 左移一位
STA $FF11 ; 写回
JSR DELAY_200MS
JMP LOOP
; 4×4矩阵键盘扫描
; GPIO[7:4] = 行输出,GPIO[3:0] = 列输入(上拉)
LDA #$F0 ; 高4位输出,低4位输入
STA $FF10 ; 方向寄存器
SCAN_ROW0:
LDA #$EF ; 行0拉低(1110_1111)
STA $FF11
NOP ; 等待稳定
LDA $FF11 ; 读取列输入
AND #$0F ; 屏蔽高4位
CMP #$0F ; 全高=无按键
BEQ SCAN_ROW1
; 有按键按下,A的低4位中为0的位就是被按下的列
JSR DECODE_KEY
SCAN_ROW1:
LDA #$DF ; 行1拉低(1101_1111)
STA $FF11
; ... 继续扫描
; GPIO控制继电器(bit7 = 继电器,低有效)
LDA #$80 ; bit7输出
STA $FF10
RELAY_ON:
LDA $FF11
AND #$7F ; bit7=0 → 继电器开
STA $FF11
RTS
RELAY_OFF:
LDA $FF11
ORA #$80 ; bit7=1 → 继电器关
STA $FF11
RTS
当前GPIO输出是"读-改-写"方式:先读整个8位,修改某一位,再写回。这在中断环境下可能出错——如果中断也在修改同一端口的另一位。实现"位设置/位清除"寄存器:写1到设置寄存器的某位只置位该位,写1到清除寄存器的某位只清除该位,其他位不受影响。
用两个GPIO引脚实现I²C协议:一个做SCL(时钟),一个做SDA(数据)。I²C要求开漏输出+上拉——GPIO的Hi-Z模式+外部上拉正好模拟开漏。编写I²C起始条件、停止条件、字节发送的汇编子程序。
8个GPIO引脚可能不够用。用74HC595(串入并出移位寄存器)扩展输出,用74HC165(并入串出移位寄存器)扩展输入。设计3线SPI接口(DATA/CLK/LATCH),用3个GPIO引脚扩展出8+8=16个额外IO。
机械按键在按下/释放时会产生10-20ms的弹跳噪声。实现硬件去抖:当检测到电平变化后,启动一个20ms的定时器,定时器到期后才确认状态变化并更新输入寄存器。这比软件延时去抖更高效。
你实现了完整的8位GPIO控制器!这包括:
GPIO是数字世界与物理世界的桥梁——你的8位电脑终于可以"触摸"真实世界了!