MATLAB/Simulink AI模型代码生成与嵌入式部署实战指南
2026/9/18 14:43:45 网站建设 项目流程

直接进入正题。

做嵌入式AI方向的工程师,应该都有过这种经历:模型在PC上跑得飞起,准确率也不错,一提到部署到板子上就头疼。尤其是用MATLAB/Simulink做算法原型验证的团队,从仿真环境到嵌入式硬件之间,好像总隔着一道无形的墙。模型要转成C代码,要处理定点量化,要适配不同的编译器,还要考虑内存带宽,步骤繁琐,到处是坑。

这篇文章就是系列第四篇,专门讲AI模型在MATLAB/Simulink环境下如何完成代码生成,并真正部署到嵌入式硬件上。前面几篇我们聊了嵌入式AI的整体概念、MATLAB/Simulink里怎么搭AI模型、以及模型压缩和量化的准备工作。现在,我们进入最关键的环节——把训练好的AI模型变成能在MCU、MPU或者Linux目标机上运行的C/C++代码,并让它在真实硬件上稳定工作。

这篇内容主要面向三类读者:一是算法工程师,想把自己训练的深度学习模型快速移植到嵌入式平台验证效果;二是嵌入式软件工程师,需要接手算法团队交付的模型,把它集成到现有的嵌入式工程里;三是做教学和科研的朋友,想把MATLAB的AI能力落地到实际硬件上,而不只是停留在仿真阶段。无论你是哪一类,这篇文章都会给你一条清晰可执行的路径,以及我在实际项目中踩过的坑和总结的经验。

1. 三条代码生成路径:先搞清楚该走哪条路

在动手之前,最重要的事情不是打开MATLAB就开始敲命令,而是先想清楚一个问题:我的目标硬件是什么类型,代码生成要达到什么目的。因为MATLAB/Simulink针对不同场景提供了不同的生成路径,选错了路径,后面会走很多弯路。

1.1 三种路径的适用场景对照

我把常用的路径整理成了一张表,方便对照选择:

路径生成语言目标硬件适用场景集成难度
Deep Learning Toolbox + codegenC/C++任意支持C/C++编译器的平台,如Linux、Windows、ARM板将训练好的网络导出为独立可执行的库函数,集成到现有C/C++工程中等
Simulink模型生成C代码C通过Embedded Coder支持的目标平台,如ARM Cortex-M、自定义硬件需要在Simulink中完成预处理、后处理、控制逻辑与AI模型联合仿真的场景较高
GPU CoderCUDANVIDIA GPU(Jetson系列、独立显卡)需要GPU加速推理的场景,如实时视频检测、高性能计算中等
Deep Learning HDL ToolboxHDLFPGA需要极低延迟、高吞吐量的硬件加速极高

1.2 我为什么推荐大多数场景先走codegen路径

从实际经验看,对于MCU级别的嵌入式设备(不带操作系统的裸机环境或RTOS环境),最简单的往往是第一条路径:用Deep Learning Toolbox里的codegen命令,把训练好的网络导出为C/C++代码,然后手动集成到你的嵌入式工程里。

原因有几个:

  • 生成的是独立、自包含的C/C++源代码,不依赖MATLAB运行时环境,可以直接丢进IAR、Keil、STM32CubeIDE或者GCC工程里编译。
  • 支持多个主流网络层,包括卷积层、全连接层、激活函数、池化层等,对于分类、检测、语义分割这类常见任务完全够用。
  • 部署灵活性最高,你完全可以控制生成的代码如何与底层硬件驱动交互。

如果你在Simulink里搭建了完整的信号处理链路,AI模型只是其中的一个模块,那么走Simulink模型生成C代码会更合适。举例来说,一个工业设备故障诊断系统,传感器采集振动数据后,需要经过滤波、特征提取,再送入AI模型做分类,最后驱动报警模块。这种情况下,整条链路在Simulink里建模,最终统一生成C代码,部署效率会高很多。

