Программный декодер MP3(+). Переход на платформу STM32F407

В предыдущих статьях цикла был описан проигрыватель на основе STM32F105. MP3 играет без проблем, однако хочется чего-то большего. А ресурсы контроллера — уже на пределе. Поэтому дальнейшее развитие проекта решено перенести на платформу SMT32F4DISCOVERY.

Итак, сегодня мы:

Кстати, недавно появилась ещё одна интересная плата — STM32F429IDISCOVERY. Примечательна тем, что на борту есть аж 64 Мбит дополнительного внешнего ОЗУ. И контроллер немного шустрее. Райские условия для проигрывания трекерной музыки! Но огорчает неравноценный размен аудиоЦАП на ЖКИ-экран. Поэтому не будем пока отвлекаться на разные посторонние продукты, берём SMT32F4-ОТКРЫТИЕ и начинаем работать.

Подготовка железа
На этот раз контроллер перепаивать не будем, ведь эта плата выбрана как раз с учётом его наличия. Большим плюсом считаем присутствие звукового ЦАП. Хотя это снова не HI-FI, наши непрофессиональные уши звуком будут довольны. Но теперь очень не хватает слота под карту памяти — придётся прикручивать.

Да, и с кнопками опять не повезло — одна пользовательская и одна для сброса. Зато лампочек аж четыре штуки, и все разного цвета. Ляпота…

На плате есть две USB-дырки. Одна используется для отладки, а вторая — на усмотрение программиста. К ней можно подключить например USB-Flash-свисток. И использовать его как носитель для нашей музыки. Для mp3 — самый раз (на просторах интернета уже был такой проект). Однако, могут возникнуть проблемы с быстрым чтением сэмплов из MOD/XT-файлов. Поэтому воспользуемся всё той же картой памяти SD. А USB можно использовать в качестве связи компутера и проигрывателя, — в виде виртуального COM-порта. Но для отладки он неудобен — при каждом перезапуске контроллера виртуальный порт будет отваливаться.

Контроллер STM32F407 имеет на борту специализированный интерфейс, предназначенный для подключения карт памяти — SDIO. Грешно не воспользоваться этой полезной опцией. При использовании 4-битной шины и максимальной скорости тактирования 24 МГц теоретически можно получить производительность до 24000*4/8=12000 кбайт/сек. Зачем нужна такая скорость? Для MP3 действительно перебор, но при проигрывании трекерной музыки формата XM — очень даже может пригодиться. Ведь там число каналов увеличено с 4-х до 32 (и более), поэтому сэмплы придётся читать гораздо больше и чаще.

Так, теперь где-то нужно раздобыть слот для карты памяти. Припаять провода к переходнику micro/miniSD, купить в магазине, заказать в Китае… Есть ещё вариант. Окидываем взглядом комнату… о, вот он, будущий Почётный Донор!

Берём в руки, включаем, и… эх, какая неприятность! Упал на пол с открытым объективом. Объектив заклинило, ни туда, ни сюда… Последующее аккуратное вскрытие покажет, что аппарат починке и сборке до исходного состояния уже не подлежит:

Останется пообещать жене купить новый фотик, — и в наших руках отличный надёжный слот с хитрым механизмом «вставил-вынул».

К интерфейсу SDIO STM32F407 карта microSD обычно подключается так:

На полноразмерной карте есть дополнительный земляной вывод. Для подключения к Дискавери можно воспользоваться следующим наглядным пособием (Vss — общий, Vcc — питание 3В):

Удивительно, что несмотря на хаотичное подключение портов контроллера, нужные цепи расположены компактно с одной стороны разъёма. Необходимо только отдельным проводом дотащить питание 3В.

Также необходимо подключить датчик наличия карты (и при необходимости — датчик защиты от записи). В прилагаемом проекте использован порт PA8 (контроль карты).

Обращаю внимание: частота работы интерфейса довольно высока, поэтому проводники должны иметь минимальную длину. Если длина проводов превышает 5-10 см, то рекомендуется применение плоского шлейфа с чередованием сигнальных и земляных цепей.

В Сети есть масса примеров по использованию SDIO. Например, почитаем местный форум форум форум форум:
… Спасибо огромное, парни. Начал смотреть — пока разрыв мозга...)))

Повторим народный опыт не спеша, по шагам. Чтобы не допустить этого самого разрыва. И попутно обходя и собирая разбросанный где попало садово-огородный инструмент.

Грабли № раз

Крайне желательно заблокировать цепь питания карты конденсатором (тантал + керамика) как можно ближе к контактам слота. Иначе можем получить множество странных глюков. На случай, если ссылка вдруг поломается, укажу здесь итог полуторамесячных поисков:
John, thank you so much!!! I added these capacitors and now it works much better with the 4 bit mode! I now get 2,5mb/s write and read speed. THANK YOU!

Грабли № два

Знатный садовый инструмент, многие на него наступали: интерфейс SDIO требует наличия подтяжки сигнальных цепей (кроме клока). Обычно это делается внешними резисторами 47к (см. схему выше). Но вполне должно хватить и внутренней подтяжки, встроенной в порты контроллера. Только не забыть её включить. Без подтяжки интерфейс SDIO начинает глючить и зависать.


Как подружить FatFs и SDIO

А у FatFs обновление. Вышла версия R0.10. Обновляемся и ищем драйвер для интерфейса SDIO.

Martin THOMAS в этом деле нам не помощник, он честно признаётся, что
… на моей демоплате карта памяти подключается через SPI1, а контроллер STM32F103 часто не имеет порта SDIO; если хотите использовать SDIO, вам поможет STM fwlib...

upd: на сайте Кокоса (см. Source/Components/Memory/FATFS_SD for STF4) есть подходящая библиотека. Можно попробовать использовать её.


