動(dòng)開發(fā)實(shí)戰(zhàn):從I2C設(shè)備樹到多點(diǎn)觸控上報(bào))
簡介ET6238驅(qū)動(dòng)是一份面向嵌入式開發(fā)者的I2C接口驅(qū)動(dòng)源碼用于控制ET6238芯片驅(qū)動(dòng)最多16行8列LED矩陣顯示適合數(shù)字儀表盤、信息屏、游戲設(shè)備等需要段位顯示的項(xiàng)目。資源包共2個(gè)文件包含1個(gè)C源文件與1個(gè)頭文件壓縮包僅2KB體量輕便便于直接集成到現(xiàn)有工程。頭文件對(duì)外暴露初始化、命令與數(shù)據(jù)寫入、亮度調(diào)節(jié)等接口源文件則實(shí)現(xiàn)I2C底層通信、從設(shè)備地址設(shè)置、LED段位位操作及PWM灰度控制并兼顧超時(shí)重試等錯(cuò)誤處理與同步、異步調(diào)用方式。已有405人學(xué)習(xí)下載可作為快速上手ET6238顯示控制的參考實(shí)現(xiàn)幫助開發(fā)者理清I2C尋址、矩陣刷新與亮度調(diào)節(jié)的代碼組織思路減少從零調(diào)試的時(shí)間成本。1. 從一顆觸摸芯片說起ET6238 驅(qū)動(dòng)到底能幫你省下多少事如果你手里有一塊帶 ET6238 觸摸控制器的板子插上電卻發(fā)現(xiàn)系統(tǒng)根本不認(rèn)它或者能識(shí)別但坐標(biāo)亂跳、多點(diǎn)觸控時(shí)斷時(shí)續(xù)那你大概率需要一份能直接編譯進(jìn)內(nèi)核的驅(qū)動(dòng)源碼。ET6238 是一顆電容式觸摸屏控制芯片常見于中小尺寸的工業(yè) HMI、車載中控和便攜終端。它通過 I2C 總線與主控通信上報(bào)觸摸坐標(biāo)、按壓狀態(tài)和手勢(shì)事件。這份 C 源碼驅(qū)動(dòng)的價(jià)值在于它把芯片初始化、中斷處理、坐標(biāo)解析、多點(diǎn)觸控協(xié)議上報(bào)這一整條鏈路都寫清楚了你不需要對(duì)著數(shù)據(jù)手冊(cè)從零啃寄存器直接改改配置就能跑。適合誰適合正在做嵌入式 Linux 或裸機(jī)觸摸屏適配的開發(fā)者尤其是那些拿到源碼包卻不知道從哪個(gè)文件開始看的人。2. 驅(qū)動(dòng)架構(gòu)拆解從 I2C 設(shè)備樹到 input 子系統(tǒng)的完整鏈路2.1 驅(qū)動(dòng)分層與文件職責(zé)拿到一份 ET6238 驅(qū)動(dòng)源碼先別急著編譯。我一般會(huì)先看目錄結(jié)構(gòu)搞清楚每個(gè)文件在整條鏈路里扮演什么角色。典型的 ET6238 驅(qū)動(dòng)會(huì)分成這么幾塊文件/模塊職責(zé)關(guān)鍵依賴et6238_core.c芯片初始化、寄存器讀寫、固件加載I2C 子系統(tǒng)et6238_ts.c觸摸事件解析、坐標(biāo)轉(zhuǎn)換、上報(bào)input 子系統(tǒng)et6238_irq.c中斷注冊(cè)、中斷處理、喚醒機(jī)制GPIO / 中斷子系統(tǒng)et6238_cfg.h寄存器地址、閾值、分辨率等宏定義無et6238_i2c.cI2C 設(shè)備匹配、probe/remove 實(shí)現(xiàn)設(shè)備樹 / ACPI這個(gè)分層不是隨便切的。core 層只關(guān)心“怎么讓芯片開始工作”ts 層只關(guān)心“怎么把原始數(shù)據(jù)變成 input 事件”irq 層只關(guān)心“什么時(shí)候去讀數(shù)據(jù)”。三層之間通過函數(shù)指針和回調(diào)銜接改一層基本不影響另外兩層。常見做法是 core 層導(dǎo)出一個(gè)et6238_read_touch_data()給 ts 層調(diào)用irq 層在中斷里觸發(fā)一個(gè)工作隊(duì)列工作隊(duì)列再去調(diào) ts 層的解析函數(shù)。2.2 設(shè)備樹節(jié)點(diǎn)與 I2C 地址配置ET6238 掛載在 I2C 總線上設(shè)備樹里必須有一個(gè)對(duì)應(yīng)的節(jié)點(diǎn)。很多新手翻車就翻在這里地址寫錯(cuò)、中斷引腳配錯(cuò)、復(fù)位引腳沒加驅(qū)動(dòng) probe 直接返回 -ENODEV。i2c1 { status okay; clock-frequency 400000; et6238: touchscreen2a { compatible etek,et6238; reg 0x2a; /* I2C 從機(jī)地址7 位 */ interrupt-parent gpio1; interrupts 12 IRQ_TYPE_EDGE_FALLING; /* INT 引腳下降沿觸發(fā) */ reset-gpios gpio1 13 GPIO_ACTIVE_LOW; /* RST 引腳低有效 */ irq-gpios gpio1 12 GPIO_ACTIVE_LOW; touchscreen-size-x 800; touchscreen-size-y 1280; touchscreen-inverted-x; touchscreen-swapped-x-y; vdd-supply vcc_3v3; status okay; }; };這段設(shè)備樹里幾個(gè)參數(shù)值得逐條說清楚。reg 0x2a是 ET6238 的 I2C 從機(jī)地址具體值看硬件原理圖上的 ADDR 引腳接法接 GND 和接 VCC 地址不一樣。interrupts里的觸發(fā)方式要和芯片 INT 引腳的實(shí)際行為匹配ET6238 一般配下降沿或低電平觸發(fā)配錯(cuò)了要么中斷丟要么中斷風(fēng)暴。touchscreen-size-x/y是觸摸面板的物理分辨率必須和屏參一致否則坐標(biāo)會(huì)縮放錯(cuò)位。touchscreen-inverted-x和touchscreen-swapped-x-y這兩個(gè)屬性是給坐標(biāo)軸方向不對(duì)時(shí)用的先不加跑起來看坐標(biāo)方向再?zèng)Q定。提示設(shè)備樹改完記得重新編譯 dtb 并更新到板子上只改 dts 不編譯等于沒改。2.3 probe 函數(shù)里的初始化順序probe 函數(shù)是驅(qū)動(dòng)的入口順序錯(cuò)了芯片可能起不來。我見過有人先注冊(cè) input 設(shè)備再去初始化芯片結(jié)果中斷來了 input 還沒準(zhǔn)備好直接空指針。正確的順序應(yīng)該是拿 GPIO → 硬復(fù)位 → I2C 通信驗(yàn)證 → 讀芯片 ID → 加載配置 → 注冊(cè) input → 申請(qǐng)中斷。static int et6238_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct et6238_ts_data *ts; int ret; ts devm_kzalloc(client-dev, sizeof(*ts), GFP_KERNEL); if (!ts) return -ENOMEM; ts-client client; i2c_set_clientdata(client, ts); /* 1. 獲取并復(fù)位 GPIO */ ts-reset_gpio devm_gpiod_get_optional(client-dev, reset, GPIOD_OUT_LOW); if (IS_ERR(ts-reset_gpio)) return PTR_ERR(ts-reset_gpio); /* 2. 硬復(fù)位拉低 10ms 再拉高等待芯片內(nèi)部初始化 */ if (ts-reset_gpio) { gpiod_set_value_cansleep(ts-reset_gpio, 0); msleep(10); gpiod_set_value_cansleep(ts-reset_gpio, 1); msleep(50); /* 數(shù)據(jù)手冊(cè)要求的上電穩(wěn)定時(shí)間 */ } /* 3. 驗(yàn)證 I2C 通信讀芯片 ID */ ret et6238_i2c_read(ts, ET6238_REG_CHIP_ID, ts-chip_id); if (ret 0) { dev_err(client-dev, failed to read chip id: %d\n, ret); return ret; } if (ts-chip_id ! ET6238_CHIP_ID) { dev_err(client-dev, chip id mismatch: 0x%02x\n, ts-chip_id); return -ENODEV; } /* 4. 加載觸摸配置參數(shù) */ ret et6238_load_config(ts); if (ret 0) return ret; /* 5. 注冊(cè) input 設(shè)備 */ ret et6238_input_init(ts); if (ret 0) return ret; /* 6. 申請(qǐng)中斷最后一步 */ ret devm_request_threaded_irq(client-dev, client-irq, NULL, et6238_irq_handler, IRQF_ONESHOT | IRQF_TRIGGER_FALLING, client-name, ts); if (ret 0) { dev_err(client-dev, failed to request irq: %d\n, ret); return ret; } return 0; }這段代碼里每一步都有講究。devm_gpiod_get_optional用 optional 是因?yàn)橛行┌遄訌?fù)位引腳直接接 RC 電路不需要軟件控制。復(fù)位后的msleep(50)是給芯片內(nèi)部固件啟動(dòng)留時(shí)間短了讀 ID 會(huì)失敗。讀 ID 那步是判斷 I2C 通不通的最快方式比讀坐標(biāo)寄存器更可靠。IRQF_ONESHOT表示中斷線程化后只觸發(fā)一次配合IRQF_TRIGGER_FALLING使用避免中斷處理函數(shù)里做 I2C 讀時(shí)被同級(jí)中斷打斷。2.4 中斷處理與坐標(biāo)上報(bào)ET6238 的中斷處理不能直接在硬中斷里做 I2C 讀因?yàn)?I2C 傳輸可能睡眠。常見做法是用request_threaded_irq把中斷線程化或者硬中斷里調(diào)度一個(gè)工作隊(duì)列。下面這段是線程化中斷處理函數(shù)的典型寫法static irqreturn_t et6238_irq_handler(int irq, void *dev_id) { struct et6238_ts_data *ts dev_id; u8 touch_num; int ret; /* 讀觸摸點(diǎn)數(shù)量寄存器 */ ret et6238_i2c_read(ts, ET6238_REG_TOUCH_NUM, touch_num); if (ret 0) { dev_err(ts-client-dev, read touch num failed\n); return IRQ_HANDLED; } touch_num 0x0F; /* 低 4 位有效高 4 位保留 */ if (touch_num 0 || touch_num ET6238_MAX_TOUCH) { et6238_report_release(ts); /* 上報(bào)抬起事件 */ return IRQ_HANDLED; } /* 讀取所有觸摸點(diǎn)坐標(biāo) */ ret et6238_read_touch_data(ts, touch_num); if (ret 0) return IRQ_HANDLED; et6238_report_touch(ts, touch_num); return IRQ_HANDLED; }touch_num寄存器低 4 位表示當(dāng)前按下的手指數(shù)量0 表示全部抬起。ET6238_MAX_TOUCH一般是 5 或 10看具體型號(hào)。et6238_report_release里要對(duì)每一個(gè)之前上報(bào)過的 slot 調(diào)用input_mt_slot和input_mt_report_slot_state(false)漏掉任何一個(gè)都會(huì)導(dǎo)致系統(tǒng)里有個(gè)“幽靈手指”一直按著。et6238_read_touch_data里讀坐標(biāo)時(shí)要注意字節(jié)序ET6238 的坐標(biāo)寄存器是 16 位高字節(jié)在前還是低字節(jié)在前看數(shù)據(jù)手冊(cè)讀反了坐標(biāo)會(huì)變成亂碼。3. 編譯與運(yùn)行從源碼到板子上跑通觸摸3.1 內(nèi)核配置與編譯選項(xiàng)ET6238 驅(qū)動(dòng)要編譯進(jìn)內(nèi)核先得讓 Kconfig 和 Makefile 認(rèn)識(shí)它。在驅(qū)動(dòng)目錄下加兩行# drivers/input/touchscreen/Makefile obj-$(CONFIG_TOUCHSCREEN_ET6238) et6238.o et6238-objs : et6238_core.o et6238_ts.o et6238_irq.o et6238_i2c.o# drivers/input/touchscreen/Kconfig config TOUCHSCREEN_ET6238 tristate ET6238 I2C touchscreen depends on I2C depends on INPUT select INPUT_MT select INPUT_MT_DIRECT help Say Y here to enable ET6238 touchscreen support.tristate表示可以編譯成模塊調(diào)試階段建議編成模塊改完 insmod 就行不用反復(fù)燒內(nèi)核。select INPUT_MT和INPUT_MT_DIRECT必須選上否則多點(diǎn)觸控協(xié)議上報(bào)會(huì)編譯報(bào)錯(cuò)。編譯命令就是標(biāo)準(zhǔn)的內(nèi)核模塊編譯make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules -j$(nproc)編完在drivers/input/touchscreen/下會(huì)生成et6238.ko拷到板子上insmod et6238.ko就能加載。3.2 加載后驗(yàn)證觸摸是否正常驅(qū)動(dòng)加載后先看內(nèi)核日志有沒有 probe 成功dmesg | grep et6238 # 期望輸出類似 # et6238 1-002a: chip id: 0x62 # et6238 1-002a: touchscreen registered # input: et6238 as /devices/platform/soc/1c2ac00.i2c/i2c-1/1-002a/input/input3然后確認(rèn) input 設(shè)備節(jié)點(diǎn)出現(xiàn)了ls /dev/input/ # 應(yīng)該能看到 eventX用 cat /proc/bus/input/devices 確認(rèn)哪個(gè)是 et6238 cat /proc/bus/input/devices | grep -A5 et6238最直接的驗(yàn)證是用evtest工具evtest /dev/input/event3 # 手指按上去應(yīng)該能看到 ABS_MT_POSITION_X/Y 事件如果evtest里坐標(biāo)不動(dòng)先查中斷計(jì)數(shù)cat /proc/interrupts | grep et6238 # 按屏幕時(shí)計(jì)數(shù)應(yīng)該增加不增加說明中斷沒觸發(fā)中斷計(jì)數(shù)不漲九成是設(shè)備樹里中斷觸發(fā)方式配錯(cuò)了或者 INT 引腳被別的驅(qū)動(dòng)占用了。中斷漲但坐標(biāo)不動(dòng)那就是 I2C 讀數(shù)據(jù)失敗用i2cdetect -y 1確認(rèn)芯片地址能不能掃到。3.3 坐標(biāo)校準(zhǔn)與方向調(diào)整坐標(biāo)方向不對(duì)是最常見的問題?,F(xiàn)象是手指往右滑光標(biāo)往上跑或者左右鏡像。解決方式有兩種改設(shè)備樹屬性或者改驅(qū)動(dòng)里的坐標(biāo)轉(zhuǎn)換函數(shù)。設(shè)備樹方式最簡單加touchscreen-inverted-x、touchscreen-inverted-y、touchscreen-swapped-x-y這三個(gè)屬性的組合就能覆蓋八種方向。如果設(shè)備樹屬性不夠用就在et6238_report_touch里改static void et6238_report_touch(struct et6238_ts_data *ts, u8 touch_num) { int i; for (i 0; i touch_num; i) { u16 x ts-touch_data[i].x; u16 y ts-touch_data[i].y; /* 坐標(biāo)轉(zhuǎn)換根據(jù)實(shí)際方向調(diào)整 */ if (ts-swap_xy) swap(x, y); if (ts-invert_x) x ts-max_x - x; if (ts-invert_y) y ts-max_y - y; input_mt_slot(ts-input, i); input_mt_report_slot_state(ts-input, MT_TOOL_FINGER, true); input_report_abs(ts-input, ABS_MT_POSITION_X, x); input_report_abs(ts-input, ABS_MT_POSITION_Y, y); } input_mt_sync_frame(ts-input); input_sync(ts-input); }swap_xy交換 X/Y 軸invert_x和invert_y做鏡像翻轉(zhuǎn)。input_mt_sync_frame是收尾動(dòng)作告訴 input 子系統(tǒng)這一幀數(shù)據(jù)完了。漏掉input_sync的話事件會(huì)攢在緩沖區(qū)里不發(fā)出表現(xiàn)就是觸摸完全沒反應(yīng)。4. 避坑與排查ET6238 驅(qū)動(dòng)調(diào)試中最容易翻車的幾個(gè)點(diǎn)4.1 中斷風(fēng)暴導(dǎo)致系統(tǒng)卡死現(xiàn)象加載驅(qū)動(dòng)后系統(tǒng)變卡top里看到irq/xx-et6238占用率很高/proc/interrupts里中斷計(jì)數(shù)瘋漲。原因中斷觸發(fā)方式配成了電平觸發(fā)但芯片 INT 引腳在數(shù)據(jù)沒讀完后一直保持有效電平導(dǎo)致中斷反復(fù)觸發(fā)?;蛘咧袛嗵幚砗瘮?shù)里沒有清中斷標(biāo)志。解決把設(shè)備樹里的IRQ_TYPE_EDGE_FALLING改成邊沿觸發(fā)或者在中斷處理函數(shù)開頭先讀一次狀態(tài)寄存器清標(biāo)志。ET6238 一般用下降沿觸發(fā)就夠了不要用電平觸發(fā)。4.2 probe 返回 -ENODEV 但 I2C 能掃到地址現(xiàn)象i2cdetect能看到 0x2a 有設(shè)備但驅(qū)動(dòng) probe 失敗日志報(bào)chip id mismatch。原因讀芯片 ID 的寄存器地址寫錯(cuò)了或者讀出來的字節(jié)序不對(duì)。有些板子 I2C 上拉電阻偏大400kHz 下讀數(shù)據(jù)不穩(wěn)定讀回來的 ID 是隨機(jī)值。解決先用i2cget手動(dòng)讀一次 ID 寄存器確認(rèn)值對(duì)不對(duì)i2cget -y 1 0x2a 0x00 w # 對(duì)比數(shù)據(jù)手冊(cè)里的 CHIP_ID 默認(rèn)值如果手動(dòng)讀也不對(duì)把 I2C 頻率降到 100kHz 試試。如果手動(dòng)讀對(duì)但驅(qū)動(dòng)讀不對(duì)檢查驅(qū)動(dòng)里et6238_i2c_read的寄存器地址偏移和字節(jié)序處理。4.3 多點(diǎn)觸控只上報(bào)一個(gè)點(diǎn)現(xiàn)象evtest里只能看到一個(gè)手指的坐標(biāo)第二個(gè)手指按上去沒反應(yīng)。原因input_mt_init_slots時(shí) slot 數(shù)量傳了 1或者ABS_MT_SLOT沒有正確設(shè)置。還有一種情況是input_mt_report_slot_state的第三個(gè)參數(shù)傳了 false導(dǎo)致 slot 被標(biāo)記為未使用。解決確認(rèn)input_mt_init_slots(ts-input, ET6238_MAX_TOUCH, INPUT_MT_DIRECT)里第二個(gè)參數(shù)是最大觸摸點(diǎn)數(shù)不是 1。每個(gè)觸摸點(diǎn)上報(bào)前必須調(diào)input_mt_slot(ts-input, i)指定 slot 編號(hào)i 從 0 開始。4.4 坐標(biāo)跳變或漂移現(xiàn)象手指不動(dòng)坐標(biāo)自己小幅跳動(dòng)或者滑動(dòng)時(shí)坐標(biāo)突然跳到邊緣。原因坐標(biāo)寄存器讀取時(shí)沒有做有效性判斷讀到了無效數(shù)據(jù)。ET6238 在觸摸抬起瞬間坐標(biāo)寄存器可能還保留著上一次的值如果此時(shí) touch_num 已經(jīng)變成 0 但代碼還在讀坐標(biāo)就會(huì)上報(bào)臟數(shù)據(jù)。解決在讀坐標(biāo)前先判斷touch_num是否大于 0等于 0 直接上報(bào) release。另外可以在驅(qū)動(dòng)里加一個(gè)簡單的濾波連續(xù)兩次坐標(biāo)差值超過閾值就丟棄當(dāng)前幀。4.5 休眠喚醒后觸摸失效現(xiàn)象系統(tǒng) suspend 再 resume 后觸摸沒反應(yīng)dmesg里也沒有中斷計(jì)數(shù)增加。原因suspend 時(shí)芯片進(jìn)入了低功耗模式resume 后沒有重新初始化?;蛘?GPIO 狀態(tài)在 suspend 時(shí)被改變INT 引腳不再觸發(fā)中斷。解決實(shí)現(xiàn)et6238_suspend和et6238_resume回調(diào)resume 里重新做一次硬復(fù)位和配置加載。如果芯片支持自動(dòng)喚醒確認(rèn)ET6238_REG_POWER_MODE寄存器在 resume 后被正確設(shè)置。5. 二次開發(fā)改造從能跑到好用我一般會(huì)動(dòng)這幾個(gè)地方驅(qū)動(dòng)跑通只是第一步真正放到產(chǎn)品里還得改。我拿到一份能跑的 ET6238 驅(qū)動(dòng)后通常會(huì)從三個(gè)方向做二次開發(fā)性能調(diào)優(yōu)、功能擴(kuò)展、可維護(hù)性改造。性能調(diào)優(yōu)方面最直接的是改中斷上報(bào)頻率。ET6238 默認(rèn)每次中斷上報(bào)一幀數(shù)據(jù)如果手指快速滑動(dòng)中斷太頻繁會(huì)占 CPU。我一般會(huì)在中斷處理里加一個(gè)簡單的節(jié)流兩次上報(bào)間隔小于 5ms 就跳過用jiffies做判斷。另一個(gè)點(diǎn)是 I2C 傳輸優(yōu)化把連續(xù)讀多個(gè)寄存器的操作合并成一次i2c_transfer減少總線開銷。實(shí)測(cè)下來合并傳輸能把一次觸摸讀取從 6 次 I2C 傳輸降到 2 次。功能擴(kuò)展方面ET6238 支持手勢(shì)識(shí)別但很多驅(qū)動(dòng)只實(shí)現(xiàn)了基礎(chǔ)觸摸。我會(huì)在et6238_irq_handler里加一個(gè)手勢(shì)寄存器讀取把雙擊、長按、滑動(dòng)方向解析出來通過input_report_key上報(bào)。手勢(shì)閾值可以在et6238_cfg.h里定義成宏方便不同項(xiàng)目調(diào)整。還有一個(gè)實(shí)用改造是加 sysfs 節(jié)點(diǎn)把固件版本、觸摸點(diǎn)數(shù)、坐標(biāo)范圍暴露出來產(chǎn)線測(cè)試時(shí)不用接串口就能讀??删S護(hù)性改造方面我會(huì)把芯片相關(guān)的寄存器操作全部收進(jìn)et6238_reg.h和et6238_reg.c用宏定義代替裸地址。這樣換一顆同系列的芯片只需要改寄存器定義文件核心邏輯不動(dòng)。另外加一套 debugfs 接口運(yùn)行時(shí)能動(dòng)態(tài)改閾值和坐標(biāo)方向不用重新編譯驅(qū)動(dòng)。驗(yàn)證改造效果的方法很簡單改完先insmod加載用evtest看事件流是否正常然后用getevent -lt看時(shí)間戳確認(rèn)上報(bào)間隔穩(wěn)定最后跑一個(gè)滑動(dòng)測(cè)試腳本連續(xù)畫圈 100 次看有沒有丟點(diǎn)或跳點(diǎn)。我一般會(huì)寫一個(gè)簡單的 Python 腳本讀/dev/input/eventX統(tǒng)計(jì)每秒事件數(shù)和坐標(biāo)方差方差突然變大就說明有臟數(shù)據(jù)。注意改中斷節(jié)流參數(shù)時(shí)不要拍腦袋定值先用evtest測(cè)出實(shí)際觸摸幀率再根據(jù)幀率設(shè)節(jié)流窗口。設(shè)大了會(huì)感覺觸摸不跟手設(shè)小了等于沒節(jié)流。從那以后我每次改完驅(qū)動(dòng)都強(qiáng)制走一遍“insmod → evtest 看事件 → getevent 看時(shí)間戳 → 滑動(dòng)測(cè)試腳本跑 100 次”這個(gè)流程少一步都可能把問題帶到產(chǎn)線。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取