从C到Simulink: 使用 simulation_stubs(仿真存根)处理MBD中的硬件依赖
使用 simulation_stubs(仿真存根)是基于模型设计(MBD)中处理硬件依赖的标准解法。它的核心思想是:在 PC 上仿真时,给编译器看的是"假"的函数定义;在生成代码下载到板子时,才给编译器看"真"的硬件驱动。
下面我手把手教你如何实施这个方案。
核心策略:封装头文件
不要直接在 Simulink 的 C Caller 中包含 stm32f7xx_hal.h 或 FreeRTOS.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 中进行配置
现在你需要告诉 Simulink,在仿真时去哪里找这些文件。
- 打开模型配置 (
Ctrl+E) -> 仿真目标。 - 配置包含目录 :
- 点击 "包含目录" 右侧的文件夹图标。
- 添加 你的两个文件夹路径:
$(MATLAB_ROOT)/simulink_stubs(存根文件夹)$(MATLAB_ROOT)/app_code(封装头文件夹)
- 重要 :在仿真目标的包含路径中,不要 包含
Drivers/STM32F7xx_HAL_Driver或FreeRTOS/Source/portable等底层硬件路径!让它们在仿真时"消失"。
- C Caller Block 设置 :
-
在你的 C Caller 模块中,头文件只需包含:
c#include "app_bsp.h" -
不要再写
#include "stm32f7xx_hal.h"。
-
第五步:测试仿真
- 点击 "更新图表" 或 "运行"。
- 此时,编译器会进入
app_bsp.h。 - 因为是在仿真环境,
MATLAB_MEX_FILE被定义。 - 编译器会去读
simulation_stubs.h。 - 它会看到假的
HAL_GPIO_WritePin和STM32F767xx定义,编译通过,逻辑顺利运行。
第六步:如何生成代码下载到板子?
当你准备生成代码时:
- 在模型配置中,切换到 "代码生成" 选项卡。
- 找到 "自定义代码" -> "包含目录"(或者在"代码生成 -> 报表"下的相同设置)。
- 这里 你需要添加真实的驱动路径:
Drivers/STM32F7xx_HAL_Driver/IncMiddlewares/Third_Party/FreeRTOS/Source/include... (所有你需要的真实路径)
- 预处理器定义 :
- 这里不需要 加特殊的宏,因为
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 代码"的问题,实现了跨平台的算法复用。