当前位置:首页 > 工业控制 > 工业控制
[导读]单MCU既跑HMI又跑PLC逻辑,最容易翻车的不是屏驱不起来,而是“界面卡一下,电机也抖一下”。正确架构只有一条:控制逻辑永远是高优先级确定任务,HMI是低优先级消费方,两边只通过一块共享内存映射区交换变量,不允许GUI代码直接读IO、也不允许控制循环里刷屏。


单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走;真要出问题也是控制任务不喂狗整体复位,而不是界面拖控制一起抖。工业现场怕的不是屏不亮,是屏卡了机器还在动——共享内存+优先级隔离就是把这两件事拆开。



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