I2S (Inter-IC Sound) — это цифровой протокол передачи звука, который довольно часто используется в современной электронике. I2S не имеет ничего общего с I2C кроме похожего названия, поэтому важно эти протоколы не путать. В рамках поста мы постараемся разобраться, на что вообще похож I2S, и как с ним работать.

На базе устройств, использующих I2S, существует немало готовых плат, в частности:

При написании этого поста я использовал PmodI2S производства компании Digilent . Данный модуль построен на базе чипа CS4344 (типичная маркировка «344C 1609», даташит [PDF] ). Устройство было приобретено в Чип-и-Дипе, но на момент написания этих строк оно успело куда-то пропасть с сайта магазина. Впрочем, для повторения описанных далее шагов вы можете использовать любой аналогичный модуль. Внешний вид использованного мной модуля:

Модуль PmodI2S на базе чипа CS4344

Типичный I2S-сигнал выглядит в PulseView как-то так:

I2S-сигнал в PulseView

Здесь SCK представляет собой тактовый сигнал. WS (он же LRCLK) отвечает за выбор канала. Через SDA (он же SDIN) передаются сами данные. Сигнала MCLK, строго говоря, нет в спецификации I2S [PDF] . Но на практике многие устройства используют его для синхронизации своих внутренних операций. Обычно сюда идет тактовый сигнал с частотой в 256 раз больше частоты дискретизации звука.

Fun fact! Если хочется извлечь звук из записанного I2S-сигнала, это можно сделать такой командой:

sigrok-cli -i i2s.sr -P i2s: sd =SDA: ws =WS: sck =SCK -A i2s =right |
cut -c27-30 | xxd -r -p |
sox -t raw -B -b 16 -c 1 -e signed-integer -r 48k — audio.wav

Для экспериментов с модулем я воспользовался отладочной платой Nucleo-F411RE . Микроконтроллер, используемый в этой плате, имеет аппаратную поддержку I2S, которой и было решено воспользоваться. Какие настройки доступны в STM32CubeMX и к каким пинам микроконтроллера следует подключать модуль, вы без труда разберетесь самостоятельно по полной версии проекта. Поговорим лучше непосредственно о коде.

Генерация синусоидального сигнала с частотой 100 Гц осуществляется так:

#define PI 3.14159265358979323846
#define TAU (2.0 * PI)

void loop ( ) {
HAL_StatusTypeDef res ;
int16_t signal [ 46876 ] ;
int nsamples = sizeof ( signal ) / sizeof ( signal [ 0 ] ) ;

int i = 0 ;
while ( i < nsamples ) {
double t = ( ( double ) i / 2.0 ) / ( ( double ) nsamples ) ;
signal [ i ] = 32767 * sin ( 100.0 * TAU * t ) ; // left
signal [ i + 1 ] = signal [ i ] ; // right
i += 2 ;
}

while ( 1 ) {
res = HAL_I2S_Transmit ( & hi2s2 , ( uint16_t * ) signal , nsamples ,
HAL_MAX_DELAY ) ;
if ( res != HAL_OK ) {
UART_Printf ( «I2S — ERROR, res = %d! r n » , res ) ;
break ;
}
}
}

Интересно, что по каким-то причинам микроконтроллеры STM32 не могут использовать традиционные значения частоты дискретизации, такие, как 44100 Гц или 48000 Гц. В частности, при выборе частоты 48000 Гц реальная частота составит 46876 Гц (на 2.34% меньше). Впрочем, на слух такая разница совершенно незаметна. Все эти различия между желаемой и реальной частотой отображаются прямо в STM32CubeMX.

Подключаем осциллограф , проверяем:

Проверяем звук, переданный по I2S, с помощью осциллографа

Но это еще не все. Если вы попытаетесь, например, просто взять и проиграть WAV-файл с SD-карты «в лоб», то у вас ничего не получится. Звук будет периодически обрываться и слушать такое будет совершенно невозможно. Решение заключается в том, чтобы использовать прерывания и двойную буфферизацию . Другими словами, параллельно с проигрыванием одного отрывка файла должен читаться следующий отрывок. Таким образом, когда проигрывание текущего отрывка завершится, следующий отрывок будет уже готов, и не придется тратить время на его чтение с SD-карты (что и является источником обрывов в звуке).

