4. 模型与代码的映射关系:Simulink模块到C代码的映射规则、数据类型与存储类的映射
好,咱们进入第四讲。前面几章我们把SIL的环境搭起来了,也跑了几个简单的例子。但很多朋友做完这一步就开始发懵——Simulink里拖个模块,点一下生成,代码是出来了,可这代码跟模型到底怎么对应的?我改模型,代码会怎么变?
说白了,这就是模型与代码的映射关系。不懂这个,你生成的代码就是个黑盒子,出了问题根本没法查。我个人习惯,在开始做任何SIL测试之前,先把映射规则摸清楚。今天咱们就掰开揉碎了讲这个。
4.1 Simulink模块到C代码的映射规则
先问个问题:一个Simulink加法模块,生成C代码里是什么?
嗯,大部分情况下就是一个 + 运算符。但如果是饱和加法呢?如果是定点加法呢?代码就完全不一样了。所以映射规则不是死板的,它取决于模块类型、参数配置、还有你选的目标环境。
4.1.1 基本模块的映射规律
我总结了一个规律,你可以记一下:
| Simulink模块类别 | 典型模块 | C代码映射方式 | 备注 |
|---|---|---|---|
| 数学运算 | Add, Product, Gain | 直接映射为C运算符(+、*、*) | 定点类型会插入移位和饱和处理 |
| 逻辑运算 | Logical Operator, Relational Operator | 映射为C逻辑运算符(&&、||、==) | 布尔类型映射为 boolean_T |
| 存储单元 | Unit Delay, Memory | 映射为全局或局部静态变量 | 存储类决定变量作用域 |
| 查表模块 | Lookup Table, Interpolation | 映射为数组 + 插值函数 | 一维/二维查表代码结构差异大 |
| 状态机 | Stateflow Chart | 映射为switch-case或if-else状态机 | 层次化状态机会生成嵌套switch |
| 子系统 | Subsystem, Atomic Subsystem | 映射为函数调用 | 原子子系统会生成独立函数 |
这个表格你最好存一下。我在项目里遇到过好几次,工程师把Lookup Table当成普通数学模块,结果生成的代码里冒出一大段插值算法,把内存撑爆了。嗯,查表模块的代码量,比你想象的大得多。
4.1.2 函数化映射:子系统与函数的关系
这里有个关键点——子系统。Simulink里你画一个子系统,默认情况下生成代码时会被内联展开,也就是不生成独立函数。但如果你把它设成原子子系统(Atomic Subsystem),代码生成器就会把它单独编译成一个C函数。
为什么要这么做?
我举个例子。你有一个控制算法,里面包含三个独立的计算模块:滤波、PID、输出限幅。如果你把它们都内联了,整个 step() 函数会变得巨长无比,可读性差,测试也不好做。但如果你把它们设成原子子系统,生成代码就会变成:
void control_step(void)
{
filter_step(); // 滤波
pid_step(); // PID计算
limit_step(); // 限幅
}
每个函数都可以单独做单元测试,SIL测试的粒度就细多了。我个人习惯,只要模块逻辑相对独立,就设成原子子系统。后期调试能省一半时间。
subsystem_1、subsystem_2,你看着都头疼。
4.2 数据类型的映射
数据类型映射,是SIL测试里最容易出问题的地方。为什么?因为Simulink是双精度浮点的世界,而嵌入式C代码里,你用的可能是定点数、单精度、甚至自定义的整数类型。映射不对,测试结果全是错的。
4.2.1 Simulink内置类型到C类型的映射表
先看一张标准映射表:
| Simulink类型 | 默认C类型 | 说明 |
|---|---|---|
| double | real_T(即double) | 默认浮点类型 |
| single | real32_T(即float) | 单精度浮点 |
| int8 | int8_T | 有符号8位整数 |
| uint8 | uint8_T | 无符号8位整数 |
| int16 | int16_T | 有符号16位整数 |
| uint16 | uint16_T | 无符号16位整数 |
| int32 | int32_T | 有符号32位整数 |
| uint32 | uint32_T | 无符号32位整数 |
| boolean | boolean_T(即unsigned char) | 布尔类型,0/1 |
| 定点类型 | 见下方说明 | 映射为整数类型+缩放因子 |
你可能会问,为什么Simulink不直接用 int 而要用 int32_T?
嗯,这是为了跨编译器兼容。你在Windows上用VS编译,int是32位;换到某个16位MCU上,int可能就变成16位了。用 int32_T 这种自定义类型,可以保证在任何平台上都是32位。这是嵌入式代码生成的基本素养。
4.2.2 定点数据类型的映射——最容易踩坑的地方
定点数映射,是SIL测试里的大头。Simulink里你定义一个定点类型,比如 fixdt(1,16,10),表示有符号、16位字长、10位小数位。生成C代码时,它会被映射为一个 int16_T,但背后带了一堆缩放信息。
代码里长什么样?
/* 模型中的定点信号:fixdt(1,16,10) */
/* 实际存储类型:int16_T */
/* 缩放因子:2^(-10) */
int16_T my_signal; // 存储的是真实值 * 2^10
/* 使用时的转换 */
real_T real_value = (real_T)my_signal * 0.0009765625; // 2^(-10)
你看,代码里不会直接出现浮点运算,而是用移位和乘法来模拟小数。这就是定点数的本质——用整数算小数。
4.3 存储类的映射
存储类(Storage Class)决定了你的信号和参数在C代码里怎么存放、怎么访问。说白了,就是告诉代码生成器:这个信号是全局变量还是局部变量?是外部可访问的还是内部隐藏的?
4.3.1 常见的存储类及其映射
| 存储类 | C代码映射 | 典型用途 |
|---|---|---|
| Auto | 局部变量或结构体字段 | 内部信号,不对外暴露 |
| ExportedGlobal | 全局变量(extern声明) | 需要被外部代码读取的信号 |
| ImportedExtern | extern全局变量(由外部定义) | 从外部传入的参数 |
| ModelDefault | 模型默认的存储方式 | 大部分内部信号 |
| Volatile | volatile关键字修饰 | 硬件寄存器、中断共享变量 |
| BitField | 位域结构体 | 多个布尔信号打包存储 |
这里我重点说一下 ExportedGlobal 和 ImportedExtern 的区别。前者是模型自己定义变量,然后导出给外部用;后者是模型声明一个变量,但实际定义在外部代码里。SIL测试时,如果你用ImportedExtern,记得在测试环境里把对应的变量定义好,不然链接会报错。
4.3.2 存储类对SIL测试的影响
存储类的选择,直接影响SIL测试的接口设计。举个例子:
你有一个PID控制器的输出信号 pid_out。如果你把它设成 Auto,生成代码里它可能是一个局部变量,外部根本访问不到。那你在SIL测试里怎么观测这个信号?
嗯,只能通过模型的输出端口来间接观测。但如果你把它设成 ExportedGlobal,代码里就会生成一个全局变量:
/* 模型生成的全局变量 */
extern real_T pid_out; // 可以在测试代码中直接访问
这样你在SIL测试代码里就可以直接读取 pid_out,跟模型里的信号值做对比。方便很多,但也要注意全局变量的命名冲突问题。
4.4 知识结构总览
为了让你对整个映射关系有个全局认识,我画了一张结构图:
4.5 映射关系对SIL测试的实战意义
讲这么多理论,最后落到SIL测试上。映射关系到底怎么用?
我总结了三步:
- 读代码前先看映射表——拿到生成的代码,先别急着读逻辑,先找到模型里关键信号对应的C变量名和类型。这个在
model.h和model_types.h里都有定义。 - 定点信号重点关注缩放因子——SIL测试比对时,模型输出是浮点数,代码输出是定点整数。比对前一定要做缩放转换,不然误差会让你怀疑人生。
- 存储类决定测试接口——如果信号是Auto,测试代码里访问不到,只能通过模型输出端口比对。如果是ExportedGlobal,可以直接在测试代码里读写,方便做注入测试。
核心一句话: 模型与代码的映射关系,就是SIL测试的“翻译手册”。不懂映射,你测出来的结果对不上,都不知道是模型错了还是代码错了。