
В этом тексте я покажу, как из подручных материалов на основе микроконтроллера, аудиокодека и SD карты запрограммировать самый настоящий диктофон. Покажу принципы работы звукозаписывающей аппаратуры. Но обо всем по порядку...
Постановка задачи
Разработать прототип диктофона на основе микроконтроллера STM32F407VG и аудио кодека WM8731. Написать прошивку для записи звука в wav файлы. Звук передавать по интерфейсу I2S2. Аудиокодек конфигурировать по интерфейсу I2C2. Использовать фирменный HAL SDK от производителя микроконтроллера STM. Записанные файлы хранить на SD карте, подключенной по интерфейсу SDIO. Использовать DMA потоки для интерфейса I2S2_RX, SDIO и UART2_TX. Внутри SD карты должна быть файловая система FAT16. В прошивке использовать функции из API файловой системы FatFS. Обеспечить возможность записывать в wav файлы через интерфейс командной строки поверх UART2. Указывать длительность записываемого файла аргументами командной строки. Обеспечить возможность работать со стерео WAV файлом на частоте дискретизации 96kHz, семплами разрешением 16-бит. Вот, пожалуй, и все требования к прототипу диктофона.
Аппаратная часть
Итак, танцуем от печки... Из чего же я буду собирать диктофон? Основа диктофона - это учебно-тренировочная электронная плата DevEBox STM32F4XX_M_V3. PCB хороша тем, что она миниатюрна и на ней построен гараж для microSD карты. Вот так она выглядит.

Вот блок-схема DevEBox STM32F4XX_M_V3.

Аудиокодек - это ASIC WM8731 и модуль на его основе.

WM8731 - это по сути 2xADC + 2xDAC с торчащим наружу I2S и с управлением по I2C.

Надо учинить вот такой прототип. Следует установить всего 12 перемычек. Вот так, просто и незатейливо:

Микрофон я выбрал первый попавшийся под рукой. Это оказался микрофон aceline AMIC-7.

Блок-схема диктофона получается такая

В качестве накопителя я выбрал микро SD карту на 128 MByte.

Теория цифровой звукозаписи
Главная трудность записи звука на микроконтроллерах заключается в том, что в микроконтроллерах нет столько много RAM памяти, чтобы за раз записать привычный нам звуковой файл. Одна песня в WAV формате может занимать 60MByte. Для решения этой проблемы существует классическое решение на основе двух массивов. Суть проста. Пока по I2S записывается в RAM память фрагмент 1, микроконтроллер сохраняет из RAM памяти в FAT16 файл фрагмент 2. Пока по I2S записывается в RAM память фрагмент 2, микроконтроллер сохраняет из RAM памяти в файл фрагмент 1. Таким образом становится возможным непрерывно записывать файлы, размер которых в тысячи раз превышает количество RAM памяти в микроконтроллере.

Для контролирования процесса записи внутри прошивки должен работать вот такой простой конечный автомат. Состояние автомата можно переключать прямо внутри обработчика прерываний по DMA каналам. А непосредственную запись в SD карту делать в супер цикле функции main по командам конечного автомата записи.

