В этом тексте я покажу как собрать проигрыватель аудио файлов буквально из подручных материалов.

Постановка задачи
Разработать прототип музыкального проигрывателя на основе микроконтроллера 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

https://elm-chan.org/fsw/ff/

Сборка 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(а))

https://habr.com/ru/articles/703588/

STM32 + FREERTOS + SDIO + FATFS
@muha_bob

https://habr.com/ru/articles/993760/

Отладка интерфейса I2S

https://habr.com/ru/articles/698572/

Разбор I2S трафика в программе Logic 2

https://habr.com/ru/articles/758188/

Цифровой звук на STM32: подключаем аналоговый микрофон через SAI и NAU88C22
@a3x

https://habr.com/ru/companies/selectel/articles/892852/

Настройка ToolChain(а) для Win10+GCC+С+Makefile+ARM Cortex-Mx+GDB

https://habr.com/ru/articles/673522/

Сравнение микросхем аудио кодеков

https://docs.google.com/spreadsheets/d/1r7tuNrQ8PPfIVcM2FeGpwa1eeWjyq41CD0MGq_Q_VV8/edit?gid=0#gid=0

Как Работать с UART на Микроконтроллерах (UART + FIFO = LOG) @danil_12345

https://habr.com/ru/articles/981028/

STM32 - uSD - SDIO 4bit - DMA

https://github.com/dtiziano/stm32_uSD_SDIO4bit/tree/main

Утилита для генерации тестировочных wav файлов

https://github.com/aabzel/Artifacts/tree/main/sonar

Отладка STM32 программатором J-LINK по SWD @danil_12345

https://habr.com/ru/articles/995996/

Аналитика по прототипу WAV Player

https://docs.google.com/spreadsheets/d/1twizxcenUCPF2iNNpEq3qrrdMX3we23pn7XMsVpdRhA/edit?pli=1&gid=0#gid=0

Подключение SD карты по SPI (Капсула памяти) @karenic

https://habr.com/ru/articles/974076/

WavePlayer using STM32 Discovery

https://controllerstech.com/waveplayer-using-stm32-discovery/

Пуск I2S трансивера на Artery

https://habr.com/ru/articles/830184/

Запуск I2S Трансивера на Artery [часть 2] (DMA, FSM, PipeLine)

https://habr.com/ru/articles/834304/

Конечный Aвтомат Аппаратного I2C-Трансивера

https://habr.com/ru/articles/856548/

Обзор Усилителя Звука из Apple AirTag

https://habr.com/ru/articles/767386/

Обзор Aппаратного Aудио кодека MAX9860 (2x ADC+DAC)

https://habr.com/ru/articles/758140/

Обзор AудиоКодека NAU8814YG

https://habr.com/ru/articles/808499/

Вопросы:
1) В каком формате хранятся 16 битные семплы в WAV файле Little Endian или BigEndian?
2) Какой модульный автотест можно написать, чтобы проверить корректность работы I2S?
3) Какой модульный автотест можно написать, чтобы проверить корректность работы I2С?
5) Что такое De-emphasis в настройках аудио кодека?
6) Как в real-time декодировать MP3 файл, чтобы воспроизвести его на аудио кодеке?
7) Как в преобразовать MP3 файл в WAV файл?
7) Как в преобразовать WAV файл в MP3 файл?

Only registered users can participate in poll. Log in, please.
Вы делали воспроизведение звука на микроконтроллере?
72.73%да8
27.27%нет3
11 users voted. 1 user abstained.