ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FPGA自带温度传感器读取:XADC IP核与Verilog状态机完整实现

2026/9/29 17:09:20 拓冰建站 浏览量
FPGA自带温度传感器读取:XADC IP核与Verilog状态机完整实现 FPGA跑着跑着烫手你却说不出它到底多少度这在排查逻辑和供电问题时特别被动。其实Xilinx 7系列FPGA内部早就集成了一块双12位ADC叫XADC只要通过XADC IP核把内部温度寄存器读出来就能实时监控芯片温度甚至能驱动风扇调速、触发过热保护。这篇文章我会从IP核配置、Verilog状态机到仿真上板完整走一遍读取FPGA内置温度传感器的全过程代码可以直接抄注释也按工程标准写好了适合正在做FPGA监控系统或者刚接触XADC的同学。1. 为什么我说XADC是每个FPGA开发者都该掌握的“自带仪表”1.1 XADC到底能干什么Xilinx 7系列FPGA在芯片内部集成了一颗双12位、最高1MSPS的ADC也就是XADC。这颗ADC不仅能测外部模拟信号还专门留出了几路内部监控通道比如芯片结温、VCCINT核心电压、VCCAUX辅助电压、VCCBRAM存储电压。其中最有用的就是温度通道因为在工程调试阶段往往一跑大资源逻辑或者高速接口芯片就开始明显升温如果能在RTL里实时看到温度值很多问题一下子就能定位到“是不是温度导致时序收敛变差”这种方向上去。XADC内部温度测量用的是一片温度敏感二极管温度变化会转化成电压变化再由ADC量化成12位数据。这套测量链路的精度经过Xilinx出厂校准在0到85摄氏度的范围内误差一般在正负几度以内做工业级板卡的温度监控完全够用。更重要的是这颗ADC是芯片自带的不需要外挂传感器也不占用额外的PCB面积直接通过IP核或原语就能访问。1.2 IP核和原语到底选哪个XADC本身是一个硬件原语但直接在RTL里例化原语比较麻烦因为它涉及到校准系数、配置寄存器、报警逻辑还有一堆采样通道的控制时序。实际工程里我强烈建议直接用Vivado提供的XADC Wizard IP核也就是xadc_wiz。IP核把底层寄存器、校准参数、报警阈值和可选的AXI接口都封装好了你可以像用普通IP核一样例化它省去大量手写配置寄存器的时间。XADC IP核对外提供两种主接口AXI4-Lite和DRP。如果是在Zynq里用ARM核读温度走AXI4-Lite很自然但如果你的项目是纯FPGA逻辑比如温度监控、风扇调速、过温保护这类场景用DRP接口会更轻量不占AXI互联资源控制时序也直观。我这边所有代码都是基于DRP接口写的这也是很多国产FPGA兼容设计和参考工程里最常见的形式。1.3 我最终采用的读取方案整个工程我拆成了四个模块XADC IP核、温度读取状态机、温度数值换算、数码管显示。IP核负责采集温度并产生EOC转换结束信号状态机在EOC到来后发起一次DRP读操作把温度寄存器值取回来换算模块把这个原始值转成适合人类阅读的摄氏度最后通过数码管扫描显示出来。这里有一个很关键的思路不要每时每刻都去读XADC。XADC在连续采样模式下大约每1微秒完成一次转换我们只需要在EOC脉冲到来后读一次温度寄存器其余时间让状态机休眠即可。这样既不会给DRP总线带来不必要的压力也能保证你看到的数据永远是最近一次转换结果。2. Vivado里配置XADC IP核的完整操作2.1 新建XADC IP核时的关键选项打开Vivado工程后在IP Catalog里搜索“XADC”找到XADC Wizard双击创建。组件名我习惯保持默认的xadc_wiz_0方便后面直接复制代码。Basic配置页有四个地方需要特别关注。第一Interface选择DRP不要选AXI4-Lite否则后面还要多挂一组总线逻辑。第二ADC Settings里把Sequencer Mode设为Continuous Sequence这样XADC会自动循环采样所有使能的通道。第三启用Temperature通道其余通道按需勾选我的例子里只用了温度通道。第四DCLK Frequency里填入你实际的输入时钟频率比如50MHzIP核会根据这个频率自动计算内部时序参数。Alarm选项卡可以设置温度报警阈值。默认是结温超过某个值就拉高alarm_out信号我建议一开始先保留默认值等能正常读到温度了再按自己的散热方案去调。Output Options里可以勾选生成Verilog例化模板文件后面接信号时会用到。2.2 例化模板里的信号怎么接XADC IP核生成后在Sources窗口里展开IP核并双击打开Example Design能看到官方生成的例化代码。DRP模式下需要关注的信号主要有这些信号方向信号名称作用输入daddr_in[6:0]DRP寄存器地址输入dclk_inDRP工作时钟必须和配置里声明的一致输入den_inDRP读/写使能脉冲输入di_in[15:0]DRP写数据输入dwe_inDRP写使能读操作固定为0输出do_out[15:0]DRP读数据输出drdy_outDRP读数据有效信号输出eoc_outADC转换完成脉冲输出eos_outADC序列完成脉冲输出alarm_out报警输出含温度和电压报警温度寄存器的DRP地址是7’h00该寄存器高12位保存的就是ADC转换后的温度原始值低4位固定为0。如果需要读取VCCINT地址换成7’h01VCCAUX对应7’h02VCCBRAM对应7’h03。我们在状态机里可以直接把daddr固定为0只读温度通道。2.3 一个容易踩坑的时钟问题XADC的DCLK输入不是随便给的。它的频率范围一般在1MHz到250MHz之间但IP核配置界面里你填多少内部时序就会按多少计算实际给的时钟频率必须和配置完全一致否则采样率和报警寄存器都会算错。我自己踩过一次开发板时钟是50MHz偏偏我当时图省事在IP核配置里填了100MHz结果上板之后温度读数明显偏高排查了很久才发现是DCLK配置和实际时钟不匹配。所以建IP的时候一定要看清楚时钟来源。另外如果XADC使用的是PLL产生的时钟要确保PLL锁定之后再让XADC工作。翻遍Xilinx文档也没有强制要求但实际电路里如果时钟在复位期间还在抖动转换结果可能会出现一些毛刺。我习惯在复位释放之前等PLL locked信号再启动温度读取状态机。3. 核心Verilog代码逐段拆解3.1 第一段XADC温度读取状态机温度读取的逻辑不复杂核心是一个小状态机。系统空闲时等待EOC上升沿这个边沿代表XADC刚完成一次转换收到边沿后我们向DRP接口发起一次读操作地址指向温度寄存器等到drdy为高时把do_out上的数据锁存下来同时点亮一个指针提示上层模块数据已经更新。我把这个模块单独写成xadc_temp_reader.v方便后面复用module xadc_temp_reader ( input wire clk, // 系统时钟50MHz input wire rst_n, // 低电平复位 input wire eoc, // XADC转换结束脉冲 input wire [15:0] do, // DRP读数据 input wire drdy, // DRP数据有效 output reg [6:0] daddr, // DRP寄存器地址 output reg den, // DRP读使能 output reg dwe, // DRP写使能固定0 output reg [15:0] di, // DRP写数据固定0 output reg [15:0] raw_temp, // 温度寄存器原始值 output reg temp_valid // 一次有效读取完成脉冲 ); reg [1:0] state; reg eoc_d0, eoc_d1; wire eoc_pos eoc_d0 ~eoc_d1; localparam IDLE 2d0; localparam START_READ 2d1; assign dwe 1b0; assign di 16h0; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; daddr 7h00; den 1b0; eoc_d0 1b0; eoc_d1 1b0; raw_temp 16h0; temp_valid 1b0; end else begin eoc_d0 eoc; eoc_d1 eoc_d0; case (state) IDLE: begin temp_valid 1b0; if (eoc_pos) begin daddr 7h00; // 温度寄存器地址 den 1b1; // 启动一次DRP读 state START_READ; end end START_READ: begin den 1b0; if (drdy) begin raw_temp do; temp_valid 1b1; state IDLE; end end default: state IDLE; endcase end end endmodule这段代码里有一个细节我在工程中调整过多次EOC边沿检测用两级触发器打拍目的是消除亚稳态风险。XADC的eoc_out和系统时钟在理论上已经由IP核同步过但为了仿真严谨和实际抗干扰加两级触发器成本极低值得保留。状态机只有两个状态等于让DRP读操作每次都从头开始。有人可能会问为什么不连续发读请求其实连续读完全没有必要因为温度本身变化很慢一秒钟能有几十次更新已经足够监控用了。EOC每次到来读一次数据永远是最新的逻辑又简单这就是上面那句“让状态机休眠”的思路也是最省资源、最不容易出错的做法。3.2 第二段温度原始值怎么换算成摄氏度XADC温度寄存器的原始值是12位ADC码。根据UG480手册的线性关系0摄氏度对应的ADC码大约在542左右温度每升高1摄氏度ADC码大约增加1/0.4846也就是每大约2个码对应1摄氏度。为了在数码管上显示整数温度我直接做了一个粗略的线性近似温度整数摄氏度约等于(adc_code - 542)除以2。这个公式在0到100摄氏度范围内误差在2摄氏度以内做演示和监控完全够用。如果你要做精确温度采集可以在FPGA里用定点乘法器实现更精确的换算公式原理是一样的只是要额外处理小数位和除法。换算模块我单独写成temp_calc.vmodule temp_calc ( input wire [15:0] raw_temp, output reg [11:0] temp_int // 整数摄氏度 ); wire [11:0] adc_code raw_temp[15:4]; // 取12位有效数据 always (*) begin if (adc_code 12d542) temp_int (adc_code - 12d542) 1; else temp_int 12d0; end endmodule为什么要取raw_temp的高12位因为XADC的温度寄存器低4位是固定0有效数据都在[15:4]。如果你把整个16位拿来算温度结果会整整多出16倍读出来动辄几千度这个坑在论坛里反复被问到。3.3 第三段二进制数转成BCD码数码管不能直接显示二进制必须先把换算后的二进制温度转成BCD码。最常用的方法是“移位加3”算法也叫Double Dabble。原理很简单从二进制最高位开始逐位左移每次左移后检查每个BCD位是否大于4大于4就加3经过足够的位移次数后BCD区里留下的就是十进制结果。这里直接给出一个12位二进制转16位BCD的模块module bin2bcd ( input wire [11:0] bin, output reg [15:0] bcd ); integer i; reg [27:0] tmp; always (*) begin tmp 28d0; tmp[11:0] bin; for (i 0; i 12; i i 1) begin if (tmp[27:24] 4) tmp[27:24] tmp[27:24] 3; if (tmp[23:20] 4) tmp[23:20] tmp[23:20] 3; if (tmp[19:16] 4) tmp[19:16] tmp[19:16] 3; if (tmp[15:12] 4) tmp[15:12] tmp[15:12] 3; tmp tmp 1; end bcd tmp[27:12]; end endmodule这个模块是纯组合逻辑综合后相当于一串加法和移位电路在几十MHz下没有任何时序压力。注意for循环的次数要和输入位宽匹配12位输入就循环12次写多了会出错写少了结果不对。3.4 第四段数码管扫描显示温度值现在变成了BCD码但一颗FPGA不可能为每一位数码管单独分配几十个引脚所以实际开发板都会把数码管段选并在一起用扫描方式轮流点亮。只要扫描频率高于人眼的闪烁阈值看起来就是4位数同时亮着。我写了一个通用的四位数码管扫描模块输入就是上一步生成的16位BCD数据module seg_scan_display ( input wire clk, input wire rst_n, input wire [15:0] bcd_data, // 4位BCD数据 output reg [6:0] seg, // 段选共阳极低有效 output reg [3:0] sel // 位选低有效 ); reg [17:0] cnt; reg [1:0] scan; reg [3:0] current_data; always (posedge clk or negedge rst_n) begin if (!rst_n) cnt 18d0; else cnt cnt 1b1; end assign scan cnt[17:16]; always (*) begin case (scan) 2d0: {sel, current_data} {4b0111, bcd_data[3:0]}; 2d1: {sel, current_data} {4b1011, bcd_data[7:4]}; 2d2: {sel, current_data} {4b1101, bcd_data[11:8]}; 2d3: {sel, current_data} {4b1110, bcd_data[15:12]}; default: {sel, current_data} {4b1111, 4h0}; endcase end always (*) begin case (current_data) 4h0: seg 7b1000000; 4h1: seg 7b1111001; 4h2: seg 7b0100100; 4h3: seg 7b0110000; 4h4: seg 7b0011001; 4h5: seg 7b0010010; 4h6: seg 7b0000010; 4h7: seg 7b1111000; 4h8: seg 7b0000000; 4h9: seg 7b0010000; default: seg 7b1111111; endcase end endmodule段码表我按最常见的共阳极数码管写的段a到g分别对应bit6到bit0小数点该灭就灭不参与显示。如果你的开发板是共阴极数码管把每段取反即可。3.5 顶层模块把四块拼起来顶层模块负责把XADC IP核和上面三个自定义模块串起来。我假定你的工程里已经添加了名为xadc_wiz_0的XADC IP核并且开发板有一个50MHz系统时钟。module xadc_top ( input wire sys_clk, input wire rst_n, output wire [6:0] seg, output wire [3:0] sel, output wire temp_alarm_led ); wire [6:0] daddr; wire den; wire dwe; wire [15:0] di; wire [15:0] do; wire drdy; wire eoc; wire eos; wire alarm_w; // XADC IP核 xadc_wiz_0 u_xadc ( .daddr_in (daddr), .dclk_in (sys_clk), .den_in (den), .di_in (di), .dwe_in (dwe), .do_out (do), .drdy_out (drdy), .eoc_out (eoc), .eos_out (eos), .alarm_out (alarm_w), .alarm_temp_out (), .alarm_power_out () ); // 温度读取状态机 wire [15:0] raw_temp; wire temp_valid; xadc_temp_reader u_reader ( .clk (sys_clk), .rst_n (rst_n), .eoc (eoc), .do (do), .drdy (drdy), .daddr (daddr), .den (den), .dwe (dwe), .di (di), .raw_temp (raw_temp), .temp_valid (temp_valid) ); // 温度换算 wire [11:0] temp_int; temp_calc u_calc ( .raw_temp (raw_temp), .temp_int (temp_int) ); // 二进制转BCD wire [15:0] bcd_data; bin2bcd u_bcd ( .bin (temp_int), .bcd (bcd_data) ); // 数码管扫描显示 seg_scan_display u_seg ( .clk (sys_clk), .rst_n (rst_n), .bcd_data (bcd_data), .seg (seg), .sel (sel) ); assign temp_alarm_led alarm_w; endmodule这里有个细节值得提一下我把xadc_wiz_0的alarm_out直接引到了LED上这样即使不上数码管也能通过LED快速判断芯片是否触发过温报警。做项目时我习惯给报警信号留一个LED或者GPIO方便现场快速验证。4. 仿真与上板验证一步一步确认数据通路4.1 Testbench怎么写很多人拿到XADC工程后第一步就上板结果发现数码管不亮或者数据不动又不知道哪里出了问题。我建议先在Vivado里做一次行为仿真。XADC IP核的仿真模型会在仿真环境里自动模拟ADC转换EOC、DRDY这些信号都能看到。Testbench不需要写得太复杂只要能拉高复位、给一个50MHz时钟然后把顶层模块例化进去就行module tb_xadc_top; reg sys_clk; reg rst_n; wire [6:0] seg; wire [3:0] sel; wire temp_alarm_led; initial begin sys_clk 0; rst_n 0; #100 rst_n 1; end always #10 sys_clk ~sys_clk; xadc_top u_top ( .sys_clk (sys_clk), .rst_n (rst_n), .seg (seg), .sel (sel), .temp_alarm_led (temp_alarm_led) ); endmodule跑仿真时重点观察EOC、DRDY和DO这几个信号。如果EOC周期性能到每次EOC之后DRDY也出现高电平脉冲DO上能看到一个非零的温度寄存器值说明XADC IP核和读取状态机的工作时序已经接对了。4.2 波形上要重点确认的三件事第一确认EOC上升沿出现后DEN信号紧接着拉高并且只保持一个周期。如果DEN被拉高超过一个周期DRP接口可能识别成两次操作需要回头检查状态机的状态切换。第二确认DRDY信号到来时DO上的数据和RAW_TEMP锁存值一致。这一步能直接判断寄存器地址是否写对了。第三确认TEMP_VALID脉冲不是一直为高而是每次读完数据才跳一下否则说明状态机可能停留在某个状态没有正确回跳。我通常在仿真里跑至少50微秒因为XADC转换周期大约1微秒50微秒能覆盖几十次完整读取足以确认时序稳定。4.3 上板实测记录仿真通过后把生成好的BIT文件下载到开发板观察数码管。第一次上板我预期看到30到50摄氏度之间的数据因为FPGA即使待机也会有一定温升。如果数码管显示的是个位数或者几百的大数基本可以断定温度换算模块的取位有问题最常见的就是没有右移4位直接用整个16位寄存器计算。我有一次上板显示温度比实测低了差不多10度最后发现是手不小心碰到芯片散热片导致热传太快读数自然偏低。后来我用热风枪在芯片上方吹风温度读数会明显上升再把手移开读数慢慢回落才确认整个链路是真的在工作。如果你也想验证动态响应这个方法很好用。5. 常见问题速查与工程经验5.1 常见问题速查表故障现象可能原因排查方向温度一直显示0XADC IP核未配置连续序列或者DEN时序不对检查配置里的Sequencer Mode再看仿真波形中EOC是否存在温度值偏大很多倍用了整个16位寄存器做换算确认是否取do[15:4]温度值负值或乱跳温度寄存器地址写错把daddr固定为7’h00重新测试数码管全灭位选信号极性错误确认开发板数码管是共阳极还是共阴极数码管亮度不均扫描频率太慢将扫描时钟分频系数改小提高扫描频率XADC读出来一直不更新DCLK频率配置和实际不一致严格保证IP核配置频率等于实际时钟报警LED常亮温度超过默认阈值检查实际温度确认是否过温或调高报警阈值5.2 调试中的几个独家经验第一个经验是千万不要忽略IP核配置界面里的DCLK频率参数。这个参数不仅影响DRP时序计算还影响报警比较器和采样时钟分频。很多拿到模板就改时钟的人烧进去以后读数就是不对问题恰恰出在这里。第二个经验是二进制转BCD的模块不要用组合逻辑硬扛大位宽。12位到16位没问题但如果你把整个16位原始温度寄存器拿来转或者后面要显示带小数的值组合逻辑路径会变得很长高主频下容易出现时序违例。遇到这种情况就改成多周期移位寄存器用状态机一位一位移。第三个经验是在工程里保留一个“直通测试”模式。我通常会在顶层加一个拨码开关拨到直通模式时数码管直接显示12位ADC原始码的高四位这样调试时能快速区分问题出在XADC读取还是后续显示换算。等确定原始数据没问题再把开关拨到正常模式问题范围一下就缩小了。5.3 这个项目还能怎么扩展读完温度之后扩展方向其实很多。你可以把XADC IP核的VCCINT、VCCAUX、VCCBRAM三个电压寄存器都读出来在数码管上轮流显示做一个简易电源状态指示器。也可以把温度值和设定的阈值做比较输出PWM波形控制散热风扇转速实现一个简单的温控风扇系统。如果以后要做Zynq平台可以把这个DRP读取模块换成AXI4-Lite接口用ARM核去读温度软件里做更复杂的温度补偿和系统调度。但核心思路完全一样EOC来了读一次寄存器数据进ARM剩下的就是软件逻辑了。我在实际工程里最喜欢XADC的一点就是它让FPGA真正有了“自感知”能力。系统过热前你不仅能预判还能主动响应这在工业现场、电力电子、通信设备里都是很实用的能力。多花一晚上把这条链路跑通后续项目会省下很多排查异常的时间和精力。