当前文档版本为 v0.8.0,您可以访问当前页面的 开发中 版本以获取最近可能的更新。

Bluetooth: Central Audio

1 功能概述

本文主要介绍PAN1080 BLE音频传输-主机例程,负责通过蓝牙接收从机端的音频数据,并在音频设备播放出来。

系统框图如图所示:

image

BLE音频传输系统框图

2 环境要求

  • board: pan108xxb5_evb

  • uart (option): 显示串口log

  • ES8316模块:带喇叭或耳机

需要搭配一个运行 peripheral_audio 的板子一起使用。

3 编译和烧录

例程位置:zephyr\samples_panchip\bluetooth\central_audio

使用 ZAL 工具可以对其进行编译、烧录、打开 VS Code 调试等操作。关于 ZAL 工具的详细介绍请参考:Zephyr APP Launcher 工具介绍

4 演示说明

  1. 端口连接

    IIC端口连接:

    PAN1080 EVB板端口

    ES8316模块端口

    P0_0(I2C0_SCL)

    IIC:CLK

    P0_1(I2C0_SDA)

    IIC:DAT

    IIS端口连接:

    PAN1080 EVB板端口

    ES8316模块端口

    P4_3(I2SMCLK)

    MCLK

    P4_4(I2SM_WS)

    DLRCLK

    P4_0(I2SM_CLK)

    SCLK

    P4_1(I2SM_DI)

    Useless

    P4_2(I2SM_DO)

    DSDIN

  2. 配置语音发送端(参考peripheral_audio)。

  3. 上电,等待蓝牙连接。

  4. 蓝牙连接成功后,喇叭或耳机就能听到对端的声音。

5 开发说明

5.1 音频数据采集

从机端的CODEC芯片将MIC的数据通过I2S传输给PAN1080,此时PAN1080作I2S的SLAVE,CODEC芯片作I2S的MASTER,PAN1080接收从CODEC芯片采集的数据。

CODEC芯片初始化的接口:

void I2S_HW_Init(void)
{
	uint32_t data_len,cycle_cnt = 0;
    printk("\nCPU @ %dHz,\n", SystemCoreClock);

	I2S_GPIO_Init();
	I2C_InitRole(I2C0,I2C_MODE_MASTER,ADR10BIT_DISABLE);
    I2C_SetTxTirggerLevel(I2C0, I2C_TX_TL_1);

	data_len = sizeof(codec8316_startup);
	while(cycle_cnt < data_len)
    {
		I2C_Write(I2C0,codec8316_startup+cycle_cnt,2);
		cycle_cnt += 2;
		if((codec8316_startup[cycle_cnt-2] == 0x0) &&
           (codec8316_startup[cycle_cnt-1] == 0xc0))
        {
			k_sleep(50);
		}
		if(cycle_cnt == 2)
        {
			k_sleep(5);
		}
		if(cycle_cnt == 6)
        {
			k_sleep(30);
		}
	}
	I2S_DMA_Init();
}

PAN1080 DMA方式接收音频数据初始化接口:

void I2S_DMA_Init(void)
{
	DMAC_ChannelConfigTypeDef RxConfigTmp = {0,};

	CLK_APB1PeriphClockCmd(CLK_APB1Periph_I2SS,ENABLE);

	I2S_ModduleInit();
	I2S_SysInit();
	DMAC_Init(DMA);
	IRQ_DIRECT_CONNECT(DMA_IRQn, 0, DMA_IRQHandler, 0);
	irq_enable(DMA_IRQn);
	I2S_EnableI2s(TGT_IIS);
	I2S_EnableRecBlock(TGT_IIS);

    RxConfigTmp.AddrChangeDst   = DMAC_AddrChange_Increment;
    RxConfigTmp.AddrChangeSrc   = DMAC_AddrChange_NoChange;
    RxConfigTmp.BurstLenDst     = DMAC_BurstLen_4;
    RxConfigTmp.BurstLenSrc     = DMAC_BurstLen_4;
    RxConfigTmp.DataWidthDst    = DMAC_DataWidth_16;
    RxConfigTmp.DataWidthSrc    = DMAC_DataWidth_16;
    RxConfigTmp.TransferType    = DMAC_TransferType_Per2Mem;
    RxConfigTmp.FlowControl     = DMAC_FlowControl_DMA;
    RxConfigTmp.HandshakeDst    = DMAC_Handshake_Hardware;
    RxConfigTmp.HandshakeSrc    = DMAC_Handshake_Hardware;
    RxConfigTmp.PeripheralSrc   = DMAC_Peripheral_IIS_Rx;
    RxConfigTmp.IntEnable       = ENABLE;

	/*get free dma channel;*/
	gDMARxChNum = DMAC_AcquireChannel(DMA);
	/*enable dma transfer interrupt*/
	DMAC_ClrIntFlagMsk(DMA,gDMARxChNum,DMAC_FLAG_INDEX_TFR);
	DMAC_SetChannelConfig(DMA,gDMARxChNum,&RxConfigTmp);
	I2S_Recv_Enable();
	printk("dma init\n");
}

