物理层处理的FPGA实现

1、CRC添加与校验

 并不是所有 TB 都用 24A。如果 TB 很小(A≤3824 bits),协议为了降低开销,会使用 CRC16。当  A>3824bits 时,统一使用 CRC24A。

串行 CRC 是基于 LFSR(线性反馈移位寄存器)的模 2 除法。若多项式为G(x) ,输入数据流为M(x) ,我们计算,在并行实现中,我们不再逐比特移位,而是一次性处理 W 个比特。数学本质是将状态转移矩阵的 W 次幂预先计算出来,并固化为异或树。

状态转移公式

关键参数确认:

位序 (Bit-Ordering): 5G NR 协议规定数据流为 MSB First。这意味着对于并行总线Data_in[63:0],Data_in[63] 是在该时钟周期内到达的最早比特(即 ),这是设计的基准。

初始值 (Initialization): 非常重要! 5G NR 协议规定 CRC 寄存器的初始值为 All Ones (全1)。在 Verilog 代码中,复位逻辑应该是 reg_crc <= 24'hFFFFFF;

FLSR的两种流向的移位寄存器:

A. 第一种:数据向低位移位 (Right-Shift / Shift-to-LSB)  R24寄存器的值更新给R23,R23给R22,依次类推。

物理意义: 这通常对应于“将输入比特从 LSB 端进入”的设计。
适用场景: 当生成多项式  的高阶项(例如 )被定义在 LSB 端时使用。这种结构在处理协议要求“LSB 先行”的某些数据格式时非常直观。

第二种:数据向高位移位 (Left-Shift / Shift-to-MSB) —— 这是 5G NR 更常用的视角

物理意义: 这对应于“将输入比特从 MSB 端进入”,数据不断向高位索引偏移。

作为例子的verilog 代码如下:

左移架构 (Shift-to-MSB) 的实现

// CRC-4 左移架构并行逻辑 (8-bit 并行)
// 假设 S[3:0] 是当前寄存器状态,D[7:0] 是输入
// 逻辑方程是基于 S_next = A^8 * S + B_parallel * D
module crc4_left_shift (
    input  [3:0] s_in,
    input  [7:0] data_in,
    output [3:0] s_out
);
    // 每一个 output bit 都是一个庞大的异或树
    // 这些方程是通过 A^8 和 B_parallel 预计算得到的
    assign s_out[3] = s_in[2] ^ data_in[6] ^ data_in[2]; 
    assign s_out[2] = s_in[1] ^ data_in[5] ^ data_in[1];
    assign s_out[1] = s_in[0] ^ data_in[4] ^ data_in[0];
    assign s_out[0] = s_in[3] ^ data_in[7] ^ data_in[3];
endmodule

右移架构 (Shift-to-LSB) 的实现

// CRC-4 右移架构并行逻辑 (8-bit 并行)
module crc4_right_shift (
    input  [3:0] s_in,
    input  [7:0] data_in,
    output [3:0] s_out
);
    // 注意观察:同样的输入数据,但其对应的异或逻辑项发生了移位
    // 这就是“镜像”效果的体现
    assign s_out[3] = s_in[0] ^ data_in[0] ^ data_in[4];
    assign s_out[2] = s_in[3] ^ data_in[7] ^ data_in[3];
    assign s_out[1] = s_in[2] ^ data_in[6] ^ data_in[2];
    assign s_out[0] = s_in[1] ^ data_in[5] ^ data_in[1];
endmodule

两种架构的校验值是完全镜像的。比如左移校验值1011,右移的校验值就是1101,这里会要求通信链路里的发射机与接收机,同构设计。

那对于FPGA来说,TB的大小A,TB分几个CB的个数C,每个CB的有效信息K,都是MAC层下发的,一般对于FPGA加速卡的架构来说,是带内标注法 (In-Band Sideband Signals)与Out-of-Band / 带外控制的联合写入,用AXI-stream的模式下,通过valid与tuser的握手,识别描述符,此后都是一个TB的数据,再用axi-lite的方式控制一些静态配置,比如24A还是24B,是否加扰等。

// 这是一个状态机
always @(posedge clk) begin
    if (tvalid && tuser_header) begin
        // 抓取描述符
        current_tb_id = tdata[31:24]; // 0xAA
        cb_count      = tdata[23:16]; // 02
        k_size        = tdata[15:0];  // 0400
        
        // 关键点:把这个 TB_ID 存入 CRC 内部的一个小 FIFO
        tb_id_fifo.push(current_tb_id);
    end
    
    if (tvalid && !tuser_header) begin
        // 数据计算逻辑...
        // 计算结束时,把 TB_ID 取出来作为“校验结果的标签”
        final_crc_result[current_tb_id] = calculated_val;
    end
