首先我们来看一下官方的文档中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]
)
参数说明:
Argument | Description | Default |
---|---|---|
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
进行配置:
Property | Valid values |
---|---|
clockEdge | RISING , FALLING |
resetKind | ASYNC , SYNC , and BOOT which is supported by some FPGAs (where FF values are loaded by the bitstream) |
resetActiveLevel | HIGH , LOW |
softResetActiveLevel | HIGH , LOW |
clockEnableActiveLevel | HIGH , 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]
)
Argument | Description | Default |
---|---|---|
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)
}
- 首先我们看到,代码中定义了一个
pll
类(相位锁相环)的组件,pll()
是一个用于生成和配置 FPGA 中的相位锁定环(Phase-Locked Loop,PLL)的方法。是一种常用的时钟管理电路,用于生成稳定的时钟信号,并提供时钟频率的倍频或分频功能。它有三个输入和两个输出端口:
-
clkIn
:输入端口,表示 PLL 的输入时钟信号。 -
clockOut
:输出端口,表示从 PLL 生成的时钟信号。 -
reset
:输出端口,表示复位信号。在这个示例中,
pll
类并未实际实现PLL的逻辑,而是简单地将输入的时钟信号直接传递给输出时钟信号,并将复位信号设置为高电平。
- 定义一个名为
InternalClockWithPllExample
的组件。它有三个输入和一个输出端口:
clk100M
:输入端口,表示输入的 100MHz 时钟信号。aReset
:输入端口,表示外部复位信号。result
:输出端口,是一个 4 位无符号整数。
-
定义了一个名为
myClockDomain
的内部时钟域,使用ClockDomain.internal
方法创建。在此示例中,没有直接指定时钟和复位信号,而是使用了 PLL 的输出信号。 -
实例化一个
pll
对象,表示一个 PLL 组件,并将clkIn
输入端口连接到外部输入的clk100M
时钟信号。将
myClockDomain
的时钟信号clock
和复位信号reset
分别连接到 PLL 组件的输出信号clockOut
和reset
。 -
在
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
那么内部时钟域有什么作用呢?
- 时钟域划分:FPGA 中常常存在多个时钟信号,每个时钟信号都有自己的时钟域。通过划分内部时钟域,可以将逻辑电路分组,并为每个时钟信号提供独立的时钟和复位控制。这样可以更好地控制和管理时钟信号,减少时序问题的出现。
- 时序约束:在 FPGA 设计中,时序约束用于指定各个时钟域中的信号的时序要求,包括时钟频率、时钟分频比、时序关系等。通过定义内部时钟域,可以更加精确地指定时序约束,并帮助实现对时序的控制和优化。
- 时钟域转换:当不同的时钟域之间需要进行数据交互时,需要进行时钟域转换。内部时钟域可以提供一个清晰的界限,方便进行时钟域转换操作,例如使用 FIFO 缓冲区、异步 FIFO 等方法实现跨时钟域的数据传输。
- 时钟分频和倍频:内部时钟域可以用于实现时钟的分频和倍频。通过使用 PLL 或其他时钟管理电路,可以将输入的时钟信号分频或倍频,以获得所需的时钟频率。
- 同步和寄存器设计:内部时钟域用于同步和寄存器设计。在 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)
}
-
首先,代码定义了一个名为
DemoExternalClock
的组件。它包含两个输入信号myClk
和myReset
,分别表示外部时钟和复位信号。noIoPrefix()
函数用于设置信号的IO名称不带前缀。 -
接下来,代码通过
ClockDomainConfig
配置了一个新的时钟域(clock domain),具体的配置包括时钟上升沿触发(RISING)的时钟边沿、异步复位(ASYNC)且复位信号低电平有效(resetActiveLevel = LOW)。 -
然后,通过
ClockDomain
实例化了一个时钟域对象clkDomain
,并将myClk
和myReset
连接到该时钟域对象的时钟和复位信号。 -
在时钟域对象内部,代码使用
ClockingArea
创建了一个时钟域区域clockArea
,用于定义在该时钟域下的逻辑。在该区域内,定义了一个8位无符号整数(UInt)变量a
和一个寄存器b
,其中b
通过RegNext
接收a
的值。 -
接下来,代码定义了另一个名为
DemoExternalClock1
的组件,它包含与外部时钟相关的输入信号myClk
和myReset
。在该组件内部,实例化了DemoExternalClock
组件的对象demoClock
,并使用<>
运算符将demoClock
的输入输出端口与外部信号进行连接。 -
最后,通过
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
,通过使用<>
运算符,将myClk
和myReset
信号连接到demoClock
模块对象的输入端口。(这里的.myClk
和.myReset
表示具体的端口名称,分别对应于DemoExternalClock
模块的输入端口myClk
和myReset
。在这个例子中,.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
自动帮我们定义了时钟和复位
那么外部时钟有什么用呢?
外部时钟在电子系统中起着关键的作用,特别是在数字设计中。以下是外部时钟的几个主要作用:
- 同步信号:外部时钟提供了系统中各个模块之间的统一时钟信号,用于同步数据的传输和操作。通过统一的时钟信号,可以确保各个模块在相同的时钟周期内进行操作,实现数据的准确传输和处理。
- 定时控制:外部时钟可以用于精确控制系统的操作和时序要求。通过调整时钟频率、占空比和相位等参数,可以实现对操作和数据传输的精确控制。
- 时序约束:外部时钟对于时序分析和优化至关重要。在数字设计中,时序约束用于定义各个时钟域的时序要求,包括时钟频率、时钟间距、时钟延迟等。通过设置合适的时序约束,可以保证系统的稳定性、性能和正确功能。
- 电源管理:外部时钟还可以用于电源管理和功耗优化。通过动态调整时钟频率和工作模式,可以实现节能和功耗优化的目标。
总之,外部时钟在数字设计中是至关重要的,它提供了统一的时钟信号,用于同步和控制系统的操作。通过合理的时钟设计和时序约束,可以确保系统的正确功能、稳定性和性能。同时,外部时钟也在功耗优化和电源管理方面发挥着重要作用。
补充知识
在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
寄存器会被置为零。所以也被称为异步低复位,同理,还存在异步高复位。