国产MCU跑TinyML:AT32F435部署正弦波模型实战

发布时间:2026/9/8 8:07:48
国产MCU跑TinyML:AT32F435部署正弦波模型实战 最近这阵子一直在琢磨一件事国产单片机到底能不能也跑 AI正好手头有一块雅特力的 AT32F435主频 288MHz、512KB Flash、192KB SRAM配置在 MCU 里算是很能打的了。我就拿 TinyML 最经典的“HelloWorld 级正弦波模型”试了一把从模型训练、量化导出到把 C 数组塞进单片机工程、烧录跑通整个过程都记录在这篇文章里。如果你也想在自己手头的国产 MCU 上跑个小模型搞明白训练侧和部署侧是怎么对接的这篇文章可以直接当参考。不需要你有多少机器学习基础只要会一点 Python、能用 Keil 开工程就行。我会把关键代码、参数、以及我实际踩过的坑全摊开来说。1. 为什么选AT32F435和“正弦波”这个模型1.1 AT32F435这颗芯片到底能跑什么先看硬件底子。AT32F435 是雅特力的高性能 Cortex-M4F 内核芯片带硬件浮点单元和 DSP 指令主频最高 288MHz。我用的型号是 AT32F435RGT7LQFP64 封装512KB Flash、192KB SRAM外设方面有双路 DAC、三路 ADC、多个 USART/SPI/I2C还有 USB、SDIO、XMC 外部存储控制器。这颗芯片的定位其实很明确在传统 MCU 里面属于“大内存、高主频”一类跑 RTOS、跑复杂协议栈都毫无压力资源余量比老一代的 M3/M0 芯片大得多。而 TinyML 这种“要在资源受限设备上跑推理”的场景正好需要这种底子。你可以把 Flash 理解成仓库SRAM 理解成操作台模型权重和运行时都要占地方如果芯片本身只有 32KB Flash、4KB SRAM那能腾挪的空间就很紧张跑起来相当憋屈。我用它跑 TinyML最直接的感受就是Flash 和 RAM 都很宽裕不用为了省几十 KB 去反复裁剪代码。模型权重只有几百字节TFLite Micro 运行时大概占 80~120KB Flash取决于编译选项和解析器模式tensor arena 分配 16KB 左右剩下的空间还很充足。这跟当年在 64KB Flash 的芯片上跑 TinyML 是完全不一样的体验。1.2 正弦波模型为什么是“Helloworld”TinyML 的 Helloworld 示例官方就叫 sine 模型也就是用神经网络去拟合一个正弦函数输入是 x02π 之间的一个数输出是对应的 sin(x) 预测值。为什么所有教程都拿这个当入门原因很朴实数据生成容易网络训练快结果可视化直观而且流程很完整。首先训练数据不需要你去采集用 numpy 一行代码就能生成几千个样本点。其次模型结构非常简单一个两层或三层的全连接网络就能把正弦拟合得相当好训练时间在 CPU 上也就几十秒。更重要的是结果可以“看见”把模型部署到 MCU 后连续给不同的 x模型会输出一串预测值把这些值画出来就是一条正弦曲线。你能用肉眼判断 AI 到底跑对了没有这比对着串口打印一堆数字有意义多了。别小看这个“最简单的模型”它的流程跟跑真正的项目完全一致。训练、量化、转换、部署、推理、结果可视化每一步都要走通。真实项目只是把数据换掉、把网络换大、把部署目标换到具体应用骨架不会变。所以把这个 Helloworld 跑明白后面做语音唤醒、异常检测、传感器数据分类思路完全是通用的。1.3 照搬官方示例还不够官方 TensorFlow Lite Micro 仓库里也有现成的 sine 模型示例但默认针对的是 Arduino、STM32F746 这类板子。你把它直接搬到一个国产 MCU 工程里会发现有很多地方要自己处理芯片头文件、启动文件、时钟配置、串口驱动、模型文件路径全部要跟 AT32F435 对齐。所以这篇文章没有直接说“下载例程烧录就好”而是按从零搭建的方式讲。这样你拿到任意一款国产 MCU只要硬件资源够都能照着这个流程迁移过去。AT32 的 BSP 结构跟 STM32 标准库风格很像如果你之前在 STM32 上开发过过渡到 AT32 几乎无感。2. 跑通TinyML需要准备的完整工具链2.1 PC端环境Python、TensorFlow与tflite-micro先做训练侧准备。PC 端需要装 Python 3.8 以上版本然后安装 TensorFlow。我这边用的是 TensorFlow 2.x具体版本没太挑2.10 之后到 2.16 都能跑通。安装命令就是常规操作pip install tensorflow注意装的时候选 CPU 版本就够了模型极小用不到 GPU。训练期间 CPU 占用也会很轻不需要担心电脑扛不住。接着要拿一份 TFLite Micro 的运行时源码。现在的官方仓库已经独立出来在 GitHub 上的 tensorflow/tflite-micro直接 clone 下来备用git clone https://github.com/tensorflow/tflite-micro.git这份源码后面要用来给 MCU 工程提供推理运行时所以下载完不要随手删。如果你本地网络访问 GitHub 不方便也可以从 TensorFlow 主仓库的 tensorflow/lite/micro 目录拿到同样一套东西只是路径稍有不同。这里额外提醒一句如果是第一次接触 TinyML建议先把 PC 端的环境跑通再碰单片机。你在单片机上折腾了半天发现跑不出结果结果最后定位到是模型训练阶段就没对那就很浪费时间了。环境问题越早暴露越好。2.2 MCU端环境Keil工程与AT32 BSP单片机侧我用的是 Keil MDK。AT32F435 是 Cortex-M4 内核MDK 对它的调试支持很成熟。装完 MDK 之后需要去雅特力官网下载对应型号的 BSP 固件库关键词就是“AT32F435_437 Firmware Library”解压后里面有很多官方例程我拿一个最基本的 GPIO 或 UART 工程作为起点然后往里加东西。AT32 BSP 的工程文件结构大概是这样Firmware 目录下是标准外设库包含 at32f435_437.h 头文件、system_at32f435_437.c 时钟初始化文件以及各个外设驱动源文件。工程模板里已经帮我们配好了启动文件、链接脚本和 Flash 下载算法这部分不用自己折腾省了很多事。另外注意一点国产芯片在 Keil 里如果识别不到型号通常是两个原因一是 Pack 没装二是芯片选项里的 Flash 下载算法没选对。AT32 的 Pack 在官网下载安装后Keil 的 Device 列表里就能找到 AT32F435 系列。下载算法选 AT32F435 Flash 对应的算法否则烧录会失败。2.3 怎么搭建“PC训练MCU部署”的目录结构我习惯把训练脚本和 MCU 工程分开目录但模型文件要能快速同步。我的项目目录长这样AT32F435_TinyML/ ├── train/ │ ├── train_sine.py │ ├── convert_to_tflite.py │ └── model_data.cc ├── mcu_project/ │ ├── /AT32F435_437/... │ ├── /User/main.c │ ├── /User/model_data.cc │ └── /ThirdParty/tflite-micro/...train 目录统一管理训练和转换脚本model_data.cc 可以手动拷贝到 mcu_project/User 目录。tflite-micro 源码我直接放在 mcu_project/ThirdParty 里面方便 Keil 引用。这种拆分有个好处训练代码和嵌入式代码互不干扰换芯片时只需要重新处理 mcu_project 里面的东西train 目录可以原样复用。后面如果真的要在其他 MCU 上跑同一个模型直接复制工程比重新训练更快。3. 模型训练、量化与导出从正弦波到model_data.cc3.1 训练数据怎么生成正弦波模型的训练数据不需要真实采集用 numpy 直接生成即可。标准做法是在 0 到 2π 区间内均匀取 1000 个点计算对应的正弦值然后作为输入和标签。import numpy as np import tensorflow as tf sampling_count 1000 x_values np.linspace(0, 2 * np.pi, sampling_count, dtypenp.float32) y_values np.sin(x_values).astype(np.float32) x_train x_values.reshape(-1, 1) y_train y_values.reshape(-1, 1)这里有一个细节numpy 的 linspace 默认是 float64但神经网络模型里通常用 float32 就够而且后面量化时更平滑所以我一上来就把数据类型转成了 float32。如果这里不转换训练时会多一步自动类型转换虽然也能跑但数据来回转类型容易埋坑。为什么采样 1000 个点因为正弦函数本身很平滑1000 个点足够覆盖 0 到 2π 区间里的特征了。实际你多采样一倍、两倍最终模型精度提升不大反而训练时间变长。对于这种玩具级任务数据量不是瓶颈。3.2 网络结构设计为什么两层隐藏层就够正弦函数是一个连续的非线性函数理论上三层全连接网络输入层一个隐藏层输出层就能拟合得很好。我实际用的是两层隐藏层每层 16 个神经元激活函数选 ReLU输出层不加激活直接输出数值。model tf.keras.Sequential([ tf.keras.layers.InputLayer(input_shape(1,)), tf.keras.layers.Dense(16, activationrelu), tf.keras.layers.Dense(16, activationrelu), tf.keras.layers.Dense(1) ]) model.compile(optimizeradam, lossmse) history model.fit(x_train, y_train, epochs500, verbose0)这个网络总共有多少个参数算一下第一层是 1×16 个权重再加 16 个偏置一共 32 个参数第二层是 16×16 再加 16共 272 个参数输出层是 16×1 再加 1共 17 个参数。加起来 321 个参数。这 321 个浮点参数转成 int8 量化后再浓缩一下模型文件只有几百字节放在 MCU 的 Flash 里几乎可以忽略不计。这也是这个例子适合入门的原因之一你完全可以拿着计算器算出模型的资源占用不会出现“怎么看都觉得不对劲”的玄学问题。训练 500 个 epoch 后loss 一般能降到 10⁻⁴ 量级。如果你发现 loss 降不下去可以把神经元扩到 32或者把 epoch 加到 1000。但也不用追求极致精度这不是要参加竞赛理解流程比堆指标重要。3.3 int8量化与模型转换模型训练完后下一步是转成 TensorFlow Lite 格式并做 int8 量化。为什么要量化因为 MCU 上没有 GPU也没有大内存去存 float32 权重int8 量化能把模型体积缩小到原来的四分之一推理速度也更快。def representative_dataset(): for _ in range(100): data np.random.uniform(0, 2 * np.pi, (1, 1)).astype(np.float32) yield [data] converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] converter.representative_dataset representative_dataset converter.target_spec.supported_ops [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] tflite_model converter.convert() with open(model.tflite, wb) as f: f.write(tflite_model)这里最关键的是 representative_dataset。量化过程需要一些“真实输入代表”来确定每个浮点数值对应的定点范围它不需要很多100 个随机采样点足够。如果没有这个校准数据集量化会退化成纯动态范围量化精度会受影响。还有一点要注意target_spec.supported_ops [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]这个配置会强制模型全部转成 int8包括输入和输出张量。这会让 MCU 端推理更快但代价是输入输出不再是浮点MCU 端代码需要做定点与浮点的换算。如果你想省掉换算的麻烦可以不写这一行让转换器保留 float32 输入输出只量化权重。两种方案都能跑但部署代码不一样要根据实际情况选。我对这个模型是强制全 int8 的因为想测一测国产 MCU 在纯定点推理下的表现。后面第四部分会给出对应的输入输出换算代码。3.4 导出C数组模型文件变成头文件模型文件 model.tflite 现在只是二进制文件MCU 上一般没有文件系统所以我们需要把它变成 C 语言的字节数组直接和固件一起编译烧录。最简单粗暴的方式是用 xxd 工具转换xxd -i model.tflite model_data.ccLinux/macOS 自带 xxd。Windows 下可以用 Python 替代with open(model.tflite, rb) as f: data f.read() with open(model_data.cc, w) as f: f.write(const unsigned char model_data[] __attribute__((aligned(16))) {\n) for i in range(0, len(data), 12): chunk data[i:i12] f.write(, .join(f0x{b:02x} for b in chunk)) f.write(,\n) f.write(};\n) f.write(fconst unsigned int model_data_len {len(data)};\n)同时生成一个 model_data.h 声明外部变量#ifndef MODEL_DATA_H #define MODEL_DATA_H extern const unsigned char model_data[]; extern const unsigned int model_data_len; #endif这里我特意加了一个__attribute__((aligned(16)))这个对齐属性是给后面 MCU 端加载模型时用的。TFLite Micro 在解析模型时有时会做一些对齐访问不对齐可能触发 hardfault提前标上能省不少排查时间。3.5 在PC端先验证量化模型在把模型搬到 MCU 之前一定要先在 PC 端用 TensorFlow Lite 的 Python 接口验证一下量化后的模型输出是否还正常。这一步能帮你把“模型有问题”和“MCU端代码有问题”隔离开。import tensorflow as tf import numpy as np interpreter tf.lite.Interpreter(model_pathmodel.tflite) interpreter.allocate_tensors() input_details interpreter.get_input_details() output_details interpreter.get_output_details() print(input shape:, input_details[0][shape], dtype:, input_details[0][dtype]) print(output shape:, output_details[0][shape], dtype:, output_details[0][dtype])然后选几个点做推理把输出反量化成 float 再画出来对比 sin 曲线。如果这一步的曲线和真实正弦曲线差太多说明量化出了问题需要回去调整 representative_dataset 或数量。如果这一步正常那后面 MCU 上的问题就可以更自信地去排查。4. 在AT32F435上部署工程集成与推理代码4.1 把tflite-micro运行时加进Keil工程这一节是整个部署过程里最繁琐的部分没经验的人很容易在这一步卡住。我的做法是把 tflite-micro 源码作为第三方库直接塞进 Keil 工程。打开 Keil 工程新建一个组Group叫tflite_micro然后把下列源文件添加进去tensorflow/lite/micro/micro_interpreter.cpptensorflow/lite/micro/micro_error_reporter.cpptensorflow/lite/micro/micro_allocator.cpptensorflow/lite/micro/all_ops_resolver.cpptensorflow/lite/micro/micro_mutable_op_resolver.cpptensorflow/lite/core/api/flatbuffer_conversions.cpptensorflow/lite/core/api/op_resolver.cpptensorflow/lite/core/api/tensor_utils.cpptensorflow/lite/micro/kernels/fully_connected.cpptensorflow/lite/micro/kernels/softmax.cpptensorflow/lite/micro/kernels/add.cpptensorflow/lite/micro/kernels/mul.cpp这里到底要加多少文件取决于你模型里用了哪些算子。我们的正弦波模型只有全连接层和可能被融合掉的 ReLU理论上只需要 fully_connected 和配套的文件就够了。但我图省事直接用了AllOpsResolver把所有算子都编译进去这样后面换模型时不需要再回头调整源文件列表。代价是代码体积会大一些但对 AT32F435 的 512KB Flash 来说完全不是问题。接下来设置 include 路径。在 Keil 的 C/C 选项卡里把 tflite-micro 源码的根目录、tensorflow 目录、third_party 目录都加进去否则编译器会找不到头文件。我记得大致的路径是tensorflow/lite/micro tensorflow/lite/micro/kernels tensorflow/lite/micro/examples/hello_world tensorflow/lite tensorflow/lite/third_party/flatbuffers/include如果编译时报找不到某个 .h不要慌按报错路径往上找把对应的 include 目录加进去就行。这一步本质就是“养编译器”报一个加一个很快就能过。另外一个很容易踩的坑是 Keil 默认的 C99 和 C 标准。tflite-micro 的源码是 C要确保源文件后缀是 .cpp并且在工程里对应的组属性里加上--cpp11或更高版本编译选项。我用的是 AC6 编译器加上--c11后顺利通过。4.2 推理主流程代码从加载模型到逐点预测模型文件进来后写推理主流程就很直白了。核心步骤是分配 tensor arena、创建 interpreter、加载模型、把输入数据填进去、调用 Invoke、读取输出。下面这段代码就是我最终跑通的精简版流程#include stdio.h #include at32f435_437.h #include tensorflow/lite/micro/all_ops_resolver.h #include tensorflow/lite/micro/micro_error_reporter.h #include tensorflow/lite/micro/micro_interpreter.h #include tensorflow/lite/schema/schema_generated.h #include model_data.h static constexpr int kTensorArenaSize 16 * 1024; static uint8_t tensor_arena[kTensorArenaSize]; static tflite::MicroErrorReporter micro_error_reporter; static tflite::AllOpsResolver resolver; static tflite::MicroInterpreter *interpreter nullptr; static TfLiteTensor *input_tensor nullptr; static TfLiteTensor *output_tensor nullptr; extern C void tiny_ml_init(void) { const tflite::Model *model tflite::GetModel(model_data); if (model-version() ! TFLITE_SCHEMA_VERSION) { while(1); } interpreter new tflite::MicroInterpreter( model, resolver, tensor_arena, kTensorArenaSize, micro_error_reporter); TfLiteStatus status interpreter-AllocateTensors(); if (status ! kTfLiteOk) { while(1); } input_tensor interpreter-inputs(0); output_tensor interpreter-outputs(0); }注意这里的interpreter用了裸指针和 new。这是因为 TFLite Micro 的 MicroInterpreter 依赖 tensor arena 里面预先分配好的内存不能随随便便作为栈变量而且它的生命周期要覆盖整个推理过程。写单片机程序时很多人习惯把对象定义成static这没问题但要注意 MicroInterpreter 不能频繁构造和析构否则 tensor arena 会被反复申请和释放可能踩到内存碎片。推理部分我单独抽了一个函数extern C float tiny_ml_predict(float x) { float input_scale input_tensor-params.scale; int input_zero_point input_tensor-params.zero_point; float output_scale output_tensor-params.scale; int output_zero_point output_tensor-params.zero_point; if (input_tensor-type kTfLiteInt8) { int8_t q (int8_t)(x / input_scale input_zero_point); input_tensor-data.int8[0] q; } else { input_tensor-data.f[0] x; } TfLiteStatus status interpreter-Invoke(); if (status ! kTfLiteOk) { return 0.0f; } if (output_tensor-type kTfLiteInt8) { int8_t q output_tensor-data.int8[0]; return (q - output_zero_point) * output_scale; } return output_tensor-data.f[0]; }这段代码里最关键的是量化换算。int8 模型的输入需要把浮点 x 先除以 scale 加上 zero_point转成 int8输出则需要反向操作把 int8 结果减掉 zero_point 再乘 scale才能得到真实的浮点预测值。很多人在 MCU 上跑出“一条直线”或者“满屏乱码”十有八九是这里偷懒了直接把 data.int8 的值当成结果打印。4.3 验证手段一串口绘图跑通推理后最直观的验证方式是把输入 x 和预测值一起通过串口发出来在 PC 端画图看波形。AT32F435 的串口输出我用的是标准库的 printf 重定向。将 fputc 重定向到 USART 发送寄存器即可int fputc(int ch, FILE *f) { while(usart_flag_get(USART1, USART_FLAG_TDBE) RESET); usart_data_transmit(USART1, ch); return ch; }然后在主循环里这样输出for (int i 0; i 100; i) { float x (2.0f * PI * i) / 100.0f; float y tiny_ml_predict(x); printf(%.4f;%.4f\n, x, y); delay_ms(50); }PC 端我用串口助手把数据保存成 CSV然后画图也可以用 Arduino IDE 自带的 Serial Plotter 工具把串口接到虚拟串口选好波特率直接就能看到动态曲线。我用的是 115200 波特率实测很稳。如果串口没数据优先检查三处波特率是否匹配、波特率寄存器是否因时钟配置错误而产生偏差、printf 是否真的被重定向到了串口而不是默认的 debug 通道。AT32 的时钟配置里面如果 PLL 没设置对系统主频跑了 288MHz 但外设总线时钟没跟上串口就会变成乱码。4.4 验证手段二DAC输出模拟波形串口画图虽然直观但毕竟还要转一道线。如果想要更“硬核”一点可以直接用 AT32F435 内置的 DAC把模型预测值输出成模拟电压信号用示波器看波形。AT32F435 的 DAC 分辨率是 12 位输出范围通常是 0~3.3V。正弦预测值在 -1 到 1 之间需要先映射到 0~3.3V 的电压范围float voltage (y * 1.65f) 1.65f; // -1~1 - 0~3.3V uint16_t dac_val (uint16_t)(voltage / 3.3f * 4095.0f); dac_data_set(DAC1, DAC_ALIGN_12B_R, dac_val);然后让主循环以一个固定的时间步长喂 xDAC 就会输出连续的正弦波。把示波器探头接到 DAC 对应引脚AT32F435 的 DAC 输出脚一般是 PA4就能看到一条平滑的正弦曲线。这个输出效果比串口绘图更有“在硬件上跑 AI”的真实感。4.5 验证手段三LED亮度呼吸灯如果想在没有任何外部设备的情况下演示连示波器都不想接那可以把预测值映射成 PWM 占空比让 LED 亮度跟随正弦波呼吸。虽然看起来只是一个呼吸灯但本质上和“AI 控制灯光亮度”没什么区别。我在定时器输出比较通道上配了一个 PWM 输出频率设成 1kHz占空比根据预测值实时更新timer_channel_value_config(TMR1, TMR_SELECT_CHANNEL_1, (uint32_t)((y * 0.5f 0.5f) * 999));这样整个系统跑起来LED 会从一个非常暗的状态开始慢慢变亮再慢慢变暗循环往复。不懂的人看着就是一个呼吸灯但内核其实是一个神经网络正在单片机上做推理。5. 常见问题与排查技巧实录5.1 编译报错的两种典型原因Keil 编译 tflite-micro 最容易踩的坑第一是没开 C11 导致一些模板语法报错第二是文件路径没配齐导致头文件找不到。前者修改编译选项即可后者需要根据报错信息逐个添加 include 路径。还有一种情况是模型数组太大编译时直接撑爆了优化选项的内存预算。Keil 的 AC6 编译器在默认优化级别下对超大数组有时会提示 “section overflow”。当时的解法有两种一是把 tflite-micro 相关源文件的优化等级调到-O2二是把 tensor arena 从全局数组改成 static 局部数组缩小作用域。实测下来开-O2后工程体积能明显下降Flash 占用从 160KB 左右降到 110KB 上下。5.2 运行时卡死先看AllocateTensors返回值如果程序烧录后跑不起来第一步不是去翻 Debug而是盯紧AllocateTensors()的返回值。我在调试时遇到过kTfLiteError定位结果是 tensor arena 给得太小了。模型虽然很小但 TFLite Micro 在内部还要为中间状态分配内存对 arena 的需求比单纯模型文件体积大好几倍。我当时从 8KB 改成 16KB 就好了。判断 arena 是否够用有个标准做法把 kTensorArenaSize 从很小开始比如 4KB然后不断往上加直到 AllocateTensors 返回 kTfLiteOk。这个值就是当前模型的最小需求。对于我们的正弦波模型经验值是 8KB 以上给 16KB 最保险。5.3 输出数值不对八成是量化换算问题模型能跑、串口也能出数但画出来的曲线和 sin 对不上这种情况我见得太多了。最常见的原因就是前面提到的 int8 输入输出没有做 scale/zero_point 换算。排查很容易在代码里打印模型输入输出的 scale 和 zero_pointprintf(input scale %f, zero %d\n, input_tensor-params.scale, input_tensor-params.zero_point); printf(output scale %f, zero %d\n, output_tensor-params.scale, output_tensor-params.zero_point);如果 scale 是 0.0235294、zero_point 是 0 这类带小数的数说明模型确实是 int8 模型必须做换算。如果 scale 是 1.0、zero_point 是 0那就说明你的模型根本没量化成功输入输出仍是浮点类型代码也要走另一条分支。5.4 下载失败与芯片锁死救砖方法搞嵌入式的人都知道改完时钟配置后第一次下载程序最容易碰到 SWD 连接不上。AT32F435 如果引脚被复用成其他功能或者进入某种异常状态调试器可能就识别不到芯片了。救砖方法很简单把 BOOT0 引脚拉高重新上电让芯片进入系统存储器引导模式然后用雅特力官方提供的 ISP 工具连接串口或 USB把 Flash 全擦除再拉低 BOOT0 重启芯片就恢复正常了。这个操作我至少用过两次一次是改了 SWD 引脚复用到 SPI一次是 PLL 配置写错导致主频跑飞。建议所有玩 AT32 的朋友都记住这个操作关键时刻非常救命。5.5 代码体积优化与推理速度实测最后聊一下性能。tflite-micro 运行时加上模型文件用 AllOpsResolver 编译出来的固件在 Flash 里大概占 100KB 多用 MicroMutableOpResolver 只注册需要算子后能再压缩几十 KB。如果你对 Flash 占用敏感建议改用后者static tflite::MicroMutableOpResolver10 resolver; resolver.AddFullyConnected();推理耗时我用 DWT 计数器测过在 288MHz 主频下这个 321 参数的小模型每次推理大约几百微秒具体数值和 Keil 编译优化等级有关。这个速度对传感器数据分类、异常检测这类实时性要求不高的应用来说完全够用。真正耗时的场景是跑卷积神经网络或连续音频处理那就需要另想办法了比如用 CMSIS-NN 优化算子。最后再分享一点小经验这个项目看着简单但它把“训练一个神经网络”和“在单片机上做推理”这两件事之间的桥真正搭起来了。我最大的体会是TinyML 的门槛不在模型训练也不在 C 语言而在于你能否静下心把整个工具链捋顺。tflite-micro 的源码很开放编译不过就查路径跑不对就查量化下载不了就 ISP 擦除每一步都有规律。如果你也准备入坑我的建议是不要想着一步到位跑语音识别或摄像头模型先把这个正弦波 Helloworld 完完整整跑通再换一个你实际场景里的简单模型思路是完全一样的。国产 MCU 的算力和外设在逐渐变强这套方法论会越来越实用。