使用SpinalHDL编写7段数码管模块,并使用cocotb进行仿真验证

实验环境

  1. scala: 2.11.12
  2. sbt script version: 1.10.5
  3. Vivado 2023.2
  4. cocotb 1.9.0

数码管扫描程序Verilog描述

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
module scan_led_hex_disp_4(
input clk, rst, /* 100MHz时钟与复位 */
input [3:0] hex0, hex1, hex2, hex3, /* 显存 */
input [3:0] dp, /* 小数点 */
output reg [3:0] an, /* 片选信号 */
output reg [7:0] sseg /* 八段数码管接口 */
);

localparam N = 16 + 2; /* 100MHz时钟分频, 100Mhz/ 2^16 */
reg [N-1:0] regN;

always @(posedge clk, posedge rst) begin
if (rst) /* 异步, 高电平复位 */
regN <= 0;
else
regN <= regN + 1;
end

always @(*) begin /* 时分复用 */
case (regN[N-1:N-2])
2'b00: begin
an <= 4'b0001;
sseg[6:0] <= dt_translate(hex0);
sseg[7] <= dp[3];
end
2'b01: begin
an <= 4'b0010;
sseg[6:0] <= dt_translate(hex1);
sseg[7] <= dp[2];
end
2'b10: begin
an <= 4'b0100;
sseg[6:0] <= dt_translate(hex2);
sseg[7] <= dp[1];
end
2'b11: begin
an <= 4'b1000;
sseg[6:0] <= dt_translate(hex3);
sseg[7] <= dp[0];
end
endcase
end

function [6:0] dt_translate; /* 数码管译码函数 */
input [3:0] data;
begin
case(data)
4'd0: dt_translate = 7'b1111110; //number 0 -> 0x7e
4'd1: dt_translate = 7'b0110000; //number 1 -> 0x30
4'd2: dt_translate = 7'b1101101; //number 2 -> 0x6d
4'd3: dt_translate = 7'b1111001; //number 3 -> 0x79
4'd4: dt_translate = 7'b0110011; //number 4 -> 0x33
4'd5: dt_translate = 7'b1011011; //number 5 -> 0x5b
4'd6: dt_translate = 7'b1011111; //number 6 -> 0x5f
4'd7: dt_translate = 7'b1110000; //number 7 -> 0x70
4'd8: dt_translate = 7'b1111111; //number 8 -> 0x7f
4'd9: dt_translate = 7'b1111011; //number 9 -> 0x7b
endcase
end
endfunction
endmodule

使用SpinalHDL描述

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class seven_seg_display extends Component {
val io = new Bundle {
val clk, rstn: Bool = in Bool() /* clock and reset */
val hex0, hex1, hex2, hex3: Bits = in Bits(4 bits) /* 4 4-bit hex input */
val dp: Bits = in Bits(4 bits) /* 1-bit dp output */
val anodes: Bits = out Bits(4 bits) /* 4-bit chip-select output */
val segments: Bits = out Bits(8 bits) /* 7-bit segment output */
}

/* division setting */
val N_DIV = 16 + 2

/* clock domain config */
val coreClockDomain = ClockDomain(
clock = io.clk,
reset = io.rstn,
config = ClockDomainConfig(
clockEdge = RISING, /* rising edge */
resetKind = ASYNC, /* asynchronous reset */
resetActiveLevel = LOW /* active low */
)
)

/* clock division counter */
val myCounter: UInt = Reg(UInt(N_DIV bits)) init (0)

/* sequential logic */
val counterArea = new ClockingArea(coreClockDomain) {
myCounter := myCounter + 1 /* increment counter */
}

/* time-slot from div-counter, scan at clk_in divided by 2^16 */
val time_slot: UInt = myCounter(myCounter.high-1 , 2 bits)

/* combinational logic, work to do in different time-slot */
switch(time_slot) {
is(0) {
io.anodes := B"0001"
io.segments := io.dp(0) ## hexToSegments(io.hex0)
}
is(1) {
io.anodes := B"0010"
io.segments := io.dp(1) ## hexToSegments(io.hex1)
}
is(2) {
io.anodes := B"0100"
io.segments := io.dp(2) ## hexToSegments(io.hex2)
}
is(3) {
io.anodes := B"1000"
io.segments := io.dp(3) ## hexToSegments(io.hex3)
}
}

/* convert hex to 7-segment */
def hexToSegments(hex: Bits): Bits = {
val segments = Bits(7 bits)
switch(hex) {
is(B"4'x0") { segments := B"1111110" }
is(B"4'x1") { segments := B"0110000" }
is(B"4'x2") { segments := B"1101101" }
is(B"4'x3") { segments := B"1111001" }
is(B"4'x4") { segments := B"0110011" }
is(B"4'x5") { segments := B"1011011" }
is(B"4'x6") { segments := B"1011111" }
is(B"4'x7") { segments := B"1110000" }
is(B"4'x8") { segments := B"1111111" }
is(B"4'x9") { segments := B"1111011" }
is(B"4'xA") { segments := B"1110111" }
is(B"4'xB") { segments := B"0011111" }
is(B"4'xC") { segments := B"1001110" }
is(B"4'xD") { segments := B"0111101" }
is(B"4'xE") { segments := B"1001111" }
is(B"4'xF") { segments := B"1000111" }
}
segments
}
}

