SpinalHDL中的Clock domains时钟域学习

首先我们来看一下官方的文档中Introduction对时钟域的解释

在这里插入图片描述

下面我们来解释一下:

​ 在SpinalHDL中,时钟(clock)和复位(reset)信号可以结合在一起创建一个时钟域(clock domain)。时钟域可以应用于设计的某些区域,然后在这些区域中实例化的所有同步元件将隐式地使用该时钟域。

​ 时钟域应用的工作方式类似于堆栈,这意味着如果你处于某个时钟域中,仍然可以在本地应用另一个时钟域。

​ 请注意,寄存器在创建时捕获其时钟域,而不是在分配时。因此,请确保在所需的ClockingArea内创建它们。

​ 最有一句是什么意思呢,其实是指在SpinalHDL中,当你创建一个寄存器(register)时,它会被分配到一个特定的时钟域(clock domain),而这个时钟域是在创建寄存器时确定的,而不是在给寄存器赋值时确定的。

换句话说,当你使用SpinalHDL语言创建一个寄存器时,你需要明确指定该寄存器所属的时钟域。这是因为时钟域决定了寄存器的时钟信号来源以及与之相关的时序逻辑。寄存器会在创建时与特定的时钟域相关联,然后在该时钟域的时钟边沿触发时,寄存器会根据输入信号更新其存储值。因此,要确保寄存器正确地与所需的时钟域关联,你需要在创建寄存器时将其放置在正确的时钟域(ClockingArea)中。这样可以确保寄存器在设计中的正确时钟域中操作,并与其他相关元件同步工作。

1.1 时钟域的定义

定义时钟域的语法如下:

ClockDomain(
  clock: Bool
  [,reset: Bool]
  [,softReset: Bool]
  [,clockEnable: Bool]
  [,frequency: IClockDomainFrequency]
  [,config: ClockDomainConfig]
)

参数说明:

ArgumentDescriptionDefault
clock定义域的时钟信号必选
reset复位信号。如果存在需要重置的寄存器,而时钟域没有提供,则会显示错误消息null
softReset复位,它推断一个额外的同步复位null
clockEnable此信号的目标是禁用整个时钟域上的时钟,而无需在每个同步元素上手动实现null
frequency允许您指定给定时钟域的频率,然后在设计中读取它UnknownFrequency
config指定信号的极性和信号的性质Current config

下面一个例子来说明如何定义一个时钟域

 import spinal.core._
 class DemoClockDomain extends Component {
   val io = new Bundle{
     val myClk = in Bool()
     val myRst = in Bool()
   }
   noIoPrefix() // 去掉生成代码的io前缀
   // Define a new clock domain
   val clkDomain = ClockDomain(io.myClk,io.myRst)
   // Use this domain in an area of the design
   val clkArea = new ClockingArea(clkDomain) {
     val a = UInt(8 bits)
     val b = RegNext(a) init(0)
   }
 }
 
 object DemoClockDomain extends App{
   SpinalConfig(targetDirectory = "rtl").generateVerilog(new DemoClockDomain)
 }

生成的verilog代码如下:

module DemoClockDomain (
  input               myClk,
  input               myRst
);

  wire       [7:0]    clkArea_a;
  reg        [7:0]    clkArea_b;

  always @(posedge myClk or posedge myRst) begin
    if(myRst) begin
      clkArea_b <= 8'h0;
    end else begin
      clkArea_b <= clkArea_a;
    end
  end
endmodule

这里生成了一个异步高复位的代码

除了构造函数参数之外,每个时钟域的以下元素都可以通过ClockDomainConfigclass进行配置:

PropertyValid values
clockEdgeRISING, FALLING
resetKindASYNC, SYNC, and BOOT which is supported by some FPGAs (where FF values are loaded by the bitstream)
resetActiveLevelHIGH, LOW
softResetActiveLevelHIGH, LOW
clockEnableActiveLevelHIGH, LOW

