1. 现象:正余弦编码器角度轨迹上的8 LSB台阶
1.1 现场描述
前阵子调一套高精度运动控制板,前端用的是正余弦编码器,输出Sin、Cos两路差分信号,经调理电路后送到两片14位SAR型ADC做同步采样,FPGA负责时序控制和角度解算。基本原理不复杂:把两路AD采样值做atan2运算,就能得到机械角度,再差分得到速度。整套链路里ADC看起来是最“标准”的一环,数据手册指标也都正常,14位分辨率、无失码、INL在几个LSB以内。但实际跑起来,角度数据和理论预期对不上。
现象非常典型:编码器匀速转动时,角度值本应线性递增,但细看波形,每隔一段会出现一个固定的小台阶,台阶高度换算成ADC码值,不多不少正好8个LSB。用上位机把裸采样数据拉出来也验证了这一点——两路ADC的输出并不是平滑递进,而是走8个码、停一拍、再走8个码,像是有人在中间隔了一拍才让数据通过。这个台阶肉眼扫一眼可能觉得“就是量化噪声嘛”,但在闭环系统里后果很直接:速度环反馈出现周期性扰动,电机低速运行时能听到明显的“咯噔咯噔”声。
这个案例值得写出来,是因为问题根源不在ADC本身,而在采样时序的理解上。尤其对刚接触FPGA+ADC配合的工程师,很容易踩进去:看着数据手册里的BUSY、DRDY、SCK时序图觉得都懂,一到Verilog里写锁存逻辑,就差那么一拍,系统性能就打了折扣。
1.2 把8 LSB换算成看得懂的误差
先算一下8 LSB到底是个什么量级。以常用的2.5V参考电压、14位ADC为例:
1 LSB = 2.5V / 2^14 = 2.5V / 16384 ≈ 152.6µV 8 LSB ≈ 1.22mV如果换算成角度,假设正余弦编码器信号经过调理后满摆幅正好对应ADC的满量程,一圈360度对应16384个码,那么8 LSB对应的角度就是:
360° / 16384 × 8 ≈ 0.176°单看0.176度似乎不大,但放在高精度运动控制里,这相当于把一个14位编码器系统的有效分辨率直接降到了11位左右(因为按8步长跳跃,等效只用了2048个台阶)。再叠加机械减速比、负载惯量,反映到末端执行机构上可能就是一个不小的位置偏差。更要命的是这个台阶是周期性的、确定性的,而不是随机噪声,控制器学不会“忽略”它,只能跟着抖。
所以“8 LSB台阶”不是精度完美主义者小题大做,是实打实的系统级缺陷。排查的时候,我第一反应是查电源纹波、参考电压、模拟前端带宽,折腾了一圈,最后发现真凶藏在数字时序里,这一步排查思路值得展开讲讲。
2. 根因:14位ADC采样时序中的“半拍”陷阱
2.1 从数据手册读出的关键参数
我用的ADC是常见的一类并行/SPI接口SAR型芯片。这类芯片有一个共同的标志性信号:转换完成指示,有的叫BUSY,有的叫DRDY,国产芯片上还可能叫EOC。它们的工作流程都差不多:外部给一个启动转换脉冲(比如CONVST或CNV引脚拉低一下),芯片内部开始逐次逼近,此时BUSY拉高;转换完成后BUSY拉低,同时数据寄存器里的结果准备好,可以对外读出了。
卡点就在“BUSY拉低”和“数据真正稳定”之间。数据手册上有一组参数专门描述这段时间,比如tOD、tDO、tVALID,意思是输出使能之后到数据线上出现有效逻辑电平的延迟。很多SAR ADC的这个延迟在10ns到40ns之间,看起来很短,但对于系统时钟动辄50MHz、100MHz的FPGA来说,这就是一到两个时钟周期的事情。
我当时的错误很典型:状态机里检测到BUSY下降沿,下一个时钟周期就直接去锁存数据总线。严格来说,这等于在数据的建立时间窗口内拍了一下,锁存到的值可能还是上一拍的旧数据,或者正处在翻转中间的不稳定值。
2.2 为什么多看的这一拍如此关键
这里的“多看一拍”,指的是在检测到BUSY下降沿之后,不立即锁存数据,而是先在代码里插入一个或半个等待周期,让数据线真正稳定下来,再去采样。为什么这么小的延迟会造成周期性的8 LSB台阶?
关键在于旧值和新值的差值。编码器匀速旋转时,两路正弦信号以固定频率振荡,ADC每个采样周期里信号变化量基本恒定。比如某个工作条件下,一个采样周期内Sin通道的信号自然增量正好对应8 LSB,那么如果FPGA有一小半几率读的是旧值,系统就会看到“当前值和上一拍相同,下一拍又比当前值大16 LSB”的跳变。换个角度看,就是从原始16 LSB的均匀步进,变成了8 LSB步进、0步进、8 LSB步进交替,数据序列上就形成了等宽的台阶。
这类“半拍”问题还有另一种表现:不是读旧值,而是读到亚稳态。如果恰好锁存在数据线翻转的中间点,D触发器输出可能在一个时钟周期内是不确定状态,下一拍才稳定下来。这会让台阶高度变成随机的,而不是固定的8 LSB。我这次遇到的是固定8 LSB,说明数据线翻转很快、延迟非常一致,状态机每次读到的时机都稳定地“早了半个有效窗口”,所以台阶才那么整齐。
2.3 为什么偏偏是8 LSB而不是随机跳变
这大概是整个排查过程里最让人困惑的一点。如果纯粹是亚稳态,台阶应该是跳跃的、不重复的;如果纯粹是外部干扰,台阶应该是抖动而非固定步长。但8 LSB台阶在多次上电、多次匀速跑动实验中都稳定复现,说明它是一个确定性的系统误差,和信号斜率、采样周期有固定的数学关系。
验证方法很简单:把电机速度翻倍,再观察台阶高度。如果信号斜率翻倍,而“少看那一拍”的时间不变,那么旧值和新值的差值也会翻倍,台阶应该从8 LSB变成16 LSB。实测确实是这样的——台阶高度从8 LSB变成了16 LSB左右,这基本就锁定了问题方向:读取时刻相对数据有效时刻的延迟是固定的,而信号变化率是可变的。
再做一个反向验证:把ADC采样率提高,保持电机速度不变,每个采样周期内信号增量会变小,8 LSB台阶也会等比缩小。当采样率高到一定程度,台阶缩小到1-2个LSB以内,看起来就像普通量化噪声了,这时候问题就被“掩盖”了。但掩盖不等于解决,所以最终还是得回到时序上动刀。
3. 修复:采样逻辑多看一拍的正确姿势
3.1 修改锁存时序的两种做法
确定了根因之后,改动很小,但要改得规范。我在FPGA里用了两种处理,结合起来效果最好。
第一种是“数据延迟锁存”:原本检测到BUSY下降沿后立刻读数据,改成检测到BUSY下降沿后,延时一个系统时钟周期再读。系统时钟50MHz,一个周期20ns,覆盖了ADC约30ns的数据有效延迟。代码很短:
reg busy_d, busy_dd; wire busy_fall = busy_d & ~busy_dd; always @(posedge clk) begin busy_d <= busy; busy_dd <= busy_d; end // 检测到下降沿后,晚一个时钟再锁存 reg [13:0] adc_data_reg; always @(posedge clk) begin if (busy_fall) begin adc_data_reg <= adc_data_reg; // 不动作,留给下一拍 end else if (busy_go) begin adc_data_reg <= adc_data; // 这一拍数据已稳定 end end实际工程里我不会真的写得这么绕,直接用一个延迟计数器:
reg [1:0] delay_cnt; always @(posedge clk) begin if (busy_fall) delay_cnt <= 0; else if (delay_cnt < 2) delay_cnt <= delay_cnt + 1; else adc_data_reg <= adc_data; end第二种是“打两拍同步”:把BUSY信号本身先打两拍,消除跨时钟域带来的亚稳态,同时用打拍后的边沿作为锁存使能。这个方法在FPGA工程里是基本功,但很多人只在对外部异步信号处理时用,没想过对ADC完成信号也要这么干。
3.2 参数计算:延迟多少纳秒才够
延迟不是越久越好。虽然“多看一拍”能解决建立时间不足,但如果看得太多拍,数据虽然稳定,却已经是“过期数据”了。对正余弦采样来说,Sin和Cos两路必须保持同步,如果一路多延迟了几拍、另一路没有,atan2算出来的角度就会带上相位误差,表现为低速时的角度非线性。
所以需要按数据手册算出一个安全窗口。以我用的那款ADC为例:
BUSY下降沿到数据输出有效:最大30ns FPGA系统时钟周期:20ns(50MHz) 建议锁存时刻:BUSY下降沿后 +30ns ~ +80ns 之间30ns大约是1.5个时钟周期,80ns是4个时钟周期。我最后选的是BUSY下降沿之后数到2个时钟周期再锁存,也就是40ns处锁存。这个位置既保证了数据已经完全建立,又不会因为延迟过大引入明显的相位滞后。如果时钟换成100MHz(周期10ns),同样的ADC建议数到3拍,也就是30ns处锁存,留一点余量。
这里有个通用原则:锁存时刻不要卡在参数边界上。比如ADC的tOD最大30ns、典型20ns,你按典型值20ns算作1个时钟去锁存,就可能在最坏情况芯片上踩坑。按最大值再加20%余量来算,颗粒度粗一点、稳定一点,比省那一拍划算得多。
3.3 验证:台阶消失后的实测数据对比
改完时序后,重新跑同样的匀速实验。裸数据曲线从“走8步、停一步”变成了均匀的、接近理论斜率的递增序列。由于ADC本身还存在INL/DNL,相邻点差值不会完全等长,但不再出现周期性的大台阶。角度轨迹上的锯齿消失,速度环反馈波形从周期性脉动变成了接近白噪声的小幅抖动,电机的“咯噔”声也听不到了。
更直观的验证是把同一段数据拿去做直方图统计。修复前,采样点集中落在若干个间隔8 LSB的窄带上;修复后,采样点在满量程范围内的分布明显更均匀,量化步长回到了真实的1 LSB水平。这个统计方法以后排查ADC类问题时也可以复用,比眼睛看波形要可靠得多。
4. 复盘:还有哪些“多看一拍”的坑
4.1 通道切换需要多等建议时间
如果是多通道ADC,比如一片ADC内部带模拟多路开关,专门用来轮询采样Sin和Cos,那“多看一拍”还有一个更重要的应用场景:通道切换后的模拟建立时间。SAR ADC内部的采样电容在切换到新通道时,会带着上一个通道的电荷,必须经过足够的时间把电容充到新通道的电压,否则读出的值就是新旧通道的加权混合。
这种情况下,芯片手册会给出一个“通道切换稳定时间”,常见的是几百纳秒到几微秒。如果软件里切换通道后立刻启动转换,数据就会偏高或偏低,而且误差大小随信号频率变化。处理办法就是:切换通道之后,多读一次并丢弃,或者插入固定延时再启动转换。原理和“多看一拍”完全一样——给系统足够的时间去看清真实世界的电压,而不是急着抓一把就走。
4.2 SPI接口的DRDY/DOUT时序
很多14位ADC用的是SPI接口,比如AD7946这一类的PulSAR结构芯片。SPI模式下同样存在“多看一拍”的时序陷阱,而且更隐蔽。芯片的DRDY信号拉低表示转换完成,但此时SDO引脚上的第一个数据位还没有真正输出,必须由主控端给SCK时钟,数据才会逐个移出来。如果状态机在DRDY下降沿后立刻读DOUT,读到的大概率是上一帧的残留位或者中间态。
常见解法是在DRDY下降沿后先给一个空SCK脉冲,或者用“半拍延迟”的方式,让第一个SCK的有效采样点落在SDO稳定之后。我在SPI模式下调过一款芯片,手册要求DRDY下降沿后至少等待50ns才能开始发SCK,换算成25MHz的SPI时钟就是还要多等1个多周期。工程上最稳妥的做法是状态机里加一个微周期计数器,而不是用固定拍数——这样换芯片或改时钟频率时,不用重写整个状态机。
4.3 哪些情况不该盲目加一拍
“多看一拍”不是万能药,加多了也会引入新问题。比如在校验角度闭环时,如果Sin和Cos两路ADC都各自加了一拍延迟,理论上相位保持一致,但实际因为走线长度、片间差异,两路延迟可能不完全相等,反而引入新的Sin/Cos相位失配。这种情况表现为角度轨迹出现正弦形状的误差,频率是编码器信号频率的2倍,和原来的台阶特征完全不同。
还有一种情况:如果ADC带内部数字滤波/均值功能(很多多通道ADC有过采样平均),那么转换完成信号本身就不是实时结果,而是若干次平均后的值。这时候“多看一拍”只会让数据更旧,不会改善噪声。遇到这类芯片,正确的做法是反过来:仔细读数据手册,确认BUSY信号对应的是哪一次转换的结果,再决定锁存时机。我见过有同事把过采样模式当成直出模式调了两天,最后发现每隔一段读出来的值都偏小,其实就是数据管道延迟没对齐。
5. 调试技巧与工具心得
5.1 逻辑分析仪+VIO抓时序
排查这次问题,最大的功臣是FPGA内部的逻辑分析仪IP(比如Xilinx的ILA)和VIO在线调试工具。我把BUSY信号、系统时钟、数据锁存使能、以及最终的锁存值都拉进波形里,触发条件设成“BUSY下降沿”,然后抓一段编码器匀速转动的数据。这样能直接看到锁存使能相对于BUSY下降沿的相对位置,和ADC数据手册上的时序要求一对比,立刻就能发现锁存早了还是晚了。
用逻辑分析仪抓数据总线的时候有个经验:要把时钟信号一起抓进去,观察数据线和时钟沿的相对关系。很多工程师只抓数据线和BUSY,一看波形觉得“BUSY都拉低了,数据也没问题”,实际上数据在哪个时钟沿被采进寄存器才是关键。把“锁存使能”这一个内部信号拉出来看,比盯外部引脚有用得多。
5.2 8 LSB台阶的快速判别方法
以后再遇到类似“固定LSB台阶”问题,我建议按这个顺序快速排查:
- 第一步,确认台阶高度是否随信号斜率线性变化。改变转速或信号频率,如果台阶高度跟着变,说明和采样时序相关;如果不变,优先怀疑基准、电源或ADC本身。
- 第二步,确认台阶是“周期性”还是“随机性”。周期性台阶大概率是数字时序问题;随机性台阶先考虑抖动和亚稳态。
- 第三步,用示波器同时抓模拟输入和BUSY下降沿,看看模拟信号在BUSY拉低前后是否已经完全建立。有些问题其实是模拟前端带宽不够,导致信号没建立完,看起来和时序问题一模一样。
- 第四步,对照数据手册确认“数据有效”的精确时刻。不要只看时序图的大概位置,要看具体的参数符号和数值。
这四步走下来,大部分ADC台阶问题都能定位。我自己这次就是栽在第二步和第四步之间:太早相信了“ADC哪有什么时序问题”这个先入为主的判断。
5.3 数据后处理:插值和中值滤波的联合使用
在彻底修改时序之前,我还试过一个“软补丁”:用中值滤波把偶发的台阶滤掉,然后在角度解算前做线性插值。中值滤波对孤立跳点确实有效,但面对“固定8 LSB台阶”这种连续出现的模式,效果就很一般——它会把正常数据和台阶数据各滤掉一半,角度曲线变平滑了,但等效分辨率并没有真正恢复。
后来我意识到,后处理只能掩盖症状,不能解决根因。ADC采样链条里的时序问题,修复成本可能只是几行代码,但如果不修,后处理模块再复杂也是白搭。这也是这次调试给我印象最深的地方:很多“玄学”一样的台阶、毛刺、偶发跳变,落到时序图上一看,都是很朴素的原因。芯片手册不是拿来查参数用的,是拿来解读信号行为用的,尤其是那些带BUSY、DRDY、VALID之类“完成标志”的接口,读懂它们的建立时间,工程问题至少能少一半。