GPIO 设计:输入同步、去抖、输出/OD 与中断
更新时间:2026-09-19。本篇是低速外设接口首篇,配置寄存器侧假设挂 APB 或 AXI4-Lite。涉及的同步/亚稳态原理见 建立保持与亚稳态 与 CDC 基础。
本文要回答的问题
- 外部输入为什么必须先经两级触发器同步?
- 按键抖动是什么,去抖计数器怎么写?
- GPIO 有哪些必备寄存器,置位/清零为什么分两个地址?
- 开漏(open-drain)怎么实现,I2C 为什么要用它?
- 边沿中断怎么产生,为什么要在 IO 时钟域检测再跨域送中断控制器?
一、寄存器地图
一个 GPIO 端口(N 位)通常包含:
| 寄存器 | 作用 |
|---|---|
| DIR(方向) | 每位 1=输出、0=输入 |
| OUT(输出值) | 输出锁存值 |
| SET / CLR | 写 1 置位/清零对应位(写 0 无效),避免读改写 |
| TOG | 写 1 翻转 |
| IN(输入值,只读) | 同步后的引脚电平 |
| ODR/OPENDRAIN | 开漏使能 |
| PUE/PDE | 内部上拉/下拉(连 IO 单元) |
| IER / IEDGE / ILEVEL / IRQ_RAW / IRQ_EN | 中断使能、边沿/电平选择、原始挂起、屏蔽 |
| DEBOUNCE | 去抖使能与周期 |
SET/CLR 分开的价值:软件改一个引脚不必先读出整个 OUT 再写回(读改写在并发/位带场景易丢更新),直接对 SET 或 CLR 地址写位掩码即可。
二、输入:先同步,再做边沿检测
外部引脚和内核时钟异步,直接进组合逻辑/触发器会产生亚稳态。标准做法是两级同步器:
reg sync1, sync2, sync3;
always @(posedge clk or negedge rst_n)
if (!rst_n) {sync1,sync2,sync3} <= 3'b0;
else {sync1,sync2,sync3} <= {pad_in, sync1, sync2};
wire rise = sync2 & ~sync3; // 上升沿(多一级用于边沿检测)
wire fall = ~sync2 & sync3; // 下降沿用三拍(多一级 sync3)纯粹是为了拿到稳定的旧值做边沿比较;仅防亚稳态两级即可,原理见 双触发器同步。IN 寄存器读 sync2。
三、去抖:过滤机械抖动与毛刺
按键、继电器等机械触点在闭合/断开瞬间会在数 ms 内反复通断。去抖思路:只有输入稳定保持 N 个时钟周期才认可变化。
// 当同步输入与已确认值不同,启动计数;计数到阈值才更新
reg [15:0] cnt;
reg db_level;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin cnt<=0; db_level<=1'b1; end // 按键常假设默认高
else if (sync2 === db_level) cnt <= 0;
else begin
cnt <= cnt + 1'b1;
if (cnt == DEBOUNCE_MAX) begin db_level <= sync2; cnt <= 0; end
end
end阈值按时钟和目标去抖时间换算(如 50 MHz、10 ms ≈ 500_000 拍)。中断边沿检测应基于去抖后电平 db_level,否则抖动会产生一串伪中断。去抖只对需要的引脚开,避免给高速信号平白加延迟。
四、输出与开漏
普通推挽输出:pad_out = OUT[i](经 IO 单元)。开漏模式下,引脚只有「拉低」和「高阻」两态,不主动输出高:
// 开漏:OUT=0 拉低;OUT=1 -> 高阻,靠外部上拉电阻拉高
assign pad_oe = DIR & (opendrain ? ~OUT : 1'b1); // 使能输出驱动
assign pad_out = opendrain ? 1'b0 : OUT;I2C 的 SCL/SDA 就是开漏 + 外部上拉:多个设备都只能拉低、不能主动驱高,从而天然实现「线与」和时钟同步/仲裁,避免两个输出对顶。参见 I2C 主机与开漏。电气上还要复用 IO pad 的驱动强度、上拉/下拉、压摆率配置(见 IO 与 Pads)。
五、中断:IO 时钟域检测,再跨域递交
中断逻辑顺序:
- 在引脚同步时钟域(一般就是 GPIO 时钟)对去抖后电平做上升/下降/双边沿或电平检测,置位
IRQ_RAW(锁存,写 1 清零/或按电平)。 - 与 IER 屏蔽相「与」,汇总成
irq送中断控制器(GIC/NVIC/PLIC)。 - 若 GPIO 时钟与中断总线/CPU 时钟非同源,
irq或挂起位跨域要走脉冲同步器/握手,不能直接多 bit 跨域(见 CDC 基础)。寄存器配置面跨域同样要做。 - 电平中断要在「电平仍有效」期间保持,边沿中断靠挂起位保持到软件清。W1C(写 1 清零)最常用。
六、常见的坑
- 外部信号不做同步直接用:偶发亚稳态导致误触发、仿真难复现;必须两级同步。
- 边沿检测用未经同步/去抖信号:抖动产生几十次伪中断,软件误判。
- 边沿寄存器只打两拍就比较:拿不稳旧值可能漏沿;比较需保留 sync3。
- 开漏当推挽驱高:多设备对顶,I2C 死锁甚至烧脚;开漏只能拉低/高阻。
- 改输出用读改写:与其他位/中断并发时丢更新;用 SET/CLR 地址。
- 中断跨时钟域直接拉多 bit:CDC 违例、丢中断;用脉冲同步并做挂起位。
- 引脚默认态不定:复位期间 DIR 应默认输入(高阻),防止上电驱动外部器件误动作。
相关与延伸
总线侧见 APB、AXI4-Lite 从机;同步原理见 亚稳态、CDC 基础;开漏总线见 I2C 主机;IO 电气见 IO 与 Pads、电平转换。
总结
GPIO 控制器 = 方向/输出/置位清零/输入/开漏/中断一组寄存器:外部输入先两级(边沿用三级)同步、机械信号再做计数器去抖,开漏只拉低+高阻靠上拉线与,边沿中断在 IO 时钟域对去抖电平检测并锁存、跨时钟域用脉冲同步,复位默认高阻输入——简单外设的坑几乎全在同步、去抖和开漏上。