Top.Mail.Ru

Аудио-плеер на STM32. Воспроизведение WAV-файла.

Всех приветствую, и сегодня мы займемся воспроизведением музыки и сделаем свой собственный мини-аудио-плеер. Конечно же, на базе контроллера STM32, как же без него. Итак, воспроизводить будем файл формата WAV, соответственно нам понадобятся:

  • Карта памяти для хранения аудио-файлов. Я буду использовать MicroSD, подключенную к микроконтроллеру по интерфейсу SDIO. Кроме того, для организации работы с файловой системой нужно добавить в проект поддержку FatFs.
  • Динамик и аудио-усилитель, в качестве которого возьмем микросхему LM386. А сами данные с контроллера будем выдавать при помощи цифро-аналогового преобразователя (DAC).
  • И из периферии нам еще понадобится таймер, но об этом чуть позже.

Схема подключения.

В итоге структурная схема нашего устройства будет выглядеть так:

STM32 wav, структурная схема

Я сегодня буду использовать STM32F103VE, но это не особо важно, для другого контроллера последовательность действий, да и код, будут практически идентичны. LM386 подключаем по стандартной схеме:

Подключение LM386

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

Разъем для MicroSD на моей плате подключен так:

Подключение SD карты

SDIO для карты памяти используем в 4-х битном режиме. Питание контроллера, внешний кварцевый резонатор, SWD разъем для программирования - здесь все стандартно.

Так, со схемой закончили, давайте обсудим, как мы будем получать нужные аудио-данные из WAV-файла. Структура файла довольно проста - после заголовка сразу же следуют непосредственно цифровые данные, которые мы можем преобразовывать в аналоговый сигнал при помощи ЦАП и выдавать на динамик.

Можно использовать два варианта представления этих данных - 8-битное и 16-битное. Вспоминаем, что ЦАП у нас 12-ти битный, а значит при использовании 8-битного формата мы не задействуем его возможности в полной мере. Поэтому лучше возьмем WAV-файл с 16-ти битным представлением данных, а в программе масштабируем эти данные до 12-ти бит. Итоговое качество звука при этом будет выше.

В WAV-файле данные хранятся со знаком, то есть для 16 бит мы получаем диапазон значений от -32767 - до 32768. На ЦАП нам необходимо выдавать значения от 0 до 4095 (12 бит). Поэтому сначала преобразуем исходные данные в беззнаковые, добавив 32767:

DAC_{temp} = DAC_{raw} + 32767 

Полученные числа (0 - 65535) надо преобразовать в значения от 0 до 4095:

DAC_{final} = DAC_{temp} \medspace \frac{4095}{65535} \approx \frac{DAC_{temp}}{16}

Заголовок WAV-файла может содержать разное количество байт, в зависимости от хранимой в нем дополнительной информации (исполнитель, название и т. д.). Чуть позже, создавая практический пример, разберемся, как определить конец заголовка и, соответственно, начало аудио-данных.

С преобразованием в нужный нам вид разобрались, но открытым остается вопрос, как часто нужно будет изменять сигнал на выходе DAC, с какой периодичностью. И тут все зависит от частоты дискретизации аудио-файла. Будем использовать частоту 16 КГц. Это значит, что именно с такой частотой надо обновлять данные на выходе ЦАП. Рассчитаем период, соответствующий частоте 16 КГц.

T = \frac{1 \medspace с}{16000} = 62.5 \medspace мкс

Получается, что нам нужен таймер, который будет генерировать прерывания каждые 62.5 мкс. И в прерывании мы будем выдавать на модуль DAC новое значение.

Таким образом, при конвертировании, к примеру, mp3 в wav, необходимо выбирать именно эти параметры - 16 бит данных + частота дискретизации 16 КГц.

Создание проекта.

Итак, все готово, переходим к созданию проекта в STM32CubeMx. Первым делом активируем DAC, второй канал, то есть вывод PA5. Настроек минимум:

