
В этом тексте я покажу как собрать проигрыватель аудио файлов буквально из подручных материалов.
Постановка задачи
Разработать прототип музыкального проигрывателя на основе микроконтроллера STM32F407VG и аудио кодека WM8731. Написать прошивку проигрывателя wav файлы. Звук отправлять в I2S2. Аудиокодек конфигурировать по I2C2. Использовать фирменный HAL SDK от STM. Файлы хранить на SD карте, подключенной по SPIO. Использовать DMA потоки для интерфейса I2S, SDIO и UART. Внутри SD карты должна быть файловая система FAT32. В прошивке использовать API файловой системы FatFS. Обеспечить возможность запускать wav файлы на выбор через интерфейс командной строки поверх UART2. Обеспечить возможность работать со стерео WAV файлом на частоте дискретизации 96kHz, семплами разрешением 16-бит. Вот, пожалуй, и все требования к прототипу музыкального проигрывателя.
Аппаратная часть
В качестве отладочной платы можно выбрать учебно тренировочную плату DevEBox_V3_0.

Блок схема отладочной платы. На ней установлен микроконтроллер STM32F407VG.

Мне потребуются вот эти пины.

В качестве аудиокодека я выбрал WM8731, так как он простой и доступный.

Надо собрать прототип проигрывателя согласно вот этой подсказке. Надо примонтировать всего лишь 11 перемычек.

Получается вот такая архитектура аппаратной части.

Вот так выглядит прототип проигрывателя в натуре.

Теория
Ключевая проблема воспроизведения звука на микроконтроллере заключается в том, что мы не можем прочитать огромный файл в RAM и воспроизвести его из RAM памяти. На МК просто нет столько RAM памяти, чтобы разместить там целый звуковой файл. В микроконтроллере всего 192kByte RAM. WAV файлы это особенно большие файлы, так как хранят звук в явном виде как массив семплов без сжатия. WAV файл одной песенки может запросто быть 50 MByte размером. Понятное дело, что WAV файл надо воспроизводить по частям. При этом воспроизведение звуковых файлов нельзя прерывать. Иначе звук будет со скрежетом.
Для решения этого противоречия придумана техника двух массивов. Пока воспроизводится часть 1 надо вычитывать часть 2. Пока воспроизводится часть 2 надо вычитывать часть 1. Этот конвейер показан на схеме ниже.

Чтобы этот конвейер заработал нужно в коде прошивки реализовать вот такой конечный автомат.

