:手寫SPI主機模塊的協(xié)議解析與調(diào)試經(jīng)驗)
寫SPI的FPGA實現(xiàn)這事兒我太熟了。這幾年調(diào)試各種ADC、Flash、傳感器SPI是我在FPGA上寫過最多次的接口協(xié)議沒有之一。很多朋友一上來就找現(xiàn)成IP核或者去GitHub上扒代碼我倒覺得SPI這玩意兒把時序理清楚之后自己動手寫一個真的不難而且自寫的主機/從機模塊用起來比啥都順手。這篇就把我在實際項目中摸索出來的經(jīng)驗和思路做個總結(jié)從協(xié)議細節(jié)到Verilog實現(xiàn)再到板級調(diào)試一步步拆開聊希望能給正在搞FPGA通信接口的朋友一些參考。1. 動手之前先把SPI協(xié)議的底褲扒干凈SPI的全稱是Serial Peripheral Interface串行外設(shè)接口是Motorola公司搞出來的一種同步串行通信協(xié)議。FPGA開發(fā)里它出鏡率極高幾乎所有板上外設(shè)都留了SPI接口。它跟UART這類異步協(xié)議不一樣SPI靠獨立的時鐘線來同步收發(fā)數(shù)據(jù)所以時序天然就可預測只要時鐘沿干凈基本不需要擔心UART那種波特率誤差的問題。SPI總線上一般有四種信號線SCLK串行時鐘、MOSI主設(shè)備輸出/從設(shè)備輸入、MISO主設(shè)備輸入/從設(shè)備輸出、CS/SS片選信號。主機產(chǎn)生時鐘同時決定片選拉低哪個從設(shè)備所有通信由主機掌控。這里經(jīng)常有新手問MOSI和MISO能不能合并成一根線當然可以那就是半雙工模式三線制SPI。不過大多數(shù)普通器件都是標準的四線全雙工結(jié)構(gòu)除非是某些特殊傳感器或節(jié)省IO的應(yīng)用否則不建議自己搞三線排查問題會麻煩不少。SPI協(xié)議最核心也最容易繞暈的一點就是它的四種工作模式。這個模式由時鐘極性CPOL和時鐘相位CPHA兩個參數(shù)決定。CPOL決定空閑時SCLK是高還是低CPHA決定數(shù)據(jù)在第一個還是第二個時鐘沿被采樣。不少工程師在這里栽過跟頭因為數(shù)據(jù)手冊上寫著“Mode 3”或者“CPOL1, CPHA1”翻譯到代碼里就是一堆糾結(jié)。這兩個參數(shù)我建議這樣理解CPOL0表示空閑時時鐘是低電平CPOL1表示空閑時時鐘是高電平CPHA0表示數(shù)據(jù)在第一個時鐘邊沿采樣CPHA1表示在第二個時鐘邊沿采樣。把所有組合都列出來就是下面這張表模式CPOLCPHA采樣邊沿常見應(yīng)用場景Mode 000上升沿大多數(shù)Flash、ADC、SD卡Mode 101下降沿較少見部分傳感器Mode 210下降沿部分工業(yè)級DACMode 311上升沿SPI FlashW25Q系列常用這張表不是讓你背的是讓你查的。我開始做FPGA那會兒每次遇到新器件都要翻出這張表對照手冊確認一次。用Verilog寫主機的時候我傾向于把CPOL和CPHA做成模塊參數(shù)而不是直接寫死在代碼里。這樣遇到換器件、換模式的時候改個頂層參數(shù)就行不用動核心時序邏輯。實測下來這個習慣幫我省了非常多的時間強烈推薦。還有個關(guān)鍵概念MSB First還是LSB First。絕大多數(shù)SPI器件是MSB先行但個別器件反著來比如某些LCD驅(qū)動芯片就是LSB First。Verilog設(shè)計時需要把移位方向做成可配置否則換器件又得改代碼。我自己習慣用參數(shù)配置加上通用移位邏輯后面會放代碼詳細介紹。2. 主機還是從機先定方案再寫代碼SPI設(shè)備分主從但FPGA這邊做主機的場景占絕大多數(shù)。比如給ADC采集數(shù)據(jù)、給DAC送波形、讀寫Flash、配置傳感器全是FPGA當主機去主動發(fā)起通信。少數(shù)場景下FPGA要當從機比如跟MCU通信、被外部處理器配置但這種一般用現(xiàn)成IP核更省事自己寫從機的收益不大。我在項目里遇到最多的組合是FPGA主機 多個SPI從設(shè)備??刂扑悸泛芎唵蚊總€從設(shè)備分配一個獨立的CS信號MOSI、MISO、SCLK可以共享。但實際上這里有個坑如果多個從設(shè)備的MISO都掛在同一根線上那么必須保證同一時刻只有一個從設(shè)備的CS被拉低否則MISO上會出現(xiàn)總線沖突。更穩(wěn)妥的做法是給每個從設(shè)備加上輸出使能控制從設(shè)備沒被選中時它的MISO輸出必須處于高阻態(tài)。這一點在選型和設(shè)計階段就要確認清楚有些器件的手冊會注明“MISO is tri-stated when CS is high”但有些器件不會遇到后者只能軟件上額外處理。關(guān)于硬件片選和軟件片選我也簡單說說。硬件片選就是CS直接由FPGA的GPIO控制拉低即選中拉高即釋放簡單直接。軟件片選則是在有些場景下希望通過發(fā)送特定的命令字來切換從設(shè)備或者片選信號由某個移位寄存器的輸出去控制這樣做的好處是省IO壞處是時序復雜度上來了而且一旦軟件跑飛總線可能一直掛著。我個人的建議是在FPGA資源允許的情況下老老實實用硬件片選一個設(shè)備一個CS引腳代碼清晰時序可控排查問題也方便。只有在GPIO嚴重不足的極端場景下才考慮軟件片選方案。主從方案定了之后接下來就是傳輸速率和SCLK頻率的規(guī)劃。SPI的速率上限取決于從設(shè)備能跑多快以及FPGA內(nèi)部邏輯能不能跟上。比如W25Q64這款Flash新的版本支持104MHz甚至更高但老版本或者走長飛線的時候就只能跑20MHz以內(nèi)。FPGA側(cè)生成SCLK有兩種思路一種是直接用系統(tǒng)時鐘分頻得到整數(shù)分頻的SCLK另一種是使用PLL/MMCM產(chǎn)生精確的時鐘。前者適合低速通信后者的抖動特性更好適合告訴傳輸。這里有個容易被新手忽略的問題分頻產(chǎn)生的SCLK相位抖動和占空比不對稱在高速模式下會限制通信距離。如果只是跑一個板級Flash30MHz以下隨便分頻都能用如果是要拉長線跟外部設(shè)備通信或者SCLK超過50MHz就必須用PLL去生成干凈時鐘了。我在一個需要跟外部工業(yè)設(shè)備通信的項目里吃過這個虧代碼看著沒問題就是偶發(fā)傳錯數(shù)據(jù)最后用示波器一測發(fā)現(xiàn)是SCLK在邊沿附近有毛刺改成PLL出時鐘后問題立刻消失。3. RTL實戰(zhàn)寫一個參數(shù)化的SPI主機模塊我平時寫SPI主機核心就是一個帶狀態(tài)機的移位寄存器。別看網(wǎng)上講得玄乎其實就三步拉低CS按位收發(fā)數(shù)據(jù)拉高CS。復雜點的場景會加FIFO做緩沖加DMA做數(shù)據(jù)搬運但通信內(nèi)核沒變。先上一個最精簡的四線主機代碼框架這也是我項目里反復在用、逐步完善的版本module spi_master #( parameter CPOL 1, // 時鐘極性 parameter CPHA 1, // 時鐘相位 parameter DATA_WIDTH 8, // 數(shù)據(jù)位寬 parameter SCLK_DIV 4, // SCLK clk / (SCLK_DIV*2) parameter MSB_FIRST 1 // 1: MSB先行, 0: LSB先行 )( input wire clk, input wire rst_n, input wire start, input wire [DATA_WIDTH-1:0] tx_data, output reg [DATA_WIDTH-1:0] rx_data, output reg busy, output reg sclk, output reg cs_n, output reg mosi, input wire miso ); localparam IDLE 2d0; localparam TX 2d1; localparam DONE 2d2; reg [1:0] state; reg [7:0] clk_cnt; reg [3:0] bit_cnt; reg sclk_r; wire sclk_rise (sclk_r 1b0) (sclk 1b1); wire sclk_fall (sclk_r 1b1) (sclk 1b0); always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; busy 0; cs_n 1; sclk CPOL; mosi 0; bit_cnt 0; clk_cnt 0; end else begin case (state) IDLE: begin cs_n 1; busy 0; if (start) begin cs_n 0; busy 1; bit_cnt 0; clk_cnt 0; state TX; end end TX: begin // SCLK 分頻生成 if (clk_cnt SCLK_DIV - 1) begin clk_cnt 0; sclk ~sclk; end else begin clk_cnt clk_cnt 1; end // 發(fā)送和接收數(shù)據(jù) if (sclk_fall) begin if (MSB_FIRST) mosi tx_data[DATA_WIDTH - 1 - bit_cnt]; else mosi tx_data[bit_cnt]; end if (sclk_rise) begin if (MSB_FIRST) rx_data[DATA_WIDTH - 1 - bit_cnt] miso; else rx_data[bit_cnt] miso; if (bit_cnt DATA_WIDTH - 1) state DONE; else bit_cnt bit_cnt 1; end end DONE: begin cs_n 1; busy 0; state IDLE; end endcase end end always (posedge clk or negedge rst_n) begin if (!rst_n) sclk_r CPOL; else sclk_r sclk; end endmodule這段代碼默認支持CPOL/CPHA參數(shù)化采樣和更新沿也是通過sclk_rise和sclk_fall這兩個脈沖信號來統(tǒng)一控制的。有個細節(jié)值得注意我在發(fā)送數(shù)據(jù)的時候用sclk_fall更新MOSI在sclk_rise采樣MISO這樣能保證數(shù)據(jù)變化沿和采樣沿錯開半個周期從設(shè)備端更不容易采到跳變中的數(shù)據(jù)。當然如果你的CPHA配置不同這兩個沿的對應(yīng)的采樣關(guān)系會變但只要抓住“數(shù)據(jù)在采樣沿穩(wěn)定即可”這個原則代碼微調(diào)一下就能適配所有模式。這個模塊在功能上是能用的但實際項目里我不會直接這么裸用。因為真實場景下需要處理連續(xù)傳輸、FIFO對接、多設(shè)備切換等復雜邏輯所以我會在頂層再包一層狀態(tài)機負責裝配命令、啟動傳輸、校驗返回值。比如在Flash芯片的讀操作里主機先發(fā)送讀命令和24位地址然后再產(chǎn)生額外的時鐘來讀數(shù)據(jù)。你會發(fā)現(xiàn)一個單次的8位SPI主機是不夠用的需要把單次傳輸?shù)奈粚挃U展或者連續(xù)多次調(diào)用模塊接口。我后來把模塊擴展成可配置位寬最長支持64位單次傳輸這樣不管讀Flash還是讀ADC狀態(tài)寄存器都夠用了。再說從機實現(xiàn)。FPGA做從機代碼上不復雜但驗證起來麻煩。核心思路是借助外部時鐘直接采樣數(shù)據(jù)。但有一種場景我會特別注意就是從機是FPGA主機是MCU或DSP時鐘線上可能有毛刺或者電平不穩(wěn)這時候需要加輸入同步器和邊沿檢測。我的做法是先把SCLK打兩拍同步到系統(tǒng)時鐘域然后再取上升沿和下降沿。注意這里的同步打拍會引入兩個時鐘周期的延遲對從機來說問題不大因為SPI本身就是主機主導的協(xié)議主機發(fā)送的時序不會等從機反饋。但如果你要在這個場景下做高精度的回環(huán)測試就要把這幾個周期的延遲算進去了。4. 上板調(diào)試別急著接外設(shè)先用仿真和ILA把時序跑穩(wěn)我在工程調(diào)試中養(yǎng)成了一個習慣任何SPI模塊先做仿真再用ILA集成邏輯分析儀實測最后才接真實外設(shè)。這三步順序不能亂否則出了問題很難定位是邏輯錯了、時序約束不夠還是外部器件本身響應(yīng)異常。仿真階段的重點是檢查波形邊沿關(guān)系。我給SPI模塊搭的testbench會生成一個模擬從機響應(yīng)這樣能驗證MISO數(shù)據(jù)的接收路徑。很多人只在發(fā)送側(cè)看MOSI波形忽略了回讀校驗實際上SPI是雙向的仿真階段把回讀路徑打通后面接真實器件會順暢很多。用Vivado的同學仿真完之后記得拉一個ILA核來抓實際波形。我這里說幾個ILA調(diào)試的小技巧觸發(fā)條件可以用start信號的上升沿也可以用cs_n下降沿看你要觀察哪個階段采樣深度不用開太大SPI波形比較規(guī)律512或1024足夠如果需要觀察SCLK邊沿附近的毛刺ILA的采樣時鐘必須比SCLK高4倍以上才有意義否則采出來的波形是混疊的別把ILA的probe加太多我見過有人把每個內(nèi)部寄存器都拉出來最后調(diào)試界面根本看不過來。實測階段有一個經(jīng)驗每次接新板子我都會先做一個最簡單的回環(huán)測試把MOSI直接短接到MISO然后讓FPGA發(fā)送一組遞增數(shù)據(jù)并回讀比對是否一致。這個測試做通了說明主機的發(fā)送、接收、時序都沒問題之后再去接外部器件。回環(huán)測試通過之后再去連真實從設(shè)比如Flash芯片先讀JEDEC ID0x9F命令如果ID能讀出來通常說明菜鳥階段最擔心的“時序不對”已經(jīng)過去了后面就是根據(jù)具體協(xié)議操作寄存器的事了。我遇到很多新手上來就懟著W25Q64的讀數(shù)據(jù)操作調(diào)半天結(jié)果發(fā)現(xiàn)寄存器配置都還沒理清。所以這里特別強調(diào)接口調(diào)通和芯片功能調(diào)通是兩回事。接口調(diào)通靠的是SPI邏輯正確芯片功能調(diào)通需要吃透芯片手冊。建議先從最簡單的操作入手一步一步來。5. 便宜好用的“接口測試利器”邏輯分析儀和必要的上拉電阻調(diào)試SPI最痛苦的事情之一是遇到“看起來波形都對但數(shù)據(jù)不對”的情況。這里我強烈建議備一個便宜的邏輯分析儀不需要什么高級貨市面上幾十塊錢的USB邏輯分析儀就行采樣率24MHz的夠用了。用邏輯分析儀去抓SCLK、MOSI、MISO、CS四根線跟用ILA相比有一個好處不占用FPGA內(nèi)部資源也不需要重新綜合布線而且能看到真實的引腳電平而不是內(nèi)部信號。用邏輯分析儀時要注意探頭的輸入電容會引入信號邊沿變緩。低速率比如幾MHz下影響不大但速率一高波形邊沿可能變得不那么陡峭容易誤判。所以排查高速SPI問題最好還是用示波器邏輯分析儀適合看協(xié)議層示波器適合看物理層兩者各有分工。另外特別想說一下上拉電阻的問題。SPI的MOSI、SCLK、CS一般由主機強驅(qū)動不需要上拉。但MISO這條線如果從設(shè)備支持三態(tài)輸出建議加一個10kΩ左右的上拉電阻避免在所有從設(shè)備都未被選中時MISO浮空。浮空的輸入引腳在邏輯器件看來是高電平還是低電平是不確定的容易導致主機在非通信期間采到隨機數(shù)據(jù)。如果板上已經(jīng)預留了上拉位置就焊上沒有預留就在FPGA內(nèi)部把MISO引腳的弱上拉打開。當然如果所有從設(shè)備的MISO都是推挽輸出且不會同時使能可以不處理但這個習慣養(yǎng)成了以后做項目會少很多奇怪的問題。說到這順便提一個容易踩的坑FPGA和外部設(shè)備的電平標準不一致。現(xiàn)在很多傳感器和Flash是1.8V或2.5V供電而FPGA的Bank電壓可能是3.3V。直接相連輕則通信異常重則燒引腳。做SPI對接之前一定先查清楚兩邊IO的電平標準和Bank電壓設(shè)置。Xilinx 7系列FPGA的HR Bank支持1.2V到3.3VHP Bank最高支持1.8V選Bank和分配引腳時就得考慮電平匹配不能隨便找一個空閑引腳就分配出去。6. 常見問題排查與波形實例我把自己和身邊朋友做SPI時踩過的一些典型坑整理成了下面這張表遇到問題直接按圖索驥問題現(xiàn)象可能原因排查方法通信完全沒反應(yīng)MISO一直高電平CS信號沒拉低、從設(shè)備供電異常、MISO上拉導致空閑被拉高先查從設(shè)備電源再用邏輯分析儀看CS有沒有正確拉低數(shù)據(jù)偶爾錯一位或錯幾位SCLK頻率過高、走線過長導致信號質(zhì)量差、時鐘采樣沿選錯降低SCLK頻率試一下再用示波器看SCLK邊沿是否惡化讀回來的數(shù)據(jù)全FF或全00SPI模式不匹配CPOL/CPHA配錯、從設(shè)備沒被正確喚醒對著手冊確認Mode用回環(huán)測試驗證主機側(cè)多個從設(shè)備掛在總線上時互相干擾某個從設(shè)備MISO沒有三態(tài)導致總線沖突逐個拉CS測試判斷是哪個設(shè)備在搗亂高速通信時波形有回溝信號反射、阻抗不匹配、走線過孔過多降低速率先驗證功能再優(yōu)化PCB走線或串聯(lián)33Ω電阻這里重點講一下SPI模式不匹配的問題。你寫代碼的時候CPOL和CPHA各配一個參數(shù)看著很簡單但一旦碰到那種“必須完全匹配器件手冊才能工作”的從設(shè)備就很容易抓狂。我記得有次調(diào)試一顆ADC數(shù)據(jù)手冊明明寫著Mode 3結(jié)果按Mode 3配怎么讀都是亂碼后來用邏輯分析儀對比了廠家驅(qū)動板卡的波形才發(fā)現(xiàn)它實際是Mode 0。這種情況雖不常見但一旦遇到記住示波器和邏輯分析儀上看到的實際數(shù)據(jù)才是王道別迷信手冊。另一個高頻問題就是跨時鐘域。如果你的SPI主機模塊工作在100MHz系統(tǒng)時鐘下而從設(shè)備的響應(yīng)時間是微妙級別那么在狀態(tài)機設(shè)計時必須有超時處理機制否則從設(shè)備無響應(yīng)時整個模塊會卡在某個狀態(tài)出不來。我的做法是在狀態(tài)機里加一個超時計數(shù)器超過閾值自動回到IDLE并置位錯誤標志。這個在ADC或Flash等響應(yīng)時間不確定的器件上特別實用。7. 進階思路FIFO緩沖、DMA和時鐘域隔離到這里你已經(jīng)能用自寫的SPI主機跟Flash、ADC、傳感器打交道了。但遇到高吞吐量的應(yīng)用比如高速數(shù)據(jù)采集或圖像數(shù)據(jù)讀取純靠CPU / 狀態(tài)機搬運數(shù)據(jù)就不夠用了。這時候就需要在SPI主機外面再加兩層東西FIFO和DMA控制器。FIFO的作用是緩沖數(shù)據(jù)避免主機和SPI之間頻繁握手。主機模塊負責往FIFO里寫讀到的數(shù)據(jù)DMA或者上層讀邏輯再從FIFO里批量取走。這個結(jié)構(gòu)能讓SPI一直跑在滿速率狀態(tài)不會因為等待取走數(shù)據(jù)而停頓。DMA控制器跟SPI對接的時候主要注意兩點一是FIFO的空滿標志要能正確觸發(fā)DMA請求二是DMA傳輸結(jié)束之后要能產(chǎn)生中斷或標志位通知上層軟件處理。在Xilinx平臺上通常的做法是用AXI4-Stream接口把SPI數(shù)據(jù)流接到AXI DMA IP核上這樣能跟PS側(cè)內(nèi)存直接打交道。不過這套做法的學習曲線比較陡建議基礎(chǔ)功能跑通之后再一步步加。時鐘域隔離是另一個容易被忽視的點。假設(shè)你的SPI跑在25MHz而系統(tǒng)總線跑在150MHz如果直接讓跨時鐘域信號在系統(tǒng)邏輯里使用可能需要加同步器或者異步FIFO來處理。我在使用專用IP核時一般都能滿足這個需求但如果自己寫主機還接了很多外設(shè)就需要注意時鐘域的約束問題。Vivado里可以建不同的時鐘域約束文件把SCLK分頻時鐘和主時鐘定義清楚不然綜合結(jié)果可能不理想。芯片設(shè)計里常說“時序收斂”FPGA開發(fā)也一樣只是層次不同。8. 拓展應(yīng)用SPI與I2C怎么選以及后續(xù)擴展方向很多朋友在項目選型時會問SPI和I2C到底用哪個其實沒有絕對更好的答案只有更合適的應(yīng)用場景。SPI優(yōu)點是速率高、全雙工、時序簡單缺點是引腳多4根起步且每個從設(shè)備都要單獨拉一根CS。I2C優(yōu)點就是省引腳兩根線上能掛一票設(shè)備還有設(shè)備地址機制缺點是速率低、半雙工、時序協(xié)議相對復雜還需要處理ACK仲裁之類的問題。如果只是在FPGA內(nèi)部跟板級器件打交道而且速率要求不低SPI壓倒性勝出。但如果是要跟一堆低速傳感器交互比如溫度傳感器、EEPROMI2C可能更合適。我之前有一塊板子板級溫度監(jiān)控用的是I2C數(shù)據(jù)采集鏈路用的是SPI各司其職互不沖突。所以別糾結(jié)誰取代誰作為FPGA開發(fā)者我建議SPI和I2C的接口代碼都能隨手寫出基本功扎實了后續(xù)擴展做什么都順。至于SPI的后續(xù)擴展方向有兩條我覺得值得關(guān)注一個是把SPI主機封裝成AXI4-Lite或AXI4-Stream接口方便做SoC集成這也是從邏輯設(shè)計走向系統(tǒng)設(shè)計必須跨過的一步另一個是在高速SPI場景下去研究硬核SPI控制器比如Zynq UltraScale里的SPI IP功耗和時序表現(xiàn)都比純邏輯好很多。FPGA開發(fā)就是這樣先把基礎(chǔ)接口吃透后面無論做什么項目這些底子都會成為你最穩(wěn)定的“技術(shù)底盤”。我個人在實際項目中還有一個習慣就是每做好一個SPI模塊就把它的Testbench、約束文件和調(diào)試筆記歸檔到一個模板工程里。下次遇到類似的從設(shè)備直接在這個模板上改參數(shù)半天就能把接口調(diào)通。這比每次從零開始寫要快得多而且隨著沉淀的模塊越來越多后續(xù)項目的開發(fā)速度會有明顯提升。分享出來希望能幫到你。