Аудио-плеер на 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
15 комментариев
старее
новее большинство голосов
Inline Feedbacks
View all comments
Паша
Паша
5 месяцев назад

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

Alex
Alex
3 месяцев назад

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

Last edited 3 месяцев назад by Alex
Андрей
Андрей
3 месяцев назад

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

Андрей
Андрей
Reply to  Aveal
3 месяцев назад

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

Андрей
Андрей
Reply to  Aveal
3 месяцев назад

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

lock
lock
1 месяц назад

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

Oll
Oll
1 месяц назад

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

Oll
Oll
Reply to  Aveal
1 месяц назад

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

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

Сергей
Сергей
1 час назад

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

Присоединяйтесь!

Profile Profile Profile Profile Profile
Vkontakte
Twitter

Язык сайта

Апрель 2021
Пн Вт Ср Чт Пт Сб Вс
 1234
567891011
12131415161718
19202122232425
2627282930  

© 2013-2020 MicroTechnics.ru