Top.Mail.Ru

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

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

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

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

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

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

Время традиционной вставки: поскольку компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе, я создал новый, посвященный работе уже с новыми инструментами, так что буду рад видеть вас там - STM32CubeMx. Кроме того, вот глобальная рубрика по STM32, а также статья на смежную тему из нового курса: Аудио-плеер на STM32. Воспроизведение WAV-файла.

Создаем проект в 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 initCS43L22()
{
	uint8_t sendBuffer[2];

	GPIO_SetBits(GPIOD, GPIO_Pin_4);

	delay(0x5FFFF);

	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 микросхеме CS43L22.

Мы упростим себе задачу и будем подавать просто прямоугольные импульсы с частотой 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();
	initCS43L22();

	// Включаем 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

33 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Aleks
Aleks
10 лет назад

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

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

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

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

Егор
Егор
Ответ на комментарий  Сергей
10 лет назад

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

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

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

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

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

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

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

Alex
Alex
10 лет назад

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

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

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

alex_Pl
alex_Pl
9 лет назад

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

I2C_Init(I2C1, &i2c);

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

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

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

Вася
Вася
2 лет назад

Build target 'Target 1'
main.c(2): warning: In file included from...
F:/Keil_v5/Arm/Packs/Keil/STM32F4xx_DFP/2.15.0/Drivers/CMSIS/Device/ST/STM32F4xx/Include\stm32f4xx.h(133): warning: In file included from...
F:/Keil_v5/Arm/Packs/Keil/STM32F4xx_DFP/2.15.0/Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f407xx.h(167): error: 'core_cm4.h' file not found
#include "core_cm4.h"            /* Cortex-M4 processor and core peripherals */

что не так?

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