Реализация
Настройка GPIO
Для работы диктофона надо активировать альтернативные функции на I2S2, I2C2, SDIO, UART2. Плюс добавить несколько GPIO пинов для отладки на логическом анализаторе или осциллографе.
--> -->gl +-----+-------+---------+------+------+-----+-----+---------------+ | No | pad | mode | dir | pull |MuxS |MuxG | name | +-----+-------+---------+------+------+-----+-----+---------------+ | 0 | PA8 | Out | Out | Air | 0 | 0 | I2S2_TX_DMA | | 1 | PA4 | Out | Out | Air | 0 | 0 | I2S2_RX_DMA | +-----+-------+---------+------+------+-----+-----+---------------+ | 2 | PB13 | ALT1 | Out | Down | 5 | 5 | I2S2_CK | | 3 | PB12 | ALT1 | Out | Down | 5 | 5 | I2S2_WS | | 4 | PB15 | ALT1 | In | Down | 5 | 5 | I2S2_SD | +-----+-------+---------+------+------+-----+-----+---------------+ | 5 | PB10 | ALT1 | Out | Up | 4 | 4 | I2C2_SCL | | 6 | PB11 | ALT1 | Io | Up | 4 | 4 | I2C2_SDA | +-----+-------+---------+------+------+-----+-----+---------------+ | 7 | PA2 | ALT1 | Out | Air | 7 | 7 | USART2_TX | | 8 | PA3 | ALT1 | In | Up | 7 | 7 | USART2_RX | +-----+-------+---------+------+------+-----+-----+---------------+ | 10 | PC12 | ALT1 | Out | Air | 12 | 12 | SD_CLK | | 11 | PD2 | ALT1 | Out | Up | 12 | 12 | SD_CMD | | 12 | PC8 | ALT1 | Io | Up | 12 | 12 | SD_D0 | | 13 | PC9 | ALT1 | Io | Up | 12 | 12 | SD_D1 | | 14 | PC10 | ALT1 | Io | Up | 12 | 12 | SD_D2 | | 15 | PC11 | ALT1 | Io | Up | 12 | 12 | SD_D3 | +-----+-------+---------+------+------+-----+-----+---------------+ | 9 | PA1 | Out | Out | Air | 0 | 0 | LedGreem | +-----+-------+---------+------+------+-----+-----+---------------+ -->
Настройка I2S трансивера
Для работы я выбрал I2S2. I2S2 трансивер надо сконфигурировать на режим записи. Микроконтроллер должен выступать в роли мастера. Есть два варианта для пина I2S2_SD: PC3 и PB15.
GPIO | Dir | MCU pin | PinMux | Connector | Connector |
PC3 | out | 18 | AF5 | J3.35 | -- |
PB15 | in | 54 | AF5 | J2.8 | J4.3 |
В STM32 каждый отдельный I2S трансивер может либо записывать, либо отправлять звук через пин I2Sx_SD. Поэтому для записи и последующего воспроизведения вам придется пере инициализировать I2S трансивер. Есть и отдельный режим полного дуплекса, но он требует еще одного экземпляра I2S трансивера. Полный дуплекс достигается за счет тандемной работы двух трансиверов, но это тема для отдельной статьи.
Запись звука должна происходить на таких настройках GPIO.
PinMux | GPIO | DIR | Pull | PIN | PinMux | Connector | Connector |
I2S2_SD | PB15 | in | down | 54 | AF5 | J2.8 | J4.3 |
GPIO | PC3 | in | down | 18 | 0 | J3.35 | -- |
I2S2_CK | PB13 | out | down | 47 | 5 | J3.13 | -- |
I2S2_WS | PB12 | Out | down | 51 | 5 | J3.11 | J4.5 |
Синтез WAV файла
Как только поступит запрос на запись надо понять сколько вообще надо семплов звука. Для простоты положим, что в запросе на запись задается продолжительность записи в секундах. Известна частота дискретизации, размер одного семпла, количество каналов. Надо определить сколько памяти потребуется для поля данных. Это можно рассчитать по простой формуле:
MemNeed [byte] = SampleFreq [Hz] x SampleSize [byte] x Channels [Num] x RevDuration [s]
Зная параметры предстоящей записи надо синтезировать заголовок WAV файла. Этот заголовок прописывается в WAV файл и после этого дается отмашка DMA на пуск I2S_RX .
Настройка каналов DMA
Записывать звук следует через DMA канал для I2S2_RX. Должен быть включен циклический режим, активированы DMA прерывания, выравнивание 16 бит. Тут надо сразу вспомнить что в STM32 микроконтроллерах I2S - это режим SPI. Поэтому настройки каналов DMA надо тоже копировать от SPI.

