第一次接触立创梁山派GD32F470ZGT6开发板时,我就被它强悍的性能吸引了。作为一款Cortex-M4内核的MCU,主频高达240MHz,内置512KB SRAM和3MB Flash,完全能满足嵌入式GUI的需求。我手头这块1.69寸的SPI屏幕分辨率是240x280,虽然不大,但显示效果足够细腻。
硬件连接其实很简单,主要就是SPI接口的接线:
这里有个小技巧:如果屏幕支持硬件SPI+DMA,一定要用这个方案。我之前用软件SPI刷屏,帧率只有15FPS左右,换成硬件SPI+DMA后直接飙到60FPS,效果立竿见影。GD32F470的DMA控制器有8个通道,配置起来也很方便。
开发环境我选择了Keil MDK 5.37,主要是考虑到对GD32芯片的支持比较完善。安装完GD32F4xx_Firmware_Library V2.1.3的固件库后,记得检查一下设备支持包是否安装正确。有次我忘记安装设备包,编译时一堆奇怪的报错,排查了半天才发现问题。
从GitHub下载LVGL源码时,建议直接clone最新稳定版。我这次用的是v8.2.0,解压后重点需要这几个文件:
在工程目录新建LVGL文件夹,把上述文件放进去后,记得把lv_conf_template.h重命名为lv_conf.h。这个文件是LVGL的配置中枢,后面会频繁修改。
文件整理有个小技巧:examples文件夹里其他示例都可以删掉,只保留porting目录。这样既节省空间,又避免编译时引入不必要的文件。我遇到过因为多余示例文件导致的链接错误,清理后问题就解决了。
Keil工程配置要注意三点:
第一次编译通常会遇到路径错误,主要检查两点:
c复制void __aeabi_assert(const char *err, const char *file, int line) {
// 简单处理即可
}
打开lv_port_disp.c文件,这里需要实现三个关键函数:
c复制static void disp_init(void) {
LCD_Init(); // 你的LCD初始化函数
}
c复制static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) {
for(int y = area->y1; y <= area->y2; y++) {
for(int x = area->x1; x <= area->x2; x++) {
LCD_DrawPointFlush(x,y,color_p->full);
color_p++;
}
}
lv_disp_flush_ready(disp_drv);
}
c复制void lv_port_disp_init(void) {
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LCD_W * 10]; // 单缓存方案
lv_disp_draw_buf_init(&draw_buf, buf, NULL, LCD_W * 10);
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &draw_buf;
disp_drv.flush_cb = disp_flush;
disp_drv.hor_res = LCD_W;
disp_drv.ver_res = LCD_H;
lv_disp_drv_register(&disp_drv);
}
这里有个性能优化点:如果内存充足,建议使用双缓存方案。我在GD32F470上测试,双缓存能使帧率提升30%左右。修改方法很简单:
c复制static lv_color_t buf1[LCD_W * 20], buf2[LCD_W * 20];
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LCD_W * 20);
lv_conf.h里有几个关键参数需要特别注意:
c复制#define LV_COLOR_DEPTH 16 // 匹配大多数SPI屏幕
#define LV_COLOR_16_SWAP 1 // SPI传输时需要字节交换
c复制#define LV_MEM_SIZE (48 * 1024) // 根据可用SRAM调整
c复制#define LV_USE_PERF_MONITOR 1 // 开启FPS显示
#define LV_USE_MEM_MONITOR 1 // 显示内存使用
c复制#define LV_REFR_DEF_PERIOD 10 // 10ms刷新周期(100FPS)
字体选择也很重要,默认的Montserrat字体比较美观:
c复制#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_16 1
#define LV_FONT_MONTSERRAT_18 1
特别注意:在Keil的Target选项里,一定要取消"Use MicroLIB"的勾选,否则程序会卡死。这是因为LVGL与MicroLIB的兼容性问题。
主函数里的初始化流程有严格顺序要求:
c复制int main(void) {
systick_config(); // 1. 先初始化系统时钟
usart_gpio_config(); // 2. 初始化调试串口
LCD_Init(); // 3. 初始化显示屏
lv_init(); // 4. 初始化LVGL
lv_port_disp_init(); // 5. 注册显示驱动
// 创建测试界面
lv_obj_t * label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Hello LVGL!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
while(1) {
lv_task_handler(); // 6. 主循环调用任务处理器
lv_tick_inc(1); // 7. 在systick中断中调用
}
}
定时器配置有个坑要注意:lv_tick_inc()必须在精确的1ms中断中调用。我最初尝试用延时实现,结果动画卡顿严重。正确做法是用systick中断:
c复制void SysTick_Handler(void) {
lv_tick_inc(1);
}
如果发现界面刷新不正常,可以按这个步骤排查:
对于280x240的1.69寸屏,DPI应该这样计算:
c复制#define LV_DPI_DEF 153 // sqrt(280²+240²)/1.69
经过实际测试,我总结出几个提升帧率的有效方法:
c复制spi_init(SPI0, SPI_MASTER, SPI_TRANSMODE_FULLDUPLEX, SPI_FRAMEFORMAT_8BIT,
SPI_CK_PL_HIGH_PH_2EDGE, SPI_PSC_8); // 30MHz时钟
c复制void DMA0_Channel0_IRQHandler(void) {
if(dma_interrupt_flag_get(DMA0, DMA_CH0, DMA_INT_FLAG_FTF)) {
// 切换缓冲区
}
}
c复制disp_drv->full_refresh = 0; // 局部刷新
disp_drv->direct_mode = 1; // 直接写入模式
有个特别实用的调试技巧:在lv_conf.h中开启性能监控后,屏幕上会显示实时的FPS和内存使用情况。我发现当FPS低于30时,通常是因为disp_flush()函数处理太慢,这时就要考虑优化绘制算法了。
移植过程中我踩过不少坑,这里分享几个典型问题的解决方法:
c复制#define LV_MEM_CUSTOM 1 // 使用自定义内存管理
void * my_malloc(size_t size) {
return malloc_from_external_ram(size);
}
c复制#define LV_FONT_SUBPX_BGR 1
最后提醒一点:GD32F470的Flash写入速度较慢,如果发现界面加载延迟,可以考虑将资源文件放到外部存储器,或者使用LVGL的文件系统接口。