Как стать автором
Обновить
Selectel
IT-инфраструктура для бизнеса

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

Уровень сложностиСредний
Время на прочтение16 мин
Количество просмотров9.4K

ИИ обложка, потому что модно.

Иногда простой проект превращается в увлекательное исследование. Разбираясь с записью звука на STM32L432 через аудиокодек NAU88C22, я не только подключил микрофон и настроил кодек, но и столкнулся с рядом интересных задач. Разбираясь с ними, я узнал много нового о SAI, работе с SD-картой и нюансах цифрового звука — и теперь хочу поделиться этим опытом.

Железка, на которой идет разработка, — это не просто плата для экспериментов, а прототип, сделанный как образец для одного проекта. Так что любопытство, конечно, присутствовало, но двигало процесс скорее желание довести до ума конкретное (будущее) устройство.

Немного о железе


В основе устройства лежит STM32L432 KCUx — микроконтроллер из линейки Ultra-low-power, потому что работать ему предстоит от аккумулятора.

Основные характеристики
  • CPU: ARM Cortex-M4, до 80 МГц (и да, где-то с компьютерных небес на меня укоризненно смотрит мой первый компьютер на Pentium 75).

    В Low power режиме частота снижается до 26 МГц (но нас это пока не интересует).
  • ОЗУ: до 64 КБ (48 КБ основного SRAM + 16 КБ SRAM2).
  • Аккумулятор: Li-Ion 3.7В 800 мАч.


Если у вас есть вопросы по плате, их можно задать автору the_bat. А прочитать подробнее про процесс разработки платы можно по ссылке.

На первый взгляд — задача несложная, ведь у STM32 есть поддержка SAI-интерфейса, кодек управляется по I2C, ну а SD-карта — это база!

