В этом тексте я покажу, как из подручных материалов на основе микроконтроллера, аудиокодека и 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
DevEBox STM32F4XX_M_V3

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

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



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

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

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

микрофон aceline AMIC-7
микрофон 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 карту
утилита H2testw проверяет SD карту

Утилита 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

Для увеличения скорости записи пришлось принять ряд совсем неочевидных мер:

  1. Увеличить частоту ARM Cortex-M4 ядра до максимума: 168MHz (было 100 MHz). Так системная шина тактирует SDIO MAC с максимальной частотой 84MHz.

  2. Установил подтяжки к питанию для всех 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 |
    +-----+-------+--------+-------+------+------+-----+-----+---------+
  3. Записывать блоки размером 4k Byte за раз.

  4. Перевести SDIO на 4-битный режим. То есть задействовать параллельную шину.

  5. Работать в SDIO в режиме DMA2.

  6. Закомментировать отладочное логирование в коде SDIO операций чтения и записи.

  7. Повысить приоритет DMA каналов для SDIO до максимума.

  8. Повысить приоритет прерываний для DMA каналов SDIO до максимума.

  9. Отформатировать SD карту в файловую систему FAT16. Назначить Allocation Unit size равным 16 kбайт.

  10. Увеличил тактирование на SDIO тактировании до 24 MHz (максимальное).

  11. Увеличил стековую 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.

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

В чем трудность создания диктофона?

Сделать прошивку прототипа диктофона оказалось не такой уж и тривиальной задачей. От записи к записи возникали всяческие неожиданные загвоздки:

  1. Как это ни странно, но в 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. (продолжение 1)

  2. FatFS функция f_write() при записи сваливается в ошибку FR_INT_ERR. В этом случае файловая система на SD карте рушится и laptop PC потом не узнает файловую систему на этой SD карте. Увеличение стека прошивки в два раза частично решает эту проблему, но не полностью. Есть набор SD карт где FR_INT_ERR вообще не появляется, при этом прошивка на STM32F407VG та же самая.

  3. Функция f_write иной раз может просто заклинить. Возвращает, что записано ноль байт и последующие записи тоже выводят 0 байт. При этом код ошибки возвращается FR_OK. Обычно такое происходит, когда в SD карте просто больше нет свободного места. Это особенно актуально на малых картах объёмом 128MByte. Поэтому перед созданием файла надо убедиться, что на SD карте есть свободное место.

  4. Функция f_write работает относительно быстро только, если записывать блоки по 4kByte. Другие размеры обычно не проходят тест на производительность (384000 Byte/s) и запись в SD карту происходит слишком медленно. В результате происходит срыв потока, запись просто теряет свою непрерывность и смысл.

  5. FatFS на файловой системе FAT16 работает стабильнее, чем на FAT32. В случае с FAT32 так и не прошел модульный тест на запись 1MByte с последующей проверкой записанных байт.

  6. При включенном цифровом фильтре нижних частот аудиокодек на левый канал выдает помехи. При этом правый канал формируется с нормальным сигналом.

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

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

  9. В финальной WAV записи на левый канал эпизодически просачиваются вкрапления подозрительных одиночных пиков.

    Водянистый звуковой сигнал
    Водянистый звуковой сигнал

Итог

В результате мне удалось написать STM32F407VG-прошивку для I2S - совместимого диктофона c возможностью записывать WAV файлы на SD карту по SDIO с файловой системой FAT16 на основе широко распространенной учебной платы dev_ebox_stm32f4x и такого же распространенного отладочного аудиокодека WM8731. Бинари микропрограммы представлены на github. Исходный код прошивки диктофона тут же.

Думаю не требуется перечислять все те возможные приложения, где может потребоваться запись звука. Надеюсь этот текст поможет кому-нибудь добавить возможность звукозаписи в свои проекты.

Ссылки

Название

URL

Диктофоны для книги рекордов

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

Диктофон и медицина
@555www555

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

Проигрыватель WAV файлов на STM32

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

Обзор диктофона Edic Weeny A110 с функцией «чёрного ящика»
@EgorKotkin

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

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

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

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

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

Медленная работа SD карточек — кто виноват и что делать?
@GarryC

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

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

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

Audacity

https://www.audacityteam.org/

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

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

FatFs - Generic FAT Filesystem Module

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

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

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

STM32 - uSD - SDIO 4bit - DMA

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

Исходный код прошивки диктофона

https://github.com/aabzel/trunk/tree/main/source/projects/dev_ebox_stm32f4x_sound_recorder_gcc_m

Чип AudioСodec(а) WM8731 (или (ADC/DAC)*2 из iPod(а))

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

Wave File Format - формат звукового файла WAV

https://microsin.net/programming/pc/wav-format.html

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

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

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

@a3x

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

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

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

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

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

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

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

Формат WAV-файлов

http://solidstate.karelia.ru/p/tutorial/asni/Glava 2/Index3.htm

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

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

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

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

dev_ebox_stm32f4x_sound_recorder_gcc_m

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

утилита H2testw.exe для испытания SD карт

https://www.softportal.com/getsoft-45617-h2testw-1.html

Вопросы

  1. Что если реальная звуковая дорожка не совпадает с тем, что указано в заголовке WAV файла?

  2. Существует ли формат аудиозаписи, который в бинарном виде хранит такие метаданные как точное время начала записи и геолокацию места, где была инициирована данная запись звука?

  3. Как на Си изменить конкретный массив в середине бинарного файла, зная отступ от начала файла, размер заменяемого фрагмента и новое значение размера массива? В качестве API файловой системы используется Fat-FS.

  4. Как измерить реальную поддерживаемую скорость записи данной конкретной microSD карты?

  5. Существуют ли в продаже готовые отладочные платы с ARM Cortex M4 микроконтроллером (STM32F4X), каким-н простым аудиокодеком как WM8731, audio-jack портов для наушников, микрофона (и желательно LineIn), гаражом для microSD карты подключаемой по интерфейсу SDIO (или SPI), LEDом и желательно графическим дисплеем? Чтобы можно было полноценно прототипировать и отлаживать прошивки диктофонов и плейеров.

  6. Как проверить, что диктофонная запись звука в самом деле получилась непрерывная? Как убедиться, что звукозаписывающая аппаратура не пропустила 10-30 миллисекунд (или не добавила лишние семплы) ?

  7. Существует ли 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?

  8. Поддерживают ли microSD карты “горячее подключение”?

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы делали запись звука на основе микроконтроллера?
45%да9
55%нет11
Проголосовали 20 пользователей. Воздержался 1 пользователь.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы работали с интерфейсом I2S?
65%да13
35%нет7
Проголосовали 20 пользователей. Воздержался 1 пользователь.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы работали с файловой системой FATFS?
68.42%да13
31.58%нет6
Проголосовали 19 пользователей. Воздержался 1 пользователь.