CODEC采样频率可设置成8khz或16khz,采样频率设置接口:

void I2S_SetI2sClkDivider(uint16_t u32Div)
{
	CLK->APB1_CLK_CTRL1 &=  ~0xFFFF;
	//CLK->APB1_CLK_CTRL1 |= u32Div;
    CLK->APB1_CLK_CTRL1 |= (3 << 16) | (CODEC_FREQ << 18);       //lrclk
}

注:DMA中断优先级设成0,BLE中断优先级设成1。DMA中断优先级比BLE中断优先级高是为了防止BLE影响音频数据的采集从而导致音频数据采集不连续。

5.2 音频数据编解码

本系统音频编解码采用的是ADPCM编解码方式。

ADPCM (ADPCM Adaptive Differential Pulse Code Modulation),是一种针对16bit (或者更高) 声音波形数据的一种有损压缩算法,它将声音流中每次采样的 16bit 数据以 4bit 存储,所以压缩比1:4。而且编解码算法简单,所以是一种低空间消耗,高质量声音获得的好途径。

音频数据编码接口:

int adpcm_encoder(short *indata, unsigned char *outdata, int len)
{
    short *inp;         /* Input buffer pointer */
    unsigned char *outp;/* output buffer pointer */
    int val;            /* Current input sample value */
    int sign;           /* Current adpcm sign bit */
    unsigned int delta; /* Current adpcm output value */
    int diff;           /* Difference between val and valprev */
    unsigned int udiff; /* unsigned value of diff */
    unsigned int step;  /* Stepsize */
    int valpred;        /* Predicted output value */
    unsigned int vpdiff;/* Current change to valpred */
    int index;          /* Current step change index */
    unsigned int outputbuffer = 0;/* place to keep previous 4-bit value */
    int bufferstep;     /* toggle between outputbuffer/output */
    int count = 0;      /* the number of bytes encoded */

    outp = outdata;
    inp = indata;

    valpred = s_valprev;
    index = s_index;

    step = stepsizeTable[index];

    bufferstep = 1;

    while (len-- > 0 ) {
        val = *inp++;

        /* Step 1 - compute difference with previous value */
        diff = val - valpred;
        if(diff < 0)
        {
            sign = 8;
            diff = (-diff);
        }
        else
        {
            sign = 0;
        }
        /* diff will be positive at this point */
        udiff = (unsigned int)diff;

        /* Step 2 - Divide and clamp */
        /* Note:
        ** This code *approximately* computes:
        **    delta = diff*4/step;
        **    vpdiff = (delta+0.5)*step/4;
        ** but in shift step bits are dropped. The net result of this is
        ** that even if you have fast mul/div hardware you cannot put it to
        ** good use since the fixup would be too expensive.
        */
        delta = 0;
        vpdiff = (step >> 3);

        if ( udiff >= step ) {
            delta = 4;
            udiff -= step;
            vpdiff += step;
        }
        step >>= 1;
        if ( udiff >= step  ) {
            delta |= 2;
            udiff -= step;
            vpdiff += step;
        }
        step >>= 1;
        if ( udiff >= step ) {
            delta |= 1;
            vpdiff += step;
        }

        /* Phil Frisbie combined steps 3 and 4 */
        /* Step 3 - Update previous value */
        /* Step 4 - Clamp previous value to 16 bits */
        if ( sign != 0 )
        {
            valpred -= vpdiff;
            if ( valpred < -32768 )
                valpred = -32768;
        }
        else
        {
            valpred += vpdiff;
            if ( valpred > 32767 )
                valpred = 32767;
        }

        /* Step 5 - Assemble value, update index and step values */
        delta |= sign;

        index += indexTable[delta];
        if ( index < 0 ) index = 0;
        if ( index > 88 ) index = 88;
        step = stepsizeTable[index];

        /* Step 6 - Output value */
        if ( bufferstep != 0 ) {
            outputbuffer = (delta << 4);
        } else {
            *outp++ = (char)(delta | outputbuffer);
            count++;
        }
        bufferstep = !bufferstep;
    }

    /* Output last step, if needed */
    if ( bufferstep == 0 )
    {
        *outp++ = (char)outputbuffer;
        count++;
    }

    *outp++=s_valprev & 0xff;	//save value prev
	*outp++=(s_valprev >> 8) & 0xff;
	*outp=s_index;		        //save index
	count = count + 3;

    s_valprev = (short)valpred;
    s_index = (char)index;

    return count;
}

