SpinalHDL(五):时钟和时钟域

SpinalHDL(五):时钟和时钟域

SpinalHDL的时钟和时钟域设计一开始就设计的相对比较完整,Chisel起初只能支持同步复位,这个在工程上带来了很多麻烦,最近chisel3好像可以支持异步复位。
例化方式非常别扭,我更喜欢SpinalHDL的清晰直观。非常重要的一个区别写SpinalHDL你很清楚哪些是硬件哪些是软件,而chisel就特别模糊,充斥着大量的软件思维,去了解他们各自开发者的背景也就不难理解。
所以一个合格的产品设计者,首先是一个合格的用户。

几个简单的示例

  1. 默认时钟
1
2
3
4
class Top extends Component{
val a = in Bits(8 bits)
val b = RegNext(a) init 0
}

默认会得到一个clk,reset时钟复位信号,并且SpinalHDL复位默认是上升沿

1
2
3
4
5
6
7
8
9
10
11
12
13
module Top (
input [7:0] a,
input clk,
input reset);
reg [7:0] b;
always @ (posedge clk or posedge reset) begin
if (reset) begin
b <= (8'b00000000);
end else begin
b <= a;
end
end
endmodule
  1. 自定义时钟源

如果我们不喜欢默认是时钟复位名称,可以自己修改时钟信号名,通过ClockDomain来创建一个时钟域(其实spinal的Component里隐含了一个叫
clockDomain变量的默认domain)

1
2
3
4
5
6
val myclk,myrst = in Bool()
val myClockDomain = ClockDomain(myclk,myrst)
new ClockingArea(myClockDomain){
val reg0 = RegNext(a) init 0
b := reg0
}

如果让寄存器生效,需要用clockingArea(myClockDomain)把寄存器包裹起来
便可得到如下verilog代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module Top (
input myclk,
input myrst,
input [7:0] a,
output [7:0] b);
reg [7:0] _zz_1_;
assign b = _zz_1_;
always @ (posedge myclk or posedge myrst) begin
if (myrst) begin
_zz_1_ <= (8'b00000000);
end else begin
_zz_1_ <= a;
end
end
endmodule

可以看到复位信号是posedege,如改为negedge,
只要在创建时钟域时按如下修改覆盖config即可

1
2
3
4
5
6
val myClockDomain = ClockDomain(myclk,myrst,
config = ClockDomainConfig(
clockEdge = RISING,
resetKind = ASYNC,
resetActiveLevel = LOW
))
  1. 覆盖子模块默认时钟

每次用clockArea
来包裹代码块,非常啰嗦,我能不能在直接修改该模块的默认时钟域,目前呢需要在该模块的上一层用myclockdomain包裹模块例化即可

1
2
3
4
5
6
7
8
9
class Sub extends Component{
val a = in Bits(8 bits)
val b = out(RegNext(a) init 0)
}
class Top extends Component{
val myclk,myrst = in Bool()
val mycd = ClockDomain(myclk,myrst)
val u_sub0 = mycd(new Sub) //mycd时钟包裹Sub模块的例化
}

生成的verilog可以看到,子模块的时钟复位信号都被修改过来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module Sub (
input [7:0] a,
output reg [7:0] b,
input myclk,
input myrst);
always @ (posedge myclk or negedge myrst) begin
if (!myrst) begin
b <= (8'b00000000);
end else begin
b <= a;
end
end
endmodule
module Top (
...
endmodule

ClockDomain详解

SpinalHDL的ClockDomain概念为一组时钟复位方案,并不单指一个时钟信号
完整的时钟域配置分两块

  • ClockDomain
1
2
3
4
5
6
7
8
ClockDomain(
clock: Bool
[,reset: Bool]
[,softReset: Bool]
[,clockEnable: Bool]
[,frequency: IClockDomainFrequency]
[,config: ClockDomainConfig]
)
  • ClockDomainConfig
1
2
3
4
5
case class ClockDomainConfig(clockEdge: EdgeKind = RISING,
resetKind: ResetKind = ASYNC,
resetActiveLevel: Polarity = HIGH,
softResetActiveLevel: Polarity = HIGH,
clockEnableActiveLevel: Polarity = HIGH)

创建clockDomain有4种方法

  • 默认clockDomain
    该时钟域又SpinalHDL默认创建,前面我们将如何替换默认时钟以外,我们还可以直接在SpinalConfig里面修改默认时钟域的配置
1
2
3
4
5
6
SpinalConfig(mode = Verilog,
defaultConfigForClockDomains = ClockDomainConfig(resetKind = ASYNC,
clockEdge = RISING,
resetActiveLevel = LOW),
targetDirectory="tmp/")
.generate(new MyTop)
  • 从端口信号创建 前面已经提到过这种方法,很简单
1
2
val myclk,myrst = in Bool()
val mycd = ClockDomain(myclk,myrst)
  • external函数
    有时候我们在例化一个异步FIFO的时候可能一个时钟需要从顶层接进来,如果hierarchy很深,一级一级连进来相当麻烦,Spinal提供了一个external
    函数,可以非常方便的创建一个时钟,并且接到顶层
1
val myClockDomain = ClockDomain.external("myClock")

得到的Verilog:

1
2
3
4
5
6
module ExternalClockExample (
....
input myClockName_clk,
input myClockName_reset);
....
endmodule
  • internal函数
    一般SOC系统的时钟往往来自于一个时钟管理单元,由PLL产生,那么这时候,我们需要internal函数从内部创建clockDomain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class InternalClockWithPllExample extends Component {
val io = new Bundle {
val clk100M = in Bool
val aReset = in Bool
val result = out UInt (4 bits)
}
// myClockDomain.clock will be named myClockName_clk
// myClockDomain.reset will be named myClockName_reset
val myClockDomain = ClockDomain.internal("myClockName")
// Instanciate 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 := io.aReset || !pll.io.

// 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
}
}

时钟门控处理

目前Spinal在处理门控的时候还是需要手动例化,

1
2
3
4
5
6
class Top extends Component {
val u_cgCell0 = new CG
val u_cgCell1 = new CG
...
val cgd0 = ClockDomain(u_cgCell0.ECK, clockDomain.readResetWire)
val cgd1 = ClockDomain(u_cgCell1.ECK, clockDomain.readResetWire)

手动例化两个ClockDomain会存在一个问题,会将它们判定为两个异步时钟。目前Spinal提供一个函数setSyncWith,讲两个clockDomain的Family全部
设置为同步,这样在异步检查的时候就不会报错
51

1
2
3
4
   cgd0.setSyncWith(cgd1)
val u_sub0 = cgd0(new MySub)
val u_sub1 = cgd1(new MySub)
}

时钟工厂函数

除了时钟门控,还有时钟分频也会涉及到这类问题.
对于同源同相幂次分频我们也会认为是同一个时钟域来处理。
52
所以一种更优雅的方式应该是,提供工厂函数,让Spinal自动处理
53

1
2
3
4
5
val cgd0  = ClockDomain.cgFrom(clockDomain,  cg_en)
val cgd1 = ClockDomain.cgFrom(clockDomain, cg_en)

val cdiv2 = ClockDomain.divFrom(clockDomain, times=2)
val cdiv3 = ClockDomain.divFrom(clockDomain, times=4)

注:该方案还没有合入Spinal最新分支,会在1.3.7以后版本实现

评论