FPGA教程系列- 存储结构与参数化
在学习代码的时候,发现参数化与存储结构的重要性,拿来单独的说明一下
数据聚合
在 AXI4-Stream 协议中,一个"传输单元"不仅仅包含数据(tdata),还包含很多"伴随信号"(Sideband Signals):
在实际的应用中,可能会用到很多参数,比如AXI的通信协议就会有很多信号,逐个的去定义,存储会比较冗余,比如说建立很多数组:
verilog
// ❌由于时序难以对齐,不建议这样做
reg [7:0] data_mem [1023:0];
reg last_mem [1023:0];
reg user_mem [1023:0];
这样做会导致综合器可能生成多个独立的 RAM 块,且读写控制逻辑分散,容易在高速时钟下出现时序偏差。
但是如果将所有的信号进行一个拼接的操作,拼接 成一个巨大的二进制向量,存入同一个 RAM, 就能够实现完全的同步。虽然很多时候可能用不到,但是保持一个良好的习惯,会在将来省去很多麻烦,当然,如果不这么做一般也不会有太大问题,可能自己也到不了那个高度,但是,能够直接拿来优秀的东西,何乐而不为。
verilog
// ✅ 所有信号拼在一起,物理上完全同步
reg [WIDTH-1:0] mem[(2**ADDR_WIDTH)-1:0];
动态位宽计算
常规操作:
verilog
wire [$clog2(DEPTH):0] status_depth,
动态的实现参数化的定义wire或reg,这样做的好处非常明显,只需要修改DEPTH参数,既可以实现最优化的内存占用。
进阶版:
verilog
localparam KEEP_OFFSET = DATA_WIDTH;
localparam LAST_OFFSET = KEEP_OFFSET + (KEEP_ENABLE ? KEEP_WIDTH : 0);
localparam ID_OFFSET = LAST_OFFSET + (LAST_ENABLE ? 1 : 0);
localparam DEST_OFFSET = ID_OFFSET + (ID_ENABLE ? ID_WIDTH : 0);
localparam USER_OFFSET = DEST_OFFSET + (DEST_ENABLE ? DEST_WIDTH : 0);
localparam WIDTH = USER_OFFSET + (USER_ENABLE ? USER_WIDTH : 0);
这个过程是计算每个信号在总线中的起始位置,同时根据使能信号实现了动态的调整。
视觉图解: 假设 DATA=32 (4字节), KEEP=4 (4位), LAST=1 (1位), USER=1 (1位)。
内存中的一个单元(Word)长这样:
verilog
Bit: [37] [36] [35..32] [31..........0]
Name: USER | LAST | KEEP | DATA
偏移量的应用:通过 generate 块可以根据参数有选择地赋值
verilog
wire [WIDTH-1:0] s_axis; // 这是一个临时的大宽线
generate
// 1. 数据永远有
assign s_axis[DATA_WIDTH-1:0] = s_axis_tdata;
// 2. 如果开启了 KEEP,就赋值到对应的偏移量位置
if (KEEP_ENABLE) assign s_axis[KEEP_OFFSET +: KEEP_WIDTH] = s_axis_tkeep;
// 3. LAST 信号处理(这里还包含了帧标记逻辑,先只看赋值)
if (LAST_ENABLE) assign s_axis[LAST_OFFSET] = s_axis_tlast | mark_frame_reg;
// ... 其他信号同理
endgenerate
"通用" 的 Verilog 代码写法:
不要写死位宽 :永远用 parameter 定义。
不要分散存储 :将相关联的数据和控制信号打包(Struct-like),存入同一个存储器,保证物理同步。
使用计算偏移量:利用 localparam 自动计算信号在总线中的位置,这样无论用户如何配置参数,代码逻辑都不需要修改