知識點:
數碼管控制分為位選和段選,透過位控制哪一個數碼管亮,透過段選控制數碼管中某一段亮。
我硬體買的上面的是共陽極的,也就是段選位給低電平就能亮。
下面是段選的位控制要顯示的資料。比如數字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
最終透過兩個撥碼開關選擇顯示我們需要的數字。這邊的例化類似於引用標頭檔案一樣。