1. 项目概述几年前我在一个老房子的阁楼里折腾我的第一个嵌入式项目。那是个雨天雨点敲打屋顶的声音特别清晰我一边调试代码一边听着这个背景音。当时我就想要是这个小小的开发板能“听懂”下雨然后自动帮我关窗或者打开氛围灯那该多酷。这个念头一直没实现直到我接触到了TinyML。简单来说TinyML就是让机器学习模型能在指甲盖大小的微控制器上跑起来不用联网几毫瓦的功耗就能完成实时推理。这彻底改变了嵌入式开发的玩法让“边缘智能”从概念变成了手头的项目。这次我决定用STMicroelectronics的STEVAL-STWINKT1B传感器节点开发板结合他们自家的NanoEdge AI Studio工具把当年那个“雨声识别”的想法给实现了。整个过程走下来从数据采集、模型训练到最终烧录进STM32踩了不少坑也总结了一套比较顺滑的流程。这篇文章我就把这套从零到一的实践过程拆开揉碎了讲给你听无论你是刚接触嵌入式还是想了解如何把AI塞进MCU应该都能找到有用的东西。核心就是用最少的资源干一件特定且智能的事。2. TinyML项目核心思路与工具选型2.1 为什么是TinyML而不是云端AI在做这个项目之前我也考虑过更“常规”的方案比如用开发板采集音频通过Wi-Fi上传到云端服务器做识别再把结果传回来。但这个方案有几个硬伤首先它依赖网络网络不稳定或者断了功能就废了。其次有延迟从采集、上传、处理到回传几百毫秒甚至更长的延迟对于某些实时应用是不可接受的。最后隐私和功耗也是大问题持续传输音频数据既不安全也对电池供电的设备极不友好。TinyML的思路正好相反把智能放在最边缘就地解决。模型直接在微控制器上运行无需网络响应速度在毫秒级功耗极低所有原始数据都在本地处理隐私性最好。当然代价是模型必须非常小通常只有几十KB到几百KB并且只能完成特定的、相对简单的任务比如识别特定的几种声音而不是像ChatGPT那样进行通用对话。对于“识别雨声”这种单一、明确的分类任务TinyML是绝配。2.2 硬件平台STEVAL-STWINKT1B 开发板解析工欲善其事必先利其器。我选择STEVAL-STWINKT1B后面简称STWIN板有几个关键原因集成度高它本身就是一个完整的传感器节点板载了数字麦克风IMP34DT05、运动传感器、环境传感器等。我们做音频识别它自带的MEMS麦克风质量足够省去了外接麦克风模块的麻烦。核心强大主控是STM32L4R9ZI这是一颗基于Arm Cortex-M4内核的微控制器主频120MHz拥有2MB Flash和640KB RAM。对于TinyML模型来说这个资源算是“豪华”配置了有足够的空间运行相对复杂的模型和预处理代码。生态支持好ST为这块板子提供了完整的TinyML工具链NanoEdge AI Studio和丰富的功能软件包FP-SNS-DATALOG1从数据记录到模型部署都有现成的例子和库能极大降低开发门槛。注意虽然这块板子资源丰富但我们在设计模型时依然要时刻绷紧“资源受限”这根弦。不能因为硬件好就随意设计大模型要养成面向MCU的优化思维这是TinyML开发者的基本功。2.3 软件工具链NanoEdge AI Studio vs. 其他方案模型训练工具有好几个选择我主要对比了NanoEdge AI Studio和Edge Impulse。Edge Impulse非常流行社区活跃提供从数据采集、标注、训练到部署的全流程Web IDE。它支持多种硬件平台生成的模型可以导出为TensorFlow Lite格式通用性更强。但有时对特定硬件的底层优化需要自己多费心思。NanoEdge AI StudioST的“亲儿子”工具专门为STM32系列优化。它的最大优势是高度集成和自动化。你几乎不需要关心模型结构是神经网络、随机森林还是其他只需要提供数据它就能自动搜索出最适合你硬件和数据的最优模型并生成直接可用的C库文件.a,.h。对于快速原型开发和专注于应用而非算法细节的工程师来说效率极高。我最终选择了NanoEdge AI Studio因为我们的硬件是固定的STWIN板用它能够获得最好的软硬件协同优化效果部署步骤也最简洁。它的工作流程可以概括为上传数据 - 自动学习/训练 - 验证性能 - 下载为嵌入式库。后面我们会详细走通这个流程。3. 数据采集项目成败的生命线几乎所有机器学习项目的老手都会告诉你同一句话垃圾进垃圾出Garbage in, garbage out。在TinyML项目里这句话的权重还要再乘以十倍。因为我们的模型容量小算力弱它更像一个在严格条件下工作的“特长生”而不是“通才”。如果训练数据不能很好地代表真实场景模型在设备上的表现一定会让你大跌眼镜。3.1 采集策略与实战要点为了采集到高质量的雨声数据我制定了以下策略并严格执行在真实环境采集我并没有从网上下载雨声音频而是直接把STWIN板放在我家的阁楼窗户边在多个不同的下雨天进行录制。这样能确保采集到的声音包含了真实环境中的背景噪音如远处的车声、风声、房间混响等模型学习到的才是“在我家阁楼听到的雨声”。固定设备与参数始终使用STWIN板自带的麦克风进行采集采样率固定为16kHz对于雨声识别足够。避免中途更换设备或调整增益确保数据的一致性。覆盖多样性雨量变化录制了小雨、中雨、大雨、暴雨等多种强度的雨声。距离与角度将板子放在离窗户不同距离0.5米 1米 2米和不同角度正对窗户侧对窗户的位置进行录制。干扰场景特意录制了“非雨声”但可能混淆的场景如水龙头流水声、风扇声、室内对话背景音、键盘敲击声、马路噪音等。这些将作为“其他”类或负样本。数据均衡确保“雨声”和“非雨声”的样本数量大致相当并且雨声内部不同强度、不同位置的样本数量也尽量均衡避免模型对某一种情况“过拟合”。3.2 两种数据采集方法详解ST提供了两种将开发板变为数据记录仪Data Logger的方法我都实践了。方法一使用高速数据记录功能包FP-SNS-DATALOG1这是功能更强大、更灵活的方法。你需要给开发板烧录一个特定的固件HSDatalog.bin之后板子可以通过USB虚拟串口或手机AppST BLE Sensor记录数据到SD卡。操作流程使用STM32CubeProgrammer将HSDatalog.bin固件烧录到STWIN板。插入MicroSD卡。通过USB连接电脑板子会被识别为一个虚拟串口。使用ST提供的Python脚本如hsdatalog_data_export.py与板子通信开始记录。你可以指定记录时长、传感器类型这里选择音频。记录结束后数据以二进制格式保存在SD卡中。需要使用另一个Python脚本hsdatalog_to_wav.py将其转换为标准的WAV文件。优点可以长时间离线记录数据直接存SD卡不占用电脑资源可以通过手机蓝牙控制部署灵活。缺点步骤稍多需要熟悉Python环境和命令行操作。方法二通过NanoEdge AI Studio直接串口采集推荐给新手这是更简单直接的方法尤其适合快速构建初始数据集。首先你需要给STWIN板烧录另一个固件例如项目包里的STWIN_DTMF_Classifier.bin这个固件实现了基本的音频采集和串口通信功能。用USB线连接板子和电脑。打开NanoEdge AI Studio在创建数据集的环节选择“从串口实时采集”。软件会自动识别板子的串口你点击“开始录制”然后制造声音比如播放雨声音频到麦克风前或者直接在雨天录制软件就会实时接收并保存音频片段。你可以一边采集一边给数据打上标签例如点击“Record”按钮时就标记为“Rain”类。实操心得我建议先用方法二快速收集一小批数据用于模型的原型验证和流程跑通。等整个流程验证无误后再使用方法一进行大规模、长时间、多样化的数据采集用于训练最终的生产级模型。这样效率最高。3.3 数据预处理与初步分析拿到一堆WAV文件后别急着扔给训练工具。先做一次“人工质检”和简单分析。听一遍随机播放一些样本确保录音质量没问题没有破音、奇怪的爆音。可视化分析写个简单的Python脚本看看你的数据“长什么样”。最常用的就是看波形图和频谱图FFT。import numpy as np import matplotlib.pyplot as plt from scipy.io import wavfile # 加载一个雨声样本和一个非雨声样本 rain_rate, rain_data wavfile.read(rain_light.wav) noise_rate, noise_data wavfile.read(keyboard.wav) # 转换为单声道如果是立体声 if len(rain_data.shape) 2: rain_data np.mean(rain_data, axis1) if len(noise_data.shape) 2: noise_data np.mean(noise_data, axis1) # 绘制时域波形对比 fig, axes plt.subplots(2, 2, figsize(12, 8)) axes[0, 0].plot(rain_data[:8000]) # 取前0.5秒16kHz采样率 axes[0, 0].set_title(Rain Sound - Time Domain) axes[0, 0].set_xlabel(Sample) axes[0, 0].set_ylabel(Amplitude) axes[1, 0].plot(noise_data[:8000]) axes[1, 0].set_title(Keyboard Sound - Time Domain) axes[1, 0].set_xlabel(Sample) axes[1, 0].set_ylabel(Amplitude) # 计算并绘制频域FFT对比 rain_fft np.abs(np.fft.rfft(rain_data[:8000])) noise_fft np.abs(np.fft.rfft(noise_data[:8000])) freqs np.fft.rfftfreq(8000, 1/16000) axes[0, 1].plot(freqs, rain_fft) axes[0, 1].set_title(Rain Sound - Frequency Domain) axes[0, 1].set_xlabel(Frequency (Hz)) axes[0, 1].set_ylabel(Magnitude) axes[0, 1].set_xlim([0, 8000]) # 聚焦于人耳可闻范围 axes[1, 1].plot(freqs, noise_fft) axes[1, 1].set_title(Keyboard Sound - Frequency Domain) axes[1, 1].set_xlabel(Frequency (Hz)) axes[1, 1].set_ylabel(Magnitude) axes[1, 1].set_xlim([0, 8000]) plt.tight_layout() plt.show()通过这个对比图你可能会发现雨声在低频部分比如200Hz以下和中高频比如2kHz-8kHz的分布与键盘敲击声有明显不同。这种物理特征上的差异正是模型能够学习并区分它们的基础。如果连人眼都能从频谱上看出区别那模型学会的概率就大得多。4. 模型训练与NanoEdge AI Studio实战数据准备好了就进入核心环节——训练模型。这里我们完全在NanoEdge AI Studio的Web界面中完成。4.1 项目创建与数据导入登录与新建访问NanoEdge AI Studio官网注册登录后创建一个新项目选择“N-class Classification”多分类类型因为我们要区分“雨声”和“其他声音”至少两类。选择硬件在目标设备中选择“STEVAL-STWINKT1B”。这一步至关重要工具会根据你指定的硬件资源CPU RAM Flash来优化和筛选模型。上传数据你可以将之前准备好的WAV文件按类别打包成ZIP文件上传。例如一个rain.zip里面全是雨声WAV一个other.zip里面全是其他声音。更推荐使用之前提到的串口实时采集功能。在Studio内启动采集用STWIN板现场录制并即时打标签。这种方式数据质量最高因为路径最短麦克风-开发板-Studio没有中间文件转换的潜在损失。信号参数设置告诉Studio你的音频参数。采样率16kHz、样本长度我设置了1秒即16000个点。样本长度是一个需要权衡的参数太短可能信息不足太长会增加计算量和延迟。1秒对于雨声识别是个不错的起点。4.2 模型训练与性能评估数据上传后Studio会自动进行预处理比如可能计算MFCC等特征。接下来点击“开始训练”。自动学习Studio会在后台尝试多种不同类型的微型机器学习算法如微型神经网络、决策树集成等并评估它们在你的数据和你的硬件上的表现。这个过程可能需要几分钟到几小时取决于数据量。理解输出报告训练完成后你会得到一份详细的报告其中最重要的两个部分是混淆矩阵一个表格展示了模型在测试集上的分类情况。理想情况下对角线上的数字正确分类应该很高非对角线的数字混淆错误应该很低。比如雨声被误判为“其他”的比例应该很小。交叉验证结果Studio会用类似K折交叉验证的方法评估模型的稳健性。你会看到一个平均准确率和波动范围。例如“准确率95% ± 3%”。这个结果比单次训练-测试分割更可靠。重要警告警惕过拟合如果报告显示准确率高达99.9%甚至100%先别高兴太早。这很可能是过拟合的标志——模型完美“记住”了训练数据但无法泛化到新的、没见过的声音上。避免过拟合的最佳方法就是我前面强调的用高质量、多样化、代表真实场景的数据集。如果出现过拟合你需要回去检查并扩充你的数据集。4.3 模型导出与库文件解析当你对模型的性能满意后就可以点击“编译”或“导出”。NanoEdge AI Studio会生成一个针对你的STWIN板优化好的“知识库”文件包主要包含三个文件libneai.a这是一个静态库文件里面封装了训练好的模型权重、推理函数以及所有底层计算代码。你不需要知道里面具体是什么只需要把它链接到你的工程里。NanoEdgeAI.h头文件提供了使用这个AI库的API接口。主要会用到初始化函数、推理函数等。knowledge.h或类似文件里面定义了一个巨大的常量数组这就是模型的“知识”——经过压缩和量化后的参数。libneai.a中的函数会使用这些参数进行计算。至此模型训练部分就完成了。你会发现我们几乎没写一行机器学习代码也没调整任何模型超参数这就是NanoEdge AI Studio的威力——让嵌入式工程师也能快速应用AI。5. 嵌入式工程创建与模型集成模型有了现在要把它塞进STM32的工程里并写出让整个系统跑起来的逻辑。5.1 使用STM32CubeMX配置硬件外设STM32CubeMX是一个图形化配置工具用来初始化微控制器的时钟、外设、引脚等并生成工程框架。我们的项目需要配置以下关键部分时钟树确保系统时钟、外设时钟正确设置。STM32L4R9ZI最高可运行在120MHz我们按此配置以获得最佳性能。音频采集STWIN板的数字麦克风IMP34DT05通过DFSDM数字滤波器用于Σ-Δ调制器接口连接到MCU的PB6引脚。在CubeMX中找到DFSDM1外设配置Filter 0并将其关联到Channel 0。配置Channel 0的数据源为外部引脚PB6。设置采样率、数据精度等参数与之前数据采集时保持一致如16kHz 16位。启用DFSDM的DMA直接存储器访问让音频数据可以自动、不占用CPU地搬运到内存中的缓冲区。这是实现实时、低功耗音频处理的关键。GPIO输出我们需要一个引脚来控制LED指示是否检测到雨声。查看STWIN板原理图找到用户可用的GPIO。例如我选择了连接器CN2上的PG12引脚。在CubeMX中将PG12配置为GPIO_Output。串口用于调试信息输出。配置一个USART如USART1并启用中断或DMA。配置完成后点击“GENERATE CODE”选择你的IDE我们选STM32CubeIDECubeMX就会生成一个包含所有底层驱动初始化代码的完整工程。5.2 在STM32CubeIDE中集成AI库与编写应用逻辑打开STM32CubeIDE导入刚刚生成的工程。集成AI库文件将libneai.a复制到工程目录下的Core/Src文件夹。将NanoEdgeAI.h和knowledge.h复制到Core/Inc文件夹。在IDE中刷新工程确保这些文件出现在项目浏览器中。需要配置链接器让它能找到libneai.a。通常在工程属性的C/C Build-Settings-Tool Settings-MCU GCC Linker-Libraries中添加neai并在Library search path中添加libneai.a所在的目录../Core/Src。编写主循环逻辑打开main.c在/* USER CODE BEGIN */和/* USER CODE END */之间添加我们的业务代码。核心逻辑如下// 包含头文件 #include NanoEdgeAI.h #include knowledge.h // 定义一些变量 #define AUDIO_BUFFER_SIZE 16000 // 1秒音频16kHz采样率 #define CONSECUTIVE_THRESHOLD 5 // 连续检测到5次才确认防误触发 int16_t audio_buffer[AUDIO_BUFFER_SIZE]; // DMA填充的原始音频缓冲区 float input_user_buffer[DATA_INPUT_USER]; // 提供给AI模型的输入缓冲区预处理后 uint16_t buffer_index 0; volatile uint8_t audio_data_ready 0; // DMA传输完成标志 uint8_t neai_class 0; // AI推理结果 uint8_t last_neai_class 0; uint8_t consecutive_count 0; // DMA传输完成中断回调函数 void DFSDM_TransferComplete_Callback(void) { audio_data_ready 1; // 设置标志位通知主循环有新数据 } int main(void) { // HAL初始化、外设初始化由CubeMX生成 // ... // 初始化NanoEdge AI库 neai_classification_init(knowledge); // 启动DFSDM DMA开始连续采集音频 HAL_DFSDM_Start_DMA(hdfsdm1_filter0, (int32_t*)audio_buffer, AUDIO_BUFFER_SIZE); while (1) { // 等待一帧音频数据采集完成 if (audio_data_ready) { audio_data_ready 0; // 1. 数据预处理可选取决于模型要求 // 例如将int16_t转换为float或进行归一化。 // NanoEdge AI库通常要求特定格式的输入请参考其文档。 for (int i 0; i DATA_INPUT_USER; i) { // 这里假设模型需要的是原始采样点我们直接转换类型 input_user_buffer[i] (float)audio_buffer[buffer_index i] / 32768.0f; // 归一化到[-1, 1] } // 2. 执行AI推理 neai_class neai_classification(input_user_buffer); // 3. 根据结果进行决策防抖逻辑 if (neai_class 1) { // 假设类别1是“雨声” consecutive_count; if (consecutive_count CONSECUTIVE_THRESHOLD) { HAL_GPIO_WritePin(GPIOG, GPIO_PIN_12, GPIO_PIN_SET); // 点亮LED // 可以通过串口打印信息printf(Rain detected!\r\n); } } else { consecutive_count 0; HAL_GPIO_WritePin(GPIOG, GPIO_PIN_12, GPIO_PIN_RESET); // 熄灭LED } // 4. 更新缓冲区索引循环缓冲区 buffer_index DATA_INPUT_USER; if (buffer_index (AUDIO_BUFFER_SIZE - DATA_INPUT_USER)) { buffer_index 0; } // 5. 可选处理其他任务或进入低功耗模式 // HAL_Delay(10); } } }代码逻辑解读DMA中断音频采集由DMA负责填满缓冲区后触发中断主循环只需检查audio_data_ready标志。这保证了CPU不会被频繁的采样占用可以高效处理其他任务或进入休眠省电。预处理将采集到的原始整数样本转换为模型需要的浮点格式并进行归一化。这一步必须严格按照模型训练时的数据预处理流程来。推理调用neai_classification()函数传入预处理后的缓冲区得到分类结果。决策与防抖这是在实际产品中非常重要的技巧。单次推理结果可能因噪声而波动。我们设置了一个阈值CONSECUTIVE_THRESHOLD只有当连续多次如5次都识别为雨声时才最终触发动作点亮LED。这能有效减少误报。循环缓冲区使用一个大的音频缓冲区通过移动索引来实现滑动窗口分析可以持续不断地进行识别。5.3 编译、下载与调试编译在STM32CubeIDE中点击编译按钮确保没有错误。链接器会正确地将libneai.a中的模型代码链接到你的可执行文件中。连接硬件用USB线将STLINK-V3MINI调试器连接到STWIN板的SWD接口并给板子上电。下载程序点击IDE中的“Run”或“Debug”按钮将程序下载到开发板。观察结果打开串口调试助手如Tera Term设置正确的波特率可以看到程序打印的调试信息如果你在代码中启用了printf。当播放雨声音频或真实下雨时观察板载或外接的LED是否按预期点亮。你可以调整CONSECUTIVE_THRESHOLD的值来改变识别的灵敏度。6. 项目优化与问题排查实录第一个能跑起来的版本只是开始。要让它在真实环境中稳定可靠地工作还需要进行优化和解决实际问题。6.1 性能优化技巧模型精简回到NanoEdge AI Studio尝试在满足准确率要求的前提下选择“更小”或“更快”的模型版本。工具通常会提供几个在精度、内存占用和推理速度之间取得不同平衡的候选模型。降低采样率或帧长如果1秒的音频识别延迟你觉得太长可以尝试用更短的音频片段如0.5秒进行训练和推理。这能降低延迟但可能会影响准确率需要重新训练和测试。优化预处理代码检查main.c中的预处理循环。确保使用了编译器优化如-O2并考虑是否能用查表法、整数运算代替浮点运算来加速。对于Cortex-M4启用硬件FPU能大幅提升浮点计算速度记得在CubeMX和编译器设置中打开。功耗管理在while(1)循环中如果audio_data_ready标志未置位可以让CPU进入低功耗模式如WFI指令等待DMA中断唤醒。这是电池供电设备的必备技能。6.2 常见问题与解决方案下面是我在开发过程中遇到的一些典型问题及解决方法整理成了速查表问题现象可能原因排查步骤与解决方案编译错误undefined reference toneai_classification‘AI静态库未正确链接。1. 检查libneai.a是否已放入Core/Src并刷新工程。2. 检查工程属性的链接器设置是否添加了-lneai库和正确的库搜索路径。程序运行后无反应LED不亮。1. 音频数据未正确采集。2. 模型推理结果始终不是雨声类别。1.检查DMA和DFSDM配置用调试器查看audio_buffer数组是否有数据变化。或者通过串口打印出前几个采样值看是否在合理范围非全0。2.检查数据预处理确保传递给neai_classification的input_user_buffer数据格式、归一化方式与模型训练时完全一致。将预处理后的数据通过串口发送到电脑与训练时用的一个样本进行对比。3.检查模型类别ID确认代码中判断的类别ID如if(neai_class 1)与NanoEdge AI Studio中“雨声”类别的实际ID一致。识别准确率低误报多。1. 训练数据质量差或缺乏多样性。2. 真实环境与训练环境差异大。3. 防抖阈值设置不合理。1.回顾数据集这是最常见的原因。检查你的“其他声音”类是否包含了足够多的、容易与雨声混淆的负样本如流水声、风声。2.实地测试与数据增强在最终部署环境中采集一些新数据加入到训练集中重新训练模型。3.调整决策逻辑增大CONSECUTIVE_THRESHOLD值要求更连续的检测才触发。或者加入“非雨声”的连续计数只有雨声计数显著高于其他声音时才触发。推理速度慢导致音频断断续续或丢失。1. 模型太大或太复杂。2. 预处理或后处理代码效率低。3. 系统时钟未配置到最高。1. 在NanoEdge AI Studio中选择更小、更快的模型变体。2. 优化代码使用编译器优化减少循环中的计算使用内存对齐访问。3. 在CubeMX中确认系统时钟SYSCLK是否已配置到MCU支持的最高频率STM32L4R9ZI是120MHz。功耗过高。CPU一直全速运行未进入低功耗模式。在主循环中当audio_data_ready 0时调用HAL_PWR_EnterSLEEPMode(...)或__WFI()指令让CPU休眠等待DMA传输完成中断唤醒。6.3 从原型到产品化的思考这个雨声识别项目作为一个原型已经完成了。但如果想把它变成一个真正的产品比如一个自动关窗器还需要考虑更多电源管理设计低功耗电路使用电池供电并优化软件使设备大部分时间处于深度睡眠只有定时或由声音事件唤醒时才进行采集和识别。模型更新产品出厂后如果发现新的干扰声音如何更新设备里的模型可以考虑通过蓝牙或USB接口进行OTA空中下载模型更新。环境自适应不同季节、不同地区的雨声可能不同。能否让模型具备一定的在线学习能力微调参数以适应新环境这属于更高级的TinyML应用范畴。外壳与结构麦克风的位置、外壳的声学设计都会影响拾音效果需要在实际产品中仔细设计。做完这个项目我最深的体会是TinyML真正降低了嵌入式智能的门槛。它把复杂的算法工程封装成了简单的“数据-训练-部署”流程让嵌入式工程师可以更专注于解决具体的应用问题。整个过程里最花时间、最需要耐心的反而不是编程和调试而是前期数据的采集与整理。这或许就是AI时代的特征数据是燃料质量决定能跑多远。下次如果你也想给一个小设备加上“听觉”、“视觉”或“感觉”不妨就从准备一份好的数据开始剩下的交给像NanoEdge AI Studio这样的工具它们会帮你把想法变成电路板上闪烁的灯光。
基于STM32与NanoEdge AI Studio的TinyML雨声识别实战
1. 项目概述几年前我在一个老房子的阁楼里折腾我的第一个嵌入式项目。那是个雨天雨点敲打屋顶的声音特别清晰我一边调试代码一边听着这个背景音。当时我就想要是这个小小的开发板能“听懂”下雨然后自动帮我关窗或者打开氛围灯那该多酷。这个念头一直没实现直到我接触到了TinyML。简单来说TinyML就是让机器学习模型能在指甲盖大小的微控制器上跑起来不用联网几毫瓦的功耗就能完成实时推理。这彻底改变了嵌入式开发的玩法让“边缘智能”从概念变成了手头的项目。这次我决定用STMicroelectronics的STEVAL-STWINKT1B传感器节点开发板结合他们自家的NanoEdge AI Studio工具把当年那个“雨声识别”的想法给实现了。整个过程走下来从数据采集、模型训练到最终烧录进STM32踩了不少坑也总结了一套比较顺滑的流程。这篇文章我就把这套从零到一的实践过程拆开揉碎了讲给你听无论你是刚接触嵌入式还是想了解如何把AI塞进MCU应该都能找到有用的东西。核心就是用最少的资源干一件特定且智能的事。2. TinyML项目核心思路与工具选型2.1 为什么是TinyML而不是云端AI在做这个项目之前我也考虑过更“常规”的方案比如用开发板采集音频通过Wi-Fi上传到云端服务器做识别再把结果传回来。但这个方案有几个硬伤首先它依赖网络网络不稳定或者断了功能就废了。其次有延迟从采集、上传、处理到回传几百毫秒甚至更长的延迟对于某些实时应用是不可接受的。最后隐私和功耗也是大问题持续传输音频数据既不安全也对电池供电的设备极不友好。TinyML的思路正好相反把智能放在最边缘就地解决。模型直接在微控制器上运行无需网络响应速度在毫秒级功耗极低所有原始数据都在本地处理隐私性最好。当然代价是模型必须非常小通常只有几十KB到几百KB并且只能完成特定的、相对简单的任务比如识别特定的几种声音而不是像ChatGPT那样进行通用对话。对于“识别雨声”这种单一、明确的分类任务TinyML是绝配。2.2 硬件平台STEVAL-STWINKT1B 开发板解析工欲善其事必先利其器。我选择STEVAL-STWINKT1B后面简称STWIN板有几个关键原因集成度高它本身就是一个完整的传感器节点板载了数字麦克风IMP34DT05、运动传感器、环境传感器等。我们做音频识别它自带的MEMS麦克风质量足够省去了外接麦克风模块的麻烦。核心强大主控是STM32L4R9ZI这是一颗基于Arm Cortex-M4内核的微控制器主频120MHz拥有2MB Flash和640KB RAM。对于TinyML模型来说这个资源算是“豪华”配置了有足够的空间运行相对复杂的模型和预处理代码。生态支持好ST为这块板子提供了完整的TinyML工具链NanoEdge AI Studio和丰富的功能软件包FP-SNS-DATALOG1从数据记录到模型部署都有现成的例子和库能极大降低开发门槛。注意虽然这块板子资源丰富但我们在设计模型时依然要时刻绷紧“资源受限”这根弦。不能因为硬件好就随意设计大模型要养成面向MCU的优化思维这是TinyML开发者的基本功。2.3 软件工具链NanoEdge AI Studio vs. 其他方案模型训练工具有好几个选择我主要对比了NanoEdge AI Studio和Edge Impulse。Edge Impulse非常流行社区活跃提供从数据采集、标注、训练到部署的全流程Web IDE。它支持多种硬件平台生成的模型可以导出为TensorFlow Lite格式通用性更强。但有时对特定硬件的底层优化需要自己多费心思。NanoEdge AI StudioST的“亲儿子”工具专门为STM32系列优化。它的最大优势是高度集成和自动化。你几乎不需要关心模型结构是神经网络、随机森林还是其他只需要提供数据它就能自动搜索出最适合你硬件和数据的最优模型并生成直接可用的C库文件.a,.h。对于快速原型开发和专注于应用而非算法细节的工程师来说效率极高。我最终选择了NanoEdge AI Studio因为我们的硬件是固定的STWIN板用它能够获得最好的软硬件协同优化效果部署步骤也最简洁。它的工作流程可以概括为上传数据 - 自动学习/训练 - 验证性能 - 下载为嵌入式库。后面我们会详细走通这个流程。3. 数据采集项目成败的生命线几乎所有机器学习项目的老手都会告诉你同一句话垃圾进垃圾出Garbage in, garbage out。在TinyML项目里这句话的权重还要再乘以十倍。因为我们的模型容量小算力弱它更像一个在严格条件下工作的“特长生”而不是“通才”。如果训练数据不能很好地代表真实场景模型在设备上的表现一定会让你大跌眼镜。3.1 采集策略与实战要点为了采集到高质量的雨声数据我制定了以下策略并严格执行在真实环境采集我并没有从网上下载雨声音频而是直接把STWIN板放在我家的阁楼窗户边在多个不同的下雨天进行录制。这样能确保采集到的声音包含了真实环境中的背景噪音如远处的车声、风声、房间混响等模型学习到的才是“在我家阁楼听到的雨声”。固定设备与参数始终使用STWIN板自带的麦克风进行采集采样率固定为16kHz对于雨声识别足够。避免中途更换设备或调整增益确保数据的一致性。覆盖多样性雨量变化录制了小雨、中雨、大雨、暴雨等多种强度的雨声。距离与角度将板子放在离窗户不同距离0.5米 1米 2米和不同角度正对窗户侧对窗户的位置进行录制。干扰场景特意录制了“非雨声”但可能混淆的场景如水龙头流水声、风扇声、室内对话背景音、键盘敲击声、马路噪音等。这些将作为“其他”类或负样本。数据均衡确保“雨声”和“非雨声”的样本数量大致相当并且雨声内部不同强度、不同位置的样本数量也尽量均衡避免模型对某一种情况“过拟合”。3.2 两种数据采集方法详解ST提供了两种将开发板变为数据记录仪Data Logger的方法我都实践了。方法一使用高速数据记录功能包FP-SNS-DATALOG1这是功能更强大、更灵活的方法。你需要给开发板烧录一个特定的固件HSDatalog.bin之后板子可以通过USB虚拟串口或手机AppST BLE Sensor记录数据到SD卡。操作流程使用STM32CubeProgrammer将HSDatalog.bin固件烧录到STWIN板。插入MicroSD卡。通过USB连接电脑板子会被识别为一个虚拟串口。使用ST提供的Python脚本如hsdatalog_data_export.py与板子通信开始记录。你可以指定记录时长、传感器类型这里选择音频。记录结束后数据以二进制格式保存在SD卡中。需要使用另一个Python脚本hsdatalog_to_wav.py将其转换为标准的WAV文件。优点可以长时间离线记录数据直接存SD卡不占用电脑资源可以通过手机蓝牙控制部署灵活。缺点步骤稍多需要熟悉Python环境和命令行操作。方法二通过NanoEdge AI Studio直接串口采集推荐给新手这是更简单直接的方法尤其适合快速构建初始数据集。首先你需要给STWIN板烧录另一个固件例如项目包里的STWIN_DTMF_Classifier.bin这个固件实现了基本的音频采集和串口通信功能。用USB线连接板子和电脑。打开NanoEdge AI Studio在创建数据集的环节选择“从串口实时采集”。软件会自动识别板子的串口你点击“开始录制”然后制造声音比如播放雨声音频到麦克风前或者直接在雨天录制软件就会实时接收并保存音频片段。你可以一边采集一边给数据打上标签例如点击“Record”按钮时就标记为“Rain”类。实操心得我建议先用方法二快速收集一小批数据用于模型的原型验证和流程跑通。等整个流程验证无误后再使用方法一进行大规模、长时间、多样化的数据采集用于训练最终的生产级模型。这样效率最高。3.3 数据预处理与初步分析拿到一堆WAV文件后别急着扔给训练工具。先做一次“人工质检”和简单分析。听一遍随机播放一些样本确保录音质量没问题没有破音、奇怪的爆音。可视化分析写个简单的Python脚本看看你的数据“长什么样”。最常用的就是看波形图和频谱图FFT。import numpy as np import matplotlib.pyplot as plt from scipy.io import wavfile # 加载一个雨声样本和一个非雨声样本 rain_rate, rain_data wavfile.read(rain_light.wav) noise_rate, noise_data wavfile.read(keyboard.wav) # 转换为单声道如果是立体声 if len(rain_data.shape) 2: rain_data np.mean(rain_data, axis1) if len(noise_data.shape) 2: noise_data np.mean(noise_data, axis1) # 绘制时域波形对比 fig, axes plt.subplots(2, 2, figsize(12, 8)) axes[0, 0].plot(rain_data[:8000]) # 取前0.5秒16kHz采样率 axes[0, 0].set_title(Rain Sound - Time Domain) axes[0, 0].set_xlabel(Sample) axes[0, 0].set_ylabel(Amplitude) axes[1, 0].plot(noise_data[:8000]) axes[1, 0].set_title(Keyboard Sound - Time Domain) axes[1, 0].set_xlabel(Sample) axes[1, 0].set_ylabel(Amplitude) # 计算并绘制频域FFT对比 rain_fft np.abs(np.fft.rfft(rain_data[:8000])) noise_fft np.abs(np.fft.rfft(noise_data[:8000])) freqs np.fft.rfftfreq(8000, 1/16000) axes[0, 1].plot(freqs, rain_fft) axes[0, 1].set_title(Rain Sound - Frequency Domain) axes[0, 1].set_xlabel(Frequency (Hz)) axes[0, 1].set_ylabel(Magnitude) axes[0, 1].set_xlim([0, 8000]) # 聚焦于人耳可闻范围 axes[1, 1].plot(freqs, noise_fft) axes[1, 1].set_title(Keyboard Sound - Frequency Domain) axes[1, 1].set_xlabel(Frequency (Hz)) axes[1, 1].set_ylabel(Magnitude) axes[1, 1].set_xlim([0, 8000]) plt.tight_layout() plt.show()通过这个对比图你可能会发现雨声在低频部分比如200Hz以下和中高频比如2kHz-8kHz的分布与键盘敲击声有明显不同。这种物理特征上的差异正是模型能够学习并区分它们的基础。如果连人眼都能从频谱上看出区别那模型学会的概率就大得多。4. 模型训练与NanoEdge AI Studio实战数据准备好了就进入核心环节——训练模型。这里我们完全在NanoEdge AI Studio的Web界面中完成。4.1 项目创建与数据导入登录与新建访问NanoEdge AI Studio官网注册登录后创建一个新项目选择“N-class Classification”多分类类型因为我们要区分“雨声”和“其他声音”至少两类。选择硬件在目标设备中选择“STEVAL-STWINKT1B”。这一步至关重要工具会根据你指定的硬件资源CPU RAM Flash来优化和筛选模型。上传数据你可以将之前准备好的WAV文件按类别打包成ZIP文件上传。例如一个rain.zip里面全是雨声WAV一个other.zip里面全是其他声音。更推荐使用之前提到的串口实时采集功能。在Studio内启动采集用STWIN板现场录制并即时打标签。这种方式数据质量最高因为路径最短麦克风-开发板-Studio没有中间文件转换的潜在损失。信号参数设置告诉Studio你的音频参数。采样率16kHz、样本长度我设置了1秒即16000个点。样本长度是一个需要权衡的参数太短可能信息不足太长会增加计算量和延迟。1秒对于雨声识别是个不错的起点。4.2 模型训练与性能评估数据上传后Studio会自动进行预处理比如可能计算MFCC等特征。接下来点击“开始训练”。自动学习Studio会在后台尝试多种不同类型的微型机器学习算法如微型神经网络、决策树集成等并评估它们在你的数据和你的硬件上的表现。这个过程可能需要几分钟到几小时取决于数据量。理解输出报告训练完成后你会得到一份详细的报告其中最重要的两个部分是混淆矩阵一个表格展示了模型在测试集上的分类情况。理想情况下对角线上的数字正确分类应该很高非对角线的数字混淆错误应该很低。比如雨声被误判为“其他”的比例应该很小。交叉验证结果Studio会用类似K折交叉验证的方法评估模型的稳健性。你会看到一个平均准确率和波动范围。例如“准确率95% ± 3%”。这个结果比单次训练-测试分割更可靠。重要警告警惕过拟合如果报告显示准确率高达99.9%甚至100%先别高兴太早。这很可能是过拟合的标志——模型完美“记住”了训练数据但无法泛化到新的、没见过的声音上。避免过拟合的最佳方法就是我前面强调的用高质量、多样化、代表真实场景的数据集。如果出现过拟合你需要回去检查并扩充你的数据集。4.3 模型导出与库文件解析当你对模型的性能满意后就可以点击“编译”或“导出”。NanoEdge AI Studio会生成一个针对你的STWIN板优化好的“知识库”文件包主要包含三个文件libneai.a这是一个静态库文件里面封装了训练好的模型权重、推理函数以及所有底层计算代码。你不需要知道里面具体是什么只需要把它链接到你的工程里。NanoEdgeAI.h头文件提供了使用这个AI库的API接口。主要会用到初始化函数、推理函数等。knowledge.h或类似文件里面定义了一个巨大的常量数组这就是模型的“知识”——经过压缩和量化后的参数。libneai.a中的函数会使用这些参数进行计算。至此模型训练部分就完成了。你会发现我们几乎没写一行机器学习代码也没调整任何模型超参数这就是NanoEdge AI Studio的威力——让嵌入式工程师也能快速应用AI。5. 嵌入式工程创建与模型集成模型有了现在要把它塞进STM32的工程里并写出让整个系统跑起来的逻辑。5.1 使用STM32CubeMX配置硬件外设STM32CubeMX是一个图形化配置工具用来初始化微控制器的时钟、外设、引脚等并生成工程框架。我们的项目需要配置以下关键部分时钟树确保系统时钟、外设时钟正确设置。STM32L4R9ZI最高可运行在120MHz我们按此配置以获得最佳性能。音频采集STWIN板的数字麦克风IMP34DT05通过DFSDM数字滤波器用于Σ-Δ调制器接口连接到MCU的PB6引脚。在CubeMX中找到DFSDM1外设配置Filter 0并将其关联到Channel 0。配置Channel 0的数据源为外部引脚PB6。设置采样率、数据精度等参数与之前数据采集时保持一致如16kHz 16位。启用DFSDM的DMA直接存储器访问让音频数据可以自动、不占用CPU地搬运到内存中的缓冲区。这是实现实时、低功耗音频处理的关键。GPIO输出我们需要一个引脚来控制LED指示是否检测到雨声。查看STWIN板原理图找到用户可用的GPIO。例如我选择了连接器CN2上的PG12引脚。在CubeMX中将PG12配置为GPIO_Output。串口用于调试信息输出。配置一个USART如USART1并启用中断或DMA。配置完成后点击“GENERATE CODE”选择你的IDE我们选STM32CubeIDECubeMX就会生成一个包含所有底层驱动初始化代码的完整工程。5.2 在STM32CubeIDE中集成AI库与编写应用逻辑打开STM32CubeIDE导入刚刚生成的工程。集成AI库文件将libneai.a复制到工程目录下的Core/Src文件夹。将NanoEdgeAI.h和knowledge.h复制到Core/Inc文件夹。在IDE中刷新工程确保这些文件出现在项目浏览器中。需要配置链接器让它能找到libneai.a。通常在工程属性的C/C Build-Settings-Tool Settings-MCU GCC Linker-Libraries中添加neai并在Library search path中添加libneai.a所在的目录../Core/Src。编写主循环逻辑打开main.c在/* USER CODE BEGIN */和/* USER CODE END */之间添加我们的业务代码。核心逻辑如下// 包含头文件 #include NanoEdgeAI.h #include knowledge.h // 定义一些变量 #define AUDIO_BUFFER_SIZE 16000 // 1秒音频16kHz采样率 #define CONSECUTIVE_THRESHOLD 5 // 连续检测到5次才确认防误触发 int16_t audio_buffer[AUDIO_BUFFER_SIZE]; // DMA填充的原始音频缓冲区 float input_user_buffer[DATA_INPUT_USER]; // 提供给AI模型的输入缓冲区预处理后 uint16_t buffer_index 0; volatile uint8_t audio_data_ready 0; // DMA传输完成标志 uint8_t neai_class 0; // AI推理结果 uint8_t last_neai_class 0; uint8_t consecutive_count 0; // DMA传输完成中断回调函数 void DFSDM_TransferComplete_Callback(void) { audio_data_ready 1; // 设置标志位通知主循环有新数据 } int main(void) { // HAL初始化、外设初始化由CubeMX生成 // ... // 初始化NanoEdge AI库 neai_classification_init(knowledge); // 启动DFSDM DMA开始连续采集音频 HAL_DFSDM_Start_DMA(hdfsdm1_filter0, (int32_t*)audio_buffer, AUDIO_BUFFER_SIZE); while (1) { // 等待一帧音频数据采集完成 if (audio_data_ready) { audio_data_ready 0; // 1. 数据预处理可选取决于模型要求 // 例如将int16_t转换为float或进行归一化。 // NanoEdge AI库通常要求特定格式的输入请参考其文档。 for (int i 0; i DATA_INPUT_USER; i) { // 这里假设模型需要的是原始采样点我们直接转换类型 input_user_buffer[i] (float)audio_buffer[buffer_index i] / 32768.0f; // 归一化到[-1, 1] } // 2. 执行AI推理 neai_class neai_classification(input_user_buffer); // 3. 根据结果进行决策防抖逻辑 if (neai_class 1) { // 假设类别1是“雨声” consecutive_count; if (consecutive_count CONSECUTIVE_THRESHOLD) { HAL_GPIO_WritePin(GPIOG, GPIO_PIN_12, GPIO_PIN_SET); // 点亮LED // 可以通过串口打印信息printf(Rain detected!\r\n); } } else { consecutive_count 0; HAL_GPIO_WritePin(GPIOG, GPIO_PIN_12, GPIO_PIN_RESET); // 熄灭LED } // 4. 更新缓冲区索引循环缓冲区 buffer_index DATA_INPUT_USER; if (buffer_index (AUDIO_BUFFER_SIZE - DATA_INPUT_USER)) { buffer_index 0; } // 5. 可选处理其他任务或进入低功耗模式 // HAL_Delay(10); } } }代码逻辑解读DMA中断音频采集由DMA负责填满缓冲区后触发中断主循环只需检查audio_data_ready标志。这保证了CPU不会被频繁的采样占用可以高效处理其他任务或进入休眠省电。预处理将采集到的原始整数样本转换为模型需要的浮点格式并进行归一化。这一步必须严格按照模型训练时的数据预处理流程来。推理调用neai_classification()函数传入预处理后的缓冲区得到分类结果。决策与防抖这是在实际产品中非常重要的技巧。单次推理结果可能因噪声而波动。我们设置了一个阈值CONSECUTIVE_THRESHOLD只有当连续多次如5次都识别为雨声时才最终触发动作点亮LED。这能有效减少误报。循环缓冲区使用一个大的音频缓冲区通过移动索引来实现滑动窗口分析可以持续不断地进行识别。5.3 编译、下载与调试编译在STM32CubeIDE中点击编译按钮确保没有错误。链接器会正确地将libneai.a中的模型代码链接到你的可执行文件中。连接硬件用USB线将STLINK-V3MINI调试器连接到STWIN板的SWD接口并给板子上电。下载程序点击IDE中的“Run”或“Debug”按钮将程序下载到开发板。观察结果打开串口调试助手如Tera Term设置正确的波特率可以看到程序打印的调试信息如果你在代码中启用了printf。当播放雨声音频或真实下雨时观察板载或外接的LED是否按预期点亮。你可以调整CONSECUTIVE_THRESHOLD的值来改变识别的灵敏度。6. 项目优化与问题排查实录第一个能跑起来的版本只是开始。要让它在真实环境中稳定可靠地工作还需要进行优化和解决实际问题。6.1 性能优化技巧模型精简回到NanoEdge AI Studio尝试在满足准确率要求的前提下选择“更小”或“更快”的模型版本。工具通常会提供几个在精度、内存占用和推理速度之间取得不同平衡的候选模型。降低采样率或帧长如果1秒的音频识别延迟你觉得太长可以尝试用更短的音频片段如0.5秒进行训练和推理。这能降低延迟但可能会影响准确率需要重新训练和测试。优化预处理代码检查main.c中的预处理循环。确保使用了编译器优化如-O2并考虑是否能用查表法、整数运算代替浮点运算来加速。对于Cortex-M4启用硬件FPU能大幅提升浮点计算速度记得在CubeMX和编译器设置中打开。功耗管理在while(1)循环中如果audio_data_ready标志未置位可以让CPU进入低功耗模式如WFI指令等待DMA中断唤醒。这是电池供电设备的必备技能。6.2 常见问题与解决方案下面是我在开发过程中遇到的一些典型问题及解决方法整理成了速查表问题现象可能原因排查步骤与解决方案编译错误undefined reference toneai_classification‘AI静态库未正确链接。1. 检查libneai.a是否已放入Core/Src并刷新工程。2. 检查工程属性的链接器设置是否添加了-lneai库和正确的库搜索路径。程序运行后无反应LED不亮。1. 音频数据未正确采集。2. 模型推理结果始终不是雨声类别。1.检查DMA和DFSDM配置用调试器查看audio_buffer数组是否有数据变化。或者通过串口打印出前几个采样值看是否在合理范围非全0。2.检查数据预处理确保传递给neai_classification的input_user_buffer数据格式、归一化方式与模型训练时完全一致。将预处理后的数据通过串口发送到电脑与训练时用的一个样本进行对比。3.检查模型类别ID确认代码中判断的类别ID如if(neai_class 1)与NanoEdge AI Studio中“雨声”类别的实际ID一致。识别准确率低误报多。1. 训练数据质量差或缺乏多样性。2. 真实环境与训练环境差异大。3. 防抖阈值设置不合理。1.回顾数据集这是最常见的原因。检查你的“其他声音”类是否包含了足够多的、容易与雨声混淆的负样本如流水声、风声。2.实地测试与数据增强在最终部署环境中采集一些新数据加入到训练集中重新训练模型。3.调整决策逻辑增大CONSECUTIVE_THRESHOLD值要求更连续的检测才触发。或者加入“非雨声”的连续计数只有雨声计数显著高于其他声音时才触发。推理速度慢导致音频断断续续或丢失。1. 模型太大或太复杂。2. 预处理或后处理代码效率低。3. 系统时钟未配置到最高。1. 在NanoEdge AI Studio中选择更小、更快的模型变体。2. 优化代码使用编译器优化减少循环中的计算使用内存对齐访问。3. 在CubeMX中确认系统时钟SYSCLK是否已配置到MCU支持的最高频率STM32L4R9ZI是120MHz。功耗过高。CPU一直全速运行未进入低功耗模式。在主循环中当audio_data_ready 0时调用HAL_PWR_EnterSLEEPMode(...)或__WFI()指令让CPU休眠等待DMA传输完成中断唤醒。6.3 从原型到产品化的思考这个雨声识别项目作为一个原型已经完成了。但如果想把它变成一个真正的产品比如一个自动关窗器还需要考虑更多电源管理设计低功耗电路使用电池供电并优化软件使设备大部分时间处于深度睡眠只有定时或由声音事件唤醒时才进行采集和识别。模型更新产品出厂后如果发现新的干扰声音如何更新设备里的模型可以考虑通过蓝牙或USB接口进行OTA空中下载模型更新。环境自适应不同季节、不同地区的雨声可能不同。能否让模型具备一定的在线学习能力微调参数以适应新环境这属于更高级的TinyML应用范畴。外壳与结构麦克风的位置、外壳的声学设计都会影响拾音效果需要在实际产品中仔细设计。做完这个项目我最深的体会是TinyML真正降低了嵌入式智能的门槛。它把复杂的算法工程封装成了简单的“数据-训练-部署”流程让嵌入式工程师可以更专注于解决具体的应用问题。整个过程里最花时间、最需要耐心的反而不是编程和调试而是前期数据的采集与整理。这或许就是AI时代的特征数据是燃料质量决定能跑多远。下次如果你也想给一个小设备加上“听觉”、“视觉”或“感觉”不妨就从准备一份好的数据开始剩下的交给像NanoEdge AI Studio这样的工具它们会帮你把想法变成电路板上闪烁的灯光。