当前位置:首页 > 显示光电 > 显示光电
[导读]工业HMI不是“点灯加按钮”那么简单:参数面板、报警页、趋势曲线、启停确认,都要稳定刷屏且不卡触摸。STM32上跑LVGL最稳的路线是:先裁lv_conf、再接显示flush、再挂触摸输入、最后UI逻辑放单一线程改对象。


工业HMI不是“点灯加按钮”那么简单:参数面板、报警页、趋势曲线、启停确认,都要稳定刷屏且不卡触摸。STM32上跑LVGL最稳的路线是:先裁lv_conf、再接显示flush、再挂触摸输入、最后UI逻辑放单一线程改对象。

一、lv_conf裁剪

先关掉用不到的模块,Flash/RAM立刻松出来:

#define LV_USE_OS                  0      // 裸机先0,FreeRTOS再改

#define LV_MEM_SIZE                (32 * 1024)

#define LV_COLOR_DEPTH            16      // RGB565

#define LV_USE_TICK_CUSTOM        0

#define LV_THEME_DEFAULT_INIT     0

字体只留HMI用到的:ASCII+数字+必要汉字,别把demo字体全开。不需要文件系统、GPU、输入法就关掉。

二、显示缓冲:部分缓冲最实用

320×240全缓冲要320×240×2=150KB,小STM32扛不住。工业屏常用“若干行”部分缓冲:

#define BUF_ROWS 20

static lv_color_t buf1[LCD_W*BUF_ROWS];

static lv_color_t buf2[LCD_W*BUF_ROWS];

static lv_disp_draw_buf_t draw_buf;


lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LCD_W*BUF_ROWS);

lv_disp_drv_t drv; lv_disp_drv_init(&drv);

drv.hor_res = LCD_W; drv.ver_res = LCD_H;

drv.draw_buf = &draw_buf;

drv.flush_cb = disp_flush;

drv.full_refresh = 0;

lv_disp_drv_register(&drv);

单缓冲省RAM但刷的时候CPU等;双缓冲配合DMA才真正并行。RAM够就buf2开着,不够就buf2传NULL。

三、flush回调:DMA完成再flush_ready

LVGL把脏矩形区域交给你,你搬到屏上对应窗口。SPI屏先设地址窗口,再发数据:

static volatile int dma_done = 0;


void disp_flush(lv_disp_drv_t*drv, const lv_area_t*area, lv_color_t*color_p){

   lcd_set_window(area->x1, area->y1, area->x2, area->y2);

   dma_done = 0;

   lcd_spi_dma_send((uint8_t*)color_p,

       (area->x2-area->x1+1)*(area->y2-area->y1+1)*2);

}


void DMA_Cplt_IRQ(void){

   dma_done = 1;

   lv_disp_flush_ready(&disp_drv);   // 必须等DMA真发完

}

最大坑:在flush里调完HAL_SPI_Transmit就立刻flush_ready,DMA还在发,LVGL已经复用缓冲→花屏撕裂。

主循环里也别死等DMA,裸机可在while(!dma_done);,RTOS交给定时器任务。

四、触摸输入:read回调只回报状态

电容屏(FT5x06/CST816)用INT脚进EXTI,主循环/定时读I2C,别每次lv_timer_handler都扫I2C。

void touch_read(lv_indep_drv_t*d, lv_indev_data_t*data){

   static int16_t lx=0, ly=0;

   if(tp_irq_flag){

       tp_irq_flag=0;

       uint16_t x,y;

       if(ts_read_point(&x,&y)){

           lx=x; ly=y;

           data->state = LV_INDEV_STATE_PRESSED;

       }

   } else {

       data->state = LV_INDEV_STATE_RELEASED;

   }

   data->point.x = lx; data->point.y = ly;

}

注册:

lv_indev_drv_t idrv; lv_indev_drv_init(&idrv);

idrv.type = LV_INDEV_TYPE_POINTER;

idrv.read_cb = touch_read;

lv_indev_drv_register(&idrv);

坐标偏移用lv_indev_set_read_cb里做线性校准,X/Y反了、镜像了都在这一层修,别改UI布局。

五、心跳与主循环

LVGL靠1ms心跳跑动画和长按:

void SysTick_Handler(void){

   HAL_IncTick();

   lv_tick_inc(1);

}

裸机:

while(1){

   lv_timer_handler();

   HAL_Delay(5);

   hmi_periodic_refresh();

}

FreeRTOS下单独建ui_task,别在多个任务里同时改lv_obj——LVGL不是线程安全,要改就放一个任务,别的任务用队列传数据。

六、工业HMI控件套路

启停按钮、状态灯、数值标签、报警条、曲线图:

lv_obj_t* btn = lv_btn_create(lv_scr_act());

lv_obj_set_size(btn,120,48);

lv_obj_align(btn,LV_ALIGN_TOP_LEFT,10,10);

lv_obj_t* lb = lv_label_create(btn);

lv_label_set_text(lb,"启动");

lv_obj_add_event_cb(btn, btn_cb, LV_EVENT_CLICKED, NULL);


void btn_cb(lv_event_t*e){

   if(lv_event_get_code(e)!=LV_EVENT_CLICKED) return;

   run_state = !run_state;

   HAL_GPIO_WritePin(MOTOR_GPIO,GPIO_PIN_SET, run_state);

   lv_label_set_text(lb, run_state?"停止":"启动");

   lv_obj_set_style_bg_color(btn, run_state?lv_color_hex(0x2e7d32)

                                         :lv_color_hex(0xb71c1c),0);

}

参数刷新别在中断里改label,主循环从队列取:

if(xQueueReceive(hmi_q,&val,0)==pdTRUE){

   lv_label_set_text_fmt(temp_lb,"温度 %.1f℃", val.temp);

   lv_bar_set_value(press_bar, val.bar, LV_ANIM_ON);

}

报警用lv_obj_add_state切LV_STATE_USER_1样式,别删了重建。

七、落坑清单

缓冲只开单块还开DMA→花屏

flush里提前flush_ready→撕裂

触摸每次轮询都读I2C→总线被占、响应抖

多任务直接改lv_obj→对象损坏

lv_tick_inc没接→动画不动、长按失效

字体全开→Flash爆

坐标没校准→点按钮偏一格

全屏full_refresh小RAM芯片→卡顿

八、落地顺序

lv_conf裁到最小,lv_init能过

显示部分缓冲+SPI DMA,静态画面不花

lv_tick_inc+主循环lv_timer_handler

触摸read回调,点按钮有反应

建启停/状态/报警页

传感器数据走队列进UI

FreeRTOS下单开ui_task

STM32工业HMI用LVGL的核心就三句:显示缓冲和DMA完成解耦、触摸只回报状态不忙读、UI对象归单一上下文改。剩下都是控件拼装和事件回调,SquareLine画完导出代码也能用,但驱动层这三处不稳,界面再漂亮也上不了产线。



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