1_to_7串并轉(zhuǎn)換實戰(zhàn)解析)
1. 項目背景與核心思路如果有人問我FPGA里最容易被低估的硬核資源是什么我大概率會回答Serdes。這玩意兒平時藏在高速接口后面大家只把它當“跑高速的通道”用可是當你真的需要做并行數(shù)據(jù)寬度變換、對接一些非標準的突發(fā)協(xié)議時Serdes的靈活程度遠超你想象。最近我在一個數(shù)據(jù)采集板卡的項目里就撞上一個需求傳感器輸出的高速串行數(shù)據(jù)需要按7個UI一組打包成并行7bit字再交給后級FIFO和DSP處理。翻遍參考設計最后落在Xilinx官方的XAPP585應用筆記上用它的SDR模式把Serdes配成1_to_7串并轉(zhuǎn)換問題迎刃而解。這里把整個踩坑和實現(xiàn)過程掰開揉碎了講一遍希望能幫到同樣被非標準串并比折磨的人。XAPP585是Xilinx提供給Virtex-5等早期FPGA上RocketIO GTP/GTX收發(fā)器的一個參考設計原本作用是把千兆以太網(wǎng)的SGMII接口轉(zhuǎn)換成8bit并行數(shù)據(jù)但它的核心思想——通過Serdes的SDR模式支持任意整數(shù)分頻的串并轉(zhuǎn)換——完全可以脫離以太網(wǎng)場景使用。我們項目里數(shù)據(jù)鏈路的串行速率是2.45Gbps要求邏輯側(cè)得到350Mbps時鐘下的7bit并行數(shù)據(jù)乍一聽不是標準的8b/10b、不是4字節(jié)對齊常規(guī)的Serdes配法也做不到但XAPP585給的正是這個思路不進PCS的8B/10B編解碼也不做標準comma對齊直接把串行數(shù)據(jù)當原始比特流用SDR模式按想要的位寬“切開”。為什么放著DDR模式不用非要選SDR這也是我最初糾結(jié)的地方。7bit字寬在Serdes的世界里其實很尷尬因為大多數(shù)Serdes的并行因子是4的倍數(shù)或者整數(shù)可配。Virtex-5的GTP收發(fā)器在SDR模式下內(nèi)部串并轉(zhuǎn)換的工作方式是把輸入時鐘2分頻后再做N分頻因此可以支持比較靈活的比值。配合XAPP585里的相位校準邏輯能夠把2.45Gbps一個bit一個bit地存入移位鏈每攢夠7個bit輸出一拍。這個能力對非標準位寬通信特別珍貴。整個方案的核心有三塊一是Serdes模塊的初始化與鎖定務必保證RX端恢復到正確的時鐘二是1_to_7的模式切換必須讓Serdes內(nèi)部的接收數(shù)據(jù)寬度寄存器按SDR模式正確設置三是從Serdes拿到的7bit數(shù)據(jù)不能直接丟給后級因為數(shù)據(jù)在跨時鐘域后可能出現(xiàn)比特順序“錯位”需要一層“對齊調(diào)整”邏輯XAPP585里叫word alignment這個才是讓7bit數(shù)據(jù)能用的關鍵。2. Serdes SDR模式原理解讀要理解1_to_7怎么實現(xiàn)得先從Serdes的SDR模式原理說起。絕大多數(shù)FPGA的Serdes模塊都是為高速串行通信設計的發(fā)送端把并行數(shù)據(jù)變成串行比特流接收端把串行比特流恢復成并行數(shù)據(jù)。這個過程通常會經(jīng)過一個8B/10B編解碼、通道綁定、comma檢測等“標準”處理鏈路。而在XAPP585的設計里這些全被繞開。它走的是FPGA內(nèi)部原語早期叫ROCKETIO_WRAPPER后來叫GTP/GTXE1最底層的“裸”模式直接把串行比特流送到一個移位寄存器鏈由接收時鐘的若干分頻得到并行采樣時鐘對齊到想要的bit數(shù)后并行輸出。SDR和DDR的區(qū)別DDR模式下上下沿都采樣一個GTX時鐘周期取2個bit并行輸出通常是偶數(shù)位寬比如2、4、8SDR模式下只在上沿采樣一個GTX時鐘周期取1個bit通過數(shù)字邏輯可以很靈活地控制并行寬度。對1_to_7這種7為奇數(shù)的轉(zhuǎn)換SDR是唯一合理的路徑。為什么不能直接用一個7bit的并行因子參數(shù)因為很多Serdes原語自帶的并行寬度配置并不支持7這個數(shù)Xilinx只有1、2、4、8、16等固定檔位好在SDR模式下我們可以自己控制上游分頻讓采樣時鐘在每7個bit后產(chǎn)生一個并行“有效”脈沖然后把移位寄存器里的7bit一次性取走。在GTP收發(fā)器的內(nèi)部接收路徑上RXCLK是高速恢復時鐘假設頻率為2.45GHz。如果你在SDR模式下把RXOUTDIV配置設成1那么從串行到并行的基本采樣周期就是1個bit。接下來要做的是在這個基礎上做一個除法器從2.45GHz時鐘里每隔7個時鐘拉高一次“l(fā)oad_enable”同時將RX_DATAWIDTH設為7bit。XAPP585里有幾個關鍵模塊一個叫serial_to_parallel它本質(zhì)上是一個右移寄存器串行數(shù)據(jù)不斷移入另一個叫bit_align負責檢測數(shù)據(jù)流的“起點”。很多人會問既然移位寄存器是連續(xù)移位的怎么保證7bit的邊界正好是我們想要的字節(jié)邊界這就要靠用戶在應用層給到對齊標記。XAPP585的典型做法是在發(fā)送端周期性發(fā)送一個特殊的同步碼型接收端通過識別這個碼型把移位寄存器里的7個bit對應的位置“撥正”。以太網(wǎng)的SGMII走的是8b/10bcomma碼非常好識別但如果我們自定義協(xié)議沒有comma碼那就要自己在發(fā)送數(shù)據(jù)流里插入前導碼。我們項目中給傳感器輸出的數(shù)據(jù)設計了幀頭0x6A在未對齊之前先滑動搜索這個碼型一旦找到就鎖定輸出相位。這個過程要非常小心因為7bit的字位于不同相位看到的幀頭字節(jié)可能完全不同必須配合滑動窗機制。1_to_7的“1”指的是輸入的1位串行比特流“7”則是并行側(cè)的7位總線。也就是說邏輯時鐘降到350MHz2.45GHz/7數(shù)據(jù)總線上每次出現(xiàn)一個7bit向量。從帶寬角度看2.45Gbps ÷ 7 350M word/s這個字速率正好可以接到后級AXI-Stream之類的總線上后端處理按350MHz隨路時鐘采樣即可。這里還要順帶提醒一句SDR模式下由于采樣邊沿較大對PCB走線和參考時鐘質(zhì)量更敏感。別以為SDR比DDR慢就越容易實際上因為每位只靠單沿采樣恢復時鐘的抖動容忍度更低XAPP585里也專門提到要預留時鐘數(shù)據(jù)恢復的裕量這一點我們在調(diào)試時感觸很深。3. 1_to_7串并轉(zhuǎn)換的RTL實現(xiàn)細節(jié)XAPP585的完整工程在Xilinx官網(wǎng)上能下載但用起來并不像普通IP那樣雙擊生成。它是基于Virtex-5 RocketIO的老工程放到新系列芯片上往往要修改。我這里給出一個基于XAPP585思路、又不依賴具體芯片原語寫法差異的偽代碼級實現(xiàn)剝掉平臺細節(jié)這樣大家換平臺也能看懂。首先是串并轉(zhuǎn)換核心模塊。這里的關鍵是生成一個7分頻的“并行使能”信號。我在實際項目中寫了一個計數(shù)器在高速恢復時鐘域里從0數(shù)到6然后回到0。每當計數(shù)到6時置位一個脈沖enable表示此刻移位寄存器里的數(shù)據(jù)是一個完整字。同時在enable信號的下一拍將移位寄存器并行讀出。reg [6:0] shift_reg; reg [2:0] bit_cnt; wire load_en; always (posedge rx_clk_high or negedge rst_n) begin if (!rst_n) bit_cnt 3d0; else if (bit_cnt 3d6) bit_cnt 3d0; else bit_cnt bit_cnt 1b1; end assign load_en (bit_cnt 3d6); always (posedge rx_clk_high or negedge rst_n) begin if (!rst_n) shift_reg 7d0; else shift_reg {shift_reg[5:0], rx_data_in}; end reg [6:0] parallel_out; always (posedge rx_clk_high or negedge rst_n) begin if (!rst_n) parallel_out 7d0; else if (load_en) parallel_out shift_reg; end這個代碼看起來簡單但實際操作里有個大坑load_en信號在高速時鐘域只持續(xù)一個時鐘周期你必須保證后續(xù)的采集邏輯在這個脈沖處穩(wěn)定采樣。如果你的后級邏輯時鐘就是rx_clk_high直接分頻出來的那么load_en的建立/保持時序可能特別緊張建議用(* ASYNC_REG TRUE *)打兩拍同步同時使用FIFO做跨時鐘域緩沖而不是直接讓后級邏輯采這個脈沖。再說說7分頻計數(shù)器。如果你是按照XAPP585原始方案改的會發(fā)現(xiàn)它里面用的是rxclk的一個四分頻信號再計數(shù)的因為SDR模式有時會讓RXUSRCLK變成串行時鐘的一半。實際上在GTP原語配置里TXOUTCLK和RXOUTCLK的頻率設置決定了用戶時鐘與串行時鐘的比例。如果設置RXOUTDIV1那么用戶時鐘就是2.45GHz邏輯根本跑不動所以通常會把RXOUTDIV設為2即用戶時鐘1.225GHz再在FPGA邏輯里做3.5分頻得到350MHz的并行使能。由于分頻值是分數(shù)所以要用“7個高速周期內(nèi)輸出一次350MHz字”的時序而不是簡單二分頻。我的做法是直接用1.225GHz的時鐘作為主時鐘這樣每個高速時鐘對應2個串行bit串并轉(zhuǎn)換要攢7個bit就得跨3.5個時鐘周期代碼里就用一個4bit計數(shù)器在每個時鐘沿采樣2bit攢滿7bit后load一次。這樣寫邏輯其實更貼合XAPP585的推薦。關鍵的原語配置上收發(fā)器的RX_DATA_WIDTH我們必須設為7RX_DFE之類的均衡參數(shù)按速率調(diào)整。在GTX/GTH上需要關注的還有RXOUT_DIV、RXUSRCLK_DIV等。另外因為不做8B/10BPCS部分要把RX_COMMA_DET_ENABLE關掉否則它會誤檢測逗號碼然后強制對齊破壞我們的自定義邊界。除了RTL邏輯時序約束是成敗關鍵。Serdes的用戶時鐘往往不在系統(tǒng)時鐘樹上必須明確設置create_generated_clock。我通常這樣約束create_generated_clock -name rx_clk_high \ -source [get_pins gt_inst/RXOUTCLK] \ -divide_by 1 [get_pins gt_inst/RXOUTCLK] create_generated_clock -name rx_en_clk \ -source [get_ports custom_clk] \ -divide_by 1 -add \ -master_clock [get_clocks rx_clk_high] [get_pins div_7_reg/C]約束的核心目的是讓綜合工具知道你所謂350MHz其實是“350MHz的脈沖串”不是真正的均勻時鐘所以建議把所有后級邏輯都放在rx_clk_high域只是通過使能門控而不是創(chuàng)建兩個物理時鐘。如果直接創(chuàng)建350MHz時鐘讓工具去收斂基本是過不了的。因為數(shù)據(jù)變化不是均勻分布在每個周期而是在load_en后的那一拍變化。最好的方式是讓后級邏輯全部跑在rx_clk_high上用寄存器使能來處理7bit數(shù)據(jù)的節(jié)拍。這樣時序約束就是一組普通的單時鐘約束DFF的CE腳非??煽?。4. 仿真與上板調(diào)試過程我在測試時先寫了一個簡單的testbench給一個固定串行數(shù)據(jù)流模擬傳感器在連續(xù)發(fā)送幀頭加數(shù)據(jù)。仿真時最容易犯的錯誤是直接把并行數(shù)據(jù)期待成“從移位寄存器并行輸出第一個bit為最高位”的形式。但實際上1_to_7串并轉(zhuǎn)換出來后bit順序和串行流里誰先誰后完全是反著的。比如串行流依次到來是bit0、bit1……bit6如果你期望輸出7bit的二進制數(shù)值為{bit0,bit1,bit2,bit3,bit4,bit5,bit6}而你代碼里寫的是new_data {data, new_data}之類那么最終出來的數(shù)值位寬順序就會反轉(zhuǎn)。我建議在仿真階段專門寫一個參考模型把串行比特流一位位送進一個延遲線再按同樣的時鐘脈沖取出來和RTL輸出做比對。一旦不一致先判斷是相位問題還是位順序問題。仿真過程中還有一個典型的“假失敗”因為我們在load_en后并行輸出而后級FIFO的寫使能信號和寫數(shù)據(jù)之間存在一個周期的相位差。如果testbench直接在同一時刻斷言FIFO里的數(shù)據(jù)看起來就是差一拍。我習慣在仿真腳本里把輸出打拍后延遲檢查或者干脆等數(shù)據(jù)穩(wěn)定兩拍后再采樣。這不是設計bug而是仿真的慣性思維問題。上板調(diào)試階段我建議用ILA觀察高速鏈路的數(shù)據(jù)。你可以把觸發(fā)條件設為檢測到幀頭0x6A然后抓取并行數(shù)據(jù)和valid信號。注意ILA采樣時鐘也應該用rx_clk_high而不是某個分頻時鐘否則會采到毛刺。第一次抓波形時很大概率看到并行數(shù)據(jù)“缺位”——也就是7bit里可能只出現(xiàn)6個有效bit或者輸出數(shù)據(jù)為0x01、0x03這種明顯錯位的數(shù)值。這通常是相位對齊沒完成也就是word alignment沒生效。這時候問題不在分頻器而在“對齊模塊”。XAPP585里的對齊模塊是核心中的核心。我在實現(xiàn)時借鑒了它的狀態(tài)機初始狀態(tài)為搜索不斷從當前的7bit輸出中提取一個比較窗和幀頭模板比對。如果不等就在移位寄存器層面“跳過”一個bit再試如果連續(xù)N次相等就認為對齊并進入鎖定狀態(tài)。在自定義數(shù)據(jù)格式里必須注意幀頭要能夠區(qū)分相位。如果用0x6A二進制1101010做幀頭它需要滿足一個條件無論從哪個bit開始截取窗內(nèi)都不會循環(huán)得到相同的無效匹配。比如某些重復碼型0x55就不行因為0x55循環(huán)移位后仍然在同一個7bit窗內(nèi)得到0x55這樣無法判定邊界。我最終選定的0x6A經(jīng)過驗證7種相位下只有正確的相位能匹配上且和后面數(shù)據(jù)的組合也不會產(chǎn)生偽匹配。基于實際經(jīng)驗幀頭長度最好等于并行寬度或者更長但復雜度會高。調(diào)試過程中還有一個讓我印象極深的坑GTP的RX復位順序。如果RX復位不完全接收時鐘雖然鎖定但內(nèi)部串并轉(zhuǎn)換可能仍然處于錯誤狀態(tài)尤其當SDR模式下PMA復位和PCS復位沒有按正確順序釋放時輸出數(shù)據(jù)可能“反復跳變”。我用的順序是先復位整個收發(fā)器等待PLL鎖定然后釋放PMARX復位等待rxbyteisaligned置位最后釋放RX復位。在SDR模式下接收字節(jié)對齊信號可能不直接反映我們的7bit邊界必須用自定義對齊狀態(tài)機去替代。這是我踩了很久才理解到的點——不要依賴原生信號要自己接管。5. 常見問題與排查技巧實錄為了讓大家少走彎路我把整個過程中遇到的典型問題和對應解決辦法整理成一個排查表。這個表我每次做類似接口都會打開看一眼省了不少時間?,F(xiàn)象可能原因排查方法解決手段輸出全0或全1Serdes未鎖定RX復位未釋放檢查RXRESETDONE、PLL鎖定狀態(tài)檢查參考時鐘按正確順序復位并行數(shù)據(jù)錯位不穩(wěn)定未做word alignment觀察連續(xù)幀頭是否一致添加滑動對齊模塊搜索幀頭碼輸出數(shù)據(jù)少一個bitSDR模式下用戶時鐘分頻配錯用計數(shù)器對比每7個bit輸出一次修正RXOUT_DIV和用戶除法寄存器數(shù)據(jù)偶發(fā)跳變跨時鐘域采集導致亞穩(wěn)態(tài)做同步打拍用FIFO緩存插入兩級同步器后級全部用使能采樣幀頭偶發(fā)誤鎖幀頭碼型不具備唯一性仿真滑動窗檢查各相位混淆換用循環(huán)移位更不友好的碼型高溫或長運行后失步時鐘恢復單元失鎖檢查GT參考時鐘穩(wěn)定性減小環(huán)路帶寬增加復位檢測機制后級邏輯時序不過多時鐘約束混亂查看時序報告檢查分頻時鐘用單時鐘使能設計替代真實分頻除了表格里的問題我想再單獨說說“SDR模式下的輸出數(shù)據(jù)位序”的困惑。很多官方示例里8bit并行輸出的第一個bit往往是串行數(shù)據(jù)的MSB但在我們自定義7bit時因為時鐘分頻和移位方向的差異可能恰好相反。我在工程里統(tǒng)一用的準則是“最先進入移位寄存器的bit放在最高位”換句話說在load_data時直接用shift_reg的當前值就能滿足這個約定。但到最后發(fā)現(xiàn)后級協(xié)議需要的是“字節(jié)內(nèi)最低位先發(fā)送”這時候還得做一次位序翻轉(zhuǎn)。別把這種翻轉(zhuǎn)丟在Serdes模塊里做建議顯式在數(shù)據(jù)通路里加一個組合邏輯方便后面隨時調(diào)整assign parallel_reversed {parallel_out[0], parallel_out[1], parallel_out[2], parallel_out[3], parallel_out[4], parallel_out[5], parallel_out[6]};從排查經(jīng)驗的整體看問題最集中的還是“對齊”環(huán)節(jié)。我后來在代碼里加了一個看門狗在鎖定狀態(tài)下每隔一段時間檢查幀頭一旦連續(xù)出現(xiàn)幾次錯誤就認為失步自動重新進入搜索狀態(tài)。這個機制非常實用因為Serdes在真實環(huán)境中受到電源噪聲、溫度漂移的影響偶爾會跳一個bit有了這個自動重同步功能整個鏈路在長時間壓力測試中的可靠性明顯提高。6. 收尾與實用建議最后再分享一個小技巧如果你要做1_to_N這類非標準串并轉(zhuǎn)換不要一開始就盯著官方代碼里那一堆復雜的狀態(tài)機。先理清三個問題——數(shù)據(jù)從哪個時鐘域來、字邊界用什么碼型定義、并行輸出送給誰。把這三個問題想清楚再回頭去看XAPP585的原始代碼就會覺得那些模塊其實都圍繞這三個問題在轉(zhuǎn)只是它用了以太網(wǎng)場景來包裝。讀懂以后換到自己場景里無非是把SGMII的數(shù)據(jù)和comma碼換成你的協(xié)議幀頭而已。我建議任何要做類似功能的人都先在仿真環(huán)境里把對齊狀態(tài)機和相位搜索跑通再上板調(diào)GTP。不要為了省時間直接去調(diào)硬件那樣你很難區(qū)分是時序問題還是邏輯問題。同時在邏輯里盡量把收發(fā)器的原語包一層自己定義的wrapper把信號名統(tǒng)一改成rx_serdes_data、rx_word_valid、rx_aligned這類語義明確的名稱后續(xù)維護的人看了不累自己隔一個月再回來看也能快速上手。如果你和我一樣手上的串行數(shù)據(jù)是純突發(fā)、無編碼的自定義格式XAPP585的這套SDR自定義對齊方案幾乎是最直接有效的手段。它的核心價值不在于“串行轉(zhuǎn)并行”而在于“用任意整數(shù)位寬去切任意速率的串行流”。雖然現(xiàn)在新片子的原生支持更強了但我依然覺得把SDR模式的原理吃透能幫你應對未來各種奇奇怪怪的協(xié)議需求。