STM32CubeMx DAC

Сразу же настраиваем таймер и его прерывание:

STM32CubeMx Timer

Частота тактирования TIM6 у меня 72 МГц. Предделитель 180 (179 + 1) дает нам итоговую частоту таймера равную:

f_{timer} = \frac{72 \medspace МГц}{180} = 400 \medspace КГц

То есть один "тик" таймера - 2.5 мкс (такой период соответствует частоте 400 КГц). Значит период таймера в отсчетах должен быть равен:

\frac{62.5 \medspace мкс}{2.5 \medspace мкс} = 25

Это мы и задали в его настройках. Осталось добавить поддержку SD-карты по SDIO и драйвер FatFs:

STM32CubeMx SDIO
STM32CubeMx FatFs

В новых версиях STM32CubeMx в конфигурации FatFs появилась вкладка Platform Settings. Здесь можно настроить какой-либо из входов контроллера для использования в качестве сигнала, по уровню которого можно отследить подключение и извлечение карты памяти:

FatFs Platform Settings

В сегодняшнем проекте не будем использовать этот функционал, что приведет к тому, что CubeMx будет выдавать предупреждение:

STM32CubeMx warning

Игнорируем его и переходим к генерации кода.

Программная реализация.

Все приготовления позади, нам остается только написать код. Начинаем двигаться поэтапно, монтируем файловую систему и открываем аудио-файл. Пусть файл, который мы предварительно закинули на карту памяти, называется audio.wav. Помещаем прямо в функцию main() до цикла while(1):

/* USER CODE BEGIN 2 */
res = f_mount(&fileSystem, SDPath, 1);

uint8_t path[10] = "audio.wav";
res = f_open(&audioFile, (char*)path, FA_READ);

Чтобы не нагромождать пример, не будем проверять результат res выполнения функций FatFs, в "боевом" проекте лучше, конечно, этим не пренебрегать и отслеживать все возможные ошибки.

Следующим шагом нужно определить длину заголовка WAV-файла. Для этого воспользуемся простым механизмом - считываем 512 байт (WAV_BUF_SIZE) из файла и проверяем последовательно полученные байты. Особенностью заголовка является то, что в его конце всегда расположены символы 'data', после чего следует размер данных (4 байта). И вот после этого уже начинаются именно аудио-данные.

Таким образом, находим позицию в буфере, которая соответствует символу 'd' и прибавляем к этому значению 8 (4 байта для 'data' и 4 байта для размера данных):

uint16_t dataOffset = 0;

res = f_read(&audioFile, wavBuf[0], WAV_BUF_SIZE, &readBytes);

for (uint16_t i = 0; i < (WAV_BUF_SIZE - 3); i++)
{
	if ((wavBuf[0][i] == 'd') && (wavBuf[0][i + 1] == 'a') &&
		(wavBuf[0][i + 2] == 't') && (wavBuf[0][i + 3] == 'a'))
	{
		dataOffset = i + 8;
		break;
	}
}

Заголовок обнаружен, перемещаем указатель FatFs для работы с файлом на аудио-данные и заодно определяем количество байт данных. Для этого вычитаем из общего размера файла размер заголовка:

res = f_lseek(&audioFile, dataOffset);
wavDataSize = f_size(&audioFile) - dataOffset;

И теперь необходимо остановиться на еще одном нюансе... Нам нужно обновлять данные на выходе DAC каждые 62.5 мкс, то есть относительно часто. Чтобы не было задержек, связанных с временем, которое требуется для того, чтобы прочитать очередные 16 бит из wav-файла, нужно организовать буферизацию аудио-данных.

