---
# System prepended metadata

title: 用 Labview RT FIFO 完成寫檔功能
tags: [LabVIEW, RT-FIFO]

---

## 前言
做機器人實驗的時候，有時候滿常需要記錄下各種參數或是數據(關節指令、PID控制迴圈的Duty Cycle等等)，牽涉到不同更新時間的迴圈等等。最近在看學長Log Data的程式時，我發現他是在需要存數據的時候建立一個很大的RT FIFO然後在結束程式或是案件觸發的時候一次寫入csv檔案。但是這樣可能會有一些問題:

- 即時建立**連續的**記憶體可能會卡住，又或者沒辦法配置
    > 我覺得整個程式的生命週期應該要固定保留一塊作為存資料的緩衝區
- 好像有點浪費記憶體
    > 舊的程式開了一個size為$10^6$的RT FIFO然後每列含64個型態為double的數據(左右手的轉換矩陣、關節角度、目標指令等等)
    > 
    > 這樣花費的記憶體空間大概是$64\times8\text{byte}\times1000000=512\text{MB}$
    > 
    > 此外，這樣的寫法如果log data迴圈頻率為1kHz，最多只能記錄1000秒的數據，大約16分鐘
- 一次寫檔或許會花太久時間
- 如果中途發生斷電等等意外，沒有成功寫入檔案的數據會不見


## 什麼是 RT FIFO
![image](https://hackmd.io/_uploads/H1rgOvp5lg.png)
- **RT FIFO**：在建立時就一次性配置好**固定大小的連續記憶體**，運行過程中不會再動態分配。只能存放**固定長度**的數據型別（例如固定長度陣列、cluster、scalar），適合用來存放**高頻且連續的資料流**（如感測器數據、控制訊號）。
- **Queue**：與 RT FIFO 類似，也是常見的緩衝工具，但採用**動態記憶體配置**，運作上更有彈性，支援幾乎所有 LabVIEW 型別（字串、可變長陣列等）。在 LabVIEW RT 裡通常用於**不同迴圈之間的變數或訊息傳遞**，但不適合用在需要嚴格即時性的高頻資料流。


## 生產者消費者架構 (Producer–Consumer Structure)

在 LabVIEW RT 上，**Producer–Consumer** 是一種常見的設計模式，用來解決「即時資料擷取」和「非即時任務」之間的衝突。

### 核心概念
- **Producer Loop**
  - **高頻**迴圈，負責即時擷取或產生資料（例如 1 kHz 感測器讀值、馬達狀態）。 
  - 主要目標是 **準時**，不應該被寫檔、UI 更新這種慢速任務拖延。 
  - 只做一件事：把資料放進緩衝區（RT FIFO 或 Queue）。 

- **Consumer Loop** 
  - **低頻**迴圈，負責處理非即時的工作（寫檔、網路傳輸、顯示）。 
  - 從緩衝區取出資料，分批處理。 
  - 即使偶爾慢一點，Producer Loop 也不會受到影響。 

### 為什麼要這樣做？
- **磁碟 I/O 和網路 I/O 都不是即時的**，有時會突然卡住幾十毫秒以上。 
- 如果 **Producer 直接寫檔**，會導致取樣時間抖動甚至漏資料。 
- 使用 **FIFO/Queue 做中繼**，可以確保 Producer Loop 永遠準時完成工作。 

## Labview程式實作
主程式開始時先`create FIFO`，固定長度、固定型別。
Producer迴圈**並聯**Cosumer迴圈
> 如果把兩個迴圈串聯在一起，資料流會等第一個迴圈停止後才傳輸到第二個迴圈
### Producer端
這個迴圈就只是負責把要儲存的數據寫到到FIFO buffer裡面備用，理論上不會停止
![image](https://hackmd.io/_uploads/H1XOPJuoll.png)
> 上面迴圈執行頻率為1kHz

需注意的是Labview RT FIFO**預設是Lossy模式**，所以當資料滿過FIFO大小的時候，會自動把最舊的資料洗掉。好處是不用擔心記憶體被塞爆；但壞處是如果沒有注意緩衝是否滿溢的話可能會存到不連續的資料。
> [NI官網說明](https://www.ni.com/docs/zh-TW/bundle/lvrt-api-ref/page/menus/categories/real-time/rtfifo-mnu.html?srsltid=AfmBOoob4u0vRdGtjPj7tR_ltIX9LCUumcvVV1wuMCBljw-agKHVwRBr): Use the RT FIFO functions to send and receive data deterministically between VIs. RT FIFO functions provide a deterministic data transfer method that does not add jitter to a time-critical VI. An RT FIFO is a lossy form of communication that overwrites the oldest data element when the FIFO is full.

### Consumer端
和Producer平行執行，但是執行頻率比較低(我是設定20 ms)，主要負責把FIFO裡面的資料寫入到硬碟裡面。我用狀態機來完成這些行為。可以分為四個狀態，依序為IDLE、CLEAR FIFO、OPEN FILE、WRITE FILE、STOP WRITING。
#### IDLE
閒置狀態，維持初始狀態然後什麼都不做。當使用者按下`開始寫檔`的按鈕後就進到下一個狀態機
![image](https://hackmd.io/_uploads/rJPY00Dsxx.png)
> FIFO的那條線在迴圈上面的端點並不需要用shift register；因為其不是變數，是固定的記憶體位置

#### CLEAR FIFO
開始寫檔之前先把FIFO內的舊資料清理乾淨，labview的RT FIFO沒有清理的FIFO的函式，所以我就在這個狀態機裡面用一回圈來read fifo但是不做資料的收集；直到`ReadFIFO`中`empty?==True`時我才真正進到主要的開檔、寫檔狀態機
![image](https://hackmd.io/_uploads/H1E9R0Pslg.png)
> FIFO特性就是每讀完一次FIFO就會把讀完的該筆資料flush掉
#### OPEN FILE
開一個csv檔案，設定好header和檔名、路徑
![image](https://hackmd.io/_uploads/S1-j0Cwjle.png)

#### WRITE FILE
在`WRITE_FILE`狀態，我用一顆`Stop Writing`按鈕當離開條件；只要沒有停寫指令，就一直待在這個狀態處理資料。我在這個狀態裡面放一個內層迴圈負責從 RT FIFO 抽資料並寫檔。迴圈採用 batch 概念：每次累積到 batch_size 筆樣本就跳出來寫一次（如果 FIFO 先讀空，也會提前跳出來寫目前累積到的「部分批次」）。
![image](https://hackmd.io/_uploads/BJlnACDixx.png)
> 停止條件是`#elements <= 1`就跳出內迴圈，我試過用等於0來判斷，但是這樣收到的資料會穿插幾筆零的數據(明顯就是FIFO已經空了但還是記錄下來)

#### STOP WRITING
把一些按鈕歸位然後準備回到IDLE
![image](https://hackmd.io/_uploads/Byy60Rvole.png)

### Q&A
- 要該多大空間的FIFO呢?
    > 我抓5秒的時間，Producer以1000Hz速度執行的話這樣RT FIFO就是5000筆
    > 假設存的數據和原有的一樣，這樣的記憶體大小也才佔$64\times8\text{bytes}\times5000$
- 寫檔的速度和batch的大小要怎麼選擇呢?
    > 觀察batch大小造成FIFO裡面資料數量的影響，如果一直累積，可以適時加大batch number
