eefocus_4256949 发表于 2026-8-18 01:23:12

基于 RA8P1 的声音分类推理工程

本帖最后由 eefocus_4256949 于 2026-8-18 01:28 编辑

> **关于公开内容的重要说明**:本作品将于 8 月 20 日参加全国大学生电子设计竞赛信息科技前沿专题赛现场评审。为保护作品核心成果不泄漏,烧录进 Flash 中的二进制文件暂不公开,本文仅作原理与工程方法交流。声音分类模型目前只分辨 **狗叫、猫叫、鸟叫** 三种声音,其它声音类型的判别方式暂时不公开,敬请理解。

本文记录声音分类推理工程的完整技术路线:模型如何从 tflite 转换到可编译的 C 代码、转换产物怎么用、模型怎么烧录、板端推理链路与 UI 如何实现。整套推理完全在板端完成,不依赖上位机与网络。

## 一、模型转换:从 tflite 到 C 代码

模型先以 TensorFlow Lite 格式(int8 量化)训练导出。RA8P1 的 Ethos-U55 NPU 不能直接执行 tflite,需要先用瑞萨的 **RHUMI 套件**(Renesas Heterogeneous Unified Model Interface)做一次转换,把 tflite 变成可以直接编译进工程的 C 源码。

RHUMI 会自动分析模型图,按算子类型把网络拆成两部分:

1. **能上 NPU 的算子**(卷积、全连接等)→ 编译成 Ethos-U55 专用"命令流",运行时由 NPU 硬件执行;
2. **不能上 NPU 的算子**(量化、反量化等预处理/后处理)→ 保留为 CPU 代码,由 TFLite-Micro 内核执行。

转换输出位于 `converted/build/MCU/compilation/src/`,另外按子图分目录:

```text
sub_0000__CPU_C_CODEGEN             # CPU 子图(模型输入侧)
sub_0001__ARM_ETHOS_U55_C_CODEGEN   # NPU 子图(模型主体)
sub_0002__CPU_C_CODEGEN             # CPU 子图(模型输出侧)
```

## 二、转换产物说明

整张模型的推理被串成三个子图:**CPU 量化 → NPU 主体推理 → CPU 反量化**。`src/` 目录下的文件按职责分组:

### 1. 统一入口(直接使用,不需要改)

| 文件 | 作用 |
| --- | --- |
| `model.c / model.h` | 把三个子图串成一次 `RunModel()` 调用,并暴露输入/输出缓冲指针 |
| `hal_entry.c` | RHUMI 附带的示例入口,演示特征提取与调用模型的完整流程,可作为移植参考 |

### 2. NPU 子图(sub_0001,由 Vela 编译器生成)

| 文件 | 作用 |
| --- | --- |
| `sub_0001_model_data.c / .h` | 模型权重数据(约 6 MB 二进制的 C 数组展开,体积最大的文件) |
| `sub_0001_command_stream.c / .h` | NPU 命令流,即 Ethos-U55 要执行的指令序列 |
| `sub_0001_invoke.c / .h` | NPU 调用封装:把权重、工作区(arena)和各张量内存地址打包成 `base_addrs[]`,调用 Ethos-U 驱动 `ethosu_invoke_v3()` 执行 |
| `sub_0001_tensors.c / .h`、`sub_0001_io_data.c / .h` | 张量信息表与 IO 缓冲区地址偏移,并定义 NPU 工作区大小(491,520 字节 ≈ 480 KB arena) |

### 3. CPU 子图

| 文件 | 作用 |
| --- | --- |
| `compute_sub_0000.c / .h`、`compute_sub_0002.c / .h` | 量化/反量化等 CPU 算子的实现 |
| `kernel_library_int.c / .h`、`kernel_library_utils.c / .h` | TFLite-Micro 内核库,CPU 算子的运行时依赖 |
| `ethosu_common.h` | 张量描述结构(`TensorInfo`)定义 |

## 三、接口与调用方式

三个统一接口就够用了:

```c
float *GetModelInputPtr_serving_default_input_1_0();            /* 输入指针:96×64 的 mel 特征缓冲 */
void   RunModel(bool clean_outputs);                            /* 执行一次推理 */
float *GetModelOutputPtr_StatefulPartitionedCall_1_0_70040();   /* 输出指针:各类别置信度 */
```

`RunModel()` 内部依次执行三个子图:

```c
compute_sub_0000(compute_arena_sub_0000, input, quantized);   /* CPU:float 特征 → int8 */
memcpy(npu_arena + offset, quantized, 6144);                  /* 把量化数据送入 NPU 输入区 */
sub_0001_invoke(clean_outputs);                                 /* NPU:执行命令流 */
compute_sub_0002(compute_arena_sub_0002, npu_out, output);      /* CPU:int8 → float 置信度 */
```

应用层的用法就是"填特征 → 跑一次 → 读置信度"三步:

```c
float *input = GetModelInputPtr_serving_default_input_1_0();
memcpy(input, mel_patch, 96 * 64 * sizeof(float));   /* 填一帧特征 */
RunModel(false);                                    /* 推理 */
float *out = GetModelOutputPtr_StatefulPartitionedCall_1_0_70040();
/* 对 out 取最大值下标即类别,值即置信度 */
```

## 四、模型如何烧录与内存布局

转换产物里有两个大块,存放位置不同:

### 1. 模型权重(约 6 MB)——烧入 OSPI Flash 专用槽

默认编译方式会把权重数组直接编进固件,固件体积会撑到 6 MB 以上,不可接受。本工程的做法:

