Главная > AVR > AVR: Воспроизведение PCM-аудио с SD-карты

AVR: Воспроизведение PCM-аудио с SD-карты

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

Для начала, наверное, стоит рассказать о том, как вообще выводить звук с микроконтроллера. Тема эта, в общем, рассмотрена достаточно и много где, но я все равно пробегусь по ней, чисто для порядка. А то статья окончательно выродится в справку по двум программным модулям.

Вывод звука — суть цифро-аналоговое преобразование. Осуществить оное можно разными способами —  поставить R-2R матрицу, взять готовую микросхему ЦАП, использовать ШИМ-вывод. Нам интересен последний, ибо он позволяет обойтись наименьшим количеством внешних компонентов, а заодно и создает некоторые предпосылки для получения выского КПД усилителя ЗЧ (если цель — подключение динамика, а не вывод на линейный выход, например).

Про ШИМ (в частности, применительно к AVR) я уже писал. Вкратце, замечателен он тем, что среднеквадратичное значение такого сигнала пропорционально его коэффициенту заполнения. Соответственно, если с частотой выборок интересующего оцифрованного сигнала устанавливать значения коэффициента заполнения, пропорциональные собственно значениям выборок, среднеквадратичное значение ШИМ-сигнала в зависимости от времени будет представлять собой воспроизводимый сигнал. Останется только отфильтровать несущую частоту ШИМ (и заодно частоту дискретизации), и на выходе получим аналоговый сигнал из цифрового. В случае с динамиком можно вообще ничего не фильтровать — инерция механики сделает все сама. Тогда усилитель может работать в ключевом режиме и, соответственно, иметь высокий КПД.

Базовые соотношения:

  1. Теорема Котельникова-Найквиста: частота дискретизации должна быть как минимум в два раза выше максимальной частоты воспроизводимого сигнала.
  2. Эмпирическое соотношение: частота ШИМ-несущей должна быть как минимум в два раза выше частоты дискретизации.

Железо

Как уже сказано выше, прелесть выбранного способа в том, что из внешних компонентов требуется всего ничего — встроенный блок генерации ШИМ есть почти во всех контроллерах. Если хотим линейный выход, ставим RC-фильтр с частотой среза, определяемой теоремой Котельникова-Найквиста. Лучше бы, конечно, поставить фильтр второго порядка, но в смысле прослушивания звука сойдет и простая RC-цепь. Если хотим мощный выход на динамик — ставим ключевой каскад. Мне по ТЗ нужен был именно динамик, так что я поставил ключевой каскад (схема в лучшем качестве есть в файлах проекта):

sch

Схема спроектирована с учетом перспектив сборки в разных конфигурациях. Без изменения печатной платы возможно изготовить следующие варианты:

1. Основную схему, которая ушла заказчику.

  • не установлены: C2, DA1, Q5, Q6;
  • установлена перемычка R2;
  • R5, R6, R7 заменены перемычками, сигнал снимается с выхода PWMA.

В такой конфигурации она питается от батареи напряжением 3В (требование SD-карты) и теоретически  способна выдавать в нагрузку мощность где-то до ватта. Реально же измеренный средний ток в рабочем режиме составляет 30 мА (нагрузка 16 Ом).

2. При желании, схему можно умощнить:

  • установить Q5 и Q6, R5/R6/R7 указанных номиналов, снимать сигнал с PWMB;
  • поднять напряжение питания, убрать перемычку R2 и установить стабилизатор и C2. В этом случае предельная мощность в динамике будет определяться исключительно нагрузочной способностью выходных транзисторов и может составлять десятки ватт. Возможно, потребуется подобрать R6 для обеспечения оптимального режима работы выходного каскада (этот резистор задает защитный интервал, без него каскад при серьезной нагрузке испустит дым).

Очень рекомендуется соединить верхний по схеме вывод R3 с землей через резистор сопротивлением где-то 10K. Это убережет Q2 от работы в режиме с оторванной базой в то время, пока контроллер еще не настроил ножку OC0B на выход. Виноват, изначально проглядел это, проект делался в спешке. В принципе, работать будет и так, но лучше подстраховаться.

Динамик подключается через конденсатор емкостью около 22 мкФ, монтируемый навесом. Этого вполне хватает.

К выводу RDSW оригинально подключался геркон. Когда контакты RDSW замкнуты, схема отключена.

Распиновка разъема SD-карты не приведена на схеме, так как определяется программно — в этом проекте я использовал свой драйвер для SD-карты в исходном виде, с программным SPI. Да, в ATmega48 есть аппаратный SPI, но программный удобнее и его скорости вполне хватает, так что я использовал его.

