AT91SAM3X: порт SSC в режиме приемника-мастера I2S Печать
Добавил(а) Денис Оксин   

Поставил перед собой задачу - передавать по сети Ethernet звук от микрофона. Устройство должно быть простым (не компьютер), и вопрос хотелось решить с минимальными усилиями, опираясь на готовые популярные встраиваемые платформы.

SSC I2C microphone

[Выбор платы и процессора]

Выбор пал на платформу Arduino по следующим причинам: макетные платы весьма доступны по цене, среда разработки бесплатна, специализированные аппаратные средства разработки не требуются. Поначалу задача казалась несложной, поскольку раньше работал с 8-битными микроконтроллерами PIC, и Arduino должна быть, как обещали разработчики, максимально дружественна для новичков.

Просмотрев доступные у китайцев Arduino-подобные платы, заказал Arduino Due R3 на процессоре SAM3X8E ARM Cortex-M3 [1].

Arduino Due R3 top

У выбранного процессора должна быть достаточная для решения задачи производительность, есть встроенный быстрый АЦП, интерфейс SSC с поддержкой I2S, контроллер DMA. К этой плате можно подключать стандартные шилды Arduino. Для Ethernet дополнительно заказал Keyestudio W5100 Ethernet Shield [2].

Ks0156 W5100 Ethernet Shield

Опыта работы в Arduino IDE было очень мало, поэтому решил начать с простого - подергать ножками GPIO, научиться опрашивать АЦП.

[Подключение аналогового микрофона]

Первые эксперименты с Arduino IDE прошли на удивление удачно. Удалось задействовать несколько портов в качестве цифровых входов-выходов и один порт в качестве аналогового входа. Ethernet-шилд заработал сразу, библиотека от производителя устанавливается штатным образом, все функции работают – получение адреса по DHCP, настройка фиксированного IP адреса, отправка и получение UDP пакетов, TCP соединения. Вызовы сетевых функций и обработчики прерываний не мешают друг-другу.

Дело дошло до оцифровки звука с микрофона. Решил начать с простого – для ввода звука воспользоваться встроенным АЦП, и потренироваться с передачей данных по Ethernet. Для формирования выборок понадобился аппаратный таймер для опроса АЦП с заданной частотой. Однако оказалось, что воспользоваться в Arduino IDE таймером задача не из тривиальных. Внятной документации по функциям таймеров нет. Пришлось изучать исходники библиотек, искать примеры в Интернете. С таймерами помог ресурс [4]. Со сторожевым таймером возникли проблемы. Выяснилось, что разработчики Arduino IDE убрали поддержку WatchDog. К счастью, решение нашлось на форуме Arduino [5].

Стало понятно, почему многие разработчики ругают платформу Arduino - стандартные задачи решаются просто, однако если нужно что-то большее, то сразу наваливается куча проблем - то нет библиотеки, то библиотеки начинают конфликтовать друг с другом. Тем, кто не боится читать даташиты, в Atmel Studio было бы намного удобнее - легче доступ к аппаратуре и планирование ресурсов, сам определяешь, как что должно работать. Никто не будет выпиливать необходимые вещи, не нужно будет вручную определять системные константы по документации на процессор, документация будет нормальной, будут нормальные примеры проектов.

Удалось реализовать опрос АЦП с двойной буферизацией и передачу UDP-пакетов тестовому приложению на PC.

maket sch microphone ADC

maket microphone ADC

[Сервер, принимающий пакеты UDP]

Тест-программа PC написана в среде Visual Studio 2010 на языке C#. Это сетевой сервер, который прослушивает UDP порт 8266. Микроконтроллер ARM подключается к серверу по его IP и этому номеру порта. Для упрощения обмен данными идет только в одном направлении - звуковые данные передаются от микроконтроллера ARM серверу PC. Скриншот приложения, которое принимает и озвучивает пакеты UDP:

SSC I2S UDP test GUI

Соотношение сигнал/шум аналогового микрофона не удовлетворило моим требованиям. Следующий логичный шаг - освоить SSC (Synchronous Serial Controller) процессора для подключения микрофона с интерфейсом I2S (SPH0645).

[Подключение цифрового микрофона с интерфейсом I2S]

Платка микрофона SPH0645:

I2S MEMS Microphone SPH0645

Интерфейс SSC и Arduino Due R3:

