Асинхронное 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 кГц), работа с обратной связью улучшена
  • +7
  • 15 ноября 2015, 12:45
  • romanetz
  • 4

Комментарии (32)

RSS свернуть / развернуть
Вот это прямо очень круто. Спасибо.
0
У меня есть вопрос. Как девайс понимает, какую частоту хост хочет воспроизводить?
Нужно же PLL переключать, по крайней мере. или в примере для Discovery это уже есть?
0
Поддерживаемые частоты описываются в дескрипторе

/* USB Speaker Audio Type III Format Interface Descriptor */
  AUDIO_FORMAT_TYPE_I_DESC_SZ,          /* bLength */
  AUDIO_INTERFACE_DESCRIPTOR_TYPE,      /* bDescriptorType */
  AUDIO_STREAMING_FORMAT_TYPE,          /* bDescriptorSubtype */
  AUDIO_FORMAT_TYPE_I,                  /* bFormatType */
  0x02,                                 /* bNrChannels */
  0x02,                                 /* bSubFrameSize :  2 Bytes per frame (16bits) */
  16,                                   /* bBitResolution (16-bits per sample) */ 
  0x01,                                 /* bSamFreqType only one frequency supported */ 
  SAMPLE_FREQ(USBD_AUDIO_FREQ),         /* Audio sampling frequency coded on 3 bytes */
/* сюда дописываем остальные частоты, которые хотим поддерживать */
  /* 11 byte*/
.
В атрибутах конечной точки аудиопотока надо дописать возможность выбора частоты дискретизации (вот здесь:

/* Endpoint - Audio Streaming (Class-specific) Descriptor*/
     AUDIO_STREAMING_ENDPOINT_DESC_SIZE,   /* bLength */
     AUDIO_ENDPOINT_DESCRIPTOR_TYPE,       /* bDescriptorType */
     AUDIO_ENDPOINT_GENERAL,               /* bDescriptor */
     0x01, <----- вот сюда                                /* bmAttributes */
     0x00,                                 /* bLockDelayUnits */
     0x00,                                 /* wLockDelay */
     0x00,
     /* 07 byte*/
)
В коде нужно будет добавить обработку запросов GET_CUR, GET_MIN, GET_MAX, SET_CUR для конечной точки. И сделать переключение частоты MCLK в соответствии с выбранной хостом частотой дискретизации. Если сделано как сейчас, то хост сам передискретизирует любой звук на 48 кГц. Нужные операции все можно посмотреть в коде audio-widget, файл uac1_usb_specific_request.c
Мне это было не нужно, т.к. в проекте, кроме вывода на наушники, сигнал передискретизируется к частоте 384 кГц, преобразуется в вещательный комплексный стереосигнал и выдается через SPI на AD9951. При изменении частоты дискретизации, необходимо будет вгружать новые коэффициенты в фильтр интерполятора и изменять приращение фазы в генераторе частот для КСС.
0
Я использую внешний генератор мастерклока 12,288 МГц, подключенный на PC7, PC9 и таймеры для управления DDS AD9951. Для вывода только в наушники можно попробовать и PLLI2S использовать. Точность его частоты при использовании асинхронного вывода вполне достаточна.
0
Спасибо.

Для вывода только в наушники можно попробовать и PLLI2S использовать. Точность его частоты при использовании асинхронного вывода вполне достаточна.
У меня идея внешние генераторы повесить переключаемые на специальный вход I2S модуля. Чтобы их переключать, нужно сигнал наружу вывести и делитель переключить.
Но этим я немного позже буду заниматься, сейчас другой проект. На вегалабе тема моя есть, можете глянуть, ник тот же.
0
Я пробовал также и дуплекс делать, пока до ума не довел. Два устройства появляются (микрофон и динамики), но задумка была делать вход или выход КСС с частотой 384 кГц, застрял на том, что в этом случае нужно FIFO буфера конечных точек переинициализировать динамически, в зависимости от команд хоста. Одновременно, естественно, конечные точки такого размера не помещаются ни в полосу шины, ни в предусмотренные FIFO. Уперся, короче, в железо, как всегда.
0
А USB PHI внешнее было?
Потому что с встроенным только Full speed, 384к не влезет, конечно.

