Вывод звука на Arduino Due

В этой статье я расскажу о способе вывода звука на Arduino Due без активного использования процессора.

Получив плату и поэкспериментировав со скетчами, я понял, что не стану писать прошивки в стандартной IDE и начал искать альтернативу. Мой выбор остановился на Atmel Studio версии 6.0. Мне очень понравилась эта IDE, которая сделана на основе Visual Studio 2010. Понравилась она особенно тем, что все заработало из коробки. В мастере создания нового проекта я выбрал плату Arduino Due, выбрал проект а-ля «Hello World» (мигание светодиодом), скомпилировал и запустил. Особенно радовало то, что не было никаких скрытых от меня слоев и библиотек. Прошивка полностью собиралась из исходного кода, это меня в итоге и подкупило, и я остался на Atmel Studio. В нее, кстати, уже встроен Visual Assist, который делает написание кода еще более комфортным.

И так, передо мной встала задача вывода звука через DACC (Analog Converter Controller), но без 100% загрузки процессора. В идеале хотелось отправить в DACC очередную порцию данных и забыть о нем до тех пор, пока не потребуется отправить новую порцию.
Для этого мне пришлось задействовать PDC (Peripheral DMA Controller) и TC (Timer Counter). В итоге, все оказалось достаточно просто, но я немного помучился, прежде чем все это заработало. Если интересно, то прошу под кат.

Сам по себе вывод звука не представляет какой-либо сложности. Мы просто записываем очередное 12-ти битное значение в канал DACC, делая паузы между этими операциями. Пример по выводу звука доступен в Atmel Studio (DAC Sinewave Example). В этом примере инициализируется системный таймер (System Timer), в обработчике прерывания которого записывается значение амплитуды в канал DACC. Тоже, на первый взгляд, не сильно должно нагружать процессор, но для частоты 32 кГц нам уже понадобится 32000 прерываний в секунду, не говоря уже о том, что для других целей системный таймер не получится использовать, т.к. его придется каждый раз перенастраивать на нужную частоту.

Для того чтобы отправлять данные в DACC порциями можно использовать PDC, но он отправляет данные без паузы, т.е. ожидает когда очередное полуслово будет принято железом и тут же отправляет следующее. Для вывода звука нам этого недостаточно, т.к. данные должны записываться с конкретной частотой. Здесь нам на помощь приходит один из режимов работы DACC, а именно он умеет использовать TC в качестве триггера. А значит, нам остается настроить на нужную частоту TC и сказать DACC, чтобы он использовал один из каналов TC.

TC настраивается на режим “waveform”. Для этого в регистр Ra записывается значение каунтера, при котором на выходе появляется сигнал, а в регистр Rc значение (в нашем случае Ra + 1), при котором выходной сигнал сбрасывается. Таким образом, мы формируем сигнал заданной частоты, который передаем на вход DACC.

При использовании PDC нужно учитывать, что он не умеет передавать данные из ROM, только из RAM.
Модуль работы со звуком находится в файле sound.c
// Кольцевой буфер
static uint16_t g_buffer[4096];
// Указатели на голову, хвост и текущий размер данных в буфере
static volatile uint16_t g_buffer_head, g_buffer_tail, g_buffer_size;

// Возвращает размер непрерывного доступного куска данных в буфере
static inline uint16_t get_continuous_data_size_irq(void)
{
    return g_buffer_head <= g_buffer_tail ? g_buffer_tail - g_buffer_head : ARRAYSIZE(g_buffer) - g_buffer_head;
}

// Возвращает количество свободного места в буфере
static inline uint16_t get_buffer_capacity(void)
{
    return g_buffer_size;
}

// Копирует данные в буфер
static uint16_t copy_in_buffer(const uint16_t * data, uint16_t size)
{
    uint16_t tail = g_buffer_tail;
    
    // В этом месте не выполняем никаких проверок
    uint32_t to_copy = min(size, ARRAYSIZE(g_buffer) - tail);
    memcpy((void *)(g_buffer + tail), (void *)data, (size_t)(to_copy << 1));
    
    if (to_copy < size)
    {
        // Часть данных будут скопированы в начало буфера
        memcpy(g_buffer, data + to_copy, (size - to_copy) << 1);
        tail = size - to_copy;
    }       
    else
    {
        // Все данные удалось разместить в конце буфера
        tail += size;
    }
    
    return tail;
}

