核驅(qū)動(dòng)開(kāi)發(fā)實(shí)戰(zhàn)與調(diào)試)
簡(jiǎn)介基于內(nèi)核的si47xx驅(qū)動(dòng)開(kāi)發(fā)包面向嵌入式驅(qū)動(dòng)開(kāi)發(fā)工程師專注于SI47xx系列芯片的驅(qū)動(dòng)實(shí)現(xiàn)。該芯片支持FM發(fā)射與AM/FM/SW/LW/WB多波段接收資源圍繞I2C總線接口控制在底層驅(qū)動(dòng)之上增加了應(yīng)用層API覆蓋音量控制、頻道搜索、RSSI讀取、頻道設(shè)定以及AM/FM/RDS切換等核心功能。壓縮包共含34個(gè)文件以C源文件與H頭文件為主同時(shí)提供Makefile、Kconfig等構(gòu)建配置以及CMD腳本和編譯生成的目標(biāo)文件整體體積僅185KB結(jié)構(gòu)清晰緊湊便于直接閱讀和復(fù)用。目前已有99人學(xué)習(xí)下載適合正在調(diào)試si47xx驅(qū)動(dòng)或開(kāi)發(fā)收音機(jī)功能的驅(qū)動(dòng)開(kāi)發(fā)者參考。資源中不僅包含完整驅(qū)動(dòng)源碼與編譯配置還針對(duì)硬件控制接口和軟件命令給出了詳細(xì)說(shuō)明并配有多種操作模式的配置示例能夠幫助讀者從底層寄存器操作快速過(guò)渡到上層功能封裝其中的FMRX、AMRX、WBRX等模塊文件分別對(duì)應(yīng)不同波段處理邏輯是學(xué)習(xí)該芯片驅(qū)動(dòng)開(kāi)發(fā)的實(shí)用參考資料。 年前接了一個(gè)嵌入式項(xiàng)目要在Linux kernel下為SI47xx收音芯片做驅(qū)動(dòng)開(kāi)發(fā)。這塊芯片本身不大名氣也不如音頻codec那么響但真上手之后才發(fā)現(xiàn)從設(shè)備模型選擇到命令時(shí)序處理每一步都有不少值得記錄的細(xì)節(jié)。這篇文章把我從拿到硬件到跑通調(diào)諧、再到現(xiàn)在穩(wěn)定運(yùn)行的完整過(guò)程寫(xiě)出來(lái)包括選型理由、代碼結(jié)構(gòu)、調(diào)試方法和掉進(jìn)去的坑希望能給做同類嵌入式驅(qū)動(dòng)開(kāi)發(fā)的朋友省點(diǎn)時(shí)間。1. 想清楚SI47xx到底是個(gè)什么類型的設(shè)備1.1 內(nèi)核里的設(shè)備角色不是聲卡是受控芯片SI47xx是Silicon Labs的AM/FM/SW/LW接收芯片系列常見(jiàn)型號(hào)有Si4704、Si4705、Si4708、Si4709等。很多開(kāi)發(fā)板、車載模塊、便攜收音機(jī)里都用它控制接口是I2C音頻輸出要么走模擬LINE OUT要么通過(guò)I2S把解調(diào)后的音頻數(shù)據(jù)送給SoC側(cè)codec。這里要先把一個(gè)概念掰扯清楚SI47xx在內(nèi)核里不是音頻設(shè)備而是受控設(shè)備。它不負(fù)責(zé)PCM數(shù)據(jù)流的搬運(yùn)也不參與DMA它的核心工作就是接收命令、執(zhí)行調(diào)諧、返回狀態(tài)。所以驅(qū)動(dòng)的主要職責(zé)不是處理音頻數(shù)據(jù)而是把芯片的能力通過(guò)正確的接口暴露給上層。這個(gè)定位直接影響了框架選型。如果你把它當(dāng)成ALSA音頻設(shè)備去注冊(cè)那還得給它編一個(gè)空的pcm ops、dummy codec繞了一大圈實(shí)際控制邏輯還是得自己寫(xiě)白白增加復(fù)雜度。我最終把它做成一個(gè)misc字符設(shè)備配合ioctl接口清爽、直接。1.2 為什么不在用戶態(tài)直接操作有人可能會(huì)說(shuō)I2C設(shè)備在用戶態(tài)用/dev/i2c-x不就能控制嗎i2c-tools也能發(fā)命令為什么非要在內(nèi)核寫(xiě)驅(qū)動(dòng)這個(gè)問(wèn)題的答案是看場(chǎng)景。如果只是拿來(lái)做個(gè)驗(yàn)證Demo用python-periphery或者i2c-tools在用戶態(tài)發(fā)命令完全沒(méi)問(wèn)題幾天就能調(diào)通。但產(chǎn)品級(jí)項(xiàng)目不是這樣你會(huì)遇到幾個(gè)繞不開(kāi)的問(wèn)題。第一電源和時(shí)序控制。SI47xx對(duì)上電順序有要求復(fù)位引腳、VIO、主電源這些資源和內(nèi)核的regulator、gpio子系統(tǒng)相關(guān)聯(lián)放在用戶態(tài)就變成了一堆散落的shell命令不可控。第二休眠喚醒。系統(tǒng)進(jìn)入suspend時(shí)芯片要按順序下電喚醒之后要重新初始化這些邏輯掛到內(nèi)核的休眠鏈路上才是最自然的。第三調(diào)試手段。內(nèi)核驅(qū)動(dòng)可以用dev_dbg、debugfs、ftrace出了問(wèn)題能直接看I2C總線上的每一次傳輸這個(gè)能力用戶態(tài)程序很難比。所以產(chǎn)品項(xiàng)目選內(nèi)核驅(qū)動(dòng)不是技術(shù)上的炫技而是工程上的必然。2. 動(dòng)手之前命令集和芯片狀態(tài)機(jī)是驅(qū)動(dòng)的地基2.1 I2C上的交互模型寫(xiě)命令、讀狀態(tài)數(shù)組SI47xx的I2C交互模式和普通傳感器不一樣。普通傳感器通常是“寫(xiě)寄存器地址再讀數(shù)據(jù)”SI47xx則是主控發(fā)一串命令字節(jié)芯片執(zhí)行后用整塊狀態(tài)寄存器數(shù)組回應(yīng)。具體來(lái)說(shuō)寫(xiě)命令就是一次普通的I2C寫(xiě)長(zhǎng)度取決于命令類型讀數(shù)據(jù)則不是單獨(dú)讀某個(gè)寄存器而是從0x00開(kāi)始把整塊status寄存器數(shù)組通常是0x00到0x07某些業(yè)務(wù)需要讀到更多一次性讀回來(lái)。驅(qū)動(dòng)收到這塊數(shù)組后再按數(shù)據(jù)手冊(cè)的定義逐位解析。第0字節(jié)通常是status里面包含CTSClear to Send位、RDS中斷標(biāo)志位等。這個(gè)模型給驅(qū)動(dòng)帶來(lái)的直接影響是收發(fā)邏輯必須用i2c_transfer的組合消息來(lái)封裝。不能直接調(diào)i2c_smbus_read_byte_data去按地址讀因?yàn)樾酒瑑?nèi)部狀態(tài)是整塊快照式的只讀一個(gè)字節(jié)沒(méi)有意義。2.2 必須吃透的幾條核心命令SI47xx的命令體系本身不復(fù)雜但每條命令的參數(shù)位置、單位換算和時(shí)序要求必須摳細(xì)。我在項(xiàng)目里用到最多的幾條命令如下命令功能關(guān)鍵參數(shù)POWER_UP (0x01)芯片上電、選擇功能模式CTSIEN、XOSCEN、FUNC、OPMODEFM_TUNE_FREQ (0x20)調(diào)諧到指定頻率freq單位10kHz、ANTCAPHFM_SEEK_START (0x21)向上/向下自動(dòng)搜臺(tái)SEEKUP、SEEKTHFM_TUNE_STATUS (0x22)讀取當(dāng)前調(diào)諧狀態(tài)RSSI、SNR、FHGSET_PROPERTY (0x12)寫(xiě)入芯片屬性參數(shù)property id、valueGET_INT_STATUS (0x14)讀取中斷狀態(tài)用于判斷事件來(lái)源這里特別提醒兩點(diǎn)這兩點(diǎn)是我吃了虧之后才注意到的。第一頻率換算。FM調(diào)諧頻率字段的單位不是kHz而是10kHz。也就是說(shuō)100.0MHz要寫(xiě)10000不能寫(xiě)100000。我第一次就栽在這個(gè)換算上命令發(fā)得挺順RSSI也有值但就是收不到臺(tái)后來(lái)對(duì)著數(shù)據(jù)手冊(cè)逐字節(jié)核對(duì)才發(fā)現(xiàn)單位錯(cuò)了。第二SEEK參數(shù)。SEEK不是無(wú)腦搜索SEEKTH字段控制的是信號(hào)強(qiáng)度閾值。這個(gè)閾值設(shè)太高本來(lái)能收到的弱臺(tái)會(huì)被跳過(guò)設(shè)太低噪聲會(huì)被誤判成電臺(tái)。不同板子天線設(shè)計(jì)不一樣閾值不能照抄參考代碼必須實(shí)測(cè)調(diào)整。后面我會(huì)詳細(xì)說(shuō)調(diào)試過(guò)程。3. 驅(qū)動(dòng)框架選型misc字符設(shè)備比V4L2更適合這個(gè)場(chǎng)景3.1 三種內(nèi)核框架的取舍寫(xiě)內(nèi)核驅(qū)動(dòng)之前先要選框架。SI47xx這種收音芯片在內(nèi)核里大概有三條路可以走V4L2的radio接口、ALSA的輔助設(shè)備、還有最樸素的misc字符設(shè)備。我給它們做了一個(gè)對(duì)比框架一致性開(kāi)發(fā)復(fù)雜度適用場(chǎng)景V4L2 radio高有標(biāo)準(zhǔn)API高需要適配VIDIOC_S_FREQUENCY等一系列調(diào)用桌面Linux通用FM應(yīng)用ALSA輔助設(shè)備中高還要做dummy PCM通路音頻通路深度整合misc字符設(shè)備低自己定義ioctl低邏輯集中在文件ops專用產(chǎn)品、嵌入式定制有人會(huì)覺(jué)得V4L2最“正統(tǒng)”畢竟內(nèi)核里一直有radio接口。但V4L2的設(shè)計(jì)思路是通用的廣播接收器它要求驅(qū)動(dòng)實(shí)現(xiàn)大量標(biāo)準(zhǔn)ioctl而SI47xx的很多特性比如RDS、屬性配置、ACS自動(dòng)校準(zhǔn)映射到V4L2上十分別扭。做消費(fèi)類產(chǎn)品的時(shí)候用戶態(tài)是自己的應(yīng)用壓根不需要去兼容gnome-radio這類通用軟件直接用私有ioctl效率最高。音頻通路方面SI47xx通常是用模擬輸出直接接到功放或者耳機(jī)不經(jīng)過(guò)SoC內(nèi)部音頻總線即使走I2S也是SoC做I2S masterSI47xx做slave這跟ALSA的PCM鏈路其實(shí)不沖突但也不需要非得掛在同一個(gè)聲卡下。所以最終我選擇misc設(shè)備用_IOW、_IOR定義自己的命令碼。3.2 設(shè)備樹(shù)與資源綁定設(shè)備樹(shù)節(jié)點(diǎn)我這樣定義i2c3 { status okay; si47xx11 { compatible silabs,si4705; reg 0x11; reset-gpios gpio0 15 GPIO_ACTIVE_LOW; int-gpios gpio0 13 GPIO_ACTIVE_LOW; vdd-supply reg_3v3; silabs,region 2; /* 中國(guó)區(qū)FM頻段 87-108MHz */ silabs,de-emphasis 1; /* 75us */ }; };注意reg 0x11不是寫(xiě)死的。SI47xx的I2C地址由硬件引腳決定不同板子可能是0x10、0x11、0x22等。先查原理圖確認(rèn)地址再寫(xiě)設(shè)備樹(shù)不然probe階段就過(guò)不了。reset-gpios和int-gpios我強(qiáng)烈建議都配上。復(fù)位引腳是驅(qū)動(dòng)可靠性的關(guān)鍵中斷引腳雖然輪詢也能用但有了中斷后CTS和RDS事件處理會(huì)優(yōu)雅很多。驅(qū)動(dòng)的匹配代碼就是標(biāo)準(zhǔn)的i2c_driver這里不展開(kāi)跟普通I2C驅(qū)動(dòng)一致。重點(diǎn)看probe里的初始化和后續(xù)的命令封裝。4. 核心實(shí)現(xiàn)從probe到調(diào)諧的完整代碼鏈路4.1 probe階段的電源時(shí)序與初始化probe階段要做的事比大多數(shù)I2C驅(qū)動(dòng)多因?yàn)镾I47xx上電有一個(gè)嚴(yán)格的時(shí)序要求先要確保電源穩(wěn)定再把復(fù)位引腳拉低等待一段時(shí)間后拉高最后發(fā)送POWER_UP命令。代碼邏輯大致是這樣static int si47xx_probe(struct i2c_client *client) { struct si47xx_dev *radio; int ret; radio devm_kzalloc(client-dev, sizeof(*radio), GFP_KERNEL); if (!radio) return -ENOMEM; radio-client client; radio-regmap devm_regmap_init_i2c(client, si47xx_regmap_config); if (IS_ERR(radio-regmap)) return PTR_ERR(radio-regmap); radio-reset_gpio devm_gpiod_get_optional(client-dev, reset, GPIOD_OUT_LOW); radio-int_gpio devm_gpiod_get_optional(client-dev, int, GPIOD_IN); /* 上電時(shí)序復(fù)位拉低至少等待1ms */ if (radio-reset_gpio) { gpiod_set_value_cansleep(radio-reset_gpio, 0); msleep(1); gpiod_set_value_cansleep(radio-reset_gpio, 1); msleep(5); } ret si47xx_power_up(radio); if (ret 0) { dev_err(client-dev, power up failed: %d\n, ret); return ret; } misc_register(radio-miscdev); i2c_set_clientdata(client, radio); dev_info(client-dev, si47xx registered\n); return 0; }POWER_UP命令的參數(shù)不能錯(cuò)。FM模式下FUNC字段是2OPMODE是1。我還在初始化階段調(diào)用了SET_PROPERTY把de-emphasis和seek閾值設(shè)置好。這一步如果漏掉默認(rèn)參數(shù)有可能跟車載參考設(shè)計(jì)不一致表現(xiàn)在最終聽(tīng)感上就是聲音發(fā)悶或者高音太重。4.2 命令封裝與調(diào)諧狀態(tài)機(jī)整個(gè)驅(qū)動(dòng)的核心是一個(gè)命令封裝函數(shù)static int si47xx_command(struct si47xx_dev *radio, u8 cmd, u8 *params, int p_len, u8 *resp, int r_len) { struct i2c_msg msgs[2]; u8 buf[MAX_PARAMS 1]; int timeout 100; int ret; buf[0] cmd; memcpy(buf[1], params, p_len); msgs[0].addr radio-client-addr; msgs[0].flags 0; msgs[0].len p_len 1; msgs[0].buf buf; msgs[1].addr radio-client-addr; msgs[1].flags I2C_M_RD; msgs[1].len r_len; msgs[1].buf resp; ret i2c_transfer(radio-client-adapter, msgs, 2); if (ret 0) return ret; /* 輪詢CTS位等待芯片就緒 */ while ((resp[0] 0x80) 0 timeout--) { msleep(5); ret i2c_transfer(radio-client-adapter, msgs 1, 1); if (ret 0) return ret; } return (resp[0] 0x80) ? 0 : -ETIMEDOUT; }這里最關(guān)鍵的就是CTS位的處理。芯片執(zhí)行完命令之后會(huì)把狀態(tài)數(shù)組的第0字節(jié)的第7位置1表示可以接收下一條命令。如果驅(qū)動(dòng)在上一條命令沒(méi)結(jié)束就發(fā)新命令芯片會(huì)直接忽略表現(xiàn)出來(lái)就是“某些命令偶發(fā)失效”。這種問(wèn)題在輪詢模式下尤其隱蔽因?yàn)樗菚r(shí)序相關(guān)的不是每次都穩(wěn)定復(fù)現(xiàn)。調(diào)諧狀態(tài)機(jī)我維護(hù)了三個(gè)狀態(tài)SI47XX_IDLE、SI47XX_SEEKING、SI47XX_TUNED。調(diào)諧命令分為兩步先寫(xiě)頻率再周期性查詢調(diào)諧狀態(tài)讀到STCSeek/Tune Complete標(biāo)志后再返回給上層。這樣設(shè)計(jì)的好處是用戶態(tài)發(fā)一個(gè)TUNE ioctl不會(huì)阻塞太久狀態(tài)查詢可以異步完成。4.3 ioctl接口與用戶態(tài)配合misc設(shè)備的ops里ioctl是最核心的部分。我定義了幾個(gè)私有命令#define SI47XX_IOC_TUNE _IOW(S, 0x10, int) #define SI47XX_IOC_SEEK _IOW(S, 0x11, unsigned int) #define SI47XX_IOC_GET_STATUS _IOR(S, 0x12, struct si47xx_status) #define SI47XX_IOC_SET_MUTE _IOW(S, 0x13, int) #define SI47XX_IOC_GET_SIGNAL _IOR(S, 0x14, int)TUNE的實(shí)現(xiàn)邏輯很簡(jiǎn)單把用戶傳進(jìn)來(lái)的MHz值先乘以100轉(zhuǎn)成kHz再除以10換算成芯片能識(shí)別的單位然后調(diào)用si47xx_tune_freq。SEEK方向用0或1表示向上、向下搜索底層發(fā)FM_SEEK_START命令后進(jìn)入輪詢。用戶態(tài)配合的時(shí)候需要按“打開(kāi)設(shè)備 - TUNE - 查詢狀態(tài) - 獲取信號(hào)強(qiáng)度”的順序調(diào)用。我建議在用戶態(tài)維護(hù)一個(gè)播放線程把查詢和音頻出來(lái)的節(jié)奏分開(kāi)不要在一個(gè)線程里又發(fā)命令又等結(jié)果。int fd open(/dev/si47xx, O_RDWR); ioctl(fd, SI47XX_IOC_TUNE, 1000); /* 100.0MHz */ struct si47xx_status st; ioctl(fd, SI47XX_IOC_GET_STATUS, st); /* st.signal 就是RSSI可以拿來(lái)顯示信號(hào)格數(shù) */5. 實(shí)測(cè)中的坑和調(diào)試手段5.1 上電順序和復(fù)位引腳的坑我在測(cè)試中遇到的第一個(gè)詭異問(wèn)題系統(tǒng)剛啟動(dòng)的時(shí)候第一次調(diào)諧經(jīng)常失敗但第二次調(diào)諧就好了。排查了很久最終定位到是復(fù)位時(shí)序的問(wèn)題。板子的復(fù)位引腳接了RC延時(shí)電路而驅(qū)動(dòng)的probe在時(shí)鐘初始化完成之前就跑了導(dǎo)致復(fù)位引腳還在低電平時(shí)POWER_UP命令已經(jīng)發(fā)出去了。芯片收不到正確上電序列命令被忽略。后面改成在probe里主動(dòng)控制復(fù)位GPIO拉低、延時(shí)、拉高再延時(shí)問(wèn)題就沒(méi)再出現(xiàn)過(guò)。類似的問(wèn)題也容易出現(xiàn)在系統(tǒng)休眠喚醒之后。suspend時(shí)驅(qū)動(dòng)需要給芯片發(fā)POWER_DOWN命令然后拉低復(fù)位resume時(shí)按上電時(shí)序重新初始化。如果只做簡(jiǎn)單的重新probe不按狀態(tài)機(jī)走芯片會(huì)出現(xiàn)“半復(fù)位”狀態(tài)表現(xiàn)為能響應(yīng)一部分命令、不能響應(yīng)另一部分非常難受。5.2 中斷與CTS超時(shí)我最初用的是輪詢方式等待CTS代碼簡(jiǎn)單也能跑。但后來(lái)發(fā)現(xiàn)在高I2C總線負(fù)載下輪詢會(huì)導(dǎo)致命令擁堵上一輪結(jié)果還沒(méi)處理完下一輪又開(kāi)始發(fā)命令芯片直接忽略。后面改成用中斷引腳觸發(fā)GET_INT_STATUS查詢把CTS等待從忙輪詢變成事件驅(qū)動(dòng)穩(wěn)了很多。還有一個(gè)容易忽略的點(diǎn)GET_INT_STATUS0x14命令的副作用是清中斷標(biāo)志。如果驅(qū)動(dòng)里有多個(gè)地方讀中斷狀態(tài)讀取的時(shí)機(jī)錯(cuò)開(kāi)否則中斷標(biāo)志被提前清掉硬件事件就丟了。我在驅(qū)動(dòng)里加了一個(gè)自旋鎖保護(hù)中斷狀態(tài)讀取和清除的原子性實(shí)測(cè)基本杜絕了偶發(fā)丟事件。5.3 調(diào)試工具與經(jīng)驗(yàn)輸出內(nèi)核驅(qū)動(dòng)調(diào)試效率最高的不是到處加printk而是把內(nèi)核自帶的I2C調(diào)試開(kāi)關(guān)打開(kāi)加上ftrace。打開(kāi)CONFIG_I2C_DEBUG_CORE、CONFIG_I2C_DEBUG_ALGO、CONFIG_I2C_DEBUG_BUS之后dmesg里會(huì)打印每一次I2C傳輸?shù)牡刂贰⒎较?、長(zhǎng)度和數(shù)據(jù)內(nèi)容。對(duì)比正常和異??偩€上的數(shù)據(jù)能快速判斷是驅(qū)動(dòng)發(fā)錯(cuò)命令還是芯片沒(méi)有正確應(yīng)答。在debugfs里我也加了一個(gè)簡(jiǎn)單節(jié)點(diǎn)導(dǎo)出RSSI、SNR、芯片part number、當(dāng)前頻率這些信息。調(diào)試階段用cat就能看到芯片狀態(tài)比每次收到信號(hào)強(qiáng)度都要經(jīng)過(guò)ioctl方便得多。這個(gè)習(xí)慣我一直保留強(qiáng)烈建議所有內(nèi)核驅(qū)動(dòng)都這么做。5.4 不同內(nèi)核分支的適配注意如果是在高通CAF這類深度定制內(nèi)核上工作還要多留個(gè)心眼。老版本內(nèi)核的clock framework、GPIO desc接口跟上游不完全一致直接移植linux-next上的驅(qū)動(dòng)代碼會(huì)編不過(guò)。遇到接口差異不要硬懟把差異點(diǎn)隔離到一個(gè)小文件里加上#ifdef或者按內(nèi)核版本選擇實(shí)現(xiàn)能省掉很多后期維護(hù)煩惱。還有一點(diǎn)關(guān)于型號(hào)兼容Si4704、Si4705、Si4708雖然同屬SI47xx家族但property地址和默認(rèn)參數(shù)有細(xì)微差別。如果代碼準(zhǔn)備適配多款芯片probe的時(shí)候先讀GET_PART_INFO然后按part id走不同的初始化。不要默認(rèn)所有型號(hào)都一樣這個(gè)坑踩的人不少。我在整個(gè)項(xiàng)目里最深的一個(gè)體會(huì)是驅(qū)動(dòng)開(kāi)發(fā)的瓶頸從來(lái)不是“怎么發(fā)I2C命令”這種語(yǔ)法層面的問(wèn)題而是對(duì)芯片狀態(tài)機(jī)理解得夠不夠透。SI47xx的命令手冊(cè)不算厚但每一個(gè)寄存器位背后都有存在的理由理解它們才能寫(xiě)出真正穩(wěn)定的驅(qū)動(dòng)。后續(xù)如果你想擴(kuò)展RDS數(shù)據(jù)解析或者調(diào)頻自動(dòng)搜臺(tái)我建議在現(xiàn)有驅(qū)動(dòng)上疊加不要另起爐灶把狀態(tài)機(jī)維護(hù)在一個(gè)地方擴(kuò)展起來(lái)會(huì)順手很多。本文還有配套的精品資源點(diǎn)擊獲取