Мнемоника Описание сигнала Микрофон SPH0645LM4H Порт SAM3X8E Периф.
SAM3X8E
Порт Arduino
RF Синхроимпульс кадра приемника (вход микрофона) LRCL (WS) PB17 A A8
RD Данные приемника (выход микрофона) DOUT (DATA) PB18 A9
RK Такты приемника (вход микрофона) BCLK (CLK) PB19 A10
TK Такты передатчика   PA14 B 23
TF Синхроимпульс кадра передатчика   PA15 24
TD Данные передатчика   PA16 A0

Подключение микрофона SPH0645 к SSC:

maket sch microphone SSC I2S

При написании кода SSC/I2S снова возникли проблемы с библиотеками Arduino IDE. Поддержка I2S есть только для серии MKR с процессорами семейства SAM D21 Cortex-M0. Про SSC вообще ни слова. В Интернете каких-то примеров реальной работы с SSC практически нет.

По стандартной документации для запуска SSC необходимо следующее:

1. Настроить используемые выводы с помощью контроллера PIO, поскольку по умолчанию они настроены на другой функционал.
2. Включить тактирование SSC программированием контроллера PMC.
3. Написать обработчик прерывания SSC.
4. Настроить и разрешить прерывания от SSC.

Набор параметров у SSC весьма приличный, что сильно усложняет его правильную настройку. При выборе параметров SSC я руководствовался документацией на процессор [1], документацией на микрофон SPH0645 [3], описанием шины I2S [6], проектом-примером из Atmel Studio SSC_UNIT_TEST для процессора SAM3X. Из приборов был только мультиметр с функцией частотомера (до 30 кГц).

Разобраться с правильной настройкой периферии PIO получилось не сразу. В режиме зацикливания на себя SSC принимает и отправляет, но при переключении в нормальный рабочий режим мультиметр показывает, что часть выходов просто висят в воздухе без подтяжки к нулю или питанию. Оказалось, что это тоже особенности Arduino IDE. Библиотеки и константы есть, но не так просто найти, где они описаны. Покопавшись в исходниках платформы, нашел как правильно использовать функции управления периферией.

В результате довольно долгого процесса подбора параметров SSC удалось получить нужные частоты на выводах RK и RF, но данных с микрофона не наблюдалось. По всей видимости, какие-то из параметров сигналов не нравились микрофону и данные он отдавать не хотел. По показаниям мультиметра на шине DOUT микрофона наблюдались стабильные ноль вольт.

После более внимательного поиска в Интернете наткнулся на статью [7], где были раскрыты некоторые любопытные моменты, которые явным образом в документации Atmel не описаны. В результате удалось более осмысленно уточнить параметры, и всё заработало. Выражаю благодарность автору за столь подробное описание особенностей работы SSC.

#define PIN_RD        PIO_PB18
#define PIN_RF        PIO_PB17
#define PIN_RK        PIO_PB19
#define PIN_TD        PIO_PA16
#define PIN_TF        PIO_PA15
#define PIN_TK        PIO_PA14
 
#define DATA_BIT_LEN    32
#define FRAME_BIT_LEN   64
#define SAMPLE_RATE     44000  //количество выборок в секунду
#define SSC_BIT_RATE    (FRAME_BIT_LEN * SAMPLE_RATE)
 
// Приоритет прерывания SSC:
#define SSC_IRQ_PRIO    4
 
uint32_t SSC_RX_PIN_MASK = (PIN_RD | PIN_RF | PIN_RK);
uint32_t SSC_TX_PIN_MASK = (PIN_TD | PIN_TF | PIN_TK);
 
////////////////////////////////////////////////////////////////////////////////
// Настройка выводов SSC процессора
void ssc_configure_pio()
{
   uint32_t absr;
   // Отключение прерывания для выводов SSC:
   PIOB->PIO_IDR = SSC_RX_PIN_MASK;
   PIOA->PIO_IDR = SSC_TX_PIN_MASK;
   
   // Наcтройка входов и выходов. На выводах PIN_RD, PIN_TF, PIN_TK
   // разрешение верхних подтягивающих резисторов.
   PIOB->PIO_ODR = PIN_RD;
   PIOB->PIO_PUER = PIN_RD;
   PIOA->PIO_ODR = (PIN_TF | PIN_TK);
   PIOA->PIO_PUER = (PIN_TF | PIN_TK);
   PIOB->PIO_OER = (PIN_RF | PIN_RK);
   PIOA->PIO_OER = PIN_TD;
  
   // Переключение выводов на периферийное устройство SSC:
   PIOB->PIO_PDR = SSC_RX_PIN_MASK;
   PIOA->PIO_PDR = SSC_TX_PIN_MASK;
  
   // Подключение к периферии B:
   absr = PIOB->PIO_ABSR;
   PIOB->PIO_ABSR = (absr & ~SSC_RX_PIN_MASK);
  
   // Подключение к периферии A:
   absr = PIOA->PIO_ABSR;
   PIOA->PIO_ABSR = (absr | SSC_TX_PIN_MASK);
}
 