音频数据解码接口:

int adpcm_decoder(unsigned char *indata, short *outdata, int len)
{
    unsigned char *inp; /* Input buffer pointer */
    short *outp;        /* output buffer pointer */
    unsigned int sign;  /* Current adpcm sign bit */
    unsigned int delta; /* Current adpcm output value */
    unsigned int step;  /* Stepsize */
    int valpred;        /* Predicted value */
    unsigned int vpdiff;/* Current change to valpred */
    int index;          /* Current step change index */
    unsigned int inputbuffer = 0;/* place to keep next 4-bit value */
    int bufferstep;     /* toggle between inputbuffer/input */
    int count = 0;

    short value_tmp;
    char index_tmp;

    outp = outdata;
    inp = indata;

    value_tmp = inp[len-3] | (inp[len-2] << 8);        //recover value
    index_tmp = inp[len - 1];          //recover index
    len = len - 3;

    valpred = value_tmp;
    index = index_tmp;
    step = stepsizeTable[index];

    bufferstep = 0;

    len *= 2;

    while ( len-- > 0 ) {

        /* Step 1 - get the delta value */
        if ( bufferstep != 0 ) {
            delta = inputbuffer & 0xf;
        } else {
            inputbuffer = (unsigned int)*inp++;
            delta = (inputbuffer >> 4);
        }
        bufferstep = !bufferstep;

        /* Step 2 - Find new index value (for later) */
        index += indexTable[delta];
        if ( index < 0 ) index = 0;
        if ( index > 88 ) index = 88;

        /* Step 3 - Separate sign and magnitude */
        sign = delta & 8;
        delta = delta & 7;

        /* Phil Frisbie combined steps 4 and 5 */
        /* Step 4 - Compute difference and new predicted value */
        /* Step 5 - clamp output value */
        /*
        ** Computes 'vpdiff = (delta+0.5)*step/4', but see comment
        ** in adpcm_coder.
        */
        vpdiff = step >> 3;
        if ( (delta & 4) != 0 ) vpdiff += step;
        if ( (delta & 2) != 0 ) vpdiff += step>>1;
        if ( (delta & 1) != 0 ) vpdiff += step>>2;

        if ( sign != 0 )
        {
            valpred -= vpdiff;
            if ( valpred < -32768 )
                valpred = -32768;
        }
        else
        {
            valpred += vpdiff;
            if ( valpred > 32767 )
                valpred = 32767;
        }

        /* Step 6 - Update step value */
        step = stepsizeTable[index];

        /* Step 7 - Output value */
        *outp++ = (short)valpred;
        count++;
    }

    return count;
}

5.3 音频数据传输

PAN1080从机将采集的音频数据编码后通过BLE传输给PAN1080主机。

BLE从机上传数据的接口:

int proj_template_s2c_notify(const u8_t *data,u16_t len)
{
	return bt_gatt_notify(NULL, &proj_temp.attrs[1], data, len);
}

BLE主机接收数据的接口:

static u8_t notify_func(struct bt_conn *conn,
			   struct bt_gatt_subscribe_params *params,
			   const void *data, u16_t length)
{
	if (!data) {
		printk("[UNSUBSCRIBED]\n");
		params->value_handle = 0U;
		return BT_GATT_ITER_STOP;
	}

	P20 = 1;
	uint8_t *p_buf = (uint8_t *)decode_data;

	decode_data_len = adpcm_decoder(data,decode_data,length);
	ring_buf_put(&ringbuf_raw, p_buf, (decode_data_len * 2));
	if((I2S_data_flag == 0) && (buf_count >= 2))
	{
		I2S_data_flag = 1;
		I2S_EnableClk(TGT_IIS);
	}
	else
	{
		buf_count++;
	}
	P20 = 0;

	return BT_GATT_ITER_CONTINUE;
}

5.4 音频数据播放

主机端PAN1080将解码后的音频数据通过I2S传输给CODEC芯片,此时PAN1080作I2S的MASTER,CODEC芯片作I2S的SLAVE,PAN1080将音频数据发送给CODEC芯片播放。

