上一期我们详细的介绍了 STM32 中的多功能数字滤波器外设的使用原理,STM32N6570-DK 开发板中搭载一颗可以用MDF驱动的麦克风,使用这颗麦克风可以得到实时的环境声音数据。
本文我们在此基础上介绍使用 STM32 实现音频分类的全流程日志,包括数据采集、神经网络设计、模型量化与部署等。
1、数据采集
我们设置MDF的采样频率为15.625KHZ通过DMA实时写入内存。在此基础上使用串口将数据上传至客户端,串口波特率设置为460800(这个值绰绰有余)。
上位机程序使用python完成,启动程序后会等到STM32端发送[AUDIO] PCM_BEGINrn信号,之后逐字节解析音频数据。整个波形长度为约2s包含FFT频谱以及192x64的Log-Mel 频谱。
设计了三个按钮:噪声、人声还有音乐,点击不同按钮后就可以每 2s 根据分类保存一次原始数据。
在这里我们对每一次采集的数据将其转化为 192 * 64 的Log-Mel 频谱(进行分类任务的时候这个工作是由单片机完成的,相比于神经网络来说,这个计算量更加庞大,因此酌情设计这个输入数据的大小)
在 STM32 中实测,400MHZ 主频下每次完成192 * 64频谱计算的时间 约为40ms+,增大这个值可以增加声音数据的分辨率。减少这个值可以提高系统的运行速度。
2、神经网络设计
对于一个192 * 64 的数据来说,我们可以直接使用卷积神经网络来提取其特征,并且噪声、人声、音乐三者的声音辨识度非常高,网络层数也可以酌情减少,最后设计出如下所示的网络结构:
输入数据为192 * 64 的 Log-Mel 频谱数据,首先通过一个5x5 的输入端特征提取层初步提取出特征,将其转化为8 x 96 x 32 再通过四个小型卷积块 进一步提取特征,输出前通过一个全局池化层提取全局特性后进入全连接层和softmax进行归一化,最后输出噪声、人声以及音乐三者的概率。
这里有几点关于 NPU 的小巧思可以介绍:首先我们注意到芯片手册中对NPU部分的描述:
STM32 的卷积加速器是NPU的核心,所以Block1-Block4 所采用的卷积都是深度可分离卷积而不是普通卷积,具体是 3x3 深度可分离卷积 步长 2 以及 1x1 的逐点卷积(注意这个逐点点积是能够被加速的)。
普通卷积同时处理空间特征和通道间的信息融合,而深度可分离卷积只对每个输入通道分别进行空间特征提取,不直接融合不同通道的信息从而大幅度减少参数量和计算量。
以我们应用中的Block1为例,忽略偏置和归一化参数,普通卷积的参数量为:3x3x8x16 = 1152 而 深度卷积 + 逐点卷积可以将参数量压缩至:
( 3 x 3 x 8 )+ (1 x 1 x 8 x 16) = 200。
最后量化得到的模型权重仅为 19 kB的大小。
3、模型部署
接着我们到 CubeMX中 使用 CubeAI 工具来完成模型的部署内容,这里我们将量化好后的 tflite 格式模型通过CubeAI进行分析和部署:
可以看到除了Softmax层,所有的卷积层和池化层都是都是通过 NPU 硬件加速单元来完成运算的。
最后模针对 STM32N6 NPU平台进行模型部署与内存分析。结果显示,神经网络相关只读存储需求为 52,590 B,其中模型权重占用 10,433 B,运行时及网络代码等占用42,157 B。运行内存需求为 49,181 B,其中中间激活缓冲区占用49,152 B。
4、模型使用
while (1){/* USER CODE END WHILE */MX_X_CUBE_AI_Process();/* USER CODE BEGIN 3 */__WFI();}
所有核心代码都在MX_X_CUBE_AI_Process()函数中完成。
LL_ATON_RT_RetValues_t status;uint32_t start, cycles, elapsed_cycles, k, best = 0U;int8_t *input;constint8_t *output;if (!ai_ready || (pcm == NULL) || (result == NULL)){return-22;}input = (int8_t *)LL_Buffer_addr_start(&input_info[0]);output = (constint8_t *)LL_Buffer_addr_start(&output_info[0]);start = HAL_GetTick();if (AUDIO_ExtractFeatures(pcm, input, input_info[0].scale[0], input_info[0].offset[0]) != 0){return-23;}result->PreprocessMs = HAL_GetTick() - start;/* This pool is uncached by the NPU, but CPU D-cache is enabled. */LL_ATON_Cache_MCU_Clean_Invalidate_Range((uintptr_t)input, AUDIO_FEATURE_BYTES);/* Output shares a cache line with network scratch. Remove old CPU linesbefore execution; the generated CPU softmax produces the final output. */LL_ATON_Cache_MCU_Invalidate_Range((uintptr_t)output & ~(uintptr_t)31U, 32U);start = HAL_GetTick();cycles = DWT->CYCCNT;LL_ATON_RT_Init_Network(&NN_Instance_Default);do{status = LL_ATON_RT_RunEpochBlock(&NN_Instance_Default);if ((status != LL_ATON_RT_DONE) && ((HAL_GetTick() - start) >= AUDIO_AI_TIMEOUT_MS)){LL_ATON_RT_DeInit_Network(&NN_Instance_Default);LL_ATON_RT_RuntimeDeInit();ai_ready = 0U;return-24; /* Stop future inference rather than reusing a stuck NPU. */}/* Keep interrupts enabled. No indefinite WFE: the timeout remains live. */} while (status != LL_ATON_RT_DONE);elapsed_cycles = DWT->CYCCNT - cycles; /* Unsigned subtraction handles counter wrap. */result->InferenceMs = HAL_GetTick() - start;result->InferenceUs = cycle_timer_ready ? elapsed_cycles / cycles_per_us : result->InferenceMs * 1000U;/* Do not invalidate after the generated CPU softmax: its output may be dirtyin the CPU cache. Read before deinitializing/reusing network scratch. */for (k = 0U; k < 3U; ++k){result->Probabilities[k] = ((float)output[k] - output_info[0].offset[0]) * output_info[0].scale[0];if (result->Probabilities[k] > result->Probabilities[best]){best = k;}}result->ClassIndex = best;LL_ATON_RT_DeInit_Network(&NN_Instance_Default);return0;
模型除了初始化外,调用 AUDIO_ExtractFeatures(),将 PCM 音频转换成模型所需的输入数据。将原始数据转为192 * 64 的输入数据。之后初始化神经网络模型后进行推理。输出结果output从int转为float后得到实际的分类结果。
staticfloat fft_real[AUDIO_FFT_SIZE];staticfloat fft_imag[AUDIO_FFT_SIZE];staticfloat power[AUDIO_FFT_SIZE / 2U + 1U];staticvoidAUDIO_PowerFrame(constint16_t *pcm){uint32_t i, j, length;for (i = 0U; i < AUDIO_FFT_SIZE; ++i){uint32_t value = i, reversed = 0U, bit;for (bit = 0U; bit < 9U; ++bit){reversed = (reversed << 1U) | (value & 1U);value >>= 1U;}fft_real[reversed] = (i < AUDIO_WINDOW_SAMPLES)? ((float)pcm[i] / 32768.0f) * audio_hann[i] : 0.0f;fft_imag[reversed] = 0.0f;}for (length = 2U; length <= AUDIO_FFT_SIZE; length <<= 1U){uint32_t half = length / 2U, step = AUDIO_FFT_SIZE / length;for (i = 0U; i < AUDIO_FFT_SIZE; i += length){for (j = 0U; j < half; ++j){uint32_t a = i + j, b = a + half, w = j * step;float tr = audio_twiddle_real[w] * fft_real[b] - audio_twiddle_imag[w] * fft_imag[b];float ti = audio_twiddle_real[w] * fft_imag[b] + audio_twiddle_imag[w] * fft_real[b];fft_real[b] = fft_real[a] - tr;fft_imag[b] = fft_imag[a] - ti;fft_real[a] += tr;fft_imag[a] += ti;}}}for (i = 0U; i <= AUDIO_FFT_SIZE / 2U; ++i){power[i] = (fft_real[i] * fft_real[i] + fft_imag[i] * fft_imag[i]) * AUDIO_WINDOW_POWER_SCALE;}}voidAUDIO_SpectrumFrame(constint16_t *pcm, float *db){uint32_t i;AUDIO_PowerFrame(pcm);for (i = 0U; i <= AUDIO_FFT_SIZE / 2U; ++i)db[i] = 10.0f * log10f(fmaxf(power[i] * ((i > 0U && i < AUDIO_FFT_SIZE/2U) ? 4.0f : 1.0f), 1.0e-10f));}voidAUDIO_LogMelFrame(constint16_t *pcm, float *db){uint32_t band, j;AUDIO_PowerFrame(pcm);for (band = 0U; band < AUDIO_MEL_BANDS; ++band){float sum = 0.0f;for (j = 0U; j < audio_mel_count[band]; ++j){sum += power[audio_mel_start[band] + j] * audio_mel_weights[audio_mel_offset[band] + j];}db[band] = 10.0f * log10f(fmaxf(sum, 1.0e-10f));}}int32_tAUDIO_ExtractFeatures(constint16_t *pcm, int8_t *output, float scale, int32_t zero_point){uint32_t frame, band;float db[AUDIO_MEL_BANDS];if ((pcm == NULL) || (output == NULL) || !isfinite(scale) || (scale <= 0.0f)|| (zero_point < -128) || (zero_point > 127)){return-1;}for (frame = 0U; frame < AUDIO_FEATURE_FRAMES; ++frame){AUDIO_LogMelFrame(pcm + frame * AUDIO_HOP_SAMPLES, db);for (band = 0U; band < AUDIO_MEL_BANDS; ++band){float value = nearbyintf(db[band] / scale) + (float)zero_point;value = fmaxf(-128.0f, fminf(127.0f, value));output[frame * AUDIO_MEL_BANDS + band] = (int8_t)value;}}return0;}
FFT 和 Log-Mel 函数也贴在上面了,手搓的 FFT没用现成的库。
最后神经网络的输出转化为概率得到三个分类的置信度。
验证
最后加上屏幕绘制内容就得到了开头的视频所示,实际验证中 这个卷积神经网络的推理时间约为0.4ms 而 特征提取消耗的时间则占了大头。
过去提到神经网络,我们往往会想到显卡、服务器,以及动辄数百瓦的功耗。而现在,一颗 MCU 就能完成音频采集、特征提取、神经网络推理和屏幕显示,让设备在本地直接“听懂”周围的声音。
这次实际验证也带来了一个很有意思的变化:当神经网络推理只需要约 0.4 ms 时,性能优化的重点就转移到了特征提取上。FFT、Mel 滤波和对数运算这些步骤,反而成为了整个处理链路中的主要耗时。想让系统响应得更快,除了优化模型,还需要关注数据搬运、内存访问和前端计算。
对于嵌入式开发而言,这也意味着一种新的解决问题的方式。面对难以用固定阈值和规则描述的声音特征,我们可以通过训练,让模型从数据中学习分类依据。开发者的工作则延伸到了数据采集、样本标注、模型训练,以及部署后的实际验证。
当然,这个项目目前只实现了噪声、人声和音乐三种声音的分类。但沿着同样的思路,还可以进一步探索设备异响检测、环境声音识别等应用。越来越多的计算可以直接在设备端完成,不必等待网络传输,也能给出及时的反馈。
让 MCU 从“采集信号”走向“理解信号”,或许正是边缘 AI 最值得期待的地方。
418