国产MCU跑TinyML:基于AT32F435的正弦波预测实战

发布时间:2026/9/8 12:41:15
国产MCU跑TinyML:基于AT32F435的正弦波预测实战 1. 为什么要在国产MCU上玩TinyML1.1 TinyML到底是啥和树莓派上跑AI有什么区别TinyML字面意思是“极微型机器学习”。它的核心不是让你用GPU训练大模型而是把训练好的神经网络模型塞进一个只有几百KB Flash、几十KB内存的单片机里做推理Inference。和你在树莓派、手机上跑AI完全不同的是MCU上没有操作系统没有大内存甚至很多时候没有文件系统所有东西都得精打细算。拿我们这次的目标AT32F435来说它是一颗ARM Cortex-M4F内核的国产MCU主频最高288MHz带FPU浮点运算单元。这个性能跑Linux那是做梦但跑一个参数不到100个的小型全连接神经网络绰绰有余。正弦波预测模型恰恰是TinyML社区最经典的“Hello World”就像学编程先打印一行Hello World一样搞TinyML先让MCU学会预测一个sin函数把环境搭建、模型转换、推理部署整条链路跑通比什么都重要。1.2 选择AT32F435做实验平台的理由市面上做TinyML实验大家第一反应都是STM32F746或者Nucleo系列因为TensorFlow Lite for Microcontrollers官方直接支持。但我想说国产MCU这几年进步真的很大AT32F435就是一股不可忽视的力量。选它有几个硬理由主频288MHz还是Cortex-M4F带FPU。这个算力跑小模型绰绰有余甚至有点浪费。512KB Flash 224KB SRAM的型号很常见内存比很多同价位MCU大一圈不用天天纠结变量放哪。价格相对友好芯片容易买开发板也不贵。有成熟的AT32 Work Bench图形化初始化工具类似STM32CubeMX能快速生成外设初始化代码。说实话用国产MCU跑AI这件事技术难度不在于“能不能跑”而在于“有没有人把路蹚出来”。我希望这篇实测能给想试水的朋友省点时间。1.3 为什么“正弦波”会是TinyML的Hello World很多人不理解预测个正弦波有什么了不起的MCU自带的数学库math.h里一个sin()函数就搞定了何必折腾一个神经网络上去这个想法没错但搞错了重点。TinyML正弦波例子的价值不在“替代sin函数”而是用最简单的输入输出验证整套流程是否通畅。它的套路是这样输入一个角度值x范围是-\pi到\pi。输出对应sin(x)的预测值。用一个带隐藏层的小网络拟合S形曲线。整个过程就是一个最精简的“端到端AI落地”模型PC端训练、导出权重、C数组读入、MCU端加载、推理、输出结果。如果这坨流程你能完整跑通那以后换成“轴承异常检测”“语音关键词唤醒”“手势识别”等真实场景核心经验完全可以复用。碰到了问题也好查因为模型够小算一下就知道哪里不对。2. 方案选型TFLM框架还是手写推理2.1 标准Hello World长什么样TensorFlow Lite for Microcontrollers官方仓库里有一个hello_world示例结构非常清晰先用PC端TensorFlow训练一个预测正弦波的全连接网络然后通过TensorFlow Lite Converter转成tflite模型再转换成C字节数组最后在MCU上通过TFLM解释器加载并执行推理。整个过程看起来很美好但实际落地时要注意AT32F435并不在TFLM官方支持列表里。这意味着你不能像STM32F746那样直接打开示例工程编译运行得自己移植平台层代码至少要让TFLM的C库在你的编译环境下正确编译还要把tflite模型数组放对位置。2.2 我为什么不绕道移植TFLM而是手写推理当然TFLM这个框架很强大它带了一个轻量级解释器可以动态加载模型、分配张量非常灵活。但对于“正弦波预测”这种规模的问题TFLM是杀鸡用牛刀。我最终选择手写推理直接实现全连接网络的前向传播。原因有三个第一模型实在太小一条主线串下来代码量极少。我们网络结构是“1个输入节点 → 16个ReLU隐藏节点 → 1个输出节点”总参数只有49个手写矩阵乘法加ReLU激活函数不超过30行C代码。第二手写推理能让你彻底看清神经网络在MCU上到底做了什么。外部框架封装得再好你也只是“会用”真出了诡异问题还是无从下手。手写一遍之后权重怎么排、偏置怎么加、激活函数在哪个位置生效全部一清二楚。第三省去一堆链路上的坑。TFLM库本身要占不少Flash还有动态内存分配需求。对AT32F435这种“虽然内存不小但完全没必要浪费”的场景手写推理更轻、更快、更好控制。2.3 整体数据流从训练到示波器在动手之前我先把整条链路捋了一遍。整个工程分两大块PC端和MCU端。PC端负责用PythonTensorFlow训练模型然后把权重导出成C头文件。MCU端做三件事通过定时器产生等间隔的相位输入调用手写推理函数得到预测的sin值最后通过USART打印到串口绘图软件或者通过DAC输出模拟电压接到示波器。这里有个容易被忽略的关键点模型训练的输入范围必须和实际推理时的输入范围一致。训练时我把x归一化到-\pi到\piMCU端推理时的phase也保持在同一个范围内否则模型会“看到”分布外数据输出就会莫名其妙。3. PC端模型训练与权重导出3.1 生成训练数据和Keras网络搭建模型训练这一步没什么高深的我的做法是用Keras搭一个两层全连接网络。隐藏层节点数直接选了16个激活函数用ReLU输出层不加激活函数做回归任务。损失函数用MSE优化器用Adam训练数据就是在-\pi到\pi之间均匀采样的1000个点标签是标准的sin(x)。import numpy as np import tensorflow as tf # 生成训练数据x 在 [-pi, pi] 均匀采样y sin(x) x_train np.linspace(-np.pi, np.pi, 1000).astype(np.float32) y_train np.sin(x_train).astype(np.float32) # 构建模型输入1维隐藏层16个ReLU节点输出1维 model tf.keras.Sequential([ tf.keras.layers.Dense(16, activationrelu, input_shape(1,)), tf.keras.layers.Dense(1) ]) model.compile(optimizeradam, lossmse) model.fit(x_train, y_train, epochs200, verbose1)关于隐藏层节点数我试过8、16、32。8个节点拟合出来的曲线在峰值附近有明显偏差32个节点精度提升有限但参数量翻倍16个节点在精度和规模之间最平衡。你也完全可以从16起步跑完看误差再调。3.2 训练结果与权重检查P200轮训练loss基本能降到0.0001以下换算成RMSE大约在0.01左右也就是说预测值和真实sin值之间的平均误差大约1%。对于演示用途这个精度已经足够。这里我强烈建议训练完之后把预测值和真实值一起画出来看一眼别只看loss数字。神经网络的loss低不代表曲线形态好万一在边界处外插发散后面MCU上看到波形就是歪的。我当时还专门打印了输入为\pi和-\pi时的预测值确认它们都在0附近稳定输出才放心往下一步走。3.3 把权重导出成C头文件权重就是模型各层的参数矩阵和偏置项。Keras里通过model.get_weights()就能拿到返回值是一个Python列表每个元素是一个NumPy数组。我写了个小脚本把这些浮点数格式化输出成C语言数组。# 导出为C语言头文件格式 weights model.get_weights() w1, b1, w2, b2 weights def arr_to_c(name, arr): flat arr.flatten() body , .join(f{v:.8f}f for v in flat) print(fconst float {name}[{flat.shape[0]}] {{{body}}};) arr_to_c(w1, w1) arr_to_c(b1, b1) arr_to_c(w2, w2) arr_to_c(b2, b2)这里有个细节Keras的Dense层默认权重形状是(输入节点数, 输出节点数)。所以w1的形状是(1,16)b1是(16,)w2是(16,1)b2是(1,)。如果直接在MCU端套公式很容易搞反维度我建议打开头文件核对一下数组长度。4. AT32F435端工程搭建4.1 用AT32 Work Bench初始化时钟和外设MCU端工程我是用AT32 Work Bench生成的这个工具和STM32CubeMX逻辑几乎一样选型号、勾外设、配时钟、设引脚然后Generate Code。我的配置思路是时钟用外部高速晶振作为时钟源PLL倍频到288MHz这是这颗芯片的最高主频也是体现算力优势的点。USART1波特率1152008位数据、1位停止位、无校验用于向串口绘图软件发送推理结果。DAC1输出模式选择独立输出12位右对齐用于把预测值变成模拟电压接示波器看波形。TMR6基本定时器定时周期设为100us作为采样节拍产生中断在中断里推进相位并触发一次推理。AT32的库函数命名和ST有一定差异但整体思路是相通的。如果之前写过AT32的基础外设驱动这部分二十分钟就能搞定。如果没写过先用官方例程点灯和串口打印跑通了再往下加功能。4.2 移植推理代码到MCU工程推理代码就是照着神经网络前向传播的公式翻译成C。全连接ReLU的前向传播本质是两次矩阵乘法和一次激活函数核心代码就两层循环。#define HIDDEN_SIZE 16 // 模型权重由Python脚本导出的头文件 const float w1[1 * HIDDEN_SIZE] { ... }; const float b1[HIDDEN_SIZE] { ... }; const float w2[HIDDEN_SIZE] { ... }; const float b2[1] { ... }; float model_predict(float x) { float hidden[HIDDEN_SIZE]; // 第一层x * w1 b1, 然后ReLU for (int i 0; i HIDDEN_SIZE; i) { float s b1[i] x * w1[i]; // 输入是1维所以w1顺序取就可以 hidden[i] (s 0.0f) ? s : 0.0f; } // 第二层hidden * w2 b2线性输出 float y b2[0]; for (int i 0; i HIDDEN_SIZE; i) { y hidden[i] * w2[i]; } return y; }初始化外设之后在while循环里不断调用model_predict就能得到正弦波预测值。这种极简推理函数的执行时间极短后面我会专门测。4.3 主循环设计预测、输出、控制节奏主循环逻辑看起来简单实际上有几个细节值得说道。第一个细节是相位累加。我定义了一个phase变量每次定时器中断或主循环累加一个步长stepstep大小决定了输出正弦波的频率。计算公式是step 2 * PI * 目标频率 / 采样率比如采样率设为10kHz想输出100Hz的正弦波那每个周期有100个采样点step就是2\pi/100。如果采样率改成10kHz但step不变输出频率就会跟着变调试时很容易搞混。第二个细节是输出映射。模型预测值范围基本在-1到1之间直接发给串口或者塞给DAC都是不对的。串口绘图我直接输出预测值和真实值方便对比DAC输出则要做一次线性映射把-1到1映射到0到4095的12位数字量公式是dac_value (y_predict 1.0f) / 2.0f * 4095.0f;第三个细节是节奏控制。我初期图省事直接在while循环里跑预测、改DAC输出发现波形频率完全不可控CPU空转速度时快时慢。后来改用定时器中断方式主循环里设置一个标志位中断到了置位主循环才做一次推理和输出这样采样节奏精确稳定波形才不会抖动。5. 实测数据与效果还原5.1 资源占用Flash、RAM、推理耗时先把大家最关心的数据摆出来。工程我用Keil MDK编译AC5编译器O2优化等级。Flash占用约28KB其中包含了初始化代码、串口驱动、DAC驱动、printf重定向和推理函数。如果只算模型推理部分其实连1KB都用不到。RAM占用约4KB主要消耗在printf缓冲、DAC数据处理和系统栈上。手写推理的模型数组是const常量存放在Flash不占RAM。单次推理耗时这个我单独测了。用DWT-CYCCNT寄存器读CPU周期计数在调用model_predict前后各读一次相减得到141个周期。288MHz主频下换算一下单次推理耗时约0.49微秒。这个性能表现说明一个问题对这种几十个参数的小模型AT32F435的算力是严重过剩的。你用一根主循环空转的时间就能推理几百次。真正限制吞吐的不是CPU而是外设输出速度。5.2 串口绘图与DAC演示波形验证效果我做了两种方式。第一种是用串口绘图。USART以115200波特率持续输出“预测值,真实值”的文本行PC端打开串口绘图工具直接两条曲线对比。可以看到预测曲线和真实sin曲线几乎重叠相位上也没有明显滞后。这里要注意串口文本格式化其实非常耗时一个浮点数用printf格式化加输出大概要几百微秒所以我不建议用串口测实时性能它只适合看波形走势。第二种方式更直接。我把DAC输出引脚接到示波器上跑起来能看到一条平滑的正弦波频率和设计值一致。示波器还可以顺便测一下DAC输出的毛刺情况因为预测本身没有突变波形整体非常干净只有极轻微的量化台阶感。5.3 精度分析模型误差与DAC量化误差示波器上看到的波形虽然平滑但细看还是有误差这口锅不能全让模型背。误差来源主要有两个一是模型本身的能力上限。16个ReLU节点拟合正弦函数在曲线峰值附近会出现轻微“圆不够圆”的情况直观感受就是波形顶点处有一点点削平趋势。正常训练MSE在0.0001左右实际输出误差在正负0.02以内。二是DAC的12位量化误差。3.3V参考电压12位分辨率对应步长约0.8mV这个误差相对模型误差来说几乎可以忽略但如果你用示波器放大波形还是能看见阶梯状纹理。如果想要更高精度可以把隐藏层加到24或32个节点训练误差还能再降一个数量级。不过在288MHz主频下这点算力开销依旧可以忽略。6. 踩坑记录与扩展建议6.1 我踩过的几个坑这一路也踩了不少坑有些坑属于新手必踩写出来给大家排雷。第一个坑是printf重定向。AT32的工程默认没有把printf接到串口直接调用printf屏幕上什么都不会有。Keil环境里需要开启MicroLib然后重写fputc函数把输出字符通过USART发送出去。这一步不做后面所有串口数据都是空的。第二个坑是数据类型的size。我最初导出权重时在Python里用了float64C端用了float两边不匹配导致每次打印结果都有微小的随机偏差。后来统一在Python端用astype(np.float32)强制转换问题立刻消失。第三个坑是DAC参考电压。AT32F435的DAC参考电压默认是VREF引脚电平如果你的开发板VREF不是干净稳定的3.3V示波器上会出现随电源噪声抖动的波形。我当时就是随手接了一根杜邦线结果波形噪声很大后来换用开发板上的AVREF跳线帽短接到VDD问题才解决。第四个坑是相位累加的精度。float类型在累加过程中会逐渐累积舍入误差长时间运行后phase可能超出-\pi到\pi范围导致模型外推输出异常。解决办法是每次累加后检查范围超了就减掉2\pi保证相位始终在有效区间。6.2 想跑更大模型/更快速度怎么改正弦波模型跑通之后很多人问的第一句话就是“我想跑一个更强的模型怎么办”。从手写推理转向更强的模型我建议分三步走。第一步优先用CMSIS-NN。AT32F435的Cortex-M4F内核完全兼容ARM的CMSIS-DSP/CMSIS-NN软件库这个库为Cortex-M做了深度优化同样一个全连接层计算速度快几倍很正常。你只需要把矩阵乘法替换成arm_fully_connected_float比纯手写性能好很多。第二步从浮点模型切到Int8量化模型。TFLM库官方支持Int8量化推理8位整数运算一方面能减小模型体积另一方面在保持足够精度的前提下可以利用ARM的SIMD指令加速。不过对AT32F435这种带FPU的芯片来说小规模模型的浮点运算本来就快量化带来的收益主要是缩小Flash和RAM占用。第三步如果模型大到一个MCU放不下就需要考虑模型压缩或者知识蒸馏。把大网络在PC端训练好然后把大网络的输出作为小网络的训练目标。这个路线比直接在小网络上硬训练更容易收敛效果也更好。6.3 把正弦波换成真实场景怎么落地TinyML的终极目标当然不是画波形。跑通了正弦波这个Hello World你其实已经拥有了一个完整的端侧AI最小系统接下来的路就很清晰了。如果对传感器数据处理感兴趣可以尝试一个异常检测任务用加速度传感器采集振动数据在PC上训练一个分类网络然后把模型部署到AT32F435上实时判断设备状态。输入从单维相位变成高维特征数组你的推理函数需要扩展到矩阵乘法但整体链路不变。如果对信号处理感兴趣可以做一个关键词唤醒识别在PC上用特定数据集训练小规模音频分类模型量化后部署到MCU麦克风采集音频通过数字信号处理提取特征再送入网络做识别。如果对控制方向感兴趣可以做一个简单的PID参数自适应传感器读取实时误差小模型输出一组推荐PID参数主控制器根据预测值动态调整形成闭环。不管哪个方向套路都是一样的PC端训练模型、导出权重、嵌入MCU工程、完成前向推理、处理输出。我在正弦波模型这只“麻雀”上把整条链路摸透之后后面遇到真实项目就不会再心虚了。最后再分享一个小技巧。尽量把“模型推理”和“业务逻辑”用C文件彻底分开。model.c只放权重数组和predict函数不掺任何外设代码业务主循环放在另一个文件。这样以后模型迭代了重新生成头文件替换就行整个工程别的部分完全不用动。我做完正弦波这个项目之后因为模型换过好几个版本这个分层设计真的帮我省了大量时间。