基于FPGA的64路模拟量采集与NAND Flash存储系统设计
简介:《基于FPGA的数据采集系统》是一份面向电子信息、自动化及FPGA初学者与毕业设计学生的技术文档,围绕多路信号实时采集与存储需求,给出从课题背景、整体方案到硬件电路设计的完整论述。压缩包仅含1个doc文件,约526KB,正文为本科毕业设计说明书,重点涉及64路0~5V模拟信号、8路无源开关量及1路TTL/12V脉冲信号的采集方案,并讨论FPGA中心控制、A/D转换、存储器、接口电路与PC通信等模块。已有638人学习,适合用作数据采集系统课程设计、毕业设计选题参考,也可帮助读者理解多路模拟开关、电压跟随器、AD9221与并口传输等实现思路,并借鉴系统实时性、可靠性、灵活性与可扩展性的设计考量。文档对开题报告、方案论证和硬件框图说明的撰写亦有参考价值。
1. 六十四路模拟量、八路开关量、一路脉冲:这套 FPGA 数据采集系统到底解决什么问题
实验室里最容易吃灰的板子,不是画错的那一版,而是“仿真波形全对、上电就是没数”的那一版。我手上这套基于 FPGA 的数据采集系统就属于后者:64 路 0~5V 模拟信号、8 路无源开关量、1 路 TTL 或 12V 数字脉冲,全部进现场可编程门阵列做时序调度,AD9221 负责模数转换,K9F1G08 负责掉电不丢数据,最后经并口把数据倒进 PC 做分析。整套东西定位很明确——不是通用采集卡,而是一块“多通道、低速轮询、能脱机存储、事后再回读”的存储测试板,适合做状态监测、故障记录、多路传感器巡检这类场景。
它适合两类人:一类是刚接手毕业设计、手上只有一小块 FPGA 开发板和一堆传感器,想搞清楚“多路复用 + ADC + Flash”这条链路怎么搭起来的人;另一类是手上已经有采样需求,想找个能跑通的最小系统照着改的工程师。下面按我拆这套图纸的顺序讲:先看架构怎么切,再看模拟通道怎么算建立时间,然后是数字量调理和 Flash 的页块操作,最后是我踩过的坑和把这套东西改造成能用的采集卡的思路。
2. 架构拆解:从 0~5V 输入端到并口输出,一条链路四个模块
2.1 四个模块怎么切:采集、控制、存储、接口
这套系统的模块划分非常“教科书”,但每一块都有它不得不这么切的理由。整条链路是:信号采集模块(模拟、数字、开关三种通道各自的调理电路)→ 中心控制模块(FPGA 及其外围,包括晶振、配置 EPROM、供电)→ 存储器模块(K9F1G08 NAND Flash)→ 接口电路(并口)→ PC。四个模块之间不是简单串联,FPGA 同时要给 ADC 发时钟、给模拟开关发通道地址、给 Flash 发命令和地址、给并口发握手信号,它其实是整块板子的“节拍器”。
模拟通道部分,64 路信号先各走一级电压跟随器,再进 4 片 16 选 1 的模拟开关,选通后送 AD9221 转成 12 位并行数据。数字通道只有 1 路,但要兼容 TTL 和 12V 两种电平,所以先做钳位再进 FPGA。开关量 8 路是无源的,必须先隔离、再驱动、再整形,才能被 FPGA 采到。这三条链路最终都汇到一个点上:FPGA 内部的写缓存。
接口这块用的是并口,也就是打印口那套 SPP/EPP 时序。选它的原因很实在——FPGA 侧只要几个 IO 加一个状态机就能实现,PC 侧用inp/outp直接读写端口,不用写驱动、不用做协议栈。代价是速度慢、线缆短、现在的机器基本没这个口了,这也是后面二次开发要动刀的第一处。
2.2 三种信号的调理链路差异
把三条链路的差异摊开看,会发现它们根本不是同一类问题。模拟量的问题是“精度”,从运放失调、模拟开关导通电阻、ADC 量化误差一路累加下来,12 位 ADC 实际能用的有效位可能只有 10 位出头。数字量的问题是“电平”,12V 脉冲直接怼进 3.3V 的 IO,轻则不识别,重则打穿输入级。开关量的问题是“干扰”,现场的地电位差和电磁场会让无源触点读出乱七八糟的抖动。
所以调理电路的设计目标完全不同:模拟通道追求的是阻抗匹配和建立时间,跟随器把高阻信号源变成低阻,让模拟开关和 ADC 采样保持电路能快速充电;数字通道追求的是钳位和限幅,把任何高于 3.3V 的输入削平;开关通道追求的是电气隔离,用光耦把现场侧和板子侧彻底断开。这三条链路共用一个 FPGA,但它们的时序预算要分开算,混在一起算必然出问题。
2.3 时钟与总线:谁给谁打拍子
整块板子只有一个主时钟源,一个晶振进 FPGA,FPGA 内部用 DCM 或者计数器分频出几路节拍:一路给 AD9221 的 CLK,一路给模拟开关的通道切换做计时,一路给 Flash 的命令状态机,还有一路给并口握手。之所以让 FPGA 当唯一时钟源,是因为 AD9221 的转换时钟和通道切换必须严格对齐——通道刚切完就启动转换,采到的就是上一路的残影。
这里有个容易忽略的点:AD9221 是流水线结构,从时钟沿到数据有效有固定的延迟拍数。数据出来的时候,通道号已经走过去了。如果不在 FPGA 里做延迟对齐,写进 Flash 的每一条记录都会“张冠李戴”,通道 5 的数据被打上通道 6 的标签,而且这个错误是系统性的,看波形根本看不出来,只有回读分析时才会发现某几路的曲线整体错位。
2.4 资源预算:XC2S50 这点家底怎么分
选的是 Spartan-II 系列的 XC2S50,属于小容量器件,几千个逻辑单元、十几块 4Kbit 的 Block RAM,加起来片上存储也就 8KB 这个量级。这个家底决定了设计上必须抠:64 路通道计数、ADC 接口、Flash 状态机、并口协议,这四块逻辑要挤在一起,FIFO 还不能开太大。
我一般会先做一张资源分配表再动手写代码,避免写到一半发现 BRAM 不够:
| 模块 | 主要占用 | 预估比例 | 备注 |
|---|---|---|---|
| 通道扫描与 ADC 接口 | 计数器、移位寄存器 | 约 15% 逻辑 | 必须用同步设计,别用门控时钟 |
| Flash 命令状态机 | 状态寄存器、地址计数器 | 约 20% 逻辑 | 状态编码用独热码更省译码逻辑 |
| 写缓存 FIFO | Block RAM | 约 60% BRAM | 深度按“一页 Flash 容量”倒推 |
| 并口协议 | 组合逻辑 + 少量寄存器 | 约 10% 逻辑 | 握手信号要打两拍防亚稳态 |
这张表的意义在于,它提前告诉你 FIFO 深度不可能做很大。K9F1G08 一页是 2112 字节,如果 FIFO 想缓存整页,8KB 的 BRAM 就直接见底了。实际做法是 FIFO 只做几百字节的弹性缓冲,靠 Flash 页编程期间的“写等待”来倒逼上游降速,或者干脆在采集阶段先把数据攒在外部 SRAM 里——但原设计没有外扩 SRAM,所以只能靠降低有效采样率来匹配 Flash 的写入速度。
3. 模拟通道实战:LM324 跟随、ADG506 选通与 AD9221 采样节拍
3.1 LM324 跟随器:单电源 5V 下的摆幅陷阱
64 路模拟输入的第一级是电压跟随器,用的是四运放 LM324。选它的理由书上写得很清楚:电源范围宽、静态功耗小、可单电源、便宜。但真正焊上去才会发现,这几个优点背后藏着一个尖锐的问题——LM324 不是轨到轨运放,单电源 5V 供电时,输出高电平大概只能到 3.5V 到 4V 左右(取决于负载和批次),低电平倒是能贴到地。
这意味着如果输入信号真的跑到 5V,跟随器输出会被削顶,ADC 采到的永远是 4095 附近的平台。更隐蔽的是,这个削顶不是硬削,是软压缩,前段看着线性,接近上限时斜率逐渐变小,做出来的曲线像是传感器非线性,排查半天怀疑到传感器头上。我的处理办法有两个:要么把运放供电抬到 7~9V,让它在上限附近仍有裕量;要么在通道标定时把上限压到 4V 用,剩下的量程用软件补偿。前者改动小、效果直接,我一般优先走这条。
跟随器的另一个作用是阻抗变换。传感器输出可能有几十 kΩ 的内阻,模拟开关导通电阻本身就有两百欧上下,两者串联再给 ADC 的采样保持电容充电,建立时间会明显变长。加一级跟随器之后,源阻抗降到欧姆级,建立时间的问题基本就消掉了。
3.2 4 片 ADG506 拼 64 路:地址译码与通道漏电
64 路要进一个 ADC,只能用多路复用。选的 ADG506 是 16 选 1 的 CMOS 模拟开关,导通电阻约 200Ω,导通和关断时间都在 200ns 量级,泄漏电流典型值 20pA。4 片拼起来正好 64 路,每片的使能端由 FPGA 单独控制,地址线 A0~A3 四片共用,这样 6 根线(4 位地址 + 2 位片选)就能完成 64 路寻址。
最关键的一点:任一时刻只能有一片使能。如果两片同时导通,两路信号在输出端直接短接,轻则串扰,重则把前级跟随器拖进非线性区。所以片选信号必须做互斥译码,我习惯在 FPGA 里用独热码直接生成 4 个使能,从源头上杜绝“两片同时亮”的可能。
// 64 路寻址:高 2 位选片(独热码),低 4 位选通道 // ch_sel[5:4] 片号,ch_sel[3:0] 片内通道号 reg [5:0] ch_sel; reg [3:0] mux_en; // 4 片 ADG506 的使能,独热码 always @(*) begin case (ch_sel[5:4]) 2'd0: mux_en = 4'b0001; // 第 1 片,通道 0~15 2'd1: mux_en = 4'b0010; // 第 2 片,通道 16~31 2'd2: mux_en = 4'b0100; // 第 3 片,通道 32~47 default: mux_en = 4'b1000; // 第 4 片,通道 48~63 endcase end wire [3:0] mux_addr = ch_sel[3:0]; // 四片共用地址线这段是纯组合逻辑,ch_sel一变,片选和地址立刻跟着变。用独热码而不是二进制译码,是因为在 FPGA 里四个使能信号直接对应四个引脚,不需要额外的 2-4 译码器,时序路径更短,也不会出现译码毛刺导致两片瞬时同时导通。参数上要注意ch_sel的位宽必须是 6 位,用 5 位就会在 32 之后回绕,表现是后 32 路永远采不到。
通道间的漏电是这套结构固有的毛病。ADG506 断开时是高阻,但不是无穷大,20pA 乘上源阻抗,在慢速采集时影响不大;但如果前级是高阻信号源,断开通道的残压会通过寄生电容耦合到导通通道上,表现是某几路之间数值“粘连”。解决办法就是前面说的跟随器,把源阻抗压下去,这个现象基本消失。
3.3 AD9221 的采样时序与流水线延迟
AD9221 是 12 位、最高 1.5MSPS 的流水线型 ADC,单电源 +5V 供电,输出并行接口、兼容 TTL 电平,时钟由 FPGA 提供。流水线结构的优点是速度快、分辨率不低,代价是有固定的转换延迟,从时钟沿打进去到 12 位数据稳定输出,中间隔了好几个时钟周期。
这个延迟如果不补偿,就会出现前面说的“数据张冠李戴”。补偿的办法很土但有效:把通道号也当成一路数据,跟着 ADC 的时钟一起往后移同样多的拍数,等数据出来时,移位寄存器里对应的那个通道号正好对齐。
// AD9221 流水线延迟补偿:数据和通道号一起打拍,保证一一对应 // PIPE_LEN 取 7,具体值要按器件手册和实际时钟频率实测确认 localparam PIPE_LEN = 7; reg [11:0] data_pipe [0:PIPE_LEN-1]; reg [5:0] ch_pipe [0:PIPE_LEN-1]; integer i; always @(posedge adc_clk or negedge rst_n) begin if (!rst_n) begin for (i = 0; i < PIPE_LEN; i = i + 1) begin data_pipe[i] <= 12'd0; ch_pipe[i] <= 6'd0; end end else begin data_pipe[0] <= adc_data; // ADC 原始输出先进第一级 ch_pipe[0] <= ch_sel; // 当前正在转换的通道号 for (i = 1; i < PIPE_LEN; i = i + 1) begin data_pipe[i] <= data_pipe[i-1]; ch_pipe[i] <= ch_pipe[i-1]; end end end wire [11:0] adc_data_aligned = data_pipe[PIPE_LEN-1]; // 对齐后的采样值 wire [5:0] ch_aligned = ch_pipe[PIPE_LEN-1]; // 对齐后的通道号PIPE_LEN这个参数千万别照抄。不同批次、不同时钟频率下实际延迟可能有出入,正确做法是喂一个已知的斜坡信号或者直流电平,用逻辑分析仪同时抓时钟和输出,数一下从时钟沿到数据变化隔了几个周期,再把这个数填进去。我吃过一次亏:照着手册写了个估算值,结果 64 路里有一路整体偏移,查了两天才发现是延迟常数差了一拍。
3.4 通道切换的建立时间预算
这是整个模拟通道里最容易被忽略、又最影响实际采样率的一环。AD9221 标称 1.5MSPS,但这不意味着 64 路每路都能跑到 1.5MSPS——那是 96MSPS 的总吞吐,根本不可能。真实的有效采样率是这么算的:每切换一次通道,要等模拟开关导通(200ns)、信号在寄生电容上建立、运放输出稳定,这段时间统称建立时间;建立完之后才能启动 ADC 转换,转换本身又要占若干个时钟周期。
我把这笔账列成表,改参数的时候直接套:
| 环节 | 典型耗时 | 说明 |
|---|---|---|
| 模拟开关导通 | 约 200ns | ADG506 手册值,实际受温度影响 |
| 信号建立到 0.5LSB | 300~800ns | 取决于源阻抗和分布电容,跟随器能显著缩短 |
| ADC 采样保持 + 转换 | 约 4~7 个时钟 | 1.5MSPS 下约 2.7~4.7us |
| FIFO 写入与握手 | 1~2 个时钟 | 同步 FIFO,可忽略 |
按每路 1us 建立加 3us 转换来算,一路大概 4us,64 路轮一圈就是 256us,也就是单通道有效采样率不到 4kSPS。这个数字很多人第一反应是“太低了吧”,但对状态监测、温度巡检这类应用完全够用——被测物理量本身的变化时间常数就在秒级。
// 64 路轮询节拍:先等建立,再放开 ADC 时钟,最后收数并切下一路 localparam SETTLE_MAX = 8'd40; // 50MHz 下 800ns,等模拟开关和运放建立 localparam CONV_MAX = 8'd24; // 480ns 转换窗口,留足 ADC 流水线填充时间 reg [5:0] ch_sel; reg [7:0] wait_cnt; reg adc_clk_en; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin ch_sel <= 6'd0; wait_cnt <= 8'd0; adc_clk_en <= 1'b0; end else if (wait_cnt < SETTLE_MAX) begin wait_cnt <= wait_cnt + 8'd1; // 刚切完通道,时钟先关掉 adc_clk_en <= 1'b0; end else if (wait_cnt < SETTLE_MAX + CONV_MAX) begin wait_cnt <= wait_cnt + 8'd1; adc_clk_en <= 1'b1; // 建立完成,放开 ADC 时钟采样 end else begin wait_cnt <= 8'd0; adc_clk_en <= 1'b0; ch_sel <= ch_sel + 6'd1; // 一路结束,切下一路 end endSETTLE_MAX是最该调的参数。调小了,通道间串扰明显,表现为相邻通道数值接近;调大了,采样率白掉。我的习惯是从小往大试,先用 20 个周期跑一遍,看相邻通道是不是“串味”,再逐步加到波形干净为止,然后乘 1.5 倍留裕量。别一步到位设很大,那样板子能跑,但你永远不知道真实下限在哪。
4. 数字量与存储:12V 脉冲钳位、光耦隔离和 K9F1G08 页块操作
4.1 12V 脉冲进 3.3V 系统:钳位电路的取舍
那一路数字脉冲要兼容 TTL 电平和“低电平 0V、高电平 12V”两种形式,后者直接接进 FPGA 的 IO 是要出事的。原设计的做法很直接:串一个限流电阻,再并一个 3.3V 稳压管到地,超过 3.3V 的部分被稳压管钳住,低于 3.3V 的部分原样通过。
这个电路能工作,但有几个隐含前提。第一,限流电阻要选得够大,让稳压管工作在额定电流附近而不是过流烧掉,同时又要够小,保证信号上升沿不被 RC 拖得太慢。第二,稳压管的钳位不是瞬时的,纳秒级的尖峰它反应不过来,高频毛刺还是能漏过去。第三,长期工作在钳位状态,稳压管发热,夏天机箱里温度一上来,钳位电压会漂。
我的改良做法是两级:先电阻分压把 12V 降到 4V 左右,再用钳位管兜底,最后进施密特触发器整形。分压把大部分能量消耗在电阻上,钳位管只处理残余过冲,发热和漂移都好很多。如果脉冲频率不高,也可以直接用光耦隔离,把电平问题变成电流问题,顺带把地环路也断了。
4.2 光耦隔离:CTR 与限流电阻怎么算
8 路无源开关量走的是光耦隔离。光耦的原理不复杂,发光二极管把电信号变光,光敏三极管再把光变回电,输入输出在电气上完全断开,隔离电压能做到千伏级。对现场设备来说,这一步是保命的——地电位差、浪涌、电磁干扰都被挡在光耦另一侧。
设计上有两个电阻要算。输入侧的限流电阻决定 LED 电流,一般取几毫安,太小了 LED 老化快,太大了光敏三极管驱动不足。输出侧的上拉电阻要看电流传输比,也就是 CTR——输入多少电流能换来输出多少电流。CTR 是光耦最要命的参数,它不是常数,会随温度和使用时间衰减,手册标 50% 的器件,用了几年可能只剩 30%。
计算的思路是反着来:先定输出低电平需要拉多少电流,再除以最坏情况下的 CTR,得到 LED 必须有的最小电流,再据此算限流电阻的上限。输出上拉电阻如果选得太大,拉电流能力不足,输出低电平下不去;选得太小,光敏三极管的暗电流又会影响高电平。这个取舍没有通用解,只能按你实际用的光耦型号和负载来定,我一般留 30% 以上裕量。
还有一个细节:光耦是反相的。输入高电平点亮 LED,光敏三极管导通,输出被拉低。所以进 FPGA 之前逻辑要反一次,不然读到的开关状态全是相反的。这个错误很隐蔽,因为开关量本来就只有两个状态,反了之后“开”显示成“关”,如果不做已知状态测试,根本发现不了。
4.3 74HC14 整形与去抖
隔离之后的信号还不能直接进 FPGA,因为上升沿下降沿可能很缓,或者带毛刺。这里用 74HC14 施密特反相器做整形,它的特点是输入从低到高的翻转阈值高于从高到低的翻转阈值,中间有个回差带,缓慢变化的信号经过它之后边沿会变陡,小幅毛刺被回差吃掉。
对无源开关量来说,机械触点的抖动是绕不过去的。施密特触发器能解决电气层面的抖动,但机械抖动持续时间能到毫秒级,光靠硬件整形不够,还得在 FPGA 里做软件消抖——连续采到 N 个相同电平才认定状态变化。我一般取 10ms 左右的消抖窗口,对开关量这种变化缓慢的信号完全够用,也不会漏掉真实动作。
4.4 K9F1G08 的页/块操作与命令时序
存储用的是 K9F1G08,128M×8 的 NAND Flash,单电源 3.3V 供电,掉电不丢数据。它的组织方式是:页是最小读写单位,一块包含若干页,擦除必须以块为单位,不能单独擦一页。这个“读改写粒度不一致”是 NAND 最反直觉的地方,也是新手最容易翻车的地方。
引脚上要认准几个:CLE 是命令锁存使能,ALE 是地址锁存使能,CE 是片选,WE 是写使能,RE 是读使能,WP 是写保护,R/B 是忙闲指示。命令、地址、数据全部走同一组 8 位 IO,靠 CLE 和 ALE 区分当前传的是什么。也就是说,同一个 IO 口,这一拍传的是命令 0x80,下一拍可能就是地址低字节,再下一拍就是数据——全靠控制信号区分。
// K9F1G08 页编程流程:命令 -> 4 个地址周期 -> 数据 -> 执行 -> 查状态 localparam CMD_WRITE = 8'h80; // 页编程命令 localparam CMD_EXEC = 8'h10; // 启动编程 localparam CMD_STATUS = 8'h70; // 读状态寄存器 localparam S_IDLE = 3'd0, S_CMD = 3'd1, S_ADDR = 3'd2, S_DATA = 3'd3, S_EXEC = 3'd4, S_WAIT = 3'd5, S_CHECK = 3'd6; reg [2:0] state; reg [2:0] addr_step; // 0~1 列地址,2~3 行地址 reg [10:0] col_addr; // 页内偏移,0~2111 reg [15:0] row_addr; // 页号,0~65535 reg rdy; // 上游 FIFO 有数据 always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; addr_step <= 3'd0; end else case (state) S_IDLE: if (fifo_used >= PAGE_BYTES) state <= S_CMD; S_CMD: begin flash_cle <= 1'b1; // 命令周期,CLE 拉高 flash_io <= CMD_WRITE; state <= S_ADDR; end S_ADDR: begin flash_cle <= 1'b0; flash_ale <= 1'b1; // 地址周期,ALE 拉高 flash_io <= (addr_step < 2) ? col_addr[7:0] : row_addr[7:0]; addr_step <= addr_step + 3'd1; if (addr_step == 3'd3) begin // 4 个周期传完 flash_ale <= 1'b0; state <= S_DATA; end end S_DATA: if (!rdy) state <= S_EXEC; // FIFO 空,数据写完 S_EXEC: begin flash_cle <= 1'b1; flash_io <= CMD_EXEC; // 0x10 启动内部编程 state <= S_WAIT; end S_WAIT: if (flash_rb) state <= S_CHECK; // R/B 变高表示内部操作结束 S_CHECK: begin flash_cle <= 1'b1; flash_io <= CMD_STATUS; // 读状态,bit0 为 1 表示失败 state <= S_IDLE; end endcase end地址周期是 4 个,前两个是列地址、后两个是行地址,这是 K9F1G08 这类 1Gbit 器件的固定要求,换成更大容量的芯片可能是 5 个周期,改型号时这个必须核对。S_WAIT状态不能靠固定延时糊弄,必须等 R/B 引脚真的变高——页编程的典型时间在几百微秒量级,但最坏情况可能翻好几倍,写死延时要么浪费时间要么偶发失败。
S_CHECK里读回来的状态字节要看 bit0,为 1 表示这一页编程失败。失败不一定是坏事,NAND 出厂就有坏块,用着用着还会新增坏块,正常的做法是给每个块建一张坏块表,写之前先查表跳过。这套毕业设计里没有做坏块管理,数据量小的时候碰不上,一旦连续写满几片就必然遇到。
4.5 FPGA 内部 FIFO 与写盘状态机
三种信号进到 FPGA 之后并不是立刻写 Flash,而是先进 FIFO 缓冲。原因是 Flash 页编程期间是不能接收新数据的,那几百微秒里上游要是还在采,数据就丢了。FIFO 的作用是把“连续采集”和“间歇写入”这两个速率不匹配的过程解耦。
设计上 FIFO 深度不需要很大,但必须大于“一次页编程期间上游产生的数据量”。按每路 4us、64 路轮一圈 256us 来算,一次页编程 300us 里最多产生不到一圈半的数据,也就是 100 条记录左右,每条记录 12 位数据加 6 位通道号,打包成 3 字节,300 字节出头。FIFO 开 512 字节就够,剩下的 BRAM 留着做别的。
写盘状态机要处理一个边界情况:采集结束的那一刻,FIFO 里可能还剩半页数据,没凑满一页。这时候不能直接丢掉,要按“半页编程”处理——K9F1G08 支持写入不满一页的数据,只是剩余空间的内容是随机的,读的时候必须靠额外的长度字段来界定有效数据。我一般会在这页的 spare 区写一个长度标记,回读时先读标记再决定读多少。
5. 避坑与排查:五个让板子“仿真通过、上电没数”的坑
5.1 上电后 ADC 有时钟没数据:复位没接对
现象是逻辑分析仪能看到 FPGA 输出的 ADC 时钟在跑,但 12 位数据线一直是某个固定值,既不变化也不为 0。第一反应是 ADC 坏了,换一片还是老样子。
原因基本都出在复位上。AD9221 内部有基准和偏置电路,上电之后需要一个稳定时间,如果 FPGA 复位一释放就开始猛打时钟,ADC 还在上电初始化过程中,输出就是无效的。另外有些板子的 ADC 复位引脚直接接了上拉,没有受 FPGA 控制,上电时序完全靠 RC 延时,这个延时在不同电源斜率下差别很大。
解决的办法是给 ADC 单独做一个上电延时计数器,比如复位释放后先等 10ms 再放开时钟,同时把 ADC 的复位引脚也接到 FPGA 上,由状态机统一控制。改完之后这类“换芯片不解决问题”的毛病就再没出现过。
5.2 只有前 16 路有数:片选译码写错
现象很典型:回读数据里通道 0 到 15 有正常数值,16 到 63 全是 0 或者全是一个固定噪声。第一反应是模拟开关后面几片没焊好,万用表量了半天,使能信号确实有。
原因是通道计数器的位宽或者片选逻辑写错了。用 4 位计数器去数 64 路,数到 15 就回绕,后 48 路永远轮不到;或者片选译码的时候把ch_sel[5:4]写成了ch_sel[4:3],导致片号和地址错位,某些片永远不被使能。
排查方法很笨但很快:让 FPGA 固定输出某一片的使能,用示波器量 4 片的使能引脚,看是不是只有一片在动。同时把计数器的高位引到一个测试引脚上,看它是不是真的在 0~3 之间循环。这类问题都是位宽和位序的低级错误,但在一堆信号里找起来很费眼睛。
5.3 数据整体错位一路:流水线延迟没对齐
现象是回读曲线看着都对,但每一路的标签都往后偏了一格,通道 0 的数据被打上了通道 1 的标记。如果被测的 64 路信号本身很接近,这个错误几乎发现不了;只有当某一路接了明显不同的信号,才会看出来“标签和数值对不上”。
原因就是前面说的 AD9221 流水线延迟没有补偿,或者补偿的拍数不对。数据出来的时候通道号已经加过一了,直接拿当前通道号去打包就是错的。
解决方法是把通道号和数据一起打拍,拍数用实测值而不是手册估值。实测的办法是给所有通道接同一个直流电平,只把其中一路接一个明显不同的电平,然后扫一遍,看这个“异类”出现在哪个标号下,差几路就补几拍。这个校准过程只需要做一次,但要写进代码注释里,不然换个人接手又得重来一遍。
5.4 Flash 首块是坏块,整板卡在等待状态
现象是采集正常、FIFO 正常、状态机也进了写流程,但就是一直停在S_WAIT出不来,R/B 引脚永远是低电平。断电重启有时候好有时候坏。
原因是上电后直接往第 0 块写数据,而这一块恰好是出厂坏块。NAND 的坏块标记写在每块第一页或第二页的 spare 区,出厂坏块在出厂时就已经标记好,如果程序不读这个标记直接写,写操作就会失败,R/B 可能长时间不释放。
正确的做法是上电初始化时先扫一遍坏块表,把每块第一页的 spare 区第一个字节读出来,不是 0xFF 就标记为坏块,写数据时跳过。这套流程要放在状态机最前面,作为上电自检的一部分。我现在的习惯是,任何带 NAND 的板子,第一版代码里先把坏块扫描写进去,宁可多花几百毫秒上电时间。
5.5 并口读回全是 0xFF:方向位和握手没配对
现象是 PC 侧程序跑起来不报错,但读回来的每个字节都是 0xFF,换个并口模式还是一样。板子那一侧用逻辑分析仪看,数据确实在变。
原因通常有两个。一是并口的数据端口方向没设对,标准的并口数据口是 8 位输出,要做输入必须走 EPP 或者设方向寄存器,没设对的话读到的就是总线上的悬空电平,全 1。二是握手信号接反了,FPGA 认为已经给了一个字节,PC 却还在等“数据有效”,两边互相等,读到的是上一次的残留值。
排查时先在 PC 侧写一个最简单的程序,只做一个动作:往数据口写 0x55,然后立刻读回来,看是不是 0x55。这一步能验证端口方向和地址是否正确。然后再加握手,一步一步来。并口这套东西没有协议栈兜底,全靠时序对齐,不这么分步验证,很容易在“到底是硬件还是软件”的问题上耗掉一整天。
6. 二次开发:把并口采集卡改成 USB 高速采集的四步改造
这套设计的瓶颈不在 FPGA,也不在 ADC,而在那条并口线上。并口 SPP 模式理论速率也就几百 KB/s,EPP 模式好一些但线缆长度受限,而且现在的 PC 基本没有这个接口了。要让它真正能用,第一步就是换接口。
我的建议是分四步走,每步都能独立验证,不要一口气全改。第一步,把并口换成 USB FIFO 芯片,FPGA 侧的逻辑几乎不用动——原来给并口的握手时序,改成给 FIFO 的读写使能,数据宽度还是 8 位,改动量最小。上位机换成对应的驱动库,读写变成批量传输,速度能上到几 MB/s,比并口快一个数量级。
| 改造项 | 原方案 | 建议方案 | 改动量 | 收益 |
|---|---|---|---|---|
| 主机接口 | 并口 SPP/EPP | USB FIFO 或百兆网口 | 中 | 速度提升、线缆通用 |
| 触发方式 | 无,上电即采 | 预触发环形缓冲 + 边沿触发 | 大 | 能抓到事件前的数据 |
| 坏块管理 | 无 | 上电扫描 + 坏块表 | 小 | 长期存储可靠性 |
| 通道标定 | 无 | 上位机两点标定 | 小 | 消除运放和 ADC 的增益偏差 |
第二步是加触发。现在这套是上电就一直采,有用的数据可能只占百分之一,其余全是无效记录。做法是在 FIFO 前面加一个环形缓冲,一直循环写入但不落盘,检测到触发条件(某个开关量跳变、某路模拟量越限)才开始真正往 Flash 写,同时把环形缓冲里之前的预触发数据一起写进去。这一步对“抓偶发故障”这类需求价值最大,改动主要在状态机的启动条件上。
第三步是坏块管理,前面已经说过。这一步不难,但必须在长时间连续采集之前做完,否则跑一天下来发现一半数据写失败,返工成本很高。
第四步是通道标定。LM324 的失调、模拟开关的导通电阻、ADC 的增益误差,这些加起来会让 64 路的读数彼此不一致。做法是给所有通道输入同一个标准电压,记录每路的读数,算出各自的增益和偏移,存到上位机的配置文件里,显示时做一次线性修正。这一步不改硬件,但能让整套系统的可用精度上一个台阶。
最后说个血泪经验。我一开始改接口的时候,图省事把 FPGA 侧的并口状态机整段删掉重写,结果新代码和原来的采集节拍对不上,采出来的数据每隔几十个点就丢一个。后来老老实实保留原来的采集状态机,只在输出端换了个“出口”,问题立刻消失。从那以后我每次动这种“能跑但不好用”的板子,都强制走一遍“先保留能跑的部分,只在边界上替换”的流程,哪怕多写几十行胶水逻辑,也比推倒重来稳。
这套基于 FPGA 的数据采集系统,电路不复杂,器件也都是常见型号,真正花时间的地方全在时序对齐和信号调理的细节上。把 64 路轮询的建立时间算清楚、把 ADC 的流水线延迟对齐、把 Flash 的坏块跳过,这三件事做扎实,它就能从毕业设计变成一台能干活的设备。希望帮到你。
本文还有配套的精品资源,点击获取