格雷码(Gray Code / Reflected Binary Code)是以Frank Gray命名的编码方式。在异步FIFO中使用的格雷码具有特殊性质:
最简公式:gray = bin XOR (bin >> 1)
原理:格雷码的第i位 = 二进制的第i位 XOR 第i+1位。这是"差分编码"——每个格雷码位编码了二进制相邻位的差异。
// bin_to_gray.v
// 二进制转格雷码 — 组合逻辑,单周期完成
module bin_to_gray #(
parameter WIDTH = 5
)(
input wire [WIDTH-1:0] bin,
output wire [WIDTH-1:0] gray
);
assign gray = bin ^ (bin >> 1);
endmodule
这是逆变换,需要从最高位开始逐位恢复:
bin[MSB] = gray[MSB]
bin[i] = gray[i] XOR bin[i+1]
// gray_to_bin.v
// 格雷码转二进制 — 逐位异或恢复
module gray_to_bin #(
parameter WIDTH = 5
)(
input wire [WIDTH-1:0] gray,
output wire [WIDTH-1:0] bin
);
assign bin[WIDTH-1] = gray[WIDTH-1];
genvar i;
generate
for (i = WIDTH-2; i >= 0; i = i - 1) begin : gen_g2b
assign bin[i] = gray[i] ^ bin[i+1];
end
endgenerate
endmodule
在异步FIFO中,指针需要递增。最直接的方法是:格雷码→二进制→加1→格雷码。但也可以直接在格雷码空间递增:
// gray_counter.v
// 格雷码计数器
// 方法:格雷码→二进制→加1→格雷码
module gray_counter #(
parameter WIDTH = 5
)(
input wire clk,
input wire rst_n,
input wire inc, // 递增使能
output wire [WIDTH-1:0] gray_out,
output wire [WIDTH-1:0] bin_out
);
reg [WIDTH-1:0] bin_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
bin_cnt <= {WIDTH{1'b0}};
else if (inc)
bin_cnt <= bin_cnt + 1'b1;
end
assign bin_out = bin_cnt;
assign gray_out = bin_cnt ^ (bin_cnt >> 1);
endmodule
这是异步FIFO设计中最需要严谨推导的部分。
空 = 写指针 == 读指针(FIFO中无数据)
在格雷码中:空 = wr_gray == rd_gray_sync
这是直接的——格雷码是双射(一一对应),二进制相等当且仅当格雷码相等。
满 = 写指针比读指针多转了一圈,且低N位相同
二进制条件:wr_bin[N] != rd_bin[N] 且 wr_bin[N-1:0] == rd_bin[N-1:0]
这意味着 wr_bin = rd_bin + 2^N(刚好差一圈)
转换为格雷码条件,需要分析当二进制差2^N时格雷码的关系:
令 rd_bin = X,则 wr_bin = X + 2^N
差2^N意味着只有第N位翻转(额外位),低N位相同
但格雷码不是这样直接的!格雷码中满的条件是:
wr_gray[MSB] != rd_gray_sync[MSB]
wr_gray[MSB-1] != rd_gray_sync[MSB-1]
wr_gray[MSB-2:0] == rd_gray_sync[MSB-2:0]
即:最高2位都取反,其余位相同。
证明概要:当二进制 wr_bin = rd_bin + 2^N 时:
// gray_fifo_flags.v
// 基于格雷码指针的异步FIFO满空标志生成
module gray_fifo_flags #(
parameter ADDR_WIDTH = 4 // 指针宽度 = ADDR_WIDTH + 1
)(
// 写域
input wire [ADDR_WIDTH:0] wr_ptr_gray,
input wire [ADDR_WIDTH:0] rd_ptr_gray_sync, // 同步到写域的读指针
output wire full,
// 读域
input wire [ADDR_WIDTH:0] rd_ptr_gray,
input wire [ADDR_WIDTH:0] wr_ptr_gray_sync, // 同步到读域的写指针
output wire empty
);
// ========== 空判断 ==========
// 格雷码直接比较
assign empty = (wr_ptr_gray_sync == rd_ptr_gray);
// ========== 满判断 ==========
// 格雷码:最高2位取反,其余位相同
wire [ADDR_WIDTH:0] full_cmp;
assign full_cmp = {~rd_ptr_gray_sync[ADDR_WIDTH:ADDR_WIDTH-1],
rd_ptr_gray_sync[ADDR_WIDTH-2:0]};
assign full = (wr_ptr_gray == full_cmp);
endmodule
格雷码的关键优势:即使采样窗口内指针正在变化,最多只有1位不确定。那么这1位不确定会导致什么?
假设格雷码指针正从值A变到值B(只差1位),同步器可能采样到:
不可能采样到"半个A半个B"以外的值,因为只有1位在变!这与二进制指针(多位同时变)形成鲜明对比。
但有一个微妙的前提:格雷码指针的各位必须在几乎同一时刻变化。如果各位到达同步器的时间差超过一个时钟周期,可能出现"看到位i的新值但位j的旧值"的情况——这相当于采样到了"跳过一步"的值。
解决方案:确保格雷码指针的各位使用相同的布线长度到达同步器,或在布局时添加等长约束。
当指针从2^N - 1递增到0时(溢出回绕),格雷码的变化是否符合单位距?
// 验证格雷码的循环性
// 5位格雷码(指针宽度5,深度16)
// 当二进制从 10000 (16) → 00000 (0) 时:
// 格雷码:11000 → 00000
// 变化了3位!不是单位距!
// 但在异步FIFO中,指针宽度是 ADDR_WIDTH+1
// 深度2^ADDR_WIDTH = 2^4 = 16
// 指针从0到31循环(5位),不会只走0-15
// 额外位的存在使得指针绕一圈后二进制从16变32
// 实际上:指针在0-31之间循环
// 5位格雷码:0→1→3→2→6→7→5→4→12→13→15→14→10→11→9→8→24→...
// 每步都只变1位,包括 31→0(11110→00000,也只变1位!)
// 因为5位格雷码的首尾也是单位距
N+1位格雷码指针(支持2^N深度FIFO)在0到2^(N+1)-1之间循环,每步只变1位,包括首尾。这正是为什么异步FIFO的深度必须是2的幂——只有2的幂深度才能保证格雷码指针的完美循环性。
// tb_gray_counter.v
// 格雷码计数器和满空判断验证
\`timescale 1ns/1ps
module tb_gray_counter;
reg clk, rst_n, inc;
wire [4:0] gray_out, bin_out;
gray_counter #(.WIDTH(5)) uut (
.clk(clk), .rst_n(rst_n), .inc(inc),
.gray_out(gray_out), .bin_out(bin_out)
);
reg [4:0] prev_gray;
integer errors;
initial begin
$dumpfile("gray_counter.vcd");
$dumpvars(0, tb_gray_counter);
clk = 0; rst_n = 0; inc = 0;
errors = 0;
#20 rst_n = 1;
#10;
// 测试1: 连续递增32次,验证格雷码单位距
$display("--- Test 1: Unit distance check ---");
inc = 1;
prev_gray = 0;
repeat(32) begin
@(posedge clk);
#1; // 等待输出稳定
if ($countbits(gray_out ^ prev_gray, '1) != 1) begin
$display("ERROR: Non-unit distance! prev=%b curr=%b diff=%b",
prev_gray, gray_out, gray_out ^ prev_gray);
errors = errors + 1;
end
prev_gray = gray_out;
end
// 测试2: 验证环绕(31→0)
$display("--- Test 2: Wrap-around ---");
if ($countbits(gray_out ^ 5'b00000, '1) != 1) begin
$display("ERROR: Wrap-around not unit distance!");
errors = errors + 1;
end else
$display("Wrap-around OK: %b -> %b", prev_gray, 5'b00000);
// 测试3: 验证bin↔gray转换一致性
$display("--- Test 3: Round-trip consistency ---");
inc = 0;
repeat(32) begin
@(posedge clk);
inc = 1;
@(posedge clk);
inc = 0;
// 验证: bin_to_gray(bin) == gray
wire [4:0] expected_gray = bin_out ^ (bin_out >> 1);
if (gray_out !== expected_gray) begin
$display("ERROR: gray mismatch at bin=%b", bin_out);
errors = errors + 1;
end
end
$display("=== Gray Counter Test: %0d errors ===", errors);
$finish;
end
always #5 clk = ~clk;
endmodule
如果FIFO深度不是2的幂怎么办?有几种方案:
| 方案 | 描述 | 可行性 |
|---|---|---|
| 向上取整到2^N | 深度5→用8(浪费3个位置) | ✅ 简单有效 |
| 截断格雷码 | 用N+1位格雷码,但跳过某些值 | ⚠️ 复杂,跳转时多位变化 |
| 计数器法 | 放弃格雷码,用二进制+计数器 | ⚠️ 需要握手CDC,非标准 |
| 多组格雷码 | 将非2幂深度拆为2幂之和 | ❌ 过于复杂 |
工业实践:几乎总是向上取整到2的幂。多几个RAM位置的面积开销远小于复杂指针逻辑的开销。
在实际硬件中,格雷码指针的处理可以进一步优化:
满空判断中,空条件可以直接比较格雷码,无需转换。但可编程满/空需要计算元素个数,必须转回二进制。为了减少转换逻辑的延迟,可以:
// gray_diff_counter.v
// 格雷码差值计算器 — 用于可编程满空标志
// 将两侧格雷码分别转二进制后相减
module gray_diff_counter #(
parameter WIDTH = 5 // 格雷码指针宽度
)(
input wire [WIDTH-1:0] wr_gray,
input wire [WIDTH-1:0] rd_gray_sync,
output wire [WIDTH-1:0] count
);
// 格雷码→二进制
wire [WIDTH-1:0] wr_bin, rd_bin_sync;
assign wr_bin[WIDTH-1] = wr_gray[WIDTH-1];
assign rd_bin_sync[WIDTH-1] = rd_gray_sync[WIDTH-1];
genvar i;
generate
for (i = WIDTH-2; i >= 0; i = i - 1) begin : gen_g2b
assign wr_bin[i] = wr_gray[i] ^ wr_bin[i+1];
assign rd_bin_sync[i] = rd_gray_sync[i] ^ rd_bin_sync[i+1];
end
endgenerate
// 元素个数 = 写指针 - 读指针
assign count = wr_bin - rd_bin_sync;
endmodule
满条件可以不经过二进制转换,直接在格雷码空间比较:
// gray_full_detector.v
// 直接格雷码满检测器
// 无需二进制转换,纯组合逻辑
module gray_full_detector #(
parameter WIDTH = 5
)(
input wire [WIDTH-1:0] wr_gray,
input wire [WIDTH-1:0] rd_gray_sync,
output wire full
);
// 满条件:wr_gray最高2位取反 == rd_gray_sync最高2位
// wr_gray其余位 == rd_gray_sync其余位
assign full = (wr_gray[WIDTH-1] != rd_gray_sync[WIDTH-1]) &&
(wr_gray[WIDTH-2] != rd_gray_sync[WIDTH-2]) &&
(wr_gray[WIDTH-3:0] == rd_gray_sync[WIDTH-3:0]);
endmodule
指针宽度 = ADDR_WIDTH + 1(包含额外位)。常见配置:
| FIFO深度 | ADDR_WIDTH | 指针宽度 | 同步器FF数(双向) |
|---|---|---|---|
| 4 | 2 | 3 | 12 |
| 16 | 4 | 5 | 20 |
| 64 | 6 | 7 | 28 |
| 256 | 8 | 9 | 36 |
| 1024 | 10 | 11 | 44 |
同步器FF数 = 指针宽度 × 2级 × 2方向(写→读 + 读→写)
1. 证明题:完整证明5位格雷码满条件的等价性:格雷码"最高2位取反+其余相同"等价于二进制"额外位不同+低N位相同"。
2. 编程题:实现一个参数化的格雷码递增器,直接在格雷码空间计算下一个值(不经过二进制转换)。提示:利用格雷码最低位的规律。
3. 分析题:如果格雷码指针的各位布线延迟差为Δt,在什么条件下会出现"看到跳过一步"的值?计算临界条件。
4. 设计题:实现一个可编程满/空标志,基于格雷码指针计算。提示:需要将格雷码转回二进制计算差值。
5. 思考题:Xilinx FPGA的FIFO IP使用什么指针方案?阅读Xilinx PG057文档总结其格雷码实现方式。
🎯 深入掌握了格雷码的数学性质和在异步FIFO中的应用
📍 里程碑:理解了满空判断的严格推导
💡 下一步:异步FIFO完整实现