【项目实战】基于串口校时的数字钟设计
项目要求
- 编写可以通过串口修改时间的简易数字钟
- 使用数码管显示,能够显示时分秒
- 能够接收串口发送过来的设置时间的信息,并修改时间
- 能够将当前时间值通过串口以1秒一次的速率发送到电脑
tip:前几节已经有编写的模块代码,本次主要注重模块之间的调用和顶层的逻辑实现
设计思路
-
在顶层模块中,通过计数器,分别对小时、分钟、秒进行计数,在分别计数到24、60、60后清零
-
要求1可以利用数码管模块实现,在每次计满1秒时,将时分秒的三种数据输入到数码管模块中
-
由于数码管的显示原理,数据变化时间不能过长,否则实际上将观察不到,因此上述数据输入数码管的过程,不能以每秒的变化为周期,而是要以时钟周期(50MHz)为基准
-
数码管显示时,还需要注意,在代码中要将十六进制转换为十进制,数码管的表示限制在0~9.
-
要求2对于时间进行修改,由实际钟表规则可知,在对时分秒任一单位进行修改时,另外的两个单位不变,例如:16:24:23,在对分钟修改为50后,应该从16:50:23开始记起(可能有些规则要求秒钟也清零,这里并未考虑进去)
-
实现要求2,就需要使用接收标识符,每次接收成功后,都成功计数,不同计数对应着不同单位的修改(修改一般可以从时、分、秒或者秒、分、时的顺序展开,这里以前者为准)
-
要求3将实时数据发送给PC机,由于前面的串口发送模块以8bit为一位发送,这里例化3个串口发送模块,实现分别实时发送时分秒数据
fpga框图
代码解析
顶层模块 Digital_clock
`timescale 1ns / 1ps
module Digital_clock(
clk,
reset_n,
uart_rx,
uart_tx1,
uart_tx2,
uart_tx3
);
input clk;
input reset_n;
input uart_rx;
output uart_tx1;
output uart_tx2;
output uart_tx3;
reg [32-1:0] disp_data;
wire [8-1:0] SEL;
wire [8-1:0] SEG;
//仿真需要,计数值减小以便观察
//TIME_==50_000_000-1
parameter TIME_CNT = 5_000-1;//1us
//数码管模块
hex8 hex8(
.clk(clk),
.reset_n(reset_n),
.disp_data(disp_data),
.SEL(SEL),
.SEG(SEG)
);
wire [8-1:0] Data_rx;
wire Rx_Done;
uart_byte_rx uart_byte_rx(
.clk(clk),
.reset_n(reset_n),
.uart_rx(uart_rx),
.baud_set('d4),
.Data(Data_rx),
.Rx_Done(Rx_Done)
);
//发送模块三次例化相关定义
reg [8-1:0] Data_tx1;
reg [8-1:0] Data_tx2;
reg [8-1:0] Data_tx3;
reg Send_go;
wire Tx_Done;
uart_byte_tx uart_byte_tx1(
.clk(clk),
.reset_n(reset_n),
.Data(Data_tx1),
.baud_set('d4),
.Send_go(Send_go),
.Tx_Done(Tx_Done),
.uart_tx(uart_tx1)
);
uart_byte_tx uart_byte_tx2(
.clk(clk),
.reset_n(reset_n),
.Data(Data_tx2),
.baud_set('d4),
.Send_go(Send_go),
.Tx_Done(Tx_Done),
.uart_tx(uart_tx2)
);
uart_byte_tx uart_byte_tx3(
.clk(clk),
.reset_n(reset_n),
.Data(Data_tx3),
.baud_set('d4),
.Send_go(Send_go),
.Tx_Done(Tx_Done),
.uart_tx(uart_tx3)
);
//分频计数,计数周期由参数TIME_CNT确定
reg [26-1:0] div_cnt;
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
div_cnt<=0;
else if(div_cnt>=TIME_CNT)
div_cnt<=0;
else
div_cnt<=div_cnt+1;
end
//时、分、秒计数器
reg [6-1:0] seconds_cnt;
reg [6-1:0] minutes_cnt;
reg [5-1:0] hours_cnt;
//每当分频计数满,即为1秒
//秒计数器
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
seconds_cnt<=0;
else if(div_cnt==TIME_CNT)
if(seconds_cnt==60-1)
seconds_cnt<=0;
else
seconds_cnt<=seconds_cnt+1;
else
seconds_cnt<=seconds_cnt;
end
//分钟计数器
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
minutes_cnt<=0;
else if((seconds_cnt==60-1)&&(div_cnt==TIME_CNT))
if(minutes_cnt==60-1)
minutes_cnt<=0;
else
minutes_cnt<=minutes_cnt+1;
else
minutes_cnt<=minutes_cnt;
end
//小时计数器
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
hours_cnt<=0;
else if((seconds_cnt==60-1)&&(minutes_cnt==60-1)&&(div_cnt==TIME_CNT))
if(hours_cnt==24-1)
hours_cnt<=0;
else
hours_cnt<=hours_cnt+1;
else
hours_cnt<=hours_cnt;
end
//计数结果输出到数码管中
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
disp_data<=0;
else if(div_cnt==TIME_CNT)begin
disp_data[3:0]<=seconds_cnt%10;
disp_data[7:4]<=seconds_cnt/10;
disp_data[11:8]<=0;
disp_data[15:12]<=minutes_cnt%10;
disp_data[19:16]<=minutes_cnt/10;
disp_data[23:20]<=0;
disp_data[27:24]<=hours_cnt%10;
disp_data[31:28]<=hours_cnt/10;
end
else
disp_data<=disp_data;
end
//接收计数,用于区分数据输出类别(时、分、秒)
reg [2-1:0]rx_cnt;
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
rx_cnt<=0;
else if(rx_cnt>=3)
rx_cnt<=0;
else if(Rx_Done==1)
rx_cnt<=rx_cnt+1;
else
rx_cnt<=rx_cnt;
end
//传输控制标识符
//接收成功时置1,计满1秒时置0
reg Trans_CTRL_Done;
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
Trans_CTRL_Done<=0;
else if(Rx_Done==1)
Trans_CTRL_Done<=1;
else if(div_cnt==TIME_CNT)
Trans_CTRL_Done<=0;
else
Trans_CTRL_Done<=Trans_CTRL_Done;
end
//当数据通过串口传输成功后,开始数据更新
always@(posedge clk)begin
if(Trans_CTRL_Done==1)
case(rx_cnt)
1: seconds_cnt<=Data_rx[3:0]+Data_rx[7:4]*10;
2: minutes_cnt<=Data_rx[3:0]+Data_rx[7:4]*10;
0: hours_cnt<=Data_rx[3:0]+Data_rx[7:4]*10;
default:;
endcase
end
//发送控制脉冲,计满1秒置1
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
Send_go<=0;
else if(div_cnt==TIME_CNT)
Send_go<=1;
else
Send_go<=0;
end
//数据发送
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)begin
Data_tx1<=0;
Data_tx2<=0;
Data_tx3<=0;
end
else begin
Data_tx1<=seconds_cnt;
Data_tx2<=minutes_cnt;
Data_tx3<=hours_cnt;
end
end
endmodule
-
顶层模块的输入输出并不多,只有来自PC机的时钟数据修改端口uart_rx,和实时发送时钟端口uart_tx1~3
-
在仿真中,计数时间需要小一点以便观察仿真波形
-
顶层模块和数码管模块的连接只有数据传输disp_data,在传输时要注意十进制的转换
-
当接收到修改数据时,需要区分修改的单位,以秒、分、时的顺序进行修改
-
通过传输控制标识符Trans_CTRL_Done,将数据更新,也就是更改时分秒计数器的值即可
-
因为在数码管显示时,数据接收频率非常高,几乎可以在完成修改的同时就完成显示
-
实时数据发送脉冲Send_go,用于控制串口发送模块的使能
-
时分秒计数器将会实时更新到tx端的数据口上
数码管模块 hex8
`timescale 1ns / 1ps
module hex8(
clk,
reset_n,
disp_data,
SEL,
SEG
);
input clk;
input reset_n;
input [32-1:0] disp_data;
//选择8位数码管
output reg [8-1:0] SEL;
//选择显示数字
output reg [8-1:0] SEG;
//用于选择数码管的计数
reg [3-1:0] num_cnt;
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
num_cnt<=0;
else if(num_cnt>=5)
num_cnt<=0;
else
num_cnt<=num_cnt+1;
end
always@(posedge clk)
case(num_cnt)
0: SEL<=8'b1000_0000;
1: SEL<=8'b0100_0000;
2: SEL<=8'b0001_0000;
3: SEL<=8'b0000_1000;
4: SEL<=8'b0000_0010;
5: SEL<=8'b0000_0001;
endcase
//仿32位data中提取对应数码管位数上的显示数字data
//直接将data和数码管相连掿
reg [4-1:0] disp_temp;
always@(posedge clk)begin
case(num_cnt)
5:disp_temp<=disp_data[3:0];
4:disp_temp<=disp_data[7:4];
3:disp_temp<=disp_data[15:12];
2:disp_temp<=disp_data[19:16];
1:disp_temp<=disp_data[27:24];
0:disp_temp<=disp_data[31:28];
default:;
endcase
end
//数码管显示数字和对应的编码的映射
always@(posedge clk)
case(disp_temp)
0: SEG<=8'hc0;
1: SEG<=8'hf9;
2: SEG<=8'ha4;
3: SEG<=8'hb0;
4: SEG<=8'h99;
5: SEG<=8'h92;
6: SEG<=8'h82;
7: SEG<=8'hf8;
8: SEG<=8'h80;
9: SEG<=8'h90;
4'ha: SEG<=8'h88;
4'hb: SEG<=8'h83;
4'hc: SEG<=8'hc6;
4'hd: SEG<=8'ha1;
4'he: SEG<=8'h86;
4'hf: SEG<=8'h8e;
default:SEG<=8'h00;
endcase
endmodule
串口接收模块 uart_byte_rx
`timescale 1ns/1ps
module uart_byte_rx(
clk,
reset_n,
uart_rx,
baud_set,
Data,
Rx_Done
);
input clk;
input reset_n;
input uart_rx;
input [3-1:0] baud_set;
output reg [8-1:0] Data;
output reg Rx_Done;
// reg width name number
reg [3-1:0] r_data [8-1:0];
reg [3-1:0] sta_bit;
reg [3-1:0] sto_bit;
//边沿检测(上升沿和下降沿)
//通过两位宽的寄存器来实现
//电路实现上面就是两级D触发器
reg [1:0] uart_rx_r;
always@(posedge clk) begin
uart_rx_r[0] <= uart_rx;
uart_rx_r[1] <= uart_rx_r[0];
end
//上升沿,两种写法
wire pedge_uart_rx;
// assign pedge_uart_rx = ((uart_rx_r[1]==0) && (uart_rx_r[0]==1));
assign pedge_uart_rx = (uart_rx_r==2'b01);
//下降沿,两种写法
wire nedge_uart_rx;
// assign nedge_uart_rx = ((uart_rx_r[1]==1) && (uart_rx_r[0]==0));
assign nedge_uart_rx = (uart_rx_r==2'b10);
//波特率设置
//波特率单位,每秒传输的bit数量,()bit/s
//baud_DR计算为每一个bit所需要的时钟周期,这里的单位是ns
//这里时钟周期以50MHz为例,也就是一个周期有20ns,因此是/20
//同时在接收时,将1个bit的数据划分成为16位
reg [8-1:0] Bps_DR;
always@(*)
case(baud_set)
0: Bps_DR<=1_000_000_000/9600/20/16-1;
1: Bps_DR<=1_000_000_000/19200/20/16-1;
2: Bps_DR<=1_000_000_000/38400/20/16-1;
3: Bps_DR<=1_000_000_000/57600/20/16-1;
4: Bps_DR<=1_000_000_000/115200/20/16-1;
default: Bps_DR<=1_000_000_000/9600/20/16-1;
endcase
//接收使能
reg Rx_EN;
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
Rx_EN<=0;
//当接收当下降沿时,开始接收
else if(nedge_uart_rx==1)
Rx_EN<=1;
//当接收成功时,将使能置0
//为避免起始位误判,起始位若为高电平(也就是sta_bit>=4),也将使能置0
else if((Rx_Done==1)||(sta_bit>=4))
Rx_EN<=0;
end
//分频计数
reg [8-1:0] div_cnt;
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
div_cnt<=0;
//在使能时,才开始计数
else if(Rx_EN)begin
//计数1bit所需周期数
if(div_cnt==Bps_DR)
div_cnt<=0;
else
div_cnt<=div_cnt+1;
end
//没有使能时,一直置0
else
div_cnt<=0;
end
//数据传输标识符,只有在1bit数据计数到中间时
//也就是div_cnt==Bps_DR/2时,标识符置1
//相较于bps_clk,进一步16倍划分了
wire bps_clk_16x;
assign bps_clk_16x = (div_cnt==Bps_DR/2);
//数据传输计数
//bps_cnt标记着数据传输的数量
//每次bps_clk出现时,bps_cnt才会加1或者清零
reg [8-1:0] bps_cnt;
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
bps_cnt<=0;
else if(Rx_EN)begin
if(bps_clk_16x)begin
//16倍细分后,总共就需要计数16*(10位bit)=160
if(bps_cnt==160)
bps_cnt<=0;
else
bps_cnt<=bps_cnt+1;
end
else
bps_cnt<=bps_cnt;
end
else
bps_cnt<=0;
end
//核心:数据传输
//对于1bit数据,将其细分为16份,检测中间部分
//通过对中间部分的电平计数,判断该位的电平高低
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)begin
sta_bit<=0;
sto_bit<=0;
r_data[0]<=0;
r_data[1]<=0;
r_data[2]<=0;
r_data[3]<=0;
r_data[4]<=0;
r_data[5]<=0;
r_data[6]<=0;
r_data[7]<=0;
end
else if(bps_clk_16x) begin
//对bps_cnt计数,当技术到每16个区间中间部分时,进行数据判断
//每次都自加上uart_rx数据线上传来的内容
case(bps_cnt)
0:begin
sta_bit<=0;
sto_bit<=0;
r_data[0]<=0;
r_data[1]<=0;
r_data[2]<=0;
r_data[3]<=0;
r_data[4]<=0;
r_data[5]<=0;
r_data[6]<=0;
r_data[7]<=0;
end
5,6,7,8,9,10,11: sta_bit<=sta_bit+uart_rx;
21,22,23,24,25,26,27: r_data[0]<=r_data[0]+uart_rx;
37,38,39,40,41,42,43: r_data[1]<=r_data[1]+uart_rx;
54,55,56,57,58,59,60: r_data[2]<=r_data[2]+uart_rx;
69,70,71,72,73,74,75: r_data[3]<=r_data[3]+uart_rx;
85,86,87,88,89,90,91: r_data[4]<=r_data[4]+uart_rx;
101,102,103,104,105,106,107: r_data[5]<=r_data[5]+uart_rx;
117,118,119,120,121,122,123: r_data[6]<=r_data[6]+uart_rx;
133,134,135,136,137,138,139: r_data[7]<=r_data[7]+uart_rx;
149,150,151,152,153,154,155: sto_bit<=sto_bit+uart_rx;
default:;
endcase
end
end
//在统计完每一位的数据后,进行电平判断
//对bps_cnt计数到每一位的中间部分(7位)累加
//最终高电平有0,1,2,3次出现,判断为低电平
//有4,5,6次出现,则判断为高电平
//判决门限??
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
Data<=0;
else if(bps_clk_16x && (bps_cnt==160))begin
Data[0]<=(r_data[0]>=4)? 1:0;
Data[1]<=(r_data[1]>=4)? 1:0;
Data[2]<=(r_data[2]>=4)? 1:0;
Data[3]<=(r_data[3]>=4)? 1:0;
Data[4]<=(r_data[4]>=4)? 1:0;
Data[5]<=(r_data[5]>=4)? 1:0;
Data[6]<=(r_data[6]>=4)? 1:0;
Data[7]<=(r_data[7]>=4)? 1:0;
end
end
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
Rx_Done<=0;
//当bps_cnt最终计数到第16个bit的最后一份(1/16)时(160)
//再计满半个bit位所花费的周期(Bps_DR/2),则返回接收成功标识符
else if((div_cnt==Bps_DR/2) && (bps_cnt==160))
Rx_Done<=1;
else
Rx_Done<=0;
end
endmodule
串口发送模块 uart_byte_tx
`timescale 1ns/1ps
module uart_byte_tx(
clk,
reset_n,
Data,
baud_set,
Send_go,
Tx_Done,
uart_tx
);
input clk;
input reset_n;
input Send_go;
input [3-1:0] baud_set;
input [8-1:0] Data;
output reg Tx_Done;
output reg uart_tx;
reg [12-1:0] baud_DR;
always@(*)begin
if(reset_n == 0)
baud_DR <= 1_000_000_000/9600/20;
else case (baud_set)
0: baud_DR <= 1_000_000_000/9600/20;
1: baud_DR <= 1_000_000_000/19200/20;
2: baud_DR <= 1_000_000_000/38400/20;
3: baud_DR <= 1_000_000_000/57600/20;
4: baud_DR <= 1_000_000_000/115200/20;
default: baud_DR <= 1_000_000_000/9600/20;
endcase
end
reg [20-1:0] div_cnt;
reg [4-1:0] bps_cnt;
wire bps_clk;
//数据传输时钟,在分频计数丿1时拉髿
//标志弿始新丿轮传辿
assign bps_clk = (div_cnt == 1);
reg Send_en;
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
Send_en <= 1'b1;
else if(Send_go==1)
Send_en <= 1'b1;
else if(Tx_Done==1)
Send_en <= 1'b0;
end
//r_Data = register for data
//用于数据保持,避免数据变更时出现意外
reg [8-1:0] r_Data;
always@(*)begin
if(Send_go==1)
r_Data <= Data;
else
r_Data <= r_Data;
end
//分频计数
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
div_cnt <= 1'b0;
else if(Send_en)begin //Send_en使能控制
if (div_cnt == baud_DR - 1 )
div_cnt <= 1'b0;
else
div_cnt <= div_cnt + 1'b1;
end
else
div_cnt <= 1'b0;
end
//数据传输计数
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
bps_cnt <= 1'b0;
else if(Send_en) begin //Send_en使能控制
if (bps_clk)begin
if(bps_cnt == 11)//1位起姿+8位数捿+1位结板
bps_cnt <= 1'b0;
else
bps_cnt <= bps_cnt + 1'b1;
end
end
else
bps_cnt <= 1'b0;
end
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
uart_tx <= 1'b1;
else if (Send_en) begin
case(bps_cnt)//总计10位,仿1~10,第11位用于收尿
1: uart_tx <= 1'b0;
2: uart_tx <= r_Data[0];
3: uart_tx <= r_Data[1];
4: uart_tx <= r_Data[2];
5: uart_tx <= r_Data[3];
6: uart_tx <= r_Data[4];
7: uart_tx <= r_Data[5];
8: uart_tx <= r_Data[6];
9: uart_tx <= r_Data[7];
10: uart_tx <= 1'b1;
11: uart_tx <= 1'b1;
default: uart_tx <= 1'b1;
endcase
end
end
//发鿁成功标志符
always@(posedge clk or negedge reset_n)begin
if(reset_n==0)
Tx_Done <= 1'b0;
else if(bps_clk==1 && bps_cnt==10)
Tx_Done <= 1'b1;
else
Tx_Done <= 1'b0;
end
endmodule
测试平台 Digital_clock_tb
`timescale 1ns / 1ps
module Digital_clock_tb();
reg clk;
reg reset_n;
reg uart_rx;
wire uart_tx1;
wire uart_tx2;
wire uart_tx3;
Digital_clock Digital_clock(
.clk(clk),
.reset_n(reset_n),
.uart_rx(uart_rx),
.uart_tx1(uart_tx1),
.uart_tx2(uart_tx2),
.uart_tx3(uart_tx3)
);
initial begin
clk=1;
forever #10 clk=~clk;
end
initial begin
reset_n=0;
uart_rx=0;
#201;
reset_n=1;
#200;
#20_000_000
uart_tx_byte(8'h30);
#20_000_000
uart_tx_byte(8'h11);
#20_000_000
uart_tx_byte(8'h5);
#100_000_000;
$stop;
end
task uart_tx_byte;
input [8-1:0] tx_data;
begin
uart_rx = 1;
#20;
uart_rx = 0;
//刚好是1s/115200
//也就是1bit所需要的周期时间
#8680;
uart_rx = tx_data[0];
#8680;
uart_rx = tx_data[1];
#8680;
uart_rx = tx_data[2];
#8680;
uart_rx = tx_data[3];
#8680;
uart_rx = tx_data[4];
#8680;
uart_rx = tx_data[5];
#8680;
uart_rx = tx_data[6];
#8680;
uart_rx = tx_data[7];
#8680;
uart_rx = 1;
#8680;
end
endtask
endmodule
个人理解
在最近阅读verilog数字系统设计教程(夏宇闻)一书的验证部分,对于`测试平台`有了进一步的理解体会。
对于数字系统设计过程中,可以大体分为`设计`和`验证`两个部分。
在初学阶段,一般是完成了逻辑设计后,需要自己编写测试平台(testbench)来验证逻辑是否正确,要通过仿真软件观察波形。
但在实际应用中,设计和验证是分开进行的,在设计完成后交由验证部门进行工作,
同样验证的语法也不仅是现在的简单代码,可以使用更加灵活的编写方式,
如果要加强验证部分的能力,还有System Verilog和UVM等内容需要学习?
加强对于设计和验证部分的理解,对于fpga实现流程也是有帮助的,特别是工程上的理解。
目前项目还存在着不足,还请多多指正。