1. 先把 `sub_0001_model_data.c` 里的权重数组**提取为二进制文件**(`MODEL_AUDIO.BIN`,约 6 MB);
2. 烧录时写入 OSPI Flash 的专用 **8 MiB 槽位(地址 `0x80800000`)**,推理前做 **CRC 校验**;
3. 工程源码里把权重数组替换为一个**指针常量**,让 NPU 直接从 OSPI 的 memory-mapped 窗口读取,不占 SDRAM、不占固件空间:

```c
const uint8_t * const sub_0001_model_data = (const uint8_t *)0x80800000UL;
```

4. 模型更新走板载串口 **XMODEM-CRC 上传命令**(2 Mbps),下载前自动停止推理会话,上传后校验匹配再允许恢复推理。

### 2. NPU 工作区 arena(约 480 KB)——必须放非缓存 SDRAM

arena 是 NPU 推理时的可写工作区,由 NPU 的 DMA 直接读写。放普通可缓存 RAM 时,CPU 会从 D-Cache 读到陈旧数据导致结果错误。工程里把生成代码的 arena 定义加一段属性:

```c
/* 转换产物原样 */
__attribute__((aligned(16))) uint8_t sub_0001_arena;

/* 工程版:放入非缓存 SDRAM 段 */
__attribute__((section(".sdram_nocache"), aligned(16))) uint8_t sub_0001_arena;
```

输入/输出缓冲(float 特征与 float 置信度)无 DMA 参与,放普通内存即可。

## 五、本工程的声音推理

推理链路:

1. **采集链路**:麦克风信号经 WM8960 编解码器以 48.828 kHz 立体声采集,由 DMA 写入双缓冲,再经重采样器转成 16 kHz 单声道,供后续分析与识别共用。
2. **分段处理**:推理会话按 **5 秒一段** 切分。每段数据直接写入片内内存 PCM 缓冲(约 160 KB,位于 SDRAM),段满即送推理——全程不写 SD 卡,**无卡也能运行**。
3. **特征提取**:对每一段音频计算 mel 频谱图(96×64 特征图),作为模型输入。
4. **分段表决**:为提高鲁棒性,每一段会切分为若干短片段分别推理,再对结果做**多数表决**,表决结果作为该段的最终类别;段置信度取"胜出类在判定为自身的短片段上的平均置信度"。表决机制能显著降低单点误判带来的抖动。
5. **实时展示**:识别结果按 **狗叫 / 猫叫 / 鸟叫** 展示,屏幕同时给出置信度百分比、已分析段数与失败段数,每 5 秒刷新一次。

## 六、UI:单页推理界面

界面保持"单页、无跳转"原则,开机直接进入推理页:

- 左侧两个按钮:**START** 开始推理、**STOP** 停止推理;运行中再次触发无效(会话防重入门控),按钮置灰显示。
- 右侧实时结果卡:当前轮计时秒数、当前分类与置信度、已分析/失败段数。
- 停止后计时定格,显示本轮汇总(总时长 HH:MM:SS、各段统计),结果只保留在内存,**不写入 SD、不保存历史**,可立即开始下一轮。

界面文案全部使用 ASCII 字符集,配套字库裁剪为 ASCII 子集,字库生成脚本自动完成。

!(https://www.eefocus.com/forum/data/attachment/forum/202608/18/012807a71gt7277xw4p4y7.jpeg)



## 七、内存与缓存约定(板级验证)

| 数据 | 位置 | 约束 |
| --- | --- | --- |
| 录音采集双缓冲 | 内部 SRAM 的 `.ram_noinit_nocache` 段,32 字节对齐 | I2S DMA 写、CPU 读,必须绕过 D-Cache |
| 推理段 PCM 缓冲 | SDRAM 的 `.sdram_noinit` 段 | CPU 写 / CPU 读(同线程推理),无 DMA |
| NPU 工作区 arena | SDRAM 的 `.sdram_nocache` 段,约 480 KB | NPU 运行期读写,不得放入 OSPI |
| 模型权重 | OSPI Flash memory-mapped(`0x80800000` 槽) | 推理前 CRC 校验,直接读取 |
| FAT 缓存缓冲 | 内部 SRAM 的 `.ram_noinit_nocache` 段,32 字节对齐 | SDHI DMAC 直接写入,避免 CPU 读到陈旧缓存行 |

## 八、工程代码结构(与声音分类相关部分)

| 文件 | 职责 |
| --- | --- |
| `src/player_Thread_entry.c` | 音频线程(录音主循环、DSP、段 PCM 累积) |
| `src/middleware/audio/audio_cmd_handler.c` | 音频命令、5 秒段循环、推理会话状态机 |
| `src/application/audio/model/audio_model_infer.c` | mel 特征提取与 Ethos-U 推理入口 |
| `src/application/audio/model/ethosu_freertos.c` | NPU 互斥/信号量 FreeRTOS 钩子 |
| `src/application/audio/model/ai_model_slot.c / ai_model_upload.c` | OSPI 模型槽校验与 XMODEM 上传 |
| `src/application/audio/model/generated/` | Vela 编译器生成的 NPU 命令流(不含权重数据) |
| `src/application/core/app_bridge.c` | 线程间命令/消息桥与推理会话门控 |
| `src/application/ui/ui_manager.c、ui_page_infer.c` | 单页推理 UI |

篇幅所限,模型训练、量化与 Vela 编译流程未展开;感兴趣的队伍欢迎交流。

通过网盘分享的文件:音频模型.zip
链接: https://pan.baidu.com/s/1Wno2164rDZrxV4Ddf8Isng?pwd=siwq 提取码: siwq

页: [1]
查看完整版本: 基于 RA8P1 的声音分类推理工程