BSP移植實(shí)戰(zhàn):從時鐘到emWin顯示鏈路全解析)
簡介新唐 N9H30 系列 ARM9 微控制器在無操作系統(tǒng)環(huán)境下的 emWin 圖形庫 BSP 開發(fā)包適合嵌入式工程師進(jìn)行 LCD 界面設(shè)計、控件交互與中文字體顯示的快速移植。包內(nèi)文件總數(shù) 1030 個以 392 個 h 頭文件和 330 個 c 源碼文件為主體便于閱讀和二次開發(fā)同時附帶 html、pdf 文檔、uvproj/project 工程文件以及少量庫文件滿足編譯、調(diào)試與文檔查閱需求。壓縮包整體約 29.44MB目錄結(jié)構(gòu)典型可直接導(dǎo)入 Keil 等工具鏈?zhǔn)褂?。GUIDEMO 系列示例涵蓋縮放旋轉(zhuǎn)、位圖、洗衣機(jī)界面、圖片流等圖形演示cc936、cc949、cc950 編碼文件支持中文等多語言顯示可幫助理解 emWin 在 ARM9 非 OS 環(huán)境下的資源分配與底層驅(qū)動對接。目前已有 365 人瀏覽學(xué)習(xí)適合正在評估或移植 N9H30 emWin 方案的開發(fā)者參考。 前陣子幫客戶調(diào)一塊 N9H30 的工控顯示板對方第一次用新唐這顆 ARM9拿到官方 N9H30_emWin_NonOS 這個 BSP 包時特別樂觀覺得驅(qū)動都現(xiàn)成了編譯燒錄就能出畫面。結(jié)果整整三天屏幕黑得跟沒通電一樣。說實(shí)話這事我見過太多次了問題不在 emWin 本身而在整個 NonOS BSP 的底層鏈路——從時鐘、SDRAM 初始化到 LCD 控制器寄存器任何一處和你實(shí)際板子對不上前面全都白搭。這篇內(nèi)容就是把這條鏈路拆開講N9H30 在非操作系統(tǒng)裸機(jī)環(huán)境下官方 BSP 到底幫你做了哪些事、你拿到之后要改什么、怎么把 emWin 的圖形層真正調(diào)到能交付的狀態(tài)。適合正在評估 N9H30 做 HMI/工控面板、或者被官方 Demo 折騰得頭疼的工程師也適合學(xué)校里頭一次接觸 BSP 移植、想弄清廠商代碼為什么不能直接燒的學(xué)生朋友。1. 先搞清楚這個BSP到底幫你做了什么1.1 N9H30 這顆芯片適合接什么樣的活N9H30 是新唐的 ARM9 系列 SoC主頻 300MHz內(nèi)部集成 SDRAM 控制器帶 TFT LCD 控制器和 2D 圖形加速器。這顆芯片最典型的去處是大家電面板、電梯樓層顯示器、工控人機(jī)界面、小型醫(yī)療設(shè)備。這些場景有一個共同點(diǎn)界面不算復(fù)雜但要求上電快、成本敏感不想為了一個按鈕界面就把 Linux 那套東西全拖進(jìn)來。很多人會問都用 ARM9 了為什么不直接跑 Linux答案很實(shí)在。如果你的產(chǎn)品只是幾個頁面、幾組數(shù)字、幾個按鈕Linux 的啟動時間、文件系統(tǒng)維護(hù)、驅(qū)動適配成本都是負(fù)擔(dān)。裸機(jī)環(huán)境下 emWin 直接操作 LCD 控制器畫面從上電到顯示出來可以做到一秒內(nèi)完成這對很多設(shè)備來說是實(shí)打?qū)嵉馁u點(diǎn)。N9H30 的 NonOS BSP 存在的意義就是讓你用最小的軟件復(fù)雜度把 GUI 跑起來。1.2 官方包拆開之后哪些東西是參考哪些是成品N9H30_emWin_NonOS 這個包本質(zhì)上是一套能在官方評估板上跑通的最小系統(tǒng)。里面一般包含這幾塊啟動與板級初始化代碼復(fù)位向量、PLL 時鐘配置、SDRAM 初始化LCD 控制器驅(qū)動負(fù)責(zé)把顯存內(nèi)容按時序刷到屏幕上觸摸輸入驅(qū)動通過 ADC 采樣電阻屏坐標(biāo)串口、GPIO 等基礎(chǔ)外設(shè)驅(qū)動用來調(diào)試和擴(kuò)展emWin 預(yù)編譯庫.lib沒有源碼以及 GUIConf、LCDConf 這套配置層官方 BSP 最大的價值是給你一份可運(yùn)行的參考答案而不是拿來就能上量產(chǎn)的成品。官方評估板的晶振頻率、屏幕參數(shù)、SDRAM 顆粒型號跟你自己的板子大概率不一樣這部分適配工作躲不掉。我見過不少工程師拿到 BSP 后第一件事就是點(diǎn)編譯完全不看 release note結(jié)果串口亂碼、屏幕黑屏、程序跑飛最后回頭一條條核對配置浪費(fèi)的時間反而更多。2. 編譯燒錄前必須核對的三組硬件參數(shù)2.1 時鐘鏈路從晶振到 300MHz錯一步后面全亂N9H30 的時鐘一般從外部晶振開始經(jīng)過 PLL 倍頻到 300MHz再分頻給 HCLK、PCLK 和 LCD 控制器。官方 BSP 默認(rèn)按某一顆晶振來配置通常工程里會留一個宏讓你改外部晶振頻率。如果你的板子上晶振和官方評估板不一致又不改配置最典型的癥狀是串口波特率完全對不上、LCD 像素時鐘偏得離譜。調(diào)試時鐘問題有個笨辦法但很有效先用串口打印把系統(tǒng)主頻跑出來確認(rèn)串口通信正常再往下調(diào) LCD。串口都亂碼說明最底層的時鐘就沒對后面所有東西都不可信。另外要注意改 PLL 參數(shù)后最好實(shí)際量一下 MCLK 或 LCD 像素時鐘引腳別只靠代碼里注釋的值。2.2 SDRAM 參數(shù)行地址、列地址、bank 數(shù)、刷新率N9H30 集成 SDRAM 控制器但具體接的是多大容量、什么顆粒由你的板子決定。BSP 里對 SDRAM 的配置通常包括行地址位數(shù)、列地址位數(shù)、bank 數(shù)、CAS 延遲和刷新周期。這些參數(shù)里任何一個和實(shí)際顆粒不匹配輕則訪問越界數(shù)據(jù)錯亂重則上電直接 HardFault。我之前遇到過一塊板子現(xiàn)象是程序燒進(jìn)去之后能跑但跑上一兩分鐘就隨機(jī)死機(jī)。排查到最后發(fā)現(xiàn)是刷新率參數(shù)有問題SDRAM 在高溫環(huán)境下數(shù)據(jù)保持不住偶發(fā)位翻轉(zhuǎn)。這種問題最惡心的地方在于它不是必現(xiàn)的而且看起來像代碼 bug。所以拿到 BSP 后一定先從芯片數(shù)據(jù)手冊和內(nèi)存顆粒手冊里把行列地址確認(rèn)清楚不要想當(dāng)然用默認(rèn)值。2.3 LCD 時序參數(shù)像素時鐘與前后肩LCD 屏的時序配置是黑屏/花屏的重災(zāi)區(qū)。BSP 里會有一組參數(shù)描述屏幕的分辨率、像素時鐘、行場同步極性、前后肩等。以常見的 4.3 寸 480x272 RGB 屏為例像素時鐘一般在 9MHz 附近HBP/HFP/VBP/VFP 各有幾十個像素或幾行具體以屏廠規(guī)格書為準(zhǔn)。如果你用的屏幕型號和官方不同這組參數(shù)必須重填。參數(shù)含義不匹配的典型癥狀像素時鐘每秒傳輸?shù)南袼財?shù)畫面閃爍、顏色發(fā)虛、有斜紋HBP/HFP行同步前后的消隱時間畫面左右偏移、邊緣有黑邊VBP/VFP幀同步前后的消隱時間畫面上下偏移、滾動同步極性HSYNC/VSYNC 有效電平畫面抖動、完全無法同步核對 LCD 時序沒有捷徑老老實(shí)實(shí)翻屏廠給的規(guī)格書。BSP 里的默認(rèn)值只對官方那顆屏有效換了屏不改時序后面調(diào) emWin 全是白費(fèi)功夫因為問題根本不在圖形層。3. 從復(fù)位到 main 再到 emWinNonOS 的啟動與內(nèi)存布局3.1 復(fù)位向量到 main為什么 SDRAM 要先于 C 語言初始化裸機(jī) BSP 的啟動流程和 Linux 完全是兩回事。N9H30 上電后固化在芯片內(nèi)部的 bootloader 會把 SPI NOR Flash 里的代碼搬運(yùn)到 SDRAM 執(zhí)行而這套搬運(yùn)邏輯跑在匯編階段C 環(huán)境還沒建立起來。所以啟動代碼里有一個嚴(yán)格的順序關(guān)閉看門狗、初始化 SDRAM 控制器、配置 PLL 升頻、設(shè)置堆棧指針、拷貝 RW 段、清 ZI 段最后才跳進(jìn) main。這個順序不能亂。很多新手在 main 里才去初始化 SDRAM但代碼本身已經(jīng)在 SDRAM 里跑了一初始化控制器就等于把自己腳下的地板抽掉。如果調(diào)試器單步跟到某一行突然 PC 飛了先檢查是不是在代碼還位于 SDRAM 時動了 SDRAM 配置。3.2 emWin 內(nèi)存池放哪里、放多大、對齊怎么做emWin 在裸機(jī)下需要一塊連續(xù)的內(nèi)存池所有窗口、控件、字體位圖都從里面分配。這個池子的大小區(qū)間由 GUIConf.h 里的 GUI_NUMBYTES 控制一般先給 1MB跑起來之后用 emWin 的 GUI_ALLOC_GetNumFreeBytes 這類接口觀察實(shí)際使用量再調(diào)整。內(nèi)存池在 SDRAM 里的位置很關(guān)鍵。不要直接把一個大數(shù)組定義在 main 里那樣它落在 ZI 段編譯器可能把它安排在棧附近棧一大就踩。更好的做法是在分散加載文件或鏈接腳本里單獨(dú)劃出一塊區(qū)域比如RW_EMWIN 0x800000 0x00100000 { ; 1MB . ANY (RW_ZI) }然后在代碼里聲明__attribute__((section(RW_EMWIN), aligned(32))) uint8_t GUI_Heap[1024 * 1024];對齊到 32 字節(jié)對 emWin 的性能和穩(wěn)定性都有好處。裸機(jī)沒有 MMU內(nèi)存池必須是物理連續(xù)地址這也是它只能放在 SDRAM 固定區(qū)域而不能隨便動態(tài)分配的原因。4. emWin 圖形驅(qū)動接入LCDConf、GUIConf 與底層渲染回調(diào)4.1 GUIConf 先定運(yùn)行模式GUIConf.c 里通常有這幾組關(guān)鍵的宏#define GUI_OS (0) // 0: 非操作系統(tǒng)模式 #define GUI_NUMBYTES (1024 * 1024) #define GUI_SUPPORT_TOUCH (1) #define GUI_DEFAULT_FONT GUI_Font6x8GUI_OS 設(shè)為 0相當(dāng)于告訴 emWin 內(nèi)核不要調(diào)用任何 RTOS 相關(guān)函數(shù)一切都在單線程裸機(jī)環(huán)境下跑。這個時候 emWin 內(nèi)部對延時和獲取時間的處理會走 GUI_X_Delay 和 GUI_X_GetTime這兩個函數(shù)在 NonOS BSP 里一般由板上定時器或 SysTick 驅(qū)動移植時需要注意別讓 GUI_X_Delay 空轉(zhuǎn)死等否則界面刷新會卡死。4.2 LCDConf.c 的兩個核心函數(shù)LCDConf.c 是 emWin 和硬件驅(qū)動之間的適配層。重點(diǎn)看兩個函數(shù)void LCD_X_Config(void) { GUI_DEVICE_CreateAndLink(GUIDRV_Template_API, GUICC_M565, 0, 0); LCD_SetSizeEx(0, 480, 272); LCD_SetVSizeEx(0, 480, 272); }LCD_X_Config 里決定了 emWin 用什么樣的顏色格式訪問顯存。GUICC_M565 是 16 位 RGB565如果屏幕接口是 18 位或 24 位就要對應(yīng)換成 GUICC_M888 或 GUICC_M566。數(shù)據(jù)手冊上寫著 RGB888但軟件卻在用 RGB565畫面顏色就一定是錯的。另一個核心函數(shù)是 LCD_X_DisplayDriver它處理的是底層硬件相關(guān)的回調(diào)int LCD_X_DisplayDriver(unsigned LayerIndex, unsigned Cmd, void *pData) { switch (Cmd) { case LCD_X_INITCONTROLLER: // 初始化 N9H30 LCD 控制器寄存器 break; case LCD_X_SETORG: // 切換顯存起始地址用于滾動或多緩沖 break; case LCD_X_SHOWBUFFER: // 幀同步時切換顯示緩沖 break; default: break; } return 0; }這個回調(diào)是騾子是馬拉出來遛遛的地方。BSP 提供的底層驅(qū)動最終就是把這些 Cmd 翻譯成 LCD 控制器的寄存器操作emWin 通過它感知顯存地址的變化。4.3 底層刷新CPU 不用逐像素搬運(yùn)但地址要對N9H30 的 LCD 控制器有專門的 DMA 邏輯會周期性地從顯存讀取數(shù)據(jù)送到屏幕接口這個過程不占用 CPU。emWin 只需要往正確的顯存地址寫數(shù)據(jù)屏幕自然就刷新了。所以底層驅(qū)動最核心的兩個點(diǎn)就是顯存起始地址要落在 LCD 控制器能訪問的 SDRAM 區(qū)間顏色格式和屏接口位寬要匹配。如果屏幕能顯示但刷新率上不去先別急著懷疑 CPU 慢。有時是顯存總線寬度沒配到位SDRAM 帶寬不夠LCD 控制器讀數(shù)據(jù)跟不上像素時鐘畫面就會出現(xiàn)橫向撕裂。N9H30 的 2D 加速器可以加速位塊拷貝但第一次跑通時先不用開等軟件渲染正常了再考慮優(yōu)化。4.4 觸摸部分電阻屏的坐標(biāo)換算別省N9H30 的觸摸方案在多數(shù)低成本板子上是四線電阻屏通過 ADC 采樣電壓值換算坐標(biāo)。emWin 里觸摸輸入的接入點(diǎn)在 GUI_PID_STATE 結(jié)構(gòu)體通常由 BSP 的底層驅(qū)動填充坐標(biāo)。裸機(jī)下最簡單的方式是在主循環(huán)里周期性采樣 ADC、濾波、做線性換算然后把結(jié)果告訴 GUI_TOUCH_StoreState。校準(zhǔn)算法建議用兩點(diǎn)或三點(diǎn)校準(zhǔn)不要用固定的等比縮放。屏體電阻、ADC 基準(zhǔn)電壓、安裝應(yīng)力都會讓實(shí)際坐標(biāo)和理論坐標(biāo)有偏差只靠分辨率直接換算基本沒法用。校準(zhǔn)參數(shù)算出來后存到外部 SPI Flash 或片上 Flash下次開機(jī)直接加載別每次讓用戶重新點(diǎn)十字。5. 實(shí)測中繞不開的幾個典型坑5.1 顏色完全不對先懷疑 RGB 線序再懷疑 GUICC 宏偏色問題分兩種一種是顏色整體發(fā)藍(lán)或發(fā)紅一種是畫面像底片一樣反色。前者多半是 RGB 線序接錯或顏色格式位寬不匹配后者一般是對比度/極性設(shè)置反了。排查時先脫離 emWin。用 BSP 里最底層的方法往顯存里填純紅、純綠、純藍(lán)直接看屏上顯示是否正確。如果這一步都不對那是硬件接線或 LCD 控制器寄存器的問題和 emWin 半毛錢關(guān)系沒有。純色測試通過之后再接 emWin如果這時候顏色不對再去檢查 GUIDRV_Template_API 后面掛的 GUICC 宏。我見過有人在這上面折騰兩天最后只是把 GUICC_M565 改成了 GUICC_M888。5.2 畫面撕裂和閃爍單緩沖的鍋雙緩沖來解決裸機(jī) BSP 為了省內(nèi)存默認(rèn)往往只開一個顯示緩沖。LCD 控制器正在從顯存讀數(shù)據(jù)的時候CPU 又往同一塊顯存寫新內(nèi)容畫面中間就會出現(xiàn)一條明顯的撕裂帶。頁面切換越頻繁撕裂越明顯。標(biāo)準(zhǔn)解法是雙緩沖。emWin 內(nèi)部可以通過 GUI_MULTIBUF_Config 開啟但底層還需要 LCD_X_DisplayDriver 里正確處理 LCD_X_SETORG 和 LCD_X_SHOWBUFFER讓控制器在垂直消隱期間切換顯存地址。這樣 CPU 畫新幀的同時屏幕顯示的是上一幀畫完了在 VSYNC 時刻切換畫面就是完整的。雙緩沖意味著顯存占用翻倍配合 GUI_NUMBYTES 一起規(guī)劃內(nèi)存池時要把這個因素算進(jìn)去。SDRAM 不夠的項目至少也做一個臟矩形機(jī)制只更新變化區(qū)域減少撕裂發(fā)生的概率。5.3 觸摸漂移到?jīng)]法點(diǎn)準(zhǔn)濾波和校準(zhǔn)參數(shù)持久化電阻屏本身就是個模擬器件ADC 采樣值受電源噪聲、溫度、屏體老化影響很大。裸機(jī) BSP 里如果直接拿一次采樣結(jié)果去映射坐標(biāo)手指按上去光標(biāo)亂跳是很正常的。我的做法是采樣 8 次去掉最大值和最小值剩下 6 次取平均再做坐標(biāo)換算。這個濾波在 300MHz 主頻下開銷可以忽略但手感提升非常明顯。校準(zhǔn)參數(shù)不能只存在 RAM 里否則每次斷電就得重新校準(zhǔn)。批量出貨的設(shè)備建議出廠前在校準(zhǔn)模式下采一組數(shù)據(jù)存 Flash運(yùn)行階段直接加載只在溫度劇烈變化或觸摸明顯偏移時再提示用戶重新校準(zhǔn)。5.4 開優(yōu)化后程序瘋狂 HardFault先查棧和未初始化變量Keil 的 -O0 跑得好好的換成 -O2 就時不時進(jìn) HardFault這個問題在 NonOS BSP 里很常見。核心原因通常是兩類棧溢出或者某個變量依賴了編譯器在低優(yōu)化下保留的某種行為。排查時先把硬錯誤中斷里的 PC/LR 打印出來看它崩在什么位置。如果崩在 GUI 相關(guān)函數(shù)里先試試把啟動文件里的棧加大到 0x4000 甚至 0x8000如果崩在中斷服務(wù)函數(shù)里看看是否有共享變量沒加 volatile。還有一個經(jīng)驗把 GUI_NUMBYTES 暫時調(diào)小到 512KB 再跑如果崩潰消失說明內(nèi)存池和棧在布局上可能產(chǎn)生了重疊。6. 從 Demo 跑通到產(chǎn)品落地字庫、架構(gòu)與性能優(yōu)化6.1 中文顯示裸機(jī)沒有文件系統(tǒng)字庫要自己做emWin 自帶 FontCvt 工具可以把 TTF 轉(zhuǎn)成 C 數(shù)組或二進(jìn)制字庫。但 NonOS 環(huán)境沒有文件系統(tǒng)全字庫比如 GB2312 的 6763 個字做成 C 數(shù)組會非常占 Flash一般做法是放到外部 SPI NOR Flash通過自定義讀取接口按需加載。頁面上的中文字符串建議提前用工具把需要的字符提取出來做成段字庫或者動態(tài)字庫索引。產(chǎn)品只需要幾十個漢字時沒必要把整個國標(biāo)字庫塞進(jìn)去。如果發(fā)現(xiàn)中文控件打開速度明顯慢多半是片外 Flash 讀取太頻繁可以用一次讀取一行字符、加上簡單的軟件 Cache 來優(yōu)化。6.2 非OS下別把代碼寫成一大坨輪詢裸機(jī)項目最容易失控的地方就是 main 里堆滿 while 加 delay。我的習(xí)慣是主循環(huán)只做四件事輪詢輸入事件、調(diào)用 GUI_Exec、驅(qū)動應(yīng)用狀態(tài)機(jī)、執(zhí)行 GUI_Delay。頁面之間的切換用狀態(tài)機(jī)不用每個頁面獨(dú)立死循環(huán)。while (1) { Poll_Touch(); Poll_Uart(); GUI_Exec(); App_StateMachine(); GUI_Delay(10); }GUI_Delay(10) 會讓 emWin 有節(jié)奏地刷新界面同時把 CPU 時間讓出來。如果某個外設(shè)處理耗時太長用狀態(tài)機(jī)分片別在一次循環(huán)里做完所有事。否則觸摸會斷續(xù)、動畫會掉幀用戶感知到的就是卡。6.3 后面要上 RTOS 的話現(xiàn)在就要留后路很多項目先在 NonOS 上驗證之后加協(xié)議?;蚨嗳蝿?wù)再切 FreeRTOS 或 RT-Thread。emWin 本身的移植很簡單GUI_OS 改成 1補(bǔ)上 GUI_X_GetTime、GUI_X_Delay 以及鎖相關(guān)函數(shù)即可。但 BSP 層的一些設(shè)計如果不提前注意切 OS 時會很痛苦。最典型的是 LCD 驅(qū)動的可重入問題。NonOS 階段沒有任何并發(fā)概念驅(qū)動函數(shù)可以隨便調(diào)用上了 OS 之后如果 GUI 任務(wù)和別的任務(wù)同時訪問顯存或 LCD 控制器寄存器必須加互斥。中斷里如果有和 GUI 共享的數(shù)據(jù)也要用臨界區(qū)保護(hù)。所以裸機(jī)階段寫驅(qū)動時盡量把寄存器配置和業(yè)務(wù)邏輯分開這樣切 OS 時只需要在中間加鎖層。6.4 刷新性能不夠時按這個順序調(diào)如果頁面復(fù)雜后 CPU 渲染跟不上我一般按下面的順序排查和優(yōu)化顏色深度降到 RGB565比 RGB888 減少三分之一的數(shù)據(jù)傳輸量開啟 N9H30 的 2D 加速塊拷貝、填充這類操作交給硬件減少不必要的全屏重繪利用 emWin 的 invalidate 機(jī)制只更新臟區(qū)域控件層數(shù)不要嵌套太深每層半透明混合都是一筆不小的開銷靜態(tài)界面盡量用位圖資源不要在運(yùn)行時反復(fù)畫線條和填充最后說一個我自己的經(jīng)驗?zāi)玫?N9H30 這類帶 LCD 控制器的新板子先別急著掛 emWin用 BSP 的底層驅(qū)動寫一個純色、漸變色的彩條測試程序確認(rèn) RGB 線序、像素時鐘、時序參數(shù)全部正確之后再去接 GUI 層。這個步驟看著多花半小時實(shí)際上能替你省掉后面好幾天針對黑屏、花屏、偏色的排錯時間。BSP 里的代碼再全終究是要落到你的板子上才算數(shù)。本文還有配套的精品資源點(diǎn)擊獲取