Zynq UltraScale+ SYSMON实战:从寄存器读出温度电压到Linux报警中断
刚拿到ZCU104那块板子的时候,我一度觉得系统监视模块是个鸡肋——板子上例程已经把温度读出来了,直接print就行,还要我研究寄存器干什么。直到后面做电源轨验证,我才发现自己拿万用表去点BGA底下的电容是一件多蠢的事。Zynq UltraScale+ MPSoC这块芯片自带的SYSMON,不仅能在用户态一两条命令拿到结温,还能把VCCINT、VCCAUX、VCCBRAM这些内部电源轨实时监测出来,关键是精度足够你看趋势、设报警。这篇文章就把我在这套系统上从“会读例程”到“自己写驱动”的过程完整梳理一遍,偏实战,适合正在调MPSoC板子、或者想在Linux下做温度电压监控的朋友。
1. SYSMON在Zynq UltraScale+里的定位:嵌在芯片里的多功能万用表
1.1 从XADC到SYSMON:监控器到底变在哪
用过7系列的话,一定对XADC不陌生。传统的XADC是一个双通道12位ADC硬核,可以采内部温度,也可以通过VAUX引脚采外部模拟电压。到了Zynq UltraScale+ MPSoC这一代,这部分模块改叫SYSMON,功能上不是简单换个名字,而是把监控对象扩大了一圈。它除了保留温度采样,还直接对芯片内部的多个电源域做了分压采样,VCCINT、VCCAUX、VCCBRAM这些轨,你不需要在板级放大电路上动手脚,SYSMON硬核已经把电阻分压网络集成进硅片内部,读数直接通过寄存器给你。
从工程视角看,这个设计最大的价值是降低测量回路的不确定性。你外接万用表或者ADC,不可避免要引入探针接触电阻、引线噪声、共地压差,搞不好还会把高速数字信号的开关噪声耦合进采样结果。SYSMON是片内采样,模拟前端已经在die内部,走线短、抗干扰强,拿到的是芯片角落的真实电压,做电源跌落定位的时候特别有用。
还有一个和XADC的明显区别:SYSMON在MPSoC里属于PL(可编程逻辑)侧的资源,但它的控制端口是AXI4-Lite接口。也就是说,你可以在Vivado Block Design里把它当成一个普通AXI外设接上,然后由PS(ARM核)通过寄存器读写来触发转换、读结果。这个改动让上位机介入变得非常干净,不需要再像老一代那样依赖JTAG或者私有的DRP端口。
1.2 能看的东西比想象多:温度、电源轨、外部差分输入
先列一下我在实际工程里用到过的通道,大致分成三类。
第一类是温度通道。SYSMON内建温度传感器,测的是结温,也就是硅片内部实际温度。注意这和散热器表面温度不是一回事,满载跑起来,结温往往比外壳高十几度,所以做功耗和散热评估要以这个数为准。
第二类是内部电源轨。MPSoC内部电源种类很多,常规能监控的包括:
- VCCINT:PL核心逻辑电压,通常在0.85V左右
- VCCAUX:辅助电压,1.8V或3.3V级别
- VCCBRAM:Block RAM供电轨
- VCCO:I/O bank供电
- VCCINT_IO、VCCAUX_IO:特定IO域专用轨
这些通道在IP配置界面里默认就会打开,转化好的值放在一长串连续的寄存器里,读取非常方便。
第三类是外部模拟输入,也就是VAUX引脚。如果你需要在板子上额外监控一路外部电压,比如某个DDR电源或者PHY的模拟供电,可以把信号分压后接到VAUXP/VAUXN差分对,然后复用SYSMON的ADC通道。这个用得不多,但关键时候能省掉一颗外部ADC。
1.3 一次转换到底多快:采样率、精度、平均
SYSMON内部是一个逐次逼近型ADC,典型采样率在200kSPS量级,单通道转换时间大概几微秒。对于温度电压监控这种慢变量,这个速度绰绰有余。你甚至可以让它跑序列模式,把所有通道轮询一遍,每轮也就几十微秒,根本感觉不到延迟。
精度方面,原始ADC是10位有效分辨率,但寄存器里以16位左对齐形式保存。10位分辨率换算到温度,差不多能分辨0.5℃以内,测电压轨也能分辨几个毫伏。如果嫌噪声大,SYSMON还提供平均滤波器,可以对单通道做最多64次平均,代价是转换周期变长。用来做长期监控,我一般把平均值设在16或64,读数非常稳定。
2. 寄存器访问逻辑:独立模式、序列模式和一条换算公式
2.1 通道选择寄存器与独立模式
SYSMON最基础的用法是“独立模式”。你往通道选择寄存器里写一个通道号,硬件就触发一次该通道的转换,转换完成后结果落在对应的测量寄存器里。
这里有个容易看晕的地方:通道选择寄存器地址是0x20,而通道号并不是1、2、3这种自然递增。以温度通道为例,标准映射是:
| 通道号 | 目标 |
|---|---|
| 0 | 温度 |
| 1 | VCCINT |
| 2 | VCCAUX |
| 3 | VCCBRAM |
| 4 | VCCO |
| 5 | VCCINT_IO |
| 6 | VCCAUX_IO |
具体编号在UG580里有一整张表,不同型号略有差异。你不需要背,但要知道有这个映射关系,因为拿到的寄存器值必须对应到正确通道,否则后面算出来的物理量全是错的。
独立模式适合做单点巡检,比如上电自检时只读一次温度。缺点是每次都要软件触发,如果要同时盯好几个电源轨,你得不断切换通道。
2.2 序列模式适合没人管的实时监控
很多时候我们希望SYSMON自己在那循环采,不需要CPU反复干预,这就是序列模式。你在序列通道寄存器里把需要监控的通道使能位写1,然后启动序列器,硬件会自动按顺序把每个通道轮流转换一遍,结果不断刷新到对应的测量寄存器。CPU想什么时候读就什么时候读,永远拿到的是最近一次的结果。
序列模式还有个额外好处:报警功能依赖它。报警阈值比较器会在每个通道转换结束后自动和阈值寄存器比较,超了就置位报警状态。这个机制做保护和告警特别合适,下文我会专门讲。
我习惯的配置是:温度、VCCINT、VCCAUX、VCCBRAM四个通道进序列,平均设为16,这样每轮刷新时间在毫秒级,足够满足绝大多数监控场景。
2.3 温度电压换算:从寄存器值到物理量
寄存器里拿到的是一串16位数字,要换算成温度或者电压,必须用官方给的线性公式。
温度换算公式是:
温度(℃) = 509.494 × (code / 65536) - 273.75举个例子,我从温度寄存器读到0x961E,十进制是38430。套进公式:
509.494 × 38430 / 65536 - 273.75 ≈ 25.07℃这个值和我当时办公室环境温度非常接近,说明芯片基本处于空闲状态。
电压通道的换算更简单,以3V满量程为例:
电压(V) = 3.0 × (code / 65536)如果VCCINT寄存器读到0x49A6,十进制18854,换算出来就是:
3.0 × 18854 / 65536 ≈ 0.863VZynq UltraScale+的VCCINT典型值0.85V,实测0.863V在允许范围内,说明供电轨工作正常。
有一点特别提醒:温度寄存器用的是偏移二进制格式,不是补码。也就是说,寄存器值本身就是“温度物理量加上273.75”线性映射出来的结果,并不是直接把负数温度用补码表示。所以写报警阈值时,千万别把阈值当成带符号数处理,直接按公式把目标温度算成无符号16位码,否则阈值会错得离谱。
3. 在Vivado Block Design里把SYSMON接上PS的AXI总线
3.1 添加SYSMON IP的配置要点
打开Vivado的Block Design,在IP Catalog里搜索“SYSMON”,能找到“System Monitor”这个IP。添加进画布后,双击打开配置界面,你会看到好几页配置项,但真正影响使用的只有几个地方。
第一是模式选择。默认有三种模式:独立模式、连续序列模式、安全模式。我一般选连续序列模式,理由前面说了,硬件自己循环采样,CPU零负担。选完模式后,下方会有“Average”选项,建议设成16或64,启动后读数稳定很多。
第二是报警配置。配置界面里有一个Alarm标签,可以勾选需要监控的报警项。勾选后,界面会让你填阈值,你填温度和电压的物理量,IP会帮你换算成对应寄存器码,这比自己手算省心。但如果是在代码里动态修改阈值,必须知道换算关系,不能依赖工具。
第三是通道使能。在Sequence页面里勾选你想加入序列的通道,比如Temperature、VCCINT、VCCAUX、VCCBRAM。记住,通道勾选得越少,轮询一轮越快;勾选过多且用64次平均的话,报警响应会变慢,要注意取舍。
3.2 与PS侧AXI互联的连接方式
IP添加完成后,把SYSMON的s_axi端口接到AXI Interconnect上。通常流程是:先添加Zynq UltraScale+ MPSoC PS IP,打开PS配置,把M_AXI_HPM0_FPD这个主接口使能,然后添加一个AXI Interconnect,一端接PS的M_AXI_HPM0_FPD,另一端接SYSMON的s_axi。
在Address Editor里给SYSMON分配一个地址段。我自己习惯给0xA0000000,范围0x1000。这个地址是Linux配置设备树、驱动里边ioremap的基准地址,尽量保持固定,方便后续维护。
连接完成后,生成比特流并导出硬件。这里有一个容易忽略的问题:SYSMON的s_axi时钟,也就是s_axi_aclk,需要和外部的AXI总线频率匹配。我一般直接从PS FCLK0引出100MHz,接给AXI Interconnect以及SYSMON。时钟频率不要随便给,因为SYSMON内部会基于这个时钟分频出ADC转换时钟,频率不对,转换时间会变得不可控。
3.3 设备树:内核怎么知道这块寄存器
在Linux下访问SYSMON,设备树必须有节点,告诉内核这个外设的物理地址和中断。以ZCU104上我分配0xA0000000为例,设备树里可以这样描述:
sysmon: sysmon@a0000000 { compatible = "xlnx,sysmon-axi"; reg = <0x0 0xa0000000 0x0 0x1000>; clocks = <&clk100m>; };如果你自己写驱动,compatible可以任意定义,只要驱动和设备树对得上。如果只是临时测试,这个节点其实也可以不建,直接用/dev/mem去读物理地址,后面我会演示。但正规产品上,还是建议建好节点,用platform driver管理,申请中断时也方便。
4. 在ARM侧把数据读出来:裸机例程与Linux实测
4.1 裸机读温度的最简代码
在Vitis(SDK)里做裸机开发时,读取过程可以非常精简。假设已经通过硬件抽象层获得SYSMON基地址,下面的代码可以读出温度并换算:
#include "xil_io.h" #include "xil_printf.h" #define SYSMON_BASE 0xA0000000U #define SYSMON_CHAN_SEL 0x20U #define SYSMON_TEMP_OFFSET 0x140U uint16_t sysmon_sample_temp(void) { /* 1. 选择温度通道(通道0),同时触发一次转换 */ Xil_Out32(SYSMON_BASE + SYSMON_CHAN_SEL, 0x0); /* 2. 等待转换完成,固定延时100us足够安全 */ usleep(100); /* 3. 读取温度寄存器低16位 */ return (uint16_t)(Xil_In32(SYSMON_BASE + SYSMON_TEMP_OFFSET) & 0xFFFFU); } int main(void) { uint16_t code; float temp_c; code = sysmon_sample_temp(); temp_c = 509.494f * ((float)code / 65536.0f) - 273.75f; xil_printf("Temp Code: 0x%04X, Temp: %.2f C\r\n", code, temp_c); return 0; }注意第1步里往通道选择寄存器写0,实际上是在独立模式下手动触发一次温度转换。如果你的SYSMON已经配置成连续序列模式,这一步就不是必须的,读寄存器就能拿到最新值。但为了代码在不同工程间通用,触发一下也没坏处,最多多等一个转换周期。
我特意没有使用状态寄存器的EOC位来判断转换是否结束,因为不同IP版本对状态寄存器的定义略有差异,固定延时虽然笨,但是稳定,100μs对单通道转换足够充裕。
4.2 用devmem快速验证,不写代码也能测
在Linux下想快速看数据,不一定急着写驱动。busybox的devmem命令可以直接读物理地址,适合做通路验证。
先确认SYSMON映射地址是0xA0000000,然后用devmem往0x20写通道号0:
devmem 0xA0000020 32 0x0 sleep 0.01 devmem 0xA0000140 32第二条命令读到的32位值,低16位就是温度寄存器数据。比如读到0x0000961E,把低16位0x961E换算成温度就是25℃左右。
如果你想看VCCINT,通道号改为1:
devmem 0xA0000020 32 0x1 sleep 0.01 devmem 0xA0000144 32低16位除以65536再乘3,就是VCCINT电压值。整个过程不用编译任何代码,几十秒就能验证硬件链路是否通。
4.3 数值验证:我读到的数据合理吗
拿到读数之后,第一步不是记录,而是判断合理性。比如空闲状态下温度读到80℃,那肯定有问题,要么是散热没装好,要么是校准系数没生效。判断标准很简单:和环境温度对比,温升在几度到十几度之间是正常的,满载跑起来四五十度也很常见,但如果空闲直接飙到70℃以上,基本可以断定读数链路出问题了。
电压轨的判断同理。VCCINT理想值0.85V,偏差超过3%就要留意,先复查供电设计,再考虑是不是分压系数理解错了。VCCAUX如果是3.3V供电,读数应该在3.2V到3.4V之间,如果差很多,大概率是通道映射理解错位。
5. 报警阈值与中断:从我看着它到它通知我
5.1 阈值寄存器格式与上限计算
监控不只是看数据,更重要的是在异常时能自动通知。SYSMON的报警比较器会在每次转换后自动比较测量值和阈值寄存器。报警通道和序列通道一一对应,比如温度上限报警、温度下限报警、VCCINT上限报警、VCCINT下限报警等等。
阈值寄存器是16位的,格式和测量寄存器完全一致,直接用公式反推。假如想设一个温度上限85℃,那么:
code = (85 + 273.75) × 65536 / 509.494 ≈ 0xB440往温度上限报警寄存器里写0xB440即可。下限也类似,假设要设-10℃:
code = (-10 + 273.75) × 65536 / 509.494 ≈ 0x8488注意还是那句:温度格式是偏移二进制,不是补码,别把-10℃先转成补码再写进去,那样会得到完全错误的结果。
5.2 报警输出到PS中断的接法
SYSMON的报警输出是一组低电平有效的信号,在Block Design里体现为alarm_out总线。比如alarm_out[0]对应温度上限报警,alarm_out[1]对应温度下限报警,以此类推。想把这些报警变成PS的中断,最直接的办法是把对应位接到PL到PS的中断引脚上。
在Zynq UltraScale+里,PL到PS的中断一般是pl_ps_irq0或者pl_ps_irq1,具体编号要看你的Block Design里PS配置。接好之后,在设备树的sysmon节点里加上interrupts属性:
sysmon: sysmon@a0000000 { compatible = "xlnx,sysmon-axi"; reg = <0x0 0xa0000000 0x0 0x1000>; interrupt-parent = <&gic>; interrupts = <0 91 4>; };这里的中断号要和你实际用的PL中断编号一致,不同板卡差异很大,务必以生成的设备树为准。我在ZCU104上就踩过坑,直接抄了网上的91,结果中断根本触发不了,最后还是得查xsa导出的中断映射表。
5.3 Linux里的中断测试
驱动里写中断处理函数时,有一点要比普通外设小心:报警状态寄存器是只读的,比较器是组合逻辑,测量值回到阈值范围后,报警状态会自动恢复,不会像普通中断标志位那样需要软件写1清除。因此在中断处理里,不要去等待或清除状态位,做好事件记录、执行保护动作(比如降频、弹窗、关断外设)即可。
触发阈值测试有个技巧:临时把温度上限阈值改成比当前温度低的值,看中断是否会立刻来。如果硬件链路和驱动都没问题,几十毫秒内就能收到上报。测完记得把阈值恢复,否则系统会一直报警。
6. 实战中容易踩的四个坑
6.1 读回全零:时钟和复位是第一个检查项
最经典的故障现象就是,读所有寄存器都是0。很多人第一反应是地址写错了,其实绝大多数时候是两个原因:一是SYSMON的s_axi_resetn被拉低,AXI从机一直处在复位状态;二是s_axi_aclk没给对,或者时钟频率低到硬件状态机跑不动。
解决办法:在Block Design里先确认复位信号连接,我习惯直接用Processor System Reset生成的peripheral_aresetn,接到SYSMON上去。时钟方面,检查FCLK0的频率,不要低于50MHz。如果这两个都没问题,再查地址映射,看看Address Editor里的地址是不是被改动过。
6.2 温度整体偏高,先看校准系数
遇到过一种情况:室温25℃,SYSMON读出来却是28.5℃,整体偏高3℃多。排查到最后,发现是IP配置时没有选择使用出厂校准。SYSMON芯片内部有eFUSE校准系数,上电后需要正确加载到配置寄存器里。Vivado的IP配置界面里一般默认会处理好,但如果你在定制IP时手动修改了配置寄存器,就可能把校准位覆盖掉。
处理方法是恢复IP默认配置,或者在驱动初始化里显式配置校准相关寄存器。实测校准生效后,温度读数和热像仪对同一片die的结温能对上1℃以内。
6.3 电源轨读数和万用表有差异
很多人喜欢拿万用表测板载电容两端,然后和SYSMON读数对比,发现差了几十毫伏,就怀疑SYSMON不准。其实SYSMON测的是die内部电压,万用表测的是板级电容两端的电压,这两者之间隔着PCB走线阻抗、电源模块的动态响应、芯片内部压降,有几十毫伏差异很正常。换句话说,拿万用表去验证SYSMON,本质上是在拿两种不同位置的测量值做对比,方向本身就错了。
如果真想验证SYSMON的电压通道精度,更合理的方式是同时读两个监控点:一个在靠近芯片的电源输出端,一个在寄存器里。只要两者相对趋势一致,没有大的偏差,就说明SYSMON工作正常。
6.4 报警误触发:阈值写错格式的连锁反应
最后这个坑很隐蔽。由于温度是偏移二进制格式,如果你把“-10℃”换算错了,写了个正数十进制数进去,那么下限报警就会在温度非常低时才触发,起不到保护作用。反过来,如果温度上限阈值按补码方式理解,可能写出来的值特别小,导致芯片刚跑起来就报警。
我现在的习惯是:所有阈值都用宏定义,统一通过换算公式计算,不在代码里手写十六进制数字。虽然多写几行,但避免了低级错误,更重要的是,别人接手代码时能一眼看出阈值对应的物理量,可维护性好很多。
最后说一句个人体会:SYSMON这种模块,平时不显山不露水,但真正做电源验证、散热摸底、现场故障定位的时候,它几乎是我第一个打开的功能模块。花半天时间把寄存器访问跑通,后面换板子、改设计,维护成本都会低很多。如果你正在调MPSoC,建议别停留在例程打印的层面,自己把通道切换、阈值报警、中断上报这条链路完整走一遍,收获比想象中大。