Программный декодер MP3 на STM32F10x. Часть 2. Запуск ЦАП

Модуль ЦАП является ведущим звеном в нашем проигрывателе, синхронизирующим работу остальных программных модулей. Для работы будет использована следующая периферия микроконтроллера: таймер, задающий частоту дискретизации; контроллер DMA; и собственно двухканальный ЦАП.

Данные блоки необходимо сконфигурировать таким образом, чтобы обеспечить минимальную нагрузку на ядро контроллера. Богатые возможности периферии STM32 предоставляют нам по крайней мере два варианта конфигурации:
  1. таймер используется для генерации запроса DMA и для запуска преобразования ЦАП;
  2. таймер используется для запуска преобразования DAC, который в свою очередь формирует запрос DMA.



Хотя второй вариант выглядит более логичным, реализуем первый (как показали дальнейшие исследования, оба варианта работают идентично). Номер таймера выбираем любой из доступных и подключенных ко входу DAC_trigger (TIM2, 4..7, 3/8).

Подробно рассматривать устройство периферии STM32 здесь не будем, при необходимости читаем популярные статьи на сайтах http://we.easyelectronics.ru/, http://mycontroller.ru/, http://easystm32.ru/, http://microtechnics.ru/ и т.д.

Инициализация модуля ЦАП
Все команды инициализации помещаем в одну функцию DAC_TaskInit, однократно вызываемую из модуля main.c.

Инициализация таймера:
#define DMA_TIMER		TIM4
#define DMA_TIMER_IRQn		TIM4_IRQn
#define DMA_TIMER_IRQ_vector	TIM4_IRQHandler
#define DMA_RCC_APB1Periph	RCC_APB1Periph_TIM4
#define DMA_TIMER_DBG_STOP	DBGMCU_CR_DBG_TIM4_STOP

// конфигурирование таймера частоты дискретизации
RCC_APB1PeriphClockCmd(DMA_RCC_APB1Periph, ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
TIM_TimeBaseStructure.TIM_Prescaler = 0x0000;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0x0000;
// в качестве альтернативы можно воспользоваться функцией инициализации по умолчанию:
//TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseInit(DMA_TIMER, &TIM_TimeBaseStructure);

// формировать TRGO по переполнению таймера
// TRGO будет использоваться для запуска преобразования ЦАП
TIM_SelectOutputTrigger(DMA_TIMER, TIM_TRGOSource_Update);

// формировать запрос DMA по переполнению таймера
TIM_DMACmd(DMA_TIMER, TIM_DMA_Update, ENABLE);

Если мы не хотим, чтобы таймер считал при остановленном ядре контроллера, необходимо установить соответствующий бит:
DBGMCU->CR |= DMA_TIMER_DBG_STOP;

DMA контроллера STM32 может генерировать прерывание по окончании всей передачи (Transfer complete) либо по передаче половины данных (Half-transfer). Задержки при формировании звука недопустимы, поэтому было бы логично зациклить передачу DMA (Circular mode) и обрабатывать оба прерывания и подгружать новыми данными освободившуюся половину буфера.
Однако возникает неудобство: нужно чётко знать, какая половина буфера свободна, соответственно нужно передавать эту информацию из модуля ЦАП декодеру. Нет, не спорю, алгоритм вполне реализуем. Но попробуем его всё же немного упростить. DMA будем запускать в однократном режиме, а настройку DMA выполнять в обработчике прерывания Transfer complete.

Инициализация 7 канала DMA, к которому подключен сигнал запроса DMA от таймера 4:
#define AUDIO_DMA_ch        DMA1_Channel7
#define AUDIO_DMA_TC_Flag   DMA1_FLAG_TC7

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(AUDIO_DMA_ch);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) & DAC->DHR12LD;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&silence_data;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = sizeof(silence_data)/4;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;	// DMA_Mode_Circular
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(AUDIO_DMA_ch, &DMA_InitStructure);

// сбросить флаг прерывания
DMA_ClearFlag(AUDIO_DMA_TC_Flag);
// разрешить прерывание по окончании передачи буфера
DMA_ITConfig(AUDIO_DMA_ch, DMA_IT_TC, ENABLE);
NVIC_EnableIRQ(AUDIO_DMA_IRQn);

И наконец инициализация ЦАП
#define DAC_Trigger_TRGO    DAC_Trigger_T4_TRGO

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
	
// настроить пины ЦАП как аналоговые входы
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_5;
GPIO_Init(GPIOA, &GPIO_InitStructure);

DAC_InitTypeDef DAC_InitStructure;
DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude = 0;
DAC_InitStructure.DAC_Trigger = DAC_Trigger_TRGO;
DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable;
DAC_Init(DAC_Channel_1, &DAC_InitStructure);
DAC_Init(DAC_Channel_2, &DAC_InitStructure);
DAC_Cmd(DAC_Channel_1, ENABLE);
DAC_Cmd(DAC_Channel_2, ENABLE);

Загрузку DMA будем выполнять по прерыванию. Перед загрузкой регистров DMA не забываем его выключать.
#define DAC_DMA_IRQHandler    DMA1_Channel7_IRQHandler

void DAC_DMA_IRQHandler(void)
{
    DMA1->IFCR = AUDIO_DMA_TC_Flag;    // сбросить флаг прерывания

    TEST_PIN1_HIGH;

    AUDIO_DMA_ch->CCR &= (uint16_t)(~DMA_CCR1_EN);    // выключить канал DMA
    AUDIO_DMA_ch->CNDTR = sizeof(test_data)/4;        // счётчик данных
    AUDIO_DMA_ch->CMAR = (uint32_t)&test_data;        // указатель на данные
    AUDIO_DMA_ch->CCR |= DMA_CCR1_EN;                 // включить канал DMA

    TEST_PIN1_LOW;
}