2. 入口函数与数据类型标注:codegen生成代码前必做的三件事

codegen是Deep Learning Toolbox里最重要的代码生成命令,它能把trainNetwork或者importNetworkFromTensorFlow训练出来的网络对象转成C代码。但是,codegen本身可不会自动帮你处理所有事情,你需要先写好一个入口函数,告诉MATLAB这个网络的输入输出是什么样子。

2.1 入口函数怎么写

假设你已经有了一个训练好的图像分类网络,输入是224x224x3的RGB图像,输出是分类得分和标签。那么入口函数可以这样写:

function [scores, label] = classifyImage(inImage) % 使用coder.extrinsic声明不需要生成C代码的MATLAB函数 coder.extrinsic('grp2idx'); persistent mynet; if isempty(mynet) mynet = coder.loadDeepLearningNetwork('trainedNet.mat', 'myNet'); end % 网络输入要求是single类型 inImage = single(inImage); [scores, labelIdx] = mynet.predict(inImage); % 标签向量需要预先定义好 classes = {'cat', 'dog', 'bird'}; label = classes{labelIdx}; end

这里有几个细节值得注意。

第一个细节是coder.loadDeepLearningNetwork这个函数。它能让你在生成的C代码中加载预先训练好的网络,网络结构和权重会被包含在生成的代码中。这样生成的是一个自包含的库,不需要在运行时从外部文件加载权重,这对嵌入式环境尤其重要。

第二个细节是数据类型。mynet.predict的输入通常是single类型,也就是32位浮点。如果你在仿真时习惯用double类型,生成代码时会报警告,同时影响推理速度。嵌入式处理器(比如Cortex-M4以上内核)对单精度浮点的支持都很好,所以尽早统一为single类型能省去很多麻烦。

第三个细节是标签的映射。上面代码中我用了一个简单的cell数组来演示,实际项目里你可能会面对几百个类别,这时候不要手写,最好用grp2idx先保存一份类别索引表,然后用查表的方式来做映射。

2.2 使用codegen生成代码的完整命令

写好入口函数后,用一行命令生成代码:

codegen -config coder.config('lib') classifyImage -args {ones(224,224,3,'single')} -report

解读一下各参数的作用:

  • -config coder.config('lib'):生成静态库,而不是可执行文件。静态库方便后续集成到嵌入式工程中。
  • -args {ones(224,224,3,'single')}:定义输入参数的类型和大小。这一步非常关键,MATLAB需要知道输入的shape和类型,才能生成内存访问和数据处理代码。
  • -report:生成一个HTML报告,里面能查看代码生成日志、数据流分析结果以及生成的源文件列表。报告对排查问题很有帮助。

生成后会在当前目录下产生一个codegen/lib/classifyImage/文件夹,里面就是完整的C/C++源码、头文件和静态库。你可以直接把这些文件拷贝到嵌入式工程中编译。

2.3 我试过的最省事的编译方式

如果你用的是STM32系列MCU,最省事的方式是直接用STM32CubeIDE新建一个C工程,把codegen生成的源文件加进去,然后在main.c里调用classifyImage_initialize()进行初始化,之后就可以反复调用classifyImage这个函数进行推理了。当然,你需要自己写一个函数从摄像头或SD卡中读出图像数据,转成float数组,再传入classifyImage

这里有一个常见的坑:MATLAB生成代码的默认堆栈使用量较大。在内存有限的MCU上运行时,可能会出现HardFault或者栈溢出。解决办法是在生成代码时配置堆栈大小,或者修改coder.extrinsic的使用范围,尽量在入口函数内减少局部变量。更直接的办法是调大链接脚本中的堆栈设置。

3. Simulink模型转C代码:从算法仿真到嵌入式工程的最后一公里

如果你的AI模型不是独立使用的,而是作为整个信号处理链路的一部分,那么Simulink模型代码生成就是一个更好的选择。这个场景下,你需要在Simulink中把AI模型封装成一个子系统,配置好数据接口,然后用Embedded Coder生成整个系统的C代码。

