1. 三态门:FPGA双向IO的“交通警察”
如果你刚开始接触FPGA,看到“三态门”这个词可能会有点懵。别担心,你可以把它想象成一个十字路口的交通警察。这个路口只有一条车道(对应一根物理的IO引脚),但有两辆车(两个逻辑模块)都想用这条路。如果两辆车同时开上去,那肯定就撞车了,也就是我们常说的“总线冲突”。三态门这个“警察”的作用,就是指挥谁在什么时候可以占用这条车道,其他时候必须让路,把路权(引脚控制权)交出来。
在硬件世界里,这个“让路”的状态,就是高阻态,在Verilog里用 z 来表示。它既不是高电平(1),也不是低电平(0),而是一种“断开连接”的状态,相当于这辆车挂空挡、松离合,对车道没有任何驱动能力。这样一来,另一辆车就可以安全地使用这条车道了。这种一根线既能当输入又能当输出的引脚,就是我们说的双向IO。
在FPGA开发中,双向IO的应用场景非常多。最常见的就是各种低速的板级通信总线,比如连接EEPROM存储器的 I2C总线,连接Flash或显示屏的 SPI总线,以及老式单片机常用的并行数据总线。这些总线为了节省引脚、简化布线,都采用了这种共享线路的设计。如果你要做一块FPGA核心板,想通过I2C去配置一个音频编解码芯片,或者通过SPI读取一个传感器,那你就必须和三态门打交道。可以说,掌握三态门是FPGA工程师从点灯进阶到实际通信应用的必经之路。
我自己刚开始学的时候,也犯过糊涂,以为双向IO就是简单地把一个寄存器变量同时接到 input 和 output 上,结果综合器报了一堆错。后来才明白,硬件描述语言描述的是真实的电路,一根物理连线在同一时刻只能有一个驱动源,否则就会短路。三态门和 inout 端口,就是为解决这个矛盾而生的语法和电路结构。下面,我们就从最基础的Verilog语法开始,一步步拆解这个“交通警察”是如何工作的。
2. Verilog三态门基础语法精讲
2.1 核心语法:assign语句与条件运算符
三态门在Verilog中的实现,核心就是一条 assign 连续赋值语句,结合条件运算符(? :)。它的标准写法看起来非常简单:
assign bidir_io = direction_enable ? data_to_send : 1'bz;
我们来拆解一下这个“三板斧”:
assign: 这是连续赋值语句的关键字。它意味着右边表达式的结果会持续地、实时地驱动左边的线网(wire)。只要右边有任何信号发生变化,左边立刻跟着变。这非常适合用来描述三态门这种组合逻辑。bidir_io: 这就是我们的双向端口信号,在模块声明中,它的类型必须是inout。注意,它通常被定义为wire类型,因为reg类型不能被inout直接连接(在顶层或Testbench中连接时需要注意)。direction_enable ? data_to_send : 1'bz: 这是一个条件运算符,也就是三元表达式。它的逻辑非常直白:如果direction_enable为真(1),那么就把data_to_send的值赋给bidir_io;如果为假(0),就把1'bz赋给bidir_io。
这里最关键的就是 1'bz。这个 z 就是高阻态(High-Impedance)的标识。在仿真中,它表示一个未知的、无驱动的状态;在综合后,它对应到FPGA的IOB(输入输出块)中一个特定的三态缓冲器(Tristate Buffer)使能端。当输出 z 时,这个缓冲器被禁用,其输出端与内部电路断开,引脚对外呈现高阻。
2.2 深入理解inout端口与wire类型
很多新手会在 inout 端口的数据类型上栽跟头。这里有一个必须牢记的准则:在声明 inout 端口时,通常将其定义为 wire 类型。
为什么呢?因为 inout 端口在物理上就是一根双向走线。当你的模块驱动它时(使能有效),你是在向这根线“写入”数据;当你的模块不驱动它时(使能为高阻),你是在从这根线“读取”数据。wire 类型正是用来描述这种多驱动源、需要解析最终值的连线逻辑。如果你错误地声明为 reg 类型,大多数综合工具会报错,因为 reg 通常意味着寄存器驱动,不适合描述这种双向的、可能被外部驱动的场景。
在模块内部,如果你想读取 inout 端口进来的数据,直接使用它就可以了,就像使用一个 input 信号一样。例如:
module my_i2c_master (
input clk,
input rst_n,
inout sda, // 双向数据线
output scl // 时钟线(通常为输出)
);
reg sda_en; // 内部SDA输出使能寄存器
reg sda_out; // 内部要发送的数据
wire sda_in; // 从SDA线读取的数据
// 三态门驱动逻辑
assign sda = sda_en ? sda_out : 1'bz;
// 读取SDA线状态
assign sda_in = sda; // 直接赋值,读取外部驱动的值
// ... 其他I2C状态机逻辑
endmodule
在这个例子中,sda 被声明为 inout wire。我们通过 assign 语句和 sda_en 来控制何时驱动它。同时,我们又通过 assign sda_in = sda; 这条语句,随时捕获 sda 线上的当前电平(可能是外部设备拉低的),用于判断应答位等。这就是双向IO在模块内部的典型用法。
3. 一个完整可综合的I2C控制器模块实战
光讲理论不

1768

被折叠的 条评论
为什么被折叠?



