4、分拣机控制器模型搭建:PLC/嵌入式控制器模型建立、控制逻辑仿真、故障注入接口设计

好,咱们进入第四章。这一章,我打算聊聊控制器模型搭建这件事。

做HIL测试,说白了就是拿虚拟环境去骗控制器,让它以为自己在带真实设备。那控制器本身呢?它也得有个“替身”才行。这个替身,就是控制器模型。

我个人习惯把控制器模型分成两类:PLC模型和嵌入式控制器模型。别看它们都是控制器,脾气秉性差得挺远。

4.1 PLC控制器模型建立

PLC这东西,在分拣机里太常见了。逻辑简单、稳定可靠,但你要在HIL里模拟它,还真得花点心思。

我建议用两种方式建PLC模型:

  • 指令级仿真:把PLC的梯形图或ST语言翻译成C代码或Simulink模块。这种方式精度高,但工作量也大。
  • 行为级仿真:只模拟PLC的输入输出关系,不关心内部扫描周期。速度快,适合系统级验证。

我在项目中遇到过一个问题:PLC的扫描周期是10ms,但HIL仿真步长是1ms。你想想看,如果每1ms都去读一次PLC输出,那CPU直接炸了。所以,我一般会在模型里加一个“扫描周期同步模块”,让PLC模型每10ms才更新一次输出。

关键点:PLC模型的扫描周期必须与真实PLC一致。否则,时序逻辑全乱套。

举个例子,一个简单的分拣推杆控制逻辑:

// 伪代码:PLC推杆控制
IF (传感器1 = ON AND 传感器2 = OFF) THEN
    推杆伸出
ELSE
    推杆缩回
END_IF

在HIL里,这个逻辑会被翻译成:

if (sensor1 == 1 && sensor2 == 0) {
    pusher_out = 1;
} else {
    pusher_out = 0;
}

嗯,这里要注意:真实PLC里还有上升沿、下降沿检测,这些在模型里也得实现。我一般用“前值保持”的方法来做。

4.2 嵌入式控制器模型建立

嵌入式控制器,比如STM32、DSP这类,比PLC复杂多了。它们有中断、有定时器、有DMA,甚至还有RTOS。

建这种模型,我建议分三步走:

  1. 硬件抽象层:把GPIO、ADC、PWM这些外设接口抽象成函数。这样上层逻辑就不用管底层硬件了。
  2. 任务调度仿真:模拟RTOS的任务切换。我一般用状态机来实现,每个任务一个状态。
  3. 中断仿真:这个最头疼。真实中断是硬件触发的,模型里得用事件触发。我习惯把中断优先级做成一个队列,按优先级排队执行。

我的小技巧:嵌入式模型里,定时器中断最容易出问题。我曾经因为定时器溢出没处理好,导致整个分拣机模型跑飞。后来我加了一个“溢出保护”模块,每次定时器计数值超过阈值就自动复位。

来看一个简单的电机控制任务模型:

// 伪代码:电机控制任务
void motor_control_task(void) {
    while(1) {
        speed_ref = get_speed_reference();  // 读取速度参考值
        speed_act = get_encoder_speed();    // 读取编码器速度
        pwm_out = pid_controller(speed_ref, speed_act); // PID计算
        set_pwm_duty(pwm_out);              // 设置PWM占空比
        delay_ms(10);                       // 任务周期10ms
    }
}

在HIL模型里,这个任务会被拆成:

// HIL模型:电机控制任务仿真
void motor_control_model_step(void) {
    speed_ref = get_speed_reference_model();
    speed_act = get_encoder_speed_model();
    pwm_out = pid_controller_model(speed_ref, speed_act);
    set_pwm_duty_model(pwm_out);
}

你看,区别就在于“delay_ms(10)”变成了“step()”。模型里没有时间概念,只有步进概念。

4.3 控制逻辑仿真

控制逻辑仿真,说白了就是让模型跑起来,看看它能不能正确响应输入。

我一般会做三类测试:

  • 正常工况仿真:给标准输入,看标准输出。比如给个1000rpm的转速指令,看电机模型是不是真的跑到1000rpm。
  • 边界条件仿真:给极限输入,看控制器会不会崩溃。比如给个0V的传感器信号,看PLC会不会误动作。
  • 时序仿真:检查信号时序是否满足要求。比如推杆伸出信号必须在传感器触发后50ms内发出。

警告:控制逻辑仿真时,一定要把“初始状态”设对。我见过有人仿真时忘了初始化变量,结果模型一跑就报错,查了半天才发现是初始值不对。

举个例子,分拣机的“包裹到位检测”逻辑:

输入条件 预期输出 实际输出 结果
传感器1=ON, 传感器2=OFF 推杆伸出 推杆伸出 通过
传感器1=OFF, 传感器2=ON 推杆缩回 推杆缩回 通过
传感器1=ON, 传感器2=ON 报警 推杆伸出 失败

你看,第三个用例就暴露了问题:两个传感器同时触发时,控制器没有进入报警状态。这就是控制逻辑仿真的价值。

4.4 故障注入接口设计

故障注入,是HIL测试的灵魂。没有故障注入,你测的只是“理想情况”,根本不知道控制器在真实故障下会怎样。

我建议在控制器模型里预留三类故障注入接口:

  • 传感器故障:比如信号丢失、信号漂移、信号卡死。
  • 执行器故障:比如电机堵转、推杆卡住、阀门失效。
  • 通信故障:比如CAN总线断线、数据包丢失、CRC错误。

我曾经在项目里吃过亏:故障注入接口设计得太复杂,测试人员根本不知道怎么用。后来我学乖了,把接口做成“开关式”的——一个布尔变量控制一个故障类型,简单粗暴。

设计原则:故障注入接口要“可配置、可观测、可恢复”。可配置是指能选择故障类型;可观测是指能看到故障注入后的效果;可恢复是指测试完后能一键清除故障。

来看一个传感器故障注入的代码示例:

// 传感器故障注入接口
typedef struct {
    bool fault_enable;      // 故障使能
    uint8_t fault_type;     // 故障类型:0=无,1=丢失,2=漂移,3=卡死
    float fault_value;      // 故障值(漂移时用)
    float sensor_raw;       // 原始传感器值
    float sensor_output;    // 输出给控制器的值
} SensorFaultInject_t;

// 故障注入处理函数
void sensor_fault_inject(SensorFaultInject_t *fault) {
    if (fault->fault_enable) {
        switch (fault->fault_type) {
            case 1: // 信号丢失
                fault->sensor_output = 0.0f;
                break;
            case 2: // 信号漂移
                fault->sensor_output = fault->sensor_raw + fault->fault_value;
                break;
            case 3: // 信号卡死
                fault->sensor_output = fault->sensor_output; // 保持上次值
                break;
            default:
                fault->sensor_output = fault->sensor_raw;
                break;
        }
    } else {
        fault->sensor_output = fault->sensor_raw;
    }
}

你看,这个接口设计得很简单。测试人员只需要在测试脚本里设置fault_enable = true,然后选一个故障类型就行了。

避坑指南:我曾经把故障注入接口放在模型的最顶层,结果每次修改都要重新编译整个模型。后来我把接口做成独立的“故障注入模块”,用UDP或共享内存与主模型通信。这样,修改故障逻辑时只需要重启故障注入模块,不用动主模型。

最后,我想说一句:控制器模型搭建,不是一蹴而就的事。你得反复迭代,不断加细节。但记住,别一开始就追求完美。先让模型跑起来,再慢慢优化。这是我从无数次失败里总结出来的经验。