移植LVGL 8.2驅(qū)動(dòng)7寸屏完整指南)
先說(shuō)結(jié)論STM32F103裸機(jī)跑LVGL 8.2、帶一塊7寸800x480的屏幕這事能成而且沒(méi)有想象中那么恐怖。網(wǎng)上搜F103LVGL十個(gè)帖子有八個(gè)在勸退理由不外乎內(nèi)存不夠、沒(méi)有FPU、刷新跟不上。這些理由在F407上成立放到F103上要分情況看——LVGL從v7到v8一直在為低資源設(shè)備做優(yōu)化分塊渲染加臟矩形刷新機(jī)制讓全幀緩沖不再是必需品。下面這套流程是我這次實(shí)際移植時(shí)走通的完整路線包括源碼裁剪、lv_conf.h配置、顯示和觸摸回調(diào)對(duì)接還有移植過(guò)程中踩過(guò)的幾個(gè)坑。適合手上正好有F103和一塊7寸屏、想徹底換掉老式控件UI的人參考也適合被舊教程的v7/v8 API差異搞到頭疼的初學(xué)者。1. 移植前的資源賬72MHz主頻和64KB SRAM夠不夠1.1 內(nèi)存賬為什么全幀緩沖直接出局但LVGL依然能跑7寸屏分辨率通常是800x480RGB565一個(gè)像素占2字節(jié)一幀全畫面需要768000字節(jié)約750KB。STM32F103ZET6的SRAM只有64KBC8T6只有20KB。凡是上來(lái)就在代碼里定義一個(gè)uint16_t framebuffer[800 * 480]的編譯能過(guò)運(yùn)行必掛。這是很多人在F103上移植失敗的第一道墻不是LVGL不行是方案選錯(cuò)了。LVGL解決這個(gè)問(wèn)題的思路是分塊渲染。它內(nèi)部維護(hù)一個(gè)draw buffer大小遠(yuǎn)小于全幀緩沖需要刷新時(shí)LVGL把整個(gè)需要重繪的區(qū)域切成一堆小塊一塊一塊地完成像素計(jì)算再通過(guò)flush_cb把這塊像素交給LCD。對(duì)一塊800寬的屏來(lái)說(shuō)如果給draw buffer分配16KB大約能存800x10行像素也就是一次只畫10行。渲染完這10行就馬上丟給屏幕控制器接著畫下一批10行。屏幕控制器內(nèi)部自帶GRAM的型號(hào)比如RA8875、SSD1963這類驅(qū)動(dòng)IC帶數(shù)百KB顯存MCU其實(shí)只負(fù)責(zé)把小塊數(shù)據(jù)傳過(guò)去真正的一幀畫面存在屏端。這也解釋了為什么F103這種小內(nèi)存MCU跑大屏UI理論上是可行的。內(nèi)存具體怎么分以ZET6的64KB SRAM為例兩個(gè)draw buffer各16KB加起來(lái)32KB丟給刷新用LVGL對(duì)象內(nèi)存池LV_MEM_SIZE開(kāi)24KB用來(lái)存放控件、樣式、事件等數(shù)據(jù)結(jié)構(gòu)剩下8KB左右留給中斷、局部變量和棧。如果屏幕是SPI接口且沒(méi)有足夠吞吐率可以退化成單draw buffer省16KB。這個(gè)預(yù)算很緊但做幾個(gè)頁(yè)面加若干控件夠用。1.2 刷新賬并口和SPI屏的差距不是一點(diǎn)半點(diǎn)7寸屏模塊的接口五花八門但對(duì)F103來(lái)說(shuō)接口基本決定了UI能做到多順滑。16位并口8080時(shí)序通過(guò)FSMC外設(shè)把LCD映射成一塊外部SRAM寫一個(gè)像素就是一次普通總線寫。72MHz下FSMC總線帶寬遠(yuǎn)大于UI刷新需求只要規(guī)劃好LCD窗口一屏畫面理論寫入時(shí)間在二三十毫秒量級(jí)跑LVGL的過(guò)渡動(dòng)畫可以接受。8位并口寫入一個(gè)像素需要兩次總線寫速度下降一半用是能用畫面略慢。SPI接口SPI1最高18Mbit/s換算約2.25MB/s傳完整屏768KB需要340ms以上。也就是說(shuō)刷新一整個(gè)畫面要接近0.35秒拖動(dòng)畫、滑菜單這類操作基本沒(méi)法看。SPI屏更適合做靜態(tài)界面或者在屏廠驅(qū)動(dòng)IC自帶局部刷新指令時(shí)針對(duì)小塊區(qū)域更新。所以要玩LVGLF1037寸屏我的建議很直接手上是并口屏就用起來(lái)SPI屏盡量換塊屏再折騰。網(wǎng)傳“F103帶不動(dòng)7寸屏”的說(shuō)法很多是用SPI慢屏試出來(lái)的錯(cuò)覺(jué)換成FSMC并口后體驗(yàn)完全不同。1.3 芯片型號(hào)ZET6、RCT6和C8T6的差別移植到一半才會(huì)暴露資源賬算完具體芯片選型也有講究。F103系列SRAM容量差異很大STM32F103ZET664KB SRAM、512KB Flash做這個(gè)方案最合適。STM32F103RCT648KB SRAM、256KB Flash湊合能用draw buffer壓到每塊12KBLV_MEM_SIZE給16KB也能轉(zhuǎn)起來(lái)。STM32F103C8T620KB SRAM、64KB Flash不推薦。draw buffer和LVGL內(nèi)存池根本分不過(guò)來(lái)加上LVGL 8.2編譯出來(lái)基礎(chǔ)體積就有小幾十KBFlash也很緊張。這幾個(gè)型號(hào)的工程代碼基本通用差別主要在啟動(dòng)文件的堆棧大小和linker腳本的內(nèi)存范圍。如果手上只有C8T6我的建議是降低分辨率換4.3寸屏或2.4寸屏UI一樣能玩別硬上7寸。2. LVGL 8.2源碼整理與lv_conf.h配置后面能不能跑起來(lái)全看這里2.1 源碼目錄精簡(jiǎn)三個(gè)該拿掉的文件夾和其他注意事項(xiàng)下載LVGL 8.2源碼后解壓出來(lái)會(huì)看到demos、examples、src幾個(gè)主要目錄。裸機(jī)移植時(shí)編譯對(duì)象只需要srcdemos和examples里的東西統(tǒng)統(tǒng)拿掉它們主要是給你參考和演示用的加進(jìn)來(lái)會(huì)拖慢編譯并且白白消耗Flash。很多CubeMX工程模板默認(rèn)會(huì)把整個(gè)lvgl文件夾全編譯等燒錄后才發(fā)現(xiàn)Flash不夠排查半天最后發(fā)現(xiàn)是demo占了三分之一。src目錄內(nèi)部不建議按文件夾手動(dòng)刪文件因?yàn)長(zhǎng)VGL各模塊之間依賴比較復(fù)雜刪錯(cuò)一個(gè)文件編譯報(bào)錯(cuò)會(huì)讓你懷疑人生。正確做法是保留整個(gè)src然后通過(guò)lv_conf.h里的開(kāi)關(guān)把用不到的組件、主題、字體關(guān)掉。例如不做圖表就不開(kāi)LV_USE_CHART不用下拉框就關(guān)LV_USE_DROPDOWN每關(guān)一個(gè)組件都能省下對(duì)應(yīng)的Flash和內(nèi)存占用。還有一點(diǎn)LVGL 8.2要求工程能編譯那些帶lv_前綴的一堆源文件。用Keil時(shí)建議直接把src下所有.c文件加入工程組文件數(shù)量很多但值得編譯時(shí)間也就幾十秒。官方提供的lvgl.mk是給Makefile工程用的也能自動(dòng)收集文件如果用Makefile不必手動(dòng)羅列。2.2 lv_conf.h里必改的宏顏色深度、內(nèi)存池、緩沖區(qū)掛鉤把lvgl根目錄下的lv_conf_template.h復(fù)制出來(lái)改名為lv_conf.h并確保它能被所有源文件包含。這個(gè)文件是整個(gè)移植最關(guān)鍵的一環(huán)幾個(gè)宏直接決定系統(tǒng)能不能跑#define LV_COLOR_DEPTH 16 #define LV_COLOR_16_SWAP 1 /* 后續(xù)根據(jù)屏幕實(shí)測(cè)再調(diào) */ #define LV_MEM_CUSTOM 0 /* 使用LVGL內(nèi)部?jī)?nèi)存分配器 */ #define LV_MEM_SIZE (24 * 1024) #define LV_TICK_CUSTOM 0 #define LV_DISP_DEF_REFR_PERIOD 30 #define LV_FONT_MONTSERRAT_14 1 #define LV_FONT_MONTSERRAT_20 1LV_COLOR_DEPTH必須跟LCD面板實(shí)際格式一致絕大多數(shù)7寸屏是RGB565也就是16位色。如果板子配的是RGB888的屏修改成24后draw buffer占用會(huì)暴漲50%內(nèi)存預(yù)算要重新算。LV_COLOR_16_SWAP是個(gè)很容易踩的坑。它是用來(lái)調(diào)整像素字節(jié)序的LVGL在內(nèi)存中按小端方式存放16位色但不同LCD控制器對(duì)RGB兩個(gè)字節(jié)的接收順序有差異?,F(xiàn)象就是界面“顏色偏藍(lán)”或“偏紫”把這項(xiàng)在0和1之間切換有時(shí)候就能解決。LV_MEM_SIZE直接決定能創(chuàng)建多少控件。24KB聽(tīng)起來(lái)不多但LVGL內(nèi)部是按對(duì)象拆分的一個(gè)普通按鈕幾十字節(jié)一頁(yè)十幾二十個(gè)控件完全能放下。如果創(chuàng)建控件后界面沒(méi)反應(yīng)、復(fù)位循環(huán)先懷疑這個(gè)值太小。2.3 別急著用malloc裸機(jī)環(huán)境下內(nèi)存分配有更穩(wěn)的選擇LV_MEM_CUSTOM這個(gè)宏是很多人會(huì)忽略的。它設(shè)為1時(shí)LVGL會(huì)調(diào)用malloc/free設(shè)為0時(shí)LVGL使用內(nèi)置的lv_mem_alloc/lv_mem_free內(nèi)存池就是2.2里那個(gè)LV_MEM_SIZE定義的靜態(tài)數(shù)組。裸機(jī)上直接用標(biāo)準(zhǔn)malloc我踩過(guò)坑。Keil的MicroLIB或者默認(rèn)堆分配器在長(zhǎng)時(shí)間運(yùn)行后容易產(chǎn)生碎片而且標(biāo)準(zhǔn)malloc的堆大小由啟動(dòng)文件里的Heap_Size決定跟linker腳本又耦合排查起來(lái)很麻煩。LVGL內(nèi)置分配器是為嵌入式場(chǎng)景設(shè)計(jì)的內(nèi)部有塊合并機(jī)制更適合裸機(jī)。如果確實(shí)想用自己的分配邏輯比如外部PSRAM才把LV_MEM_CUSTOM設(shè)為1并讓LV_MEM_CUSTOM_INCLUDE包含對(duì)應(yīng)的頭文件。F103沒(méi)有外部?jī)?nèi)存控制器老老實(shí)實(shí)用內(nèi)置分配器最省心。主循環(huán)里調(diào)試時(shí)還可以用lv_mem_monitor函數(shù)看內(nèi)存占用判斷控件創(chuàng)建多了還是泄漏了后面避坑部分會(huì)再提。3. flush_cb顯示回調(diào)像素如何從LVGL畫布跑到LCD上3.1 FSMC并口屏把7寸屏映射成一塊外部SRAM16位并口8080屏接到F103ZET6最標(biāo)準(zhǔn)的方式是用FSMC外設(shè)。FSMC本是用來(lái)擴(kuò)展NOR Flash和SRAM的但LCD控制器本質(zhì)上也是“讀寄存器/寫數(shù)據(jù)”的并行設(shè)備于是可以把LCD當(dāng)成一塊外部SRAM掛在Bank1上。我們只要對(duì)某個(gè)內(nèi)存地址寫一個(gè)值FSMC就會(huì)自動(dòng)產(chǎn)生時(shí)序把數(shù)據(jù)送到LCD數(shù)據(jù)總線。連線思路是NE1負(fù)責(zé)片選NOE接RD引腳NWE接WR引腳地址線A16接LCD寄存器選擇RS16根數(shù)據(jù)線D0-D15接LCD的DB0-DB15。FSMC對(duì)應(yīng)的GPIO要設(shè)置為50MHz復(fù)用推挽輸出同時(shí)打開(kāi)FSMC外設(shè)時(shí)鐘GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | ... | GPIO_Pin_15; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOD, GPIO_InitStructure);關(guān)鍵在CMD和DATA的地址區(qū)分。LCD控制器通過(guò)RS引腳高低來(lái)區(qū)分當(dāng)前是寫命令還是寫數(shù)據(jù)。RS接到FSMC的A16在16位數(shù)據(jù)模式下對(duì)應(yīng)地址偏移是0x20000。源碼里可以這樣定義#define LCD_BASE ((uint32_t)0x60000000) #define LCD_REG (*(volatile uint16_t *)(LCD_BASE)) #define LCD_RAM (*(volatile uint16_t *)(LCD_BASE | 0x00020000))先給LCD_REG寫命令再給LCD_RAM寫數(shù)據(jù)。很多例程會(huì)把第二個(gè)地址寫成0x60010000這是把A16偏移算錯(cuò)了結(jié)果就是LCD控制器收到的命令一塌糊涂屏幕初始化失敗白屏。如果RS接的是A18偏移就是0x80000務(wù)必對(duì)著自己的原理圖算偏移別照抄。FSMC時(shí)序初始化時(shí)地址建立時(shí)間給1個(gè)HCLK、數(shù)據(jù)建立時(shí)間給2個(gè)HCLK通常就能穩(wěn)定驅(qū)動(dòng)SSD1963這類控制器FSMC_NORSRAMInitStructure.FSMC_AddressSetupTime 1; FSMC_NORSRAMInitStructure.FSMC_DataSetupTime 2;如果屏幕出現(xiàn)閃爍或偶爾花屏把DataSetupTime加到3往往就好了。這也是個(gè)經(jīng)驗(yàn)值具體參照屏幕控制器datasheet的讀寫周期要求調(diào)整。3.2 flush_cb完整寫法設(shè)置窗口再連續(xù)寫像素LVGL要刷新一塊區(qū)域時(shí)會(huì)調(diào)用注冊(cè)的flush_cb給一個(gè)area區(qū)域左上右下坐標(biāo)和一個(gè)color_p像素指針。我們收到這套數(shù)據(jù)后要先告訴LCD控制器一個(gè)窗口范圍然后連續(xù)寫入像素。static void lcd_fill_area(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, const uint16_t *pixels) { LCD_REG 0x2A; /* 列地址設(shè)置 */ LCD_RAM x1 8; LCD_RAM x1 0xFF; LCD_RAM x2 8; LCD_RAM x2 0xFF; LCD_REG 0x2B; /* 行地址設(shè)置 */ LCD_RAM y1 8; LCD_RAM y1 0xFF; LCD_RAM y2 8; LCD_RAM y2 0xFF; LCD_REG 0x2C; /* 存儲(chǔ)器寫 */ uint32_t len (uint32_t)(x2 - x1 1) * (y2 - y1 1); for (uint32_t i 0; i len; i) { LCD_RAM pixels[i]; } }注意一定要先設(shè)置窗口再連續(xù)寫數(shù)據(jù)別按整個(gè)屏幕的線性地址去推算像素位置。不同LCD控制器具體命令號(hào)有差異RA8875的窗口設(shè)置命令和SSD1963不一樣但思路相同。flush回調(diào)本身不復(fù)雜static void disp_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { lcd_fill_area(area-x1, area-y1, area-x2, area-y2, (uint16_t *)color_p); lv_disp_flush_ready(disp_drv); }FSMC循環(huán)寫是同步操作數(shù)據(jù)寫完后就能通知LVGL緩沖區(qū)可以復(fù)用了。這里有一個(gè)很多教程沒(méi)講透的細(xì)節(jié)在flush_cb里調(diào)lv_disp_flush_ready的位置本質(zhì)上表示“這塊區(qū)域的數(shù)據(jù)我已經(jīng)拿走了”。FSMC寫法簡(jiǎn)單直接寫完立刻調(diào)用沒(méi)問(wèn)題DMA后臺(tái)傳輸則不行必須等傳輸完成中斷里再調(diào)用不然下一幀的像素可能覆蓋當(dāng)前正在傳輸?shù)木彌_區(qū)畫面會(huì)出現(xiàn)撕裂。3.3 SPI屏的DMA方案與“等傳輸完成再發(fā)ready”問(wèn)題如果手里的7寸屏是SPI接口建議先權(quán)衡刷新速度非要移植的話DMA是必須的否則每像素都走CPU等待收發(fā)刷新直接沒(méi)法看。SPI屏的flush思路是先向LCD控制器發(fā)送窗口設(shè)置命令再啟動(dòng)DMA把color_p里的像素?cái)?shù)據(jù)轉(zhuǎn)成SPI輸出。關(guān)鍵點(diǎn)是lv_disp_flush_ready不能在啟動(dòng)DMA后立刻調(diào)用要在DMA傳輸完成中斷里調(diào)用void DMA1_Channel3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC3)) { DMA_ClearITPendingBit(DMA1_IT_TC3); lv_disp_flush_ready(disp_drv); } }這里還有一個(gè)對(duì)齊問(wèn)題。STM32F103的DMA對(duì)源地址有對(duì)齊要求color_p是LVGL從draw buffer里切出來(lái)的地址是隨機(jī)偏移可能不滿足4字節(jié)或2字節(jié)對(duì)齊。實(shí)測(cè)中不放心的話可以在自己定義的flush函數(shù)里先把像素拷貝到一個(gè)靜態(tài)對(duì)齊數(shù)組里再啟動(dòng)DMA。注意數(shù)組要帶對(duì)齊屬性static uint16_t dma_buf[800 * 10] __attribute__((aligned(4)));不過(guò)拷貝也費(fèi)時(shí)間若能確保draw buffer起始地址和每次切塊的偏移都對(duì)齊就能省掉這步。剛上手時(shí)我建議先加拷貝穩(wěn)定性比重那一點(diǎn)速度重要。4. SysTick心跳與XPT2046觸摸讓UI活過(guò)來(lái)的兩個(gè)底層驅(qū)動(dòng)4.1 心跳時(shí)基SysTick被HAL庫(kù)占用后怎么辦LVGL所有動(dòng)畫、定時(shí)器、控件狀態(tài)刷新都依賴一個(gè)毫秒級(jí)時(shí)基裸機(jī)上就是把lv_tick_inc(1)按固定周期喂給它。最簡(jiǎn)單方案是SysTick 1ms中斷但如果你用CubeMX的HAL庫(kù)生成工程SysTick已經(jīng)被HAL庫(kù)用來(lái)做HAL_GetTick的時(shí)基此時(shí)直接覆蓋中斷函數(shù)會(huì)引發(fā)HAL_Delay死循環(huán)。兩個(gè)處理辦法。第一在SysTick_Handler里同時(shí)調(diào)用HAL_IncTick()和lv_tick_inc(1)void SysTick_Handler(void) { HAL_IncTick(); lv_tick_inc(1); }這個(gè)辦法代碼少但SysTick里干了兩件事以后想改時(shí)基會(huì)牽一發(fā)動(dòng)全身。第二用TIM2做1ms定時(shí)中斷單獨(dú)把時(shí)基交給LVGLvoid TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update)) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); lv_tick_inc(1); } }我推薦第二種。TIM2和HAL互不干擾排查問(wèn)題的時(shí)候邏輯清晰。lv_conf.h里L(fēng)V_TICK_CUSTOM保持0LVGL就會(huì)用lv_tick_inc這個(gè)接口。主循環(huán)結(jié)構(gòu)也需要注意while (1) { lv_timer_handler(); delay_ms(5); }lv_timer_handler的調(diào)用間隔決定了LVGL的刷新速率不能讓它被其他耗時(shí)任務(wù)阻塞太久。如果主循環(huán)里還有別的業(yè)務(wù)邏輯盡量保證一次循環(huán)的耗時(shí)在10ms以內(nèi)。4.2 XPT2046讀取代碼從SPI時(shí)序到觸摸點(diǎn)坐標(biāo)7寸電阻觸摸屏最常見(jiàn)的觸摸控制器是XPT2046它用SPI接口輸出12位坐標(biāo)數(shù)據(jù)。讀取流程是拉低CS發(fā)送一個(gè)控制字節(jié)然后把SPI時(shí)鐘繼續(xù)打兩輪從返回?cái)?shù)據(jù)中拼出坐標(biāo)??刂谱止?jié)里0xD0表示讀X坐標(biāo)0x90表示讀Y坐標(biāo)。一個(gè)可用的讀取函數(shù)static uint16_t tp_read_channel(uint8_t cmd) { uint16_t value 0; TP_CS_LOW(); spi_read_write(cmd); uint8_t b0 spi_read_write(0x00); uint8_t b1 spi_read_write(0x00); TP_CS_HIGH(); value ((uint16_t)b0 8 | b1) 3; return value; }右移3位是把16位拼接結(jié)果里的多余低位丟掉保留12位有效數(shù)據(jù)。SPI頻率建議控制在1MHz左右XPT2046對(duì)時(shí)序要求不高但線長(zhǎng)干擾多時(shí)降速更穩(wěn)。裸機(jī)上讀取觸摸可以放在LVGL的indev回調(diào)里被動(dòng)讀取也可以用定時(shí)器主動(dòng)掃描后保存全局變量。LVGL的indev回調(diào)本身就是周期調(diào)用直接在回調(diào)里讀就行。電阻屏有個(gè)物理特性按下瞬間的采樣值不穩(wěn)定建議連續(xù)讀三到五次取中間值或者做平均不然會(huì)出現(xiàn)點(diǎn)擊跳點(diǎn)。4.3 坐標(biāo)映射與旋轉(zhuǎn)觸摸錯(cuò)位的三個(gè)高頻原因觸摸回調(diào)用來(lái)喂給LVGL的是屏幕邏輯坐標(biāo)而屏幕上應(yīng)當(dāng)是控件所在的像素坐標(biāo)。XPT2046輸出的是0-4095范圍內(nèi)的12位原始值背后是觸摸面板的物理坐標(biāo)系跟屏幕像素坐標(biāo)系可能有鏡像、翻轉(zhuǎn)、縮放三種關(guān)系。最簡(jiǎn)單的一種映射是線性縮放data-point.x (uint16_t)((uint32_t)raw_x * 800 / 4096);>x 800 - x; /* 水平鏡像 */ y 480 - y; /* 垂直鏡像 */第二個(gè)是xy互換。有些7寸屏觸摸面板的X方向?qū)?yīng)LCD的Y方向需要交換x和y再做縮放。第三個(gè)是LVGL的顯示旋轉(zhuǎn)和觸摸旋轉(zhuǎn)沒(méi)有同步。LVGL 8.2的disp_drv結(jié)構(gòu)體里有rotated字段旋轉(zhuǎn)屏幕后觸摸回調(diào)收到的坐標(biāo)系仍然是原始物理坐標(biāo)如果只旋轉(zhuǎn)顯示不旋轉(zhuǎn)觸摸點(diǎn)擊必然錯(cuò)位。我踩坑后的做法是盡量不依賴LVGL的rotated字段而是在自己的觸摸映射函數(shù)里統(tǒng)一完成旋轉(zhuǎn)和鏡像這樣邏輯最簡(jiǎn)單出錯(cuò)也容易排查。5. 實(shí)測(cè)避坑記錄從白屏到觸摸偏位的完整排查過(guò)程5.1 白屏不是LVGL沒(méi)跑是FSMC寫操作壓根沒(méi)進(jìn)LCD白屏是7寸屏移植時(shí)遇到最多的現(xiàn)象大部分人第一反應(yīng)是LVGL初始化失敗其實(shí)九成是LCD通路本身沒(méi)打通。我的排查順序是這樣的先在main函數(shù)里、任何LVGL代碼之前單獨(dú)跑一段LCD驅(qū)動(dòng)測(cè)試——直接向LCD_REG寫0x2C命令然后連續(xù)寫1000個(gè)0xF800紅色和0x07E0綠色。如果屏幕有紅綠交替顯示說(shuō)明FSMC映射、LCD控制器初始化、GPIO配置都正確問(wèn)題在LVGL側(cè)如果屏一直白著問(wèn)題在FSMC或LCD驅(qū)動(dòng)跟LVGL無(wú)關(guān)。白屏?xí)r重點(diǎn)檢查這四處FSMC時(shí)鐘是否開(kāi)啟GPIO是否配置為AF_PPCMD/DATA地址偏移是否按RS所接地址線正確計(jì)算LCD控制器初始化時(shí)序是否夠?qū)捤捎绕涫荄ataSetupTime太小有沒(méi)有把LCD的復(fù)位腳拉高并延時(shí)足夠長(zhǎng)有些屏需要50ms以上。按這個(gè)順序排查基本能定位到根因。5.2 顏色發(fā)藍(lán)或偏紅RGB與BGR的字節(jié)序問(wèn)題UI跑起來(lái)了但顏色整體不對(duì)比如紅色按鈕變成藍(lán)色、膚色發(fā)紫。這個(gè)現(xiàn)象百分之八十是RGB565字節(jié)序不匹配。LVGL內(nèi)部畫像素時(shí)用的是16位整數(shù)值內(nèi)存里低字節(jié)和高字節(jié)的排列順序是固定的但LCD控制器端的串轉(zhuǎn)并邏輯可能期望高字節(jié)在前或低字節(jié)在前。這時(shí)去lv_conf.h里切換LV_COLOR_16_SWAP的值0改1或1改0問(wèn)題通常立刻消失。如果切換后顏色還是偏再去看LCD控制器的RGB順序寄存器。RA8875和SSD1963都有類似“RGB/BGR”的模式位例如設(shè)置0x36寄存器可以讓內(nèi)存中的RGB順序最終映射到屏幕像素。我當(dāng)時(shí)同時(shí)動(dòng)了lv_conf.h和LCD寄存器才把顏色調(diào)正而且兩個(gè)地方的效果是疊加的所以調(diào)試時(shí)要一次只改一個(gè)變量別兩個(gè)一起改。另外要注意draw buffer里的顏色位深必須與LV_COLOR_DEPTH一致。如果源工程是8位屏改過(guò)來(lái)的LV_COLOR_DEPTH還是8會(huì)出現(xiàn)顏色慘不忍睹的塊狀漸變。直接確認(rèn)這個(gè)宏在16就行。5.3 運(yùn)行幾分鐘后HardFault內(nèi)存碎片與棧溢出界面能顯示但運(yùn)行一段時(shí)間后突然卡死進(jìn)HardFault中斷這是裸機(jī)移植的經(jīng)典難題。原因通常是以下三個(gè)之一棧溢出。LVGL繪制過(guò)程中會(huì)嵌套調(diào)用大量函數(shù)尤其帶樣式、帶圓弧的控件棧使用量比想象中大。啟動(dòng)文件里Stack_Size如果用默認(rèn)的0x200或0x400很容易爆棧。我建議把Stack_Size改成0x1000也就是4KB。對(duì)于64KB RAM的ZET6來(lái)說(shuō)這個(gè)開(kāi)銷值得。內(nèi)存池耗盡。LV_MEM_SIZE設(shè)小了控件創(chuàng)建多了之后LVGL內(nèi)部分配失敗但上層代碼沒(méi)有正確判斷繼續(xù)操作空指針。解決方法是打開(kāi)lv_conf.h里的LV_USE_MEM_MONITOR在外部循環(huán)定期調(diào)用lv_mem_monitor輸出剩余內(nèi)存觀察是不是隨著操作單調(diào)遞減。如果數(shù)值在下降多半是某些控件的事件回調(diào)里反復(fù)創(chuàng)建對(duì)象但沒(méi)有刪除。DMA緩沖對(duì)齊問(wèn)題。SPI DMA傳輸時(shí)如果源地址不滿足控制器要求DMA會(huì)卡住或者傳錯(cuò)數(shù)據(jù)。最穩(wěn)妥是給DMA專用緩沖加aligned屬性或者先拷貝到對(duì)齊數(shù)組成員。HardFault問(wèn)題難排查好解決之后要總結(jié)出規(guī)律每次添加新控件或新動(dòng)畫后先把棧加大一檔、內(nèi)存池觀察一段時(shí)間再上線復(fù)雜業(yè)務(wù)邏輯。5.4 界面刷新慢優(yōu)化效果與參數(shù)實(shí)測(cè)7寸屏上LVGL的默認(rèn)刷新率配置是30Hz理論上每33ms刷新一次臟區(qū)域。實(shí)際卡頓的優(yōu)化方向很清晰編譯器優(yōu)化。Keil下-O0和-O2在純渲染場(chǎng)景實(shí)測(cè)差距可能有1.5倍以上。LVGL這類需要密集像素計(jì)算和內(nèi)存操作的代碼不開(kāi)優(yōu)化等于讓F103背著沙袋跑步。工程最終發(fā)布時(shí)務(wù)必開(kāi)-O2。draw buffer尺寸。從一個(gè)16KB單緩沖改成兩個(gè)16KB雙緩沖撕裂明顯減少再把每個(gè)buffer從16KB提到24KB渲染整塊區(qū)域時(shí)批處理比例更高CPU反而更輕松。內(nèi)存如果實(shí)在緊張優(yōu)先保雙緩沖尺寸可以壓縮。減少透明效果和陰影。LVGL的默認(rèn)主題帶圓角、陰影、透明度這些效果在F103上每幀要額外計(jì)算一遍alpha混合。想流暢就把LV_THEME_DEFAULT的shadow_width改為0border_width用默認(rèn)但關(guān)掉陰影。下面是我在ZET6上測(cè)過(guò)的幾種配置效果配置實(shí)際感受單緩沖16KB-O0切頁(yè)有明顯閃爍滑動(dòng)卡頓雙緩沖16KBx2-O0不閃了但拖動(dòng)響應(yīng)不夠跟手雙緩沖16KBx2-O2基本流暢動(dòng)畫能看雙緩沖24KBx2-O2主觀跟手復(fù)雜頁(yè)面偶有掉幀F(xiàn)103畢竟是72MHz的老架構(gòu)想把7寸屏拖出F4那種絲滑感不現(xiàn)實(shí)但只要避開(kāi)透明動(dòng)畫、大面積全屏重繪這幾個(gè)高成本操作簡(jiǎn)單列表、按鈕、儀表盤頁(yè)面完全穩(wěn)得住。我這次移植用的是SSD1963并口屏、XPT2046電阻觸摸最后定下來(lái)雙緩沖24KBx2、LV_MEM_SIZE 24KB、-O2優(yōu)化。7寸屏跑起來(lái)普通頁(yè)面切換和按鈕交互都沒(méi)問(wèn)題唯一需要注意的是別在F103上做大量列表項(xiàng)的同時(shí)開(kāi)陰影和漸變否則幀率肉眼可見(jiàn)地掉。這套參數(shù)在RCT6上我也試過(guò)把draw buffer壓到12KBx2之后依然能跑只是內(nèi)存更緊張。如果你也打算移植先在裸板上把LCD單通測(cè)試過(guò)了再談LVGL配置這條路走下來(lái)會(huì)順暢很多。