Асинхронное USB аудио на STM32

Для того, чтобы вывести звук из компьютера в микроконтроллер, у современного распространенного компьютера есть четыре подходящих интерфейса: Ethernet, Bluetooth, USB и S/PDIF. В МК, три из них требует использования внешнего железа (S/P DIF вход напрямую стал поддерживаться начиная с STM32F446), а Ethernet, кроме того — еще и специального драйвера на стороне компьютера. Поэтому выбор пал на USB.

Архитектура шины USB предполагает, что все данные передаются пакетами. Пакеты объединяются в кадры. Кадры отделяются друг от друга специальными маркерами (это просто один из видов пакетов) — SOF, в котором передается 11-ти битный номер кадра и другая информация. Спецификация USB Audio class определяет, что для передачи звука используется изохронный тип передач. Это значит, что звуковой сигнал поступает пакетами с интервалом 1 миллисекунда. Поскольку Для синхронизации поступления данных от USB и вывода их в ЦАП стандарт определяет три типа синхронизации:
-синхронный, когда частота воспроизведения выделяется из частоты маркеров SOF;
-адаптивный, когда частота воспроизведения подстраивается на основе частоты маркеров SOF;
-асинхронный, когда устройство само генерирует частоту воспроизведения, а ее отношение к частоте маркеров SOF передает в специально выделенную конечную точку (explicit feedback endpoint).
Первые два алгоритма требует использования петли ФАПЧ, поскольку для работы сопременных ЦАПов с передискретизацией нужна опорная частота, так называемый мастерклок, кратный частоте дискретизации аудиосигнала. Третий алгоритм требует организации петли обратной связи. В процессорах STM32 существует аппаратная поддержка как для петли ФАПЧ (из внешних компонентов нужен фазовый детектор, ФНЧ и ГУН) — путем вывода сигнала SOF на вывод PA8 и деления мастерклока кодека таймером в 256 раз — так и для асинхронного режима: таймер 2 умеет измерять период сигналов SOF относительно своего источника тактирования. Используя в качестве источника тактирования таймера мастерклок кодека, напрямую измеряем отношение мастерклока к SOF, а, поскольку оно является кратным к частоте дискретизации, — то и требуемое стандартом Fs/Fsof.
Для этого таймер 2 настраиваем с внешним тактированием от сигнала ETR, сбросом от канала 1 и захватом канала 1. Сигнал SOF при этом аппаратно передается в процессоре от модуля USB в канал 1 таймера 2.

//timer2 is clocked via the MCLK frequency and captures its counter value by SOF event
void TMR2_Config(uint32_t freq, uint32_t SYSFREQ)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_ICInitTypeDef TIM_ICInitStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;      
    GPIO_InitTypeDef GPIO_InitStructure;

    // TIM2_CH1_ETR pin (PA.15) configuration
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource15, GPIO_AF_TIM2);

    /* Enable the TIM2 clock */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    TIM_Cmd(TIM2, DISABLE);
    /* Time base configuration */
    TIM_TimeBaseStructure.TIM_Period            = 0xffffffff;
    TIM_TimeBaseStructure.TIM_Prescaler         = 0;
    TIM_TimeBaseStructure.TIM_ClockDivision     = 0;
    TIM_TimeBaseStructure.TIM_CounterMode       = TIM_CounterMode_Up;
    TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
    //clock TIM2 via ETR pin
    TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0);
    /* TIM2 input trigger selection */
    /* Необходимо по приходу сигнала SOF захватывать значение счетчика таймера 2 в регистр захвата, а сам счетчик таймера 2 - сбрасывать
     В процедуре обработки SOF'а флаг захвата сбрасывается, а значение накапливается, для того, чтобы выдать значение feedback rate
     Поскольку система имеет три независимых источника тактовой частоты - генератор MCLK, частота USB SOF от хоста и HSE PLL,
     таймер 2 тактируется от частоты MCLK=12288 кГц. Такми образом, между SOFами
     таймер 2 должен насчитывать примерно 12200-12400. Это значение должно попадать в регистр захвата таймера 2. Т.к. согласно стандарту
     значение feedback value должно выдаваться в формате 10.14 и содержать отношение fs/fsof, а накопление идет 2^SOF_VALUE периодов,
     получаем за период SOF - 12288 импульса
     сдвинуть нужно на 6 разрядов влево, чтобы получить feedback_value
     */

    /* Enable capture*/
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_TRC;
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICFilter = 0;
    TIM_ICInit(TIM2, &TIM_ICInitStructure);

    //программируем TMR2 на захват периода фреймов USB
    TIM_RemapConfig(TIM2,TIM2_USBFS_SOF);
    TIM_SelectInputTrigger(TIM2, TIM_TS_ITR1);
    TIM_SelectSlaveMode(TIM2,TIM_SlaveMode_Reset);

    TIM_Cmd(TIM2, ENABLE);
}

