跳转至

快速开发*

1. 说明*

本文旨在帮助开发者基于已有框架迅速开发应用程序,只介绍应用开发必须了解的内容,不对框架具体流程作说明。

2. 多核异构与核间通信*

  • VSP 是用于语音信号处理的软件框架,运行在 MCU 、 DSP 、 CPU 和 NPU 上。
  • VSP 各核之间通过中断消息和共享内存通信。
  • 核间通信消息共5条,每条中断可附带32字节数据

     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 的内存即片内 1.5MB SRAM,MCU DSP 和各个模块都可访问。 DSP 独占部分在编译配置SRAM size kept for DSP(KB) 选项配置,将会在 SRAM 尾部为 DSP 留下这么多的独占内存。其他部分各模块共享。 MCU使用 MCU域地址,其他核以及周边模块使用 DEV域地址。通过如下宏函数切换:
    #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))
    
    GX8008 和 GX8008C 支持XIP,MCU 和 DSP 对 XIP 使用需编译配置分别使能 MCU settingsDSP settings 内各自的 Enable XIP。 使能 XIP 后 DSP 测通过在变量或函数加如下两个两个宏来将其置入 XIP
    #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硬件输入通道

图1.芯片引脚

图4-1
  • Dmic引脚复用

图2.Dmic引脚复用

图4-2
  • Dmic原理图
1
![图3.Dmic原理图](./pictures/Dmic.png)
图4-3
  • 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 组成新的结构体

     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;
    
    新的结构体通过循环 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标记不同事件,并可携带相关参数。

    1
    2
    3
    4
    5
    6
    7
    typedef struct {
        unsigned int event_id;
        union {
            unsigned int    ctx_index;
            int             uac_volume;
        };
    } PLC_EVENT;
    
    event_id 小于 100 的事件是系统事件,100 - 200 一般作为 KWS 事件, 大于 200 的事件用于用户自定义 系统事件在使能 Enable system 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/KconfigVSP Customize Functions Settings ---> 中增加新建的 APP 选项