简介:适用于BladeRF平台的16-APSK DVB-S2发射器完整工程源码,面向软件定义无线电(SDR)开发者、FPGA设计工程师及卫星通信方向学习者。项目以Verilog/VHDL实现DVB-S2标准中的16-APSK调制链路,包含与BladeRF硬件交互的控制逻辑,可帮助用户理解高阶调制算法并落地到实际射频收发实验。资源包共741个文件,约5.32MB,主要包含VHDL源码(114个vhd)、C语言驱动与嵌入式代码(90个c、69个h)、Verilog及SystemVerilog文件(61个v、31个sv),以及Quartus工程配置、仿真脚本和文档等,覆盖从编译、仿真到上板运行的完整流程。目前已有90人学习。压缩包内含工程主目录bladerf-dvbs2-master,解压后可按文档搭建工具链生成比特流并加载到BladeRF设备。对于想掌握DVB-S2标准、研究16-APSK调制实现或基于BladeRF做SDR开发的读者,这是一份难得的完整参考源码。
1. 一套现成的 16-APSK DVB-S2 发射器源码,值得拆开看的点在哪
乍看 DVB-S2 发射器,第一反应是去 GNU Radio 里找 dvbs2 模块,调几个参数就能出波形。但卫星链路上的 16-APSK 是嵌套环星座,涉及 LDPC 编码、物理层帧头、加扰和成形滤波,要在通用处理器上实时跑 20 Msps 以上的发射链路,资源占用和调度抖动都会非常难看。这个压缩包给的是另一种思路:把调制链搬进 FPGA。工程在 BladeRF 上实现 16-APSK DVB-S2 发射器,源码同时涉及 Verilog 和 VHDL,还保留了 NIOS II 软核控制射频前端的完整工程痕迹,适合手里有 BladeRF 的 SDR 工程师,也适合正在做 DVB-S2 发射端 FPGA 设计的人当参考骨架。解包之后,重点不是找到某个能跑的比特流,而是看懂文件之间的分工、约束文件的坑,以及前端射频芯片和 FPGA 的数据通路是怎么对齐的。
2. DVB-S2 物理层与 16-APSK 星座映射的工程取舍
2.1 为什么 16-APSK 比 8PSK 和 32APSK 更难“直接照抄”
DVB-S2 的调制方式不是随便选个星座图就能用。标准里的 16-APSK 确切地说是一个 4+12 APSK,即内环 4 个点、外环 12 个点,内环和外环的半径比值随编码率变化,不是固定比例。这与普通 QAM 的最大区别是:QAM 星座点的判决门限全是直线,而 APSK 的判决边界一部分是圆弧、一部分是径向线。FPGA 里实现时,如果按直角坐标直接量化,误差向量幅度对环半径比会非常敏感,量化步长稍微大一点,外环 12 个点之间的相位裕度就不够用了。
工程实现里最值得看的不是 QPSK 那种查表即可的映射,而是两件事:16-APSK 的比特到符号映射表,以及映射之前比特到幅度和相位的重新排列。DVB-S2 是在 FECFRAME 的比特序列上做串并转换,每次取 4 个比特构成一个符号。标准规定外环相位偏置为 π/12,内环则为 π/4,这个细节很容易被忽略。如果直接把外环第一个点放在 0 度,得到的是一个被旋转过的星座,接收端解调时所有符号都会判错。
提示:编码率 2/3 和 9/10 使用的环半径比相差近 20%,修改工程参数时,必须同时改发射端 ROM 系数和接收端解映射表,只改一侧会导致判决边界完全错位。
2.2 编码率对应的环半径比和相位偏置参数表
在写 RTL 之前,先把 DVB-S2 标准里的 16-APSK 参数表拉出来看。下面的参数来自 ETSI EN 302 307,工程里要做的是把这些值换算成归一化 IQ 坐标:
| 编码率 | 环半径比 γ | 内环点数 | 外环点数 | 外环相位偏置 |
|---|---|---|---|---|
| 2/3 | 3.15 | 4 | 12 | π/12 |
| 3/4 | 2.85 | 4 | 12 | π/12 |
| 4/5 | 2.75 | 4 | 12 | π/12 |
| 5/6 | 2.70 | 4 | 12 | π/12 |
| 8/9 | 2.60 | 4 | 12 | π/12 |
| 9/10 | 2.57 | 4 | 12 | π/12 |
环比率只是相对值。实际发送时为了不让发射平均功率随调制方式变化,需要把星座图按能量归一化。设外环半径为 R_o,内环半径 R_i = R_o / γ,再按 4·R_i² + 12·R_o² = 16 的关系缩放。R_i 和 R_o 在硬件里只能以定点数表示,工程里一般取 8 到 12 bit 带符号数,量化步长直接决定 EVM 上限。
有经验的 FPGA 工程师会直接把归一化好的 I/Q 值预生成 ROM 初始化文件,放进 block RAM,而不是在硬件里用除法器现场计算。BladeRF 的 FPGA 资源有限,16 个符号 × 16 bit 的 ROM 就够用。想自己改参数的话,用脚本算好再转定点,比在 RTL 里做除法靠谱得多。
2.3 用 Python 生成归一化 16-APSK 星座点
我自己动手的第一步是把上面的表格变成一排定点 I/Q 数据,再去对照 RTL 里的 case 语句。下面是生成 16-APSK 星座点并做能量归一化的参考实现:
import numpy as np def dvbs2_16apsk(code_rate, bits_iq=12): # 编码率对应的环半径比,来自 DVB-S2 标准 gamma = {"2/3": 3.15, "3/4": 2.85, "4/5": 2.75, "5/6": 2.70, "8/9": 2.60, "9/10": 2.57}[code_rate] inner_n = 4 outer_n = 12 # 内环起始相位 pi/4,外环起始相位 pi/12 inner_ph = np.pi / 4 + np.arange(inner_n) * 2 * np.pi / inner_n outer_ph = np.pi / 12 + np.arange(outer_n) * 2 * np.pi / outer_n R_o = 1.0 R_i = R_o / gamma # 平均功率归一化:让 16 个点总能量等于 16 avg_power = (inner_n * R_i**2 + outer_n * R_o**2) / (inner_n + outer_n) scale = np.sqrt(1.0 / avg_power) pts = [] for idx in range(inner_n): pts.append((scale * R_i * np.cos(inner_ph[idx]), scale * R_i * np.sin(inner_ph[idx]), idx)) for idx in range(outer_n): pts.append((scale * R_o * np.cos(outer_ph[idx]), scale * R_o * np.sin(outer_ph[idx]), inner_n + idx)) max_val = max(abs(p[0]) for p in pts) iq_scale = (2**(bits_iq - 1) - 1) / max_val print(f"// 16-APSK {code_rate}, {bits_iq}-bit signed I/Q") for i, q, sym in pts: i_int = int(round(i * iq_scale)) q_int = int(round(q * iq_scale)) print(f"iq_rom[{sym:2d}] = 16'sd{i_int:5d}; " f"iq_rom[{sym + 16:2d}] = 16'sd{q_int:5d};")脚本里,inner_ph和outer_ph决定星座点的起始相位。内环按 π/4 起转,外环按 π/12 起转,这一点与 DVB-S2 的星座图定义一致。scale做能量归一化,让不同编码率拥有相同的平均发射功率。最后打印的iq_rom可以直接复制进 Verilog 的 ROM 初始化块,I、Q 各自占 16 个地址,映射器查表时用sym_in取 I,用sym_in + 16取 Q。
2.4 为什么说原工程的模块划分是合理的
原本看到libhal_bsp.a、nios_system.bsf这些文件,容易误以为发射器的调制全靠软件。实际上 NIOS II 只是配置 LMS6002D 的寄存器、设置发射频率和增益、管理数据包元数据,不会参与逐符号的 16-APSK 调制。把调制放到 FPGA 里的直接收益是确定性:FPGA 侧时钟由晶振直接驱动,不存在操作系统调度抖动。
USB 3.0 通路在软件侧有缓冲,但缓冲溢出在 GNU Radio 方案里会造成符号中断;FPGA 内的tx_meta_fifo以硬件 FIFO 吸收这种突发,射频输出不会出现间隙。这个架构划分是 DVB-S2 发射器能长期稳定工作的前提。
3. 源码结构、文件职责与 bitstream 编译流程
3.1 压缩包里各文件到底承担什么角色
解压后扫一眼文件列表,能看出这不是单一厂商的干净工程包,而是带移植痕迹的完整工程。先把文件归类,再决定从哪里下手:
| 文件 | 语言/格式 | 职责与判读 |
|---|---|---|
bladerf-hosted.vhd.bak | VHDL | BladeRF hosted 模式顶层,包含 USB 接口和 FX3 握手的实体;.bak 表示改动前的备份 |
bladerf.sdc.bak | SDC 时序约束 | 定义 DAC/ADC 时钟和 FPGA 与 LMS6002D 之间的 I/Q 接口约束 |
nios_system.bsf | Quartus 符号 | NIOS II 软核系统与外设连接的框图,把射频前端控制寄存器映射给软件 |
fir_filter.bsf | 模块符号 | 成形滤波器,对应 DVB-S2 的基带脉冲整形 |
tx_meta_fifo.bsf | 模块符号 | TX 元数据 FIFO,隔离 host 与符号率时钟域 |
settings.bsp | NIOS II BSP 配置 | 定义 NIOS II 外设驱动、堆栈和中断参数 |
libhal_bsp.a | 预编译库 | NIOS II HAL 感知库,配合settings.bsp使用 |
pkt_legacy.c | C 源码 | 旧版元数据包格式的兼容处理 |
alt_log_printf.c | C 源码 | 日志输出重定向到 JTAG UART |
devices.c | C 源码 | 列出 NIOS II 系统中各外设的基地址和中断号 |
.bsf是 Quartus 的块符号文件,在顶层原理图里代表一个模块。用文本编辑器打开能看到WIRE和PORT,它本身不包含逻辑,只表达连接关系。真正重建 bitstream 时,.bsf对应的底层 HDL 文件必须在工程里,否则 Quartus 会报找不到符号。看源码时我习惯直接用 VSCode 打开.v和.vhd,配合 Verilog-HDL 插件做语法检查,比在 Quartus 自带编辑器里翻页快很多。
3.2 用 Quartus Prime 命令行完成编译
建议先不开图形界面,直接用命令行把流程跑通,换机器也能复现。以下命令以 Quartus Prime 标准版为例:
# 假设解压后的工程目录名是 bladerf-dvbs2-master cd bladerf-dvbs2-master # 1) 分析与综合 quartus_map --read_settings_files=on --write_settings_files=off bladerf_dvbs2_top # 2) 布局布线 quartus_fit bladerf_dvbs2_top # 3) 生成 bitstream quartus_asm bladerf_dvbs2_top # 4) 时序分析,验证 bladerf.sdc 里的约束有没有满足 quartus_sta bladerf_dvbs2_top四个命令对应完整 FPGA 流程。quartus_map里的--read_settings_files=on让工具读取已有工程配置文件;如果目录里缺.qsf,需要先打开 Quartus 新建工程,再把源码和.sdc加进来。quartus_fit最耗时,布线后时序不满足时,报告会写在bladerf_dvbs2_top.sta.rpt里,重点看 reg-to-reg 路径的 slack。
quartus_sta这步最容易暴露问题。很多网上工程里的.sdc时钟名和顶层端口对不上,报Can't find clock。这里尤其注意bladerf.sdc.bak只是备份,后缀是.bak不代表能用。遇到这种情况,先用get_ports查顶层端口名,再把create_clock的 target 改正确,否则时序约束等于没写。
3.3 移植到 Xilinx 平台时要改哪些东西
如果你手上没有 BladeRF,或者只有 Vivado 许可证,这个工程不能原样编进 Xilinx 器件。需要改的是四类文件:约束、时钟原语、NIOS II 替代逻辑、FIFO 的 IP 例化方式。
约束方面,Xilinx 用 XDC 而不是 SDC,基本语法兼容,主要把get_ports {adc_clk}改成get_ports adc_clk,再删掉 Altera 特有的set_false_path写法。时钟原语上,Altera 的altpll要换成MMCME2_BASE,参数和复位时序都不一样。
NIOS II 在这里承担的任务不重,只是配置 SPI 寄存器。如果不想在 Xilinx 里再造一个 MicroBlaze,可以用一个简单的状态机把devices.c里的配置序列搬过来,再把寄存器地址映射改成 AXI-Lite 地址,省掉软核的启动时间。
3.4 加载到 BladeRF 的两种方式
拿到.sof之后,加载命令有两种:
# 方式一:下载到 FPGA 配置 SRAM,重启后失效 bladeRF-cli -l bladerf_dvbs2_top.sof # 方式二:烧写到 SPI flash,掉电保存 bladeRF-cli -f bladerf_dvbs2_top.rbf -x方式一适合调试,重启 BladeRF 后恢复出厂配置;方式二适合现场部署。烧写 flash 前需要先把.sof转成.rbf,用quartus_cpf -c bladerf_dvbs2_top.sof bladerf_dvbs2_top.rbf即可。转换后文件比.sof小很多,烧写时间也更短。
4. 调制器与 TX 数据通路的 RTL 实现细节
4.1 数据通路:host 包如何变成 I/Q 符号
BladeRF hosted 模式下,host 通过 FX3 的 USB 3.0 把基带数据包送到 FPGA,FPGA 侧入口就是tx_meta_fifo。数据包头带有元数据,数据体是待发射比特流。必须加 FIFO 的原因是 USB 传输有突发性:host 可能一次写入几千字节然后短暂停顿,而调制器必须按固定符号率取数,两者速率不匹配,要用异步 FIFO 把读时钟域和写时钟域隔开。
FIFO 之后是加扰器和 FEC 编码器。压缩包源码中 FEC 对应 DVB-S2 的 BCH 外码和 LDPC 内码。LDPC 在 FPGA 里通常实现为部分并行结构,完整并行会消耗太多 LUT。这部分如果不需要改,建议当成黑盒。重点注意 16-APSK 映射器前后的时序对齐,因为 LDPC 输出和调制输入之间有流水线延迟,没对齐会出现符号错位。
4.2 可综合的 16-APSK 映射器模块
下面是一个可直接综合的 16-APSK 映射模块,用 case 语句或 ROM 查表完成比特到 I/Q 转换。数值来自 2.3 节脚本对 3/4 编码率的计算结果:
module apsk16_mapper #( parameter IQ_WIDTH = 16 )( input wire clk, input wire rst_n, input wire sym_valid, input wire [3:0] sym_in, output reg [IQ_WIDTH-1:0] i_out, output reg [IQ_WIDTH-1:0] q_out, output reg sym_valid_out ); // 16-APSK 4+12,按符号索引存放归一化定点 IQ // 数据来自 16-APSK 3/4 编码率生成脚本,12bit 定点扩展到 16bit reg [IQ_WIDTH-1:0] iq_rom [0:31]; initial begin // 内环 4 点,相位偏置 pi/4,半径 650/2047 iq_rom[0] = 16'sd461; iq_rom[16] = 16'sd461; // 内环点0 iq_rom[1] = -16'sd461; iq_rom[17] = 16'sd461; // 内环点1 iq_rom[2] = -16'sd461; iq_rom[18] = -16'sd461; // 内环点2 iq_rom[3] = 16'sd461; iq_rom[19] = -16'sd461; // 内环点3 // 外环 12 点,相位偏置 pi/12,半径 2047/2047 iq_rom[4] = 16'sd1977; iq_rom[20] = 16'sd530; // 外环点0 iq_rom[5] = 16'sd1448; iq_rom[21] = 16'sd1448; // 外环点1 iq_rom[6] = 16'sd530; iq_rom[22] = 16'sd1977; // 外环点2 // ... 其余外环点由脚本生成 end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sym_valid_out <= 1'b0; i_out <= {IQ_WIDTH{1'b0}}; q_out <= {IQ_WIDTH{1'b0}}; end else begin // 延迟一拍输出,和后续成形滤波器对齐 i_out <= iq_rom[sym_in]; q_out <= iq_rom[sym_in + 16]; sym_valid_out <= sym_valid; end end endmodule实现有两个关键点。第一,把 I 和 Q 放进同一个 ROM,用sym_in和sym_in + 16作为两个地址,综合时 Quartus 或 Vivado 能推断为 block RAM,而不是一堆 LUT 选择器。第二,输出打了一拍,让sym_valid_out和 IQ 数据在流水线上保持同步,后面接成形滤波器时不会错位。
外环剩余点的系数建议直接用脚本生成,不要手动填。手动填 12 个外环点的最大问题是相位偏置容易算错,尤其 π/12 换算成带符号定点数后,一旦符号位处理错,星座会整体旋转 15 度,接收端很难纠正这种固定旋转。
4.3 成形滤波器与上变频接口的时序
调制器输出的是符号速率的 IQ 采样点,BladeRF 的 DAC 工作频率更高,中间必须插入成形滤波器。工程里的fir_filter.bsf承担的就是这个角色。DVB-S2 推荐滚降系数为 0.20、0.25 或 0.35,默认通常取 0.20。不同滚降系数对应的占用带宽和符号率约束如下:
| 滚降系数 | 20 MHz 采样下最大符号率 | 占用带宽 | 建议 DAC 回退 |
|---|---|---|---|
| 0.20 | 16.67 MSps | 20 MHz | 7-8 dB |
| 0.25 | 16.00 MSps | 20 MHz | 7-8 dB |
| 0.35 | 14.81 MSps | 20 MHz | 6-7 dB |
成形滤波器在实现上有两种做法:多相 FIR 或查找表插值。多相 FIR 的系数可以在软件里按滚降系数重新计算,工程里用的是 FIR 结构,系数放在 ROM。符号率变化时,NIOS II 软件更新系数,但更新期间调制器必须停止,否则输出符号率会不连续。
接入 DAC 之前还要处理增益和直流偏置。BladeRF 的 LMS6002D 在 16-APSK 这种高峰均比波形下必须留足够回退。如果 DAC 满刻度对应 -10 dBm,IQ 幅度建议设置在满刻度的 -6 dB 到 -8 dB,防止 7 dB 以上峰均比造成削波。工程里没有现成寄存器调这个,需要在应用层调整发射增益。
4.4 工程里容易踩的时序坑
第一是复位顺序。nios_system.bsf里的 NIOS II 软核有cpu_reset信号,它只复位 CPU 和外设寄存器,不会复位tx_meta_fifo。如果整个系统共用全局复位,FIFO 读指针和写指针可能出现初始状态不一致,表现为首包数据丢失。处理方式是在软件初始化射频前端之后,再单独拉高 FIFO 复位。
第二,bladerf.sdc.bak里的时钟约束可能是旧版 PLL 的,时钟名和当前工程不一致时,Quartus 会忽略约束并给 warning。此时输出频谱会出现明显杂散,因为时序约束没生效。建议删掉旧create_clock,重新从原理图找 PLL 输出时钟名。
第三,异步 FIFO 的读写指针建议用三段式状态机按各自时钟域独立控制。直接照搬单时钟 FIFO 代码,读时钟域会出现亚稳态。BladeRF 工程里 FIFO 两侧时钟频率不同,这个问题一旦出现,表现为偶发的符号错位,很难用逻辑分析仪直接抓到。
5. 从 ILA 到 bladeRF-cli:实时验证 16-APSK 发射链路的技巧
5.1 先抓芯片内部信号,别急着看频谱
把信号送上天线之前,先在 FPGA 内部挂逻辑分析仪。BladeRF 工程里建议抓三个信号:tx_meta_fifo的空满标志、调制器输入sym_valid、成形滤波器输出i_out。逻辑分析仪触发条件设为tx_meta_fifo_empty == 0 && sym_valid == 1,一次抓 2048 个点。这一步能看出符号流是否连续。如果sym_valid有毛刺,或 FIFO 空信号频繁拉高,问题在 host 数据供应端,不在调制器。
5.2 利用 bladeRF-cli 捕获 IQ 并做离线 EVM 分析
波形稳定输出后,在 TX 口和 RX 口之间接一根短射频线做回环采集:
# -s 设置采样率,-n 设置样本数,-f 指定输出文件 bladeRF-cli -f loopback_iq.sc16q11 -s 20M -n 16Msc16q11是 BladeRF 的定点 IQ 格式,I/Q 各 16 bit 有符号数,11 bit 有效位。采集后用 Python 读取,做匹配滤波和星座图绘制。注意回环时 RX 增益不要开太高,否则 LNA 饱和会让外环 12 个点向外扩张,看起来像 EVM 很差,其实是接收链路削波。
5.3 判断结果对错的三个指标
第一,看星座点是否落在两个环上。16-APSK 的 I/Q 平面里内环 4 个点半径较小,外环 12 个点半径较大。如果看到 16 个点挤成一群,说明环比率参数没生效。第二,看是否整体旋转。内环应起于 π/4,外环应起于 π/12,旋转超过 10 度就要检查 ROM 系数。第三,计算 EVM。无噪声回环条件下 16-APSK 的 EVM 应低于 1%,如果超过 5% ,先查成形滤波器滚降系数和 DAC 回退量。
回环验证时,RX 路径的直流偏置会叠加在信号上。校验发射机时可以直接减去均值;但真实卫星链路里的直流偏置需要用接收端专门估计,不能简单地减均值了事。调试到这一步,发射链路从符号映射到射频回环的验证就算闭环了,接下来就可以直接接上真实码流做端到端的 TS 层测试。
本文还有配套的精品资源,点击获取