从C到Simulink: 使用 `simulation_stubs`(仿真存根)处理MBD中的硬件依赖

从C到Simulink: 使用 simulation_stubs(仿真存根)处理MBD中的硬件依赖

使用 simulation_stubs(仿真存根)是基于模型设计(MBD)中处理硬件依赖的标准解法。它的核心思想是:在 PC 上仿真时,给编译器看的是"假"的函数定义;在生成代码下载到板子时,才给编译器看"真"的硬件驱动。

下面我手把手教你如何实施这个方案。


核心策略:封装头文件

不要直接在 Simulink 的 C Caller 中包含 stm32f7xx_hal.hFreeRTOS.h。我们要创建一个中间层头文件 (比如叫 app_bsp.h),由它来决定是加载"仿真存根"还是"真实驱动"。

第一步:准备目录结构

在你的工程目录下创建一个专门存放仿真适配代码的文件夹,例如:

text 复制代码
MyProject/
├── simulink_stubs/          <-- 新建这个文件夹
│   └── simulation_stubs.h   <-- 存放假的驱动函数
├── app_code/
│   └── app_bsp.h            <-- 你的算法调用的统一接口头文件
└── Drivers/                 <-- 真实的 STM32 HAL 库 (原有)
    └── ...

第二步:编写仿真存根文件 (simulation_stubs.h)

打开 simulink_stubs/simulation_stubs.h,把你在 C Caller 中用到的 STM32 和 FreeRTOS 函数"伪造"出来。
注意:只写你用到的函数,不需要把整个 HAL 库都写一遍。

c 复制代码
/* simulink_stubs/simulation_stubs.h */
#ifndef SIMULATION_STUBS_H
#define SIMULATION_STUBS_H
#include <stdint.h>
#include <stdlib.h>
/* ------------------------------------------------------------------
 * 1. 解决芯片型号宏定义问题 (防止 stm32f7xx.h 报错)
 * ------------------------------------------------------------------ */
#ifndef STM32F767xx  // 假设你用的是 767
#define STM32F767xx
#endif
/* ------------------------------------------------------------------
 * 2. 定义基本类型
 * ------------------------------------------------------------------ */
typedef uint32_t TickType_t;
typedef void * QueueHandle_t;
typedef void * TaskHandle_t;
typedef struct { uint32_t dummy; } GPIO_TypeDef;
/* ------------------------------------------------------------------
 * 3. 伪造 STM32 HAL 函数 (示例)
 * ------------------------------------------------------------------ */
// 模拟 GPIO 写入
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
    // 仿真环境下什么都不做,或者打印调试信息
    // printf("GPIO Write: Pin=%d, State=%d\n", GPIO_Pin, PinState);
}
// 模拟延时
void HAL_Delay(uint32_t Delay)
{
    // 在 PC 上不需要精确延时,直接跳过,或者用 sleep(1)
}
/* ------------------------------------------------------------------
 * 4. 伪造 FreeRTOS 函数 (示例)
 * ------------------------------------------------------------------ */
// 模拟队列创建
QueueHandle_t xQueueCreate(uint32_t uxQueueLength, uint32_t uxItemSize)
{
    // 返回一个非空指针假装成功
    return (QueueHandle_t)malloc(100); 
}
// 模拟队列发送
BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait)
{
    // 假装发送成功
    return 1; // pdPASS
}
// 模拟任务延时
void vTaskDelay(TickType_t xTicksToDelay)
{
    // PC 仿真空转
}
#endif /* SIMULATION_STUBS_H */

第三步:编写中间封装头文件 (app_bsp.h)

这是最关键的一步。这个文件会根据当前的编译环境自动切换。

c 复制代码
/* app_code/app_bsp.h */
#ifndef APP_BSP_H
#define APP_BSP_H
/* ------------------------------------------------------------------
 * 关键魔法:判断当前是 PC 仿真 还是 生成代码
 * ------------------------------------------------------------------ */
#ifdef MATLAB_MEX_FILE 
    /* 
     * MATLAB_MEX_FILE 是 Simulink 在编译仿真目标时自动定义的宏。
     * 当这个宏存在时,说明我们在 PC 上跑,必须包含仿真存根。
     */
    #include "simulation_stubs.h"
#else
    /* 
     * 否则,说明是在生成代码给嵌入式编译器,包含真实的驱动。
     * 这里的路径请根据你实际项目修改。
     */
    #include "stm32f7xx_hal.h"
    #include "cmsis_os.h"  // FreeRTOS 的头文件
#endif
/* ------------------------------------------------------------------
 * 这里可以放一些你自己的通用宏定义
 * ------------------------------------------------------------------ */
#define APP_LED_PIN    GPIO_PIN_13
#endif /* APP_BSP_H */

