
В этом тексте я покажу как собрать проигрыватель аудио файлов буквально из подручных материалов.
Постановка задачи
Разработать прототип музыкального проигрывателя на основе микроконтроллера STM32F407VG и аудио кодека WM8731. Написать прошивку проигрывателя wav файлы. Звук отправлять в I2S2. Аудиокодек конфигурировать по I2C2. Использовать фирменный HAL SDK от STM. Файлы хранить на SD карте, подключенной по интерфейсу SDIO. Использовать 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 трансивер
I2S - это по сути труба для чисел в аудиоустройствах. Это проводной последовательный синхронный полнодуплексный интерфейс физического уровня для аудио ЦАП(ов) и АЦП. Интерфейс для связи в пределах одной печатной платы. Битовые частоты под 3 MHz поэтому эту технологию можно причислить к Hi Load.

Топология Master-Slave. Мастер выдает частоту синхронизации битов SCL и частоту дискретизации WS семплов. Стандарт определяет битовую частоту до 2,7 MHz.
Прежде всего следует подать тактирование на 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. Это основные настройки. Пин I2S2_MCK пришлось отключить, так как с ним не проходила инициализация I2S трансивера.
#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, }, };
Настройка FatFs
SD карту надо отформатировать в файловую систему FAT32. В конфиге для FAT_FS для прошивки прописать вот эти макро определения:
#define _FFCONF 68300 #define _FS_READONLY 0 #define _FS_MINIMIZE 0 #define _USE_STRFUNC 2 #define _USE_FIND 0 #define _USE_MKFS 1 #define _USE_FASTSEEK 1 #define _USE_EXPAND 0 #define _USE_CHMOD 0 #define _USE_LABEL 0 #define _USE_FORWARD 0 #define _CODE_PAGE 437 #define _USE_LFN 1 #define _MAX_LFN 255 #define _LFN_UNICODE 0 #define _STRF_ENCODE 3 #define _FS_RPATH 0 #define _VOLUMES 1 #define _STR_VOLUME_ID 0 #define _VOLUME_STRS "RAM","NAND","CF","SD","SD2","USB","USB2","USB3" #define _MULTI_PARTITION 0 #define _MIN_SS 512 #define _MAX_SS 4096 #define _USE_TRIM 0 #define _FS_NOFSINFO 0 #define _FS_TINY 0 #define _FS_EXFAT 1 #define _FS_NORTC 1 #define _NORTC_MON 8 #define _NORTC_MDAY 2 #define _NORTC_YEAR 2022 #define _FS_LOCK 0 #define _FS_REENTRANT 0 #define _USE_MUTEX 0 #define _FS_TIMEOUT 1000
Настройка DMA
Для обеспечения высокой пропускной способности и низкой нагрузки на процессор тут как ни крути, а надо запускать DMA. Для данного микроконтроллера могут быть такие каналы. Для I2S надо настроить циклический режим. Это значит, что после отправки массива I2S мгновенно начнет отправлять тот же массив заново.

