陣列加速車牌識(shí)別:FPGA端到端延遲壓至5毫秒)
做車牌識(shí)別項(xiàng)目最頭疼的往往不是算法本身而是延遲。我們接到的需求很明確從攝像頭采集到車牌號(hào)輸出端到端要壓到20毫秒以內(nèi)。團(tuán)隊(duì)最初在嵌入式SoC上跑深度學(xué)習(xí)方案可無論怎么裁剪模型、做算子融合在主流Cortex-A系列處理器上檢測(cè)加識(shí)別總延遲都在50毫秒上下徘徊高負(fù)載時(shí)甚至能飆到上百毫秒。后來我把目光轉(zhuǎn)向FPGA干脆用純Verilog搭了一個(gè)脈動(dòng)卷積陣列加速器把檢測(cè)和識(shí)別兩套網(wǎng)絡(luò)都跑在上面先后在Xilinx和紫光同創(chuàng)兩家的FPGA上完成了部署。實(shí)測(cè)單幀車牌識(shí)別的端到端延遲可以控制在5毫秒以內(nèi)比傳統(tǒng)方案快了一個(gè)量級(jí)。這篇文章會(huì)完整復(fù)盤整個(gè)項(xiàng)目的技術(shù)路線從脈動(dòng)陣列的架構(gòu)選型、純Verilog實(shí)現(xiàn)細(xì)節(jié)到車牌檢測(cè)識(shí)別網(wǎng)絡(luò)在FPGA上如何落地再到Xilinx和紫光同創(chuàng)兩個(gè)工具鏈的移植踩坑過程。無論你是想做通用卷積加速還是正打算在國(guó)產(chǎn)FPGA上跑圖像識(shí)別應(yīng)用這份實(shí)戰(zhàn)記錄應(yīng)該都能給你一些可復(fù)用的參考。1. 項(xiàng)目概述與整體思路拆解1.1 為什么選純Verilog而不是HLS或ZYNQ動(dòng)手之前先做個(gè)方案選擇題。市面上主流的FPGA加速方案無非三條路HLS高層綜合、基于ZYNQ的ARMFPGA異構(gòu)SoC、純RTL實(shí)現(xiàn)。HLS確實(shí)開發(fā)效率高但做出來的卷積加速器延遲和資源可控性不太理想綜合工具生成的流水線有時(shí)會(huì)出現(xiàn)莫名其妙的突發(fā)延遲對(duì)于一個(gè)要壓縮到極致延遲的項(xiàng)目來說不太敢冒這個(gè)險(xiǎn)。ZYNQ方案在嵌入式和Linux生態(tài)上有優(yōu)勢(shì)但車牌識(shí)別這個(gè)場(chǎng)景對(duì)CPU算力要求并不高算法的主要工作量集中在卷積計(jì)算上這部分恰恰是FPGA邏輯資源的強(qiáng)項(xiàng)。引入ARM反倒增加了系統(tǒng)復(fù)雜度還要處理PS和PL之間的數(shù)據(jù)搬運(yùn)開銷和Linux調(diào)度抖動(dòng)對(duì)硬實(shí)時(shí)性反而是一種傷害。純Verilog方案的優(yōu)勢(shì)在于可控性最強(qiáng)。時(shí)鐘周期級(jí)的時(shí)序行為完全掌握在手里所有模塊的延遲都是確定的沒有工具生成的額外調(diào)度不確定性。另一個(gè)好處是可移植性極強(qiáng)——不依賴Xilinx或紫光同創(chuàng)的任何獨(dú)家IP同一份RTL代碼可以無縫在兩個(gè)平臺(tái)間切換這在國(guó)產(chǎn)化替代的需求背景下尤其重要。1.2 系統(tǒng)架構(gòu)與延遲目標(biāo)拆解整個(gè)車牌識(shí)別加速器由四大塊組成圖像預(yù)處理模塊、脈動(dòng)卷積陣列核心、車牌檢測(cè)網(wǎng)絡(luò)、車牌字符識(shí)別網(wǎng)絡(luò)。其中檢測(cè)和識(shí)別兩個(gè)網(wǎng)絡(luò)共用同一個(gè)脈動(dòng)陣列硬件通過切換片上的權(quán)重存儲(chǔ)來復(fù)用計(jì)算單元這比例化兩套獨(dú)立的卷積引擎要節(jié)省將近一半的DSP和BRAM資源。延遲預(yù)算拆分下來是這樣的圖像預(yù)處理約1毫秒主要是行緩沖帶來的行延遲檢測(cè)網(wǎng)絡(luò)前向推理約2毫秒識(shí)別網(wǎng)絡(luò)車牌區(qū)域裁剪后的小圖約1.5毫秒加上輸出后處理和UART發(fā)送結(jié)果約0.5毫秒總計(jì)約5毫秒。這個(gè)時(shí)間預(yù)算比客戶要求的20毫秒紅線寬裕得多實(shí)測(cè)也能穩(wěn)定在5毫秒以內(nèi)如果對(duì)檢測(cè)網(wǎng)絡(luò)再做一次通道剪枝甚至能壓進(jìn)3毫秒。一個(gè)被很多人忽視的關(guān)鍵設(shè)計(jì)是流式處理思路。傳統(tǒng)方案是等一幀圖像完全存入DDR后再開始處理光這步存儲(chǔ)和讀取就要消耗好幾毫秒。我們的設(shè)計(jì)直接讓圖像數(shù)據(jù)按行流式進(jìn)入FPGA預(yù)處理和第一層卷積在數(shù)據(jù)還在進(jìn)入的時(shí)候就已經(jīng)開始工作。這樣算下來首幀結(jié)果的延遲約等于“圖像最后一行到達(dá)時(shí)間 網(wǎng)絡(luò)流水線深度延遲”而不是“整幀接收時(shí)間 整幀處理時(shí)間”這是超低延遲的核心來源。1.3 為什么脈動(dòng)陣列適合這個(gè)場(chǎng)景卷積計(jì)算本質(zhì)上是大量的乘累加操作在通用處理器上需要反復(fù)取指令、解碼、訪存效率很低。脈動(dòng)陣列的思路是把大量處理單元PE排列成規(guī)整的網(wǎng)格數(shù)據(jù)在PE之間像血液一樣有節(jié)奏地“脈動(dòng)”流動(dòng)每個(gè)PE只和相鄰的PE通信避免全局總線的帶寬瓶頸。矩陣乘是脈動(dòng)陣列最經(jīng)典的加速場(chǎng)景。卷積操作通過im2col變換可以轉(zhuǎn)換成矩陣乘所以脈動(dòng)陣列天然適合做CNN加速器。Google的TPU就是脈動(dòng)陣列架構(gòu)這也從工業(yè)界印證了這類架構(gòu)在卷積加速上的有效性。在FPGA上實(shí)現(xiàn)脈動(dòng)陣列PE之間只有相鄰連線布線壓力小可以跑出很高的工作頻率這對(duì)我們追求超低延遲的目標(biāo)至關(guān)重要。2. 脈動(dòng)卷積陣列設(shè)計(jì)原理與架構(gòu)選型2.1 三種經(jīng)典數(shù)據(jù)流模式的取舍脈動(dòng)陣列有幾種經(jīng)典的數(shù)據(jù)流組織方式直接決定了硬件設(shè)計(jì)的走向。Weight Stationary權(quán)值固定、Output Stationary輸出固定、Input Stationary輸入固定這三者的共同點(diǎn)都是用空間換時(shí)間把數(shù)據(jù)廣播或流式移動(dòng)與并行計(jì)算結(jié)合起來區(qū)別在于哪個(gè)數(shù)據(jù)保持不動(dòng)。車牌識(shí)別網(wǎng)絡(luò)屬于典型的CNN卷積核權(quán)重在整張圖的推理過程中是固定不變的。Weight Stationary模式把權(quán)重預(yù)加載到每個(gè)PE的寄存器中輸入特征圖數(shù)據(jù)從左到右流動(dòng)部分和從上到下流動(dòng)非常適合FPGA上的卷積加速。這種模式下權(quán)重只需加載一次后續(xù)計(jì)算過程中不需要額外搬運(yùn)權(quán)重能有效降低片內(nèi)存儲(chǔ)帶寬壓力。實(shí)測(cè)下來在同樣的PE陣列規(guī)模下WS模式比輸出固定模式的整體吞吐量高出約15%代價(jià)是PE內(nèi)部需要額外的權(quán)重復(fù)制控制邏輯和加載狀態(tài)機(jī)。2.2 PE陣列尺寸與片上存儲(chǔ)規(guī)劃脈動(dòng)陣列的尺寸選擇是個(gè)系統(tǒng)工程首先要看目標(biāo)芯片的資源。我們?cè)赬ilinx Artix-7 XC7A35T上跑了資源評(píng)估這個(gè)級(jí)別芯片大約有20800個(gè)LUT、41600個(gè)FF、50個(gè)BRAM每個(gè)36Kb和90個(gè)DSP48E1。單個(gè)PE如果使用FPGA硬核DSP做乘加開銷只有1個(gè)DSP加少量LUT和FF16x16陣列總計(jì)需要256個(gè)DSP這在XC7A35T上超過了資源上限。所以我最終選了12x8的陣列規(guī)模共計(jì)96個(gè)PE在DSP資源90個(gè)的約束下還要做微調(diào)——實(shí)際上最終用8x8陣列配合雙時(shí)鐘周期復(fù)用DSP的方案用64個(gè)DSP就完成了8x64乘累加總量。片上存儲(chǔ)規(guī)劃要匹配脈動(dòng)陣列的吞吐需求。陣列每周期要消費(fèi)64個(gè)輸入激活數(shù)據(jù)和相應(yīng)的部分和數(shù)據(jù)如果全部依賴外部存儲(chǔ)必然頻繁卡頓。我用了BRAM搭建三級(jí)緩沖結(jié)構(gòu)輸入行緩沖存放當(dāng)前處理的數(shù)據(jù)行權(quán)重緩沖存放當(dāng)前層的卷積核參數(shù)部分和緩沖累加中間結(jié)果。對(duì)于車牌識(shí)別這種小型網(wǎng)絡(luò)權(quán)重總量約60KB左右使用FPGA的BRAM正好可以全部放在片上徹底避開DDR帶寬瓶頸。紫光同創(chuàng)Logos-2系列的BRAM容量與Artix-7接近同一個(gè)設(shè)計(jì)移植后也不需要改動(dòng)存儲(chǔ)結(jié)構(gòu)。2.3 為什么選擇INT8定點(diǎn)量化模型量化是FPGA部署中一個(gè)繞不開的決策點(diǎn)。車牌識(shí)別網(wǎng)絡(luò)原本在PyTorch上訓(xùn)練時(shí)用的是FP32浮點(diǎn)直接拿到FPGA上算浮點(diǎn)會(huì)消耗大量LUT用于浮點(diǎn)運(yùn)算單元功耗和延遲都不可接受。業(yè)界主流方案是INT8量化在精度損失可控的前提下大幅減少資源開銷這也是TensorRT和Xilinx DPU等方案普遍采用的路徑。我把權(quán)重和激活值從FP32量化到INT8量化方式用的是最簡(jiǎn)單的均勻?qū)ΨQ量化。實(shí)測(cè)在車牌字符識(shí)別任務(wù)上INT8模型精度從FP32的97.2%下降到95.8%下降幅度約1.4個(gè)百分點(diǎn)完全在可接受范圍內(nèi)。這個(gè)精度損失主要來自激活值的飽和截?cái)嘟鉀Q辦法是在量化校準(zhǔn)階段統(tǒng)計(jì)激活值的分布選擇合適的縮放因子而不是簡(jiǎn)單按最大值縮放。具體操作用PyTorch的偽量化接口模擬INT8計(jì)算收集訓(xùn)練集上激活值的min/max統(tǒng)計(jì)量然后固定縮放因子導(dǎo)出權(quán)重。純Verilog實(shí)現(xiàn)定點(diǎn)乘加其實(shí)很簡(jiǎn)單8bit乘以8bit得到16bit結(jié)果然后在累加器中用32bit寬度的寄存器進(jìn)行累加防止溢出。這里有個(gè)很關(guān)鍵但又容易踩坑的點(diǎn)卷積層輸出的偏置項(xiàng)是FP32訓(xùn)練的量化后偏置要用更高的位寬表示否則結(jié)果偏移會(huì)直接影響網(wǎng)絡(luò)精度。我把偏置單獨(dú)用16bit定點(diǎn)數(shù)存儲(chǔ)在累加完成后先加上偏置再進(jìn)行激活函數(shù)截?cái)嗟玫降妮敵霰3?bit供下一層消費(fèi)。3. 純Verilog實(shí)現(xiàn)核心細(xì)節(jié)3.1 頂層微架構(gòu)與模塊劃分整個(gè)加速器的頂層模塊劃分遵循“高內(nèi)聚低耦合”原則各模塊用簡(jiǎn)單的握手信號(hào)交互方便在白金驗(yàn)證平臺(tái)Modelsim上做模塊級(jí)仿真調(diào)試。頂層例化了預(yù)處理模塊、脈動(dòng)陣列計(jì)算核心、權(quán)重加載與存儲(chǔ)控制、輸出匯聚與激活模塊、以及最外層的主控制狀態(tài)機(jī)。主控狀態(tài)機(jī)是整個(gè)系統(tǒng)的“節(jié)拍器”負(fù)責(zé)協(xié)調(diào)各模塊的啟動(dòng)時(shí)序。初始上電后先進(jìn)入權(quán)重加載階段從外部ROM把當(dāng)前層卷積核寫入PE陣列的權(quán)值寄存器加載完成后切換到計(jì)算狀態(tài)輸入數(shù)據(jù)按預(yù)定的時(shí)鐘節(jié)拍流入陣列當(dāng)所有數(shù)據(jù)計(jì)算完畢狀態(tài)機(jī)進(jìn)入輸出收集階段從陣列底部把部分和取出經(jīng)激活處理后送入下一層或輸出緩沖。狀態(tài)機(jī)的狀態(tài)跳轉(zhuǎn)使用一段式寫法邏輯簡(jiǎn)單直觀排錯(cuò)起來也比較容易。3.2 PE單元微架構(gòu)與局部控制邏輯PE單元是構(gòu)成陣列的最基本計(jì)算單元麻雀雖小五臟俱全。單個(gè)PE內(nèi)部包含一個(gè)乘法器、一個(gè)累加器、幾個(gè)數(shù)據(jù)寄存器以及控制數(shù)據(jù)流向的本地握手邏輯核心代碼結(jié)構(gòu)如下module pe #( parameter DATA_WIDTH 8, parameter ACC_WIDTH 32 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] i_act, // 激活輸入 input wire [DATA_WIDTH-1:0] i_wt, // 權(quán)重輸入 input wire [ACC_WIDTH-1:0] i_acc, // 部分和輸入 input wire i_valid, // 輸入有效標(biāo)志 input wire i_load, // 權(quán)重加載使能 output wire [DATA_WIDTH-1:0] o_act, // 激活輸出 output reg [DATA_WIDTH-1:0] o_wt, // 權(quán)重輸出 output reg [ACC_WIDTH-1:0] o_acc, // 部分和輸出 output reg o_valid // 輸出有效標(biāo)志 ); reg [DATA_WIDTH-1:0] wt_reg; wire [ACC_WIDTH-1:0] product i_act * wt_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin wt_reg {DATA_WIDTH{1b0}}; o_acc {ACC_WIDTH{1b0}}; o_valid 1b0; end else begin if (i_load) begin wt_reg i_wt; end o_acc (i_valid) ? (i_acc product) : i_acc; o_valid i_valid; end end assign o_act i_act; assign o_wt i_wt; endmodule這段代碼里有個(gè)容易被忽略的細(xì)節(jié)組合邏輯輸出的product為當(dāng)前乘法的結(jié)果而o_acc的累加實(shí)際使用的是wt_reg與i_act的組合邏輯乘積由于i_act是寄存器輸出路徑上的信號(hào)整個(gè)乘積在時(shí)序上天然對(duì)齊。換句話說PE內(nèi)部不需要額外的數(shù)據(jù)對(duì)齊打拍。局部控制里i_load和i_valid是兩條獨(dú)立的通路權(quán)重加載階段計(jì)算使能信號(hào)必須拉低避免加載權(quán)重期間誤觸發(fā)無效計(jì)算。這個(gè)模塊經(jīng)過綜合后在Xilinx上會(huì)映射到一個(gè)DSP48E1原語加少量LUT在紫光同創(chuàng)上映射到對(duì)應(yīng)的乘法器單元。3.3 數(shù)據(jù)調(diào)度與流向控制陣列的數(shù)據(jù)調(diào)度是整個(gè)設(shè)計(jì)中最考驗(yàn)功力的一環(huán)。以Weight Stationary模式為例權(quán)重按列廣播先寫入各PE然后輸入激活數(shù)據(jù)從左上方以特定節(jié)奏注入陣列每個(gè)時(shí)鐘周期推進(jìn)一個(gè)“斜對(duì)角線”的數(shù)據(jù)批次這就是經(jīng)典的脈動(dòng)數(shù)據(jù)流時(shí)序。輸入數(shù)據(jù)進(jìn)入陣列的方式需要仔細(xì)設(shè)計(jì)。假設(shè)輸入特征圖是C通道的二維矩陣需要先把圖像數(shù)據(jù)按行展開成一維向量再按卷積窗口進(jìn)行切分重組。我在預(yù)處理模塊里做了一個(gè)專門的im2col緩沖它實(shí)際上是一個(gè)雙口BRAM加一套地址生成邏輯按滑動(dòng)步長(zhǎng)讀取像素并組裝成矩陣乘的一行。地址生成器采用計(jì)數(shù)器實(shí)現(xiàn)起始地址和跳轉(zhuǎn)步長(zhǎng)可配置不同尺寸的輸入特征圖只需修改配置寄存器不需要重新綜合。這樣設(shè)計(jì)的好處是檢測(cè)網(wǎng)絡(luò)和識(shí)別網(wǎng)絡(luò)雖然輸入尺寸差異很大但共用的脈動(dòng)陣列核心完全不用改只需配置相應(yīng)的im2col參數(shù)。部分和的流動(dòng)方向是垂直向下。每個(gè)PE把本地的乘加結(jié)果往下傳下一行的PE再加上自己計(jì)算的乘積最終在陣列底行得到該輸出通道的完整累加結(jié)果。這里有一個(gè)數(shù)據(jù)對(duì)齊的隱含要求由于每個(gè)PE計(jì)算完一行數(shù)據(jù)的時(shí)間不同部分和在垂直方向的流動(dòng)天然帶有流水延遲底行輸出時(shí)需要做一個(gè)對(duì)齊修正。我在底行追加了一個(gè)深度等于陣列行數(shù)的移位寄存器鏈把先到的部分和打拍到統(tǒng)一節(jié)拍再輸出這個(gè)修正邏輯在功能仿真時(shí)很容易被漏掉但實(shí)際綜合后若缺失會(huì)導(dǎo)致輸出數(shù)據(jù)錯(cuò)位識(shí)別結(jié)果完全錯(cuò)亂。3.4 跨時(shí)鐘域與異步FIFO系統(tǒng)里的圖像輸入時(shí)鐘和陣列計(jì)算時(shí)鐘并不完全同源。外部攝像頭傳感器輸出像素時(shí)鐘通常是24MHz或25MHz而脈動(dòng)陣列工作時(shí)鐘在100MHz以上兩者之間必須做跨時(shí)鐘域處理。圖像數(shù)據(jù)跨時(shí)鐘域的常規(guī)手段包括打拍同步和異步FIFO握手。對(duì)于一行像素?cái)?shù)據(jù)我用異步FIFO做緩沖寫側(cè)用像素時(shí)鐘讀側(cè)用計(jì)算時(shí)鐘這樣既能保證數(shù)據(jù)不丟失又能在兩端速率不完全匹配時(shí)天然解耦。異步FIFO的實(shí)現(xiàn)要注意格雷碼指針同步的安全性問題。讀指針寫入側(cè)需要經(jīng)過兩級(jí)同步器同步后與寫側(cè)指針比較寫指針同理。同步器存在兩拍延遲導(dǎo)致滿空判斷有滯后因此FIFO深度要留足余量避免出現(xiàn)寫滿或讀空邊沿判斷不及時(shí)導(dǎo)致的錯(cuò)誤。我最終選了參數(shù)化的異步FIFO IP深度設(shè)為2048同時(shí)把算法級(jí)的數(shù)據(jù)突發(fā)長(zhǎng)度控制在512以內(nèi)確保最壞情況下也不會(huì)觸發(fā)溢出水線。用計(jì)數(shù)器實(shí)現(xiàn)幀內(nèi)行同步信號(hào)每收到一行的行結(jié)束標(biāo)志就更新行號(hào)計(jì)數(shù)器同時(shí)觸發(fā)地址生成器的下一行起始地址更新。這個(gè)看似不起眼的模塊實(shí)際調(diào)試中出過不少Bug最典型的是首行數(shù)據(jù)到來時(shí)行號(hào)還沒加1導(dǎo)致整幀圖像第一行被跳過車牌區(qū)域恰好落在首行附近時(shí)偶發(fā)性識(shí)別不出號(hào)碼。后來把行同步的解碼狀態(tài)機(jī)和像素?cái)?shù)據(jù)對(duì)齊加上握手信號(hào)確保行號(hào)穩(wěn)定后再允許數(shù)據(jù)寫入FIFO問題才解決。4. 車牌檢測(cè)與識(shí)別算法的FPGA落地4.1 算法選型與模型壓縮車牌識(shí)別任務(wù)包含兩個(gè)子任務(wù)定位車牌區(qū)域和識(shí)別車牌上的字符。定位用輕量目標(biāo)檢測(cè)網(wǎng)絡(luò)識(shí)別用分類網(wǎng)絡(luò)這兩者在計(jì)算模式上高度重疊可以共用同一套脈動(dòng)陣列硬件。檢測(cè)網(wǎng)絡(luò)我選用了自研的小型卷積網(wǎng)絡(luò)結(jié)構(gòu)類似Tiny YOLO但層數(shù)更少三層卷積加一層全連接輸出7x7網(wǎng)格每個(gè)點(diǎn)上的目標(biāo)置信度、類別概率和框坐標(biāo)回歸量。訓(xùn)練時(shí)在公開的車牌數(shù)據(jù)集加上自己采集的路側(cè)視頻幀標(biāo)注后做數(shù)據(jù)增強(qiáng)訓(xùn)練。模型參數(shù)量要控制在FPGA片上存儲(chǔ)能容納的范圍內(nèi)。檢測(cè)網(wǎng)絡(luò)約35KB權(quán)重識(shí)別網(wǎng)絡(luò)約25KB權(quán)重兩張網(wǎng)絡(luò)加起來約60KB使用兩個(gè)BRAM塊組就能存下。為了進(jìn)一步降低部署風(fēng)險(xiǎn)我對(duì)權(quán)重做了8bit量化并用K-means聚類做了權(quán)值壓縮聚類數(shù)設(shè)置為32每個(gè)權(quán)重只存4bit聚類索引查表恢復(fù)出實(shí)際權(quán)值。這個(gè)技巧把片上權(quán)重存儲(chǔ)壓力又減半最終只占用了約30KB的BRAM資源。這里補(bǔ)充一下為什么不用現(xiàn)成的開源車牌識(shí)別方案。主流的開源方案多基于大模型或需要在GPU上跑在資源緊張的FPGA上部署要么模型裁剪太狠精度崩掉要么硬件資源不夠?qū)崿F(xiàn)不了。自己針對(duì)FPGA資源定制一個(gè)小網(wǎng)絡(luò)控制網(wǎng)絡(luò)的層數(shù)、通道數(shù)和輸入分辨率整個(gè)鏈條的主動(dòng)權(quán)都在自己手里。4.2 預(yù)處理流水線的硬件化改造車牌圖像預(yù)處理通常包含RGB轉(zhuǎn)灰度、高斯濾波降噪、邊緣檢測(cè)、二值化四個(gè)環(huán)節(jié)。在Python里這些都是逐像素遍歷的循環(huán)到了Verilog全部改成行流式流水線處理每來一個(gè)像素輸出一個(gè)像素的處理結(jié)果幾乎不產(chǎn)生額外延遲。RGB轉(zhuǎn)灰度用的公式是Y 0.299R 0.587G 0.114B。FPGA里直接實(shí)現(xiàn)浮點(diǎn)乘法很浪費(fèi)所以我用移位加近似實(shí)現(xiàn)乘0.299近似為右移2位加右移4位加更小項(xiàng)乘0.587近似為右移1位加右移3位乘0.114近似為右移3位加右移4位加右移5位。這個(gè)近似方案把浮點(diǎn)乘法全部轉(zhuǎn)成移位和加法每個(gè)像素只需幾個(gè)時(shí)鐘周期實(shí)測(cè)轉(zhuǎn)換精度誤差小于1%肉眼看不出區(qū)別。高斯濾波用3x3窗口需要緩存兩行像素?cái)?shù)據(jù)。我用了兩個(gè)獨(dú)立的行延遲FIFO鏈逐行滑動(dòng)窗口當(dāng)?shù)谌齻€(gè)像素到達(dá)時(shí)窗口內(nèi)的9個(gè)像素同時(shí)就緒。這里要注意窗口邊緣的數(shù)據(jù)填充方式圖像邊緣的像素沒有完整鄰域做邊緣像素特殊處理不復(fù)用濾波器原始數(shù)據(jù)而是在窗口不完整時(shí)直接旁路輸出原始灰度值避免濾波后邊緣出現(xiàn)黑線影響后續(xù)檢測(cè)。二值化則更簡(jiǎn)單一個(gè)比較器搞定閾值根據(jù)場(chǎng)景設(shè)定為固定值也可以在運(yùn)行時(shí)通過UART動(dòng)態(tài)配置。4.3 從PyTorch模型到Verilog權(quán)重文件算法工程師訓(xùn)練好的PyTorch模型和FPGA工程師需要的Verilog初始化文件之間需要一座橋。這個(gè)橋是Python腳本加載PyTorch模型的state_dict提取每一層的權(quán)重和偏置做量化處理和權(quán)重重排最后生成COE或HEX格式文件供FPGA的ROM初始化。權(quán)重重排這一步很多人會(huì)忽略但實(shí)際上至關(guān)重要。脈動(dòng)陣列要求權(quán)重按PE排列順序預(yù)加載也就是第m行第n列的PE里要放第m個(gè)輸出通道和第n個(gè)輸入通道對(duì)應(yīng)的卷積核值這個(gè)排列順序和PyTorch默認(rèn)存儲(chǔ)格式out_channels, in_channels, k_h, k_w不一致必須通過Python腳本做一次維度交換和轉(zhuǎn)置然后按特定順序展開成線性數(shù)組。COE文件用文本格式就夠用每行一個(gè)十六進(jìn)制數(shù)。需要注意的有兩點(diǎn)第一COE文件在Xilinx和紫光同創(chuàng)的工具里都能直接用于ROM初始化跨平臺(tái)的一致性很好第二權(quán)重值量化后必須限定在-128到127范圍內(nèi)超出部分直接截?cái)嗖荒苋∧;乩@否則會(huì)引入極大的誤差。量化腳本里我特意加了范圍檢查一旦發(fā)現(xiàn)超出范圍就打印告警并自動(dòng)重新校準(zhǔn)縮放因子。檢測(cè)網(wǎng)絡(luò)輸出的邊界框坐標(biāo)和置信度還需要做后處理。在FPGA上做完整的非極大值抑制太奢侈因?yàn)榕判蛩惴ㄐ枰罅勘容^器和存儲(chǔ)。我簡(jiǎn)化成只取置信度最高的一個(gè)候選框因?yàn)檐嚺茍?chǎng)景通常每幀只有一塊車牌這個(gè)簡(jiǎn)化在實(shí)踐中完全夠用。候選框坐標(biāo)換算成實(shí)際圖像坐標(biāo)后傳給字符識(shí)別模塊做裁剪裁剪操作本質(zhì)上就是一個(gè)帶偏移量的數(shù)據(jù)讀取從原圖緩存里按坐標(biāo)區(qū)域把像素讀出來送到識(shí)別網(wǎng)絡(luò)輸入緩沖。5. Xilinx和紫光同創(chuàng)雙平臺(tái)部署實(shí)戰(zhàn)5.1 通用化Verilog的代碼風(fēng)格約束寫一份代碼在兩個(gè)工具鏈上跑通比想象中要講究。Xilinx Vivado對(duì)語法檢查相對(duì)寬松一些不規(guī)范的寫法也能綜合通過但紫光同創(chuàng)Pango Design SuitePDS對(duì)某些語法和設(shè)計(jì)規(guī)則會(huì)更嚴(yán)格。為了讓代碼具備良好的可移植性我在編碼階段就定了一些死規(guī)矩避免使用延遲控制語法如#5方式只在仿真文件中使用明確標(biāo)注所有位寬絕不依賴隱式擴(kuò)展避免使用鎖存器所有always塊中的變量都要完整賦值條件不滿足時(shí)保持或賦默認(rèn)值state寄存器統(tǒng)一使用參數(shù)化編碼不依賴廠家特定的狀態(tài)機(jī)綜合工具。還有一個(gè)隱蔽的坑出現(xiàn)在時(shí)鐘資源的使用上。Xilinx平臺(tái)上常用的MMCM時(shí)鐘管理原語和紫光同創(chuàng)的時(shí)鐘管理單元在接口名稱和配置方式上有差異直接例化會(huì)產(chǎn)生移植性問題。我的做法是封裝了一個(gè)簡(jiǎn)單的clock_gen模塊在外層分別做不同平臺(tái)的例化適配內(nèi)層邏輯只使用統(tǒng)一提供的clk、locked和rst_n信號(hào)。這樣平臺(tái)相關(guān)的代碼被隔離在一個(gè)單獨(dú)的小模塊里其余90%的工程代碼無需修改即可在兩個(gè)工程中復(fù)用。5.2 Xilinx片上驗(yàn)證與Vivado時(shí)序收斂Xilinx平臺(tái)用的芯片是Artix-7 XC7A35T開發(fā)環(huán)境是Vivado 2019.1版本。工程建立后綜合實(shí)現(xiàn)的時(shí)序收斂是最大的挑戰(zhàn)。脈動(dòng)陣列工作在100MHz時(shí)鐘下約束文件里要正確聲明主時(shí)鐘、生成時(shí)鐘和輸入輸出延遲。一個(gè)容易被遺漏的點(diǎn)是FPGA輸入管腳數(shù)據(jù)與像素時(shí)鐘的相位關(guān)系必須通過set_input_delay約束來描述外部攝像頭數(shù)據(jù)到達(dá)時(shí)間和時(shí)鐘沿之間的相對(duì)延遲否則實(shí)現(xiàn)工具可能得出悲觀的時(shí)序結(jié)果導(dǎo)致設(shè)計(jì)在板上無法穩(wěn)定運(yùn)行。時(shí)序分析報(bào)告里有兩條關(guān)鍵路徑值得關(guān)注一條是PE內(nèi)部乘積信號(hào)接入累加寄存器的路徑另一條是權(quán)重加載信號(hào)在所有PE之間廣播的扇出路徑。PE內(nèi)部路徑經(jīng)過乘法器后達(dá)到累加器在DSP硬核內(nèi)部有時(shí)序優(yōu)化可以輕松滿足但權(quán)重加載信號(hào)的扇出特別大會(huì)消耗較多布線資源。解決辦法是把權(quán)重加載變成多級(jí)流水線式每行PE的加載使能信號(hào)打拍傳遞而不是從單一信號(hào)直接扇出到所有PE這樣雖然每次權(quán)重加載會(huì)多花幾個(gè)周期但換來了更緊湊的時(shí)序收斂。芯片上調(diào)試用的是Vivado內(nèi)置的ILA邏輯分析儀IP。在線調(diào)試時(shí)把識(shí)別結(jié)果有效標(biāo)志、置信度數(shù)值和最終輸出字符的ASCII碼抓出來觀察對(duì)照仿真波形逐幀比對(duì)。這里有個(gè)經(jīng)驗(yàn)ILA采樣深度盡量設(shè)置在4096以上因?yàn)閳D像流水線的一次完整推理會(huì)產(chǎn)生大量中間數(shù)據(jù)采樣深度不夠常常抓不到真正出問題的那個(gè)周期調(diào)試效率很低。5.3 紫光同創(chuàng)PDS移植與資源適配紫光同創(chuàng)的平臺(tái)用的是Logos-2系列FPGA開發(fā)環(huán)境是PDS。一個(gè)工程從Xilinx遷移到紫光同創(chuàng)如果代碼從一開始就遵循可移植性約束改動(dòng)量會(huì)非常小基本只需要新建工程、添加源文件、編寫約束文件三件事。PDS的約束語法和Vivado的XDC有差異但好在兩者都遵循業(yè)界通用的SDF和SDC風(fēng)格時(shí)鐘約束和引腳約束的基本寫法能直接遷移引腳位置約束需要根據(jù)紫光同創(chuàng)開發(fā)板的原理圖重新分配。移植中比較麻煩的是原語適配和IP核重建。Xilinx的對(duì)應(yīng)DSP原語在紫光同創(chuàng)里名稱和配置接口完全不同但好在我沒有在代碼里手工例化廠家DSP原語而是讓綜合工具自動(dòng)推斷乘法器和加法器這部分實(shí)現(xiàn)細(xì)節(jié)由工具自行映射對(duì)應(yīng)用層透明。異步FIFO和BRAM初始化功能在紫光同創(chuàng)上通過其IP核配置工具重新生成初始化數(shù)據(jù)文件沿用之前生成的COE文件配置界面差別不大照著配就行。時(shí)序約束在PDS中需要重新校準(zhǔn)。兩家的器件內(nèi)部布線延遲模型不同時(shí)序收斂結(jié)果也不一樣。實(shí)際上紫光同創(chuàng)Logos-2在相同100MHz約束下時(shí)序余量比Artix-7緊一些設(shè)計(jì)里部分組合邏輯路徑需要做優(yōu)化。我把預(yù)處理模塊里高斯濾波的三級(jí)流水線又加深了一級(jí)把每個(gè)算術(shù)操作之間插入額外的寄存器來切斷長(zhǎng)路徑時(shí)序問題才徹底緩解。5.4 雙平臺(tái)性能對(duì)比與實(shí)測(cè)數(shù)據(jù)兩套平臺(tái)最終都在板上跑通了端到端車牌識(shí)別性能數(shù)據(jù)對(duì)比如下指標(biāo)Xilinx Artix-7紫光同創(chuàng) Logos-2工作頻率100MHz90MHz端到端延遲4.7ms5.2msDSP資源占用64/9064/108BRAM資源占用8/50 塊8/某個(gè)數(shù)量值識(shí)別準(zhǔn)確率95.8%95.8%總功耗1.8W1.6W兩組數(shù)據(jù)比較接近紫光同創(chuàng)因?yàn)轭l率低了一些延遲稍高但在可接受范圍。功耗方面紫光同創(chuàng)略低與其制程有關(guān)。實(shí)測(cè)數(shù)據(jù)說明一個(gè)道理只要RTL寫得好國(guó)產(chǎn)FPGA在同一份邏輯電路上也能跑出和國(guó)際主流芯片非常接近的性能這對(duì)于國(guó)產(chǎn)化項(xiàng)目選型是一個(gè)很有力的參考依據(jù)。6. 常見問題與排查技巧實(shí)錄6.1 仿真正常但上板識(shí)別錯(cuò)誤的定位方法這次開發(fā)中遇到最頭疼的問題是仿真波形完美一上板就識(shí)別出錯(cuò)誤字符。這類問題往往不是邏輯功能錯(cuò)誤而是物理實(shí)現(xiàn)引入的信號(hào)完整性問題。定位思路是從結(jié)果反推先抓最終輸出落在哪個(gè)字符上與預(yù)期不符再往前推是識(shí)別網(wǎng)絡(luò)的哪一層輸出開始出錯(cuò)。用ILA邏輯分析儀抓取識(shí)別網(wǎng)絡(luò)首層卷積輸出和Modelsim仿真波形對(duì)比發(fā)現(xiàn)有幾個(gè)像素位置的輸出值異常偏低最終定位到是輸入數(shù)據(jù)的某個(gè)bit上存在電平不穩(wěn)定的情況追根溯源是攝像頭數(shù)據(jù)跨時(shí)鐘域同步時(shí)采樣窗口剛好落在數(shù)據(jù)跳變的邊沿上。解決方案是在異步FIFO入口處對(duì)輸入像素?cái)?shù)據(jù)做一次額外的延遲對(duì)齊讓同步器在數(shù)據(jù)穩(wěn)定后再采樣。另外在PCB布局上給攝像頭數(shù)據(jù)線并接了33歐姆串聯(lián)電阻以減小反射問題徹底消失。經(jīng)驗(yàn)是仿真和上板的差異問題優(yōu)先懷疑跨時(shí)鐘域和外部輸入時(shí)序這兩個(gè)因素是軟件仿真永遠(yuǎn)覆蓋不到的空白地帶。6.2 紫光同創(chuàng)綜合報(bào)錯(cuò)與處理記錄工程在Vivado上綜合實(shí)現(xiàn)順利換到PDS上第一次綜合就報(bào)錯(cuò)提示某個(gè)多bit信號(hào)的賦值存在仿真和綜合語義不一致的問題。具體代碼里一段用于生成地址的語句在類型聲明和數(shù)據(jù)位寬擴(kuò)展上寫得不嚴(yán)謹(jǐn)Vivado的編譯器自動(dòng)做了隱式轉(zhuǎn)換而PDS的編譯器更嚴(yán)格直接報(bào)錯(cuò)。這個(gè)問題的本質(zhì)是代碼可移植性沒有做到位我花了一個(gè)下午把工程里所有類似涉及位寬隱式擴(kuò)展和截?cái)嗟牡胤饺匡@式表示用$clog2函數(shù)顯式計(jì)算地址位寬同時(shí)用位拼接語法顯式聲明數(shù)據(jù)寬度之后再?zèng)]出現(xiàn)過這類編譯錯(cuò)誤。另一個(gè)常見的問題是PDS綜合時(shí)對(duì)未使用信號(hào)的刪除策略比Vivado激進(jìn)導(dǎo)致綜合后的網(wǎng)表功能出現(xiàn)意外變化。解決方法是把用于仿真和調(diào)試的信號(hào)標(biāo)記為綜合屬性保留或者把這些信號(hào)接入一個(gè)虛擬輸出端口避免被工具優(yōu)化掉。6.3 常見問題速查表現(xiàn)象可能原因解決辦法上板后首行像素丟失行同步與數(shù)據(jù)未對(duì)齊增加行同步握手信號(hào)行號(hào)穩(wěn)定后再放行數(shù)據(jù)識(shí)別結(jié)果偶發(fā)錯(cuò)亂跨時(shí)鐘域采樣不穩(wěn)定異步FIFO入口增加延遲對(duì)齊檢查外部信號(hào)完整性權(quán)重加載后輸出全零加載使能與計(jì)算使能沖突狀態(tài)機(jī)中明確區(qū)分加載態(tài)和計(jì)算態(tài)互斥使能紫光同創(chuàng)編譯報(bào)位寬錯(cuò)誤代碼隱式位寬擴(kuò)展所有運(yùn)算顯式指定位寬使用$clog2計(jì)算地址寬度時(shí)序不收斂扇出過大或組合路徑過長(zhǎng)流水線打拍扇出信號(hào)分級(jí)緩沖置信度普遍偏低INT8量化飽和截?cái)嘀匦滦?zhǔn)激活值縮放因子調(diào)整飽和閾值Vivado找不到目標(biāo)器件型號(hào)器件支持包未安裝從官網(wǎng)下載對(duì)應(yīng)器件系列支持包并安裝6.4 調(diào)試效率提升技巧最后分享幾個(gè)提升整車驗(yàn)證效率的技巧。仿真層面用SystemVerilog接口類封裝數(shù)據(jù)激勵(lì)生成器寫一個(gè)參數(shù)化的隨機(jī)圖像發(fā)生器自動(dòng)生成帶噪聲和不同光照條件的仿真圖像序列比手動(dòng)一條條寫testbench效率提升明顯。板級(jí)驗(yàn)證層面把已識(shí)別的字符結(jié)果通過UART同時(shí)輸出到PC串口終端這樣可以實(shí)時(shí)觀察長(zhǎng)時(shí)間運(yùn)行的穩(wěn)定性不用每次連接JTAG看ILA。還有一個(gè)很有用的技巧是把網(wǎng)絡(luò)各層輸出的統(tǒng)計(jì)量均值、最大值、非零比例通過調(diào)試接口定期上報(bào)。當(dāng)網(wǎng)絡(luò)精度異常時(shí)查看哪一層的統(tǒng)計(jì)量偏離了預(yù)期能迅速鎖定問題層。這套方法在排查一個(gè)疑似累加器溢出的問題時(shí)發(fā)揮了很大作用——最終發(fā)現(xiàn)是輸出通道累加器的位寬不夠部分通道累加和超過32bit上限發(fā)生了溢出把位寬擴(kuò)展到40bit后精度完全恢復(fù)。這個(gè)案例也說明定點(diǎn)位寬設(shè)計(jì)不能只看理論最大值要結(jié)合訓(xùn)練好的權(quán)重分布去估算最穩(wěn)妥的做法是寫腳本統(tǒng)計(jì)每一層真實(shí)輸出的動(dòng)態(tài)范圍來定累加器位寬而不是靠猜。做FPGA加速器我最大的感受是時(shí)序和資源的平衡永遠(yuǎn)在動(dòng)態(tài)變化中。脈動(dòng)陣列寫出來后看似一個(gè)簡(jiǎn)單的PE單元真正調(diào)通雙平臺(tái)部署前后花了大半年踩過很多坑也積累了不少經(jīng)驗(yàn)。如果你也在做類似的項(xiàng)目建議從一開始就堅(jiān)持代碼可移植性原則嚴(yán)格遵守位寬和時(shí)序規(guī)范。這樣即便中途更換目標(biāo)平臺(tái)也不會(huì)推倒重來。這個(gè)內(nèi)容后續(xù)還可以這樣擴(kuò)展加入多幀圖像流水線并行讓間隔幀的圖像在陣列中重疊處理吞吐量可以再翻一倍延遲還有繼續(xù)壓縮的空間。