1. 为什么选择Zynq来做便携式γ能谱仪
1.1 从“核辐射探测”这个场景说起
γ能谱仪本质上是一台“能量显微镜”——它测量放射性核素放出的γ射线能量分布,通过特征峰位反推核素种类,通过峰面积推算活度。传统台式γ能谱仪通常由高压电源、前置放大器、主放大器、多道脉冲幅度分析器(MCA)、上位机软件几大块组成,体积大、功耗高、连线复杂。而便携式设备的核心诉求是:小、轻、低功耗、能长时间脱离市电工作,同时还要保证能量分辨率和计数率性能不打太多折扣。
这就对主控平台提出了几个硬性要求:第一,要有足够快的采样能力,γ脉冲经过成形放大后宽度通常在亚微秒到几微秒量级,要准确捕获峰值必须用几十到上百MHz的ADC;第二,要有实时数字脉冲处理能力,包括基线恢复、峰值检测、堆积判弃、幅度提取;第三,要有友好的交互界面和存储通信能力;第四,功耗和体积要可控。
纯FPGA方案能解决前两条,但做界面和文件系统很别扭;纯ARM方案做实时脉冲处理又力不从心。Zynq这种SoC把FPGA可编程逻辑(PL)和ARM处理器系统(PS)集成在一颗芯片上,天然适合这种“前端硬实时+后端软交互”的架构。我在实际选型时对比过几种方案,最终锁定Zynq-7020,原因后面细说。
1.2 Zynq相比分立方案的几个实在优势
用Zynq做γ能谱仪,最直接的收益是数据通路短。ADC采样数据直接进PL,在PL里做数字成形和峰值提取,提取出的幅度值通过AXI总线送到PS端的DDR里,ARM再去做能谱统计、显示、存储。整条链路没有外部总线暴露,抗干扰好,延迟低。
第二个优势是功耗和体积。分立方案里FPGA和ARM各一颗芯片,加上两者之间的接口电路、各自的电源树、时钟树,PCB面积和功耗都上去了。Zynq一颗芯片搞定,典型功耗在1.5W到3W之间(取决于PL资源占用和PS负载),用一块3.7V/5000mAh锂电池加DC-DC就能撑好几个小时。
第三个优势是开发灵活性。PL部分可以随时改数字信号处理算法,PS部分跑Linux,文件系统、USB、网口、LCD驱动都是现成的。我试过用PetaLinux 2025.1来构建系统,虽然版本比较新踩了一些坑,但整体流程比早年顺手多了。
注意:Zynq-7020的PL资源(85K逻辑单元、220个DSP48E1、4.9Mb BRAM)对于γ能谱的数字处理是够用的,但如果你打算做多路ADC并行处理或者复杂的实时算法,建议评估一下资源余量,必要时上7030或7045。
1.3 目标读者与前置知识
这篇内容适合几类人参考:一是做核仪器仪表开发的工程师,想了解怎么把传统模拟MCA搬到SoC上;二是FPGA/嵌入式开发者,想找一个综合性的Zynq项目练手;三是电子类研究生做相关课题。前置知识方面,你需要了解基本的数字电路、Verilog或VHDL、C语言、Linux基本操作。γ能谱的原理我会在用到的地方解释,不要求你先精通核物理。
2. 系统整体架构与关键器件选型
2.1 信号链从探测器到ADC的完整路径
γ射线进入探测器(这里以NaI(Tl)闪烁体+光电倍增管PMT为例,也可以用LaBr3或CZT半导体探测器),产生光脉冲,PMT把光信号转换成电流脉冲。这个电流脉冲很微弱(纳安到微安级),需要经过前置放大器转换成电压脉冲,再经过成形放大器整形成准高斯脉冲,宽度一般在1到4微秒可调。
成形后的脉冲幅度与γ射线能量成正比,这就是能谱测量的物理基础。接下来ADC要对这个脉冲采样。这里有个关键参数:ADC采样率与脉冲宽度的匹配。假设成形脉冲半高宽为2微秒,为了准确捕获峰值,采样率至少要达到脉冲带宽的5到10倍。成形脉冲的等效带宽大约在100kHz到500kHz量级,理论上1MSPS就够,但实际为了做数字滤波和基线估计,我选了40MSPS的ADC,这样每个脉冲有几十个采样点,后续做数字成形和峰值拟合都有余量。
ADC选型上,我用的是ADI的AD9238这类双通道12位40MSPS并行输出ADC,也可以用AD9226(12位65MSPS)。选并行输出而不是LVDS串行,是因为Zynq PL端直接接并行总线最简单,不需要额外的解串器,时序约束也好写。12位分辨率对应4096道,对于NaI探测器(能量分辨率约7%@662keV)完全够用,甚至8位都勉强能用,但12位给数字处理留了余量。
2.2 Zynq PS与PL的任务划分
任务划分的原则是:硬实时、高吞吐的放PL,软实时、交互性的放PS。
PL端负责:
- ADC数据接收与缓存
- 数字基线恢复(用滑动平均或中值滤波)
- 脉冲到达检测(阈值触发+峰值搜索)
- 峰值幅度提取(最大值法或抛物线插值)
- 堆积判弃(如果两个脉冲间隔太近则丢弃)
- 把有效事件的幅度值+时间戳打包,通过AXI-Stream或AXI-Lite送到PS
PS端负责:
- 能谱数据统计(把幅度值映射到道址,累加计数)
- LCD显示(能谱曲线、计数率、峰位信息)
- 数据存储(SD卡或eMMC,存成CSV或二进制格式)
- USB/网口通信(把数据传到上位机)
- 人机交互(按键、触摸屏)
这个划分的好处是PL端处理是确定性的,不受Linux调度影响;PS端处理是非实时的,但能谱统计本身对实时性要求不高,只要不丢事件就行。PL端用一个FIFO做缓冲,PS端批量读取,实测下来很稳。
2.3 电源与模拟前端设计要点
便携设备的电源设计是个大坑。Zynq需要多路电源:PS端核心1.0V、DDR 1.5V、PL端1.0V、辅助3.3V、IO Bank电压根据外设定。我用的是TI的TPS65218这类PMIC,一颗芯片出多路,效率在85%以上。
模拟前端(前置放大器、成形放大器、高压偏置)对电源噪声极其敏感。我的做法是模拟部分单独用LDO供电,和数字电源完全分开,地平面也分开,在ADC下方单点连接。高压部分(PMT需要800到1200V)用小型高压模块,注意屏蔽和爬电距离。
实操心得:ADC的模拟输入前端一定要加抗混叠滤波器,截止频率设在采样率的1/2以下。我一开始省了这个滤波器,结果高频噪声混叠进来,能谱低能端本底明显抬高。后来加了二阶巴特沃斯低通,截止频率15MHz,本底就干净了。
3. PL端数字脉冲处理的核心实现
3.1 ADC数据接收与基线恢复
ADC以40MHz连续采样,数据是12位并行。在PL里用一个简单的状态机接收,写入一个深度256的环形缓冲区。基线恢复的目的是消除脉冲之间的直流漂移。最简单的做法是滑动平均:取脉冲到达前的一段(比如64个采样点)求平均作为基线,脉冲幅度减去基线。
但滑动平均对脉冲堆积敏感,如果缓冲区里混入了脉冲,基线会被抬高。改进做法是用中值滤波或者“剔除最大值后的平均”。我在实现时用了一个取巧的办法:维护一个长度为128的滑动窗口,每次新数据进来,如果它超过当前基线一定阈值就标记为“疑似脉冲”,不计入基线统计。这样基线估计更稳。
// 简化的基线恢复逻辑(示意) always @(posedge clk) begin if (adc_valid) begin if (adc_data > baseline + THRESHOLD) begin // 疑似脉冲,不更新基线 pulse_flag <= 1'b1; end else begin // 正常数据,更新滑动平均 baseline <= baseline + (adc_data - baseline) >> 7; pulse_flag <= 1'b0; end end end这个右移7位相当于除以128,是滑动平均的近似,省去了除法器。实测基线跟踪效果不错,漂移在几个LSB以内。
3.2 峰值检测与幅度提取
脉冲到达检测用阈值触发:当ADC数据连续N个点超过基线+阈值时,认为有脉冲到达。N取3到5,防止噪声误触发。触发后进入峰值搜索状态,在接下来的一个窗口(比如128个采样点,对应3.2微秒)内找最大值。
直接取最大值有个问题:如果采样点没正好落在峰值上,会有误差。12位ADC、40MSPS下,一个2微秒宽的脉冲有80个采样点,峰值附近采样点间隔对应幅度变化很小,直接取最大值的误差可以接受。但如果想更精确,可以用抛物线插值:取最大值点和它左右各一个点,用三点拟合抛物线求顶点。
// 抛物线插值求峰值(示意) // y0, y1, y2 为连续三个采样点,y1为最大值 // 峰值位置偏移 delta = 0.5*(y0 - y2)/(y0 - 2*y1 + y2) // 峰值幅度 = y1 - 0.25*(y0 - y2)*delta这个插值在FPGA里用定点数实现,注意除法要用流水线或者查表。我实测下来,插值后能量分辨率能改善0.2到0.3个百分点,对于追求性能的场合值得做。
3.3 堆积判弃与死时间管理
γ能谱仪在高计数率下会遇到脉冲堆积:两个脉冲靠得太近,成形后叠在一起,幅度提取会出错。堆积判弃的逻辑是:如果当前脉冲的峰值搜索窗口还没结束,又来了一个新触发,就丢弃后一个脉冲(或者两个都丢)。
死时间是指系统处理一个脉冲期间无法接受新脉冲的时间。我的设计里,峰值搜索窗口128个采样点(3.2微秒),加上基线恢复的等待时间,总死时间大约5微秒。对应最大计数率约200kcps。实际NaI探测器在常规环境下的计数率远低于这个值,所以死时间修正可以忽略。但如果你做高计数率测量,就需要做死时间校正,把计数率除以(1 - 计数率×死时间)。
注意:堆积判弃会引入计数损失,但比错误幅度导致的能谱畸变要好。宁可丢计数,不要污染能谱。我在早期版本里没做堆积判弃,高计数率下能谱峰明显展宽,后来加上就好了。
4. PS端Linux系统构建与能谱软件开发
4.1 PetaLinux 2025.1环境搭建与boot.bin生成
PS端跑Linux,用Xilinx的PetaLinux工具链。2025.1版本相比老版本有些变化,我踩了几个坑记录一下。
首先创建工程:
petalinux-create -t project --name gamma_spec --template zynq cd gamma_spec petalinux-config --get-hw-description=../hardware/硬件描述文件是Vivado导出的.xsa。在配置菜单里要选对DDR型号、串口、SD卡、以太网等。关键配置项:
Subsystem AUTO Hardware Settings→ 确认DDR大小和地址Image Packaging Configuration→ 选SD卡启动,rootfs类型选ext4或initramfsDevice Drivers→ 使能USB、SD、GPIO等
配置完编译:
petalinux-build生成boot.bin:
petalinux-package --boot --fsbl --fpga --u-boot --force这个命令会生成BOOT.BIN,包含FSBL、bitstream、U-Boot。注意2025.1里--fpga参数需要指定bit文件路径,如果bit文件在images/linux/下会自动找。
生成boot.scr和image.ub:
petalinux-package --boot --u-boot实际上image.ub是petalinux-build自动生成的,包含内核、设备树、rootfs(如果是initramfs模式)。boot.scr是U-Boot的启动脚本,可以用petalinux-package --boot生成,也可以手写。
4.2 SD卡制作完整步骤
制作启动SD卡,我习惯在Linux主机上操作。假设SD卡设备是/dev/sdb(用lsblk确认,千万别搞错设备名):
# 分区 sudo fdisk /dev/sdb # 创建两个分区: # 分区1:FAT32,大小约200MB,存BOOT.BIN、image.ub、boot.scr # 分区2:ext4,剩余空间,存rootfs和用户数据 # 格式化 sudo mkfs.vfat -F 32 /dev/sdb1 sudo mkfs.ext4 /dev/sdb2 # 挂载并拷贝 sudo mount /dev/sdb1 /mnt/boot sudo cp images/linux/BOOT.BIN /mnt/boot/ sudo cp images/linux/image.ub /mnt/boot/ sudo cp images/linux/boot.scr /mnt/boot/ sudo umount /mnt/boot # 如果rootfs是独立分区 sudo mount /dev/sdb2 /mnt/rootfs sudo tar xzf images/linux/rootfs.tar.gz -C /mnt/rootfs sudo umount /mnt/rootfsboot.scr的内容大致是:
setenv bootargs "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw rootwait" load mmc 0:1 0x3000000 image.ub bootm 0x3000000实操心得:SD卡分区1一定要是FAT32,Zynq的BootROM只认FAT。分区2用ext4,Linux下读写稳定。如果rootfs用initramfs打包进image.ub,分区2可以省掉,但那样每次改文件都要重新打包,开发阶段不方便。
4.3 能谱统计与显示程序
PS端的能谱程序用C或C++写,跑在Linux上。核心是一个道址数组,比如4096个uint32,每个事件到来时把幅度值右移几位映射到道址,对应计数加一。
从PL读数据的方式:我用的是AXI-Lite寄存器+AXI-Stream FIFO。PL端把事件幅度写入FIFO,PS端通过/dev/mem映射FIFO的寄存器地址,轮询读取。更优雅的方式是写一个内核驱动,用中断通知,但开发量大。对于能谱这种低速数据(最大200kcps,每个事件2字节),轮询完全够用。
// 简化的能谱统计(示意) #define FIFO_BASE 0x43C00000 #define FIFO_DATA (*(volatile uint32_t *)(FIFO_BASE + 0x00)) #define FIFO_STATUS (*(volatile uint32_t *)(FIFO_BASE + 0x04)) uint32_t spectrum[4096] = {0}; while (running) { if (FIFO_STATUS & 0x01) { // 有数据 uint32_t amp = FIFO_DATA & 0xFFF; // 12位幅度 uint32_t channel = amp >> 2; // 映射到1024道 if (channel < 4096) spectrum[channel]++; } }显示部分用Qt或者轻量级的LVGL。Qt在Zynq上跑需要配置好framebuffer和触摸屏驱动。如果用Qt的serialport库,编译时要注意在PetaLinux的rootfs里加上qt5的包,或者自己交叉编译。我试过用Qt 5.15交叉编译,serialport库需要先编译qtbase和qtserialport,依赖比较多,建议直接用PetaLinux的meta-qt层。
5. 实测性能与常见问题排查
5.1 能量分辨率与计数率实测
用Cs-137源(662keV)实测,NaI(Tl)探测器配这个系统,能量分辨率(FWHM)在7.5%左右,和商用便携式能谱仪相当。数字处理相比模拟MCA没有明显劣化,反而因为基线恢复做得好,低能端本底更干净。
计数率方面,用不同活度的源测试,死时间在5微秒时,100kcps输入下计数损失约33%,经过死时间校正后能谱峰面积误差在5%以内。如果要做高计数率,需要缩短成形脉冲宽度或者用更快的探测器。
| 参数 | 实测值 | 备注 |
|---|---|---|
| 能量分辨率@662keV | 7.5% | NaI(Tl) 2英寸 |
| 最大计数率 | 200kcps | 死时间5微秒 |
| 功耗 | 2.8W | 含LCD和探测器高压 |
| 连续工作时间 | 4.5小时 | 5000mAh电池 |
| 道数 | 1024 | 12位ADC右移2位 |
5.2 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 能谱无峰 | ADC无数据/阈值太高 | 用示波器看ADC输入 | 调低阈值,检查ADC时钟 |
| 峰位漂移 | 基线恢复失效/温度漂移 | 观察基线寄存器值 | 重新校准基线,加温度补偿 |
| 高能端截断 | ADC溢出/放大器饱和 | 检查脉冲幅度范围 | 调整放大器增益 |
| 计数率异常高 | 噪声误触发 | 看触发信号 | 提高阈值,加数字滤波 |
| SD卡启动失败 | 分区格式/文件缺失 | 串口打印 | 确认FAT32和BOOT.BIN |
| Qt程序无法启动 | 库缺失/显示驱动 | ldd检查依赖 | 补装库,配framebuffer |
5.3 几个踩过的坑
第一个坑是ADC时钟抖动。一开始用PL的MMCM生成40MHz时钟给ADC,相噪不太好,能谱峰有点胖。后来改用外部晶振经时钟缓冲器直接给ADC,MMCM只做同步,峰形就锐了。ADC的采样时钟质量对能谱性能影响很大,别省这个钱。
第二个坑是AXI总线带宽。PL往PS送数据如果走AXI-Lite,每次传输要几个周期,200kcps下CPU占用率很高。后来改成AXI-Stream+DMA,CPU占用率降到5%以下。如果你数据率高,一定要用DMA。
第三个坑是PetaLinux 2025.1的boot.scr生成。新版本里petalinux-package --boot的参数有变化,--fsbl和--fpga的路径要写对,否则生成的BOOT.BIN启动不了。我卡了半天,后来看串口输出发现FSBL没找到bitstream。建议生成后用bootgen工具检查一下BOOT.BIN的内容。
第四个坑是Qt serialport库编译。在Zynq上交叉编译Qt serialport,需要先有qtbase的编译产物,而且要用和rootfs里一致的Qt版本。我一开始版本不匹配,程序跑起来找不到符号。后来直接用PetaLinux的SDK里的Qt,省事很多。
6. 后续扩展方向
这套架构的可扩展性不错。如果想做多路探测器(比如符合测量或者成像),PL端可以例化多个脉冲处理通道,共享一个AXI-Stream合并模块,PS端做符合逻辑。Zynq-7020的资源做4路应该没问题。
如果想提升能量分辨率,可以换LaBr3探测器(分辨率约3%),但LaBr3的脉冲更窄,ADC采样率要提到100MSPS以上,PL端的处理时钟也要相应提高。或者上CZT半导体探测器,直接电荷灵敏,省掉PMT。
软件方面,可以在PS端加一个简单的能谱分析功能,自动找峰、算峰面积、识别核素。用C写个寻峰算法(比如Mariscotti方法)不难,跑在ARM上实时性也够。再进一步,可以把能谱数据通过网口传到上位机,用Python做更复杂的分析。
我个人在实际操作中的体会是,Zynq做便携式仪器最大的价值在于“一颗芯片解决所有问题”,硬件设计简化了,软件开发的灵活性也高。但前提是你要对PL和PS的分工有清晰的认识,该放PL的别放PS,该用DMA的别用轮询。这个项目我从选型到出样机花了大约三个月,其中一半时间在调PL的数字处理和Linux系统构建上,硬件本身反而很快。如果你也在做类似的东西,建议先把PL的数据通路调通,再搞PS的软件,这样问题定位会清晰很多。