从零开始用FPGA读取温度传感器:Vivado实战全记录
你有没有过这样的经历?明明代码写得一丝不苟,时序也反复推敲,可I2C总线上就是“静如止水”——SDA纹丝不动,SCL也没波形。或者更糟,读回来的数据全是0xFF,像在跟你开个无声的玩笑。
别急,这正是我们今天要一起攻克的实战项目:在Xilinx FPGA上,用Vivado从头搭建一个I2C主控制器,读取TMP102这类常见温度传感器的数据。这不是理论课,而是一次真实开发流程的完整复现——从创建工程、写状态机、加约束、仿真验证,到板级调试、抓波形、改bug,全程手把手。
为什么选I2C?因为它“小但难搞”
I2C看似简单:两根线,SCL和SDA。但它对时序要求严苛,又是开漏结构,还得处理三态控制和应答机制。对于初学者来说,它就像一块“试金石”——能通,说明你真懂了FPGA的时序和协同设计;不通,那问题往往藏得很深。
更重要的是,I2C是嵌入式系统中最常见的低速外设接口之一。掌握它,意味着你能轻松对接EEPROM、RTC、触摸屏、环境传感器……这些在工业控制、物联网节点中无处不在的模块。
而Vivado,作为Xilinx主流开发工具,它的价值不仅在于综合与实现,更在于那一套完整的硬件协同开发闭环:仿真、ILA在线调试、约束管理、IP集成。这次我们就用最“硬核”的方式,把这套流程走一遍。
I2C协议的本质:一场精确的“电平舞蹈”
在动手前,先搞清楚I2C到底在做什么。
两根线,四种动作
- SCL:时钟线,由主机(这里是FPGA)完全控制。
- SDA:数据线,双向,所有设备共用。
通信靠的是四个关键信号跳变:
| 动作 | SCL状态 | SDA变化 |
|---|---|---|
| 起始(Start) | 高 → 保持高 | 高 → 低 |
| 停止(Stop) | 高 → 保持高 | 低 → 高 |
| 数据写入 | 低 → 保持低 | 可变 |
| 数据采样 | 上升沿后 | 必须稳定 |
记住一句话:SDA只能在SCL为低时改变,在SCL为高时必须保持稳定,否则可能误触发起始/停止条件。
一次温度读取的真实流程
以TMP102为例,我们要读它的温度寄存器(地址0x00),典型操作如下:
- Start
- 发送
Slave_Write_Address(比如0b10010000) - 等待ACK
- 发送寄存器地址
0x00 - 等待ACK
- ReStart
- 发送
Slave_Read_Address(0b10010001) - 接收第一个字节
- 回复ACK
- 接收第二个字节
- 回复NACK(表示不再接收)
- Stop
整个过程涉及两次地址传输、一次寄存器选择、两次数据接收,中间夹着多个ACK/NACK判断。稍有延迟或提前,从机就可能“罢工”。
FPGA怎么“演”好I2C主机?状态机是核心
要在FPGA里实现这个流程,最可靠的方法是有限状态机(FSM) + 精确计数。
状态机怎么分?按通信阶段切
我把整个流程拆成13个状态,清晰对应每一步操作:
typedef enum logic [3:0] { IDLE, START, ADDR_WR, ACK1, REG_SET, RESTART, ADDR_RD, ACK2, READ_BYTE1, ACK3, READ_BYTE2, NACK, STOP, DONE } i2c_state_t;每个状态干一件事,比如:
START:拉高SCL和SDA → 拉低SDAADDR_WR:逐位发送从机写地址(7位地址 + 0)ACK1:释放SDA,等待从机拉低(ACK)READ_BYTE1:在SCL上升沿采样SDA,移位保存
状态转移靠内部计数器推进。例如,发送8位地址需要8个SCL周期,我们就用一个bit_cnt从0计到7。
时钟怎么分?别让SCL太快
假设FPGA主频是100MHz,目标SCL = 100kHz,那么每个SCL周期是10μs,高低各5μs。
100MHz下,一个时钟周期是10ns,所以每半个SCL周期需要计数:
5μs / 10ns = 500也就是说,SCL高/低电平各维持500个系统时钟。用一个clk_div计数器即可实现。
⚠️ 注意:实际设计中建议留些裕量,比如设为550,避免因布线延迟导致时序紧张。
Vivado实战:从创建工程到下载运行
第一步:建工程,选对芯片
打开Vivado,新建RTL工程:
- Project name:
i2c_temp_reader - Device: 根据你的开发板选择,比如Digilent Nexys A7-100T →
xc7a100tcsg324-1 - 不勾选“Sources in external location”
添加两个文件:
i2c_temp_sensor_top.v:顶层模块i2c_master_ctrl.v:I2C主控核心
第二步:引脚约束(XDC)不能错
I2C信号必须接到支持inout的IO上。以PMOD JA为例:
set_property PACKAGE_PIN J17 [get_ports {scl_io}] ;# PMOD JA1 set_property PACKAGE_PIN K16 [get_ports {sda_io}] ;# PMOD JA2 set_property IOSTANDARD LVCMOS33 [get_ports {scl_io sda_io}] set_property CONFIG_PACKAGE_PIN_PULL none [get_ports {scl_io sda_io}] ;# 关闭内部弱上拉,外接电阻更稳 # 复位按键 set_property PACKAGE_PIN C12 [get_ports {btn_rst}] set_property IOSTANDARD LVCMOS33 [get_ports btn_rst]🔧 实践提示:虽然FPGA IO有弱上拉,但I2C总线最好外接4.7kΩ上拉电阻到3.3V。我吃过亏——没接电阻,通信成功率不到30%。
第三步:三态控制是关键
FPGA的SDA和SCL都是inout端口,必须通过oe(output enable)控制方向:
// 在顶层连接 wire scl_o, sda_o; wire scl_i, sda_i; wire scl_oe, sda_oe; assign scl_io = scl_oe ? scl_o : 1'bz; assign sda_io = sda_oe ? sda_o : 1'bz; assign scl_i = scl_io; assign sda_i = sda_io;- 当
oe=1,输出由o决定 - 当
oe=0,呈高阻态,允许从机驱动
在状态机中,只有主机发送数据或生成Start/Stop时才使能输出;接收ACK时需释放SDA,让从机拉低。
仿真验证:别等上板才发现逻辑错了
写Testbench,模拟一次完整读取:
initial begin clk = 0; forever #5 clk = ~clk; // 100MHz end initial begin rst_n = 0; start_req = 0; slave_addr = 7'b1001000; reg_addr = 8'h00; #100 rst_n = 1; #100 start_req = 1; #20 start_req = 0; wait(done); $display("✅ 温度读取完成,值为: %h %h", temp_data[15:8], temp_data[7:0]); #100 $finish; end用Vivado Simulator跑一下,重点看:
- Start条件是否正确生成(SDA下降早于SCL)
- 地址和寄存器是否匹配
- ACK是否在第9个SCL周期被采样
- 两个字节是否完整接收
如果仿真都过不去,上板只会更糟。
板级调试:ILA是你最好的朋友
仿真通过了,烧录到板子,结果还是没数据?
别慌,插入ILA核,实时抓信号!
在Block Design中添加ILA IP,监控这些信号:
state:当前状态机状态scl_o,sda_o:实际输出scl_oe,sda_oe:输出使能ack_err:是否有ACK错误
重新综合、实现、生成比特流,下载后打开Hardware Manager,点击“Debug Probes”,就能看到实时波形。
常见坑点与解决方法
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| SCL无波形 | 时钟未启用或分频错误 | 检查MMCM配置,确认计数器是否启动 |
读回0xFF | SDA未释放,总线被锁死 | 检查三态控制,确保ACK阶段oe=0 |
| ACK失败 | 地址错误或从机未响应 | 用逻辑分析仪确认地址是否0x90(写)和0x91(读) |
| 通信偶尔成功 | 上拉电阻过大或分布电容大 | 改用1.8kΩ~3.3kΩ电阻,降低SCL频率至50kHz |
有一次我折腾了半天,最后发现是TMP102的ADDR引脚接地不良,导致地址变成了0x4C而不是默认的0x48。焊好之后,立马通了。
系统扩展:让温度看得见
光读出来还不够,我们得让它“说话”。
可以在系统中加入:
- UART Lite模块:将温度值格式化为字符串,发送到PC串口助手
- LED指示灯:用4个LED显示粗略温度区间(如<25°C蓝,>30°C红)
- OLED显示:通过SPI驱动SSD1306,本地显示温度曲线
甚至可以加个MicroBlaze软核,跑FreeRTOS,定时采集并上传云端。
写在最后:为什么这个项目值得做?
因为它是从理论到落地的完整闭环。
你不仅写了Verilog,还用了Vivado的约束、仿真、ILA调试、比特流生成——这些都是工程师日常工作的缩影。你学会了:
- 如何用状态机建模复杂时序
- 如何处理inout端口和三态控制
- 如何通过ILA定位硬件bug
- 如何与真实传感器“对话”
下次当你面对SPI、UART、甚至是自定义协议时,你会更有底气。
如果你也在用Vivado做FPGA开发,不妨试试这个项目。哪怕只为了亲眼看到那一行“Temperature: 26.5°C”从串口蹦出来,也值得。
毕竟,硬件的魅力就在于——你写的每一行代码,最终都会变成实实在在的电信号,在芯片间流动。
欢迎在评论区分享你的I2C踩坑经历,或者你用FPGA连过的最奇怪的传感器是什么?
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考