• 正文
  • 相关推荐
申请入驻 产业图谱

STM32的NPU从训练到部署完成一个本地语音分类,推理仅0.5ms!

10/12 13:05
418
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

上一期我们详细的介绍了 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 lines     before 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 dirty     in 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 最值得期待的地方。

相关推荐

嵌入式开发兴趣爱好者,热衷于电子信息技术,嵌入式开发,物联网技术等领域开发。