数字时钟设计

数字时钟设计

要求

基础部分

1.能够显示时、分

2.使用LED显示秒,频率为 \(1\text{Hz}\)

3.两位七段数码管显示时和分,默认显示分钟,按 K4 显示小时

发挥部分

1.按下 K1K2 调整时间,分别为加和减,默认调整分钟,按 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

输入和输出的看变量名就行,不过为什么 AIseg 我要叫 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

屏幕截图 2026-06-09 112025

总结

总体上而言代码量上看再少也少不了多少了,

我这些是加起来总共 \(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_numberclock_timer 搞出来了

因为做基础部分很简单,只需要往死里计数就可以了,

但是做发挥部分就会多出来几个按钮,会比较让人搞不清头脑

所以又是捋了又捋,问了又问,删了又删,

才把第一个模块 clock_number 搞出来

然后第二个模块是喜闻乐见的秒钟发生器,

所以随便写写就行了

然后就是最神的一集

因为我实在是累了,于是开启 deepseek 的自动编程

于是我只是略一询问,deepseek 大人直接生成了所有的显示部分模块

deepseek大人吓哭了

顺带一提,还有 deepseek 大人的代码自动补全

甚至连我写赛题代码都能自动补全,吓哭了

最后是稍微问了一下 deepseek 主函数的事

想了一想根本不需要用时序,直接组合就可以了

遂手搓主函数


写代码其实还好,上电测试就是牢爆了

一开始上电甚至显示都是错的

还没有办法进位

甚至秒针是常亮的

只比隔壁wsp好一点

起码我能显示,虽然那个显示也唐的没边

确实很棘手,不过其实最应该做的还是先修显示的锅

于是查了手册发现 其实和管脚分配有关系

然后发现电平和管脚分配不对

调了调管脚分配发现完全取了反

于是对 seq 的电平取了反,

总算是能输出正常数字了

那么问题来了,我怎么知道它是怎么输出的呢?

于是聪明的我采用了固定时钟和分钟

一开始不知道哪个是时钟哪个是分钟,

那就两个分别设置不同的固定值

然后记住这两个数分别在屏上显示什么

把一个改成另一个,

那么根据显示的值就知道先显示时钟还是先显示分钟

然后就同时知道了两位数对应的错误的 BCD 码和显示的顺序

既然这样的话,那想一想就知道按下按钮到底是高电平还是低电平了

于是光速修好,现在形状是正确的,不过亮度非常鬼畜

先修一下秒针常亮的问题吧,

发现是 \(1 \text{Hz}\) 的脉冲信号作为脉冲太短了

于是修成占空比 \(50\%\) 的方波就可以正常闪秒针了

不过二进制转 BCD 是 AI 写的只能让 AI 调

好在 deepseek 不愧是 deepseek 几下就发现了问题所在

然后调了一下 DIP 就可以了

接下来是不进位问题

仍然没招问 deepseek

发现还是脉冲的问题,于是彻底怒了

把所有的脉冲全部改成了方波

大重写了一手于是所有的功能全部正常

遂凌晨一点半奖励自己朋友圈

鉴定为最疯狂的疯狂星期四


最后则是修了修灯

如果没有问题的话应该是到这里就可以拿满了

我的数电摸鱼五节课的计划终于要跑通了吗?!!

计划通!

posted @ 2026-06-05 01:09  2K22  阅读(35)  评论(0)    收藏  举报