在实际的工程应用和学术研究中,仅仅依靠Simulink自身的功能和模块往往难以满足需求。在这种情况下集成外部的功能、算法、模型等资源是最优的选择。

其中,基于C/C++语言的各类资源最为丰富,同时也拥有最优越的性能,还与底层同样基于C/C++的Simulink有着良好的兼容性,因此广泛应用在以下场景中:

  1. 重用既有算法/库
    许多成熟项目中,已有用 C/C++ 写好的算法库、设备驱动或数学工具包,直接在 Simulink 中调用,可避免重复造轮子。
  2. 性能优化
    关键计算模块(如大规模矩阵运算、信号滤波、FFT 等)用手写 C 实现通常比纯 Simulink block 或 MATLAB Function 更高效,尤其在实时仿真或部署到硬件时。
  3. 嵌入式目标代码生成
    Simulink Coder 生成的代码如果要在目标微控制器/DSP 上运行,经常需要与厂商提供的外部库(HAL、RTOS 接口、外设驱动)链接。
  4. 接口真实硬件/仿真器
    在硬件在环(HIL)测试中,经常要调用专有的实时驱动或仿真API,这些 API 多以 C 语言形式提供。
  5. 混合开发流程
    工程团队中,算法工程师用 Simulink 快速建模、验证思路;软件工程师用 C 代码实现、调优。二者结合,可以大幅缩短开发周期、降低验证成本。

因此,Simulink 提供了一系列工具,可以将来自不同来源的现有组件和代码集成到 Simulink 中,以创建大规模模型。

Simulink 集成外部C/C++代码的方法主要有以下几种:

  1. C Caller
  2. C Function
  3. S-Function
  4. MATLAB Function + coder.ceval
  5. Stateflow + coder.ceval

1. C Caller

Simulink从R2018b版本开始, 增加 “C Caller” 模块,用于在 Simulink 模型中直接调用 C/C++ 函数,旨在简化与 Simulink 模型集成的过程,特别是对于不需要 S-function 复杂性的场景。

优点:

  • 易用性:图形化配置,自动解析头文件,无需手动编写 S-Function。
  • 集成性:与 Simulink 的仿真和代码生成工具无缝集成。
  • 灵活性:支持标量、向量、矩阵和结构体等复杂数据类型。
  • 代码生成支持:通过 Simulink Coder 自动将 C 代码嵌入目标系统,适合嵌入式应用。
  • 验证支持:与 Simulink Coverage、Test 和 Design Verifier 兼容,便于测试和验证。

缺点:

  • 功能有限:仅适合调用单一或少量 C 函数,无法实现复杂的动态系统逻辑(如需要状态更新的算法)。
  • 数据类型限制:需要确保 C 函数的输入输出与 Simulink 的数据类型兼容,可能需要额外的数据转换。
  • 调试复杂性:如果 C 代码出现错误,调试可能需要检查生成的 C 代码或使用外部调试工具。

适用场景:

  • 简单函数调用:调用单一的 C 函数,如数学运算、滤波器或控制算法。
  • 代码重用:集成现有的 C 代码库,避免重新开发。
  • 嵌入式系统开发:在生成嵌入式代码时,确保特定 C 实现被包含在目标系统中。
  • 快速原型设计:快速验证 C 代码在 Simulink 环境中的行为。
  • 测试与验证:与 Simulink Test 或 Simulink Design Verifier 配合,验证外部 C 代码的功能。

配置流程:

  1. 准备 C 代码

编写或准备 C 源文件(.c)和头文件(.h),其中定义了需要调用的函数,这里以一个简单的求和函数myFunction为例:

// myFunction.h
#ifndef MYFUNCTION_H
#define MYFUNCTION_H
double myFunction(double input1, double input2);
#endif

// myFunction.c
#include "myFunction.h"
double myFunction(double input1, double input2) {
    return input1 + input2; // 示例:返回两个输入之和
}
  1. 配置 Simulink 模型

打开Configuration Parameters (配置参数)> Simulation Target(仿真目标)

在 Simulation Target 面板中:

在 Source files (源文件)中添加 C 源文件(如 myFunction.c)。

添加代码源文件

在 Header files (头文件)字段中添加头文件(如 myFunction.h),也可以使用“基于源文件自动填充”功能。

添加代码头文件

指定包含路径(Include directories)和其他编译选项(如有需要)。

  1. 添加并配置C caller

打开 Simulink 库浏览器,导航到 Simulink > User-Defined Functions,拖放 C Caller 块到模型中。

