工业显示与控制融合编程:在同一MCU上同时驱动HMI屏幕与PLC逻辑
单MCU既跑HMI又跑PLC逻辑,最容易翻车的不是屏驱不起来,而是“界面卡一下,电机也抖一下”。正确架构只有一条:控制逻辑永远是高优先级确定任务,HMI是低优先级消费方,两边只通过一块共享内存映射区交换变量,不允许GUI代码直接读IO、也不允许控制循环里刷屏。
一、分层:控制核与界面解耦
物理IO(SPI DI/DO) ─▶ IO镜像区
│
PLC扫描(1~5ms)
│
共享变量表(映射区)
┌─────┴─────┐
控制写状态 HMI读状态
HMI写命令 控制读命令
│
LVGL/显示任务(50~100ms)
控制侧只认“映射区”,不认触摸屏;HMI侧只认“映射区”,不认继电器。两者通过地址表绑定,和Modbus HMI+PLC思路一致。
二、共享变量表
typedef struct {
/* HMI写,PLC读 */
uint16_t cmd_start;
uint16_t cmd_stop;
int16_t set_speed;
uint16_t mode_sel;
/* PLC写,HMI读 */
uint16_t stat_run;
uint16_t stat_fault;
int16_t act_speed;
int16_t temp_x10;
uint16_t diag_code;
} io_map_t;
__attribute__((section(".no_cache_ram")))
volatile io_map_t g_io;
关键规矩:
命令位和状态位分开地址,HMI不读自己写的命令位当反馈
多字节变量(speed/temp)由PLC单边写,HMI只读
放非缓存RAM区,DMA/LTDC和CPU看到同一份
写共享区用临界区或原子32位写,别半截更新
三、控制周期:定时器中断/RTOS任务
裸机用SysTick或TIM触发标志,控制扫描固定周期:
void TIM3_IRQHandler(void){ // 1ms
ctrl_tick = 1;
TIM3->SR &= ~UIF;
}
void plc_scan_1ms(void){
uint16_t di = di_read(); // 从SPI输入芯片读
/* 读HMI命令 */
if(g_io.cmd_start && !g_io.cmd_stop){
motor_enable();
}
if(g_io.cmd_stop){
motor_coast();
}
set_pwm(g_io.set_speed);
/* 回写状态 */
g_io.stat_run = motor_is_run();
g_io.act_speed = enc_speed();
g_io.stat_fault = fault_check();
}
主循环只扫标志:
while(1){
if(ctrl_tick){ ctrl_tick=0; plc_scan_1ms(); }
if(disp_tick){ disp_tick=0; hmi_refresh(); }
kick_ctrl_watchdog();
}
控制任务别调lvgl、别做sprintf整屏、别等I2C触摸完成。
四、HMI侧:只搬映射区
LVGL里数值和按钮绑的不是硬件,是g_io:
void hmi_refresh(void){
lv_label_set_text_fmt(label_speed, "%d rpm", g_io.act_speed);
lv_obj_set_state(btn_run, g_io.stat_run ? LV_STATE_CHECKED : 0);
if(g_io.stat_fault) show_alarm(g_io.diag_code);
}
void btn_start_cb(lv_event_t*e){
g_io.cmd_start = 1; // 只写命令位
}
void btn_stop_cb(lv_event_t*e){
g_io.cmd_stop = 1;
}
显示周期50~100ms足够人眼,别跟控制周期绑死。触摸回调里只置位命令,真正动作在下一次PLC扫描生效,顺序不会乱。
五、通信失效/界面卡死兜底
HMI死循环不代表控制该停。反过来才危险:控制死了界面还在跑。
独立看门狗只由控制任务周期喂,或做任务心跳计数
HMI任务超时只切“降级界面”,不复位MCU
上位机/触屏断连→PLC按本地安全序列跑:减速、回零、输出安全值
急停按钮走硬件DI+中断,不绕GUI
写输出前过“使能锁存”,HMI命令位需PLC确认后清零
if(!hmi_heartbeat_ok()){ // 500ms无刷新
g_io.cmd_start = 0; // 不再接受新启动
motor_coast();
}
六、RTOS版写法
FreeRTOS里分两个任务:
void task_plc(void*p){ // prio高
for(;;){
plc_scan_1ms();
os_delay_until(&next, 1);
}
}
void task_hmi(void*p){ // prio低
for(;;){
lv_timer_handler();
hmi_refresh();
os_delay(20); // 50fps上限
}
}
队列只用来传事件(页面切换),过程数据走g_io共享区,不进消息队列——否则控制侧等队列阻塞就完了。
七、落坑清单
控制循环里调lvgl_draw→刷屏卡死拖垮IO
HMI直接读GPIO当状态→和PLC扫描打架
命令位和状态位同地址→按钮灯假反馈
多字节变量没临界保护→显示读到半新半旧
看门狗放main空转里喂→控制死透也不复位
急停走触摸按钮→屏碎了机台停不了
显示周期设1ms→CPU全耗在GUI
共享区放Cache RAM→LTDC显示旧帧
HMI断连还继续执行启动命令→安全隐患
PLC扫描和显示同函数混写→没法单测
八、落地顺序
先定io_map_t地址表,命令/状态/过程值分块
控制扫描1ms独立任务,只读写g_io和物理IO
HMI任务50~100ms刷屏,只绑g_io
按钮回调只写命令位,状态以PLC回写为准
共享区放非缓存RAM,多字节加临界
独立看门狗由控制任务喂,HMI心跳单独计
急停/使能走硬件DI,GUI只发请求
通信丢失进安全态,输出切预置值
单MCU融HMI+PLC的本质是“控制是主、显示是从”:PLC扫描出确定周期,界面只是把映射区画出来;命令从屏到控制只过共享内存,不穿透到IO层。这样屏驱动崩了、触摸卡了、LVGL跑满帧,电机逻辑依旧按1ms走;真要出问题也是控制任务不喂狗整体复位,而不是界面拖控制一起抖。工业现场怕的不是屏不亮,是屏卡了机器还在动——共享内存+优先级隔离就是把这两件事拆开。





