FPGA学习(10)-数码管

前3节视频目的是实现显示0~F的数码管仿真,后3节是用驱动芯片驱动数码管。

目录

1.数码管显示原理

2.代码过程

2.1仿真结果

3.串行移位寄存器原理

3.1原理

[​编辑 3.2 数据手册](#编辑 3.2 数据手册)

[3.3 先行设计思路](#3.3 先行设计思路)

4.程序

4.1确定SRCLK的频率

4.2序列计数器

[4.3 不同counter值干不同事](#4.3 不同counter值干不同事)

4.4仿真

5.实验结果


1.数码管显示原理

分为共阳极与共阴极驱动,共阴极:发光二极管负极都接地,a~dp接入端口引脚,输出高电平就点亮,共阳极与之相反。

不同位的高低电平可以组成不同的数字符号,后面查找表的值就看这张图。

在驱动多个数码管场景中,往往a~dg与每个数码管对应的二极管是互联的,而sel端的电压可以控制三极管是否导通,以此可以单独让某个数码管亮灭。

根据以上分析可以画出电路图:

1.首先SEL决定了点亮哪个数码管,需要用到38译码器进行选择,而38译码器的输出选择需要用到一个3位的状态计数切换器。

2.如何让状态切换,即加入一个计数器记到1ms,当大于1ms时,让状态加1,且计数器本身清0。

3.SEG决定了数码管哪段亮,在没有规律的情况下可以用查表法,把这些状态一一列出来,例如这里有16个状态,那么需要一个4位的变量来表示对应的索引号。

4.将状态计数切换器值传到8选1数据选择器,分别代表8个数码管对应的通道值,作为索引。

2.代码过程

1.实现1ms的计数器;

2.位切换逻辑,计数器每满一次,状态位切换+1;

3.38译码器实现连接哪个数码管;

4.位选择控制逻辑。

(1)查找表case,完成SEG的配置。

(2)位切换逻辑将8个数码管对应的值当作索引传进去**。这里数组顺序错了。**

代码:

module Shuma_guan(
    input                   clk,
    input                   reset,
    input[31:0]             Dis_data,
    output reg[7:0]         SEL,
    output reg[7:0]         SEG
    );
reg[29:0] counter_1ms;
reg[2:0] which_sel;
reg[3:0] data_temp;         //存放索引值
//数码管亮的时间
parameter Base_freq = 50_000_000;
parameter goal_freq = 1000;
parameter cworth = Base_freq/goal_freq-1;

//计数器
always@(posedge clk or negedge reset)
begin
    if(!reset)
         counter_1ms <= 1'd0;
    else if(counter_1ms == cworth)
         counter_1ms <= 1'd0;
    else
         counter_1ms <= counter_1ms+1'd1;
end

//数码管状态切换
always@(posedge clk or negedge reset)
begin
    if(reset)
        which_sel <= 1'd0;
    else if(counter_1ms == cworth)
        which_sel <= which_sel+1'd1;
    else
        which_sel <= which_sel;
end

//38译码器确定哪个数码管亮
always@(posedge clk)
begin
    case(which_sel)
        0:SEL <= 8'b0000_0001;
        1:SEL <= 8'b0000_0010;
        2:SEL <= 8'b0000_0100;
        3:SEL <= 8'b0000_1000;
        4:SEL <= 8'b0001_0000;
        5:SEL <= 8'b0010_0000;
        6:SEL <= 8'b0100_0000;
        7:SEL <= 8'b1000_0000;
    endcase
end

//建立查找表,共阳
always@(posedge clk)
begin
    case(data_temp)
        0:SEG <= 8'b1100_0000;
        1:SEG <= 8'b1111_1001;
        2:SEG <= 8'b1010_0100;
        3:SEG <= 8'b1011_0000;
        4:SEG <= 8'b1001_1001;
        5:SEG <= 8'b1001_0010;
        6:SEG <= 8'b1000_0010;
        7:SEG <= 8'b1111_1000;
        8:SEG <= 8'b1000_0000;
        9:SEG <= 8'b1001_0000;
        10:SEG <= 8'b1000_1000;
        11:SEG <= 8'b1000_0011;
        12:SEG <= 8'b1100_0110;
        13:SEG <= 8'b1010_0001;
        14:SEG <= 8'b1000_0110;
        15:SEG <= 8'b1000_1110;
    endcase  
end

//将状态值传给索引
always@(*)
begin
    case(which_sel)
        0:data_temp <= Dis_data[3:0];
        1:data_temp <= Dis_data[7:4];
        2:data_temp <= Dis_data[11:8];
        3:data_temp <= Dis_data[15:12];
        4:data_temp <= Dis_data[19:16];
        5:data_temp <= Dis_data[23:20];
        6:data_temp <= Dis_data[27:24];
        7:data_temp <= Dis_data[31:28];
   endcase
end
    
endmodule

仿真:

`timescale 1ns / 1ns

module Shuma_guan_tb();
reg                   clk;
reg                   reset;
reg[31:0]             Dis_data;
wire[7:0]             SEL;
wire[7:0]             SEG;

Shuma_guan Shuma_guan_inst0(
      .clk(clk),
      .reset(reset),
      .Dis_data(Dis_data),
      .SEL(SEL),
      .SEG(SEG)
);  
 
initial clk = 1;
always #10 clk=~clk;

initial begin
    reset = 0;
    Dis_data = 32'h12345678;
    #201;
    reset = 1;
    #20_000_000;
    Dis_data = 32'h9abcdef0;
    #20_000_000;
    $stop;
end
endmodule

2.1仿真结果

切换状态计数没有随着计数器增加到49999而加1,原因是在切换状态的时序逻辑中,复位没有取反,直接if(reset),相当于复位信号为1时,状态计数就会为0,所以一直不会增加。

修改后正确:

3.串行移位寄存器原理

3.1原理

为了节省IO口引脚,一般会使用驱动芯片74HC595驱动数码管,芯片的原理是基于串行移位寄存器。将上节输出的SEG与SEL传输到74HC595。

基本原理为,顺次连接4个D触发器,每经过一个时钟,就依次将DATA存储的值传送到下一个D触发器,例如,经过5个时钟周期后, 此时DFF3~DFF0存储的值为:1001。如果想在这个时刻把每个D触发器存储的值取出来,就在每个D触发器的输出端再接入一个D触发器,加的这4个D触发器单独用一个时钟进行控制,然后将输出分别接至各自端口。

当要移位8位,16位时,直接加更多的D触发器就行。

74HC595能够驱动8位移位寄存器,SER是数据传输口,SRCLK是第一路串联触发器的时钟,SRCLR非应该是复位信号,RCLK是是每个单独加入的触发器的时钟,OE非是输出的使能信号。第1个74HC595的输出串联到了第2个74HC595的输入,以此构成16位移位寄存器输出。

左边的74HC连接的是选择使能哪个数码管,右边的74HC是决定数码管哪段亮。DIO是数据输入,SRCLK是第一路串联触发器的时钟,RCLK是加入单个D触发器的时钟。

3.2 数据手册

从时序图可以看出,RCLK有方波时,才有输出。

移位寄存器时钟为上升沿时,其实也就是让移位寄存器存前一阶段的数据。RCLK为上升沿时,移位寄存器的数据存储在存储寄存器中。

时钟频率最低为5MHz,其中,SRCLK/RCLK的高低电平时间至少为时钟周期的一半,电平为2V时,SER在SRCLK上升沿前125ns必须稳定,SRCLK的上升沿必须RCLK上升沿之前的94ns。

3.3 先行设计思路

4.程序

4.1确定SRCLK的频率

取50MHz的乘除整数倍,取SRCLK的频率为12.5MHz,定义一个分频计数器,在SRCLK下降沿将数据传到DIO,上升沿DIO将数据传给驱动芯片的寄存器。计数器计数两次为1个SRCLK周期,可根据counter找到SRCLK的上升沿和下降沿。

4.2序列计数器

根据时序图与原理图,先传的SEG,后传的SEL。1ms切换了一次,人眼看不出来,实际上应该是闪烁的数字循环跳变。共32个状态,需要拉低拉高。

4.3 不同counter值干不同事

counter=0,拉低SRCLK,将SEG[7]的值传给DIO,将RCLK拉高。

counter=1,拉高SRCLK(驱动芯片自动会将DIO的数据传给寄存器),RCLK拉低。

......

counter=31,拉高SRCLK

4.4仿真

仿真的RCLK刚开始就拉高了,这是因为if后面没有跟else,直接按照每个时钟沿开始执行程序了。

正确的仿真波形:

此时代码:

module tube_active(
    input               clk,
    input               reset,
    input[7:0]          SEL,
    input[7:0]          SEG,
    output reg          SRCLK,
    output reg          RCLK,
    output reg          DIO
    );
    
reg[29:0]       divi_counter;
reg[4:0]        state_counter;          //序列计数器
//确定SRCLK的频率为12.5MHz
parameter base_frequence = 50_000_000;
parameter goal_frequence = 12500_000;
parameter goal_counter = base_frequence/(goal_frequence*2)-1;

//分频计数器
always@(posedge clk or negedge reset)
begin
    if(!reset)
        divi_counter <= 1'd0;
    else if(divi_counter == goal_counter)
        divi_counter <= 1'd0;
    else
        divi_counter <= divi_counter +1'd1;    
end

//序列计数器
always@(posedge clk or negedge reset)
begin
    if(!reset)
        state_counter <= 1'd0;
    else if(divi_counter == goal_counter)
        state_counter <= state_counter+1'd1;
    else
        state_counter <= state_counter;
end

//不同counter值做不同事,下降沿将值传给DIO,上升沿将DIO的值送给驱动芯片
always@(posedge clk or negedge reset)
begin
    if(!reset)begin
        SRCLK <= 1'd0;
        RCLK <= 1'd0;
        DIO <= 1'd0;
    end
    else begin
    case(state_counter)
        0:begin DIO <= SEG[7]; SRCLK <= 1'd0; RCLK <= 1'd1; end
        1:begin SRCLK <= 1'd1; RCLK <= 1'd0; end
        2:begin DIO <= SEG[6]; SRCLK <= 1'd0;end
        3:begin SRCLK <= 1'd1; end
        4:begin DIO <= SEG[5]; SRCLK <= 1'd0;end    
        5:begin SRCLK <= 1'd1; end
        6:begin DIO <= SEG[4]; SRCLK <= 1'd0;end
        7:begin SRCLK <= 1'd1; end
        8:begin DIO <= SEG[3]; SRCLK <= 1'd0;end 
        9:begin SRCLK <= 1'd1; end
        10:begin DIO <= SEG[2]; SRCLK <= 1'd0;end
        11:begin SRCLK <= 1'd1; end
        12:begin DIO <= SEG[1]; SRCLK <= 1'd0;end
        13:begin SRCLK <= 1'd1; end
        14:begin DIO <= SEG[0]; SRCLK <= 1'd0;end
        15:begin SRCLK <= 1'd1; end
        16:begin DIO <= SEL[7]; SRCLK <= 1'd0;end
        17:begin SRCLK <= 1'd1; end
        18:begin DIO <= SEL[6]; SRCLK <= 1'd0;end
        19:begin SRCLK <= 1'd1; end
        20:begin DIO <= SEL[5]; SRCLK <= 1'd0;end    
        21:begin SRCLK <= 1'd1; end
        22:begin DIO <= SEL[4]; SRCLK <= 1'd0;end
        23:begin SRCLK <= 1'd1; end
        24:begin DIO <= SEL[3]; SRCLK <= 1'd0;end 
        25:begin SRCLK <= 1'd1; end
        26:begin DIO <= SEL[2]; SRCLK <= 1'd0;end
        27:begin SRCLK <= 1'd1; end
        28:begin DIO <= SEL[1]; SRCLK <= 1'd0;end
        29:begin SRCLK <= 1'd1; end
        30:begin DIO <= SEL[0]; SRCLK <= 1'd0;end
        31:begin SRCLK <= 1'd1; end
    endcase
    end
end
endmodule

仿真代码:

`timescale 1ns / 1ns

module tube_active_tb();
 reg clk;
 reg reset;
 reg [7:0]SEL;
 reg [7:0]SEG;
 wire SRCLK;
 wire RCLK;
 wire DIO;

tube_active tube_active_inst0(
    .clk(clk),      
    .reset(reset),  
    .SEL(SEL),
    .SEG(SEG),
    .SRCLK(SRCLK),
    .RCLK(RCLK),  
    .DIO(DIO)  
);

initial clk = 1;
always #10 clk = ~clk;

initial begin
        reset = 0;
        SEL = 8'b0000_0001;
        SEG = 8'b0101_0101;
        #201;
        reset = 1;
        #5000;
        SEL = 8'b0000_0010;
        SEG = 8'b1010_1010;        
        #5000;
        SEL = 8'b1010_0101;
        SEG = 8'b0000_1101;     
        #5000;
        $stop;
end
endmodule

在现有模块添加新的源文件,必须点击这里的+,左上角的会出错。新源文件调用了上面的两个模块,让这两个模块连在一起,然后引出三根线与驱动芯片连接。

module tube_shuma_test(
    input               clk,
    input               reset,
    output              SRCLK,
    output              RCLK,
    output              DIO,
    input [1:0]         SW
);

wire[7:0] SEL,SEG; //将两个例化的端口连接起来
reg[31:0] Dis_data;  

//调用的并转串模块
tube_active tube_active_inst0(
    .clk(clk),      
    .reset(reset),  
    .SEL(SEL),
    .SEG(SEG),
    .SRCLK(SRCLK),
    .RCLK(RCLK),  
    .DIO(DIO)  
);

//调用的hex8模块
Shuma_guan Shuma_guan_inst0(
      .clk(clk),
      .reset(reset),
      .Dis_data(Dis_data),
      .SEL(SEL),
      .SEG(SEG)
);  

always@(*)
begin
    case(SW)
        0:Dis_data <= 32'h01234567;
        1:Dis_data <= 32'h89abcdef;
        2:Dis_data <= 32'haaaabbbb;
        3:Dis_data <= 32'h66666666;
    endcase
end



endmodule

5.实验结果

根据拨码开关的开关情况,数码管显示不同的数字。