简介:面向电子工程、嵌入式与FPGA初学者及课程设计人员,这份文档围绕基于FPGA的多通道采样系统展开,重点讲解用VHDL有限状态机控制AD7892、音频信号采样及通道选择与串并输出模块的集成思路,可帮助读者理解高速采样控制器的整体架构、外围电路简化与被动串行配置方案,也涉及SRAM掉电丢失配置信息及专用配置器件的取舍。资源包仅含1个doc文件,约938KB,篇幅集中、便于通读,适合作为课程设计或项目起步的参考文档。已有295人学习,说明其在相关教学与实践中具备一定参考价值。内容还讨论串行/并行输出在通信采集与处理器读取中的不同用途,并对系统调试和应用作简要说明,读者可据此梳理FPGA软件设计流程、配置模式选择及稳定性设计要点,快速形成可实施的方案框架。
1. 八路音频同时进采,为什么有人偏要用 FPGA 而不是买采集卡
八路音频同时进来,单通道还要压住 44.1kHz 的 CD 标准,第一次听到这个指标,多数人的第一反应是买一块多通道采集卡。这份基于 FPGA 的多通道采样系统设计走的是另一条路:把通道切换、AD 控制时序、串并输出选择、FIFO 缓存全部塞进一片 FPGA,板子上只剩下运放、滤波器和一颗 8 选 1 模拟开关。适合的人群很具体——手上有 ACEX 1K 这类老器件、要做 8 路音频同步采集、并且想弄明白 VHDL 有限状态机到底怎么卡住 AD7892 那 1.6μs 转换窗口的人。它不适合想开箱即用的人,因为从引脚约束到配置模式,每一步都得自己动手。
2. 方案选型与系统骨架:3 条候选路线为什么最后落在 AD7892 + 片内 FIFO
2.1 三条候选路线的量化对比
方案选型阶段最容易犯的错是只比器件价格,不比时间预算。把三条路线摊在同一个采样率坐标系里看,结论会非常清楚。
| 方案 | AD 器件 | 标称转换速率 | 8 通道轮询后每通道 | 数据暂存 | 外围器件数 | 主要问题 |
|---|---|---|---|---|---|---|
| 方案一 | AD574 | 50 kSPS | 6.25 kSPS | FPGA 内部锁存 | 少 | 远低于音频 20 kHz 上限 |
| 方案二 | AD7892 | 500 kSPS | 62.5 kSPS | 外部 RAM | 多(RAM + 地址锁存) | 总线争用、读写速率受限 |
| 方案三 | AD7892 | 500 kSPS | 62.5 kSPS | 片内 FIFO | 少 | 需要额外的空满控制逻辑 |
方案一被否掉的原因不是它做不出来,而是采样率算下来只有 6.25 kSPS,连 20 kHz 音频信号的上限都覆盖不住,采样定理这一关就过不了。方案二在速率上够用,问题出在"边采边存"的时序上:AD 每 2μs 出一个样本,外部 RAM 的地址建立、片选、写脉冲加起来很容易吃掉几百纳秒,再加上 FPGA 还要分时复用同一组数据总线,实际吞吐会掉下来,而且多一颗 RAM 就多一组走线、多一份成本。
方案三保留 AD7892,把存储搬进 FPGA 内部,用 FIFO 做速率解耦。写侧跟着采样节拍走,读侧跟着输出接口走,两边不用互相等待,读写各自只需要一个时钟周期就能完成一次操作。选它的核心理由是 FIFO 把"采样"和"存储/传输"这两个不同速率的过程拆开了,原来的瓶颈问题被绕过去,而不是被硬扛过去。
2.2 采样率预算:44.1 kHz × 8 = 352.8 kHz 是怎么一步步落到 500 kSPS 的
这一段是整套方案的数字地基,算错一次后面所有设计都会跟着歪。
音频信号频率范围 20 Hz~20 kHz,按奈奎斯特采样定理,采样率至少要 40 kHz;工程上对齐 CD 格式取 44.1 kHz。8 个通道轮询,总采样率需求就是 44.1 k × 8 = 352.8 kSPS。选型时留余量,取 500 kSPS 的 AD7892,这样每通道分到 62.5 kSPS,余量约 42%。
但"500 kSPS"这个数字是连续转换的理想值,落到轮询结构里要重新拆时间预算。
| 时间项 | 符号 | 数值 | 说明 |
|---|---|---|---|
| 单次转换时间 | t_CONV | 1.6 μs | AD7892-1 的典型值 |
| 采样保持采集时间 | t_ACQ | ≥0.4 μs | 转换结束后到下一次有效采样 |
| 单通道最小周期 | T_min | 2.0 μs | 上面两项之和,对应 500 kSPS |
| 8 通道一轮理想时间 | T_round | 16 μs | 每通道 62.5 kSPS |
| 保住 44.1 kSPS 的最大轮询周期 | T_round_max | 22.68 μs | 1/44.1 kHz |
| 留给通道切换建立的总余量 | — | 6.68 μs | 22.68 − 16 |
| 单次通道切换可用建立时间 | t_SETTLE | ≤0.835 μs | 6.68 / 8 |
这张表里最关键的一行是最后一行。0.835μs 是模拟开关切换后信号必须稳定到 12 位精度以内的硬上限——超过它,通道间采样率就掉到 CD 标准以下。后面写状态机时那个"延时模块"到底延时多少拍,答案就藏在这个数里,不是拍脑袋给的。
2.3 系统骨架与模块职责划分
整个系统按信号流向切成四段:模拟前端(放大 + 带通滤波)、通道切换(8 选 1)、AD 转换(AD7892)、FPGA 控制与缓存。FPGA 内部再细分五个功能块。
| 模块 | 位置 | 输入 | 输出 | 关键参数 |
|---|---|---|---|---|
| 音频放大 | 板级 | 单端音频约 300 mV | 差分/单端 ±10 V 量程内 | NE5532 两级,前级 10 倍、后级 1~3 倍可调 |
| 带通滤波 | 板级 | 放大后音频 | 20 Hz~20 kHz 通带 | 二阶高通 + 二阶低通级联 |
| 通道选择 | 板级 + FPGA | 8 路模拟信号 | 单路送 AD | CD4051,3 位二进制地址 |
| AD 控制 | FPGA 内 | 状态机命令 | CONVST/CS/RD | t_CONV 1.6 μs,EOC 脉冲 60 ns |
| 通道地址生成 | FPGA 内 | 采样完成脉冲 | 3 位地址 | 0~7 循环 |
| 延时 | FPGA 内 | 时钟 | 等待使能 | 计数器实现,参数化 |
| 串并输出选择 | FPGA 内 | 12 位并行数据 | 并行总线 / 串行位流 | 模式选择引脚控制 |
| FIFO | FPGA 内 | 写使能 + 数据 | 读数据 + 空满标志 | 深度按突发长度算 |
音频放大部分选 NE5532 是因为它带宽够、噪声低、转换速率 9 V/μs,±3~±22 V 的宽电源范围也方便和后面的 ±10 V 量程对接。这里有个容易被忽略的细节:AD7892-1 的输入范围由 VIN2 引脚决定,VIN2 接模拟地时是 ±10 V,接 VIN1 时是 ±5 V。选 ±10 V 看起来余量大,但意味着前端放大倍数要更高,噪声也跟着放大;如果音源本身幅度稳定,±5 V 量程反而能多拿到一位有效精度。
滤波部分没有做成单级带通,而是高通和低通级联。原因很简单:20 Hz 到 20 kHz 的上下截止频率之比是 1000,远大于一个倍频程,属于宽带滤波器,单级有源带通的 Q 值和增益会互相牵制,调截止频率时增益跟着漂移,调试起来很别扭。拆成两级之后,每级只负责一个截止点,元件值可以分别算、分别调。
2.4 时钟与复位规划
主时钟选 50 MHz,一个时钟周期 20 ns。这个选择直接决定了几件事能不能做成:
- CONVST 脉宽要求大于 35 ns,50 MHz 下 2 拍就是 40 ns,够用且还有余量;
- EOC 是 60 ns 的窄脉冲,50 MHz 下能采到 3 个时钟沿,做边沿检测不会漏;
- 1.6μs 的转换时间对应 80 个时钟周期,状态机里用一个 7 位计数器就能数完。
复位用异步复位、同步释放:复位信号直接进触发器的异步复位端,但释放时先打两拍再分发到各模块。这样做是为了避免复位撤销时刻正好落在时钟沿附近,导致部分寄存器复位、部分没复位——老器件上这种亚稳态引发的问题查起来极其费劲。
-- 异步复位、同步释放 process(clk, rst_n) begin if rst_n = '0' then rst_sync <= (others => '0'); elsif rising_edge(clk) then rst_sync <= rst_sync(1 downto 0) & '1'; -- 两级同步后输出高电平 end if; end process; sys_rst_n <= rst_sync(1); -- 各模块统一使用这一路复位这段代码里rst_sync是一个 3 位向量,最低位恒接 '1',每个时钟上升沿整体左移一位。上电后经过两个时钟周期,最高位才变成 '1',这就是"同步释放"的延迟。参数上要注意:同步级数不要少于 2 级,也不要超过 3 级,级数多了会拖长复位建立时间,在某些需要快速响应的模块里反而不合适。
3. AD7892 时序与 VHDL 有限状态机:把 1.6μs 转换窗口卡进 80 个时钟周期
3.1 AD7892-1 的关键时序参数与控制引脚
AD7892 是 12 位逐次逼近型 ADC,单 +5 V 供电,内部带采样保持器和 2.5 V 基准,转换结果可以并行输出也可以串行输出,由 MODE 引脚决定。这套设计用的是并行模式,MODE 接高电平。
| 参数 | 含义 | 数值 | 设计上的含义 |
|---|---|---|---|
| t1 | CONVST 负脉冲宽度 | ≥35 ns | 50 MHz 下给 2 拍(40 ns) |
| t_CONV | 转换时间 | 1.6 μs | 状态机等待 80 拍 |
| t_ACQ | 采样保持采集时间 | ≥400 ns | 转换结束后不能立刻再启动 |
| EOC 脉宽 | 转换结束指示 | 约 60 ns | 必须做边沿检测,不能靠电平判断 |
| t6 | CS/RD 有效到数据有效 | 40 ns | 读操作后至少等 2 拍再取数 |
| t5 | RD 脉宽 | ≥35 ns | 给 2 拍 |
| VIN2 接法 | 决定输入量程 | 接地 ±10 V / 接 VIN1 ±5 V | 直接决定前端放大倍数 |
引脚分工上,CONVST 是启动转换输入,上升沿触发采样保持进入保持态并开始转换;EOC 是转换结束输出,转换完成后输出一个低脉冲;CS 和 RD 配合读数据,两个同时有效后经过 t6 时间,DB0~DB11 上出现转换结果。MODE 接高时,DB0~DB11 作为并行数据口;MODE 接低时,这几个引脚复用成 SDATA、SCLK、RFS 串行接口,所以 MODE 这根线在 PCB 上不能悬空,也不能在运行中随意翻转。
一个常被忽略的点是 EOC 的宽度。60 ns 在 50 MHz 时钟下只有 3 拍,如果状态机用"电平判断"的写法去等 EOC 变低,很可能在两次采样之间错过整个脉冲,然后永远等下去。正确做法是同步后做下降沿检测,或者干脆用固定延时替代 EOC——因为 AD7892-1 的转换时间是确定的 1.6μs,等满 90 拍再读数据,在时钟稳定的前提下反而更省事,代价是损失一点余量。
3.2 状态划分:7 个状态覆盖一次完整采样
一次采样从"切换通道"开始,到"数据写入 FIFO"结束,中间拆成 7 个状态。状态划分的原则是:每个状态只做一件事,且每个状态的停留时间可以用整数个时钟周期表达。
| 状态 | 停留时间 | 动作 | 跳转条件 |
|---|---|---|---|
| S_IDLE | 1 拍 | 清计数器,等待启动 | 启动使能有效 |
| S_SETTLE | 40 拍(800 ns) | 输出通道地址,等模拟建立 | 计数器满 |
| S_CONV | 2 拍 | 拉低 CONVST 再拉高 | 固定跳转 |
| S_WAIT_EOC | ≤100 拍 | 等 EOC 下降沿,带超时 | EOC 到 或 超时 |
| S_READ | 2 拍 | 拉低 CS 和 RD | 固定跳转 |
| S_PUSH | 1 拍 | 锁存 DB,产生 FIFO 写使能 | 固定跳转 |
| S_NEXT | 1 拍 | 通道地址 +1,回到 S_IDLE | 固定跳转 |
把 40 拍、2 拍、100 拍这些数字加起来,一轮 8 通道大约 8 ×(40 + 2 + 80 + 2 + 1 + 1)= 1008 拍,50 MHz 下约 20.2μs,对应每通道采样率约 49.5 kHz,仍高于 44.1 kHz 的门槛。这个余量不是白来的——它是在 2.2 节那张时间预算表里提前留出来的。
architecture rtl of ad_ctrl is type state_t is (S_IDLE, S_SETTLE, S_CONV, S_WAIT_EOC, S_READ, S_PUSH, S_NEXT); signal st : state_t := S_IDLE; signal cnt : unsigned(7 downto 0) := (others => '0'); signal eoc_sync : std_logic_vector(1 downto 0) := (others => '1'); signal eoc_fall : std_logic; constant SETTLE_CYC : unsigned(7 downto 0) := to_unsigned(40, 8); -- 800 ns @50 MHz constant CONV_CYC : unsigned(7 downto 0) := to_unsigned(80, 8); -- 1.6 μs 转换窗口 begin -- EOC 两级同步 + 下降沿检测,避免 60 ns 窄脉冲被漏采 process(clk, rst_n) begin if rst_n = '0' then eoc_sync <= (others => '1'); elsif rising_edge(clk) then eoc_sync <= eoc_sync(0) & eoc_n_sync_in; -- 输入先过一级 IBUF end if; end process; eoc_fall <= '1' when eoc_sync = "10" else '0'; process(clk, rst_n) begin if rst_n = '0' then st <= S_IDLE; cnt <= (others => '0'); convst_n <= '1'; ad_cs_n <= '1'; ad_rd_n <= '1'; fifo_wr <= '0'; elsif rising_edge(clk) then case st is when S_IDLE => cnt <= (others => '0'); fifo_wr <= '0'; st <= S_SETTLE; -- 通道地址已在上一轮 S_NEXT 更新 when S_SETTLE => if cnt = SETTLE_CYC - 1 then cnt <= (others => '0'); st <= S_CONV; else cnt <= cnt + 1; end if; when S_CONV => convst_n <= '0'; -- 负脉冲宽度 2 拍 = 40 ns > 35 ns if cnt = 1 then convst_n <= '1'; cnt <= (others => '0'); st <= S_WAIT_EOC; else cnt <= cnt + 1; end if; when S_WAIT_EOC => if eoc_fall = '1' or cnt = CONV_CYC + 20 then -- 留 20 拍超时保护 cnt <= (others => '0'); st <= S_READ; else cnt <= cnt + 1; end if; when S_READ => ad_cs_n <= '0'; ad_rd_n <= '0'; -- 两个同时有效才出数据 if cnt = 1 then ad_cs_n <= '1'; ad_rd_n <= '1'; cnt <= (others => '0'); st <= S_PUSH; else cnt <= cnt + 1; end if; when S_PUSH => fifo_din <= ad_db; -- 40 ns 后数据已稳定,此处锁存 fifo_wr <= '1'; -- 单拍写脉冲 st <= S_NEXT; when S_NEXT => fifo_wr <= '0'; if ch_addr = 7 then ch_addr <= (others => '0'); else ch_addr <= ch_addr + 1; end if; st <= S_IDLE; when others => st <= S_IDLE; end case; end if; end process; end architecture;这段状态机有几个参数值得单独说。SETTLE_CYC取 40 是按 2.2 节算出的 0.835μs 上限取的保守值,实际调试时可以往下压,压到 20 拍(400 ns)还能保住通道隔离度的话,每通道采样率能再往上抬一截。CONV_CYC + 20这个超时保护是必须的,没有它,一旦 EOC 因为焊接虚焊或者器件损坏一直不翻转,状态机会永久卡在 S_WAIT_EOC,整个系统看起来像死机,而真正的原因藏在模拟侧。fifo_wr只拉高一个时钟周期,这是 FIFO 写使能的通用要求——写使能持续多个周期会被当成多次写入,数据流会整体错位。
3.3 延时模块:为什么不能用 wait for
VHDL 里的wait for 800 ns写起来最直观,但它只存在于仿真世界,综合工具会直接报错或者忽略。可综合的延时只有一种实现方式:计数器。
entity delay_gen is generic ( CLK_HZ : integer := 50_000_000; DELAY_NS : integer := 800 ); port ( clk : in std_logic; rst_n : in std_logic; start : in std_logic; -- 单拍启动脉冲 done : out std_logic -- 延时到期,单拍高电平 ); end entity; architecture rtl of delay_gen is constant CYC : integer := (DELAY_NS * (CLK_HZ / 1_000_000)) / 1000; signal cnt : integer range 0 to CYC := 0; signal busy : std_logic := '0'; begin process(clk, rst_n) begin if rst_n = '0' then cnt <= 0; busy <= '0'; done <= '0'; elsif rising_edge(clk) then done <= '0'; if start = '1' then busy <= '1'; cnt <= 0; elsif busy = '1' then if cnt = CYC - 1 then busy <= '0'; done <= '1'; else cnt <= cnt + 1; end if; end if; end if; end process; end architecture;CYC这个常量在 50 MHz、800 ns 下算出来是 40,正好对应 2.2 节的上限。写成 generic 而不是写死数字,是为了换时钟或者换模拟开关时只改参数不改结构。start必须是单拍脉冲,如果上游给的是电平信号,这里会反复重装计数器,延时永远到不了期——这种问题在仿真里看不出来,上板之后表现为"偶尔正常、偶尔卡住",属于典型玄学故障。
3.4 仿真验证:盯住三根线就够了
写测试平台不用面面俱到,盯住 CONVST、EOC、RD 这三根线的相对关系,就能覆盖 80% 的时序错误。
-- 简化版测试平台:模拟 AD7892 的转换行为 stim_proc : process begin eoc_n <= '1'; wait until falling_edge(convst_n); -- 等到启动转换 wait for 1600 ns; -- 模拟 1.6 μs 转换时间 eoc_n <= '0'; wait for 60 ns; -- EOC 低脉冲只有 60 ns eoc_n <= '1'; ad_db <= x"5A3"; -- 给出固定转换结果便于比对 wait for 2000 ns; end process;这段激励的关键在wait for 60 ns这一行。很多人的测试平台把 EOC 脉冲写成 100 ns 甚至 1μs,仿真一路绿灯,上板就翻车——因为真实器件的脉冲只有 60 ns,刚好卡在同步器的采样窗口边缘。仿真时把脉宽写成和手册一致,问题在仿真阶段就能暴露。另外ad_db给固定值 0x5A3,方便在波形窗口里一眼看出 FIFO 里存进去的数据是不是错位或者少了高位。
功能仿真过了之后还要跑一次时序仿真,把综合后的网表加上延时文件一起仿。这一步能抓到组合逻辑路径过长导致的建立时间违例,尤其是状态机里cnt比较器和通道地址译码这两条路径,在老器件上很容易成为关键路径。
4. 通道切换、串并输出与 FIFO:外围电路怎么"做减法"
4.1 CD4051 的地址生成与模拟建立时间
CD4051 是 8 选 1 的 CMOS 模拟开关,3 位二进制地址 A/B/C 选通一路,INH 引脚高电平时全部关断。地址是二进制编码,不是独热码——这点看起来是废话,但每年都有人把 8 根使能线直接怼上去,然后疑惑为什么只有第一路有信号。
通道地址的生成有两种写法,一种是在状态机的 S_NEXT 状态里加一,另一种是独立的 3 位计数器。前者把地址和时序绑死,后者更灵活,可以在需要时手动指定通道做单通道高速采集。我一般用独立计数器,状态机只发"下一个通道"的脉冲。
process(clk, rst_n) begin if rst_n = '0' then ch_addr <= (others => '0'); elsif rising_edge(clk) then if ch_next = '1' then if ch_addr = std_logic_vector(to_unsigned(CH_NUM - 1, 3)) then ch_addr <= (others => '0'); else ch_addr <= std_logic_vector(unsigned(ch_addr) + 1); end if; end if; end if; end process; cd4051_a <= ch_addr(0); cd4051_b <= ch_addr(1); cd4051_c <= ch_addr(2); cd4051_inh <= '0'; -- 常通,靠地址切换通道CH_NUM用 generic 传进来,默认 8,改成 4 通道采集时只需要改这一个参数,回绕逻辑自动适配。cd4051_inh固定接低是简化处理,如果板子上有多片 CD4051 级联,INH 就要参与片选,不能一直接地。
模拟建立时间是这一段最容易被低估的部分。CD4051 的导通电阻典型值在 100~200Ω 量级,切换瞬间,前级放大器的输出阻抗、开关导通电阻、后级输入电容构成一个 RC 网络,建立到 12 位精度(约 0.024%)需要的时间跟源阻抗直接相关。如果前级运放直接驱动开关,源阻抗低,几百纳秒能稳住;如果中间串了分压电阻,建立时间会翻几倍。工程上的常规做法是在 CD4051 后面加一级电压跟随器做缓冲,把后级的输入电容和开关隔离,这样建立时间可控,通道间串扰也小。
4.2 串行与并行输出的两条通路
输出模式可选是这套设计里比较实用的一点。AD7892 本身支持串行输出,但这份设计里串行通路是在 FPGA 内部用移位寄存器实现的——AD 侧统一走并行模式,数据先读进 FPGA,再由 FPGA 决定往外吐并行还是串行。这样做的好处是 AD 的 MODE 引脚状态固定,不用担心运行中切换模式导致接口时序混乱。
| 输出模式 | 数据宽度 | 典型用途 | 对端接口 | 速率约束 |
|---|---|---|---|---|
| 并行 | 12 位 + 写脉冲 | 接高速存储器或处理器总线 | 三态数据总线 | 受对端总线周期限制 |
| 串行 | 1 位 + 位时钟 | 通信信号采集、远距离传输 | 移位寄存器 / 调制器 | 受位时钟频率限制 |
串行通路的帧结构需要自己定:常见做法是 1 位帧头 + 3 位通道号 + 12 位数据,共 16 位一帧,位时钟由系统时钟分频得到。
-- 并串转换:16 位帧,高位先出 process(clk, rst_n) begin if rst_n = '0' then shift_reg <= (others => '0'); bit_cnt <= 0; ser_sclk <= '0'; ser_sdata <= '0'; elsif rising_edge(clk) then if load = '1' then shift_reg <= '1' & ch_addr & ad_data; -- 帧头 + 通道号 + 12 位数据 bit_cnt <= 16; elsif bit_cnt > 0 then ser_sdata <= shift_reg(15); shift_reg <= shift_reg(14 downto 0) & '0'; ser_sclk <= not ser_sclk; -- 二分频产生位时钟 if ser_sclk = '1' then -- 下降沿计数,保证数据先于时钟稳定 bit_cnt <= bit_cnt - 1; end if; end if; end if; end process;ser_sclk采用二分频翻转,每一位占两个系统时钟周期,接收端在时钟上升沿采样时数据已经稳定了半个周期。16 位帧在 50 MHz 下需要 32 个时钟周期,约 640 ns,折算位速率 1.56 Mbps,对于远距离传输来说偏高,实际用的时候通常再分频到 100~500 kbps 更稳妥。bit_cnt的初值 16 和移位寄存器的位宽必须严格对应,少一位会截断数据,多一位会多出无效位。
4.3 FIFO 深度怎么算:写快读慢是常态
FIFO 不是加上去就完事,深度算错要么浪费片内 RAM,要么照样溢出。计算的基本式子是:
深度 ≥ (写速率 − 读速率)× 突发持续时间
| 输出接口 | 读速率 | 写速率 | 速率差 | 1 ms 突发所需深度 | 建议深度 |
|---|---|---|---|---|---|
| UART 115200 bps | 0.115 Mbps | 8 Mbps | 7.885 Mbps | 约 493 字(16 位) | 不适用,必须降采样 |
| SPI 4 MHz | 4 Mbps | 8 Mbps | 4 Mbps | 250 字 | 512 × 16 |
| 并行口 20 MHz | 320 Mbps | 8 Mbps | 负值 | 不需要缓冲 | 64 × 16 即可 |
这张表最值得看的是第一行。UART 在 115200 波特率下每秒只能送约 11.5 k 字节,而 8 通道 62.5 kSPS 的原始数据率是 500 k 样本/秒,差了四十多倍。这种情况下 FIFO 深度再大也没用,只能吸收极短的突发,长期必然溢出。要么提高读出带宽,要么在 FPGA 里做抽取降采样,把每通道的采样率降到 1~2 kSPS 再往外送。认清这一点能省下很多调试时间——我见过有人在 FIFO 深度上反复加码,从 512 加到 4096,溢出照旧。
-- FIFO 写侧:只在非满时写入,满则丢弃并计数 process(clk, rst_n) begin if rst_n = '0' then fifo_wr_en <= '0'; drop_cnt <= (others => '0'); elsif rising_edge(clk) then fifo_wr_en <= '0'; if sample_valid = '1' then if fifo_full = '0' then fifo_wr_en <= '1'; fifo_din <= frame_data; else drop_cnt <= drop_cnt + 1; -- 溢出计数器,调试时读出来看丢了多少 end if; end if; end if; end process;drop_cnt这个计数器看着不起眼,实际排错时非常有用。它的值一直为零说明 FIFO 深度够用;如果它稳步增长,就说明读侧带宽不足,问题在读接口而不在采样侧,方向一下就明确了。这个计数器最好接到一个可读的寄存器上,通过调试接口读出来,不要只留在综合报告里。
4.4 顶层集成与引脚分配
顶层只做两件事:例化子模块、连信号。真正容易出问题的是引脚分配。
| 引脚类型 | 处理方式 | 原因 |
|---|---|---|
| 时钟输入 | 接专用 CLK 引脚 | 普通 I/O 进全局时钟网络要走长线,抖动大 |
| 配置引脚 | DCLK、DATA0、nCONFIG、nSTATUS、CONF_DONE 保留 | 被用户逻辑占用会导致无法配置 |
| 未用引脚 | 设为三态输入 | 设为输出可能和板上其他器件打架 |
| 模拟开关地址 | 普通 I/O,就近布局 | 走线长会引入额外延时和串扰 |
| AD 数据总线 | 同一 Bank,等长 | 12 位并行,偏斜过大会采到混合数据 |
u_ad_ctrl : entity work.ad_ctrl port map ( clk => clk, rst_n => sys_rst_n, start => sample_start, ch_addr => ch_addr, convst_n => ad_convst_n, eoc_n => ad_eoc_n, ad_cs_n => ad_cs_n, ad_rd_n => ad_rd_n, ad_db => ad_db, fifo_din => fifo_din, fifo_wr => fifo_wr ); u_fifo : entity work.fifo_16x512 port map ( wrclk => clk, wrreq => fifo_wr, data => fifo_din, rdclk => out_clk, rdreq => fifo_rd, q => fifo_dout, full => fifo_full, empty => fifo_empty );注意 FIFO 的读写用了两个不同时钟:写侧跟 50 MHz 系统时钟,读侧跟输出接口时钟。跨时钟域的地方必须用双时钟 FIFO,不能用两个时钟去操作同一块双口 RAM 的地址指针,否则空满判断会出现亚稳态,表现为"偶尔多读一个旧数据"或者"偶尔丢一个样本"。
5. 避坑与排查:五个血泪坑,每个都值得提前知道
5.1 上电后 FPGA 毫无反应,像是没烧进去
现象:下载线连着的时候一切正常,拔掉下载线重新上电,所有输出都是高阻或者固定电平,AD 的 CONVST 一动不动。
原因:配置模式引脚接错,或者被动串行模式下 DCLK、DATA0 没有正确驱动。这类器件的配置信息存在 SRAM 里,掉电即失,上电时必须由外部重新灌入。如果 MSEL 引脚组合选的是被动串行,但板上又没有配置器件,上电后 FPGA 就停在等待配置的状态,用户逻辑根本没加载。
解决:先确认 MSEL 引脚的电平组合与设计意图一致,再确认 nCONFIG 上电后能被拉高、nSTATUS 能正常释放。调试阶段可以用下载线在线配置;脱机运行必须补上配置器件或由板上的微控制器模拟配置时序。这一条在项目正文里其实已经点明了——SRAM 工艺的器件每次上电都要重新配置,野外作业尤其不方便,专用配置器件能自动完成加载,代价是成本。
5.2 状态机卡在 WAIT_EOC 不动
现象:仿真完全正常,上板后 CONVST 有波形,但状态机再也不往下走,FIFO 一个数据都没写进去。
原因:EOC 是 60 ns 的窄脉冲,如果系统时钟频率偏低(比如分频到 10 MHz,周期 100 ns),整个脉冲落在两个时钟沿之间,同步器根本采不到。另一个常见原因是 EOC 没有做同步就直接进状态机,亚稳态导致判断随机失败。
解决:把采样时钟提到足够高(50 MHz 下 60 ns 能覆盖 3 拍),EOC 先过两级同步器再做下降沿检测;或者干脆放弃 EOC,用固定延时替代——转换时间 1.6μs 是确定的,等 90 拍再读,稳定性反而更好,代价是损失一点速率余量。
5.3 通道之间"串味",幅度忽大忽小
现象:给某个通道输入正弦波,相邻通道也能看到衰减后的同频信号;或者同一路信号在不同通道间切换时幅度差 5% 以上。
原因:三种可能。一是通道切换后建立时间不够,前一路的电荷还残留在采样保持电容上;二是 CD4051 的导通电阻和后级输入阻抗形成分压,后级阻抗不够高时增益随通道变化;三是模拟地和数字地处理不当,开关瞬间的 digital 噪声耦合进模拟通路。
解决:先把 S_SETTLE 的延时从 40 拍往上加,看串扰是否减小,能定位是不是建立时间问题;再检查 CD4051 后面有没有加电压跟随器,源阻抗是否足够低;最后查地平面,模拟地和数字地单点连接,AD 的 AGND 和 DGND 分别在各自区域铺铜。
5.4 标称 500 kSPS,实测每通道只有 41 kHz
现象:示波器抓 CONVST,一轮 8 个脉冲的总周期是 24μs 而不是 16μs,每通道采样率算下来 41.7 kHz,刚好卡在 CD 标准下面。
原因:状态机里加了多余的等待状态,或者延时模块的计数拍数比预期多。24μs 减去 16μs 转换时间,多出来的 8μs 分摊到 8 个通道就是每通道 1μs,正好是"建立延时给多了"的典型症状。
解决:把 2.2 节那张时间预算表拿出来逐项对,把 S_SETTLE 从 40 拍压到 20 拍试试,看通道隔离度是否还能接受;同时检查 S_READ 和 S_PUSH 是否有多余的冗余状态,能合并的合并。目标是把一轮压到 22.68μs 以内,留出安全余量。
5.5 FIFO 满标志常亮,数据成片丢失
现象:采集一开始正常,跑几十毫秒后数据流中断,drop_cnt 计数器飞速增长。
原因:写速率远大于读速率。8 通道 62.5 kSPS、每样本 16 位,写速率 8 Mbps;如果读侧走的是低速串口,读速率只有零点几 Mbps,FIFO 再深也会被填满。
解决:先算清楚读侧的真实带宽,再决定是提高读出时钟、加宽数据位,还是在 FPGA 内部做抽取降采样。如果只是偶尔突发,按 4.3 节的公式重算深度;如果是持续性的速率不匹配,加深 FIFO 只是拖延时间,治标不治本。
6. 被动串行配置与上电自加载:把"每次上电都要重配"这件事做干净
调试阶段用下载线在线配置很舒服,一旦设备要脱机运行,SRAM 工艺器件的短板立刻暴露:断电即失,上电必须重新灌配置。项目正文里选择了被动串行模式,理由是可扩展性好、引脚占用少,但没配专用配置器件,因为成本。这个取舍在小批量样机上没问题,真要做到"上电即工作",还是得把配置链路补全。
| 配置方案 | 上电自加载 | 额外成本 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| 下载线在线配置 | 否 | 无 | 实验室调试 | 每次上电都要连电脑 |
| 专用串行配置器件 | 是 | 中等 | 脱机设备、野外作业 | 配置器件与 FPGA 的 DCLK/DATA0 要一一对应 |
| 微控制器模拟被动串行 | 是 | 低 | 板上已有 MCU 的系统 | 需要严格按 nCONFIG→nSTATUS→CONF_DONE 顺序拉时序 |
| 被动串行 + 上位机 | 否 | 低 | 多板卡集中管理 | 依赖上位机,单板无法独立启动 |
被动串行的时序不复杂,但顺序错一步就配不进去:先把 nCONFIG 拉低至少 2μs 让器件复位,释放后等待 nSTATUS 变高,然后以 DCLK 的节奏把配置数据一位一位从 DATA0 送入,全部送完后等待 CONF_DONE 拉高。整个过程 DCLK 的频率有上限,老器件通常在 10 MHz 量级,跑太快会配置失败,而失败的表现是 nSTATUS 反复拉低,看起来像"一直在重启"。
用微控制器模拟这套时序是成本最低的自加载方案,代价是要在固件里嵌一份配置文件,还得处理配置失败重试。用专用配置器件则省心得多,代价是 BOM 上多一颗芯片和几根走线。这两条路怎么选,取决于板子上有没有现成的 MCU——有就顺手做掉,没有就别为了省一颗配置器件去加一颗 MCU。
从那以后我每次画这类器件的板子,都会在第一版就把配置链路和 MSEL 跳线一起布上去,哪怕样机阶段只用下载线。第一版图省事只留了下载线座子,结果设备要拿去现场演示,临时飞线加配置器件,飞线太长导致配置时序不稳,来回折腾了一整晚。这种后悔药,能不吃的尽量别吃。希望帮到你。
本文还有配套的精品资源,点击获取