ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FPGA BPSK上下变频器实战:Verilog串口控制与Matlab联合验证

FPGA BPSK上下变频器实战:Verilog串口控制与Matlab联合验证 1. 为什么用FPGA做BPSK变频从Matlab仿真到硬件加速的思维转变1.1 一次原型开发经历Matlab里跑通了一切上板却全乱套先说个我自己的经历。去年做通信系统课程设计第一阶段我在Matlab里搭了完整的BPSK收发链路随机比特、根升余弦成型滤波、上变频到中频10MHz、加噪声、下变频、匹配滤波、判决误码率仿真曲线跟教科书几乎完全重合。当时天真地以为把这段代码翻译成Verilog跑到FPGA板上最多一两个星期就能收工。结果呢上板第一天就是灾难。DAC输出的正弦波带着明显毛刺频谱仪上看根本不像10MHz的干净载波串口发过去的频率控制字板子像是没收到一样载波纹丝不动接收端解调出来的星座图散成一团完全看不出BPSK该有的两簇点。这期间我用Vivado的ILA抓内部信号才发现问题根本不在算法翻译——Matlab里天然以浮点方式工作的混频、滤波、参数传递到FPGA上全变成定点数、时钟节拍、跨时钟域同步这些硬件才有的烦恼。这篇文章就是冲着这类问题写的。标题里那套组合——FPGA实现BPSK信号上下变频器基于Xilinx主控、Verilog串口控制、结合Matlab验证——几乎是我这类做通信物理层原型验证的人每天都要面对的标准流程。它适合谁看适合正在做通信课程设计、软件无线电项目、或者想把Matlab里的算法模型搬到真实硬件上验证的工程师和学生。我会把从链路设计、串口控制、Xilinx IP核配置到Matlab联合仿真的完整思路讲清楚重点放在那些文档里不会写、但实际必踩的坑上。1.2 下变频到底在做什么频率搬移不是减一个频率很多第一次做数字变频的人会有一个直觉错误既然要把10MHz的信号搬到基带是不是在数字域里减去10MHz就行不是。数字变频的本质是时域相乘不是频谱相减。一个实信号 s(t) A·cos(2πf_c·t φ)如果乘上本地载波 cos(2πf_LO·t)用积化和差展开s(t)·cos(2πf_LO·t) A/2·cos[2π(f_c - f_LO)t φ] A/2·cos[2π(f_c f_LO)t φ]看出门道了吗乘法同时产生和频与差频两个分量。取差频那一路就是下变频取和频那一路就是上变频实际还要配合镜频抑制设计。所以数字变频器在FPGA里最核心的部件就两个一个是能产生任意频率本地载波的数控振荡器NCO通常用DDS IP核实现另一个是乘法器。BPSK信号因为只有0和π两种相位解调时本质上是判断收到信号的相位落在哪个半圆里——这个特性决定了它比QPSK、16QAM更容易在FPGA上实现很适合作为入门通信物理层设计的第一个完整项目。1.3 FPGA vs 单片机 vs DSP为什么选Xilinx方案BPSK变频器如果只要求低速、几兆符号率用STM32加模拟混频器也能做。但一旦符号率上升到几Msps以上或者希望一套硬件同时支持多种载波频率、多种速率配置单片机做数字混频就力不从心了——它不是不能乘而是乘不了那么多次。FPGA的优势在于并行乘法器和流水线结构DDS和FIR滤波器都能在一个时钟周期内完成计算瓶颈只在DAC/ADC的采样率。选Xilinx的理由更实际Vivado生态成熟DDS Compiler、FIR Compiler这些IP核都是免费的部分高版本需要许可证自带AXI4接口可以无缝接入串口配置总线ILA集成逻辑分析仪调试体验比竞争对手的同类工具顺手得多。Matlab那边还有HDL Coder可以直接生成IP核不过我用下来还是更喜欢手写Verilog原因后面会讲——全自动生成的代码遇到约束问题时你根本不知道它内部怎么处理的排查起来非常痛苦。2. 上下变频链路拆解NCO混频、FIR滤波与BPSK星座图的关系2.1 发射通路基带成型→数字上变频→DAC先看发射端整体框图可以按数据流分成四段基带数据源伪随机序列或者串口下发的用户比特经过0→1、1→-1的映射变成BPSK符号序列脉冲成型滤波用根升余弦RRC滤波器限制信号带宽避免相邻符号干扰。成型滤波在Matlab里是一行rcosdesign在FPGA里就是一个FIR IP核数字上变频成型后的基带信号乘上NCO产生的载波把频谱搬到中频或射频DAC输出将数字信号变成模拟信号再经射频前端放大、发射。这套链路里最容易让人忽略的地方是插值。基带符号率比如1Msps成型滤波输出1M采样点每秒但NCO的载波是10MHz要在10MHz的采样率下做混频的话符号率和DAC采样率必须匹配。工程上常见的做法是先内插到DAC采样率比如40MHz再做混频。内插可以用CIC滤波器加补偿滤波器实现也可以通过多相FIR直接完成。我在项目里用了FIR Compiler内置的插值模式系数由Matlab的firpm设计后导出省了不少事。2.2 接收通路ADC→数字下变频→抽取滤波→解调判决接收端是发射端的逆过程但有它自己的麻烦ADC采样中频信号如果前端没有模拟下变频到低中频的话数字下变频乘上NCO产生的本地载波把信号从载频搬回基带同时产生I/Q两路低通滤波滤掉混频产生的高频分量和带外噪声。这里如果用CIC抽取滤波器可以同时完成滤波和采样率降低两件事载波同步和符号定时同步这是BPSK接收最难的部分后面我会专门展开判决对基带信号做符号级采样按极性判决出0/1比特。接收链路有个实际工作中一定会遇到的细节混频之后的信号幅值只有原来的一半因为和频差频各分走一半能量而且滤波器会有群延迟导致符号判决时刻和理论值偏了几个时钟周期。如果不做符号定时同步直接在固定时刻采样误码率会明显上升。Matlab仿真里这个不明显因为浮点运算下你随手就能找到最优采样点FPGA里数据是流水线式的采样时刻必须用硬件逻辑固定下来这属于仿真不会教你的部分。2.3 链路参数算给你看10MHz载波、40MHz采样、1Msps符号率我把一组我实测用的参数列出来后面所有模块都围绕这组参数设计参数数值说明符号率 Rs1 MspsBPSK符号速率中频载波 fc10 MHzDAC/ADC中心频率DAC/ADC采样率 fs40 MHz满足带通采样与内插需求基带采样率40 Msps每符号40个采样点NCO频率字位宽32 bit频率分辨率 fs/2^32 ≈ 0.0093 Hz混频后数据位宽16 bitDAC输入位宽RRC滚降系数0.35经典取值关键在于NCO频率控制字的计算。DDS的核心理念是一个N位相位累加器每个时钟周期累加一个频率控制字FTWFrequency Tuning Word累加器溢出时的频率就是输出频率。公式是f_out FTW × f_clk / 2^N反过来要输出10MHz载波在40MHz时钟下、32位累加器时FTW 10e6 × 2^32 / 40e6 1073741824即0x40000000这个数在Verilog里可以直接用parameter定义。我见过不少人直接把FTW做成十进制浮点然后取整忘了32位字长的溢出范围——一个很小的浮点误差在累加器里长期运行后会累积成明显的相位误差所以一定要用整数精确计算并配合Matlab的fixed-point工具验证频率精度。3. Verilog串口控制模块频率字、相位字和使能信号的寄存器规划3.1 为什么用串口控制调试友好、无需JTAG反复下载我之前做过一个用拨码开关设频率的方案八个拨码只能给256档频率精度根本不够。后来又试过用Vivado的VIO虚拟IO在线改参数VIO确实方便但有个致命问题掉电就没了每次上电都要重新打开Vivado手动设置一遍。对实验室演示问题不大但如果你想把这套变频器做成一个独立硬件模块必须有一个脱离开发工具也能配置的通道。UART串口控制是性价比最高的方案一个USB转TTL模块加三根线TX、RX、GNDPC上任意一个串口助手就能发指令。整条链路的交互逻辑是PC通过串口发送指令帧FPGA内部UART接收模块解析指令将频率字、相位字、使能标志写入对应寄存器NCO和混频模块实时读取这些寄存器。整个过程不需要重新综合也不依赖JTAG调参效率高了一个量级。3.2 UART接收模块的时序与FIFO缓冲串口配置模块看起来简单实际上翻车点很多。我一开始图省事直接在系统时钟40MHz下用采样法判断起始位结果波特率漂移一点点就偶发性丢字节。后来老老实实写了标准的16倍过采样UART接收器先用一个更高时钟比如系统时钟对RX线采样检测到起始位后在每个bit的中间时刻采样确保远离信号跳变沿。// UART接收核心16倍过采样取中点采样 reg [3:0] clk_cnt; reg [7:0] rx_data; always (posedge clk) begin if (!rx_axis !receiving) begin receiving 1b1; clk_cnt 4d0; end else if (receiving) begin if (clk_cnt 4d8) begin // 每bit中间时刻采样 // 采样当前bitshifter移位 rx_shift {rx_axis, rx_shift[7:1]}; end clk_cnt (clk_cnt 4d15) ? 4d0 : clk_cnt 1b1; if (bit_idx 4d9) begin // 8数据位 1停止位收完 receiving 1b0; rx_data rx_shift; rx_valid 1b1; end end end接收完成的关键不是把数据放进寄存器就行而是要考虑背压问题。如果PC连发一串指令而NCO模块还在忙新到的指令就可能覆盖没来得及处理的旧指令。我的做法是加了一个8深度的FIFOUART接收端把字节写入FIFO指令解析状态机从FIFO里取数据。8深度对连续几十条配置指令已经足够了如果配置项更多建议做到16或32深度——FIFO深度这个参数我在第6章的调试记录里会专门讲一次实际踩坑。3.3 寄存器地址映射表串口协议我用的是最简单的地址数据两帧结构第一字节是寄存器地址第二字节是数据第三字节是命令结束符0xAA。解析状态机收到0xAA就把当前地址和数据写入对应寄存器。寄存器映射表如下地址名称位宽默认值说明0x01FTW_L320x40000000NCO频率字低32位0x02FTW_H320x00000000NCO频率字高32位本设计未用0x03PHASE_OFFSET160x0000相位偏移用于BPSK相位旋转0x04TX_EN11b1发射通道使能0x05RX_EN11b0接收通道使能0x06MODE22b0000:正常变频 01:自环测试 10:连续波FTW_L一个寄存器就够了吗上电时UART还没配置NCO默认输出10MHz没问题因为我在设计阶段直接把0x40000000写成了寄存器默认值。但注意如果以后想把载波做到几MHz精度连续可调32位FTW全都要用上这里只是演示我简化成低32位够用的场景。3.4 指令解析状态机设计状态机我用三段式写法状态转移、条件判断、输出寄存状态划分很简单IDLE、ADDR、DATA、END四个状态。串口FIFO有数据时从IDLE跳到ADDR收到地址后等数据数据到了等0xAA收到0xAA就写寄存器回到IDLE。实际测试中这套协议有两个体验细节值得分享第一命令结束符为什么不直接用回车换行因为很多串口助手发送时会自动追加\r\n导致协议解析不稳定。用固定的0xAA则完全可控终端里也可以手动输入十六进制。第二状态机一定要加超时保护。如果PC只发了半个帧就断线状态机会卡在DATA状态永远等不到0xAA后续所有指令都会被堵在FIFO里。我的做法是加一个计数器超过一定时间未收到新字节就强制跳回IDLE这个半帧保护逻辑在正式项目中非常实用。4. Xilinx工程实现细节AXI接口、IP核配置和时序收敛4.1 Vivado工程结构规划Vivado工程我建议按功能模块分目录组织而不是把所有文件堆在一个文件夹里。这个项目我用了如下结构rtl/top.v顶层模块例化所有子模块并连线rtl/uart_rx.v、rtl/uart_cmd_parser.v串口接收与指令解析rtl/reg_bank.v寄存器堆所有配置参数集中管理rtl/modulator.v发射通路成型滤波DDS混频rtl/demodulator.v接收通路DDS混频抽取滤波判决ip/存放Vivado IP核DDS Compiler、FIR Compiler、FIFO Generator顶层模块的连线是新手最容易乱的地方——其实核心就是三组信号串口信号rx_axis、tx_axis、时钟复位信号clk_40m、rst_n、以及射频相关数据信号dac_data、adc_data。时钟方案上用板载50MHz晶振通过MMCM产生40MHz主时钟这样DDS的频率字计算值和实际硬件完全一致。4.2 DDS IP核配置要点打开Vivado的DDS Compiler IP核界面里一堆参数我最在意的就几个Implementation选Phase Generator and SIN/COS需要同时输出正弦和余弦方便接收端做I/Q解调Output Width选16位匹配DAC位宽Phase Increment Width选32位频率分辨率足够高Noise Shaping默认None就好除非你非常在意SFDR否则不要打开它会显著增加资源占用。还有一个很多人忽略的关键参数Phase Increment Programmability。要选Programmable这样频率字才能通过配置接口实时修改。如果不选IP核运行时频率是固定的只能重新综合才能改频率——那就失去了串口控制的意义。DDS输出的余弦初始相位是零但实际系统中发射端和接收端的NCO相位不一定一致所以我在接收端NCO后面加了一个可配置的相位偏置模块PHASE_OFFSET寄存器就是干这个用的。调试接收链路时先在Matlab里算好最佳相位偏移再串口写入寄存器比在代码里改常量重新编译高效太多。4.3 FIR滤波器IP核参数选择系数可以用Matlab算好再导入发射端的成型滤波和接收端的低通滤波我都用了FIR Compiler IP核。先说系数来源用Matlab的firpm函数设计一个41阶低通滤波器通带截止频率设为符号率一半即0.5MHz阻带起始频率设为1.5MHz然后量化成16位定点系数导出成.coe文件在Vivado FIR Compiler的Coefficient File里直接导入。% Matlab生成FIR系数并导出 Fs 40e6; % 采样率 Fpass 0.5e6; % 通带 Fstop 1.5e6; % 阻带 b firpm(40, [0 Fpass/Fs*2 Fstop/Fs*2 1], [1 1 0 0]); qf fi(b, 1, 16, 15); % 16位定点量化1位符号位 fid fopen(lp_fir.coe, w); fprintf(fid, Radix16;\nCoefficient_Width16;\nCoefficient_Fraction15;\n); fprintf(fid, Coefficient_Data \n); for i 1:length(qf)-1 fprintf(fid, %04X,\n, hex(qf(i))); end fprintf(fid, %04X;\n, hex(qf(end))); fclose(fid);这里有个经验教训FIR Compiler的Coefficient Fraction必须和.coe文件里声明的小数位宽一致否则IP核综合出来滤波器频响完全不对。我最初导出的coe文件用的Fraction15但IP核界面默认是16结果滤波器通带完全变形星座图根本打不开。这个错误排查了整整一个晚上最后用Vivado自带的频率响应分析工具才发现系数解析错误。4.4 时序收敛跨时钟域处理与约束这个项目有两个典型的跨时钟域点处理不好时序就黄一是串口接收时钟和系统时钟的关系。UART有时候用独立的小晶振有时候直接从系统时钟分频。我用的是系统40MHz时钟下产生波特率所以不存在真正的跨时钟域问题——但这要求你计算波特率误差40MHz分频产生115200波特率误差只有0.014%完全没有问题。如果你用的是独立时钟源所有进入系统时钟域的UART信号都要打两拍同步。二是ADC数据的输入时钟。如果ADC和FPGA使用同源时钟我用了开发板自带的AD/DA模块两者共享40MHz直接采就行如果ADC独立提供时钟那ADC数据和时钟都要做跨时钟域处理。上板跑时序收敛失败最常出现在DDS和FIR的IP核时钟——Vivado默认给IP核加set_property约束但如果你在顶层忽略了其中一个IP的复位信号综合器会报严重的时序路径。我的建议是先跑check_timing和report_timing_summary优先解决时序违规最大的那一条如果违规集中在某个IP内部多半是参数配置中某个时钟使能位写错了不要贸然去加false_path掩盖问题。第二个建议是异步复位的同步释放全局复位rst_n进入后先用两级触发器同步到时钟域再分发到各模块这一步不做上电瞬间随机出现的复位毛刺会让你调试时怀疑人生。5. Matlab联合验证方案激励生成、数据回读与误码率评估5.1 用Matlab生成BPSK基带测试矢量Matlab在这个项目里的角色不只是设计滤波器系数它是整个验证流程的基准参照。我习惯先写在Matlab里完整跑通的理论模型再用它的输出作为FPGA仿真的激励标准。生成测试向量的代码很简单% 生成BPSK基带测试矢量导出十六进制文件供testbench使用 N 1000; % 符号数 bits randi([0 1], N, 1); symbols 2*bits - 1; % 0--1, 1-1 % 成型滤波与FPGA FIR系数一致 rrc rcosdesign(0.35, 6, 40, sqrt); % 滚降0.35每符号40采样 baseband upfirdn(symbols, rrc, 40); % 脉冲成型 % 上变频到10MHz t (0:length(baseband)-1) / 40e6; carrier cos(2*pi*10e6*t); tx_signal baseband .* carrier; % 导出16位定点数据 tx_fixed round(tx_signal / max(abs(tx_signal)) * 32767); fid fopen(tx_vec_hex.txt, w); fprintf(fid, %04X\n, tx_fixed double(tx_fixed 0) * 65536); fclose(fid);这段代码的导出格式要格外注意Matlab的round结果如果是负数需要加上65536转成十六进制补码表示否则testbench里的$readmemh读进来的数直接成了正数混频结果就全错了。我在这个转换上吃过亏当时查了半天信号为什么越来越大最后发现是负数被无符号化乘法器把它当成巨额正数处理了。5.2 Vivado仿真中如何组织testbenchtestbench我的思路很简单就是从上面这个文件读激励数据接连到发射通路输入端同时把FPGA内部的关键信号dump出来最后导出回Matlab做频谱和星座图分析。核心技巧是不要用$display打印海量数据那会拖垮仿真速度直接在合适的位置用$fwrite把I/Q两路解调输出的采样点写到文件即可integer fd; initial begin fd $fopen(demod_iq_out.txt, w); end always (posedge clk) begin if (rx_valid_sym) begin $fwrite(fd, %d %d\n, $signed(i_data), $signed(q_data)); end endtestbench的运行时钟周期建议用10ns100MHz配合DDS的40MHz主时钟整个仿真跑1000个符号约需仿真时间25ms——Vivado自带的xsim跑起来可能要几分钟属于可接受范围。如果你有ModelSim或Questa仿真速度会更快但对于小规模工程xsim足够了。5.3 实测回读数据与Matlab分析仿真跑通只是第一步真正验证FPGA行为正确的方法是把版级数据回读进Matlab通过ILA抓取DAC输出和ADC输入数据导出CSV再与Matlab仿真结果对比。% 读取ILA导出的CSV数据 data readmatrix(ila_data.csv); adc_data data(:,2); % ADC采样值 % 画时域波形 figure; plot(adc_data(1:2000)); title(FPGA版级ADC采集数据 时域波形); % 画频谱 fs 40e6; Nfft 4096; f linspace(-fs/2, fs/2, Nfft); spec fftshift(fft(adc_data, Nfft)); plot(f, 20*log10(abs(spec)/Nfft)); grid on; % 解调后画星座图需要和FPGA解调数据格式对齐 iq_data readmatrix(demod_iq_out.txt); scatter(iq_data(:,1), iq_data(:,2), 5, filled); axis equal; grid on;这套流程的价值在于它是连接Matlab算法模型和FPGA硬件实现的桥梁。当我看到版级频谱和Matlab频谱主瓣位置一致、星座图两簇点清晰可分时我才有信心说这个变频器的基本链路是通的。误差控制在工程上是不可避免的但你要能分清可接受的量化误差和压根是逻辑错误的区别——后者的典型表现是频谱上出现不存在的镜像分量、星座图出现四簇点而非两簇点。6. 实测问题与调试心得从仿真通过到板卡跑通的差异6.1 问题一DAC输出频谱塌陷罪魁祸首是阻抗匹配上板第一次看频谱10MHz主峰是出来了但整个底噪抬高了约20dB波形有明显振铃。查了一圈发现是我用的板卡DAC输出没有做阻抗匹配——差分输出直接接了个高阻探头反射信号叠加在主波形上相当于给信号叠了一条重影。解决办法说起来很简单在DAC差分输出到SMA连接器之间做了50欧姆到地的终端匹配电阻并且用差分转单端的巴伦结构。很多开发板的原理图其实已经做了这部分但如果你用的是自己画的子板一定要确认这一步。还有一个容易踩的坑示波器探头要用10x档而不是1x档1x档的容性负载会把几十MHz的载波信号直接衰减掉一大截。6.2 问题二串口丢命令FIFO深度是隐性杀手我前面提到FIFO深度这次实测终于碰到了极限情况。用串口助手连续下发16条配置指令正常调试时谁会只发1条结果有两条没有生效。用ILA抓UART接收端发现数据确实进来了解析状态机也确实产生了写使能但写入寄存器的时候被新来的指令覆盖了——本质是寄存器的写操作是先读后写还是直接写的问题而FIFO深度8在连续高速下发时不够用。我把FIFO加深到16同时给每条指令回了一个ACK字节PC端每收到ACK才发下一条。这样一来虽然慢了一点点但每条指令都能确认执行结果调试可靠性大幅提升。ACK字节也解决了指令到底收没收到的争论——不然你永远在怀疑是协议错了还是逻辑错了。6.3 问题三解调星座图旋转载波同步不做是不行的这是我这次项目里学到的最重要一课。自环测试时发射输出直接连到接收输入没有模拟信道星座图完美但一旦信号走真实线缆和射频前端接收端解调出来的星座图开始缓慢旋转。根本原因很简单发射端和接收端的NCO虽然用同一个标称频率值但两片晶振存在微小频差导致接收端本地载波相位在持续漂移。教科书会告诉你这叫载波同步问题标准解法是Costas环或者平方环。我一开始天真地想频率那么接近应该没关系吧实测发现相位漂移速度大概是0.1度每毫秒看起来很小但一个符号周期40us内就积累了0.004度等等——重新算如果频差是1ppm10MHz载波就差了10Hz每毫秒相位漂移3.6度BPSK判决容限是正负90度大约25毫秒之后误码就会爆发。因此任何实用化的BPSK接收机都必须做载波同步。我在FPGA里实现了一个简化版的判决辅助Costas环BPSK信号本身有能量集中在载频两侧的谱线特性可以通过硬判决误差信号驱动环路滤波器产生频率校正字反馈到NCO的相位累加器。这个环路在Matlab里用浮点模型验证通过后再转为定点Verilog环路带宽设为10kHz——太宽会引入噪声太窄会跟不上频偏这个折衷调节花了我不少时间。6.4 调试工具链建议ILA逻辑分析仪的正确用法最后分享一点调试工具的使用心得。ILA集成逻辑分析仪几乎是FPGA实物调试的唯一利器但用起来有讲究触发条件一定要设对。调试串口指令时我用rx_valid上升沿触发一帧指令进来就抓一整段波形。如果你触发条件设置太宽ILA存满以后全是无关波形分析效率极低采样深度按需设置。ILA默认可能设到16384或更大但抓一次数据要占大量BRAM还会拖慢布局布线。调试串口时256深度足够抓解调星座图时才需要大深度把内部信号按方便观察的方式再拉一组出来。比如观察环路滤波器输出时直接看寄存器值很难理解可以同时拉出滤波器的输入和输出两个信号在波形里对比着看收敛过程。我现在的标准调试流程是先仿真验证逻辑再上板用ILA抓关键节点信号最后回Matlab做定量分析。三者缺一不可——仿真验证不了真实晶振频偏ILA给不了误码率曲线Matlab分析不了板级波形的毛刺来源。把这套流程梳理通之后这个BPSK上下变频器才算真正能拿得出手。最后再分享一个我个人的习惯每个调试阶段开始前先在笔记本上写清楚当前现象、怀疑对象、验证方法三行话。听起来很像是项目管理的要求但在跟时钟和噪声搏斗两小时之后你会庆幸自己留下了决策记录——不然你根本想不起来自己上一轮改的是滤波器系数还是环路带宽又要从头重新试一遍了。
返回列表