[X]关闭

2-3-17 基于FPGA实现SPI驱动HC595点亮数码管

文档创建者:uisrc
浏览次数:388
最后更新:2023-12-29
文档课程分类
AMD: FPGA部分(2024样板资料) » 2_FPGA实验篇(仅旗舰) » 1-FPGA基础入门实验
软件版本:VIVADO2021.1
操作系统:WIN10 64bit
硬件平台:适用XILINX A7/K7/Z7/ZU/KU系列FPGA
登录米联客(MILIANKE)FPGA社区-www.uisrc.com观看免费视频课程、在线答疑解惑!
1 概述
前面课程我们编写SPI通信FPGA的收发程序,那么这一节课,我们将展示下SPI接口的应用,使用SPI接口的74HC595实现数据的串并转换,并且动态点亮数码管。
本节课的目标,不仅仅是点亮数码管,笔者希望大家掌握笔者的学习方法,通过对74HC595芯片的内部结构,接口信号,以及驱动时序的分析,掌握如何通过SPI通信接口去控制一些SPI接口的芯片。对于搞FPGA的人来说,经常会使用SPI接口去读取ADC或者控制DAC。所以读者非常有必要掌握好SPI接口FPGA程序的设计,笔者这里给出的每个例子都是具备学习的价值意义的。
2 硬件设计分析2.1 数码管动态显示原理
2504661-20231229132052586-941004325.jpg
如上图所示,为我们8位共阳极7位数码管的电路原理图,这里的数码管是共阳极驱动,所以当74HC595 U2的QA、QB、QC、QD控制数码管的阳极,也就是负责选通哪一个数码管点亮;而74HC595 U1控制显示具体的数值内容。动态显示的原理就是某一个时刻只显示1个数码管,对于我们人眼来说刷新率在25hz以上就会感觉所有数码管都是同时点亮的效果。
我们看到数码管的字母abcdefgdp代表的是什么意思呢?如下图所示,就是代表了每个LED,当设置abcdefgh中任意位为0就是点亮数码管。所以我们可以得出一个表,用于显示相应的数值。
2504661-20231229132053092-1212015148.jpg
Value
Code
Value
Code
0
8'hc0
8
8'h80
1
8'hf9
9
8'h90
2
8'ha4
A
8'h88
3
8'hb0
B
8'h83
4
8'h99
C
8'hc6
5
8'h92
D
8'ha1
6
8'h82
E
8'h96
7
8'hf8
F
8'h8e


