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