跳转至

vsp算法移植指南*

  • 为了帮助算法工程师更快的部署自己的算法,本文所讲的一些配置都是基于GX8008C_Wukong_Prime开发板,简称悟空开发板,因此手中如何没有此开发板,请联系我们的当地的销售人员。

1. 开发板系统框图:*

2. example固件编译和下载*

2.1 example配置*

可以根据下面配置和需求修改

  • example_lib: 一个包含TX和RX处理的算法包
    • TX: 一个简单的多通道混音算法,把输入的n路mic数据混音作为输出
    • RX: 对RX音频作增益放大
  • 8008c_wukong_v1.4_example_lib_16k_2Amic_1Aref_UAC4ch_SPKtxout.config
    • 采样率 16k
    • UAC1.0声卡模式
    • UAC上行输出4路数据,1ch-TX_OUT,2ch-Amic,1ch-Aref
    • AudioOut输出TX_OUT
  • 8008c_wukong_v1.4_example_lib_16k_2Amic_1Aref_UAC6ch_SPKrxout.config
    • 采样率 16k
    • UAC1.0声卡模式
    • UAC上行输出6路数据,1ch-TX_OUT,1ch-RX_OUT,2ch-Amic,1ch-Aref,1ch-RX_IN
    • AudioOut输出RX_OUT
  • 8008c_wukong_v1.4_example_lib_16k_4Dmic_1Aref_UAC6ch_SPKtxout.config
    • 采样率 16k
    • UAC1.0声卡模式
    • UAC上行输出6路数据,1ch-TX_OUT,4ch-Dmic,1ch-Aref
    • AudioOut输出TX_OUT
  • 8008c_wukong_v1.4_example_lib_16k_4Dmic_1Aref_UAC8ch_SPKrxout.config
    • 采样率 16k
    • UAC1.0声卡模式
    • UAC上行输出8路数据,1ch-TX_OUT,1ch-RX_OUT,4ch-Dmic,1ch-Aref,1ch-RX_IN
    • AudioOut输出RX_OUT
  • 8008c_wukong_v1.4_example_lib_48k_UAC2ch_SPKrxout2ch.config
    • 采样率 48k
    • UAC1.0声卡模式
    • UAC上行输出2路数据,2ch-RX_OUT,UAC下行双通道48k
    • AudioOut输出RX_OUT

2.2 如何编译:*

  • vsp_sdk 下载到本地之后,在 vsp_sdk 目录下执行命令
    $ cp configs/example_lib/8008c_wukong_v1.4_example_lib_16k_2Amic_1Aref_UAC4ch_SPKtxout.config .config
    $ make menuconfig # 打开menuconfig,保存并退出
    $ make clean;
    $ make
    
    编译完成后,生成的固件在 output 目录中,mcu_nor.binmcu 的固件,dsp.fwdsp 的固件,vsp.bin 是两个部分合并的固件,

2.3 如何烧录固件 [两种方式]:*

  • 烧录 vsp.bin
    $ sudo tools/bootx/bootx -m leo_mini -t u -c "download 0 output/vsp.bin;reboot"
    
  • 烧录 mcu_nor.bindsp.fw
    $ cd tools/bootx
    $ ./flash_nor_mini.sh
    
  • 下载完成后PC会识别到声卡设备,就可以通过 audacity(录音工具) 进行录音。具体录音操作说明点这里

3. 输入输出通道的配置*

  • 执行 make menuconfig,进入 VSP I/O Buffer settings

    • Channel settings: 设置通道,根据需求设置对应的 micref 通道,输出通道也是根据需求来配置,如果需要 uac 录音必须选择交织输出,勾选 Interlaced OUT Channels,并按需配置你需要通过 uac 录制的路数。
    • Frame settings: 根据需求来配置相应的采样率和帧长
    • Context settings: dsp 算法处理的对象是 context,需要按需配置 Frame Number in a Context

4.如何构建自己的算法包*

vsp_sdk/dsp/vpa 目录下是一个个互相独立的算法包,可以通过 menuconfig 来选择所需要的算法包,本节我们参考 example_lib 来构建属于自己的算法包

  • 拷贝 example_lib 算法目录,重命名为需要移植的算法, 例如改为 gx_lib
  • 修改 gx_lib 目录下的 vpa.nameMakefileKconfig 文件的内容,主要是需要修改部分路径和宏

    • vpa.name 中的内容替换为
          config VSP_VPA_GX_LIB
            bool "GX [Library]"
      
    • Kconfig 中的 VSP_VPA_EXAMPLE_LIB 替换为 VSP_VPA_GX_LIB

    • Makefile 中的 CONFIG_VSP_VPA_EXAMPLE_LIB 替换为 CONFIG_VSP_VPA_GX_LIB,将 SRC_DIR = vpa/example_lib 替换为 SRC_DIR = vpa/gx_lib

  • 构建完算法包后,执行 make menuconfig ,在 Voice Process Algorithm select 中选中新增的算法 gx_lib,编译的就是你新增的算法包

5. 算法移植*

