4. SIL环境搭建与代码集成
SIL(Software-in-the-Loop)验证的核心思想,是把控制器模型通过自动代码生成得到的C代码,与原本在Simulink等建模环境中运行被控对象模型放在同一台主机上闭环运行。这样做的价值在于:验证的不再是图形化模型,而是真正要烧录到ECU里的那份代码。因此本节的关键任务可以归纳为四件事——把代码生成配置对、把SIL仿真框架搭起来、把被控对象模型与生成代码正确闭环、把编译与运行脚本固化下来。
一、自动代码生成配置
以Embedded Coder为例,代码生成配置的核心是让生成代码的接口、数据类型和调度方式与后续集成环境一致。常用配置项包括:系统目标文件选择ert.tlc(Embedded Real-Time目标),语言选择C,代码生成仅生成代码而不构建(Generate code only)在SIL阶段通常先勾选,便于单独控制编译过程。
接口配置上,求解器建议使用固定步长(Fixed-step)、离散求解器(discrete),步长与后续SIL仿真步长保持一致,例如0.001 s。这样生成代码中的步进函数才会以确定周期被调用。对于被控对象与控制器分属不同模型的场景,控制器模型应配置为导出函数(Export function)或可重入(Reusable function),并明确步进函数名,例如Controller_step。
数据类型方面,需要保证控制器模型中的信号类型与生成代码一致,避免出现double与single混用导致的精度差异。若控制器使用定点,应在配置中指定定点目标,并检查生成代码中的类型定义头文件。一个典型的配置检查清单如下:
- 系统目标文件:
ert.tlc,语言C; - 求解器:固定步长、离散,步长与SIL一致;
- 代码生成:仅生成代码,生成报告;
- 接口:导出步进函数,函数名与集成脚本一致;
- 数据类型:与目标一致,检查
rtwtypes.h; - 代码风格:可读性优先,便于人工核对。
二、SIL仿真框架
SIL仿真框架的本质是一个"调度器+接口层"。调度器负责按固定步长推进时间,接口层负责在每一步中先调用被控对象模型计算传感器输出,再把该输出送入生成代码的步进函数,最后把生成代码返回的控制量送回被控对象模型。这个顺序不能颠倒,否则会引入一步延迟,导致闭环特性与真实系统不符。
在Simulink中,常用做法是把生成代码封装成S-Function或使用Legacy Code Tool,也可以直接用coder.ceval在MATLAB Function块中调用。无论哪种方式,接口层需要完成三件事:初始化(调用生成代码的初始化函数)、步进(调用步进函数)、终止(调用终止函数)。初始化只在仿真开始时执行一次,步进在每个采样周期执行,终止在仿真结束时执行。
框架中还需要处理输入输出缓冲。生成代码的输入输出通常是结构体指针或数组指针,接口层要把Simulink信号转换成对应的内存布局。例如控制器输入为real_T rtw_u[2],输出为real_T rtw_y[1],接口层就要在每一步把传感器信号写入rtw_u,再从rtw_y读取控制量。
三、被控对象模型与生成代码的闭环连接
闭环连接的关键是明确信号方向与单位。以直流电机速度控制为例:被控对象模型输入为电压U(单位V),输出为转速n(单位rpm);控制器输入为转速误差,输出为电压。连接时,被控对象输出的转速送入控制器,控制器输出的电压送回被控对象,形成负反馈。
需要注意的是,被控对象模型与生成代码的采样周期必须一致。如果被控对象用连续求解器、控制器用离散步进,就会出现采样时刻不匹配。推荐做法是被控对象也离散化,或使用固定步长求解器,使两者在同一时间网格上推进。此外,控制器中的积分器、滤波器等状态量由生成代码内部维护,接口层不要重复初始化,否则会破坏状态连续性。
下面这张图展示了SIL闭环的数据流与调用关系,重点体现"被控对象—接口层—生成代码"三者的信号方向与步进顺序。
四、编译与运行脚本
手工点击编译和运行容易出错,也不利于回归。推荐把编译与运行固化成脚本。编译脚本负责把生成代码、接口层代码和必要的运行时文件一起编译成可执行文件或动态库;运行脚本负责启动仿真、加载参数、记录日志。
以MATLAB环境为例,可以用mex命令编译S-Function,把生成代码的.c文件与接口层.c文件一起加入编译列表。下面是一个简化的编译脚本示例:
% build_sil.m —— 编译SIL接口
src = { ...
'Controller.c', ... % 生成代码
'Controller_data.c', ...
'sil_interface.c' ... % 接口层
};
inc = { ...
'-I./codegen', ...
'-I./include'
};
mex('-v', src{:}, inc{:}, 'sil_wrapper.c', '-output', 'sil_model');
运行脚本则负责设置仿真参数并启动。例如把仿真时长设为2 s、步长0.001 s,给定转速阶跃指令1000 rpm,然后运行模型并保存日志:
% run_sil.m —— 运行SIL仿真
set_param('sil_top', 'StopTime', '2');
set_param('sil_top', 'FixedStep', '0.001');
set_param('sil_top', 'Solver', 'FixedStepDiscrete');
ref = 1000; % 目标转速 rpm
simOut = sim('sil_top');
n = simOut.logsout.get('n').Values;
u = simOut.logsout.get('U').Values;
运行结束后,判断SIL是否通过,通常看三个指标:一是转速是否在合理时间内跟踪到指令值,例如1000 rpm阶跃下超调不超过10%、稳态误差不超过1%;二是控制量是否在物理范围内,例如电压不超过24 V;三是生成代码的输出与纯模型仿真(MIL)结果是否一致,若存在偏差,应检查数据类型、采样周期和状态初始化。
把上述四步串起来,SIL环境就具备了可重复、可回归的基础。代码生成配置保证"生成什么",仿真框架保证"怎么调度",闭环连接保证"信号对不对",编译运行脚本保证"每次都能一致地跑起来"。这四者缺一不可,也是后续HIL阶段能够顺利迁移的前提。