1 环境搭建
软件:Windows 10、TD5.6.5、vscode
硬件:EG4S20BG256
- 安装TD软件,版本号为5.6.5。
- 安装vscode。
- 安装插件
Verilog-HDL/SystemVerilog/Bluespec SystemVerilog
、Verilog HDL
、Verilog Snippet
,以优化编程体验。
2 数码管静态显示
2.1 实现的功能
数码管从0000开始实现十六进制计数,每隔固定时间四位同时+1,直到FFFF后归零。按下复位后重新从0000开始计数。
2.2 设计思路
八段数码管介绍
八段数码管结构如下图所示,每一段都是发光二极管,给予电平后会亮或灭。本芯片中为共阳极数码管,即给予低电平0,对应段选的数码管会被点亮。如数字3表示为8'b1011_0000
。
位选信号也类似,即给予低电平0,对应段选的数码管会被点亮。如想点亮4、3、2号数码管,表示为4'b0001
。
为了节约资源,开发板中的段选信号相连(即段选信号唯一),由位选信号控制段选信号,这就导致理论上同一数码管要么不亮、要么亮且显示的数字均相同。
程序框图与端口信号
Port name | Direction | Type | Description |
clk_24m | input | wire | 24MHz时钟 |
rstn | input | wire | 复位 |
sm_seg | output | wire[7:0] | 数码管段选 |
sm_bit | output | wire[3:0] | 数码管位选 |
确定计数范围
数字跳变要有相同的间隔。假设每隔0.5s数字+1,具体计算如下:开发板提供了一个24MHz的晶振,即时钟周期为(1000/24)ns,计数0.5s即计数5×108ns,需要计数5×108÷(1000/24)=1.2×107,即计数范围0~12_000_000-1。
初始显示0000,当计数满0.5s时,四位数码管一起+1,为1111,以此类推,计数到ffff后下一次跳变回0000。
位选和段选
位选信号sm_bit
始终为4'b0000
,以保持四个数码管同时点亮。
每当计数满时,数码管跳变,段选信号sm_seg
点亮对应的若干个管段。
2.3 程序设计
计数器模块counter.v
module counter
#(
parameter CNT_MAX =12_000_000-1, //计数0.5s
parameter N =$clog2(CNT_MAX) //即计算log2(12_000_000),向上取整,为24
)
(
input wire clk_24m,
input wire rstn,
output wire[3:0] num_out
);
reg [N-1:0] cnt;
reg [3:0] num_out_reg;
//——————————————————————————————主计数器,计数0.5s——————————————————————————————
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
cnt <=0;
else if(cnt==CNT_MAX)
cnt <=0;
else
cnt <=cnt + 1'b1;
end
//——————————————————————————————各位的计数逻辑——————————————————————————————
assign num_out =num_out_reg;
always @(posedge clk_24m or negedge rstn) begin
if(~rstn)
num_out_reg <=0;
else if(cnt==CNT_MAX)begin
if(num_out_reg==15)
num_out_reg <=0;
else
num_out_reg <=num_out_reg + 1'b1;
end
//else保持,省略
end
endmodule
数码管驱动模块seg_driver.v
module seg_driver
(
input wire clk_24m,
input wire rstn,
input wire [3:0] num_out,
output wire [7:0] sm_seg, //数码管段选
output wire [3:0] sm_bit //数码管位选
);
reg [7:0] sm_seg_reg; //数码管段选,由具体数字译码而来
localparam S0 = 4'b0000 ,
S1 = 4'b0001 ,
S2 = 4'b0010 ,
S3 = 4'b0011 ,
S4 = 4'b0100 ,
S5 = 4'b0101 ,
S6 = 4'b0110 ,
S7 = 4'b0111 ,
S8 = 4'b1000 ,
S9 = 4'b1001 ,
SA = 4'b1010 ,
SB = 4'b1011 ,
SC = 4'b1100 ,
SD = 4'b1101 ,
SE = 4'b1110 ,
SF = 4'b1111 ;
//———————————————————————————————————位选——————————————————————————————————
assign sm_bit =4'b0000;
//———————————————————————————————————段选——————————————————————————————————
assign sm_seg =sm_seg_reg;
always@(*) begin //共阳极数码管,最高位是小数点,低七位是七段管。0为亮,1为暗。
case (num_out)
S0: sm_seg_reg <=8'b1100_0000;
S1: sm_seg_reg <=8'b1111_1001;
S2: sm_seg_reg <=8'b1010_0100;
S3: sm_seg_reg <=8'b1011_0000;
S4: sm_seg_reg <=8'b1001_1001;
S5: sm_seg_reg <=8'b1001_0010;
S6: sm_seg_reg <=8'b1000_0010;
S7: sm_seg_reg <=8'b1111_1000;
S8: sm_seg_reg <=8'b1000_0000;
S9: sm_seg_reg <=8'b1001_0000;
SA: sm_seg_reg <=8'b1000_1000;
SB: sm_seg_reg <=8'b1000_0011;
SC: sm_seg_reg <=8'b1100_0110;
SD: sm_seg_reg <=8'b1010_0001;
SE: sm_seg_reg <=8'b1000_0110;
SF: sm_seg_reg <=8'b1000_1110;
default:sm_seg_reg <=8'b1100_0000;
endcase
end
endmodule
顶层模块top_seg_static.v
module top_seg_static
#(
parameter CNT_MAX =12_000_000-1, //计数0.5s
parameter N =$clog2(CNT_MAX) //即计算log2(12_000_000),向上取整,为24
)
(
input wire clk_24m,
input wire rstn,
output wire[7:0] sm_seg, //数码管段选
output wire[3:0] sm_bit //数码管位选
);
wire [3:0] num_out; //0~f,16个数,需要4bit来存储。
counter # (
.CNT_MAX (CNT_MAX),
.N (N)
)
counter_inst (
.clk_24m (clk_24m),
.rstn (rstn),
.num_out (num_out)
);
seg_driver seg_driver_inst (
.clk_24m (clk_24m),
.rstn (rstn),
.num_out (num_out),
.sm_seg (sm_seg),
.sm_bit (sm_bit)
);
endmodule
引脚分配文件seg_static.adc
# 时钟
set_pin_assignment { clk_24m } { LOCATION = K14; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
# 复位
set_pin_assignment { rstn } { LOCATION = G11; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
# 位选
set_pin_assignment { sm_bit[0] } { LOCATION = B1; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[1] } { LOCATION = C3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[2] } { LOCATION = C2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[3] } { LOCATION = F3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
# 段选
set_pin_assignment { sm_seg[0] } { LOCATION = E3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[1] } { LOCATION = B3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[2] } { LOCATION = F4; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[3] } { LOCATION = E4; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[4] } { LOCATION = F5; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[5] } { LOCATION = D3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[6] } { LOCATION = B2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[7] } { LOCATION = A2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
2.4 上板验证
烧录至开发板
在TD5.6.5中新建工程文件,导入三个设计文件,导入引脚文件,编译后将生成的比特流下载到开发板中。具体步骤可参考基于TD5.6.5与vscode的FPGA简易开发教程——以多路选择器为例。
观察进位与复位
将顶层模块top_seg_static.v
文件中的parameter CNT_TIME = 12_000_000-1
修改为其他更小的值,烧录。
观察进位是否符合逻辑。按下复位键后,观察是否回到0000。
3 数码管动态显示
3.1 实现的功能
数码管从0000开始实现四位数的十进制进位,每隔固定时间个位+1,变为0001,直到9999后归零。
3.2 设计思路
程序框图与端口信号
Port name | Direction | Type | Description |
clk_24m | input | wire | 24MHz时钟 |
rstn | input | wire | 复位 |
sm_seg | output | wire[7:0] | 数码管段选 |
sm_bit | output | wire[3:0] | 数码管位选 |
确定计数范围
数字跳变要有相同的间隔。假设每隔0.5s数字+1,具体计算如下:开发板提供了一个24MHz的晶振,即时钟周期为(1000/24)ns,计数0.5s即计数5×108ns,需要计数5×108÷(1000/24)=1.2×107,即计数范围0~12_000_000-1。
初始显示0000,当计数满0.5s时,只有第一个数码管+1,变为0001,以此类推,计数到9999后下一次跳变回0000。
位选与段选
由于开发板中的段选信号相连(即段选信号唯一),位选信号控制段选信号,这就导致理论上同一数码管要么不点亮、要么显示相同的数字。
但因为人眼观察物体时存在视觉残像,外加二极管停止供电时仍能保持一小段时间的亮度(余晖效应),因此,可在短时间内依次循环点亮四个数码管,只要速度足够快,便可在视觉上呈现同一时间四个数码管同时点亮且显示数字不同的现象。
这时便需要循环扫描,有多种方式可以选择。
法1:定义一个小位宽的计数器,如cnt_w[15:0],用最高两位控制位选。每当bit(13:0)计满进位时,bit(15:14)会在00、01、10、11变化,循环选择四个数码管,且间隔时间相同。这种扫描方式比较简单,但无法精准控制扫描时间,而且比较适用于2的n次幂的数码管。
非2的n次幂情况:如六个数码管,用高三位控制位选,则101后会轮空2次计数,造成第六个数码管额外多点亮了两轮计数,间隔时间不同,会造成轻微影响,比如眼睛够好可以看到第六个数码管更亮一点。
法2:确定间隔时间,计算最大计数值,以此定义计数器。复位时只点亮第一个管子(4’b1110),每次计数满后移位、切换点亮下一个管子。这种方式能精准控制切换时间,且更通用,但比较麻烦。
3.3 程序设计
计数器模块counter.v
module counter
#(
parameter CNT_MAX =12_000_000-1, //计数0.5s
parameter N =$clog2(CNT_MAX) //即计算log2(12_000_000),向上取整,为24
)
(
input wire clk_24m,
input wire rstn,
output wire[15:0] num_out
);
//计数器
reg [N-1:0] cnt;
reg [3:0] sm_bit1_num;
reg [3:0] sm_bit2_num;
reg [3:0] sm_bit3_num;
reg [3:0] sm_bit4_num;
//——————————————————————————————主计数器,计数0.5s——————————————————————————————
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
cnt <=0;
else if(cnt==CNT_MAX)
cnt <=0;
else
cnt <=cnt + 1'b1;
end
//——————————————————————————————各位的计数逻辑——————————————————————————————
assign num_out ={sm_bit4_num, sm_bit3_num, sm_bit2_num, sm_bit1_num};
//个位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit1_num <=0;
else if(cnt==CNT_MAX) begin //0.1Hz
if(sm_bit1_num==9)
sm_bit1_num <=0;
else
sm_bit1_num <=sm_bit1_num + 1'b1;
end
//else保持,省略
end
//十位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit2_num <=0;
else if(cnt==CNT_MAX && sm_bit1_num==9) begin //个位
if(sm_bit2_num==9)
sm_bit2_num <=0;
else
sm_bit2_num <=sm_bit2_num + 1'b1;
end
//else保持,省略
end
//百位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit3_num <=0;
else if(cnt==CNT_MAX && sm_bit2_num==9 && sm_bit1_num==9) begin
if(sm_bit3_num==9)
sm_bit3_num <=0;
else
sm_bit3_num <=sm_bit3_num + 1'b1;
end
//else保持,省略
end
//千位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit4_num <=0;
else if(cnt==CNT_MAX && sm_bit3_num==9 && sm_bit2_num==9 && sm_bit1_num==9) begin
if(sm_bit4_num==9)
sm_bit4_num <=0;
else
sm_bit4_num <=sm_bit4_num + 1'b1;
end
//else保持,省略
end
endmodule
数码管驱动模块(法1)seg_driver.v
module seg_driver
(
input wire clk_24m,
input wire rstn,
input wire [15:0] num_out,
output wire [7:0] sm_seg, //数码管段选
output wire [3:0] sm_bit //数码管位选
);
reg [15:0] cnt_w;
reg [3:0] sm_seg_num; //具体数字
reg [7:0] sm_seg_reg; //数码管段选,由具体数字译码而来
reg [3:0] sm_bit_reg; //数码管位选
localparam S0 = 4'b0000 ,
S1 = 4'b0001 ,
S2 = 4'b0010 ,
S3 = 4'b0011 ,
S4 = 4'b0100 ,
S5 = 4'b0101 ,
S6 = 4'b0110 ,
S7 = 4'b0111 ,
S8 = 4'b1000 ,
S9 = 4'b1001 ;
//———————————————————————————————————扫描计数器——————————————————————————————————
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
cnt_w <=0;
else if(&cnt_w) //按位与,即计数到最大,16位都是1
cnt_w <=0;
else
cnt_w <=cnt_w + 1'b1;
end
//———————————————————————————————————位选与段选——————————————————————————————————
//sm_seg_num用于显示具体的数据
always@(posedge clk_24m or negedge rstn) begin
if(~rstn) begin //这两行自定义复位时哪几个会亮,亮的显示什么
sm_bit_reg <=4'b1111;
sm_seg_num <=0;
end
else begin
case( cnt_w[15:14] )
2'b00: begin //最高两位为00,则点亮第一个管子,且具体数字由sm_bit_num决定
sm_bit_reg <=4'b1110;
sm_seg_num <=num_out[3:0];
end
2'b01: begin
sm_bit_reg <=4'b1101;
sm_seg_num <=num_out[7:4];
end
2'b10: begin
sm_bit_reg <=4'b1011;
sm_seg_num <=num_out[11:8];
end
2'b11: begin
sm_bit_reg <=4'b0111;
sm_seg_num <=num_out[15:12];
end
endcase
end
end
//段选译码,控制每一位的显示数字
always@(*) begin //共阳极数码管,最高位是小数点,低七位是七段管。0为亮,1为暗。
case ( sm_seg_num )
S0: sm_seg_reg =8'b1100_0000;
S1: sm_seg_reg =8'b1111_1001;
S2: sm_seg_reg =8'b1010_0100;
S3: sm_seg_reg =8'b1011_0000;
S4: sm_seg_reg =8'b1001_1001;
S5: sm_seg_reg =8'b1001_0010;
S6: sm_seg_reg =8'b1000_0010;
S7: sm_seg_reg =8'b1111_1000;
S8: sm_seg_reg =8'b1000_0000;
S9: sm_seg_reg =8'b1001_0000;
default:sm_seg_reg =8'b1100_0000;
endcase
end
assign sm_seg =sm_seg_reg;
assign sm_bit =sm_bit_reg;
endmodule
数码管驱动模块(法2)seg_driver2.v
module seg_driver2(
input wire clk_24m,
input wire rstn,
input wire [15:0] num_out,
output wire [7:0] sm_seg, //数码管段选
output wire [3:0] sm_bit //数码管位选
);
reg [15:0] cnt_w;
reg [3:0] sm_seg_num; //具体数字
reg [7:0] sm_seg_reg; //数码管段选,由具体数字译码而来
reg [3:0] sm_bit_reg; //数码管位选
localparam S0 = 4'b0000 ,
S1 = 4'b0001 ,
S2 = 4'b0010 ,
S3 = 4'b0011 ,
S4 = 4'b0100 ,
S5 = 4'b0101 ,
S6 = 4'b0110 ,
S7 = 4'b0111 ,
S8 = 4'b1000 ,
S9 = 4'b1001 ;
//———————————————————————————————————扫描计数器——————————————————————————————————
//1ms点亮一个灯,那么4个灯都点亮一遍需要4ms。
//时钟频率24MHz,则时钟周期(1000/24)ns,则计数1ms需要1000000÷(1000/24)=24000
localparam CNT_1MS = 24000-1;
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
cnt_w <=0;
else if(cnt_w==CNT_1MS) //按位与
cnt_w <=0; //计数到最大就清零。感觉也可以省略。
else
cnt_w <=cnt_w + 1'b1;
end
//———————————————————————————————————位选与段选——————————————————————————————————
//位选
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit_reg <=4'b1110;
else if(cnt_w==CNT_1MS)
sm_bit_reg <={sm_bit_reg[2:0], sm_bit_reg[3]};
//else保持,省略
end
//段选
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_seg_num <= 4'b0000;
else begin
case( sm_bit_reg )
4'b1110: sm_seg_num <=num_out[3:0];
4'b1101: sm_seg_num <=num_out[7:4];
4'b1011: sm_seg_num <=num_out[11:8];
4'b0111: sm_seg_num <=num_out[15:12];
endcase
end
end
//段选译码,控制每一位的显示数字
always@(*) begin //共阳极数码管,最高位是小数点,低七位是七段管。0为亮,1为暗。
case ( sm_seg_num )
S0: sm_seg_reg =8'b1100_0000;
S1: sm_seg_reg =8'b1111_1001;
S2: sm_seg_reg =8'b1010_0100;
S3: sm_seg_reg =8'b1011_0000;
S4: sm_seg_reg =8'b1001_1001;
S5: sm_seg_reg =8'b1001_0010;
S6: sm_seg_reg =8'b1000_0010;
S7: sm_seg_reg =8'b1111_1000;
S8: sm_seg_reg =8'b1000_0000;
S9: sm_seg_reg =8'b1001_0000;
default:sm_seg_reg =8'b1100_0000;
endcase
end
assign sm_seg =sm_seg_reg;
assign sm_bit =sm_bit_reg;
endmodule
顶层模块seg_dynamic.v
module top_seg_dynamic
#(
// parameter CNT_MAX =12_000_000-1, //计数0.5s
parameter CNT_MAX =3_000_000-1, //模拟的,快一点
parameter N =$clog2(CNT_MAX)
)
(
input wire clk_24m,
input wire rstn,
output wire[7:0] sm_seg, //数码管段选
output wire[3:0] sm_bit //数码管位选
);
wire [15:0] num_out; //0~f,16个数,需要4bit来存储。现在有四个数码管,需要16bit
counter # (
.CNT_MAX (CNT_MAX),
.N (N)
)
counter_inst (
.clk_24m (clk_24m),
.rstn (rstn),
.num_out (num_out)
);
seg_driver seg_driver_inst (
.clk_24m (clk_24m),
.rstn (rstn),
.num_out (num_out),
.sm_seg (sm_seg),
.sm_bit (sm_bit)
);
/*
seg_driver2 seg_driver2_inst (
.clk_24m (clk_24m),
.rstn (rstn),
.num_out (num_out),
.sm_seg (sm_seg),
.sm_bit (sm_bit)
);
*/
endmodule
引脚分配文件seg_dynamic.adc
set_pin_assignment { clk_24m } { LOCATION = K14; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { rstn } { LOCATION = G11; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { sm_bit[0] } { LOCATION = B1; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[1] } { LOCATION = C3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[2] } { LOCATION = C2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[3] } { LOCATION = F3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[0] } { LOCATION = E3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[1] } { LOCATION = B3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[2] } { LOCATION = F4; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[3] } { LOCATION = E4; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[4] } { LOCATION = F5; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[5] } { LOCATION = D3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[6] } { LOCATION = B2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[7] } { LOCATION = A2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
3.4 上板验证
烧录至开发板
在TD5.6.5中新建工程文件,导入三个设计文件,导入引脚文件,编译后将生成的比特流下载到开发板中。具体步骤可参考基于TD5.6.5与vscode的FPGA简易开发教程——以多路选择器为例。
观察进位与复位
将顶层模块top_seg_static.v
文件中的parameter CNT_TIME = 12_000_000-1
修改为其他更小的值,烧录。
观察各个位的进位是否符合逻辑。按下复位键后,观察是否回到0000。