При воспроизведении двухканального 16 битного звука на частоте дискретизации 96kHz от микроконтроллера к аудиокодеку происходит перекачка данных со скоростью 384000 байт в секунду. Это 375k Byte/s. Не мало. Поэтому и вычитывать из SD карты надо тоже со скоростью более 375k Byte/s (скорость воспроизведения). Иначе просто нарушится непрерывность воспроизведения звуковой дорожки.
Разработка
Подготовка тестировочных звуковых дорожек
Прежде всего надо подготовить звуковой файл, который я буду проигрывать для отладки своей прошивки. Сгенерировать wav файл с 16 битными семплами двухканального звука на частоте дискретизации 96 kHz. В файле должен быть прописан синус сигнал на частоте 1k Hz и амплитудой 1000 PCM. Длительность звуковой дорожки должна составлять 5 секунд. Этот файл надо прописать на карту SD micro в файловой системе FAT32.
Я написал консольную утилиту, которая позволяет сгенерировать wav файл с тональным сигналом. Настраиваем через консоль параметры DDS и генерируем файл.
25.765-->ddf 1 1000 23.277,138,I,[DDS],argc 2 23.279,139,I,[DDS],DDS1 Set,frequency 1000 Hz 25.935--> 26.054-->wav_gen2ch_from_dds 1 1 1 35.326,139,I,[WAV],Generate2CHfile 35.329,140,I,[WAV],DDS1,1,Mode:SIN,Amp:1000,Freq:1000.0 Hz,0,Pha 0 ms, Duty:50.0,Sam:480000,Duration:5.000000 s,16 bit,Player:3,SignalPer:1. 35.339,141,I,[WAV],DDS2,2,Mode:PWM,Amp:1,Freq: 1.0 Hz,0,Pha 0 ms,Duty:50.0, Sam:480000,Duration:0.010000 s,16 bit,Player:4,SignalPer:1.000 35.349,142,I,[WAV],ChunkId:RIFF,ChunkSize:10036 Byte,Format:WAVE,Subchunk1Id:fmt , Subchunk1Size:16,AudioFormat:0x0001,NumChannels:2,SampleRate:96000 Hz, ByteRate:384000 Byte,BlockAlign:4 Byte,BitsPerSample:16 bit, Subchunk2Id:data,DataSize:10000 Byte 35.379,143,I,[WAV],Write,Header,Ok 35.382,144,I,[WAV],SampleCnt:480000 Sam 35.713--> 36.354-->wai track.wav 54.757,145,I,[WAV],argv0 [track.wav] 54.760,146,I,[WAV],FileName:[track.wav] 54.763,147,I,[WAV],OpenFile:[track.wav]Ok 54.769,148,I,[WAV],Read,Ok 54.773,149,I,[WAV],ChunkId:RIFF,ChunkSize:10036 Byte,Format:WAVE,Subchunk1Id:fmt , Subchunk1Size:16,AudioFormat:0x0001,NumChannels:2,SampleRate:96000 Hz, ByteRate:384000 Byte,BlockAlign:4 Byte,BitsPerSample:16 bit, Subchunk2Id:data,DataSize:10000 Byte 54.796,150,I,[WAV],SampleCnt:2500,SampleTime:10.417u s,PlayDir:0.026042 s 54.802,151,I,[WAV],Ok 54.809-->
Настройка GPIO
Надо подать тактирование на GPIO, и переключить альтернативные функции для I2C, I2S, SDIO и UART
+-----+-------+--------+-------+------+------+-----+-----+---------------+ | No | pad | mode | level | dir | pull |MuxS |MuxG | name | +-----+-------+--------+-------+------+------+-----+-----+---------------+ | 0 | PC2 | ALT1 | H | In | Up | 6 | 6 | I2S2_SDEXT | | 1 | PB13 | ALT1 | L | Out | Down | 5 | 5 | I2S2_CK | | 2 | PB12 | ALT1 | H | Out | Up | 5 | 5 | I2S2_WS | | 3 | PC3 | ALT1 | H | Out | Up | 5 | 5 | I2S2_SD | | 4 | PB10 | ALT1 | H | Out | Up | 4 | 4 | I2C2_SCL | | 5 | PB11 | ALT1 | H | IO? | Up | 4 | 4 | I2C2_SDA | | 6 | PA2 | ALT1 | H | Out | Air | 7 | 7 | USART2_TX | | 7 | PA3 | ALT1 | H | In | Up | 7 | 7 | USART2_RX | | 8 | PC12 | ALT1 | L | Out | Air | 12 | 12 | SD_CLK | | 9 | PD2 | ALT1 | H | Out | Up | 12 | 12 | SD_CMD | | 10 | PC8 | ALT1 | H | IO | Up | 12 | 12 | SD_D0 | | 11 | PC9 | ALT1 | H | IO | Up | 12 | 12 | SD_D1 | | 12 | PC10 | ALT1 | H | IO | Up | 12 | 12 | SD_D2 | | 13 | PC11 | ALT1 | H | IO | Up | 12 | 12 | SD_D3 | | 14 | PA1 | Out | H | Out | Air | 0 | 0 | LedGreem | | 16 | PA0 | In | L | In | Down | 0 | 0 | WKUP | +-----+-------+--------+-------+------+------+-----+-----+---------------+
Настроить I2S трансивер
Прежде всего следует подать тактирование на I2S2 трансивер, который сидит на системной шине APB1. Там может быть максимум 42MHz. В микроконтроллерах STM I2S трансивер это один из режимов SPI трансивера. Поэтому настройка I2S во многом похоже на настройку SPI.

Однако у I2S есть отдельный PLL источник тактирования. Надо активировать PLL для I2S.

I2S это liload интерфейс, поэтому отправлять данные надо в режиме DMA. Внутри STM32F407VG передатчик I2S2_TX заложен в DMA1_Stream4_Channel0.

