手撕代码

宝鼎茶闲烟尚绿,幽窗棋罢指犹凉

Posted by George Lin on December 1, 2025

以下内容参考自多平台,如有侵权,联系删除

二进制vs格雷码转换

部分内容搬运自🔥🗓手撕代码:二进制vs格雷码转换 - 小红书

核心问题:为什么二进制移位异或之后,就会变成只有1bit会发生变化的格雷码?

简答:异或记录的不是数字本身,而是数字变化的边界。核心是比如4b0111加一变成4b1000,后面三位中每连续的两位都是一致的,异或出来的结果也是一致的,只有到了变化边界这里,才会出现变化。

image-20251201103733175

image-20251201103900967

image-20251201104258129

image-20251201104317945

模三检测:使用verilog代码,设计电路,判断输入序列能否被三整除,能的时候输出1,不能的时候输出0。 当输入序列中存在模三余数为0的子序列时,检测器会输出一个逻辑“1”信号;否则,输出逻辑“0”信号。

这是一个使用有限状态机 (FSM) 实现的同步电路,用于判断串行输入的二进制序列中是否存在一个连续的子序列,其代表的十进制数值能够被 3 整除。一旦找到,输出 (sub_div_3_found) 将保持高电平。

1. 设计原理

我们采用 MSB 在前的串行输入方式。FSM 的状态跟踪的是从序列开始到当前位结束前缀的模 3 余数 $R$。

\[R_{\text{next}} = (2 \cdot R_{\text{current}} + \text{data\_in}) \pmod 3\]
状态 编码 意义 (当前前缀 (mod3))
S0 2'b00 余数 $R = 0$
S1 2'b01 余数 $R = 1$
S2 2'b10 余数 $R = 2$

2. 子序列检测逻辑

电路必须检查所有以当前输入位结束的子序列

  1. 完整前缀序列: 如果新的前缀值 $\pmod 3 = 0$ (即 next_state == S0),则找到了一个子序列。
  2. 长度为 1 的子序列: 如果当前输入位 $\text{data_in} = 0$,则 $0 \pmod 3 = 0$,找到了一个子序列。

只要以上任一条件满足,输出寄存器 (sub_div_3_found) 就会被置位并保持为 1

3. Verilog 代码

Verilog