////////////////////////////////////////////////////////////////////////////////
// Настройка параметров и разрешение работы SSC
static void ssc_setup()
{
   uint32_t ul_mck;
   clock_opt_t rx_clk_option;
   data_frame_opt_t rx_data_frame_option;
   // Инициализация локальных переменных:
   ul_mck = 0;
   memset((uint8_t *)&rx_clk_option, 0, sizeof(clock_opt_t));
   memset((uint8_t *)&rx_data_frame_option, 0, sizeof(data_frame_opt_t));
   // Включение тактирования SSC:
   pmc_set_writeprotect(false);
   pmc_enable_periph_clk(ID_SSC);
  
   // Разрешение прерываний SSC в контроллере прерываний NVIC:
   NVIC_DisableIRQ(SSC_IRQn);
   NVIC_ClearPendingIRQ(SSC_IRQn);
   NVIC_SetPriority(SSC_IRQn, SSC_IRQ_PRIO);
   NVIC_EnableIRQ(SSC_IRQn);
   // Настройка выводов контроллера SSC:
   ssc_configure_pio();
   // Сброс SSC:
   ssc_reset(SSC);
   // Настройка параметров обмена данными:
   ul_mck = SystemCoreClock;
   ssc_set_clock_divider(SSC, SSC_BIT_RATE, ul_mck);
   Serial.print("RK Clock = ");
   Serial.println(ul_mck / 2 / SSC->SSC_CMR, DEC);
   Serial.print("Sample rate = ");
   Serial.println(ul_mck / 2 / SSC->SSC_CMR / 64, DEC);
   // Режим работы тактового генератора приемника непрерывный,
   // от встроенного делителя, вывод RK работает как выход:
   rx_clk_option.ul_cks = SSC_RCMR_CKS_MCK;
   rx_clk_option.ul_cko = SSC_RCMR_CKO_CONTINUOUS;
   rx_clk_option.ul_cki = 0; // Falling edge
   rx_clk_option.ul_ckg = SSC_RCMR_CKG_CONTINUOUS;
  
   // Выбор способа запуска приема по заднему фронту сигнала RF:
   rx_clk_option.ul_start_sel = SSC_RCMR_START_RF_FALLING;
   // Старт без задержки:
   rx_clk_option.ul_sttdly = 0;
   // Выбор делителя для периода опроса и выдачи сигнала "Новый кадр":
   rx_clk_option.ul_period = (FRAME_BIT_LEN / 2) - 1;
  
   // Настройка кадра приемника. Будем принимать одно слово длиной 32 бита.
   // Фактически в нем только 24 бита данных от микрофона, из которых
   // только 18 старших могут иметь ненулевое значение.
   rx_data_frame_option.ul_datlen = DATA_BIT_LEN - 1;
   rx_data_frame_option.ul_datnb = 0;
   rx_data_frame_option.ul_msbf = SSC_RFMR_MSBF;   // старший бит первый
   // Количество бит, сохраняемых в приемном регистре:
   rx_data_frame_option.ul_fslen = (FRAME_BIT_LEN / 2) - 1 - 16;
   // Длина импульса равна FSLEN + (FSLEN_EXT * 16) + 1 периодов тактов приема:
   rx_data_frame_option.ul_fslen_ext = 1;
   // Настройка вывода RF. 1: SSC_RFMR_FSOS_NEGATIVE, отрицательный импульс,
   // RF вывод работает как выход:
   rx_data_frame_option.ul_fsos = SSC_RFMR_FSOS_NEGATIVE; 
   // Прерывание по отрицательному фронту RF:
   rx_data_frame_option.ul_fsedge = SSC_RFMR_FSEDGE_POSITIVE;
  
   // Запись подготовленной структуры в настройки SSC:
   ssc_set_receiver(SSC, &rx_clk_option, &rx_data_frame_option);
   // Если раскомментировать, то эта строчка включит тестовый
   // режим (зацикливание "на себя"):
   //ssc_set_loop_mode(SSC);
   // Включение приема:
   ssc_enable_rx(SSC);
   // Разрешение прерываний приема:
   ssc_enable_interrupt(SSC, SSC_IER_RXRDY);
}
 
