STM32 и Audio. Воспроизведение звука на STM32F4Discovery.

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

Одним из преимуществ отладочной платы STM32F4Discovery является наличие аудио-ЦАП со встроенным усилителем. Эту роль выполняет замечательная микросхема CS43L22. Так что на основе Discovery можно сделать кучу разнообразных аудио-девайсов! Мы сегодня, для начала, просто разберемся, как произвести инициализацию и первоначальную настройку всего этого добра, ну и попробуем что-нибудь пропищать 🙂

Микросхема CS43L22 поддерживает несколько интерфейсов для обмена данными с контроллером, и использовать мы их будем совместно. Тут дело в том, что для управления  CS43L22 (то есть для отправки управляющих команд) используется I2C, а для передачи аудио данных – I2S. Микроконтроллер STM32F4 поддерживает и то, и другое. Давайте посмотрим на кусок схемы платы Discovery:

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

Как видно из схемы, для управляющих команд используются выводы PB9 и PB6. А это у нас I2C1. Так что с этим все понятно 🙂 А линии I2S подключены к пинам SPI3 контроллера. Это все нам предстоит настроить, когда будем писать пример.

Помимо инициализации периферии и портов ввода-вывода необходимо произвести начальную конфигурацию CS43L22. Я, пожалуй, не буду останавливаться отдельно на регистрах этой микросхемы – они все описаны в документации на нее, мы лучше уделим больше внимания настройке микроконтроллера.

Создаем проект в Keil’е и приступаем.. Начинаем, как обычно, с подключения необходимых файлов и переменных (будем использовать библиотеку SPL):

/***************************************************************************************/
#include "stm32f4xx.h"
#include "stm32f4xx_gpio.h"
#include "stm32f4xx_rcc.h"
#include "stm32f4xx_i2c.h"
#include "stm32f4xx_spi.h"
#include "stm32f4xx_tim.h"


/***************************************************************************************/
GPIO_InitTypeDef gpio;
I2S_InitTypeDef i2s;
I2C_InitTypeDef i2c;
TIM_TimeBaseInitTypeDef timer;
uint8_t state = 0x00;


/***************************************************************************************/

Ну а теперь надо настроить все, что мы будем использовать. И начнем с портов ввода-вывода GPIO:

/***************************************************************************************/
void initGPIO()
{
	// Включаем тактирование
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOD | RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOC, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1 | RCC_APB1Periph_SPI3, ENABLE);
	
	// У I2S свой отдельный источник тактирования, имеющий повышенную точность, включаем и его
	RCC_PLLI2SCmd(ENABLE);

	// Reset сигнал для CS43L22
	gpio.GPIO_Pin = GPIO_Pin_4;;
	gpio.GPIO_Mode = GPIO_Mode_OUT;
	gpio.GPIO_PuPd = GPIO_PuPd_DOWN;
	gpio.GPIO_OType = GPIO_OType_PP;
	gpio.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOD, &gpio);

	// Выводы I2C1
	gpio.GPIO_Mode = GPIO_Mode_AF;
	gpio.GPIO_OType = GPIO_OType_OD;
	gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_9;
	gpio.GPIO_PuPd = GPIO_PuPd_NOPULL;
	gpio.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &gpio);

	GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_I2C1);
	GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_I2C1);

	// А теперь настраиваем выводы I2S
	gpio.GPIO_OType = GPIO_OType_PP;
	gpio.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_10 | GPIO_Pin_12;
	GPIO_Init(GPIOC, &gpio);

	gpio.GPIO_Pin = GPIO_Pin_4;
	GPIO_Init(GPIOA, &gpio);

	GPIO_PinAFConfig(GPIOA, GPIO_PinSource4, GPIO_AF_SPI3);
	GPIO_PinAFConfig(GPIOC, GPIO_PinSource7, GPIO_AF_SPI3);
	GPIO_PinAFConfig(GPIOC, GPIO_PinSource10, GPIO_AF_SPI3);
	GPIO_PinAFConfig(GPIOC, GPIO_PinSource12, GPIO_AF_SPI3);

	// Сбрасываем Reset в ноль
	GPIO_ResetBits(GPIOD, GPIO_Pin_4);
}



/***************************************************************************************/

Первый шаг позади, продолжаем! На очереди периферийный модуль I2S и его конфигурация:

/***************************************************************************************/
void initI2S()
{
	SPI_I2S_DeInit(SPI3);
	i2s.I2S_AudioFreq = I2S_AudioFreq_48k;
	i2s.I2S_MCLKOutput = I2S_MCLKOutput_Enable;
	i2s.I2S_DataFormat = I2S_DataFormat_16b;
	i2s.I2S_Mode = I2S_Mode_MasterTx;
	i2s.I2S_Standard = I2S_Standard_Phillips;
	i2s.I2S_CPOL = I2S_CPOL_Low;

	I2S_Init(SPI3, &i2s);
}


/***************************************************************************************/

Осталось только произвести настройку I2C:

