快速开发*
1. 说明*
本文旨在帮助开发者基于已有框架迅速开发应用程序,只介绍应用开发必须了解的内容,不对框架具体流程作说明。
2. 多核异构与核间通信*
- VSP 是用于语音信号处理的软件框架,运行在 MCU 、 DSP 、 CPU 和 NPU 上。
- VSP 各核之间通过中断消息和共享内存通信。
-
核间通信消息共5条,每条中断可附带32字节数据
GX8008 和 GX8008C 的内存即片内 1.5MB SRAM,MCU DSP 和各个模块都可访问。 DSP 独占部分在编译配置的 SRAM size kept for DSP(KB) 选项配置,将会在 SRAM 尾部为 DSP 留下这么多的独占内存。其他部分各模块共享。 MCU使用 MCU域地址,其他核以及周边模块使用 DEV域地址。通过如下宏函数切换:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// MCU -> DSP int DspPostVspMessage(const uint32_t *message, const uint32_t size); int DspSendVspMessage(const uint32_t *request, const uint32_t size, DSP_VSP_CALLBACK callback, void *priv, unsigned int timeout_ms); // DSP -> MCU int McuPostVspMessage(const unsigned int *response, const unsigned int size); // MCU -> CPU int CpuPostVspMessage(const uint32_t *message, const uint32_t size); // CPU -> MCU static int _VspSendMcuRequest(VSP_CORE *vsp_core, VSP_MSG_CPU_MCU *request, VSP_MSG_MCU_CPU *response); static int _VspPostMcuRequest(VSP_CORE *vsp_core, VSP_MSG_CPU_MCU *request); // MCU -> NPU int SnpuRunTask(SNPU_TASK *task, SNPU_CALLBACK callback, void *private_data);
GX8008 和 GX8008C 支持XIP,MCU 和 DSP 对 XIP 使用需编译配置分别使能 MCU settings 和 DSP settings 内各自的 Enable XIP。 使能 XIP 后 DSP 测通过在变量或函数加如下两个两个宏来将其置入 XIP#define MCU_TO_DEV(x) ((unsigned int)x >= 0x40000000 ?\ (void *)((unsigned int)x - 0x40000000) : (void *)((unsigned int)x - 0x20000000)) #define DEV_TO_MCU(x) ((void *)((unsigned int)x + 0x40000000))
#define XIP_TEXT_ATTR __attribute__((section(".xip.text"))) #define XIP_RODATA_ATTR __attribute__((section(".xip.rodata")))
3. 编译配置系统*
- 源文件是 vsp_sdk 中的各 Kconfig 文件。
- 执行此条命令打开编译配置界面:
$ make menuconfig
- 方向键上下进行项目选择,左右选择底部菜单,空格选择勾选或取消勾选当前项目,回车键用于确认和进入下级菜单。
- 该配置系统比较复杂,需要结合软件硬件整体框架才能说明清楚。 本文不进行深入展开,仅在后文对必要的部分进行介绍。
4. 板级说明*
- 板级配置相关代码是与硬件相关的初始化配置,大部分内容根据硬件决定。
4.1 板级选择*
- 板级代码在 mcu/boards/ ,种类较多。mcu/boards/nationalchip 中是国芯公版。
- 命令行执行 $make menuconfig 打开编译配置:
4.2 板级配置*
- 如果选择了 GX8008C Wukong Prime Device Board V1.4,可以进入 Board Options: 进行相关配置, 引脚默认复用为GPIO:
4.3 通道配置示例*
- 以下示例基于 GX8008C Wukong Prime Device Board V1.4板级,通道的顺序可以根据需求来调换的,通道的有效数量与VSP I/O Buffer settings 中配置的通道数量相关,通道数量配置说明见 5.数据格式与数据流
- 注意:如果硬件开发板不支持以下示例,请联系我司销售人员或者硬件工程师
4.3.1 Mic通道硬件和软件的关系*
- Mic硬件输入通道
- Dmic引脚复用
- Dmic原理图
1 |
|
- Amic在软件中的配置0对应硬件中如图4-1的AIN0,以此类推,3对应AIN3。
- Dmic在软件中的 配置0对应硬件中 如图4-3的0-0,前面的0是代表挂在 PDM_DATA0线;对应的配置1对应硬件的0-1;配置2对应1-0;配置3对应1-1。
4.3.2 例1:4Amic+2Inter_Ref*
- 示例1固件包下载地址
- 这个配置示例中,mic channel 0 对应的配置是0,所以mic channel 0对应的硬件就是AIN0,如果配置的是1,那么mic channel 0对应的硬件就是AIN1
4.3.3 例2:4Dmic+2Amic_Ref*
- 示例2固件包下载地址
- 这个配置示例中,mic channel 0 对应的配置是0,所以mic channel 0对应的硬件就是DMic0-0,如果配置的是1,那么mic channel 0对应的硬件就是DMic0-1
4.3.4 例3:2Amic+2Amic_Ref*
- 示例3固件包下载地址
- 这个配置示例中,mic channel 0 对应的配置是0,所以mic channel 0对应的硬件就是AIN0,如果配置的是1,那么mic channel 0对应的硬件就是AIN1
4.4 增益配置*
4.4.1 Mic增益*
- Mic增益根据Mic的类型会有不同的步进,Amic范围是0~98,步进是2dB;Dmic是0~54,步进是6dB。
4.4.2 Ref增益*
- Ref增益根据Ref的源的不同会有不同的步进,Amic范围是0~98,步进是2dB;Dmic/IIS/internal是0~54,步进是6dB。
4.5 注意事项*
-
引脚复用冲突:例如 dsp uart与i2s out以及spi1引脚冲突,这几个功能不能同时使用,还有其他部分引脚冲突,需要关注mcu/boards/nationalchip/leo_mini_gx8008c_wukong_prime_1_4v/misc_board.c这个文件,这里有引脚说明。
-
如果选择了其他板级,需要在相应板级的
audio_board.c
中修改 mic ref 和增益配置,在misc_board.c
修改引脚复用。 可参考mcu/boards/nationalchip/leo_mini_gx8008c_wukong_prime_1_4v/ 。 当然,如果板级原本的配置就满足您的使用,则不需另作修改。
5. 数据格式与数据流*
- VSP 音频处理采用流水线结构,在核间通信里通过 context 传递数据:
[Audio In] --中断-> [MCU] --中断-> [DSP] --中断-> [MCU] --中断-> [CPU] `-- 应用框架-> [APP]
-
context 数据格式由结构体 VSP_CONTEXT 进行规定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
typedef struct { VSP_CONTEXT_HEADER *ctx_header; // DEVICE ADDRESS unsigned mic_mask:16; unsigned ref_mask:16; unsigned int frame_index; // FRAME index of the first frame in CONTEXT unsigned int ctx_index; // CONTEXT index from 0 - (2^32 - 1) unsigned int vad:8; unsigned int kws:8; // 关键词值,kws触发后此处被填上大于等于 100 的值 unsigned int mic_gain:8; unsigned int ref_gain:8; int direction; SAMPLE *out_buffer; // DSP 输出 buffer,输出音频时一般分为多通道使用,需要主要是否交织 void *features; // DEVICE ADDRESS void *snpu_buffer; // DEVICE ADDRESS void *ext_buffer; // DEVICE ADDRESS } VSP_CONTEXT;
VSP_CONTEXT 与其内管理的 buffer 组成新的结构体
新的结构体通过循环 buffer 管理1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
typedef struct { VSP_CONTEXT context; #if CONFIG_VSP_OUT_NUM > 0 SAMPLE out_buffer[VSP_FRAME_SIZE * CONFIG_VSP_OUT_NUM * CONFIG_VSP_FRAME_NUM_PER_CONTEXT]__attribute__ ((aligned (128))); #endif #ifdef CONFIG_VSP_VPA_FEATURES_DIM float features[CONFIG_VSP_VPA_FEATURES_DIM * CONFIG_VSP_FRAME_NUM_PER_CONTEXT]; #endif #ifdef CONFIG_VPA_SNPU_BUFFER_SIZE SNPU_FLOAT snpu_buffer[CONFIG_VPA_SNPU_BUFFER_SIZE]; #endif #ifdef CONFIG_VSP_VPA_EXT_BUFFER_SIZE unsigned int ext_buffer[(CONFIG_VSP_VPA_EXT_BUFFER_SIZE * 1024) / 4]; #endif } __attribute__ ((aligned (8))) VSP_CONTEXT_BUFFER;
1
static VSP_CONTEXT_BUFFER s_context_buffer[CONFIG_VSP_SRAM_CONTEXT_NUM] __attribute__((aligned(128)));
-
context 的循环 buffer 通过 VSP_CONTEXT_HEADER 进行管理 VSP_CONTEXT_HEADER 中同时也管理着 mic_buffer 和 ref_buffer 等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
typedef struct { unsigned int version; unsigned int mic_num; // 1 - 8 unsigned int ref_num; // 0 - 2 unsigned int spk_num; // 0 - 2 unsigned int out_num; // 1 - 15 unsigned int out_interlaced; // 1: interlace or 0: non-interlace unsigned int frame_num_per_context; // the FRAME num in a CONTEXT unsigned int frame_num_per_channel; // the total FRMAE num in a CHANNEL unsigned int frame_length; // 10ms, 16ms unsigned int sample_rate; // 8000, 16000, 48000 int ref_offset_to_mic; // MIC signal offset to REF in unit of context. // If REF is saved before MIC, the offset is > 0; // otherwise, it will be < 0. unsigned int features_dim_per_frame; // Features dimension per FRAME // CTX buffer for GLOBAL void *ctx_buffer; // Context Buffer header point unsigned int ctx_num; // Context number unsigned int ctx_size; // Context size // SNPU, EXTRA and OUT buffer for CONTEXT unsigned int snpu_buffer_size; // Bytes unsigned int ext_buffer_size; // Bytes unsigned int out_buffer_size; // Bytes // MIC buffer for GLOBAL SAMPLE *mic_buffer; // DEVICE ADDRESS unsigned int mic_buffer_size; // Bytes // REF buffer for GLOBAL SAMPLE *ref_buffer; // DEVICE ADDRESS unsigned int ref_buffer_size; // Bytes // SPK buffer for GLOBAL SAMPLE *spk_buffer; // DEVICE ADDRESS unsigned int spk_buffer_size; // Bytes // TMP buffer for GLOBAL void *tmp_buffer; // DEVICE ADDRESS unsigned int tmp_buffer_size; // Bytes } VSP_CONTEXT_HEADER;
-
一般数据流程如下所示,省略了部分细节(可保存动图,按帧播放):
-
图中例子设置了4路 mic 、2路 ref 和1路 spk,这几个 buffer 地址连续,通道长度为12个context周期。 AudioIn 每个 context 周期通过中断上报一次采集到的数据。 采集的音频数据将在第12个音频周期后被覆盖,在这之前最多保存11个context周期的数据供随时取用。 设置了4个 context,各 context 中的 output 地址不连续。 context循环使用,在不影响流水的情况下,各核可不用及时向下传递。
-
这些音频采集相关参数需要在 make menuconfig 中的VSP I/O Buffer settings 进行相关配置 这些配置也同步设置了相关 buffer 参数:
buffer 管理的代码在
mcu/vsp/vsp_buffer.h, mcu/vsp/vsp_buffer.c
6. 工作模式*
- VSP 支持选择不同的工作模式,不同工作模式的侧重不同
- GX8010 和 GX8009 支持切换模式,GX8008 和 GX8008C 一般不支持切换工作模式
- GX8008C和GX8008常用的工作模式是 UAC 和 PLC
- UAC 模式提供已支持的所有 UAC 功能,对应用框架的支持比 PLC 模式弱:
mcu/vsp/vsp_mode_uac.c
- PLC 模式支持部分UAC功能,对应用框架支持较好:
mcu/vsp/vsp_mode_plc.c
7. UAC*
- UAC 核心功能是
下行播放
与上行录音
- 使用 UAC 功能需要在编译配置进行相关设置,建议选 UAC 模式。
UAC 1.0 兼容性较好,UAC 2.0 可支持更多路的数据
- 下行播放需要使能
Enable UAC Playback
然后设置需要的参数 如果您需要获取下行数据另行处理的话请使能Enable get playback data at APP or other
使能Play by Audio out
可通过 UAC 框架直接播放 - 上行录音的数据源是 output_buffer 的数据
上行录音数据必须是交织的,且通道数由
OUT Channel Interlaced Number
指定。 其他选项参考工作模式 - 需要录制 mic ref 等通道的原始数据时建议,vsp_sdk 中的 bypass 算法
Voice Process Algorithm select: (Bypass [Source])
该算法可将 mic ref 等通道原始数据交织放入 output buffer 供 UAC 上行使用。 - 如果您使用的开发板是
GX8008C Wukong Prime Device Board V1.4
可使用./configs/nationalchip_public_version/8008c_wukong_v1.4_uac_demo.config
编译生成 UAC demo 烧录生成的固件到板子中,连接上 PC ,可发现名为Nationalchip
的音频输入和输出设备,可进行 4 通道录音,和 2 通道播音。
8. 获取音频数据*
8.1 MCU侧获取音频数据*
-
参考
mcu/vsp/hook/vsp_hook_codec_v1_0.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
int HookEventResponse(PLC_EVENT plc_event) { if (plc_event.event_id == DSP_PROCESS_DONE_EVENT_ID && plc_event.ctx_index > 20) { VSP_CONTEXT *ctx_addr; unsigned int ctx_size; unsigned context_index = plc_event.ctx_index; VspGetSRAMContext(context_index, &ctx_addr, &ctx_size); VSP_CONTEXT_HEADER *ctx_header = VspGetSRAMContextHeader(); int frame_length = ctx_header->frame_length * ctx_header->sample_rate / 1000; int context_sample_num = frame_length * ctx_header->frame_num_per_context; int context_sample_size = frame_length * ctx_header->frame_num_per_context * sizeof(SAMPLE); int context_num_per_channel = ctx_header->frame_num_per_channel / ctx_header->frame_num_per_context; int channel_sample_size = frame_length * ctx_header->frame_num_per_channel * sizeof(SAMPLE); int current_context_index = ctx_addr->ctx_index % context_num_per_channel; void *current_mic_addr = ctx_header->mic_buffer + context_sample_num * current_context_index; void *current_ref_addr = ctx_header->ref_buffer + context_sample_num * current_context_index; void *current_spk_addr = ctx_header->spk_buffer + context_sample_num * current_context_index; void *current_out_addr = ctx_addr->out_buffer; int play_size = (ctx_header->out_buffer_size / ctx_header->out_num) / 8 * 8; // 一个通道一个context周期对应数据长度 #ifdef CONFIG_CODEC_CHANNEL0_OUT0 void *ch0_addr = current_out_addr + play_size * 0; #endif ... } }
8.2 DSP侧获取音频数据*
- 参考 DSP开发 ,也可参考
dsp/vpa/bypass/vpa_process.c
9. DSP开发*
10. 前后台*
- VSP 为前后台系统,前台即各种中断,后台即 main() 中的主循环。
- VSP 中的中断不会被打断,各中断可保存一次中断状态;中断中不可执行耗时过长的操作。
- 中断主要有 AudioIn 采集中断、DSP 返回中断、CPU 中断、AudioOut 播放中断、按键中断、定时器中断等。
- AudioIn 采集中断是数据流水的起点。DSP 中断中触发了一些重要应用事件需要重点关注, 比如 KWS 事件。
UAC 模式:
_UacAudioInRecordCallback(), _UacDspCallback()
PLC 模式:_PlcAudioInRecordCallback(), _PlcDspCallback()
- 后台的主循环中挂载了模式的 tick(), 模式的 tick() 里挂载了应用框架的 tick(), 应用框架的 tick() 中挂载了应用的 tick()这些 tick()都在主循环中被循环调用。
1 2 3
while(VspModeTick()) { ... }
11. 应用框架*
- VSP 的应用框架名为 PLC 通过事件驱动,通过
[事件触发->入队列,出队列->事件响应,后台循环tick]
这样简单的流程实现。 - 应用框架在使用前需要在模式初始化函数中初始化,其中会调用到 APP 的初始化接口
1 2 3 4 5 6 7 8 9 10 11
int VspInitializePlcEvent(void) { ... VspQueueInit(&s_plc_misc_event_queue, s_plc_misc_event_queue_buffer, VSP_PLC_MISC_QUEUE_LEN * sizeof(PLC_EVENT), sizeof(PLC_EVENT)); HookProcessInit(); ... s_init_flag = 1; return 0; }
-
事件触发->入队列 可在 PLC 初始化后任意位置进行,包括中断中。 通过event_id标记不同事件,并可携带相关参数。
event_id 小于 100 的事件是系统事件,100 - 200 一般作为 KWS 事件, 大于 200 的事件用于用户自定义 系统事件在使能 Enable system event 后才会正常触发1 2 3 4 5 6 7
typedef struct { unsigned int event_id; union { unsigned int ctx_index; int uac_volume; }; } PLC_EVENT;
// Private Event ID [1~99] for PLC #define VSP_WAKE_UP_EVENT_ID (90) #define ESR_GOODBYE_EVENT_ID (99) // It defined in the plc_json #define DSP_PROCESS_DONE_EVENT_ID (98) // Used to notify board to send wav to bluetooth #define AUDIO_IN_RECORD_DONE_EVENT_ID (97) #define UAC_DOWN_STREAM_OFF (80) #define UAC_DOWN_STREAM_ON (81) #define UAC_DOWN_SET_VOLUME (82) #define UAC_UP_STREAM_OFF (83)
-
出队列->事件响应 由框架在后台循环tick中进行,受限于 MCU 整体算力负载,不一定及时。 部分事件可由框架进行相应,APP 无需动作,如播放语音回复。 全部事件都会传递给应用接口
1 2 3 4
if (VspQueueGet(&s_plc_misc_event_queue, (unsigned char *)&plc_event)) { HookEventResponse(plc_event); }
- 后台循环tick
1 2 3 4 5 6 7 8
void VspPlcEventTick(void) { ... HookProcessTick(); ... }
12. 新建用户应用*
用户可以新建自己的应用,在上面做二次开发,HookProcessInit()
、 HookEventResponse()
和 HookProcessTick()
这三个接口是用户 APP 需要且仅需要实现的三个函数。
新建 APP 请参考 mcu/vsp/hook/vsp_hook_null.c
, 并修改 mcu/vsp/hook/Kconfig
在 VSP Customize Functions Settings ---> 中增加新建的 APP 选项