Чтобы рассказать хосту об используемом способе синхронизации, нужно изменить дескриптор интерфейса — это самая простая часть,

  AUDIO_INTERFACE_DESC_SIZE,                    /* bLength */
  USB_INTERFACE_DESCRIPTOR_TYPE,        /* bDescriptorType */
  0x01,                                 /* bInterfaceNumber */
  0x01,                                 /* bAlternateSetting */
  0x02,                                 /* bNumEndpoints - Audio Out and Feedback enpoint*/
  USB_DEVICE_CLASS_AUDIO,               /* bInterfaceClass */
  AUDIO_SUBCLASS_AUDIOSTREAMING,        /* bInterfaceSubClass */
  AUDIO_PROTOCOL_UNDEFINED,             /* bInterfaceProtocol */
  0x00,                                 /* iInterface */
  /* 09 byte*/

.
Кроме нее, потребуется изменить дескриптор конечной точки для приема аудиопотока (меняем тип синхронизации и адрес конечной точки синхронизации) и дописать дескриптор конечной точки синхронизации

/* Endpoint 1 - Standard Descriptor */
AUDIO_STANDARD_ENDPOINT_DESC_SIZE,    /* bLength */
USB_ENDPOINT_DESCRIPTOR_TYPE,         /* bDescriptorType */
AUDIO_OUT_EP,                         /* bEndpointAddress 1 out endpoint*/
0x5                                   /* bmAttributes */
AUDIO_OUT_PACKET+4,0,                 /* wMaxPacketSize in Bytes ((Freq(Samples)+1)*2(Stereo)*2(HalfWord)) */
0x01,                                 /* bInterval */
0x00,                                 /* bRefresh */
AUDIO_IN_EP,                          /* bSynchAddress */
/* 09 byte*/

/* Endpoint 2 - Standard Descriptor */
  AUDIO_STANDARD_ENDPOINT_DESC_SIZE,    /* bLength */
  USB_ENDPOINT_DESCRIPTOR_TYPE,         /* bDescriptorType */
  AUDIO_IN_EP,                         /* bEndpointAddress 2 in endpoint*/
  0x11,                                                    /* bmAttributes */
  3,0,                                                      /* wMaxPacketSize in Bytes 3 */
  1,                                                            /* bInterval 1ms*/
  SOF_RATE,                                                     /* bRefresh 2ms*/
  0x00,                             /* bSynchAddress */
  /* 09 byte*/

Поскольку в асинхронном USB аудио синхронизация идет с помощью изменения длины пакета данных в кадре, код приема данных в кольцевой буфер должен учитывать длину принятого пакета.

