
1. 从算法到硬件的最后一公里模型部署的整体思路做嵌入式 AI 的人应该都有体会在 PC 上把模型训练出来、精度达标只是走完了半程。真正的挑战在于如何把一个动辄几百兆浮点计算量的 AI 模型塞进一个只有几百 KB RAM、主频几十到几百 MHz 的 MCU 里还要让它跑得够快、够稳、够省电。MATLAB/Simulink 这套工具链在传统控制领域已经积累了很深的基础几乎每一个做电机控制、电源变换、机器人控制的人都在用。而当嵌入式设备开始引入 AI 能力这套工具链的价值就更明显了——它能把“算法设计—仿真验证—代码生成—硬件部署”整条链路串起来中间没有断点。这一系列文章前面几篇分别讲了 AI 模型的设计、训练和验证这一篇专门解决“最后一公里”的问题怎么把训练好的模型无论是神经网络还是传统的机器学习模型通过代码生成的方式部署到嵌入式硬件上。具体覆盖几个关键环节模型如何量化压缩、Simulink 如何完成 C 代码生成、生成后的代码怎么集成到单片机工程里以及实际部署中会踩到哪些坑。这篇文章适合谁看如果你正在做嵌入式 AI 相关的项目比如想在 STM32、ESP32 或者国产 MCU 上跑一个宠物识别、关键词唤醒、异常检测之类的模型又不打算引入 TensorFlow Lite Micro 或者 CMSIS-NN 这一套而是希望在自己熟悉的 MATLAB/Simulink 环境里直接完成整个流程那这篇文章就是给你写的。同样如果你只是好奇“Simulink 模型到底怎么变成能在单片机上跑的 C 代码”也可以读下去。先说结论用 MATLAB/Simulink 做嵌入式 AI 部署核心优势不是单点工具的性能而是整条链路的连贯性。你在 Simulink 里搭的模型、调的参数、验证过的逻辑可以在生成代码后保持完全一致不存在“算法工程师交付的模型和嵌入式工程师拿到的代码是两回事”这种割裂。2. 部署前的关键决策量化策略与目标硬件匹配2.1 嵌入式硬件的资源约束从哪来在开始生成代码之前你需要先搞清楚一个问题你的目标硬件到底能承受多大的模型。这不是拍脑袋决定的而是由几个硬指标决定的。第一个指标是 Flash 空间。模型权重存哪单片机不像 PC 有硬盘所有可执行代码和常量数据都要放在片内 Flash 里。一个典型的 MobileNet 类模型float32 精度的权重大约是 10~30 MB而一颗中端 ARM Cortex-M4 芯片的 Flash 通常只有 256 KB~1 MB。这意味着什么不压缩连模型都装不下。第二个指标是 RAM。MCU 的 RAM 通常在几十 KB 到几百 KB 之间而推理过程中每一层的中间特征图都要占用内存。2048 字节的输入图像经过几层卷积之后特征图尺寸可能膨胀到几十 KB再加上激活函数输出的暂存区、DMA 缓冲区内存紧张是常态。第三个指标是算力。Cortex-M 系列没有独立的浮点单元FPU或者只有单精度 FPU跑 float32 卷积是非常奢侈的。这时候就需要做定点化——把浮点运算转成整数运算。2.2 量化策略怎么选浮点、定点还是整型在 MATLAB/Simulink 里做模型量化常用路径有这么几条我按推荐程度排个序。第一种直接用 float32 部署。只适合 Flash 和 RAM 都非常充裕的场景比如带外部存储的高端 SoC或者模型本身极小的情况——比如一个只有几百个参数的单层感知机。大部分 MCU 场景这条路走不通。第二种用 Fixed-Point Designer 做定点转换。这是 MATLAB 生态里非常成熟的一套工具。它会把模型里的浮点运算自动转成定点运算同时做精度分析告诉你哪些变量需要多少位宽、多少小数位。定点数相比浮点数最大的好处是计算快、占用资源少代价是需要开发者对数据范围有清晰的认知。第三种转换成 int8 整型推理。这在 PC 端深度学习里已经很常见了在 MCU 上用得相对少主要是因为 MCU 上的矩阵运算库对 int8 的优化程度参差不齐。不过如果你的目标芯片是带 DSP 指令的 Cortex-M7 或者国产的 RISC-V 芯片int8 路线值得认真考虑。我自己的经验是大部分嵌入式 AI 项目定点fixed-point是最稳的选择因为它在精度、资源占用和工具链成熟度之间取得了最好的平衡。Fixed-Point Designer 自带的数据范围分析功能能自动推导出每个中间变量的动态范围这比手工估算靠谱得多。2.3 硬件选型对部署难度的影响这里再花点篇幅说硬件选型。很多人习惯先选芯片再考虑部署但我建议反过来——先搞清楚你的算法对资源的需求再倒推需要什么样的芯片。如果你的模型只是一个简单的分类器输入是几十个特征值输出是几个类别那 Cortex-M0 级别的芯片都够用。如果模型是图像类任务比如宠物识别——猫狗实时识别这种场景输入至少是 96×96 或 128×128 的灰度图中间层还有卷积运算那你至少需要 Cortex-M4 以上带 FPU 和 DSP 指令Flash 在 512 KB 以上RAM 在 128 KB 以上才比较舒服。提示如果目标芯片是国产的 GD32、华大、沁恒这类 M 内核芯片部署流程和 STM32 基本一致因为核心架构是 ARM Cortex-M。不需要因为换了个品牌就担心工具链不兼容。3. Simulink 模型到 C 代码的生成链路全解析3.1 代码生成的前置条件模型要“可生成”Simulink 里不是所有模型都能直接生成 C 代码。这一点很多人一开始不知道搭了一个很复杂的模型到点击“Generate Code”的时候才发现报了一堆错。要让模型具备代码生成能力有几个隐藏条件需要注意。第一模型里使用的所有模块必须是 Embedded Coder 支持列表里的。大部分常用的数学运算、逻辑运算、信号路由模块都支持但有些可视化相关的模块、部分工具箱里的演示模块不支持。第二模型中不能有代数环——简单说就是信号在同一个仿真步长内形成了循环依赖这种结构在仿真时可以靠迭代求解硬解出来但生成代码时无法确定计算顺序。第三采样时间必须显式指定不能全部用默认的连续采样时间。我的建议是在搭建模型之前就先想好部署目标从一开始就按照可生成代码的规范来搭模型。而不是搭完之后再回头改那样工作量非常大。3.2 Simulink Coder 与 Embedded Coder 的分工很多人分不清 Simulink Coder 和 Embedded Coder 的区别这里用一句话说清楚Simulink Coder 负责把 Simulink 模型转换成 C/C 代码Embedded Coder 是它的专业增强版面向嵌入式场景做了大量优化和代码精简。具体来说Embedded Coder 比 Simulink Coder 多干了这几件事代码风格更适合嵌入式工程规范变量命名可配置生成的代码不依赖运行时环境可以直接和单片机上的裸机代码集成还支持代码追踪——从生成的 C 代码能直接回溯到 Simulink 模型里的对应模块这对调试和做文档都很关键。我见过不少人在初期为了省钱只用 Simulink Coder生成的代码确实能跑但代码体积偏大函数接口也不够干净到了后期调试的时候非常痛苦。如果项目是产品级的建议直接上 Embedded Coder省下来的调试时间远超授权成本。3.3 从模型到代码的完整配置过程接下来就是核心实操。我以一个具体的例子来走一遍流程假设我们已经在 Simulink 里搭好了一个宠物检测 AI 模型的推理子系统输入是一路图像信号像素流或矩阵形式输出是“猫”和“狗”两个类别的得分现在要把它生成 C 代码部署到 STM32 上。第一步打开配置参数Configuration Parameters对话框。在 Solver 选项卡里把求解器类型设置为 Fixed-step定步长这一步非常关键。因为面向嵌入式部署所有计算都必须在确定的时间步长内完成不能用变步长求解器做自适应积分那在硬件上没法实现。第二步设置目标硬件。在 Hardware Implementation 选项卡里选择你用的芯片设备类型比如 ARM Cortex-M。这个设置会影响代码生成时的一些底层细节包括数据类型位宽、字节对齐方式、编译器特有的语法等。选对了生成的代码才能无缝集成到你的 Keil 或者 IAR 工程里。第三步配置代码生成选项。进入 Code Generation 选项卡选择代码生成目标为 Embedded Coder语言选 C。这里有几个值得注意的选项一个是 Generate code only如果选上不会自动打开编译过程适合把代码拿到外部集成另一个是 Code interface 里的参数配置把函数命名、数据类型定义的风格设置好避免生成一堆默认名称的变量让你后期看得头大。第四步点击 Generate Code。等待编译完成之后你会得到一组 C 源文件和头文件主要包括模型初始化函数、步进函数、模型参数结构体、数据类型定义头文件等。核心的就两个模型初始化函数负责分配内存、设置初始参数步进函数是推理的主循环每次调用执行一次完整的推理计算。3.4 生成代码的结构解读生成完之后建议花点时间把代码结构看懂。我见过很多人拿到代码之后直接丢给嵌入式工程师结果出了问题两头都说不清楚。Embedded Coder 生成的代码一般包含这几类文件model.h是模型对外接口的头文件声明了初始化函数和步进函数model.c是模型实现的主文件包含所有模型算法代码model_types.h定义模型需要的自定义数据类型rtwtypes.h定义跨平台的基础数据类型映射关系还有一个model_data.c或者类似的常量定义文件存放模型参数和权重数据。看懂这些文件的结构之后你就知道集成的时候哪些要改、哪些不能动。我自己通常的做法是把生成的代码全部放在一个单独的文件夹里用 Embedded Coder 的代码生成报告先看一眼整体结构再做集成。4. 部署实操将生成代码集成到嵌入式工程4.1 集成路径的三选一生成完 C 代码之后把它集成到嵌入式工程里有三种常见方式适用场景不同。第一种直接复制源码集成。把生成的 .c 和 .h 文件全部复制到 Keil/IAR/STM32CubeIDE 的工程目录下然后在主程序里包含头文件、调用初始化函数和步进函数。这种方式最简单直接适合小型工程。第二种编译成静态库集成。先把生成的源文件编译成 .a 或 .lib 静态库再链接进主工程。这种方式适合工程比较大、源码目录已经有严格规范的情况而且静态库可以避免源码被意外修改。第三种通过 Simulink 的外部模式External Mode实时调试。这不算严格意义上的部署但在开发调试阶段非常好用。它可以让 Simulink 和单片机建立实时通信直接在 Simulink 界面上实时观察模型内部信号、调整参数不用反复烧录固件。这对于验证“模型仿真结果和硬件运行结果是否一致”非常有价值。4.2 以 STM32 为例的完整部署步骤下面以 STM32 为例给一套我在项目中反复使用的标准部署流程。第一步准备工程。在 STM32CubeMX 或者直接在你的 IDE 里建好基础工程保证一个裸机程序能正常跑起来串口能正常输出日志。不用一开始就加入 AI 相关代码先把基础设施跑通。第二步复制生成代码。把 MATLAB 生成的全部 .c 和 .h 文件放到工程目录下的ai_model/文件夹里。注意检查一下生成代码的头文件依赖确保所有头文件都在工程包含路径里能找到。第三步修改主程序。在主函数的初始化阶段调用模型初始化函数在主循环里按固定周期调用步进函数。比如一个图像分类模型主循环里做的事就是读取摄像头数据 → 预处理成模型输入格式 → 调用步进函数推理 → 读取输出得分 → 通过串口或者屏幕输出结果。第四步内存管理检查。看生成的模型代码里有没有使用动态内存分配也就是 malloc/free。Embedded Coder 默认情况下可以配置为不使用动态内存所有缓冲区都是静态分配的。如果你发现生成的代码里有 malloc需要回到 Code Generation 的配置里把动态内存分配关掉。MCU 上跑产品级代码尽量不要用动态内存容易产生碎片。第五步堆栈设置。AI 推理通常是计算密集型的调用深度也可能比较大。在启动文件里把堆栈空间设大一些我一般会设置到 8 KB 以上如果还报栈溢出就继续往上加。第六步性能测量。在步进函数前后分别读一次系统时钟寄存器计算出单次推理的时钟周期数再换算成毫秒。这一步非常重要它决定了你的系统能不能满足实时性要求。4.3 宠物识别模型的端到端部署案例热词里出现的“宠物检测 AI 模型——嵌入式设备上的猫狗实时识别”是个很好的案例我拿它把整个流程完整串一遍。假设你的模型已经在 MATLAB 里训练好输入是 128×128 的 RGB 图像输出是猫和狗两个类别的置信度。在把模型接入 Simulink 之前你还需要做一个预处理图像缩放和归一化。这些可以放在 Simulink 模型里用 Simulink 的机器视觉工具箱模块实现也可以直接在 C 代码里写——我建议前期用 Simulink 搭好确认预处理逻辑没问题后再考虑是否手写优化。模型推理子系统在 Simulink 里就长这样输入端接一个 128×128×3 的图像信号经过训练好的神经网络层用 Deep Learning Toolbox 的 Predict 模块加载输出端接一个支持分类结果输出的信号线。生成代码并集成到 STM32 之后整个系统的运行逻辑是OV2640 摄像头拍一张图 → DMA 传输到内存 → 预处理函数把图像缩放到 128×128 并做归一化 → 调用步进函数执行推理 → 得到两个类别的得分取最大值对应的类别作为识别结果 → 通过串口发送到上位机或者驱动一个小屏幕显示“猫”或“狗”。这套流程跑通了之后我再额外强调一个点预处理部分的计算量往往被人忽视。图像缩放如果按最朴素的最近邻插值每秒跑几帧问题不大如果你用双线性插值计算量会翻几倍可能在 MCU 上成为瓶颈。实测下来在 168 MHz 的 STM32F407 上双线性插值把一张 320×240 的图缩放到 128×128耗时约 15~20 毫秒这已经比很多轻量模型的推理时间还长了。所以预处理一定要做性能优化不能想当然。5. 部署实战中的性能调优与问题排查5.1 推理速度不够快从哪里入手优化部署之后最常遇到的问题就是模型能跑但速度达不到要求。这里的优化手段按性价比从高到低排个序。第一优先开启编译器的优化选项。Keil 的 -O3 和 IAR 的 High 优化级别可能直接带来 30% 以上的性能提升。如果你的项目连这个都没开先别急着折腾其他手段。第二优先确认是否使用了硬件 FPU。如果你用的是带 FPU 的 M4/M7 芯片在编译选项里打开 FPU 支持浮点运算速度能快一个数量级。但要注意这要求你在 MATLAB 代码生成时选择的硬件类型也带 FPU否则生成代码可能默认走软件浮点库。第三优先检查数据类型的位宽。如果模型内部大量用了 double 类型而你的 MCU 是单精度 FPU建议回到 MATLAB 把所有 double 改成 single。这个改动在模型层面就能完成影响非常明显。第四优先考虑用芯片厂商提供的 DSP 库做卷积加速。比如 ARM 的 CMSIS-DSP 库里有针对矩阵运算的优化函数国产 MCU 厂商也会提供类似的库。需要把生成的代码里耗时最多的循环部分手动替换成库函数改动量不小但效果显著。5.2 我在实际项目中踩过的坑踩过的坑太多挑几个典型的说。第一个坑生成代码烧进去之后模型输出结果全是零。排查到最后发现是模型输入数据格式不对。Simulink 里默认的图像数据排列是通道在后CHW而摄像头采集的裸数据通常是逐行扫描的 HWC 格式直接把摄像头数据丢给模型模型读到的完全是乱码。第二个坑模型在仿真里精度很高部署之后精度明显下降。这个大概率是量化精度问题。我遇到过 Fixed-Point Designer 自动选的定点格式位宽不够导致某个关键中间变量的表示精度不足。解决思路是打开 Fixed-Point 的精度分析报告找到误差最大的变量组手动提高小数位的位数。这个过程比较繁琐但精度问题基本都能通过这种方式定位和解决。第三个坑代码生成时选了 Generate code only结果没有勾选 Generate a test bench导致集成的同事没法做单元测试。代码生成不是你点几下就完事了生成的代码最好能配套一个测试程序验证生成代码的计算结果和 Simulink 仿真结果是否一致。Embedded Coder 支持生成 SILSoftware-in-the-Loop测试代码建议在部署前先跑一遍 SIL这是性价比最高的验证方式。5.3 常见问题速查表这里整理一个速查表把我在多个项目里反复遇到的问题和对应的排查路径列出来方便你直接对照。现象可能原因排查路径生成的代码编译不过头文件路径没配全检查所有 .h 的包含路径是否都在 IDE 里配置烧录后程序跑飞栈空间不够或堆栈溢出查看启动文件的堆栈分配调大到 8 KB 以上输出全是同一个值模型输入数据格式与训练时不匹配检查通道排列顺序、像素取值范围是否一致推理结果和仿真不一致定点和浮点精度差异大打开 Fixed-Point 精度分析报告定位最大误差源芯片发热严重主循环没有睡眠一直全速推理在无任务时进入低功耗模式单次推理时间忽长忽短可能使用了动态内存分配检查代码里是否有 malloc配置关闭动态内存串口输出乱码串口波特率与配置不一致检查串口初始化参数和上位机设置注意很多部署问题的根源其实是“模型结构设计不合理”。比如全连接层过多、参数量过大、激活函数计算复杂度过高这些问题在部署阶段再优化成本很高。所以我在前几篇里一直强调做嵌入式 AI 的模型设计从一开始就必须要考虑部署约束。6. 我的几条经验性建议最后分享几条我做完多个部署项目之后的个人体会不一定写在官方文档里但非常实用。第一整个验证链路里的 SIL 测试一定要做。很多人跳过 SIL直接上硬件调试结果分不清问题是出在代码生成阶段还是硬件集成阶段。花半小时跑一次 SIL能显著减少后续调试工作量。第二生成的代码不要随意手动修改。如果发现生成代码有缺陷优先回到 Simulink 模型层面去改再重新生成。手改生成代码一时痛快但代码库一更新就全被覆盖了而且手改的代码无法通过代码追踪保证与模型一致。第三遇到性能瓶颈时先量化分析再动手优化。用 MATLAB 的 Performance Advisor 或者直接在代码里插入计时逻辑找出真正耗时的函数是什么别凭感觉优化。我见过有人花了两天优化一个只占总耗时 3% 的函数真正的瓶颈在另一个地方。第四如果你的项目是产品级的建议从一开始就建立完整的文档和版本管理。Simulink 模型本身是文本格式的可以用 Git 管理。代码生成配置也可以导出成 .m 脚本放到工程仓库里。这样团队协作的时候每个人生成代码的配置都是一致的不会出现“我这边的代码生成配置和你不一样”的纠纷。从个人经历来说我越来越觉得“模型部署”是整个嵌入式 AI 链路里最需要全局观的一环——对模型的了解程度、对硬件的理解深度、对 C 代码的功底缺一个都会卡住。但反过来一旦你在 MATLAB/Simulink 这套工具链上把这条路走通了后续做新项目基本就是模板化操作效率和稳定性都能得到明显提升。