2021年5月24日 星期一

Nixie Tube, Miniature Atomic Clock, Frequency measurement, Clock. - Part C (Nixie Clock Driving)

Nixie Tube的驅動方式這幾年來陸陸續續也是換了滿多種的做法,從剛開始用74141到74141 + 陽極掃描到灑一堆高壓電晶體,這次的設計先來上電路圖:

這次電路設計是因為空間的關係,直接用了HV5623的高壓位移暫存器,每一個HV5623可以接32個IO,所以八個真空管的10個位數x8 + 兩個小數點 x 8 剛好三個HV5623,然後每個HV5623都接在一個SPI Bus上。至於控制亮度則是有兩組,一個是高壓端透過PCA9685的PWM控制單獨每一個真空管,加上HV5623的全域開關控制亮度。

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

沒有留言:

張貼留言