
刚拿到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外设接上然后由PSARM核通过寄存器读写来触发转换、读结果。这个改动让上位机介入变得非常干净不需要再像老一代那样依赖JTAG或者私有的DRP端口。1.2 能看的东西比想象多温度、电源轨、外部差分输入先列一下我在实际工程里用到过的通道大致分成三类。第一类是温度通道。SYSMON内建温度传感器测的是结温也就是硅片内部实际温度。注意这和散热器表面温度不是一回事满载跑起来结温往往比外壳高十几度所以做功耗和散热评估要以这个数为准。第二类是内部电源轨。MPSoC内部电源种类很多常规能监控的包括VCCINTPL核心逻辑电压通常在0.85V左右VCCAUX辅助电压1.8V或3.3V级别VCCBRAMBlock RAM供电轨VCCOI/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温度1VCCINT2VCCAUX3VCCBRAM4VCCO5VCCINT_IO6VCCAUX_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: sysmona0000000 { compatible xlnx,sysmon-axi; reg 0x0 0xa0000000 0x0 0x1000; clocks clk100m; };如果你自己写驱动compatible可以任意定义只要驱动和设备树对得上。如果只是临时测试这个节点其实也可以不建直接用/dev/mem去读物理地址后面我会演示。但正规产品上还是建议建好节点用platform driver管理申请中断时也方便。4. 在ARM侧把数据读出来裸机例程与Linux实测4.1 裸机读温度的最简代码在VitisSDK里做裸机开发时读取过程可以非常精简。假设已经通过硬件抽象层获得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写通道号0devmem 0xA0000020 32 0x0 sleep 0.01 devmem 0xA0000140 32第二条命令读到的32位值低16位就是温度寄存器数据。比如读到0x0000961E把低16位0x961E换算成温度就是25℃左右。如果你想看VCCINT通道号改为1devmem 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: sysmona0000000 { 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建议别停留在例程打印的层面自己把通道切换、阈值报警、中断上报这条链路完整走一遍收获比想象中大。