Итак, драйвер файловой системы будет строиться по схеме:
  • FatFs — собственно одноимённый драйвер;
  • diskio — драйвер-прослойка между FatFs и драйвером интерфейса;
  • xxx_sdio_sd — драйвер физического интерфейса SDIO;
  • sdio_low_level — модуль низкоуровневой инициализации (порты микроконтроллера, DMA, прерывания).

На местном форуме по указанным выше ссылкам приведено более десятка патченных/доработанных/переделанных драйверов. В их основе — один и тот же драйвер от MCD Application Team, прилагаемый производителем ST к своим демонстрационным примерам.

Драйвер неплохо документирован, имеет шапку с указанием номера версии. Но этот номер начинается с первого для каждого типа отладочной платы. Например, для STM322xG-EVAL драйвер имеет версию 5.1.1, а для STM324x7I-EVAL — v1.0.0. А по сути и содержимому оба драйвера одинаковы (за исключением названия контроллера и демоплаты в комментариях). Поэтому при поиске и анализе версий ориентируемся на автора (MCD Application Team) и дату (не ранее 2013г).

Найти нужную библиотеку на сайте ST непросто, но мы уж постараемся. На странице с описанием демоплаты STM3240G-EVAL есть ссылка на исходники STSW-STM32106. Они то нам и нужны. Используем файл stm324x7i_eval_sdio_sd.c и его одноимённый заголовок *.h.
Драйвер более свежей версии можно найти в STM32F4xx_DSP_StdPeriph_Lib_V1.2.0. Берём здесь.

В указанной библиотеке есть наш файл stm324x7i_eval_sdio_sd.c от сентября 2013 года с одним изменением. В функции SD_WriteMultiBlocks это
SDIO_ITConfig(... SDIO_IT_RXOVERR ... , ENABLE);
изменено на
SDIO_ITConfig(... SDIO_IT_TXUNDERR ... , ENABLE);
Поверим, что так и надо. Но тогда и функцию SD_WriteBlock разработчикам тоже следовало бы поправить.

Вся информация по применению драйвера указана в начале файла. Как обычно, реализовано чтение/запись при помощи DMA и программного поллинга. Что лучше? Ну конечно, DMA. Правда потребуются дополнительные телодвижения по его настройке. Чуть позже сравним оба варианта по производительности.

Теперь займёмся модулем diskio.c. Шаблон нужно взять из комплекта FatFs и нарастить его скелет мясом.

1. Функция disk_initialize — вызывается при первом обращении к карте памяти со стороны FatFs:
disk_initialize
{
    // инициализация переменной состояния
    ...
    // инициализация интерфейса SDIO
    SD_Init();
    // инициализация прерываний
    SD_IRQ_Init();
}
Примечание: здесь и далее я опускаю некоторые непринципиальные особенности программы (например, в каждой функции модуля diskio желательно проверять номер диска pdrv, а также контролировать результат выполнения функций интерфейса SDIO).

2. Функция disk_status — запрос статуса карты (опрос контактных датчиков наличия карты и защиты от записи). Здесь рекомендую контролировать именно извлечение карты, и в принудительном порядке устанавливать флаг STA_NOINIT. Таким образом мы будем сообщать системе о необходимости повторной инициализации.
Также рекомендуется независимо контролировать наличие карты по таймеру или по прерыванию, уведомляя верхнее ПО об извлечении/установке карты.

3. Функция disk_read — чтение одного или нескольких секторов. Для чтения драйвер SDIO предлагает две процедуры: SD_ReadBlock и SD_ReadMultiBlocks. Первая предназначена для чтения только одного блока (сектора). Вторая — несколько секторов, расположенных подряд. Вторая функция перекрывает по возможностям первую, но в SD_ReadMultiBlocks реализована только передача DMA. Поэтому можно сделать так:
disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count)
{
#if defined (SD_POLLING_MODE)
    for (int i = 0; i < count; i++)
    {
        SD_ReadBlock(buff + i*512, (sector + i)*512, 512);
        SD_WaitReadOperation();
        while(SD_GetStatus() != SD_TRANSFER_OK);
    }
#endif
#if defined (SD_DMA_MODE)
    SD_ReadMultiBlocks(buff, sector*512, 512, count);
    SD_WaitReadOperation();
    while(SD_GetStatus() != SD_TRANSFER_OK);
#endif
}
Внимание! В функцию disk_read передаётся параметр «номер сектора», а SD_ReadBlock и SD_ReadMultiBlocks принимают абсолютный адрес первого байта сектора. Т.е. номер сектора нужно домножить на размер сектора. В Сети есть «fixed»- версия библиотеки (функции SD_ReadMultiBlocksFIXED и SD_WriteMultiBlocksFIXED), доработанная энтузиастами для поддержки карт более 4 Гб. Однако такого извращения доработки не требуется, если в функцию SD_Read(Write)MultiBlocks правильно передавать абсолютный адрес, а не номер сектора.

Кстати, функция SD_WaitReadOperation ждёт окончание DMA-передачи. В многозадачной вытесняющей RTOS в этом месте можно приостановить задачу и использовать процессор для обработки других задач.

4. Функция disk_write — запись одного или нескольких секторов. Имеет примерно тот же вид, что и disk_read.

5. Функция disk_ioctl — набор служебных команд. Это команды для обслуживания диска, не связанные непосредственно с чтением/записью. Из всего многообразия FatFs использует только эти:
  • CTRL_SYNC (используется функциями записи);
  • GET_SECTOR_COUNT (только для f_fdisk и f_mkfs);
  • GET_SECTOR_SIZE (вызывается большинством функций через служебную функцию find_volume)
  • GET_BLOCK_SIZE (f_mkfs);
  • CTRL_ERASE_SECTOR (f_mkfs и в функциях создания/изменения/удаления файла/директории).

CTRL_SYNC — по этой команде устройство должно записать данные, хранящиеся в кэше и завершить запись. Мы работаем с картой напрямую без кэша, данные записываем и всегда дожидаемся окончания записи, поэтому по этой команде можно ничего не делать.