SpinalHDL语法分析

模块接口的数据类型分析

1
2
3
4
5
6
7
val io = new Bundle {
val clk, rstn: Bool = in Bool() /* clock and reset */
val hex0, hex1, hex2, hex3: Bits = in Bits(4 bits) /* 4 4-bit hex input */
val dp: Bits = in Bits(4 bits) /* 1-bit dp output */
val anodes: Bits = out Bits(4 bits) /* 4-bit chip-select output */
val segments: Bits = out Bits(8 bits) /* 7-bit segment output */
}
  1. Bool类型,可以视作一个bit的值,可以通过Bool()创建。
  2. Bits类型,没有算数意义的bit向量,可以通过Bits(x bits)创建。
  3. UInt/Sint类型,可以参与运算的符号数的bit向量。
  4. Bundle,定义一组命名的信号,类似结构体,表示数据结构、总线或接口的模型。

时序逻辑分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* clock domain config */
val coreClockDomain = ClockDomain(
clock = io.clk,
reset = io.rstn,
config = ClockDomainConfig(
clockEdge = RISING, /* rising edge */
resetKind = ASYNC, /* asynchronous reset */
resetActiveLevel = LOW /* rst active low */
)
)
/* time-slot from div-counter, scan at clk_in divided by 2^16 */
val time_slot = UInt(2 bits)

/* sequential logic */
val counterArea = new ClockingArea(coreClockDomain) {
/* clock division counter */
val myCounter: UInt = Reg(UInt(N_DIV bits)) init (0)

myCounter := myCounter + 1 /* increment counter */
time_slot := myCounter(myCounter.high-1 , 2 bits)
}

在Verilog中时序逻辑的主要载体为带有触发选项的always语句块,在SpinalHDL将时钟和复位信号(可选)结合定义为一个时钟域,通过ClockDomain创建一个时钟域,通过ClockingArea使用对应的时钟域。

生成的verilog语句如下:

1
2
3
4
5
6
7
always @(posedge io_clk or negedge io_rstn) begin
if(!io_rstn) begin
counterArea_myCounter <= 18'h0;
end else begin
counterArea_myCounter <= (counterArea_myCounter + 18'h00001);
end
end

仿真验证

仿真程序

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
26
27
28
29
30
31
32
33
34
35
36
37
38
"""
@Project :cocotb_study
@File :testbench_7segDisplay.py
@Author :PLMaple
@Date :2024/11/27 10:53
@Brief : 4位七段数码管模块仿真程序
"""
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import Timer, FallingEdge

@cocotb.test()
async def main(dut):
# 设定参数
dut.io_hex0.value = 0x1
dut.io_hex1.value = 0x2
dut.io_hex2.value = 0x3
dut.io_hex3.value = 0x4
dut.io_dp.value = 0b1111

# 异步低电平复位
dut.io_rstn.value = 0
await Timer(20, units='ns')
dut.io_rstn.value = 1

# 生成100MHz时钟
clk_1mhz = Clock(dut.io_clk, 10, 'ns')
await cocotb.start(clk_1mhz.start())
local_capture = dut.io_anodes.value

dut._log.info("初始化成功,当前片选为%s, 段选信号为%s", dut.io_anodes.value.binstr, dut.io_segments.value.binstr)
for i in range(4):
while 1:
await FallingEdge(dut.io_clk)
if dut.io_anodes.value != local_capture:
local_capture = dut.io_anodes.value
dut._log.info("片选信号改变,当前片选为%s, 段选信号为%s", dut.io_anodes.value.binstr, dut.io_segments.value.binstr)
break

仿真结果

1
2
3
4
5
6
7
     0.00ns INFO     cocotb.regression                  running main (1/1)
20.00ns INFO cocotb.seven_seg_display 初始化成功,当前片选为0001, 段选信号为10110000
655375.00ns INFO cocotb.seven_seg_display 片选信号改变,当前片选为0010, 段选信号为11101101
1310735.00ns INFO cocotb.seven_seg_display 片选信号改变,当前片选为0100, 段选信号为11111001
1966095.00ns INFO cocotb.seven_seg_display 片选信号改变,当前片选为1000, 段选信号为10110011
2621455.00ns INFO cocotb.seven_seg_display 片选信号改变,当前片选为0001, 段选信号为10110000
2621455.00ns INFO cocotb.regression main passed

片选信号的扫描周期为输入信号的65536倍分频
$$f = \frac{f_{in}}{2^{16}} $$

$$T = 65536*10(ns) = 0.65536(ms)$$

在20ns时,模块完成复位,此时时钟处于上升沿,由于测试程序的触发为下降沿,在$20 + 655360 - 5 = 655375(ns) $的下降沿处检测到片选信号更新,之后再次更新的周期即为$1310735 - 655375 = 65536*10(ns) $,测试结果无误。

参考资料

  1. SpinalHDL’s documentation
  2. cocotb’s documentation