数字时钟设计
数字时钟设计
要求
基础部分
1.能够显示时、分
2.使用LED显示秒,频率为 \(1\text{Hz}\)
3.两位七段数码管显示时和分,默认显示分钟,按 K4 显示小时
发挥部分
1.按下 K1 和 K2 调整时间,分别为加和减,默认调整分钟,按 K4 调整小时
2.整点报时:时间达到整点后,则按 \(1\text{Hz}\) 闪烁对应次数
3.其他
时间处理
默认时钟为 \(12\text{MHz}\),
显然如果造一个 \(1\text{Hz}\) 的时钟,那么时间处理就已经结束了
剩下的部分,有秒钟处理进位就会有分钟,有分钟同理就有时钟
而手搓一个计时器,本质上是一个计数器,
只不过是恰好记到那个频率就清零就行了
注意波形是根据自己的需要来决定的
但是脉冲波信号的变化时间如果太短,有些长信号的变换采样很容易漏掉
所以选择一个合适的时钟非常重要
我是大概估计了一个比较舒适的数值,是 \(0.1s\) 也就是整出来一个 \(10\text{Hz}\) 的信号
这个用来反应按钮
读秒的话则是 \(1\text{Hz}\)
占空比则是常见的 \(50\%\)
注意由于采样的问题,
如果是需要判断的信号是脉冲信号的话,
因为高电平时间很短很容易采不上
点击查看代码
module clock_timer(div,clk,upo);
input wire [26:0] div;
input wire clk;
output reg upo=1'b0;
reg [26:0] cnt=0;
always @(posedge clk)begin
cnt=cnt+1'b1;
if(cnt==div)begin
upo=~upo;
cnt=0;
end
else if(cnt==div/2)upo=~upo;
end
endmodule
得到一个这样的计时器之后,再得到一个计数器就简单了
点击查看代码
module clock_number(val,term,upi,upo,now,clk_sys);
parameter freq=12000000;
input wire clk_sys;
input wire [1:0] val;
input wire [7:0] term;
input wire upi;
output reg upo=0;
output reg [7:0] now=0;
reg upil;
always @(posedge clk_sys)begin
upo=1'b0;
if(val==2'b11)begin
if(upi==1 && upil==0)now=now+1;
end
else if(val==2'b10)now=now+1;
else if(val==2'b01)now=now-1;
if(now==term)begin
if(val==2'b11)upo=~upo;
now=0;
end
else if(now==255)now=term-1;
upil=upi;
end
endmodule
这个计数器则是比较通用的,可以通过 term 控制计数的模值
还有就是做了防溢出,如果不幸减爆了就会自动取值成 term-1
同时 val 集成了按钮的功能,如果不按按钮则是默认高电平,
这个时候就收来自输入的低位进位脉冲 upi 来决定是否进位
有一个小细节是卡准那个 upi 的上升沿,避免重复进位
输出的话两个,一个是 now 用来表示当前位,还有一个 upo 是用来表示输出的进位脉冲
提一嘴,说是脉冲,实际上进位的时间很长,足够系统处理了,只不过看起来像是脉冲一样
不过这里进位的信号高电平的时间约 \(0.1s\),
这里反正系统截高电平的时间也是 \(0.1s\) 所以不用担心采不到高电平的问题
显示处理
这一部分主要是麻烦,并不一定有多难
包括也有很多试错的成分,比方说数码管的 DIP
如果不接的话就会导致七段管显示非常鬼畜
查阅说明书可知那个七段管的高低电平“看起来”好像是自己选的
实际上因为这个 LED 反向导通才能发光所以必须要对应调整
然后就是经典的二进制转 BCD 然后再 BCD 转七段码再根据按钮输出
输出的时钟随便选一个就可以,不过为了快速显示进位,这里用的系统时钟
因为有进位必须要实时显示,建议这里的时钟比进位更快
二进制转BCD码是一个据AI说比较经典的算法,这里不加介绍
如果不嫌麻烦写表也行
点击查看代码
module bin2bcd(
input wire clk,
input wire [7:0] bin,
output reg [7:0] bcd,
output reg done
);
reg [15:0] shift = 16'd0;
reg [4:0] cnt = 4'd0;
reg busy = 1'b0;
always @(posedge clk) begin
done <= 1'b0;
if (!busy) begin
shift={8'b0, bin};
cnt=3'd0;
busy<= 1'b1;
end else begin
if (shift[11:8]>=5)shift[11:8]=shift[11:8]+3;
if (shift[15:12]>=5)shift[15:12]=shift[15:12]+3;
shift = shift << 1;
cnt = cnt + 1;
if (cnt == 8) begin
busy <= 1'b0;
bcd <= shift[15:8];
done <= 1'b1;
end
end
end
endmodule
然后接下来是 BCD 转七段码
这个七段码的顺序和赋值也是有讲究的,
因为和数码管的排序,和 DIP 位的赋值,都有关系
所以排列组合一下方案很多,这里只写我的一种
点击查看代码
module seg14(
input wire [7:0] digit,
output reg [13:0] seg
);
always @(*) begin
case (digit[7:4])
4'd0: seg[13:7] = 7'b1000000;
4'd1: seg[13:7] = 7'b1111001;
4'd2: seg[13:7] = 7'b0100100;
4'd3: seg[13:7] = 7'b0110000;
4'd4: seg[13:7] = 7'b0011001;
4'd5: seg[13:7] = 7'b0010010;
4'd6: seg[13:7] = 7'b0000010;
4'd7: seg[13:7] = 7'b1111000;
4'd8: seg[13:7] = 7'b0000000;
4'd9: seg[13:7] = 7'b0010000;
default: seg[13:7] = 7'b1111111;
endcase
case (digit[3:0])
4'd0: seg[6:0] = 7'b1000000;
4'd1: seg[6:0] = 7'b1111001;
4'd2: seg[6:0] = 7'b0100100;
4'd3: seg[6:0] = 7'b0110000;
4'd4: seg[6:0] = 7'b0011001;
4'd5: seg[6:0] = 7'b0010010;
4'd6: seg[6:0] = 7'b0000010;
4'd7: seg[6:0] = 7'b1111000;
4'd8: seg[6:0] = 7'b0000000;
4'd9: seg[6:0] = 7'b0010000;
default: seg[6:0] = 7'b1111111;
endcase
seg=~seg;
end
endmodule
可以看到确实是直接打表然后复制粘贴的
最后只需要把这两个串起来,
就可以一键从二进制的 now 直接打到数码管输出的 seq 上了
点击查看代码
module display(
input wire clk,
input wire [7:0] now_hr,
input wire [7:0] now_mn,
input wire K,
output reg [13:0] seg
);
wire [7:0] hour_bcd, min_bcd;
bin2bcd u_bcd_hr(.clk(clk), .bin(now_hr), .bcd(hour_bcd));
bin2bcd u_bcd_mn(.clk(clk), .bin(now_mn), .bcd(min_bcd));
wire [7:0] cur_digit = ~K ? hour_bcd : min_bcd;
wire [13:0] seg_out;
seg14 u_seg(.digit(cur_digit), .seg(seg_out));
always @(posedge clk) begin
seg <= seg_out;
end
endmodule
还有就是注意一下那个切换按钮,这里叫 K
输入和输出的看变量名就行,不过为什么 AI 叫 seg 我要叫 seq 呢?
绝对不是我打错字了
注意显示的切换一定要很快!所以这里接的时钟一定是系统时钟!
整点报时
整点报时的逻辑非常简单,
小时数扔进去,然后分钟往小时进位的那个信号顺手触发一下就可以
逻辑也是新开一个计数器,有信号触发就开始工作
没有信号触发就不用管,亮灯就可以,或者不亮灯也行
信号触发了之后给新计数器赋初值,过一秒减一次就可以
不过这个位置因为先前的那个 clock_timer 那个模板是写死了的
然后 Verilog 作为硬件语言显然不会面向对象才有的模板类继承吧
所以最让人恼火的事情发生了:需要把那个计数器的代码重新写一遍
点击查看代码
module tik(clk_sys,in,num,out,K);
//clk 时钟,in输入触发,num次数,out输出,K是模式
parameter freq=12000000;
input wire clk_sys;
input wire in;
input wire [6:0] K;
input wire [7:0] num;
output reg [5:0] out;
reg inl=0;
reg [5:0] res=0;
reg [26:0] now=0;
always @(posedge clk_sys)begin
if(in==1&&inl==0)res=num;
if(res>0)begin
now=now+1;
if(now==freq/2)out=63;
if(now==freq)begin
out=K;
res=res-1;
now=0;
end
end
else out=K;
inl=in;
end
endmodule
clk_sys 和前面所有代码的 clk_sys 一样,
都是接的 \(10\text{Hz}\) 的时钟,
这个是系统响应按钮/时间处理操作的时钟
然后 in 传一个进位信号 upo_mn
表示分钟往时钟进一,来触发这个模块
num 是传闪几次灯,这个参数显然对应的是 hr
不过这个部分有竞争-冒险,会因为进位的信号传递错开一位
这个只能是直接人为修正误差了
now 是计时器的模块,用来计数同时控制 RGB 灯的输出
如果进入闪灯模式的话,
这个 RGB 的行为逻辑在计时器控制下是一个类似方波的形态
res 是表示当前还差几次没闪够,
如果 res 不是 \(0\) 就继续闪灯
否则就是处于常亮状态
然后是 out
这个变量是用来控制灯的,它和输入的 K 是一样的,都表示某种闪灯的模式
但是输入的那个 K 是常亮状态下的模式
如果是闪灯的话,就需要人手动控制亮和灭,
也就是实际上 out 并不完全等于 K
接下来是改灯,
这个很简单,接到一次按钮就改一次就可以了
点击查看代码
module mode_change(K,out,clk_sys);
input wire K;
input wire clk_sys;
output reg [5:0] out=0;
always @(posedge clk_sys)begin
if(K==0)out=out+1;
if(out==63)out=0;
end
endmodule
主函数
主函数就是最简单的了,注意到 wire 是连续变化的
所以主函数实际上不需要时序,只用组合就可以了
先写输入的系统时钟 clk_sys 和按钮 K[4]
然后输出的部分就是时分的显示和秒的闪灯
中间的变量主要是计数器的变量,类型和所属注意区分一下
挂着 hr 的表示时,挂着 mn 的表示分钟,挂着 se 的则表示秒钟
这个所属没有办法封装起来,不过看那个 clock_number 的计数器模板
它的输入和输出,和主函数对应的变量是一致的
这里把 se 单独拎出来是因为发挥部分可能会涉及到 se 的控制
逻辑很简单,就是是产生一个秒的信号,
然后用秒的信号驱动秒针进位给分钟,然后分钟进位给时钟
可以看出为了方便驱动,上面那个 clock_number 其实自己没有内置计时器
点击查看代码
之前的版本是有内置计时器的,
不过想了想,拿这个究竟要用来干嘛?
直接外接一个计时器其实会更好一些
时间处理在主函数搞完之后就要搞显示
这里显示其实是和其他的代码一起并行的,
这与很多计算机语言按照顺序执行并不一致
点击查看代码
注意 always 语句不等于循环结构
那么什么可以算是一个循环结构呢?
答案是计数器,虽然也差得比较远
所以直接放在一起就行
反正会在一起做,甚至不需要考虑顺序
这个就是电路语言的独特优势
不过为了方便照顾代码阅读的习惯,还是有一个内在的逻辑比较好
还有一个改灯的逻辑,由 RGBmode 表示,受 K[2] 的控制
这个部分对应的输出则是 RGB
点击查看代码
module top(clk_sys,K,seq,clk,dig,RGB);
parameter freq=12000000;
parameter HR=24,MN=60,SE=60;
input wire clk_sys;
input wire [3:0]K;
wire [1:0] val=K[1:0];
wire upo_mn,upo_hr,upi_mn,upi_hr;
wire [1:0] val_hr=(K[3]==1'b0)?val:2'b11;
wire [1:0] val_mn=(K[3]==1'b1)?val:2'b11;
wire [1:0] val_se=2'b11;
wire [7:0] now_mn,now_hr,now_se;
wire clk_in;
output wire clk;
clock_timer timer1(.div(freq/10),.clk(clk_sys),.upo(clk_in));
clock_timer clock_se(.div(freq),.clk(clk_sys),.upo(clk));
clock_number se(.val(val_se),.term(SE),
.upi(clk),.upo(upi_mn),
.now(now_se),.clk_sys(clk_in));
clock_number mn(.val(val_mn),.term(MN),
.upi(upi_mn),.upo(upo_mn),
.now(now_mn),.clk_sys(clk_in));
clock_number hr(.val(val_hr),.term(HR),
.upi(upo_mn),.upo(upo_hr),
.now(now_hr),.clk_sys(clk_in));
output wire [13:0] seq;
output wire [1:0] dig;
assign dig=2'b00;
wire [5:0] RGBmode;
output wire [5:0]RGB;
mode_change mode(.K(K[2]),.clk_sys(clk_in),.out(RGBmode));
tik talker(.clk_sys(clk_sys),.in(upo_mn),.num(now_hr),.out(RGB),.K(RGBmode));
display dis(.clk(clk_sys),.now_hr(now_hr),.now_mn(now_mn),.K(K[3]),.seg(seq));
endmodule

总结
总体上而言代码量上看再少也少不了多少了,
我这些是加起来总共 \(200\) 行左右
也是吃了很多语法和风格不太熟悉的亏,
问了 deepseek 很多问题才搞明白,
也包括很多试错的时间
不过如果纯依赖 AI 的话自己如果不会,
那么显然是更加费时间的
不过都已经 \(6062\) 年了还有人坚持古法编程吗
最后就是推荐用一些好点的 AI,这个还用豆包真的能调出来吗
点击查看代码
恐怖 DEEPSEEK 大人通过 AI 接入 COPILOT
一键代码补全大杀四方
使我痛失一瓶可乐钱
点击查看代码
module bin2bcd(
input wire clk,
input wire [7:0] bin,
output reg [7:0] bcd,
output reg done
);
reg [15:0] shift = 16'd0;
reg [4:0] cnt = 4'd0;
reg busy = 1'b0;
always @(posedge clk) begin
done <= 1'b0;
if (!busy) begin
shift={8'b0, bin};
cnt=3'd0;
busy<= 1'b1;
end else begin
if (shift[11:8]>=5)shift[11:8]=shift[11:8]+3;
if (shift[15:12]>=5)shift[15:12]=shift[15:12]+3;
shift = shift << 1;
cnt = cnt + 1;
if (cnt == 8) begin
busy <= 1'b0;
bcd <= shift[15:8];
done <= 1'b1;
end
end
end
endmodule
module seg14(
input wire [7:0] digit,
output reg [13:0] seg
);
always @(*) begin
case (digit[7:4])
4'd0: seg[13:7] = 7'b1000000;
4'd1: seg[13:7] = 7'b1111001;
4'd2: seg[13:7] = 7'b0100100;
4'd3: seg[13:7] = 7'b0110000;
4'd4: seg[13:7] = 7'b0011001;
4'd5: seg[13:7] = 7'b0010010;
4'd6: seg[13:7] = 7'b0000010;
4'd7: seg[13:7] = 7'b1111000;
4'd8: seg[13:7] = 7'b0000000;
4'd9: seg[13:7] = 7'b0010000;
default: seg[13:7] = 7'b1111111;
endcase
case (digit[3:0])
4'd0: seg[6:0] = 7'b1000000;
4'd1: seg[6:0] = 7'b1111001;
4'd2: seg[6:0] = 7'b0100100;
4'd3: seg[6:0] = 7'b0110000;
4'd4: seg[6:0] = 7'b0011001;
4'd5: seg[6:0] = 7'b0010010;
4'd6: seg[6:0] = 7'b0000010;
4'd7: seg[6:0] = 7'b1111000;
4'd8: seg[6:0] = 7'b0000000;
4'd9: seg[6:0] = 7'b0010000;
default: seg[6:0] = 7'b1111111;
endcase
end
endmodule
module display(
input wire clk,
input wire [7:0] now_hr,
input wire [7:0] now_mn,
input wire K,
output reg [13:0] seg
);
wire [7:0] hour_bcd, min_bcd;
bin2bcd u_bcd_hr(.clk(clk), .bin(now_hr), .bcd(hour_bcd));
bin2bcd u_bcd_mn(.clk(clk), .bin(now_mn), .bcd(min_bcd));
wire [7:0] cur_digit = K ? hour_bcd : min_bcd;
wire [13:0] seg_out;
seg14 u_seg(.digit(cur_digit), .seg(seg_out));
always @(posedge clk) begin
seg <= seg_out;
end
endmodule
module clock_number(val,term,upi,upo,now,clk_sys);
parameter freq=12000000;
input wire clk_sys;
input wire [1:0] val;
input wire [7:0] term;
input wire upi;
output reg upo=0;
output reg [7:0] now=0;
wire clk_in;
reg upil;
clock_timer timer1(.div(freq/10),.clk(clk_sys),.upo(clk_in));
always @(posedge clk_in)begin
upo=1'b0;
if(val==2'b11)begin
if(upi==1 && upil==0)now=now+1;
end
else if(val==2'b01)now=now+1;
else if(val==2'b10)now=now-1;
if(now==term)begin
if(val==2'b11)upo=~upo;
now=0;
end
else if(now==255)now=term-1;
upil=upi;
end
endmodule
module clock_timer(div,clk,upo);
input wire [26:0] div;
input wire clk;
output reg upo=1'b0;
reg [26:0] cnt=0;
always @(posedge clk)begin
cnt=cnt+1'b1;
if(cnt==div)begin
upo=~upo;
cnt=0;
end
else if(cnt==div/2)upo=~upo;
end
endmodule
module top(clk_sys,K,seq,clk,dig);
parameter freq=12000000;
parameter HR=24,MN=60;
input wire clk_sys;
input wire [3:0]K;
wire [1:0] val=K[1:0];
wire upo_mn=0,upo_hr=0,upi_mn=0,upi_hr=0;
wire [1:0] val_hr=(K[3]==1'b0)?val:2'b11;
wire [1:0] val_mn=(K[3]==1'b1)?val:2'b11;
wire [7:0] now_mn,now_hr;
output wire clk;
wire se;
clock_timer clock_se(.div(freq),.clk(clk_sys),.upo(clk));
clock_timer sec(.div(freq).clk(clk_sys),.upo(se));
clock_timer clock_mn(.div(60),.clk(se),.upo(upi_mn));
clock_number hr(.val(val_hr),.term(HR),
.upi(upo_mn),.upo(upo_hr),
.now(now_hr),.clk_sys(clk_sys));
clock_number mn(.val(val_mn),.term(MN),
.upi(upi_mn),.upo(upo_mn),
.now(now_mn),.clk_sys(clk_sys));
output wire [13:0] seq;
output wire [1:0] dig;
assign dig=0;
display dis(.clk(clk_sys),.now_hr(now_hr),.now_mn(now_mn),.K(K[3]),.seg(seq));
endmodule
复盘
起因是上个数电实验完全是由 wss 这个神秘人物完成设计搭建调试的
结果到最后一下午加上一个上午过去了没调出来
这个能是人?
然后那天早上突然萌生了一个使用 deepseek api 的想法
于是就用了 deepseek api 把它接入了 Copilot
于是打开了潘多拉魔盒
点击查看代码
虽然早就知道现在 `AI` 发达到不止一点半点,
尤其是写代码这一块,最顶尖的大模型在一年前还对很多东西无能为力
现在已经可以在算法竞赛领域随意战胜最顶尖的人类强者了
但是还是被 `AI` 的很多辅助功能吓哭了
简单地试了试水,想了想启航赛的项目能不能做
发现好像能做就交流了一下自己的理解
然后就开始学 Verilog 语法
其实一开始写代码的时候很唐,一直在问 deepseek 老师
然后发现自己好像写逻辑的时候更唐
因为完全不知道怎么写
于是想了又想,参考了又参考
终于算是想明白逻辑搞清楚代码了
于是花费长达两节课的自习把 clock_number 和 clock_timer 搞出来了
因为做基础部分很简单,只需要往死里计数就可以了,
但是做发挥部分就会多出来几个按钮,会比较让人搞不清头脑
所以又是捋了又捋,问了又问,删了又删,
才把第一个模块 clock_number 搞出来
然后第二个模块是喜闻乐见的秒钟发生器,
所以随便写写就行了
然后就是最神的一集
因为我实在是累了,于是开启 deepseek 的自动编程
于是我只是略一询问,deepseek 大人直接生成了所有的显示部分模块
被deepseek大人吓哭了
顺带一提,还有 deepseek 大人的代码自动补全
甚至连我写赛题代码都能自动补全,吓哭了
最后是稍微问了一下 deepseek 主函数的事
想了一想根本不需要用时序,直接组合就可以了
遂手搓主函数
写代码其实还好,上电测试就是牢爆了
一开始上电甚至显示都是错的
还没有办法进位
甚至秒针是常亮的
只比隔壁wsp好一点
起码我能显示,虽然那个显示也唐的没边
确实很棘手,不过其实最应该做的还是先修显示的锅
于是查了手册发现 其实和管脚分配有关系
然后发现电平和管脚分配不对
调了调管脚分配发现完全取了反
于是对 seq 的电平取了反,
总算是能输出正常数字了
那么问题来了,我怎么知道它是怎么输出的呢?
于是聪明的我采用了固定时钟和分钟
一开始不知道哪个是时钟哪个是分钟,
那就两个分别设置不同的固定值
然后记住这两个数分别在屏上显示什么
把一个改成另一个,
那么根据显示的值就知道先显示时钟还是先显示分钟
然后就同时知道了两位数对应的错误的 BCD 码和显示的顺序
既然这样的话,那想一想就知道按下按钮到底是高电平还是低电平了
于是光速修好,现在形状是正确的,不过亮度非常鬼畜
先修一下秒针常亮的问题吧,
发现是 \(1 \text{Hz}\) 的脉冲信号作为脉冲太短了
于是修成占空比 \(50\%\) 的方波就可以正常闪秒针了
不过二进制转 BCD 是 AI 写的只能让 AI 调
好在 deepseek 不愧是 deepseek 几下就发现了问题所在
然后调了一下 DIP 就可以了
接下来是不进位问题
仍然没招问 deepseek
发现还是脉冲的问题,于是彻底怒了
把所有的脉冲全部改成了方波
大重写了一手于是所有的功能全部正常
遂凌晨一点半奖励自己朋友圈
鉴定为最疯狂的疯狂星期四
最后则是修了修灯
如果没有问题的话应该是到这里就可以拿满了
我的数电摸鱼五节课的计划终于要跑通了吗?!!
计划通!

浙公网安备 33010602011771号