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

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

  • Общие принципы подключения I2S микрофонов к контроллеру с очень общими словами, но с правильным посылом, которая является переводом и адаптацией AN5027 (ссылка)

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

  • Презентация по SAI интерфейсу STM32L4. В этой статье есть постоянная ссылка, если эта ссылка отвалится. (ссылка)

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

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

INMP441

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

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

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

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

Смотрим на картинку таймингов из презентации от 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);
  }
}

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