跳到主要内容

5.4 实验原理

5.4.1 数码管连接方式

在前面之前的教程中数码管章节已为大家介绍了数码管独立显示的相关内容,关于独立显示这里就不在赘述。我们的实验平台的扩展板卡上有8位数码管,根据驱动方法不同,有以下比较:

alt text
独立显示数码管
  • 独立显示:控制每个数码管至少需要8个I/O口控制,8位数码管就需要8*8 = 64根信号线才能分别显示。独立显示实现简单,但是需要大量的信号线。
alt text
扫描显示数码管
  • 扫描显示:将每位数码管的同一段选信号连接在一起,这样我们就只需要8根段选信号和8根位选信号,共计16根信号。扫描显示可以有效节约I/O口资源,实现起来稍显复杂。

上图中我们用了4位数码管,共用了段选控制8+位选控制4=12管脚,如果是8位数码管,按照上面方式连接,段选控制还是8管脚,位选控制也增加到8管脚,共计16管脚,当然硬件的连接还需要结合软件的驱动,扫描显示数码管的驱动方法和独立显示数码管驱动方法不同,接下来我们一起来学习。

5.4.2 数码管模块电路连接

数码管连接方式部分我们了解到数码管常用的两种连接方式,独立显示数码管的原理及驱动方法我们在基础数字电路实验部分就已经详细学习了,本实验我们来学习扫描显示数码管工作原理及驱动方法。

前面我们说8位数码管通过扫描显示方式连接需要16管脚,对于大部分控制器件来说依然有点多,所以我们需要一种串行通信控制并行输出的驱动器,处理器通过串行接口(引脚占用少)驱动驱动器完成16或更多信号的控制,74HC595就是这么一款驱动器。通过3路串行输入(兼容SPI协议)控制8路并行输出,而且可以级联使用。

alt text
74HC595驱动数码管电路

5.4.3.数码管模块驱动设计

我们知道数码管分位共阴极和共阳极,我们底板上使用的就是8位共阴极数码管,驱动数码管显示需要字库,方便程序中通过数据索引对应字库,在基础数字电路实验部分我们已经学习过。

7段共阴极数码管字库定义如下:

reg[6:0] seg [15:0];       [MSB~LSB]={G,F,E,D,C,B,A}
always @(negedge rst_n) begin
seg[0] = 7'h3f; // 0
seg[1] = 7'h06; // 1
seg[2] = 7'h5b; // 2
seg[3] = 7'h4f; // 3
seg[4] = 7'h66; // 4
seg[5] = 7'h6d; // 5
seg[6] = 7'h7d; // 6
seg[7] = 7'h07; // 7
seg[8] = 7'h7f; // 8
seg[9] = 7'h6f; // 9
seg[10] = 7'h77; // A
seg[11] = 7'h7c; // b
seg[12] = 7'h39; // C
seg[13] = 7'h5e; // d
seg[14] = 7'h79; // E
seg[15] = 7'h71; // F
end

数码管显示需要段选(a b c d e f g)输出字库数据,位选(dig)输出选通信号,前面硬件电路连接部分看到8位数码管的段选全部对应连在了一起,好像不能同时显示8个不同的数字,是的,我们理解的没错,想要扫描显示数码管工作需要采用分时扫描的方式驱动。怎么样分时扫描?

1.首先我们考虑怎么样让第1个数码管显示数字1,FPGA控制数码管段选端输出1的字库7'h06(G=0、F=0、E=0、D=0、C=1、B=1、A=0),同时控制数码管位选端只选通第1个数码管显示(DIG1=0、DIG2~DIG8都为1)

2.然后如果我们有8秒的时间,第1秒时间内控制第1个数码管显示数字1,第2秒时间内控制第2个数码管显示数字2,依次类推,这样我们就可以在8秒时间内将8个不同数字分时显示出来

3.最后我们将8秒的时间变成8毫秒,每个数码管显示1毫秒,这样我们在1秒时间内将8个数字显示(扫描)125次,即刷新率为125次/秒,人眼睛有视觉暂留效应,当数码管闪烁刷新频率足够高(例如125)时,我们看到的是8个数码管同时显示。

以上描述的就是扫描显示的数码管的工作原理及驱动方法

数码管扫描显示程序实现如下:

// dat_1[3:0]   //SEG1 显示的数据输入
// dat_2[3:0] //SEG2 显示的数据输入
// dat_3[3:0] //SEG3 显示的数据输入
// dat_4[3:0] //SEG4 显示的数据输入
// dat_5[3:0] //SEG5 显示的数据输入
// dat_6[3:0] //SEG6 显示的数据输入
// dat_7[3:0] //SEG7 显示的数据输入
// dat_8[3:0] //SEG8 显示的数据输入
// dat_en[7:0] //数码管数据位显示使能,[MSB~LSB]=[SEG1~SEG8]
// dot_en[7:0] //数码管小数点位显示使能,[MSB~LSB]=[SEG1~SEG8]
// data[15:0] //数码管扫描控制数据,[15:8]为段选,[7:0]为位选