Конфиг файл для I2S трансивера представлен ниже. Указываю режим stereo, master режим, 16бит на 1 канал семпла. Частота дискретизации звука тоже настраивается в I2S. Указываю 96kHz. Это основные настройки.
#include "i2s_config.h" #include "data_utils.h" #include "dma_channel_config.h" #ifdef HAS_I2S2 bool I2s2CallBackTxHalf(void){ bool res = false ; res = I2sDmaCallBackTxHalf(2); return res; }; bool I2s2CallBackTxDone(void){ bool res = false ; res = I2sDmaCallBackTxDone(2); return res; }; bool I2s2CallBackRxHalf(void){ bool res = false; res= I2sDmaCallBackRxHalf(2); return res; }; bool I2s2CallBackRxDone(void){ bool res = false; res = I2sDmaCallBackRxDone(2); return res; }; static uint16_t I2s2TxSampleArray[I2S_MEM_SIZE]={0}; static uint16_t I2s2RxSampleArray[I2S_MEM_SIZE]={0}; #define I2S_CONFIG_I2S2 \ { \ .CallBackTxHalf = I2s2CallBackTxHalf , \ .CallBackTxDone = I2s2CallBackTxDone, \ .CallBackRxHalf = I2s2CallBackRxHalf, \ .CallBackRxDone = I2s2CallBackRxDone, \ .dma_channel_tx_num=DMA_CHANNEL_NUM_I2S2_TX, \ .dma_channel_rx_num=DMA_CHANNEL_NUM_I2S2_RX, \ .num = 2, \ .led_tx_num = 1, \ .led_rx_num = 1, \ .dir_role = I2S_DIR_BUS_MODE_MASTER_TX, \ .sample_mode = SAMPLE_MODE_STEREO, \ .direction = CONNECT_DIR_TRANSMIT, \ .audio_frequency_hz = AUDIO_FREQ_96K, \ .bus_role = IF_BUS_ROLE_MASTER , \ .data_format = I2S_DATA_FORMAT_16B, \ .irq_priority = 0 , \ .move_mode = MOVE_MODE_DMA , \ .RxArray = I2s2RxSampleArray, \ .TxArray = I2s2TxSampleArray, \ .samples_cnt = ARRAY_SIZE(I2s2TxSampleArray), \ .full_duplex = FULL_DUPLEX_ON, \ .mclk_out = I2S_MCLKOUT_OFF, \ .standard = I2S_STD_PHILIPS, \ .cpol = I2S_CLOCK_POL_LOW, \ .clock_source = I2S_CLK_PLL, \ .name = "WavPlayer", \ .valid=true, \ }, #else #define I2S_CONFIG_I2S2 #endif /*constant compile-time known settings*/ const I2sConfig_t I2sConfig[] = { I2S_CONFIG_I2S2 }; I2sHandle_t I2sInstance[] = { #ifdef HAS_I2S2 { .num = 2, .valid=true, }, #endif }; COMPONENT_GET_CNT(I2s, i2s)
Настройка I2C трансивера
I2C трансивер должен увидеть на шине микросхему с адресами 0x34 ; 0x35.

Конфиг для I2C трансивера
#include "i2c_config.h" /*constant compile-time known settings*/ const I2cConfig_t I2cConfig[] = { #ifdef HAS_I2C2 { .num = 2, .PadSda = { .port=PORT_B, .pin=11, }, .PadScl = { .port=PORT_B, .pin=10, }, .own_addr = 2, .interrupt_priority = 2, .interrupt_on = true, .clock_speed = 400000, .name = "AudioCodec", .valid = true, }, #endif }; I2cHandle_t I2cInstance[] = { #ifdef HAS_I2C2 {.num=2, .valid=true, }, #endif };
Настройка SDIO трансивера
Сам звуковой файл хранится на SD карте. Микроконтроллер и SD карта соединены по интерфейсу SDIO. Чтобы не было проблем с подгрузкой кусков аудиофайла, я установил частоту тактирования SDIO шины на максимум 25MHz. Надо активировать тактирование на SDIO, активировать прерывания и выбрать режим DMA.
#include "sdio_config.h" /* 2 MHz, 1 bit, DMA - OK totalSize:1920044 Byte,Duration:12832 ms,ReadSpeed:149629 Byte/s 4 MHz, 1 bit, DMA - OK totalSize:1920044 Byte,Duration:6671 ms, ReadSpeed:287819 Byte/s 8 MHz, 1 bit, DMA - OK totalSize:1920044 Byte,Duration:3828 ms,ReadSpeed:501578 Byte/s 16 MHz, 1 bit, DMA - OK totalSize:1920044 Byte,Duration:2425 ms,ReadSpeed:791770 Byte/s=773 kByte/s 25 MHz, 1 bit, DMA - OK totalSize:1920044 Byte,Duration:1943 ms,ReadSpeed:988185 Byte/s=965.02441 kByte/s */ /*constant compile-time known settings in Flash*/ const SdioConfig_t SECTION_CFG_DATA SdioConfig[] = { { .num = 1, .bit_rate_hz = MHZ_2_HZ(20), .name = "SdCard", .interrupt_on = true, .move_mode = MOVE_MODE_DMA, .valid = true, }, }; SdioHandle_t SdioInstance[] = { { .num = 1, .valid = true, }, }; COMPONENT_GET_CNT(Sdio, sdio)
Настройка DMA
Для обеспечения высокой пропускной способности и низкой нагрузки на процессор тут как ни крути, а надо запускать DMA. Для данного микроконтроллера могут быть такие каналы. Для I2S надо настроить циклический режим. Это значит, что после отправки массива I2S мгновенно начнет отправлять тот же массив заново.

