1. 从一个“伪随机数”的坑说起
几年前做一个嵌入式项目,需要给传感器数据加一点随机扰动,避免多个节点同时上报造成信道拥堵。当时图省事,直接用了标准库的rand(),结果发现每次上电后节点发出的序列完全一样——因为没播种,种子固定为1。后来改成用 ADC 采集悬空引脚的噪声做种子,效果是好了一些,但悬空引脚在某些板子上读出来是稳定的直流电平,序列又退化了。
那会儿我才认真去翻线性反馈移位寄存器(LFSR)的资料。LFSR 这个东西,说白了就是一个用移位和异或搭起来的“伪随机序列发生器”,它不需要乘法器、不需要查表、不需要复杂的状态机,几个触发器加一两个异或门就能跑出周期极长的 0/1 序列。在通信扰码、CRC 校验、BIST 内建自测试、扩频通信、白噪声生成这些场景里,LFSR 几乎是标配。它的核心就两个东西:反馈多项式和初始状态(种子)。多项式选得好,周期能到 2^n - 1;选得不好,跑几十拍就循环了。
这篇文章我打算把 LFSR 从数学原理到 Verilog 实现、从多项式选取到实际踩过的坑,完整地捋一遍。不管你是刚学数字电路的学生,还是正在做通信基带、做 FPGA 的工程师,只要你想把 LFSR 用对、用稳,这篇内容应该都能帮上忙。我会尽量少堆公式,多用“人话”和实际代码来说明问题。
2. LFSR 到底在做什么:核心原理拆解
2.1 移位寄存器 + 反馈 = 状态机
先抛开“线性反馈”这个听起来很唬人的词。LFSR 的本质就是一个有限状态机,状态由寄存器里当前存的那几个 bit 决定。每个时钟沿到来时,所有 bit 向右(或向左)移动一位,空出来的那一位由“反馈函数”算出来填进去。
最简单的形式是这样的:假设有一个 4 位寄存器,初始值1001。每个时钟周期右移一位,最高位由最低位和某个中间位异或得到。比如反馈抽头在第 0 位和第 1 位:
next_bit = reg[0] ^ reg[1] reg = {next_bit, reg[3:1]}跑几拍看看:
| 周期 | 寄存器状态 | 反馈位 |
|---|---|---|
| 0 | 1001 | - |
| 1 | 1100 | 1^0=1 |
| 2 | 0110 | 0^0=0 |
| 3 | 0011 | 0^1=1 |
| 4 | 1001 | 1^1=0 |
第 4 拍回到了1001,周期只有 4。4 位寄存器理论上最多有 15 个非零状态(全零状态是死循环,后面会讲),但这里只用了 4 个,说明反馈抽头选得不好。
这就引出了 LFSR 最核心的问题:抽头怎么选,才能让周期最长?答案就在反馈多项式上。
2.2 反馈多项式:把电路翻译成数学
把上面的电路用多项式表示:寄存器的每一位对应 x 的幂次,最低位是 x^0,最高位是 x^3。反馈抽头在第 0 位和第 1 位,对应的多项式就是:
f(x) = x^4 + x^1 + x^0一般写成f(x) = x^4 + x + 1。这个多项式的“系数”就决定了哪些位参与异或。常数项 1 是必须有的,它代表反馈回路始终存在。
LFSR 的周期由这个多项式决定。如果多项式是本原多项式(primitive polynomial),那么 n 位 LFSR 的周期就是 2^n - 1。比如x^4 + x + 1就是一个 4 次本原多项式,用它做反馈,周期是 15。
为什么是 2^n - 1 而不是 2^n?因为全零状态是吸收态。如果寄存器全是 0,反馈位算出来还是 0,状态永远停在 0,出不来。所以有效的非零状态只有 2^n - 1 个,本原多项式能让 LFSR 遍历所有这些状态。
2.3 斐波那契 vs 伽罗瓦:两种拓扑结构
LFSR 有两种常见的实现结构,名字听起来很学术,其实区别就在异或门放的位置。
斐波那契结构(Fibonacci LFSR),也叫 Many-to-One。所有抽头位的值异或后,反馈到最高位(或最低位)的输入端。上面那个例子就是斐波那契结构。它的特点是:异或门集中在一个地方,抽头多的时候组合逻辑路径会比较长,影响最高工作频率。
伽罗瓦结构(Galois LFSR),也叫 One-to-Many。异或门分散在寄存器链中间,每个抽头位置放一个异或门,反馈位同时送到多个位置。它的特点是:组合逻辑路径短,适合高速设计;但状态和斐波那契结构不是一一对应的,同样的多项式在两种结构下产生的序列是“镜像”关系。
我用一个 4 位、多项式x^4 + x + 1的例子对比一下:
斐波那契(右移,抽头 0 和 1):
always @(posedge clk) begin if (rst) reg <= 4'b1001; else reg <= {reg[0] ^ reg[1], reg[3:1]}; end伽罗瓦(右移,抽头对应 x^1 和 x^0):
always @(posedge clk) begin if (rst) reg <= 4'b1001; else begin reg[3] <= reg[0]; reg[2] <= reg[3]; reg[1] <= reg[2] ^ reg[0]; reg[0] <= reg[1] ^ reg[0]; end end伽罗瓦结构里,reg[0]是反馈源,它同时异或到reg[1]和reg[0]的下一拍值上。实际工程中,如果跑几百 MHz 以上,我一般优先选伽罗瓦结构,时序更容易收敛。
注意:两种结构的多项式表示方式有细微差别。斐波那契结构的多项式直接对应抽头位置;伽罗瓦结构的多项式需要做一点“翻译”,通常是把斐波那契多项式反过来写。选型时别把两者的抽头搞混,否则周期会完全不对。
3. 本原多项式怎么选:从查表到验算
3.1 为什么必须是本原多项式
前面说了,本原多项式保证周期最大。那什么是本原多项式?严格定义是:在 GF(2) 上,一个 n 次不可约多项式,如果它的根是 GF(2^n) 的本原元,它就是本原多项式。这话太绕了,换个说法:
- 不可约:不能分解成两个次数更低的多项式乘积。比如
x^4 + x^2 + 1 = (x^2 + x + 1)^2,可约,不能用。 - 本原:以它为特征多项式的 LFSR,周期达到 2^n - 1。
不是所有不可约多项式都是本原的。比如x^4 + x^3 + x^2 + x + 1是不可约的,但它的周期只有 5,不是本原多项式。
3.2 常用本原多项式速查
实际工程里没人每次都从头推导,都是查表。下面这张表是我常用的几个位宽对应的本原多项式,格式是十六进制掩码,方便直接写进代码:
| 位宽 n | 本原多项式 | 抽头掩码(含 x^n 项) | 周期 |
|---|---|---|---|
| 4 | x^4 + x + 1 | 0x13 | 15 |
| 8 | x^8 + x^6 + x^5 + x^4 + 1 | 0x171 | 255 |
| 16 | x^16 + x^14 + x^13 + x^11 + 1 | 0x1D800 | 65535 |
| 32 | x^32 + x^22 + x^2 + x + 1 | 0x80200003 | 约 4.29e9 |
掩码的用法:最低位对应 x^0,最高位对应 x^n。比如 0x13 = 二进制10011,对应 x^4 + x^1 + x^0,正好是x^4 + x + 1。
对于 32 位,0x80200003展开是 bit31、bit21、bit1、bit0 置位,对应 x^32 + x^22 + x^2 + x + 1。这个多项式在软件实现里很常用,因为抽头少,异或操作快。
3.3 自己验算周期的方法
如果你需要的位宽不在表里,或者想验证某个多项式是不是本原的,可以写个小脚本暴力验算。思路很简单:用该多项式跑 LFSR,看多少拍后回到初始状态。
def lfsr_period(taps, n, seed=1): state = seed period = 0 while True: feedback = 0 for t in taps: feedback ^= (state >> t) & 1 state = ((state << 1) | feedback) & ((1 << n) - 1) period += 1 if state == seed: return period # x^4 + x + 1,抽头 0 和 1 print(lfsr_period([0, 1], 4)) # 输出 15这段代码跑 4 位瞬间出结果。16 位最多跑 65535 次,也很快。32 位就不适合暴力跑了,得用数学方法判断本原性,或者直接查权威表格。
实操心得:我曾经用过一个网上抄来的 16 位多项式,跑出来周期只有 255,查了半天才发现那个多项式是可约的。后来养成习惯,任何新多项式上线前,先用小脚本验一遍周期,确认是 2^n - 1 再用。
4. 从零实现一个可配置的 LFSR 模块
4.1 接口设计与参数化
下面这个 Verilog 模块是我在多个项目里复用过的,支持斐波那契和伽罗瓦两种模式,位宽和抽头都参数化。先看接口:
module lfsr #( parameter WIDTH = 16, parameter TAPS = 16'hD800, // 抽头掩码,不含 x^n 项 parameter SEED = 16'hACE1, parameter GALOIS = 1 // 1=伽罗瓦,0=斐波那契 )( input wire clk, input wire rst_n, input wire enable, output wire [WIDTH-1:0] dout );TAPS参数用掩码表示,比如 16 位多项式x^16 + x^14 + x^13 + x^11 + 1,去掉 x^16 项后,抽头是 bit14、bit13、bit11、bit0,掩码就是0x6801。这样写比用数组更紧凑,综合工具也能直接优化。
4.2 斐波那契结构的实现
斐波那契结构逻辑最直观:把所有抽头位异或,结果移入最高位。
generate if (!GALOIS) begin : fib wire feedback; assign feedback = ^(dout & TAPS); // 按位与后做归约异或 always @(posedge clk or negedge rst_n) begin if (!rst_n) reg_fib <= SEED; else if (enable) reg_fib <= {reg_fib[WIDTH-2:0], feedback}; end assign dout = reg_fib; end^(dout & TAPS)这个写法很妙:dout & TAPS把非抽头位清零,然后归约异或^把所有位异或起来,等价于只对抽头位做异或。综合出来就是一棵异或树,比写一堆^更简洁。
4.3 伽罗瓦结构的实现
伽罗瓦结构需要逐位处理,因为异或门分散在链中间。下面是一个通用的写法:
else begin : gal reg [WIDTH-1:0] reg_gal; wire fb = reg_gal[0]; // 反馈源是最低位 integer i; always @(posedge clk or negedge rst_n) begin if (!rst_n) reg_gal <= SEED; else if (enable) begin reg_gal[WIDTH-1] <= fb; for (i = 0; i < WIDTH-1; i = i + 1) begin if (TAPS[i]) reg_gal[i] <= reg_gal[i+1] ^ fb; else reg_gal[i] <= reg_gal[i+1]; end end end assign dout = reg_gal; end endgenerate注意伽罗瓦结构的抽头掩码和斐波那契不完全一样。对于同一个多项式,伽罗瓦结构通常把抽头位置“镜像”过来。比如斐波那契用 bit14、bit13、bit11、bit0,伽罗瓦可能用 bit1、bit3、bit4、bit15。具体对应关系取决于移位方向,实际使用时最好用仿真确认周期。
4.4 仿真验证:周期和序列正确性
写完模块,第一件事是跑仿真确认周期。下面是一个简单的 testbench 片段:
initial begin rst_n = 0; #10 rst_n = 1; enable = 1; #1000000; $finish; end // 在仿真中记录状态,检查是否回到 SEED always @(posedge clk) begin if (dout == SEED && past_reset) $display("Period detected at time %t", $time); end对于 16 位 LFSR,周期应该是 65535。仿真跑 70000 个周期,看是否在 65535 拍后回到种子。如果提前回到种子,说明多项式不是本原的,或者抽头掩码写错了。
踩坑记录:伽罗瓦结构的抽头掩码我曾经直接照搬斐波那契的,结果周期只有几百。后来用 Python 脚本把两种结构的抽头对应关系算出来,才发现伽罗瓦的抽头是斐波那契的“反向”。具体来说,如果斐波那契抽头是 T,伽罗瓦的抽头掩码是 T 的位反转再右移一位。这个细节很多资料都不提,但实际写代码时非常关键。
5. 常见问题与排查技巧实录
5.1 为什么我的 LFSR 周期不对
这是最常见的问题,原因通常有三个:
第一,多项式不是本原的。很多人从网上随便抄一个多项式,没验证就用。比如x^8 + x^4 + x^3 + x^2 + 1看起来挺像样,但它不是本原的,周期只有 51。解决办法:用 3.3 节的脚本验一遍,或者查权威的本原多项式表。
第二,抽头掩码写错。比如把 bit0 漏掉了,或者把 x^n 项也算进去了。掩码里不应该包含 x^n 项,因为 x^n 对应的是反馈位本身,不是寄存器里的位。检查方法:把掩码打印出来,数一数置位的个数,和多项式里的项数对比。
第三,全零状态。如果种子设成了 0,LFSR 永远出不来。解决办法:种子必须非零。可以在复位时加一个判断,如果种子是 0,自动改成 1。
5.2 伽罗瓦和斐波那契的序列为什么不一样
同一个多项式,两种结构产生的序列是“镜像”关系,不是完全一样。具体来说,斐波那契结构从最高位输出,伽罗瓦结构从最低位输出,两者输出的序列在时间上是反的。如果你需要和某个标准协议对接,一定要确认协议用的是哪种结构,否则对不上。
5.3 高速设计下的时序问题
斐波那契结构在抽头多的时候,异或树的延迟会成为关键路径。比如 32 位 LFSR 有 4 个抽头,异或树是 3 级,在 28nm 工艺下大概能跑 500 MHz 左右。如果要求更高频率,有两个办法:
- 换成伽罗瓦结构,异或门分散,关键路径只有一级异或。
- 在斐波那契结构里插入流水线寄存器,把异或树切成两级。代价是输出序列会延迟一拍,需要做相位对齐。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 周期远小于 2^n-1 | 多项式非本原 | 用脚本验算周期 | 换本原多项式 |
| 周期只有 1 | 种子为 0 | 检查复位值 | 种子设为非零 |
| 序列和预期不符 | 抽头掩码错误 | 打印掩码对比多项式 | 修正掩码 |
| 伽罗瓦结构周期不对 | 抽头未做镜像 | 对比两种结构抽头 | 按镜像关系重算 |
| 高速下时序违例 | 异或树太长 | 看时序报告 | 换伽罗瓦或插流水线 |
| 输出全 1 或全 0 | 反馈逻辑被综合优化掉 | 检查综合日志 | 加keep属性或改写法 |
独家技巧:在 FPGA 上调试 LFSR 时,可以用 ILA(集成逻辑分析仪)抓取寄存器状态,设置触发条件为“状态等于种子”,这样能直接看到周期。比跑仿真快得多,尤其适合在板级调试阶段快速定位问题。
6. 几个真实场景下的 LFSR 应用
6.1 通信扰码:让数据频谱更“白”
在串行通信里,如果发送的数据长期是 0 或 1,接收端的时钟恢复电路会失锁。解决办法是用 LFSR 生成一个伪随机序列,和发送数据异或,把数据“打散”。接收端用同样的 LFSR 再异或一次,就能恢复原始数据。
这个场景对 LFSR 的要求是:收发双方必须严格同步。通常的做法是发送端在帧头里插入一个已知的同步字,接收端检测到同步字后,把 LFSR 复位到约定种子,之后就能逐位对齐。如果中间丢了一位,整个后续数据都会错,所以通信协议里通常还有 CRC 做校验。
6.2 CRC 校验:LFSR 的“变体”
CRC 的本质就是一个带输入数据的 LFSR。普通 LFSR 的反馈只来自寄存器内部,CRC 的反馈还异或了输入数据位。多项式选得好,CRC 能检测出所有单比特错、双比特错、奇数个错,以及大部分突发错。
以 CRC-16/CCITT 为例,多项式是x^16 + x^12 + x^5 + 1,对应掩码0x1021。实现时,每个时钟周期把数据位异或进反馈路径:
wire fb = crc[15] ^ data_bit; crc <= {crc[14:0], 1'b0} ^ (fb ? 16'h1021 : 16'h0000);这种写法比逐位异或更高效,综合出来是一条异或链加一个条件异或。实际工程中,CRC 通常用查表法在软件里实现,但在高速硬件里,LFSR 形式的 CRC 更常见。
6.3 BIST 内建自测试:用 LFSR 生成测试向量
芯片流片后要做自测试,需要给被测电路灌入大量测试向量。用 LFSR 生成伪随机向量,比用 ROM 存储固定向量省面积。LFSR 的周期足够长,能覆盖大部分故障模型。
这个场景对 LFSR 的要求是:种子和多项式要选得让向量覆盖率高。通常会用多个不同种子的 LFSR 并行,或者用“加权 LFSR”调整 0/1 的比例。如果被测电路对某些位有偏置要求,还会用“相位偏移”技术,从同一个 LFSR 的不同抽头取输出,生成多个不相关的序列。
6.4 白噪声生成:音频和射频里的应用
在音频处理里,LFSR 生成的伪随机序列经过低通滤波,可以做成“白噪声”用于测试或音效。在射频里,LFSR 用于生成扩频码,比如 GPS 的 C/A 码就是一个 10 位 LFSR,多项式是x^10 + x^3 + 1,周期 1023。
这个场景对 LFSR 的要求是:序列的统计特性要好。本原多项式生成的序列在周期内 0 和 1 的个数只差 1,自相关函数接近冲激函数,互相关也小。如果多项式选得不好,序列会有周期性成分,听起来像“嗡嗡”声而不是“沙沙”声。
7. 写在最后:一些个人体会
LFSR 这个结构,看起来简单,但真正用起来,细节非常多。我踩过的坑包括:多项式抄错、抽头掩码漏位、伽罗瓦和斐波那契搞混、种子设成 0、高速下时序不收敛。每一个坑都让我多花半天到一天去排查。
现在我的习惯是:任何 LFSR 上线前,先用 Python 脚本验周期,再用 Verilog 仿真跑 2^n 个周期确认,最后在板子上用 ILA 抓一次实际波形。三步走完,基本不会出问题。
另外,如果你要做的是安全相关的应用,比如加密或认证,LFSR 本身是不够的。它的线性特性意味着,只要知道 2n 个连续输出位,就能通过解线性方程组推算出整个序列。所以 LFSR 通常只用于扰码、测试、扩频这些非安全场景。真要做加密,得用非线性反馈移位寄存器(NFSR)或者分组密码。
最后分享一个小技巧:如果你需要多个不相关的伪随机序列,不用实例化多个 LFSR,可以从同一个 LFSR 的不同抽头取输出。比如一个 32 位 LFSR,取 bit0、bit7、bit15、bit23 作为四个独立输出,它们之间的互相关很小,足够应付大多数测试场景。这样省面积,也省功耗。