begin
cnt_main <= cnt_main + 1'b1;
case(cnt_main)
//对8位数码管逐位扫描
// data [15]+[14:8]为段选, [7:0]为位选
3'd0: data <= {dot_en[7],seg[dat_1],dat_en[7]?8'hfe:8'hff};
3'd1: data <= {dot_en[6],seg[dat_2],dat_en[6]?8'hfd:8'hff};
3'd2: data <= {dot_en[5],seg[dat_3],dat_en[5]?8'hfb:8'hff};
3'd3: data <= {dot_en[4],seg[dat_4],dat_en[4]?8'hf7:8'hff};
3'd4: data <= {dot_en[3],seg[dat_5],dat_en[3]?8'hef:8'hff};
3'd5: data <= {dot_en[2],seg[dat_6],dat_en[2]?8'hdf:8'hff};
3'd6: data <= {dot_en[1],seg[dat_7],dat_en[1]?8'hbf:8'hff};
3'd7: data <= {dot_en[0],seg[dat_8],dat_en[0]?8'h7f:8'hff};
default: data <= {8'h00,8'hff};
endcase
end

如果不考虑74HC595驱动芯片,FPGA直接连接数码管模块的段选和位选信号,data作为输出端口分配给段选和位选控制管脚,程序到这里差不多就OK了,然而为了节约IO管脚资源的占用,我们电路里有串转并的驱动芯片74HC595,所以我们还需要增加程序设计,将data的数据通过串行的方式传输到74HC595芯片,然后74HC595芯片将会根据data的内容控制数码管模块显示。

74HC595是较为常用的串行转并行的芯片,内部集成了一个8位移位寄存器、一个存储器和8个三态缓冲输出。在最简单的情况下我们只需要控制3根引脚输入得到8根引脚并行输出信号,而且可以级联使用,我们使用3个I/O口控制两个级联的74HC595芯片,产生16路并行输出,连接到扫描显示的8位数码管上。

不同的IC厂家都可以生产74HC595芯片,功能都是一样的,然而不同厂家的芯片手册对于管脚的命名会存在差异,管脚顺序相同,大家可以对应识别 上图是本设计中74HC595芯片的硬件电路连接,参考74HC595数据手册了解其具体用法,下图中我们了解到OE#(G#)和MR#(SCLR#)信号分别为输出使能(低电平输出)和复位管脚(低电平复位),OE#(G#)我们接GND让芯片输出使能,MR#(SCLR#)我们接VCC让芯片的移位寄存器永远不复位,如此FPGA只需要控制SH_CP(SCK)、ST_CP(RCK)和DS(SER)即可。

alt text
74HC595引脚功能描述
alt text
74HC595内部结构图
alt text
74HC595时序图

根据74HC595内部结构及时序图可以得知,SH_CP(SCK)每个上升沿都会将DS(SER)的数据采样到8位移位寄存器中,当ST_CP(RCK)上升沿时,8位移位寄存器中的数据被所存到8位锁存器中,同时对应Q0~Q7管脚刷新对应输出。对于我们的硬件,我们首先通过SH_CP(SCK)和DS(SER)配合将需要传输的16位数据输出,然后控制ST_CP(RCK)产生上升沿,所以我们需要至少16个SH_CP(SCK)周期完成1次数码管控制。

74HC595串行驱动 程序实现如下:

//data[15:0] //数码管扫描控制数据,[15:8]为段选,[7:0]为位选
//seg_rck //74HC595的RCK管脚
//seg_sck //74HC595的SCK管脚
//seg_din //74HC595的SER管脚