Плата оттрассировалась на одном дыхании. Уложился в одну сторону:

pcb

Софт

Итак, мы собираемся играть PCM-звук с SD-карты. Начнем с первой части, про звук.

Воспроизведение звука

Для воспроизведения звука был написан отдельный модуль (pcm_out.c + pcm_out.h), реализующий настройку двух таймеров (один для генерации ШИМ, другой для отсчета временных интервалов дискретизации), вывод восьмибитных выборок в прерывании, а также двойную буферизацию. Извне модуля доступны две функции и две переменные:

//Enables audio output
#define PCM_ENABLED			0x01
//Gets set when entire buffer was played
#define PCM_BUFFER_ENDED		0x02
//Istructs system to flip buffers when curent buffer ends
#define PCM_ENABLE_AUTOFLIP		0x04
//Gets set when 3/4 of buffer was played
#define PCM_BUFFER_ENDS			0x08
extern volatile uint8_t PCM_status;

//Buffer to write PCM data to
extern uint8_t volatile * volatile PCM_write_buffer;

//Initialize PCM module
void PCM_Init(void);
//Flip internal buffer with the new one
void PCM_FlipBuffers(void);

PCM_Init(), как нетрудно догадаться, инициализирует все, относящееся к выводу звука. Далее работа с модулем выглядит так:

  • чтобы запустить воспроизведение буфера, устанавливаем в PCM_status бит PCM_ENABLED;
  • пока воспроизводится теневой буфер, пишем новые данные в PCM_write_buffer; когда буфер кончится, в PCM_status будет установлен бит PCM_BUFFER_ENDED;
  • тут мы можем вызвать PCM_FlipBuffers(). Эта функция меняет буфера местами, и мы снова можем писать в PCM_write_buffer, уже новый, пока воспроизводится тот, в который мы писали в предыдущий раз;
  • tсли взвести PCM_ENABLE_AUTOFLIP, то после окончания теневого буфера он будет автоматически заменен на новый, но только один раз, после чего этот флаг будет сброшен.

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

//16 KHz @ 8 MHz main clock
#define PCM_SAMPLE_PERIOD		0x01F4
//8 msec of audio at 16 KHz sample rate
#define PCM_BUFSIZE			128

На всякий случай, формат воспроизведения, разумеется, unsigned 8-bit PCM.

Со звуком все.

Доступ к SD-карточке.

Вот с этого момента начинатся самое интересное. По крайней мере, для меня.

Хочется читать данные с карточки. Естесственно, хочется, чтобы карточка сохраняла совместимость со стандартной файловой системой. При этом разбирать FAT — не вариант, так как памяти у нас после инициализации звука остается всего чуть больше двухсот байт.

Эту проблему я решил следующим образом: на карточку кладется файл, по сути своей представляющий упрощенную файловую систему (я назвал ее minifs). В начале оного находится сигнатура, по которой контроллер может найти его при посекторном чтении карточки. Если поместить файл на только что отформатированную карточку, проблем с фрагментацией не возникнет — он будет представлять собой один цельный блок данных нужного формата. Очень удобно — находим начальное смещение по сигнатуре и читаем подряд, обходя FAT, но не конфликтуя с ним.

Таким образом, у нас имеется отдельная файловая система для МК, которая видится на компьютере как файл и, таким образом, легкодоступна для редактирования.

Сам файл minifs представляет собой склеенные блоки данных — «внутренние» файлы — и имеет формат, описанный ниже (все смещения, разумеется, выровнены по границе 512 байт, физическому размеру сектора карточки).

В самом начале файла (смещение 0х00000000) расположена сигнатура — строка «MINIFS v1.0 FILE STARTS HERE»:

0x4D 0x49 0x4E 0x49 0x46 0x53 0x20 0x76 0x31 0x2E 0x30 0x20 0x46 0x49 0x4C 0x45
0x20 0x53 0x54 0x41 0x52 0x54 0x53 0x20 0x48 0x45 0x52 0x45 0x00 0x00 0x00 0x00

Выше показана сигнатура в шестнадцатеричном виде с учетом выравнивания по границе 16 байт. Последующие 480 байт сектора (до границы следующего блока в 512 байт) зарезервированы.

Далее (со смещения 0х00000200) расположена таблица смещений файлов внутри minifs. Она состоит из четырехбайтных записей. Первая запись содержит количество файлов в minifs, последующие указывают на смещения концов файлов. Т.е., вторая запись указывает, где начинается второй файл / кончается первый, и т.п. Все смещения выровнены по границе 512 байт. Максимальный размер таблицы — 512 байт.

Дальше (со смещения 0х00000400) начинаются непосредственно данные — первый файл в minifs.