3 认识74HC595使用
对于FPGA基础的课程内容,笔者认为多学点知识的过程比做出结果更有意义,所以我们这里把74HC595芯片的使用也分析下。
3.1 74HC595的内部功能单元
2504661-20231229132058569-2025265425.jpg
我们先从74HC595内部的原理图看下芯片的功能。可以看到里面最关键功能单元是8个D触发器用于移位和另外8个D触发器用于数据的输出到功能PIN脚上。
我们SPI接口的数据从Serial input A输入,并且通过SHIFT CLOCK每个时钟移位进入74HC595的触发器中,一个74HC595支持8bit IO扩展,如果需要更多IO,则可以通过74HC595的级联输出到下一个74HC595芯片的输入。
如上面图片所示,我们为了动态驱动数码管,扩展了16个IO(其中有4个本例中没有用到)。比如本文中的例子,我们完成了16bit数据的移位,那么需要通过LATCH CLK 把数据从移位寄存器打入输出寄存器。
3.2 74HC595驱动真值表
2504661-20231229132059122-1743571279.jpg
以上这张表是来源于74HC595的计数手册,笔者红线框出的部分就是我们需要设计的驱动时序要求。从中可以看到,74HC595工作需要设置复位管脚为高电平,并且设置Output Enable为低电平。第一个红框中数据在每个shift Clock的上升沿打入移位寄存器。在第二个红框中Latch Clock为上升沿的时候,数据从移位寄存器打入输出寄存器。
3.3 74HC595关键的时序要求
对于FPGA的外设接口,我们一定要掌握驱动时序的设计。这里笔者给出2张表和1张图。
2张表如下:
2504661-20231229132059624-2018534513.jpg
2504661-20231229132105110-203890396.jpg
一张图如下:
2504661-20231229132105614-708843803.jpg
作为一个FPGA初学者应该如何设计理解上面的时序呢?
1)、对于一个SPI的接口器件,首选我们应该看最高的接口速度。从上面表种参数fmax在3.0V工作情况下,85°的工作温度,可以达到10MHZ,有了这个参数我们就可以设计我们的SPI 接口的时钟了,比如我们这里设计的是100KHZ的频率,肯定满足要求的。
2)、建立时间和保存时间,数据为了可以正确打入D触发器需要有足够的建立时间有保存实际。74HC595的数据建立和保持时要求如figure5所示,tsu=50ns th=5ns,我们设计的SPI发送程序,是在SPI时钟的下降沿发送数据。假设以25M系统时钟,以及100倍分频,SPI时钟来说,SPI时钟为250khz,SPI时钟周期为4us,具有2us的建立时间和保持时间,绰绰有余了,这样我们的数据肯定可以满足建立时间和保持时间要求。
3)、LATCH CLOCK和SHIFT CLOCK也是需要满足建立时间和保持时间要求,如Figure6所示。Tsu=70ns 并且tw为50ns。
我们的SPI用户代码部分是在所有数据位发送完成后设置LATCH CLOCK位1, LATCH CLOCK的建立时间有10个系统时钟周期,对于25M的时钟具有4us的时间,我们SPI用户代码部分控制了LATCH CLOCK的tw大于50ns这样设计就满足时序要求了。
2504661-20231229132106108-1578003408.jpg
本例中,复位RESET和OE都是设置了电平常量,所以只要设计好了以上三个关键时序就可以了。掌握74HC595SPI时序分析比我们点亮数码管更具有意义和价值。
4 用户控制代码
硬件电路中,通过3根数据线,2片74HC595实现16个GPIO的扩展。使用FEP-BASE卡具有4个数码管,以动态扫描的方式点亮。动态扫描代表每一时刻只有一个数码管被点亮,只要扫描的速度足够快,看起来就是同时点亮的。
笔者依然先给出程序,到这个阶段有些读者应该可以自己完成代码的阅读了,对于基础不好的读者继续看下面笔者对代码的解析。
4.1 ui_displed.v
module ui_displed#
(
parameter CLK_DIV = 100
)
(
input  I_clk,          //系统时钟
input  I_rstn,         //复位
output O_spi_sclk,     //hc595移位时钟
output O_spi_mosi,     //hc595串行数据
output O_hc595_lach,   //hc595数据加载到输出寄存器
input  [3:0]I_disp_led0, //Display LED0
input  [3:0]I_disp_led1, //Display LED1
input  [3:0]I_disp_led2, //Display LED2
input  [3:0]I_disp_led3, //Display LED3
input  [3:0]I_disp_led4, //Display LED4
input  [3:0]I_disp_led5, //Display LED5
input  [3:0]I_disp_led6, //Display LED6
input  [3:0]I_disp_led7  //Display LED7
);

//数码管真值表
localparam
            DS_0        = 8'hC0,//数码管显示0
            DS_1        = 8'hF9,//数码管显示1
            DS_2        = 8'hA4,//数码管显示2
            DS_3        = 8'hB0,//数码管显示3
            DS_4        = 8'h99,//数码管显示4
            DS_5        = 8'h92,//数码管显示5
            DS_6        = 8'h82,//数码管显示6
            DS_7        = 8'hf8,//数码管显示7
            DS_8        = 8'h80,//数码管显示8
            DS_9        = 8'h90,//数码管显示9
            DS_A        = 8'h88,//数码管显示A
            DS_B        = 8'h83,//数码管显示B
            DS_C        = 8'hc6,//数码管显示C
            DS_D        = 8'ha1,//数码管显示D
            DS_E        = 8'h86,//数码管显示E
            DS_F        = 8'h8e,//数码管显示F
            DS_BC_ON    = 8'hbf,//b c点亮
            DS_BC_OFF   = 8'hff;//b c点灭

reg [7:0]   spi_tx_data = 8'd0;
reg         spi_tx_req  = 1'b0;
wire        spi_busy;
reg         lach595     = 1'b0;
reg [3:0]   M_S         = 3'd0;

reg [2:0]   disp_num    = 4'd0;
reg [7:0]   disp_led_n  = 8'd0;
reg [3:0]   disp_led    = 4'd0;
reg [7:0]   disp_truth_value = 8'd0;
wire        disp_en ;