Для I2S2 настройки каналов DMA получаются такие
Flow | DMA | Stream | Channel | Mode | Direction |
I2S2_RX | DMA1 | Stream_3 | Channel_0 | CIRCULAR | PERIP->MEM |
I2S2_TX | DMA1 | Stream_4 | Channel_0 | CIRCULAR | MEM->PERIP |
В коде это выглядит так
#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 \ { \ .DmaChPad = { .dma_num = 1, .stream=3, .channel = 0, .name = "I2S2_RX", }, \ DMA_CHANNEL_I2S_COMMON \ .per_inc = DMA_INC_OFF, \ .mem_inc = DMA_INC_ON, \ .dir = DMA_MCAL_DIR_PERIPH_TO_MEMORY, \ .name = "I2S2_RX", \ .num = DMA_CHANNEL_NUM_I2S2_RX, \ .base_addr_source = (uint32_t) fromArray, \ .base_addr_destination = (uint32_t) &(I2S2ext->DR), \ .block_size = (uint32_t) DMA_MEMCPY_SIZE, \ .CallBackHalf = CallBackHalfI2s2Rx, \ .CallBackDone = CallBackDoneI2s2Rx, \ },
Настройка и интерфейса SDIO
В экспериментах я работал на частоте тактирования SDIO равной 12 MHz (фактическое значение по осциллографу). Повышение частоты до 24MHz не оказало существенного прироста производительности. SDIO интерфейс надо настроить на работу с DMA каналами.
Flow | DMA | Stream | Channel | Mode | Direction |
SDIO_RX | DMA2 | Stream_3 | Channel_4 | PFCTRL | PERIP->MEM |
SDIO_TX | DMA2 | Stream_6 | Channel_4 | PFCTRL | MEM->PERIP |
В коде это выглядит так
#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_VERY_HIGH, \ .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 */
Настройка SD карты
Может так получиться, что вы нашли SD карту на 2GB, установили в PC и вам показывает 7 MByte. Дело в том что на ней уже прописана какая-н linux-овая файловая система и win это не видит. Надо удалить старые разделы и заново отформатировать карту памяти. Это можно сделать утилитой DISKPART. Для пуска утилиты DISKPART потребуются админские права.
Microsoft DiskPart version 10.0.19041.3636 Copyright (C) Microsoft Corporation. On computer: LAPTOP-xxxxxx DISKPART> DISKPART> list disk list VDISK Disk ### Status Size Free Dyn Gpt -------- ------------- ------- ------- --- --- Disk 0 Online 476 GB 1024 KB * Disk 1 Online 7600 MB 7592 MB DISKPART> select disk 1 Disk 1 is now the selected disk. DISKPART> DISKPART> list part Partition ### Type Size Offset ------------- ---------------- ------- ------- Partition 1 Primary 7568 KB 31 KB DISKPART> DISKPART> select part 1 Partition 1 is now the selected partition. DISKPART> DISKPART> delete part DiskPart successfully deleted the selected partition. DISKPART> DISKPART> create part pri DiskPart succeeded in creating the specified partition. DISKPART> DISKPART> format fs=fat32 quick 100 percent completed DiskPart successfully formatted the volume. DISKPART> DISKPART> exit
Затем перед установкой SD карты в устройство надо проверить, что карта в самом деле поддерживает нужную битовую скорость на запись. Для этого есть другая утилита, называется H2testw.exe. Скачать H2testw можно по ссылке. Утилита H2testw не стирает данные с карты. Эта утилита по частям пишет , читает и проверяет прочитанное с записанным. И так по всей памяти.

Утилита H2testw проверяет SD карту в случае успеха утилита выдает строку Test finished without errors. Тут же есть отчет по битовой скорости записи и чтения.

Результат тестирования SD карты положительный. Фактическая скорость получилась 13.8 MByte/s. Это в 37.68 раз превышает необходимую скорость записи звука.
Warning: Only 3758 of 3820 MByte tested. Test finished without errors. You can now delete the test files *.h2w or verify them again. Writing speed: 13.8 MByte/s Reading speed: 67.4 MByte/s H2testw v1.4
Однако может попасться и бракованная SD карта. В этом случае утилита H2testw выдаст примерно такое красное сообщение

