本帖最后由 正点原子运营 于 2023-11-10 16:33 编辑
第十四章 动态数码管显示实验
1)实验平台:正点原子 ATK-DFPGL22G开发板
2) 章节摘自【正点原子】ATK-DFPGL22G之FPGA开发指南_V1.0
6)FPGA技术交流QQ群:435699340
经过上一章的学习,我们已经知道如何使用数码管静态驱动的方式使数码管显示数字,但在很多情况下,我们需要让数码管各个位显示不同的数字,这就需要以动态驱动的方式驱动数码管。本章我们主要介绍数码管动态驱动的原理以及如何使用动态驱动的方式在数码管上显示变化的数字。 本章包括以下几个部分: 1.1 简介 1.2 实验任务 1.3 硬件设计 1.4 程序设计 1.5 下载验证
1.1 简介在“数码管静态显示实验”章节我们详细地介绍了有关数码管方面的知识和数码管的静态驱动,让大家对数码管的驱动有了基本的了解。由于一般的静态驱动操作虽然方便,但占用的I/0口较多,例如要驱动6位8段数码管,以静态驱动方式让数码管各个位显示不同的数值,如“123456”,需要占用6×8=48个I/O口,虽然对于FPGA这种I/O口较多的芯片而言,在资源允许的情况下可以使用,但一般不建议浪费宝贵的I/O口资源,尤其在I/O口资源紧张的情况下,所以对于多位数码管一般采用动态驱动方式使数码管显示数字。那么什么是动态驱动方式呢?
为了更好的理解数码管动态驱动,我们首先了解下市面上常见的多位数码管的内部连接。以两位数码管为例,其内部连接如下图。由此图可知,两位8段数码管共10个引脚,每位数码管的阳极连接在一起,为共阳极数码管,每位数码管相同段的led的阴极连接在一起,这样当给第10和第5脚高电平,给第3脚低电平时,两个数码管的发光二极管A都点亮,对于此种数码管以静态方式驱动显然不可能显示像“18”这种个位与十位不同的数字。那么该如何显示数字“18”呢? 既然同时给第10和第5脚高电平不可行,那么是不是可以先给第5脚高电平,第10脚低电平,此时,让其显示数字“8”时,左边的数码管不显示,右边的数码管显示数字“8”;然后给第10脚高电平,第5脚低电平,此时,让其显示数字“1”时,左边的数码管显示数字“1”,右边的数码管不显示,这样就可以显示数字“18”了。但有一个问题,多长时间切换显示的数码管呢,时间如果太长就只能看到数字“8”或数字“1”了,时间太短呢,结果是显示不清晰而且显示亮度不够。由于人眼的视觉暂留(人眼在观察景物时,光信号传人大脑神经,需经过一段短暂的时间,光的作用结束后,视觉形象并不立即消失,这种残留的视觉称“后像”,视觉的这一现象则被称为“视觉暂留”)及发光二极管的余辉效应(当停止向发光二极管供电时,发光二极管亮度仍能维持一段时间),每位数码管的点亮时间为1~2ms时,显示效果能满足使用需要。数码管的这种驱动方式称为数码管的动态驱动,实际上就是分时轮流控制不同数码管的显示。
1.2 实验任务本节实验任务是使用FPGA开发板上的6位数码管以动态方式从0开始计数,每100ms计数值增加一,当计数值从0增加到999999后重新从0开始计数。
1.3 硬件设计数码管接口部分的硬件设计原理及本实验中各端口信号的管脚分配与“数码管静态显示实验”完全相同,请参考“数码管静态显示实验”中的硬件设计部分。
1.4 程序设计由实验任务和动态驱动的原理我们可以知道,若要让6个数码管轮流显示对应的数字,首先需要一个数码管动态显示模块,能够依次点亮6个数码管,并将对应的数据输出至数码管,也就是需要分别控制段选和位选信号;同时还需要一个计数模块,能够将0—999999依次输出至数码管动态显示模块。根据实验任务,我们可以大致规划出系统的控制流程:首先我们需要一个数码管动态显示模块在数码管上显示数据,其次需要一个计数控制模块实现从0到999999的变化,并将产生的数值通过数码管动态显示模块在数码管上显示出来。由此画出系统的功能框图如下所示: 程序中各模块端口及信号连接如下图所示: FPGA顶层(top_seg_led)例化了以下两个模块:计数模块(count)以及数码管动态显示模块(seg_led)。实现各模块之间数据的交互。计数模块将计数值通过data端口传递给数码管动态显示模块,使能信号en使能数码管显示数据,小数点显示信号point控制小数点的显示,符号信号sign可以让数码管显示负号。
计数模块(count):显示的数字每100ms加“1”。
数码管动态显示模块(seg_led):数码管动态显示模块在数码管上以动态方式显示数值。
顶层模块的代码如下: - 1 module top_seg_led(
- 2 //global clock
- 3 input sys_clk , // 全局时钟信号
- 4 input sys_rst_n, // 复位信号(低有效)
- 5
- 6 //seg_led interface
- 7 output [5:0 seg_sel , // 数码管位选信号
- 8 output [7:0 seg_led // 数码管段选信号
- 9 );
- 10
- 11 //wire define
- 12 wire [19:0 data; // 数码管显示的数值
- 13 wire [ 5:0 point; // 数码管小数点的位置
- 14 wire en; // 数码管显示使能信号
- 15 wire sign; // 数码管显示数据的符号位
- 16
- 17 //*****************************************************
- 18 //** main code
- 19 //*****************************************************
- 20
- 21 //计数器模块,产生数码管需要显示的数据
- 22 count u_count(
- 23 .clk (sys_clk ), // 时钟信号
- 24 .rst_n (sys_rst_n), // 复位信号
- 25
- 26 .data (data ), // 6位数码管要显示的数值
- 27 .point (point ), // 小数点具体显示的位置,高电平有效
- 28 .en (en ), // 数码管使能信号
- 29 .sign (sign ) // 符号位
- 30 );
- 31
- 32 //数码管动态显示模块
- 33 seg_led u_seg_led(
- 34 .clk (sys_clk ), // 时钟信号
- 35 .rst_n (sys_rst_n), // 复位信号
- 36
- 37 .data (data ), // 显示的数值
- 38 .point (point ), // 小数点具体显示的位置,高电平有效
- 39 .en (en ), // 数码管使能信号
- 40 .sign (sign ), // 符号位,高电平显示负号(-)
- 41
- 42 .seg_sel (seg_sel ), // 位选
- 43 .seg_led (seg_led ) // 段选
- 44 );
- 45
- 46 endmodule
复制代码顶层模块中主要完成对其余模块的例化,并且实现各模块之间信号的交互。计数模块输出的数值data连接至数码管显示模块的输入端口data,数码管显示模块将输入的数据data输出至数码管上显示。
计数模块的代码如下所示: - 1 module count(
- 2 //mudule clock
- 3 input clk , // 时钟信号
- 4 input rst_n, // 复位信号
- 5
- 6 //user interface
- 7 output reg [19:0 data , // 6个数码管要显示的数值
- 8 output reg [ 5:0 point, // 小数点的位置,高电平点亮对应数码管位上的小数点
- 9 output reg en , // 数码管使能信号
- 10 output reg sign // 符号位,高电平时显示负号,低电平不显示负号
- 11 );
- 12
- 13 //parameter define
- 14 parameter MAX_NUM = 23'd5000_000; // 计数器计数的最大值
- 15
- 16 //reg define
- 17 reg [22:0 cnt ; // 计数器,用于计时100ms
- 18 reg flag; // 标志信号
- 19
- 20 //*****************************************************
- 21 //** main code
- 22 //*****************************************************
- 23
- 24 //计数器对系统时钟计数达100ms时,输出一个时钟周期的脉冲信号
- 25 always @ (posedge clk or negedge rst_n) begin
- 26 if (!rst_n) begin
- 27 cnt <= 23'b0;
- 28 flag<= 1'b0;
- 29 end
- 30 else if (cnt < MAX_NUM - 1'b1) begin
- 31 cnt <= cnt + 1'b1;
- 32 flag<= 1'b0;
- 33 end
- 34 else begin
- 35 cnt <= 23'b0;
- 36 flag <= 1'b1;
- 37 end
- 38 end
- 39
- 40 //数码管需要显示的数据,从0累加到999999
- 41 always @ (posedge clk or negedge rst_n) begin
- 42 if (!rst_n)begin
- 43 data <= 20'b0;
- 44 point <=6'b000000;
- 45 en <= 1'b0;
- 46 sign <= 1'b0;
- 47 end
- 48 else begin
- 49 point <= 6'b000000; //不显示小数点
- 50 en <= 1'b1; //打开数码管使能信号
- 51 sign <= 1'b0; //不显示负号
- 52 if (flag) begin //显示数值每隔0.1s累加一次
- 53 if(data < 20'd999999)
- 54 data <= data +1'b1;
- 55 else
- 56 data <= 20'b0;
- 57 end
- 58 end
- 59 end
- 60
- 61 endmodule
复制代码码中第14行的参数MAX_NUM为计数的最大计数值,由于是对时钟计数,相当于计时,第25行的always语句块表示的是当计数器cnt计数值小于MAX_NUM - 1'b1时,标志(flag)为“0”,否则标志(flag)为“1”,并且计数器cnt清零。
数码管动态显示模块的代码如下: - 1 module seg_led(
- 2 input clk , // 时钟信号
- 3 input rst_n , // 复位信号
- 4
- 5 input [19:0 data , // 6位数码管要显示的数值
- 6 input [5:0 point , // 小数点具体显示的位置,从高到
- 7 input en , // 数码管使能信号
- 8 input sign , // 符号位(高电平显示“-”号)
- 9
- 10 output reg [5:0 seg_sel, // 数码管位选,最左侧数码管为最
- 11 output reg [7:0 seg_led //数码管段选
- 12 );
- 13
- 14 //parameter define
- 15 localparam CLK_DIVIDE =4'd10 ; // 时钟分频系数
- 16 localparam MAX_NUM = 13'd5000 ; // 对数码管驱动时钟(5MHz)计数1m
- 17
- 18 //reg define
- 19 reg [ 3:0 clk_cnt ; // 时钟分频计数器
- 20 reg dri_clk ; // 数码管的驱动时钟,5MHz
- 21 reg [23:0 num ; // 24位bcd码寄存器
- 22 reg [12:0 cnt0 ; // 数码管驱动时钟计数器
- 23 reg flag ; // 标志信号(标志着cnt0计数达1m
- 24 reg [7:0 cnt_sel ; // 数码管位选计数器
- 25 reg [3:0 num_disp ; // 当前数码管显示的数据
- 26 reg dot_disp ; // 当前数码管显示的小数点
- 27
- 28 //wire define
- 29 wire [3:0 data0 ; // 个位数
- 30 wire [3:0 data1 ; // 十位数
- 31 wire [3:0 data2 ; // 百位数
- 32 wire [3:0 data3 ; // 千位数
- 33 wire [3:0 data4 ; // 万位数
- 34 wire [3:0 data5 ; // 十万位数
- 35
- 36 //*****************************************************
- 37 //** main code
- 38 //*****************************************************
- 39
- 40 //提取显示数值所对应的十进制数的各个位
- 41 assign data0 = data % 4'd10; // 个位数
- 42 assign data1 = data / 4'd10 % 4'd10 ; // 十位数
- 43 assign data2 = data / 7'd100 % 4'd10 ; //百位数
- 44 assign data3 = data / 10'd1000 % 4'd10 ; // 千位数
- 45 assign data4 = data / 14'd10000 % 4'd10; // 万位数
- 46 assign data5 = data / 17'd100000; // 十万位数
- 47
- 48 //对系统时钟10分频,得到的频率为5MHz的数码管驱动时钟dri_clk
- 49 always @(posedge clk or negedge rst_n) begin
- 50 if(!rst_n) begin
- 51 clk_cnt <= 4'd0;
- 52 dri_clk <= 1'b1;
- 53 end
- 54 else if(clk_cnt == CLK_DIVIDE / 2 - 1) begin
- 55 clk_cnt <= 4'd0;
- 56 dri_clk <= ~dri_clk;
- 57 end
- 58 else begin
- 59 clk_cnt <= clk_cnt +1'b1;
- 60 dri_clk <= dri_clk;
- 61 end
- 62 end
- 63
- 64 //将20位2进制数转换为8421bcd码(即使用4位二进制数表示1位十进制数)·
- 65 always @ (posedge dri_clk or negedge rst_n) begin
- 66 if (!rst_n)
- 67 num <= 24'b0;
- 68 else begin
- 69 if (data5 || point[5]) begin //如果显示数据为6位十进制数,
- 70 num[23:20 <= data5; //则依次给6位数码管赋值
- 71 num[19:16 <= data4;
- 72 num[15:12 <= data3;
- 73 num[11:8 <= data2;
- 74 num[ 7:4 <= data1;
- 75 num[ 3:0 <= data0;
- 76 end
- 77 else begin
- 78 if (data4 || point[4]) begin //如果显示数据为5位十进制数,则给
- 79 num[19:0 <= {data4,data3,data2,data1,data0};
- 80 if(sign)
- 81 num[23:20 <= 4'd11; //如果需要显示负号,则最高位(第6
- 82 else
- 83 num[23:20 <= 4'd10; //不需要显示负号时,则第6位不显示
- 84 end
- 85 else begin //如果显示数据为4位十进制数,则给
- 86 if (data3 || point[3]) begin
- 87 num[15: 0 <= {data3,data2,data1,data0};
- 88 num[23:20 <= 4'd10; //第6位不显示任何字符
- 89 if(sign) //如果需要显示负号,则最高位(第5
- 90 num[19:16 <= 4'd11;
- 91 else //不需要显示负号时,则第5位不显示
- 92 num[19:16 <= 4'd10;
- 93 end
- 94 else begin //如果显示数据为3位十进制数,则给
- 95 if (data2 || point[2]) begin
- 96 num[11: 0 <= {data2,data1,data0};
- 97 //第6、5位不显示任何字符
- 98 num[23:16 <= {2{4'd10}};
- 99 if(sign) //如果需要显示负号,则最高位(第4
- 100 num[15:12 <= 4'd11;
- 101 else //不需要显示负号时,则第4位不显示
- 102 num[15:12 <= 4'd10;
- 103 end
- 104 else begin //如果显示数据为2位十进制数,则给
- 105 if (data1 || point[1]) begin
- 106 num[ 7: 0 <= {data1,data0};
- 107 //第6、5、4位不显示任何字符
- 108 num[23:12 <= {3{4'd10}};
- 109 if(sign) //如果需要显示负号,则最高位(第3
- 110 num[11:8 <= 4'd11;
- 111 else //不需要显示负号时,则第3位不显示
- 112 num[11:8 <= 4'd10;
- 113 end
- 114 else begin //如果显示数据为1位十进制数,则给
- 115 num[3:0 <= data0;
- 116 //第6、5位不显示任何字符
- 117 num[23:8 <= {4{4'd10}};
- 118 if(sign) //如果需要显示负号,则最高位(第2
- 119 num[7:4 <= 4'd11;
- 120 else //不需要显示负号时,则第2位不显示
- 121 num[7:4 <= 4'd10;
- 122 end
- 123 end
- 124 end
- 125 end
- 126 end
- 127 end
- 128 end
- 129
- 130 //每当计数器对数码管驱动时钟计数时间达1ms,输出一个时钟周期的脉冲信号
- 131 always @ (posedge dri_clk or negedge rst_n) begin
- 132 if (rst_n == 1'b0) begin
- 133 cnt0 <= 13'b0;
- 134 flag <= 1'b0;
- 135 end
- 136 else if (cnt0 < MAX_NUM - 1'b1) begin
- 137 cnt0 <= cnt0 + 1'b1;
- 138 flag <= 1'b0;
- 139 end
- 140 else begin
- 141 cnt0 <= 13'b0;
- 142 flag <= 1'b1;
- 143 end
- 144 end
- 145
- 146 //cnt_sel从0计数到5,用于选择当前处于显示状态的数码管
- 147 always @ (posedge dri_clk or negedge rst_n) begin
- 148 if (rst_n == 1'b0)
- 149 cnt_sel <= 8'b0;
- 150 else if(flag) begin
- 151 if(cnt_sel < 8'd11)
- 152 cnt_sel <= cnt_sel + 1'b1;
- 153 else
- 154 cnt_sel <= 8'b0;
- 155 end
- 156 else
- 157 cnt_sel <= cnt_sel;
- 158 end
- 159
- 160 //控制数码管位选信号,使6位数码管轮流显示
- 161 always @ (posedge dri_clk or negedge rst_n) begin
- 162 if(!rst_n) begin
- 163 seg_sel <= 6'b000000; //位选信号高电平有效
- 164 num_disp <= 4'b0;
- 165 dot_disp <= 1'b1; //共阳极数码管,低电平导通
- 166 end
- 167 else begin
- 168 if(en) begin
- 169 case (cnt_sel)
- 170 8'd0 :begin
- 171 seg_sel <= 6'b000001; //显示数码管最低位
- 172 num_disp <= num[3:0 ; //显示的数据
- 173 dot_disp <= ~point[0]; //显示的小数点
- 174 end
- 175 8'd1 :begin
- 176 seg_sel <= 6'b000000; //清空
- 177 num_disp <= 4'd10;
- 178 end
- 179 8'd2 :begin
- 180 seg_sel <= 6'b000010; //显示数码管第1位
- 181 num_disp <= num[7:4 ;
- 182 dot_disp <= ~point[1];
- 183 end
- 184 8'd3 :begin
- 185 seg_sel <= 6'b000000; //清空
- 186 num_disp <= 4'd10;
- 187 end
- 188 8'd4 :begin
- 189 seg_sel <= 6'b000100; //显示数码管第2位
- 190 num_disp <= num[11:8];
- 191 dot_disp <= ~point[2];
- 192 end
- 193 8'd5 :begin
- 194 seg_sel <= 6'b000000; //清空
- 195 num_disp <= 4'd10;
- 196 end
- 197 8'd6 :begin
- 198 seg_sel <= 6'b001000; //显示数码管第3位
- 199 num_disp <= num[15:12];
- 200 dot_disp <= ~point[3];
- 201 end
- 202 8'd7 :begin
- 203 seg_sel <= 6'b000000; //清空
- 204 num_disp <= 4'd10;
- 205 end
- 206 8'd8 :begin
- 207 seg_sel <= (6'b010000); //显示数码管第4位
- 208 num_disp <= num[19:16];
- 209 dot_disp <= ~point[4];
- 210 end
- 211 8'd9 :begin
- 212 seg_sel <= 6'b000000; //清空
- 213 num_disp <= 4'd10;
- 214 end
- 215 8'd10 :begin
- 216 seg_sel <= 6'b100000; //显示数码管最高位
- 217 num_disp <= num[23:20];
- 218 dot_disp <= ~point[5];
- 219 end
- 220 8'd11 :begin
- 221 seg_sel <= 6'b000000; //清空
- 222 num_disp <= 4'd10;
- 223 end
- 224 default :begin
- 225 seg_sel <= 6'b000000;
- 226 num_disp <= 4'b0;
- 227 dot_disp <= 1'b1;
- 228 end
- 229 endcase
- 230 end
- 231 else begin
- 232 seg_sel <= 6'b000000; //使能信号为0时,所有数码管均不
- 233 num_disp <= 4'b0;
- 234 dot_disp <= 1'b1;
- 235 end
- 236 end
- 237 end
- 238
- 239 //控制数码管段选信号,显示字符
- 240 always @ (posedge dri_clk or negedge rst_n) begin
- 241 if (!rst_n)
- 242 seg_led <= ~(8'hc0);
- 243 else begin
- 244 case (num_disp)
- 245 4'd0 : seg_led <= ~{dot_disp,(7'b1000000)}; //显示数字 0
- 246 4'd1 : seg_led <= ~{dot_disp,(7'b1111001)}; //显示数字 1
- 247 4'd2 : seg_led <= ~{dot_disp,(7'b0100100)}; //显示数字 2
- 248 4'd3 : seg_led <= ~{dot_disp,(7'b0110000)}; //显示数字 3
- 249 4'd4 : seg_led <= ~{dot_disp,(7'b0011001)}; //显示数字 4
- 250 4'd5 : seg_led <= ~{dot_disp,(7'b0010010)}; //显示数字 5
- 251 4'd6 : seg_led <= ~{dot_disp,(7'b0000010)}; //显示数字 6
- 252 4'd7 : seg_led <= ~{dot_disp,(7'b1111000)}; //显示数字 7
- 253 4'd8 : seg_led <= ~{dot_disp,(7'b0000000)}; //显示数字 8
- 254 4'd9 : seg_led <= ~{dot_disp,(7'b0010000)}; //显示数字 9
- 255 4'd10: seg_led <= ~8'b11111111; //不显示任何字符
- 256 4'd11: seg_led <= ~8'b10111111; //显示负号(-)
- 257 default:
- 258 seg_led <= {~dot_disp,~(7'b1000000)};
- 259 endcase
- 260 end
- 261 end
- 262
- 263 endmodule
复制代码在170行到223行中,可以看到由于数码管硬件引起的程序改动,即在每次刷新一位显示后关闭所有显示,这样会使FPGA管脚受程序控制直接接地,从而快速释放MOS管的电压,这样可以有效避免显示残留的问题。
数码管动态显示模块不仅可以将数值显示在数码管上,而且可以控制小数点的显示以及显示负数。数码管驱动模块没有在高位填充“0”,除非该位显示小数点。结合第131行开始的always语句块可知,cnt每1ms的时间变化一次;而从第161行的case语句块可知,cnt控制数码管的位选和段选。
1.5 下载验证首先将下载器一端连接电脑,另一端与开发板上的JTAG下载口相连,最后连接电源线并打开电源开关。接下来我们下载程序,验证数码管动态显示的功能。
下载完成后观察到开发板上数码管显示的值从“0”增加到“999999”,如下图所示,说明数码管动态显示实验程序下载验证成功。 |