static uint8_t  usbd_audio_DataOut (void *pdev, uint8_t epnum)
{     
          uint32_t curr_length,curr_pos,rest;

        if (epnum == AUDIO_OUT_EP)
  {
          curr_length=USBD_GetRxCount (pdev,epnum);
          curr_pos=(IsocOutWrPtr-IsocOutBuff);
          rest=TOTAL_OUT_BUF_SIZE-curr_pos;
          //monitor sample rate conversion
          if (curr_length<AUDIO_OUT_PACKET) {STM_EVAL_LEDToggle(LED3);};
          if (curr_length>AUDIO_OUT_PACKET) {STM_EVAL_LEDToggle(LED5);};

          if (rest<curr_length)
          {
          if (rest>0)
          {memcpy((uint8_t*)IsocOutWrPtr,tmpbuf,rest);
          IsocOutWrPtr = IsocOutBuff;
          curr_length-=rest;
          };
          if ((curr_length)>0)
          {memcpy((uint8_t*)IsocOutWrPtr,(uint8_t *)(&tmpbuf[0]+rest),curr_length);
          IsocOutWrPtr+=curr_length;};
          }
          else
          {
          if (curr_length>0)
          {memcpy((uint8_t*)IsocOutWrPtr,tmpbuf,curr_length);
          // Increment the Buffer pointer
          IsocOutWrPtr += curr_length;};
          }
          //roll it back when all buffers are full
          if (IsocOutWrPtr >= (IsocOutBuff + (AUDIO_OUT_PACKET * OUT_PACKET_NUM)))
                  IsocOutWrPtr = IsocOutBuff;
    /* Toggle the frame index */
    ((USB_OTG_CORE_HANDLE*)pdev)->dev.out_ep[epnum].even_odd_frame =
      (((USB_OTG_CORE_HANDLE*)pdev)->dev.out_ep[epnum].even_odd_frame)? 0:1;
           DCD_EP_PrepareRx(pdev,
                             AUDIO_OUT_EP,
                             (uint8_t*)tmpbuf,
                             AUDIO_OUT_PACKET+16);
    /* Trigger the start of streaming only when half buffer is full */
    if ((PlayFlag == 0) && (IsocOutWrPtr >= (IsocOutBuff + AUDIO_OUT_PACKET*2)))
    {
      /* Enable start of Streaming */
      PlayFlag = 1;
      AUDIO_OUT_fops.AudioCmd((uint8_t*)(IsocOutRdPtr),  /* Samples buffer pointer */
                          AUDIO_OUT_PACKET,          /* Number of samples in Bytes */
                          AUDIO_CMD_PLAY);           /* Command to be processed */
    }
  }

  return USBD_OK;
}

Дальше — сплошные грабли. В виде неотключаемого фильтра четности фрейма на конечных точках.

static uint8_t  usbd_audio_IN_Incplt (void  *pdev)
{
        //This ISR is executed every time when IN token received with "wrong" PID. It's necessary
        //to flush IN EP (feedback EP), get parity value from DSTS, and store this info for SOF handler.
        //SOF handler should skip one frame with "wrong" PID and attempt a new transfer a frame later.

        USB_OTG_DSTS_TypeDef  FS_DSTS;
          FS_DSTS.d32 = USB_OTG_READ_REG32(&(((USB_OTG_CORE_HANDLE*)pdev)->regs.DREGS->DSTS));
          dpid=(FS_DSTS.b.soffn)&0x1;
        if (flag)
           {flag=0;
                DCD_EP_Flush(pdev,AUDIO_IN_EP);
           };
        return USBD_OK;
}

Обработчик маркера SOF обновляет данные для конечной точки синхронизации