Отрицательный тест SD карты
The media is likely to be defective. 951.9 MByte OK (1949692 sectors) 2 KByte DATA LOST (4 sectors) Details:0 KByte overwritten (0 sectors) 0 KByte slightly changed (< 8 bit/sector, 0 sectors) 2 KByte corrupted (4 sectors) 0 KByte aliased memory (0 sectors) First error at offset: 0x000000000df64e00 Expected: 0x000000000df64e00 Found: 0x000000000df24e00 H2testw version 1.3 Writing speed: 6.47 MByte/s Reading speed: 20.9 MByte/s H2testw v1.4
У каждой SD карты есть регистры: CSD, CID и прочие. Вот значения регистров той SD карты с которой я делал эксперименты.
3.191-->sdio_diag_ll 4.299,308,W,[SDIO], CardCSD... 4.300,309,I,[SDIO],CSD_CRC:119 ,ECC:0,FileFormat:0,TempWrProtect:0 ,PermWrProtect:0,CopyFlag:0,FileFormatGroup:0,ContentProtectAppli:0 ,WriteBlockPaPartial:0,MaxWrBlockLen:9,WrSpeedFact:5,ManDeflECC:0 ,WrProtectGrEnable:1,WrProtectGrSize:3,EraseGrMul:127,EraseGrSize:0 ,DeviceSizeMul:4,MaxWrCurrentVDDMax:5,MaxWrCurrentVDDMin:5,MaxRdCurrentVDDMax:5 ,MaxRdCurrentVDDMin:5,DeviceSize:3879,DSRImpl:0,RdBlockMisalign:0 ,WrBlockMisalign:0,PartBlockRead:1,RdBlockLen:9,CardComdClasses:501 ,MaxBusClkFrec:50,NSAC:0,TAAC:94,SysSpecVersion:0,CSDStruct:0 CID... 4.327,343,I,[SDIO],ManID:29 0x1d AData 4.328,344,I,[SDIO],OemAppId:16708 0x4144 DA 4.328,345,I,[SDIO],ProdName1:1297293344 0x4d532020 MS 4.331,346,I,[SDIO],ProdRev:0x10=0.1 4.331,347,I,[SDIO],ProdSN:939530821 0x38001a45 4.333,348,I,[SDIO],ManufactDate: Month:11 Year:2000 4.334,349,I,[SDIO],CID_CRC:55=0x37 4.337,350,W,[SDIO], DsCardInfo... 4.337,351,I,[SDIO],CardVersion:0 4.337,352,I,[SDIO],Class:501 4.340,353,I,[SDIO],RelCardAdd:45928 4.340,354,I,[SDIO],BlockNbr:248320 4.340,355,I,[SDIO],BlockSize:512 4.343,356,I,[SDIO],TotalSize:127139840 Byte=124160 kByte=121.25 MBytes 4.344,357,I,[SDIO],LogBlockNbr:248320 4.346,358,I,[SDIO],LogBlockSize:512 4.346,359,I,[SDIO],CardType:0 4.347,360,I,[SDIO],1 CardState ?
Отформатировать SD карту в файловую систему FAT16. Назначить Allication Unit size равным 16 kбайт.
Диктофоны - это устройства hi-load обработки. Сейчас объясню почему... Для записи набегающего потока двухканальных 16-битных аудио семплов на частоте дискретизации 96kHz надо чтобы в SDIO, скорость записи превышала 384000 байт в секунду. Это не мало. Каждый семпл дает Вам в WAV файле 4 байта: левый и правый каналы по 16 бит каждый. Внутри прошивки я измерил фактическую битовую скорость записи в файловую систему FAT16 и получил такую справку. Понял, что надо заняться оптимизацией и ускорить запись в файл.
SDIO clock, MHz | kByte/s | Byte/s | ratio | режим, bit |
5 | 159 | 162847 | 0,42 | 1 |
16.6 | 262 | 269003 | 0.70 | 1 |
12 | 737 | 754883 | 1.9* | 4 |
25 | 292 | 299304 | 0,77 | 1 |
Для увеличения скорости записи пришлось принять ряд совсем неочевидных мер:
Увеличить частоту ARM Cortex-M4 ядра до максимума: 168MHz (было 100 MHz). Так системная шина тактирует SDIO MAC с максимальной частотой 84MHz.