Идея здесь заключается в следующем. Объявим два буфера размером 512 байт, в которые поочередно будем считывать новые данные из файла с карты памяти. Алгоритм будет такой:

  • Сначала заполняем оба буфера данными и начинаем воспроизведение (то есть вывод данных на ЦАП) из буфера 1.
  • Как только буфер 1 подошел к концу (512 его байт выданы на ЦАП), переходим к выводу данных из буфера 2. И параллельно заполняем буфер 1 новыми данными с карты.
  • Когда мы использовали данные буфера 2, переходим снова к буферу 1, а в буфер 2 считываем новую порцию.

То есть переключаемся между этими буферами, и во время активности одного из них заполняем неактивный буфер новыми данными. Реализуем этот механизм и первым делом заполняем оба буфера данными:

  res = f_read(&audioFile, wavBuf[0], WAV_BUF_SIZE, &readBytes);
  res = f_read(&audioFile, wavBuf[1], WAV_BUF_SIZE, &readBytes);

Поскольку данные готовы, спокойно включаем DAC и TIM6 на генерацию прерываний:

  HAL_DAC_Start(&hdac, DAC_CHANNEL_2);
  HAL_TIM_Base_Start_IT(&htim6);

Размер буфера задан в main.h:

/* USER CODE BEGIN Private defines */
#define WAV_BUF_SIZE                                                    512

/* USER CODE END Private defines */

В основном цикле программы занимаемся обновлением данных, то есть чтением их с карты памяти:

if (wavReadFlag == 1)
{
	uint8_t readBufIdx = 0;
	
	if (curBufIdx == 0)
	{
		readBufIdx = 1;
	}

	res = f_read(&audioFile, wavBuf[readBufIdx], WAV_BUF_SIZE, &readBytes);
	wavReadFlag = 0;
}

В переменную wavReadFlag в прерывании по таймеру будем записывать единицу в том случае, если буфер подошел к концу и его данные можно обновлять. Счетчик curBufIdx содержит текущий номер активного буфера, то есть того, из которого сейчас идет считывание данных для DAC. Соответственно, это либо значение 0 - для первого буфера, либо 1 - для второго.

Поскольку буфер с индексом curBufIdx активен, то считывать данные нам напротив нужно в неактивный, номер которого помещаем в readBufIdx. После этого обнуляем флаг wavReadFlag.

Кроме того, сразу же в while(1) помещаем код, который отвечает за окончание воспроизведения, когда файл подошел к концу:

if (stopFlag == 1)
{
	res = f_close(&audioFile);
	stopFlag = 0;
}

В main() нам в этом случае требуется только закрыть файл. Аналогично, флагом stopFlag мы управляем из прерывания таймера, к которому и переходим:

void TIM6_IRQHandler(void)
{
	/* USER CODE BEGIN TIM6_IRQn 0 */

	/* USER CODE END TIM6_IRQn 0 */
	HAL_TIM_IRQHandler(&htim6);
	/* USER CODE BEGIN TIM6_IRQn 1 */
	uint16_t dacData = (((wavBuf[curBufIdx][curBufOffset + 1] << 8) | wavBuf[curBufIdx][curBufOffset]) + 32767);
	dacData /= 16;
	HAL_DAC_SetValue(&hdac, DAC_CHANNEL_2, DAC_ALIGN_12B_R, dacData);
	
	curBufOffset += 2;
	curWavIdx += 2;
	
	if (curWavIdx >= wavDataSize)
	{
		HAL_TIM_Base_Stop_IT(&htim6);
		stopFlag = 1;
	}
	else
	{
		if (curBufOffset == WAV_BUF_SIZE)
		{
			curBufOffset = 0;
			
			if (curBufIdx == 0)
			{
				curBufIdx = 1;
			}
			else
			{
				curBufIdx = 0;
			}
		
			wavReadFlag = 1;
		}
	}
	/* USER CODE END TIM6_IRQn 1 */
}