Кстати, формат minifs, как легко видеть, очень напоминает FAT. Только очень упрощенный.🙂

Для создания minifs была написана программа mkmfs. На вход ей в параметрах командной строки передаются файлы, которые надо спрессовать в minifs. Смысл работы mkmfs сводится к их чтению и последовательному копированию в файл minifs.bin с рассчетом описанных выше смещений и занесением оных в описанную выше таблицу. В процессе работы создается лог. Поскольку доступ к файлам внутри minifs производится по их номеру, оный полезен для сопоставления имен файлов с их номерами.

С другой стороны, для контроллера написан модуль (minifs.c + minifs.h), инкапсулирующий функции работы с описанной структурой данных. Модуль опирается на драйвер SD-карты. Он содержит четыре функции:

//Searches for a minifs file, if found, returns 1, 0 otherwise.
uint8_t MINIFS_SearchFS(void);
//Get a start offset of a file specified by its number
uint32_t MINIFS_FileStartOffset(uint8_t file_number);
//Get an end offset of a file specified by its number
uint32_t MINIFS_FileEndOffset(uint8_t file_number);
//Returns a number of files stored in minifs
uint32_t MINIFS_FileCount(void);

MINIFS_SearchFS() необходимо вызвать после инициализации SD-карты, но перед работой с minifs. Эта функция ищет рассмотренный выше файл, запоминает его начальное смещение (minifs_root в исходниках), необходимое для всех дальнейших вычислений, и загружает таблицу смещений. Если ничего не найдено, возвращает ноль.

MINIFS_FileStartOffset()/MINIFS_FileEndOffset() — возвращают, соответственно, смещение начала и конца блока данных с указанным порядковым номером внутри minifs. Слово «file» в названии всех функций модуля относится именно к блоку данных внутри того, что считает файлом FAT и вместе с ней компьютер (minifs.bin). Т.е., это те файлы, которые мы передавали mkmfs.

MINIFS_FileCount() — возвращает количество блоков данных в minifs. По сути, просто читает первый элемент таблицы смещений.

Из доступных настроек, которые имеет смысл редатировать, имеются следующие:

//Minifs file offset table address, IN FOUR-BYTE WORDS!!!
//So, 32 words results in 128 bytes array.
#define MINIFS_TABLE_SIZE	32
//Starting and ending bounds for a minifs search. In bytes.
//START ADDRESS MUST BE ALIGNED TO 512 BYTES
#define MINIFS_SEARCH_START	0x0003E800UL
#define MINIFS_SEARCH_STOP	0x00FFFFFFUL

MINIFS_TABLE_SIZE — размер таблицы смещений, т.е., максимальное количество блоков данных внутри файла, которое будет доступно контроллеру. Всего под таблицу отводится 512 байт, но в условиях ограниченной памяти размер можно ограничить, что я здесь и сделал.

MINIFS_SEARCH_START/MINIFS_SEARCH_STOP — стартовый и конечный адреса для поиска сигнатуры файла-minifs. Смысл в том, что искать с самого начала карточки, конечно, надежно, но долго (пара секунд для 2 GB карточки при частоте МК 8 МГц) и избыточно, если принять во внимание, что большой объем младших адресов там занят FAT’ом и другими секторами со служебными данными, в которых искомого файла точно не будет. Задавая стартовый адрес отличным от нуля, можно исключить из поиска служебные сектора и тем самым сильно ускорить процесс.

Стоповый адрес нужен, чтобы функция поиска не вылетела за пределы карточки. Ну и опять же, для ограничения максимального времени поиска.

Как все это работает вместе.

Ниже приведен основной код программы, использующей описанные выше модули, с комментариями.

//Включаем основные заголовочники, все как обычно.
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/eeprom.h>
#include <stdint.h>
#include <util/delay.h>

//Включаем самописные модули для работы с карточкой, minifs, выводом звука
#include "pcm_out.h"
#include "sdcard_api.h"
#include "minifs.h"

//Переменная, хранящая последнюю воспроизведенную запись
uint8_t prev_file EEMEM;

