Асинхронное 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
Добавить комментарий