Nixie Tube的驅動方式這幾年來陸陸續續也是換了滿多種的做法,從剛開始用74141到74141 + 陽極掃描到灑一堆高壓電晶體,這次的設計先來上電路圖:
Nixie Tube有一個很有趣的特性是他啟動和關閉都需要一點時間,如果開啟的速度太短就會像上圖一樣來不及整個數字發光。下面這兩張圖都鎖定了曝光時間/光圈/ISO,第一張圖是在低PWM的時候會發生的狀況:
如果仔細看第二篇的High Level System Diagram的話就會發現有一個PDM的Block,而不是常見的PWM。因為晚上我不想要燈光太亮但是又需要看到時間,所以為了解決低亮度的時候斷線的問題,加上因為HV5623的控制線接上FPGA,硬體彈性很高,直接設計了一個16Bit的PDM去驅動全域的亮度。
這邊先來講PDM跟PWM的差異,PDM顧名思義就是脈波的密度,PWM是脈波的寬度。PDM透過一個固定寬度的脈波但是調整脈波之間的密度,PWM透過控制脈波的寬度(占比)。所以這就是為什麼PDM可以解決低亮度下的問題,因為我可以設計上讓PDM的單位脈波的寬度寬到讓每一個數字都能完整的點亮,然後透過脈波調整密度控制亮度。
PWM在驅動Nixie Tube的時候會遇到的就是假如PWM速度太快,當Duty Cycle太低導致脈波寬度低於Nixie Tube可以完整點亮的時候就會導致斷線,而且就算我降低PWM的速度,終究是會有Duty Cycle低於完整點亮的時候,而且PWM一旦降低就會造成閃爍。低亮度調整範圍就會被限制在會斷線的Duty Cycle。
這是兩個不同亮度的PDM波形,你可以看到上下兩張圖的脈波是一樣的,只是第二張的密度變高了。
PDM Block在FPGA裡面我是控制了兩個參數,一個是PDM的Value,另一個是Clock Divider(脈波寬度),所以我能同時控制密度跟脈波寬度。End Result就是在低亮度的狀況下還是可以保持完整的顯示:
當然,PDM不是萬靈丹,如果無限制的往下調亮度因為脈波密度太低,終究是會變成閃爍的燈光,但是PDM可以延伸到比PWM再更暗的亮度,在夜晚顯示的時候就比較好看而且不會刺眼。
最後的最後的就是因為是FPGA,我可以很隨意的拉高PDM的bit寬度,拉到16bit有非常大的調整空間可以讓我用log value亮度控制曲線。能夠設計使用PDM去控制亮度算是用FPGA的附加效應了,一般的MCU我也不確定要怎麼用硬體的Block去實作,也許可能用Raspberry Pi Pico的PIO Block?
FPGA推HV5623的話就很簡單了,基本上就是SPI Controller + BCD−to−Decimal,設計上input是八個數字,input編碼是二進位的BCD,加上兩個Byte的小數點。
每一個Digit的BCD Code是4bit,八個剛好32Bit,然後每組Digit的BCD接上BCD−to−Decimal轉換邏輯。最後把這些bit串成一組96bit要送出去的資料打出去,trigger就是FPGA內部的Pulse Per Second訊號,或者是Raspberry Pi直接寫FPGA Register的訊號(這邏輯就是在其他Block實作了,這邊就單純當作收到pps)。
module NixieCounter (
input clk,
input rst,
input wire [31:0] NixieBCD,
input wire [15:0] digitpoint,
output reg NIXIE_LE,
output reg NIXIE_CLK,
output NIXIE_DIN,
input pps,
output reg Done
);
wire[95:0] nixiedata_96bit;
binarytoNixiedigit conv8(.x(NixieBCD[3:0]),.z(nixiedata_96bit[96-2:84+1]));
binarytoNixiedigit conv7(.x(NixieBCD[7:4]),.z(nixiedata_96bit[84-2:72+1]));
binarytoNixiedigit conv6(.x(NixieBCD[11:8]),.z(nixiedata_96bit[72-2:60+1]));
binarytoNixiedigit conv5(.x(NixieBCD[15:12]),.z(nixiedata_96bit[60-2:48+1]));
binarytoNixiedigit conv4(.x(NixieBCD[19:16]),.z(nixiedata_96bit[48-2:36+1]));
binarytoNixiedigit conv3(.x(NixieBCD[23:20]),.z(nixiedata_96bit[36-2:24+1]));
binarytoNixiedigit conv2(.x(NixieBCD[27:24]),.z(nixiedata_96bit[24-2:12+1]));
binarytoNixiedigit conv1(.x(NixieBCD[31:28]),.z(nixiedata_96bit[12-2:0+1]));
assign {nixiedata_96bit[0] ,nixiedata_96bit[12],nixiedata_96bit[24],nixiedata_96bit[36],nixiedata_96bit[48],nixiedata_96bit[60],nixiedata_96bit[72],nixiedata_96bit[84],nixiedata_96bit[11+0] ,nixiedata_96bit[11+12],nixiedata_96bit[11+24],nixiedata_96bit[11+36],nixiedata_96bit[11+48],nixiedata_96bit[11+60],nixiedata_96bit[11+72],nixiedata_96bit[11+84]} = digitpoint;
wire[95:0] nixiedata_32bit ;
function [32-1:0] bitOrder (
input [32-1:0] data
);
integer i;
begin
for (i=0; i < 32; i=i+1) begin : reverse
bitOrder[32-1-i] = data[i]; //Note how the vectors get swapped around here by the index. For i=0, i_out=15, and vice versa.
end
end
endfunction
//assign sample_rev = bitOrder(sample_in); //swap the bits.
assign nixiedata_32bit[31:0] = bitOrder(nixiedata_96bit[96-1:64]);
assign nixiedata_32bit[63:32] = bitOrder(nixiedata_96bit[64-1:32]);
assign nixiedata_32bit[95:64] = bitOrder(nixiedata_96bit[32-1:0]);
//assign nixiedata_32bit[31:0] = nixiedata_96bit[96-1:64];
//assign nixiedata_32bit[63:32] = nixiedata_96bit[64-1:32];
//assign nixiedata_32bit[95:64] = nixiedata_96bit[32-1:0];
localparam IDLE = 3'b000;
localparam SHIFT_0 = 3'b001;
localparam SHIFT_1 = 3'b010;
localparam START_0 = 3'b011;
localparam START_1 = 3'b100;
localparam START_2 = 3'b111;
localparam END = 3'b101;
localparam END_WAIT = 3'b110;
reg [2:0] currentState;
reg [2:0] nextState;
reg [15:0] dataCounter;
reg [95:0] nixie_data;
reg [1:0] data_in_valid_d;
assign NIXIE_DIN = nixie_data[95]; // send MSB first
reg [1:0] fallingEdgePPS_d;
wire fallingEdgePPS = (fallingEdgePPS_d == 2'b10);
always @(posedge clk) begin
fallingEdgePPS_d <= {fallingEdgePPS_d[0],pps};
if (!rst) begin
currentState <= IDLE;
NIXIE_CLK <= 0;
NIXIE_LE <= 1;
dataCounter <= 0;
nixie_data <= 0;
Done <= 0;
fallingEdgePPS_d <= 0;
end
else begin
currentState <= nextState;
case(currentState)
IDLE: begin
if(fallingEdgePPS) begin
nixie_data <= nixiedata_32bit;
dataCounter <= 0;
end
NIXIE_LE <= 1;
NIXIE_CLK <= 0;
Done <= 0;
end
START_2: begin
NIXIE_LE <= 1;
NIXIE_CLK <= 0;
end
START_0: begin
NIXIE_LE <= 0;
NIXIE_CLK <= 0;
end
START_1: begin
NIXIE_LE <= 0;
NIXIE_CLK <= 1;
end
SHIFT_0: begin
NIXIE_LE <= 0;
NIXIE_CLK <= 1;
dataCounter <= dataCounter + 1;
nixie_data <= {nixie_data[94:0], 1'b0};
end
SHIFT_1: begin
NIXIE_LE <= 0;
NIXIE_CLK <= 0;
end
END_WAIT: begin
NIXIE_LE <= 0;
NIXIE_CLK <= 0;
Done <= 1;
end
END: begin
NIXIE_LE <= 1;
NIXIE_CLK <= 0;
end
default: begin
end
endcase
end
end
always @(*) begin
nextState = currentState;
case(currentState)
IDLE: begin
if(fallingEdgePPS) begin
nextState = START_2;
end
end
START_2: begin
nextState = START_0;
end
START_0: begin
nextState = START_1;
end
START_1: begin
nextState = SHIFT_0;
end
SHIFT_0: begin
nextState = SHIFT_1;
end
SHIFT_1: begin
if(dataCounter == 15'd95)begin
nextState = END_WAIT;
end
else begin
nextState = SHIFT_0;
end
end
END_WAIT: begin
if(pps)begin
nextState = END;
end
else begin
nextState = END_WAIT;
end
end
END: begin
nextState = IDLE;
end
default: begin
nextState = IDLE;
end
endcase
end
endmodule
module binarytoNixiedigit(
input [3:0]x,
output [9:0]z
);
reg [9:0] z /* synthesis syn_romstyle = "select_rom" */;
always @*
case (x)
4'b0000 : //Hexadecimal 0
z = 10'b1000000000;
4'b0001 : //Hexadecimal 1
z = 10'b0000000001;
4'b0010 : // Hexadecimal 2
z = 10'b0000000010;
4'b0011 : // Hexadecimal 3
z = 10'b0000000100;
4'b0100 : // Hexadecimal 4
z = 10'b0000001000;
4'b0101 : // Hexadecimal 5
z = 10'b0000010000;
4'b0110 : // Hexadecimal 6
z = 10'b0000100000;
4'b0111 : // Hexadecimal 7
z = 10'b0001000000;
4'b1000 : //Hexadecimal 8
z = 10'b0010000000;
4'b1001 : //Hexadecimal 9
z = 10'b0100000000;
default: // Hexadecimal A
z = 10'b0000000000;
endcase
endmodule
其他的細節像是每小時random,半小時跑sequence掃一遍所有的數字就跟之前一樣了
為了避免Cathode Poisoning,需要掃一下每個Digit,我可能之後做成random最後結果是當時的市電頻率之類的XD