// Перемещает указатель на начало данных, увеличивая емкость буфера
static inline void move_head_irq(uint16_t size)
{
    g_buffer_head += size;
    g_buffer_size += size;
    if (g_buffer_head >= ARRAYSIZE(g_buffer) && g_buffer_head > g_buffer_tail)
        g_buffer_head -= ARRAYSIZE(g_buffer);
}

// Данные необходимые для работы с PDC

// Описание пакета данных
static pdc_packet_t g_pdc_packet;
// Размер данных в буфере PDC
static volatile uint32_t g_pdc_packet_size;
// Если не 0, то PDC все еще активен
static volatile uint8_t g_pdc_active;

// Запрещает прерывания от DACC
static inline void disable_irq(void)
{
    dacc_disable_interrupt(DACC, DACC_IER_ENDTX);
}

// Если PDC активен, то разрешает прерывания от DACC
static inline void restore_irq(void)
{
    if (g_pdc_active)
        dacc_enable_interrupt(DACC, DACC_IER_ENDTX);
}

#define DACC_PDC_PACKET_MAX_SIZE 512

// Обработчик прерываний DACC
// Вызывается, когда очередной пакет был отправлен PDC
void DACC_Handler(void)
{
    // Причина прерывания
    uint32_t status = dacc_get_interrupt_status(DACC);

    if (status & DACC_IER_ENDTX)
    {
        // Очередной пакет отправлен, модифицируем буфер
        move_head_irq(g_pdc_packet_size);
        uint16_t data_size = get_continuous_data_size_irq();

        if (data_size == 0)
        {
            // Кончились данные
            g_pdc_packet_size = 0;
            g_pdc_active = 0;
            // Запрещаем прерывания от DACC
            dacc_disable_interrupt(DACC, DACC_IDR_ENDTX);
        }
        else
        {
            // Настраиваем PDC на отправку очередного пакета
            g_pdc_packet.ul_addr = (uint32_t)(g_buffer + g_buffer_head);
            g_pdc_packet_size = min(DACC_PDC_PACKET_MAX_SIZE, data_size);
            g_pdc_packet.ul_size = g_pdc_packet_size;
            
            // Отправляем
            pdc_tx_init(PDC_DACC, &g_pdc_packet, NULL);
        }
    }
}

// Настраивает канал 0 TC на генерацию с указанной частотой
static void tc_adjust_frequency(uint16_t frequency)
{
    uint32_t divider = sysclk_get_cpu_hz() / frequency;
    divider >>= 1;

    tc_write_ra(TC0, 0, divider);
    tc_write_rc(TC0, 0, divider + 1);
}

// Инициализируем железо
void sound_init_hardware(void)
{
    // Включаем модуль TC
    sysclk_enable_peripheral_clock(ID_TC0);

    // Переводим TC0 в режим waveform
    tc_init(TC0, 0,
            TC_CMR_TCCLKS_TIMER_CLOCK1
            | TC_CMR_WAVE /* Waveform mode is enabled */
            | TC_CMR_ACPA_SET /* RA Compare Effect: set */
            | TC_CMR_ACPC_CLEAR /* RC Compare Effect: clear */
            | TC_CMR_CPCTRG /* UP mode with automatic trigger on RC Compare */
    );

    // Устанавливаем какую-то частоту для TC, не важно какую
    tc_adjust_frequency(8000);

    // Запускаем таймер
    tc_start(TC0, 0);

    // Включаем модуль DACC
    sysclk_enable_peripheral_clock(ID_DACC);

    // Сбрасываем все регистры DACC
    dacc_reset(DACC);

    // Переводим DACC в режим "half word"
    dacc_set_transfer_mode(DACC, 0);

    // Запрещаем экономию энергии
    dacc_set_power_save(DACC, 0, 0);

    // Выбираем канал TC0 в качестве триггера
    dacc_set_trigger(DACC, 1);

    // Запрещаем TAG и выбираем первый канал DACC
    dacc_set_channel_selection(DACC, 1);

    // Включаем первый канал DACC
    dacc_enable_channel(DACC, 1);

    // Настройка параметров работы канала
    dacc_set_analog_control(DACC, DACC_ACR_IBCTLCH0(0x02) | DACC_ACR_IBCTLCH1(0x02) | DACC_ACR_IBCTLDACCORE(0x01));
    
    // Очищаем все прерывания DACC ожидающие обработки
    NVIC_DisableIRQ(DACC_IRQn);
    NVIC_ClearPendingIRQ(DACC_IRQn);
    NVIC_EnableIRQ(DACC_IRQn);

    // Разрешаем использование PDC для пересылки данных в DACC
    pdc_enable_transfer(PDC_DACC, PERIPH_PTCR_TXTEN);
    
    // Инициализация глобальных переменных
    g_pdc_packet_size = 0;
    g_pdc_active = 0;
}

