Воспроизведение звука на 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;
 
 
 
/************************************************************************/

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

/************************************************************************/
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 и наслаждаемся звуком ) Если изменить период работы таймера, то изменится частота следования прямоугольных импульсов, а значит и высота звука 😉

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

Понравилась статья? Поделись с друзьями!

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

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

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

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

    • Не помню, что там с периодом замутил, но это не принципиально, смотри просто какая у тебя частота таймера и какой нужен звук ) Если менять значение, то будет высота звука меняться, а громкость останется такая же. А про инициализацию — CS32L22 имеет ряд своих внутренних регистров, в которых производится ее настройка, по битам они в пдфке на микруху описаны

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

    • Сделаю после праздников небольшой пример с разными нотами и длительностью, но вообще это примитивный способ воспроизведения звука по большому счету =)

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

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

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

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

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

    • И так понятно, по большому количеству факторов, что программа написана на SPL. А то, что ST не поддерживают библиотеку не значит, что никто ей не пользуется и не будет…

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

    • Да не вопрос, я допишу предложение, что используется SPL, просто это то же самое, что написать статью про автомобиль и отдельно указать, что у него 4 колеса)

    • Если серьезно, то просто есть набор статей для начинающих, там описано и какие библиотеки и куда копировать файлы и все остальные подробности. Но не будешь же в каждой статье описывать все с нуля про создание проекта и библиотеки.

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

    • Есть рубрика «STM32 с нуля», но я же не могу часть каждой статьи посвящать тому, чтобы направлять людей на изучение основ. Подразумевается, что если человек дошел до работы с USB, то он явно не новичок.

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

    • Ну так курс то не вчера был написан, а тогда, когда HAL еще не было и близко. Поэтому с появлением HAL появился новый курс — рубрика «STMCube» — там описано все с самого начала и с постепенным усложнением.

      И возвращаясь к тому, с чего наш мини-спор начался… Если изучить курс SPL с нуля, а не сразу с USB, то станет понятно, что в данной конкретной статье используется SPL. Если рассуждать так — SPL не поддерживается, зачем мне это изучать, то можно пойти и почитать новый курс по HAL. И абсолютно аналогично после прочтения нового курса будет совершенно очевидно, что в данной статье HAL не используется.

      Таким образом, дело совсем не в библиотеках, а в том, что нерационально переходить к работе с USB, не имея представления о данных контроллерах в целом. Собственно, более сложные статьи рассчитаны на то, что дойдя до них человек освоил все более простые.

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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *