2021年12月24日 星期五

Mini Raspberry Pi Compute Module 4 Cluster

 

標題跟圖片好像就夠解釋了,總之這台是一個迷你型叢集電腦,一共有四組Raspberry Pi Compute module 4 (CM4) + Raspberry Pi 4 作為控制器

先上架構圖:


架構圖是有點過度複雜,因為實際上就是 Gigabit ethernet Hub + USB hub + Serial port/GPIO control.

先來講講這片Cluster Hat:

Gigabit ethernet switch用的是RTL8367N,主要是因為Raspberry Pi 擴充板的面積不是很大,沒有什麼空間可以放大顆QFP封裝的IC,我也不想弄成複雜的雙面電路板,這顆是5-port的剛好四個Node + 一個聯外。 
這種高密度的Cluster有一個比較麻煩的是網路PHY-PHY直接對接,因為其實這麼短的線路沒有必要放隔離變壓器佔空間,但是PHY直接對接就要看PHY是怎麼設計推動變壓器的,幸好這個只需要中間放一個.1uF的電容既可.


板子上有預留SPI Flash不過不確定能不能弄成managed switch,不過現在用起來也沒有感覺非要managed switch的必要。

CM4有一些可以被控制的GPIO,EN pin控制板子PMIC開關,BOOT控制USB開機(透過USB燒錄CM4),RUN控制SoC的Reset,RST監控SoC狀態,我稍微算了一下其實買I2C GPIO expender其實不是很划算,相比RP2040其實差不了多少所以......


RP2040除了有一堆GPIO,還有四個ADC pins所以我拿來測量個別CM4的耗電量,而RP2040可以透過UART或者是USB跟Raspberry Pi 溝通,而Raspberry Pi可以透過兩個GPIO模擬SWD上傳程式。這應該算是滿廢的用法我覺得,但是 1 USD + 1 USD的零件真的想不到為啥要買I2C GPIO expender。

USB 2.0 Hub是USB2517,主要是用來燒錄CM4以及一條除了ethernet以外直接跟Raspberry Pi 4控制版對接的方式。另外因為我找不到一個組合可以透過GPIO config出我要的,所以還是放了一個I2C eeprom放config。

除此之外板子上也有一些周邊,這種高密度的Cluster一定是需要風扇,所以拿了EMC2301來控制PWM 4cm 5v的風扇,另外因為有一堆DC-DC是直接吃Input供電,剛上電的時候因為一堆分散的電容在各處,Inrush 電流會很高,所以我拿了一個eFuse IC當作保險絲兼緩啟動(Soft-Start)。最後順便塞一個INA226監控輸入電流跟電壓。

風扇的連接器也要小心放,因為很容易就會碰到RPI4的連接器的外殼。
喔對了電源輸入跟網路孔之間還有一個Qwiic連接器在中間XD

擴充板上大概就是這樣,接下來介紹的是Node版:

Node版很簡單,就是把CM4的IO拉到一個M.2的連接器加上5V DC-DC 供電。拉到M.2的只有gigabit ethernet跟USB還有些許的GPIO跟I2C,還有一個很有意思的PCIe不想浪費,所以直接在Node版放了一個M.2 Connector讓我能夠接上NVMe SSD,也為了這個M.2 Connector 的供電所以板子上又放了一組DC-DC降壓。

另外一個就是板子上有一個TMP117溫度計在CM4的SoC正下方,透過散熱墊(?)跟CM4接觸,這個溫度計的I2C是直接接上控制的Raspberry Pi 4,所以風扇溫度控制並不需要Node透過其他方式回報溫度而是RPI4直接測量後控制。


當然最後就是Input供電的保護以及eFUSE,為了熱插拔所以放了eFUSE作爲緩啟動,另外也放了一個IC型的TSV突波保護。Camera Connector只是因為M.2 Connector有這個空間就順便放上去了。
這顆eFUSE也能順便幫我測量電流,所以Node的電流測量不需要另外裝電流檢測電阻跟相關的電路了。
左邊那顆是TVS,右邊是eFUSE
只能說晶片缺的另一個理由就是越來越多傳統元件被IC取代,好久沒有在電源輸入用到傳統的保險絲跟TVS Diode。


第二版想說省一點空間所以把M.2放到背面,另外轉個方向朝內,缺點就是沒辦法支援2280長度的裝置但是我找到一批便宜的2242 Optane 16G SSD所以就沒差了.


整組叢集電腦如果火力全開(CPU/跟網路壓力測試但是沒有接NVMe)的話大概可以飆到32W,放桌上冬天當手的暖氣還稍嫌不足就是。


以上。


2021年8月29日 星期日

Nixie Tube, Miniature Atomic Clock, Frequency measurement, Clock. - Part D (AC waveform measurement)

市電頻率一向是我很有興趣的一個觀測對象,理由是讀歷史資料的時候看到了兩種對於市電頻率衝突的描述。有一種說法是市電頻率非常的精確,很多工業用的計時基準跟同步裝置甚至是直接用了市電。另外一種則是指市電頻率可以觀測到當下供電端跟受電端平衡,當供給大於需求的時候頻率上升,反之下降。所以既然都入手了原子鐘要來做時間基準,當然是想辦法塞進監控AC頻率來看看到底頻率變化是長甚麼樣子。

測量AC頻率最基本的方式就是Zero-Crossing觸發,然後再透過外部的MCU/FPGA計算每次觸發之間的時間間隔來計算。Onsemi有一份App Note真的不錯,第一版的電路就是照這個做: https://www.onsemi.com/pub/Collateral/AND9282-D.PDF

之後把觸發訊號送到FPGA內的計數器這樣。不過第一版為了簡化供電跟安全考量,沒有直接市電而是透過一個AC變壓器轉成12VAC之後送進去,12VAC在內部轉DC之後降壓。所造成的問題也可想而知,因為原子鐘的耗電量不低,電流量大的狀況下AC整流那一塊的電容跟二極體發熱量不低。

這個是顯示AC的計數器,1/166421 * 10Mhz = 60.088Hz,殘影是因為頻率後面幾位數變化不低





第二版就把AC的訊號跟供電分開,簡化整流的部分,另外為了能夠在比較精確的方式監控Vrms,把第一版的LTC1966換成了直接用ADC測量波形後用軟體計算Vrms。ADC用的是ADS8681,因為SAR架構下可以直接吃正負電的訊號,我也不用另再用OPA處理進來的訊號,而且還有內部的PGA可以調放大倍率。ADC跟FPGA之間透過SPI通訊,採樣速度50Khz,觸發採樣的訊號也是原子鐘出來的,所以可以確保時間的精確度。資料透過RPI計算Vrms,頻率跟資料保存。

軟體很簡化的長這樣,其中比較重要的是Polling是先讀FPGA內FIFO的數量,然後把該數量的資料讀回來。 Polling thread特別讓他獨佔了一個Core,Linux的Schdeular只能用其他三個,然後寫入的部分開了一些Ram Disk的空間寫入,每分鐘定時再把資料搬到NVMe Drive上面。

Polling還有一個需要提到的是SPI在這種高密度的讀寫的時候,如果用的是spidev的介面,由於預設的狀況下系統很容易用到Interrupt Base的處理,這時候就會造成不可預期的時間delay。所以我在/boot/cmdline.txt加了這一行: spi_bcm2835.polling_limit_us=65535,這樣一來就可以確保系統在讀寫SPI的時候會是busy loop的方式等SPI結束。

ADC的校正則是用了一台Keysight 34410A,先校正ADC電壓測量,再來就是直接督插座之後兩邊比對AC Vrms資料


波形大概長這樣

另外多虧這次AC變成純訊號端,把供電接上UPS就可以抓到跳電/停電瞬間的波形:

其實真的很有趣,那時候跳電跳到停電之前就有感覺到燈光閃了一下,波形上就很明顯看到那一段,而且更有趣的是跳電瞬間之後電壓居然比之前高了一點,之後才整個停電。

拉近一點看第一次跳電的話,可以看到在100~200ms之內就幾乎沒電了。

不過直接用ADC採樣的話要抓精確的頻率就需要非常高的採樣頻率,這也是第二版最大的問題,ADC本身是可以跑到1M的,問題在於FPGA。即便是已經單獨給CPU/SPI用Polling,還是很容易掉幾筆資料。我用的FPGA是iCE5LP,算是滿小顆的FPGA,內部並沒有多少的ram block可以給我當FIFO用,而Linux下還是很常遇到數ms的中斷,導致FIFO overflow,加上為了標記每一筆資料的時間,每個Sample都是16bit的timestamp+16bit的ADC value,16bit的timestamp其實很尷尬,為了資料好處理,50Khz的話剛好每一秒timestamp可以歸0,這樣比較容易把資料跟真實時間擺在一起。

總結來說錯估了RPI的SPI通訊的能力跟FIFO的需求,所以沒有設計外部的DRAM只能靠內部太淺的FIFO。還好就是跟好這個FPGA的封裝可以換一個有內建比較大容量BRAM的高效能版本,可能之後rework FPGA替換掉之後再來看速度可以拉到多少。

至於頻率精確度,我大致上能理解為什麼會有兩種說法,問題在於看的是哪個時間平均,如果單純用來推時鐘,那長期下來<4ppm的時間誤差其實也比很多石英震盪器來的精確,但是短時間的誤差會飄很大,如果直接拿這來當PLL的時間基準是不行的。另外很有趣的是市電電壓跟需求量還滿有關的,需求越高,電壓越低(線阻?)

總結來說算是回答了我原本的疑問,接下來就是看有甚麼有趣的市電異常的現象發生了。

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做電流/電壓的監控。

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