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

2021年5月17日 星期一

Nixie Tube, Miniature Atomic Clock, Frequency measurement, Clock. - Part B (High Level System Arch)

 


上次的Part. A過了兩年......終於有機會能夠重新Revisit這個Project,主要是因為第一版受限於一些限制(後續說明),軟體上有點卡住。直到去年推出的Raspberry Pi Compute Module 4 解除了一些軟體上的限制,所以這次重新設計了底板以及系統架構。 現在軟體跟硬體大致底定,有空可以來寫幾篇。

這篇主要會先講的是整體系統的架構,大致上就是講這張Block Diagram:
綠色是FPGA內建的Hard IP,紫色是FPGA的Verilog Block,藍色是IC/Sensor,紅色是Power,灰色是Connector。

以功能來說,核心功能主要有以下幾個:
  • GPS校正系統時鐘,以及監控誤差跟時鐘基準輸出
  • AC 頻率以及Vrms監控
  • Nixie Clock
  • 環境監測
因為前三點都需要非常精準的Timing,所以設計上交給了FPGA,其他部分則交給Raspberry Pi Compute Module來做。FPGA跟Raspberry Pi中間透過兩個SPI,主要是因為我想拉高SPI的Clock,所以多了一組由FPGA內建的48MHz震盪器驅動的SPI Peripheral,所以你也可以看到FPGA內部有兩個Clock Domain,高速SPI是為了收ADC的資料,所以跨Domain有一個FIFO。

FPGA另外還會輸出兩個Time Signal,其中一個PPS接上一般的IO,另外一個Clock輸出是接上Raspberry Pi Compute Module 4上面的IEEE 1588 Sync In,當然中間經過了一個3.3v->1.8v的Buffer,等到比較正式的IEEE 1588 Support出來之後我就可以來搭配有線網路來當作我的內網路PTP Clock Source,因為FPGA的彈性,所以Clock要怎麼樣打可以等Support出來再來弄,先暫時只用RPI的PPS GPIO當作NTP的校正源。

其他的周邊則是直接接上Raspberry Pi,GPS以及Miniture Atomic Clock都是透過UART,這邊要提到的是Raspberry Pi 4支援非常多的UART Port,一次就支援五組UART可以用。前一版本的設計因為被Raspberry Pi Zero受限,所以MAC的UART需要透過FPGA幫忙轉SPI,徒增複雜度。

I2C的狀況也是類似,Raspberry Pi 4支援了非常多組的I2C,由架構圖可以看到我掛了非常多的東西在I2C Bus上,I2C Bus也有兩條,其中一條是接上上層的Nixie Tube的板子,另一條則是底板用的。

需要設定I2C和SPI Bus的話,透過dtoverlay既可,以下的範例是設定I2C 0跟I2C1 還有 SPI的CS pin,
    dtoverlay=i2c-gpio,bus=0,i2c_gpio_sda=22,i2c_gpio_scl=23
    dtoverlay=i2c-gpio,bus=1,i2c_gpio_sda=2,i2c_gpio_scl=3
    dtoverlay=spi0-2cs,cs0_pin=8,cs1_pin=20

Raspberry Pi 的 Image已經預載了這些dtoverlay,到/boot/overlay看有哪些之後,用這個指令查看怎麼設定
    dtoverlay -h [overlay名稱]

Power 的架構則是稍微複雜一點,主要是因為MAC在預熱過程至少需要3A,保險起見我想要拉到4A,加上因為是供應原子鐘,我不想要直接透過DC-DC供電。所以從上圖的架構圖可以看到MAC的供電是降壓到5.5V之後再送到LDO穩壓成5V,先透過DC-DC降壓的話LDO的消耗可以大幅地降低。另外還是有一組5V LDO是直接供電ADC用,因為耗電量不大,只要Noise夠低就好。接下來是CM4用的 5V,直接用DC-DC降壓,這組5V同時供應USB-C (Host Mode的話),還有再經過3.3V LDO降壓給底板用的Sensor們。最後就是Nixie Tube的Power Supply,我想做一些比較Fancy的控制,所以有一組AD/DA去控制跟監控Nixie的Voltage。供電的部分都有LED燈顯示狀況,也能夠用一個MOS把這些Power LED關掉。監控的部分則是有四組INA219做電流/電壓的監控。

基本上是這樣,接下來會針對每一個功能來寫比較詳細的文章。