3.1 在Simulink里嵌入AI模型的两种方式

第一种方式,使用Deep Learning Toolbox里提供的Predict模块。这个模块可以在Simulink中直接加载训练好的网络,进行推理计算。你只需要把它拖到模型中,然后在模块参数中指定网络文件即可。这种方法最直观,适合快速验证。

第二种方式,使用MATLAB Function块封装codegen入口函数。这种方式更灵活,适合需要自定义预处理或后处理的场景。你可以在MATLAB Function块里写类似上面的入口函数逻辑,然后作为普通子系统参与到整个Simulink模型中。

我在实际项目里更倾向第二种方式。原因很简单:Predict模块的参数配置界面虽然好用,但遇到复杂的预处理逻辑(比如自定义图像归一化、数据增强)时,还是需要用MATLAB Function块把逻辑写清楚。

3.2 生成代码前的配置要点

准备好模型后,在Simulink中配置代码生成参数,这一步很关键。打开模型配置参数对话框,重点设置以下几个选项:

  • 求解器:如果模型本身没有连续状态,选择固定步长离散求解器,步长可以设为0.01或更小。代码生成后,这个步长会对应到定时器中断的周期。
  • 代码生成:选择Embedded Coder,确保勾选了Generate code only,这样不会自动打开外部编译环境,方便你在自己的工程里编译。
  • 硬件实现:在Hardware Implementation面板中,把设备供应商和设备类型选为你目标MCU对应的型号。这会影响数据类型定义、字节对齐方式等底层细节。比如选STM32F4系列,MATLAB会生成针对ARM Cortex-M4的优化代码。
  • 代码替换库:在Code Generation > Interface中,可以启用针对ARM Cortex-M的代码替换库(CRL)。CRL能用CMSIS-DSP等优化库函数替换默认实现,实现显著的速度提升。对于卷积、激活函数等计算密集的操作,效果非常明显。

配置完成后,使用Ctrl+B生成代码。生成的文件包括模型对应的.c.h文件,以及一个ert_main.c示例主程序,你可以参考这个文件来了解初始化流程和定时器配置。

3.3 代码替换库带来的实际性能提升

举个例子,在一个基于Cortex-M7的工业控制器上跑一个小型CNN模型(5个卷积层,输入64x64x3),不启用CRL时,单次推理大约需要350ms。启用ARM Cortex-M的CRL后,同样是这个模型,推理时间降到了210ms左右。提升接近40%。这主要得益于CMSIS-DSP中针对ARM内核优化的矩阵乘法和激活函数实现。

不过有一说一,CRL也不是万能的。它主要优化的是数学运算库函数维度,对于模型本身结构复杂(比如大量分支、动态shape)的情况,可能发挥不了太大作用。所以如果你的模型推理时间始终不达标,优化的重点还是要放在模型压缩和量化上,这个在系列第三篇里详细讲过。

4. MCU部署的真正挑战:内存、位宽和实时性问题

有了C代码只是第一步,真正把AI模型跑在MCU上,你一定会遇到三个硬骨头:内存不够用、浮点太慢、实时性保证不了。这三个问题单独拿出来都能写一篇文章,我这里重点讲一下我在实际项目里怎么处理。

4.1 内存估算:在烧录之前先算一笔账

MCU的内存分为Flash和RAM。Flash用来存代码和网络权重,RAM用来存中间计算缓冲区。一个典型的嵌入式AI模型,Flash占用通常是网络权重大小加上代码体积,RAM占用则取决于网络激活值的峰值。

举个例子,一个输入为128x128x3的MobileNetV1网络,模型权重大约4.2MB,激活值峰值大约1.8MB。如果一个MCU只有2MB Flash和512KB RAM,那这个模型根本塞不进去。这时候你需要的是模型压缩和网络结构精简,单纯靠代码优化解决不了根本问题。

