動(dòng)調(diào)試實(shí)戰(zhàn):Linux平臺(tái)SPI配置與數(shù)據(jù)穩(wěn)定讀取)
簡介面向硬件開發(fā)者的 AD7190 高精度 24 位 Σ-Δ 模數(shù)轉(zhuǎn)換器驅(qū)動(dòng)代碼包聚焦工業(yè)與醫(yī)療場景下的傳感器接口設(shè)計(jì)解決微控制器與 AD7190 之間的 SPI 通信、采樣配置與數(shù)據(jù)解析問題。壓縮包共 21 個(gè)文件、約 57KB以 C 源碼、頭文件、Keil 工程文件、編譯中間文件及 HEX 固件為主便于直接移植到嵌入式項(xiàng)目中。目前已有 698 人學(xué)習(xí)適合正在調(diào)試高精度測量電路的軟硬件工程師及電子相關(guān)專業(yè)學(xué)生。驅(qū)動(dòng)內(nèi)容涵蓋初始化、數(shù)據(jù)讀取、錯(cuò)誤處理、中斷管理與電源控制模塊并附帶示例代碼可幫助開發(fā)者快速掌握可編程增益放大器、數(shù)字濾波器及多通道輸入的實(shí)際配置方法。圍繞 SPI 接口時(shí)序、寄存器操作和典型硬件連接提供完整參考能有效減少底層驅(qū)動(dòng)開發(fā)與排錯(cuò)時(shí)間。 前陣子在調(diào)試一塊高精度稱重信號(hào)采集板主控選了 STM32模擬前端定了 AD7190。同事發(fā)來一個(gè)“AD7190驅(qū)動(dòng)程序.zip”解壓后是一套 Linux 平臺(tái)下的 SPI 參考驅(qū)動(dòng)附帶設(shè)備樹片段、用戶態(tài)讀取例程和一份不算太完整的 README。這套現(xiàn)成驅(qū)動(dòng)對(duì)快速啟動(dòng)項(xiàng)目幫助不小但真正調(diào)板子時(shí)會(huì)發(fā)現(xiàn)寄存器配置順序、SPI 時(shí)序、校準(zhǔn)時(shí)機(jī)這些點(diǎn)如果不搞清楚驅(qū)動(dòng)大概率跑偏。這篇文章就按我這次實(shí)際調(diào)試的順序把 AD7190 驅(qū)動(dòng)從拿到 zip 到把數(shù)據(jù)穩(wěn)定讀出來的過程拆開講一遍。內(nèi)容主要面向剛接觸 AD7190、要在 Linux 或 MCU 平臺(tái)上做高精度數(shù)據(jù)采集的開發(fā)者。你把驅(qū)動(dòng)文件重新整理過一遍之后會(huì)發(fā)現(xiàn)讀原始數(shù)據(jù)并不難難的是把噪聲和漂移壓下去。1. 先看清壓縮包里都有什么1.1 一份參考驅(qū)動(dòng)的典型文件結(jié)構(gòu)我拿到的這個(gè) zip 解壓之后常見的文件組織方式大概是這樣的AD7190驅(qū)動(dòng)/ ├── ad7190.c ├── ad7190.h ├── ad7190.dts ├── tools/ │ ├── ad7190_read.c │ └── read_ad7190.py ├── README.md └── Makefilead7190.c 是核心驅(qū)動(dòng)文件實(shí)現(xiàn)寄存器讀寫、設(shè)備初始化、校準(zhǔn)和 IIO 設(shè)備注冊。ad7190.h 里面是寄存器地址、掩碼、通道和增益相關(guān)的宏定義。ad7190.dts 是設(shè)備樹示例用來告訴內(nèi)核這個(gè) SPI 設(shè)備掛在哪條總線上、主頻多少、參考電壓用什么。tools 目錄下的兩個(gè)程序是用戶態(tài)讀取示例一個(gè)用 C一個(gè)用 Python方便在驅(qū)動(dòng)加載后直接讀取轉(zhuǎn)換結(jié)果。如果你拿到的包不是這個(gè)結(jié)構(gòu)也不用慌。ADI 官方倉庫里的源碼以及網(wǎng)上流傳的各種移植版本核心邏輯都差不多區(qū)別主要在平臺(tái)相關(guān)的 SPI 接口封裝上。拿到驅(qū)動(dòng)第一步不是編譯而是先打開 ad7190.c 找?guī)讉€(gè)關(guān)鍵函數(shù)reset、write、read、init。把數(shù)據(jù)通路理清楚后面怎么改都順手。1.2 為什么說“驅(qū)動(dòng)”不只是讀寫寄存器很多人覺得 ADC 驅(qū)動(dòng)就是通過 SPI 寫幾個(gè)寄存器然后讀數(shù)據(jù)這個(gè)理解沒錯(cuò)但對(duì) AD7190 這種高精度 Σ-Δ ADC 來說只做表面那層是不夠的。AD7190 內(nèi)部有一個(gè) 24 位 Σ-Δ 調(diào)制器SPI 接口只是它的外部通道真正的難點(diǎn)在于時(shí)鐘配置、濾波器配置、校準(zhǔn)時(shí)序、轉(zhuǎn)換完成判斷。驅(qū)動(dòng)需要處理的第一個(gè)問題是 DOUT/RDY 引腳。這個(gè)引腳在轉(zhuǎn)換完成時(shí)會(huì)拉低告訴主機(jī)“可以讀數(shù)據(jù)了”。如果驅(qū)動(dòng)只按固定延時(shí)去讀數(shù)據(jù)寄存器很可能讀到新舊數(shù)據(jù)混合的半更新狀態(tài)造成結(jié)果跳變。所以好的驅(qū)動(dòng)要么把 DOUT/RDY 配成中斷輸入要么在每次讀數(shù)據(jù)前先查詢狀態(tài)寄存器里的 RDY 位。另一個(gè)容易被忽略的是復(fù)位時(shí)序AD7190 上電后需要收到一段特定的時(shí)鐘序列才能穩(wěn)定進(jìn)入工作狀態(tài)這一步做不好后面寫寄存器可能完全不生效。把這些問題統(tǒng)一封裝成接口用戶態(tài)拿到的就不是一堆底層 SPI 字節(jié)而是一個(gè)干凈的in_voltage0_raw這也就是“驅(qū)動(dòng)”真正的價(jià)值。2. 初始化與寄存器配置驅(qū)動(dòng)能不能跑關(guān)鍵在這2.1 關(guān)鍵寄存器速覽AD7190 的寄存器不算多但每個(gè)都要清楚用途。我把最常用的幾個(gè)整理成了一張表寄存器位寬作用通信寄存器8 位每次訪問其他寄存器的“指路牌”選擇后續(xù)讀寫地址和操作方式狀態(tài)寄存器8 位包含 RDY 狀態(tài)位、校準(zhǔn)狀態(tài)、通道狀態(tài)等模式寄存器24 位選擇時(shí)鐘源、濾波模式SINC3/SINC4、輸出數(shù)據(jù)速率、校準(zhǔn)模式配置寄存器24 位配置通道選擇、PGA 增益、單/雙極性、模擬輸入緩沖等數(shù)據(jù)寄存器24 位存放最新的轉(zhuǎn)換結(jié)果二進(jìn)制補(bǔ)碼格式ID 寄存器8 位芯片 ID可用于檢查 SPI 通信是否正常第一次接觸 AD7190 的時(shí)候我最容易搞混的是通信寄存器和實(shí)際寄存器地址。AD7190 的每一個(gè)寄存器訪問動(dòng)作都要先通過通信寄存器“投石問路”發(fā)送一個(gè) 8 位指令告訴芯片接下來是讀還是寫目標(biāo)寄存器是哪一個(gè)。然后主機(jī)再繼續(xù)發(fā)送或接收對(duì)應(yīng)位寬的數(shù)據(jù)。這個(gè)機(jī)制決定了驅(qū)動(dòng)里所有讀寫操作都不是簡單的spi_write而是要先組織一個(gè)指令頭再拼上目標(biāo)數(shù)據(jù)。2.2 從驅(qū)動(dòng)代碼里讀出的初始化流程AD7190 的初始化流程我建議嚴(yán)格按這個(gè)順序來復(fù)位芯片。通過連續(xù)向 DIN 寫 40 個(gè) 1 觸發(fā)復(fù)位讓芯片回到默認(rèn)狀態(tài)。復(fù)位后所有寄存器恢復(fù)默認(rèn)值之后必須等待復(fù)位完成時(shí)間不同主頻下等待時(shí)間不同。配置模式寄存器。選擇時(shí)鐘源是用外部晶振還是內(nèi)部 4.92MHz 時(shí)鐘選擇濾波模式 SINC3 還是 SINC4設(shè)置輸出數(shù)據(jù)速率。輸出速率和濾波字直接決定噪聲水平。配置配置寄存器。選擇要用的模擬輸入通道設(shè)置 PGA 增益確認(rèn)是單極性還是雙極性輸出。做校準(zhǔn)。AD7190 支持內(nèi)部零標(biāo)度校準(zhǔn)和滿量程校準(zhǔn)。校準(zhǔn)的目的是消除 PGA 和調(diào)制器本身的失調(diào)與增益誤差。等待 RDY 信號(hào)讀取數(shù)據(jù)寄存器。為什么不能顛倒順序原因在于通信寄存器具有“粘滯性”芯片會(huì)記住上一次通信指定的目標(biāo)寄存器如果上一步?jīng)]讀完整下一步可能直接錯(cuò)位。其次配置寄存器的通道和增益設(shè)置必須在校準(zhǔn)之前穩(wěn)定下來否則校準(zhǔn)結(jié)果對(duì)應(yīng)的是錯(cuò)誤的配置。驅(qū)動(dòng)代碼里如果發(fā)現(xiàn)校準(zhǔn)數(shù)據(jù)異常先回看配置寄存器有沒有寫對(duì)。下面是一段初始化函數(shù)的偽代碼示意實(shí)際使用時(shí)要根據(jù)數(shù)據(jù)手冊補(bǔ)全宏定義static int ad7190_init(struct ad7190_state *st) { ad7190_reset(st); // 連續(xù)發(fā)送 40 個(gè) 1 ad7190_write_mode(st, AD7190_MODE_SEL_SINC3 | AD7190_MODE_CLKSRC_INT | AD7190_MODE_RATE(100)); // 設(shè)置濾波和輸出速率 ad7190_write_config(st, AD7190_CONF_CH_AIN1_AIN2 | AD7190_CONF_GAIN_1 | AD7190_CONF_BIPOLAR); // 差分輸入、PGA1、雙極性 ad7190_calibrate(st, AD7190_MODE_CAL_INT_ZSC); ad7190_calibrate(st, AD7190_MODE_CAL_INT_FSC); return 0; }這段代碼里的函數(shù)名和宏是我按常見驅(qū)動(dòng)風(fēng)格寫的示意不同版本可能有差異但邏輯順序是一致的。實(shí)際調(diào)試時(shí)我會(huì)在每次寫完寄存器后回讀一遍確認(rèn)寫入生效這種“寫后讀”的習(xí)慣能幫你省大量排查時(shí)間。3. SPI 通信細(xì)節(jié)和內(nèi)核驅(qū)動(dòng)整合3.1 SPI 模式和指令格式AD7190 的 SPI 接口通常工作在 Mode 1對(duì)應(yīng) CPOL 0、CPHA 1也就是 SCLK 空閑時(shí)為低電平數(shù)據(jù)在第二個(gè)邊沿被采樣。主控端配置 SPI 時(shí)必須把模式對(duì)準(zhǔn)否則讀出來的數(shù)據(jù)每一位都是亂的而且這種錯(cuò)位往往不會(huì)讓你看到規(guī)律性的錯(cuò)誤只會(huì)覺得噪聲大得離譜。每一次通信都由通信寄存器開頭。比如我想讀數(shù)據(jù)寄存器先發(fā)一個(gè)指令字節(jié)這個(gè)字節(jié)的高位表示寫或讀中間幾位表示目標(biāo)寄存器地址低幾位表示后續(xù)訪問方式。發(fā)完指令字節(jié)后根據(jù)寄存器位寬繼續(xù)發(fā)或收 8/24 位數(shù)據(jù)。MSB 先行這是所有 AD7190 驅(qū)動(dòng)共同遵守的規(guī)則。有個(gè)小坑如果主控 SPI 外設(shè)的片選是硬件自動(dòng)拉低的而 AD7190 對(duì) CS 拉低到 SCLK 第一個(gè)邊沿之間的建立時(shí)間有要求某些高速主控可能達(dá)不到。為了保險(xiǎn)建議把 CS 用 GPIO 控制軟件拉低完成一組完整 SPI 操作后再拉高這樣時(shí)序最可控。Linux 下如果使用 spidev 直接操作CS 模式也建議明確設(shè)置為mode SPI_MODE_1。3.2 Linux 驅(qū)動(dòng)集成從一個(gè) .c 文件到 sysfsLinux 下的 AD7190 驅(qū)動(dòng)一般掛在 IIO 子系統(tǒng)下注冊之后用戶態(tài)可以通過 sysfs 目錄訪問。核心流程是在probe函數(shù)里設(shè)置 SPI 參數(shù)讀取設(shè)備樹屬性初始化芯片最后注冊struct iio_dev用戶態(tài)就能在/sys/bus/iio/devices/iio:device0/下面看到in_voltage0_raw。內(nèi)核配置方面需要開啟CONFIG_IIO和CONFIG_AD7190。設(shè)備樹里至少要聲明 compatible、reg、spi-max-frequency 和 vref-supply。參考寫法如下spi0 { status okay; ad71900 { compatible adi,ad7190; reg 0; spi-max-frequency 1000000; vref-supply vref_3v3; #io-channel-cells 1; }; };vref-supply要指向?qū)嶋H的參考電壓節(jié)點(diǎn)驅(qū)動(dòng)在初始化時(shí)會(huì)用它計(jì)算轉(zhuǎn)換系數(shù)。如果你的板子上 DOUT/RDY 連到了某個(gè) GPIO也可以在設(shè)備樹里加drdy-gpios讓驅(qū)動(dòng)用中斷方式等待轉(zhuǎn)換完成而不是輪詢狀態(tài)寄存器。編譯時(shí)需要注意驅(qū)動(dòng)代碼可能在很老的內(nèi)核版本上寫的API 和新的 IIO 框架有差異。常見的坑是spi_write_then_read和iio_validate_scan_mask_onehot這些接口在新版本里改了名字編譯報(bào)錯(cuò)時(shí)不要硬改內(nèi)核版本優(yōu)先找對(duì)應(yīng)內(nèi)核的源碼分支。3.3 裸機(jī)平臺(tái)移植思路如果最終目標(biāo)是 STM32 或 ESP32完全可以復(fù)用這份驅(qū)動(dòng)的邏輯只是把 Linux SPI 相關(guān)調(diào)用替換成平臺(tái) HAL 庫。需要改動(dòng)的部分并不多主要是三處SPI 收發(fā)函數(shù)。把spi_write/spi_read替換成HAL_SPI_Transmit/HAL_SPI_Receive注意設(shè)置 CPOL/CPHA。CS 控制。Linux 里通常由 SPI 框架管理裸機(jī)下用 GPIO 輸出控制。延時(shí)函數(shù)。Linux 內(nèi)核用usleep_range裸機(jī)下用HAL_Delay/delay_us。驅(qū)動(dòng)核心邏輯比如寄存器組織、校準(zhǔn)流程、RDY 判斷完全不用動(dòng)。這也是我建議先讀 Linux 驅(qū)動(dòng)源碼而不是直接找裸機(jī)例程的原因——Linux 版本往往考慮得更全面錯(cuò)誤處理也更完整。4. 實(shí)際調(diào)試中的坑和解決實(shí)錄4.1 讀出來全是 0 或者滿量程這是最常見的現(xiàn)象。我遇到的情況是 SPI 模式配成 Mode 0導(dǎo)致通信寄存器指令字節(jié)沒有被正確解析芯片根本沒進(jìn)入讀數(shù)據(jù)模式。排查方法很簡單先讀 ID 寄存器。AD7190 的 ID 寄存器有固定值如果能讀到預(yù)期值說明 SPI 通信正常如果讀出來是 0x00 或 0xFF基本就是時(shí)序、模式或者片選的問題和 ADC 本身的寄存器配置無關(guān)。排除 SPI 問題后再看通道和增益。AD7190 的配置寄存器有通道選擇位和 PGA 增益位。如果通道沒對(duì)上比如實(shí)際信號(hào)在 AIN1/AIN2而驅(qū)動(dòng)配置的是 AIN3/AIN4讀出來自然不對(duì)。另外把增益設(shè)置得太高而輸入信號(hào)又比較大轉(zhuǎn)換結(jié)果就會(huì)進(jìn)飽和區(qū)表現(xiàn)為接近滿量程。我整理了一個(gè)問題速查表現(xiàn)象可能原因排查動(dòng)作讀 ID 寄存器返回 0SPI 模式錯(cuò)誤、CS 時(shí)序不對(duì)、接線錯(cuò)誤檢查 SPI Mode 1、示波器抓 SCLK/DIN/DOUT讀數(shù)據(jù)全零輸入信號(hào)為負(fù)、單極性配置、通道沒接對(duì)檢查配置寄存器、用萬用表量輸入電壓讀數(shù)據(jù)接近滿量程PGA 增益過高、參考電壓異常降低增益檢查 VREF數(shù)值一直亂跳濾波設(shè)置不對(duì)、DOUT/RDY 未判斷檢查模式寄存器改中斷等待4.2 噪聲大、數(shù)據(jù)漂移AD7190 本身的噪聲指標(biāo)很漂亮但那是建立在良好的電源和參考電壓基礎(chǔ)上的。驅(qū)動(dòng)層面能做的是把輸出數(shù)據(jù)速率壓低啟用 SINC4 濾波并正確完成校準(zhǔn)。實(shí)際調(diào)試中我把輸出速率從 4800 SPS 降到 50 SPS峰峰值噪聲肉眼可見地降了一個(gè)數(shù)量級(jí)。還有一點(diǎn)很多人忽略AD7190 的數(shù)據(jù)寄存器是 24 位的如果你在轉(zhuǎn)換完成之前讀取可能拿到半更新的數(shù)據(jù)。如果你的程序只是用HAL_Delay等固定時(shí)間而不判斷 DOUT/RDY 引腳就會(huì)出現(xiàn)偶發(fā)跳變。最簡單的做法是把 DOUT/RDY 接到外部中斷引腳每次拉低時(shí)再啟動(dòng) SPI 讀取這樣能保證每次讀到的都是完整幀。校準(zhǔn)對(duì)最終精度影響也很大。內(nèi)部校準(zhǔn)的前提是輸入引腳處于零標(biāo)度和滿量程對(duì)應(yīng)狀態(tài)。如果接的是橋式傳感器零標(biāo)度校準(zhǔn)要在傳感器空載時(shí)做如果傳感器已經(jīng)受力校準(zhǔn)結(jié)果會(huì)把當(dāng)前受力當(dāng)成零點(diǎn)后續(xù)讀數(shù)就帶了一個(gè)很大的固定偏移。4.3 Linux 下設(shè)備加載不到板子上電后/sys/bus/iio/devices/下面沒有新設(shè)備。先確認(rèn)內(nèi)核編譯時(shí)有沒有把 AD7190 驅(qū)動(dòng)編進(jìn)去zcat /proc/config.gz | grep CONFIG_AD7190。再確認(rèn)設(shè)備樹 compatible 字符串和驅(qū)動(dòng)里的 match 表一致很多問題出在字符串大小寫或者多了一個(gè)空格。如果驅(qū)動(dòng)加載成功但 sysfs 下沒有讀數(shù)查看dmesg輸出。常見報(bào)錯(cuò)是 vref-supply 未定義導(dǎo)致 probe 函數(shù)返回失敗。另外 SPI 的最大頻率不要設(shè)太高我一般建議從 1MHz 開始跑通后再一點(diǎn)點(diǎn)往上加AD7190 對(duì) SCLK 的上升沿要求比較嚴(yán)格頻率太高容易采到毛刺。4.4 下載的驅(qū)動(dòng)包被安全軟件攔截如果你在 Windows 環(huán)境下解壓驅(qū)動(dòng)包系統(tǒng)提示“無法驗(yàn)證此設(shè)備所需的驅(qū)動(dòng)程序的數(shù)字簽名”或者“檢測為易受攻擊的驅(qū)動(dòng)程序”多半是包里的 Windows 驅(qū)動(dòng)沒有微軟簽名或者版本太舊。遇到這種情況我一般不會(huì)貿(mào)然關(guān)閉系統(tǒng)安全設(shè)置而是去原廠官網(wǎng)找最新版本或者只把這類驅(qū)動(dòng)放在隔離的測試機(jī)里使用。對(duì) Linux 驅(qū)動(dòng)來說源碼編譯的模塊不受簽名機(jī)制影響但如果是在 Windows 下通過 DLL 調(diào)用 AD7190就要格外留意驅(qū)動(dòng)來源。5. 快速使用讀取轉(zhuǎn)換結(jié)果5.1 通過 sysfs 讀原始碼驅(qū)動(dòng)加載成功后讀取就很直接了cd /sys/bus/iio/devices/iio:device0 cat in_voltage0_raw cat in_voltage0_scalein_voltage0_raw是 ADC 原始碼in_voltage0_scale是每個(gè)碼對(duì)應(yīng)的微伏數(shù)。實(shí)際電壓換算公式是電壓(V) raw * scale / 1e6如果配置的是雙極性模式raw 是二進(jìn)制補(bǔ)碼可能為負(fù)值計(jì)算時(shí)直接用乘法就能得到正確符號(hào)不用自己轉(zhuǎn)換。5.2 用 Python 寫一個(gè)最簡單的讀取循環(huán)驗(yàn)證驅(qū)動(dòng)有沒有跑通我通常直接寫個(gè)小腳本import time def read_adc(path/sys/bus/iio/devices/iio:device0): with open(path /in_voltage0_raw) as f: raw int(f.read().strip()) with open(path /in_voltage0_scale) as f: scale float(f.read().strip()) return raw * scale / 1e6 while True: voltage read_adc() print(f{voltage:.6f} V) time.sleep(0.1)這個(gè)腳本對(duì)新手小白也很友好不依賴任何第三方庫直接把 sysfs 文件當(dāng)普通文本讀就行。如果每秒需要采幾千個(gè)點(diǎn)用 Python 讀 sysfs 就不太合適了這時(shí)候應(yīng)該走 IIO buffer配合 trigger 批量讀取。但先跑通這個(gè)簡單循環(huán)確認(rèn)硬件和驅(qū)動(dòng)沒問題再上高級(jí)功能是我自己的習(xí)慣。5.3 什么時(shí)候不能直接用現(xiàn)成驅(qū)動(dòng)現(xiàn)成驅(qū)動(dòng)覆蓋了大多數(shù)場景但如果你要同時(shí)采集多個(gè)通道并且每個(gè)通道需要不同的濾波和增益那就需要對(duì)驅(qū)動(dòng)做擴(kuò)展。比如稱重應(yīng)用里一個(gè)通道接傳感器另一個(gè)通道接溫度傳感器做冷端補(bǔ)償這兩個(gè)通道的配置不一樣驅(qū)動(dòng)需要支持通道切換和重新校準(zhǔn)。另一個(gè)需要擴(kuò)展的場景是同步采集。如果你有多片 AD7190 同時(shí)采樣要求各片的數(shù)據(jù)嚴(yán)格對(duì)齊那就不能只靠 sysfs 一個(gè)個(gè)讀要使用 GPIO 觸發(fā)或外部同步引腳結(jié)合 IIO trigger 來實(shí)現(xiàn)。這部分內(nèi)容比較深但順序一定是先跑通單設(shè)備 sysfs 讀取再去碰 buffer 和多設(shè)備同步否則出了問題很難定位。我這次調(diào)完這套驅(qū)動(dòng)后最大的體會(huì)是AD7190 這種高精度 ADC寄存器配置只要順序?qū)α薙PI 通信只要模式對(duì)了基本都能出數(shù)。真正讓人頭疼的是數(shù)據(jù)出來以后怎么判斷準(zhǔn)不準(zhǔn)、穩(wěn)不穩(wěn)以及怎么把噪聲壓下去。你先學(xué)會(huì)看 ID 寄存器和 RDY 引腳再談精度這比研究任何玄學(xué)都管用。如果你也正在折騰 AD7190希望這份驅(qū)動(dòng)拆解能幫你把啟動(dòng)時(shí)間從一兩天縮短到半天。本文還有配套的精品資源點(diǎn)擊獲取