На пробу можно модуль типа такого поставить.
USB3300 USB HS Board Host OTG PHY Low Pin ULPI Evaluation Development Module Kit
0
Можно, но тропа та не хожена почти, да еще и UAC2… Где тут можно застрелиться?
384/моно — влезает, там один фрейм 768 байт. Для радиовещания самое то, там реально необходимая полоса частот до 60 кГц (0-15 — моно сигнал, сумма каналов, 23-53 — разностный сигнал каналов с DSB-модуляцией, 55-59 RDS)
0
А какой программой ксс формируется?
0
Там же, в коде проекта, mpx.c
Есть версия поновее, с RDS и улучшенной обработкой, но там асинхронный режим еще не реализован.
Если сделать вход от компа с частотой дискретизации 384 кГц, можно и на компе, например, ПО StereoTool или MPXTool использовать для КСС+RDS.
0
Круто!
То есть от компа идет обычное стерео на обычной частоте, а в устройстве преобразуется в ксс и выдается на I2S в формате 384 моно? нет, на DSS… В формате SPI?

Это коммерческая разработка?
0
Нет, GPL.
0
PHY встроенное, фулспид. На ПЛИСине (Cyclone V) я этот проект буду делать скоро, там еще и компорт надо в составное устройство встроить. Брал один из примеров с opencores, энумерация проходила нормально.
0
На ПЛИСине (Cyclone V) я этот проект буду делать скоро, там еще и компорт надо в составное устройство встроить. Брал один из примеров с opencores, энумерация проходила нормально.
В этом циклоне usb phy hs встроенный?
Ниос использовался?
В первый циклон влезет?
Можно, но тропа та не хожена почти, да еще и UAC2
Мне кажется, в библиотеке usb режим ULPI для внешнего PHY есть уже.
0
Проверять надо… Без UAC2 смысла нету даже заморачиваться :-) я сильно глубоко в тему UAC2 не вникал, но, похоже, не-ASIO драйвера с открытыми исходниками так и не существует? Чтобы в системе появлялось звуковое устройство обычное с набором соответствующих частот дискретизации. На вегалабе это можно повыяснять у nikkov (он автор драйвера, кстати, тоже омич).
0
Хорошо, я на этом пока остановлюсь. Спасибо.
0
В двух словах: частоту дискретизации выбирает хост, посылая запрос на управляющую конечную точку 0. Их нужно обрабатывать и отвечать.
0
Загрузил usb_audio.bin в STM32F4-Discovery, светодиоды мигают, но звука не слышно почему-то. Так и должно быть?
0
У меня в схеме внешний генератор с частотой 12,288 МГц подключен на PC7, PC9.
0
PC7 это же MCLK кодека CS43L22. Именно генератор подключен или осциллятор?
0
Я сейчас переделываю на встроенный PLLI2S, выложу в статье. Тогда из всех соединений останется только перемычка с PC7 на PA15. Внешний использовался потому, что для адаптивной синхронизации лучше иметь отдельный генератор, который подстраивать под SOF'ы. Иначе придется двигать кварц самого контроллера по частоте, а это не есть хорошо (хотя двигать нужно, в пределах погрешности кварца: с 8,000 до 8,001488 МГц, это 186 ppm и на работе USB не скажется.
0
А есть возможность взглянуть на схему?
0
Совсем правильной схемы никто не рисовал. Есть набросок, vrtp.ru/index.php?showtopic=23730&st=210
Но, боюсь, он только еще больше запутает
0
как раз закончил отладку usb-i2s транспорта на стм32. очень интересно почитать про решение тех же проблем постфактум.
0
  • avatar
  • xar
  • 16 ноября 2015, 09:42
у вас подход чем-то отличается? было бы интересно взглянуть на эту часть исходника, чтобы заработало из-под Linux
0
пока не читал. понедельник, работа… позже отпишусь.
0
да собственно ничего нового видимо не скажу. в процессе брождения по куче граблей ничего толкового в голове и не отложилось. тот же asynchronous isochronous, через плл. в планах переход на внешние генераторы, но пока не нашел 3.3в генераторов за адекватные деньги.
0
собсвтенно если кто находил 3.3в генераторы на 44.1кГц и 48кГц сетки — буду рад ссылкам )
0
Я здесь брал
У этого продавца ассортимент широкий.
0
ну хоть что то. а то на али вроде и есть, а в описании больше рекламы лавки, ни слова о параметрах.
0
Сегодня поправил код и запустил его на частоте 44100. Как и предполагалось, точности PLLI2S вполне хватает, остальное «дотягивает» обратная связь
0
Обновил код, почистил косяки, добавил схему, превращающую вашу дискавери в хайэнд передатчик :-)
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.