点击标记1位置的刷新,Simulink 会自动解析头文件中定义的函数。接着从下拉菜单中选择要调用的函数(如 myFunction)

选择函数

端口设定:

  • 确认输入和输出端口与 C 函数的签名匹配。例如,myFunction 有两个 double 输入和一个 double 输出,Simulink 会自动生成两个输入端口和一个输出端口。

  • 如果函数涉及复杂数据类型(如结构体),需要在头文件中定义,并在 Simulation Target 中正确映射。

  1. 仿真和验证

运行 Simulink 模型,验证 C Caller 块是否正确调用 C 函数并返回预期结果。

C caller仿真结果

如果需要生成嵌入式代码,使用 Simulink Coder 配置目标硬件并生成代码,C Caller 块会自动将 C 代码嵌入生成的目标代码中。

2. C Function

C Function 是 Simulink 提供的一个 可视化编程与 C 语言结合的接口模块,首次引入于 R2020a版本,允许你直接在 Simulink 模型中编写、编译并运行 自定义 C 代码,而不需要额外生成 MEX 文件或编写 S-Function 模板。

C Function 模块的底层是基于 Simulink S-Function API 的封装,它帮你自动生成大部分 S-Function 框架,只保留给你编写的核心算法部分。

优点:

  • 灵活性高:支持数据预后处理、多函数调用和持久数据管理,适合复杂集成。
  • 自定义强:允许为仿真和代码生成指定不同代码,便于优化。
  • 内存管理:支持分配/释放内存和初始化/终止代码。
  • 集成性好:与 Simulink Coder 无缝配合,支持嵌入式部署。
  • 代码重用:轻松集成现有 C 库,减少重复开发。

缺点:

  • 配置复杂:相比 C Caller 块,需要更多手动配置和代码编写,学习曲线较陡。
  • 功能限制:不适合动态系统(如连续状态),需转向 S-Function。
  • 调试难度:自定义代码错误可能导致编译问题,需要检查 Diagnostic Viewer。
  • 版本依赖:R2024a 前后配置略有差异,可能影响兼容性。

配置流程:

  1. 准备 C 代码

第一步同样是准备要集成的代码,这里使用与上文C caller案例中相同的代码。

  1. 配置 Simulink 模型

这里与上文一致,即在Configuration Parameters (配置参数)中配置代码文件。

  1. 添加并配置C Function

打开 Simulink 库浏览器,导航到 Simulink > User-Defined Functions,拖放 C Function 块到模型中。

配置C Function

如图所示:

  1. 在 C Function 模块参数对话框的 Simulation 选项卡下的输出一栏,编写模块在仿真期间执行的代码。

  2. 使用“端口和参数”表来定义块中代码中使用的符号。

  3. 仿真和验证

运行 Simulink 模型,验证 C Function块是否正确调用 C 函数并返回预期结果。

C Function仿真结果

C Function块 与 C Caller 块的主要区别:

  • C Caller 块:专用于调用单个 C 函数,配置简单,适合简单函数调用。自动解析头文件,无需手动编写预处理代码。
  • C Function 块:用于调用可能需要修改的外部 C 算法,提供更多自定义选项,如数据处理、初始化和多函数调用。适用于复杂场景,但配置更复杂。
  • 其他区别:对于动态系统(如连续状态或状态变化),推荐使用 S-Function 块而非 C Function 块。C Function 块更注重数据管理和自定义代码,而 C Caller 块强调快速集成。

3. S-Function

S-Function(System Function) 是 Simulink 的可编程模块接口,它允许用户用 C、C++、MATLAB、Fortran 等语言,扩展 Simulink 的功能,实现自定义模块。

S-Function 的核心是通过回调函数(callback methods)与 Simulink 交互,这些函数在仿真过程中被调用,以更新状态、计算输出等。

优点:

  • 功能最强:支持动态端口、事件触发、复杂状态管理。
  • 高性能:C MEX S-Function 的执行速度接近纯 C 代码。
  • 硬件集成:方便调用底层驱动(SPI、CAN、传感器等)。
  • 跨平台:一次实现,可在不同平台(Simulink 仿真 / Embedded Coder 生成代码)运行。
  • 灵活数据接口:支持任意维度、数据类型、采样时间。

缺点:

  • 开发成本高:必须熟悉 Simulink S-Function API。
  • 调试难度大:需要用 mex 编译,调试要依赖外部调试器(如 gdb、Visual Studio)。
  • 维护成本高:比 MATLAB Function/C Function 代码更难维护。
  • 新手学习曲线陡:API 较多,生命周期管理复杂。

