??目前我遇到过的数码管驱动电路大致分为三种,一种采用三极管位选电路驱动,所有数码管使用同一组数据线,每个数据管使用一个位选信号,需要使用的引脚个数等于8根数据线加上数码管个数。
图1 三极管驱动数码管
??第二种位选信号通过三八译码器驱动,八个数码管的位选信号就只需要3个引脚驱动,如下图所示。每增加8个数码管,只需要增加3个引脚,比使用三极管驱动更加节省IO。
图2 三八译码器驱动数码管
??最后还有一种采用移位寄存器芯片来驱动数码管的,最开始还是在稚晖君的键盘信号采集的时候看到过移位寄存器芯片的使用。如果想要最大限度节省引脚,使用74HC595这样的移位寄存器最好。如下图所示,采用移位寄存器驱动数码管,8个数码管只需要三个引脚就能控制。使用这三个引脚可以驱动任意个数码管,这就是其强大的地方。
图3 74HC595驱动数码管
??第一种电路的驱动方式在前文已经做了详细讲解,第二种只需要把第一种位选信号修改即可,第三种电路需要在第一种电路的基础上添加一个74HC595的驱动模块。本文对75HC595的工作方式及时序进行讲解,最后实现该模块。
1、74HC595芯片
??74HC595就是一款移位寄存器,只要学过数电,下面电路应该不难理解吧,就是16个D触发器构成的电路,对应74HC595的主要功能(复位等辅助功能没有画)。
图4 74HC595芯片内部等价图
??如上图所示,ds是串行数据输入,Q0~Q7是并行数据输出,74HC595实现串行输入转并行输出。串行输入数据在移位时钟sclk的上升沿从D触发器的输入端口传输到输出端口,之后保持不变,所以ds输入的数据经过8个时钟周期到达Q7S端口,输出该芯片。此时如果rclk出现上升沿,那么Q0~Q7将输出这八个时钟周期DS输入的数据。
??注意这个Q7S引脚,该引脚还可以连接下一片74HC595芯片的数据输入管脚ds,实现更多并行数据的输出。
??下图是恩智浦的74HC595芯片的内部结构图,STAGE0~STAGE7这八个D触发器对应的就是图4中蓝色的D触发器,而锁存器LATCH就是图4中橙色的D触发器。由于锁存器的成本比D触发器成本更低,输出端口一般会采用锁存器。
??DS就是图4中串行数据线,SHCP就是移位寄存器时钟信号sclk,STCP就是图4中锁存时钟rclk。锁存器的输出接了一个三态门,由OE控制,OE有效的时候,锁存器的输出才会到达引脚,无效的时候,Q0~Q7就是高阻态。MR信号是移位寄存器的复位信号。
图5 74HC595芯片内部结构图
??因此,74HC595芯片就是通过8个D触发器组成的移位寄存器,输入的串行数据DS在移位时钟SHCP的上升沿依次移入每个D触发器,经过8个时钟周期后,DS第一个数据到达STAGE7的输出端口Q7S,DS最后一个数据位于STAGE0输出端口。此时锁存器的时钟信号STCP出现上升沿,则8个锁存器就会把8个D触发器输出端口的数据输出到Q0~Q7端口。
??下图是来自74HC595芯片手册的接口时序图,图中移位寄存器时钟SHCP的上升沿与锁存器时钟STCP的下降沿对齐,当DS的数据在移位寄存器SHCP的上升沿进入D触发器后,接着锁存器的时钟STCP就会到来,就会将8个移位寄存器的数据输出到Q0~Q7端口(前提是三态门打开,OE为低电平)。
??因此当SHCP上升沿处DS为1时,第一个D触发器输出1,STCP的第一个上升沿之后,Q0输出高电平。之后SHCP的上升沿处DS为0,STCP第二个上升沿后,Q0为低电平,Q1为高电平。经过8个STCP上升沿后,Q7输出高电平,所以DS的数据移位到Q7需要8个SHCP时钟周期。
图6 74HC595芯片的时序图
??因为Q7S以SHCP作为时钟,而Q7是以STCP作为时钟,所以Q7S变化会提前Q7。
??注意:74HC595的移位时钟信号SHCP上升沿采集D触发器输入端的数据,那么FPGA就应该在SHCP的下降沿输出数据DS,保证DS的数据在SHCP的上升沿是稳定的,不会出现抖动。
??该芯片的原理就这么多了,功能实现就是依靠下面一组串行连接的D触发器组成移位寄存器,将串行D触发器的输出端口作为锁存器的输入端口,进而通过控制D触发器和锁存器的时钟变化,来控制数据的输出。
2、驱动模块设计
??经过前文讲解熟悉了74HC595芯片的工作原理,再来查看数码管的原理图,如下图所示。经过前文讲解,驱动8个数码管,需要一组8位的数据线,每个数码管还有一个位选信号,所以总共需要16位数据线,一片74HC595能够提供8位并行输出,所以需要两片74HC595芯片进行级联。
图7 数码管原理图
??上图中,两片74HC595芯片的移位时钟端口sclk和锁存时钟端口rclk连接在一起,第一片74HC595的QH(就是前文的Q7S引脚)连接到第二片74HC595的串行数据输入端口SER(前文的DS引脚),进而可以把16位串行输入数据DIND经过16个移位时钟sclk上升沿转换成16位并行数据。
??注意上图中第二片的8位并行输出连接8个数码管的数据线,而第一片的8位并行输出连接8个数码管的位选信号。所以在进行串行数据输出时,应该先输出数码管的数据信号,然后在输出位选信号。
??怎么驱动这数码管呢?直接看下面时序吧,需要点亮第7个数码管,数码管数据是8’h5b。当din_vld为高电平时,表示需要刷新数据,此时开始产生移位时钟信号sclk,在sclk时钟的下降沿开始依次输出数据信号和位选信号16‘h5bbf,从高位开始输出。当16位数据都输出完毕后,把锁存时钟rclk拉高一个时钟周期,将16个移位寄存器输出端口的数据输出到锁存器端口,实现数码管数据段信号和位选信号的更新。
图8 74HC595芯片的驱动时序
??设计思路:当检测到有位选和数据信号需要刷新(din_vld位高电平)时,把标志信号flag拉高,并且把位选和段选信号暂存。对应代码如下:
//标志信号,当需要刷新时拉高,当刷新完成时拉低; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin//初始值为0; flag <= 1'b0; end else if(end_cnt)begin//计数器计数结束,表示刷新完成; flag <= 1'b0; end else if(din_vld)begin//有数据需要刷新; flag <= 1'b1; end end //当输入数据有效时,将需要显示的数据暂存; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin//初始值为0; din_r <= 16'd0; end else if(din_vld)begin//数据信号存在高位,先输出; din_r <= {segment[7:0],seg_sel[7:0]}; end end
??当标志信号为高电平时,分频计数器div_cnt开始工作,对系统时钟clk进行分频,产生移位寄存器时钟信号sclk。另外一个计数器cnt用于对发送数据的个数进行计数,由于一次需要发送8位数码管数据和8位段选信号,并且最后需要把锁存时钟rclk拉高一个时钟,所以计数器cnt计数到17结束,当分频计数器计数结束表示1位数据发送完成,此时计数器cnt加1。对应代码如下:
//分频系数计数器,当flag信号为高电平时有效; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin// div_cnt <= 0; end else if(add_div_cnt)begin if(end_div_cnt) div_cnt <= 0; else div_cnt <= div_cnt + 1; end end assign add_div_cnt = flag;//处于刷新状态时,计数器对系统时钟计数; assign end_div_cnt = add_div_cnt && div_cnt == SCLK_DIV - 1;//计数到分频系数清零; //计数发送数据的位数,需要发送16位数据,且需要将锁存时钟拉高,所以需要计数17; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin// cnt <= 0; end else if(add_cnt)begin if(end_cnt) cnt <= 0; else cnt <= cnt + 1; end end assign add_cnt = end_div_cnt;//当分频计数器计数结束表示1位数据发送完成,此计数器加1。 assign end_cnt = add_cnt && cnt == 17 - 1;//当发送完16位数据且锁存时钟拉高后清零,表示完成刷新;
??当计数器计数到分频系数一半时,sclk拉高,当分频计数器为0时,sclk拉低。对应代码如下所示:
//产生74hc595的移位时钟信号; //add_div_cnt && div_cnt == 0表示sclk的下降沿; //add_div_cnt && div_cnt == SCLK_DIV/2-1表示sclk的上升沿; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin//初始值为0; sclk <= 1'b0; end else if(add_div_cnt)begin if(div_cnt == 0)//当分频计数器为0且计数条件有效时拉低。 sclk <= 1'b0; else if(div_cnt == (SCLK_DIV >> 1))//当分频计数器计数到一半时拉高; sclk <= 1'b1; end end
??在移位时钟sclk的下降沿,依次把暂存的16位数据输出,对应代码如下:
//在SCLK下降沿输出数据; //FPGA需要在SCLK下降沿更新数据,74HC595在上升沿才能采集稳定的数据。 always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin//初始值为0; ds <= 1'b0; end//当分频计数器为0,且发送的数据小于16时,输出数据; else if(add_div_cnt && (div_cnt == 0) && (cnt < 16))begin ds <= din_r[15 - cnt];//其实就是在sclk下降沿输出数据; end end
??当16位数据全部输出后,把锁存时钟信号rclk拉高一个时钟周期,更新74HC595芯片锁存器输出数据,对应代码如下所示:
//产生锁存时钟信号,当数据全部发送完毕后,将锁存时钟拉高一个时钟周期。 always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin//初始值为0; rclk <= 1'b0; end else if(end_cnt)begin rclk <= 1'b0; end else if(add_div_cnt && (cnt == 16))begin rclk <= 1'b1; end end
??对前面用到的数码管驱动模块做了一点小修改,需要输出一个指示信号让驱动模块工作,修改后的代码如下所示:
module seg_disp #( parameter TCLK = 20 ,//系统时钟周期,单位ns。 parameter TIME_20US = 20_000 ,//数码管刷新时间,默认20us。 parameter SEG_NUM = 8 //需要显示的数码管个数。 )( //输入信号定义 input clk ,//系统时钟,50MHz。 input rst_n ,//系统复位,低电平有效。 input [(SEG_NUM * 4) - 1 :0] din ,//需要数码管显示的BCD码数码; //输出信号定义 output reg [7 : 0] segment ,//数码管的数据线; output reg [SEG_NUM - 1 : 0] seg_sel ,//数码管的位选信号; output reg dout_vld //为高电平时,表示段选和位选信号有效; ); //参数定义 localparam TIME = TIME_20US/TCLK ; localparam TIME_W = clogb2(TIME-1) ;//计算数码管扫描时间的时钟数据位宽; localparam SEG_W = clogb2(SEG_NUM) ; localparam ZERO = 8'h3F ; //8'hC0;前面的数据是共阴数码管使用的,后面数据是共阳数码管使用的; localparam ONE = 8'h06 ; //8'hF9; localparam TWO = 8'h5B ; //8'hA4; localparam THREE = 8'h4F ; //8'hB0; localparam FOUR = 8'h66 ; //8'h99; localparam FIVE = 8'h6D ; //8'h92; localparam SIX = 8'h7D ; //8'h82; localparam SEVEN = 8'h07 ; //8'hF8; localparam EIGHT = 8'h7F ; //8'h80; localparam NINE = 8'h6F ; //8'h90; localparam ERR = 8'h77 ; //8'h86; //中间信号定义 reg [3 : 0] sel_result ; reg [SEG_W - 1 : 0] sel ; reg [SEG_W - 1 : 0] sel_ff0 ; reg [TIME_W - 1 : 0] cnt_20us ; reg add_sel_r ;// wire end_cnt_20us; wire add_sel ; wire end_sel ; //自动计算位宽的函数; function integer clogb2(input integer depth); begin if(depth==0) clogb2=1; else if(depth!=0) for(clogb2=0; depth>0;clogb2=clogb2+1) depth=depth>>1; end endfunction //20us计数器,用于对一个数码管点亮的持续时间进行计数,计数器初始值为0,对 always@(posedge clk or negedge rst_n)begin if(!rst_n)begin//计数器初始值为0; cnt_20us <= 0; end else if(end_cnt_20us)begin//当计数器计数到20us时,表示一个数码管已经被点亮20US了,将计数器清零; cnt_20us <= 0; end else begin//否则,计数器加一; cnt_20us <= cnt_20us + 1'b1; end end //计数器结束条件,当计数器计数到TIME-1时表示20US已经到了,将计数器清零; assign end_cnt_20us = cnt_20us == TIME - 1; //计数器sel,用于计数此时点亮的时第几个数码管,上电复位时点亮第零个数码管,所以初始值为0,之后当计数器cnt_20us计数结束时,表示一个数码管点亮时间已经到了,此时计数器sel加一,表示该点亮下一个计数器了,当点亮SEG_NUM-1个计数器完成(end_sel有效)时表示数码管都被点亮了一次,此时计数器sel清零,又从第一个数码管开始点亮; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin//初始值为0; sel <= 0; end else if(add_sel) begin if(end_sel)//当计数器sel计数结束时,计数器清零; sel <= 0; else sel <= sel + 1; end end assign add_sel = end_cnt_20us;//计数器sel的加一条件是,计数器cnt_20us计数器结束; assign end_sel = add_sel && sel == SEG_NUM - 1;//计数器sel计数到SEL_NUM-1时,计数器sel清零; //sel_result信号是当前被点亮数码管需要显示的数据,根据计数器sel的值确定此时应该将输入信号的哪几位数据译码输出给数码管进行显示; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin//初始值为0; sel_result <= 4'd0; end else if(add_sel)begin//取输入信号din[4*sel+3 : 4*sel]信号给译码部分进行译码,之后输出给数码管数据信号驱动数码管显示该数据; sel_result <= din[4*sel+3 -: 4];//{din[4*sel+3],din[4*sel+2],din[4*sel+1],din[4*sel]}; end end //译码器部分,将sel_result十进制信号译码成数码管显示该数字对应的八位数据信号; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin//初始上电时,所有数码管显示数据0; segment <= ZERO; end else if(add_sel_r)begin case(sel_result)//将sel_result译码成对应的segment数据,segment数据驱动数码管才能显示sel_result代表的数字; 0: segment <= ZERO ;//想要数码管显示0,就要给数码管数据信号segment输入ZERO数据,其余类似; 1: segment <= ONE ; 2: segment <= TWO ; 3: segment <= THREE; 4: segment <= FOUR ; 5: segment <= FIVE ; 6: segment <= SIX ; 7: segment <= SEVEN; 8: segment <= EIGHT; 9: segment <= NINE ; default: segment <= ERR; endcase end end //为了与段选动态扫描,保持同步,此时位选应该打一拍再赋给位选信号 seg_sel always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin sel_ff0 <= 0; end else if(add_sel)begin sel_ff0 <= sel; end end always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin//初始值为0,全部数码管被点亮; seg_sel <= {{SEG_NUM}{1'b0}}; end else if(add_sel_r)begin//将1右移sel_ff0位之后取反,seg_sel的第sel_ff0输出低电平,对应的第sel_ff0个数码管被点亮了,其余位输出高电平,对应的数码管熄灭; seg_sel <= ~({1'b1,{{SEG_NUM-1}{1'b0}}} >> sel_ff0);//~(6'h1<<sel_ff0); end end //移位寄存器,将数据更新的指示信号暂存; //dout_vld与segment、seg_sel对齐。 always@(posedge clk)begin add_sel_r <= add_sel; dout_vld <= add_sel_r; end endmodule
??顶层模块需要对数码管驱动模块和74HC595驱动模块进行连线,对应代码如下所示。
module top #( parameter TCLK = 20 ,//系统时钟周期,默认20ns. parameter TIME_20US = 20_000 ,//每个数码管刷新时间,默认20us。 parameter SEG_NUM = 8 ,//数码管的个数,默认8个; parameter SCLK_DIV = 10 //sclk与系统时钟的分频系数。 )( input clk ,//系统时钟信号,50MHz,周期20ns。 input rst_n ,//系统复位信号,低电平有效; input [31 : 0] bcd_out ,//数码管需要显示的32位BCD码。 output ds ,//74HC595串行数据线; output sclk ,//74HC595移位寄存器时钟; output rclk //74HC595锁存器时钟; ); wire [7 : 0] segment ;//八段数码管段选信号; wire [SEG_NUM - 1 : 0] seg_sel ;//八段数码管位选信号; wire sel_vld ; //例化数码管显示模块; seg_disp #( .TIME_20US ( TIME_20US ),//每个数码管刷新时间,默认20us。 .TCLK ( TCLK ),//系统时钟周期,默认83ns. .SEG_NUM ( SEG_NUM ) //数码管的个数,默认8个; ) u_seg_disp ( .clk ( clk ),//系统时钟信号,默认12MHz。 .rst_n ( rst_n ),//系统复位信号,低电平有效; .din ( bcd_out ),//将BCD码进行显示; .segment ( segment ),//数码管的段选信号; .seg_sel ( seg_sel ),//数码管的位选信号; .dout_vld ( sel_vld ) ); //例化74hc595驱动模块; hc595_drive #( .SEG_NUM ( SEG_NUM ),//需要显示的数码管个数。 .SCLK_DIV ( SCLK_DIV ) //sclk与系统时钟的分频系数。 ) u_hc595_drive ( .clk ( clk ),//系统时钟,50MHz。 .rst_n ( rst_n ),//系统复位,低电平有效。 .segment ( segment ),//数码管的数据线; .seg_sel ( seg_sel ),//数码管的位选信号; .din_vld ( sel_vld ),//段选和位选有效指示信号; .ds ( ds ),//74HC595串行数据线; .sclk ( sclk ),//74HC595移位寄存器时钟; .rclk ( rclk ) //74HC595锁存器时钟; ); endmodule
3、模块仿真
??对应的TestBench如下所示:
`timescale 1 ns/1 ns module test(); localparam CYCLE = 20 ;//系统时钟周期,单位ns,默认20ns; localparam RST_TIME = 10 ;//系统复位持续时间,默认10个系统时钟周期; localparam TIME_20US = 4000 ;//每个数码管刷新时间,默认20us; reg clk ;//系统时钟,默认100MHz; reg rst_n ;//系统复位,默认低电平有效; reg [31 : 0] bcd_out ; wire ds ; wire sclk ; wire rclk ; //例化需要测试的模块; top #( .TCLK ( CYCLE ), .TIME_20US ( TIME_20US ) ) u_top ( .clk ( clk ), .rst_n ( rst_n ), .bcd_out ( bcd_out ), .ds ( ds ), .sclk ( sclk ), .rclk ( rclk ) ); //生成周期为CYCLE数值的系统时钟; initial begin clk = 0; forever #(CYCLE/2) clk = ~clk; end //生成复位信号; initial begin rst_n = 1;bcd_out <= 0; #1; rst_n = 0;//开始时复位10个时钟; #(RST_TIME*CYCLE); rst_n = 1; #(20*CYCLE); repeat(10)begin//产生10个随机数据作为测试; bcd_out <= {$random}; repeat(TIME_20US*2)@(posedge clk); end #(20*CYCLE); $stop;//停止仿真; end endmodule
??仿真结果如下图所示,仿真没有出现错误。
图9 仿真结果
??分频系数就涉及到74HC595芯片能够工作的最大时钟频率了,芯片供电电压越大,工作频率越高,数据手册中并没有3.3V供电的时钟频率要求,大概估计最大时钟频率在12MHz左右,只要小于该频率即可。
??上述实现8个数码管的驱动,实现16个数码管的驱动也只需要增加1片74HC595就行了,第2片的QH输出连接第三片的串行输入引脚,从而可以增加8个数码管的位选信号控制。每次串行传输的数据会增加8位,并不需要额外增加引脚。
??需要上述工程的在公众号后台回复”基于FPGA的74HC595驱动”(不包括引号)即可。使用该模块时,需要注意自己原理图的第2片74HC595芯片接的是数码管的位选还是片选信号,接法不同,?输入数据进行拼接的方式不同。