赞
踩
通过74HC595芯片点亮8位数码管,通过计时器实现数码管计时显示
软件开发环境高云V1.99版本
硬件开发环境采用小梅哥ACG525(主芯片GW5A-LV25-UG324C2)
本章节内容分为分两个子模块和一个主模块,一个子模块负责驱动74hc595一个子模块负责数码管显示,主模块负责计时和调用子模块进行数码管显示。
首先我们设计数码管显示子模块,数码管有两种结构:共阴极与共阳极。 这两者的区别在于,公共端是连接到地还是高电平,对于共阴数码管需要给对应段以高电平才会使其点亮, 而对于共阳极数码管则需要给低电平才会点亮。 本次实验环境上是使用的共阳数码管,同时为了显示数字或字符,必须对数字或字符进行编码译码。这里的表格dp点默认全部显示,当我们需要显示dp点的时候与上8’b01111111即可,相应的关闭显示就或上8’b10000000;
数码管的显示方式也分为两种静态显示和动态显示,静态显示的特点是每个数码管的段选必须接一个 8 位数据线来保持显示的字形码。当送入一次字形码后,显示字形可一直保持,直到送入新字形码为止。这种方法由于每一个数码管均需要独立的数据线因此硬件电路比较复杂,成本较高,很少使用为了节约 IO 以及成本一般采用如下图所示的电路结构,这样 3 个数码管接在一起就比静态的少了 7*2 个 I/O。
这样就实现了另一种显示模式,动态显示。动态显示的特点是将所有位数码管的段选线并联在一起,由位选线控制是哪一位数码管有效。选亮数码管采用动态扫描显示。所谓动态扫描显示即轮流向各位数码管送出字形码和相应的位选,利用发光管的余辉和人眼视觉暂留作用,使人的感觉好像各位数码管同时都在显示,板载设计的电路为动态显示电路,下面开始设计数码管显示代码;
位选控制也就是我们上面说到的轮流选择8位数码管的位信号线(sel);
/*********************位选扫描时钟**********************/ reg clk_1k; // 1k时钟 reg [14:0]clk_count; // 系统时钟计数 reg [7:0]sel_reg;//数码管位选寄存器,通过信号传送给seg_sel // 系统clk采用50mhz进行计算 always@(posedge clk or posedge reset) begin if(reset) clk_count <= 15'd0; else if(!en) clk_count <= 15'd0; else if(clk_count == 24999) clk_count <= 15'd0; else clk_count <= clk_count + 1'b1; end always@(posedge clk or posedge reset) begin if(reset) clk_1k <= 1'b0; else if(clk_count == 24999) clk_1k <= ~clk_1k; else clk_1k <= clk_1k; end always@(posedge clk_1k or posedge reset) begin if(reset) sel_reg <= 8'b0000_0001; else if(sel_reg == 8'b1000_0000) sel_reg <= 8'b0000_0001; else sel_reg <= sel_reg << 1; end assign sel = (en)?sel_reg:8'b0000_0000;
段选信号,通过上面的译码表实现查表传送给seg端口,这里的dp点显示我以位判断的方式进行显示,哪一位需要显示dp点给dp寄存器哪一位或1即可
/***********************段选数据***********************/ reg [3:0]data_tmp;//数据缓存 always@(*) begin case(sel_reg) 8'b0000_0001:data_tmp = disp_data[3:0]; 8'b0000_0010:data_tmp = disp_data[7:4]; 8'b0000_0100:data_tmp = disp_data[11:8]; 8'b0000_1000:data_tmp = disp_data[15:12]; 8'b0001_0000:data_tmp = disp_data[19:16]; 8'b0010_0000:data_tmp = disp_data[23:20]; 8'b0100_0000:data_tmp = disp_data[27:24]; 8'b1000_0000:data_tmp = disp_data[31:28]; default:data_tmp = 4'b0000; endcase end always@(*) begin case(data_tmp) 4'h0:seg = 8'b11000000; 4'h1:seg = 8'b11111001; 4'h2:seg = 8'b10100100; 4'h3:seg = 8'b10110000; 4'h4:seg = 8'b10011001; 4'h5:seg = 8'b10010010; 4'h6:seg = 8'b10000010; 4'h7:seg = 8'b11111000; 4'h8:seg = 8'b10000000; 4'h9:seg = 8'b10010000; 4'ha:seg = 8'b10001000; 4'hb:seg = 8'b10000011; 4'hc:seg = 8'b11000110; 4'hd:seg = 8'b10100001; 4'he:seg = 8'b10000110; 4'hf:seg = 8'b10001110; endcase if((sel_reg&dp) > 0) seg = seg & 8'b01111111; end
完整代码
module hex8( input clk, input reset_n, input en, input [31:0]disp_data, //显示数据 input [7:0]dp, output [7:0]sel,//数码管段选(当前要显示的内容) output reg [7:0]seg //数码管位选(选择当前要显示的数码管) ); assign reset=~reset_n; /*********************位选扫描时钟**********************/ reg clk_1k; // 1k时钟 reg [14:0]clk_count; // 系统时钟计数 reg [7:0]sel_reg;//数码管位选寄存器,通过信号传送给seg_sel always@(posedge clk or posedge reset) begin if(reset) clk_count <= 15'd0; else if(!en) clk_count <= 15'd0; else if(clk_count == 24999) clk_count <= 15'd0; else clk_count <= clk_count + 1'b1; end always@(posedge clk or posedge reset) begin if(reset) clk_1k <= 1'b0; else if(clk_count == 24999) clk_1k <= ~clk_1k; else clk_1k <= clk_1k; end always@(posedge clk_1k or posedge reset) begin if(reset) sel_reg <= 8'b0000_0001; else if(sel_reg == 8'b1000_0000) sel_reg <= 8'b0000_0001; else sel_reg <= sel_reg << 1; end assign sel = (en)?sel_reg:8'b0000_0000; /***********************段选数据***********************/ reg [3:0]data_tmp;//数据缓存 always@(*) begin case(sel_reg) 8'b0000_0001:data_tmp = disp_data[3:0]; 8'b0000_0010:data_tmp = disp_data[7:4]; 8'b0000_0100:data_tmp = disp_data[11:8]; 8'b0000_1000:data_tmp = disp_data[15:12]; 8'b0001_0000:data_tmp = disp_data[19:16]; 8'b0010_0000:data_tmp = disp_data[23:20]; 8'b0100_0000:data_tmp = disp_data[27:24]; 8'b1000_0000:data_tmp = disp_data[31:28]; default:data_tmp = 4'b0000; endcase end always@(*) begin case(data_tmp) 4'h0:seg = 8'b11000000; 4'h1:seg = 8'b11111001; 4'h2:seg = 8'b10100100; 4'h3:seg = 8'b10110000; 4'h4:seg = 8'b10011001; 4'h5:seg = 8'b10010010; 4'h6:seg = 8'b10000010; 4'h7:seg = 8'b11111000; 4'h8:seg = 8'b10000000; 4'h9:seg = 8'b10010000; 4'ha:seg = 8'b10001000; 4'hb:seg = 8'b10000011; 4'hc:seg = 8'b11000110; 4'hd:seg = 8'b10100001; 4'he:seg = 8'b10000110; 4'hf:seg = 8'b10001110; endcase if((sel_reg&dp) > 0) seg = seg & 8'b01111111; end endmodule
模块使用
wire [31:0]disp_data; wire [7:0] sel;//数码管位选(选择当前要显示的数码管) wire [7:0] seg;//数码管段选(当前要显示的内容) wire [7:0]dp_data;//数码管小数点(某位点亮某位置1) reg dp_flag; // 小数点寄存器 reg [31:0]disp_data_reg = 32'h00000000; // 显示数据寄存器 hex8 hex8_mod( .clk(clk), // 50m时钟 .reset_n(reset_n), // 复位信号 .en(1'b1), // 使能模块寄存器 .disp_data(disp_data), // 32位数据显示,每一个数码管可以显示0-f占4位 .sel(sel), // 位选信号 .seg(seg), // 段选信号 .dp(dp_data) // 8位dp信号 );
为了节省IO引脚开发板数码管设计采用了74HC595来扩展IO,该芯片的作用是位移位寄存器,FPGA 只需要输出 3 个管脚,即可达到发
送数码管数据的目的,与传统段选、位选方式相比,大大节省了 IO 设计资源,在该原理图下,将第一片74HC595的Q7‘串行输出端接到第二片的数据输入端Ds,实现级联功能。经过14个时钟SHcp上升沿后,数据已经全部移位进入移位寄存器,一次共输入14位数据,那么第一位输入的串行数据会在第二片74HC595芯片的Q5输出,此时给一个上升沿的STcp信号就可以将信号移入存储寄存器,OE信号持续给低, 即可输出。这里贴一个博主做的很好理解的gif图。
设计代码如下
module hc595( input clk, input reset_n, input [15:0]data, input s_en, output reg sh_cp, output reg st_cp, output reg ds ); assign reset=~reset_n; /*******************时钟模块*********************************/ parameter CNT_MAX = 2; reg [15:0]r_data; //数据寄存器 reg [7:0]clk_count;//分频计数器; always@(posedge clk) begin if(s_en) r_data <= data; end always@(posedge clk or posedge reset) begin if(reset) clk_count <= 0; else if(clk_count == CNT_MAX - 1'b1) clk_count <= 0; else clk_count <= clk_count + 1'b1; end wire sck_plus; assign sck_plus = (clk_count == CNT_MAX - 1'b1); //对 sck_pluse进行计数, 用于查找表实现数据的串行输入以及移位时钟 sh_cp与存储时钟 st_cp 的产生 reg [5:0]SHCP_EDGE_CNT; always@(posedge clk or posedge reset) begin if(reset) SHCP_EDGE_CNT <= 0; else if(sck_plus)begin if(SHCP_EDGE_CNT == 6'd32) SHCP_EDGE_CNT <= 0; else SHCP_EDGE_CNT <= SHCP_EDGE_CNT + 1'b1; end else SHCP_EDGE_CNT <= SHCP_EDGE_CNT; end // 查找表实现状态输出 always@(posedge clk or posedge reset) begin if(reset)begin st_cp <= 1'b0; ds <= 1'b0; sh_cp <= 1'd0; end else begin case(SHCP_EDGE_CNT) 0: begin sh_cp <= 0; st_cp <= 1'd0;ds <= r_data[15];end 1: begin sh_cp <= 1; st_cp <= 1'd0;end 2: begin sh_cp <= 0; ds <= r_data[14];end 3: begin sh_cp <= 1; end 4: begin sh_cp <= 0; ds <= r_data[13];end 5: begin sh_cp <= 1; end 6: begin sh_cp <= 0; ds <= r_data[12];end 7: begin sh_cp <= 1; end 8: begin sh_cp <= 0; ds <= r_data[11];end 9: begin sh_cp <= 1; end 10: begin sh_cp <= 0; ds <= r_data[10];end 11: begin sh_cp <= 1; end 12: begin sh_cp <= 0; ds <= r_data[9];end 13: begin sh_cp <= 1; end 14: begin sh_cp <= 0; ds <= r_data[8];end 15: begin sh_cp <= 1; end 16: begin sh_cp <= 0; ds <= r_data[7];end 17: begin sh_cp <= 1; end 18: begin sh_cp <= 0; ds <= r_data[6];end 19: begin sh_cp <= 1; end 20: begin sh_cp <= 0; ds <= r_data[5];end 21: begin sh_cp <= 1; end 22: begin sh_cp <= 0; ds <= r_data[4];end 23: begin sh_cp <= 1; end 24: begin sh_cp <= 0; ds <= r_data[3];end 25: begin sh_cp <= 1; end 26: begin sh_cp <= 0; ds <= r_data[2];end 27: begin sh_cp <= 1; end 28: begin sh_cp <= 0; ds <= r_data[1];end 29: begin sh_cp <= 1; end 30: begin sh_cp <= 0; ds <= r_data[0];end 31: begin sh_cp <= 1; end 32: st_cp <= 1'd1; default: begin st_cp <= 1'b0; ds <= 1'b0; sh_cp <= 1'd0; end endcase end end endmodule
完成了数码管驱动的显示,要实现计时的功能我们需要在主模块中设计一个1秒定时器来实现计数,然后在调用我们写好的模块将计数值传入到数码管中进行显示,为了更简单的理解这里没有添加更多的功能,但接口都是比较完整的可以自行DIY优化设计属于自己的数值钟,这里开拓一些功能(按键调整时间,设置定时器闹钟,轮流显示日期温度和时间,网络对时,掉点保存等等),好下面开始设计我们的简单计时代码;
module seg_top( input clk, // system 50m input reset_n, output sh_cp, output st_cp, output ds ); parameter MCNT = 49_999_999; // 一秒计数器 wire [31:0]disp_data; wire [7:0] sel;//数码管位选(选择当前要显示的数码管) wire [7:0] seg;//数码管段选(当前要显示的内容) wire [7:0]dp_data;//数码管小数点(某位点亮某位置1) reg dp_flag; // 小数点寄存器 reg [31:0]disp_data_reg = 32'h00000000; // 显示数据寄存器 reg [25:0]cnt; //定义计数器寄存器 reg[3:0] hour_reg_h; reg[3:0] hour_reg_l; reg[3:0] min_reg_h; reg[3:0] min_reg_l; reg[3:0] sec_reg_h; reg[3:0] sec_reg_l; //assign disp_data = disp_data_reg; assign disp_data = {4'h2,4'h4,hour_reg_h,hour_reg_l,min_reg_h,min_reg_l,sec_reg_h,sec_reg_l}; assign dp_data = (dp_flag) ? 8'b0101_0100 : 8'b0101_0000; hc595 hc595_mod( .clk(clk), .reset_n(reset_n), .data({seg,sel}), .s_en(1'b1), .sh_cp(sh_cp), .st_cp(st_cp), .ds(ds) ); hex8 hex8_mod( .clk(clk), .reset_n(reset_n), .en(1'b1), .disp_data(disp_data), .sel(sel), .seg(seg), .dp(dp_data) ); assign reset=~reset_n; //计数器计数进程 always@(posedge clk or posedge reset) begin if(reset) cnt <= 25'd0; else if(cnt == MCNT) cnt <= 25'd0; else cnt <= cnt + 1'b1; end //时钟 输出控制进程 always@(posedge clk or posedge reset) begin if(reset) begin dp_flag <= 1'b1; disp_data_reg = 0; end else if(cnt == 24_999_999) // 0.5反转一次dp点 dp_flag <= ~dp_flag; else if(cnt == MCNT) // 一秒计时 begin dp_flag <= ~dp_flag; disp_data_reg = disp_data_reg+1; sec_reg_l = sec_reg_l+1; if(sec_reg_l == 4'd9) begin sec_reg_l <= 0; sec_reg_h <= sec_reg_h + 1; if(sec_reg_h == 4'd5) begin sec_reg_h <= 0; min_reg_l <= min_reg_l + 1; if(min_reg_l == 4'd9) begin min_reg_l <= 0; min_reg_h <= min_reg_h + 1; if(min_reg_h == 4'd5) begin min_reg_h <= 0; hour_reg_l <= hour_reg_l + 1; if((hour_reg_h==4'd2) && (hour_reg_l==4'd3)) begin hour_reg_l <=0; hour_reg_h <=0; end if(hour_reg_l == 4'd9) begin hour_reg_l <= 0; hour_reg_h <= hour_reg_h + 1; end end end end end end else dp_flag <= dp_flag; end endmodule
当逻辑输入设计完成后需要对其进行验证,该部分由软件部分进行验证,如果逻辑输入有问题需要检查语法错误或则重新设计设计输入
当分析和综合通过后应该进行功能性验证,针对项目设计定义的功能使用设计的逻辑输入验证其功能能否实现,一般的做法都是通过功能仿真的方式进行验证,比如软件逻辑分析仪,modelsim、vivado等软件自带的仿真工具进行仿真验证
功能仿真也称为行为仿真,主旨在于验证电路的功能是否符合设计要求,其特点是不考虑电路门延迟与线延迟,主要是验证电路与理想情况是否一致。也可以叫做RTL仿真(test bench)
`timescale 1ns/1ns `define clk_period 20 module hex8_tb; reg clk; //50M reg reset_n; reg en; //数码管显示使能,1使能_0关闭 reg [31:0]disp_data; wire [7:0] sel;//数码管位选(选择当前要显示的数码管) wire [6:0] seg;//数码管段选(当前要显示的内容) hex8 hex8( .clk(clk), .reset_n(reset_n), .en(en), .disp_data(disp_data), .sel(sel), .seg(seg) ); initial clk = 1; always#(`clk_period/2) clk = ~clk; initial begin reset_n = 1'b0; en = 1; disp_data = 32'h12345678; #(`clk_period*20); reset_n = 1; #(`clk_period*20); #20000000; disp_data = 32'h87654321; #20000000; disp_data = 32'h89abcdef; #20000000; $stop; end endmodule
`timescale 1ns/1ns `define clk_period 20 module hc595_tb; reg clk; reg reset_n; reg [15 : 0] data; //data to send reg s_en; //send en wire sh_cp; //shift clock wire st_cp; //latch data clock wire ds; //shift serial data hc595 hc595_mod( .clk(clk), .reset_n(reset_n), .data(data), .s_en(s_en), .sh_cp(sh_cp), .st_cp(st_cp), .ds(ds) ); initial clk = 1; always#(`clk_period/2) clk = ~clk; initial begin reset_n = 1'b0; s_en = 1; data = 16'b1010_1111_0110_0101; #(`clk_period*20); reset_n = 1; #(`clk_period*20); #5000; data = 16'b0101_0101_1010_0101; #5000; $stop; end endmodule
当我们的IO可以开始分配了我们首先需要分配IO,如果当前还没到IO分配的时候我们可以将IO分配放在最后,当IO分配完成后我们就可以通过软件进行布局布线,在芯片内部生成芯片电路
时序仿真也称为布局布线后仿真,是指电路已经映射到特定的工艺环境以后,综合考虑电路的路径延迟与门延迟的影响,验证电路能否在一定时序条件下满足设计构想的过程,能较好地反映芯片的实际工作情况,当时序仿真不通过的时候可能还会设计到时序约束的一个过程(在比较复杂的设计中也需要用到);这个在上一个步骤(功能仿真中)我们已经完成了时序仿真的波形查看
如果在布局布线时未进行IO分配在该步骤进行IO分配并生成BIT流文件,这里我们直接查看小梅哥给出的excel文档,填写我们需要输出的时钟引脚;
//Copyright (C)2014-2023 Gowin Semiconductor Corporation. //All rights reserved. //File Title: Physical Constraints file //Tool Version: V1.9.9 (64-bit) //Part Number: GW5A-LV25UG324C2/I1 //Device: GW5A-25 //Device Version: A //Created Time: Sat 02 17 19:32:14 2024 IO_LOC "ds" F4; IO_PORT "ds" PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; IO_LOC "st_cp" F3; IO_PORT "st_cp" PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; IO_LOC "sh_cp" H4; IO_PORT "sh_cp" PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; IO_LOC "reset_n" B16; IO_PORT "reset_n" PULL_MODE=NONE BANK_VCCIO=3.3; IO_LOC "clk" T9; IO_PORT "clk" PULL_MODE=NONE BANK_VCCIO=3.3;
到这里软件的模拟仿真验证就完成了最后是烧录到板子上进行测试验证
时序通过了后需要通过硬件进行验证也就是最后一步的实物验证
当系统出现问题运行不正常我们可以通过外部硬件示波器或者逻辑分析仪进行实际引脚信号抓取分析
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。