p壞:從讀取流程到寄存器配置的完全排查)
LPS22DF first FIFO element corruption這串字符是我前幾天在搜索ST官方社區(qū)時完整輸入的一個搜索詞。原因是當(dāng)時手上的LPS22DF氣壓傳感器出現(xiàn)了非常詭異的現(xiàn)象每次讀FIFO數(shù)據(jù)批次的第一個樣本要么是一個明顯越界的壓力突變值要么直接是0x00而后續(xù)樣本全部正常。FIFO模式無論怎么切換問題都穩(wěn)定出現(xiàn)在“首元素”上。如果你也遇到過類似的傳感器FIFO首數(shù)據(jù)異常大概率會懷疑芯片體質(zhì)或者layout干擾但這個坑其實多半是讀取流程和寄存器配置的細節(jié)問題。這篇文章會從FIFO的物理工作方式講起把首元素?fù)p壞的幾種可能性一條條拆開最后給出一個可以直接抄的穩(wěn)定讀取方案。1. 先搞清LPS22DF的FIFO到底是“怎么彈”的1.1 采樣怎么進FIFO、讀的時候又在發(fā)生什么LPS22DF是ST的一款高精度氣壓傳感器量程260到1260 hPa絕對精度標(biāo)稱0.5 hPa內(nèi)部帶了一個深度512層的FIFO。我一開始以為這個FIFO就是普通的內(nèi)存緩沖區(qū)跟MCU里那個FIFO一樣讀的時候地址不變、數(shù)據(jù)不變隨便讀多少次都行。實際上這里是完全不同的邏輯。LPS22DF的FIFO本質(zhì)上是一個“彈出式”隊列。數(shù)據(jù)按照設(shè)置的ODR速率寫入FIFOODR最高能到200 Hz也就是每5毫秒寫入一個樣本。每個FIFO元素是32位由24位壓力原始值和8位溫度原始值拼接而成。讀取的時候你訪問的是FIFO_DATA_OUT寄存器地址是0x78這個寄存器比較特殊讀一次就彈出一個字節(jié)內(nèi)部讀指針自動前進一次。想要取完一個完整樣本必須連續(xù)讀4次——先讀壓力低字節(jié)再讀壓力高字節(jié)再讀壓力擴展字節(jié)最后讀溫度字節(jié)。這個“讀一次動一次”的機制和普通狀態(tài)寄存器有本質(zhì)區(qū)別。普通寄存器是無狀態(tài)的每次讀都一樣FIFO寄存器是有副作用的讀就相當(dāng)于消費了一個數(shù)據(jù)。一旦讀取過程中多讀了一個字節(jié)或者少讀了一個字節(jié)后面所有數(shù)據(jù)都會錯位。而錯位的結(jié)果往往不是“全部亂掉”而是“第一個數(shù)據(jù)看起來像壞了后面的反而對齊了”因為錯位會在后續(xù)讀取中被重新同步。這個反直覺的現(xiàn)象正是很多首元素異常問題的來源。1.2 為什么偏偏是“第一個元素”容易出事FIFO內(nèi)部是一個典型的狀態(tài)機狀態(tài)包括寫指針、讀指針、當(dāng)前水位、溢出標(biāo)志、空標(biāo)志。第一個元素是狀態(tài)機從“空”過渡到“非空”的臨界產(chǎn)物任何指針初值不確定、模式切換瞬態(tài)、跨時鐘域同步未完成都會先在這個元素上暴露出來。還有個容易被忽略的點LPS22DF的數(shù)據(jù)寫入FIFO用的是傳感器內(nèi)部的采樣時鐘而讀取FIFO用的是MCU側(cè)的SPI或I2C時鐘。這兩個時鐘是完全異步的??鐣r鐘域讀數(shù)據(jù)時第一個元素恰好處于寫入側(cè)和讀取側(cè)交接的邊界如果寫入剛剛完成、讀取馬上開始采樣數(shù)據(jù)在內(nèi)部總線上可能還沒穩(wěn)定下來讀出來的就是不穩(wěn)定值。從模塊設(shè)計角度講這就是一個典型的同步FIFO跨時鐘域問題和FPGA里異步FIFO處理不好會出首字錯誤是同一個道理。所以要排查這類問題第一反應(yīng)不應(yīng)該是對著芯片手冊懷疑“FIFO壞了”而是先想清楚我的讀取流程讓狀態(tài)機處于什么狀態(tài)我有沒有在FIFO還沒準(zhǔn)備好數(shù)據(jù)的時候就去讀我有沒有在模式切換的瞬間去讀理解了這一點后面的排查思路就順了。2. 嫌疑最大的一類讀時序和寄存器地址的處理2.1 0x78彈出式讀與0x28實時讀的本質(zhì)區(qū)別我見過不少同事在這上面翻車。LPS22DF除了FIFO_DATA_OUT0x78之外還有一組實時輸出寄存器PRESS_P_OUT_L0x28、PRESS_P_OUT_H0x29、PRESS_P_OUT_XL0x2A、TEMP_OUT_L0x2B。這一組寄存器讀的是當(dāng)前時刻的最新采樣值不經(jīng)過FIFO讀多少次都一樣。問題在于這兩個地址看起來很像有些人讀FIFO的時候寄存器地址寫成了0x28或者用ST官方驅(qū)動的時候誤把實時數(shù)據(jù)讀接口當(dāng)成了FIFO讀接口。讀出來是什么表現(xiàn)當(dāng)你連續(xù)讀0x28時拿到的是當(dāng)前最新的實時壓力不是FIFO里緩存的歷史樣本。如果FIFO里攢了一堆數(shù)據(jù)你的讀取結(jié)果是第一個值可能是某個中間時刻的實時值后面跟著的才是FIFO里彈出來的值這就會表現(xiàn)出“第一個元素和后續(xù)元素不連續(xù)”。另一個坑是IF_ADD_INC位。這個位在CTRL3寄存器里默認(rèn)通常是使能的作用是地址自動遞增。如果使能你連續(xù)讀0x78時會自動切換到0x79、0x7A、0x7B如果為了省事或者為了兼容性關(guān)掉了這個位那么連續(xù)讀4次0x78拿到的可能是同一個字節(jié)——因為每次讀的都是同一個地址。表現(xiàn)出來就是FIFO數(shù)據(jù)完全不對第一個字節(jié)被反復(fù)讀像極了“corruption”。我之前排查過一個類似問題最后發(fā)現(xiàn)就是一個工程師為了匹配某個第三方驅(qū)動把IF_ADD_INC關(guān)了導(dǎo)致FIFO讀出來的字節(jié)順序全是亂的。2.2 SPI模式下地址字節(jié)被當(dāng)成數(shù)據(jù)的經(jīng)典誤讀這是SPI接口下最容易踩的坑而且專門坑第一批讀出來的數(shù)據(jù)。SPI是全雙工協(xié)議MCU在發(fā)一個字節(jié)的同時會收到一個字節(jié)。標(biāo)準(zhǔn)讀操作是拉低CS發(fā)送8位地址最高位為1表示讀然后從第9個SCK開始從機在MISO線上輸出數(shù)據(jù)字節(jié)。問題來了MCU發(fā)送地址字節(jié)的這8個SCK周期里MISO線上也在輸出東西。LPS22DF在這個階段會輸出什么可能是0x00可能是上電后的隨機值也可能是上一次操作的殘留值。如果驅(qū)動實現(xiàn)得粗糙——尤其是一些手寫的SPI驅(qū)動在DMA模式下一次性發(fā)送N1個字節(jié)把地址字節(jié)和數(shù)據(jù)字節(jié)放在同一個緩沖區(qū)里同時把收到的所有字節(jié)都當(dāng)成有效數(shù)據(jù)——那么第一個數(shù)據(jù)字節(jié)就是地址階段的MISO垃圾值。地址是0x78讀命令是0xF8這8個SCK期間MISO上如果出來一個無效字節(jié)它就會被當(dāng)成第一個FIFO數(shù)據(jù)字節(jié)。正確做法是發(fā)完地址字節(jié)后丟棄這一輪收到的字節(jié)從第9個SCK開始才記錄數(shù)據(jù)。用邏輯分析儀很容易看出來地址階段MISO上有一個明顯的無效低電平或隨機波形而第9個SCK之后才是有效數(shù)據(jù)。如果軟件上不好改也可以用SPI接口的硬件NSS和SCK延時配合讓CS拉低到第一個數(shù)據(jù)SCK之間有足夠的準(zhǔn)備時間。還有一個更隱蔽的SPI mode配置錯了。LPS22DF支持SPI Mode 0即CPOL0、CPHA0時鐘空閑為低數(shù)據(jù)在上升沿采樣。如果配成Mode 1或者Mode 2/3第一個字節(jié)的采樣點可能落在數(shù)據(jù)線電平切換的瞬間MISO采出來的就是不確定值。這個通常表現(xiàn)為首字節(jié)錯誤后面的字節(jié)因為相位差反而不一定錯——又是一個“首元素?fù)p壞”的迷惑現(xiàn)場。2.3 I2C模式下的指針與連續(xù)讀問題I2C接口下讀取FIFO的標(biāo)準(zhǔn)流程是先發(fā)設(shè)備地址加寫位然后發(fā)寄存器地址0x78接著發(fā)repeated start再發(fā)設(shè)備地址加讀位然后連續(xù)讀4個字節(jié)。如果驅(qū)動漏掉了repeated start直接把0x78當(dāng)成設(shè)備地址發(fā)送從機會返回NACK讀出來的第一個字節(jié)是垃圾值。這種屬于驅(qū)動寫錯不算芯片問題但現(xiàn)象幾乎一樣。I2C還有一個問題設(shè)備地址的7位是0x5C還是0x5D取決于SA0引腳電平。如果用錯了地址一般表現(xiàn)為設(shè)備不響應(yīng)I2C讀操作失敗。但如果驅(qū)動對錯誤有容錯處理比如超時后返回0xFF之類讀出來的FIFO數(shù)據(jù)里就會出現(xiàn)一個異常首字節(jié)。有些單片機在硬件I2C上做FIFO讀取時DMA收到最后一個字節(jié)后的NACK時序沒處理好會多采一個字節(jié)造成FIFO讀指針錯位。這個問題在高頻I2C400 kHz下更容易出現(xiàn)。I2C模式下如果遇到首元素異常我會建議先把I2C時鐘降到100 kHz試一下。如果問題消失說明是時序邊緣問題如果問題還在就回過來查驅(qū)動流程。實測下來LPS22DF的I2C接口相對SPI更穩(wěn)定一些首元素異常大部分還是驅(qū)動流程的問題。3. 疑似配置問題模式切換、WTM水位和復(fù)位時序3.1 FMODE切換后FIFO殘留數(shù)據(jù)沒有清掉LPS22DF的FIFO_CTRL寄存器0x14里有一個FMODE字段可以設(shè)置多種工作模式Bypass、FIFO、Continuous-to-FIFO、Bypass-to-FIFO、Continuous、Bypass-to-Continuous。每種模式對FIFO的寫入規(guī)則不同。比如Bypass模式下FIFO根本不緩存數(shù)據(jù)直接輸出Continuous模式下FIFO滿后新數(shù)據(jù)覆蓋舊數(shù)據(jù)FIFO模式下FIFO滿后停止寫入新數(shù)據(jù)。很多人切換模式時只改了FMODE位沒有考慮FIFO內(nèi)部指針的狀態(tài)。比如在Bypass模式下跑了一段時間然后直接切到FIFO模式FIFO里可能殘留著之前模式下未清空的舊數(shù)據(jù)讀指針也可能停在一個未知位置。切到FIFO模式后你讀到的第一個元素就是殘留的舊數(shù)據(jù)。這個舊數(shù)據(jù)的產(chǎn)生時間、對應(yīng)的氣壓值都有可能和當(dāng)前環(huán)境完全不符看起來就是一個典型的“損壞元素”。處理方式很直接每次切換FMODE之后先把FIFO讀空一直讀到FIFO_STATUS寄存器里的FIFO_EMPTY位置1再開始正常采集。不要指望芯片在切換模式時自動清空FIFO至少從實測看有時候它不會清。3.2 WTM_POINT設(shè)成0或1時的競爭窗口FIFO_WTM寄存器0x15的WTM_POINT字段決定水印閾值。當(dāng)FIFO里的樣本數(shù)達到這個閾值時會觸發(fā)水印中斷。很多人想當(dāng)然地認(rèn)為水印閾值設(shè)得越小越好設(shè)成0或1這樣FIFO一有數(shù)據(jù)就能立刻通知MCU延遲最低。但這也是首元素?fù)p壞的高發(fā)配置。我當(dāng)時就踩了WTM1的坑。FIFO里剛寫入第一個樣本時中斷就觸發(fā)了MCU中斷響應(yīng)進來后開始讀FIFO。但由于ODR可能還在繼續(xù)跑第二個樣本正在寫入而我的讀取函數(shù)是一口氣讀4個字節(jié)的如果第二個樣本的寫入剛好打斷了第一個樣本的讀出讀出來的字節(jié)就是新舊混拼的。首元素自然就壞了。這個現(xiàn)象不是每批都會出現(xiàn)而是概率性的恰好卡在寫入和讀取的競爭窗口里才會發(fā)生。解決思路不是靠運氣而是給FIFO留出緩沖。WTM_POINT建議至少設(shè)為4或者更大讓FIFO攢夠一批數(shù)據(jù)再觸發(fā)中斷。這樣即使中斷響應(yīng)有點延遲FIFO里已經(jīng)有足夠多的完整樣本讀取時可以避免遇到正在寫入的邊界數(shù)據(jù)。實測下來WTM設(shè)為8到16之后首元素異常的概率基本降到了零。3.3 SWRESET之后立即讀的隱藏風(fēng)險LPS22DF的CTRL2寄存器里有一個SWRESET位寫1觸發(fā)軟件復(fù)位。軟件復(fù)位會重新初始化內(nèi)部邏輯和校準(zhǔn)參數(shù)這個過程需要時間數(shù)據(jù)手冊一般會給出tBOOT的參考值。但很多人復(fù)位后沒有等待足夠長的時間就去配置FIFO、讀取數(shù)據(jù)。復(fù)位瞬態(tài)期間內(nèi)部采樣邏輯可能還在穩(wěn)定過程中FIFO里可能會被寫入非法的采樣值。這些非法值就成了FIFO的第一個元素。另外CTRL2里還有一個BOOT位寫1會重新加載校準(zhǔn)參數(shù)。這個操作同樣需要時間。如果把BOOT當(dāng)成普通的配置位寫完立刻去讀FIFO首元素也容易異常。穩(wěn)妥的操作是軟件復(fù)位或BOOT操作之后至少等待10毫秒再操作FIFO相關(guān)寄存器。如果MCU的初始化流程比較緊湊建議在FIFO初始化之前加一個固定的延時函數(shù)別用“寫完了就算完了”的思路。4. 一套可復(fù)現(xiàn)的排查鏈路與驗證方法4.1 先把SPI/I2C原始字節(jié)流抓出來遇到首元素異常不要急著改代碼先上邏輯分析儀。我在這類問題上吃過虧憑感覺改了一堆配置最后發(fā)現(xiàn)是SPI讀取流程把地址階段的字節(jié)誤計了。用邏輯分析儀抓CS、SCK、MISO、MOSI四根線把一次完整的FIFO讀取事務(wù)抓下來逐字節(jié)對照。具體看幾個點第一CS拉低到第一個SCK上升沿之間有沒有足夠的建立時間第二地址字節(jié)傳輸期間MISO線上是什么電平第三從第9個SCK開始MISO輸出的第一個字節(jié)是否和軟件記錄的第一個字節(jié)一致。如果軟件記錄的字節(jié)比MISO上真正數(shù)據(jù)字節(jié)多了一個或者少了一個問題就清楚了。I2C的話抓SCL和SDA重點看有沒有repeated start、讀操作時從機是否有ACK、DMA讀取結(jié)束時有沒有多余的一個SCL脈沖。I2C的時序問題在邏輯分析儀上非常直觀一眼就能看出來。4.2 用FIFO_STATUS的EMPTY/LEVEL位做判據(jù)而不是依賴定時另一個常見問題是想當(dāng)然地按固定次數(shù)讀FIFO。比如FIFO里只有3個樣本你卻循環(huán)讀了10次或者你按水印中斷觸發(fā)后以為FIFO里至少有8個樣本實際上可能只有7個。多讀或者少讀都會導(dǎo)致FIFO指針錯位。正確的方法是每次讀FIFO之前先讀FIFO_STATUS寄存器0x16查看FIFO_EMPTY位和FIFO_LEVEL字段。如果FIFO_EMPTY為1說明FIFO已經(jīng)空了不應(yīng)該再讀FIFO_DATA_OUT。如果FIFO_LEVEL顯示只有N個樣本就只讀N個。把這個邏輯放到讀取循環(huán)里比任何定時估算都可靠。有一個細節(jié)要注意FIFO_STATUS里的FIFO_OVR位也就是溢出位代表FIFO曾經(jīng)發(fā)生過溢出數(shù)據(jù)可能已經(jīng)丟棄了一部分。如果檢測到溢出位為1建議清空FIFO并重新開始讀取而不是繼續(xù)從錯位的數(shù)據(jù)里恢復(fù)。這種情況下首元素異常往往只是表象真正的問題是溢出錯位。4.3 靜止態(tài)對比實驗實時寄存器與FIFO值的差如果你的問題還在排查中可以做一個簡單的靜止態(tài)實驗。把傳感器放在桌面上環(huán)境壓力基本穩(wěn)定然后用兩種方式分別讀數(shù)據(jù)一是讀實時輸出寄存器0x28到0x2B二是讀FIFO。實時寄存器的值可以作為參考基準(zhǔn)。連續(xù)讀幾十個FIFO樣本把壓力原始值換算成物理值畫出曲線。正常情況下所有樣本應(yīng)該在環(huán)境壓力附近小幅波動比如幾個Pa的噪聲。如果第一個樣本偏離了幾百Pa甚至幾千Pa那就是異常。接下來把每個批次的首元素單獨打印出來看是不是每次都是同一個固定值——如果是大概率是讀取流程把某個固定的垃圾值讀進來了如果每次都不一樣可能是競爭窗口導(dǎo)致的亞穩(wěn)態(tài)。還可以做一個分組對比實驗批量讀取時人為丟棄第一個樣本然后用第二個到最后一個樣本來計算均值和方差。如果丟棄首元素之后數(shù)據(jù)全部正常說明FIFO數(shù)據(jù)本身沒問題異常只出在第一個元素的讀取過程。這個實驗?zāi)軒湍惆选皵?shù)據(jù)問題”和“讀取問題”區(qū)分開。5. 修復(fù)方案與長期預(yù)防5.1 模式切換后清空FIFO的標(biāo)準(zhǔn)流程綜合前面的排查我最后采用的修復(fù)方案很樸素切換模式后先清FIFO再使能水印中斷。具體流程如下void lps22df_fifo_clear(void) { uint8_t dummy[4]; uint8_t status; do { status lps22df_read_reg(LPS22DF_FIFO_STATUS); if (status LPS22DF_FIFO_EMPTY_MASK) { break; } lps22df_read_burst(LPS22DF_FIFO_DATA_OUT, dummy, 4); } while (1); }注意每次讀4個字節(jié)因為一個FIFO樣本就是4個字節(jié)。清FIFO的時候不能一個一個字節(jié)地清要按樣本粒度清否則讀到一半樣本就停止了殘留的還是半個樣本。在初始化函數(shù)里順序是先配置FIFO工作模式和水位再調(diào)用清FIFO函數(shù)最后才使能水印中斷。這個順序很重要——如果先使能中斷再清FIFO清FIFO的過程中會觸發(fā)一堆水印中斷ISR里還要去處理這些無效數(shù)據(jù)處理不好反而引入新問題。5.2 中斷讀取時的臨界區(qū)與WTM推薦值水印中斷觸發(fā)后FIFO里的樣本還在按照ODR持續(xù)寫入。ISR里讀取FIFO的過程應(yīng)該盡量快不要在ISR里做耗時的數(shù)據(jù)處理。如果MCU支持DMA建議用DMA把FIFO_DATA_OUT的讀取做成一個事務(wù)一次性搬走需要的字節(jié)數(shù)。DMA讀取的好處是字節(jié)之間沒有軟件延遲不會因為中斷嵌套把4字節(jié)樣本的讀取過程切碎。如果MCU的SPI外設(shè)支持FIFO突發(fā)讀取模式用硬件NSS拉低整個事務(wù)過程中保持CS持續(xù)拉低確保FIFO讀指針在整個批次讀取期間不會被CS的重復(fù)拉高拉低打斷。CS在讀取中途被拉高會把FIFO讀操作強制終止下一次再拉低時讀指針的位置不確定這也會造成首元素錯亂。WTM的推薦值我一般設(shè)成8到16。這樣MCU不會被頻繁喚醒FIFO里也有足夠的緩沖樣本競爭窗口的問題基本消失。采樣率200 Hz的情況下WTM設(shè)16意味著大約80毫秒觸發(fā)一次中斷CPU負(fù)載很低。如果你的應(yīng)用對數(shù)據(jù)實時性要求很高WTM可以降到4但最好不要低于4。#define LPS22DF_FIFO_WTM_VALUE 8u void lps22df_fifo_start(uint8_t fmode) { // 1. 配置FIFO模式先不使能中斷 lps22df_write_reg(LPS22DF_FIFO_WTM, LPS22DF_FIFO_WTM_VALUE); lps22df_write_reg(LPS22DF_FIFO_CTRL, (uint8_t)(fmode 5)); // 2. 清空FIFO殘留 lps22df_fifo_clear(); // 3. 使能水印中斷 lps22df_write_reg(LPS22DF_CTRL3, LPS22DF_INT_WTM_EN_MASK); }5.3 更穩(wěn)健的FIFO讀取封裝最后分享一個我一直在用的FIFO讀取函數(shù)。它不做任何假設(shè)完全以FIFO_STATUS的狀態(tài)位為準(zhǔn)讀完一個完整樣本的4個字節(jié)然后返回實際讀到的樣本數(shù)量。如果你要抄作業(yè)抄這個基本不會翻車。typedef struct { uint32_t pressure_raw; /* 24位壓力原始值 */ uint8_t temp_raw; /* 8位溫度原始值 */ } lps22df_fifo_sample_t; int lps22df_fifo_read_block(lps22df_fifo_sample_t *out, int max_samples) { uint8_t rbuf[4]; uint8_t status; int count 0; while (count max_samples) { status lps22df_read_reg(LPS22DF_FIFO_STATUS); if (status LPS22DF_FIFO_EMPTY_MASK) { break; } lps22df_read_burst(LPS22DF_FIFO_DATA_OUT, rbuf, 4); out[count].pressure_raw ((uint32_t)rbuf[2] 16) | ((uint32_t)rbuf[1] 8) | (uint32_t)rbuf[0]; out[count].temp_raw rbuf[3]; count; } return count; }這個函數(shù)在每次循環(huán)里都讀FIFO_STATUS判斷空標(biāo)志不會出現(xiàn)空讀的情況。而且它嚴(yán)格按4字節(jié)粒度讀取不會破壞FIFO指針的對齊。唯一需要注意的是如果FIFO_OVR位曾經(jīng)置位過最好在檢測到溢出后主動調(diào)用lps22df_fifo_clear()清一次再重新開始采集。我個人在這塊踩過的坑是之前圖省事把FIFO只當(dāng)成一個“多讀幾個字節(jié)”的增強寄存器來用從來不認(rèn)真處理模式切換和狀態(tài)標(biāo)志。吃過這次虧之后凡是涉及FIFO的啟動流程我都會默認(rèn)加一段“切模式 - 清FIFO - 查狀態(tài) - 再使能中斷”的固定動作。這個習(xí)慣幫我避開過好幾次類似的首元素異常問題。如果你也被LPS22DF或者其他傳感器FIFO的首元素?fù)p壞困擾先別懷疑芯片把讀取流程梳理一遍大概率能找到答案。