5.1 目录介绍*

  • example_lib整个目录结构如下,高亮部分是工程师需要重点关注的部分
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    dsp/vpa/example_lib
    ├── include # 头文件的目录
       ├── usr_alg.h # 算法的对外头文件
       └── vsp_algorithm.h
    ├── Kconfig
    ├── lib
       └── libusr_alg.a # 算法源码编译生成的库文件
                         # 会先编译usr_alg内的源码生成库文件在当前目录
                         # 如果已经是库文件,则直接把库文件放到这里,忽略目录usr_alg
    ├── Makefile
    ├── src
       ├── usr_alg # 算法的源码可以存放这里,方便统一维护和裁剪
          ├── Makefile
          └── usr_alg.c
       ├── vsp_algorithm.c # 算法集成的代码放这里,方便统一维护
       └── vsp_process.c
    └── vpa.name
    

5.1 算法的初始化*

  • 算法需要初始化的代码放在 VspInitialize 接口中,这个接口会在上电后调用一次
    1
    2
    3
    4
    5
    XIP_TEXT_ATTR int VspInitialize(VSP_CONTEXT_HEADER *context_header)
    {
        VspDoExampleInit(context_header);
        return 0;
    }
    
    1
    2
    3
    4
    5
    IRAM0_TEXT_ATTR int VspDoExampleInit(VSP_CONTEXT_HEADER *context_header)
    {
        /* 算法初始化代码 */
        return 0;
    }
    

5.2 算法的处理*

  • 算法处理的入口函数在 vpa_process.c 中的 VspProcessActive,所有的算法都是在这个函数中进行处理,这个函数会在初始化后,根据配置的context的时长回调。比如Frame Number in a Context是2和Frame settings是16ms,则VspProcessActive每32ms回调一次。

5.2.1 TX 处理*

 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
IRAM0_TEXT_ATTR int VspDoTxExampleProc(VSP_CONTEXT *context, int *output_index)
{
    VSP_CONTEXT_HEADER *ctx_header  = context->ctx_header;
    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 ref_num                    = ctx_header->ref_num;
    int mic_num                    = ctx_header->mic_num;
    int channel_num                = mic_num + ref_num;

    short *mic_buffer[ref_num];
    short *ref_buffer[mic_num];

    short out[context_sample_num];

    int i, j;

    for (i = 0; i < mic_num; i++) {
        mic_buffer[i] = VspProcessGetMicFrame(context,i,0); //获取到N个mic数据
    }
    for (i = 0; i < ref_num; i++) {
        ref_buffer[i] = VspProcessGetRefFrame(context,i,0); //获取到N个ref数据
    }

    for (j = 0; j < context_sample_num; j++) { // 把mic和ref按照算法api需要的格式组织
        for (i = 0; i < mic_num; i++) {
            all_data[i+j*channel_num] = mic_buffer[i][j];
        }
        for (i = 0; i < ref_num; i++) {
            all_data[mic_num+i+j*channel_num] = ref_buffer[i][j];
        }
    }

    // TX data processing. eg:
    MixAudio((short *)all_data, (short *)out, mic_num, ref_num, context_sample_num);

    memcpy(context->out_buffer, out, context_sample_num*sizeof(short)); // 把算法的输出数据搬运到context->out_buffer
    return 0;
}

5.2.2 RX 处理*

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
IRAM0_TEXT_ATTR int VspDoRxExampleProc(VSP_CONTEXT *context, int *output_index)
{
    VSP_CONTEXT_HEADER *ctx_header  = context->ctx_header;

    if (ctx_header->rx_num == 2) { // 双通道处理
        VspCopyRxChannelToOut(context, 0, *output_index);
        VspCopyRxChannelToOut(context, 1, *output_index+1);
        // RX data processing. eg:
        VSPDoSetOutputGain(context, *output_index, -3);  // 对rx数据增益-3dB
        VSPDoSetOutputGain(context, *output_index+1, -3);// 对rx数据增益-3dB

        *output_index += 2;
    }
    else if (ctx_header->rx_num == 1) { // 单通道处理
        VspCopyRxChannelToOut(context, 0, *output_index);
        // RX data processing. eg:
        VSPDoSetOutputGain(context, *output_index, -3);  // 对rx数据增益-3dB

        *output_index += 1;
    }

    return 0;
}

6. 算法相关的 helper 接口*

  • 所有相关的 helper 接口都在 vsp_sdk/dsp/vsp/vsp_helper.c 中。