Здесь мы, во-первых, формируем данные для DAC в точности так, как обсудили в начале статьи, и отправляем их на выход. Также увеличиваем счетчики байт curBufOffset и curWavIdx на 2, поскольку одна порция данных у нас равна двум байтам (16 битам):

  • curBufOffset - номер текущего байта в буфере, этот счетчик, соответственно, будет изменяться от 0 до 512.
  • curWavIdx - общий счетчик байт wav-файла. Его мы используем, чтобы обнаружить конец трека и остановить воспроизведение.

В общем-то по окончанию файла отключаем таймер и выставляем флаг stopFlag в 1:

if (curWavIdx >= wavDataSize)
{
	HAL_TIM_Base_Stop_IT(&htim6);
	stopFlag = 1;
}

Если конец файла еще не достигнут, то проверяем счетчик curBufOffset. Если значение равно 512 (WAV_BUF_SIZE), то буфер подошел к концу, а значит надо изменить номер активного буфера и выставить флаг wavReadFlag:

if (curBufOffset == WAV_BUF_SIZE)
{
	curBufOffset = 0;
	
	if (curBufIdx == 0)
	{
		curBufIdx = 1;
	}
	else
	{
		curBufIdx = 0;
	}
	
	wavReadFlag = 1;
}

Вот, в принципе, на этом и все. Собираем проект и прошиваем плату, результатом будет звук из динамика 👍 Конечно, качество не будет таким как в серийно выпускающихся проигрывателях, но, в общем-то, довольно-таки годным.

И на этом на сегодня заканчиваем, любые вопросы можно писать в комментарии, либо на форуме, либо в группе ВК, короче, где угодно )

Ссылка на проект - MT_AudioPlayer.

Подписаться
Уведомить о
guest

28 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Паша
Паша
5 лет назад

Когда то собирал wav плеер на attiny85, попробуем теперь на stm-ке.

Alex
Alex
5 лет назад

Великолепный урок, закрывающий сразу две не малые области - чтение с SD карты и вывод данных через DAC. Их не так то просто связать, а тут все работает "из коробки"... Лично мне сильно помог.

Андрей
Андрей
4 лет назад

Добрый день. В предыдущих версиях CubeMX максимальный объем карты с которым работала файловая система был не более 4ГБ. У Вас на картинке карточка 128ГБ. Неужели Cube доработали и он теперь может создавать проекты для работы с нормальными объемами карточек?

Андрей
Андрей
Ответ на комментарий  Aveal
4 лет назад

Какая жалость, я было обрадовался, что Cube доработали и теперь можно использовать карточки с нормальным объемом.
Оказывается ничего у них не поменялось.

Андрей
Андрей
Ответ на комментарий  Aveal
4 лет назад

Если время позволит, то я попробую в ближайшем будущем. И тогда отпишусь по результатам. Я слышал, что в Cube библиотека FATFS обновлялась, но не знаю в чем именно. В общем, надо ручками пощупать)))

lock
lock
4 лет назад

Вот бы аналогичный проект воспроизведения wav файла с помощью PWM для stm32f103f8, у которого нет на борту DAC (ЦАП)

Oll
Oll
4 лет назад

А чтобы ШИМ использовать на камне где нет ЦАП, что нужно переделать в текущем проекте?
Спасибо!

Oll
Oll
Ответ на комментарий  Aveal
4 лет назад

К сожалению 16 битный (моно) звук не смог вывести через ШИМ, только 8 бит с такой конструкцией: uint16_t dacData = wavBuf[curBufIdx][curBufOffset + 1 ] ;

Вместо HAL_DAC_SetValue(), делал для 16 бит:
 __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, dacData);

Сергей
Сергей
4 лет назад

RAR архив битый, ошибка открытия.

Антон
Антон
3 лет назад

Без преувеличения, этот сайт по stm32 и по другим темам - лучший!
Пробовал оцифровывать звук и тут же конвертировать назад в аналог на stm32f407, скорость позволяла работать на 44100.гц
Интересно, можно ли реализовать беспроводные наушники на nrf24l01, с внешними, более "продвинутыми" АЦП и ЦАП.