适用场景:

  • 大型已有 C/C++ 库的接入(如信号处理库、控制算法库)
  • 硬件驱动(传感器读写、总线通信)
  • 特殊仿真逻辑(自定义事件调度、非标信号类型)
  • 高性能计算(替换 MATLAB Function 模块,减少解释器开销)

配置流程:

  1. 编写 S-Function 文件

这里依旧以上文适用的代码为例,这里需要新建一个 mySfunc.c 文件,用来封装 myFunction

#define S_FUNCTION_NAME  mySfunc
#define S_FUNCTION_LEVEL 2

#include "simstruc.h"
#include "myFunction.h" // 引入你的函数声明

/* 初始化端口数量和属性 */
static void mdlInitializeSizes(SimStruct *S)
{
    ssSetNumSFcnParams(S, 0); // 无参数

    /* 输入端口 1 */
    if (!ssSetNumInputPorts(S, 2)) return;
    ssSetInputPortWidth(S, 0, 1); // 第1个输入端口:标量
    ssSetInputPortWidth(S, 1, 1); // 第2个输入端口:标量
    ssSetInputPortDirectFeedThrough(S, 0, 1);
    ssSetInputPortDirectFeedThrough(S, 1, 1);

    /* 输出端口 1 */
    if (!ssSetNumOutputPorts(S, 1)) return;
    ssSetOutputPortWidth(S, 0, 1); // 输出是标量

    ssSetNumSampleTimes(S, 1);
}

/* 设置采样时间 */
static void mdlInitializeSampleTimes(SimStruct *S)
{
    ssSetSampleTime(S, 0, INHERITED_SAMPLE_TIME); // 继承采样时间
    ssSetOffsetTime(S, 0, 0.0);
}

/* 核心计算 */
static void mdlOutputs(SimStruct *S, int_T tid)
{
    InputRealPtrsType u0 = ssGetInputPortRealSignalPtrs(S, 0);
    InputRealPtrsType u1 = ssGetInputPortRealSignalPtrs(S, 1);
    real_T *y = ssGetOutputPortRealSignal(S, 0);

    /* 调用外部函数 */
    *y = myFunction(*u0[0], *u1[0]);
}

/* 终止处理(此例无资源释放) */
static void mdlTerminate(SimStruct *S) {}

/* 选择正确的编译接口 */
#ifdef MATLAB_MEX_FILE
#include "simulink.c"
#else
#include "cg_sfun.h"
#endif
  1. 编译MEX文件

在 MATLAB 命令行执行(确保 .c.h 文件在当前工作目录或路径中):

mex mySfunc.c myFunction.c

编译sfunction

这样会生成:

mySfunc.mexw64

此文件就是Simulink可直接调用的模块实现。

  1. 在 Simulink 中调用

打开 Simulink Library Browser,添加 S-Function 模块(位于 User-Defined Functions → S-Function),双击打开模块,在 S-function name 中填 mySfunc,此时S function模块会根据代码创建相应输出输出端口。

sfunction仿真结果

运行 Simulink 模型,验证 S function 块是否正确实现 C 函数的功能并返回预期结果。

S-Function是一种非常强大的方法,其拥有多种创建方式与接口等级,例如:Level-1 S-Function、Level-2 S-Function、C MEX S-Function、MATLAB S-Function等,本文仅做简单介绍,后续会推出专题进一步介绍。

4. MATLAB Function + coder.ceval

MATLAB Function 模块是 Simulink 提供的一个可以直接在模型中用 MATLAB 语言编写算法的模块。
它的作用类似于 MATLAB 脚本,但有以下特性:

  • 可以生成高性能的 C 代码(需要 Simulink Coder)。
  • 可以在 MATLAB 代码里调用外部 C 函数(通过 coder.ceval)。
  • 支持输入输出信号映射到端口,直接参与仿真。

coder.ceval 是 MATLAB Coder 提供的一个接口,用于在 MATLAB 代码生成过程中调用外部 C/C++ 函数。

coder.ceval('函数名', 参数1, 参数2, ...)
  • '函数名' 必须是字符串,与外部 C 文件中的函数名一致。
  • 参数必须是 C 类型可接受的变量(标量、指针、数组)。
  • 如果 C 函数有返回值,需要用 coder.ceval 的返回值接收。
  • 只能在 代码生成 过程中使用(普通 MATLAB 运行时需要提供 MEX 或者模拟实现)。

