玩FPGA/CPLD肯定少不了点数码管,一个数码管有有八个段(包括小数点)和一个公共端。如果点亮8位数码管那就需要8*9=72个IO?那太浪费IO资源了,并且接几十根杜邦线也是体力活,但也不是没有解决办法,某宝上就有卖使用两片74HC595 8位串行移位寄存器来控制的8位数码管
整个模块才5个引脚,除去电源和地,就是说只用三个IO口就能驱动8位数码管了,比起刚才动辄几十个IO,用这个模块显然要“省”得多 !
那这个模块是怎样实现的呢,要看74HC595芯片的内部结构
从内部结构逻辑图就能清楚地看到,第一级是8个级联的D触发器,SCK每来一个脉冲,当前D触发器的输出就会变成前一个D触发器的输出(即当前D触发器的输入),那么来8个SCK脉冲,8位的串行数据就能全部输入到74HC595中“保存”起来了。并且第一级的8个D触发器的输出也都分别接到了D触发器 ,当第一级的8个串行数据都输入进来后,再给第二级的D触发器一个时钟信号,那么8位数据就会同时输出了。这也实现了串行转并行的功能,一个数码管是8段,刚刚好够我们用!但我们用的是8位的数码管,那咋办呢,就用两片74HC595级联起来,一片用来控制段(控制显示什么数字),一片用来控制位(哪一位数码管显示),这不就好了嘛
那下面就开始写代码了,还是用的EPM570核心板
建立工程,具体步骤可以看上一篇博客
先把单纯的8位数码管的代码写一下,新建Verilog HDL文件,命名为tube_8
保存,注意文件名要和module的名字一样
module tube_8(
clk,
reset_n,
disp_data,
sel,
seg
);
input clk;
input reset_n;
input [31:0] disp_data; //8位数码管上字符值,每个数码管占四位
output reg [7:0] sel; //8段选
output reg [7:0] seg; //8位选
reg [2:0] cnt_sel; //位选,3-8译码
reg [15:0]div_cnt; //1ms计数
parameter clock_freq = 50_000_000;//50MHz时钟
parameter turn_freq = 1000;//数码管切换速率1ms,即1000Hz
parameter MCNT = clock_freq/turn_freq - 1;
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;
always@(posedge clk or negedge reset_n)
if(!reset_n)
cnt_sel <= 0;
else if(div_cnt == MCNT)
cnt_sel <= cnt_sel + 1'd1;
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;//查找表
always@(posedge clk)
case(data_temp) //共阳数码管
0:seg <= 8'b1100_0000; //0
1:seg <= 8'b1111_1001; //1
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;//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
default seg <= 8'b1111_1111;
endcase
always@(*)
case(cnt_sel) //每一位的数码管对应要显示的值
0: data_temp <= disp_data[3:0];
1: data_temp <= disp_data[7:4];
2: data_temp <= disp_data[11:8];
3: data_temp <= disp_data[15:12];
4: data_temp <= disp_data[19:16];
5: data_temp <= disp_data[23:20];
6: data_temp <= disp_data[27:24];
7: data_temp <= disp_data[31:28];
endcase
endmodule
上面的代码中,输出8位的sel和8位的seg,就是分别用来控制数码管的8个位和8个段的。核心板的时钟频率是50Hz,我们设置数码管切换的频率是1000Hz,由于切换频率很高和人眼视觉暂留效应,我们并不会看到数码管在一位一位地切换,而是会清楚地观察到8位数码管上清晰稳定地显示8个数字 。定义8位的cnt_sel;每个数码管可以显示0~F,因此定义4位的data_temp用来存放数码管将要显示的数值,总共是8个数码管,因此还要有32位的disp_data用来存放8位数码管上将要显示的值。然后按照cnt_sel从0到7的不同值,分别给段码data_temp和位选sel赋值。最后根据data_temp的值来给seg赋值,这决定了数码管上要显示的字符是什么样子。我这个模块用的是共阳数码管,即公共端接到高电平,当段选为低时,对应的LED段就会点亮。以数字8为例,7个段要亮,小数点不亮,因此赋给seg的值是8b1000_0000;
接下来要写74HC595部分了,我们的目标不是写一个74HC595芯片,根据74HC595的工作原理设计一个接口。
从时序图可以看出来,每来一个时钟脉冲上升沿,数据就会读进去。并且要读入的数据最好在前一个下降沿时就给出来,这样在时钟脉冲上升沿到来时数据就会很稳定。否则在时钟脉冲上升沿到来时同时给出数据会容易出错
module hc595(
clk,
reset_n,
seg,
sel,
dio,
srclk,
rclk
);
input clk;
input reset_n;
input [7:0] seg;
input [7:0] sel;//seg8位,sel也8位
output reg dio;//sel和seg都通过dio输入进595芯片
output reg srclk;//每一位的dio稳定时,srclk拉高,将dio送进595
output reg rclk;//送完8位sel8位seg,rclk拉高一下,代表发完
parameter MCNT = 3-1;//50MHz时钟,半个周期3个脉冲,一个周期就6个,srclk时钟8.3MHz
reg [4:0]div_cnt; //产生srclk的计数器
reg [5:0]srclk_cnt; //最多计32个值(srclk一个周期要计2个值)
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;
always @(posedge clk or negedge reset_n)
if(!reset_n)
srclk_cnt<=0;
else if(div_cnt == MCNT) begin
if(srclk_cnt == 31)
srclk_cnt <= 0;
else srclk_cnt <= srclk_cnt + 1'd1;
end
always@(posedge clk or negedge reset_n)
if(!reset_n) begin
rclk <= 0;
srclk <= 1'd0;
dio <= seg[7];
end
else begin
case (srclk_cnt)
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
default: rclk <= 0;
endcase
end
endmodule
可以很方便地用线性序列机的方法写出来。dio是要输入的数据,srclk是给芯片的时钟,rclk是传输完成的标志,也是这一个标志让8位段选和8位位选输出。
完成了数码管和74HC595的驱动后写一个测试看看效果
module nixietube(
clk,
reset_n,
dio,
srclk,
rclk,
key
);
input clk;
input reset_n;
reg [31:0]disp_data;
input [1:0]key;
output dio;//sel和seg都通过dio输入进595芯片
output srclk;//每一位的dio稳定时,srclk拉高,将dio送进595
output rclk;//送完8位sel8位seg,rclk拉高一下,代表发完
wire [7:0]sel,seg;
always @(*)
case (key)
0: disp_data <= 32'h01010101;
1: disp_data <= 32'h10101010;
2: disp_data <= 32'hab0101cd;
3: disp_data <= 32'hee1010ff;
endcase
hc595 hc595_inst0(
.clk(clk),
.reset_n(reset_n),
.seg(seg),
.sel(sel), //hc595和hex8中的seg和sel只要名称一样就连接起来了
.dio(dio),
.srclk(srclk),
.rclk(rclk)
);
hex8 hex8_inst0(
.clk(clk),
.reset_n(reset_n),
.disp_data(disp_data),
.sel(sel),
.seg(seg)
);
endmodule
板子上有两个按键,用这两个按键来控制显示不同的内容。
必须将最后的这个文件设为Top-Level Entity
综合编译,将未使用引脚设为三态输入,Pin Planner分配引脚,再编译
综合完成,这个设计用了86个逻辑单元
打开Programmer,下载
看看现象
成功!