1. 先说说我为什么把 Zynq-7020 的 CPU 从 667 MHz 降到 400 MHz
1.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 进入 ArmPLL,ArmPLL 通过内部反馈分频系数把输入频率倍频到一个比较高的值,然后再经过 CPU_6OR4_CLKCTRL 寄存器里的分频器,最终输出到 ARM Cortex-A9 双核以及 L2 Cache。换句话说,CPU 最终跑多少,由两个参数决定:ArmPLL 的倍频系数(FBDIV),以及后续的分频数(DIVISOR)。
关键寄存器需要心里有数:
- ArmPLL 配置寄存器:0xF8000108,位 [13:6] 是 FBDIV
- CPU_6OR4_CLKCTRL 寄存器:0xF800012C,位 [27:24] 是 DIVISOR
ArmPLL 的 FBDIV 合法范围是 6 到 90,DIVISOR 常见取 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_CLK | ArmPLL FBDIV | 分频数 DIVISOR | 实际计算值 |
|---|---|---|---|---|
| 667 MHz(默认) | 33.333 MHz | 40 | 2 | 666.67 MHz |
| 500 MHz | 33.333 MHz | 30 | 2 | 500.00 MHz |
| 400 MHz | 33.333 MHz | 24 | 2 | 400.00 MHz |
| 333 MHz | 33.333 MHz | 20 | 2 | 333.33 MHz |
这三个降频档位都是把 DIVISOR 保持为 2,只改 FBDIV,这样对时钟树的影响面最小。我最终选了 400 MHz,原因有两个:一是 400 MHz 下性能和温度相对平衡,CoreMark 跑分还有 667 MHz 的六成以上;二是 400 MHz 和常见的 500 MHz 相比,进一步降低了发热,而且 FBDIV=24 这个系数在 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。我直接把它修改为 400,Vivado 会校验这个频率值是否合法,然后自动反解出对应的 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>; cpu@0 { compatible = "arm,cortex-a9"; device_type = "cpu"; reg = <0>; clock-frequency = <666666666>; }; cpu@1 { 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 是 24,DIVISOR 是 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(默认) | 51 | 71 | 20 |
| 500 MHz | 48 | 63 | 15 |
| 400 MHz | 46 | 57 | 11 |
| 333 MHz | 45 | 53 | 8 |
这组数据是在我手上的板子测出来的,不同板卡、不同散热条件会有差异,但趋势是一致的。667 MHz 满载时芯片温度到了 71℃,已经明显高于我的警戒线;降到 400 MHz 之后满载温度 57℃,比默认可控得多;再往下降到 333 MHz,温度只降了 4℃,收益开始收窄。
整机功耗方面我也简单测了一下:667 MHz 满载整板功耗约 9.8W,400 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 档位,或者从散热结构上做根本性改进了。