可以通过ClockDomainConfigclass来配置时钟的边沿是上升还是下降,以及是同步复位还是异步复位等等

在这里插入图片描述

假设想要生成异步低复位的代码,只需要按照下面的参数进行配置

 object DemoClockDomain extends App{
   val clkCfg = ClockDomainConfig(clockEdge=RISING,resetKind= ASYNC,resetActiveLevel = LOW)
   SpinalConfig(defaultConfigForClockDomains = clkCfg,targetDirectory = "rtl").generateVerilog(new DemoClockDomain)  
 }

加上这两行代码之后,生成的verilog代码如下:

module DemoClockDomain (
  input               myClk,
  input               myRst
);

  wire       [7:0]    clkArea_a;
  reg        [7:0]    clkArea_b;

  always @(posedge myClk or negedge myRst) begin
    if(!myRst) begin // 异步低复位
      clkArea_b <= 8'h0;
    end else begin
      clkArea_b <= clkArea_a;
    end
  end

endmodule

当然,配置代码也可以写在时钟域里面

在这里插入图片描述

1.2 internal clock 内部时钟

ClockDomain.internal(
  name: String,
  [config: ClockDomainConfig,]
  [withReset: Boolean,]
  [withSoftReset: Boolean,]
  [withClockEnable: Boolean,]
  [frequency: IClockDomainFrequency]
)
ArgumentDescriptionDefault
name时钟信号或者复位信号的名字
config指定信号的极性和复位的性质Current config
withReset添加一个复位信号true
withSoftReset添加软复位信号false
withClockEnable添加时钟启动false
frequency时钟域的频率UnknownFrequency

下面我们直接来看例子

import spinal.core._

class Pll() extends Component {
  val io = new Bundle{
    val clkIn = in Bool()
    val clockOut = out Bool()
    val reset = out Bool()
  }
  io.clockOut := io.clkIn
  io.reset := True
}

class InternalClockWithPllExample extends Component {
  val io = new Bundle {
    val clk100M = in Bool()
    val aReset  = in Bool()
    val result  = out UInt (4 bits)
  }
  // define a internal clockdomain
  val myClockDomain = ClockDomain.internal("myClockName") // 在这里没有指定时钟和复位,而是使用锁相环的输出

  // Instantiate a PLL (probably a BlackBox)
  val pll = new Pll()
  pll.io.clkIn := io.clk100M

  // Assign myClockDomain signals with something
  myClockDomain.clock := pll.io.clockOut
  myClockDomain.reset := pll.io.reset

  // Do whatever you want with myClockDomain
  val myArea = new ClockingArea(myClockDomain) {
    val myReg = Reg(UInt(4 bits)) init(7)
    myReg := myReg + 1

    io.result := myReg
  }
}

object DemoInternalClock extends App {
  SpinalVerilog(new InternalClockWithPllExample)
}
  1. 首先我们看到,代码中定义了一个pll类(相位锁相环)的组件,pll() 是一个用于生成和配置 FPGA 中的相位锁定环(Phase-Locked Loop,PLL)的方法。是一种常用的时钟管理电路,用于生成稳定的时钟信号,并提供时钟频率的倍频或分频功能。它有三个输入和两个输出端口:
  • clkIn:输入端口,表示 PLL 的输入时钟信号。

  • clockOut:输出端口,表示从 PLL 生成的时钟信号。

  • reset:输出端口,表示复位信号。

    在这个示例中,pll类并未实际实现PLL的逻辑,而是简单地将输入的时钟信号直接传递给输出时钟信号,并将复位信号设置为高电平。

  1. 定义一个名为 InternalClockWithPllExample 的组件。它有三个输入和一个输出端口:
  • clk100M:输入端口,表示输入的 100MHz 时钟信号。
  • aReset:输入端口,表示外部复位信号。
  • result:输出端口,是一个 4 位无符号整数。
  1. 定义了一个名为 myClockDomain 的内部时钟域,使用 ClockDomain.internal 方法创建。在此示例中,没有直接指定时钟和复位信号,而是使用了 PLL 的输出信号。

  2. 实例化一个 pll 对象,表示一个 PLL 组件,并将 clkIn 输入端口连接到外部输入的 clk100M 时钟信号。

    myClockDomain 的时钟信号 clock 和复位信号 reset 分别连接到 PLL 组件的输出信号 clockOutreset

  3. myArea 的时钟域中,定义了一个名为 myReg 的 4 位寄存器,并初始化为 7。在每个时钟周期中,myReg 的值递增 1。将 myReg 的值输出到 result 输出端口。