end

2、速率匹配与解速率匹配的做法

速率匹配:

速率匹配的缓冲区的做法:例化两块RAM ,实现乒乓架构,乒乓 0 处理当前 CB;乒乓 1 预加载下一个 CB;切换无气泡,支持连续码块无中断输出;但依然每块 RAM 只存 1 个 CB,不是一块 RAM 塞多个 CB。

完整链路比特流顺序:

MAC 下发 TB 分割为码块原始信息比特 Kori,不足 22Zc时尾部补 0 填充,得到输入 LDPC 编码器的信息位 K=22Zc;
LDPC 编码器基于 K=22Zc生成校验比特 M=46Zc;
编码器串行输出完整码字总序列:系统比特段 (22Zc) + 校验比特段 (46Zc) = 68Zc;
速率匹配第一步:直接丢弃完整码字最开头 2Zc 系统比特,剩余 66Zc 写入循环缓存 
Ncb =66Zc。

RAM的大小:66*Z c,max=66*384=25344bit 。

那发射速率匹配的RV0 RV1 RV2 RV3对应的RAM地址以及长度是怎么定义的呢?

协议公式:

BG1 RV 固定偏移表:offset = [0, 17, 33, 56]

RV0:offset=0 → k 0=0,从缓存最开头系统比特开始读
RV1:offset=17 → 从中间校验段起始读
RV2:offset=33 → 后半段校验比特
RV3:offset=56 → 缓存末尾校验比特

问题1:比如现在是22Zc个系统比特,Zc是368bit,那么总共是8448bit,但是在LDPC编码前,7000~8448bit是补零的,为了凑够22Zc,那速率匹配发送不发送这部分补零信息?解速率匹配接收到这部分信息吗?

速率匹配仅去掉前2Zc,补零信息会发送,算入E的长度。接收端接收到的是LLR信息,7000~8448bit对应的LLR信息并不会设置为0,但是LDPC译码核会知道这部分信息是补零信息,会算成0.

解速率匹配的做法就是将收到的速率匹配的值按照对应的地址关系,存到RAM里去。

3、Sub-block Interleaver子块交织器

在速率匹配前有一个交织模块,根据 3GPP TS 38.212 协议第 5.4.2.2 节,5G NR 的 LDPC 子块交织器 (Sub-block Interleaver) 并不是像 LTE 那样根据调制方式动态变,它是为 LDPC 编码后的  个比特设计的。

 核心数学模型
协议将交织器定义为一个R*C的矩阵,其中:列数 (Columns):  (固定为 32)
行数 (Rows): R=N/32 (比如66*Zc/32
)

交织过程分为两步:
行置换 (Row Permutation): 协议定义了一个固定的 32 位向量 。
行优先写入:编码比特依次按行填满 R×32 矩阵,先填第 0 行全部 32 列,再第 1 行…… 直到填满;
按置换列序读出:遍历置换表 P [0]~P [31],每一列内部从上到下读出全部 R 个比特;
即先读原矩阵 P [0] 整列,再 P [1] 整列,直到 P [31],输出交织后序列,送入循环缓存写入。

% 协议规定的 32 列置换表 P
P = [0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14, 30, ...
     1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23, 15, 31];

function generate_3gpp_interleaver(Zc)
    C = 32;
    R = (66 * Zc) / C;
    P = [0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14, 30, ...
         1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23, 15, 31];
    
    map_table = zeros(66 * Zc, 2); % [bank_id, local_addr]
    W = 192; % FPGA 并行位宽
    
    for i = 0 : (66 * Zc - 1)
        row = floor(i / C);
        col = mod(i, C);
        
        % 协议规定的交织后索引
        k = row * C + P(col + 1); 
        
        % 映射到 FPGA 物理存储
        map_table(i+1, 1) = mod(k, W);      % Bank ID
        map_table(i+1, 2) = floor(k / W);   % Local Addr
    end
    
    writematrix(map_table, 'interleaver_3gpp.txt');
    disp('映射表生成完毕,包含 Bank_ID 和 Local_Addr');
end

解交织

% 假设 map_table 是你之前生成的映射关系
% map_table 第一列是原始索引 i,第二列是交织后索引 k
% 解交织就是通过 k 找回 i

% 生成逆映射表
inverse_map = zeros(66 * Zc, 1);
for i = 0 : (66 * Zc - 1)
    k = map_table(i+1, 2); % 取出交织后的索引 k
    inverse_map(k+1) = i;  % 在逆表中记录 k 对应的原始 i