// Настраивает оборудование для вывода звука указанной частоты
void sound_start(uint16_t frequency)
{
    // На всякий случай стоп
    sound_stop();
    // Перенастроим таймер
    tc_adjust_frequency(frequency);
}   

// Останавливает вывод звука
void sound_stop(void)
{
    // Прерывания от DACC запрещены
    dacc_disable_interrupt(DACC, DACC_IER_ENDTX);

    g_pdc_packet_size = 0;
    g_buffer_head = g_buffer_tail = 0;
    g_buffer_size = ARRAYSIZE(g_buffer);
    g_pdc_active = 0;
}

// Отправляет очередную порцию данных в очередь на вывод звука
uint8_t sound_data(const uint16_t * data, uint16_t size)
{
    // Проверяем емкость буфера
    uint16_t capacity = get_buffer_capacity();
    if (capacity < size)
        return 0;
        
    // Копируем данные в буфер
    uint16_t tail = copy_in_buffer(data, size);
    // Модифицируем указатели буфера
    disable_irq();
    g_buffer_tail = tail;
    g_buffer_size -= size;
    if (g_buffer_head >= ARRAYSIZE(g_buffer))
        g_buffer_head -= ARRAYSIZE(g_buffer);
    g_pdc_active = 1;
    restore_irq();
    
    // Все прошло нормально
    return 1;
}


Я написал небольшой пример использования.
#include <asf.h>

#include "sound.h"

#define STRING_HEADER "-- Sound firmware --\r\n" \
"-- "BOARD_NAME" --\r\n" \
"-- Compiled: "__DATE__" "__TIME__" --\r"

static void configure_console(void)
{
    const usart_serial_options_t uart_serial_options = {
        .baudrate = 115200,
        .paritytype = UART_MR_PAR_NO
    };

    sysclk_enable_peripheral_clock(ID_UART);
    stdio_serial_init(UART, &uart_serial_options);
}


#include "sound_data.c"
static uint32_t g_sound_pos = 0;

int main (void)
{
    sysclk_init();
    board_init();
    configure_console();

    puts(STRING_HEADER);
    printf("CPU Frequency %li MHz\r\n", sysclk_get_cpu_hz() / 1000000);
    
    sound_init_hardware();
    sound_start(16000);

    puts("Work...\r");
    
    while (1)
    {
        // Отправляем кусками по 512 слов или то, что осталось
        uint32_t size = min(512, ARRAYSIZE(g_sound_data) - g_sound_pos);
        if (sound_data(g_sound_data + g_sound_pos, size))
        {
            // Данные успешно записаны в буфер
            g_sound_pos += size;
            if (g_sound_pos >= ARRAYSIZE(g_sound_data))
                g_sound_pos = 0;
        }
        else
        {
            // Нет места в буфере, пробуем еще раз  
        }           
    }
}


Здесь в цикле мы отправляем данные из массива в очередь вывода, в случае удачи двигаемся к концу массива, достигнув конца, возвращаемся в начало. Работаем бесконечно.

Этот модуль я использую в другом проекте, там звук я вывожу при получении очередного пакета через USB, так что задача не нагружать процессор была выполнена.
Также можно отправлять звуковые данные с заранее рассчитанной паузой между пакетами, например, если размер пакета 512 полуслов, а частота звука 8 кГц, то пауза между отправкой будет 512 / 8000 = 64 мс.

Было бы неплохо избавиться от копирования данных, если мы точно уверены, что они поступают не из ROM. А еще PDC умеет передавать сразу два пакета, это тоже можно использовать.