总体而言,该代码展示了如何使用 SpinalHDL 描述内部时钟域和相位锁定环,并在内部时钟域中实现一个简单的寄存器,输出一个递增的计数器值。

生成的verilog代码如下:

module InternalClockWithPllExample (
  input               io_clk100M,
  input               io_aReset,
  output     [3:0]    io_result
);

  wire                pll_1_io_clockOut;
  wire                pll_1_io_reset;
  wire                myClockName_clk;
  wire                myClockName_reset;
  reg        [3:0]    myArea_myReg;

  Pll pll_1 (
    .io_clkIn    (io_clk100M       ), //i
    .io_clockOut (pll_1_io_clockOut), //o
    .io_reset    (pll_1_io_reset   )  //o
  );
  assign myClockName_clk = pll_1_io_clockOut;
  assign myClockName_reset = pll_1_io_reset;
  assign io_result = myArea_myReg;
  always @(posedge myClockName_clk or posedge myClockName_reset) begin
    if(myClockName_reset) begin
      myArea_myReg <= 4'b0111;
    end else begin
      myArea_myReg <= (myArea_myReg + 4'b0001);
    end
  end


endmodule

module Pll (
  input               io_clkIn,
  output              io_clockOut,
  output              io_reset
);


  assign io_clockOut = io_clkIn;
  assign io_reset = 1'b1;

endmodule

那么内部时钟域有什么作用呢?

  1. 时钟域划分:FPGA 中常常存在多个时钟信号,每个时钟信号都有自己的时钟域。通过划分内部时钟域,可以将逻辑电路分组,并为每个时钟信号提供独立的时钟和复位控制。这样可以更好地控制和管理时钟信号,减少时序问题的出现。
  2. 时序约束:在 FPGA 设计中,时序约束用于指定各个时钟域中的信号的时序要求,包括时钟频率、时钟分频比、时序关系等。通过定义内部时钟域,可以更加精确地指定时序约束,并帮助实现对时序的控制和优化。
  3. 时钟域转换:当不同的时钟域之间需要进行数据交互时,需要进行时钟域转换。内部时钟域可以提供一个清晰的界限,方便进行时钟域转换操作,例如使用 FIFO 缓冲区、异步 FIFO 等方法实现跨时钟域的数据传输。
  4. 时钟分频和倍频:内部时钟域可以用于实现时钟的分频和倍频。通过使用 PLL 或其他时钟管理电路,可以将输入的时钟信号分频或倍频,以获得所需的时钟频率。
  5. 同步和寄存器设计:内部时钟域用于同步和寄存器设计。在 FPGA 中,时钟边沿通常用于触发寄存器的更新操作。通过在内部时钟域中定义逻辑和寄存器,可以确保同步和时序正确性,避免由异步信号引起的时序问题。

综上所述,内部时钟域在 FPGA 设计中起到了划分、控制、优化和管理时钟信号的作用,有助于实现稳定、可靠的设计,并满足复杂的时序要求。

1.3 external clock 外部时钟

ClockDomain.external(
  name: String,
  [config: ClockDomainConfig,]
  [withReset: Boolean,]
  [withSoftReset: Boolean,]
  [withClockEnable: Boolean,]
  [frequency: IClockDomainFrequency]
)

直接来看例子

import spinal.core._

class DemoExternalClock  extends Component{
  val io = new Bundle{
    val myClk = in Bool()
    val myReset = in Bool()
  }
  noIoPrefix()

  // define a new clock domain
  val clkCfg = ClockDomainConfig(clockEdge = RISING, resetKind = ASYNC, resetActiveLevel = LOW)
  val clkDomain = ClockDomain(
    clock = io.myClk,
    reset = io.myReset,
    config = clkCfg
  )
  val clockArea = new ClockingArea(clkDomain) {
    val a = UInt(8 bits)
    val b = RegNext(a)
  }

}

class DemoExternalClock1  extends Component{
  val io = new Bundle{
    val myClk = in Bool()
    val myReset = in Bool()
  }
  noIoPrefix()

  // 例化DemoExternalClock中的时钟信号
  val demoClock = new DemoExternalClock
  demoClock.io.myClk <> io.myClk // <> 不用考虑哪个是input 哪个是output
  demoClock.io.myReset <> io.myReset

}

object DemoExternalClock extends App {
  SpinalVerilog(new DemoExternalClock1)
}
  1. 首先,代码定义了一个名为DemoExternalClock的组件。它包含两个输入信号myClkmyReset,分别表示外部时钟和复位信号。noIoPrefix()函数用于设置信号的IO名称不带前缀。

  2. 接下来,代码通过ClockDomainConfig配置了一个新的时钟域(clock domain),具体的配置包括时钟上升沿触发(RISING)的时钟边沿、异步复位(ASYNC)且复位信号低电平有效(resetActiveLevel = LOW)。

  3. 然后,通过ClockDomain实例化了一个时钟域对象clkDomain,并将myClkmyReset连接到该时钟域对象的时钟和复位信号。

  4. 在时钟域对象内部,代码使用ClockingArea创建了一个时钟域区域clockArea,用于定义在该时钟域下的逻辑。在该区域内,定义了一个8位无符号整数(UInt)变量a和一个寄存器b,其中b通过RegNext接收a的值。

  5. 接下来,代码定义了另一个名为DemoExternalClock1的组件,它包含与外部时钟相关的输入信号myClkmyReset。在该组件内部,实例化了DemoExternalClock组件的对象demoClock,并使用<>运算符将demoClock的输入输出端口与外部信号进行连接。

  6. 最后,通过SpinalVerilog函数将DemoExternalClock1组件转换为Verilog代码,以供综合工具进行后续的综合和生成硬件描述文件。

总体而言,这段代码演示了如何在SpinalHDL中使用外部时钟,并展示了时钟域的使用和信号连接的方法。

下面是生成的verilog代码

module DemoExternalClock1 (
  input               myClk,
  input               myReset
);


  DemoExternalClock demoClock (
    .myClk   (myClk  ), //i
    .myReset (myReset)  //i
  );

endmodule

module DemoExternalClock (
  input               myClk,
  input               myReset
);

  wire       [7:0]    clockArea_a;
  reg        [7:0]    clockArea_b;

  always @(posedge myClk) begin
    clockArea_b <= clockArea_a;
  end

endmodule

可以看到DemoExternalClock1模块实例化了一个名为DemoExternalClock的模块对象demoClock,通过使用<>运算符,将myClkmyReset信号连接到demoClock模块对象的输入端口。(这里的.myClk.myReset表示具体的端口名称,分别对应于DemoExternalClock模块的输入端口myClkmyReset。在这个例子中,.myClk (myClk)表示将顶层模块DemoExternalClock1的输入信号myClk连接到DemoExternalClock模块的输入端口myClk.myReset (myReset)表示将顶层模块的输入信号myReset连接到DemoExternalClock模块的输入端口myReset。)

可以看到,当我们进行例化多级的时候,需要手动定义时钟和复位,这样过于复杂,我们只需要使用

ClockDomain.external()进行定义即可,可以在资源中的任何位置定义一个由外部驱动的时钟域。 然后它会自动将来自顶层输入的时钟和复位线添加到所有同步元件。

下面看代码;

// 顶层模块
module DemoExternalClock1 (
  input               myClk_clk,
  input               myClk_reset
);


  DemoExternalClock demoClock (
    .myClk_clk   (myClk_clk  ), //i
    .myClk_reset (myClk_reset)  //i
  );

endmodule

module DemoExternalClock (
  input               myClk_clk,
  input               myClk_reset
);

  wire       [7:0]    clockArea_a;
  reg        [7:0]    clockArea_b;

  always @(posedge myClk_clk) begin
    clockArea_b <= clockArea_a;
  end


endmodule

可以看到顶层模块DemoExternalClock1自动帮我们定义了时钟和复位

那么外部时钟有什么用呢?

外部时钟在电子系统中起着关键的作用,特别是在数字设计中。以下是外部时钟的几个主要作用:

  1. 同步信号:外部时钟提供了系统中各个模块之间的统一时钟信号,用于同步数据的传输和操作。通过统一的时钟信号,可以确保各个模块在相同的时钟周期内进行操作,实现数据的准确传输和处理。
  2. 定时控制:外部时钟可以用于精确控制系统的操作和时序要求。通过调整时钟频率、占空比和相位等参数,可以实现对操作和数据传输的精确控制。
  3. 时序约束:外部时钟对于时序分析和优化至关重要。在数字设计中,时序约束用于定义各个时钟域的时序要求,包括时钟频率、时钟间距、时钟延迟等。通过设置合适的时序约束,可以保证系统的稳定性、性能和正确功能。
  4. 电源管理:外部时钟还可以用于电源管理和功耗优化。通过动态调整时钟频率和工作模式,可以实现节能和功耗优化的目标。

总之,外部时钟在数字设计中是至关重要的,它提供了统一的时钟信号,用于同步和控制系统的操作。通过合理的时钟设计和时序约束,可以确保系统的正确功能、稳定性和性能。同时,外部时钟也在功耗优化和电源管理方面发挥着重要作用。

补充知识

Verilog代码中,同步复位和异步复位是两种常见的复位电路设计方法。他们用于在电路中将寄存器或者逻辑元件置于已知状态。

1. 同步复位

同步复位是在时钟边沿(通常是上升沿)处于有效状态时,才会对寄存器或逻辑元件执行复位操作。这意味着复位信号与时钟信号同步,并且复位信号的变化只在时钟的作用下才会生效。下面是一个使用同步复位的简单例子:

module sync_reset_example (
    input wire clk,
    input wire reset,
    output wire reg_out
);

    always @(posedge clk) begin
        if (reset) begin
            reg_out <= 0; // 将寄存器置零
        end else begin
            // 正常操作
            // ...
        end
    end

endmodule

在上面的例子中,当复位信号 reset 在时钟上升沿为有效状态时,reg_out 寄存器会被置为零。这里当复位信号 reset 为高电平时,在时钟上升沿时 reg_out 寄存器会被置为零。也可以称为同步高复位,同理,还存在同步低复位。

2. 异步复位

异步复位是在复位信号瞬间为有效状态时,立即对寄存器或逻辑元件执行复位操作。复位信号的变化不受时钟的影响,因此可以在任何时刻对电路进行复位。下面是一个使用异步复位的简单例子:

module async_low_reset_example (
    input wire clk,
    input wire reset_n, // 异步低复位信号,n表示取反
    output wire reg_out
);

    always @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            reg_out <= 0; // 将寄存器置零
        end else begin
            // 正常操作
            // ...
        end
    end

endmodule

在上面的例子中,当异步低复位信号 reset_n 为低电平时,在时钟上升沿时 reg_out 寄存器会被置为零。所以也被称为异步低复位,同理,还存在异步高复位。

参考资料

  1. Clock domains — SpinalHDL documentation

  2. SpinalHDL教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值