Соответствующий код:

volatile bool end_of_file_reached = false ;
volatile bool read_next_chunk = false ;
volatile uint16_t * signal_play_buff = NULL ;
volatile uint16_t * signal_read_buff = NULL ;
volatile uint16_t signal_buff1 [ 4096 ] ;
volatile uint16_t signal_buff2 [ 4096 ] ;

void HAL_I2S_TxCpltCallback ( I2S_HandleTypeDef * hi2s ) {
if ( end_of_file_reached )
return ;

volatile uint16_t * temp = signal_play_buff ;
signal_play_buff = signal_read_buff ;
signal_read_buff = temp ;

int nsamples = sizeof ( signal_buff1 ) / sizeof ( signal_buff1 [ 0 ] ) ;
HAL_I2S_Transmit_IT ( & hi2s2 , ( uint16_t * ) signal_play_buff , nsamples ) ;
read_next_chunk = true ;
}

int playWavFile ( const char * fname ) {
UART_Printf ( «Openning %s… r n » , fname ) ;
FIL file ;
FRESULT res = f_open ( & file , fname , FA_READ ) ;
if ( res != FR_OK ) {
UART_Printf ( «f_open() failed, res = %d r n » , res ) ;
return 1 ;
}

UART_Printf ( «File opened, reading… r n » ) ;

unsigned int bytesRead ;
uint8_t header [ 44 ] ;
res = f_read ( & file , header , sizeof ( header ) , & bytesRead ) ;
if ( res != FR_OK ) {
UART_Printf ( «f_read() failed, res = %d r n » , res ) ;
f_close ( & file ) ;
return 2 ;
}

if ( memcmp ( ( const char * ) header , «RIFF» , 4 ) != 0 ) {
UART_Printf ( «Wrong WAV signature at offset 0: »
«0x%02X 0x%02X 0x%02X 0x%02X r n » ,
header [ 0 ] , header [ 1 ] , header [ 2 ] , header [ 3 ] ) ;
f_close ( & file ) ;
return 3 ;
}

if ( memcmp ( ( const char * ) header + 8 , «WAVEfmt » , 8 ) != 0 ) {
UART_Printf ( «Wrong WAV signature at offset 8! r n » ) ;
f_close ( & file ) ;
return 4 ;
}
if ( memcmp ( ( const char * ) header + 36 , «data» , 4 ) != 0 ) {
UART_Printf ( «Wrong WAV signature at offset 36! r n » ) ;
f_close ( & file ) ;
return 5 ;
}

uint32_t fileSize = 8 + ( header [ 4 ] | ( header [ 5 ] << 8 ) |
( header [ 6 ] << 16 ) | ( header [ 7 ] << 24 ) ) ;
uint32_t headerSizeLeft = header [ 16 ] | ( header [ 17 ] << 8 ) |
( header [ 18 ] << 16 ) | ( header [ 19 ] << 24 ) ;
uint16_t compression = header [ 20 ] | ( header [ 21 ] << 8 ) ;
uint16_t channelsNum = header [ 22 ] | ( header [ 23 ] << 8 ) ;
uint32_t sampleRate = header [ 24 ] | ( header [ 25 ] << 8 ) |
( header [ 26 ] << 16 ) | ( header [ 27 ] << 24 ) ;
uint32_t bytesPerSecond = header [ 28 ] | ( header [ 29 ] << 8 ) |
( header [ 30 ] << 16 ) | ( header [ 31 ] << 24 ) ;
uint16_t bytesPerSample = header [ 32 ] | ( header [ 33 ] << 8 ) ;
uint16_t bitsPerSamplePerChannel = header [ 34 ] | ( header [ 35 ] << 8 ) ;
uint32_t dataSize = header [ 40 ] | ( header [ 41 ] << 8 ) |
( header [ 42 ] << 16 ) | ( header [ 43 ] << 24 ) ;

UART_Printf (
«— WAV header — r n »
«File size: %lu r n »
«Header size left: %lu r n »
«Compression (1 = no compression): %d r n »
«Channels num: %d r n »
«Sample rate: %ld r n »
«Bytes per second: %ld r n »
«Bytes per sample: %d r n »
«Bits per sample per channel: %d r n »
«Data size: %ld r n »
«—————— r n » ,
fileSize , headerSizeLeft , compression , channelsNum ,
sampleRate , bytesPerSecond , bytesPerSample ,
bitsPerSamplePerChannel , dataSize ) ;

if ( headerSizeLeft != 16 ) {
UART_Printf ( «Wrong `headerSizeLeft` value, 16 expected r n » ) ;
f_close ( & file ) ;
return 6 ;
}

if ( compression != 1 ) {
UART_Printf ( «Wrong `compression` value, 1 expected r n » ) ;
f_close ( & file ) ;
return 7 ;
}

if ( channelsNum != 2 ) {
UART_Printf ( «Wrong `channelsNum` value, 2 expected r n » ) ;
f_close ( & file ) ;
return 8 ;
}

if ( ( sampleRate != 44100 ) || ( bytesPerSample != 4 ) ||
( bitsPerSamplePerChannel != 16 ) || ( bytesPerSecond != 44100 * 2 * 2 )
|| ( dataSize < sizeof ( signal_buff1 ) + sizeof ( signal_buff2 ) ) ) {
UART_Printf ( «Wrong file format, 16 bit file with sample »
«rate 44100 expected r n » ) ;
f_close ( & file ) ;
return 9 ;
}

res = f_read ( & file , ( uint8_t * ) signal_buff1 , sizeof ( signal_buff1 ) ,
& bytesRead ) ;
if ( res != FR_OK ) {
UART_Printf ( «f_read() failed, res = %d r n » , res ) ;
f_close ( & file ) ;
return 10 ;
}

res = f_read ( & file , ( uint8_t * ) signal_buff2 , sizeof ( signal_buff2 ) ,
& bytesRead ) ;
if ( res != FR_OK ) {
UART_Printf ( «f_read() failed, res = %d r n » , res ) ;
f_close ( & file ) ;
return 11 ;
}

read_next_chunk = false ;
end_of_file_reached = false ;
signal_play_buff = signal_buff1 ;
signal_read_buff = signal_buff2 ;

HAL_StatusTypeDef hal_res ;
int nsamples = sizeof ( signal_buff1 ) / sizeof ( signal_buff1 [ 0 ] ) ;
hal_res = HAL_I2S_Transmit_IT ( & hi2s2 , ( uint16_t * ) signal_buff1 ,
nsamples ) ;
if ( hal_res != HAL_OK ) {
UART_Printf ( «I2S — HAL_I2S_Transmit failed, »
«hal_res = %d! r n » , hal_res ) ;
f_close ( & file ) ;
return 12 ;
}

while ( dataSize >= sizeof ( signal_buff1 ) ) {
if ( ! read_next_chunk ) {
continue ;
}

read_next_chunk = false ;

res = f_read ( & file , ( uint8_t * ) signal_read_buff ,
sizeof ( signal_buff1 ) , & bytesRead ) ;
if ( res != FR_OK ) {
UART_Printf ( «f_read() failed, res = %d r n » , res ) ;
f_close ( & file ) ;
return 13 ;
}

dataSize -= sizeof ( signal_buff1 ) ;
}

end_of_file_reached = true ;

res = f_close ( & file ) ;
if ( res != FR_OK ) {
UART_Printf ( «f_close() failed, res = %d r n » , res ) ;
return 14 ;
}

return 0 ;
}

Передача данных по I2S осуществляется асинхронно при помощи процедуры HAL_I2S_Transmit_IT . По завершении передачи данных вызывается коллбэк HAL_I2S_TxCpltCallback . Если это известно, то остальная часть кода становится тривиальной.

Напомню, что с форматом WAV-файлов и библиотекой FatFs мы ранее познакомились в рамках статей Парсинг заголовка и проигрывание WAV-файла на Scala и Работа с FAT32 и exFAT с помощью библиотеки FatFs соответственно.

Это все, о чем я хотел рассказать. Исходники к посту вы найдете на GitHub .

EnglishRussianUkrainian