1. 先说说我为什么把 Zynq-7020 的 CPU 从 667 MHz 降到 400 MHz1.1 从一次外壳烫手的项目事故说起上个月我负责的一台基于 Zynq-7020 的边缘计算设备在客户现场连续跑了三天之后被退回来了。故障描述很简单设备外壳温度过高工业现场的工程师拿红外测温枪一打外壳局部接近 67℃已经超过了客户内部规范里外壳温升不超过 40K的硬性红线。当时我的第一反应是把问题归咎于散热设计。板子装在密闭铝壳里没有主动风扇靠被动散热片导热负载一上来必然积热。我前后试了加大散热片面积、改导热垫、调整外壳开孔效果有但幅度有限。后面仔细用功耗仪测整板功耗才发现问题不只是散热路径而是 PS 侧 ARM Cortex-A9 双核跑满的时候发热本身就占了相当比例。于是我开始认真考虑CPU 降频这条路。当时板子默认配置是主频 667 MHz这是 XC7Z020-1 速度等级下的标称最高频率。如果我把 CPU 降到 400 MHz理论上动态功耗可以降约 40%整机温升会有明显好转。但问题是降频会不会把性能也拖垮我在网上搜了一圈关于 Zynq 降频的实战文章少得可怜大部分都是直接改个设备树属性然后说我降频成功了并没有人把 PLL 计算、寄存器操作、实测温度数据完整串起来。所以这篇文章我想完整记录一下整个过程。包括时钟链路怎么算、降频的落地方式、实测温度对比以及我踩过的几个坑。1.2 降频不是性能降级是拿频率换热预算很多人一听到降频就觉得是性能妥协。但在真实的工业嵌入式场景里性能从来都不是唯一指标热功耗、可靠性、MTBF 同样重要。尤其是在无风扇密闭环境里温度每降低 10℃电解电容寿命理论上能翻倍这个账很容易算。关键是要分析应用场景里 CPU 到底在忙什么。我这个项目里的任务分三类一是跑工业以太网协议栈大部分时间在等报文二是做 Modbus 轮询和 IO 控制逻辑简单频率不敏感三是偶尔做一次轻量级的边缘计算但数据量不大而且可以接受耗时变长。真正对 CPU 主频敏感的任务占比不到 20%。我把 CPU 频率从 667 MHz 降到 400 MHz 之后用 CoreMark 简单跑了一下整数性能下降了约 40%。但实测项目里的协议轮询周期几乎没受影响因为瓶颈在网络上而不在 CPU。边缘计算任务耗时从原来的 35 ms 变成了 58 ms完全在可接受范围内。这就是我说的热预算换性能——你以为降频损失了性能其实损失的大部分是你根本用不到的性能冗余。当然如果你的应用是跑密集算法或者要做高实时性的信号处理降频方案就要慎重。所以第一步想清楚需求边界再继续往下读。2. 动手前先看明白 Zynq-7020 的 CPU 时钟是怎么生产出来的2.1 PS 时钟树与 ArmPLL 的本质Zynq-7020 的 PS 侧有外部参考时钟输入 PS_CLK绝大多数开发板用的是 33.333 MHz 晶振。这个频率本身不高但 PS 内部有多个 PLL锁相环负责倍频。CPU 主频走的是 ArmPLL 这条链路跟 DDRPLL、IOPLL 相互独立。时钟的流向大概是这样的外部 PS_CLK 进入 ArmPLLArmPLL 通过内部反馈分频系数把输入频率倍频到一个比较高的值然后再经过 CPU_6OR4_CLKCTRL 寄存器里的分频器最终输出到 ARM Cortex-A9 双核以及 L2 Cache。换句话说CPU 最终跑多少由两个参数决定ArmPLL 的倍频系数FBDIV以及后续的分频数DIVISOR。关键寄存器需要心里有数ArmPLL 配置寄存器0xF8000108位 [13:6] 是 FBDIVCPU_6OR4_CLKCTRL 寄存器0xF800012C位 [27:24] 是 DIVISORArmPLL 的 FBDIV 合法范围是 6 到 90DIVISOR 常见取 2。所以 CPU 频率的计算公式可以简化为CPU_CLK PS_CLK × FBDIV / DIVISOR拿默认 667 MHz 来说33.333 MHz × 40 / 2 666.67 MHz这正是 XC7Z020-1 的标称频率。注意这套计算里的 PS_CLK 一定要以板子实际的晶振频率为准我见过有人用 50 MHz 参考时钟去套 33.333 MHz 的默认参数结果差之千里。2.2 从 667 MHz 到 400 MHz 的参数怎么算既然公式明确了降频目标就很好反推。我常用的几个目标档位如下目标 CPU 频率PS_CLKArmPLL FBDIV分频数 DIVISOR实际计算值667 MHz默认33.333 MHz402666.67 MHz500 MHz33.333 MHz302500.00 MHz400 MHz33.333 MHz242400.00 MHz333 MHz33.333 MHz202333.33 MHz这三个降频档位都是把 DIVISOR 保持为 2只改 FBDIV这样对时钟树的影响面最小。我最终选了 400 MHz原因有两个一是 400 MHz 下性能和温度相对平衡CoreMark 跑分还有 667 MHz 的六成以上二是 400 MHz 和常见的 500 MHz 相比进一步降低了发热而且 FBDIV24 这个系数在 PLL 工作范围内特别稳。为什么不选 333 MHz从后面的实测数据能看出来频率从 400 降到 333温度改善只有两三度但性能损失已经能感知到。降频也有边际递减效应不是越低越好。2.3 降频时电压不要动这里要特别提醒一个很多新手容易顺手做的事为了进一步降低功耗想把核心电压 VCCPINT 也跟着调低。Zynq-7020 的 VCCPINT 默认是 1.0V调低确实能按电压平方关系省电但 CPU 的时序收敛是经过芯片出厂验证的你动电压等于在挑战芯片良率极限没有任何量产板敢这么干。我实测过把 VCCPINT 从 1.0V 降到 0.95V系统能跑起来但一旦温度升高随机死机的概率急剧上升。所以本文所有降频操作都不涉及电压调整频率和电压是两个维度不要混淆。3. 两个可靠的降频落地路径以及配套验证3.1 路径一通过 Vivado 的 Zynq PS 配置界面改最省事的方式是在 Vivado 的 Block Design 里直接改 PS 时钟配置。打开 Zynq7 Processing System IP 的配置界面进入 Clock Configuration找到 CPU 时钟相关的入口。不同版本的 Vivado 界面略有差异但都会有一个 CPU Clock 或 CPU_6x4x 输入框默认值通常是 666.666666 MHz。我直接把它修改为 400Vivado 会校验这个频率值是否合法然后自动反解出对应的 ArmPLL FBDIV 等参数。改完这一步需要重新生成 Bitstream 和 FSBL 工程。这里有个关键点FSBL 里的 ps7_init 代码必须同步重新生成否则你只是改了硬件工程描述实际运行时的初始化代码还是老的。我建议在 Vivado 里 File - Export Hardware 之后用 Vitis 重新编译 FSBL然后把新生成的 boot.bin 烧进去。整个过程不复杂但一定要确保 FSBL 是最新导出的这一点特别容易漏。3.2 路径二直接改 ps7_init 中的 ArmPLL 参数如果你的工程没有走完整的 Vivado 流程或者你想快速验证一个自定义频率可以直接改 FSBL 工程里的 ps7_init_gpl.c 文件。打开文件之后搜索 ARM_PLL 相关的宏定义会看到类似这样的内容#define PS7_ARM_PLL_FBDIV 40 #define PS7_ARM_PLL_Q 8 #define PS7_ARM_PLL_F 5把 FBDIV 从 40 改成 24保存后重新编译 FSBL重新生成 boot.bin就完成了降频。这里 Q 和 F 这两个参数一般不要去动它们跟 PLL 的锁定范围有关默认值已经做了优化。只改 FBDIV 就能得到 400 MHz。有个细节值得注意ps7_init_gpl.c 生成后Vitis 有可能在下次 Export Hardware 的时候把它覆盖回默认值。所以如果你选择了手动改这条路建议把这个文件加入版本管理并且每次重新导出硬件后检查一下 FBDIV 是否还是你改过的值。3.3 设备树 clock-frequency 一定要同步修改降频之后设备树里 CPU 节点的 clock-frequency 属性必须同步修改这一步很多人会忽略。设备树里默认写的通常是cpus { #address-cells 1; #size-cells 0; cpu0 { compatible arm,cortex-a9; device_type cpu; reg 0; clock-frequency 666666666; }; cpu1 { compatible arm,cortex-a9; device_type cpu; reg 1; clock-frequency 666666666; }; };我把两个 cpu 节点的 clock-frequency 都改成了 400000000。这个属性本质上是给内核一个参考信息让内核里的定时器校准、延迟函数、调度统计能按真实频率工作。如果实际已经降到 400 MHz但设备树还写着 666 MHz内核会认为 CPU 跑得快得多导致 jiffies 校准错乱、udelay 时间偏长最典型的症状就是网络超时、外设时序对不上。说句掏心窝的话设备树改了不等于降频了真正的频率是 PLL 决定的设备树只是让内核知道现在是多少。所以两条腿都要走FSBL 负责真正改变硬件时钟设备树负责告诉软件真相。3.4 怎么确认降频真的生效了烧录新 boot.bin 之后别急着跑业务先确认频率确实降下来了。最直接的方法是用 devmem 读寄存器# 读 ArmPLL 配置寄存器得到 FBDIV devmem 0xF8000108 # 读 CPU_6OR4_CLKCTRL 寄存器得到 DIVISOR devmem 0xF800012C拿到两个寄存器的值之后按照公式 PS_CLK × FBDIV / DIVISOR 手动算一遍。以我的板子为例读回 FBDIV 是 24DIVISOR 是 2配合 PS_CLK 33.333 MHz计算得到 400 MHz这就确认降频成功了。另外可以配合启动日志确认。Linux 启动过程中会有一行类似 Calibrating delay loop在 667 MHz 时显示约 1333 BogoMIPS降到 400 MHz 之后大约在 800 左右。虽然 BogoMIPS 不是精确的频率测量但可以作为快速佐证。4. 温度对比实测不同频率下的空载与压力温度表现4.1 测试环境与负载方法为了得到可对比的数据我把测试条件固定下来保证每个频率档位下唯一变量就是 CPU 主频。测试板卡是我自研的一块 Zynq-7020 核心板DDR3 频率 533 MHz 不变PL 侧只加载了一个简单的点灯逻辑基本不产生额外热量。散热条件是铝制散热片无风扇 导热硅垫环境温度恒定在 25℃ 的空调房。测温用的就是 Zynq 内部的 XADC通过 Linux 的 thermal 节点读取cat /sys/class/thermal/thermal_zone0/temp这个值读出来是毫摄氏度667 意味着 66.7℃记得除以 1000。压测工具用的是 stress双核全部跑满stress -c 2 -t 600 每个频率档位先空载稳定 10 分钟记录空载温度然后跑压力负载 10 分钟记录稳定后的满载温度期间不再做任何操作。4.2 实测数据667/500/400/333 MHz 温度对比四个频率档位的实测数据如下CPU 频率空载温度℃满载温度℃满载温升667 MHz默认517120500 MHz486315400 MHz465711333 MHz45538这组数据是在我手上的板子测出来的不同板卡、不同散热条件会有差异但趋势是一致的。667 MHz 满载时芯片温度到了 71℃已经明显高于我的警戒线降到 400 MHz 之后满载温度 57℃比默认可控得多再往下降到 333 MHz温度只降了 4℃收益开始收窄。整机功耗方面我也简单测了一下667 MHz 满载整板功耗约 9.8W400 MHz 时约 8.2W降幅在 16% 左右。之所以比理论动态功耗降幅小是因为板上的 DDR、PHY、PL 静态功耗这些不随 CPU 频率变化它们占了相当比例。4.3 数据说明了什么降频对整机散热的真实意义从数据能得出几个结论。第一667 MHz 满载 71℃ 意味着无风扇密闭环境里必须降频这不是可选项而是必选项。第二400 MHz 是一个甜点频率温度降了 14℃核心性能还保留了六成以上对大多数 IO 密集型工业应用完全够用。第三降频存在边际递减从 500 降到 400 温度改善 6℃从 400 降到 333 只有 4℃说明频率越低动态功耗占整体功耗的比例越小静态漏电功耗开始主导。另外要泼一盆冷水如果你的 Zynq 板子发热主要来自 PL 侧的大规模逻辑CPU 降频帮不上大忙。PL 侧的翻转功耗占据了大头这时候优先应该考虑降低 PL 逻辑的时钟频率、减少不必要的信号翻转或者优化综合策略。我这个项目里 PL 几乎是空的所以 CPU 降频的效果才这么明显。5. 降频过程中我踩过的几个实打实的坑5.1 只改设备树、不动 PLL看起来降频了其实什么都没有我最初犯的第一个错误就是在设备树里把 clock-frequency 改成了 400000000满怀期待地启动了系统结果温度纹丝不动满载照样奔着 70℃ 去。原因就是我前面强调过的设备树的属性只是告诉内核当前频率是多少并不具备改变时钟的能力。真正的频率由 FSBL 在启动早期配置 PLL 的那一刻就定死了。这个坑特别容易踩因为网上很多帖子只写了设备树的修改完全没有提 FSBL 和 PLL。如果你搜到有人说改一下设备树就降频了基本可以断定他没验证过温度或者温度没变化。正确顺序一定是先改 PLL 配置再同步设备树顺序反了等于白干。5.2 运行中直接写 ArmPLL 寄存器系统直接死透在验证思路的时候我做过一次大胆的尝试系统已经跑起来的情况下直接用 devmem 往 0xF8000108 写新的 FBDIV 值打算实现运行中降频。结果写进去的瞬间系统就完全卡死串口无输出网络 ping 不通只能断电重启。原因是 CPU 的时钟是由 ArmPLL 直接提供的你在运行中突然改变 PLL 配置相当于发动机高速运转的时候你直接换变速箱齿轮处理器一条指令可能都执行不完就失去时钟了。而且 ArmPLL 的寄存器是被 SLCR 锁保护的直接写无效必须先解锁但解锁再写进去基本也是死路一条。所以我的建议是除非你用的是带 CPUFreq 驱动的内核且外设时钟树完全支持动态调频否则不要试图在运行中通过 devmem 降频。Zynq 平台的主线内核 CPUFreq 支持一直很有限很多开发板的 /sys/devices/system/cpu/cpu0/cpufreq 目录根本不存在动态调频这条路在 Zynq 上并不成熟。安全可靠的做法就是 FSBL 阶段定死频率重启生效。5.3 改完 CPU 频率后系统定时器时间不对这个问题是我在把设备树改成 400 MHz、系统正常运行后发现的现象是系统时间明显变慢大约每过 10 分钟就慢了 2~3 秒。这个问题的根源在于 Zynq 的 ARM Global Timer 和系统定时器是基于 CPU 时钟的当实际 CPU 时钟变了但内核里某些时间基准没有同步更新就会导致 tick 周期偏移。我的解决方式是设备树、内核配置、FSBL 三处同步确认。设备树里不仅 cpus 节点还要检查定时器节点里是否有引用 CPU 时钟的时钟源配置。在 Xilinx 官方设备树里clock 和 clock-frequency 的引用关系需要保持一致。如果你用的不是官方 BSP 而是自编设备树一定要重点排查 timer 节点。5.4 温度读数单位与测量位置引发的误判最后说一个容易让人误判的细节。XADC 读回来的温度是芯片内部的温度传感器数值和外壳温度、散热片温度都不是一回事。早期我一度以为降频没用因为我用手摸散热片感觉还是很烫后来才发现 XADC 读的 Junction Temperature 已经比默认低了不少只是热传导路径太长散热片温度变化滞后。另外/sys/class/thermal/thermal_zone0/temp读出来的数值是毫摄氏度不除以 1000 的话你会看到 57000 这种离谱的数字容易误判为温度失控。我见过有人拿这个数值直接发告警结果系统误报过热。正确做法是除以 1000 后再和阈值比较。6. 留到最后给后来者的几句实在话如果这个项目让我重来一次我会在原理图阶段就把散热预算算进去而不是等样机出来了才想着降频补锅。但如果你已经走到了降频这一步我的建议是先按文章开头的方法评估你的应用对 CPU 主频的敏感度然后把 400 MHz 作为首选测试档位实测温度数据说话不要凭感觉定频率。降频不为难真正需要花心思的是把你的整个启动链路的配置统一起来——FSBL 里的 PLL 参数、设备树里的 clock-frequency、内核启动日志的预期值三者必须一致。改一处漏一处迟早会在现场出妖蛾子。我这次就是设备树漏改导致定时器偏差排查了一整天才定位到问题。另外如果你准备把这个方案固化到正式产品里我建议多做一轮高低温测试。我实测的是 25℃ 环境温度下的数据但工业设备经常要在 50℃ 甚至更高的环境里工作那时 400 MHz 档位的温度余量还够不够用需要重新评估。如果高温环境下 400 MHz 依然压不住可能就要考虑 333 MHz 档位或者从散热结构上做根本性改进了。