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

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

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

Alex
Alex
3 лет назад

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

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

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

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

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

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

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

lock
lock
3 лет назад

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

Oll
Oll
3 лет назад

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

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

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

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

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

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

Антон
Антон
1 год назад

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

Антон
Антон
1 год назад

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

Алекsей
7 месяцев назад

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

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

21
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x