我在实际项目中的经验是,在烧录之前先用MATLAB的analyzeNetwork检查网络各层的激活值大小,找到激活值最大的层,针对性地做结构优化。很多时候,问题出在全连接层上——全连接层的权重往往占据了整个模型的80%以上,如果任务允许,把它替换成全局平均池化+更小的全连接层,内存占用能下降一大截。

4.2 浮点位宽选择:一刀切用single不一定最优

MCU的浮点运算分两种:单精度浮点(float)和半精度浮点(half)。Cortex-M4以上的内核带FPU,能直接算float,速度很快。但Cortex-M0这类低功耗内核不支持硬件浮点,用软件模拟浮点运算会非常慢,这时候就必须考虑定点运算。

MATLAB提供了Deep Learning Quantizer工具,可以把训练好的浮点网络量化为8位定点(int8)甚至4位定点。量化后的模型体积减少4倍,推理速度提升4到8倍,代价是精度可能会有轻微下降。

我做过的宠物识别项目就是典型场景。在树莓派和Jetson Nano这类Linux平台上,直接跑float模型没压力,但如果目标是部署在Cortex-M4的MCU上,就必须量化到int8。用Deep Learning Quantizer量化后,一个猫狗识别模型的精度从98.2%下降到了96.8%左右,对于实际应用来说完全可以接受,但模型体积从15MB降到了3.8MB,推理时间从1.2s降到了260ms。这个交换非常划算。

4.3 实时性保障:定时器中断与推理时间的匹配

在MCU上跑AI模型,实时性意味着推理必须在规定时间内完成。比如一个振动监测系统,每10ms采集一组数据,推理也要在10ms内完成。如果做不到,数据就会丢失,系统就失去了意义。

我常用的做法是,把数据采集放在定时器中断里,把推理放在主循环里。定时器中断负责把采集到的数据写入环形缓冲区,主循环检测到缓冲区满了就取出来做推理。这样即推理稍微超过10ms,也不会丢失数据,只是系统的响应频率会降低,但至少不会出错。

如果推理时间实在压不下来,还可以考虑双缓冲区方案。ADC在DMA模式下持续采集,数据写入A缓冲区;主循环在处理A缓冲区数据的同时,DMA已经在往B缓冲区写数据了。这样ADC采集永远不会中断,推理也能利用完整的时间片。

5. 部署不等于烧录:设备端集成与运行验证

把生成的C代码编译烧录到板子上,这只能算部署完成了一半。真正让模型在设备端稳定运行,还需要解决数据接口、内存分配和异常处理等问题。这一节我讲一个我实际完成的宠物检测项目,从头到尾展示了整个链路是怎么打通的。

5.1 一个宠物检测项目的全流程拆解

项目需求是在一个基于Cortex-A72的嵌入式Linux板卡上,通过USB摄像头实时检测画面中的猫和狗,并在LCD屏幕上显示识别结果。算法团队交付了一个训练好的ResNet18分类模型(猫、狗、背景),以及对应的预处理代码。

我拿到模型后的处理流程如下:

  1. 把模型转成ONNX格式,再用MATLAB的importNetworkFromONNX导入。
  2. 写入口函数,定义输入为224x224x3的RGB图像,输出为三个类别的置信度。
  3. 使用codegen生成C++代码(目标平台选Linux)。
  4. 把生成的库文件放到Linux板卡的工程目录,用GCC编译成可执行文件。
  5. 在Linux侧通过V4L2接口读取摄像头图像帧,进行缩放和归一化,送入AI推理函数。
  6. 将推理结果通过LCD驱动显示在屏幕上。

整个开发过程最耗时间的不是代码生成,而是摄像头图像格式转换。摄像头默认输出YUV422格式,而模型输入是RGB,这个转换得自己写。我在SIMD层面做了优化,用NEON指令集实现颜色空间转换,最终把单帧处理时间从35ms降到了12ms。

5.2 编译阶段一定要避开的三个坑