assign O_hc595_lach = lach595;               //hc595数据加载到输出寄存器的控制信号
assign disp_en      = (M_S == 4'd15);       //到达状态15完成一次传输

//动态门控数字管
always @(posedge I_clk or negedge I_rstn)begin
    if(!I_rstn)
         disp_num <= 3'd0;                          
     else if( disp_en )
         disp_num <= disp_num + 1'b1;  
end

always @(posedge I_clk or negedge I_rstn)begin
    if(!I_rstn)begin                               //异步复位,低电平有效
        spi_tx_req  <= 1'b0;                       //req 信号归零,回归初始状态
        spi_tx_data <= 8'd0;                       //待发送数据的data信号清零
        lach595     <= 1'b0;
        M_S         <= 4'd0;
    end
    else begin
        case(M_S)
            0:if(!spi_busy)begin                       //总线不忙启动传输
               spi_tx_req       <= 1'b1;               //req信号拉高,开始第一次传输
               spi_tx_data      <= disp_led_n;        //哪一个数码管显示数字信号传输
               M_S              <= 4'd1;  
            end
            1:if(spi_busy)begin
                spi_tx_req      <= 1'b0;               //req信号拉低,等待传输完成,也就是busy信号拉低
                M_S             <= 4'd2;
            end
            2:if(!spi_busy)begin                       //总线不忙启动传输
                spi_tx_req      <= 1'b1;               //req信号拉高,开始第二次传输
                spi_tx_data     <= disp_truth_value;   //数码管显示数字数值传输
                M_S             <= 4'd3;  
            end
            3:if(spi_busy)begin
                spi_tx_req      <= 1'b0;                 //req信号拉低,等待传输完成,也就是busy信号拉低
                M_S             <= 4'd4;
            end
            4:if(!spi_busy)begin                          //向hc595输出寄存器发送数据
                lach595         <= 1'b1;                  //控制信号拉高
                M_S             <= 4'd5;
            end
            5,6,7,8,9,10,11,12,13,14:begin              //延迟一些时钟以满足发射定时要求
                M_S <= M_S + 1'b1;
            end
            15:begin
                lach595 <= 1'b0;
                M_S <= 4'd0;
            end
            default:M_S <= 4'd0;   
        endcase
     end
end  
//动态门控数字管,hc595的第1组数据
always @(*)begin
            case ( disp_num )
            0:
            begin disp_led <= I_disp_led0;disp_led_n <= 8'b00001000;end//数码管 0 选中
            1:
            begin disp_led <= I_disp_led1;disp_led_n <= 8'b00000100;end//数码管 1 选中
            2:
            begin disp_led <= I_disp_led2;disp_led_n <= 8'b00000010;end//数码管 2 选中
            3:
            begin disp_led <= I_disp_led3;disp_led_n <= 8'b00000001;end//数码管 3 选中
            4:
            begin disp_led <= I_disp_led4;disp_led_n <= 8'b10000000;end//数码管 4 选中
            5:
            begin disp_led <= I_disp_led5;disp_led_n <= 8'b01000000;end//数码管 5 选中
            6:
            begin disp_led <= I_disp_led6;disp_led_n <= 8'b00100000;end//数码管 6 选中
            7:
            begin disp_led <= I_disp_led7;disp_led_n <= 8'b00010000;end//数码管 7 选中
            endcase
end
//动态门控数字管,hc595第2组数据  
always @(*)begin
            case( disp_led )
                4'h0: disp_truth_value <= DS_0; //disp_led数字选择几,就disp_truth_value值就传输对应的数码管真值表
                4'h1: disp_truth_value <= DS_1;
                4'h2: disp_truth_value <= DS_2;
                4'h3: disp_truth_value <= DS_3;
                4'h4: disp_truth_value <= DS_4;
                4'h5: disp_truth_value <= DS_5;
                4'h6: disp_truth_value <= DS_6;
                4'h7: disp_truth_value <= DS_7;
                4'h8: disp_truth_value <= DS_8;
                4'h9: disp_truth_value <= DS_9;
                4'ha: disp_truth_value <= DS_BC_ON;
                4'hb: disp_truth_value <= DS_BC_OFF;                        
                default : disp_truth_value <= 8'd0; // disp_truth_value的值复位清零
            endcase
end
//spi主控制器
uimspi_tx#
(
.CLK_DIV(CLK_DIV),
.CPOL(1'b0),
.CPHA(1'b0)
)
uimspi_tx_inst(
.I_clk(I_clk),//全局时钟信号
.I_rstn(I_rstn),//全局复位
.O_spi_mosi(O_spi_mosi),//spi 数据传输信号
.O_spi_sclk(O_spi_sclk),//spi 时钟信号
.I_spi_tx_req(spi_tx_req),//spi_tx_req信号为高时,表示传输开始
.I_spi_tx_data(spi_tx_data),//spi tx传输驱动需要传输的数据
.O_spi_busy(spi_busy)//spi 忙信号,拉高表示正在传输,新的数据暂停刷入传输寄存器
);

endmodule


4.2 ui_displed程序的分析
第一步:正确认识硬件原理图和驱动时序
我们在前面的内容里面介绍了一些硬件知识,包括数码管动态显示的原理,以及74HC595芯片的内部结构和驱动时序。对于我们一个FPGA程序员来说,掌握好硬件的知识,以及时序相关的内容,就可以设计程序了。
第二步:构建主要功能单元
1、构建用户代码的状态机单元
根据前面的74HC595和数码管的硬件设计图纸,通过分析我们需要发送2个8bit的数据共计16bit。因为我们需要修改前面章节中关于SPI测试程序的用户发送状态机,另外为了满足74HC595 LACH CLK的tsu(肯定满足)和tw周期时间要求,我们还需要设置lach clk的延迟。
always @(posedge I_clk or negedge I_rstn)begin
    if(!I_rstn)begin                               //异步复位,低电平有效
        spi_tx_req  <= 1'b0;                       //req 信号归零,回归初始状态
        spi_tx_data <= 8'd0;                       //待发送数据的data信号清零
        lach595     <= 1'b0;
        M_S         <= 4'd0;
    end
    else begin
        case(M_S)
            0:if(!spi_busy)begin                       //总线不忙启动传输
               spi_tx_req       <= 1'b1;               //req信号拉高,开始第一次传输
               spi_tx_data      <= disp_led_n;        //哪一个数码管显示数字信号传输
               M_S              <= 4'd1;  
            end
            1:if(spi_busy)begin
                spi_tx_req      <= 1'b0;               //req信号拉低,等待传输完成,也就是busy信号拉低
                M_S             <= 4'd2;
            end
            2:if(!spi_busy)begin                       //总线不忙启动传输
                spi_tx_req      <= 1'b1;               //req信号拉高,开始第二次传输
                spi_tx_data     <= disp_truth_value;   //数码管显示数字数值传输
                M_S             <= 4'd3;  
            end
            3:if(spi_busy)begin
                spi_tx_req      <= 1'b0;                 //req信号拉低,等待传输完成,也就是busy信号拉低
                M_S             <= 4'd4;
            end
            4:if(!spi_busy)begin                          //向hc595输出寄存器发送数据
                lach595         <= 1'b1;                  //控制信号拉高
                M_S             <= 4'd5;
            end
            5,6,7,8,9,10,11,12,13,14:begin              //延迟一些时钟以满足发射定时要求
                M_S <= M_S + 1'b1;
            end
            15:begin
                lach595 <= 1'b0;
                M_S <= 4'd0;
            end
            default:M_S <= 4'd0;   
        endcase
     end
end


2、构建数值显示查表单元
把需要显示的数值,通过查表,找到数码管对应数值的二进制码
//数码管真值表
localparam
            DS_0        = 8'hC0,//数码管显示0
            DS_1        = 8'hF9,//数码管显示1
            DS_2        = 8'hA4,//数码管显示2
            DS_3        = 8'hB0,//数码管显示3
            DS_4        = 8'h99,//数码管显示4
            DS_5        = 8'h92,//数码管显示5
            DS_6        = 8'h82,//数码管显示6
            DS_7        = 8'hf8,//数码管显示7
            DS_8        = 8'h80,//数码管显示8
            DS_9        = 8'h90,//数码管显示9
            DS_A        = 8'h88,//数码管显示A
            DS_B        = 8'h83,//数码管显示B
            DS_C        = 8'hc6,//数码管显示C
            DS_D        = 8'ha1,//数码管显示D
            DS_E        = 8'h86,//数码管显示E
            DS_F        = 8'h8e,//数码管显示F
            DS_BC_ON    = 8'hbf,//b c点亮
            DS_BC_OFF   = 8'hff;//b c点灭

//动态门控数字管,hc595第2组数据  
always @(*)begin
            case( disp_led )
                4'h0: disp_truth_value <= DS_0; //disp_led数字选择几,就disp_truth_value值就传输对应的数码管真值表
                4'h1: disp_truth_value <= DS_1;
                4'h2: disp_truth_value <= DS_2;
                4'h3: disp_truth_value <= DS_3;
                4'h4: disp_truth_value <= DS_4;
                4'h5: disp_truth_value <= DS_5;
                4'h6: disp_truth_value <= DS_6;
                4'h7: disp_truth_value <= DS_7;
                4'h8: disp_truth_value <= DS_8;
                4'h9: disp_truth_value <= DS_9;
                4'ha: disp_truth_value <= DS_BC_ON;
                4'hb: disp_truth_value <= DS_BC_OFF;                        
                default : disp_truth_value <= 8'd0; // disp_truth_value的值复位清零
            endcase
end


3、构建动态扫描单元
我们可以设置固定的刷新频率,这样需要设置一个时间计数器,每过一定的时间刷新数码管,我们这里以全速刷新,只要状态机完成一次发送数据,就开始下次的刷新。由于数码管是共阳极的,所以当需要显示的数码管的共阳极设置位1这个数码管就可以正确显示。
//动态门控数字管,hc595的第1组数据
always @(*)begin
            case ( disp_num )
            0:
            begin disp_led <= I_disp_led0;disp_led_n <= 8'b00001000;end//数码管 0 选中
            1:
            begin disp_led <= I_disp_led1;disp_led_n <= 8'b00000100;end//数码管 1 选中
            2:
            begin disp_led <= I_disp_led2;disp_led_n <= 8'b00000010;end//数码管 2 选中
            3:
            begin disp_led <= I_disp_led3;disp_led_n <= 8'b00000001;end//数码管 3 选中
            4:
            begin disp_led <= I_disp_led4;disp_led_n <= 8'b10000000;end//数码管 4 选中
            5:
            begin disp_led <= I_disp_led5;disp_led_n <= 8'b01000000;end//数码管 5 选中
            6:
            begin disp_led <= I_disp_led6;disp_led_n <= 8'b00100000;end//数码管 6 选中
            7:
            begin disp_led <= I_disp_led7;disp_led_n <= 8'b00010000;end//数码管 7 选中
            endcase
end


5 RTL仿真
先对上面的代码进行RTL功能仿真。
5.1 仿真代码
仿真测试文件源码如下:

`timescale 1ns / 1ns //定义仿真时间刻度/精度

module master_spi_tb;

reg I_sysclk;
reg I_rstn;  
wire O_spi_sclk;
wire O_spi_mosi;

spi_hc595_displed#
(
.CLK_DIV(100)   
)
spi_hc595_displed_inst(
.I_sysclk(I_sysclk),
.I_rstn(I_rstn),
.O_spi_sclk(O_spi_sclk),
.O_spi_mosi(O_spi_mosi)
);
initial begin
    I_sysclk= 1'b0;               //sysclk_p赋初值
    I_rstn = 1'b0;                //低电平复位模拟产生
    #100;
    I_rstn = 1'b1;                //复位结束
end

always
    begin
        #10 I_sysclk= ~I_sysclk;//系统时钟翻转
    end

endmodule


5.2 仿真结果
注意看下图红色的标记,红色标记中lach就是LACH CLK,每次16bit数据发送,LACH CLK有效把数据输出到74HC595的芯片管脚。
另外一个红色框内是数码管实现动态显示的切换计数器,每次发送完数据后,完成一次切换。
2504661-20231229132106549-272006043.jpg
6 硬件连接
(该教程为通用型教程,教程中仅展示一款示例开发板的连接方式,具体连接方式以所购买的开发板型号以及结合配套代码管脚约束为准。)
请确保下载器和开发板已经正确连接,并且开发板已经上电。(注意JTAG端子不支持热插拔,而USB接口支持,所以在不通电的情况下接通好JTAG后,再插入USB到电脑,之后再上电,以免造成JTAG IO损坏)
2504661-20231229132107122-1437916171.jpg
7 测试结果
2504661-20231229132107623-557627245.jpg

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则