/***************************************************************************************/
void initI2C()
{
	I2C_DeInit(I2C1);
	i2c.I2C_ClockSpeed = 100000;
	i2c.I2C_Mode = I2C_Mode_I2C;
	i2c.I2C_OwnAddress1 = 0x33;
	i2c.I2C_Ack = I2C_Ack_Enable;
	i2c.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	i2c.I2C_DutyCycle = I2C_DutyCycle_2;

	I2C_Cmd(I2C1, ENABLE);
	I2C_Init(I2C1, &i2c);
}


/***************************************************************************************/

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

/***************************************************************************************/
void writeI2CData(uint8_t bytesToSend[], uint8_t numOfBytesToSend)
{
	uint8_t currentBytesValue = 0;

	// Ждем пока шина освободится
	while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
	
	// Генерируем старт
	I2C_GenerateSTART(I2C1, ENABLE);
	while (!I2C_GetFlagStatus(I2C1, I2C_FLAG_SB));
	
	// Посылаем адрес подчиненному устройству - микросхеме CS43L22
	I2C_Send7bitAddress(I2C1, 0x94, I2C_Direction_Transmitter);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

	// И наконец отправляем наши данные
	while (currentBytesValue < numOfBytesToSend)
	{
		I2C_SendData(I2C1, bytesToSend[currentBytesValue]);
		currentBytesValue++;
		while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));
	}

	while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_BTF));

	I2C_GenerateSTOP(I2C1, ENABLE);
}


/***************************************************************************************/

Нам еще понадобится простенькая функция для формирования задержки, ну и заодно напишем инициализацию CS43L22:

/***************************************************************************************/
void delay(uint32_t delayTime)
{
	uint32_t i = 0;
	for (i = 0; i < delayTime; i++);
}


/***************************************************************************************/
void initCS32L22()
{
	uint8_t sendBuffer[2];

	GPIO_SetBits(GPIOD, GPIO_Pin_4);

	delay(0xFFFF);
	delay(0xFFFF);
	delay(0xFFFF);
	delay(0xFFFF);
	delay(0xFFFF);

	sendBuffer[0] = 0x0D;
	sendBuffer[1] = 0x01;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x00;
	sendBuffer[1] = 0x99;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x47;
	sendBuffer[1] = 0x80;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x32;
	sendBuffer[1] = 0xFF;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x32;
	sendBuffer[1] = 0x7F;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x00;
	sendBuffer[1] = 0x00;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x04;
	sendBuffer[1] = 0xAF;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x0D;
	sendBuffer[1] = 0x70;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x05;
	sendBuffer[1] = 0x81;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x06;
	sendBuffer[1] = 0x07;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x0A;
	sendBuffer[1] = 0x00;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x27;
	sendBuffer[1] = 0x00;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x1A;
	sendBuffer[1] = 0x0A;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x1B;
	sendBuffer[1] = 0x0A;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x1F;
	sendBuffer[1] = 0x0F;
	writeI2CData(sendBuffer, 2);

	sendBuffer[0] = 0x02;
	sendBuffer[1] = 0x9E;
	writeI2CData(sendBuffer, 2);
}


/***************************************************************************************/

Давайте разберемся, как вообще можно добиться воспроизведения звука. Мы хотим услышать какую-нибудь нотку, пусть это будет нота ля первой октавы. Звуковое колебание представляет из себя синусоиду определенной частоты (в данном случае 440 Гц). Как раз такой сигнал нам и надо подать по I2S микросхеме CS32L22.

Мы упростим себе задачу и будем подавать просто прямоугольные импульсы с частотой 440 Гц 🙂 Такой частоте соответствует период 2.272 мс. Настроим таймер на генерацию прерываний каждые 2.27 мс и в прерывании будем менять уровень сигнала. Итак инициализация таймера:

/***************************************************************************************/
void initTimer()
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

	TIM_TimeBaseStructInit(&timer);
	timer.TIM_Prescaler = 7200;
	timer.TIM_Period = 4;
	TIM_TimeBaseInit(TIM2, &timer);
}


/***************************************************************************************/

И в прерывании инвертируем значение переменной state:

/***************************************************************************************/
void TIM2_IRQHandler()
{
	TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	state ^= 0x01;
}


/***************************************************************************************/

Почти все уже готово! Пришло время самого главного – функции main():

/***************************************************************************************/
int main(void)
{
	// Разрешаем прерывания
	__enable_irq();
	
	// Инициализация
	initGPIO();
	initTimer();
	initI2C();
	initI2S();
	initCS32L22();

	// Включаем SPI3
	I2S_Cmd(SPI3, ENABLE);

	// настраиваем прерывание по переполнению таймера
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
	TIM_Cmd(TIM2, ENABLE);
	NVIC_EnableIRQ(TIM2_IRQn);

	while(1)
	{
		// Если флаг выставлен, то можно передавать данные
		if (SPI_I2S_GetFlagStatus(SPI3, SPI_I2S_FLAG_TXE))
		{
			if (state == 0x00)
			{
				// Если переменная state = 0, то посылаем нули
				SPI_I2S_SendData(SPI3, 0x00);
			}
			else
			{
				// А если переменная state != 0, то посылаем максимальное значение,
				// в итоге получаем прямоугольные импульсы
				SPI_I2S_SendData(SPI3, 0xFF);
			}
		}
	}
}


