我第一次在一张入门级FPGA板上把Remote ID的发射端和接收端同时跑起来的时候,最直观的感受是:无线调制解调这件事,看起来是算法问题,做起来全是时序、位宽和状态机问题。你在MATLAB里画出来的GFSK波形再漂亮,放进FPGA以后也要面对DDS相位累加器位宽选多大、高斯滤波器系数怎么量化、解调侧残余频偏怎么消除这类具体到不能再具体的工程问题。这篇内容就是围绕“Remote ID的调制解调FPGA实现”这个项目,把基于BLE 5.0物理层的GFSK调制解调链路从发送到接收完整拆开,从调制器设计、解调器结构、帧同步状态机到验证调试手段,逐段说明白。适合正在做无人机识别、无线数传或者想在FPGA上入门软件无线电基带的工程师参考,也适合那些卡在“仿真能通、上板不通”问题上的朋友对照排查。
1. Remote ID对调制解调方案的约束,比你想的更多
1.1 信号到底长什么样
Remote ID要解决的事情,本质上是在一定范围内周期性地广播飞行器的身份、位置、速度等信息。现在行业里主流的传输载体之一就是BLE 5.0,原因是它这个物理层本身就是为低速率、低功耗、中等距离广播设计的,而且几乎所有移动终端都具备接收能力,接收门槛很低。BLE 5.0的1M PHY物理层,核心参数并不多:符号速率1Msps,使用GFSK调制,BT值为0.5,调制指数标称0.5,对应的峰值频偏约250kHz。这个参数组合意味着,信号本身的频谱效率不算高,但对接收机的要求比较友好,即便在移动终端那种低成本射频前端下也能解调。
我在这里用“GFSK”而不是“GMSK”,是因为严格来说GMSK是GFSK的一种,特指调制指数为0.5、高斯滤波后的连续相位频移键控。GMSK的术语更多出现在GSM、LoRa等系统中,而BLE规范里明确写的是GFSK。两者在基带处理上非常类似,都是对基带比特先做高斯脉冲成形,再用成形后的信号控制载波瞬时频率。FPGA实现时,你完全可以借鉴GMSK那套经典的“高斯滤波器+DDS频控”结构,只是把频偏系数按照BLE的参数重新计算即可。
从数据链路看,一个典型的BLE广播包由前导码、访问地址、协议数据单元PDU和CRC组成。广播通道的访问地址固定为0x8E89BED6,PDU里再承载Remote ID的应用层数据。调制解调器解决的问题就是用物理层方式把这一串比特可靠地搬移到空中,并且在接收端从被噪声干扰的波形中恢复出原始比特。应用层的什么位置、什么高度、什么ID,其实跟基带解调没有直接关系,你只需要保证MAC层拿到的是完整无误的比特流。
1.2 为什么不在MCU或者DSP里做
有人会问,Remote ID的符号速率只有1Msps,一些高端MCU也能通过定时器加DMA的方式模拟GFSK生成,为什么要用FPGA?我的看法是:能“生成”和能“可靠收发”是两码事。MCU的缺点在于实时性不稳定,中断抖动和任务调度会导致符号边界出现微小的时间偏移,这个偏移在经过射频前端和空中信道后会被放大,接收端定时恢复会变得非常痛苦。DSP则更多受限于并行度,一个通道还好,如果以后要支持多通道接收、多频点同时扫描,DSP的负担就会直线上升。
FPGA的优势在于,调制解调的每一条数据通路都是独立的硬件逻辑,符号定时是时钟驱动的,不存在“任务被打断”的问题。发射机可以把数据包做成严格的时隙调度,接收机可以在同一个FPGA里跑多个解调通道,分别监听不同的广播频点。这对于Remote ID这种需要周期广播、并且可能同时接收多路信号的场景很合适。另外,FPGA内部的逻辑延迟是可预测的,这给基带延迟控制带来很大方便,调制器能在精确的时刻把前导码发送出去,接收机也能通过硬件计数器确认每一个比特到达的时间。
多提一句:FPGA实现调制解调,并不意味着要把整个软件无线电协议栈都放进去。以我的项目为例,MAC层和App层我仍然放在一个软核处理器里跑,FPGA只负责物理层的调制解调、比特同步和线路编解码。这种软硬划分既保留了FPGA对时序和并行处理的优势,又让上层逻辑可以用C语言快速迭代,整体开发效率高很多。
2. 调制侧:在FPGA里生成一张能过无线的GFSK波形
2.1 高斯滤波器的设计与量化
GFSK调制器在FPGA里的实现路径非常固定:原始比特先做NRZ映射,也就是比特1对应正电平、比特0对应负电平,然后通过一个高斯低通滤波器对电平序列进行成形,用滤波后的信号去控制DDS的瞬时频率。这里最关键的就是高斯滤波器。
高斯滤波器的冲激响应由BT值决定,BT值越小,信号带宽越窄,但码间串扰越大。BLE规定BT=0.5,这是一个比较折中的选择,既保证了频谱占用在2MHz通道内,又让接收端的差分检测不至于因为码间串扰太严重而失效。在FPGA里做高斯滤波器,我不会直接设计一个高阶FIR,而是根据发射机的升采样率预先计算好滤波系数,再加载到ROM表中。
比如系统内部升采样率为16,即每个符号周期内输出16个采样点,那么对称FIR滤波器所需的系数就是16乘以滤波器span。取span为3个符号周期,就得到48个滤波器系数。这48个系数可以离线用firpm或者gaussdesign计算,再量化成16位定点数存储到ROM里。为什么用16位?因为实测下来,10位系数量化在部分频偏条件下会带来约0.8dB的解调灵敏度损失,而12位以上就基本看不到差别了。综合考虑资源,16位是零风险的稳定选择。
滤波器的工作方式有两种。第一种是直接法:输入比特流按其速率进入一个较短的移位寄存器,每个符号周期计算一次完整的48阶FIR输出,然后以16倍符号速率输出。这种方式逻辑资源稍多一些,但结构直观。第二种是多相结构:把48个系数分成16个子滤波器,每个子滤波器只有3个系数,在符号速率时钟下完成计算,再把16个并行的结果按顺序读出。多相结构最省乘法器,适合资源紧张的芯片。我实际项目用的是多相结构,因为FPGA要同时跑调制和解调,DSP乘法器资源不够。
2.2 DDS调制器的主频率字
高斯滤波器输出的是一个带限的基带控制信号,接下来就进入DDS部分。DDS的经典结构是相位累加器加波形查找表。GFSK本质上是频率调制,所以不需要直接控制相位,而是把滤波后的信号映射为频率控制字,累加器不断累加,生成连续变化的相位,再通过查找表得到正弦和余弦波形。
这里频率控制字的计算值得仔细看。假设FPGA工作时钟是48MHz,DDS累加器位宽取32位,那么频率分辨率是48MHz除以2的32次方,大约0.011Hz,远好于我们需要的250kHz精度。BLE标称频偏250kHz,那么在48MHz时钟下对应的相位累加步进是:
[ \Delta\phi = \frac{250\mathrm{kHz}}{48\mathrm{MHz}} \times 2^{32} ]
算一下大概是22369621,也就是0x01555555附近。这个数很有特点,因为250kHz和48MHz之间存在一个友好的比例。当输入比特为1时,累加步进等于载波中心频率对应的步进加上正频偏步进;为0时减去。
实际实现中不需要每个采样点都调用乘法器。高斯滤波后的信号可以预先量化成一个小位宽的查找表索引,比如8位,而每种索引值对应的频率控制字增量都预先算好存入ROM。这样调制器最关键路径上只剩加法和累加器,时序很容易满足。相位累加器输出的高位作为正弦查找表的地址,通常取高12位作为波形表地址,因为12位相位分辨率能保证SFDR大约在-70dB以上,对2MHz带宽的信号足够。
我还做了另一个处理:在调制器输出端加了一级2倍插值半带滤波器,把内部数据率从16Msps提升到32Msps以后再送DAC。这样做有两个好处,一是DAC镜像抑制更好,二是后级的抗混叠模拟滤波器设计压力小很多。FPGA里做半带滤波器代价很低,只需要少量乘法器,但射频指标改善明显,值得加。
2.3 发射数据链路安排
调制器本身只是把比特流变成波形,但你不能把一个字节一个字节随便丢进去。GFSK是一个有记忆的调制方式,符号间隔之间是关联的,必须保证比特按照符号周期的节奏进入高斯滤波器。所以在调制器前端,一定要做一个发射状态机,负责把前导码、访问地址、PDU和CRC这几部分按顺序拼接成完整的比特流,并且以一个稳定的符号时钟逐bit送入调制器。
一个容易犯的错误是,把要发送的数据放在异步FIFO里之后,读取时没有严格对齐符号时钟。FIFO读侧如果由一个和系统时钟同源的时钟驱动,看上去没问题,但如果你用FIFO几乎空标志来触发发送,实际上会产生不确定的延迟。我后来把发送调度做得非常“死板”:一个符号计数器从0计到包长度,每个符号周期从固定的RAM地址取bit,取完就发,发完就停。这种方式的优点是,每一帧从第一个前导码bit开始到最后一个CRC bit结束,时间都是确定的,接收端做时隙统计和同步都非常方便。
3. 解调侧:ADC之后,IQ流变比特流的三道关
3.1 低中频还是零中频,先想清楚
接收机的射频前端方案会直接决定FPGA里解调器的整体架构,所以这个问题必须先定下来再动手。零中频方案,射频前端输出的是I/Q两路模拟基带信号,ADC直接采样I/Q,FPGA拿到的是以0Hz为中心的二相基带信号。这种方法实现起来直观,但对射频前端的镜像抑制要求很高,直流偏置问题也明显,GFSK信号本身是双边带调频,对直流偏置容忍度稍好,但依然需要做偏置校正。
低中频方案,射频前端把信号搬到一个几兆赫兹到几十兆赫兹的中频,ADC采样后,FPGA里通过数字混频器把中频搬回基带。低中频方案对直流偏置不敏感,对射频前端的镜像抑制要求也下降不少。FPGA实现时需要额外付出一个数字混频器和一个抗镜像滤波器的资源代价。我在项目里选的是低中频方案,因为这是一块通用SDR板卡,中频频率取10.7MHz,ADC采样率80Msps。
FPGA内部做的第一级数字混频,本质上就是乘上(e^{-j2\pi f_{IF} nT})。混合后的信号包含期望信号和2倍中频的镜像分量,后面再接一个抽取滤波器。设计抽取滤波器时我建议先做CIC滤波器把数据率从80Msps降到16Msps,CIC后接补偿FIR把带内平坦度修回来。为什么先CIC?因为80Msps下的FIR阶数如果太高,资源开销会非常吓人,CIC可以在零乘法器的情况下完成大倍数抽取,非常划算。CIC级联4级,微分延迟取1,在16Msps输出下能提供足够阻带抑制。
3.2 差分相位鉴别与频偏补偿
GFSK信号里的信息是调制在瞬时频率上的,也就是相位的导数。因此一个最直接的解调思路是差分检测:用当前采样点的I/Q值乘以前一个采样点的共轭,得到的复数相角就是相邻采样点之间的相位增量。在FPGA里,这个复数乘法的实部其实就是:
[ I[n]I[n-1] + Q[n]Q[n-1] ]
虚部是:
[ Q[n]I[n-1] - I[n]Q[n-1] ]
取虚部除以实部的反正切,就得到相位差。但在FPGA里用CORDIC做反正切,资源消耗不小,而且相位差很小,直接算反正切对噪声比较敏感。我实际项目里简化了一步:由于相邻采样点间隔很短,相位差远小于1弧度,此时相位差可以直接用虚部近似,省掉CORDIC。当然,在做判决之前,需要一个归一化操作来消除信号幅度的影响,否则AGC没收敛时虚部幅度会随着输入信号强度波动。
这里必须处理一个比想象中更麻烦的问题:频率误差。射频前端本振和发射机之间存在最大可达几十ppm的频率偏差,2.4GHz频段上30ppm就是72kHz频率误差。这个误差加到250kHz峰值频偏信号上,会导致差分检测结果整体抬升一个固定量,直接导致比特1和比特0的判决边界偏移。不消除的话,即使信噪比很高,误码率也会很高。
FPGA里做频偏估计的常用方法是,利用前导码已知的0101序列做累加平均。前导码的频偏模式是正负交替的,把所有正相位差加起来减去所有负相位差,得到2倍的频偏估计值。收完前导码后,把估计的频偏对应到差分检测判据里,就可以对后续payload做有效补偿。需要注意的是,这个估计只适用于慢变频偏,如果收端本振漂移比较快,则需要在payload中间周期性插入已知序列,或者采用判决反馈方式持续校准。对于Remote ID这种短数据包,前导码估计一次够用。
3.3 符号定时恢复
做完差分检测和频偏补偿,得到的是一串带有符号信息的波形,但要决定在每个符号周期的哪个点上去采样,这就是符号定时同步。如果我们只是做板级自环,发射机和接收机共用一个时钟,定时问题可以靠固定计数解决。可一旦接上真实的射频前端、经过空中信道,或者从一个独立的信号源收发,采样时钟和符号时钟之间虽然来自同一个标称频率,但微小偏差会逐符号累积,一定是需要动态调整的。
我用的是Gardner定时恢复算法。它的好处是,对GFSK这种有一定脉冲成形的信号依然有效,而且不依赖前导码,可以实现盲定时恢复。Gardner算法计算方式是这样的:每个符号周期有两个采样点,一个是最佳判决点(y[k]),另一个是两个判决点之间的中间点(y[k-1/2]),定时误差为:
[ e = (y[k] - y[k-1]) \cdot y[k-1/2] ]
这个误差信号会通过一个环路滤波器,转换为小数插值器的分数间隔更新。在FPGA里,我用了一个Farrow结构的立方插值器来完成分数延迟调整,插值器系数直接用定点数表示。GFSK信号因为经过高斯滤波,波形比NRZ圆滑,Gardner算法收敛稍慢,所以我把环路带宽设置得比较低,这样虽然锁定时间变长,但稳态抖动更小。符号速率1Msps,环路带宽设置在几百赫兹,实测几毫秒内能完成锁定。
数字配方的定点数也要注意。Gardner环路滤波器里的比例项和积分项系数非常小,如果直接用整数运算,很容易被截位吞掉。我把误差信号先左移若干位,环路滤波器全用32位定点,最后输出给插值器时再截断。这个细节决定了定时环在小频偏下能否长时间稳定。
4. 协议层:比特流怎么变成一包可用的Remote ID
4.1 前导检测与访问地址相关
解调器输出的是连续比特流,但解调器不知道数据包从哪个比特开始。所以接收端必须有前导检测机制。BLE的前导码是01010101或者10101010,这种交替模式在解调后的比特流中表现为周期性的特征,前导检测电路要做的工作就是对这个特征做实时相关。
前导检测的实现在FPGA里有几种办法。最简单的就是移位寄存器和相关器:保留最近的32个bit,与本地模板做匹配,超过阈值就认为检测到前导。相关器的阈值设定很关键,设得太低,噪声引起的误触发会非常高,后面状态机会被假前导带跑;设得太高,真实前导在信噪比较低时检测不到。我采用的方案是对前导检测输出加一个“连续确认”机制:第一次相关超过阈值后,不立刻进入解包状态,而是在下一个比特时再对照理论值做一次确认,两次一致才认定同步。这样虽然损失半个符号周期的响应时间,但误触发率显著降低。
访问地址相关比前导检测更严格。BLE广播通道的访问地址固定为0x8E89BED6,32比特。在解调器完成前导检测后,后续的32个比特会进入一个访问地址比较器,只有精确匹配才允许继续接收payload。这里用到了前面定时恢复的质量:如果符号定时抖动了半个周期,相关峰就会明显下降。实际调试中,访问地址匹配率是最能反映解调器质量的一个硬指标,匹配率上不去,几乎可以断定问题在定时恢复或者频偏补偿环节。
4.2 CRC校验与整体状态机
访问地址确认之后,后续数据进入payload缓存区,同时实时计算CRC。BLE使用的CRC24多项式为(x^{24}+x^{10}+x^9+x^6+x^4+x^3+x+1),FPGA实现就是一个24位线性反馈移位寄存器。每收一个bit就送入LFSR计算,等最后一个payload bit收完后,把收到的CRC字段与本地计算值比较。整个包的状态机可以定义为:
IDLE等待前导,PREAMBLE检测前导,SYNC核对访问地址,PAYLOAD收payload数据并计算CRC,CRC_CHECK比较CRC,最后回到IDLE等待下一包。状态跃迁必须考虑异常路径:比如在PAYLOAD阶段预设一个最大超时计数器,如果收到一半没有后续数据,超时强制回到IDLE,这样可以防止一个虚假前导把接收状态机卡死。
这个CRC状态机在FPGA里属于典型的时序逻辑,难度不大,但有一个细节常常踩坑:CRC计算的使能信号和有效比特对齐。如果计算使能和收数据使能之间差了一个周期,那么每一个bit都错位计算,最后CRC永远对不上。我调试时花费了大量时间排查这类“看似算法错误、实则是时序错位”的问题,后来统一用一个bit_valid信号贯穿数据通路,才从根上解决。
4.3 解调器输出到MAC层的时钟域处理
解调器和上层处理器通常运行在不同的时钟域。解调器输出也许是16MHz采样率,每个符号在固定位置提取出一bit,这bit要交给一个工作在其他频率的MCU总线去读取,中间必须做跨时钟域处理。很多人在这里直接用一个异步FIFO,理论上是正确的,但实际中经常因为FIFO读写指针的Gray码转换有问题导致偶发丢包。
更稳妥的做法是:接收侧不再用逐bit中断去骚扰CPU,而是把完整的一包数据(包括payload和CRC校验结果)放在一个双口RAM中,一口气写入,写完后产生一个短暂高电平脉冲通知CPU来读取。CPU读完后写一个清除位。这样跨时钟域交互被缩短到一个脉冲和一个状态位,race条件出现的概率大幅降低。我在Vivado里验证过,这个设计即使跑上连续一整晚,也不会出现丢包错包的情况。
5. 验证调试与资源优化:怎样才算真的“通了”
5.1 板级回环:先把数字底座打稳
接到射频、拉天线之前,我强烈建议先做板级回环。所谓板级回环就是把FPGA内部发射机的数字基带输出直接送到接收机的数字基带输入,或者通过DAC->ADC做一个模拟回环。这个测试能验证调制器和解调器之间的算法是否匹配、定时恢复能否收敛、帧同步状态机是否正确,把问题限制在数字逻辑范围内。
我做数字回环时,用了一个简单的信号注入方式:发射机输出的匹配滤波后波形,经过一个可调增益器和一个加法器注入高斯噪声,噪声由FPGA内部的LFSR生成。通过调节噪声方差,可以快速测量不同信噪比下的误码率曲线。这里有个容易忽视的点:注入噪声的带宽要和信号带宽一致,不能把噪声加在16Msps采样率下然后只做通带滤波,否则解调器看到的等效信噪比和你预期的完全不一样。
板级回环通过之后,再过渡到DAC->ADC回环。这时要关注的是通道增益、直流偏置、DAC和ADC的采样时钟是否同源。我踩过一个坑:发射和接收各自使用独立的晶振时,即便都是标称16MHz,实际频率偏差也有几个ppm,这个微小差异会让符号定时环路一直追,虽然Gardner能扛住,但误码率会比同源时钟高不少。建议回环测试用同源时钟验证算法正确性,再换独立时钟验证定时同步裕量。
5.2 上射频之前的检查清单
真正接上射频前端以后,问题就从数字域转向模拟域。我总结几个必查项:
第一,发射功率和频谱是否正常。用频谱仪看GFSK信号时,如果看到明显的离散谱线,多半是高斯滤波没有生效或者符号时钟抖动太大。好的GFSK信号频谱轮廓平滑,没有明显毛刺。
第二,接收AFC是否正确工作。搜索一个已知信号源,看解调后的频偏估计值是否稳定在真实偏差附近。如果估计值反复横跳,说明前导码相关窗口和数据bit没有对齐。
第三,前导相关阈值是否合理。通过日志记录误触发次数,如果误触发过多,适当调高阈值;如果丢包严重,就适当调低阈值。这两者之间需要根据现场环境找一个平衡点。
第四,天线位置和遮挡对多径的影响。Remote ID是低速率广播,多径会造成频率选择性衰落,在走廊或者室内测试时会出现偶发的深度衰落。FPGA解调器里如果加了一个简单的均衡器会比较稳妥,但如果短期没精力做均衡,可以在接收端用双天线分集接收,Altera和Xilinx的FPGA资源都足够支持两路解调。
5.3 资源优化与后续扩展
整个调制解调链路在主流中端FPGA上,比如Xilinx Artix-7或Intel Cyclone 10 GX,资源消耗大约是:DSP乘法器60到80个,BRAM 20到30个,逻辑单元约8K到12K。如果你有多个接收通道需求,资源会线性增加,这时候就需要优化。
一个很有效的优化点是把多通道解调分成两段:第一段是共享的DDC和CIC抽取,第二段才是每通道独立的差分检测和定时恢复。因为DDC和CIC处理的是整个目标频段,各通道的数据来自同一个宽带数字化结果,没必要每个通道都复制一份混频器和抽取滤波器。这个结构我在资源受限的板子上验证过,3通道接收的额外开销只相当于单通道的1.6倍左右。
另一个可以优化的点是波形查找表。如果要同时兼顾正弦和余弦输出,直接用两个48x16位查找表,BRAM开销不小。更好的做法是只存1/4周期正弦表,通过地址变换生成完整正弦和余弦。地址位宽取14位,输出16位,占用的BRAM约28Kb,比双完整表省一半。
最后提一个扩展思路:现在做的是单一GFSK解调,但Remote ID场景里还存在其他调制模式。FPGA的好处是可以把多种解调器做成模块池,通过配置寄存器选择当前的解调模式。我在项目里预留了一个QPSK解调器接口,未来切换时不用重新综合整个工程,只需要例化对应模块就可以了。
现在回到我的体会。每次有人问我Remote ID调制解调到底难不难,我总觉得单纯讨论算法难度是没意义的:GFSK调制解调原理课本上几页纸就写完了,但真正让一个项目变得复杂的地方,全在那些算法课本不讲的工程细节里。比如采样率选16M还是32M,决定滤波器的阶数和资源消耗;比如差分检测时候的频偏补偿精度,直接决定接收机能不能在低成本晶振下稳定工作;再比如前导检测阈值定多少,决定了误触发率和灵敏度之间如何取舍。这些细节一旦处理好,FPGA的调制解调链路其实非常可靠。如果这篇文章的某个细节能帮你在调试中少走一次弯路,那它就算没白写。