Установил подтяжки к питанию для всех SDIO проводов кроме тактирования.
-->gl sd +-----+-------+--------+-------+------+------+-----+-----+---------+ | No | pad | mode | level | dir | pull |MuxS |MuxG | name | +-----+-------+--------+-------+------+------+-----+-----+---------+ | 3 | PC12 | ALT1 | L | out | Air| 12 | 12 | SD_CLK | | 4 | PD2 | ALT1 | H | out | Up | 12 | 12 | SD_CMD | | 5 | PC8 | ALT1 | H | io | Up | 12 | 12 | SD_D0 | | 6 | PC9 | ALT1 | H | io | Up | 12 | 12 | SD_D1 | | 7 | PC10 | ALT1 | H | io | Up | 12 | 12 | SD_D2 | | 8 | PC11 | ALT1 | H | io | Up | 12 | 12 | SD_D3 | +-----+-------+--------+-------+------+------+-----+-----+---------+Записывать блоки размером 4k Byte за раз.
Перевести SDIO на 4-битный режим. То есть задействовать параллельную шину.
Работать в SDIO в режиме DMA2.
Закомментировать отладочное логирование в коде SDIO операций чтения и записи.
Повысить приоритет DMA каналов для SDIO до максимума.
Повысить приоритет прерываний для DMA каналов SDIO до максимума.
Отформатировать SD карту в файловую систему FAT16. Назначить Allocation Unit size равным 16 kбайт.
Увеличил тактирование на SDIO тактировании до 24 MHz (максимальное).
Увеличил стековую RAM память до 16k Byte.
После этих мер скорость записи файлов заметно увеличилась и стала доходить до 853 kByte/s. Можно провернуть модульный тест fat_fs_write для измерения фактической скорости записи в файл.

Отладка диктофона
Чтобы начать запись надо открыть программу PuTTY, подключиться к главной консоли управления на UART2 и выполнить команду sound_recorder_restc DurationS. Первый аргумент задает продолжительность записи. В CLI это выглядит так

То же в виде текста
7:44-->sound_recorder_restc 10 465.426,431,I,[SoundRecorder],Duration,Ok 465.427,432,I,[WAV],File:R_4096_UT462732_10000Ms.wav,state:0,state:3,i2s_error_cnt:0, rec_cnt:4,sample_cnt:960000,ErrCnt:0,total_file_size:3840044, start_rec_ms:462753,stop_rec_ms:459991,data_size:3840000, Spin:6871236,cur_save_size:0,Init:On, 465.429,433,I,[WAV],ChunkId:RIFF,ChunkSize:1920036 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:1920000 Byte 465.440,434,I,[I2S],I2S_2,Set,Operation:Rx 465.441,435,I,[I2S],2 init 465.442,436,W,[DmaChannel],N:3=I2S2_RX 465.442,437,W,[Dma],3,DMA1,Stream_3,Init 465.443,438,I,[Dma],Init Id:3 Dma:1 Stream:3 Ok! 465.443,439,I,[I2S],I2S2 initOk 465.444,440,I,[GPIO],Init,PA0,IN 465.444,441,I,[GPIO],Init,PC3,IN 465.445,442,W,[GPIO],PB12,DrvSel:?,I2S2_WS,Dir:Out,Mode:ALT1,Mux:5,LL:H,Pull:Down, Speed:High,Pin:0, 465.446,443,W,[GPIO],PB13,DrvSel:?, I2S2_CK,Dir:Out,Mode:ALT1,Mux:5,LL:H,Pull:Down, Speed:High,Pin:0, 465.447,444,W,[GPIO],PB15,DrvSel:?,I2S2_SDEXT,Dir:In,Mode:ALT1,Mux:5,LL:H,Pull:Down, Speed:High,Pin:0, 465.447,445,I,[SoundRecorder],RecStart... 7:45--> 7:48--> 7:49--> 7:49-->475.277,446,I,[I2S],DmaStopOk! 475.277,447,I,[SoundRecorder],I2sDmaStop,Ok 475.281,448,I,[SoundRecorder],CloseFile:[R_4096_UT465427_10000Ms.wav]Ok 475.281,449,I,[SoundRecorder],RecDuration:9833 ms 475.282,450,I,[SoundRecorder],WrOkCnt:938 475.283,451,I,[SoundRecorder],errorCnt:0 475.283,452,I,[SoundRecorder],WrSzErrorCnt:0 475.283,453,I,[SoundRecorder],longWriteErrorCnt:0
После появления в консоли строки RecDuration на SD карте кристаллизуется уникальный WAV файл, содержащий запись звуковой дорожки. Тут же в консоли можно увидеть файлы CLI командой ftfs (fat_fs_scan)