Антон
Антон
3 лет назад

Попробовал передачу потокового аудио, вроде получилось, но sample rate максимум удалось сделать 32K. Вопрос: а можно ли передавать по nrf сразу 16 битные переменные? Просто я разделял 12 битные переменные на 2 восьмибитные.

Алекsей
2 лет назад

Доброго времени !
А не подскажите, может находили программу которая может "готовить" из wav файлов, файлы для размещения непосредственно в программе микроконтроллера. При выполнении которых контроллер будет выводить звук "говорилки".

По идее, там ничего сложного. Надо считать данные (по одному байту) и объединить их в 16 бит (и преобразовать в 12 бит) )

OWA
OWA
7 месяцев назад

У меня проц F103R, там ЦАПа нет.
Сделал плеер для WAV, используя как ЦАП ШИМ-выход одного из таймеров.
Ограничения: невысокая частота квантования, всего 22050, чтобы диапазон для ШИМ был не менее 11 разрядов.
Музыку играет, и довольно приятно на слух! Из WAV считываю выборки через раз, чтобы проглотить WAV с частотой 44100. Зато - у таймера 4 PWM-выхода, можно недорого замутить квадро-стерео! :)))
16-бит данные из WAV делю на 32, т.е. оставляю свои 11 разрядов.
Громкость регулирую программно, изменением делителя для данных. При совсем малой громкости коэфф деления уже 320 (10% громкости), на слух заметны артефакты слабой дискретизации.
Выход ШИМ надо снабдить мощным фильтром, подавляющим несущую 22кГц.

OWA
OWA
Ответ на комментарий  Aveal
7 месяцев назад

На слух, снижение до 30-50% (т.е. до 9-10 эффективных разрядов) никак совершенно не чувствуется. Но меломаны наверное придерутся...
Шумов как таковых не заметил.
Вот что заметно - для снижения частоты с 44100 до 22050 надо соседние выборки из вава усреднять, не просто пропускать.
Но ШИМ-ЦАП это был совсем колхозный вариант. Я уже получил MCP4922, честный 12бит ЦАП, вот буду прикручивать. 🙂
Чтобы сделать 12-бит полноценный ЦАП из ШИМ, надо тактовую на таймер 48000*4096=196МГц, а это не F103

Евген
11 дней назад

Здравствуйте, спасибо большое за статью! Я хочу сделать музыкальный сэмплер на основе этого, но не могу понять, как мне организовать параллельное проигрывание файлов =(. Суть такова - 6 кнопок, 6 .wav файлов, по кнопке надо воспроизвести соответствующий файл. Кнопка, отвечающая за 2.wav будет нажата ещё до того, как будет закончено воспроизведение 1.wav (к примеру). Надо не останавливая проигрывание первого начать проигрывать второй файл, смешав всё что "внахлёст"(переполнения буфера не боимся, конец файла довольно тихий). И ещё от нажатия кнопки до начала проигрывания должно пройти не более 6мс. Только начал вкатываться в микроконтроллеры, не могу понять как мне это сделать, подскажите, пожалуйста Т_Т. Я ещё в группу в вк написал, там можно со мной связаться

Евген
Ответ на комментарий  Aveal
11 дней назад

Если по второй кнопке останавливать проигрывание и чтение и сразу новый файл начинать, то проблема с параллельным чтением и микшированием вроде как уходит, но я боюсь будут щелчки либо задержки, плюс хотелось бы накладывать звуки, дабы красиво на слух было =)

28
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x
Обзор конфиденциальности

На этом сайте используются файлы cookie, что позволяет нам обеспечить наилучшее качество обслуживания пользователей. Информация о файлах cookie хранится в вашем браузере и выполняет такие функции, как распознавание вас при возвращении на наш сайт и помощь нашей команде в понимании того, какие разделы сайта вы считаете наиболее интересными и полезными.