第1章 数据类型与信号属性
大家好,我是老李。做嵌入式代码生成这些年,我见过太多工程师在数据类型和信号属性上栽跟头。说白了,这部分就是Simulink建模的"地基"——地基没打好,后面跑再快也是白搭。
今天咱们就聊聊这个基础但极其重要的主题。我会结合自己踩过的坑,把Simulink里的数据类型、定点数、浮点数、信号属性这些概念掰开揉碎了讲清楚。
核心观点:数据类型和信号属性的选择,直接决定了生成代码的ROM/RAM占用、执行效率以及数值精度。这不是小事。
1.1 Simulink内置数据类型
Simulink支持的数据类型,说白了就两大类:整数类型和浮点类型。我刚开始用的时候也觉得种类太多,其实常用的就那么几个。
| 类型 | 位宽 | 范围 | 典型用途 |
|---|---|---|---|
| int8 | 8位 | -128 ~ 127 | 小范围计数、状态机 |
| uint8 | 8位 | 0 ~ 255 | 字节数据、索引 |
| int16 | 16位 | -32768 ~ 32767 | 中等精度传感器数据 |
| uint16 | 16位 | 0 ~ 65535 | ADC采样值 |
| int32 | 32位 | -2^31 ~ 2^31-1 | 通用整数运算 |
| single | 32位 | ±1.18e-38 ~ ±3.4e38 | 浮点运算(MCU常用) |
| double | 64位 | ±2.23e-308 ~ ±1.79e308 | 高精度仿真(不推荐用于嵌入式) |
我个人习惯是:能用整数绝不用浮点。为什么?你想想看,在C2000或者STM32这类MCU上,一次浮点乘法可能要几十个时钟周期,而整数乘法只要一个周期。这个差距在实时控制系统中是致命的。
小技巧:在Model Explorer里可以批量修改信号的数据类型。我一般会先建一个数据字典,把常用的类型别名定义好,后面直接引用就行。
1.2 定点数与浮点数
说到定点数,很多新手会觉得头疼。其实没那么复杂——定点数就是带小数点的整数。
Simulink里定点数的表示方式是 fixdt(1,16,8) 这种格式。我来解释一下:
- 第一个参数:符号位(1表示有符号,0表示无符号)
- 第二个参数:总位宽
- 第三个参数:小数位数
举个例子,fixdt(1,16,8) 表示16位有符号定点数,其中8位是小数位。它的精度就是 2^(-8) ≈ 0.0039。我在做电机控制项目时,电流环的PI控制器就用的这种格式,效果很好。
注意:定点数运算要特别小心溢出!我曾经在一个项目中,因为没注意两个16位定点数相乘的结果范围,导致控制器在极限工况下直接飞车。从那以后,我每次做定点数设计都会先做一次范围分析。
浮点数呢?Simulink里主要用single和double。对于嵌入式目标,我强烈建议用single。double虽然精度高,但占用的RAM和ROM都是single的两倍,运算速度也慢不少。
什么时候用浮点?我个人经验是:
- 算法原型验证阶段——用浮点方便调试
- 数值范围变化大的场景——比如GPS坐标计算
- 对精度要求极高但性能要求不高的场景
1.3 信号属性
信号属性这块,我把它拆成三个维度来讲:维度、采样时间、数据类型。
1.3.1 维度
维度就是信号的"形状"。标量是0维,向量是1维,矩阵是2维。Simulink里默认的信号是标量,但实际项目中很少只用标量。
我记得有一次做电池管理系统,需要同时处理12节电池的电压。如果用12个单独的标量信号,模型会乱成一锅粥。后来我用了一个1×12的向量,配合Selector模块,模型瞬间清爽了。
维度设置要注意:
- 在Signal Attributes库中用
Width模块可以查看信号宽度 - 用
Reshape模块可以改变维度 - 矩阵运算建议用
Matrix Concatenate而不是手动拼接
1.3.2 采样时间
采样时间,说白了就是你的模型多久执行一次。Simulink里采样时间分为:
- 连续时间(0):用于模拟连续系统,实际代码生成时会离散化
- 离散时间(>0):固定步长执行,比如0.001表示1ms执行一次
- 继承(-1):从上游信号继承采样时间
我建议:嵌入式代码生成一定要用离散时间。连续时间在仿真时看着舒服,但生成代码后你会发现它变成了一个巨大的状态方程,执行效率很低。
经验之谈:多速率系统要特别注意。我曾经在一个项目中,一个10ms的任务和一个1ms的任务共享了同一个全局变量,结果出现了数据竞争。后来我用Rate Transition模块做了缓冲,问题才解决。
1.3.3 数据类型
数据类型这块前面已经讲了很多,这里补充一点:数据类型的传播。
Simulink有个特性,信号的数据类型会沿着连线传播。比如你给一个Constant模块设置了uint8,它连出去的Gain模块默认也会用uint8。这个特性很方便,但也很容易出问题。
我遇到过最坑的一次:一个加法运算,两个输入都是uint8,结果溢出后变成了0,导致后续的除法直接除零崩溃。从那以后,我养成了一个习惯——在关键节点手动设置数据类型,用Data Type Conversion模块显式转换。
1.4 别名类型与总线对象
这部分是Simulink的高级特性,但用好了能极大提升模型的可维护性。
1.4.1 别名类型
别名类型,就是给已有的数据类型起个新名字。比如:
% 在MATLAB命令行定义
myVoltage = Simulink.AliasType;
myVoltage.BaseType = 'uint16';
myVoltage.HeaderFile = 'my_types.h';
这样在模型里就可以直接用 myVoltage 这个类型了。生成代码时,会自动包含 my_types.h 头文件。
我个人习惯把项目中所有的物理量都定义成别名类型。比如电压用 Voltage_t,电流用 Current_t,温度用 Temp_t。这样做的好处是:
- 代码可读性极强——看到类型就知道是什么物理量
- 修改方便——如果某天需要把电压从uint16改成uint32,只需要改一处定义
- 类型安全——编译器能帮你检查类型匹配
1.4.2 总线对象
总线对象,说白了就是结构体。在Simulink里用Bus Creator和Bus Selector来操作。
定义总线对象的方法:
% 创建总线对象
elems(1) = Simulink.BusElement;
elems(1).Name = 'voltage';
elems(1).DataType = 'uint16';
elems(2) = Simulink.BusElement;
elems(2).Name = 'current';
elems(2).DataType = 'int16';
myBus = Simulink.Bus;
myBus.Elements = elems;
生成代码后,你会得到一个结构体:
typedef struct {
uint16_t voltage;
int16_t current;
} myBus;
强烈建议:在大型项目中,一定要用总线对象来组织信号。我见过一个项目,有200多个信号在模型里飞来飞去,调试时根本分不清谁是谁。用总线对象后,信号按功能模块分组,清晰多了。
总线对象还有几个实用技巧:
- 可以嵌套——总线里套总线,实现层次化数据结构
- 可以设置初始值——在BusElement里设置InitialValue
- 可以生成代码时自动包含头文件——在Bus对象里设置HeaderFile
避坑指南:我曾经在总线对象里混用了不同采样时间的信号,结果生成代码时出现了奇怪的时序问题。记住:同一个总线里的信号,采样时间必须一致。如果确实需要混合,用Rate Transition模块做隔离。
1.5 本章知识体系
说了这么多,我画了一张图来总结本章的核心内容。这张图展示了数据类型、信号属性和总线对象之间的关系,以及它们如何影响最终的嵌入式代码。
这张图把本章的知识点串起来了。你看,左边是数据类型,中间是信号属性,右边是高级特性。这三者最终汇聚到一点——生成高效、可靠的嵌入式C代码。
好了,关于数据类型与信号属性,我就讲这么多。记住一句话:选对类型,事半功倍;选错类型,调试到天亮。下一章咱们聊聊Simulink的建模规范,到时候我会分享一些让模型更"清爽"的小技巧。
公众号:蓝海资料掘金营,微信deep3321