Перейти к содержанию

STM32 SAI и микрофон INMP441

Представим, что у нас есть STM32L4 серии и на нем мы пытаемся подключить микрофон INMP441 через интерфейс SAI. Данный микрофон выводит данные сразу в PCM коде и имеет хорошие звуковые характеристики для своего ценового диапазона.

Быстрым гуглением мы можем найти три основные ссылки по данному вопросу:

  • Общие принципы подключения I2S микрофонов к контроллеру с очень общими словами, но с правильным посылом, которая является переводом и адаптацией AN5027 (ссылка)
  • Сайт челика, который сделал USB-микрофон используя пример от STM под USB-микрофон и обернул основную программу в C++ (ссылка). Есть гитхаб и видос. В его случае исползуется интерфейс I2S, который менее гибкий в настройке и, соответственно, который легче сконфигурировать, т.к. примеров в сети очень много.
  • Презентация по SAI интерфейсу STM32L4. В этой статье есть постоянная ссылка, если эта ссыль отвалится. (ссылка)

Допустим, у нас подключение микрофона в режиме моно и активен только левый канал. Подключение имеет следующий вид:

Открываем даташит на микрофон INMP441 и смотрим, что там по таймингам в его протоколе

INMP441

https://invensense.tdk.com/wp-content/uploads/2015/02/INMP441.pdf

Ссылка на даташит

Видим, что один полный цикл требует 64 тика на линии SCK. На один слот отведено 32 тика в течении которых передаются 24 бита. В слотах MSB-бит является старшим. WS(Word Select) работает в режиме идентификации каналов. Данные имеют смещение от WS (FS) в 1 бит. Слотов максимум два. Строб (синхронизация) SCK и WS идет по спадающему фронту.

Это все, что нам нужно знать, чтобы сконфигурировать SAI интерфейс на нашем контроллере.

https://programel.ru/files/en.STM32L4_Peripheral_SAI.pdf

На случай, если интернет забудет эту презентацию

Смотрим на картинку таймингов из презентации от ST. Легко видеть, что нам нужно выставить FSPOL = 1, FSOFF = 1 и SCKSTR = 1. С последним у меня возникли ментальные сложности, т.к. я ассоциировал этот регистр с «защелкиванием» данных как, например, в любом другом интерфейсе вроде SPI, I2C, USART и т.д. Сыграл свою роль даташит с таймингами от микрофона с указанием восходящего фронта в середине бита. Я не понимал, почему не показана середина бита в слоте при «защелкивании», а показано его начало и конец — списал на ошибку в презентации. В данном случае, SCKSTR выполняет роль настройки именно строба синхронизации с WS (FS). Данные уже читаются в нужный момент при правильной настройке строба.

Преступим к настройке самого интерфейса, когда уже известно чего от него хочется

Стоит обратить внимание на строку с Real Audio Frequency. Название говорящее, комментарии излишни.

Есть некоторая сложность с тем, чтобы выбрать правильные настройки PLLSAI1P, подходящие под выбранную частоту семплирования. В AN5027 есть некоторые предлагаемые настройки с самыми ходовыми частотами семплирования. У меня вышло вот так, для выбранной частоты 11025 Гц.

Добавляем ДМА в кольцевом режиме с увеличением адреса в памяти. Ширину слова я поставил в слово (32 бита). По желанию можно выставить в 16 бит (половина слова), тогда результат чтения будет в записываться в две ячейки памяти.

Включаем ОБА прерывания в настройках прерываний. Обработчик прерывания от SAI косвенно связан с прерываниями от DMA, если он включен.

Генерируем проект. Что осталось?

Есть один момент, связанный с порядком инициализации тактирования DMA в SAI. Нужно инициализировать тактирование DMA ДО инициализации SAI, хоть этот код и содержится в библиотеке HAL, она не отрабатыват так как необходимо. Поэтому в main.c ДО инициализации SAI, но ПОСЛЕ инициализаии HAL добавим следующее:

  /* USER CODE BEGIN SysInit */
  __HAL_RCC_DMA2_CLK_ENABLE();
  /* USER CODE END SysInit */

Где именно это записать — ориентируйтесь по комментариям, которые генерирует Cube.

Завести SAI в режиме DMA и складывать данные из DMA-буффера. Нужно организовать некий промежуточный буффер, т.к. данные в изначальном буффере будут перезаписываться по мере работы DMA. Данные складываются так — в прерывании о середине заполнения буффера считываем буффер с начала и до середины. В прерывании об полной передаче считываем данные с середины и уже до конца. Все действия производятся в main.c файле