现在你需要告诉 Simulink,在仿真时去哪里找这些文件。

  1. 打开模型配置 (Ctrl+E) -> 仿真目标
  2. 配置包含目录
    • 点击 "包含目录" 右侧的文件夹图标。
    • 添加 你的两个文件夹路径:
      • $(MATLAB_ROOT)/simulink_stubs (存根文件夹)
      • $(MATLAB_ROOT)/app_code (封装头文件夹)
    • 重要 :在仿真目标的包含路径中,不要 包含 Drivers/STM32F7xx_HAL_DriverFreeRTOS/Source/portable 等底层硬件路径!让它们在仿真时"消失"。
  3. C Caller Block 设置
    • 在你的 C Caller 模块中,头文件只需包含:

      c 复制代码
      #include "app_bsp.h"
    • 不要再写 #include "stm32f7xx_hal.h"


第五步:测试仿真

  1. 点击 "更新图表""运行"
  2. 此时,编译器会进入 app_bsp.h
  3. 因为是在仿真环境,MATLAB_MEX_FILE 被定义。
  4. 编译器会去读 simulation_stubs.h
  5. 它会看到假的 HAL_GPIO_WritePinSTM32F767xx 定义,编译通过,逻辑顺利运行。


第六步:如何生成代码下载到板子?

当你准备生成代码时:

  1. 在模型配置中,切换到 "代码生成" 选项卡。
  2. 找到 "自定义代码" -> "包含目录"(或者在"代码生成 -> 报表"下的相同设置)。
  3. 这里 你需要添加真实的驱动路径:
    • Drivers/STM32F7xx_HAL_Driver/Inc
    • Middlewares/Third_Party/FreeRTOS/Source/include
    • ... (所有你需要的真实路径)
  4. 预处理器定义
    • 这里不需要 加特殊的宏,因为 app_bsp.h 里的 #else 分支会自动处理。
    • 确保定义了 STM32F767xx(如果 HAL 需要的话)。
      工作流程总结:
      | 阶段 | MATLAB_MEX_FILE 宏 | 包含的头文件 | 使用的编译器 | 结果 |
      | :--- | :--- | :--- | :--- | :--- |
      | Simulink 仿真 | 已定义 (自动) | simulation_stubs.h | MinGW/MSVC | 编译成功 ,运行假函数,算法逻辑验证通过。 |
      | 生成代码 | 未定义 | stm32f7xx_hal.h 等 | ARM Compiler | 编译成功,链接真实硬件驱动,下载板子运行。 |

常见问题与进阶技巧

Q: 我用了 C Caller 自动生成函数原型,它报错说找不到 GPIO_TypeDef 的定义。
A: 在你的 simulation_stubs.h 中,必须把所有 C Caller 用到的结构体(如 GPIO_TypeDef, UART_HandleTypeDef 等)都声明一个空的"假"结构体,就像上面例子中写的:

c 复制代码
typedef struct { uint32_t dummy; } GPIO_TypeDef;

这样 C Caller 的类型检查就能通过了。
Q: 我的算法依赖于全局变量(比如 extern UART_HandleTypeDef huart1;)。
A:simulation_stubs.h 中也需要声明这个全局变量:

c 复制代码
extern UART_HandleTypeDef huart1;

但不要给它分配空间(即不要写 UART_HandleTypeDef huart1;),否则链接时可能会报重复定义。如果仿真时需要用到它的成员,你可以写一个假的初始化函数并在仿真模型的 Init 回调中调用它。

通过这种方式,你就彻底绕过了"PC 编译器无法编译 ARM 代码"的问题,实现了跨平台的算法复用。

相关推荐
或许好运来3 小时前
MATLAB 低级图形错误
matlab
代码游侠3 小时前
应用——基于C语言实现的简易Web服务器开发
运维·服务器·c语言·开发语言·笔记·测试工具
墨辰JC4 小时前
STM32架构基于调度器的非阻塞按键状态机设计
stm32·microsoft·架构·状态机·调度器
zhangx1234_4 小时前
C语言 题目2
c语言·开发语言
yong99904 小时前
超宽带系统链路 MATLAB 仿真
开发语言·算法·matlab
qq_401700415 小时前
C/C++中的signed char和unsigned char详解
c语言·c++·算法
创界工坊工作室5 小时前
DPJ-148 基于Arduino六自由度机械手设计(源代码+proteus仿真)
stm32·单片机·嵌入式硬件·51单片机·proteus
无限进步_5 小时前
【C语言】循环队列的两种实现:数组与链表的对比分析
c语言·开发语言·数据结构·c++·leetcode·链表·visual studio
金色光环5 小时前
裸机stm32移植双串口modbus从机(附源码)
stm32·单片机·嵌入式硬件