Одним из преимуществ отладочной платы STM32F4Discovery является наличие аудио-ЦАП со встроенным усилителем. Эту роль выполняет замечательная микросхема CS43L22. Так что на основе Discovery можно сделать кучу разнообразных аудио-девайсов! Мы сегодня, для начала, просто разберемся, как произвести инициализацию и первоначальную настройку всего этого добра, ну и попробуем что-нибудь пропищать )
Микросхема CS43L22 поддерживает несколько интерфейсов для обмена данными с контроллером, и использовать мы их будем совместно. Тут дело в том, что для управления CS43L22 (то есть для отправки управляющих команд) используется I2C, а для передачи аудио данных - I2S. Микроконтроллер STM32F4 поддерживает и то, и другое. Давайте посмотрим на кусок схемы платы Discovery:
Как видно из схемы, для управляющих команд используются выводы 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 и наслаждаемся ) Если изменить период работы таймера, то изменится частота следования прямоугольных импульсов, а значит и высота звука.
В общем, на этом пока все, но со звуком обязательно еще продолжим в будущих статьях!