从零构建温度采集系统:Ego1开发板实战全解析
最近带学生做FPGA大作业,发现很多人卡在“温度传感器数据采集”这个项目上。其实这看似复杂的系统,拆解开来不过就是信号怎么来、数据怎么传、结果怎么用三个问题。
今天我就以Xilinx Ego1开发板为平台,带你完整走一遍基于Vivado的温度采集系统设计流程——不讲空话,只说实战中真正踩过的坑和验证有效的解法。
为什么选TMP102?不是所有传感器都适合FPGA新手
项目第一步永远是选型。面对热敏电阻、DS18B20、LM35、TMP102一堆选项,我为什么推荐TMP102?
因为对FPGA初学者来说,数字接口 > 模拟接口,标准协议 > 私有协议。
- 热敏电阻需要外接ADC + 校准曲线拟合,直接劝退;
- DS18B20虽然也是数字输出,但用的是单总线(1-Wire),时序极其严格,一个周期差几纳秒就丢包;
- TMP102走I²C,两根线、有规范、速率稳定,哪怕你代码写得糙一点,只要主循环够快,基本都能通。
更重要的是:它不需要外部元件。你在面包板上插个芯片,接两个4.7kΩ上拉电阻到3.3V,SCL/SDA连到FPGA引脚,就能通信。这对调试太友好了。
它的关键参数你也得记住:
| 参数 | 数值 |
|---|---|
| 地址(默认) | 0x48 |
| 分辨率 | 12位(0.0625°C/LSB) |
| 测温范围 | -55°C ~ +128°C |
| 接口速率 | 支持100kHz / 400kHz |
这些数字会直接影响你的状态机设计和数据处理逻辑。
FPGA如何“假装”是一个I²C主机?手把手教你写软核驱动
Artix-7没有硬核I²C控制器,所以我们必须“模拟”出整个协议过程——这就是所谓的Bit-Banging。
别被术语吓到,其实就是让FPGA按顺序控制SCL和SDA的电平变化,模仿出起始条件、地址传输、应答检测等动作。
关键挑战:时序精度
I²C协议要求:
- 起始条件:SCL高时,SDA由高变低;
- 数据稳定期:SCL为高时,SDA不能变;
- 数据采样点:SCL下降沿写入,上升沿读取。
如果你用50MHz系统时钟(周期20ns),要生成100kHz的SCL(周期10μs),那每个SCL高低电平至少要维持50个时钟周期。
所以第一步:先做一个分频器
localparam CLK_DIV = 50_000_000 / (4 * 100_000) - 1; // ≈124 reg [7:0] clk_cnt; reg i2c_clk; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin clk_cnt <= 0; i2c_clk <= 0; end else if (clk_cnt >= CLK_DIV) begin clk_cnt <= 0; i2c_clk <= ~i2c_clk; end else begin clk_cnt <= clk_cnt + 1; end end这里我们把SCL分成四段,每段约2.5μs,这样可以在中间点进行数据切换和采样,避开边沿抖动区。
💡 小技巧:实际设计中建议使用三段式状态机,并在SCL低电平时更新SDA,在SCL高电平时采样SDA,避免违反“数据必须在SCL高期间保持稳定”的规定。
状态机才是核心:一步步发指令读数据
下面是简化版的状态流转图:
IDLE → START → 发送设备地址+写 → 等ACK → 发寄存器地址 → 等ACK → RESTART → 发设备地址+读 → 等ACK → 连续读2字节 → NACK → STOP对应Verilog中的枚举类型:
typedef enum logic [3:0] { IDLE, START, ADDR_WR, WAIT_ACK1, REG_PTR, WAIT_ACK2, RESTART, ADDR_RD, WAIT_ACK3, READ_MSB, READ_LSB, SEND_NACK, STOP, DONE } i2c_state_t;重点在于:每一个状态只做一件事,比如“发送一位地址”或“等待ACK”,而不是在一个状态里塞进多个操作。
双向IO怎么处理?inout端口的经典玩法
SDA是双向引脚,作为主机时既要能输出命令,又要能接收从机返回的ACK/NACK。
解决方法是引入三态控制:
inout sda; reg sda_out; reg sda_en; // 1=输出模式,0=输入模式(高阻) assign sda = sda_en ? sda_out : 1'bz;当你要发送数据时,sda_en=1,sda_out赋值;当你想读ACK时,设sda_en=0,然后读sda引脚的真实电平。
⚠️ 坑点提醒:一定要确保在释放SDA前,其他设备没有同时驱动!否则可能造成短路。
数据拿到之后怎么办?别忘了符号扩展!
TMP102返回的是16位数据,格式如下:
bit[15:12]: 温度整数部分(补码) bit[11:8]: 小数部分(0.0625°C步长) bit[7:0]: 保留(读出来可能是随机值)例如收到16'h8080,你以为是正数?错!最高位是1,说明是负温度。
正确解析方式:
wire signed [15:0] raw = {rx_data[15], rx_data[15], rx_data[15], rx_data[15], rx_data[15:4]}; // 扩展成完整补码 wire [15:0] abs_val = (raw[15]) ? (~raw + 1) : raw; real temp_c = $itor(abs_val) * 0.0625; if (raw[15]) temp_c = -temp_c;当然,你也可以在纯逻辑中用组合逻辑判断符号位并取反加一,但要注意延迟匹配。
更高级玩法:MicroBlaze + AXI 让开发效率翻倍
如果你已经掌握了纯逻辑设计,下一步可以尝试加入MicroBlaze 软核处理器,把复杂的数据处理交给C语言来做。
为什么这么做?
想想看:你现在要用数码管显示温度,还要支持小数点闪烁、负号显示、单位标注……这些逻辑用Verilog写起来又臭又长。
但如果有一个CPU呢?
你可以:
- 写一段C代码格式化字符串;
- 通过UART打印到电脑;
- 或者调用LCD驱动库直接显示;
- 甚至加个按键实现“切换摄氏/华氏”。
这一切只需要在Vivado里添加一个MicroBlaze IP核,再配一个AXI GPIO或自定义AXI Lite外设即可。
如何连接你的I²C模块?
最简单的办法:把你原来的temp_data输出接到一个只读寄存器上,然后把这个寄存器挂到AXI总线上。
在SDK中就可以这样访问:
uint16_t raw = Xil_In16(XPAR_TEMP_SENSOR_0_BASEADDR); float temp = (int16_t)(raw >> 4) * 0.0625f; xil_printf("Temp: %.2f°C\n", temp);是不是瞬间清爽了?
而且一旦出了问题,你可以打printf调试,不像纯逻辑只能靠ILA抓波形。
✅ 实战建议:前期先用纯逻辑验证I²C通信是否正常;后期迁移到MicroBlaze提升交互能力。
实际搭建时最容易翻车的几个点
别以为仿真过了就能跑通,现场调试才是真考验。
1. 上拉电阻没接 or 阻值不对
I²C是开漏输出,必须外加上拉电阻!常见的错误包括:
- 忘记接;
- 接成10kΩ导致上升沿太慢(超过300ns);
- 只给SCL加上拉,SDA没接。
标准做法:SCL和SDA各接一个4.7kΩ到3.3V电源。
2. 引脚约束写错电压标准
Ego1开发板使用LVCMOS33电平。如果你在XDC文件里写成了LVCMOS25,轻则通信不稳定,重则烧IO。
正确的约束示例:
set_property PACKAGE_PIN "P14" [get_ports i2c_scl] set_property IOSTANDARD LVCMOS33 [get_ports i2c_scl] set_property PACKAGE_PIN "P15" [get_ports i2c_sda] set_property IOSTANDARD LVCMOS33 [get_ports i2c_sda]3. 时钟分频算错,导致速率超标
有人直接拿50MHz除以100k得到500,然后计数到500翻转SCL——这是错的!
因为你每个状态至少占一个时钟周期,而SCL一个完整周期需要两次翻转(高→低→高),所以实际频率是clk_freq / (2 * count)。
更安全的做法是:宁可慢一点,也不要超限。跑50kHz也能读数据,但跑500kHz可能直接失败。
4. 多次读取之间没有延时
TMP102内部转换需要时间(典型35ms)。如果你连续发起读操作,很可能拿到旧数据。
解决方案:
- 在状态机里加一个“delay”状态,持续若干毫秒;
- 或者查手册启用“one-shot”模式,每次触发才采样。
如何快速定位问题?ILA是你最好的朋友
Integrated Logic Analyzer(ILA)是Vivado内置的在线逻辑分析仪,能把FPGA内部信号实时抓出来看。
建议你至少监控这几组信号:
-scl,sda:观察实际波形是否符合I²C协议;
-curr_state:确认状态机有没有卡住;
-rx_data,done:检查数据是否正确接收。
设置触发条件也很关键:
- 触发条件设为start == 1;
- 捕获深度设为1024点以上;
- 采样时钟选比I²C快至少4倍的时钟。
你会发现,很多时候你以为SDA拉低了,实际上因为竞争条件根本没生效。
最后一点思考:这个项目到底教会了我们什么?
完成一次温度采集,远不止“读个数”那么简单。
它让你亲手实践了:
-硬件描述语言的真实用途:不是写代码,而是“描述电路行为”;
-跨时钟域的基本意识:高速逻辑如何与低速外设握手;
-软硬件分工的设计思维:哪些该用逻辑实现,哪些交给软件更高效;
-从仿真到实物的鸿沟跨越:理论再完美,不上电就不知道哪里漏了上拉。
而这,正是现代嵌入式系统工程师的核心能力。
未来你可以在这个基础上继续拓展:
- 加一个PWM风扇,做成闭环温控;
- 接Pmod WIFI,把温度上传云端;
- 用BRAM缓存历史数据,画趋势图;
- 甚至部署轻量级神经网络做异常检测。
但所有这一切,都要从你第一次成功读出那个0x0080开始。
所以,现在就打开Vivado,新建工程吧。
遇到问题别怕,评论区见。