`timescale 1ns / 1ps

// 设计名称: Divisible_by_3_Subsequence_Detector

module Divisible_by_3_Subsequence_Detector (
    input wire clk,          // 时钟信号
    input wire rst_n,        // 同步复位信号 (低电平有效)
    input wire data_in,      // 串行输入数据 (MSB在前)
    output reg sub_div_3_found // 输出:存在模3余数为0的子序列时为1
);

    // 状态定义
    localparam [1:0] S0 = 2'b00; // 余数 0
    localparam [1:0] S1 = 2'b01; // 余数 1
    localparam [1:0] S2 = 2'b10; // 余数 2

    reg [1:0] current_state;
    reg [1:0] next_state;

    // --- 组合逻辑: 状态跳转逻辑 (Next State Logic) ---
    always @(*) begin
        next_state = current_state; // 默认保持

        case (current_state)
            S0: begin // R=0: R_next = (0*2 + data_in) mod 3
                if (data_in == 1'b0) next_state = S0; // 0 -> 0
                else                  next_state = S1; // 1 -> 1
            end
            S1: begin // R=1: R_next = (1*2 + data_in) mod 3
                if (data_in == 1'b0) next_state = S2; // 2 -> 2
                else                  next_state = S0; // 3 -> 0
            end
            S2: begin // R=2: R_next = (2*2 + data_in) mod 3
                if (data_in == 1'b0) next_state = S1; // 4 -> 1
                else                  next_state = S2; // 5 -> 2
            end
            default: next_state = S0;
        endcase
    end

    // --- 时序逻辑: 状态寄存器和输出寄存器 ---
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            current_state       <= S0;    // 复位:余数 0
            sub_div_3_found     <= 1'b0;  // 复位:未找到
        end else begin
            // 更新主序列状态
            current_state <= next_state;

            // -----------------------------------------------------------
            // 输出逻辑 (记忆功能)
            // -----------------------------------------------------------
            // 如果找到子序列能被3整除,输出置 1 并保持。

            if (next_state == S0 || data_in == 1'b0) begin
                // 条件1: 完整序列能被3整除 (next_state = S0)
                // 条件2: 长度为1的子序列能被3整除 (data_in = 0)
                sub_div_3_found <= 1'b1;
            end
            // 否则,保持 sub_div_3_found 的值 (即保持 0 或保持 1)
        end
    end

endmodule

无毛刺时钟切换电路

参考时钟切换电路(无毛刺)——clock switching glitch free - shroud404 - 博客园

总结:无毛刺时钟切换电路的要点是:1. 先屏蔽旧时钟,再打开新时钟:为此采用一级寄存器寄存sel; 2. 防止在时钟高电平时切换:为此采用的寄存器是下降沿触发的。3. 做一些异步处理:在前面再插一级寄存器。4. 寄存器初值要设置成0,要不然假如其中一个时钟不work了,会输出一直是1

随着越来越多的多频时钟被应用在今天的芯片中,尤其是在通信领域中,经常需要在芯片运行时切换时钟线的源时钟。这通常是通过在硬件中复用两个不同的频率时钟源,并通过内部逻辑控制多路选择器选择线来实现的。

这两个时钟频率可能彼此完全无关联,或者它们可以是彼此之间存在倍数的关系。在这两种情况下,都有可能在切换时在时钟线上产生毛刺(glitch)。时钟线上的毛刺对整个系统来说是十分危险的,因为它可以被一些寄存器解释为捕获时钟边缘(满足建立时间等),而其他寄存器忽略此毛刺,则整个系统数据出现混乱。

本文将会介绍两种时钟切换方法,分别对应两种情况,第一种时两个时钟源的频率呈倍数关系,第二种是两个时钟源完全没有关系。

1.时钟切换的毛刺问题

切换电路:

img

图1:实时时钟切换

 电路语言描述:

 assign outclk = (clk1 & select) | (~select & clk0);

  当SELECT变化时,可能会由于从当前时钟源的输出立即切换到下一个时钟源而引起毛刺。当前时钟(Current Clock)是当前SELECT选择的时钟源,而下一个时钟(Next Clock)是对应于新SELECT值的时钟源。

img

图2:实时时钟切换时序

  仿真结果:

img

图3:实时时钟切换仿真

 图2中的时序图和图3的仿真结果显示了当SELECT控制信号发生变化时,输出时钟如何产生毛刺(glitch)。这种切换导致的问题是切换控制信号(SELECT)可以相对于源时钟的任何时间发生改变(本质是SELECT信号完全异步),从而产生了切断输出时钟或在输出处产生毛刺的潜在可能。SELECT控制信号最有可能是由两个源时钟中的任一个驱动的寄存器生成的,这意味着它要么与两个时钟具有已知的时序关系,要么这两个时钟是彼此的倍数,或者如果源时钟不存在任何的关系,则它可能与至少一个时钟异步。

  在不知道这些时钟的频率或相位关系的情况下,需要避免在任一时钟的高状态期间进行切换。固定延迟可用于引起两个源时钟的开始和停止时间之间的间隔,但仅当两个时钟源之间存在固定关系时可以使用。它不能在输入频率未知或时钟不相关的情况下使用。

  在图4中给出了防止时钟切换导致输出毛刺的解决方案,其中两个时钟源频率成倍数关系。在每个时钟源的选择路径中插入下降沿触发的D触发器。在时钟的下降沿上用寄存器寄存一下SELECT控制信号,以及仅在其他时钟被取消选择之后才启用选择(既先屏蔽旧时钟,然后在开启新时钟),从而在输出端防止毛刺的产生。

  在时钟的下降沿处寄存SELECT信号,保证在任意一个时钟处于高电平时,时钟输出(CLOCK_OUT)中不发生变化,从而防止对输出时钟进行切割(Chopping)。从一个时钟选择到另一个时钟的反馈使得在开始下一个时钟的传播之前必须等待当前时钟的取消,从而避免任何毛刺的产生。

img

图5:相关时钟无毛刺切换电路

img

图6:相关时钟无毛刺切换时序

 电路语言描述:

复制代码

  reg     out1;
  reg     out0;
  always @(negedge clk1 or negedge rst_n)begin
      if(rst_n == 1'b0)begin
          out1 <= 0;
      end
      else begin
          out1 <= ~out0 & select;
      end
  end
  
  
  always @(negedge clk0 or negedge rst_n)begin
      if(rst_n == 1'b0)begin
          out0 <= 0;
      end
      else begin
          out0 <= ~select & ~out1;
      end
  end
  
  assign outclk = (out1 & clk1) | (out0 & clk0);

复制代码

  图6显示了SELECT信号从0到1的转换时,首先在CLK0的下降沿时停止了CLK0的输出,然后在CLK1的下降沿处的开始输出CLK1时钟到OUT CLOCK。

  仿真结果:

img

img

  亚稳态问题:

  在该电路中,有三个时序路径需要特别考虑:SELECT控制信号到两个下降沿触发触发器中的任一个、DFF0输出到DFF1的输入以及DFF1的输出到DFF0的输入。如果在这三个路径中的任何一个信号与目标触发器时钟的捕获边缘(这里是下降沿)同时变化,那么该寄存器的输出可能变为亚稳态,这意味着它可能进入理想的“1”和理想的“0”之间的状态。时钟多路复用器和另一触发器的使能反馈可以对亚稳态进行不同的解释。因此在异步接口中,需要把两个触发器的捕获边沿和SELECT信号的变换沿(SELECT信号的上升沿)分开,避免亚稳态的产生。这可以容易地通过使用适当的多周期保持约束或最小延迟约束(时序约束)来实现,因为两个时钟之间的时序关系是已知的。

3.时钟容错

  在芯片启动时间,两个触发器DFF0和DFF1都应该重置为“0”状态,使得时钟中的任何一个都不被作为初始传播。通过在“零”状态下启动触发器,将容错建立在时钟切换中。

  假设其中一个时钟由于启动时的故障而没有切换。如果与故障时钟相关联的触发器已在“1”状态启动,则它将阻止选择其他时钟作为下一个时钟,并且由于缺少运行时钟,其自身状态不可改变。通过以“零”状态启动两个触发器,即使其中一个源时钟未运行,仍然能够将另一个好的时钟传播到开关的输出,保证输出时钟的稳定。

4.非相关时钟切换的毛刺避免Glitch protection for unrelated clock sources

  上述避免时钟切换输出处的毛刺的方法需要两个时钟源彼此的倍数关系,使得用户可以避免信号与任一时钟域异步。 但在该实现中没有处理异步信号的机制(上面的办法只是通过时序约束解决异步的问题,并没有真正解决异步的问题)。这引出了实现具有同步器电路的时钟切换的第二种方法,以避免由异步信号引起的潜在的亚稳态。 当两个时钟源彼此完全无关时,异步发送的源头可以是SELECT信号或从一个时钟域到另一个时钟域的反馈。

  第二种方法是针对两个异步时钟源的切换,这个方法是在第一种方法的基础上,在选择路径上再插入一个上升沿触发D触发器,这是为了针对对两个异步时钟源产生的反馈信号以及异步信号SELECT,对选择信号进行同步处理,这样即使是两个异步的时钟源进行切换,也可以避免亚稳态的产生。同步器只是两级触发器,其中第一级通过锁定数据来帮助稳定数据,然后将数据传递到下一级,由电路的其余部分解释。

img

图7:非相关时钟切换毛刺避免电路

img

图8:非相关时钟切换毛刺避免时序

 电路语言描述:

复制代码

reg clk0_f , clk0_ff;
reg clk1_f , clk1_ff;

always@(posedge clk0 or negedge rst_n)begin
    if (rst_n == 1'b0)
        clk0_f <= 1'b0;
    else
        clk0_f <= (~select) & (~clk1_ff);
end
always@(negedge clk0 or negedge rst_n)begin
    if (rst_n == 1'b0)
        clk0_ff <= 1'b0;
    else
        clk0_ff <= clk0_f;
end

always@(posedge clk1 or negedge rst_n)begin
    if (rst_n == 1'b0)
        clk1_f <= 1'b0;
    else
        clk1_f <= (select) & (~clk0_ff);
end
always@(negedge clk1 or negedge rst_n)begin
    if (rst_n == 1'b0)
        clk1_ff <= 1'b0;
    else
        clk1_ff <= clk1_f;
end

assign clk_out = (clk0_ff & clk0) | (clk1_ff & clk1);

复制代码

  仿真电路:

img

  仿真结果:

img

图9:非相关时钟切换毛刺避免仿真结果

  仿真结果十分明显。

5.结论

  通过使用本文中介绍的方法,可以通过非常小的开销来避免在时钟源之间切换时在时钟线上产生毛刺的危险。 这些技术完全可扩展,可以扩展到时钟切换两个以上的时钟。 对于多个时钟源,每个时钟源的选择信号将通过所有其他源的反馈启用。

参考:https://www.eetimes.com/techniques-to-make-clock-switching-glitch-free/ Techniques to make clock switching glitch-free

固定优先级仲裁器

以下内容搬运自仲裁器设计(一) – Fixed Priority Arbiter

总结:1. 可以用casez实现;2.如果追求扩展性,可以用for循环从低位到高位寻找第一个1;3. 如果追求简洁,可以直接让req和其补码相与,就可以得到grant的独热码(低位高优先级)

如果我们要设计一个fixed priority arbiter,输入是一个multibit request,每一个bit代表一个模块的request, 输出一个multibit grant,每个bit代表给对应的模块的grant信号。我们可以把优先级这样安排,最低位优先级最高,最高位优先级最低。

我们先以3个模块产生request为例,大家一般在面试的时候都会碰到给定模块数目,比如3,让你设计。咱们就直接上code来表示一种写法

1 module fixed_prio_arb

2 (

3 input [2:0] req,

4

5 output logic [2:0] grant

6 );

7

8 always_comb begin

9 case (1’b1)

10 req[0]: grant = 3’b001;

11 req[1]: grant = 3’b010;

12 req[2]: grant = 3’b100;

13 default:grant = 3’b000;

14 endcase

15 end

16

17 endmodule: fixed_prio_arb

这里的技巧是利用verilog中的case语句,可以比用if else简洁,而且利用了case里的按顺序evaluate语法规则来实现了优先级。这里多说一句给verilog的初学者,尽管verilog和C看起来很像,很多关键字都是一样的,比如case,但是verilog的case和C的case是不一样的,verilog的case自带”break”,即当一个condition满足之后,就只会执行这一条冒号之后的,只有input 发生变化之后才会再次evaluate,因为这是描述硬件电路。而C语言的case里如果一条满足之后会按照顺序继续往下执行,如果下一个条件依然满足,那么就会跳到下一个条件里,所以C的case语句我们通常要加break。

如何设计一个参数化的模块。对于上面的仲裁器来说,我们希望可以参数化产生请求的个数,即设计下面的模块

1 module priority_arbiter #(

2 parameter REQ_WIDTH = 16

3 )(

4 input [REQ_WIDTH-1:0] req,

5 output [REQ_WIDTH-1:0] gnt

6 );

这样我们可以根据不同场合产生request的模块的个数来例化同一个仲裁器,只需要改变参数即可。很明显,上面利用case的写法不能scalable,我们需要用更加巧妙的办法。

首先可以想到的办法是利用for循环,思路其实非常直接,从低位到高位依次去判断,借助一个pre_req来记录低位是否已经有了request, 如果第i位有了request,那么第i+1位一直到最高位的pre_req都是1。

1 module prior_arb #(

2 parameter REQ_WIDTH = 16

3 )(

4 input logic [REQ_WIDTH-1:0] req,

5 output logic [REQ_WIDTH-1:0] grant

6 );

7

8 logic [REQ_WIDTH-1:0] pre_req;

9

10 always_comb begin

11 pre_req[0] = req[0];

12 grant[0] = req[0];

13 for (int i = 1; i < REQ_WIDTH; i = i + 1) begin

14 grant[i] = req[i] & !pre_req[i-1]; // current req & no higher priority request

15 pre_req[i] = req[i] pre_req[i-1]; // or all higher priority requests

16 end

17 end

18

19 endmodule

有没有更简洁的办法呢?先来上面的设计的变体,但是不用for循环,本质上是一样的,只有3行code。

1 module prior_arb #(

2 parameter REQ_WIDTH = 16

3 )(

4 input [REQ_WIDTH-1:0] req,

5 output [REQ_WIDTH-1:0] gnt

6 );

7

8 logic [REQ_WIDTH-1:0] pre_req;

9

10 assign pre_req[0] = 1’b0;

11

12 assign preq_req[REQ_WIDTH-1:1] = req[REQ_WIDTH-2:0] pre_req[REQ_WIDTH-2:0];

13

14 assign gnt = req & ~pre_req;

15

16 endmodule

下面的这种实现方式就更夸张了,就一行实现,但是它背后的思想却非常朴素,简直不超过小学一年级的知识。

1 module prior_arb #(

2 parameter REQ_WIDTH = 16

3 ) (

4 input [REQ_WIDTH-1:0] req,

5 output [REQ_WIDTH-1:0] gnt

6 );

7

8 assign gnt = req & (~(req-1));

9 endmodule

本质上,我们要做的是找req这个信号里从低到高第一个出现的1,那么我们给req减去1会得到什么?假设req的第i位是1,第0到第i-1位都是0,那么减去1之后我们知道低位不够减,得要向高位借位,直到哪一位可以借到呢?就是第一次出现1的位,即从第i位借位,第0到i-1位都变成了1,而第i位变为了0,更高位不变。然后我们再给减1之后的结果取反,然后把结果再和req本身按位与,可以得出,只有第i位在取反之后又变成了1,而其余位都是和req本身相反的,按位与之后是0,这样就提取出来了第一个为1的那一位,也就是我们需要的grant。再考虑一下特殊情况req全0,很明显,按位与之后gnt依然都是全0,没有任何问题。

聪明的同学可能已经联想到,减1再取反,这不是计算2的补码的算法吗?只不过我们书本上学到的给一个数求2的补码的方法是取反再加1,这里倒过来,减1再取反,本质上是一样的。这其实是2的补码的一个特性,即一个数和它的补码相与,得到的结果是一个独热码,独热码为1的那一位是这个数最低的1。所以这个仲裁器的设计方法用一句话概括:request和它的2的补码按位与

轮询仲裁器(Round-Robin Arbiter)

(71 封私信 / 80 条消息) 循环优先仲裁器(Round Robin Arbiter) - 知乎

总结:看了一圈,这个解法是最直观的。首先分区,按照最近一次grant的位置,将request分为左右两区,方法是左移一位再减一得到一个mask。接着找左半区从右往左数第一个出现的1.如果找不到,接着找右半区从右往左数第一个出现的1,方法都是和补码(取反加一或者减一取反)相与

实现方式

module arb_rr (
	input [3: 0] req,
	input [3: 0] arb_curr,
	output [3: 0] arb_next
);

assign arb_tmp = (arb_curr << 1) - 1
assign req_high = arb_tmp & req;
assign req_low  = ~arb_tmp & req;
assign req_mask = (|req_high)? req_high: req_low;

assign arb_next = req_mask & (~req_mask + 1);

endmodule

RR 仲裁问题的处理难点在于经过仲裁后,主机的优先级变得整体不连续,以上一轮输出的主机为界,主机左侧和右侧内部优先级连续,因此解法分为 2 步:(1)将不连续优先级转化为连续优先级;(2)在优先级连续的情况下,寻找优先级最高的输出项。

image-20251201144821454

image-20251201144836821

还有另外一种思路,直接用固定优先级仲裁器来实现轮询仲裁器,如下:

回想一下我们之前讲的Fixed Priority Design,我们都假定了从LSB到MSB优先级是由高到低排列的。那么我们有没有办法先设计一个fixed priority arbiter,它的优先级是一个输入呢?看下面的RTL

1 module arbiter_base #(parameter NUM_REQ = 4)

2 (

3 input [NUM_REQ-1:0] req,

4 input [NUM_REQ-1:0] base,

5 output [NUM_REQ-1:0] gnt

6 );

7

8 wire[2*NUM_REQ-1:0] double_req = {req,req};

9

10 wire[2*NUM_REQ-1:0] double_gnt = double_req & ~(double_req - base);

11

12 assign gnt = double_gnt[NUM_REQ-1:0] double_gnt[2*NUM_REQ-1:NUM_REQ];

13 endmodule

在这个模块中,base是一个onehot的信号,它为1的那一位表示这一位的优先级最高,然后其次是它的高位即左边的位,直到最高位后回到第0位绕回来,优先级依次降低,直到为1那一位右边的这位为最低。咱们以4位为例,如果base = 4’b0100, 那么优先级是bit[2] > bit[3] > bit[0] > bit[1]。

这个设计的思路和前一篇仲裁器设计(一) – Fixed Priority Arbiter最后那个1行设计的思路很像,里面double_req & ~(double_req-base)其实就是利用减法的借位去找出base以上第一个为1的那一位,只不过由于base值可能比req值要大,不够减,所以要扩展为{req, req}来去减。当base=4‘b0001的时候就是咱们上一篇里面的最后的算法。当然base=4’b0001的时候不存在req不够减的问题,所以不用扩展。

行波进位、超前进位、分组超前进位加法器

image-20251201145857903

image-20251201145919935

image-20251201145934743

image-20251201145952423

偶数分频、奇数分频、分数分频

芯日记 - 小红书

image-20251201152218635

image-20251201152258596

image-20251201152317402

image-20251201152332470

image-20251201152346099

image-20251201152409551

异步FIFO深度问题

例:一个 8bit宽的异步 FIFO,输入时钟为 100MHz,输出时钟为 95MHz,设一个 package 为4Kbit,且两个 package 之间的发送间距足够大。问异步 FIFO 的深度

答:100m存和95m时钟读相当于每输入20个数据就得剩下一个数据,那4kbit数据8位应该传512个数据,那么512除20个等于26,向上取整,所以最后fifo深度应该是26。再考虑一下异步FIFO的结构,写地址同步到读地址至少两拍,读地址再同步到写地址两拍。考虑异步拍数不准按极限算32,但是咱们搞工程的宁可资源多用一倍,也不担一点风险,用64深度差不多

1个32位加法器如何实现2个16位加法器

📃💯每日一题:1个32加法器实现2个16位 - 小红书

强制A1[15]和B1[15]输入为0,使其进位不影响A2和B2的和,然后手搭1bit全加器实现A1和B1的进位

image-20251201160733315