static uint8_t  usbd_audio_SOF (void *pdev)
{     uint8_t res;
static uint16_t n;
USB_OTG_DSTS_TypeDef  FS_DSTS;
  /* Check if there are available data in stream buffer.
    In this function, a single variable (PlayFlag) is used to avoid software delays.
    The play operation must be executed as soon as possible after the SOF detection. */
if (usbd_audio_AltSet==1)
{
        shift=0;
        gap=(IsocOutWrPtr-IsocOutRdPtr);
        tmpxx=(DMA1_Stream7->NDTR)%96;
        if (tmpxx==0) tmpxx+=96;
        if (gap<0) gap+=(AUDIO_OUT_PACKET * OUT_PACKET_NUM);
        shift=-(gap+tmpxx*2-(AUDIO_OUT_PACKET * 2))>>3;
        accum+=(TIM2->CCR1);
            if (shift!=0) accum+=shift;
            SOF_num++;
                if (SOF_num==(1<<SOF_RATE))
                {if (SOF_RATE>6)
                        {feedback_data+=accum>>(SOF_RATE-6);}
                else
                        {feedback_data+=accum<<(6-SOF_RATE);};
                feedback_data>>=1;
                SOF_num=0;
                accum=0;
                }

                if ((!flag))
                        {
                        FS_DSTS.d32 = USB_OTG_READ_REG32(&(((USB_OTG_CORE_HANDLE*)pdev)->regs.DREGS->DSTS));
                        if (((FS_DSTS.b.soffn)&0x1) == dpid)
                        {
                        DCD_EP_Tx (pdev, AUDIO_IN_EP, (uint8_t *) &feedback_data, 3);
                        flag=1;
                        };
                        };

 }

return USBD_OK;
}

Если данные успешно отправлены — все повторяется

static uint8_t  usbd_audio_DataIn (void *pdev, uint8_t epnum)
{
        if (epnum == (AUDIO_IN_EP&0x7f))
        {
                flag=0;
                SOF_num=0;
        }
        return USBD_OK;
}

В обработчике прерываний от DMA аудиокодека обновляем положение указателя буфера чтения

void update_audio_buf(void)
{uint8_t res;
/* Start playing received packet */

if(PlayFlag == 1)
{
// First time: IsocOutRdPtr = IsocOutBuff
        res=AUDIO_OUT_fops.AudioCmd((uint8_t*)(IsocOutRdPtr),  /* Samples buffer pointer */
                            AUDIO_OUT_PACKET,          /* Number of samples in Bytes */
                            AUDIO_CMD_PLAY);           /* Command to be processed */
        IsocOutRdPtr += AUDIO_OUT_PACKET;
        if (IsocOutRdPtr >= (IsocOutBuff + (TOTAL_OUT_BUF_SIZE)))
        {/* Roll back to the start of buffer */
        IsocOutRdPtr = IsocOutBuff;
        }
}
else {
        IsocOutRdPtr = IsocOutBuff;
        IsocOutWrPtr = IsocOutBuff;
        res=AUDIO_OUT_fops.AudioCmd((uint8_t*)(IsocOutRdPtr),  /* Samples buffer pointer */
                            AUDIO_OUT_PACKET,          /* Number of samples in Bytes */
                            AUDIO_CMD_PAUSE);           /* Command to be processed */
}
}

В архиве USB_STM32F4 версия с подключением внешнего генератора (на PC7, PC9, PA15 — осциллятор 12,288 МГц), USB_STM32F4_int_osc.zip — с PLLI2S в качестве источника тактирования, требуется только перемычка PC7-PA15
UPD: Обновил вложение с USB_STM32F4_int_osc.zip, была небольшая ошибка, из-за которой звук не включался
В заключение, выражаю свою благодарность автору проекта audio-widget Børge Strand-Bergesen и Dr. Tsuneo Chinzei.
UPD:Обновил вложение (768khz.zip), теперь там еще и стереокодер для ФМ-вещательного сигнала (вывод PA5) и исправлен хруст в звуке в кодеке. Схема подключения DDS AD9951 и микроконтроллера в архиве.

UPD2:Добавил возможность выбора нескольких частот дискретизации и разрешения (16 бит — до 192 кГц,24 бит — до 96 кГц), работа с обратной связью улучшена

Добавить комментарий

Обратная связь

Интересуют вопросы реализации алгоритмов, программирования, выбора электроники и прочая информация, постараюсь осветить в отдельных статьях

пишите мне на netdm@mail.ru