第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 本章知识体系

说了这么多,我画了一张图来总结本章的核心内容。这张图展示了数据类型、信号属性和总线对象之间的关系,以及它们如何影响最终的嵌入式代码。

数据类型与信号属性知识体系 数据类型 内置类型 定点数 int8/16/32 fixdt格式 uint8/16/32 精度与范围 single/double 溢出处理 信号属性 维度 采样时间 标量/向量 连续/离散 矩阵 多速率同步 类型传播 高级特性 别名类型 总线对象 物理量映射 结构体封装 类型安全 嵌套与复用 生成高效、可靠的嵌入式C代码

这张图把本章的知识点串起来了。你看,左边是数据类型,中间是信号属性,右边是高级特性。这三者最终汇聚到一点——生成高效、可靠的嵌入式C代码。

好了,关于数据类型与信号属性,我就讲这么多。记住一句话:选对类型,事半功倍;选错类型,调试到天亮。下一章咱们聊聊Simulink的建模规范,到时候我会分享一些让模型更"清爽"的小技巧。


公众号:蓝海资料掘金营,微信deep3321