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);
}
}
На этом точно все. Удачного звучания вашим платам