拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA片内温度电压监控:XADC DRP接口读写与状态机实现全解析

FPGA片内温度电压监控:XADC DRP接口读写与状态机实现全解析 调过一块7K系列板卡时遇到过一次很诡异的偶发复位排查到最后才发现是局部温度过高。当时温度监控就是靠FPGA内部XADC的DRP接口实时读出来的板卡上没有多余的外部ADC逻辑里也没地方挂一个专用监控芯片。那次之后我确信对于FPGA工程师来说XADC的DRP接口是绕不开的一课尤其是当你在纯逻辑环境里需要拿到片内温度、VCCINT、VCCAUX这些实时数据的时候。这篇文章我会从方案选型讲起把XADC IP核配置、寄存器理解、DRP读时序状态机实现、以及实际调试中的异常排查链路完整过一遍。内容以7系列和Zynq-7000为主兼顾UltraScale的差异提醒适合正在调XADC、或者打算用DRP接口做板卡健康监控的开发者直接参考。1. 方案选型三种读XADC的方式里为什么DRP最实用很多人第一次接触到XADC是因为Vivado的IP Catalog里有个XADC Wizard双击之后一脸懵不知道接口该怎么选。其实XADC的访问方式就三条路DRP接口、AXI4-Lite接口、JTAG接口。三条路各有各的适用场景但如果你是在纯逻辑里做监控DRP是性价比最高的选择。1.1 三条访问路径怎么选先看一张对比表把三条路径的使用场景拆开说访问方式典型场景实现成本实时性DRP接口纯逻辑读取温度/电压IP内部监控一个状态机即可高直接读写寄存器AXI4-LiteZynq PS通过Linux驱动读取需要AXI互联、地址译码取决于总线负载JTAGVivado硬件管理器离线看温度无需逻辑但需要外部工具低仅调试用AXI4-Lite在Zynq里用起来确实方便PS端跑一个Linux驱动/sys/bus/iio/devices底下直接读温度写上位机监控软件非常顺手。但代价是你得有PS还得挂一整套AXI总线逻辑侧如果只是为了在FPGA内部用温度做降频、过温保护、电压跌落检测就完全没有必要把AXI总线请进来。JTAG方式就不多说了Vivado的Hardware Manager里能直接看到芯片温度但那只是调试手段跑系统的时候不可能抱着电脑去读温度。DRP接口的全称是Dynamic Reconfiguration Port本质是Xilinx很多IP都带的一组低开销同步寄存器读写端口MMCM、GT、XADC都有。它不需要总线协议几个控制信号加上一个状态机的握手就能完成对XADC内部寄存器的读写。1.2 DRP接口的使用边界和代价DRP的好处是轻桎梏也很明显你必须自己把寄存器地址、数据格式、握手时序搞明白。相比AXI4-Lite那样自动帮你打点好一切DRP更像手动挡前面几十行代码都是靠自己写出来的。还有一点需要提前说明DRP和AXI4-Lite在XADC Wizard里通常是二选一。你在IP核界面勾选了DRP生成出来的端口就是dclk_in、den_in、dwe_in、daddr_in这一套如果勾选了AXI4-Lite出来的就是s_axi_*信号。不存在说两个接口同时都给你所以做方案的时候就要想清楚。选DRP还有个好处是它的时序和DCLK直接相关而这个DCLK可以由你自己的逻辑时钟分频得到不依赖专门的XADC时钟这点在第三章会展开讲。另外模块一旦写好后以后在别的工程里换一片FPGA只要地址没变、时序还是这套握手代码几乎可以复用。2. 动手配核前把通道、寄存器和数据格式这三样吃透DRP接口本身不复杂真正的坑全在XADC的寄存器模型上。拿到IP核就直接改代码大概率会在调试阶段反复怀疑人生。先把三个核心概念理清通道轮询是怎么发生的、要读哪些寄存器、数据读回来之后怎么换算成物理量。2.1 自动序列器会按顺序把结果刷新到寄存器XADC内部有两路12位ADC以及一个自动序列器。序列器的工作方式很容易理解它按预设的通道顺序去做一轮转换每转换完一个通道结果就自动写进对应的结果寄存器。你不需要主动去触发某个通道的转换只需要在合适的时机把结果寄存器里的值读走。这就带来了一个思维上的转变DRP读XADC本质上不是“发起一次转换等出结果”而是“从已经更新好的结果寄存器里抓取最新值”。转换这个动作在底层是连续自动进行的这也是为什么读时序状态机可以做得非常简单的原因。默认配置下自动序列器会覆盖内部温度、VCCINT、VCCAUX、VCCBRAM以及VP/VN这些内部和模拟输入通道。如果你只想读温度和VCCINT也可以只在IP核界面里勾选这两个通道这样可以减少序列长度让单一通道的更新频率更高。上电后不要立刻发起DRP读操作。XADC需要一点时间完成上电序列至少要等到EOS信号出现或者在逻辑里延时几百微秒再读否则很容易读到全0的初值。2.2 常用寄存器地址一览XADC的寄存器地址并不复杂但每次都要翻手册确实烦。我把调试中最常用的地址整理成一张表建议直接收藏寄存器地址含义0x00片内温度0x01VCCINT电压0x02VCCAUX电压0x03VCCBRAM电压0x04VP/VN外部模拟输入0x40状态寄存器0转换结束标志0x41状态寄存器1报警状态0x40状态寄存器里的EOC和EOS标志位非常关键一个代表单次转换完成一个代表整轮序列转换完成。调试时如果对自己读到的数据不放心可以先读0x40确认转换标志位正常再去信任0x00和0x01的数据。顺便提醒一句0x42和0x43是控制寄存器用来配置时钟分频和序列器控制。如果你用的是XADC Wizard的自动序列器模式逻辑代码里就不要去写这两个寄存器一旦写坏了整个通道轮询可能直接停摆现象比读不到数据还要难查。2.3 12位结果左对齐换算公式必须注意这是新手最容易摔跤的地方。XADC的转换结果是12位有效数值但在结果寄存器里是以16位的形式保存的有效位放在[15:4]低4位固定为0。也就是说你从DO读出0x9770真正的ADCCode是0x977而不是0x9770。数据格式这一点必须刻在脑子里。我见过不止一个工程师状态机写得很漂亮握手也完全正常最后换算出来的温度是-273.15度就是因为在公式里用错了位宽。换算公式如下温度Temp(°C) ((DO 4) × 503.975) / 4096 - 273.15VCCINT电压 ((DO 4) × 3.0) / 4096VCCAUX电压 ((DO 4) × 3.0) / 4096外部VAUX输入单端0~1V电压 ((DO 4) × 1.0) / 4096注意外部模拟输入和内部传感器的满量程不一样外部通道满量程是1V内部电源通道满量程是3.0V。公式里的503.975是温度传感器从绝对零度到满量程的跨度先换算出开尔文温度再减273.15得到摄氏度。实际验证时可以用一个很简单的基准室温25度左右温度通道的ADCCode约等于0x977于是DO大概率落在0x9770附近K7芯片的VCCINT如果是1.0V那么VCCINT通道的DO应该在0x5550附近。有了这两个数字拿到板子之后就能快速判断读到的数据靠不靠谱。3. XADC IP核配置关键选项、例化引脚和时钟分工有了上面的基础再回过来配置IP核就会清晰很多。XADC Wizard的界面选项比较多但如果你只是要DRP读温度电压真正动手需要确认的就那么几个地方。3.1 Vivado里XADC Wizard的关键选项在IP Catalog里搜XADC7系列环境下出来的名字是XADC Wizard。双击打开后有Basic、ADC Setup、Alarms、Single Channel这几个页面重点说几个必须看明白的选择Basic页面里最核心的是接口选型。在这里要明确勾选DRP接口而不是AXI4-Lite。有些版本里这个选项是以“Enable DRP”复选框的形式出现选中后生成的IP端口就是dclk_in、den_in、dwe_in、daddr_in、di_in、do_out、drdy_out这一套。如果你选成了AXI4-Lite后面找半天也找不到DRP相关信号就是这里选错了。ADC Setup页面里保持Channel Sequencer为自动模式。Average选项默认是None如果希望读数更平滑可以选16次平均或64次平均但代价是结果更新频率变低实时性要求高的场景不建议开。Alarms页面建议至少把Over Temperature报警打开。OT报警是XADC内部模拟比较器独立工作的不依赖你的轮询逻辑它一旦触发alarm_out会直接拉高。把这个信号接到逻辑里做硬保护比你在软件层轮询温度再判断要可靠得多。3.2 例化模板和易错引脚IP核生成后Vivado会给出一个例化模板以7系列的xadc_wiz_0为例常见的例化代码如下xadc_wiz_0 u_xadc ( .dclk_in (clk_100m), .den_in (drp_den), .dwe_in (drp_dwe), .daddr_in (drp_daddr), .di_in (drp_di), .do_out (drp_do), .drdy_out (drp_drdy), .reset_in (~rst_n), .vp_in (1b0), .vn_in (1b0) );有几个易错点需要特别说明。第一dclk_in必须有时钟哪怕你这个工程只是想让XADC闲着这个时钟也得给。DRP接口的所有读写操作都同步在这个时钟上没时钟等于没有读的途径。第二vp_in和vn_in如果没有接外部模拟输入不要悬空不写。标准做法是都接0否则可能会引入噪声影响内部传感器读数。第三reset_in不要直接接一个永远为低的信号。很多人喜欢用全局复位网络结果全局复位在某个状态下一直被拉高XADC的DRP逻辑就始终处于复位态DRDY永远不会拉高。调试时首先要确认复位释放了。不同的Vivado版本生成的端口名字可能略有差异比如reset_in和reset最终以IP核生成的例化模板为准上面代码省略号的用法也是为了避免误导。3.3 DCLK与内部ADC时钟的分工这是一个概念上很容易混淆的地方。DCLK只是DRP接口的同步时钟它和XADC内部做模拟转换的ADC时钟没有直接关系。ADC内部时钟是由XADC根据控制寄存器自动分频产生的你不需要在逻辑里管它。所以DCLK的选频比较自由工程里常用的100MHz直接用就行50MHz、75MHz也都没有问题。只要不超出器件手册给出的DCLK上限就可以。选用100MHz还有一个好处后续如果要做精确的定时轮询周期算起来比较方便。我见过有人把DCLK和XADC的采样率混为一谈误以为DCLK给多少就决定了XADC转换速度结果在时钟树上大费周章。实际上你改变DCLK只会影响DRP的读写快慢不影响模拟通道的采样速度这一点想通了后面的状态机设计就豁然开朗。4. DRP读操作的状态机实现从请求到DRDY握手DRP接口的读时序本身可以用一句话概括把地址放到DADDR上拉高DEN然后等DRDY拉高趁它拉高的时候把DO的数据锁存下来。但实际写状态机时有几个边界细节处理不好数据就是不对。4.1 一次读操作到底发生了什么我用口语化的方式描述一下整个过程方便新手在写代码时建立画面感。状态机处于空闲状态时DEN是低的。当你想读某个寄存器先把寄存器地址放到DADDR总线上然后拉高DEN。DRP逻辑内部采到DEN有效后会拿着这个地址去做内部寄存器访问这段时间DCLK会跑几个周期具体周期数不用关心你只需要等着DRDY。DRDY拉高表示这次读操作已经把数据准备好了DO上的值此刻是有效的。你必须在DRDY为高的这个时钟沿把DO锁存到自己的寄存器里然后拉低DEN回到空闲状态。如果不在DRDY有效窗口里锁存DO可能在之后变化就会拿到错的数据。这里有个常见的错误把DEN当作读使能一直拉高然后希望每个拍都能读到新数据。DRP不是这样工作的它是一次请求一次应答。每次读操作都要先拉低DEN等一次完整的握手结束后才能发起下一次请求。写操作和读操作的区别只在于DWE写的时候把DWE拉高同时把要写入的数据放到DI上其他流程类似。本文以读为例因为监控场景下读是最多的。4.2 可复用的单通道读状态机代码下面给一个可综合的单通道读状态机默认读温度寄存器0x00。为了让代码简洁我把IP核例化和状态机放在一起写方便理解整体连接关系。timescale 1ns / 1ps module xadc_drp_read #( parameter [6:0] RD_ADDR 7h00 )( input wire clk, input wire rst_n, input wire rd_req, output reg [15:0] rd_data, output reg rd_valid ); localparam S_IDLE 2d0; localparam S_WAIT 2d1; localparam S_HOLD 2d2; reg [1:0] state; reg den; reg [6:0] daddr; wire drdy; wire [15:0] dout; xadc_wiz_0 u_xadc ( .dclk_in (clk), .den_in (den), .dwe_in (1b0), .daddr_in (daddr), .di_in (16d0), .do_out (dout), .drdy_out (drdy), .reset_in (~rst_n), .vp_in (1b0), .vn_in (1b0) ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; den 1b0; daddr RD_ADDR; rd_data 16d0; rd_valid 1b0; end else begin case (state) S_IDLE: begin rd_valid 1b0; if (rd_req) begin daddr RD_ADDR; den 1b1; state S_WAIT; end end S_WAIT: begin if (drdy) begin rd_data dout; rd_valid 1b1; den 1b0; state S_IDLE; end end default: state S_IDLE; endcase end end endmodule调用这个模块时需要注意rd_req必须是一个单周期脉冲。如果上层的请求信号一直保持为高模块回到S_IDLE后会立刻触发下一次读操作导致数据一直在刷新。这倒不是什么致命问题但如果你只想采样一次就要在外部把rd_req做成脉冲或者在状态机里加入请求应答握手机制。上面代码里我加了一个S_HOLD状态但case里没有实际使用它。实际项目里当需要连续读取多个地址时S_HOLD可以用来插入等待周期。关于这一点下一小节展开说。4.3 多通道轮询的扩展思路实际工程里往往不只读一个温度还要读VCCINT、VCCAUX甚至多个外部模拟通道。扩展思路并不复杂核心是把读的地址从固定参数变成一个由上层控制的变量。推荐的做法是在状态机外加一个通道调度器它维护一张要读的地址表按固定周期把地址发给DRP读状态机。比如每50ms轮询一次0x00和0x01两个地址读完之后把结果打包供上层逻辑或UART上报使用。伪代码的思路如下reg [31:0] timer_cnt; reg [1:0] ch_sel; reg req_pulse; // 定时产生单周期脉冲作为通道调度的节拍 always (posedge clk) begin if (timer_cnt 32d5_000_000) begin timer_cnt 0; req_pulse 1b1; end else begin timer_cnt timer_cnt 1; req_pulse 1b0; end end // 每收到一个节拍切换一次通道地址 always (posedge clk or negedge rst_n) begin if (!rst_n) begin ch_sel 2d0; end else if (req_pulse) begin ch_sel ch_sel 2d1; end endch_sel为0时读温度为1时读VCCINT为2时读VCCAUX以此类推。调度器把来自DRP读模块的rd_valid信号作为应答应答回来之后才切换下一次请求避免你在某个通道还没读完时就发下一个请求。这里有一点要特别注意XADC的结果寄存器是自动刷新的但不是每个寄存器都在每个时刻都有新值。序列器有自己的转换节奏你连续读温度寄存器可能在短时间内读到的是同一个旧值这属于正常现象。所以轮询周期没必要做得太快十几毫秒到几十毫秒的间隔完全足够。5. 调试实战数据异常时的完整排查链路配置和代码都写完后上板调试阶段才是真正考验人的地方。这一节我把实际操作中遇到的几类典型异常现象列出来从现象倒推原因给出完整的排查链路而不是直接丢一个结论。5.1 现象一读回来的数据全是0这个现象在第一次上板时最容易出现。看到ILA里DO一直是0先别急着怀疑代码状态机写错了按下面的顺序排查。第一步确认DCLK真的进来了。看起来是废话但我真的见过有人把dclk_in接到一个没被约束的自由时钟上综合后时钟树没长出来DRP永远不工作。第二步确认复位释放了。在很多工程里全局复位信号被逻辑里某个fatal错误一直拉低结果整个IP核都处于复位状态。把reset_in直接接固定的1b0如果不是低复位的话试一下往往立刻就好了。第三步看DRDY有没有脉冲。如果DEN都拉高了很长时间DRDY始终不拉高说明DRP请求根本没有被响应。这时候去检查IP核的接口是不是真的配置成了DRP而不是AXI4-Lite。这个错误我在3.1里提醒过但实际调试中依然经常有人踩。第四步确认IP核内部至少勾选了一个通道。如果自动序列器里一个通道都没启用XADC没有数据来源结果寄存器就一直保持初值0DRP读出来的自然也是0。ILA在这里是排障利器把den_in、daddr_in、drdy_out、do_out四个信号拉进ILA一眼就能看出问题出在请求侧还是应答侧。5.2 现象二温度换算是-273.15度这个现象的特征非常明显读到的DO值可能是0x9770这样的合理数值但一换算就变成了一个极其离谱的负数甚至就是-273.15。问题几乎可以锁定在数据格式处理上。我在2.3里强调过XADC结果寄存器是左对齐的12位有效数据在bits[15:4]低4位是0。公式里的ADCCode必须是DO右移4位之后的值不能直接用DO参与计算。举个具体例子。室温25度理论ADCCode是0x977也就是十进制的2423。正确换算过程为DO 0x9770右移4位得到0x0977即2423温度 2423 × 503.975 / 4096 - 273.15 ≈ 25度如果你忘了右移用0x9770直接算结果会变成一个比实际温度大得多的数值再减去273.15如果代码里还是用整数除法很容易就算出负数或者一个没有任何物理意义的值。还有一种情况是DO数值本身就挺大看起来像温度但换算出来比正常值高一截。此时也要回到数据格式上检查是否做了右移只是你恰好因为其他因素抵消了一部分偏移。5.3 现象三同一个通道读数长时间不动如果常温下温度读数确实应该比较稳定但哪怕拿热风枪吹芯片数值也只是缓慢变化这往往不是DRP读时序的问题而是XADC内部平均或者序列配置的问题。先检查IP核的Average选项。如果开了256次平均结果更新周期会拉长很多温度响应会非常迟钝。实测时如果发现温度半天不动先把Average改为None再验证一次通常立竿见影。还有一种可能是你在逻辑里不小心写了控制寄存器0x42或0x43把序列器设置改乱了。XADC Wizard在自动模式下会在上电初始化时把这些寄存器设置好但用户逻辑如果后续再写入可能把通道使能或者序列模式覆盖掉导致某个通道的结果寄存器停止更新。调试时如果发现自己代码里有对0x42/0x43的写操作先临时注释掉试试。5.4 排查前先用ILA把信号拉出来总结三条异常现象每一类问题的排查都离不开ILA。我的习惯是在一开始调试DRP时就把下面几组信号全部挂到ILA上跑一轮抓一轮眼见为实信号作用den_in你发的请求是否真正送到了IP核daddr_in请求的地址是否正确drdy_outIP核是否回复了握手do_out读回来的原始数据state你自己状态机现在停在哪一步把state信号同步引到ILA里是我特别推荐的做法它能直接告诉你状态机是不是卡在了某个分支。DRP调试的难点很多时候不在协议本身而在于状态机和握手信号之间的配合有了ILA的波形几乎所有的时序问题都可以在十分钟内定位。6. 把DRP读取封装成通用小模块后监控才好做经过前面这些步骤DRP读XADC的链路基本能稳定跑起来了。但实际项目里我不建议每次用到XADC都在顶层现写状态机更推荐把DRP读取、右移对齐、数值换算整个封装成一个通用小模块。封装的好处很直接对外接口只需要暴露请求信号、地址、数据、应答这四个东西上层逻辑不用关心DRP时序也不用关心左对齐还是右对齐。温度告警、电压跌落检测、UART上报、PCIe读取都走同一个接口模块。我在封装时通常会保留一个debug_bus输出把state、drdy、daddr、do这些内部信号引出来给ILA这样跑板出问题时不用改逻辑就能看到内部时序。另外一个值得留的接口是转换好的物理量比如温度直接用摄氏度16位定点数表示电压用毫伏表示省得每个使用方都去查公式重复计算。如果是在Zynq平台上PS端也可以走AXI4-Lite去读同样的寄存器但既然逻辑里已经通过DRP拿到了数据链路就完全不需要再依赖PS这点在纯FPGA方案里尤其重要。最后分享一个调试阶段的小技巧板卡上电后先用Vivado Hardware Manager读一次片内温度把它作为基准再对比你DRP读上来的温度值。两边能对得上就说明你的配置、状态机、换算公式整个链路都是通的后面再做更复杂的监控逻辑也有底气。
返回列表