Simulink卡尔曼滤波器嵌入式C代码生成实战
2026/9/15 17:02:53 网站建设 项目流程

简介:本资源是一套基于MATLAB/Simulink实现Kalman滤波器建模与嵌入式代码生成的完整工程实践包,面向自动控制、信号处理及嵌入式开发领域的工程师与高年级本科生。资源聚焦于Kalman滤波算法在Simulink中的可视化建模、仿真验证及C代码自动生成全流程,特别适用于传感器数据融合、状态估计与实时系统部署等典型应用场景。压缩包共60个文件,含2个核心Simulink模型(kalman_generation.slx与can.slx)、1个可独立运行的exe可执行程序、8个C/H源码与头文件、18个HTML格式代码生成报告(含接口说明、跟踪信息、类型定义等),以及编译配置脚本、构建日志和Embedded Coder生成的ERT实时工作台文件,整体仅221KB,轻量但结构完备。目前已有308人学习下载,读者可直接复用模型、调试生成代码、理解ERT代码生成机制,并通过HTML报告快速掌握接口逻辑与数据流设计。

1. 用 Simulink 实现卡尔曼滤波器并生成可部署 C 代码:不是调参完就结束,而是让滤波逻辑真正跑在嵌入式设备上

你手头有一份matlab.rar压缩包,解压后看到一堆.slx模型文件、.m脚本和kalman相关命名——这很典型:一个基于 Simulink 的卡尔曼滤波器设计项目,目标明确:不只仿真收敛,更要导出符合 AUTOSAR 或裸机环境要求的 C 代码。很多工程师卡在“Simulink 里能跑通”和“烧进 STM32/F28379D 后数据发散”之间,根本原因不是模型逻辑错,而是状态初始化、协方差矩阵量化、浮点精度截断、采样时间对齐等细节在代码生成阶段被默认策略掩盖。本文聚焦真实工程链路:从 Simulink 模型搭建开始,严格约束浮点行为、显式声明数据类型、配置 RTW(Real-Time Workshop)参数,最终生成带完整初始化函数、无动态内存分配、可直接集成进 FreeRTOS 或裸机主循环的 ANSI C 代码。适合已掌握 Kalman 基本推导、能搭建简单 Simulink 模型,但尚未打通“仿真→代码→硬件”闭环的控制/信号处理工程师。

2. 在 Simulink 中构建可代码生成的卡尔曼滤波器模型:避开自动推导陷阱,手动实现预测-更新双步结构

2.1 为什么不能直接用 Control System Toolbox 的kalman()函数生成代码?

Simulink 提供Kalman Filter模块(位于Control System Toolbox > Estimators),但它本质是封装了kalman()的 MATLAB Function 模块,内部调用codegen不支持的动态数组操作(如size(P,1)自适应维度)。实测中,当系统阶数变化或协方差矩阵P需在线调整时,该模块生成的 C 代码会插入emxArray_real_T动态内存管理结构,导致裸机平台编译失败或运行时崩溃。可靠做法是手动搭建预测(Predict)与更新(Correct)两个子系统,完全控制矩阵运算路径。

2.1.1 手动实现卡尔曼预测步:用 Matrix Multiply + Sum 构建x_hat_minus = A*x_hat_plus + B*u