函数接口 使用说明
int VspProcessInvalidateMicBuffer(VSP_CONTEXT *context) 根据 context 中对应的 mic_buffer 地址和大小,无效 cache 中的数据
int VspProcessWritebackMicBuffer(VSP_CONTEXT *context) 根据 context 中对应的 mic_buffer 地址和大小,将 cache 中的数据写回 sram
int VspProcessInvalidateRefBuffer(VSP_CONTEXT *context) 根据 context 中对应的 ref_buffer 地址和大小,无效 cache 中的数据
int VspProcessWritebackRefBuffer(VSP_CONTEXT *context)
int VspProcessInvalidateRxBuffer(VSP_CONTEXT *context) 根据 context 中对应的 rx_buffer 地址和大小,无效 cache 中的数据
short * VspProcessGetMicFrame(VSP_CONTEXT *context, unsigned int channel_num, int frame_index) 功能:获取当前 context某个mic通道某一帧的麦克风数据起始地址
channel_nummic通道号
frame_index:当前context的第几帧
short * VspProcessGetRefFrame(VSP_CONTEXT *context, unsigned int channel_num, int frame_index) 功能:获取当前 context某个ref通道某一帧的ref数据起始地址
channel_numref通道号
frame_index:当前context的第几帧
short * VspProcessGetRxFrame(VSP_CONTEXT *context, unsigned int channel_num, int frame_index) 功能:获取当前 context某个rx通道某一帧的ref数据起始地址
channel_numref通道号
frame_index:当前context的第几帧
short * VspProcessGetOutFrame(VSP_CONTEXT *context, unsigned int channel_num, unsigned int frame_index) 功能:获取当前 context某个output通道某一帧的output数据起始地址
channel_numoutput通道号
frame_index:当前context的第几帧
VSP_CONTEXT * VspProcessGetContext(const VSP_CONTEXT *context, unsigned int index) 功能:以当前 context 为起始点,获取第 indexcontext 的地址

注意

VspProcessActive 前,我们需要调用 VspProcessInvalidateMicBuffer,VspProcessInvalidateRxBuffer,VspProcessInvalidateRefBuffer 接口无效当前 dsp 中相关的cache 数据,保正算法拿到的 mic,ref,rx 数据都是实时有效的

注意

在使用VspProcessGetRxFrame 获取到uac下行双通道的数据时,数据是交织存储的

7. 算力查看*

我们也提供了实时查看dsp算力的功能

  • 执行 make menuconfig ,进入 DSP settings
  • 使能 Enable Process Cycle StatisticEnable log printing on DSPEnable log printing on DSP 分级下的默认就行,然后重新编译固件,连接 dsp 串口,就会打印算力,超过百分百就是算力超了,需要优化算法。

提醒:如何统计某个热点函数的算力

我们提供了 unsigned xthal_get_ccount(void) 来获取当前 CCOUNT 寄存器的值,DSP 每走一拍会自动加 1,如果 DSP 的频率是400M,那么这个寄存器每秒就会自动加 400M。因此我们在热点函数前后调用 xthal_get_ccount(),然后相减就是该热点函数所消耗的算力。

8. 内存使用*

8.1 SRAM*

  • 8008/8008C 都拥有1536KBSRAM,默认代码都跑在SRAM上,1536KB由MCU和DSP共用,可以配置DSP使用的SRAM内存大小,剩下的内存留给MCU使用。
  • 执行 make menuconfig ,进入 DSP settings,配置 (1300) SRAM size kept for DSP(KB)

如何确定MCU内存

MCU这边不会用到动态内存,只要 make mcu 能编译正常,MCU内存就不会有问题

8.2 DRAM0和IRAM0*

  • DSP上除了SRAM可用,还有64k的DRAM0和64k的IRAM0,默认使用的都是SRAM,如果需要使用这个内存,需要用以下宏

    #define IRAM0_TEXT_ATTR    __attribute__((section(".iram0.text")))
    #define DRAM0_BSS_ATTR     __attribute__((section(".dram0.bss")))
    #define DRAM0_DATA_ATTR    __attribute__((section(".dram0.data")))
    #define DRAM0_RODATA_ATTR  __attribute__((section(".dram0.rodata")))
    
  • DRAM0放一些数据,如

    static short all_data[6*FRAME_LEN] DRAM0_DATA_ATTR;
    

  • IRAM0放一些代码,如

    IRAM0_TEXT_ATTR int VspProcessActive(VSP_CONTEXT *context)
    

8.3 XIP*

  • 在8008c(8008上不支持)上,还可以使用XIP技术,可以把一些执行频率低的代码或者一些只读的数据放到XIP段上面,需要用到以下宏

    #define XIP_TEXT_ATTR      __attribute__((section(".xip.text")))
    #define XIP_RODATA_ATTR    __attribute__((section(".xip.rodata")))
    

  • 执行 make menuconfig ,进入 DSP settings,配置 [*] Enable XIP

  • XIP放一些代码,如

    XIP_TEXT_ATTR int VspInitialize(VSP_CONTEXT_HEADER *context_header)
    

  • XIP放一些代码,如

    short data[3] XIP_RODATA_ATTR = {1, 2, 3};
    

  • 更加详细XIP使用看这里

9. 算法开发相关手册*

  • DSP编译工具链安装完成后,打开 Xplorer,点击 Help→PDF Documentation 就可以看到很多关于 HIFI4 的文档。包含了 Hifi4 规格书、指令集使用说明、Xtexsa编译工具链使用说明文档。

10. Xtensa IDE Lsp Modification Instructions*