據(jù)流緩沖設(shè)計(jì):從乒乓操作到握手流控的本質(zhì)解析)
1. 從“乒乓”到“流水”一個(gè)被誤解的經(jīng)典設(shè)計(jì)模式如果你在FPGA開(kāi)發(fā)領(lǐng)域摸爬滾打了一段時(shí)間大概率聽(tīng)說(shuō)過(guò)“乒乓操作”這個(gè)詞。乍一聽(tīng)這名字挺有意思讓人聯(lián)想到兩個(gè)球拍來(lái)回?fù)舸颉T诩夹g(shù)圈里它常常被描述為一種利用兩塊存儲(chǔ)區(qū)比如兩塊RAM交替進(jìn)行數(shù)據(jù)存取以實(shí)現(xiàn)數(shù)據(jù)流無(wú)間斷處理的技術(shù)。聽(tīng)起來(lái)很美好對(duì)吧一個(gè)模塊寫A區(qū)時(shí)另一個(gè)模塊讀B區(qū)寫完讀完后角色互換周而復(fù)始仿佛永不停歇的流水線。但今天我想和你聊點(diǎn)不一樣的。在我經(jīng)手過(guò)的大大小小FPGA項(xiàng)目中我越來(lái)越覺(jué)得“乒乓操作”這個(gè)叫法本身可能就是一個(gè)巨大的“望文生義”的誤會(huì)或者說(shuō)它過(guò)度簡(jiǎn)化并誤導(dǎo)了我們對(duì)一種更通用、更強(qiáng)大設(shè)計(jì)模式的理解。我們真正在做的往往不是簡(jiǎn)單的“乒乓”而是一種基于數(shù)據(jù)流控制的“生產(chǎn)者-消費(fèi)者”模型其核心在于握手Handshake與流控Flow Control存儲(chǔ)區(qū)的切換只是其實(shí)現(xiàn)表象之一。為什么這么說(shuō)因?yàn)椤捌古摇边@個(gè)詞太容易讓人把注意力全部放在“兩塊緩沖區(qū)交替使用”這個(gè)具體實(shí)現(xiàn)細(xì)節(jié)上。新手工程師可能會(huì)執(zhí)著于如何精巧地設(shè)計(jì)一個(gè)狀態(tài)機(jī)來(lái)切換讀寫指針卻忽略了最本質(zhì)的問(wèn)題數(shù)據(jù)從哪里來(lái)到哪里去速率是否匹配遇到背壓Backpressure怎么辦當(dāng)數(shù)據(jù)流不是理想的雙緩沖就能解決時(shí)比如生產(chǎn)者突發(fā)、消費(fèi)者卡頓、或者需要多級(jí)處理時(shí)單純的“乒乓”思維就會(huì)捉襟見(jiàn)肘。這篇文章我將拋開(kāi)對(duì)“乒乓操作”這個(gè)名詞的刻板印象帶你深入其本質(zhì)。我們會(huì)探討這種設(shè)計(jì)模式解決的核心矛盾數(shù)據(jù)生產(chǎn)與消費(fèi)的速率失配與時(shí)機(jī)錯(cuò)位構(gòu)建其通用的理論模型基于Valid-Ready握手協(xié)議并看看它如何演變成各種實(shí)際形態(tài)從最簡(jiǎn)單的雙緩沖到復(fù)雜的多級(jí)流水線與異步FIFO。最后我會(huì)分享幾個(gè)真實(shí)的項(xiàng)目案例看看當(dāng)“乒乓”思維失效時(shí)我們是如何用更底層的流控思想來(lái)解決問(wèn)題的。無(wú)論你是正在學(xué)習(xí)FPGA的在校生還是已經(jīng)有一定經(jīng)驗(yàn)的工程師相信這種視角的轉(zhuǎn)換都能讓你對(duì)數(shù)據(jù)流處理有更深刻的認(rèn)識(shí)。2. 解構(gòu)核心矛盾為什么我們需要“緩沖”與“切換”在深入代碼之前我們必須先弄清楚我們要對(duì)付的“敵人”是什么。在信號(hào)處理、圖像處理、網(wǎng)絡(luò)數(shù)據(jù)包處理等幾乎所有FPGA應(yīng)用場(chǎng)景中數(shù)據(jù)通常以“流”Stream的形式存在。一個(gè)典型的處理鏈路可以抽象為數(shù)據(jù)源 - 處理模塊A - 處理模塊B - ... - 數(shù)據(jù)輸出。理想情況下每個(gè)模塊的處理速度都完美匹配數(shù)據(jù)像水流過(guò)光滑管道一樣毫無(wú)阻滯。但現(xiàn)實(shí)是骨感的主要矛盾體現(xiàn)在以下幾個(gè)方面2.1 處理時(shí)鐘域與速率失配這是最經(jīng)典的場(chǎng)景。假設(shè)數(shù)據(jù)來(lái)自一個(gè)ADC模數(shù)轉(zhuǎn)換器其輸出速率是100MHz。而后續(xù)的復(fù)雜算法模塊比如一個(gè)圖像濾波器因?yàn)檫壿嬞Y源或時(shí)序限制最高只能工作在50MHz。這時(shí)ADC作為“生產(chǎn)者”速度是消費(fèi)者的兩倍。如果沒(méi)有緩沖機(jī)制每?jī)蓚€(gè)ADC時(shí)鐘周期產(chǎn)生的數(shù)據(jù)就會(huì)有一個(gè)被丟棄如果簡(jiǎn)單用使能信號(hào)選擇的話導(dǎo)致數(shù)據(jù)丟失。反之亦然如果生產(chǎn)者慢消費(fèi)者快消費(fèi)者就會(huì)經(jīng)常“餓死”無(wú)所事事降低系統(tǒng)平均吞吐率。“乒乓操作”中兩塊緩沖區(qū)的作用本質(zhì)上就是提供一個(gè)彈性的“數(shù)據(jù)池”來(lái)吸收這種瞬時(shí)速率差。當(dāng)生產(chǎn)者爆發(fā)時(shí)數(shù)據(jù)可以暫存在一個(gè)緩沖區(qū)里等待消費(fèi)者慢慢處理當(dāng)生產(chǎn)者休息時(shí)消費(fèi)者可以消費(fèi)另一個(gè)緩沖區(qū)里早已準(zhǔn)備好的數(shù)據(jù)。2.2 突發(fā)Burst數(shù)據(jù)傳輸很多接口協(xié)議或數(shù)據(jù)源并非勻速產(chǎn)生數(shù)據(jù)。例如通過(guò)DMA從外部DDR內(nèi)存讀取一幅圖像的數(shù)據(jù)它可能以極高的總線帶寬在幾十個(gè)時(shí)鐘周期內(nèi)突發(fā)傳輸完一行像素然后等待下一行地址計(jì)算中間有很長(zhǎng)的空閑期。如果后續(xù)處理模塊是勻速的就必須有一個(gè)足夠大的緩沖區(qū)來(lái)吞下整個(gè)突發(fā)數(shù)據(jù)包再勻速吐出。雙緩沖乒乓在這里可以看作一個(gè)最小規(guī)模的緩沖單元但如果突發(fā)長(zhǎng)度超過(guò)一個(gè)緩沖區(qū)的容量就需要更深或更寬的緩沖隊(duì)列。2.3 模塊處理延時(shí)的不確定性有些模塊的處理延時(shí)不是固定的。例如一個(gè)視頻編解碼模塊處理一幀數(shù)據(jù)的時(shí)間可能因畫面復(fù)雜度而異。如果前級(jí)模塊以固定幀率輸出后級(jí)模塊處理時(shí)間波動(dòng)就必須在中間設(shè)置緩沖否則會(huì)導(dǎo)致幀率不穩(wěn)定甚至丟幀。緩沖區(qū)的存在實(shí)現(xiàn)了時(shí)序解耦讓每個(gè)模塊可以按照自己的節(jié)奏工作只要長(zhǎng)期來(lái)看平均速率匹配即可。2.4 跨時(shí)鐘域CDC需求當(dāng)生產(chǎn)者和消費(fèi)者處于不同的時(shí)鐘域時(shí)問(wèn)題更加復(fù)雜。不僅速率可能不同連時(shí)鐘相位都毫無(wú)關(guān)系。直接傳遞數(shù)據(jù)和握手信號(hào)會(huì)導(dǎo)致亞穩(wěn)態(tài)Metastability系統(tǒng)崩潰。此時(shí)異步FIFOFirst In, First Out成為了實(shí)現(xiàn)“乒乓”思想的終極形態(tài)。它內(nèi)部本質(zhì)上也是用雙端口RAM作為存儲(chǔ)介質(zhì)通過(guò)精妙的讀寫指針同步邏輯安全地在兩個(gè)時(shí)鐘域之間傳遞數(shù)據(jù)和流控狀態(tài)。你可以把異步FIFO理解為一個(gè)自帶安全握手協(xié)議、且緩沖區(qū)深度可靈活配置的“超級(jí)乒乓”模塊。所以當(dāng)我們說(shuō)“要實(shí)現(xiàn)乒乓操作”時(shí)我們真正要解決的問(wèn)題是如何設(shè)計(jì)一個(gè)安全、高效、吞吐率高的緩沖機(jī)制來(lái)平滑數(shù)據(jù)流中生產(chǎn)與消費(fèi)之間的各種時(shí)序和速率矛盾。雙緩沖是解決方案的一種簡(jiǎn)單特例而握手協(xié)議是確保該方案正確工作的通信基礎(chǔ)。3. 握手協(xié)議一切流控的基石理解了問(wèn)題我們來(lái)看解決方案的核心——握手協(xié)議。這是模塊間進(jìn)行數(shù)據(jù)交換的“語(yǔ)言”。在FPGA設(shè)計(jì)中最常用、最推薦的是Valid-Ready握手協(xié)議。它清晰、簡(jiǎn)潔、易于組合是構(gòu)建復(fù)雜數(shù)據(jù)流系統(tǒng)的樂(lè)高積木。它的規(guī)則非常簡(jiǎn)單valid由生產(chǎn)者上游模塊驅(qū)動(dòng)。當(dāng)valid1時(shí)表示當(dāng)前時(shí)鐘周期data總線上的數(shù)據(jù)是有效且穩(wěn)定的可供消費(fèi)。ready由消費(fèi)者下游模塊驅(qū)動(dòng)。當(dāng)ready1時(shí)表示消費(fèi)者在當(dāng)前時(shí)鐘周期能夠接收數(shù)據(jù)。數(shù)據(jù)傳輸成功發(fā)生在同一個(gè)時(shí)鐘周期內(nèi)當(dāng)且僅當(dāng)valid1且ready1。此時(shí)數(shù)據(jù)從生產(chǎn)者傳遞到消費(fèi)者。這個(gè)協(xié)議的美妙之處在于其對(duì)稱性和流控能力。消費(fèi)者可以通過(guò)拉低ready來(lái)告訴生產(chǎn)者“我忙不過(guò)來(lái)了請(qǐng)暫停發(fā)送。” 這就是“背壓”Backpressure它使得速率較慢的模塊可以反向控制上游避免數(shù)據(jù)丟失或緩沖區(qū)溢出。讓我們用這個(gè)協(xié)議來(lái)重新定義“乒乓操作”的核心。假設(shè)我們有一個(gè)“寫控制器”生產(chǎn)者和一個(gè)“讀控制器”消費(fèi)者以及一塊被分成Bank0和Bank1的雙端口RAM。初始狀態(tài)寫控制器向Bank0寫入讀控制器從Bank1讀取假設(shè)Bank1有初始數(shù)據(jù)或?yàn)榭铡戇^(guò)程寫控制器的valid信號(hào)持續(xù)有效直到寫滿Bank0。它只關(guān)心自己能否寫通常一直能寫但真正的“寫使能”是由它自身的valid和下游緩沖區(qū)的“接收能力”共同決定的。在這個(gè)場(chǎng)景里“下游緩沖區(qū)”就是Bank0其“接收能力”可以抽象為一個(gè)狀態(tài)是否已滿。切換條件當(dāng)Bank0被寫滿或達(dá)到預(yù)定閾值時(shí)寫控制器需要通知讀控制器“Bank0準(zhǔn)備好了你可以來(lái)讀了接下來(lái)我要去寫B(tài)ank1了”。同時(shí)讀控制器在讀完Bank1后也需要通知寫控制器“Bank1讀空了你可以寫入了接下來(lái)我要去讀Bank0了”。握手實(shí)現(xiàn)切換你看這里的“通知”本質(zhì)上就是一次握手我們可以定義兩個(gè)簡(jiǎn)單的握手信號(hào)bank0_done由寫控制器產(chǎn)生 (valid)表示Bank0寫入完成且數(shù)據(jù)就緒。讀控制器需要確認(rèn) (ready) 這個(gè)通知并開(kāi)始讀取Bank0。bank1_empty由讀控制器產(chǎn)生 (valid)表示Bank1讀取完成且空間就緒。寫控制器需要確認(rèn) (ready) 這個(gè)通知并開(kāi)始寫入Bank1。這個(gè)過(guò)程完全可以用Valid-Ready握手來(lái)建模。“乒乓”的切換就是一次嚴(yán)格的數(shù)據(jù)握手只不過(guò)這次“傳輸?shù)臄?shù)據(jù)”是“緩沖區(qū)控制權(quán)”。理解了這一點(diǎn)你就掌握了從具體實(shí)現(xiàn)雙緩沖抽象到通用模型握手流控的關(guān)鍵。4. 從理論到實(shí)現(xiàn)雙緩沖RAM的Verilog設(shè)計(jì)要點(diǎn)現(xiàn)在我們動(dòng)手用一個(gè)簡(jiǎn)化的例子來(lái)實(shí)現(xiàn)一個(gè)基于握手協(xié)議的雙緩沖機(jī)制。為了突出重點(diǎn)我們假設(shè)數(shù)據(jù)生產(chǎn)者和消費(fèi)者在同一時(shí)鐘域且生產(chǎn)者速率略快于消費(fèi)者。我們將設(shè)計(jì)三個(gè)主要部分雙端口RAM模塊用于存儲(chǔ)數(shù)據(jù)。我們將它實(shí)例化兩次或者用一個(gè)RAM但邏輯上劃分為兩個(gè)Bank。緩沖區(qū)狀態(tài)機(jī)Buffer Controller核心中的核心。它管理兩個(gè)緩沖區(qū)的讀寫狀態(tài)和切換邏輯。對(duì)外握手接口與上下游模塊連接。4.1 模塊接口定義首先定義頂層模塊的接口。這里我們清晰地分離了“寫側(cè)”生產(chǎn)者接口和“讀側(cè)”消費(fèi)者接口。module ping_pong_buffer #( parameter DATA_WIDTH 32, parameter BUFFER_DEPTH 512, // 每個(gè)緩沖區(qū)的深度 parameter ADDR_WIDTH $clog2(BUFFER_DEPTH) )( // 全局信號(hào) input wire clk, input wire rst_n, // 寫側(cè)接口生產(chǎn)者 - 緩沖區(qū) input wire [DATA_WIDTH-1:0] wdata_i, input wire wvalid_i, // 生產(chǎn)者數(shù)據(jù)有效 output wire wready_o, // 緩沖區(qū)是否可寫 // 讀側(cè)接口緩沖區(qū) - 消費(fèi)者 output wire [DATA_WIDTH-1:0] rdata_o, output wire rvalid_o, // 緩沖區(qū)數(shù)據(jù)有效 input wire rready_i // 消費(fèi)者是否可讀 );4.2 緩沖區(qū)狀態(tài)機(jī)設(shè)計(jì)這是設(shè)計(jì)的精髓。狀態(tài)機(jī)需要跟蹤當(dāng)前正在被寫入的緩沖區(qū)write_bank。當(dāng)前正在被讀取的緩沖區(qū)read_bank。每個(gè)緩沖區(qū)的寫指針和讀指針。每個(gè)緩沖區(qū)的狀態(tài)空、正在填充、滿、正在清空。一個(gè)清晰的狀態(tài)定義有助于設(shè)計(jì)localparam S_IDLE 2b00; // 初始狀態(tài)緩沖區(qū)空 localparam S_WRITE_ACTIVE 2b01; // 正在向當(dāng)前寫緩沖區(qū)寫入 localparam S_FULL 2b10; // 當(dāng)前寫緩沖區(qū)已滿等待切換 // 讀側(cè)也有類似狀態(tài)但通常與寫側(cè)狀態(tài)耦合或獨(dú)立管理 reg [1:0] write_state; reg [1:0] read_state; reg write_bank; // 0 for Bank0, 1 for Bank1 reg read_bank; reg [ADDR_WIDTH:0] write_addr[0:1]; // 使用位寬多一位來(lái)區(qū)分滿/空FIFO風(fēng)格 reg [ADDR_WIDTH:0] read_addr[0:1];切換邏輯的設(shè)計(jì)要點(diǎn)切換不是隨意發(fā)生的必須滿足嚴(yán)格的條件以避免數(shù)據(jù)覆蓋或讀空。一個(gè)穩(wěn)健的策略是寫緩沖區(qū)切換條件當(dāng)wvalid_i wready_o成功寫入最后一個(gè)數(shù)據(jù)使當(dāng)前寫緩沖區(qū)達(dá)到滿狀態(tài)時(shí)寫控制器拉高bank_full信號(hào)。但是不能立即切換必須等待讀控制器確認(rèn)它已經(jīng)完全離開(kāi)了即將被寫入的另一個(gè)緩沖區(qū)即read_bank不等于將要切換到的next_write_bank并且該緩沖區(qū)處于“空”或“可寫”狀態(tài)。這本質(zhì)上是一個(gè)握手。讀緩沖區(qū)切換條件當(dāng)rvalid_o rready_i成功讀出最后一個(gè)數(shù)據(jù)使當(dāng)前讀緩沖區(qū)變空時(shí)讀控制器拉高bank_empty信號(hào)。同樣它必須等待寫控制器確認(rèn)已經(jīng)寫完了即將被讀取的緩沖區(qū)即write_bank不等于將要切換到的next_read_bank且該緩沖區(qū)處于“滿”或“數(shù)據(jù)就緒”狀態(tài)。在實(shí)際編碼中我更喜歡使用一種“目標(biāo)緩沖區(qū)預(yù)分配”和“完成信號(hào)握手”的方法// 示例代碼片段寫側(cè)切換邏輯 reg next_bank_ready; // 表示另一個(gè)緩沖區(qū)是否準(zhǔn)備好被寫入 wire write_bank_full (write_addr[write_bank] BUFFER_DEPTH); // 簡(jiǎn)化判斷 always (posedge clk or negedge rst_n) begin if (!rst_n) begin write_bank 0; // ... 其他初始化 end else begin // 檢查當(dāng)前寫銀行是否已滿且下一個(gè)銀行已準(zhǔn)備好即已被讀完 if (write_bank_full next_bank_ready) begin write_bank ~write_bank; // 切換銀行 // 復(fù)位新銀行的寫指針 write_addr[~write_bank] 0; end // 正常的寫地址遞增邏輯 if (wvalid_i wready_o) begin write_addr[write_bank] write_addr[write_bank] 1; end end end // next_bank_ready 信號(hào)需要從讀側(cè)狀態(tài)獲取 // 例如當(dāng)讀側(cè)不在使用下一個(gè)銀行且下一個(gè)銀行的讀指針等于寫指針表示空時(shí)next_bank_ready1 assign next_bank_ready (read_bank ! ~write_bank) (read_addr[~write_bank] write_addr[~write_bank]);注意上面的判斷read_addr[~write_bank] write_addr[~write_bank]在指針位寬一致時(shí)才能判斷空滿更嚴(yán)謹(jǐn)?shù)淖龇ㄊ窍癞惒紽IFO一樣使用格雷碼指針并比較最高位。這里為簡(jiǎn)化示意。4.3 握手信號(hào)的生成wready_o和rvalid_o需要根據(jù)緩沖區(qū)狀態(tài)實(shí)時(shí)產(chǎn)生。wready_o當(dāng)當(dāng)前寫緩沖區(qū)非滿時(shí)可以拉高。但更精細(xì)的控制是如果當(dāng)前緩沖區(qū)快滿了而下一個(gè)緩沖區(qū)還沒(méi)準(zhǔn)備好可以提前拉低wready_o進(jìn)行反壓給切換留出時(shí)間。rvalid_o當(dāng)當(dāng)前讀緩沖區(qū)非空時(shí)拉高。// 寫就緒信號(hào)當(dāng)前緩沖區(qū)未滿即可接收數(shù)據(jù)。如果即將滿且切換未就緒可提前降速。 assign wready_o (write_addr[write_bank] BUFFER_DEPTH) (write_bank_full ? next_bank_ready : 1b1); // 讀有效信號(hào)當(dāng)前緩沖區(qū)有數(shù)據(jù)可讀 wire read_bank_empty (read_addr[read_bank] write_addr[read_bank]); // 簡(jiǎn)化空判斷 assign rvalid_o !read_bank_empty;4.4 RAM的讀寫控制根據(jù)write_bank和read_bank選擇對(duì)應(yīng)的RAM地址進(jìn)行讀寫。注意地址是每個(gè)緩沖區(qū)獨(dú)立的。// 假設(shè)使用兩個(gè)獨(dú)立的單端口RAM實(shí)例 ram bank0 ( .clk(clk), .wea(wvalid_i wready_o (write_bank0)), .addra(write_addr[0][ADDR_WIDTH-1:0]), // 取低位作為RAM地址 .dina(wdata_i), .addrb(read_addr[0][ADDR_WIDTH-1:0]), .doutb(rdata_bank0) ); ram bank1 ( // ... 類似wea條件為 (write_bank1) // ... addra 為 write_addr[1] // ... addrb 為 read_addr[1] // ... doutb 為 rdata_bank1 ); // 根據(jù)當(dāng)前讀銀行選擇輸出數(shù)據(jù) assign rdata_o (read_bank 0) ? rdata_bank0 : rdata_bank1;這個(gè)設(shè)計(jì)框架提供了一個(gè)起點(diǎn)。在實(shí)際項(xiàng)目中你需要根據(jù)數(shù)據(jù)位寬、緩沖區(qū)深度、上下游模塊特性進(jìn)行大量調(diào)整和優(yōu)化例如添加“幾乎滿/幾乎空”閾值來(lái)提前觸發(fā)切換優(yōu)化時(shí)序等。5. 超越雙緩沖流控思想的進(jìn)階形態(tài)一旦掌握了握手協(xié)議和緩沖管理的核心思想你就會(huì)發(fā)現(xiàn)“雙緩沖”只是冰山一角。在很多復(fù)雜場(chǎng)景下我們需要更靈活的結(jié)構(gòu)。5.1 異步FIFO跨時(shí)鐘域的“自動(dòng)乒乓”當(dāng)生產(chǎn)者和消費(fèi)者時(shí)鐘不同源時(shí)雙緩沖的直接切換會(huì)因亞穩(wěn)態(tài)而失敗。異步FIFO應(yīng)運(yùn)而生。你可以把它想象成一個(gè)黑盒輸入側(cè)wdata, wvalid, wready工作在寫時(shí)鐘域。輸出側(cè)rdata, rvalid, rready工作在讀時(shí)鐘域。內(nèi)部它包含一個(gè)雙端口RAM或寄存器堆以及兩套分別同步到對(duì)方時(shí)鐘域的讀寫指針格雷碼。工作原理寫邏輯根據(jù)同步后的讀指針判斷是否有空間讀邏輯根據(jù)同步后的寫指針判斷是否有數(shù)據(jù)。其內(nèi)部的空滿判斷邏輯完美實(shí)現(xiàn)了跨時(shí)鐘域的安全緩沖和流控。使用一個(gè)深度合適的異步FIFO可以替代絕大多數(shù)手動(dòng)實(shí)現(xiàn)的、需要跨時(shí)鐘域的“乒乓操作”且更安全、更省心。5.2 多級(jí)流水線與彈性緩沖區(qū)在圖像處理管線中可能連續(xù)有多個(gè)處理模塊去噪、縮放、色彩轉(zhuǎn)換。每個(gè)模塊處理延時(shí)不同。簡(jiǎn)單的雙緩沖 between each stage 會(huì)非常低效因?yàn)榭赡芮凹?jí)輸出很慢后級(jí)早已讀完緩沖區(qū)在空等。更好的模式是構(gòu)建一個(gè)多級(jí)流水線并在每?jī)杉?jí)之間插入一個(gè)深度可調(diào)的FIFO作為彈性緩沖區(qū)。每個(gè)FIFO的深度可以根據(jù)前后級(jí)模塊的處理延時(shí)差和數(shù)據(jù)突發(fā)長(zhǎng)度來(lái)估算。這樣整個(gè)管線就像一個(gè)“彈簧鏈”能夠吸收各環(huán)節(jié)的波動(dòng)實(shí)現(xiàn)更高的吞吐率和更低的整體延時(shí)。這本質(zhì)上是將“雙緩沖”推廣為“多緩沖隊(duì)列”。5.3 基于AXI4-Stream的生態(tài)系統(tǒng)在現(xiàn)代FPGA設(shè)計(jì)中特別是使用Xilinx或Intel的高端器件和IP時(shí)AXI4-Stream協(xié)議已經(jīng)成為數(shù)據(jù)流交互的事實(shí)標(biāo)準(zhǔn)。它的核心正是TVALID和TREADY握手信號(hào)以及TDATA,TLAST等。Xilinx的Vivado IP集成器、Intel的Platform Designer都圍繞AXI4-Stream構(gòu)建了豐富的IP庫(kù)。當(dāng)你使用一個(gè)DMA IP、一個(gè)Video Frame Buffer IP、一個(gè)圖像處理加速IP時(shí)它們之間通過(guò)AXI4-Stream互聯(lián)。你幾乎不再需要手動(dòng)編寫底層的“乒乓操作”RTL代碼。你需要做的是正確配置每個(gè)IP的流接口。理解并處理TREADY反壓。例如當(dāng)顯示控制器因?yàn)榇怪毕[期間不能接收數(shù)據(jù)時(shí)它會(huì)拉低TREADY反壓會(huì)一直傳遞到DMA使其暫停數(shù)據(jù)傳輸。利用TLAST信號(hào)來(lái)標(biāo)識(shí)數(shù)據(jù)包的邊界如一行的結(jié)束、一幀的結(jié)束這對(duì)于緩沖區(qū)切換“乒乓”是關(guān)鍵信號(hào)。很多IP在檢測(cè)到TLAST且握手成功后會(huì)內(nèi)部完成一次緩沖區(qū)切換或狀態(tài)重置。在這種情況下你的設(shè)計(jì)重心從“如何實(shí)現(xiàn)乒乓”轉(zhuǎn)移到了“如何配置和連接IP以構(gòu)建正確的流處理管道”以及“如何確定合適的FIFO深度來(lái)保證性能”。6. 實(shí)戰(zhàn)踩坑當(dāng)“理想乒乓”遇上“現(xiàn)實(shí)骨感”理論很完美但實(shí)際項(xiàng)目總會(huì)給你“驚喜”。分享兩個(gè)我印象深刻的案例案例一圖像Sensor數(shù)據(jù)接收中的“縫隙”問(wèn)題在一個(gè)攝像頭項(xiàng)目中Sensor通過(guò)MIPI接口輸出圖像數(shù)據(jù)經(jīng)過(guò)解串、解包后得到像素流。我們?cè)O(shè)計(jì)了一個(gè)雙緩沖模塊期望在收到一幀結(jié)束信號(hào)VSYNC后切換緩沖區(qū)將完整的一幀送給后續(xù)的ISP處理。坑點(diǎn)Sensor輸出的幀與幀之間存在幾十到幾百個(gè)時(shí)鐘周期的消隱期Blanking。我們的設(shè)計(jì)是在VSYNC上升沿立即切換寫緩沖區(qū)。結(jié)果發(fā)現(xiàn)偶爾會(huì)有一幀的最后幾行數(shù)據(jù)和下一幀的開(kāi)頭幾行數(shù)據(jù)被錯(cuò)誤地混在同一個(gè)緩沖區(qū)里。根因分析VSYNC信號(hào)到來(lái)時(shí)最后一個(gè)像素?cái)?shù)據(jù)可能還在流水線上沒(méi)有完全寫入當(dāng)前緩沖區(qū)。立即切換導(dǎo)致這部分“在路上”的數(shù)據(jù)被寫入了新的緩沖區(qū)。解決方案引入一個(gè)“幀有效窗口”信號(hào)frame_active在VSYSYNC之后、且第一個(gè)有效像素到來(lái)之前拉高在最后一個(gè)有效像素寫入后、下一個(gè)VSYNC之前拉低。緩沖區(qū)切換的觸發(fā)條件從單純的VSYNC邊沿改為frame_active的下降沿。這確保了所有屬于當(dāng)前幀的數(shù)據(jù)都已安穩(wěn)落袋再進(jìn)行切換。這其實(shí)就是握手思想的體現(xiàn)等待“數(shù)據(jù)寫入完成”這個(gè)動(dòng)作真正結(jié)束。案例二與帶猝發(fā)功能的DMA協(xié)同工作另一個(gè)項(xiàng)目我們需要將處理好的數(shù)據(jù)通過(guò)AXI總線用DMA寫入DDR。DMA控制器支持突發(fā)Burst傳輸一次突發(fā)傳輸256個(gè)數(shù)據(jù)。我們用了雙緩沖每個(gè)緩沖區(qū)剛好256深度。坑點(diǎn)當(dāng)寫控制器填滿緩沖區(qū)A后發(fā)出切換信號(hào)啟動(dòng)DMA讀取緩沖區(qū)A。同時(shí)寫控制器開(kāi)始向緩沖區(qū)B寫入。問(wèn)題來(lái)了DMA的突發(fā)讀需要時(shí)間比如幾十個(gè)時(shí)鐘周期。在這段時(shí)間里寫控制器可能已經(jīng)把緩沖區(qū)B寫滿了一小部分。當(dāng)DMA讀完A準(zhǔn)備切換讀B時(shí)它讀到的B的數(shù)據(jù)開(kāi)頭部分其實(shí)是B中較舊的數(shù)據(jù)因?yàn)閷懼羔樣謴?開(kāi)始了這導(dǎo)致了數(shù)據(jù)錯(cuò)位。根因分析這是典型的“讀寫指針復(fù)位不同步”問(wèn)題。雙緩沖切換時(shí)不僅要把讀寫目標(biāo)緩沖區(qū)互換還必須將新激活的寫緩沖區(qū)的寫指針復(fù)位到0同時(shí)將新激活的讀緩沖區(qū)的讀指針復(fù)位到0。在我們的錯(cuò)誤設(shè)計(jì)中讀緩沖區(qū)切換后讀指針沒(méi)有復(fù)位而是接著上一次的位置讀而寫指針已經(jīng)復(fù)位并從0開(kāi)始寫造成了讀寫區(qū)域的錯(cuò)配。解決方案在切換邏輯中嚴(yán)格同步指針復(fù)位。寫側(cè)切換時(shí)write_bank ~write_bank; write_addr[~write_bank] 0;(復(fù)位新寫緩沖區(qū)的指針)讀側(cè)切換時(shí)read_bank ~read_bank; read_addr[~read_bank] 0;(復(fù)位新讀緩沖區(qū)的指針) 并且必須確保讀側(cè)切換發(fā)生在DMA開(kāi)始讀取新緩沖區(qū)之前而不是之后。這通常需要DMA控制器提供一個(gè)“傳輸開(kāi)始”或“緩沖區(qū)鎖定”的信號(hào)來(lái)進(jìn)行握手。這些坑讓我深刻認(rèn)識(shí)到“乒乓操作”絕非兩個(gè)計(jì)數(shù)器交替那么簡(jiǎn)單。它是一套關(guān)于狀態(tài)同步、指針管理和握手時(shí)序的精密舞蹈。任何一個(gè)環(huán)節(jié)的時(shí)序錯(cuò)誤都會(huì)導(dǎo)致數(shù)據(jù)混亂。最好的調(diào)試方法是在仿真中仔細(xì)繪制時(shí)序圖觀察每一個(gè)切換點(diǎn)前后讀寫指針、緩沖區(qū)選擇信號(hào)以及實(shí)際RAM地址的變化確保其符合預(yù)期。7. 設(shè)計(jì)權(quán)衡與替代方案選擇所以當(dāng)你面臨一個(gè)需要數(shù)據(jù)緩沖的場(chǎng)景時(shí)不要條件反射地就去寫“乒乓操作”。先做一番分析時(shí)鐘域是否相同不同首選異步FIFO。使用FPGA廠商提供的IP如Xilinx的FIFO Generator或經(jīng)過(guò)驗(yàn)證的成熟代碼。自己寫一個(gè)健壯的異步FIFO難度很高。相同繼續(xù)往下分析。數(shù)據(jù)流是連續(xù)的還是突發(fā)的緩沖深度需求多大連續(xù)流深度需求小幾十到幾百雙緩沖或同步FIFO是簡(jiǎn)單有效的選擇。雙緩沖的優(yōu)勢(shì)是控制邏輯完全透明易于集成特定邏輯如滿一幀才切換同步FIFOIP或RTL優(yōu)勢(shì)是接口標(biāo)準(zhǔn)Valid/Ready易于連接。突發(fā)流或深度需求大幾千以上使用基于Block RAM的大深度FIFO。雙緩沖需要兩塊同樣大的RAM可能不經(jīng)濟(jì)。大深度FIFO可以更好地平滑突發(fā)。是否需要復(fù)雜的包邊界管理如果數(shù)據(jù)以清晰的“包”如圖像幀、網(wǎng)絡(luò)數(shù)據(jù)包為單位并且處理邏輯需要以“包”為界如一幀處理完才能輸出那么帶包邊界指示的雙緩沖或Frame Buffer更合適。你可以在TLAST信號(hào)有效時(shí)觸發(fā)內(nèi)部狀態(tài)切換。如果數(shù)據(jù)是無(wú)限流無(wú)明確邊界則FIFO更合適。系統(tǒng)集成復(fù)雜度如何如果整個(gè)數(shù)據(jù)鏈路上大量使用AXI4-Stream IP那么統(tǒng)一使用AXI4-Stream接口的FIFO IP是最佳選擇可以無(wú)縫接入利用工具自動(dòng)處理時(shí)序和面積優(yōu)化。如果是一個(gè)小而美的定制模塊自己寫一個(gè)輕量級(jí)的雙緩沖或同步FIFO可能更直接。一個(gè)簡(jiǎn)單的決策樹(shù)跨時(shí)鐘域- 用異步FIFO IP。同時(shí)鐘域數(shù)據(jù)包邊界重要且包長(zhǎng)度固定/可預(yù)測(cè)- 考慮用帶顯式切換控制的雙緩沖。同時(shí)鐘域數(shù)據(jù)流連續(xù)或突發(fā)深度需求不一希望接口標(biāo)準(zhǔn)化- 用同步FIFO IP。同時(shí)鐘域資源極度緊張緩沖區(qū)很小如幾個(gè)到幾十個(gè)單元- 可以用寄存器堆實(shí)現(xiàn)一個(gè)簡(jiǎn)單的移位寄存器式緩沖區(qū)不一定需要RAM。歸根結(jié)底“乒乓操作”所代表的緩沖與流控思想是永恒的。但實(shí)現(xiàn)它的技術(shù)選型是多樣的。作為一名FPGA開(kāi)發(fā)者你的價(jià)值不在于背誦“乒乓操作”的代碼模板而在于深刻理解數(shù)據(jù)流中的矛盾并能為具體場(chǎng)景選擇或設(shè)計(jì)最合適、最穩(wěn)健的解決方案。從“實(shí)現(xiàn)一個(gè)乒乓操作”到“解決一個(gè)數(shù)據(jù)流緩沖問(wèn)題”這種思維的轉(zhuǎn)變是你從新手走向資深的關(guān)鍵一步。