Аудио-плеер на 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;
}

Вот, в принципе, на этом и все…

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

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

Поделиться!

Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments

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

Profile Profile Profile Profile Profile
Vkontakte
Twitter

Язык сайта

Октябрь 2020
Пн Вт Ср Чт Пт Сб Вс
 1234
567891011
12131415161718
19202122232425
262728293031  

© 2013-2020 MicroTechnics.ru