
1. 为什么我要用XADC读FPGA内部温度做FPGA项目时间长了你会发现一个很现实的问题板子跑起来之后芯片到底热不热尤其是在一些密闭机箱、无风扇散热或者高温工业环境下FPGA结温一旦超过安全阈值轻则时序违例、逻辑跑飞重则直接烧片。我踩过最惨的一次坑是一块7K325T在夏天高温环境里连续跑了六个小时突然开始出现偶发数据错误排查了半天才发现是芯片温度飙到了将近100度时序余量被温度吃光了。后来我就养成了一个习惯只要板子有资源余量一定把XADC用起来实时监控芯片温度和供电电压。Xilinx 7系列FPGA内部集成了一个叫做XADC的模块全称是Xilinx Analog-to-Digital Converter它是一个双通道12位、1MSPS的ADC硬核集成在芯片里不占用任何逻辑资源。它能采集片上温度传感器、供电电压也能通过外部模拟输入通道采集板级模拟信号。这篇文章我打算把用Vivado调用XADC IP核读取内部温度的完整流程讲透包括IP核配置、DRP接口时序、代码实现、实测数据验证以及我在实际项目中踩过的坑。适合已经有基本FPGA开发经验、用过Vivado但还没碰过XADC的朋友也适合想深入理解DRP动态重配置接口时序的同行。读完你至少能做到两件事第一独立完成XADC读温度的工程搭建第二看懂DRP接口的时序波形自己会改代码读其他通道。2. XADC IP核的整体设计与选型思路2.1 为什么选XADC而不是外置温度传感器很多人第一反应是我外挂一个I2C温度传感器不就行了比如TMP102、LM75这些便宜又好用。但实际项目里外置传感器测的是PCB板面温度不是FPGA芯片结温。这两者差距可能非常大尤其在芯片功耗较高、散热路径不好的情况下结温和板面温度差二三十度都很正常。你要监控的是芯片内部那个真正会烧掉的结点外置传感器给不了你这个数据。XADC的片上温度传感器直接集成在FPGA的硅片里测的就是结温精度典型值±4度分辨率12位对于工程监控来说完全够用。而且它不需要任何外部器件、不占PCB面积、不占逻辑资源唯一要做的就是通过DRP或者JTAG把数据读出来。从成本和可靠性角度看没有理由不用它。2.2 XADC IP核的两种使用模式Vivado里调用XADC IP核本质上是对这个硬核做了一层封装。它提供两种主要的使用方式第一种是独立模式IP核自己完成采样、转换、报警判断你只需要读寄存器就行。这种模式最简单适合纯监控场景。第二种是DRP模式也就是Dynamic Reconfiguration Port动态重配置端口。你可以通过这个接口在运行时读写XADC内部的寄存器改变采样通道、调整报警阈值、读取转换结果。DRP是一个同步的读写接口有自己的地址、数据、使能信号时序上跟BRAM的接口有点像。我这次用的是DRP模式原因有两个一是DRP能让我精确控制什么时候读哪个通道方便做多通道轮询二是理解了DRP时序之后你就能举一反三去操作XADC的其他寄存器比如设置报警阈值、配置平均次数等。独立模式虽然简单但灵活性差很多。2.3 IP核配置的关键参数在Vivado里配置XADC IP核的时候有几个参数必须搞清楚不然生成出来的核可能根本读不到你想要的数据。Interface Selection这里选DRP。选完之后IP核会暴露daddr、den、di、do、dwe、drdy这一组DRP信号。Startup Channel Selection启动通道选择。XADC有一个通道序列器上电后会自动按顺序采样。这里可以选默认的Temperature、VCCINT、VCCAUX等。我一般保持默认因为后面通过DRP可以手动切换。Sequencer Mode序列器模式。有Continuous、Single Channel、Default等选项。如果选ContinuousXADC会不停地循环采样所有使能的通道如果选Single Channel只采一个通道。我建议选Continuous让它在后台自己跑你需要读哪个通道的时候通过DRP去读对应的状态寄存器就行。Acquisition and Conversion Time采样和转换时间。这个跟采样时钟有关默认设置一般没问题。但如果你对采样率有要求需要根据时钟频率算一下。Alarm Settings报警阈值。温度上限默认是85度可以改。我一般会把它设到比实际安全阈值略低一点比如80度这样留出反应时间。Control/Status Ports建议把channel、eoc、eos、busy、alarm这些状态信号都使能调试的时候非常有用。配置完之后IP核的端口大概长这样左边是DRP接口和时钟复位右边是状态输出和报警输出。接下来重点讲DRP接口怎么用。3. DRP接口时序详解与代码实现3.1 DRP接口的信号定义先把DRP这组信号一个个说清楚这是后面写代码的基础。信号名方向位宽含义daddr输入7位寄存器地址决定读/写哪个寄存器den输入1位DRP使能高有效di输入16位写入数据do输出16位读出数据dwe输入1位写使能1为写0为读drdy输出1位数据就绪读操作完成时拉高一个周期dclk输入1位DRP时钟一般用50MHz或100MHzreset输入1位复位读操作的基本流程是在dclk上升沿把daddr和den拉高、dwe拉低XADC在接下来的几个周期内完成内部读取然后在drdy拉高的那个周期do上就是有效数据。写操作类似只是dwe拉高di上放要写的数据。3.2 读温度寄存器的完整时序温度寄存器的地址是7h00。这个地址在XADC的文档里有明确说明0x00就是片上温度传感器的状态寄存器。一次完整的读操作从时序上看是这样的第一个时钟上升沿den拉高dwe拉低daddr设为7h00。此时XADC开始处理读请求。接下来大概2到3个时钟周期XADC内部完成地址译码和数据准备。这段时间den可以拉低也可以保持但一般建议只拉高一个周期。然后drdy会在某个上升沿拉高一个周期表示do上的数据有效。你需要在drdy为高的时候把do锁存下来。这里有个关键点drdy不是固定延迟几个周期就来的它取决于XADC内部的转换状态。如果你在XADC正忙的时候发读请求drdy可能会晚几个周期才来。所以正确的做法是用状态机等待drdy而不是用固定计数器去数周期。3.3 温度数据的换算方法读出来的do是16位数据但温度值只占高12位。具体格式是这样的do[15:4]是12位温度ADC码do[3:0]是状态位一般忽略。温度换算公式在Xilinx文档UG480里写得很清楚温度摄氏度 (ADC码 × 503.975) / 4096 - 273.15举个例子如果读到的ADC码是0x0A5C也就是十进制的2652那么温度 2652 × 503.975 / 4096 - 273.15 326.3 - 273.15 53.15度。这个公式我实测过跟JTAG读出来的温度基本一致误差在2度以内完全满足工程监控需求。3.4 Verilog代码实现下面是我实际项目里用的读温度模块简化了一下去掉了多通道轮询部分只保留读温度的核心逻辑。module xadc_temp_read ( input wire clk, // 50MHz input wire rst_n, output reg [11:0] temp_code, // 原始ADC码 output reg [15:0] temp_value, // 换算后的温度放大100倍 output reg temp_valid ); // DRP接口信号 reg [6:0] daddr; reg den; reg [15:0] di; wire [15:0] do; reg dwe; wire drdy; // 状态机 localparam IDLE 2d0; localparam READ 2d1; localparam WAIT 2d2; localparam DONE 2d3; reg [1:0] state; reg [15:0] do_reg; // XADC IP核例化 xadc_wiz_0 u_xadc ( .dclk (clk), .reset (~rst_n), .daddr (daddr), .den (den), .di (di), .do (do), .dwe (dwe), .drdy (drdy) ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; daddr 7d0; den 1b0; dwe 1b0; di 16d0; temp_code 12d0; temp_value 16d0; temp_valid 1b0; end else begin case (state) IDLE: begin temp_valid 1b0; state READ; end READ: begin daddr 7h00; // 温度寄存器地址 den 1b1; dwe 1b0; // 读操作 state WAIT; end WAIT: begin den 1b0; if (drdy) begin do_reg do; state DONE; end end DONE: begin temp_code do_reg[15:4]; // 温度换算code * 503.975 / 4096 - 273.15 // 为避免浮点先算 code * 503975 / 4096再减27315最后除以100 temp_value (do_reg[15:4] * 503975 / 4096 - 27315); temp_valid 1b1; state IDLE; end endcase end end endmodule这段代码有几个地方需要说明。第一温度换算我用了整数运算先把503.975放大1000倍变成503975算完之后再减27315273.15放大100倍最后结果就是温度放大100倍。这样避免了浮点运算在FPGA里更高效。第二状态机在WAIT状态等drdy而不是固定等几个周期这样更可靠。第三每次读完回到IDLE下一个周期又发起新的读请求形成连续监控。3.5 实测波形分析我用Vivado的ILA抓了一组实际波形读温度的完整过程大概是这样den拉高一个周期后drdy并没有马上来而是等了大概8个时钟周期才拉高。这8个周期里XADC在忙内部的事情。drdy拉高的那个周期do上出现了0x0A5C也就是53.15度。整个过程从发起到拿到数据大概10个时钟周期在50MHz时钟下就是200纳秒。这个速度对于温度监控来说绰绰有余。如果你发现drdy一直不来或者do上一直是0大概率是IP核配置有问题或者daddr设错了。温度寄存器地址是0x00这个别搞错。4. 常见问题与排查技巧实录4.1 drdy不拉高怎么办这是最常见的问题。我遇到过好几次总结下来原因无非这几个IP核没使能DRP接口。在Vivado里配置XADC的时候Interface Selection必须选DRP否则根本不会生成drdy信号。这个错误很隐蔽因为IP核照样能生成只是端口不对。daddr设成了保留地址。XADC的寄存器地址不是随便设的0x00到0x3F有明确的定义。如果你设了一个保留地址XADC不会响应drdy自然不来。温度是0x00VCCINT是0x01VCCAUX是0x02这几个常用地址记牢。den拉高的时间不对。den必须在dclk上升沿之前稳定如果跟时钟边沿太近可能采不到。建议用寄存器输出den保证时序。复位没释放。XADC的reset信号如果一直有效整个模块不工作。检查一下复位逻辑确保上电后reset已经拉低。4.2 读出来的温度明显不对有一次我读出来是负一百多度明显离谱。排查后发现是换算公式用错了把do[15:4]当成了有符号数。实际上温度ADC码是无符号的直接按无符号算就行。还有一种情况是读到了其他通道的数据。比如你daddr设的是0x00但XADC的序列器正在采其他通道do上可能是上一个通道的残留数据。解决办法是等drdy拉高再取数drdy为高时do一定是当前请求通道的数据。4.3 温度跳动很大怎么处理XADC的原始温度数据确实会有几度的跳动这是ADC噪声和芯片内部温度梯度造成的正常现象。如果你需要更稳定的读数可以开启XADC内部的平均功能。在IP核配置里有个Averaging选项可以设成16次、64次或256次平均。开启之后温度会平滑很多代价是响应变慢。我一般设16次平均兼顾稳定性和响应速度。如果只是做报警监控256次平均也没问题。4.4 常见问题速查表现象可能原因解决方法drdy一直不拉高IP核未使能DRP重新配置IP核Interface选DRPdrdy一直不拉高daddr为保留地址确认地址温度用0x00do数据为0den时序不对用寄存器输出den温度值离谱换算公式错误确认用无符号运算温度跳动大未开启平均开启16次或64次平均读到的不是温度通道未切换等drdy后再取数温度偏高很多芯片确实热检查散热不是代码问题4.5 几个实操心得第一调试阶段一定要把ILA用起来。把daddr、den、drdy、do都抓到ILA里一眼就能看出时序对不对。比看代码猜问题快十倍。第二先读VCCINT验证通路。VCCINT的地址是0x01正常值应该在1.0V左右对应的ADC码大概是0x0AAA左右。如果你读VCCINT能读到合理值说明DRP通路没问题再去读温度就顺了。第三温度报警阈值别设太高。我一般设80度报警85度强制降频或关机。7系列FPGA的结温上限是125度但超过100度之后时序余量会明显恶化不要等到那时候才处理。第四多通道轮询时加个延时。如果你要轮流读温度、VCCINT、VCCAUX每次读之间最好隔几个周期让XADC有时间切换内部通道。我一般隔16个周期实测很稳。5. 工程扩展与多通道监控思路5.1 从单温度扩展到全芯片监控温度只是XADC能读的其中一个参数。实际上XADC内部有一个完整的通道序列器可以监控VCCINT、VCCAUX、VCCBRAM等供电电压还能通过外部模拟输入通道监控板级电压。我在实际项目里一般会做一个轮询状态机依次读温度、VCCINT、VCCAUX把数据打包通过UART或者以太网上报给上位机。轮询的实现很简单把上面读温度的状态机扩展一下加一个通道计数器每读完一个通道就切换到下一个地址。地址依次是0x00、0x01、0x02。注意每次切换通道后要等drdy不要用固定延时。5.2 报警逻辑的设计XADC IP核本身有报警输出alarm信号会在温度或电压超过阈值时拉高。但我建议在逻辑里再做一层判断因为IP核的报警阈值是固定的而你可能需要根据工作模式动态调整。我的做法是把读到的温度值跟一个寄存器里的阈值比较超过就拉高一个自定义的alarm信号。这个阈值可以通过DRP动态写入也可以在逻辑里直接改。这样更灵活也方便做分级报警比如80度警告、90度降频、100度关机。5.3 温度数据的平滑处理如果你要做温度曲线显示原始数据跳动会很难看。除了开启XADC内部平均还可以在逻辑里做滑动平均。比如维护一个16深度的FIFO每次新数据进来就丢掉最老的算平均值。这样温度曲线会非常平滑适合上位机显示。滑动平均的代价是要用一点BRAM或者寄存器资源但对于7系列FPGA来说这点资源微不足道。我一般用16深度效果很好。5.4 跨平台注意事项XADC是7系列和UltraScale系列都有的模块但UltraScale的叫SYSMON寄存器地址和接口时序基本兼容但有些细节不一样。如果你从7系列迁移到UltraScale代码大体不用改但IP核要重新生成地址要对照新文档确认。另外Zynq系列里XADC的用法稍有不同因为PS端也能访问XADC。如果你用Zynq可以通过PS的XADCPS寄存器直接读不一定非要用PL的DRP。这个看具体需求纯PL方案更通用。5.5 一个完整的监控模块框架最后给一个我在项目里常用的监控模块框架把温度、电压、报警、平滑都整合在一起module sys_monitor ( input wire clk, input wire rst_n, output reg [15:0] temp_value, output reg [15:0] vccint_value, output reg [15:0] vccaux_value, output reg alarm ); // 通道轮询状态机 // 0: 温度 0x00 // 1: VCCINT 0x01 // 2: VCCAUX 0x02 reg [1:0] ch_sel; // ... DRP读写逻辑 // ... 数据换算 // ... 报警比较 // ... 滑动平均 endmodule这个框架的核心就是通道轮询加数据换算把前面讲的读温度逻辑复制三份改一下地址和换算公式就行。报警逻辑用一个比较器滑动平均用一个移位寄存器组。整体资源消耗很小7K325T上跑几百个都不成问题。我在实际使用中发现XADC读温度的精度和稳定性完全能满足工程需求关键是DRP时序要吃透drdy一定要等不能想当然地用固定延时。另外温度监控最好从项目一开始就加上不要等到板子出问题了才想起来。这个模块一旦调通后面所有项目都能复用性价比极高。