源碼解析:V4L2與超聲波測距的驅(qū)動(dòng)設(shè)計(jì))
簡介面向Arm嵌入式開發(fā)與Qt應(yīng)用學(xué)習(xí)者這份C智能車載系統(tǒng)源碼實(shí)現(xiàn)了天氣預(yù)報(bào)、音視頻播放、倒車?yán)走_(dá)、行車記錄儀及多語言切換等常見車機(jī)模塊。整套代碼共115個(gè)文件包含cpp/h/ui核心源碼、png圖片與gif動(dòng)圖界面素材、m4a/mp3測試音頻、ts/qm國際化翻譯文件、ko驅(qū)動(dòng)模塊及makefile構(gòu)建腳本壓縮包大小26.54MB目錄結(jié)構(gòu)貼近真實(shí)項(xiàng)目便于按模塊查閱。已有468人學(xué)習(xí)下載。從預(yù)覽來看資源覆蓋攝像頭圖像采集與格式轉(zhuǎn)換、超聲波測距驅(qū)動(dòng)、按鍵輸入處理、音樂播放、倒車邏輯及多語言切換等具體實(shí)現(xiàn)讀者可同時(shí)看到底層驅(qū)動(dòng)與上層Qt界面代碼適合課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)或想了解QtLinux車載項(xiàng)目整體搭建的開發(fā)者作為參考。1. 這套車載源碼最值錢的地方不在Qt界面在驅(qū)動(dòng)與界面之間的那條鏈路把C基于Arm和Qt的智能車載系統(tǒng)源碼.zip解壓之后我第一反應(yīng)是它的文件命名相當(dāng)工程化v4l2.c、yuv2bgr.c、sr04_drv.c、key_drv.c這幾個(gè)文件一眼就能看出底層數(shù)據(jù)通路是怎么設(shè)計(jì)的而music.cpp、monitor.cpp、language.cpp又把 Qt 側(cè)的功能切得很干凈。整套源碼解決的是一個(gè)非常典型的嵌入式問題在 Arm 平臺(tái)上讓一塊 LCD 既能跑多媒體娛樂又能實(shí)時(shí)接管倒車影像和超聲波雷達(dá)數(shù)據(jù)同時(shí)保留多語言切換這類產(chǎn)品化能力。對于正在做車載 HMI、或者想把現(xiàn)有 Qt 程序遷到 Arm 板子上的開發(fā)者來說這套源碼的參考價(jià)值集中在三條線上V4L2 采集與格式轉(zhuǎn)換、SR04 測距與中斷交互、Qt 應(yīng)用層與底層驅(qū)動(dòng)的接口設(shè)計(jì)。需要先說清楚的是這套源碼里 Qt 版本屬于常規(guī)的 Widgets 工程真正需要花時(shí)間理解的是 C 驅(qū)動(dòng)文件和 C 界面之間如何通過事件和共享內(nèi)存協(xié)同工作。2. 交叉編譯環(huán)境與源碼分層把每個(gè)文件映射到車載系統(tǒng)的真實(shí)模塊2.1 源碼文件與模塊映射關(guān)系先別急著編譯把這幾個(gè)文件按功能歸類比直接讀代碼更有效率。carsystem.cpp是主程序入口負(fù)責(zé)初始化窗口和管理各模塊的調(diào)度v4l2.c封裝了 Video4Linux2 攝像頭采集通道對應(yīng)行車記錄儀和倒車影像的視頻源yuv2bgr.c是顏色空間轉(zhuǎn)換因?yàn)?V4L2 默認(rèn)輸出的 YUYV 格式無法直接被 Qt 的 QImage 使用sr04_drv.c封裝了 HC-SR04 超聲波測距負(fù)責(zé)倒車?yán)走_(dá)的距離計(jì)算key_drv.c處理實(shí)體按鍵用于在音樂、視頻、倒車界面之間切換music.cpp和monitor.cpp分別是音樂播放器和行車記錄儀的邏輯模塊backoff.cpp專門處理倒車時(shí)的界面切換與雷達(dá)數(shù)據(jù)疊加language.cpp則負(fù)責(zé)加載和切換翻譯文件。源文件職責(zé)依賴鏈路v4l2.c攝像頭設(shè)備打開、格式協(xié)商、幀緩沖映射內(nèi)核 V4L2 框架/dev/video0yuv2bgr.cYUYV 到 BGR888 轉(zhuǎn)換僅內(nèi)存操作無系統(tǒng)調(diào)用sr04_drv.c超聲波的觸發(fā)與 Echo 回波計(jì)時(shí)GPIO 操作需 root 權(quán)限key_drv.c按鍵狀態(tài)讀取與去抖按鍵 GPIO 或 input 子系統(tǒng)carsystem.cpp主窗口、模塊調(diào)度、信號(hào)鏈接依賴以上全部模塊music.cpp音頻播放、播放列表管理Qt Multimediamonitor.cpp視頻幀采集入庫、循環(huán)覆蓋舊文件v4l2.c 文件系統(tǒng)backoff.cpp倒車觸發(fā)邏輯、距離閾值判斷sr04_drv.c 視頻鏈路language.cppQTranslator 加載與切換Qt Core.qm文件從這張表能看出來這套源碼的耦合方式是典型的「底層 C 驅(qū)動(dòng) 上層 C 封裝」底層文件不依賴 Qt只暴露 C 接口上層 Qt 模塊通過 extern C 聲明或封裝類調(diào)用這些接口。這樣做的好處是驅(qū)動(dòng)部分可以單獨(dú)用 gcc 編譯驗(yàn)證不牽涉 Qt 元對象編譯調(diào)試問題面會(huì)小很多。2.2 Arm 交叉編譯工具鏈與 Qt 構(gòu)建配置這套源碼要跑在 Arm 開發(fā)板上交叉編譯是繞不開的第一步。項(xiàng)目實(shí)際編譯時(shí)我傾向于用aarch64-linux-gnu-g配合 Qt 的嵌入式 mkspec而不是直接在板子上裝編譯器——板載編譯性能和依賴完整度都不適合。常見的 Qt 交叉編譯配置有兩種一種是直接在 Qt Creator 里設(shè)置 Kit另一種是寫 Makefile 或 CMakeLists 指定交叉工具鏈。這套源碼從文件結(jié)構(gòu)看不是 CMake 工程更像手工 Makefile所以我一般建議直接用 Qt Creator 的 qmake在.pro文件里做以下配置# carsystem.pro QT widgets multimedia network TARGET carsystem TEMPLATE app # 交叉編譯工具鏈前綴根據(jù)實(shí)際編譯器調(diào)整 QMAKE_CXX aarch64-linux-gnu-g QMAKE_CC aarch64-linux-gnu-gcc # 底層C驅(qū)動(dòng)文件用gcc編譯而非g SOURCES carsystem.cpp music.cpp monitor.cpp backoff.cpp language.cpp SOURCES v4l2.c yuv2bgr.c sr04_drv.c key_drv.c HEADERS v4l2.h yuv2bgr.h sr04_drv.h key_drv.h # 鏈接數(shù)學(xué)庫超聲波距離計(jì)算需要 LIBS -lmQMAKE_CXX和QMAKE_CC分別指定 C 和 C 的交叉編譯器qmake 會(huì)根據(jù)源文件后綴自動(dòng)選擇。.c文件走 gcc 編譯路徑.cpp文件走 g 路徑這一點(diǎn)比手動(dòng)把 C 代碼改成.cpp更穩(wěn)妥因?yàn)関4l2.c里的ioctl調(diào)用在 C 編譯時(shí)必須顯式轉(zhuǎn)換void*牽一發(fā)動(dòng)全身。LIBS -lm是給sqrt()這類數(shù)學(xué)函數(shù)準(zhǔn)備的SR04 測距最終算距離時(shí)要開方。2.3 交叉編譯時(shí)最容易踩的三個(gè)環(huán)境坑第一個(gè)坑是 Qt 庫路徑不匹配。目標(biāo)板是 armv8 架構(gòu)就必須用aarch64構(gòu)建的 Qt 庫如果用armv7的庫鏈接程序啟動(dòng)時(shí)會(huì)直接報(bào)非法指令。常見做法是在板子文件系統(tǒng)上放一份libQt5Core.so等運(yùn)行時(shí)庫編譯時(shí)用-L指向交叉編譯好的庫目錄。第二個(gè)坑是qt_qpa_platform_plugin_path環(huán)境變量Qt 在 Arm 板子上啟動(dòng)時(shí)報(bào)could not find a Qt platform plugin絕大多數(shù)是 plugin 路徑?jīng)]指對。我一般會(huì)把 Qt 移植后的platforms/目錄放到與可執(zhí)行文件同級的plugins下然后通過 export 指定export QT_QPA_PLATFORMlinuxfb export QT_QPA_PLATFORM_PLUGIN_PATH/opt/carsystem/plugins export LD_LIBRARY_PATH/opt/carsystem/lib:$LD_LIBRARY_PATH這里用linuxfb平臺(tái)插件是因?yàn)樗恍枰?X11 或者 Wayland直接在frame buffer上繪圖適合當(dāng)前這種帶 LCD 但沒有桌面環(huán)境的車載硬件。LD_LIBRARY_PATH必須包含 Qt 運(yùn)行時(shí)庫目錄否則程序啟動(dòng)階段就會(huì)停在./carsystem: error while loading shared libraries。第三個(gè)坑是文件系統(tǒng)權(quán)限v4l2.c打開/dev/video0、sr04_drv.c操作 GPIO 節(jié)點(diǎn)都需要 root 權(quán)限常見做法是在啟動(dòng)腳本里加chmod 666 /dev/video0或者直接把程序放進(jìn) rootfs 的 init 啟動(dòng)鏈里。3. 底層視頻與測距鏈路v4l2.c、yuv2bgr.c 與 sr04_drv.c 的配合3.1 V4L2 采集流程與關(guān)鍵 ioctl 時(shí)序v4l2.c是整條視頻鏈路的源頭。它做的事情可以拆成四步打開設(shè)備、設(shè)置采集格式、申請幀緩沖并 mmap、啟動(dòng)流。這套流程看似標(biāo)準(zhǔn)化但實(shí)際調(diào)參時(shí)每個(gè) ioctl 都可能成為瓶頸。拿采集格式來說VIDIOC_S_FMT設(shè)置的分辨率和像素格式必須和攝像頭驅(qū)動(dòng)以及后續(xù)的yuv2bgr.c轉(zhuǎn)換邏輯匹配。// v4l2.c 中采集初始化核心片段 static int v4l2_open_device(const char *devpath, int width, int height) { int fd open(devpath, O_RDWR); if (fd 0) { perror(open video device); return -1; } struct v4l2_format fmt; memset(fmt, 0, sizeof(fmt)); fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width width; /* 采集寬度如 640 或 720 */ fmt.fmt.pix.height height; /* 采集高度 */ fmt.fmt.pix.pixelformat V4L2_PIX_FMT_YUYV; /* 與 yuv2bgr.c 匹配 */ if (ioctl(fd, VIDIOC_S_FMT, fmt) 0) { perror(set format failed); close(fd); return -1; } return fd; }VIDIOC_S_FMT成功后可以再通過VIDIOC_G_FMT回讀實(shí)際的寬度和高度因?yàn)橛行?USB 攝像頭會(huì)強(qiáng)制對齊到 32 像素整數(shù)倍。比如你請求 640x480驅(qū)動(dòng)可能返回 640x480 不變但換成 1280x720 時(shí)部分 sensor 會(huì)按 16 字節(jié)對齊調(diào)整 stride此時(shí)yuv2bgr.c轉(zhuǎn)換時(shí)就必須把行字節(jié)數(shù)計(jì)算正確否則畫面會(huì)斜切。代碼里我用V4L2_PIX_FMT_YUYV而不是MJPEG或NV12理由是 YUYV 是裸數(shù)據(jù)無需硬解碼轉(zhuǎn)換邏輯完全可控MJPEG 雖然帶寬占用低但需要額外集成 libjpeg在 Qt 側(cè)還要把 JPEG 解成 RGB鏈路變長。V4L2 常用 ioctl 命令及含義如下ioctl作用失敗時(shí)的常見表現(xiàn)VIDIOC_QUERYCAP查詢設(shè)備能力確認(rèn)是捕獲設(shè)備Invalid argument多半不是 video 設(shè)備VIDIOC_S_FMT設(shè)置采集格式EBUSY設(shè)備被占用或已有應(yīng)用獨(dú)占VIDIOC_REQBUFS申請幀緩沖個(gè)數(shù)ENOMEM顯存或 DMA 內(nèi)存不足VIDIOC_QBUF/DQBUF入隊(duì)/出隊(duì)緩沖幀出隊(duì)超時(shí)通常是幀率設(shè)太高VIDIOC_STREAMON開始傳輸數(shù)據(jù)流EPIPE攝像頭發(fā)出的流格式與設(shè)置不一致這里特別提醒VIDIOC_REQBUFS的數(shù)量不能簡單拍腦袋。常見做法是申請 4 個(gè)緩沖區(qū)而不是默認(rèn)的 2 個(gè)因?yàn)?Qt 界面繪制和yuv2bgr.c轉(zhuǎn)換都會(huì)消耗額外時(shí)間緩沖太少時(shí)DQBUF會(huì)頻繁阻塞影響 UI 線程幀率。但申請?zhí)嘁矔?huì)占滿 DMA 內(nèi)存板子上如果同時(shí)跑音樂播放和視頻采集可能出現(xiàn)物理內(nèi)存碎片導(dǎo)致 mmap 失敗。3.2 YUYV 到 BGR 的轉(zhuǎn)換少一個(gè)顏色分量圖就花了v4l2.c采集到的是 YUYV 交錯(cuò)格式每個(gè)像素由亮度和色度交織表示4 字節(jié)承載 2 個(gè)像素。Qt 的QImage雖然能直接識(shí)別Format_RGB888但不認(rèn) YUYV所以yuv2bgr.c的作用就是把這種交錯(cuò)數(shù)據(jù)展開成連續(xù)的 BGR 像素流。轉(zhuǎn)換的核心是 BT.601 標(biāo)準(zhǔn)這也是本套源碼里算法難度最集中、最值得細(xì)看的地方。// yuv2bgr.c —— YUYV 到 RGB888 的 BT.601 轉(zhuǎn)換 static unsigned char clamp_u8(int value) { return (unsigned char)(value 0 ? 0 : (value 255 ? 255 : value)); } void yuyv_to_bgr(const unsigned char *yuyv, unsigned char *bgr, int width, int height) { int total width * height / 2; /* 每 4 字節(jié)攜帶 2 個(gè)像素 */ for (int i 0; i total; i) { int y0 yuyv[0]; int u yuyv[1] - 128; int y1 yuyv[2]; int v yuyv[3] - 128; /* 像素 0 */ bgr[0] clamp_u8((int)(y0 1.772 * u)); /* B */ bgr[1] clamp_u8((int)(y0 - 0.344 * u - 0.714 * v)); /* G */ bgr[2] clamp_u8((int)(y0 1.402 * v)); /* R */ /* 像素 1 */ bgr[3] clamp_u8((int)(y1 1.772 * u)); bgr[4] clamp_u8((int)(y1 - 0.344 * u - 0.714 * v)); bgr[5] clamp_u8((int)(y1 1.402 * v)); yuyv 4; bgr 6; } }轉(zhuǎn)換時(shí)需要注意幾個(gè)參數(shù)色度信號(hào)u、v必須減去 128 才能得到有符號(hào)的范圍否則暗部會(huì)整體偏綠或偏紫clamp_u8是必要的因?yàn)楦↑c(diǎn)運(yùn)算結(jié)果可能溢出到負(fù)值或超過 255不 clamp 會(huì)導(dǎo)致QImage里出現(xiàn)噪點(diǎn)條紋。但如果板子 CPU 性能弱浮點(diǎn)運(yùn)算在 1280x720 分辨率下每幀耗時(shí)會(huì)到 20ms 以上實(shí)際項(xiàng)目里更快的方式是查表法——預(yù)計(jì)算 256 x 256 的乘法查找表把浮點(diǎn)乘改成整數(shù)查表。這套源碼作為教學(xué)驗(yàn)證用浮點(diǎn)沒問題但你要上 720p 行車記錄儀最好改成整數(shù)定點(diǎn)運(yùn)算。3.3 SR04 超聲波測距時(shí)序與中斷的取舍sr04_drv.c的活兒不是讀一個(gè)寄存器就完事而是要精確模擬 SR04 的觸發(fā)時(shí)序拉高 Trig 引腳 10us然后監(jiān)聽 Echo 引腳的高電平時(shí)長。高電平持續(xù)時(shí)間乘以聲速 340m/s 再除以 2就是障礙物距離。這個(gè)時(shí)序?qū){秒級延遲也有要求如果直接用usleep(10)觸發(fā)內(nèi)核調(diào)度的不確定性會(huì)讓 10us 變成 15us 或 20us測距結(jié)果會(huì)系統(tǒng)性偏差。// sr04_drv.c —— 超聲波距離讀取關(guān)鍵流程 int sr04_get_distance(int trig_fd, int echo_fd) { struct gpio_handle *trig gpio_open(trig_fd); struct gpio_handle *echo gpio_open(echo_fd); gpio_write(trig, 1); /* 常見的做法是用帶 ns 延遲的定時(shí)器自旋等待而不是 usleep */ for (volatile int i 0; i 120; i) __asm__(nop); gpio_write(trig, 0); while (gpio_read(echo) 0); /* 等待 Echo 拉高 */ struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, start); while (gpio_read(echo) 1); /* 等待 Echo 拉低 */ clock_gettime(CLOCK_MONOTONIC, end); double delta (end.tv_sec - start.tv_sec) * 1e9 (end.tv_nsec - start.tv_nsec); return (int)(delta * 0.034 / 2); /* 單位 cm */ }這里clock_gettime(CLOCK_MONOTONIC)必須用單調(diào)時(shí)鐘不能用gettimeofday的墻上時(shí)鐘否則系統(tǒng)同步時(shí)間或 NTP 校時(shí)會(huì)導(dǎo)致時(shí)間跳變測距結(jié)果會(huì)突然變成負(fù)數(shù)或超大值。等 Echo 從高到低的這段循環(huán)里如果超聲波沒收到回波Echo 引腳會(huì)維持高電平約 60ms導(dǎo)致線程阻塞。實(shí)際使用時(shí)要給等待循環(huán)加一個(gè)超時(shí)退出常見做法是把回波超時(shí)時(shí)間設(shè)為 30ms超過就按「測距失敗」處理避免倒車時(shí) UI 卡死?;氐絙ackoff.cpp的功能邏輯它會(huì)每隔 50ms 調(diào)用一次sr04_get_distance()把距離值取整數(shù)后轉(zhuǎn)發(fā)到主界面顯示并和用戶設(shè)置的安全閾值比對決定是否彈出倒車警告畫面。4. Qt 應(yīng)用層功能落地音樂播放、行車記錄儀與多語言切換4.1 music.cpp用 Qt Multimedia 搭播放核心music.cpp在車載場景里承擔(dān)的是音頻娛樂模塊代碼本身不復(fù)雜但有兩處需要調(diào)通一是音頻輸出設(shè)備的生命周期管理二是播放列表的切換策略。Qt 5.15 的QMediaPlayer配合QMediaPlaylist是最常見的組合播放本地 U 盤里的歌曲或廣播流都能覆蓋。// music.cpp —— 音樂播放模塊初始化 PlaylistWidget::PlaylistWidget(QWidget *parent) : QWidget(parent) { m_playlist new QMediaPlaylist(this); m_player new QMediaPlayer(this); /* 設(shè)置播放模式循環(huán)播放當(dāng)前列表 */ m_playlist-setPlaybackMode(QMediaPlaylist::Loop); m_player-setPlaylist(m_playlist); m_player-setVolume(70); /* 把列表與 UI 選擇綁定 */ connect(m_playlist, QMediaPlaylist::currentIndexChanged, this, [this](int idx) { QUrl url m_playlist-currentMedia(); ui-titleLabel-setText(url.fileName()); }); }注意setPlaybackMode的參數(shù)決定歌單循環(huán)還是單曲循環(huán)車載系統(tǒng)一般用Loop或Sequential前者適合后臺(tái)連續(xù)播放不中斷。這里我用到的是把播放列表和 UI 標(biāo)簽綁定的寫法當(dāng)currentIndexChanged信號(hào)觸發(fā)時(shí)更新界面標(biāo)題信號(hào)槽的idx參數(shù)可以用于同步歌詞或封面。整個(gè)模塊真正容易出問題的不是播放本身而是文件獲取方式——如果歌曲放在 U 盤和 SD 卡需要監(jiān)聽 Linux 的mdev或udev熱插拔事件在設(shè)備節(jié)點(diǎn)出現(xiàn)后再去掛載目錄掃描 mp3否則插拔后就找不到播放列表。另一個(gè)常被忽略的點(diǎn)是音頻焦點(diǎn)沖突當(dāng)?shù)管嚴(yán)走_(dá)發(fā)出蜂鳴提醒時(shí)音樂音量如果保持原樣駕駛員會(huì)聽不清距離提示。常見做法是在backoff.cpp中調(diào)用m_player-setVolume(20)倒車結(jié)束后恢復(fù)原音量。這種控制需要music.cpp暴露一個(gè)公共接口給主窗口調(diào)用我在主界面是用單個(gè)實(shí)例管理音樂模塊通過MusicPlayer::instance()獲取單例后調(diào)音量。4.2 monitor.cpp行車記錄儀到底在記錄什么monitor.cpp的輸入來自v4l2.c采集的連續(xù)幀但行車記錄儀不是把所有幀直接排隊(duì)寫入 SD 卡而是按時(shí)間片段切文件并做循環(huán)覆蓋。這套源碼里 monitor 模塊干的核心事情是取 V4L2 的幀、打時(shí)間戳、寫入文件、超過預(yù)設(shè)容量刪最老的片段。// monitor.cpp —— 錄像文件輪轉(zhuǎn)策略 void MonitorWorker::saveFrame(const QImage frame) { qint64 now QDateTime::currentSecsSinceEpoch(); if (now - m_currentSegmentStart m_segmentSeconds) { m_segmentWriter-close(); /* 生成帶時(shí)間戳的新文件名便于回放索引 */ QString name QString(/rec/%1_%2.h264) .arg(now) .arg(m_segmentSeconds); m_segmentWriter-open(name); m_currentSegmentStart now; /* 刪除最老文件保持總?cè)萘吭?GB以內(nèi) */ while (m_totalBytes m_limitBytes) { QString oldest m_files.takeFirst(); QFile::remove(oldest); } } m_segmentWriter-write(frame); }從這個(gè)實(shí)現(xiàn)能看到監(jiān)控錄像的存儲(chǔ)策略本質(zhì)是「滑動(dòng)窗口」m_segmentSeconds決定每個(gè)文件有多長常見設(shè)置為 60 秒或 120 秒。文件越小回放查找時(shí)間越短但文件系統(tǒng) inode 開銷增加文件越大單文件損壞影響面越大。如果是普通 720p 攝像頭每秒約 2MB 數(shù)據(jù)120 秒一個(gè)文件約 240MB16GB 存儲(chǔ)大約能存 66 個(gè)文件這個(gè)數(shù)量級在QFile::remove循環(huán)里是秒級完成的。注意這里我給出的偽代碼用m_segmentWriter-write(frame)直接寫未壓縮幀實(shí)際項(xiàng)目中會(huì)接一個(gè)硬件編碼器或libx264否則裸 YUV 數(shù)據(jù)寫入速度和存儲(chǔ)空間都撐不住長時(shí)間記錄。這套源碼的 monitor 模塊更像一個(gè)框架占位真正上產(chǎn)品時(shí)要在寫入前加 H.264 編碼你需要關(guān)注的是「時(shí)間戳命名」和「覆蓋策略」這兩個(gè)設(shè)計(jì)點(diǎn)。4.3 language.cppQt 國際化的三件套language.cpp處理的是產(chǎn)品級需求——多語言切換。Qt 國際化三件套是tr()、翻譯文件.ts、以及運(yùn)行時(shí)QTranslator。翻譯文件在開發(fā)期用lupdate掃描源碼提取詞條生成.ts用 Qt Linguist 翻譯后通過lrelease編譯成二進(jìn)制.qm文件。這套源碼里language.cpp做的事情就是把.qm加載進(jìn)運(yùn)行時(shí)并讓界面所有可見文本立即刷新。// language.cpp —— 運(yùn)行時(shí)切換語言 void LanguageWidget::changeLanguage(const QString langCode) { /* 先移除舊翻譯器再加載新翻譯器 */ qApp-removeTranslator(m_translator); if (m_translator.load(QString(:/i18n/carsystem_%1.qm).arg(langCode))) { qApp-installTranslator(m_translator); /* 關(guān)鍵點(diǎn)重發(fā)語言變更事件讓已創(chuàng)建控件刷新文本 */ QEvent ev(QEvent::LanguageChange); QApplication::sendEvent(this, ev); } else { qWarning(load translator failed: %s, qPrintable(langCode)); } }這段代碼里qApp-installTranslator之后Qt 會(huì)向所有頂層窗口發(fā)送LanguageChange事件。每個(gè)窗口的changeEvent(QEvent::LanguageChange)里要重新執(zhí)行一遍ui-label-setText(tr(xxx))界面才能立即更新。如果不重設(shè)文本只是 installTranslator已創(chuàng)建的 QLabel 文本不會(huì)自動(dòng)變化。langCode建議用系統(tǒng)語言標(biāo)準(zhǔn)比如中文用zh_CN英文用en_US這樣后續(xù)擴(kuò)展語言包和系統(tǒng) locale 對齊不用自造編碼。多語言模塊和車載功能的關(guān)系是橫向的倒車?yán)走_(dá)、行車記錄儀、天氣界面都要通過tr()包住字符串否則切換只對當(dāng)前頁面生效其他模塊文本還是舊語言。5. 聯(lián)調(diào)收尾倒車邏輯、按鍵映射與運(yùn)行時(shí)驗(yàn)證方法5.1 backoff.cpp 與 sr04 聯(lián)動(dòng)的邊界條件倒車功能是這套源碼里驅(qū)動(dòng)與 Qt 交互最直接的部分。backoff.cpp大致邏輯是定時(shí)器每 50ms 讀取sr04_get_distance()返回值當(dāng)距離小于m_dangerThreshold常見設(shè) 80cm主界面通過信號(hào)觸發(fā)切換到倒車影像視圖并在畫面上用紅色字體繪制距離值。實(shí)際聯(lián)調(diào)時(shí)我建議給這個(gè) threshold 做兩層判斷小于 80cm 只彈出警告色小于 30cm 才觸發(fā)高頻蜂鳴否則低速倒車時(shí)雷達(dá)會(huì)一直處于「過度警告」?fàn)顟B(tài)。超聲波傳感器還有一個(gè)盲區(qū)問題探測角度約 15 度車輛兩側(cè)低矮障礙物測不到所以要和倒車影像的畫面做互補(bǔ)顯示界面左側(cè)顯示影像右側(cè)顯示雷達(dá)圖。5.2 key_drv.c 映射到 Qt 事件key_drv.c讀取實(shí)體按鍵之后不能直接操作 Qt 對象因?yàn)轵?qū)動(dòng)層是一個(gè)獨(dú)立線程。常見做法是用 Linux input 子系統(tǒng)上報(bào)按鍵事件Qt 面板程序里通過讀取/dev/input/eventX或者用QSocketNotifier監(jiān)聽設(shè)備文件描述符把按鍵事件轉(zhuǎn)換成 Qt 的鍵盤事件。最省事的驗(yàn)證命令是evtest /dev/input/eventX能直接看到按鍵碼是否上報(bào)成功。如果按下去沒反應(yīng)優(yōu)先查key_drv.c里 GPIO 配置的 pin 是否和原理圖一致而不是先查 Qt 代碼。5.3 啟動(dòng)階段二十分鐘排查法把程序部署到板子后我通常按這個(gè)順序排查先跑v4l2-ctl --list-formats-ext -d /dev/video0確認(rèn)攝像頭支持的像素格式和分辨率如果返回空則攝像頭上電時(shí)序有問題再單獨(dú)調(diào)用sr04_get_distance的測試程序在終端里看距離輸出是否隨障礙物移動(dòng)而變化最后再啟動(dòng) Qt 程序。如果 Qt 畫面黑屏查看/var/log/syslog里有沒有qt.qpa相關(guān)報(bào)錯(cuò)QT_QPA_PLATFORMlinuxfb模式下還需要確認(rèn)FRAMEBUFFER和 LCD 驅(qū)動(dòng)匹配。這套源碼和一般純 Qt 工程不同它的調(diào)試順序必須從驅(qū)動(dòng)往上層走因?yàn)榻缑鎸拥漠惓:芏鄷r(shí)候是底層采集線程沒有往共享緩沖里寫入新的幀數(shù)據(jù)而不是界面代碼本身的 bug。本文還有配套的精品資源點(diǎn)擊獲取