Далее переустановив SD карту в LapTop PC мы видим файлы в файловой системе.

Этот файл можно проиграть любой мультимедийной утилитой. Например стандартной программой Media Player.

Архитектура прошивки получилась вот такой

В чем трудность создания диктофона?
Сделать прошивку прототипа диктофона оказалось не такой уж и тривиальной задачей. От записи к записи возникали всяческие неожиданные загвоздки:
Как это ни странно, но в FatFS API не всегда отрабатывает функция f_write(). Да... Особенно в режиме FAT32. Иной раз f_write выдает OK и правильную записанную длину, а по факту уже на PC в HexViewer выяснятеся, что местами прошивка просто пишет в wav файл вкрапления непрерывных нулей. Согласно наблюдениям нули в WAV файле образуются как раз тогда, когда функция f_write не успевает записать половинчатый массив семплов между прерываниями I2S DMA (RxHalf и RxDone). При этом, в случае работы на файловой системе FAT16 эта ошибка не воспроизводится. Обычно это происходит из-за высокой тактовой часты на линии CLK в составе шины SDIO. Если установить частоты тактирования на значение 6,84 MHz, то ошибка нулей исчезает даже на 16GB SD карточках с FAT32.

Для выявления этого явления я написал модульный тест test_fat_fs_write_verify.
Модульный тест проверки памяти FAT FS (test_fat_fs_write_verify)
bool test_fat_fs_verify_pattern( char *file_name, uint8_t pattern, const uint32_t part_size, uint32_t total_size_to_read) { LOG_INFO(TEST, "%s():File:%s Patt:0x%x", __FUNCTION__,file_name,pattern); bool res = true; uint32_t i = 0; uint32_t total_size = 0; BYTE mode = FA_READ | FA_OPEN_EXISTING ; FIL File = {0}; FRESULT ret = f_open(&File, (const TCHAR*) file_name, mode); LOG_INFO(TEST,"file_name:[%s],ret:%s ",file_name , FatFsResToStr(ret )); ASSERT_EQ(FR_OK, ret); while(1) { UINT read_effective = 0 ; memset(rdData, 0x01, sizeof(rdData)); ret = f_read( &File, (void*) rdData, part_size, &read_effective ); ASSERT_EQ(FR_OK, ret); if (0==read_effective) { break; } ASSERT_EQ(part_size, read_effective); uint32_t diff_ind = 0; res= is_arr_pat2(rdData, read_effective, pattern, &diff_ind); if(!res){ uint32_t rest_size = read_effective - diff_ind; uint32_t offset = total_size + diff_ind; LOG_ERROR(TEST,"Offset:%u,ReadArray:%s ",offset, ArrayToStr(&rdData[diff_ind] ,rest_size)); } ASSERT_TRUE(res); total_size += read_effective; i++; } ret = f_close(&File); LOG_INFO(TEST, "CloseCode:%s " , FatFsResToStr(ret )); ASSERT_EQ(FR_OK, ret); ASSERT_EQ(total_size_to_read, total_size); return res; } bool test_fat_fs_write_pattern( char *file_name, uint8_t pattern, const uint32_t part_size, uint32_t total_size_to_wr) { LOG_WARNING(TEST, "%s():file_name:[%s],", __FUNCTION__,file_name ); bool res = true; FRESULT ret; uint32_t i = 0; uint32_t total_size = 0; BYTE mode = FA_WRITE | FA_CREATE_ALWAYS; FIL File = {0}; FRESULT reto = f_open((FIL*)&File, (const TCHAR*) file_name, mode); LOG_INFO(TEST,"file_name:[%s],ret:%s ",file_name , FatFsResToStr(reto )); if(FR_EXIST==reto ) { #if 0==_FS_MINIMIZE ret = f_unlink(file_name); LOG_INFO(TEST,"DeleteFile:[%s],ret:%s ",file_name , FatFsResToStr(ret )); reto = f_open((FIL*)&File, (const TCHAR*) file_name, mode); ASSERT_EQ(FR_OK, reto); #endif } while(total_size < total_size_to_wr) { UINT written = 0; memset(wrData, pattern, part_size); ret = f_write(&File, wrData, part_size, &written); EXPECT_EQ(FR_OK, ret); EXPECT_EQ(part_size, written); if( FR_OK != ret ) { LOG_ERROR(TEST,"%u,WrError:%s",i, FatFsResToStr(ret )); break; } if(part_size!=written) { break; } total_size += written; i++; } ret = f_sync(&File); EXPECT_EQ(FR_OK, ret); ret = f_close(&File); LOG_INFO(TEST,"CloseCode:%s " , FatFsResToStr(ret )); ASSERT_EQ(FR_OK, ret); LOG_INFO(TEST,"totalSize:%u Byte", total_size ); EXPECT_EQ(total_size_to_wr, total_size); return res; } bool test_fat_fs_write_pattern_one(const char * const file_name , const uint8_t patt, const uint32_t part_size, const uint32_t size, const uint32_t pause_ms) { LOG_WARNING(TEST, "%s():[%s],0x%02x,PartSize:%u,Size:%u", __FUNCTION__,file_name, patt, part_size, size); bool res = true ; ASSERT_TRUE( test_fat_fs_write_pattern(file_name, patt, part_size, size)); wait_in_loop_ms(pause_ms);// 500< ASSERT_TRUE(test_fat_fs_verify_pattern(file_name, patt, part_size, size)); return res; } bool test_fat_fs_write_verify(void) { LOG_INFO(TEST, "%s():", __FUNCTION__); bool res = true; ASSERT_TRUE( test_fat_fs_write_pattern_one("Fil55.bin", 0x55,512, BIN_WRITE_VERIF_SIZE,2000 ) ); ASSERT_TRUE( test_fat_fs_write_pattern_one("FilFF.bin", 0xFF,512, BIN_WRITE_VERIF_SIZE,2000 ) ); ASSERT_TRUE( test_fat_fs_write_pattern_one("FilAA.bin", 0xAA,512, BIN_WRITE_VERIF_SIZE,2000 ) ); ASSERT_TRUE( test_fat_fs_write_pattern_one("Fil00.bin", 0x00,512, BIN_WRITE_VERIF_SIZE,2000 ) ); return res; }
(продолжение 1)
FatFS функция f_write() при записи сваливается в ошибку FR_INT_ERR. В этом случае файловая система на SD карте рушится и laptop PC потом не узнает файловую систему на этой SD карте. Увеличение стека прошивки в два раза частично решает эту проблему, но не полностью. Есть набор SD карт где FR_INT_ERR вообще не появляется, при этом прошивка на STM32F407VG та же самая.
Функция f_write иной раз может просто заклинить. Возвращает, что записано ноль байт и последующие записи тоже выводят 0 байт. При этом код ошибки возвращается FR_OK. Обычно такое происходит, когда в SD карте просто больше нет свободного места. Это особенно актуально на малых картах объёмом 128MByte. Поэтому перед созданием файла надо убедиться, что на SD карте есть свободное место.
Функция f_write работает относительно быстро только, если записывать блоки по 4kByte. Другие размеры обычно не проходят тест на производительность (384000 Byte/s) и запись в SD карту происходит слишком медленно. В результате происходит срыв потока, запись просто теряет свою непрерывность и смысл.
FatFS на файловой системе FAT16 работает стабильнее, чем на FAT32. В случае с FAT32 так и не прошел модульный тест на запись 1MByte с последующей проверкой записанных байт.
При включенном цифровом фильтре нижних частот аудиокодек на левый канал выдает помехи. При этом правый канал формируется с нормальным сигналом.

левый канал с помехами По непонятным мне причинам, самая стабильная работа диктофона наблюдается на SD карте размером 128MByte (форматирование FAT16). Даже не знаю откуда у меня такая реликтовая SD карта.

На высоких частотах тактирования SDIO может произойти осечка при монтировании файловой системы FAT32. В этих случаях надо понижать тактовую частоту тактирования интерфейса SDIO.
В финальной WAV записи на левый канал эпизодически просачиваются вкрапления подозрительных одиночных пиков.

Водянистый звуковой сигнал
Итог
В результате мне удалось написать STM32F407VG-прошивку для I2S - совместимого диктофона c возможностью записывать WAV файлы на SD карту по SDIO с файловой системой FAT16 на основе широко распространенной учебной платы dev_ebox_stm32f4x и такого же распространенного отладочного аудиокодека WM8731. Бинари микропрограммы представлены на github. Исходный код прошивки диктофона тут же.
Думаю не требуется перечислять все те возможные приложения, где может потребоваться запись звука. Надеюсь этот текст поможет кому-нибудь добавить возможность звукозаписи в свои проекты.
Ссылки
Название | URL |
Диктофоны для книги рекордов | |
Диктофон и медицина | |
Проигрыватель WAV файлов на STM32 | |
Обзор диктофона Edic Weeny A110 с функцией «чёрного ящика» | |
Подключение SD карты по SPI (Капсула памяти) | |
Конечный Aвтомат Аппаратного I2C-Трансивера | |
Медленная работа SD карточек — кто виноват и что делать? | |
Пуск I2S трансивера на Artery | |
Audacity | |
Запуск I2S Трансивера на Artery [часть 2] (DMA, FSM, PipeLine) | |
FatFs - Generic FAT Filesystem Module | |
Сравнение микросхем аудио кодеков | https://docs.google.com/spreadsheets/d/1r7tuNrQ8PPfIVcM2FeGpwa1eeWjyq41CD0MGq_Q_VV8/edit?gid=0#gid=0 |
STM32 - uSD - SDIO 4bit - DMA | |
Исходный код прошивки диктофона | https://github.com/aabzel/trunk/tree/main/source/projects/dev_ebox_stm32f4x_sound_recorder_gcc_m |
Чип AudioСodec(а) WM8731 (или (ADC/DAC)*2 из iPod(а)) | |
Wave File Format - формат звукового файла WAV | |
Разбор I2S трафика в программе Logic 2 | |
Цифровой звук на STM32: подключаем аналоговый микрофон через SAI и NAU88C22 | |
Обзор Aппаратного Aудио кодека MAX9860 (2x ADC+DAC) | |
Как Работать с UART на Микроконтроллерах (UART + FIFO = LOG) @danil_12345 | |
Настройка ToolChain(а) для Win10+GCC+С+Makefile+ARM Cortex-Mx+GDB | |
Формат WAV-файлов | http://solidstate.karelia.ru/p/tutorial/asni/Glava 2/Index3.htm |
Отладка STM32 программатором J-LINK по SWD | |
Обзор AудиоКодека NAU8814YG | |
dev_ebox_stm32f4x_sound_recorder_gcc_m | https://github.com/aabzel/Artifacts/tree/main/dev_ebox_stm32f4x_sound_recorder_gcc_m |
утилита H2testw.exe для испытания SD карт |
Вопросы
Что если реальная звуковая дорожка не совпадает с тем, что указано в заголовке WAV файла?
Существует ли формат аудиозаписи, который в бинарном виде хранит такие метаданные как точное время начала записи и геолокацию места, где была инициирована данная запись звука?
Как на Си изменить конкретный массив в середине бинарного файла, зная отступ от начала файла, размер заменяемого фрагмента и новое значение размера массива? В качестве API файловой системы используется Fat-FS.
Как измерить реальную поддерживаемую скорость записи данной конкретной microSD карты?
Существуют ли в продаже готовые отладочные платы с ARM Cortex M4 микроконтроллером (STM32F4X), каким-н простым аудиокодеком как WM8731, audio-jack портов для наушников, микрофона (и желательно LineIn), гаражом для microSD карты подключаемой по интерфейсу SDIO (или SPI), LEDом и желательно графическим дисплеем? Чтобы можно было полноценно прототипировать и отлаживать прошивки диктофонов и плейеров.
Как проверить, что диктофонная запись звука в самом деле получилась непрерывная? Как убедиться, что звукозаписывающая аппаратура не пропустила 10-30 миллисекунд (или не добавила лишние семплы) ?
Существует ли win утилита для чтения регистров SD карты: Card-specific data, Card IDentification register, SD Status register, Card Status Register, operation conditions register, SD Card Configuration register, Drive stage register , relative card address register, при её подключению к персональному компьютеру с операционной системой Windows 10?
Поддерживают ли microSD карты “горячее подключение”?
