FPGA数码管知识点整理
知识点:
数码管控制分为位选和段选,通过位控制哪一个数码管亮,通过段选控制数码管中某一段亮。
我硬件买的上面的是共阳极的,也就是段选位给低电平就能亮。
下面是段选的位控制要显示的数据。比如数字0只要让G位灭掉就行,通过给段选8'h1100_0000 (共阳极),将g和点灭掉就是0了
数码管的控制,通过动态控制,数码管的位选和段选,来控制显示数字
利用视觉残留,每1ms轮换一个数码管,20ms以内的闪烁,都可以被视觉残留利用,让人觉得一直在常亮,其实在闪烁
这样如果需要8个数码管亮灭,则需要16位IO管脚控制。这占用太多资源。这边会使用一个74HC595模块,来将数据进行移位操作+存储操作,这样就可以节省IO资源,其实就是将并转串。
将上面的位选和段选的sel 和 seg数据作为数据输入,这边是并行数据进去,通过模块移位寄存,最后将数据串行输出。
代码工程:
先完成8+8的位选和段选逻辑代码的工作。
这边是将需要显示的数据,存储在32数据当中,每4位表示一位数字,0-16.
然后通过sel和seg控制位选和段选,来显示数码管。
时间是1ms切换一次位选,这样一轮也才8ms<20ms,满足视觉残留需求
module hex8(
clk,
reset_n,
disp_data, //想要输入的值
sel, //选择那个数码管
seg //选择8段数码管哪一个亮a~g
);
input clk;
input reset_n;
input [31:0] disp_data; //每4位存储一位数据
output reg [7:0] sel;
output reg [7:0] seg;
reg [29:0] div_cnt; //计数器,1ms
reg [2:0] cnt_sel;
parameter MCNT = CLOCK_FREQ / TURN_FREQ - 1;
parameter CLOCK_FREQ = 50_000_000;
parameter TURN_FREQ = 1000;
//20ns * 50_000 = 1ms
always @(posedge clk or negedge reset_n)
if(!reset_n)
div_cnt <= 0;
else if(div_cnt == MCNT)
div_cnt <= 0;
else
div_cnt <= div_cnt + 1'd1;
//每1ms sel加一位,就是换下一个数码管亮,总共20ms内就可以利用
//人眼的视觉残留造成数码管一直亮的现象
always @(posedge clk or negedge reset_n)
if(!reset_n)
cnt_sel <= 0;
else if(div_cnt == MCNT)
cnt_sel <= cnt_sel + 1'd1;
//38译码器
//数码管位选,选择当前数码管
always @(posedge clk)
case(cnt_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
reg [3:0] data_temp; //数码管显示内容控制段选信号
//这边用阻塞赋值,wire是不是也可以?
//数码管段选,选择显示的内容
always @(posedge clk)
case(data_temp)
0: seg <= 8'b1100_0000; //0
1: seg <= 8'b1111_1001; //1
2: seg <= 8'b1010_0100; //2
3: seg <= 8'b1011_0000; //3
4: seg <= 8'b1001_1001; //4
5: seg <= 8'b1001_0010; //5
6: seg <= 8'b1000_0010; //6
7: seg <= 8'b1111_1000; //7
8: seg <= 8'b1000_0000; //8
9: seg <= 8'b1001_0000; //9
10: seg <= 8'b1000_1000; //A
11: seg <= 8'b1000_0011; //B
12: seg <= 8'b1100_0110; //C
13: seg <= 8'b1010_0001; //D
14: seg <= 8'b1000_0110; //E
15: seg <= 8'b1000_1110; //F
endcase
//disp_data 8个数码管待显示数据,每四个数据组成一个BCD码
always @(*)
case(cnt_sel)
0: data_temp <= disp_data[3:0]; //0
1: data_temp <= disp_data[7:4]; //1
2: data_temp <= disp_data[11:8]; //2
3: data_temp <= disp_data[15:12]; //3
4: data_temp <= disp_data[19:16]; //4
5: data_temp <= disp_data[23:20]; //5
6: data_temp <= disp_data[27:24]; //6
7: data_temp <= disp_data[31:28]; //7
endcase
endmodule
然后通过将上面hex8.v中的seg和sel端口作为输入引入寄存器595中的输入。
通过移位寄存输出串行数据控制数码管,一下子少了很多IO使用。
下面三个图分别是595寄存器的手册中整理的时序图和功能表等。
直接看下面这个图,移位寄存器上升沿时候,数据进入移位寄存器,在存储寄存器上升沿时输出到并行端口。
这边的srclk是移位寄存器,rclk是存储寄存器
SRCLK - SHCP
RCLK - STCP
因为这边是两个595连起来,因为需要16位数据传输,所以是每16位数据移位结束后,再进行锁存数据。
这边在移位寄存器的上升沿,通过DS/DIO串行数据输入口,不断输入数据我们这边是将seg和sel信号16位输入给dio里面
这边根据595的特性,我们是3.3V电平,寄存器工作频率采用12.5MHZ,正好是50MHZ的4分之一。
所以最小时间单位就是40ns =20ns *2
每40ns 移位寄存器电平转换一次。下面的代码中以移位寄存器的电平为时间基准来对存储寄存器和串行数据进行移位寄存处理。
一开始,肯定是移位寄存器拉高,将DIO中数据读取出来放入移位寄存器的高位,然后下一位SRCLK的高电平时继续读取DIO的数据,然后将新的数据放入高位,上一个数据放入次高位,一直下去,直到将DIO的8位数据全部读取,我们这边是两个595,所以是16位数据=sel+seg
而每16位数据存储结束,RCLK的上升沿时刻,移位寄存器中的数据转存到锁存器当中保存。这就是存储寄存器的作用。
代码中根据最小时间单位,也就是移位寄存器的高低电平变化,来进行数据移位存储。在移位寄存器变化时候,将seg与sel的值写入dio当中,最后锁存。
因为没有觉得起始点区分,这边放一段仿真之后的,仿真中给定了值
seg = 8'b0101_0101;sel = 8'b0000_0001;
我们可以看到,确实最后的结果是0101_0101_0000_0001
在黄线处是16位数移位完成,rclk高电平完成锁存操作。以这个时候为开始就是代码中的
一开始是cnt还没计时,将第一个值赋值给dio,这个时候移位寄存器还没上升沿,存储寄存器上升沿,锁存数据。(这是结束也是开始)
dio <= seg[7];srclk <= 1'd0; rclk <= 1'd1;所以后面存储寄存器一个计数后拉低,就可以开始移位寄存器操作了
然后cnt计数开始,40ns一个计数,在cnt计数满时候,这个时候将存储寄存器拉低,移位寄存器拉高,移位操作。
第二部就是srclk <= 1'd1; rclk <= 1'd0;
然后因为上面移位操作之后,最高位空出来,继续赋值操作,且拉低移位寄存器。
2:begin dio <= seg[6]; srclk <= 1'd0; end
总结:就是存数据,移位,空出位置,下一个存进来,继续移位,以此类推,最后存满16个数据,锁存数据,存储寄存器拉高
module hc595_driver(
clk,
reset_n,
seg,
sel,
dio,
srclk, //存储寄存器的时钟输入,上升沿时移位寄存器中的数据进入存储寄存器
rclk //移位寄存器的时钟输入,上升沿时移位寄存器中数据一次移动一位。下降沿不变
);
input clk;
input reset_n;
input [7:0] seg;
input [7:0] sel;
output reg dio; //ser 串行数据输入端
output reg srclk;
output reg rclk;
//595 频率 2V 5M 4.5V 24M 50Mhz 20ns一个周期,一位10ns
//取12.5Mhz 对应3.3V 正好是50Mhz的四分频 80ns一周期,一位就是40ns
parameter MCNT = CLOCK_FREQ / (SRCLK_FREQ * 2) - 1; //1
parameter CLOCK_FREQ = 50_000_000; //50MHZ时钟
parameter SRCLK_FREQ = 12_500_000; //
reg [29:0] div_cnt;
always @(posedge clk or negedge reset_n)
if(!reset_n)
div_cnt <= 0;
else if(div_cnt == MCNT) //20ns * 2 = 40ns
div_cnt <= 0;
else
div_cnt <= div_cnt + 1'd1;
reg [4:0] cnt;
always @(posedge clk or negedge reset_n)
if(!reset_n)
cnt <= 0;
else if(div_cnt == MCNT) //40ns 移位寄存器的时钟
cnt <= cnt + 1'd1;
always @(posedge clk or negedge reset_n)
if(!reset_n) begin
dio <= 1'd0;
srclk <= 1'd0;
rclk <= 1'd0;
end
else begin
case(cnt)
0:begin dio <= seg[7]; srclk <= 1'd0; rclk <= 1'd1; end
1:begin rclk <= 1'd0; srclk <= 1'd1; 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
endmodule
最后写一个测试文件,例化上面两个顶层文件
module hex8_hc595_test(
clk,
reset_n,
SW,
dio,
srclk, //存储寄存器的时钟输,上升沿时移位寄存器中的数据进入存储寄存器
rclk
);
input clk;
input reset_n;
input [1:0] SW;
output dio; //ser 串行数据输入端
output srclk;
output rclk;
reg [31:0] disp_data;
wire [7:0] sel,seg;
hc595_driver hc595_driver_inst(
.clk(clk),
.reset_n(reset_n),
.seg(seg),
.sel(sel),
.dio(dio),
.srclk(srclk),
.rclk(rclk)
);
hex8 hex8_inst(
.clk(clk),
.reset_n(reset_n),
.disp_data(disp_data),
.sel(sel),
.seg(seg)
);
always @(*)
case(SW)
0:disp_data <= 32'h01234567;
1:disp_data <= 32'h89abcdef;
2:disp_data <= 32'h02468ace;
3:disp_data <= 32'h13578bdf;
endcase
endmodule
最终通过两个拨码开关选择显示我们需要的数字。这边的例化类似于引用头文件一样。
本文来自博客园,作者:祈愿树下,转载请注明原文链接:https://www.cnblogs.com/cjl520/p/18048800
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 百万级群聊的设计实践
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期