GET_SECTOR_COUNT — команда должна возвратить количество секторов на карте памяти. Берём SDCardInfo.CardCapacity и делим на размер сектора SDCardInfo.CardBlockSize (всегда 512).

GET_SECTOR_SIZE — требуется возвратить размер сектора. Для режима _MAX_SS=512 (этот параметр всегда указывается при работе с картой памяти) команда GET_SECTOR_SIZE не используется и реализовывать её также не требуется.

Важный момент. Карта SD/SDHC всегда имеет физический сектор длиной 512 байт, операции чтения-записи должны обращаться к этому сектору целиком. Для карты простого SD-формата стандартом допускается изменение размера блока (сектора), используемого для чтения/записи, см. команду CMD16. Для чего это нужно и где использовать? Не могу сказать. Более того, в самом стандарте сказано, что
… The default block length is as specified in the CSD (512 bytes). A set block length of less than 512 bytes will cause a write error. The only valid write set block length is 512 bytes. CMD16 is not mandatory if the default is accepted…
Будет ли FatFS корректно работать c картой с изменённым размером сектора? Не проверял. Поэтому предлагаю такие карточки отбраковывать на этапе инициализации драйвера (в функции disk_initialize).

GET_BLOCK_SIZE — запрос количества секторов, объединяемых в один блок, который можно стереть. Параметр используется в функции f_mkfs для правильной разметки диска.

В этом месте начинаются проблемы с терминологией. Стандарт SD под минимальной единицей чтения/записи подразумевает block, несколько блоков объединяются в сектор (обычно 32), который можно стереть одной командой. Аналогично реализовано например в DataFlash. В драйвере FatFs всё наоборот: сектора объединяются в блоки. Поэтому, чтобы не ломать голову, указываем GET_BLOCK_SIZE = 1, т.е. размер блока и сектора будут совпадать. А карта SD допускает стирание как одного сектора, так и одного блока.

CTRL_ERASE_SECTOR — команда быстрого стирания заданного количества секторов (блоков на SD карте). Используется при разрешении _USE_ERASE=1. Думаю, что в большинстве случаев нам достаточно «неускоренного» стирания, поэтому делаем _USE_ERASE=0 и забиваем на CTRL_ERASE_SECTOR.

Вот так страшный и ужасный disk_ioctl превратился в простую незамысловатую фунцию:
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void *buff)
{
    extern SD_CardInfo SDCardInfo;
    switch (cmd)
    {
    /* Flush disk cache (for write functions) */
    case CTRL_SYNC:
        return RES_OK;

    /* Get media size (for only f_mkfs()) */
    case GET_SECTOR_COUNT:
        *(DWORD*)buff = SDCardInfo.CardCapacity/SDCardInfo.CardBlockSize;
        return RES_OK;

    /* Get sector size (for multiple sector size (_MAX_SS >= 1024)) */
    case GET_SECTOR_SIZE:
        *(WORD*)buff = SDCardInfo.CardBlockSize;
        return RES_OK;

    /* Get erase block size (for only f_mkfs()) */
    case GET_BLOCK_SIZE:
        *(WORD*)buff = 1;
        return RES_OK;

    /* Force erased a block of sectors (for only _USE_ERASE) */
    case CTRL_ERASE_SECTOR:
        return RES_OK;
    }
    return RES_PARERR;
}
Уточнение: функция disk_ioctl используется в процедурах записи (форматирования, разметки и т.д.) и если размер сектора не равен 512 (не наш случай). Т.е. при использовании только функций чтения disk_ioctl не нужна.

Грабли № два. Дополнение

В модуле stm324x7i_eval_sdio_sd.c есть хитрая функция FindSCR, которая «ищет значение регистра SCR карты памяти SD». Если подтяжка портов SDIO не реализована, на этой функции выполнение программы и завершается (зацикливается). Я тоже здесь «повисел» немного, хотя все резисторы были на месте.

Зависон наблюдался в этой части функции:
uint32_t index = 0;
uint32_t tempscr[2] = {0, 0};
...
while (!(SDIO->STA & (SDIO_FLAG_RXOVERR |
                      SDIO_FLAG_DCRCFAIL |
                      SDIO_FLAG_DTIMEOUT | 
                      SDIO_FLAG_DBCKEND | 
                      SDIO_FLAG_STBITERR)))
{
  if (SDIO_GetFlagStatus(SDIO_FLAG_RXDAVL) != RESET)
  {
    *(tempscr + index) = SDIO_ReadData();
    index++;
  }
}

Видим, что драйвер ожидает наступления какого-то события (перечисленные флаги), смело надеясь прочитать не более 2 байт.

В моей ситуации читалось ровно 2 байта, хотя требуемое событие так и не наступало. Помог вот такой костыль в цикле:
if (index >= 2) break;
Сейчас проблема самоустранилась, если будет новая информация — напишу.

Дополнительные телодвижения для интерфейса SDIO

Ещё нам нужно реализовать несколько дополнительных функций, вызываемых модулем xx_sdio_sd.c. Это:

  • SD_LowLevel_Init — инициализация GPIO, включение тактирования SDIO и DMA;
  • SD_LowLevel_DMA_RxConfig — настройка DMA для чтения данных с карты;
  • SD_LowLevel_DMA_TxConfig — настройка DMA для записи данных на карту;
  • SD_IRQ_Init — инициализация подсистемы прерываний;

и не забыть указать обработчики прерываний SDIO и DMA:
void SDIO_IRQHandler(void)
{
    SD_ProcessIRQSrc ( );
}
void SD_SDIO_DMA_IRQHANDLER(void)
{
    SD_ProcessDMAIRQ();
}

Все эти функции помещаем в отдельный модуль sdio_low_level.c (не надо мусорить в xx_sdio_sd.c !). Если использовать только программный поллинг, то всё, что касается DMA, можно опустить.

Грабли № три, такие мелкие и незаметные, а ударили очень больно!