第一个坑是编译器版本不一致。MATLAB生成代码时可能基于一些C++11的特性,如果你的嵌入式GCC版本较老,编译时会报各种奇怪的语法错误。解决办法是使用MATLAB官方支持的最低编译器版本,或者手动修改代码中不兼容的部分。需要看具体编译器和MATLAB版本的支持矩阵,但通常选用较新版GCC就能规避大多数问题。

第二个坑是动态内存分配。MATLAB生成的代码默认会使用mallocfree。在Linux平台无所谓,但在裸机MCU上,malloc可能导致内存碎片,使用久了会崩。建议在代码生成配置中,将动态内存分配关闭,全部改成静态内存分配。配置项在codegen命令的-config里,设置DynamicMemoryAllocationOff即可。

第三个坑是字节对齐。ARM平台对未对齐的内存访问会触发异常。如果代码中定义了大数组,而链接脚本没有做好对齐,运行时可能会出问题。我习惯在生成代码的变量定义后主动加上__attribute__((aligned(8)))或者在链接脚本中设置对齐,保证关键缓存和数组按8字节对齐。

5.3 实机推理稳定性验证

部署完成后,不能只是简单跑几张测试图片就认为完事了。我一般在板子上跑一个至少12小时的稳定性测试,期间循环执行推理任务,同时记录内存占用、CPU使用率和温度数据。重点关注两个指标:一是内存是否持续增长(泄漏),二是推理时间是否保持稳定(抖动)。

经验数据是,如果推理时间抖动超过20%,通常意味着系统还有调度问题,比如中断处理占用过多CPU时间,或者DMA传输与其他外设冲突。这种问题在仿真环境里完全发现不了,只有实机测试才能暴露出来。

6. 报错能急死人:五类高频问题排查记录

MATLAB代码生成和部署的报错信息五花八门,很多问题如果没人指点,一个人可能要卡好几天。我把这一两年项目里遇到过的高频报错整理了一下,希望能帮你节省排查时间。

6.1 找不到数据字典can.sldd或hwa.sldd

这是Simulink模型代码生成时很常见的错误之一。完整报错一般长这样:

找不到数据字典 'can.sldd'。 找不到数据字典 'hwa.sldd'。 组件:simulink | 类别:model 错误

这个问题的根源很简单:模型文件引用了数据字典(.sldd)文件,但MATLAB当前路径下找不到这些字典。数据字典用来集中管理信号、参数和数据类型定义,在多人协作的大型项目中很常用。模型是从别人那里拷贝过来的,或者整个项目文件夹没有完整迁移时,容易出现这种问题。

排查方法:打开模型文件后,进入Model Properties > Data选项卡,查看引用了哪些数据字典。然后把对应的.sldd文件添加到MATLAB路径中。如果找不到原始字典文件,可以新建一个空白字典,再把模型关联切到新字典,但这种方法只适用于字典中没有关键参数定义的情况,否则模型里的变量会变成未定义状态。

更彻底的办法是把数据字典中定义的参数直接固化到模型工作区(Model Workspace)中。右键模型,选择Model Workspace,把需要的参数和信号定义添加进去,然后移除模型对.sldd的依赖。这样虽然牺牲了集中管理的便利,但保证了模型的可移植性。

6.2 codegen时提示不支持的层类型

很多从PyTorch或TensorFlow导入的模型,会包含一些MATLAB不支持的特殊层,比如GroupNormMultiHeadAttention等。codegen报错会说某层不支持代码生成。

这时候有三个选择:

  1. 在MATLAB中用replaceLayer把不支持的层替换为等价支持的层组合。例如GroupNorm可以用LayerNormalization近似替代。
  2. 把该层改为coder.extrinsic调用,即该层不生成C代码,而是在运行时调用MATLAB库函数。但这种方式要求目标机器必须有MATLAB环境,嵌入式场景基本不可用。
  3. 换一种网络表达方式,在模型导出阶段就避免使用不支持的层。