打开新 Simulink 模型,添加以下基础模块:

  • Constant模块(设置A矩阵,例如 2×2 状态转移矩阵[0.99, 0.01; 0, 0.98]
  • Inport模块(标记为x_hat_plus,数据类型设为single,维度[2,1]
  • Inport模块(标记为u,数据类型single,维度[1,1]
  • Matrix Multiply模块(配置MultiplicationMatrixOutput data type设为Inherit: Inherit via internal rule→ 后续强制改为single
  • Sum模块(+端口接A*x_hat_plus+端口接B*u

注意B矩阵需单独用Constant定义,不可写成A*x + B*u单一表达式。Simulink 对复合矩阵表达式的代码生成优化不可控,易引入中间临时变量导致栈溢出。

2.1.2 手动实现卡尔曼更新步:用 Gain + Matrix Multiply + Subtraction 实现x_hat_plus = x_hat_minus + K*(y - C*x_hat_minus)

继续添加:

  • Inport模块(标记为y,数据类型single,维度[1,1]
  • Constant模块(C矩阵,例如[1, 0],数据类型single
  • Matrix Multiply模块(计算C*x_hat_minus,输出类型single
  • Subtract模块(y - C*x_hat_minus,即残差z_tilde
  • Constant模块(K增益矩阵,例如[0.5; 0.1],必须预计算好并固化,不可在线迭代)
  • Matrix Multiply模块(K * z_tilde,输出single
  • Sum模块(x_hat_minus + K*z_tilde

关键约束:所有Constant模块右键 →Block ParametersSignal Attributes→ 勾选Treat as parameter,并设置Parameter data typesingle。这确保A,B,C,K在生成代码时作为const float数组写入.c文件,而非运行时加载。

2.2 数据类型与采样时间的硬性绑定:避免仿真与代码生成结果偏差

2.2.1 全局数据类型强制为 single,禁用 double 推导

在模型配置参数(Ctrl+E)→All Parameters→ 搜索Data Type→ 设置:

  • Default parameter behavior:Inlined
  • Signal storage reuse:Off(防止复用导致状态覆盖)
  • Hardware ImplementationDevice detailsFloating-point precision:Single

提示:若模型中存在Discrete-Time Integrator等模块,其Initial condition必须显式设为single类型数值(如single(0)),否则默认double会触发类型转换警告,且生成代码中插入冗余cast函数。

2.2.2 固定步长离散化:采样时间必须与目标硬件周期严格一致

Configuration ParametersSolver→ 设置:

  • Type:Fixed-step
  • Solver:discrete (no continuous states)
  • Fixed-step size: 输入目标硬件采样周期(如0.01表示 10ms)

绝对禁止使用auto步长。Simulink 在 auto 模式下会根据模块最小步长自动选择,而代码生成器将此步长写死为0.001(1ms),导致部署到 10ms 定时器的 MCU 上时,滤波器以 10 倍速更新,状态爆炸。

2.3 模型接口标准化:定义清晰的输入/输出端口与数据流

2.3.1 使用 Bus Creator 统一封装输入总线

创建Bus Object(在 Model Explorer 中新建Simulink.Bus,命名为KalmanInputBus),添加信号:

  • y:single,[1,1]
  • u:single,[1,1]
  • timestamp_ms:uint32(用于调试时间戳对齐)

将三个Inport连接到Bus Creator,输出连接至滤波器主系统。此举使生成的 C 函数签名变为:

void kalman_filter_step(const KalmanInputBus *input, KalmanOutputBus *output);

而非混乱的多个独立参数,极大提升 C 层集成可读性。

2.3.2 输出总线包含状态与诊断量

定义KalmanOutputBus包含:

  • x_hat:single,[2,1](估计状态)
  • P_diag:single,[2,1](协方差对角线,用于监控发散)
  • execution_time_us:uint32(记录单次执行耗时,验证实时性)

注意P_diag不是必须输出,但强烈建议加入。实际部署中,若P(1,1)持续增大超阈值(如>1e6),说明滤波器发散,可触发安全降级逻辑。该信号在 Simulink 中由Extract Diagonal模块获取P矩阵对角线。

模块名称功能关键参数设置
Matrix Multiply矩阵乘法Multiplication:Matrix;Output data type:single
Gain标量/向量增益Gain value: 输入K向量;Parameter data type:single
Discrete-Time Integrator状态积分(若需连续模型离散化)Initial condition:single(0);Sample time:0.01
Bus Creator输入总线封装Bus object:KalmanInputBus

3. 配置 Embedded Coder 生成符合嵌入式约束的 ANSI C 代码:绕过默认模板,直击内存与实时性痛点

3.1 启用 Embedded Coder 并选择目标硬件抽象层

在 Simulink 工具栏点击AppsEmbedded CoderEmbedded Coder App。首次启动会引导配置:

  • Target hardware board: 选择Generic Real-Time Target(通用实时目标)
  • System target file:ert.tlc(Embedded Real-Time system target file)
  • Language:C

提示:不要选择grt.tlc(Generic Real-Time),它生成的代码包含大量printfmalloc调用,无法在无 libc 的裸机环境运行。ert.tlc是专为资源受限嵌入式设备设计的模板。

3.2 关键代码生成参数设置:禁用动态内存、固定栈大小、移除浮点异常检查

打开Configuration ParametersCode GenerationToolchain

  • Toolchain:Automatically locate tools(自动检测 GCC/ARMCC)
  • Build folder: 设置为code_gen/kalman(隔离生成目录)

进入Code GenerationInterfaceAdvanced parameters

  • Support non-inlined S-functions:None(禁用 S-function,避免不可控代码)
  • Support variable-size signals:None(所有信号尺寸必须静态确定)
  • Dynamic memory allocation:None(强制所有数组静态分配)

进入Code GenerationOptimizationMemory

  • Stack usage:On(启用栈用量分析)
  • Stack size: 输入4096(单位字节,根据 MCU RAM 裁剪)

进入Code GenerationReport→ 勾选Generate code only(跳过编译,先验证代码结构)。

3.3 生成代码并验证结构:检查是否满足裸机部署三原则

执行Build Model(或Ctrl+B),生成代码后检查code_gen/kalman/ert_main.ccode_gen/kalman/kalman_filter.c

3.3.1 原则一:无malloc/free、无printf、无全局浮点异常处理

打开kalman_filter.c,搜索malloc—— 应无结果。搜索printf—— 应仅出现在注释中。关键函数kalman_filter_step()内部应只有:

  • static数组声明(如static real32_T A[4] = { ... };
  • 纯算术运算(+,-,*,/
  • 直接内存赋值(output->x_hat[0] = x_hat_plus[0];
3.3.2 原则二:所有常量固化为const,无运行时初始化开销

kalman_filter_data.c中,确认A,B,C,K等矩阵均声明为:

const real32_T kalman_filter_A[4] = { 0.99F, 0.01F, 0.0F, 0.98F }; const real32_T kalman_filter_K[2] = { 0.5F, 0.1F };

F后缀表示float字面量,避免编译器隐式转为double

3.3.3 原则三:函数接口简洁,支持中断服务程序(ISR)调用

kalman_filter.h中函数声明应为:

#ifndef KALMAN_FILTER_H #define KALMAN_FILTER_H #include "rtwtypes.h" #include "kalman_filter_types.h" extern void kalman_filter_step(const KalmanInputBus *input, KalmanOutputBus *output); #endif

#include <stdio.h>,无extern全局变量依赖,可直接在main()TIM6_IRQHandler()中调用。

3.4 生成代码的轻量级编译验证:用 ARM GCC 检查符号与内存占用

在终端执行(假设已安装 GNU Arm Embedded Toolchain):

arm-none-eabi-gcc -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4 -O2 \ -I./code_gen/kalman -c ./code_gen/kalman/kalman_filter.c -o kalman.o arm-none-eabi-size kalman.o

输出应类似:

text data bss dec hex filename 1248 128 0 1376 560 kalman.o

text段(代码)小于 2KB,bss段(未初始化数据)为 0,证明无动态内存申请。若bss > 0,说明存在未初始化的static数组,需回模型检查Initial condition是否全部设为single(0)

4. 将生成的 C 代码集成到裸机工程:从 Keil MDK 到 STM32CubeIDE 的移植要点

4.1 在 Keil MDK 中添加生成代码:处理 CMSIS 与 HAL 冲突

code_gen/kalman/下所有.c.h文件复制到 Keil 工程Src/Inc/目录。关键修改:

  • 删除ert_main.c(仅用于 Simulink 仿真,裸机无需)
  • kalman_filter.h顶部添加:
#ifndef __STM32F4xx_HAL_H #include "stm32f4xx_hal.h" // 确保 HAL 已包含 #endif
  • 若使用 HAL 库的HAL_GetTick()获取时间戳,在KalmanInputBus.timestamp_ms赋值处调用:
input.timestamp_ms = HAL_GetTick(); // 替换原 Simulink 的 clock 模块

注意:Simulink 生成的rtGetErrorStatus()等错误处理函数在裸机中无意义,可安全删除kalman_filter.c中相关if (rt_error_status != NULL)分支。

4.2 在 STM32CubeIDE 中配置浮点单元(FPU)与链接脚本

在 CubeIDE 的Project PropertiesC/C++ BuildSettingsTool SettingsARM GCC CompilerMiscellaneous

  • 添加-mfpu=fpv4-d16 -mfloat-abi=hard(启用硬件 FPU)
  • ARM GCC LinkerGeneralScript→ 勾选Use newlib nano(减小 printf 占用)

修改STM32F407VGTx_FLASH.ld链接脚本,在._user_heap_stack段前插入:

.kalman_data : { . = ALIGN(4); *(.kalman_data) . = ALIGN(4); } > RAM

并在kalman_filter.c中,将大数组(如A,K)显式放置到 RAM:

#pragma location=".kalman_data" __root const real32_T kalman_filter_A[4] = { ... };

4.3 在主循环中调用滤波器:保证采样时间与模型一致

main.cwhile(1)循环中:

// 读取传感器原始数据(ADC/UART) float raw_y = read_adc_channel(0); // y 为单通道测量值 float raw_u = get_control_output(); // u 为控制量 // 构造输入总线 KalmanInputBus input; input.y = raw_y; input.u = raw_u; input.timestamp_ms = HAL_GetTick(); // 执行滤波 KalmanOutputBus output; kalman_filter_step(&input, &output); // 使用估计值 float estimated_position = output.x_hat[0]; set_actuator_position(estimated_position);

关键点read_adc_channel()必须是阻塞式或 DMA 完成回调,确保每次kalman_filter_step()调用间隔严格等于模型设定的Fixed-step size(如 10ms)。若用HAL_Delay(10),需确认 SysTick 配置正确,否则累积误差会导致滤波失效。

5. 验证滤波效果与代码行为一致性:用 Simulink Data Inspector 对比仿真与实测数据

5.1 在 Simulink 中启用信号日志(Signal Logging)并导出基准数据

在模型中,对关键信号(y,x_hat_plus,P_diag)右键 →Properties→ 勾选Log signal data。运行仿真(Ctrl+T),打开Simulation Data InspectorCtrl+Shift+D),点击CompareBaseline→ 保存为baseline_run.mat

5.2 在嵌入式端采集实测数据并格式化为 MAT 文件

在 STM32 代码中,当output.P_diag[0] > 1e5时,通过 UART 发送结构化数据:

char buf[64]; snprintf(buf, sizeof(buf), "Y:%.3f,X:%.3f,P:%.3f,T:%lu\n", input.y, output.x_hat[0], output.P_diag[0], input.timestamp_ms); HAL_UART_Transmit(&huart2, (uint8_t*)buf, strlen(buf), HAL_MAX_DELAY);

用串口工具(如 Tera Term)保存为embedded_run.txt,再用 Python 转为 MAT:

import numpy as np import scipy.io as sio data = np.loadtxt('embedded_run.txt', delimiter=',', skiprows=0) sio.savemat('embedded_run.mat', {'y': data[:,0], 'x_hat': data[:,1], 'P_diag': data[:,2], 't_ms': data[:,3]})

5.3 在 MATLAB 中对比仿真与实测轨迹:定位量化误差来源

加载两个 MAT 文件,在命令行执行:

load('baseline_run.mat'); load('embedded_run.mat'); figure; subplot(2,1,1); plot(baseline_run.t_ms, baseline_run.x_hat, 'b', embedded_run.t_ms, embedded_run.x_hat, 'r--'); legend('Simulink', 'Embedded'); title('State Estimate Comparison'); subplot(2,1,2); plot(baseline_run.t_ms, baseline_run.P_diag, 'b', embedded_run.t_ms, embedded_run.P_diag, 'r--'); legend('Simulink', 'Embedded'); title('Covariance Diagonal');

x_hat轨迹出现周期性抖动(非发散),大概率是single精度不足,需在 Simulink 中将A,K矩阵改为double重新生成代码(牺牲内存换精度);若P_diag持续上升,则检查Q(过程噪声)和R(观测噪声)矩阵是否在嵌入式端被错误缩放。

5.4 最小化部署体积的终极技巧:剥离未使用的数学函数

Embedded Coder 默认链接libm.a中所有浮点函数。若模型仅用+,-,*,/,可手动裁剪:

  • Configuration ParametersCode GenerationCustom CodeHeader file中添加:
#define NO_MATH_LIB
  • ToolchainCustom compiler flags中添加-DNO_MATH_LIB
  • 修改kalman_filter.c,将sqrt()等函数替换为查表或牛顿迭代近似(如sqrtf(x)x * 0.5f * (3.0f - x * 0.5f * x)迭代一次)

此操作可减少.text段 300~500 字节,对 Flash 紧张的 Cortex-M0/M3 设备至关重要。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询