Ядро Cortex-M довольно лояльно относится к выравниванию данных. В отличие например от ARM7TDMI, обычно не паникует и не уходит в исключение при попытке невыровненного доступа. И это немного расслабляет программистов.

Когда на Дискавери запустил декодер mp3, обнаружил проблему: в режиме SDIO+DMA звук квакал и заикался. А в режиме программного поллинга проблем не было. Функция MP3Decode периодически выдавала ошибку ERR_MP3_INVALID_HUFFCODES. Покопавшись пару дней в коде Хеликс-декодра, понял, что зря это делал. Декодер написан аккуратно и проблем с ним быть не должно. Оказалось — файловая система иногда выдавала подпорченные данные.

FatFs имеет буфер размером в один сектор и использует его при чтении/записи порции данных менее 512 байт. Если за один раз читается более 512 байт, то данные из интерфейса SDIO напрямую перенаправляются во входной буфер приложения. А начальный адрес этого буфера (или места в буфере, куда читается кусок файла) может быть некратным 4 байтам. Для предотвращения проблемы DMA должен оперировать не словами, а байтами. Для чего в инициализации SDIO DMA достаточно указать xx_Byte вместо xx_Word:
SDDMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;

Кстати, дефекты чтения сильно проявились при проигрывании файла с битрейтом 256 кбит/с. А файл 128 кбит/с звучал практически без проблем.

И напоследок следует проверить полярность сигнала с контакта контроля каты в слоте. Обычно при вставленной карте на соответствующем порте контроллера (SD_DETECT_GPIO_PORT + SD_DETECT_PIN) должен быть низкий уровень. Если наоборот — следует подправить функцию SD_Detect().

Особо любознательным могу предложить ресурс. Там есть полезные ссылки на спецификацию SD и на статьи с описанием тонкостей интерфейса SDIO.

Вот, наконец всё готово, можем приступать к тестированию файловой системы.


Тест SDIO

Для теста используется тот же абстрактный файл, что и в третьей статье цикла, размером порядка 10 Мб. Проверяем скорость чтения как зависимость от:
  • типа карты (старая/новая, SD/SDHC);
  • размера читаемого блока;
  • режима работы интерфейса SDIO (скорость, битность).
Время контролируется по внутреннему таймеру.

Представляю участников нашего соревнования:
  • под номером 1 выступает выпускник десятого класса, только начинающий свою фотокарьеру, — Transcend SDHC 32GB Class 10.
  • под номером 2 представлен помощник фотокорреспондента, ветеран боевых действий, вынесший с поля боя буквально на своих плечах десятки гигабайт фото- и видеоинформации — карта Transcend miniSD 512 MB 80х;

Участникам предлагается максимально быстро переместить указанный объём информации, поделенный на порции по 512/4096/65536 байт. В качестве средства передвижения — одноколёсный и четырёхколёсные велосипеды с разной максимальной частотой вращения колёс.
Результаты заездов:

  • малые порции данных свежая карта SDHC переносит заметно медленнее, чем старичок; вероятно сказывается большое количество секторов — сложнее искать нужный; но зато большие блоки читает шустро, даже иногда превышая скорость света (красный шрифт; почему — вопрос конечно интересный, но разбираться лень; вторая карта тоже так быстро летать умеет);
  • максимальная скорость участника №1 составила более 10 МБ/сек, что очень неплохо; только надобность таких больших порций данных в микроконтроллере подвергается сомнению;
  • участник №2 не смог нормально работать на шине 24 МГц (драйвер FatFs периодически выдавал ошибку FR_DISK_ERR); вероятно, это запредельные для него нагрузки;
  • довольно убедительно выглядят преимущества режима DMA. На малых блоках программный поллинг выигрывает за счёт исключения ненужной инициализации DMA, но с ростом объёма данных скорость больше не увеличивается.

От типа оптимизации кода (-O0...-Os) производительность зависит несущественно.

Для *.MP3 и *.MOD-приложений хватит любой скорости (хотя ниже 1 МБ/сек желательно не опускаться). А для запланированного XM — надо посчитать:

Максимальный поток данных будет при активности всех 32 каналов с максимальной частотой дискретизации. 3,58 МГц / 57 * 32 канал * 2 байта (сэмпл 2-байтный) = 4020 кБ/сек. Это уже очень много. Ещё не забываем, что нужны немалые ресурсы для работы 32-канального микшера.

Безусловно, это пиковая потребность, в реальных композициях одновременное использование всех каналов на максимальной скорости маловероятно. Да и некоторое количество сэмплов будет храниться во внутренней памяти. Но в любом случае, для игры XM с карты памяти (без использования внешней RAM) придётся жёстко оптимизировать функции чтения и разнести микшер и чтение файла по разным задачам RTOS.


Тест mp3-Хеликса

Файлы читать научились, теперь можно и поиграть. Сначала воспользуемся проверенным вариантом — встроенным ЦАП. Ничего нового за исключением довольно слышимых помех с частотой следования фреймов. Похоже на помеху от отладочного светодиода. Надо будет разобраться на досуге.

Поиграем тот же mp3-файл, что и в одной из предыдущих статей (320 кбит/с). Раньше я указывал время, используемое не только на декодирование, но и на загрузку данных из файла. Так как интерфейс карты ускорился, сравнение предыдущей и новой платформ по такой методике буден некорректным. Поэтому далее буду учитывать чистое время выполнения функции MP3Decode. Измерения осуществляются в режиме оптимизации -Os.

Разница в производительности двух платформ очевидна (указано время декодирования относительно длительности фрейма):

На интервале фрейма (26 мс) время работы декодера уменьшилось с 11 до 2,8 мс (практически в 4 раза). В этом в первую очередь виновато повышение тактовой частоты. Кроме более быстрого ядра, Cortex-M4 имеет DSP-сопроцессор, значительно ускоряющий выполнение типовых ЦОС-операций. Интересно было бы попробовать его в деле.