Настройки DMA канала для I2S2_Tx
#ifndef DMA_CHANNEL_I2S2_CONFIG_H #define DMA_CHANNEL_I2S2_CONFIG_H #ifdef __cplusplus extern "C" { #endif #include "std_includes.h" #include "dma_channel_types.h" #define I2S_DMA_MEMCPY_SIZE 128 bool CallBackDoneI2s2Rx(void); bool CallBackDoneI2s2Tx(void); bool CallBackHalfI2s2Rx(void); bool CallBackHalfI2s2Tx(void); bool CallBackErrorI2s2Rx(void); bool CallBackErrorI2s2Tx(void); #define DMA_CHANNEL_I2S_COMMON \ .memory_burst = DMA_BURST_SINGLE, \ .periph_burst = DMA_BURST_SINGLE, \ .aligment_per = DMA_ALIGNMENT_WORD, \ .aligment_mem = DMA_ALIGNMENT_WORD, \ .interrupt_on = true, \ .block_count =1, \ .valid = true, \ .mode = DMA_MODE_CIRCULAR, \ .fifo = DMA_FIFO_OFF, \ .priority = DMA_PRIOR_VERY_HIGH, #define DMA_CHANNEL_I2S2_RX \ { \ DMA_CHANNEL_I2S_COMMON \ .per_inc = DMA_INC_OFF, \ .mem_inc = DMA_INC_ON, \ .dir = DMA_MCAL_DIR_PERIPH_TO_MEMORY, \ .name = "I2s2rx", \ .num = DMA_CHANNEL_NUM_I2S2_RX, \ .base_addr_source = (uint32_t) fromArray, \ .DmaChPad = { .dma_num = 1, .channel = 3, .stream=3, .name = "I2s2rx", }, \ .base_addr_destination =(uint32_t) &(I2S2ext->DR), \ .block_size = (uint32_t) DMA_MEMCPY_SIZE, \ .CallBackHalf= CallBackHalfI2s2Rx, \ .CallBackDone= CallBackDoneI2s2Rx, \ }, #define DMA_CHANNEL_I2S2_TX \ { \ .dir = DMA_MCAL_DIR_MEMORY_TO_PERIPH, \ .DmaChPad = { .dma_num = 1, .channel = 0, .stream=4, .name = "I2S2_TX", }, \ .name = "I2s2Tx", \ .mem_inc = DMA_INC_ON, \ .per_inc = DMA_INC_OFF, \ DMA_CHANNEL_I2S_COMMON \ .num = DMA_CHANNEL_NUM_I2S2_TX, \ .CallBackHalf= CallBackHalfI2s2Tx, \ .CallBackDone= CallBackDoneI2s2Tx, \ .base_addr_source = (uint32_t)&(SPI2->DR), \ .base_addr_destination =(uint32_t) toArray, \ }, #define DMA_CHANNEL_I2S2 \ DMA_CHANNEL_I2S2_TX \ DMA_CHANNEL_I2S2_RX #ifdef __cplusplus } #endif #endif /* DMA_CHANNEL_I2S2_CONFIG_H */
Настройки DMA канала для SDIO
#ifndef DMA_CHANNEL_SDIO_CONFIG_H #define DMA_CHANNEL_SDIO_CONFIG_H #ifdef __cplusplus extern "C" { #endif #include "std_includes.h" #include "dma_channel_types.h" #include "dma_channel_config.h" #ifndef MIN #define MIN(n, m) (((n) < (m)) ? (n) : (m)) #endif bool CallBackDoneSdio1Rx(void); bool CallBackDoneSdio1Tx(void); bool CallBackHalfSdio1Rx(void); bool CallBackHalfSdio1Tx(void); bool CallBackErrorSdio1Rx(void); bool CallBackErrorSdio1Tx(void); /*DMA_PFCTRL DMA_PFCTRL DMA_PFCTRL DMA_PFCTRL*/ #define DMA_CHANNEL_SDIO_COMMON \ .block_size = (uint32_t) MIN( sizeof(toArray) ,sizeof(fromArray)) , \ .per_inc = DMA_INC_OFF, \ .mem_inc = DMA_INC_ON, \ .aligment_mem = DMA_ALIGNMENT_DWORD, \ .aligment_per = DMA_ALIGNMENT_DWORD, \ .memory_burst = DMA_BURST_INC4, \ .periph_burst = DMA_BURST_INC4, \ .fifo = DMA_FIFO_ON, \ .priority = DMA_PRIOR_MED, \ .mode = DMA_MODE_PFCTRL, \ .valid = true, \ .interrupt_on = true, #define DMA_CHANNEL_SDIO_RX \ { \ .DmaChPad = { .dma_num = 2, .stream = 3, .channel = 4, .name = "SDIO1_RX", }, \ .dir = DMA_MCAL_DIR_PERIPH_TO_MEMORY, \ DMA_CHANNEL_SDIO_COMMON \ .name = "SDIO1_RX", \ .num = DMA_CHANNEL_NUM_SDIO_RX, \ .CallBackHalf = CallBackHalfSdio1Rx, \ .CallBackDone = CallBackDoneSdio1Rx, \ .base_addr_source = (uint32_t) fromArray, \ .base_addr_destination = (uint32_t) toArray, \ .block_count = 1, \ .mux = 0, \ }, #define DMA_CHANNEL_SDIO_TX \ { \ .DmaChPad = { .dma_num = 2, .stream = 6, .channel = 4, .name = "SDIO_TX",}, \ .dir = DMA_MCAL_DIR_MEMORY_TO_PERIPH, \ DMA_CHANNEL_SDIO_COMMON \ .num = DMA_CHANNEL_NUM_SDIO_TX, \ .name = "SDIO_TX", \ .CallBackHalf = CallBackHalfSdio1Tx, \ .CallBackDone = CallBackDoneSdio1Tx, \ .base_addr_source = (uint32_t)fromArray, \ .base_addr_destination =(uint32_t) toArray, \ .block_count = 1, \ .mux = 0, \ }, #define DMA_CHANNEL_SDIO \ DMA_CHANNEL_SDIO_TX \ DMA_CHANNEL_SDIO_RX #ifdef __cplusplus } #endif #endif /* DMA_CHANNEL_SDIO_CONFIG_H */
Настройка аудиокодека 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; }
Получилась прошивка вот их таких программных компонентов

