简介:围绕FPGA多通道采样系统设计展开的文档,面向电子工程、嵌入式与FPGA学习者及需要搭建高速数据采集方案的开发者。内容以多通道采样控制器为主线,讲解用VHDL有限状态机控制AD7892、音频信号20Hz~20kHz采样对转换速率的要求,以及将通道选择、串并输出控制等模块集成进FPGA后带来的外围电路简化与稳定性提升;同时涉及被动串行配置、SRAM掉电丢失编程信息及专用配置器件等工程问题,并简述设计技术、系统调试与应用。资源包共1个doc文件,约938KB,已有295人学习,适合作为课程设计、毕业设计或项目预研的参考,帮助读者梳理系统组成、模块划分与FPGA软件设计重点。
1. 一块掉电就“失忆”的采集板,把 8 路音频塞进 FPGA
调试台上摆着一块板子:8 根音频线插进模拟开关,示波器探头挂在 CONVST 引脚上,每 2 微秒出现一个负脉冲,脉冲结束后 EOC 拉低,12 位数据从 AD7892 的 DB0~DB11 并行出来,被 FPGA 一个时钟周期锁进内部队列。这套基于 FPGA 的多通道采样系统,核心就是一颗 EP1K30TC144-3 加一片 500kSPS 的 AD7892,把 8 路 20Hz~20kHz 的音频信号轮流采成数字流,再按需要串行或并行送出去。
它适合两类人:一类是要做音频采集、传感器阵列、多路信号巡检的硬件工程师,外围电路极简,通道选择和时序全在 FPGA 里;另一类是学 VHDL 和有限状态机、想找一个能真正上板跑通的完整例子的学生或转岗开发者。这份资料的价值不在理论,而在它把放大量、滤波器截止频率、状态机节拍、FIFO 深度这些实际参数都固定下来了,照着做就能复现。坑也真实:FPGA 配置信息存在 SRAM 里,断电就没了,每次上电都得重新配置。
2. 系统架构与选型:8 通道音频采样为什么落到 FPGA 加 AD7892
2.1 三条方案路线和最后拍板的那一条
这套设计最有参考价值的部分,是它把方案论证过程完整保留下来了。很多项目文档只给结论,读者不知道为什么要用状态机而不是计数器硬怼,也不知道 FIFO 到底解决了什么问题。这三条路线恰好对应三种典型思路。
| 方案 | 通道切换 | AD 控制 | 数据缓存 | 采样率上限 | 主要问题 |
|---|---|---|---|---|---|
| 方案一 | 3 位计数器驱动 CD4051 | 进程并行操作 AD574 | 无,直接锁存 | 50kSPS | 8 通道音频采不动,数字部分还复杂 |
| 方案二 | 同上 | 有限状态机控制 AD7892 | 外接 RAM | 500kSPS | 多一颗 RAM,成本高,读写速度拖后腿 |
| 方案三 | 同上 | 有限状态机控制 AD7892 | 片内 FIFO | 500kSPS | 占用内部 RAM 资源 |
方案一的问题很直接:AD574 转换速率最高 50kHz,8 路分摊下来每路 6kHz 出头,20kHz 的音频信号根本恢复不出来,奈奎斯特这道坎过不去。方案二把 ADC 换成 AD7892,速率够了,但外部 RAM 的读写周期通常在几十纳秒量级,还得处理地址总线、片选、读写使能,一旦时序没对齐,采到的数据就是错位的,反而比没有缓存更麻烦。
方案三的关键改动是把缓存搬进 FPGA 内部。FIFO 的读写各自一个周期就能完成,写侧跟着 ADC 的转换节奏走,读侧跟着输出接口的节奏走,两边互不阻塞。这就是采样系统里常说的“瓶颈问题”:ADC 出数很快,输出接口很慢,中间没有缓冲就只能丢数或者降速。我第一次做类似设计时没加 FIFO,串口一发数据 ADC 就停采,波形上直接出现一段平线,查了半天才反应过来是背压传回了采样侧。
提示:FIFO 深度不是越大越好。片内 RAM 就那么多,EP1K30 的嵌入式阵列块数量有限,深度开到 512×12 已经占掉相当一部分,再大综合就报资源不足。
2.2 从音源到数字总线的信号链路
把框图摊开看,整条链路是四段:模拟前端、通道选择、模数转换、数字处理。每一段的输入输出电平、阻抗、时序都得对上,任何一段掉链子,后面全白搭。
模拟前端从普通音源取信号,输出幅度大约 300mV。AD7892-1 的输入范围可以配成 ±10V 或者 ±5V,300mV 直接进去只用到满量程的 3%,12 位分辨率等于白扔了 5 位。所以必须先放大,倍数落在 10~30 之间,用一个精密可调电阻调。放大之后进有源带通滤波器,把 20Hz 以下和 20kHz 以上的成分压掉,避免带外信号在采样时混叠回来。
通道选择用 CD4051,八选一的模拟开关,3 位地址由 FPGA 给。这里有个容易被忽略的点:模拟开关有导通电阻,通常几十到一百多欧,和后级 ADC 的输入阻抗形成分压,会带来增益误差;更麻烦的是开关切换后输入节点要重新建立,建立时间不够,采到的就是上一个通道的残留电压。
模数转换由 AD7892 完成,12 位逐次逼近型,单 +5V 供电,内含采样保持器和 2.5V 基准。它支持串行和并行两种输出,由 MODE 引脚选。本设计选了并行,12 根数据线直接挂到 FPGA 的 IO 上,控制线只有 CONVST、EOC、CS、RD 四根。
数字处理全在 FPGA 内部:通道地址生成、AD 时序控制、切换延时、FIFO 缓存、串并输出选择。原设计说得很明白,把这些都集成进去的好处是外围电路简单、稳定性强。我做板子时深有体会,外围每多一个芯片,就多一组电源去耦、多一段走线、多一个虚焊点,调试时间翻倍。
2.3 关键器件参数对照与选型依据
选型这部分,原文档给的数据够用,但没做成对照表,翻起来费劲。我按实际会影响设计决策的维度重新整理了一遍。
| 器件 | 关键参数 | 在本设计中的作用 | 选型时该盯什么 |
|---|---|---|---|
| EP1K30TC144-3 | 约 3 万门级,144 脚 TQFP,SRAM 工艺 | 全部数字逻辑载体 | 逻辑单元够不够放状态机加 FIFO;IO 数量够不够接 12 位数据总线 |
| AD7892-1 | ±10V 或 ±5V 输入,500kSPS,12 位,并行/串行可选 | 模数转换 | 输入范围匹配放大后的信号摆幅;转换加采样时间要小于通道轮询周期 |
| CD4051 | 八选一模拟开关,3 位地址 | 8 路通道切换 | 导通电阻和通道间串扰;切换后建立时间 |
| NE5532 | 双运放,增益带宽积 10MHz,转换速率 9V/μs,噪声低 | 音频前级放大 | 带宽要覆盖 20kHz 且留足余量;两级放大便于调增益 |
| LM317 | 可调三端稳压 | 提供模拟部分干净电源 | 压差和散热;模拟电源最好单独一路 |
AD7892 有三个版本:-1 是 ±10V/±5V 可设,-2 是 0~+2.5V,-3 是 ±2.5V 但转换更快。本设计选 -1,原因是音频信号放大后是双极性的,正负都跑,±10V 范围留的余量大,前端增益不用卡得那么死。如果换成 -2 那种单极性 0~2.5V,就得在放大器后面再加一级直流偏置,把信号抬到正半轴,多一级就多一份噪声和失调。
FPGA 选 EP1K30 这类 SRAM 工艺器件,代价是掉电丢配置。好处是可以反复烧写、现场改逻辑,样机阶段非常方便。文档里也提到了这个矛盾:解决方法是加专用配置器件,上电自动加载,但成本上去了,所以设计里没采用。这个取舍很实在,实验室环境和野外作业环境对这块的要求完全不同。
3. 模拟前端与 AD 采样:放大倍数、滤波器和 500kSPS 时序
3.1 两级放大与 20Hz–20kHz 带通滤波
放大部分用 NE5532,两级级联。为什么不用一级搞定?因为信号从反相端输入,输出是反相的,而 ADC 需要正相的摆幅,两级反相刚好把相位转回来。前级固定放大 10 倍,后级用可调电阻在 1~3 倍之间调,总增益落在 10~30 这个区间。
算出这个区间的依据是:音源输出约 300mV,AD7892-1 满量程按 ±10V 算,理论增益 33 倍才能吃满;按 ±5V 算是 16.7 倍。取 10~30 是为了留调节余地,实际调试时对着示波器慢慢拧,让最大音量时波形刚好不削顶,同时占满 ADC 量程的七八成。
滤波部分是本设计里比较讲究的地方。20Hz 到 20kHz,上下截止频率之比是 1000,远大于一个倍频程,属于宽带滤波器。宽带带通直接用一级电路实现很难,通常拆成一个低通加一个高通级联,两个滤波器各自的截止频率对准带通的上、下边界。
有源滤波器的设计步骤文档里列了七步,我把它压成实际能操作的四步:先根据阻带衰减速率定阶数,宽带场景一般二阶就够;再选电路形式,二阶压控电压源或者多重反馈都行;然后按传递函数列方程解元件值;最后仿真验证再上板。
元件值我习惯先用脚本粗算一遍,省得在纸上推公式出错。以二阶压控电压源低通为例,截止频率、品质因数、电阻电容的关系是:
# 二阶压控电压源低通/高通截止频率粗算 # fc = 1 / (2 * pi * R * C),Q 由反馈网络电阻比决定 import math def rc_for_cutoff(fc, c=10e-9): """给定截止频率和电容值,反算电阻""" r = 1.0 / (2 * math.pi * fc * c) return r # 高通的 20Hz 边界,电容取 0.47uF 避免阻值过大 r_hp = rc_for_cutoff(20, 0.47e-6) # 低通的 20kHz 边界,电容取 1nF r_lp = rc_for_cutoff(20e3, 1e-9) print(f"高通 R ≈ {r_hp/1000:.1f} kΩ") # 约 16.9 kΩ print(f"低通 R ≈ {r_lp/1000:.1f} kΩ") # 约 7.96 kΩ这段脚本只给出单个 RC 的时间常数,实际二阶电路两个电阻两个电容,还需要按品质因数分配比值,但先算出一个基准值再微调,比一开始就乱试快得多。注意高通那级电容别选太小,否则电阻值冲到几百千欧,偏置电流引起的失调会很明显;低通那级电容别选太大,不然电阻只有几百欧,运放输出带不动。
滤波器做完必须仿真。原文档用的是 EWB,把高通和低通串起来扫频,看通带内是不是平的、两个截止点是不是落在 20Hz 和 20kHz 附近。这里有个容易翻车的地方:两级级联后,后一级的输入阻抗会加载前一级,导致实际截止频率偏移。解决办法是让后一级输入阻抗远大于前一级输出阻抗,或者干脆在仿真里带上完整的两级一起扫。
3.2 AD7892 的引脚配置与时序约束
AD7892 的控制引脚不多,但每个都有硬性时序要求,配错一个就出不来数。
| 引脚 | 方向 | 配置要点 | 时序约束 |
|---|---|---|---|
| CONVST | 输入 | 低到高跳变启动转换 | 负脉冲宽度 ≥25ns |
| EOC | 输出 | 转换结束输出负脉冲 | 持续约 60~100ns |
| CS | 输入 | 片选,低有效 | 可与 RD 同时有效 |
| RD | 输入 | 读使能,低有效 | 宽度 ≥35ns,有效到数据有效约 40ns |
| MODE | 输入 | 高为并行,低为串行 | 本设计接高 |
| VIN2 | 输入 | 接 AGND 时 VIN1 范围 ±10V | 接 VIN1 时为 ±5V |
| STANDBY | 输入 | 低为休眠,高为正常 | 一般直接接高 |
| REFOUT/REFIN | 输出/输入 | 用内部基准时悬空或对 AGND 接 0.1μF | 外部基准时从此脚注入 2.5V |
关键数字记这一组:AD7892-1 的转换时间是 1.6μs,采样保持的采集时间要求 ≥400ns,加起来单次完整周期最小 2μs,对应 500kSPS。读取数据时 CS 和 RD 同时拉低,40ns 后 DB0~DB11 上出现有效数据。EOC 的下降沿意味着转换结束,同时也意味着下一次采样已经开始,这两个动作是重叠的,所以读取本次数据和启动下次采样可以并行做。
STANDBY 引脚千万别悬空。CMOS 输入悬空会飘到中间电平,芯片可能在正常和休眠之间反复横跳,表现是数据时有时无,特别像时序问题,实际是电平没定死。我第一次调这块板子就在这上面浪费了半天,万用表一量才发现是悬空。
3.3 采样率预算:8 路怎么分到 62.5kHz
整个系统的速率预算是这样推下来的:音频上限 20kHz,按奈奎斯特至少 40kSPS;CD 音质的标准是 44.1kSPS;8 个通道轮流采,ADC 总速率至少 44.1k × 8 = 352.8kSPS。选 500kSPS 的 AD7892,留了约 40% 的余量,分摊到每通道是 62.5kSPS,对 20kHz 信号来说过采样 3 倍多,重构滤波器的压力小很多。
这个余量不是浪费。通道切换本身要时间,模拟开关从通道 N 切到通道 N+1,输入节点要重新建立到新的电压,建立时间跟源阻抗、开关导通电阻、ADC 输入电容都有关系,通常给 100ns 以上比较稳妥。如果 ADC 速率刚好卡在 352.8kSPS,切换建立的时间就得从采样窗口里挤,挤不出来就是通道间串扰。
按 2μs 一个完整周期算,20MHz 时钟下是 40 个时钟拍。分配大致是:CONVST 负脉冲 2 拍(100ns,满足 ≥25ns)、转换等待约 32 拍(1.6μs)、读取 2~3 拍、通道切换与建立 3~4 拍。留几拍余量,正好凑满 40 拍。这个分配方式在写状态机时直接对应计数器的阈值,改时钟频率时按比例重算就行。
4. FPGA 内部模块拆解:状态机、通道轮询、FIFO 与串并输出
4.1 顶层划分与时钟复位策略
顶层不做逻辑,只做例化和连线。子模块五个:AD 控制状态机、通道地址计数器、切换延时、FIFO、串并输出选择。这样划分的好处是每个模块都能单独仿真,出问题好定位。
| 模块 | 主要端口 | 职责 |
|---|---|---|
| ad7892_ctrl | clk, rst_n, start, eoc, data_in, convst, cs_n, rd_n, data_out, dvalid | 产生 AD 转换和读取时序 |
| ch_sel | clk, rst_n, dvalid, ch_addr | 每完成一次转换切换通道 |
| ch_delay | clk, rst_n, ch_addr, convst_en | 切换后延时,禁止立即启动 |
| async_fifo | wr_clk, rd_clk, din, wr_en, rd_en, dout, empty, full | 跨速率缓存 |
| out_mux | mode, data_par, data_ser, tx_pin, par_bus | 串并输出选择 |
时钟策略上,整板用一个 20MHz 有源晶振,AD 控制、通道轮询、FIFO 写侧都在这个域里。FIFO 读侧可以同频也可以分频,取决于输出接口速率。跨时钟域的地方只在 FIFO 内部,读写指针各自在自己的域里递增,空满标志用格雷码同步过去,避免多比特同时跳变导致的亚稳态。
复位用低有效异步复位、同步释放。直接异步复位释放,不同触发器退出复位的时间有细微差异,状态机可能跑进非法状态。加两级同步器把复位释放打两拍,成本很低,能省掉一类玄学问题。
4.2 AD7892 控制状态机的 VHDL 实现
有限状态机是整个数字部分的核心,五个状态足够覆盖一次完整转换。写成三段式还是两段式看个人习惯,我这里用两段式,状态寄存器和输出逻辑合并,代码短,综合结果也不差。
-- ad7892_ctrl.vhd 单次转换控制,20MHz 时钟 library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity ad7892_ctrl is port ( clk : in std_logic; -- 20MHz 系统时钟,周期 50ns rst_n : in std_logic; -- 异步复位,低有效 start : in std_logic; -- 启动脉冲,单拍有效 eoc : in std_logic; -- 转换结束,低有效 data_in : in std_logic_vector(11 downto 0); -- DB0~DB11 convst : out std_logic; -- 转换启动 cs_n : out std_logic; -- 片选 rd_n : out std_logic; -- 读使能 data_out : out std_logic_vector(11 downto 0); -- 锁存后的采样值 dvalid : out std_logic -- 单拍有效,驱动通道切换 ); end entity ad7892_ctrl; architecture rtl of ad7892_ctrl is type state_t is (S_IDLE, S_CONV, S_WAIT, S_READ, S_HOLD); signal state : state_t := S_IDLE; signal cnt : unsigned(3 downto 0) := (others => '0'); signal eoc_r : std_logic_vector(1 downto 0) := "11"; -- EOC 同步打两拍 begin process(clk, rst_n) begin if rst_n = '0' then state <= S_IDLE; cnt <= (others => '0'); convst <= '1'; cs_n <= '1'; rd_n <= '1'; data_out <= (others => '0'); dvalid <= '0'; eoc_r <= "11"; elsif rising_edge(clk) then -- EOC 是异步输入,先同步再判断,避免亚稳态 eoc_r <= eoc_r(0) & eoc; dvalid <= '0'; -- 默认清零,只在读取完成那拍拉高 case state is when S_IDLE => convst <= '1'; cs_n <= '1'; rd_n <= '1'; if start = '1' then convst <= '0'; -- 拉低,为上升沿做准备 cnt <= (others => '0'); state <= S_CONV; end if; when S_CONV => cnt <= cnt + 1; if cnt >= 1 then -- 2 拍 = 100ns,满足 >=25ns convst <= '1'; -- 上升沿启动转换 state <= S_WAIT; end if; when S_WAIT => if eoc_r(1) = '0' then -- 同步后的 EOC 拉低,转换结束 cs_n <= '0'; rd_n <= '0'; cnt <= (others => '0'); state <= S_READ; end if; when S_READ => cnt <= cnt + 1; if cnt >= 1 then -- 读使能保持 2 拍 = 100ns,>=40ns 出数 data_out <= data_in; -- 锁存 12 位并行结果 cs_n <= '1'; rd_n <= '1'; dvalid <= '1'; -- 单拍脉冲,通知下游 state <= S_HOLD; end if; when S_HOLD => if start = '0' then -- 等启动信号撤销,防止重复触发 state <= S_IDLE; end if; when others => state <= S_IDLE; end case; end if; end process; end architecture rtl;逐段说下逻辑。复位分支把所有输出置到安全电平:CONVST 高、CS 和 RD 高,芯片处于空闲。eoc_r是个两位移位寄存器,每个时钟把外部 EOC 采样进来,用最高位判断,这样外部信号的毛刺和亚稳态都被滤掉了。这一步不能省,EOC 是芯片输出的异步信号,直接进状态机判断,稳定性和一致性都会受影响。
S_IDLE 等 start 脉冲。这里我用的是“等 start 撤销”的方式,S_HOLD 状态里检查 start 回到低才回 IDLE,避免上游给的是电平信号导致连发。
S_CONV 用计数器卡 2 拍,把 CONVST 低电平保持 100ns,满足 ≥25ns 的要求。有人图省事直接拉低一拍就拉高,20MHz 下是 50ns,理论够,但综合后的输出延迟、IO 缓冲、走线电容都会吃掉一部分,实测脉宽可能只剩 30ns 出头。用计数器卡两拍,安全边际大得多。
S_WAIT 等 EOC 拉低。AD7892-1 转换时间 1.6μs,20MHz 下是 32 拍,这段时间状态机什么都不做,就等。
S_READ 拉低 CS 和 RD,保持 2 拍共 100ns。数据手册要求 40ns 后数据有效,100ns 足够覆盖。第二个时钟沿到来时把data_in锁进data_out,同时给出一拍dvalid。
dvalid是整个系统的节拍源。通道计数器靠它推进,FIFO 写使能也靠它,一个信号串起下游所有动作。
4.3 通道轮询计数器与切换延时
通道计数器很简单,3 位,每次dvalid加一,自然回卷到 0。麻烦的是切换之后的延时:新通道的模拟电压还没建立好就启动转换,采到的是混合值。
-- ch_sel.vhd 3 位通道计数器,8 通道轮询 process(clk, rst_n) begin if rst_n = '0' then ch_addr <= (others => '0'); elsif rising_edge(clk) then if dvalid = '1' then -- 一次转换完成,切到下一通道 ch_addr <= ch_addr + 1; -- 3 位自然回卷,0..7 循环 end if; end if; end process;-- ch_delay.vhd 通道切换后禁止启动,等模拟输入建立 process(clk, rst_n) begin if rst_n = '0' then settle_cnt <= (others => '0'); convst_en <= '0'; elsif rising_edge(clk) then if ch_addr /= ch_addr_d then -- 地址变化,重新计时 settle_cnt <= (others => '0'); convst_en <= '0'; elsif settle_cnt < SETTLE_TICKS then settle_cnt <= settle_cnt + 1; else convst_en <= '1'; -- 建立完成,允许启动转换 end if; ch_addr_d <= ch_addr; end if; end process;SETTLE_TICKS是个常量,按公式取,一般给 2~4 拍,也就是 100~200ns。这个值跟前端源阻抗关系很大:源阻抗高,建立慢,就得多给。串行电阻加得越大,这个数越要往上调。不确定的时候先用 4 拍,跑起来看通道间串扰,再往下压。
ch_addr_d是打一拍的历史值,用来检测地址变化。这里没做完整的边沿检测,只比较不等,实际综合出来就是一个异或门,够用。
4.4 FIFO 缓存与串并输出选择
FIFO 解决的是速率不匹配。写侧每 2μs 来一个数据,读侧如果是串口,115200 波特率下发 12 位数据加帧头,一个字节 87μs,差距两个数量级,没缓存必丢。
片内 RAM 用双口模式,读写各自独立寻址。核心是读写指针和空满判断:
-- async_fifo.vhd 精简版,深度 512,位宽 12 -- 写指针只在 wr_clk 域递增,读指针只在 rd_clk 域递增 -- 空标志:读写指针相等(同域比较) -- 满标志:写指针追上读指针的同步值,留一格防覆盖 process(wr_clk, rst_n) begin if rst_n = '0' then wr_ptr <= (others => '0'); elsif rising_edge(wr_clk) then if wr_en = '1' and full = '0' then ram(wr_ptr) <= din; -- 双口 RAM 写端口 wr_ptr <= wr_ptr + 1; end if; end if; end process; process(rd_clk, rst_n) begin if rst_n = '0' then rd_ptr <= (others => '0'); elsif rising_edge(rd_clk) then if rd_en = '1' and empty = '0' then rd_ptr <= rd_ptr + 1; end if; end if; end process; dout <= ram(rd_ptr); -- 读侧组合输出,或打一拍寄存 empty <= '1' when rd_ptr = wr_ptr_gray_sync else '0';指针跨域时用格雷码,每次只变一位,同步过去最多错一拍,不会出现指针跳到完全无关的值。满标志的判断要留一格,写指针不能真的追上读指针,否则空满状态无法区分。
串并输出选择是一个二选一多路器,mode为高走并行总线,为低走串行帧。并行模式下 12 位数据加上 3 位通道号一起送到外部总线,外部处理器按通道号分拣。串行模式下把 12 位数据打包成帧:帧头、3 位通道号、12 位数据、校验位,从一根线发出去。
串行这套在通信场景里很有用,采到的数据调制后发到远端,远端解调还原。并行那套适合本地接处理器或者高速存储器,一次搬一整块。两种模式共用同一套采样前端和 FIFO,只换出口,这就是原设计说“输出模式可选择性使得应用相当广泛”的实际含义。
5. 上板调试与避坑:从电源轨到 FIFO 指针的排查清单
5.1 分步上电的调试顺序
整机一次上电,出问题就是黑匣子,不知道是哪一级。我习惯按信号流反着来,从后往前调,因为后级是数字的,好观测。
第一步先单独调电源。LM317 输出调到设计值,用示波器看纹波,模拟那一路的纹波要压在几十毫伏以内。数字和模拟分开供电、单点接地,这一点在图纸上很容易画,实际布线时经常被“顺手连一下”破坏掉。
第二步调音频放大。不接 ADC,输入端给 1kHz 正弦,示波器看输出。调可调电阻,让最大输入时输出摆幅接近但不碰 ±10V。这一步顺带看有没有自激,输出上有高频毛刺就是运放振荡了,多半是电源去耦没做好或者反馈走线太长。
第三步调滤波器。扫频看两个截止点,20Hz 和 20kHz 附近增益下降 3dB 的位置对不对。如果截止频率整体偏移,先查元件容差,电容标称值和实测值差 10% 很常见。
第四步单独测 ADC。不接 FPGA,用信号发生器给直流电平,手动给 CONVST 脉冲,用逻辑分析仪看 DB0~DB11 输出。这一步能确认 ADC 本身和外围连线没问题。输入加个已知电压,算一下码值对不对:12 位、±10V 范围,1LSB 约 4.88mV,输入 5V 应该出 2048 左右的码。
第五步接上 FPGA,先烧一个最简单的测试逻辑,让某个 IO 输出方波,确认配置成功、时钟正常。然后再烧完整逻辑,用逻辑分析仪抓 CONVST、EOC、CS、RD 四根线的时序关系。
5.2 五条踩坑记录:现象、原因、解决
第一条:CONVST 脉冲宽度不够,转换偶尔不启动。
现象是采到的数据大部分正常,隔一段时间出现一个重复值或者跳变值,比例大概几十次里一次。用逻辑分析仪抓波形,发现 CONVST 的负脉冲宽度只有 20 多纳秒,比设计的 50ns 窄。
原因是状态机里用单拍拉低,理论 50ns,但综合工具把输出逻辑优化进了 IO 触发器,加上输出缓冲延迟和探头电容,实测被吃掉了近一半。解决方法是改成计数器卡两拍,同时在约束里对 CONVST 加输出延迟约束,别让工具随便优化。改完之后脉宽稳定在 90ns 以上,异常消失。
第二条:EOC 没同步,状态机偶发卡死。
现象是系统跑几分钟后停住,dvalid 不再出现,复位一下又能跑。抓波形看状态机停在 S_WAIT 出不来。
原因是 EOC 直接进状态机判断,它是芯片输出的异步信号,跳变时刻和时钟沿随机对齐,偶尔产生亚稳态,采样到的值不确定。解决方法是加两级同步寄存器,用同步后的信号判断。这个改动只多两个触发器,但把这类偶发问题彻底按死了。
第三条:上电后 FPGA 不工作,重烧一次就好。
现象是每次断电再上电,板子都没反应,必须重新下载一次配置。这是 SRAM 工艺器件的固有特性,配置信息掉电就没了。
原因是配置模式设成了被动串行,但没有配专用的配置器件,上电时没人给它送配置数据流。解决方法是调试阶段用下载电缆,量产或者野外场景必须加配置芯片,上电自动加载。文档里提到因为成本没采用,这个取舍在实验室能接受,在现场不行。另外配置引脚的上拉下拉电阻不能省,模式选择错了,怎么下都下不进去。
第四条:通道间串扰,相邻通道数据混在一起。
现象是只给通道 0 加信号,其他通道悬空,结果通道 1 也能读到明显数值。
原因是切换通道后延时不够,CD4051 的输出节点还残留着上一个通道的电压,ADC 的采样保持电容一充,就把残留值采走了。模拟开关的导通电阻和 ADC 输入电容构成 RC,时间常数比想象的大。解决方法把SETTLE_TICKS从 2 拍加到 4 拍,同时在模拟开关输出到 ADC 输入之间不要加大电阻,源阻抗越低建立越快。串扰从明显可见压到了 1LSB 以内。
第五条:FIFO 偶尔丢数,读出来的通道号不连续。
现象是采一段数据回来看,通道号序列里偶尔缺一个,比如 0、1、2、4、5。
原因有两个可能,一是写满标志判断留的余量不够,写指针追上了读指针覆盖了数据;二是读侧在空的时候还在读,读出了无效值。解决方法先把满判断的余量从 1 格加到 2 格,再把读使能加上not empty的门控,两个都改完问题消失。后来复盘,实际是满标志的格雷码同步慢了一拍,写侧以为没满,实际已经贴边了。
注意:这几条里,前两条和最后一条都属于“时序看着对、逻辑看着对,就是不工作”的类型,逻辑分析仪比示波器好用得多,能同时看多根线的相对关系。手边没有分析仪的话,至少把关键信号引到测试点上。
6. 二次开发:把这个采样控制器改成你自己的前端
想在这套设计上改东西,最省力的路径是按“通道数、采样率、出口”三个维度动,别一上来就重构状态机。
扩通道数。从 8 路扩到 16 路,改两个地方:通道计数器从 3 位加到 4 位,模拟开关从 CD4051 换成十六选一的 CD4067。总采样率不变的前提下,每通道速率从 62.5kSPS 降到 31.25kSPS,如果信号带宽还是 20kHz,奈奎斯特就过不去了,得换更快的 ADC 或者降低通道数。这个账一定要先算,我见过直接把通道数翻倍结果音频高频段全糊掉的。
改采样率。ADC 的转换时间固定,想提速只能压缩等待和切换的余量,空间不大。真要提速,换 AD7892-3,转换时间 1.47μs,或者换用串行接口的 ADC 减少引脚数。改时钟频率的话,状态机里所有计数阈值都要按比例重算,SETTLE_TICKS尤其不能漏。
改出口。并行总线换成 SPI 或者 UART 是最常见的二次开发。FIFO 读侧接一个发送状态机,从 FIFO 取数打包成帧,帧格式自己定。下面这个帧结构我一般用,通道号在前,方便接收端直接分拣:
-- 串行帧:1 字节帧头 0xAA + 1 字节通道号 + 2 字节数据 + 1 字节校验 -- 校验用简单累加和,接收端重算一遍比对 frame(7 downto 0) <= x"AA"; frame(15 downto 8) <= "00000" & ch_addr; frame(31 downto 16) <= x"0" & data_out; frame(39 downto 32) <= sum8(frame(31 downto 0));改配置方案。如果板子要脱离调试环境独立运行,必须解决上电配置。加一颗专用配置器件,被动串行模式接上 DCLK、DATA0、nCONFIG、nSTATUS、CONF_DONE 这几根线,上电自动加载。代价是成本和布板面积,换来的是现场不用带电脑。
验证方法。每次改完,先做功能仿真,灌一段已知频率的正弦波模型进 ADC 行为模型,看 FIFO 输出的数据序列能不能重构出原波形。上板后用一个信号发生器扫频,从 100Hz 扫到 20kHz,逐点看输出幅度是否平坦,通带内波动超过 1dB 就回去查滤波器。最后跑长时间采集,连续几个小时,统计丢点数,这个最能暴露 FIFO 和时序的边界问题。
提示:改动之后最容易被忽略的是复位和上电顺序。模拟部分和数字部分的电源建立时间不同,如果 ADC 比 FPGA 先上电,FPGA 复位期间 IO 状态不确定,可能给 ADC 灌进非法时序。加一个电源监控复位芯片,等所有电源稳定后再释放复位,能省掉一类上电偶发故障。
从那以后我每次改这类采样设计,都强制先把速率预算表列出来,通道数乘每通道速率等于总速率,总速率必须小于 ADC 速率乘以一个不超过 0.8 的系数,余量不够就先换器件再动代码。这个习惯救过我好几次,比事后拿逻辑分析仪抓波形省太多时间。这份资料里的状态机源码、滤波器仿真图和调试记录都挺完整,照着上面的顺序走一遍,基本能复现出一块能跑的板子,希望帮到你。
本文还有配套的精品资源,点击获取