Пробуем ускориться

Чтобы эффективно задействовать DSP-акселератор, недостаточно сказать «давай, работай быстрее!». Нужно пересмотреть и проанализировать код, найти участки, интенсивно занимающиеся вычислениями, и заменить их на специализированные DSP-команды. В данном деле приветствуется хорошее знание ассемблера ARM (иначе эти знания придётся таки приобрести). Оно нам надо? В принципе, и так всё отлично работает. Но, как говорил дед Дмитрия Нагиева, дурная голова пытливый ум сам себе покоя не даёт, поэтому приступим.

Ознакомление с возможностями по ускорению можно начать со статьи Богатый набор для неординарных задач: возможности DSP в STM32F4 на ядре Cortex-M4.

Одна из ресурсоёмких функций, как я уже говорил, — это полифазный фильтр. Покопаемся в его внутренностях.

1. Ядро Cortex-M3 имеет функцию умножения двух 32-разрядных чисел с 64-битным результатом SMULL. А в Cortex-M4 DSP-модуль берёт выполнение этого умножения на себя и выполняет его за 1 такт (у М3 — 3..5 тактов). Декодер Хеликс довольно плотно использует SMULL, поэтому выигрыш может быть существенным. Собственно, код команды не изменился, поэтому здесь нам делать больше нечего.

2. Пожалуй самой распространённой DSP-командой считается умножение со сложением. Например, SMLAL (64=64+32*32). Система команд ARM её также поддерживает по умолчанию. Третий Кортекс выполняет команду за 4-7 тактов, а Кортекс-Четыре — за 1 такт. Команда также широко используется в полифазном фильтре. Здесь снова всё уже сделано за нас.

Кстати, в обычном Кортексе длительность выполнения умножения (SMULL, SMLAL и т.п.) зависит от значения обрабатываемых данных. Этим объясняется такая неравномерная нагрузка на контроллер в зависимости от музыкального контента. А у М4 она получилась заметно равномернее (см. график выше).

Вот, хотел дополнительно ускорить декодер применением специализированных DSP-команд, а оказалось, что они уже используются. Давайте посчитаем выгоду.

STM32F1x при работе на 36 МГц декодирует фрейм за 18 мс (перемерял). Тогда только повышение тактовой частоты даст прирост 18*36/168 = 3,86 мс. А мы сейчас имеем 2,8 мс — это заслуга DSP-сопроцессора (в первую очередь, ускорение указанных выше команд умножения). Достойно.


3. В ядро Cortex-M4 добавлены инструкции операций с насыщением. Они позволяют ограничивать результаты математических операций, не допуская «перепрыгивания» значения через его максимальное/минимальное значение. В качестве примера обычно приводят такую картинку:


Теоретически при декодировании не должно возникать искажений, связанных с внутренним переполнением. Но так как mp3 по своей сути является «искажающим» форматом, то от насыщения ему хуже уже не будет. В любом случае насыщение лучше, чем указанные выше искажения.

Хеликс использует клиппинг для преобразования промежуточных данных из внутреннего 64-битного формата в выходной 16-битный. Внимательно смотрим как баран на новые ворота на полифазный фильтр и изучаем макрос C64TOS:
MOV   R4,  R4, LSR #26     ; R4 = R4 >> 26
ORR   R4,  R4, R5, LSL #6  ; R4 = R4 | (R5 << 6)
MOV   R12, R4, ASR #31     ; R12 = R4 >> 31 (арифметический сдвиг, бит 31 копирует сам себя)
CMP   R12, R4, ASR #15     ; сравнить R12 и (R4 >> 15) (арифметический сдвиг R4)
IT NE                      ; проверка условия IF THEN 
EORNE  R4, R12, R14        ; это выполняется, если условие не выполнено

Проверка выхода значения за границы -32768+32767 выполняется командами 3-6 и занимает около 4 тактов. Cortex-M4 позволяет это сделать за 1 такт при помощи команды SSAT (впрочем, Cortex-M3 тоже знает эту команду).

Так, проверим, а сколько времени оно занимает? Тупо комментарим эти строки, и играем тестовый файл. Играет также, только изредка стали слышны потрескивания (см. рис. выше). Т.е. ограничитель объективно необходим. Среднее время декодирования фрейма уменьшилось на 2% (с 2,87 до 2,81 мс).

Изучаем описание команды SSAT и внедряем её в C64TOS:
MOV    R4,  R4, LSR #26
ORR    R4,  R4, R5, LSL #6
;MOV   R12, R4, ASR #31
;CMP   R12, R4, ASR #15
;IT NE
;EORNE R4, R12, R14
SSAT   R4, #16, R4
Ограничитель работает, щелчки прекратились. Результирующее время декодирования фрейма стало 2,82 мс. Прибавка совсем небольшая, особенно учитывая и так малую загрузку контроллера. Но нас ведь интересует сам творческий процесс! Кладём информацию в мозговую копилку.

Что-то ещё ускорить в Хеликсе? Думаю, возможно. Но это потребует больших усилий, а результат будет несоразмерен затраченному времени.


Запуск ЦАП