Первый пробный запуск
Выведем 5 ступенек:
const uint32_t test_data[] =
{
    0x0000 | 0x0000 << 16,
    0x4000 | 0x4000 << 16,
    0x8000 | 0x8000 << 16,
    0xC000 | 0xC000 << 16,
    0xFFF0 | 0xFFF0 << 16,
};

void dac_test_start()
{
    DMA_TIMER->ARR = 1500-1; // 72 МГц/48кГц = 1500
    DMA_Cmd(AUDIO_DMA_ch, ENABLE);
    TIM_Cmd(DMA_TIMER, ENABLE); 
}

Получаем такую картинку:


Верхняя трасса отображает выполнение обработчика прерывания (TEST_PIN1). Интервал срабатывания прерывания 104,1 мкс, делим на 5 и получаем частоту дискретизации около 48 кГц. Таймер настроен верно.

Обратим внимание на срез сигнала: он получился задержанным примерно на 2 мкс. Кроме того, на первой ступеньке присутствует колебательный переходной процесс. Такие дефекты сигнала обусловлены влиянием буферного усилителя ЦАП. При отключении усилителя сигнал становится «правильным», но амплитуда уменьшается в 2 раза (масштаб по вертикали 1 В/дел):


Хм, странный результат. Выходное напряжение оказалось притянутым к питанию 3В. Внимательно изучаем даташит на контроллер и смотрим схему отладочной платы. Ну да, всё правильно. Порт PA4 оказался подтянутым к 3 В резистором 10 кОм. А даташит рекомендует нагрузку при отключенном буферном усилителе — не менее 1,5 МОм!

Длительность среза от максимума до минимума составляет 2 мкс (буферный усилитель выключен, выход ЦАП нагружен только на высокоомный щуп осциллографа). Включение усилителя ускоряет процесс на 0,6 мкс.
А задержка среза (см. первую осциллограмму) обусловлена насыщением буферного усилителя, при уменьшении диапазона перестройки ЦАП с 0xFFF/0x000 до рекомендованного в даташите значения 0xF1C/0x0E0 насыщение пропадает.

Смотрим далее. Длительность выполнения обработчика порядка 600 нс. Прерывание возникает после преобразования ЦАП предпоследнего сэмпла. Откуда такая задержка? Почему не после последнего? Вероятно, имеется двойная буферизация на пути передачи данных от DMA до ЦАП. Проверим, сколько времени в запасе у нас есть на следующую инициализацию DMA. Для этого в обработчик прерывания перед загрузкой DMA вставим тупой пустой цикл:
for (uint32_t i = 0; i < delay; i++);

Опытным путём установлено, что максимальная задержка загрузки DMA может составлять 2 такта (40 мкс). Этого времени для наших целей более чем достаточно:


Работа ЦАП проверена, теперь можно заняться алгоритмом работы. Уточняем задачу:
  • модуль ЦАП должен быть максимально независимым от других программных модулей проигрывателя;
  • так как ЦАП является «головным» модулем, обеспечить его включение/выключение/постановку на паузу/продолжение работы;
  • интерфейс управления реализовать в виде задачи CoOS;
  • модуль ЦАП не должен иметь собственного буфера данных; данные должны приниматься через указатель на внешний (по отношению к модулю ЦАП) буфер.


Для связи с внешним миром объявляем:
  • функцию инициализации DAC_TaskInit();
  • майлбокс dac_CmdMailbox для входящих команд управления;
  • флаг dac_DataReqFlag, сигнализирующий о готовности модуля ЦАП к приёму следующей порции данных.


Алгоритм и диаграммы взаимодействия модуля ЦАП и декодера могут выглядеть например так::



Окончательная версия проекта, реализованного в соответствии с вышеизложенным алгоритмом, прилагается в конце статьи.
Итоговая осциллограмма:

Время выполнения обработчика прерывания DMA существенно увеличилось. Это время выполнения функций CoOS
CoEnterISR()
CoAcceptSingleFlag()
isr_SetFlag()
CoExitISR()
которое в сумме составляет около 10 мкс. Наиболее долгие функции — isr_SetFlag (5 мкс) и CoExitISR (3,2 мкс) при оптимизация кода -Os.
Долго, но за удобство пользования ОС нужно платить (процессорным временем).

Далее...
В следующей статье приступим к воспроизведению MP3-потока.


Оглавление
Программный декодер MP3 на STM32F10x. Демопроект (Введение)
Программный декодер MP3 на STM32F10x. Часть 2. Запуск ЦАП
Программный декодер MP3 на STM32F10x. Часть 3. Извлекаем звуки
Программный декодер MP3(+MOD) на STM32F10x. Часть 4. Трекерная музыка
Программный декодер MP3(+). Переход на платформу STM32F407

  • +15
  • 09 сентября 2013, 19:29
  • MikeSmith
  • 1
Файлы в топике: mp3_player_step2.zip

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

RSS свернуть / развернуть
Здорово!!!
0
  • avatar
  • Aneg
  • 13 сентября 2013, 17:29
Кстати, а что у тебя за осциллограф?
0
  • avatar
  • Vga
  • 13 сентября 2013, 17:34
Актаком АСК-3102. Чудо ещё то. По характеристикам вполне неплох, а пользоваться крайне неудобно.
0
А что за софтина? Родная или что-то стороннее?
0
  • avatar
  • Vga
  • 13 сентября 2013, 18:48
см. ссылку. В комплекте идёт.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.