Настройка аудиокодека WM8731
WM8731 - это стерео аудиокодек или однокристальная звуковая карта. В сущности 2 пары ADC/DAC на 24бит каждый с настраиваемой по I2C/I2S частотой дискретизации 8kHz...96kHz ролью на I2S шине и пр. Про работу с этим чипом есть отдельный текст. Аудиокодек не работает сам по себе. Перед включением песенки надо настроить внутренние регистры аудиокодека. Внутри WM8731 заложено 11 шестнандатибитных регистра, которые надо корректно настроить.

Так как кодек тактируется от кварца 12MHz, то надо выбрать режим USB. Далее установить кодек в режим ведомого устройства на I2S (HAS_WM8731_I2S_SLAVE). Выбрать 16 битный режим, интерфейс I2S. Конфиг файл для аудиокодека представлен ниже.
#include "wm8731_config.h" #if !defined(HAS_WM8731_I2S_MASTER) && !defined(HAS_WM8731_I2S_SLAVE) #error "some WM8731 I2S role must be defined!" #endif const Wm8731RegConfig_t Wm8731RegisterConfiguration[]={ {.reg_addr=0x00, .value.LeftLineInCtrl.linvol=31, .value.LeftLineInCtrl.lin_mute=MUTE_ON}, {.reg_addr=0x01, .value.RightLineInCtrl.rinvol=31, .value.RightLineInCtrl.rin_mute=MUTE_ON,}, {.reg_addr=0x02, .value.LeftHeadOutCtrl.lhpvol=127, .value.LeftHeadOutCtrl.lzcen=0,}, {.reg_addr=0x03, .value.RightHeadOutCtrl.rhpvol=127, .value.RightHeadOutCtrl.rzcen=0,}, {.reg_addr=0x04, .value.AnalogAudioPathCtrl ={.mic_boost=MIC_IN_BOOST_OFF, .mute_mic=MUTE_OFF, .insel=ADC_IN_SEL_MIC, .by_pass=BYPASS_SW_OFF, .dac_sel=DAC_SEL_ON, .side_tone=SIDE_TONE_OFF, .sideatt=0,}, }, { .reg_addr=WM8731_REG_APDIGI, .value.DigitalAudioPathCtrl.adchpd=ADC_HI_PASS_FILT_OFF, .value.DigitalAudioPathCtrl.deemp=DE_EMPH_OFF, .value.DigitalAudioPathCtrl.dacmute=DAC_SW_MUTE_OFF, .value.DigitalAudioPathCtrl.hpor=DC_OFFSET_CLEAR, }, {.reg_addr=0x06, .value.PowerDownCtrl.lineinpd=0,/**/ .value.PowerDownCtrl.micpd=0, /**/ .value.PowerDownCtrl.adcpd=0, /**/ .value.PowerDownCtrl.dacpd=0, /**/ .value.PowerDownCtrl.outpd=0, /**/ .value.PowerDownCtrl.oscpd=0, /**/ .value.PowerDownCtrl.clkoutpd=0,/**/ .value.PowerDownCtrl.poweroff=0,/**/ }, {.reg_addr=0x07, .value.DigitalAudioIfCtrl.format = FMT_I2S, .value.DigitalAudioIfCtrl.iwl = AUD_BIT_16, .value.DigitalAudioIfCtrl.lrp = I2S_DAC_PHASE_RIGHT_CH_DAC_DACLRC_HI, .value.DigitalAudioIfCtrl.lrswap = DAC_LR_CLK_RIGHT, .value.DigitalAudioIfCtrl.bclkinv = BIT_CLOCK_NORMAL, .value.DigitalAudioIfCtrl.ms = BUS_MODE_SLAVE, }, {.reg_addr=0x08, .value.SamplingCtrl.usb_normal = MODE_USB, .value.SamplingCtrl.bosr = USB_BASE_OVER_SAMPLE_RATE_250FS, .value.SamplingCtrl.sr = WM_USB_SAMPLE_RATE_96000_HZ, .value.SamplingCtrl.clkidiv2 = CORE_CLK_MCLK, .value.SamplingCtrl.clkodiv2 = CLK_OUT_CORE_CLK, }, {.reg_addr=0x09, .value.ActiveCtrl.active=1,}, }; const Wm8731Config_t Wm8731Config[] = { { .num = 1, .chip_addr = WM8731_7BIT_ADDRESS, .dds_num = 1, .i2c_num = 2, .i2s_tx_num = 2, .i2s_rx_num = 2, .left = 1, .right = 1, .RegArray = Wm8731RegisterConfiguration, .reg_cnt = ARRAY_SIZE(Wm8731RegisterConfiguration), .valid = true, }, }; Wm8731Handle_t Wm8731Instance[] = { { .num = 1, .valid = true, } }; uint32_t wm8731_get_config_cnt(void){ uint8_t cnt=0; cnt = ARRAY_SIZE(Wm8731RegisterConfiguration); return cnt; } uint32_t wm8731_get_cnt(void) { uint8_t cnt = 0; cnt = ARRAY_SIZE(Wm8731Config); return cnt; }
Получилась прошивка вот их таких программных компонентов