我个人最推荐第一种方式,虽然替换层之后精度会有轻微变化,但通常在一个可接受范围内。实测下来,把GroupNorm替换为LayerNormalization后,语义分割模型的mIoU从0.72下降到0.70左右,降幅约3%,但代码生成瓶颈解决了,完全值得。

6.3 生成代码后编译报错:undefined reference to某种函数

这个问题的根源是缺少头文件或者库文件。codegen生成的代码可能需要依赖一些额外的静态库,比如libmwcnnlibmwmathutil等。你需要在嵌入式工程的链接器设置中,把这些库加进去。

根据我多次踩坑后的经验,最稳妥的办法不是手动在IDE里逐项添加库,而是在生成代码时把-config设置为coder.config('lib')的同时,勾选Generate makefile选项。这样MATLAB会生成一个完整的makefile,里面已经包含了所有依赖库和编译参数。你可以在自己的工程里参考这个makefile的配置,手动调整路径后复用。

6.4 嵌入式中断里调用推理函数导致系统崩溃

这个问题的典型表现是:现场调试时,只要在定时器中断里调用AI推理函数,系统就死机。原因通常是中断上下文不允许执行耗时过长或占用大量栈空间的代码。

解决办法很明确:不要在中断服务函数里直接调用推理,而是设置一个标志位,把推理任务放到主循环或者高优先级任务中执行。中断里的代码应该非常精简,只做数据拷贝和标志位设置,其他事情都留到主循环处理。这也是我在上文实时性保障一节提到的那套做法的意义。

6.5 量化后精度下降超出预期

用Deep Learning Quantizer量化后,精度通常会下降,但如果下降幅度超过5%,就要排查问题出在哪里。我遇到过的情况有三种:一是量化校准数据集没有覆盖真实的输入分布,二是某些层的参数分布范围过大,量化误差被放大,三是网络中存在对精度极其敏感的层,比如检测头的输出层。

针对第一种情况,尽可能从实际设备上采集数据来做校准,而不是用训练集里的数据。针对第二种情况,可以在量化配置中,把敏感层单独设置为更高的位宽(如int16而非int8)。针对第三种情况,考虑使用混合精度量化:保持第一层和最后一层为float32,中间层用int8。MATLAB里的Mixed-Precision Optimization功能就是干这个的,可以通过报目标精度上限来自动搜索混合精度方案。

7. 写在最后的几句实在话

这套从MATLAB/Simulink到嵌入式硬件的AI模型代码生成流程,我前前后后用了快三年。从最初的摸石头过河,到后来的形成方法论,中间踩过的坑确实不少。现在回头看,有几个深切的体会想分享给看完这篇文章的朋友。

第一,先确认目标平台再决定技术路线。不同MCU/MPU的能力差异极大,Cortex-M0和Cortex-A72的处理能力差了两个数量级,适合的技术方案完全不同。不要在没确认平台前就盲目开始模型训练和量化,往往会白做很多工作。

第二,代码生成只是整个部署流程的中间环节,前面连着模型训练和量化,后面连着底层驱动和系统集成,任何一个环节出问题都会导致部署失败。别把精力全部放在代码生成这个环节上,整体链路通畅才是关键。

第三,量化意识要提前培养。很多算法工程师习惯在训练环境里用float32跑模型,到了部署阶段才开始想量化的事。如果能在网络设计阶段就考虑量化友好性(比如尽量避免大量小数值的激活层),部署时会顺利很多。

最后再说一个很实用的技巧:MATLAB生成的C代码虽然整体上是可靠的,但变量命名和代码风格确实一般般,内部函数名有时还特别长,基本不打算让人去阅读。不要试图去手工修改生成的代码来优化性能,那是事倍功半的做法。真正要调优,回到Simulink或者训练脚本里去改模型结构、量化配置,再重新生成代码,这才是高效率的思路。

就聊到这里。项目里具体的模型结构和配置参数不同,操作细节会有些差异,但大方向是通用的。如果你在部署过程中遇到什么新的坑,欢迎在评论区交流,一起把这套流程打磨得更顺滑。

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

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

立即咨询