begin
if(cnt_write >= 6'd33) cnt_write <= 1'b0;
else cnt_write <= cnt_write + 1'b1;
case(cnt_write)
//SCK下降沿时SER更新数据
6'd0: begin seg_sck <= LOW; seg_din <= data[15]; end
//SCK上升沿时SER数据稳定
6'd1: begin seg_sck <= HIGH; end
6'd2: begin seg_sck <= LOW; seg_din <= data[14]; end
6'd3: begin seg_sck <= HIGH; end
6'd4: begin seg_sck <= LOW; seg_din <= data[13]; end
6'd5: begin seg_sck <= HIGH; end
6'd6: begin seg_sck <= LOW; seg_din <= data[12]; end
6'd7: begin seg_sck <= HIGH; end
6'd8: begin seg_sck <= LOW; seg_din <= data[11]; end
6'd9: begin seg_sck <= HIGH; end
6'd10: begin seg_sck <= LOW; seg_din <= data[10]; end
6'd11: begin seg_sck <= HIGH; end
6'd12: begin seg_sck <= LOW; seg_din <= data[9]; end
6'd13: begin seg_sck <= HIGH; end
6'd14: begin seg_sck <= LOW; seg_din <= data[8]; end
6'd15: begin seg_sck <= HIGH; end
6'd16: begin seg_sck <= LOW; seg_din <= data[7]; end
6'd17: begin seg_sck <= HIGH; end
6'd18: begin seg_sck <= LOW; seg_din <= data[6]; end
6'd19: begin seg_sck <= HIGH; end
6'd20: begin seg_sck <= LOW; seg_din <= data[5]; end
6'd21: begin seg_sck <= HIGH; end
6'd22: begin seg_sck <= LOW; seg_din <= data[4]; end
6'd23: begin seg_sck <= HIGH; end
6'd24: begin seg_sck <= LOW; seg_din <= data[3]; end
6'd25: begin seg_sck <= HIGH; end
6'd26: begin seg_sck <= LOW; seg_din <= data[2]; end
6'd27: begin seg_sck <= HIGH; end
6'd28: begin seg_sck <= LOW; seg_din <= data[1]; end
6'd29: begin seg_sck <= HIGH; end
6'd30: begin seg_sck <= LOW; seg_din <= data[0]; end
6'd31: begin seg_sck <= HIGH; end
//当16位数据传送完成后RCK拉高,输出生效
6'd32: begin seg_rck <= HIGH; end
6'd33: begin seg_rck <= LOW; end
default: ;
endcase
end

如果我们设计一个状态机,将数码管扫描的程序和74HC595串行驱动程序分别做成两个状态MAIN和WRITE,控制数码管扫描程序每次产生一组控制数据,执行一次74HC595串行通信,就可以完成我们数码管模块电路的驱动设计了。

alt text
状态机设计框架

8位数码管刷新1次需要8个数码管各点亮1次,每个数码管点亮1次需要16位数据,16位数据通过串行方式传输给74HC595需要至少16个SH_CP(SCK)周期,按照我们前面说的数码管刷新率达到125次/秒,扫描方式下每个数码管会点亮1毫秒,数码管点亮的时间应该等于1次数码管控制的时间,也就是16个SH_CP(SCK)周期,所以我们可以控制74HC595的SH_CP(SCK)时钟周期计算:

SH_CP(SCK)周期 = 1ms / 16 = 62.5us

即是说,当 SH_CP(SCK)周期小于62.5us,刷新率就应该大于125次/秒(注:以上为估算的结果)

为了计算方便,我们就取SH_CP(SCK)周期为50us,那么触发74HC595串行驱动执行的敏感变量周期应该为25us,我们可以通过分频产生周期为25us时钟触发完成以上所以操作。

时钟分频程序实现如下:

localparam  CNT_40KHz = 300;    //分频系数

//计数器对系统时钟信号进行计数
reg [9:0] cnt = 1'b0;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) cnt <= 1'b0;
else if(cnt>=(CNT_40KHz-1)) cnt <= 1'b0;
else cnt <= cnt + 1'b1;
end

//根据计数器计数的周期产生分频的脉冲信号
reg clk_40khz = 1'b0;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) clk_40khz <= 1'b0;
else if(cnt<(CNT_40KHz>>1)) clk_40khz <= 1'b0;
else clk_40khz <= 1'b1;
end
alt text
状态机状态转移图

5.4.4 系统总体实现

按键消抖模块我们前面基础数字电路实验中详细介绍过,这里我们直接调用消抖模块,记分器逻辑部分其实就是对按键按动次数计数,输出0 ~ 999之间的BCD码制数据,这里也不再赘述,最后例化数码管模块将两队的比分数据显示出来。最后显示的数据为000~999,本实验例程中为了显示最小有效数据位,增加了将最高位为0的数据位不显示的设计,例如当分数为5分时,数码管本来会显示005,现在控制高两位的00不显示,只显示最低位5。

显示控制程序实现如下:

wire    [7:0]   dat_en;     //控制数码管点亮
assign dat_en[7] = 1'b0;
assign dat_en[6] = red_seg[11:8]? 1'b1:1'b0;
assign dat_en[5] = red_seg[11:4]? 1'b1:1'b0;
assign dat_en[4] = 1'b1;

assign dat_en[3] = 1'b0;
assign dat_en[2] = blue_seg[11:8]? 1'b1:1'b0;
assign dat_en[1] = blue_seg[11:4]? 1'b1:1'b0;
assign dat_en[0] = 1'b1;

数码管显示模块例化 程序实现如下:

Segment_scan u4
(
.clk (clk ), //系统时钟 12MHz
.rst_n (rst_n ), //系统复位 低有效
.dat_1 (0 ), //SEG1 显示的数据输入
.dat_2 (red_seg[11:8] ), //SEG2 显示的数据输入
.dat_3 (red_seg[7:4] ), //SEG3 显示的数据输入
.dat_4 (red_seg[3:0] ), //SEG4 显示的数据输入
.dat_5 (0 ), //SEG5 显示的数据输入
.dat_6 (blue_seg[11:8] ), //SEG6 显示的数据输入
.dat_7 (blue_seg[7:4] ), //SEG7 显示的数据输入
.dat_8 (blue_seg[3:0] ), //SEG8 显示的数据输入
.dat_en (dat_en ), //数码管数据位显示使能,[MSB~LSB]=[SEG1~SEG8]
.dot_en (8'hee ), //数码管小数点位显示使能,[MSB~LSB]=[SEG1~SEG8]
.seg_rck (seg_rck ), //74HC595的RCK管脚
.seg_sck (seg_sck ), //74HC595的SCK管脚
.seg_din (seg_din ) //74HC595的SER管脚
);

综合后的设计框图如下:

alt text
RTL设计框图