動:從寄存器到IIO子系統(tǒng)的完整實踐)
1. 為什么我放著現(xiàn)成的st_sensors不用偏要自己寫一次ASM330LHH驅(qū)動ASM330LHH這顆芯片ST官方歸類是車規(guī)級六軸慣性傳感器加速度計加陀螺儀二合一同系列的還有LSM6DSO、LSM6DSR這些消費級/工業(yè)級型號。如果你在嵌入式Linux環(huán)境里做開發(fā)第一反應肯定是內(nèi)核里不是已經(jīng)有st_sensors的驅(qū)動了嗎設(shè)備樹里配一下就能出數(shù)據(jù)何必自己寫這個問題的答案恰好是這篇學習筆記的核心。我個人的建議是能用內(nèi)核現(xiàn)成驅(qū)動就直接用但有一個前提——你必須自己能看懂它并且能在它“失靈”的時候徒手寫出替代方案。真正上手過一遍驅(qū)動編寫你對IIO子系統(tǒng)、SPI/I2C總線模型、設(shè)備樹匹配機制、傳感器寄存器級的操作邏輯會有一個完全不一樣的理解。尤其ASM330LHH這種帶硬件FIFO、支持多種中斷輸出、還額外集成溫度傳感器和機器學習內(nèi)核的器件嵌套在st_sensors框架下寫一遍比單純調(diào)用ioctl讀十個角度值要值得多。這篇筆記不是從零開始教你“什么是I2C”而是記錄我完整地走了一遍“讀手冊—搭框架—寫寄存器—配中斷—調(diào)FIFO—驗證數(shù)據(jù)”的流程中間踩了若干坑最后還跟ST官方驅(qū)動做了對照。閱讀對象建議是已經(jīng)能跑通嵌入式Linux最小系統(tǒng)、會看原理圖、至少寫過一次字符設(shè)備驅(qū)動的朋友。如果是純Linux小白建議先把設(shè)備樹、platform_driver、i2c_client這幾塊基礎(chǔ)補上再回來看。順便說一句我這次的使用場景是給一臺AGV小車做姿態(tài)反饋要求不高主要輸出角速度和線加速度后續(xù)打算疊加航向推算。所以我重點關(guān)心三個東西能不能保證1kHz的角速度采樣、FIFO的批量讀取是否穩(wěn)定、以及長時間運行后有沒有數(shù)據(jù)丟幀。這些點也會貫穿全文。2. 讀懂ASM330LHH的關(guān)鍵參數(shù)和內(nèi)部結(jié)構(gòu)比抄代碼重要十倍2.1 硬件層面的第一印象這是一顆為“動態(tài)場合”設(shè)計的芯片先看一組參數(shù)ASM330LHH的加速度計支持±2/±4/±8/±16 g四擋滿量程陀螺儀支持±125/±250/±500/±1000/±2000/±4000 dps六擋。ODR最高能到6.66 kHz但實際應用里陀螺儀1.66 kHz、加速度計1.66 kHz已經(jīng)非常夠用。噪聲密度方面陀螺儀典型值在4 mdps/√Hz上下加速度計典型值在60 μg/√Hz左右比上一代LSM6DSL數(shù)據(jù)好一些這就是車規(guī)級和消費級拉開差距的地方。最容易被忽略的一個指標是正交誤差手冊里給的是陀螺儀XY/Z軸間0.05°以內(nèi)這意味著做姿態(tài)融合時坐標系的轉(zhuǎn)換矩陣偏差較小融合算法里少一個需要額外標定的量。如果你只是拿它測個振動這個參數(shù)確實無所謂但你要做的是IMU積分那初始的軸對準誤差會直接變成姿態(tài)角漂移所以選型時我特別留意了這個。還有一個不能忽視的硬件細節(jié)ASM330LHH的封裝是LGA-14L3.5×3.5×1 mm3本質(zhì)上跟LSM6DSO是同一個封裝家族。這帶來一個直接好處——很多用于LSM6DSO的轉(zhuǎn)接板、封裝庫、參考設(shè)計可以直接復用到ASM330LHH上。我在畫測試板時直接用了一片帶LSM6DSO封裝的轉(zhuǎn)接板省了不少事。2.2 寄存器地圖別背但要知道“那幾個關(guān)鍵寄存器”在哪里驅(qū)動開發(fā)的本質(zhì)就是跟寄存器對話。ASM330LHH的寄存器布局跟ST慣導系列基本一致我實際調(diào)試過程中用到的核心寄存器就這幾個列出來供大家有個整體印象地址名稱功能說明0x0FWHO_AM_I設(shè)備ID固定值0x6B0x10CTRL1_XL加速度計ODR、滿量程、數(shù)字濾波配置0x11CTRL2_G陀螺儀ODR、滿量程、數(shù)字濾波配置0x12CTRL3_CSPI/I2C接口配置、寄存器地址自增等0x13CTRL4_C中斷引腳功能選擇、數(shù)據(jù)準備中斷使能0x14CTRL5_C陀螺儀低功耗/高精度模式0x15CTRL6_C加速度計低功耗/高精度模式0x19CTRL10_CFIFO、傳感器數(shù)據(jù)路由、外部時鐘0x1ESTATUS_REG各通道數(shù)據(jù)就緒標志0x20OUT_TEMP_L溫度數(shù)據(jù)低字節(jié)0x22OUTX_L_G陀螺儀X軸低字節(jié)后面連續(xù)是Y/Z0x28OUTX_L_A加速度計X軸低字節(jié)后面連續(xù)是Y/Z0x3A~0x3DFIFO_STATUS1~4FIFO當前已存數(shù)據(jù)條目數(shù)、FIFO水印狀態(tài)等0x78FIFO_DATA_OUT_TAG讀出FIFO數(shù)據(jù)時附帶的數(shù)據(jù)標簽0x79~0x7EFIFO_DATA_OUT_X_L等連續(xù)存放從FIFO彈出的原始數(shù)據(jù)手冊里有兩百多個寄存器位但實際一個基礎(chǔ)驅(qū)動用到的不超過三十個。我的建議是先把WHO_AM_I、兩個主控制寄存器、狀態(tài)寄存器、六個輸出寄存器搞清楚其他用到再翻。這一條對任何傳感器驅(qū)動都適用。2.3 兩種通信接口選SPI還是I2CASM330LHH同時支持I2C和SPI。I2C地址由SA0引腳電平?jīng)Q定SA0接地時7位地址是0x6A接高電平時是0x6B。SPI最高可以跑10 MHzI2C標準模式下最高400 kHz。做AGV姿態(tài)感知這種實時性要求略高的場合我推薦SPI理由有三個SPI沒有I2C那種應答/仲裁機制連續(xù)讀取六個寄存器時波形干凈不容易被其他I2C設(shè)備干擾ASM330LHH的SPI支持最高10 MHz同樣數(shù)據(jù)量下留給CPU的調(diào)度余量更多設(shè)備數(shù)量少的話SPI片選直連GPIO時序控制起來更明確。當然I2C也有它的優(yōu)勢接線少、調(diào)試簡單如果用現(xiàn)成的IMU評估板I2C幾乎零成本就能跑起來。這次調(diào)試我先用I2C驗證了寄存器讀寫確認ID和初始數(shù)據(jù)流正常之后才切換到SPI跑正式的數(shù)據(jù)采集分兩步走能明顯降低排錯難度。3. 驅(qū)動整體框架IIO子系統(tǒng)、platform_driver和“我是誰”3.1 字符設(shè)備輸入子系統(tǒng)IIO到底掛哪里Linux下驅(qū)動一個傳感器有幾種掛載方式傳統(tǒng)的字符設(shè)備驅(qū)動自己在/dev/下創(chuàng)建一個節(jié)點用戶程序用open/read/ioctl操作輸入子系統(tǒng)(input subsystem)適合按鍵、觸摸屏、遙控器這類以事件為語義的設(shè)備IIO子系統(tǒng)(Industrial I/O)專門為工業(yè)傳感器設(shè)計支持加速度計、陀螺儀、磁力計、光傳感器、ADC等提供sysfs接口和trigger緩沖機制。ASM330LHH這類六軸IMU正確的歸宿就是IIO。IIO不但幫你把數(shù)據(jù)暴露成了/sys/bus/iio/devices/iio:deviceX這樣的標準接口還自帶trigger、buffer、event這幾個抽象層次。用IIO框架寫驅(qū)動可以少寫很多樣板代碼同時后續(xù)如果用戶態(tài)要用libiio或者Python的pylibiio完全無縫銜接。3.2 搭框架probe函數(shù)里做什么一個最小的IIO傳感器驅(qū)動骨架大概長這樣static const struct iio_info asm330lhh_info { .read_raw asm330lhh_read_raw, .write_raw asm330lhh_write_raw, }; static int asm330lhh_probe(struct spi_device *spi) { struct iio_dev *indio_dev; struct asm330lhh_data *data; int ret; data devm_kzalloc(spi-dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >static int asm330lhh_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val, int *val2, long mask) { struct asm330lhh_data *data iio_priv(indio_dev); int ret; switch (mask) { case IIO_CHAN_INFO_RAW: ret asm330lhh_read_axis(data, chan-address, val); if (ret 0) return ret; return IIO_VAL_INT; case IIO_CHAN_INFO_SCALE: /* 尺度因子由滿量程寄存器決定 */ *val 0; *val2 asm330lhh_get_scale(chan,>ecspi2 { pinctrl-names default; pinctrl-0 pinctrl_ecspi2; cs-gpios gpio5 13 GPIO_ACTIVE_LOW; status okay; asm330lhh: asm330lhh0 { compatible st,asm330lhh; reg 0; spi-max-frequency 5000000; interrupt-parent gpio1; interrupts 7 IRQ_TYPE_EDGE_RISING; // 供電、復位腳等其他屬性按實際原理圖補充 }; };有幾個細節(jié)值得展開說一下。第一spi-max-frequency我寫的是5 MHz芯片能力是10 MHz但考慮到PCB走線還有轉(zhuǎn)接板先降到5 MHz能顯著減少波形邊沿的振鈴問題。等驗證全部通過再往上拉也是一種“性能回退優(yōu)先”的調(diào)試思路。第二如果同時用I2C功能測試I2C設(shè)備樹節(jié)點和SPI節(jié)點二選一不能同時把同一個物理器件掛兩條總線。雖然內(nèi)核不至于崩潰但兩套驅(qū)動會瘋狂搶設(shè)備造成各種隨機錯誤。我的辦法是先屏蔽SPI節(jié)點設(shè)備樹改為I2C方式等I2C通了再把SPI節(jié)點放開。第三中斷類型我用的IRQ_TYPE_EDGE_RISING對應的是數(shù)據(jù)準備好時INT引腳從低拉高的電平跳變。如果后面改FIFO水印中斷可能要用IRQ_TYPE_LEVEL_HIGH之類這取決于你配置的是“數(shù)據(jù)就緒”還是“FIFO水位”事件。這個看似不起眼的點會直接影響驅(qū)動收到的中斷號是否有效。5.2 編譯、加載與第一個有效數(shù)據(jù)寫完驅(qū)動后編譯以模塊方式加載make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- modules cp asm330lhh.ko /srv/nfs/rootfs/root/ # 目標板上 insmod asm330lhh.ko dmesg | tail -20如果probe成功dmesg里應該能看到掛載信息然后在/sys/bus/iio/devices/下出現(xiàn)新的設(shè)備目錄ls /sys/bus/iio/devices/ # 假設(shè)是 iio:device2 cat /sys/bus/iio/devices/iio:device2/in_accel_x_raw cat /sys/bus/iio/devices/iio:device2/in_gyro_z_raw別急著寫c程序先用命令驗證是最快的。手頭沒有iio_attr/iio_readdev這些工具時純用shell也能測試。不過這里要提醒在第一次讀到非零數(shù)據(jù)后先檢查數(shù)據(jù)的噪聲水平。靜止狀態(tài)下加速度計X/Y軸應該接近0Z軸應該接近1g對應的raw值比如±2g量程下大約是16384陀螺儀三軸應該在幾十個LSB以內(nèi)波動。如果出現(xiàn)某個軸數(shù)據(jù)特別大或者不斷跳變先懷疑是不是配置錯了量程而不是算法問題。5.3 中斷驅(qū)動的FIFO讀取和批量數(shù)據(jù)流設(shè)備樹里配了中斷驅(qū)動里就用request_threaded_irq來處理數(shù)據(jù)就緒中斷。每次中斷來臨說明FIFO里已經(jīng)有數(shù)據(jù)這時候我選擇一次性把FIFO中所有條目讀出來。讀取的流程大概是static irqreturn_t asm330lhh_irq_handler(int irq, void *private) { struct iio_dev *indio_dev private; struct asm330lhh_data *data iio_priv(indio_dev); uint8_t status; int count; /* 先看狀態(tài)寄存器確認是不是FIFO水印觸發(fā) */ asm330lhh_read_reg(data, ASM330LHH_REG_STATUS, status); if (!(status ASM330LHH_STATUS_FIFO_THRESHOLD)) return IRQ_NONE; /* 讀取FIFO中已存儲的樣本數(shù)量 */ count asm330lhh_read_fifo_count(data); /* 從0x78開始連續(xù)讀每個樣本包含 tag 6路原始數(shù)據(jù) */ asm330lhh_read_fifo(data,>