А теперь пора настроить внешний ЦАП, любезно установленный разработчиками представленной отладочной платы. Внимательно изучаем схему — и тут нас ждёт Большой Облом :-(. ЦАП подключен к интерфейсу I2S3, который по пинам пересекается с SDIO. Т.е. использовать предлагаемый
CS43L22, audio DAC with integrated class D speaker driver
совместно с SDIO невозможно :-((((. А ведь у контроллера есть ещё один порт I2S2, да и цепям I2S3 можно было бы сделать ремап, чтобы развести их с SDIO. Но — не повезло.

Дорожки резать и переподключать ЦАП не предлагаю, слишком тонкие они на этой Дискавери, не каждый справится. Поэтому далее проект будет развиваться двумя путями:

1. внутренний ЦАП + карта через SDIO (выжать максимальную производительность);
2. внешний ЦАП + карта памяти через интерфейс SPI (для аудиофилов; MP3 и MOD играть будет, а с планируемым 32-канальным XM возможно не справится).

Чтобы карта памяти не мешалась ЦАПу, перекидываем её на любой свободный SPI (а такой у нас один — SPI2) и адаптируем disc.c под обычный SPI-интерфейс. Полностью готового и простого решения не нашёл (может, плохо искал?), поэтому берём пример от Мартина Томаса, и творчески его перерабатываем. У периферии SPI, GPIO и DMA STM32F105-го и STM32F407-го есть небольшие отличия, их то и нужно учесть. Работоспособную версию прикладываю.

Возвращаемся к ЦАП CS43L22. Сначала обязательно качаем даташит и пытаемся разобраться, как оно работает. Микросхема навороченная, но описание оставляет желать лучшего. Для облегчения запуска ЦАП предлагаю краткую инструкцию по применению.

Спасибо ST и MCD Application Team за предоставленный пример-библиотеку для CS43L22. Качаем комплект примеров к нашей плате STSW-STM32068 и берём оттуда файл stm32f4_discovery_audio_codec. Эта библиотека предоставляет большинство необходимых нам функций:
  • инициализацию интерфейса управления I2C;
  • инициализацию и управление основными функциями CS43L22;
  • инициализацию интерфейса для передачи аудиосэмплов I2S;
  • поддержку передачи сэмплов через DMA либо в прерывании.
Кое-что как обычно нужно придумать-доделать самим.

Покопаемся в настройках драйвера (файл stm32f4_discovery_audio_codec.h):

//#define I2S_INTERRUPT
Использовать передачу сэмплов в прерывании I2S. Для загрузки будет вызываться callback-функция EVAL_AUDIO_GetSampleCallBack. Зачем нам прерывания, когда есть DMA. Закомментарим.

//#define AUDIO_MAL_MODE_NORMAL
Выбрать «нормальный» режим работы драйвера. В этом режиме драйвер по команде «играет» указанные сэмплы и останавливается. Может быть полезно, когда вся музыкальная композиция целиком находится в памяти. Комментарим.

#define AUDIO_MAL_MODE_CIRCULAR
Выбрать циклический непрерывный режим работы. То что нужно. Данные будем подгружать по прерыванию DMA.

#define AUDIO_MAL_DMA_IT_TC_EN
Разрешить прерывание DMA Transfer Complete interrupt. Тоже нужно. По окончании передачи буфера драйвер вызовет callback-функцию EVAL_AUDIO_TransferComplete_CallBack.

//#define AUDIO_MAL_DMA_IT_HT_EN
Разрешить прерывание DMA Half Transfer Complete interrupt. Если в программе используется один циклический аудиобуфер, то нужно разрешить. В нашем проекте используется два независимых аудиобуфера, поэтому «половинное» прерывание не используется.

//#define AUDIO_MAL_DMA_IT_TE_EN
Разрешить прерывание DMA Transfer Error interrupt. Что с ним делать, не знаем. Поэтому комментарим.

И последний важный параметр:
#define DAC_USE_I2S_DMA
Использовать DMA для передачи данных. Конечно же, будем использовать.

Применение:
void player()
{
  // указать использование I2S-интерфейса
  EVAL_AUDIO_SetAudioInterface(AUDIO_INTERFACE_I2S);

  // инициализировать интерфейс
  EVAL_AUDIO_Init(OUTPUT_DEVICE_HEADPHONE,
                  Volume, // начальная громкость
                  44100); // битрейт

  // начать играть указанный буфер с указанным размером
  EVAL_AUDIO_Play(pBuffer, Size);
	
  // приостановить проигрывание
  EVAL_AUDIO_PauseResume(AUDIO_PAUSE);
	
  // продолжить проигрывание
  EVAL_AUDIO_PauseResume(AUDIO_RESUME);

  // регулировка громкости
  EVAL_AUDIO_VolumeCtl(Volume);	// 0..100

  // выключить звук
  EVAL_AUDIO_Mute(AUDIO_MUTE_ON);
	
  // включить звук
  EVAL_AUDIO_Mute(AUDIO_MUTE_OFF);

  while(музыка_ещё_не_закончилась)
  {
     ждать_окончание();
  }

  // остановить интерфейс 
  EVAL_AUDIO_Stop(CODEC_PDWN_SW);
  EVAL_AUDIO_DeInit();
}

// обработчик прерывания DMA
EVAL_AUDIO_TransferComplete_CallBack(uint32_t pBuffer, uint32_t Size)
{
  EVAL_AUDIO_Play(pNextBuffer, NextSize);
}

Особенности:

Драйвер позволяет «гнать» звук от встроенного ЦАП транзитом через CS43L22. Говорим, что не надо:
EVAL_AUDIO_SetAudioInterface(AUDIO_INTERFACE_I2S);

Функция инициализации EVAL_AUDIO_Init() должна вызываться первоначально и всякий раз после выключения драйвера функцией EVAL_AUDIO_Stop().

Запуск работы драйвера выполняется функцией EVAL_AUDIO_Play(), по которой выполняется загрузка/инициализация/запуск DMA.

В режиме AUDIO_MAL_MODE_CIRCULAR можно использовать два алгоритма работы:
  1. используется один циклический буфер. Необходимо разрешить прерывания DMA_Half_Transfer и DMA_Transfer_Complete, и в соответствующем обработчике заполнять требуемую половину буфера;
  2. вариант, реализованный в представленном проекте: драйвер проигрывает заданный буфер до конца, потом по прерыванию DMA_Transfer_Complete драйверу предоставляется следующий буфер командой EVAL_AUDIO_Play. В этом случае мы не привязаны жёстко к одному буферу. Траблов со звуком не возникает, аппаратное буферирование потока данных не позволяет прерываться звуку при последующей инициализации DMA.

И напоследок: с управлением громкостью и mute всё довольно просто. Активный mute может автоматически выключаться по команде EVAL_AUDIO_PauseResume(AUDIO_RESUME).

Важно помнить, что после останова драйвера EVAL_AUDIO_Stop() необходимо заново инициализировать EVAL_AUDIO_Init() и запускать EVAL_AUDIO_Play() драйвер. Чтобы не заниматься лишней и ненужной инициализацией, при смене аудиотрека лучше приостанавливать и возобновлять работу ЦАП командой EVAL_AUDIO_PauseResume().

При запуске ЦАП потребовалось допилить драйвер. Смотрим функцию Audio_MAL_Play(). В ней выполняется настройка DMA.

Во-первых, перед инициализацией регистров DMA его необходимо выключить. Возможно, разработчики не предполагали использование EVAL_AUDIO_Play() в обработчике прерывания. В начале добавляем
DMA_Cmd(AUDIO_MAL_DMA_STREAM, DISABLE);

Во-вторых, какие-от непонятки с размером блока данных. EVAL_AUDIO_Play() в качестве параметра принимает размер звукового буфера в байтах (в стереосэмплах было бы логичнее). Затем уже внутри EVAL_AUDIO_Play размер делится на 4 (2 канала по 16 бит). Хорошо. Но в DMA далее загружается значение, ещё уменьшенное в 2 раза. А так как DMA отправляет «порции» по 2 байта, то реально буфер будет воспроизводиться в 4 раза короче, чем следовало бы. Исправляем:
//DMA_InitStructure.DMA_BufferSize = (uint32_t)Size/2;
  DMA_InitStructure.DMA_BufferSize = (uint32_t)Size*2;

После внесения доработок драйвер к применению практически готов. Осталось настроить систему тактирования.

Интерфейс I2S тактируется от отдельного генератора PLLI2S (см. даташит на STM32F4x7, — описание системы тактирования). В описываемом драйвере PLLI2S не настраивается, поэтому необходимо его инициализировать самостоятельно.

На данном этапе необходимо чётко представлять, как работает система тактирования I2S и CS43L22. По документации на контроллер и на аудиоЦАП понять это оказалось делом далеко не простым (таков традиционный стиль документации от ST, у Сirrus Logic описание тоже довольно мутное). Давайте разбираться.

Физический интерфейс I2S имеет минимум три линии:
  • линия данных SDIN, по которой поочерёдно передаются сэмплы для левого и правого каналов;
  • тактовый сигнал SCLK, синхронизирующий передачу данных;
  • сигнал выбора левый/правый LRCK — указание, для какого канала предназначен текущий сэмпл.


Интервал передачи одного сэмпла (интервал высокого или низкого состояния LRCK) называется фреймом. ЦАП CS43L22 24-разрядный, умеет работать с фреймом длиной от 16 до 32 бит. В 32-битном фрейме будут использованы только первые 24 бита. Так как Хеликс выдаёт 16-битные отсчёты, лишние нулевые биты в ЦАП передавать смысла нет. Да и драйвер stm32f4_discovery_audio_codec.c тоже работает только с 16-битными данными. Поэтому I2S настраивается на передачу 16-битных слов (см. функцию Codec_AudioInterface_Init).

ЦАПу нужен ещё один сигнал — MCLK. Он используется для работы сигма-дельта модулятора и должен быть как минимум в 128 раз выше частоты сэмплирования Fs.

Далее. I2S работает в режиме «ведущий-ведомый». Ведущим считается устройство, формирующее тактовые сигналы LRCK и SCLK. Причём ведущим может быть как контроллер, так и ЦАП. Если ЦАП работает в режиме мастера, LRCK и SCLK генерируются путём деления MCLK. К примеру, даташит на CS43L22 изобилует таблицами с описанием, как получить требуемую частоту дискретизации из заданной MCLK. Однако нам немного задачу упростили — тактированием будет заведовать микроконтроллер.

Рисунок 1: генератор PLLI2S, тактирующий модуль I2S:


Здесь HSE (внешний кварцевый генератор) делится на M и получается HSE/8 = 1 МГц (удобная круглая цифра). Далее 1 МГц умножается на N, делится на R и подаётся на I2S.
PLLI2SCLK = 1(МГц)*N/R;

Рисунок 2: генератор интерфейса I2S:

I2SxCLK — это наша PLLI2SCLK с предыдущего рисунка. Она делится на I2SDIV — получаем Мастерклок. Делим ещё на 4 (32-битные сэмплы) или на 8 (16-битные сэмплы) — и получаем SCLK. И наконец генерируется LRCK (который собственно и является частотой дискретизации) с периодом SCLK/2/16:
LRCK = I2SxCLK/I2SDIV/8/2/16 = PLLI2SCLK/I2SDIV/256;

В итоге:
Fs = HSE/M/N/R/I2SDIV/256 = N/R/I2SDIV/256(МГц). 

Осталось найти такие три цифры, чтобы получить необходимую частоту сэмплирования с минимальной погрешностью. Несмотря на то, что опорная частота 8 МГц некратна частоте 44,1/48 кГц, итоговая погрешность не превышает 0,02-0,1% и на слух незаметна.

Значение I2SDIV рассчитывается в драйвере (см. EVAL_AUDIO_Init) на основании текущего состояния PLLI2S. Поэтому перед инициализацией ЦАП необходимо сделать вот так:
RCC_PLLI2SCmd(DISABLE);
RCC_PLLI2SConfig(PLLI2SN, PLLI2SR);
RCC_PLLI2SCmd(ENABLE);

Готово, можно запускать...

На этой радостной ноте статью завершаю. Приятных прослушиваний!


p.s. а чего же ещё не хватает для полного счастья?
  • достойному аудиоЦАП — достойный звуковой поток! Теперь можно подумать и о включении в состав проигрывателя декодера FLAC;
  • до сих пор не распробован декодер LibMAD и формат OGG; уже можно;
  • обязательно будет реализован декодер XM, вопрос времени;
  • если подключить Ethernet-Internet, можно собрать ВебРадио; но STM32F4DISCOVERY немного неудобна, не хватает экрана и езернета (или лучше сразу WIFI), нужно прикручивать или менять платформу;
  • в качестве идеи: ресурсов контроллера хватит с запасом, чтобы сделать генератор видеосигнала; немного постараться — и можно сделать например сквош по пятому каналу с качественным и интересным звуковым сопровождением.


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

  • +18
  • 17 ноября 2013, 04:04
  • MikeSmith
  • 3

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

RSS свернуть / развернуть
Спасибо за статью.
Команды SSAT и USAT имеются и в cortex-m3, так же не понятно от куда у вас такие сведения «длительность выполнения умножения в обычном Кортексе (3-7 тактов)»?
В cortex-m3: умножение 32=32*32 — 1 такт, умножение с сложением 32=32+32*32 — 2 такта,
а вот уже 64=32*32 — от 3 до 5 и соотв. 64=64+32*32 — от 4 до 7.
0
  • avatar
  • ZiB
  • 17 ноября 2013, 07:24
Слева М4, справа М3.
0
Ответное спасибо. Верно, исправлюсь. Про такты я говорил касаемо SMULL.
0
Последующее аккуратное вскрытие покажет, что...
… причиной смерти было вскрытие. ;-)
0
… автор, эту статью можно и в конкурс добавить
0
Оставляем голос, если нравится :-). Интересно знать количество местных жителей, заинтересованных этой темой. Но в любом случае проект будет вне конкурса.
0
Я в стороне от этой темы, но вы занимательно рассказываете. :-)
0
Спасибо за статью. И ждем продолжения цикла.
+1
Прикручивал тот же самый декодер к аналогичной платформе. За основу брал проект ub_stm32f4_cs43l22_mp3_v101. Кодек отказывался играть мр3 у которых FirstFrame находился далеко от начала. Причина была в алгоритме. Надеюсь у Вас этой проблемы нет. Статья отличная и прекрасно написана. Спасибо автору.
0
Офигенно!
Спасибо.
0
  • avatar
  • ACE
  • 17 ноября 2013, 14:28
Круто.

но STM32F4DISCOVERY немного неудобна, не хватает экрана и езернета (или лучше сразу WIFI)
Для F4 есть базовая плата с эзернетом: STM32F4DIS-BB. Но стоит она непомерных денег — раза в два дороже самой дискавери.
0
Ничего нового за исключением довольно слышимых помех с частотой следования фреймов. Похоже на помеху от отладочного светодиода. Надо будет разобраться на досуге.
У меня наблюдается то же самое. Удалось ли как-то решить данную проблему?
0
данным вопросом ещё не занимался, но думаю, это не должно быть серьёзной проблемой. На STM32F105 помехи не было. ПО какое? Из статьи?
0
ПО своё — принцип тот же — вывод декодированного хеликсом буфера во встроенный ЦАП через DMA.
0
Спасибо за статью. Из инструментария обычно использую Keil или CooCox, реже IAR, но тут глядя на код решил его испробовать с Eclipse+Yagarto (держатель карточки уже прикрутил:). Последний раз тулчейн собирал лет 10 назад. В этот раз не менее занимательно оказалось — переставил подряд 3.7, 4.2, 4.3(+CodeSourcery) — ни одна не смогла нормально всосать проект — файлы брались, а свойства проекта разваливались, и ни в одной не удалось найти в Settings поле для ввода «Script (-T)»… Вписывал всё вручную. Сегодня собрал в Em::Blocks 1.42. И тут уже решил поиграться с DEBUG_MODE. При DEBUG_MODE==(2) не влезло в ОЗУ (66180). Прошу подсказать ключики компилятора/линкера, а то не уверен, что из Вашего проекта правильно списал. Лучше послать где можно научиться импортировать проект в Eclipse без потерь.
0
У меня тоже при DEBUG_MODE=2 не влезает. В этом режиме используется дополнительный буфер для чтения файлов 64кб, а вся свободная память RAM впоследствии была зарезервирована для кучи CoOS (см. OsConfig.h, параметр KHEAP_SIZE). Следует либо уменьшить KHEAP_SIZE, либо для указанного буфера выделить память из кучи. Проект доработал (v5.1), должен откомпилиться.
Проблем с импортом проекта from-Eclipse-to-Eclipse ещё не встречал. Могу предложить свой рабочий комплект Eclipse_Juno+gcc_arm_4_7+OpenOCD+etc, инсталляции не требует. Архив ~600Мб.
0
Спасибо, полегчало. Только там U32 KernelHeap[KHEAP_SIZE], потому поставил сколько не ругалось — (13*1024). Архив бы затянул. Предполагаю возможно как-то через SkyDrive
0
Залил на dropbox:
— Eclipse Kepler + Java v7,
— ARM GCC 4.8 + WinAVR-20100110,
— полезные плагины и утилиты (см. "состав_IDE.txt").
+1
Спасибо, затянул. Чуть позже буду разбираться. Запустил пока под EmBlocks.
Тест пилы не хотел работать — в DAC_Task() пришлось под if (flag) добавить flag = 0;
Тест карточки совсем не хотел работать — карточку не глядя припаял как на картинке выше:) Оказалось, что так не нужно было, а как на картинке похоже конфликтует с I2S ЦАПом.
Плеер заработал только при -O0, ну и запустился не сразу — в MP3Task() после mp3DecoderState->DAC_interface->SendData((uint32_t *)outbuf, len); добавил __ISB(); (#include <core_cmInstr.h>) и только тогда смогло. Туда и задержку на пару команд ставил — тоже шамански срабатывает — что за чудесатость не знаю.
Думаю, что ему подкрутить, чтобы с оптимизацией получилось.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.