end

% 将 inverse_map 导出,这就是解交织器的 ROM 内容
writematrix(inverse_map, 'deinterleaver_lut.txt');

一个问题:交织打散了系统比特的顺序,那RV0,RV1,RV2,RV3的系统比特的占比是不是一样的?

不一样,即便系统比特被打散了,RV0的系统比特确实下降了,但是依然高于RV1,RV2,RV3。协议设计者在定 P(置换表)的时候,其实是做了优化的:他们确保了置换后的系统位,依然会保持在“平均分布”的前提下,尽可能多地聚集在缓冲区的前部读取窗口内。

是先子块交织,再去掉前2Zc.

4、比特交织,解交织

发送端比特交织标准协议步骤

输入参数

  1. E:速率匹配选出的总比特数(rate_e
  2. (Q_m):调制阶数QPSK=2,16QAM=4,64QAM=6,256QAM=8

步骤 1:计算交织矩阵尺寸
列数固定等于调制阶数:C=Qm

行数(符号总数):

步骤 3:读出规则(列优先输出,跳过哑比特)
按逐列从上到下读出矩阵所有有效比特,跳过 NULL 哑比特,输出交织序列送入调制:
遍历列 c=0→C−1遍历行 r=0→R−1 若 matrix(r,c) 不是 NULL,输出该比特

接收端比特解交织

输入参数

  1. E:本次传输比特总数rate_e
  2. (Q_m):调制阶数,(C=Q_m)
  3. 解调符号总数

步骤 1:构建同尺寸
R×C
矩阵,全部初始化为 LLR=0(哑比特占位)
步骤 2:写入解调 LLR(列优先填入)
解调输出符号序列对应 LLR 是交织后的列优先序列,按逐列从上至下写入矩阵有效位置

所以速率匹配,解交织这里需要速率匹配的长度E,以及符号数,这跟QAM调制有关。

5、QAM调制与软解调LLR

对于16QAM,64QAM,256QAM,1024QAM就是用matlab 生成一个映射表,然后FPGA实现会根据索引去找出对应的调制点。

clear; clc;
% 配置参数
cfg_list = {
    struct('Qm',2,'level',[-1,1],'norm',sqrt(2)), ...
    struct('Qm',4,'level',[-3,-1,1,3],'norm',sqrt(10)), ...
    struct('Qm',6,'level',[-7,-5,-3,-1,1,3,5,7],'norm',sqrt(42)), ...
    struct('Qm',8,'level',-15:2:15,'norm',sqrt(170)), ...
    struct('Qm',10,'level',-31:2:31,'norm',sqrt(682)) ...
};
fixed_scale = 2048; 

for i = 1:length(cfg_list)
    cfg = cfg_list{i};
    Qm = cfg.Qm;
    levels = cfg.level;
    norm_f = cfg.norm;
    M = 2^Qm;
    half_bits = Qm / 2;
    
    % 修正:手动生成格雷码映射,避免 bin2gray 依赖
    % 格雷码 G = B ^ (B >> 1)
    gray_vals = bitxor(0:2^half_bits-1, bitshift(0:2^half_bits-1, -1));
    
    mod_table = zeros(M, 2);
    for idx = 0:M-1
        % 获取 I 和 Q 的格雷码索引
        idx_i = floor(idx / (2^half_bits));
        idx_q = mod(idx, 2^half_bits);
        
        % 映射到对应的电平索引
        i_val = levels(gray_vals(idx_i+1)+1);
        q_val = levels(gray_vals(idx_q+1)+1);
        
        % 定点化并应用归一化
        mod_table(idx+1,1) = round((i_val/norm_f) * fixed_scale);
        mod_table(idx+1,2) = round((q_val/norm_f) * fixed_scale);
    end
    
    % 导出 .mem 文件 (处理有符号数)
    fname = sprintf("%dqam_mod.mem", 2^Qm);
    fid = fopen(fname,'w');
    for k = 1:M
        % 确保输出为 16-bit 有符号的十六进制补码
        i_hex = typecast(int16(mod_table(k,1)), 'uint16');
        q_hex = typecast(int16(mod_table(k,2)), 'uint16');
        fprintf(fid, "%04X %04X\n", i_hex, q_hex);
    end
    fclose(fid);
end

FPGA的实现,

// 定义一个深度为 16 的 ROM,每个表项 32-bit (16位I + 16位Q)
reg [31:0] qam16_rom [0:15];

initial begin
    $readmemh("16qam_mod.mem", qam16_rom);
end

// 时序逻辑:输入 4-bit 比特,直接读出 I/Q
always @(posedge clk) begin
    {i_out, q_out} <= qam16_rom[bits_in]; // bits_in 就是你的 4-bit 地址
end

软解调的实现

  • 信道均衡的本质: 均衡器(Equalizer)的作用就是消除多径干扰并恢复信号的幅度。无论发射端信号在空中衰落成什么样,均衡后它都会被拉回到协议规定的“标准星座图范围”(比如你提到的幅度之内)。

  • Scale 缩放与饱和的精髓: 这个 scale 系数正是起到了“桥梁”的作用。它将浮点的欧氏距离差,成比例地放大到你指定的定点数范围内(如 6-bit 的 [-32, 31])。配合饱和截断(Saturation)逻辑,确保不管输入的复数值多大,查表输出的 LLR 绝不会发生二进制回绕(Wrap-around)导致译码错误。

举一个例子:16QAM

matlab代码

%% 全阶数自适应 QAM LLR 查找表生成 (Max-Log-MAP)
clear; clc;

% 1. 硬件定点参数配置
llr_width = 6;              % LLR 输出位宽 (如 6-bit)
llr_max = 2^(llr_width-1) - 1; % 31
llr_min = -2^(llr_width-1);    % -32

adc_width = 8;              % 输入 ADC/均衡后有符号信号位宽 (以 8-bit 为例)
adc_depth = 2^adc_width;    % 256 个地址刻度
adc_offset = adc_depth/2;   % 128 (无符号地址偏移)

% 2. 调制参数列表 (按 3GPP 协议标准归一化)
cfg_list = {
    struct('name','QPSK',   'Qm',2, 'level',[-1,1],             'norm',sqrt(2)), ...
    struct('name','16QAM',  'Qm',4, 'level',[-3,-1,1,3],         'norm',sqrt(10)), ...
    struct('name','64QAM',  'Qm',6, 'level',[-7:-2:-1,1:2:7],   'norm',sqrt(42)), ...
    struct('name','256QAM', 'Qm',8, 'level',[-15:2:15],          'norm',sqrt(170)), ...
    struct('name','1024QAM','Qm',10,'level',[-31:2:31],          'norm',sqrt(682)) ...
};

% 假设信噪比固定时的噪声方差与缩放因子 scale
sigma2 = 0.05; 
scale = 1 / (2 * sigma2); 

%% 3. 核心计算循环
for c_idx = 1:length(cfg_list)
    cfg = cfg_list{c_idx};
    Qm = cfg.Qm;
    half_bits = Qm / 2; % 每轴负责的比特数
    levels_norm = cfg.level / cfg.norm; % 归一化后的标准电平
    
    % 手动构建该轴对应的格雷码映射值
    gray_vals = bitxor(0:2^half_bits-1, bitshift(0:2^half_bits-1, -1));
    
    % 初始化当前调制模式的 LUT (256行,每行对应 half_bits 个比特的 LLR)
    % 例如 16QAM 一轴 2 比特,矩阵为 256 x 2
    llr_lut = zeros(adc_depth, half_bits);
    
    % 模拟输入定点信号 z_in 的全量程范围 [-1.0, 1.0)
    % 对应 FPGA 读 ROM 时的地址 0 ~ 255
    for addr = 0:adc_depth-1
        z_val = (addr - adc_offset) / adc_offset; % 将地址映射回 [-1, 1) 的浮点数
        
        % 对当前轴负责的每一个比特分别计算最近距离
        for b_idx = 1:half_bits
            d0_min_sq = Inf;
            d1_min_sq = Inf;
            
            % 遍历所有一维星座点
            for s_idx = 1:length(levels_norm)
                current_dianping = levels_norm(s_idx);
                dist_sq = (z_val - current_dianping)^2;
                
                % 找出当前星座点在该比特位上是 0 还是 1
                % 对应逻辑:通过格雷码反查
                mapped_binary_idx = find(gray_vals == (s_idx - 1)) - 1;
                bit_string = dec2bin(mapped_binary_idx, half_bits);
                current_bit = str2double(bit_string(b_idx));
                
                if current_bit == 0
                    if dist_sq < d0_min_sq, d0_min_sq = dist_sq; end
                else
                    if dist_sq < d1_min_sq, d1_min_sq = dist_sq; end
                end
            end
            
            % Max-Log-MAP 公式计算核心
            llr_float = scale * (d1_min_sq - d0_min_sq);
            llr_fix = round(llr_float * 10); % 乘 10 是定点化放大因子
            
            % 严格饱和截断
            if llr_fix > llr_max, llr_fix = llr_max;
            elseif llr_fix < llr_min, llr_fix = llr_min; end
            
            llr_lut(addr+1, b_idx) = llr_fix;
        end
    end
    
    % 4. 导出为 Vivado 可直接加载的 .mem 文件
    fname = sprintf("llr_lut_%s.mem", cfg.name);
    fid = fopen(fname, 'w');
    for r = 1:adc_depth
        line_str = "";
        for c = 1:half_bits
            % 转换为二进制补码形式存储
            val = llr_lut(r, c);
            if val < 0, val = val + 2^llr_width; end
            line_str = strcat(line_str, dec2hex(val, ceil(llr_width/4)));
        end
        fprintf(fid, "%s\n", line_str);
    end
    fclose(fid);
end
disp("LLR 查找表 .mem 文件全部生成成功!");

verilog代码:

// =================================================================
// 5G 物理层通用自适应 QAM 软解调模块 (支持 QPSK ~ 1024QAM)
// =================================================================
module alpha_qam_demapper #(
    parameter ADC_WIDTH = 8,   // 输入均衡后 I/Q 的位宽
    parameter LLR_WIDTH = 6    // 输出软判决 LLR 的位宽 (如 6-bit)
)(
    input  wire                   clk,
    input  wire                   rst_n,
    input  wire [2:0]             mod_mode,    // 0:QPSK, 1:16QAM, 2:64QAM, 3:256QAM, 4:1024QAM
    input  wire signed [ADC_WIDTH-1:0] i_in,   // 均衡后的 I 信号
    input  wire signed [ADC_WIDTH-1:0] q_in,   // 均衡后的 Q 信号
    input  wire                   data_vld_in,
    
    // 最大支持 1024QAM(单轴 5 个 LLR,双轴共 10 个 LLR)
    // 输出采用展平的一维总线,便于后续直接送入子块解交织 RAM
    output reg  [10*LLR_WIDTH-1:0] llr_bus_out, 
    output reg                    data_vld_out
);

    // 有符号数转无符号 ROM 地址映射 (offset = 128)
    wire [ADC_WIDTH-1:0] rom_addr_i = i_in + (1'b1 << (ADC_WIDTH-1));
    wire [ADC_WIDTH-1:0] rom_addr_q = q_in + (1'b1 << (ADC_WIDTH-1));

    // -------------------------------------------------------------
    // 实例化各阶数单轴 ROM 阵列 (这里以 I 轴为例,Q 轴完全对称复制即可)
    // -------------------------------------------------------------
    wire [1*LLR_WIDTH-1:0] qpsk_i_llr;
    wire [2*LLR_WIDTH-1:0] qam16_i_llr;
    wire [3*LLR_WIDTH-1:0] qam64_i_llr;
    wire [4*LLR_WIDTH-1:0] qam256_i_llr;
    wire [5*LLR_WIDTH-1:0] qam1024_i_llr;

    wire [1*LLR_WIDTH-1:0] qpsk_q_llr;
    wire [2*LLR_WIDTH-1:0] qam16_q_llr;
    wire [3*LLR_WIDTH-1:0] qam64_q_llr;
    wire [4*LLR_WIDTH-1:0] qam256_q_llr;
    wire [5*LLR_WIDTH-1:0] qam1024_q_llr;

    // 内部存储阵列定义 (Vivado 综合时会自动推导为分布式或块状 ROM)
    reg [1*LLR_WIDTH-1:0] rom_qpsk_i   [0:(2^ADC_WIDTH)-1];
    reg [2*LLR_WIDTH-1:0] rom_qam16_i  [0:(2^ADC_WIDTH)-1];
    reg [3*LLR_WIDTH-1:0] rom_qam64_i  [0:(2^ADC_WIDTH)-1];
    reg [4*LLR_WIDTH-1:0] rom_qam256_i [0:(2^ADC_WIDTH)-1];
    reg [5*LLR_WIDTH-1:0] rom_qam1024_i[0:(2^ADC_WIDTH)-1];
    
    // Q轴 ROM 阵列(使用完全相同的 mem 文件初始化)
    reg [1*LLR_WIDTH-1:0] rom_qpsk_q   [0:(2^ADC_WIDTH)-1];
    reg [2*LLR_WIDTH-1:0] rom_qam16_q  [0:(2^ADC_WIDTH)-1];
    reg [3*LLR_WIDTH-1:0] rom_qam64_q  [0:(2^ADC_WIDTH)-1];
    reg [4*LLR_WIDTH-1:0] rom_qam256_q [0:(2^ADC_WIDTH)-1];
    reg [5*LLR_WIDTH-1:0] rom_qam1024_q[0:(2^ADC_WIDTH)-1];

    // 加载先前由 MATLAB 生成的预计算物理参数字典
    initial begin
        $readmemh("llr_lut_QPSK.mem",    rom_qpsk_i);
        $readmemh("llr_lut_16QAM.mem",   rom_qam16_i);
        $readmemh("llr_lut_64QAM.mem",   rom_qam64_i);
        $readmemh("llr_lut_256QAM.mem",  rom_qam256_i);
        $readmemh("llr_lut_1024QAM.mem", rom_qam1024_i);
        
        $readmemh("llr_lut_QPSK.mem",    rom_qpsk_q);
        $readmemh("llr_lut_16QAM.mem",   rom_qam16_q);
        $readmemh("llr_lut_64QAM.mem",   rom_qam64_q);
        $readmemh("llr_lut_256QAM.mem",  rom_qam256_q);
        $readmemh("llr_lut_1024QAM.mem", rom_qam1024_q);
    end

    // 1个时钟周期的并行查表读取 (Pipeline Stage 1)
    reg [1*LLR_WIDTH-1:0] qpsk_i_reg,    qpsk_q_reg;
    reg [2*LLR_WIDTH-1:0] qam16_i_reg,   qam16_q_reg;
    reg [3*LLR_WIDTH-1:0] qam64_i_reg,   qam64_q_reg;
    reg [4*LLR_WIDTH-1:0] qam256_i_reg,  qam256_q_reg;
    reg [5*LLR_WIDTH-1:0] qam1024_i_reg, qam1024_q_reg;
    reg [2:0]             mod_mode_d1;
    reg                   vld_d1;

    always @(posedge clk) begin
        qpsk_i_reg    <= rom_qpsk_i[rom_addr_i];
        qpsk_q_reg    <= rom_qpsk_q[rom_addr_q];
        qam16_i_reg   <= rom_qam16_i[rom_addr_i];
        qam16_q_reg   <= rom_qam16_q[rom_addr_q];
        qam64_i_reg   <= rom_qam64_i[rom_addr_i];
        qam64_q_reg   <= rom_qam64_q[rom_addr_q];
        qam256_i_reg  <= rom_qam256_i[rom_addr_i];
        qam256_q_reg  <= rom_qam256_q[rom_addr_q];
        qam1024_i_reg <= rom_qam1024_i[rom_addr_i];
        qam1024_q_reg <= rom_qam1024_q[rom_addr_q];
        
        mod_mode_d1   <= mod_mode;
        vld_d1        <= data_vld_in;
    end

    // -------------------------------------------------------------
    // 多路复用自适应总线拼接逻辑 (Pipeline Stage 2)
    // -------------------------------------------------------------
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            llr_bus_out  <= {10*LLR_WIDTH{1'b0}};
            data_vld_out <= 1'b0;
        end else begin
            data_vld_out <= vld_d1;
            if (vld_d1) begin
                case (mod_mode_d1)
                    3'd0: begin // QPSK (2 bits: I0, Q0)
                        llr_bus_out <= {{8*LLR_WIDTH{1'b0}}, qpsk_q_reg, qpsk_i_reg};
                    end
                    3'd1: begin // 16QAM (4 bits: I1,I0, Q1,Q0)
                        llr_bus_out <= {{6*LLR_WIDTH{1'b0}}, qam16_q_reg, qam16_i_reg};
                    end
                    3'd2: begin // 64QAM (6 bits: I2,I1,I0, Q2,Q1,Q0)
                        llr_bus_out <= {{4*LLR_WIDTH{1'b0}}, qam64_q_reg, qam64_i_reg};
                    end
                    3'd3: begin // 256QAM (8 bits: I3..I0, Q3..Q0)
                        llr_bus_out <= {{2*LLR_WIDTH{1'b0}}, qam256_q_reg, qam256_i_reg};
                    end
                    3'd4: begin // 1024QAM (10 bits)
                        llr_bus_out <= {qam1024_q_reg, qam1024_i_reg};
                    end
                    default: llr_bus_out <= {10*LLR_WIDTH{1'b0}};
                endcase
            end else begin
                llr_bus_out <= {10*LLR_WIDTH{1'b0}};
            end
        end
    end

endmodule

6、HARQ进程

LDPC 译码是软输入译码,重传不丢弃上一次接收的 LLR,而是同地址 LLR 线性累加(软合并),累积更多校验信息提升译码可靠性:

  1. 初传 RV0:解调→比特解交织→解速率匹配,E 个 LLR 回填 66Zc 循环缓存,整 CB 软码字存入 HARQ 缓存;
  2. 译码 CRC 错误,基站重传 RV1/RV2/RV3;
  3. 重传接收:新的 E 段 LLR 回填对应 k0 地址;相同位置的 LLR = 旧 DDR 缓存 LLR + 本次新接收 LLR;
  4. 合并完整 66Zc 软码字后,补 2Zc 哑比特、解子块交织,送入 LDPC 译码;
  5. 若再次 CRC 失败,合并后的新 LLR 写回 DDR 覆盖旧值,等待下一次重传;译码成功则释放该 HARQ 缓存空间。

关键:HARQ 缓存存储的是完整码块 66Zc 长度 LLR 软码字,不是仅本次传输的 E 比特,每次重传只覆盖对应 RV 区间的 LLR,其余位置保留历史累加值。

5G NR 标准 HARQ 进程数量(决定 DDR 同时缓存 CB 上限)

终端 UE 侧(下行接收)

3GPP 规定:下行最多 8 个独立 HARQ 进程,每个进程对应 1 个 TB 传输;一个 TB 会被拆分成若干个 CB(码块分割 CBG),同一 TB 内所有 CB 共享 1 个 HARQ 进程,进程并行互不干扰。

  • 单 TB 最大 CB 数量:以 384Zc、TBsize 上限为例,单 TB 最多拆出32 个 CB;
  • 极端峰值缓存:8 进程 × 32 CB = 256 个 CB 同时驻留 DDR。

基站 gNB 侧(上行接收 UE 数据)

上行 HARQ 进程数同样 8 个,峰值缓存容量和 UE 一致;多用户场景下按用户数 × 8 进程扩展 DDR。

FPGA 数据流:HARQ DDR 读写完整流程(单码块)
初传流程(CRC 错误,写入 DDR)
解调 LLR → 比特解交织 → 解速率匹配:E 个 LLR 回填片上 66Zc 循环缓存;
缓存填满完整 25344 个 LLR 后,整段通过 AXI-MM 写入 DDR 对应 HARQ 进程分区;
同时片上缓存一份副本送入 LDPC 译码;
译码 CRC 失败:该 CB 在 DDR 保持不释放,等待重传;译码成功:标记该 CB 空间可复用。
重传流程(软合并叠加)
接收重传符号,解交织得到新 E 段 LLR;
根据当前 RV 算出 k0 地址范围,发起 DDR 读请求,读出该 CB 完整 25344 个历史 LLR 到片上 66Zc BRAM;
解速率匹配模块:对 k0 起始的 E 个地址,执行 llr_out = llr_old + llr_new,其余地址保持旧值不变;
合并后的完整 66Zc LLR 两路输出:
一路 AXI-MM 写回 DDR,覆盖旧缓存,保存本次合并结果;
一路送入补 2Zc 哑比特 + 解子块交织,输入 LDPC 再次译码;
若再次 CRC 错误:保留 DDR 缓存;译码正确:释放该 CB DDR 空间。

几个问题:

能不能不用 DDR,全片上 BRAM 存 HARQ?

绝对不行。单 CB 最大 18.56KB,256 个 CB 峰值需要约 4.5MB 存储;Xilinx FPGA BRAM 总容量有限,仅适合单 TB 少量 CB,多进程场景 BRAM 资源耗尽,必须外接 DDR4 大容量存储做 HARQ 上下文缓存。

缓存只存 66Zc,不需要存 68Zc?

是的。

  • HARQ 只存储解速率匹配输出的 66Zc 有效软码字;
  • 头部 2Zc 哑比特、解子块交织是译码前端片上实时补齐、实时逆置换,不占用 DDR 缓存,减少 5.8% 存储空间开销。
  • LLR 累加饱和怎么处理?

    LLR 是 6bit 有符号 [-32,31],两次累加会溢出,硬件规则:

  • llr_sum = llr_old + llr_new; if(llr_sum > 31) llr_sum = 31; if(llr_sum < -32) llr_sum = -32;

  • 饱和截断后再写回 DDR,防止数值溢出失真。

    最大重传次数限制

    3GPP 规范单 TB 最多重传 3 次(总共 4 次传输:初传 + 3 次重传),4 次译码仍失败则直接释放 DDR 缓存,丢弃该 TB。

7、DDR的地址划分是怎么划分的?

HARQ DDR 存储地址两部分协同生成

  1. 进程基地址:CPU(ARM/RISC-V)通过 AXI-Lite 配置下发给 FPGA;
  2. 码块 CB 内部偏移地址:FPGA 基带内部实时自动计算,不需要 CPU 参与。

整体架构:CPU 分配大块分区 → FPGA 在分区内自主管理每一个 CB 的偏移、读写地址。

1. CPU 负责:分配 HARQ 进程全局基地址(大块分区)

协议背景

MAC 层运行在 CPU,CPU 知道 8 个 HARQ 进程、每个 TB 最多 32 个 CB 的存储需求,提前对 DDR 做静态分区:

  • 进程 0 基地址 base_addr0
  • 进程 1 基地址 base_addr1
  • ……
  • 进程 7 基地址 base_addr7

CPU 操作流程:

  1. 系统初始化时,CPU 计算 DDR 空闲连续内存,给 8 个进程各自划分一块独立存储区间(每块最大容纳 32 个 CB);
  2. 通过 AXI-Lite 寄存器,把 8 个进程的起始基地址一次性写入 FPGA PHY 控制寄存器;
  3. 下行 PDCCH 解调后,MAC 层(CPU)下发当前传输使用的harq_pid(0~7 进程号)给 FPGA;
  4. FPGA 拿到 pid,查表得到该进程对应的 DDR 大块基地址。

CPU 只管整块进程分区的起始地址,不关心单个 CB 在分区内的偏移。

2. FPGA 自主实时生成:CB 内部偏移、完整 66Zc 读写地址
FPGA 内部维护两个计数器,完全硬件自动生成,无需 CPU 干预:
① TB 内 CB 编号偏移
一个 TB 被码块分割成 num_cb 个 CB,FPGA 内部硬件计数当前是第 N 个 CB;
单个 CB 固定占用存储空间 Size_CB,因此该 CB 在进程分区内的偏移:
cb_offset = N × Size_CB
当前 CB 完整 DDR 起始地址:
cb_start_addr = base_addr[pid] + cb_offset
② 66Zc 内部 LLR 字节 / 位偏移
读写完整 66Zc LLR 时,FPGA 根据 AXI-MM 位宽(64bit/128bit/256bit)自动生成 burst 读写地址;
重传合并时,硬件根据 RV 算出k0,仅对k0 ~ k0+E区间做 LLR 叠加,地址全部内部组合逻辑生成。
重传场景地址流程(纯硬件自动)
PDCCH 解析得到 pid、RV、num_cb、当前 CB 编号;
查表拿到进程基地址 + 硬件计算 CB 偏移,得到该 CB 66Zc 缓存起始地址;
AXI-MM 发起整段 burst 读,把 66Zc 旧 LLR 读到片上 BRAM;
解速率匹配硬件根据k0、E生成局部地址,叠加新旧 LLR;
同一 cb_start_addr 发起 burst 写回 DDR 覆盖。

两种主流工程实现架构

架构 1:静态分区(基站 / 标准终端最常用)

  1. CPU 上电一次性配置 8 个进程固定基地址,全程不再修改;
  2. FPGA 硬件自主管理每个 TB 内 0~31 号 CB 的偏移;
  3. 优点:地址逻辑简单、时序稳定,AXI burst 连续读写效率高;
  4. CPU 只下发 pid,不需要每次传输重配地址。

架构 2:动态内存池(多用户商用 gNB)

  1. CPU 维护 DDR 空闲 CB 块链表,每个 CB 块大小 = Size_CB;
  2. 新 TB 到达,CPU 分配空闲 CB 块地址,通过寄存器下发给 FPGA;
  3. 译码成功 / 重传超限后,CPU 回收地址标记为空闲;
  4. FPGA 只负责该 CB 块内部 66Zc 的 burst 读写地址;
  5. 优点:DDR 内存利用率更高,多用户并发不浪费空间;缺点:CPU 交互频繁,控制逻辑复杂。
地址类型控制方说明
HARQ 进程大块分区基地址CPU(ARM)上电静态分配,AXI-Lite 配置给 FPGA
当前传输 HARQ PID 号CPU/MAC 层PDCCH 解析后下发进程 ID
TB 内每个 CB 的分区偏移地址FPGA 硬件自动计算CB 编号 × 单 CB 存储长度
66Zc 内部 LLR Burst 读写地址FPGA 硬件自动计算AXI 位宽、66Zc 长度生成连续地址
重传 k0 对应的局部叠加区间地址FPGA 解速率匹配模块RV 实时算出 k0,内部生成局部地址
HARQ重传从DDR读取的地址都是从CB的头开始读取。跟重传版本没有关系。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值