简介:ZYNQ 7010的PS端XADC驱动开发是嵌入式开发中常见的底层驱动需求,这份配套工程包面向使用Zynq SoC且有模拟量采集需求的软硬件开发者,系统演示了在SDK环境下构建PS_XADC驱动并读取温度、电压等信号的方法,可应用于电源监测、温度采集与工业控制等场景。包内共530个文件,以h头文件、c源文件、o目标文件、so与dll动态库等编译产物为主,辅以tcl脚本、makefile、elf可执行文件,方便工程重建、编译与调试,压缩包仅7.58MB,整体目录覆盖SDK工程、设备树配置及驱动源码。内容预览中包含Xilinx标准库、PS初始化代码以及XADC驱动源文件等关键组成部分,说明内含一套可直接参考的XADC驱动库实现,既有寄存器级底层配置,也有AXI总线访问以及中断、轮询等数据读取方式,通过分析这些代码还能理解XADC单通道、双通道、并行模式的选择以及采样数据的转换处理流程。截至目前已有393人学习,对需要快速上手PS_XADC或系统梳理Xilinx SDK驱动框架的工程师来说,是一份实用性很强的工程样本,尤其适合希望从设备树到应用层完整理解XADC驱动链路的初学者。
1. PS_XADC 驱动:ZYNQ 7010 上最容易被低估的片内仪表
调试 ZYNQ 7010 板卡,我习惯第一步先跑一遍 ps_xadc 驱动——片子温度、VCCINT、VCCAUX 全都能读出来,比拿万用表去量 BGA 背面快得多。这份 zip 里打包的是 SDK 工程,基于 xadcps 库实现 PS 端对 XADC 硬核的直接访问,不需要在 PL 里例化 xadc_wizard IP,也不占 LUT。做电源监控、热保护或上电自检的人,或者被 XADC 初始化玄学卡过的人,这套工程可以直接导入 SDK 复现。下面把这套驱动从硬件配置到换算公式全部拆开写。
2. 选 PS 直驱还是 PL 例化:xadcps 库与 xadc_wizard 的两条路子
2.1 XADC 的寄存器结构与两条访问路径
XADC 是 7 系列 FPGA 里焊死在硅片上的 12 位、1MSPS 模数转换器,对 ZYNQ 7010 来说,它的测量对象包括芯片结温、PS 和 PL 各路电源电压,以及最多 17 个外部模拟输入通道。ZYNQ 里这个硬核同时挂了两个控制器接口,这是整件事的根源:一条是 PL 侧通过 xadc_wizard IP 包出来的 AXI4-Lite 接口,另一条是 PS 侧的系统监视器寄存器组,基地址固定在 0xF8007000。SDK 里的 xadcps 驱动,就是对着这组寄存器做的封装。
两条路径物理上访问的是同一块 ADC 硬核,所以它们天然互斥。PL 例化了 xadc_wizard 并跑起来,PS 侧去读 0xF8007000 往往拿到全零或者初始化失败;反过来 PS 通过 xadcps 驱动接管了采样序列,PL 侧再发控制命令也没有响应。互斥这个性质决定了选型必须先想清楚,而不是两边都写上代码再慢慢调。我见过不少人在 PL 里放着 xadc_wizard,又在 SDK 里初始化 XAdcPs,结果两边数据互相打架,排查半天才发现是同一个硬核被两个控制器抢。
2.2 为什么我选 PS 直驱
我用 ZYNQ 7010 做的大部分板卡,PL 侧的 LUT 和 FF 是要留给业务逻辑的,比如千兆网、图像采集或者自定义协议处理。xadc_wizard 虽然只占几百个 LUT,但它不是零成本,你要给它配 AXI Interconnect、分配地址空间、做引脚约束,还得花心思处理它和 PS 侧同时上电时的仲裁关系。PS 直驱则完全避开这些:Block Design 里不添加任何 XADC 相关 IP,导出硬件后 SDK 的 BSP 把 xadcps 驱动挂上,几行代码就能开始读数。
从调试角度讲,PS 直驱还有一个隐性优势:它不依赖 PL 的 bit 流。zynq 烧写 bit 流失败、或者 PL 侧被临时改成别的功能时,PS 里这段 XADC 代码照样能跑,温度监控不会跟着 PL 一起罢工。做电源时序调试、板子热稳定性测试时,这个特性非常实用,我之前就靠它在一批 PL 逻辑反复烧写测试的板子上持续盯着芯片温度,完全不受影响。对于纯监控类需求,PS 直驱的 CPU 占用也很低:XADC 自己在片内按安全序列循环转换,PS 只在要读数时花几十个周期去寄存器里取结果。我用它做 100ms 间隔的温度轮询,CPU 占用率几乎可以忽略。
| 对比项 | PS 直驱(xadcps) | PL 例化(xadc_wizard) |
|---|---|---|
| PL 资源占用 | 0 LUT | 数百 LUT + BRAM |
| 总线接口 | PS 寄存器映射 | AXI4-Lite |
| 时钟来源 | 片内时钟 | 外部引脚或 PS 提供 |
| 启动顺序 | PS 启动即可用 | 依赖 PL bit 流加载 |
| 适用场景 | 电源监控、热保护 | 外部通道采集、PL 逻辑联动 |
2.3 SDK 工程需求清单与 API 分类
开始写代码之前,先把 SDK 工程需要的东西理清楚。硬件平台文件(Vivado 导出的 hdf/xsa)里必须带 PS_XADC 资源,BSP 的 Peripherals 设置里要勾上 xadcps 库。工程里真正用到的宏只有两个:XPAR_XADCPS_0_DEVICE_ID 和 XPAR_XADCPS_0_BASEADDR,前者是设备 ID,后者是寄存器基地址 0xF8007000。这两个宏由 BSP 根据硬件平台自动生成,手写地址反而容易出问题。
xadcps 库的 API 大致分成四类:初始化类,包括 XAdcPs_LookupConfig、XAdcPs_CfgInitialize、XAdcPs_SelfTest;序列配置类,包括 XAdcPs_SetSeqChMode、XAdcPs_SetSeqInputMode、XAdcPs_SetSamplingRate;数据读取类,包括 XAdcPs_GetTemperature、XAdcPs_GetVccint、XAdcPs_GetVccaux、XAdcPs_GetADCData;报警类,包括 XAdcPs_SetAlarmEnables、XAdcPs_SetAlarmThreshold。实际监控项目里最常用的就是前两类加温度读取,把这几个函数吃透,整个驱动就掌握了一半。下面从工程搭建开始一步一步走。
3. 从 Vivado 到 SDK:导出 hdf、勾选 xadcps 并初始化
3.1 只含 PS 的最小 Block Design
第一步在 Vivado 里新建 RTL 工程,器件选 XC7Z010。然后添加 Zynq PS 核,双击进入配置,按你板子的实际型号配好 DDR(比如 DDR3L 或 DDR4,取决于硬件颗粒)、UART1(串口打印要用)和 MIO 分配。这里有个原则:Block Design 里不要添加任何 PL IP,特别是 xadc_wizard。XADC 硬核在没有任何 PL 外设占用时,默认采样序列归 PS 侧控制,这正是我们想要的干净状态。
配置完成后先 Validate Design,再 File > Export Hardware。如果你不需要 PL 侧的 bit 流,这里可以不勾 Include bitstream,导出的 hdf 文件已经带有 PS_XADC 资源信息。SDK 会拿这个 hdf 去生成 BSP,所以这一步导出的文件一定要是最新的。很多人后面 BSP 里找不到 xadcps,往回倒多半就倒在这里——Vivado 里改过 Block Design 但是没有重新 Export Hardware,老 hdf 里压根没有 PS_XADC 节点。
3.2 在 SDK 里搭 BSP 并确认 xadcps 驱动
打开 SDK 后新建 Application Project,输入工程名,Board Support Package 选择 New。创建完成后先别急着写 main.c,到 Board Support Package Settings 里看 Peripherals 列表,把 xadcps 勾上,然后点 Re-generate BSP Sources,让驱动库真正落进工程。
确认驱动安装成功有两个办法。一个是看 BSP 工程下的 system.mss,里面能找到 xadcps 字样;另一个是在 BSP 的 include 路径下找到 xadcps.h。这一步我一般会花 30 秒做掉,因为后面编译报错了再回来排查,你根本分不清是 BSP 没生成还是代码写错。如果用的是 Vitis 新版本,流程类似:在 Vivado 里导出的 xsa 文件关联到 Vitis 平台工程,BSP 里同样会列出 xadcps 驱动选项。
3.3 初始化 XADC 并登记采样序列
初始化代码是固定的三段式:
#include "xadcps.h" #include "xparameters.h" #define XADC_DEVICE_ID XPAR_XADCPS_0_DEVICE_ID static XAdcPs XAdcInst; XAdcPs_Config *Config; int Status; Config = XAdcPs_LookupConfig(XADC_DEVICE_ID); if (Config == NULL) { return XST_FAILURE; } Status = XAdcPs_CfgInitialize(&XAdcInst, Config, Config->BaseAddress); if (Status != XST_SUCCESS) { return XST_FAILURE; } XAdcPs_SetSeqChMode(&XAdcInst, XADCPS_SEQ_CH_SAFE_MODE); XAdcPs_SetSeqInputMode(&XAdcInst, XADCPS_SEQ_INPUT_MODE_SAFE);XAdcPs_LookupConfig 拿的是 BSP 根据硬件平台自动生成的配置结构体,里面已经填好基地址和输入时钟频率,不需要自己手写 0xF8007000。CfgInitialize 把配置结构体关联到驱动实例上,并把 XADC 的控制寄存器复位到默认状态。初始化成功后再设采样序列:安全模式表示 XADC 按固定顺序自动采集内部传感器——温度、VCCINT、VCCAUX、VCCBRAM、VCCPINT、VCCPAUX——不需要我们手动去维护通道轮询。SetSeqInputMode 设置输入类型,安全模式下对应内部通道的默认输入配置。
这里有个参数要留意:XAdcPs_SetSeqChMode 的第二参数如果设成 XADCPS_SEQ_CH_CONTINUOUS_MODE,XADC 会按你配置的通道列表顺序连续采集,适合外部模拟输入。内部监控用安全模式就够了,省心,也不容易把序列配错。
3.4 初始化后的状态自检
初始化返回 XST_SUCCESS 不代表数据一定是准的,我习惯再补一步自检:调用 XAdcPs_SelfTest 做寄存器读写回环检查,然后读取状态寄存器里的 EOC(转换结束)标志。安全模式下片内转换是自动持续的,程序启动后等上一次转换完成即可,没必要人为加固定延时。
Status = XAdcPs_SelfTest(&XAdcInst); if (Status != XST_SUCCESS) { xil_printf("XADC self test failed\r\n"); return XST_FAILURE; }SelfTest 只验证寄存器读写路径,不能代替数据正确性验证。真正的数据验证要等温度值读出来之后,和万用表测的供电电压对比,误差在合理范围才算通过。这步放在下一章讲换算的时候一起处理。
4. 读温度与片内电压:12 位转换码怎么换算成 °C 和 V
4.1 温度换算:为什么不是直接除以 4096
XADC 是 12 位转换器,输出范围 0~4095。如果把原始值直接拿来用,你会得到温度值从 0 到 4095 的“大数”,没有任何物理意义。温度的实际换算公式是:
T = (code × 503.975) / 4096 - 273.82
这个公式在 Xilinx 的 XADC 文档里有,我一般记成:先按 4096 满量程归一化,乘 503.975 对应 ADC 输入范围的温度系数,减 273.82 是把开尔文换回摄氏度。实测精度在 ±2°C 左右,做热保护和板级健康监控完全够用。
代码层面有个特别容易翻车的点:code 的类型。XAdcPs_GetTemperature 返回的是 u32,必须强转成 float 再算,避免整数截断把小数点后的度数丢掉。我遇到过有人写成 code / 4096 * 503.975 - 273.82,因为整数除法里 code / 4096 直接等于 0,温度算出来永远是 -273.82,还以为是硬件坏了。
4.2 VCCINT / VCCAUX 电压换算与校准
电压部分要区分两组通道。片内电源传感器测的是 PS/PL 内部供电轨,XADC 内部做了归一化处理,满量程对应 3V,换算公式统一为:
V = code × 3.0 / 4096
对应 VCCINT、VCCAUX、VCCBRAM、VCCPINT、VCCPAUX、VCCO_DDR 这些通道。比如 VCCINT 是 1.0V 供电轨,读出来的 raw_value 通常在 1365 附近,因为 1365 × 3 / 4096 ≈ 1.0V。下面这张表是 7010 上最常检查的通道:
| 通道 | 读取函数 | 典型测量值 |
|---|---|---|
| 芯片结温 | XAdcPs_GetTemperature | 30~60°C |
| VCCINT | XAdcPs_GetVccint | 1.0V |
| VCCAUX | XAdcPs_GetVccaux | 1.8V 或 2.0V |
| VCCBRAM | XAdcPs_GetVccbram | 1.0V |
外部模拟输入通道则是另一套换算,满量程取决于你配的模拟输入模式和 PGA 增益,不是这个 3V 公式。用 XAdcPs 驱动读外部通道时,需要先用 XAdcPs_SetSeqChMode 把序列切到连续模式,然后逐个通道读 XAdcPs_GetADCData,再按该通道的增益系数换算。如果只是做板卡内部电源监控,3V 公式就够用,外部通道的校准可以等真正用到时再做,不要在前期被它带偏。
4.3 完整 main.c:轮询采集并串口打印
把上面的碎片拼成一个可以跑起来的 main:
#include "xadcps.h" #include "xparameters.h" #include "xil_printf.h" #include "sleep.h" #define XADC_DEVICE_ID XPAR_XADCPS_0_DEVICE_ID static XAdcPs XAdcInst; static float adc_temp_to_celsius(u32 code) { return (code * 503.975f) / 4096.0f - 273.82f; } static float adc_volt(u32 code) { return (code * 3.0f) / 4096.0f; } int main(void) { XAdcPs_Config *Config; int Status; Config = XAdcPs_LookupConfig(XADC_DEVICE_ID); if (Config == NULL) return -1; Status = XAdcPs_CfgInitialize(&XAdcInst, Config, Config->BaseAddress); if (Status != XST_SUCCESS) return -1; XAdcPs_SetSeqChMode(&XAdcInst, XADCPS_SEQ_CH_SAFE_MODE); XAdcPs_SetSeqInputMode(&XAdcInst, XADCPS_SEQ_INPUT_MODE_SAFE); while (1) { u32 raw_t = XAdcPs_GetTemperature(&XAdcInst); u32 raw_vi = XAdcPs_GetVccint(&XAdcInst); u32 raw_va = XAdcPs_GetVccaux(&XAdcInst); xil_printf("T=%.2f C VCCINT=%.3f V VCCAUX=%.3f V\r\n", adc_temp_to_celsius(raw_t), adc_volt(raw_vi), adc_volt(raw_va)); usleep(500000); } return 0; }几个说明。xil_printf 在 Standalone BSP 里默认输出到 stdout,而 stdout 在 BSP 里已经绑定到你在 Vivado 里配置的 UART,连上板子的串口终端就能看到输出。如果串口没有任何打印,先检查 BSP 的 stdout 配置和你的调试串口是否一致。usleep(500000) 是 500ms 一次轮询,XADC 安全模式下内部转换一直在跑,所以每次读取拿到的都是最近一轮的结果,这里延时只是控制打印节奏。两个换算函数都显式用了 float 运算,raw 值先乘再除,保证小数部分不丢。
4.4 提高采集稳定性:均值滤波与报警阈值
XADC 原始数据在 12 位下多少会有点抖动,尤其是电源轨附近有开关噪声的时候。xadcps 库提供了均值功能,XAdcPs_SetAvgMode 可以设置平均次数,比如 XADCPS_AVG_16_SAMPLES,让硬件层面做 16 次累加平均。开启均值后读数更稳定,代价是理论转换速率降低,但内部传感器的采样速率本来就用不完,平均之后反而更符合监控场景。
XAdcPs_SetAvgMode(&XAdcInst, XADCPS_AVG_16_SAMPLES);温度报警阈值也经常在这里一起配。XAdcPs_SetAlarmThreshold 可以设置高温上限,XAdcPs_SetAlarmEnables 打开对应的报警输出。要注意报警中断默认是不使能的,开了阈值还要在 GIC 里配中断,对纯轮询场景可以直接在循环里比较换算后的温度值,超过阈值就点亮一个 GPIO 或打印告警,比折腾中断简单得多。
5. 避坑:XADC 读不到数据时的 5 种排查路径
5.1 现象:XAdcPs_CfgInitialize 返回 XST_FAILURE
程序跑起来后卡在 Status 判断那里,返回值不是 XST_SUCCESS。这种失败绝大多数不是代码问题,而是驱动和硬件资源对不上:BSP 侧没有生成 xadcps 库,LookupConfig 拿不到配置;或者 PL 侧例化了 xadc_wizard 抢占 XADC 控制权。解决方式按顺序排查,先在 BSP 工程右键 Re-generate BSP Sources 并确认 system.mss 里出现 XAdcPs 条目,再回到 Vivado 的 Block Design 删掉 xadc_wizard,重新导出 hdf 后同步更新 SDK 工程。两步做完,90% 的初始化失败都能收掉。
5.2 现象:温度值固定为 0x000 或 0xFFF
温度读出来要么是 0,要么是 4095,换算结果要么是 -273°C 要么是几百上千度,完全不在合理范围。原因通常是采样序列被改成了只采外部通道,XADC 根本没有运到内部传感器;另一类原因是时钟没起来,安全模式下如果没有可用时钟,转换结果就是无效满码。解决:在读取前强制调用 XAdcPs_SetSeqChMode 和 XAdcPs_SetSeqInputMode 切回安全模式,不要依赖上电默认值;时钟问题要回头确认 PS 配置里是否意外禁用了 XADC 相关资源,纯 PS 直驱模式用的是片内时钟,一般不会涉及外部引脚,但如果你在 PL 侧动过时钟约束就要多留个心眼。
5.3 现象:电压读数为真实值的一半
VCCINT 原本 1.0V,读出来只有 0.5V,第一反应是板子供电坏了。其实更常见的是读错了通道:XAdcPs_GetVccint 返回的是内部电源通道的转换结果,这个通道在片内已经按 3V 满量程归一化。如果代码里绕过驱动直接读外部通道寄存器,或者采样序列里根本没包含 VCCINT 通道,拿到的当然是别的数据。解决:确认用的是 xadcps 库提供的 XAdcPs_GetVccint / GetVccaux 系列函数,并且采样序列保持在安全模式;换算时用统一的 3V 公式,单位不要搞错。
5.4 现象:BSP 里找不到 xadcps 驱动
BSP 配置页 Peripherals 列表里 xadcps 是灰的或者根本没有这一项。原因是导出的硬件平台文件里没有携带 XADCPS 资源信息,最常见的是 Vivado 工程 Block Design 改过但没有 Validate Design 和重新 Export Hardware,SDK 拿到的还是老版本 hdf。解决:回到 Vivado,先在 Address Editor 里确认 PS 系统监视器地址段存在(0xF8007000 附近),然后重新 Export Hardware 覆盖旧文件;SDK 侧对 BSP 工程右键 Re-generate BSP Sources,再检查 system.mss。这步做完如果还没有,去 Zynq PS 配置页面里看有没有把 XADC 相关的选项误关掉了。
5.5 现象:和 PL 例化的 xadc_wizard 同时使用时数据互相干扰
PS 直驱和 PL 例化 xadc_wizard 在同一套逻辑里共存,表现为 PS 侧读温度偶尔跳变,PL 侧采集结果也不稳定。原因就是共用 XADC 硬核时没有仲裁,两个控制器各写各的寄存器,配置序列被反复覆盖。解决:要么拆除其中一个,要么在 XADC 配置寄存器里明确设置仲裁优先级。SDK 的 xadcps 驱动默认不处理双控制器模式,需要你自己操作 XADC 控制寄存器里的仲裁位。对绝大多数 7010 项目,二选一是最省心的做法,别让两个控制器同时碰这个硬核。
6. 把 XADC 数据收进帧:串口协议与连续打印技巧
6.1 串口帧结构
轮询结果直接打印给人看没问题,但给上位机解析就有点难受了。我一般会把温度和电压放大 1000 倍转成整数,按固定帧格式发出去,上位机收到再还原物理量,省得在串口里传浮点字符串。帧结构简单可靠即可:
| 字节 | 内容 |
|---|---|
| 0 | 帧头 0xAA |
| 1 | 帧头 0x55 |
| 2 | 温度高字节 |
| 3 | 温度低字节 |
| 4 | VCCINT 高字节 |
| 5 | VCCINT 低字节 |
| 6 | VCCAUX 高字节 |
| 7 | VCCAUX 低字节 |
| 8 | 累加校验和 |
6.2 发送代码与验证
void send_xadc_frame(u32 raw_t, u32 raw_vi, u32 raw_va) { u8 buf[9]; u8 sum = 0; buf[0] = 0xAA; buf[1] = 0x55; buf[2] = (raw_t >> 8) & 0xFF; buf[3] = raw_t & 0xFF; buf[4] = (raw_vi >> 8) & 0xFF; buf[5] = raw_vi & 0xFF; buf[6] = (raw_va >> 8) & 0xFF; buf[7] = raw_va & 0xFF; for (int i = 0; i < 8; i++) sum += buf[i]; buf[8] = sum; for (int i = 0; i < 9; i++) XUartPs_Send(&UartInst, &buf[i], 1); }XUartPs_Send 是 BSP 里 UART 驱动提供的发送接口,UartInst 换成你工程里初始化好的实例即可。注意 UART 波特率不需要太高,115200 在 7010 上足够稳定,数据量本身也不大。上位机按帧头 0xAA 0x55 做同步,校验和不匹配直接丢帧,这种简单协议比打印字符串好解析得多,后续要扩展外部通道采集结果,往帧里加字节就行。
从那以后我每次拿到一块新的 ZYNQ 7010 板子,只要 PS 能启动,第一件事就是先跑一遍这套 XADC 驱动,确认温度在合理范围、各路供电电压正常,再往下做业务逻辑。硬件问题在早期暴露出来,后面能省出大把排障时间。这套流程对最小系统验证、热设计评估和电源完整性粗查都很实用,希望帮到你。
本文还有配套的精品资源,点击获取