SAI (Serial Audio Interface) — это интерфейс для передачи цифрового аудио, который является более гибким и универсальным по сравнению с I2S (Inter-IC Sound).
Основные отличия
  • I2S — классический интерфейс для передачи двухканального аудио, чаще используется в простых аудиосистемах.
  • SAI поддерживает больше форматов (PCM, TDM, AC'97), больше каналов (до 8 стереопотоков) и гибкие настройки таймингов, что делает его удобнее для сложных аудиопроектов.


В этой статье я поделюсь опытом работы с STM32L432 и NAU88C22: от поиска примеров кода до первых успешных записей. Разберемся, как настроить SAI, какие параметры важны для кодека, как сохранить данные в формате WAV и какие проблемы могут возникнуть.
Если у вас есть идеи, как можно улучшить этот проект, — в конце статьи я буду рад советам!


Поиск примеров


В самом чипе серии STM32L4 нет ничего такого особенного (я пока не заметил) по сравнению с чипами серий STM32Fx, поэтому вопрос создания базового проекта — запуск периферии и клоков поднимать смысла нет.

Остановимся подробнее на трех аспектах
  1. Настройка интерфейса SAI.
    До этого у меня не было никакого опыта работы с аудиоинтерфейсами вроде I2S и SAI, поэтому первым шагом стало изучение существующих примеров. К сожалению, в STM32Cube готовых демо-проектов для работы с SAI в STM32L4 нет, но удалось найти BSP-драйвер для другого микроконтроллера той же серии. Поэтому пришлось разбираться, портировать код, исправлять ошибки и собирать на этом пути все возможные грабли.
  2. Аудио кодек и управление им по I2C
    Готового драйвера под STM для кодека я не нашел, но в качестве референса был использован драйвер из официального репозитория Nunoton и даташит.
  3. Подключение SD-карты через SPI и использование файловой системы FATFS.
    Что касается записи данных на SD-карту через SPI и файловую систему FATFS, тут ситуация обстояла куда проще. В сети нашлось достаточно примеров, которые помогли быстро запустить базовую работу с картой. Однако, как позже выяснилось, даже в этой части проекта были подводные камни, о которых расскажу далее.

Драйвер кодека NAU88C22


Портирование драйвера кодека оказалось не самым простым занятием. Официальный репозиторий Nuvoton действительно содержит драйвер, но он написан не под STM32 и не использует HAL, поэтому по факту надо было написать свой. Главная сложность, как это часто бывает, заключалась в необходимости внимательно читать даташит.

Оказывается, передача данных по I2C для этого кодека устроена немного хитрее: сначала передается адрес кодека, затем 7 бит адреса регистра, а в младший бит первого октета данных нужно записать старший бит (9-й) значения регистра. Это было неочевидно, но стоило внимательно вглядеться в описание или схему в даташите — и все сразу стало логичным. После этого запись и чтение регистров заработали корректно:


HAL_StatusTypeDef writeToCodec(uint8_t reg, uint16_t value)
{
    HAL_StatusTypeDef status;
    uint8_t data[2];

    data[0] = (reg << 1) | ((value >> 8) & 0x01);
    data[1] = value & 0xFF;

    status = HAL_I2C_Master_Transmit(&hi2c1, NAU8822_ADDR, data, 2, HAL_MAX_DELAY);

    if(status != HAL_OK)
    {
        Error_Handler();
    }

    return status;
}

uint16_t readFromCodec(uint8_t reg)
{
	uint8_t data[2] = {0};
	uint16_t result = 0xFFFF; //error

	if (HAL_I2C_Master_Transmit(&hi2c1, NAU8822_ADDR, &(uint8_t){reg << 1}, 1, HAL_MAX_DELAY) != HAL_OK)
		return result;

	if (HAL_I2C_Master_Receive(&hi2c1, NAU8822_ADDR, data, 2, HAL_MAX_DELAY) != HAL_OK)
		return result;

	result = ((data[0] & 0x01) << 8) | data[1];

	return result;
}


Дальше, ориентируясь на схему, я настроил все регистры, выставил нужные переключатели и перевел кодек в режим MASTER, чтобы он сам генерировал FS и BCLK. Казалось бы, все должно работать, но на линии ADCOUT не было осмысленных данных. Вернее, они были, но не соответствовали подаваемому сигналу: вместо синуса на входе кодека в DMA прилетали нули. И тут даташит приготовил для меня очередной сюрприз. Оказалось, что в одной из версий документа (той, что я использовал) была ошибка в значении регистра, отвечающего за режим работы кодека.


По схеме микрофон подключен к правому входу RMICP, а для получения его сигнала в выходном PCM-потоке кодек нужно было перевести в стерео-режим. Однако из-за неправильного значения регистра кодек включался в монорежиме и передавал только левый канал, которого у меня просто не было. После исправления ошибки звук наконец появился.

Разбираемся с SAI и его настройками


Настало время разобраться с настройкой SAI. Как он работает — все еще нет ни малейшего представления. Поэтому, следуя проверенному методу, берем готовый пример из BSP и начинаем вырезать все лишнее.

— DFSDM-вход от цифрового микрофона?
— До свидания.
— Воспроизведение звука на динамик?
— До свидания.

Теперь осталось только самое необходимое, и это выглядит ужасно. Только взгляните на монструозный набор функций, который вызывается при инициализации аудиовхода! Даже не буду приводить их содержимое — достаточно посмотреть на имена, чтобы понять, насколько все продумано (нет).

  • BSP_AUDIO_IN_Init
  • MX_SAI1_ClockConfig
  • SAI_MspInit
  • MX_SAI1_Init2 (моя версия MX_SAI1_Init, по аналогии с той, что была в BSP)
  • HAL_SAI_Init
  • HAL_SAI_MspInit

Тут можно оставить немного места, чтобы хейтеры HAL и прочих упрощающих жизнь фреймворков могли вставить свои комментарии о том, как они написали то же самое на ассемблере и без всяких HAL. Но не все такие как вы, так что идем дальше.

Лишний код почищен, проект собирается. Теперь надо понять, как же устроен цифровой поток интерфейса SAI.

Режимы работы SAI и основные параметры


SAI в STM32 поддерживает несколько режимов работы, включая I2S и PCM.
  1. I2S (Inter-IC Sound) — классический интерфейс для передачи цифрового аудио, используемый в большинстве аудиоустройств. В этом режиме данные передаются в виде потоков, синхронизированных с битовым (BCLK) и кадровым (FS) тактированием. Обычно используется стереоформат, где один слот предназначен для левого канала, а второй — для правого.
  2. PCM (Pulse Code Modulation) — режим, ориентированный на обработку голосовых данных и телекоммуникационные приложения. Он позволяет гибко настраивать количество слотов и их длину, что полезно, например, для работы с несколькими каналами или временным мультиплексированием (TDM).

Слоты и их параметры
Слоты используются для разделения аудиоданных внутри одного кадра (фрейма). Каждый слот может представлять отдельный канал (левый/правый в стерео) или даже отдельного пользователя в многоканальных системах.
  • Длина слота (Slot Size) определяет, сколько бит данных занимает каждый слот. Она может быть равна ширине данных (16, 24, 32 бита) или больше, если необходимо выровнять данные.
  • Число слотов в кадре зависит от режима работы. В I2S — обычно два слота (левый и правый каналы), в PCM/TDM — может быть один для монозвука или больше.

Формирование кадров (Frame Synchronization) определяет границы кадров и их структуру
  • Активный уровень FS (активный низкий/высокий) указывает, когда начинается новый кадр.
  • Ширина FS может быть кратковременной (один бит) или равной половине одного фрейма.

Дополнительные параметры включают
  • Bit Order — порядок передачи бит (MSB или LSB first).
  • Data Size — глубина данных (16, 24, 32 бита).
  • Clock Strobing — по какому фронту тактовых импульсов считываются данные (восходящий или нисходящий).

Все эти параметры определяют совместимость SAI с конкретными аудиоустройствами и форматами данных. Тут хорошим подспорьем будет презентация ST по работе SAI в STM32L4.Непосредственно режим PCM в презентации не нарисован, но чтобы его активировать — надо установить.

FSDefinition = SAI_FS_STARTFRAME
В данном случае сигнал синхронизации фрейма (FS) указывает только на начало фрейма, а не остается активным на протяжении всего фрейма, как в режиме I2S (SAI_FS_CHANNEL_IDENTIFICATION), где FS-сигнал используется для идентификации канала.

Длина сигнала синхронизации определяется параметром ActiveFrameLength = 1, что означает, что сигнал синхронизации активен в течение одного периода тактового сигнала BCLK. Аудиоданные в слоте начинаются сразу после сигнала синхронизации, с заданным смещением от его начала. Это смещение определяется параметром FSOffset = SAI_FS_BEFOREFIRSTBIT. Аудиоданные передаются со старшего бита, что задается параметром FirstBit = SAI_FIRSTBIT_MSB.

Для наглядности нарисовал картинку, где совместил временную диаграмму сигналов из презентации SAI и диаграмму из даташита на кодек:


Настройка DMA для работы с SAI


Для эффективной передачи аудиоданных в SAI необходимо настроить DMA (Direct Memory Access), который позволит автоматически загружать данные без участия процессора. Это критично, так как аудиопоток требует стабильной передачи с фиксированной частотой, а прерывания от CPU могут вызывать задержки и артефакты в звуке.

STM32L432 не поддерживает режим двойного буфера, поэтому для приема непрерывного потока данных DMA необходимо настроить в circular-режиме. В этом режиме DMA будет генерировать прерывания при заполнении половины буфера (HT) и при завершении передачи (TC). Между этими прерываниями необходимо успеть обработать и сохранить данные из буфера, иначе их можно утерять.

Отсюда следует, что размер приемного буфера должен быть как можно больше, исходя из доступной SRAM. Рассчитать размер можно по формуле: Данные в секунду = SAMPLE_RATE × количество байт на отсчет × количество каналов.

Вот таблица с расчетами количества данных, генерируемых за секунду при разных частотах дискретизации для моно- и стереосигнала с разрядностью 16 бит (2 байта на отсчет):

Частота дискретизации (Гц) Размер данных за 1 сек (моно, байт) Размер данных за 1 сек (стерео, байт)
8 000 16 000 32 000
16 000 32 000 64 000
48 000 96 000 192 000

Выбор канала и запроса DMA


В STM32 для работы с SAI необходимо выбрать правильный канал (Stream) и запрос (Request) в соответствии с таблицей привязок периферии к DMA (указано в Reference Manual). Например, для STM32L4:

  • SAI1_A → DMA1 Channel1, Request 2
  • SAI1_B → DMA1 Channel2, Request 3

Настройки передачи


Режим работы

  • Circular mode — позволяет бесконечно перезаполнять буфер, используя двойную буферизацию (удобно для непрерывной записи/воспроизведения).
  • Normal mode — используется, если необходимо разово записать или считать данные.

Размер данных

  • Определяется разрядностью аудиоформата. Если используется 16-битное аудио, то размер слова DMA должен быть Half-Word (16 бит).
  • Для 24/32 бит — Word (32 бита).

Инкремент адреса
  • Источник: фиксированный адрес (Peripheral address static) — т.к. SAI регистр данных всегда один.
  • Приемник: инкремент (Memory address increment) — т.к. данные записываются в буфер.

Приоритет
Если в системе работает несколько DMA-каналов, для SAI лучше выбрать High Priority, чтобы минимизировать задержки. Если канал один, как в нашем случае приоритет значения не имеет.

Настройки прерываний DMA
Для организации «двойной» буферизации удобно использовать прерывания:

  • XferHalfCpltCallback (половина буфера заполнена) — можно начинать обработку первой половины данных.
  • XferCpltCallback (полный буфер заполнен) — обрабатываем вторую половину, пока DMA заполняет первую половину заново.

Клоки


Аудиокодек NAU88C22 требует стабильного тактового сигнала MCLK, который может быть сформирован как самим STM32, так и внешним генератором. В зависимости от конфигурации системы возможны два режима работы: мастер, когда тактирование всех аудиосигналов (MCLK, BCLK, FS) обеспечивает микроконтроллер, и слейв, когда генерация тактовых сигналов передается на кодек, а STM32 лишь принимает данные.

Микроконтроллер в режиме MASTER


В этом случае STM32 генерирует все необходимые тактовые сигналы:
  • MCLK (Master Clock) — основной тактовый сигнал для аудиокодека, обеспечивающий работу его внутренних PLL. Если на вход подается 12,288 МГц и работа ведется на частотах, кратных 8 000 Гц (например, 8 000, 16 000, 48 000 Гц), то в кодеке используются предделители и делители для получения необходимых тактовых частот. Если же работа идет на частотах, не кратных 8 000 Гц, например 44 100 Гц, то для генерации нужных частот кодек использует встроенный PLL, который подстраивает частоту тактирования под требуемую дискретизацию.
  • BCLK (Bit Clock) — тактовый сигнал для передачи битов аудиоданных в интерфейсе SAI.
  • FS (Frame Synchronization) — сигнал синхронизации начала фрейма передачи данных.

Когда микроконтролер выступает в роли мастера, все тактовые частоты рассчитываются автоматически на основе настроек PLL. Например, при частоте дискретизации:
  • FS = 48 000 Гц → BCLK = 1,536 МГц, MCLK = 12,288 МГц
  • FS = 8 000 Гц → BCLK = 256 кГц, MCLK = 2,048 МГц

И здесь возникает проблема: аудиокодек NAU88C22 требует, чтобы частота MCLK была не менее 12 МГц. Это означает, что в режиме STM32 MASTER корректная работа возможна только при частоте дискретизации 48 000 Гц.

Микроконтроллер в режиме SLAVE


В этом варианте тактирование полностью отдается на сторону кодека:

  • внешний генератор 12,288 МГц подключается к входу MCLK кодека;
  • кодек сам формирует FS и BCLK и передает их микроконтроллеру.

Изначально предполагалось, что STM32 будет работать в режиме MASTER, но из-за ограничений по частоте MCLK было решено доработать схему: допаять внешний генератор и перевести микроконтроллер в режим SLAVE RX.

Пока я пытался заставить работать BSP-драйвер и разобраться в настройке аудиовхода, постепенно начал понимать, как все устроено. В какой-то момент я осознал, что уже настолько разобрался в конфигурации SAI, работе DMA и настройке периферии, что весь этот BSP мне попросту больше не нужен.


Он только усложнял процесс и добавлял лишний код, который приходилось вычищать. Поэтому я взял и выкинул все, что было связано с BSP, и просто настроил SAI и DMA вручную, используя стандартные возможности CubeIDE. В итоге код стал значительно чище, понятнее и не зависел от ненужных обвязок.

Работа с SD картой


На нашем микроконтроллере STM32L432 всего 26 ног, из которых почти все заняты. Поэтому для подключения SD-карты доступен только SPI-интерфейс. Это не самый быстрый способ работы с SD-картами, но выбора у нас нет.

Для начала обращаемся к тому же FW Pack’у, в котором есть BSP-драйвер для работы с SD-картой через SPI. Портируем его под наш чип, пока еще не осознавая, сколько в нем ненужного кода. Поэтому наслаждаемся классической ситуацией, когда ежики кололись, плакали, но продолжали есть кактус. Однако по сравнению с SAI здесь все оказалось проще: SPI-интерфейс достаточно стандартный, а после небольшой возни с настройками он заработал.

Настройки SPI
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_128;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 7;
hspi1.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
hspi1.Init.NSSPMode = SPI_NSS_PULSE_DISABLE;


Здесь все довольно стандартно: SPI работает в режиме мастера, передача данных идет в 8-битном формате, тактирование настраиваем по первому фронту (POLARITY_LOW, PHASE_1EDGE), NSS управляется вручную. Предделитель 128 выбран для того, чтобы скорость работы не была слишком высокой для SD-карты (особенно при инициализации). Дальше при монтировании SD-карты делитель частоты SPI_BAUDRATEPRESCALER может быть уменьшен (есть соответствующий дефайн в драйвере), для увеличения частоты работы интерфейса.

Дальше остается только настроить FATFS и протестировать запись/чтение файлов.
Монтируем SD-карту, с помощью стандартных функций FATFS открываем файл (f_open), записываем данные (f_write), закрываем файл (f_close) — все просто и без сюрпризов. Вернемся к этому вопросу немного позже, а пока надо данные из DMA собрать и подготовить для записи.

WAV-файл: теория и структура


Для хранения аудиоданных выберем формат WAV — это один из самых простых и распространенных форматов для хранения несжатого звука.

Краткая справка о WAV
WAV (Waveform Audio File Format) — это контейнерный формат для хранения аудиоданных, разработанный Microsoft и IBM. WAV использует RIFF-структуру (Resource Interchange File Format), которая организует данные в виде последовательности чанков (блоков). Самый распространенный вариант WAV — это несжатый PCM (Pulse Code Modulation), где данные представляют собой оцифрованные значения аудиосигнала без дополнительного сжатия.

Заголовок WAV
Заголовок WAV занимает 44 байта и содержит основную информацию о файле:

  • тип файла (RIFF, WAVE);
  • параметры кодирования (битность, частота дискретизации, количество каналов);
  • размер данных.

Для удобного формирования заголовка используем следующую структуру:

#pragma pack(push, 1)
typedef union
{
	uint8_t array[44];
	struct
	{
    	// RIFF Header
    	char riff_header[4];    	// "RIFF"
    	uint32_t wav_size;        	// Размер файла - 8 байт
    	char wave_header[4];    	// "WAVE"
    	// Format Header
    	char fmt_header[4];        	// "fmt "
    	uint32_t fmt_chunk_size;	// 16 (для PCM)
    	uint16_t audio_format;    	// 1 = PCM
    	uint16_t num_channels;    // 1 = моно, 2 = стерео
    	uint32_t sample_rate;    	// Частота дискретизации
    	uint32_t byte_rate;        	// sample_rate * num_channels * (bit_depth / 8)
    	uint16_t sample_alignment;// num_channels * (bit_depth / 8)
    	uint16_t bit_depth;        	// Битность (16 бит, 24 бита и т.д.)
    	// Data
    	char data_header[4];    	// "data"
    	uint32_t data_bytes;    	// Размер аудиоданных
	}
	Param;
} wav_header;
#pragma pack(pop)

Зачем нужны #pragma pack(push, 1) и #pragma pack(pop)?

Эти директивы компилятора отключают автоматическое выравнивание структуры в памяти, чтобы ее поля записывались в файл строго байт в байт, без добавления лишних выравнивающих байтов. Это важно, потому что формат WAV требует четкого расположения полей в файле.

Заполнение заголовка WAV

Вот как выглядит заполненный заголовок для записи монофонического 16-битного звука с частотой 16 кГц:

wav_header header =
{
	.param.riff_header = "RIFF",
	.param.wav_size = (sizeof(RecordBuff1)*NUM_ITERATIONS) + 468 + 44 - 8,
	.param.wave_header = "WAVE",
	.param.fmt_header = "fmt ",
	.param.fmt_chunk_size = 16,
	.param.audio_format = 1,
	.param.num_channels = 1,
	.param.sample_rate = 16000,
	.param.byte_rate = 16000 * 1 * 16 / 8,
	.param.sample_alignment = 1 * 16 / 8,
	.param.bit_depth = 16,
	.param.data_header = "data",
	.param.data_bytes = sizeof(RecordBuff1)*NUM_ITERATIONS + 468
};

Внимательный читатель заметит странную добавку +468 байт в wav_size и data_bytes. Это сделано специально, чтобы размер данных был кратен 512 байтам — размеру сектора на SD-карте. Такой подход позволяет минимизировать количество операций записи, увеличивая производительность работы с картой.

Записываем звук


Кодек настроен, SAI настроен, файлы на SD-карту писать умеем — пора запускать запись!

#define BUFFER_SIZE 16384
static volatile int16_t RecordBuff1[BUFFER_SIZE];
HAL_SAI_Receive_DMA(&hsai_BlockA1, (uint8_t*)RecordBuff1, (uint16_t)(BUFFER_SIZE));

Важный момент: в HAL_SAI_Receive_DMA передаем указатель на приемный буфер, который должен быть защищен от оптимизаций компилятора с помощью static volatile, чтобы он не решил, что его можно не обновлять (а он может). Также передаем размер данных (в байтах) — половину буфера RecordBuff1.

Как это работает

  • Когда DMA накопит половину буфера RecordBuff1, сработает прерывание XferHalfCpltCallback, в котором мы забираем первую половину данных из буфера и пишем их на SD-карту.
  • Когда буфер заполнится полностью, сработает XferCpltCallback, где мы забираем вторую половину буфера и тоже записываем ее на карту.
  • Затем цикл повторяется: первая половина → вторая половина → и так далее.


Теория vs. практика

Звучит просто. На практике — пришлось помучиться (наше все). Во время отладки выяснилось, что при срабатывании прерывания по готовности половины буфера он почему-то уже полный. То есть, под отладчиком было видно, что DMA собрал не только первую, но и вторую половину.

Это был момент жестокого разочарования. Оказалось, что отлаживать DMA привычными методами не получится, поскольку он работает автономно, не затрачивая ресурсы процессора — и дебаггер на него никак не влияет. Я конечно это знал, но знатно потупить это не помешало. Но теперь все работает как надо:

  • запускаем DMA,
  • собираем половину буфера,
  • начинаем запись на SD-карту,
  • за это время набирается вторая половина буфера,
  • записываем вторую половину,
  • повторяем.

И вот он — первый аудиофайл! 🎉

А теперь самая боль всего этого процесса


Но прежде чем все так складно получилось (кодек, SAI, SD-карта), прошли десятки итераций, где каждая новая проблема открывала дверь в еще более увлекательный мир отладки:

  1. Запустили отладку. Настроили все, нажали «Старт» — ничего не работает.
  2. Посмотрели осциллографом клоки. Ага, что-то дергается, но выглядит подозрительно.
  3. Включили синус. Отправили на вход простейший сигнал, чтобы не гадать, что там за шум записался.
  4. Записали кусок файла на SD-карту. Ну, вроде что-то пишется...
  5. Вставили карту в компьютер, открыли Audacity. Посмотрели, что же там получилось. Послушали. Ужаснулись.
  6. Вспомнили, что настройки в кодеке и в SAI не совпадают. Кто бы мог подумать, что у источника и приемника должны быть одинаковые параметры!
  7. Вспомнили, что на SD-карте уже есть файл с таким именем. Новый файл не создался, а просто дописался в конец старого. Это был сюрприз.
  8. Вспомнили, что частота процессора — 16 МГц. А на такой частоте SPI не успевает записать данные на SD-карту за отведенное время.
  9. Вспомнили, что еще примерно десяток разного рода важных вещей. О которых узнал только на своем личном опыте. И которые обязательно всплывут еще раз в будущем.
  10. Ругаюсь, вношу изменения и возвращаюсь к пункту 1. 🔄🔄🔄


И так — раз за разом, снова и снова, пока все, наконец, не заработало так, как должно.

Почти как надо

Теперь, когда основные грабли пройдены, можно детальнее остановиться на том, как именно все настроено.

  • Частота дискретизации — 16 000 Гц. Конечно, хочется больше, но пока не будем спешить.
  • Длина слова — 16 бит. Вполне стандартно.
  • Монорежим. Микрофон перепаян на левый канал, чтобы не приходилось использовать стерео и принимать 16 лишних бит пустого левого канала.
  • Формат PCM. Позволяет использовать один слот SAI — в отличие от I2S, где слотов должно быть четное количество.
  • Тактовая частота CPU — 60 МГц. Почему не 80 МГц? Потому что в этом случае SPI начинает генерировать лишние клоки, ломая передачу данных. Errata говорит: «Привет!»
  • Размер буфера приема — 16 384 int16_t (максимально возможный размер, кратный 512 байтам).
  • Дополнительный буфер в SRAM2. Заполняется по прерываниям (половина и полный буфер) и уже из него данные копируются на SD-карту.
  • Запись на SD-карту. В начало каждого WAV-файла сразу после заголовка добавлены 468 байт нулей (44 + 468 = 512), чтобы данные из буфера выравнивались по 512 байтам. Это нужно, чтобы избежать переносов между секторами SD-карты (пока не до конца понятно, но интуиция говорит, что так надо).

И вот теперь можно сказать, что данные принимаются и записываются корректно.

Что должно было быть и что получилось


Идеально, устройство должно писать два канала, 44,1 кГц, 16 бит — и делать это 20 часов подряд.

Что есть сейчас

  • 1 канал
  • 16 кГц
  • 16 бит
  • ~16 часов непрерывной записи

Для начала — неплохо, но вот что можно было бы исправить:

  • Процессор STM32L452 вместо STM32L432. У него 160 кБ SRAM (вместо 64 кБ), что позволит накапливать буфер в 2,5 раза больше и реже писать на SD-карту. Запись на SD-шку — это один из главных потребителей энергии, так что это реально даст прирост по времени работы.
  • Отказаться от FATFS и писать данные в NAND. Сейчас FATFS использовался ради удобства (достал карту → вставил в ПК → сразу есть файл). Но если писать сырые данные прямо в NAND, можно сократить накладные расходы. Минус — тогда придется разрабатывать механизм выгрузки NAND через USB. Пока это избегали, потому что SD-шка проще.
  • Запись в NAND через SPI DMA. Теоретически можно было бы настроить SPI с FATFS через DMA (меньше загрузка процессора). Очень теоретически. Реализаций не нашел, а возиться с этим сейчас не хотелось.


Но это уже вопросы будущего, если этот прототип станет реальным проектом.

Заключение


Это были увлекательные пару месяцев. SAI освоено, ачивка получена!


Но есть нюанс. Если записать чистый синус (или даже голос, но это менее заметно), то примерно с периодичностью записи на SD-карту в записанном сигнале появляется помеха (щелчки). В Audacity это можно увидеть как резкие изменения амплитуды сигнала. На временной диаграмме сверху записана тишина, на нижней тишина, но в момент записи на SD-карту.


Есть гипотеза, что это связано с тем, что SPI работает в блокирующем режиме и, возможно, DMA не успевает принимать данные или получает их со смещением на несколько бит.

Хотелось бы обратиться к общему разуму (и тем, кто дочитал до этого места):

👉 Как лучше писать аудиопоток на внешнюю память?
👉 Верна ли гипотеза, что запись по SPI блокирует работу DMA? По идее, не должна, но кто его знает…

Как точно работает драйвер SPI для SD, я пока не разобрался.
Но с драйвером из BSP все работало хуже, чем с драйвером, который я нашел где-то на GitHub.
Выкладываю проект на суд общественности — кому-то он точно должен спасти пару часов или дней.

Спасибо за внимание!

P.S.
И храни вас GitHub и портал community.st.com!
Представить не могу, во что бы вылилась эта задача, если бы это был не ST, а какой-нибудь более 中國微控制器… 😬
Теги:
Хабы:
Всего голосов 31: ↑31 и ↓0+39
Комментарии9

Публикации

Информация

Сайт
slc.tl
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия
Представитель
Влад Ефименко