Как видите, простое воспроизведение звука с карты памяти требует безупречной работы целого вороха программных компонентов: GPIO, I2S, I2C, DWT, INTERRUPT, SDIO, LED, DMA, FatFS, WAV, WM8731, UART, CLI и прочего.
Отладка проигрывателя
Для начала проверим, что файл, который мы хотим воспроизвести в самом деле лежит в файловой системе на 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 + FREERTOS + SDIO + FATFS | |
Отладка интерфейса I2S | |
Разбор I2S трафика в программе Logic 2 | |
Цифровой звук на STM32: подключаем аналоговый микрофон через SAI и NAU88C22 | |
Настройка ToolChain(а) для Win10+GCC+С+Makefile+ARM Cortex-Mx+GDB | |
Сравнение микросхем аудио кодеков | https://docs.google.com/spreadsheets/d/1r7tuNrQ8PPfIVcM2FeGpwa1eeWjyq41CD0MGq_Q_VV8/edit?gid=0#gid=0 |
Как Работать с UART на Микроконтроллерах (UART + FIFO = LOG) @danil_12345 | |
STM32 - uSD - SDIO 4bit - DMA | |
Утилита для генерации тестировочных wav файлов | |
Отладка STM32 программатором J-LINK по SWD @danil_12345 | |
Аналитика по прототипу WAV Player | |
Подключение SD карты по SPI (Капсула памяти) @karenic | |
WavePlayer using STM32 Discovery | https://controllerstech.com/waveplayer-using-stm32-discovery/ |
Пуск I2S трансивера на Artery | |
Запуск I2S Трансивера на Artery [часть 2] (DMA, FSM, PipeLine) | |
Конечный Aвтомат Аппаратного I2C-Трансивера | |
Обзор Усилителя Звука из Apple AirTag | |
Обзор Aппаратного Aудио кодека MAX9860 (2x ADC+DAC) | |
Обзор AудиоКодека NAU8814YG |
Вопросы:
1) В каком формате хранятся 16 битные семплы в WAV файле Little Endian или BigEndian?
2) Какой модульный автотест можно написать, чтобы проверить корректность работы I2S?
3) Какой модульный автотест можно написать, чтобы проверить корректность работы I2С?
5) Что такое De-emphasis в настройках аудио кодека?
6) Как в real-time декодировать MP3 файл, чтобы воспроизвести его на аудио кодеке?
7) Как в преобразовать MP3 файл в WAV файл?
7) Как в преобразовать WAV файл в MP3 файл?