CODEC芯片初始化接口:

void I2S_HW_Init(void)
{
	uint32_t data_len,cycle_cnt = 0;
    printk("\nCPU @ %dHz,\n", SystemCoreClock);

	I2S_GPIO_Init();
	I2C_InitRole(I2C0,I2C_MODE_MASTER,ADR10BIT_DISABLE);
    I2C_SetTxTirggerLevel(I2C0, I2C_TX_TL_1);

	data_len = sizeof(codec8316_startup);
	while(cycle_cnt < data_len)
    {
		I2C_Write(I2C0,codec8316_startup+cycle_cnt,2);
		cycle_cnt += 2;
		if((codec8316_startup[cycle_cnt-2] == 0x0)
           && (codec8316_startup[cycle_cnt-1] == 0xc0))
        {
			k_sleep(50);
		}
		if(cycle_cnt == 2)
        {
			k_sleep(5);
		}
		if(cycle_cnt == 6)
        {
			k_sleep(30);
		}
	}
	I2S_DMA_Init();
}

主机端CODEC芯片初始化流程和从机端CODEC芯片初始化流程一样,不同的地方是CODEC芯片的配置,从机端配置成音频数据采集,主机端配置成音频数据播放。

PAN1080 DMA方式发送音频数据初始化接口:

void I2S_DMA_Init(void)
{
	DMAC_ChannelConfigTypeDef TxConfigTmp = {0,};

	CLK_APB1PeriphClockCmd(CLK_APB1Periph_I2SM,ENABLE);
	CLK_APB1PeriphClockCmd(CLK_APB1Periph_I2SS,DISABLE);

	I2S_ModduleInit();
	I2S_SysInit();
	DMAC_Init(DMA);
	IRQ_DIRECT_CONNECT(DMA_IRQn, 0, DMA_IRQHandler, 0);
	irq_enable(DMA_IRQn);

	I2S_EnableI2s(TGT_IIS);

	I2S_SetTxTrigLevel(TGT_IIS,0,4);
	I2S_EnableTransmitCh(TGT_IIS,0);

	I2S_EnableTransmitBlock(TGT_IIS);

    /*set tx dma channel control&config register*/
    TxConfigTmp.AddrChangeDst   = DMAC_AddrChange_NoChange;
    TxConfigTmp.AddrChangeSrc   = DMAC_AddrChange_Increment;
    TxConfigTmp.BurstLenDst     = DMAC_BurstLen_8;
    TxConfigTmp.BurstLenSrc     = DMAC_BurstLen_8;
    TxConfigTmp.DataWidthDst    = DMAC_DataWidth_16;
    TxConfigTmp.DataWidthSrc    = DMAC_DataWidth_16;
    TxConfigTmp.TransferType    = DMAC_TransferType_Mem2Per;
    TxConfigTmp.FlowControl     = DMAC_FlowControl_DMA;
    TxConfigTmp.HandshakeDst    = DMAC_Handshake_Hardware;
    TxConfigTmp.HandshakeSrc    = DMAC_Handshake_Hardware;
    TxConfigTmp.PeripheralDst   = DMAC_Peripheral_IIS_Tx;
    TxConfigTmp.IntEnable       = ENABLE;

	/*get free dma channel;*/
	gDMATxChNum = DMAC_AcquireChannel(DMA);
	/*enable dma transfer interrupt*/
	DMAC_ClrIntFlagMsk(DMA,gDMATxChNum,DMAC_FLAG_INDEX_TFR);
	DMAC_SetChannelConfig(DMA,gDMATxChNum,&TxConfigTmp);

	//I2S_EnableClk(TGT_IIS);
	I2S_Send_Enable();
}

CODEC播放频率可设置成8khz或16khz,播放频率设置接口:

void I2S_SetI2sClkDivider(uint16_t codec_freq)
{
	CLK->APB1_CLK_CTRL1 &=  ~0xFFFF;
	CLK->APB1_CLK_CTRL1 |= codec_freq;
    CLK->APB1_CLK_CTRL1 |= (3 << 16) | (3 << 18);
}

注:DMA中断优先级设成0,BLE中断优先级设成1。DMA中断优先级比BLE中断优先级高是为了防止BLE影响音频数据的播放从而导致音频数据播放不连续从而产生噪音。

6 RAM/Flash资源使用情况

Memory region         Used Size  Region Size  %age Used
FLASH:      114656 B       256 KB     43.74%
SRAM:       38980 B        50 KB     76.13%