Динамически изменяя частоту звука можно сгладить неравномерности в его наполнении. Например, при заполнении буфера на 2/3 его объема чуть-чуть увеличиваем частоту, чтобы он быстрее опустошался.

Прошивал плату при помощи стандартной утилиты из IDE Arduino, было лень искать альтернативные способы, и этот меня полностью устраивал. Отправить пример в контроллер можно командой:
bossac.exe --port=COM4 -U false -e -w -v -b sound.bin –R

bossac.exe – утилита из Arduino IDE
Имя COM-порта указывайте свое.
Файл прошивки найдете в каталоге Debug или Release вашего проекта.

Проект целиком можно найти здесь github.com/artalex/arduino_due_sound

В Atmel Studio есть огромное количество примеров для Arduino Due, очень советую их изучить.

Распиновка платы описана здесь arduino.cc/en/Hacking/PinMappingSAM3X.
Вот ссылка на описание контроллера SAM3X: www.atmel.com/Images/doc11057.pdf.

Массив для примера был получен скриптом wave2hex.py. Он лежит в каталоге scripts в проекте.
Использовать так: wave2hex.py –I some.wav, где some.wav – имя файла, который нужно сконвертировать.
  • +19
  • 19.6k
  • 7
Share post

Similar posts

Comments 7

    0
    <шутка>Я думал Вы правда будете делать это на Ardino, а Вы на ATMega</шутка>
      +1
      Немного оффтопа, а никто не в курсе где почитать про миксер двух звуков одной частоты дискретизации? банальное (s1/2+s2/2) не очень хорошо работает.
        +1
        Вот простой способ, но не факт, что будет сильно лучше.
          0
          хм… в свое время извращался используя формулу сложения скоростей из СТО (v1+v2)/(1+(v1*v2)/c^2), где v1 и v2 это значения семплов, а С — это максимальное значение семпла. На слух звук нормальный.
            0
            Нашел свой старый код
            void audio_mixer_callback(void *userdata, Uint8 *stream, int len)
            {
            	memset(stream, 0, len);
            	short * output = (short*) stream;
            	short * buf = (short*) malloc(len); //FixMe: неоптимально
            
            	for (list<audioStream*>::const_iterator it = mixer_streams.begin(), end =
            			mixer_streams.end(); it != end; ++it)
            	{
            		mix_callback cb = (*it)->callback;
            		cb((*it)->userdata, (char*) buf, len);
            
            		for (int i = 0; i < (len / AUDIO_SAMPLE_SIZE); ++i)
            		{
            			//  re: нужна нормальная функция микширования
            			//  Теория относительности даёт ответ на этот вопрос.
            			//  Она расширяет понятие принципа относительности, распространяя его и на оптические процессы.
            			//  Правило сложение скоростей при этом не отменяется совсем,
            			//  а лишь уточняется для больших скоростей с помощью преобразования Лоренца:
            			//  vrel = (v1+v2)/(1+(v1*v2)/c^2) -- http://ru.wikipedia.org/wiki/Сложение_скоростей
            			//  меняем скорость света на максимальное значение семпла,
            			//  а складываемые скорости -  на текушие значения семплов.
            			//  ту-турууу теория относительности помогла написать функцию микширования
            
            			double max_sample_val = pow(2.0, (AUDIO_SAMPLE_SIZE * 8) - 1) - 1; // ибо знаковая
            			double max_val_square = pow(max_sample_val, 2);// нaша скорость света -> максимальное значение семпла ^
            
            			double a = output[i];
            			double b = (float) buf[i] * (float) ((*it)->volume) / (float) 100;
            
            			double rel_samp = (a + b) / (1 + (a * b) / max_val_square);
            
            			output[i] = rel_samp;
            		}
            	}
            	free(buf);
            }

            PS: код страшный :)
          +3
          Спасибо, очень интересно.

          От себя посоветую добавить в GitHub файл Readme.md который автоматически показывается в браузере при переходе по ссылке на репозиторий. Тогда даже непрограммисты смогут получить общее представление о сути проекта.

          Вот пример моего проекта на Arduino DUE. Код ещё не причёсан достаточно чтоб писать статью на Хабре, но даже по картинкам уже ясно о чём речь.
            +1
            Судя по картинкам, проект должен быть интересным. Пост будет?

          Only users with full accounts can post comments. Log in, please.