void main(void)
{
//Адрес чтения
	volatile uint32_t read_offset;
//ID записи, которую собираемся играть
//опять же, слово file относится к тому блоку, что внутри minifs.bin
	uint8_t file_id=0;

//Инициализируем звук
	PCM_Init();
//Подключаем SD-карту
//проверку можно не делать, ибо если с этим беда, вызов MINIFS_SearchFS()
//полюбому завершится с ошибкой
	SDCARD_Init();

//Ищем, есть ли на карточке minifs.bin с данными
	if (!(MINIFS_SearchFS()))
	{
		//Если нету - повисаем
		while (1);
	}

	sei();

	//Выбираем следующую запись

	if (MINIFS_FileCount() > 1)
	{
		file_id=eeprom_read_byte(&prev_file) + 1;

		if (file_id >= MINIFS_FileCount())
		{
			file_id=0;
		}

//и запоминаем на будущее, чего уже воспроизводили
		eeprom_write_byte(&prev_file,file_id);
	}

//добываем смещение начала интересующей записи
	read_offset=MINIFS_FileStartOffset(file_id);

//включаем вывод звука
	PCM_status|=PCM_ENABLED;

	while (1)
	{
//читаем блок данных по смещению в аудиобуфер
		SDCARD_ReadBlock((uint8_t *)PCM_write_buffer,read_offset);

//продвигаемся к следующему блоку данных
		read_offset+=PCM_BUFSIZE;

//если запись кончилась
		if (read_offset>=MINIFS_FileEndOffset(file_id))
		{
			//то выключаем звук и повисаем
			PCM_status&=~PCM_ENABLED;
			while (1);
		}

//ждем, пока кончится вопроизведение текущего буфера
		while (!(PCM_status & PCM_BUFFER_ENDED));

//меняем буфера местами
		PCM_FlipBuffers();
	}
}

Особые соображения

Размеры всех буферов сознательно выбраны по 128 байт. Это оптимальный размер, который одновременно укладывается в память используемого МК и позволяет корректно читать с SD-карты.

В статье понятие «файл» исползуется двояко — во-первых, как собственно файл на карточке, а, во-вторых, как блок данных внутри файла minifs.bin, представляющий собой один из файлов, до того обработанных с помощью mkmfs.

Если у контроллера есть необходимость что-то писать на SD-карту, можно оставить в minifs пустой блок необходимого размера и писать туда через SDCARD_WriteBlock(…). Разумеется, с учетом ограничений на адресацию (см. статью про драйвер SD-карты).

Тесты и заключение.

Тестировалось все следующим образом: брались WAV-файлы в формате 16кГц/8 бит, моно. У них отрезались заголовки, после чего получившиеся RAW-файлы передавались программе mkmfs, которая прессовала их в minifs.bin. Оный заливался на SD-карточку размером 2 Гб. Последняя подключалась к контроллеру с прошивкой, код которой приведен выше. Таким образом, при каждом включении воспроизводился очередной файл из minifs.bin.

Скачать проект можно тут. Схемы/разводки в DipTrace, проект для AVR Studio 4 / AVR-GCC. mkmfs — консольное приложение, написанное в Code::Blocks / MinGW.

Рубрики:AVR
  1. Alex
    16/09/2014 в 22:59

    Году в 2006 делал похожий проект для рекламного стенда. Контроллер mega16, писал на асме, буфер 512 байт, поделенный на два полубуфера для упреждающего чтения, поддержка fat12, fat16, fat32. Используется аппаратный SPI, тайминги для карты настраиваются в соответствии с CSR. Кроме того реализован парсер плей-листа, LCD экран и кнопочки, софтверная регулировка громкости. Рекламные сообщения — .wav файлы, плейлист по структуре похож на линуксовые .ini файлы. Делал бенч — нормально успевает до частоты дискретизации 65кГц. Вся эта красота — что-то около 5кбайт в скомпиленном виде. Недавно наткнулся на исходники — столько нелепых конструкций в коде. Если б сейчас писал еще эффективней получилось бы. Так что не надо бояться fat-ов и прочего, надо брать и делать — всё получится.

    • YS
      17/09/2014 в 21:24

      А как поддержку FAT реализовывали? Я читал описание FAT и вообще на эту тему. Если делать честную поддержку, то это задача очень нетривиальная. Даже определить, какая ФС на носителе — уже проблема. Да, там есть специальное поле, но оно не всегда верно, и т.д.

  2. 27/12/2013 в 23:18

    А мож поменять контроллер на пожирнее и реализовать FAT ?
    Вообще (если считать что памяти достаточно) mega потянет чтение FAT флэшки и одновременно формирование звука?
    Чтото уж большо заморочено у тебя (после прочтения 1 раз)… может после 5-ти раз что-то прояснится, но напрягатся сегодня не хочется :)…..

    • YS
      28/12/2013 в 01:10

      Зачем? Все и так работает.🙂

      ChaN вроде бы делал WAV-плеер на AVR и FatFS. Искать лень.🙂

      Каюсь, статью делал по-быстрому. Оттого и читается тяжеловато. Но у меня тут сессия, так что извиняйте.🙂

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

  1. No trackbacks yet.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s