优点:

  • 比 S-Function 开发简单,代码嵌入在 Simulink 模块中。
  • 调用外部 C 代码无需自己写 S-Function 框架。
  • 可直接生成嵌入式 C 代码。
  • 对小型外部 C 库的集成非常方便。

缺点:

  • 对动态端口、复杂状态管理支持不如 S-Function。
  • 必须有 Simulink Coder(或 MATLAB Coder)才能调用外部 C 代码。
  • 外部 C 文件路径和头文件需要额外配置,否则生成代码会失败。

适用场景:

需要使用到matlab function模块的场景。

配置方法:

  1. 准备C代码

这里依旧使用上文的myFunction函数代码作为示例,确保代码文件位于工作路径下。

与C caller和C Function一致,在Configuration Parameters (配置参数)中添加代码文件。

  1. 配置MATLAB Function

添加MATLAB Function模块,使用coder.ceval调用myFunction

function y = callMyFunction(u1, u2)
%#codegen  % 启用代码生成

% 声明外部函数
coder.cinclude('myFunction.h');

% 初始化输出变量 (假设返回 double 标量)
y = 0;  % double 标量

% 调用外部函数
y = coder.ceval('myFunction', u1, u2);
end
  1. 仿真和验证

运行仿真,验证MATLAB Function + coder.ceval能否正确实现 C 函数的功能并返回预期结果。

编写matlab function

matlab function仿真结果

5.Stateflow + coder.ceval

Stateflow 是 MATLAB/Simulink 的一个状态机与流程图建模工具,用于描述离散事件驱动逻辑、模式切换、控制流程等。

Stateflow 中使用 coder.ceval,可以直接调用外部 C/C++ 函数,让状态机逻辑与已有的底层算法库无缝集成。

优势:

  • 结构清晰:状态机逻辑 + 底层算法分离。
  • 性能高效:C 代码直接生成嵌入式目标代码,无额外解释开销。
  • 可维护性好:C 代码可单独调试、单元测试,Stateflow 只管理状态转移。
  • 硬件可移植:同一状态机可切换不同平台的底层驱动实现。

缺点

  • 类型与大小必须已知coder.ceval 调用时,传入和返回的变量必须在 Stateflow 中预定义类型(可用 Data 定义)。
  • 只能在代码生成模式下使用:必须启用 Embedded Coder / Simulink Coder。
  • C 函数声明必须可见:需要在 #include 语句中声明函数原型,或者通过 Custom Code 设置包含头文件。
  • 内存管理:不能直接在 C 函数里用 malloc 返回指针给 Stateflow,需在 Stateflow 侧分配内存并传指针。

配置方法:

  1. 准备 C 代码

与上文介绍的C caller和C Function一致,在Configuration Parameters (配置参数)中添加代码文件。

  1. 配置 Stateflow

在simulink中添加chart,创建状态。

通过在状态的entry 动作transition 动作中使用coder.ceval调用C函数:

y = 0;
y = coder.ceval('myFunction', a, b);
  1. 仿真验证

运行仿真,验证Stateflow + coder.ceval能否正确实现 C 函数的功能并返回预期结果。

Stateflow仿真结果

总结

本文介绍了Simulink 集成外部C/C++代码最常见的5种方法,这里将各类方法的特点总结如下:

方法 典型场景 优点 缺点 开发难度
C Caller 已有外部 C 函数(.c/.h),直接调用并在模型中使用 图形化直接映射 C 函数输入/输出;无需编写接口代码 只能调用已存在的函数;逻辑必须在外部实现,无法在模块内写逻辑
C Function 在 Simulink 模块内直接写 C 代码(算法、控制逻辑等) 代码和模型绑定;可读性高;无需额外文件 不适合大型代码;可移植性较差
S-Function 高度自定义接口、非标数据类型、复杂状态机或自定义求解器 最灵活;可访问底层 API;支持多语言 编写复杂,需实现多回调函数
MATLAB Function + coder.ceval 已有 MATLAB 算法,需要在其中调用 C 函数 结合 MATLAB 语法和 C 代码;易调试 C 调用嵌在 MATLAB 中,生成代码依赖 coder 规则
Stateflow + coder.ceval 状态机逻辑中需要调用外部 C 函数 将外部 C 调用直接嵌入状态转移逻辑;易维护状态逻辑 对非状态机逻辑冗余;调试稍复杂

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