/***************************************************************************************/

Собираем, прошиваем, втыкаем наушники или колонки в специальный разъем на STM32F4Discovery и наслаждаемся 🙂 Если изменить период работы таймера, то изменится частота следования прямоугольных импульсов, а значит и высота звука.

В общем, на этом пока все, но со звуком обязательно еще продолжим в будущих статьях!

Поделиться!

Подписаться
Уведомление о
guest
31 Комментарий
старее
новее большинство голосов
Inline Feedbacks
View all comments
Aleks
Aleks
6 лет назад

Здравствуйте. вы случаем не из Минска? Просто вот на курсы хожу по STM32F4, так мы там не так давно тоже проходили работу со звуком и тут статья ваша

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

Здравствуйте.
Подскажите, пожалуйста, как вы рассчитали значения предделителя и период для таймера. Какая опорная частота таймера?

(Уровень сигнала нужно менять каждый полупериод.)

Егор
Егор
Reply to  Сергей
6 лет назад

у таймера частота 84МГц

Другой Сергей
Другой Сергей
6 лет назад

и каким образом из 84 мегагерц получается прерывание раз в 2,27мс? Я правильно понял, если в основном цикле менять посылаемые значения – будет изменяться громкость?
initCS32L22 выглядит пугающе – откуда берутся все эти значения? что они означают?

Другой Сергей
Другой Сергей
6 лет назад

собралось и заработало с первого раза! ура!
1. не понятно, какимо образом получилась частота меандра 440Гц
2. каким образом оформить функцию для проигрывания ноты определенной частоты и определенной длительности.
3. как это совместить одновременно с рулежкой сервами и опросом кнопки 🙂
4. полифония!?
5. более благородное звучание?

Другой Сергей
Другой Сергей
6 лет назад

допустим, playnote(freq, duration) я написал. частоту изменять в этой самой playnote не получается – предполагаю, что надо отключать таймер или запрещать прерывания на время выполнения строки timer.TIM_Period = freq;
еще откуда ни возьмись появляется через каждую такую ноту белый шум, причем только в одном ухе (делаю после проигрыша ноты delay_ms(1000)
думаю, это потому, что SPI_I2S_SendData(SPI3, 0xff); вылезает на определенных нотах, а на других – SPI_I2S_SendData(SPI3, 0x00);

Alex
Alex
6 лет назад

Почему бы не вынести инициализацию CS43L2
в цикл, а коды занести в массив? Гораздо компактнее и меньше объем кода.

кроссовер мен
5 лет назад

Автор гений! Респект и уважуха! Давно искал схему в сети! Дружище – выручил…

alex_Pl
alex_Pl
5 лет назад

Доброго времени суток, подскажите пожалуйста, как к STM32F411RCT6 можно подключить 4 ЦАП(WM8718) и 1 АЦП (PCM1804).

Олег
Олег
5 лет назад

Все собрал, как указано в примере, а звука нету. Не могли бы вы скинуть готовый проект?

Владимир
Владимир
4 лет назад

Вопрос, где описан SPI_I2S_FLAG_TXE?

Владимир
Владимир
4 лет назад

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

Владимир
Владимир
4 лет назад

Ну вот мне, например, не понятно, иначе вопроса не задал бы. А догадки строить… – это к гадалке.

Владимир
Владимир
4 лет назад

Статья-то хорошая, так почему бы не скорректировать?

Владимир
Владимир
4 лет назад

Описывать, что 4 колеса не нужно, а вот какого они диаметра – знать надо. Если есть уже описание – так наверное и ссылку можно дать? Ок, спасибо за ответ.

Владимир
Владимир
4 лет назад

А Вы не учитываете, что кроме SPL, еще могут быть и HAL? Причем, в последнее время производитель HAL поддерживает, SPL – вопрос. Поэтому, для новичка как раз все равно, что изучать и проще сразу взять на курс на то, что имеет перспективы (HAL), нежели пытаться осваивать то, на что производитель махнул рукой (SPL), чтобы потом осваивать снова HAL. Это спор не ради спора, прошу понять правильно. Я, к примеру новичек, и про SPL только “звон слышал”.

Владимир
Владимир
4 лет назад

В общем, наверное это уже лишнее замечание.

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

I2C_Init(I2C1, &i2c);

нет ли в этой строчке ошибки? )

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

За что отвечают регистры: 0x00, 0x47, 0x32 в коде инициализации, в даташите про них ничего.

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

Profile Profile Profile Profile Profile
Vkontakte
Twitter

Язык сайта

Июль 2020
Пн Вт Ср Чт Пт Сб Вс
« Июн    
 12345
6789101112
13141516171819
20212223242526
2728293031  

© 2013-2020 MicroTechnics.ru