Старт DMA после инициализации переферии.

 /* USER CODE BEGIN WHILE */  
HAL_SAI_Receive_DMA(&hsai_BlockA1, (uint8_t*)pAudBuf, AUDIO_BUFF_SIZE);

Используемые в вызове функции параметры определены следующим образом

#define AUDIO_BUFF_SIZE 120
static volatile uint32_t pAudBuf[AUDIO_BUFF_SIZE];

Переопределяем в main.c следующие функции

void HAL_SAI_RxHalfCpltCallback(SAI_HandleTypeDef * hsai) {
  for (int i = 0; i < AUDIO_BUFF_SIZE / 2; i++) {
    audio_out_buffer[i * 3 + 2] = (uint8_t)(pAudBuf[i] >> 16);
    audio_out_buffer[i * 3 + 1] = (uint8_t)(pAudBuf[i] >> 8);
    audio_out_buffer[i * 3] = (uint8_t)(pAudBuf[i]);
  }
}

void HAL_SAI_RxCpltCallback(SAI_HandleTypeDef * hsai) {
  for (int i = AUDIO_BUFF_SIZE / 2; i < AUDIO_BUFF_SIZE; i++) {
    audio_out_buffer[i * 3 + 2] = (uint8_t)(pAudBuf[i] >> 16);
    audio_out_buffer[i * 3 + 1] = (uint8_t)(pAudBuf[i] >> 8);
    audio_out_buffer[i * 3] = (uint8_t)(pAudBuf[i]);
  }
}

audio_out_buffer объявлен следующим образом

static volatile uint8_t audio_out_buffer[AUDIO_BUFF_SIZE*3];

Собственно, это все. Дальше с полученным 24 битным звуком можно делать что угодно. Если 24 бита не очень удобны для работы можно просто отбросить младшие разряды и код будет следующим

void HAL_SAI_RxHalfCpltCallback(SAI_HandleTypeDef * hsai) {
  for (int i = 0; i < AUDIO_BUFF_SIZE / 2; i++) {
    audio_out_buffer[i * 2 + 1] = (uint8_t)(pAudBuf[i] >> 16);
    audio_out_buffer[i * 2] = (uint8_t)(pAudBuf[i] >> 8);
  }
}

void HAL_SAI_RxCpltCallback(SAI_HandleTypeDef * hsai) {
  for (int i = AUDIO_BUFF_SIZE / 2; i < AUDIO_BUFF_SIZE; i++) {
    audio_out_buffer[i * 2 + 1] = (uint8_t)(pAudBuf[i] >> 16);
    audio_out_buffer[i * 2] = (uint8_t)(pAudBuf[i] >> 8);
  }
}

Усилить громкость звука, можно также простым смещением. Но тут стоит внимательно отнестить к старшему биту, т.к. именно он определяем знак закодированной в ИКМ синусоиды.

void HAL_SAI_RxHalfCpltCallback(SAI_HandleTypeDef * hsai) {
  for (int i = 0; i < AUDIO_BUFF_SIZE / 2; i++) {
    if (pAudBuf[i] & 0x800000) {
      pAudBuf[i] = (pAudBuf[i] << 2) | 0x800000;
    } else {
      pAudBuf[i] = pAudBuf[i] << 2;
    }
    audio_out_buffer[i * 2 + 1] = (uint8_t)(pAudBuf[i] >> 16);
    audio_out_buffer[i * 2] = (uint8_t)(pAudBuf[i] >> 8);
  }
}

void HAL_SAI_RxCpltCallback(SAI_HandleTypeDef * hsai) {
  for (int i = AUDIO_BUFF_SIZE / 2; i < AUDIO_BUFF_SIZE; i++) {
    if (pAudBuf[i] & 0x800000) {
      pAudBuf[i] = (pAudBuf[i] << 2) | 0x800000;
    } else {
      pAudBuf[i] = pAudBuf[i] << 2;
    }
    audio_out_buffer[i * 2 + 1] = (uint8_t)(pAudBuf[i] >> 16);
    audio_out_buffer[i * 2] = (uint8_t)(pAudBuf[i] >> 8);
  }
}

На этом точно все. Удачного звучания вашим платам.

Comment

programel