Отладка проигрывателя
Для начала проверим, что файл, который мы хотим воспроизвести в самом деле лежит в файловой системе на SD карте. Для этого есть CLI команда fat_fs_scan
--> fat_fs_scan +-----+---------+----------+--------+--------+------+-----------+---------------------------+ | Num | SizeB | SizekB | fdate | ftime | attr | Attr | fname | +-----+---------+----------+--------+--------+------+-----------+---------------------------+ | 0 | 0 | 0.000 | 0x5b2c | 0x02c1 | 0x16 | ...d_.sh. | System Volume Information | | 1 | 2 | 0.002 | 0x5502 | 0x0000 | 0x20 | ..a._.... | ID_1.nv | | 2 | 1 | 0.001 | 0x5502 | 0x0000 | 0x20 | ..a._.... | ID_12.nv | | 3 | 1920044 | 1875.043 | 0x5cda | 0x01af | 0x20 | ..a._.... | sin2kHz5s.wav | +-----+---------+----------+--------+--------+------+-----------+---------------------------+ 59.861-->
Мы видим, что в файловой системе лежит файл sin2kHz5s.wav.

Посмотрим метаданные файла sin2kHz5s.wav, выполнив команду wai sin2kHz5s.wav
--> -->wai sin2kHz5s.wav 481.471,198,I,[WAV],OpenFile:[sin2kHz5s.wav]Ok 481.478,199,I,[WAV],Read,Ok 481.479,200,I,[WAV],ChunkId:RIFF,ChunkSize:960036 Byte,Format:WAVE, Subchunk1Id:fmt ,Subchunk1Size:16,AudioFormat:0x0001, NumChannels:2,SampleRate:96000 Hz,ByteRate:384000 Byte, BlockAlign:4 Byte,BitsPerSample:16 bit,Subchunk2Id:data, DataSize:960000 Byte 481.484,201,I,[WAV],SampleCnt:240000,SampleTime:10.417u s,PlayDir:2.500000 s -->
То же в консоли

Теперь остается только взять и воспроизвести этот файл командой play.

И вот звуковой файл в самом деле проигрался на наушниках. Успех!
Итог
Мне удалось запрограммировать на основе STM32 проигрыватель wav файлов с SD карты, подключенной по интерфейсу SDIO и воспроизводить звук в I2S шину. Бинари прошивки для указанного прототипа лежат на github.
Это позволяет буквально заставить любую PCB заговорить человеческим голосом.
Ссылки
Название | URL |
Аудио-плеер на STM32. Воспроизведение WAV-файла. | https://microtechnics.ru/audio-pleer-na-stm32-vosproizvedenie-wav-fajla/ |
FatFs - Generic FAT Filesystem Module | |
Сборка WAV проигрывателя на основе отладочной платы dev_ebox_stm32f4x | https://github.com/aabzel/Artifacts/tree/main/dev_ebox_stm32f4x_wav_player_gcc_m/v1 |
Исходный код прошивки WAV-I2S проигрывателя для dev_ebox_stm32f4x | https://github.com/aabzel/trunk/tree/main/source/projects/dev_ebox_stm32f4x_wav_player_gcc_m |
Чип AudioСodec(а) WM8731 (или (ADC/DAC)*2 из iPod(а)) | |
STM32 - uSD - SDIO 4bit - DMA | |
Утилита для генерации тестировочных wav файлов | |
Аналитика по прототипу WAV Player | |
WavePlayer using STM32 Discovery | https://controllerstech.com/waveplayer-using-stm32-discovery/ |
Вопросы:
1) В каком формате хранятся 16 битные семплы в WAV файле Little Endian или BigEndian?
2) Какой модульный автотест можно написать, чтобы проверить корректность работы I2S?
3) Какой модульный автотест можно написать, чтобы проверить корректность работы I2С?
5) Что такое De-emphasis в настройках аудио кодека?
