Пролог
В этом тексте вы узнаете, что общего между I2S трансивером и оладьями. Да... Именно так. Зачем программисту микроконтроллеров конвейеры и цифровые фильтры.
В этом тексте изложено как источать звук при помощи I2S DMA.
В чём проблема?
В прошлом тексте было упомянуто, что работа I2S трансивера в режиме прерываний сопряжена с относительно высокой нагрузкой на процессорное ядро. Это вызвано тем, что часто происходят вызовы обработчиков прерываний. Если мы передаём звук с частотой 48kHz, то прерывания будут происходить с частотой 96kHz.
Надо что-то сделать с этой проблемой.
Постановка задачи
Написать firmware приложение, которое будет одновременно и непрерывно записывать (R) и непрерывно транслировать (T) данные по I2S2 в микроконтроллере Artery. При этом, всё должно работать в реальном времени. Реализовать cхему Receive R-> Transmit T.
В переводе на кухонный язык, надо сделать полностью цифровое эхо. Через RAM память.
В качестве задачи со звёздочкой попробовать, ещё тут же в реальном времени слегка видоизменить принятые данные перед отправкой назад в I2S. Пропустить через какой-н цифровой фильтр. То есть реализовать схему Receive(R)->Proc(P)->Transmit(T)
Решение
Итак, танцуем от печки... Классическим решением проблемы является трансляция и запись звука через прямой доступ к памяти, direct memory access (DMA). DMA-это такой сопроцессор, который умеет только перемещать данные внутри физической памяти микропроцессора. По сути DMA - это аппаратная реализация функции memcpy(). DMA хорош тем, что при окончании отправки он генерирует прерывание DONE. Также DMA умеет генерировать прерывание об успешном перемещении половины от запланированного объёма данных. Вот так.
У DMA есть 3 режима.
№ |
Src |
Dist |
Пояснение |
1 |
RAM |
PHY |
Трансляция в периферию |
2 |
PHY |
RAM |
Запись из периферии |
3 |
RAM |
RAM |
memcpy() |
4 |
PHY |
PHY |
такой режим отсутствует |
Очевидно, что мы не может одновременно и записывать и воспроизводить одни и те же PCM данные. Надо их записывать по кусочкам и воспроизводить тоже по кусочкам. Например по 1024 семпла. Один семпл 2 канала. В каждом канале 2 байт (int16_t).
Наивно-интуитивно напрашивается вот такой способ решить проблему цифрового эхо
Однако это очень плохая идея, так как тут нарушается непрерывность записи звука и нарушается непрерывность воспроизведения звука. Такой способ будет работать только в случае, когда размер записываемой звуковой дорожки составляет всего лишь один семпл! Однако так не решить проблему слишком частых прерываний.
Как же быть?
Чтобы решить эту инженерную проблему надо вспомнить старинную русскую народную логическую задачку.
Как пожарить 3 оладьи за 3 минуты, если каждая сторона жарится по 1 мин., а на сковородку помещается только 2 оладьи ?
А решение у задачи такое. T-top, B-button.
Это, своего рода, самый первый жизненный опыт применения конвейерной обработки на бытовом уровне. Аналогичный принцип надо применить в обработке цифрового звука I2S. Только тут вместо оладьей будут массивы слов по 514 байт. Вместо сковородки - I2S трансивер. Только и всего...
Вот и получается, что надо поделить временную память на две одинаковые части Low и Hi. Пока записывается L транслировать H, пока записывается H транслировать L.
Учитывая, что в DMA есть циркулярный режим, то такая обработка будет работать сама по себе бесконечно долго. Её надо лишь завести. Как завести? Если говорить на рабоче- крестьянском языке, то I2S-DMA надо завести "с толкача". Как это выглядит?
Придется в коде прошивки реализовать вот такой простенькие конечный автомат на 4 состояния. У автомата задача, не много не мало, а обеспечить правильный старт и последующую само синхронизацию между передачей и отправкой семплов так, чтобы запись отставала по фазе от воспроизведения на полпериода. Это необходимо для реализации полностью цифрового loopback. Вот граф этого конечного автомата.
Можно выделать массив семплов I2STempData[1024], обнулить его и отправить по DMA в I2S. Как только сработает прерывание по половине отправки включить запись по DMA в этот же массив I2STempData. Далее всё будет происходить на аппаратном уровне само по себе. И цифровое эхо заработает.
Получается, что прерывания происходят каждые 512 семплов. То есть с частотой 93.75 Hz. Каждые 10.6 ms прерывание по DMA. Это в 512 раз лучше чем без DMA. Причем нагрузку можно даже уменьшить ещё, если увеличить размер временного массива с семплами I2STempData.
Тут всё в полном порядке: запись непрерывная, воспроизведение тоже непрерывное. Один лишь минус это запаздывание выходного сигнала на половину длинны временного массива. Насколько велико это запаздывание? Допустим мы работаем на частоте 48kHz. Длительность одного семпла составляет 20.8333us. Сколько времени надо для воспроизведения 512 семплов? Ответ: всего 10.6ms. В принципе всё, что меньше 15ms человеку не заметно.
А как быть, если мы хотим перед отправкой ещё немного подшаманить данные? Тогда появится новая фаза Proc (P). Получится вот такой трёхтактный конвейер.
Однако трёх-тактным конвейером не получится дирижировать, так как обычно DMA подсистемы на ARM Cortex-MCU микроконтроллерах не генерируют прерывания по 1/2 и 2/3 отправки. Да.. Вот так... У нас в распоряжении есть только одно промежуточное прерывание по 1/2 отправки. К этому приходятся приспосабливаться.
Поэтому придется сделать прозябающий конвейер, который 25% времени ничего не делает. Вот так, четырехтактный конвейер.
Это паллиaтивное решение, чисто из-за совместимости с Artery MCU (у STM та же самая ситуация). Теперь мы уже оперируем четвертинками временного массива с семплами. Однако кто отдаст приказ начать обрабатывать четвертинки?
Для этого, как ни крути, но придется запускать аппаратный таймер №8, который генерирует прерывания каждые 21.3ms. Это то самое время, которое уходит на трансляцию временного массива из 1024 семплов на частоте дискретизации 48kHz. Получается, что таймер будет переполняться с частотой 46.875 Hz. А прерывания будут происходить с частотой 93.75 Hz.
Также надо настроить на аппаратном таймере прерывание по сравнению с компаратором на половине периода. Впрямь как в DMA.
На нижнем графике All_Interrupts видно, что прерывания теперь происходят каждые 256 семплов, через каждые 5.3(3)ms. То есть с частотой 187.5 Hz. Это в 256 раз меньшая нагрузка на процессор, в сравнении с тем, когда мы источали I2S трафик по прерываниям вообще без использования DMA. Успех!
Однако кто даст отмашку аппаратному таймеру начать считать? Тут чисто по комбинаторике, как ни крути, существует всего-навсего 4 варианта
№ DMA |
Data direction |
Event |
Action |
1 |
Tx |
Half |
Timer Counter = 75% |
2 |
Tx |
Done |
Timer Counter = 25% |
3 |
Rx |
Half |
Timer Counter = 25% |
4 |
Rx |
Done |
Timer Counter = 75% |
И как видно, ответ никто. В прерываниях по DMA придется просто устанавливать значение аппаратного счетчика в 25% или 75% от периода счета. Это даже хорошо так как постоянно будет происходить синхронизация I2S и аппаратного таймера.
Так мы получим нужную шкалу для запуска процедуры обработки принятых семплов (например цифровое изменение громкости, или цифровая фильтрация). Осталось только понять кто именно даст команду на обработку каждой отдельной четвертинки принятого массива. Вот эта таблица показывает кто будет делать обработку.
С теорией определились. Теперь надо это всё реализовать на электронной плате.
Каков план?
--Подать тактирование на DMA1
--Определить канал1 DMA1 для отправки аудио потока в I2S2
--Определить канал2 DMA1 для приёма аудио потока из I2S2
--Запустить аппаратный таймер 8
--Настроить компаратор на таймере 8 и генерировать прерывание на половине периода.
--Настроить I2S2 на следующие параметры: 16bit, Master I2S, AudioFreq: 48kHz.
Практическая часть
Настало время сделать решающий эксперимент и проверить эту идею на реальном железе. В качестве прототипа я воспользуюсь учебно-треннировочной электронной платой AT-Start-F437. Перед вам двухъярусная сборка: AT-Start-F437 + WM8731.
Пришлось выбрать аудиокодек WM8731 так как это единственный аудиокодек для которого продается отладочные платы. Вот так соединены модуль аудио кодека WM8731 и учебно-треннировочную электронную плату AT-START-F473
Wire |
GPIO |
PIN MUX |
Pull |
MCU dir |
WM8731 board |
I2S2_CK |
PС7 |
5 |
Down |
out |
BCLK |
I2S2_WS |
PB9 |
5 |
Up |
out |
DACLR, ADLRC |
I2S2_SD |
PC3 |
4 |
Up |
out |
DADAT |
I2S2_SDEXT |
PC2 |
6 |
Up |
in |
ADDAT |
I2S2_MCK |
PA3 |
5 |
Air |
out |
-- |
I2C2_SDA |
PF0 |
4 |
Up |
in_out |
SDA |
I2C2_SCL |
PF1 |
4 |
Up |
out |
SCL |
схема подключения модуля с аудиокодеком WM8731.
На уровне SoC(а) AT32F437 надо активировать трансиверы SPI2 на передачу, а I2S2EXT на приём. Плюс включить TMR8
Тут надо пояснить, что в Artery, чтобы I2S заработал в полном дуплексе надо как бы составить его из двух отдельных одновременно работающих синхронно I2S трансиверов.
# |
Аппаратный модуль |
Направление |
шина |
режим |
Boundary address |
1 |
SPI2 |
Передатчик |
APB1 |
MASTER_TX |
0x4000 3800 |
2 |
I2S2EXT |
Приёмник |
APB2 |
SLAVE_RX |
0x4001 7800 |
Стоит заметить, что сброс регистров в SPI2 одновременно сбрасывает регистры в I2S2EXT. Вот такой привет со стороны Artery.
Надо назначить каналам DMA функции I2S2. Вот так.
DMA |
Channel |
Function |
Function |
Direction |
Data Width |
Direction |
1 |
1 |
13 |
SPI2_TX |
out |
HALF_WORD |
MEM->PERIPH |
1 |
2 |
110 |
I2S2_EXT_RX |
in |
HALF_WORD |
PERIPH->MEM |
Обработка принятых семплов
Как мы помним, при частоте дискретизации в 48kHz на обработку одного семпла у нас в распоряжении есть всего лишь 20.83 us. Это значит, что в конвейере на обработку непрерывной порции из 256 семплов у нас будет максимум 5.3ms. На всё. Поэтому необходимо чтобы Си функция в прошивке успела сделать свою DSP обработку за менее чем 5.3ms.
В качестве примера простой аудио обработки я попробовал реализовать искусственный эхо эффект на основе цифрового БИХ фильтра. Вот его цифровая цепь.
Все вычисления были для 16-битных семплов. На ARM Cortex-M4F при частоте ядра 250MHz эти 256 16-бит семплов обсчитываются IIR фильтром всего за 1.613 ms. Получается один семпл обрабатывается 6.3 us. Это в 3.3 раза быстрее, чем позволяет deadline. Успех.
Как отлаживать I2S?
Любая разработка начинается только тогда, когда появляются средства отладки. Особенно в I2S так как это hi load интерфейс. Тут мегагерцы в битовой скорости. Как за всем уследить? Как всё проверить? Ответ прост. Надо отлаживать аудио тракт по частям, подобно тому как в математике вычисляют интегралы по частям.
Фаза1: Проверка I2S на стороне MCU
I2S в режиме полного дуплекса можно проверить весьма остроумно. Можно установить перемычку с выхода на вход и включить передачу I2S данных. Это называется loopback Ожидается, что после остановки запишется тот же I2S поток, что и был воспроизведён. Бит в бит.
Таким образом можно будет проверить корректность настройки I2S трансивера на стороне микроконтроллера.
Фаза2: Проверка I2S на стороне аудио кодека WM8731
Аналогично можно проверить сам Audio Codec. Надо взять джампер или перемычку и соединить пины ADC-DATA и DAC_DATA. Таким образом получится полностью цифровое I2S - эхо. Бит в бит. Тот же самый loopback.
Затем можно попробовать сказать что-н в микрофон и одновременно слушать наушники.
++Если конфиг в кодеке исправен, то вы услышите то, что сказали в реальном времени. Без задержки.
---Если звука нет или появилась какая-то трескотня, то ищите ошибку в конфиге I2C ячеек внутри ASIC аудио кодека.
Вот так. Всего одним проводком вполне реально найти ошибку в конфигах, либо в MCU, либо в кодеке.
Когда заработает I2S Full Duplex режим на осциллографе должна быть вот такая картинка.
Фаза 3 Проверка синхронизации
Также можно использовать GPIO для контроля и отладки синхронизации между DMA каналами приёма и отправки. Надо лишь в обработчиках DMA прерываний выполнять вот такие действия:
DMA |
Канал |
Прерывание |
Действие GPIO |
GPIO |
пояснение |
1 |
1 |
Half |
Установить 3.3V |
PB6 |
Отправка половины |
1 |
2 |
Half |
Установить 3.3V |
PB7 |
Приём половины |
1 |
1 |
Done |
Установить 0V |
PB6 |
Отправка завершена |
1 |
2 |
Done |
Установить 0V |
PB7 |
Приём завершен |
Тогда, если всё хорошо, то должна получится вот такая осциллограмма
Как раз DMA отправка и DMA прием смещены друг относительно друга на пол периода. Так и должно быть.
Теперь можно спокойно запустить Timer8 c каналом сравнения №1 для выработки сигналов для запуска обработки принятых звуковых семплов. Тут стоит отметить, что в микроконтроллере Artery каналы сравнения есть не у всех таймеров. У таймера 6 их нет вообще. И у всех таймеров разное количество каналов сравнения. Прерывание по середине счета я сделал на основе аппаратного компаратора, которые есть в таймерах и эти компараторы тоже могут генерировать прерывания.
Как видно, удалось на практике разбить приём I2S 1024 семплов на 4 равных по времени интервала и вырабатывать триггеры на каждую четвертинку. Теперь остается только добавить в обработчики прерываний таймера 8 и DMA каналов код обработки семплов и получится обработка звука в реальном времени.
Достоинства I2S в режиме DMA
++Низкая нагрузка на центральный процессор микроконтроллера. Максимум 2 прерывания на всю передачу данных. Отправка и приём происходят полностью аппаратно. Это открывает дорогу для обработки I2S трафика в реальном времени прямо на MCU.
Недостатки I2S в режиме DMA
--В DMA нет прерываний по 1/3 и 2/3 от переданных данных. Как по мне, дак это печально. Прерывания на 33% 66% работы позволили бы выполнить оптимизацию производительности конвейера типа Read-Proc-Transmitt (R-P-T).
Итоги
Как видите, запуск I2S в режиме DMA это та ещё морока. Это hi-load тема. Надо производить всё предельно аккуратно. Нужно отладить и запустить целый конвейер. Надо разбираться в конечных автоматах. Надо знать как отлаживаться.
Зато DMA это единственно верный способ работы с такими интерфейсами как I2S, UART, SPI и прочее.
Этот текст пример того, что в программировании микроконтроллеров грамотная документация важнее, чем исходный код. По хорошей документации кто угодно напишет код. А имея только код восстановить документацию порой просто нереально.
Полученные результаты открывают дорогу для разработки всяческих приложений обработки аудио данных в реальном времени. Можно накладывать эхо эффект, дисторшн, производить цифровую фильтрацию и прочее.
Мне вот удалось реализовать цифровой эхо эффект на основе IIR фильтра, который работает полностью в реальном времени. Прямо на микроконтроллере.
Надеюсь, что текст поможет кому-н тоже разобраться в I2S и написать интересное звуковое приложение на микроконтроллере.
Словарь
Акронима |
Расшифровка |
DMA |
direct memory access |
БИХ |
бесконечная импульсная характеристика |
IIR |
Infinite impulse response |
I2S |
Inter-Integrated Circuit Sound |
R-P-T |
Read-Рrocess-Transmitt |
R-T |
Read-Transmitt |
GPIO |
General-purpose input/output |
Ссылки
# |
Гиперссылка |
1 |
|
2 |
|
3 |
|
4 |
|
5 |
Чип AudioСodec(а) WM8731 (или (ADC/DAC)*2 из iPod(а)) |
6 |
Автор: aabzel