////////////////////////////////////////////////////////////////////////////////
// Обработчик прерывания SSC
void SSC_Handler(void)
{
   uint32_t ul_ssc_sr;
   uint32_t ul_data;
   // Проверка готовности данных:
   ul_ssc_sr = ssc_get_status(SSC);
   if(ul_ssc_sr & SSC_SR_RXRDY)
   {
      // Если данные готовы, то они обрабатываются. Чтобы избежать вложенных
      // вызовов, до окончания обработки отключаются прерывания.
      // Теоретически это может вызвать потери отдельных фреймов, 
      // для звука не страшно.
      ssc_disable_interrupt(SSC, SSC_IDR_RXRDY);
      // Выборка данных:
      ssc_read(SSC, &ul_data);
      // Обнуление младших 14 бит:
      analogValue = ul_data & 0xFFFFC000;
      // Сдвиг делением на 14 бит, чтобы сохранить знак:
      analogValue = analogValue / 0x4000;
      // Компенсация смещением (подобрано опытным путем):
      analogValue += 7295; // correct DC offset
      // Далее данные складываются в активный буфер для последующей
      // отправки по сети:
      // ...
      // Разрешение прерываний:
      ssc_enable_interrupt(SSC, SSC_IER_RXRDY);
   }
}

Как выглядит получившийся макет:

maket microphone I2S

Качество звука цифрового микрофона оказалось намного лучше. Особенно порадовало соотношение сигнал/шум в сравнении с аналоговыми микрофонами, с которыми удалось поэкспериментировать – электретные, MEMS, с обычным операционным усилителем и специализированными микросхемами.

В планах освоить прямой доступ к памяти (DMA) для ускорения обработки. Текущая реализация не тянет частоты выборок выше 44000 Гц.

[Общие выводы]

Если рассматривать Arduino только для макетирования, то это просто замечательная вещь, причем приятно доступная по цене. Выбор разнообразных шилдов также не может не радовать. Однако после ознакомления с Arduino IDE и платы Due R3 выяснилось, что библиотеки для SAM3X разработчиками Arduino IDE попросту отсутствуют – поддержка реализована на уровне совместимости с платой Due на базе Mega328P, никаких SSC, I2S, DMAC. Банальные таймеры, WatchDog не реализованы (или разобраться непросто). Работать с портами GPIO процессора даже как цифровыми входами-выходами просто так не получится – нужно сначала настроить порты ввода-вывода с помощью PIO контроллера.

Не знаю, в чем именно проблема – или Arduino в принципе предназначена только для обучения и мигания светодиодами, или это была неудачная плата, которая не была удостоена должным вниманием разработчиков библиотек. На второй вариант намекает тот факт, что новые актуальные платы выпускаются с процессорами SAM D21. Расстраивает отсутствие документации на встроенные библиотечные функции, нет нормальных примеров проектов с использованием таймеров, счетчиков, прерываний, DMA. 

Семейство SAM3X в любом случае заинтересовало. Планирую приобрести нормальный программатор, чтобы можно было работать с Atmel Studio. Atmel Studio это и привычный для меня интерфейс Visual Studio, и доступ к огромной библиотеке примеров, и поддержка отладчиков. После 8-битных PIC и uC эта среда выглядит просто замечательно. Отладчик тоже хочется - буду искать, выбирать. Для таких задач процессор SAM3X, конечно, избыточен. Но для макетирования на столе это очень удобная система с богатым набором периферии. К сожалению, не нашел готовых плат Ethernet, использующих встроенный в этот процессор MAC-контроллер.  Из неприятностей – заказать у итальянцев нельзя (в Россию не отправляют), при этом в местных магазинах новых современных плат нет ни в наличии, ни в каталоге. Планирую посмотреть поближе на MKR WiFi 1010 v2 [8].

Исходный код всех проектов можно скачать по ссылке [9].

[Ссылки]

1. SAM3X8E ARM Cortex-M3 site:microchip.com.
2. Keyestudio W5100 Ethernet Shield site:keyestudio.com.
3. Adafruit I2S MEMS Microphone Breakout SPH0645LM4H site:adafruit.com.
4. Arduino Due Timers site:ko7m.blogspot.com
5. Arduino Forum > Products > Arduino Due > Watchdog timer site:forum.arduino.cc.
6. I2S Wikipedia site:wikipedia.org.
7. AT91SAM7X256: работа с портом SSC.
8. Arduino MKR WiFi 1010 site:arduino.cc.
9. 190331SSC-I2C-AT91SAM3X.zip - архив с исходным кодом проектов, документация.