1. 基带和中频的分工,很多人一开始就没搞清
带过不少刚入行的师弟师妹,发现一个很普遍的误解:觉得基带就是低速数字信号,中频就是模拟电路里的事。这个认知放到十年前的模拟中频方案里还勉强说得过去,但在现在的FPGA实现体系里,早就不是这么回事了。基带和中频处理,很大一部分已经搬到了同一个芯片里,用同一个时钟域甚至跨时钟域的流水线串在一起。搞清楚这两个概念在数字域里的边界,是做任何通信物理层开发的第一步。
1.1 一条完整的数字接收链路里,FPGA到底站在哪
先把整条链路拉通了看。一个典型的通信接收机从天线下来,经过低噪声放大、模拟下混频,把射频信号搬到中频,然后经过抗混叠滤波和AGC(自动增益控制)之后送到ADC采样。到这里为止是模拟前端的事。而从ADC出来那一刻开始,信号就变成了数字序列,后面的事情基本都可以在FPGA里完成。
FPGA要干的活包括:数字下变频(DDC)把中频信号搬到基带、抽取滤波降低数据率、自动增益控制后面跟着的信号均衡、载波同步、符号同步、解映射、信道解码,甚至连帧同步和协议状态机都可以一起做掉。当中频数据率太高、后端CPU来不及处理的时候,FPGA还要承担数据降速和缓存的工作,让DSP或者ARM处理器拿到的已经是低速的基带数据流,而不是原始的ADC采样值。
我经常用一句话跟新人解释FPGA在通信里的角色:它是模拟前端和通用处理器之间的加工厂。模拟前端负责把物理世界的连续波变成离散数字,通用处理器负责跑复杂的协议栈和应用层,而FPGA负责中间那段实时性要求极高、数据吞吐量极大、算法结构又相对固定的信号处理。基带和中频的算法算法之所以适合放在FPGA里,核心原因有三个:一是高吞吐,ADC采样率动辄几十兆到几百兆,每个采样点都要在纳秒级别内处理完;二是低延迟,很多算法是流式的,一个采样点进来,几个时钟周期后就要出结果,CPU那种取指令、解码、执行的模式根本跟不上;三是并行性,信道均衡、滤波、FFT这些运算天然就是大量乘加并行执行的,FPGA的硬件逻辑正好一一对应。
1.2 为什么选在中频数字化,而不是直接射频采样
现在高速ADC越来越猛,也有人提射频直采的方案,就是ADC直接采样射频信号,省掉模拟中频那一堆混频器和滤波器。但落到工程实现上,中频数字化仍然是当前的主流选择,原因很现实。
第一是ADC的采样率和有效位数之间的平衡。射频直采意味着ADC要工作在GHz级别,而在这个采样率下能做到的ENOB(有效位数)往往只有10位左右,动态范围受限。中频数字化则可以把采样率控制在几十到几百兆,ADC可以做到14位甚至16位,动态范围大了很多。对于宽带信号接收,多几位的动态范围,在弱信号检测和强干扰抑制上的差距是压倒性的。
第二是模拟前端的实现难度。做射频直采,模拟链路上需要极陡峭的抗混叠滤波器来抑制带外干扰,这个滤波器在GHz频率上用模拟器件做出来难如登天。而中频数字化方案里,抗混叠滤波器只要做到几十到几百兆的带宽范围,用常见的SAW滤波器甚至LC滤波器就能搞定,成本低、一致性好、调试也方便。
第三是FPGA处理速度的现实约束。射频直采的数据进来之后,DDC和滤波依然要FPGA来做,这时FPGA内部逻辑要在GHz时钟下运转,目前多数FPGA也就是在高端器件上能局部做到,功耗和成本都相当可观。中频数字化之后,FPGA运行时钟通常在100MHz到300MHz之间,设计裕量充裕,时序收敛的难度小一个量级,开发风险和迭代速度都友好得多。
所以在我实际做过的项目里,除非是面向极宽带信号且对体积有苛刻要求的场景,一般都会选择中频数字化方案。这个思路跟选FPGA型号其实是同一个逻辑:用合理的技术水平解决当前的问题,而不是为了指标上的好看,把一个本来可以稳健落地的设计逼到极限状态。
2. 实现基带与中频算法的四个核心模块
聊完系统架构,进入正题。FPGA里的通信信号处理算法说多不多,说少不少,但高频复用的其实是那么几块:数字下变频、抽取滤波、同步环路、检波与解调。这四块就像搭积木,不同通信制式往上叠加不同的组合,就能拼出各种接收机。
2.1 数字下变频:NCO、混频器与滤波抽取的配合
数字下变频是把ADC采样的数字中频信号搬到基带走的第一步,也是整个FPGA信号处理链路里的第一个重活。它要做的事在数学上极其简单:把信号乘以一个本地产生的正弦波,把频谱从载频位置搬到零频附近,然后低通滤波去掉高频分量。
关键在于本地正弦波怎么产生。在FPGA里,最常用的做法是DDS(直接数字频率合成)加查找表。DDS的核心是一个相位累加器,每个时钟沿给累加器加一个频率控制字,累加器输出的高位作为地址去查一张正弦ROM表,查出来的值就是本地本振的实时样点。频率控制字每增加1,输出频率就增加 fclk / 2^N,N是累加器位宽,这样就能实现很高的频率分辨率。
// NCO相位累加器示例,32位累加器,输出正余弦查找表地址 reg [31:0] phase_acc; always @(posedge clk or negedge rst_n) begin if (!rst_n) phase_acc <= 32'b0; else phase_acc <= phase_acc + freq_word; end wire [11:0] sin_addr = phase_acc[31:20]; // 取高12位作为查表地址这段代码是最基础的NCO实现。实际工程里要注意几个细节:相位累加器位宽决定了频率分辨率,但查表只用了高12位或者10位,低位相当于相位抖动,这会在频谱上产生一定的杂散噪声,叫相位截断杂散。想要压低这个噪声,一是增加查表位宽,二是加抖动(dithering),在截断之前给累加器加一点伪随机扰动,把杂散打成底噪。这个技巧在通信系统设计里非常常用,信号频谱干净不干净,很多时候就差这么一点。
混频器在FPGA里就是乘法器。IQ两条支路分别用余弦和正弦本振去乘输入信号,得到I路和Q路。注意这里有一个数据位宽扩展的问题:两个12位信号相乘得到24位,后续如果直接接滤波器,数据位宽会一路膨胀。所以在实际设计里,每级运放之后都要做合适的截位或者舍入,把位宽控制在一个合理范围。截位方式也很讲究,简单截断会有直流偏置,最好用舍入加抖动的方式,或者用无偏舍入,否则后面信号频谱底部会翘起来一块。
2.2 抽取滤波器组:CIC加等纹波FIR的常规打法
DDC之后信号带宽还是宽,采样率还是高,直接拿去做基带处理浪费资源。这时候就需要抽取,把采样率降下来。但抽取有个大坑:根据奈奎斯特定理,抽取因子为M,就必须在做抽取之前先把信号带宽限制在fs/2M以内,否则高频分量会折叠到低频,这就是混叠。所以抽取必须配滤波器。
滤波器怎么设计,是新手最容易踩坑的地方。如果上来就用FIR滤波器做全部滤波和抽取,比如从100MHz采样的中频信号抽取10倍到10MHz,一个FIR滤波器要做10倍抽取前后的抗混叠滤波,阶数可能得几百上千阶。每个输出采样点要计算这么多滤波系数,FPGA的DSP48资源很快被打满。
常规做法是级联结构,CIC(级联积分梳状)滤波器打头阵,做整数倍抽取,然后是半带滤波器再降一级,最后用等纹波FIR做精细的频响整形。CIC滤波器结构极其简洁,不用乘法器,只用加法器,对于大抽取比、高带宽信号的场景非常高效。代价是幅频响应在通带内有倾斜,需要用后面的FIR补偿。
级联设计的时候有个经验我提了很多次:CIC的抽取因子不要一次拉满,除非你能容忍通带倾斜加阻带泄漏。通常CIC抽2到4倍,半带抽2倍,剩下的交给FIR,这样整体滤波器组的通带平坦度和资源占用能达到一个很好的平衡。每一级滤波器工作在不同采样率下,对应的输出位宽也不同,要分别估算动态范围,别指望一套位宽走到黑。
2.3 载波同步与符号同步:从锁相环到Gardner算法
信号搬回基带、抽到合理采样率之后,还有两个必须解决的问题:发射机的载波频率和相位跟接收机本地振荡器不完全一致,符号时钟跟采样时钟也不是天然同步的。这就是载波同步和符号同步。
载波同步在FPGA里最常见的结构是Costas环,专门用于抑制载波的BPSK/QPSK等调制方式。它的做法是:I路乘以Q路得到一个跟相位误差成正比的误差信号,这个误差信号通过环路滤波器(通常是一个比例加积分结构)去调整NCO的频率控制字,形成一个负反馈。环路滤波器里的比例系数控制响应速度,积分系数控制稳态误差,工程里就是反复调这两个系数,让环路的快慢和稳定性达到平衡。
符号同步常用的定时误差检测算法是Gardner算法,它有很好的特性:每符号只需要两个采样点,而且对载波相位不敏感,可以先做符号同步再做载波同步。Gardner算法的误差公式是 (y[n] - y[n-1]) * y[n-1/2],其中y[n]是当前符号采样,y[n-1]是上一个符号采样,y[n-1/2]是它们中间的那个采样。这个误差值经过环路滤波之后用来控制插值器,从异步的采样点流中恢复出最佳符号时刻的采样值。
这里我想多提一句:新人在写同步环路的时候,往往纠结于算法本身,而忽略了环路里的数据通路延迟。一个环路从误差计算到NCO更新,中间过了多少拍,会对环路的稳定性产生直接影响。如果不把环路延迟纳入设计考虑,环路增益一调大就振荡,调小了又锁得慢,有个时候直接锁不住。工程上的做法是先把仿真模型搭出来,把延迟参数替身进去做闭环仿真,稳定之后再转Verilog,可以省掉大量的上板调参时间。
2.4 中频检波:不同调制方式下的算法选择
中频检波这个词听着有点老派,但它指代的内容在现在的FPGA实现里依然很常见,就是从调幅或者带通信号中提取包络信息。相关的热搜词里也提到“中频检波有几种方法(如同步检波)”,这里把几种方法放在通信系统和FPGA实现背景下统一梳理一遍。
检波的本质是提取信号的幅度或相位信息。最古老的做法是包络检波,也就是非相干解调,适合调幅信号。在数字域里,包络检波就是求信号的瞬时幅度,可以用CORDIC算法计算I、Q两路的平方根,也可以用绝对值加低通的近似做法。工程上常用平方律检波:I^2加Q^2之后低通滤波,既避免了开方运算的复杂度,又天然实现了包络的提取。
同步检波则是相干解调,需要本地产生一个与载波同频同相的参考信号,混频后滤除高频分量,得到基带的原始调制信号。同步检波在FPGA里实现时,最关键的就是本地载波的同步,通常也是用一个环路来实现,这里是Costas环的又一个应用场景。同步检波的优点是抗噪声性能比非相干检波好,在低信噪比条件下优势明显,适合需要更高灵敏度的接收机场景。
两种方法在FPGA实现的资源消耗上也有很大不同。包络检波占用的乘法器少,逻辑简单,适合对性能要求不高的AM解调或者AGC检测;同步检波要做乘法、环路滤波、NCO,占用的DSP和逻辑资源明显多,而且环路的收敛时间和捕获范围都是设计重点。实际选型没有绝对的好坏,完全看调制方式和信噪比要求。我自己的习惯是:能用非相干解决的绝不上相干解调,因为同步环路是所有算法里最难调试的部分之一,多一个环路就多一个深坑。
3. 从Matlab模型到Verilog代码,中间差着定点化和时序
通信算法工程师的日常工作,通常先在Matlab或者Python里用浮点建一个模型,把功能验证通了,再转成FPGA实现。这一步转换过程,往往是项目进度真正的瓶颈所在。浮点模型跑得通,不代表硬件就能跑得通,差异主要在两个地方:定点化和时序收敛。
3.1 定点数格式:Q格式的选择和量化误差控制
浮点转定点是一个取舍过程。FPGA里做浮点运算是可能的,Vivado和Quartus都有浮点IP核,但代价是资源消耗显著增加,功耗上升,时序困难。基带和中频处理链路里的绝大部分算法,其实都可以用定点数在16位或者更少的位宽下实现,性能损失控制在零点几个dB以内。这就是定点化的价值所在。
定点数的基本表示法是Q格式,Qm.n表示m位整数位、n位小数位,总位宽W等于m加n加符号位。选格式时最怕的是溢出,其次是噪声太大。溢出会导致信号瞬间爆炸,产生完全不合理的输出;噪声太大则会让信噪比下降,通信性能不达标。
我的经验是先把每一级信号的动态范围预估一遍。比如ADC输出是14位,幅度范围在正负8192之间;混频之后乘上一个接近1的本振,动态范围不变;但后面滤波器可能有增益,比如CIC滤波器的增益等于抽取因子的N次方,N是CIC级数,这个值可能高达十几甚至几十,直接在无符号整形运算里毫无问题,但在定点小数里就要特别小心。每一级滤波器之后要重新评估位宽,不要怕位宽多,DSP48的输入位宽通常支持25乘18,多出来位宽不是大问题,真正的问题在截位。
截位策略上推荐使用舍入而不是截断。简单截断相当于向下取整,会产生最大一个LSB的直流偏置,这个偏置在窄带通信系统里是会降低接收灵敏度的。舍入方式在FPGA里实现也不复杂,加半个LSB再截断就行。如果要更好的性能,可以用无偏舍入。这些细节看起来不起眼,但对信号质量的累积影响非常明显,尤其是多级处理之后。
3.2 DSP48与查找表:资源分配里的取舍逻辑
FPGA里做乘法有两种主流方式:一种是用芯片里集成的DSP48硬核,一个DSP48可以在一个时钟周期内完成18位乘18位再加一个累加;另一种是用查找表(LUT)搭建乘法器,消耗大量逻辑资源,速度还慢。只要目标器件不是太低级,滤波器、混频器、环路里的乘法都应该用DSP48实现。
但是DSP48的资源是有限的,一个中等规模的Xilinx Kintex-7器件,DSP48的数量大概在几百到一千个左右。一个复数乘法需要4个实数乘法,一个1024点FFT的蝶形运算要消耗不少乘法器。所以做资源预算的时候,要精确计算每个算法模块占用多少DSP48,做到心中有数。
一个很常见的优化手段是时分复用。比如抽取滤波器组里的FIR滤波器,如果输入数据率是10MHz,而FPGA的时钟能跑到200MHz,那一个DSP48可以在20个时钟周期内完成20个乘法操作,也就是说一个物理乘法器可以虚拟成20个逻辑乘法器。用这种方式,一个大阶数的FIR滤波器可能只需要几个DSP48,就能支撑很高的数据吞吐率。这个思路在通信信号处理里很通用,因为通信信号本身是窄带的,数据率往往远低于FPGA的运行时钟,这给资源共享留出了巨大空间。
3.3 流水线划分与时序收敛:把组合逻辑拆开
FPGA的时序收敛和算法微架构设计是紧密相关的。一个纯粹的组合逻辑深度如果超过了几十级加法器和乘法器,在100MHz以上的时钟里就很难收敛。解决方式就是插流水线寄存器,把一个周期内超长的组合逻辑路径拆成两拍甚至数拍来完成。
我的习惯是先把算法流程画成数据流图,标出每一条关键路径的延迟,然后决定在哪里插入流水线寄存器。插入流水线会引入延迟,这对纯粹的流式处理没有影响,但会影响到反馈环路的结构。比如数字锁相环里的环路滤波器输出要反馈到NCO的频率控制字,如果这里多加了流水线延迟,会改变环路的相位裕度,必须把这一拍延迟在环路增益里提前补偿掉。
这里也要注意跨时钟域的设计。中频处理工作在高速时钟域,基带处理可能在降低了采样率之后切换到低速时钟域,两个时钟域之间的数据传递要用异步FIFO或者握手协议,不能直接拿一根信号线跨过去。否则上板之后数据偶尔错位,几天查不出原因,最后发现是跨时钟域没处理好,那真是最冤的一种bug。
4. 仿真全通过、上板就翻车——我的调试经验记录
如果你的项目组已经有完善的仿真验证流程,那恭喜你,能省掉很多精力。但即使仿真和验证做得再完备,FPGA上板调试依然是一个绕不开的环节。我写过的代码里,没有一个版本是一上板跑一次就全对的。这里记录几个典型的坑和排查思路,希望能帮你缩短调试时间。
4.1 没有加约束就上板的后果
第一次做中频处理板卡的时候,我犯过一个极其经典的错误:写完了代码,Vivado综合、实现都过了,看到时序报告是绿的,就高高兴兴烧到板子上。结果出来的信号频谱完全不对,带上一个诡异的周期毛刺,怎么调参数都没用。折腾了两天,最后想起来打开约束文件看一眼,发现里面没有任何时钟约束。
没有时钟约束意味着Vivado把时钟的频率当作默认值来处理,工具并不会知道你的主时钟其实跑在125MHz。这样综合器和布局布线器就没有一个明确的目标去优化关键路径,时钟树和逻辑布局都是按照默认策略生成的,结果当然是时序不可控。加上了正确的时钟约束之后,重新实现一遍,问题立刻消除了。从那以后,我养成一个习惯:写代码之前先写约束,哪怕只是最基本的一条create_clock,也比事后补要好。布局布线之后的时序报告,必须在时序约束完整的前提下看才有意义,没有约束的绿色报告都是假绿。
4.2 一处异步复位问题导致的中频数据乱跳
另一个让我印象深刻的问题,是复位电路引起的。当时的算法模块做得很完整,可是在复位信号上偷了个懒,直接用了一个异步复位。理论上只要复位时间足够宽,异步复位也是可用的,问题出在复位释放的那一刻:当复位信号撤掉时,如果它相对于时钟的撤销时刻刚好落在触发器建立保持时间的窗口里,不同的寄存器会看到不同的复位状态,一部分寄存器先结束复位开始工作,另一部分还在复位状态,系统状态就乱了。
这个问题的表现非常隐蔽:上电之后偶尔正常偶尔不正常,有时候跑了几十分钟才出一次错误,而且错误出现没有规律,用示波器抓也抓不住。最后是在代码里把所有异步复位全部改成同步释放的异步复位——也就是复位信号先经过两级触发器同步,再统一释放。这个经典结构保证了恢复时间,从此再没出现过复位导致的随机故障。
// 异步复位、同步释放的典型写法 reg [1:0] rst_sync; always @(posedge clk or negedge rst_n) begin if (!rst_n) rst_sync <= 2'b00; else rst_sync <= {rst_sync[0], 1'b1}; end wire rst_sys_n = rst_sync[1];这件事给我的教训是:复位电路是整个芯片里影响面最大的信号之一,却在设计时最容易被忽视。越是细节的地方,越要按正规做法来。上板调试的时候如果出现这种间歇性故障,优先检查复位和时钟相关的问题,比去翻算法要高效得多。
4.3 用ILA在线调试定位毛刺的思路
碰到信号有问题,第一反应当然是想办法观察到内部节点。ILA(集成逻辑分析仪)是Xilinx FPGA里最常用的调试手段,Quartus里对应的是SignalTap。把需要观察的信号引到ILA核里,设置好触发条件,就能在板子上抓取真实的波形。
ILA调试里我踩得最多的坑是触发条件设得太死或者太松。设得太死,一个时钟都没抓到;设得太松,抓了几百万采样点,有效信号早淹没在波形里。我的办法是分两步走:第一步先做自由运行抓取,不设触发条件,抓一段数据观察标准波形是什么样的,顺便确认信号位宽和数据格式有没有接错;第二步再设触发条件,比如抓同步信号下降沿、或者数据特征值,精确捕获问题窗口。
还有一个经验:抓信号时不要把ILA的信号太多,一只ILA核抓二三十个信号已经很多了,多了会把内部的布线资源挤占,导致本来就紧的时序更难收敛,新闻的故障反而更多。宁可多放几个ILA核,分布在不同的处理阶段,分段观察,这是工程上更实用的做法。
5. 给刚入门FPGA通信算法工程师的几个建议
做了这么多年FPGA通信算法开发,带过团队,也面试过不少人,看到太多新人在同样的地方打转。最后聊聊几个我觉得对新人最有价值的建议,不涉及具体技术,但对职业发展影响很大。
5.1 先把数字信号处理基础补齐
我见过有新人写了一个星期的Verilog,代码风格已经很老练了,但问他混频之后为什么要滤波,答不上来。这其实是方向性的大问题。FPGA开发工具链和语言本身是可以通过短时间训练掌握的,但数字信号处理和通信原理属于需要长期积累的基础知识,决定一个人能走多远。
具体来说,至少要掌握离散傅里叶变换和FFT的基本性质、FIR和IIR滤波器的设计和实现、多速率信号处理和抽取插值的混叠关系、常见调制方式的星座图和误码率性能、锁相环的基本原理和环路稳定性判定。这些知识不需要达到理论专业的深度,但必须能用来指导实践。比如你设计的滤波器通带纹波大了,你很知道这会给误码率带来多少恶化,需要怎么调整指标。这些能力不是单纯写代码能培养出来的。
5.2 从最小系统开始搭平台
无论学习还是做项目,我强烈建议从最小系统开始。所谓最小系统,就是一块FPGA开发板、一个ADC板卡、一根天线或者一台信号发生器,加一个最简单的算法,比如把ADC的数据用ILA抓到电脑上看波形。先跑通数据通路,再逐步往链路上加DDC、加滤波器、加同步环路。每加一个模块,都在板子上观察一次数据变化,确认无误再加下一个。
很多新人是相反的做法:一口气写好整个接收机,然后上板调试,结果数据出不来,一大堆错误同时出现,完全不知道从哪里开始排查。这种调试方式的困难程度,比模块化逐步验证要高出好几倍。而且从最小系统开始还有个好处:你对硬件平台的熟悉程度会高很多,什么信号在哪个管脚、ADC的配置寄存器初始值是什么、时钟芯片怎么配置,这些细节到了后面排查问题时全是关键信息。
5.3 写代码之前先写Matlab参考模型
我在团队里立了一条规矩:Verilog代码动工之前,必须先把定点数的Matlab模型跑通,并且留下定点化前后的性能对比数据。这条规矩一开始被认为多此一举,后来大家都尝到了甜头。原因很简单:在FPGA里排查一个算法bug的时间成本,通常是Matlab里排查同样bug的十倍以上。算法逻辑有没有问题、滤波器系数选得合不合理、环路增益该怎么调,这些在Matlab里一天能迭代很多次,在FPGA里一次上板调试可能就是一整天。
定点数模型还可以直接作为Verilog仿真的testbench参考。Vivado或者ModelSim里做RTL仿真时,把同样的输入激励给Matlab模型和Verilog模型,对比两者输出差多少,就能快速判断RTL的实现和预期算法模型是否一致。这个流程看起来多一点前期工作,实际上能把整个开发周期缩短三分之一以上,越复杂的系统越明显。
这几年接触的项目越多,越发觉得基带与中频的FPGA实现是一个特别能体现综合功底的领域。它需要通信理论、数字信号处理、硬件结构设计、工具链熟练度、调试经验等多方面能力叠加在一起,也正因为这样,这个方向的工作才始终充满挑战和成就感。希望这篇内容能帮你少走几步弯路。