“Сигма дельта” или как сделать хорошую звуковую карту из STM32F401

Жене мешают смотреть последние новости из телефона и телевизора, приходящие поесть (первично ?) и поиграть на компьютере (вторично?) внуки. Она их конечно любит , но звуки их взаимодействия с компьютером ее сильно раздражают. Пришлось надеть на внуков наушники. А звуковой выход у компа в неудобном месте и каждый хочет со своей громкостью. Ну пришлось разработать внешнюю USB звуковую карточку. Хочется и красиво и качественно. Впрочем, внуки скорее, только повод для поностальгировать по своей старой специальности радиоконструктора и вообще, так как последние двадцать с лишним лет я далеко от нее и пишу заклинания программы реконструкции изображений для медицинских томографов в больших и не очень фирмах , то есть энжинер-погромист по специальности. Хотел написать статью на эту очень интересную и важную тему (компьютерная томография), но выяснилось что мне нельзя по условиям контракта …

Итак вернемся к нашим баранам внукам и звуковым карточкам, у нас есть в нескольких экземплярах (овер дофига, купил пока были дешевые) модули из Китая

1 Stm32f401ccu6 black pill — сейчас $3 за штучку

2 I2S DAC Decoder GY-PCM5102 →$3.5 за штучку

3 SPI display ips 1.3 inch 240×240 (controller st7789) →$7 за два

Сначала построим максимальную конфигурацию из двух экранов и I2S GY-PCM5102.

Конфигурируем куб, разбавляем его говно код своим г. кодом, добавляем ФАПЧ (фазовой автоматической подстройки частоты или PLL на ихнем) , для согласования скоростей приходящих от компьютера данных и выдачи на i2s внешний ЦАП (DAC). Хмм , звучит очень неплохо, явно лучше большинства встроенных звуковушек. PCM5102 — весьма и весьма качественный ЦАП за свою цену в пару- тройку долларов за модуль с чипом. Добавляем отображение индикаторов уровня на паре неплохих дисплеев st7789 …

С ними нужно было немного повозиться. Во первых у этих не выведен CS (Chip Select — ножка выбора чипа) . Поэтому каждому свой SPI (SPI_1 и SPI_3). Во вторых их DMA (прямой доступ к памяти) сильно тормозит. Соотвественно копирование полного экрана из памяти занимает для SPI_1 — 32 mS, а для SPI_3 — 51 миллисекунды соотвественно.

Исходя из этого стрелки измерителей и их тени (!) отрисовываются и стираются инкрементально что помещается в 8 миллисекунд в сумме на оба экрана. Положение стрелок задается максимумом в примерно 20 мс с постоянной времени затухания 300 мс (примерно как у настоящих VU измерителей)

чудесная и актуальная песня В.Высоцкого «Аисты» (video)

И тут я вспомнил , очень давно, больше 30 лет тому назад меня учили на заклинателя синего дыма , то есть я знаю зачем осциллографу ручки, а микросхемам ножки. Ручки — чтобы их дергать, а ножки — чтобы ими дергать! Может можно ли рендедерить звук самой Stm-кой?

На этом чипе (STM32f401) нет ЦАП-а. К качестве оного можно использовать таймер с его ШИМ (PWM) или SPI.

Таймеры можно инициализировать на два три или 4 канала для N уровней сигнала на частоте Fbus = 84 Мгц мы получим частоту дискретизации Fds = 84 000 000 / N, или наоборот N=Fbus/ Fds. Например для частоты дискретизации 44100 Гц мы получим 84 000 000/44100 = 1904 уровня, что соотвествует примерно log2(1904) = 10.9 бит. Ну или на 96000 Гц соотвественно 84МГц/96КГц = 875 уровней что соотвествует 9.8 бит Маловато будет.

SPI может выдавать только два уровня 0 или 1, правда до 42 мегабит в секунду.

Хмм. 1 бит мало однако …

Итак PDM (Pulse-density modulation не знаю как точно по русски).

https://en.wikipedia.org/wiki/Pulse-density_modulation

3877077afe4fe1de943d00f7b96acfa8.png

Сформулируем тоже в виде кода (входной сигнал от -1.0 до 1.0)

struct sigmaDeltaStorage
{
	float integral;
	int    y;
};

struct sigmaDeltaStorage left_chanel;
struct sigmaDeltaStorage right_chanel;

int sigma_delta(struct sigmaDeltaStorage* st,float x)
{
	st->integral += x - st->y;
	st->y =  (st->integral>0.0f) ? 1:-1;
	return st->y;
}

в цикле:

Прочитать данные левого и правого каналов из преемного циклического буфера USB c интерполяцией (лучше билинейной)

Вызвать функцию sigma_delta

Результат отправить на выдачу .

Фокус тут в том что если входной сигнал ограничен некоторой частотой, а частота семплирования (отсчетов) достаточно велика , то пропустив выходной сигнал через фильтр нижних частот получим синал приближенный к исходному, причем тем лучше чем больше частота семплирования!

Рассмотрим пример. Входной сигнал — постоянный x=0.31415

void testFunc(int NumberSamples)
{
	for(int t=0;t

для 10 вызовов фунция даст вот такую последовательность [1,-1, 1,1,-1,1,1,-1,1,1] их среднее будет 0.4, для 1000 вызовов среднее будет 0.314, для 10000 → 0.3142 и тд.

То есть, чем больше частота отсчетов, тем аккуратнее апроксимация ограниченного по частоте входного сигнала. Фокус изобрели в 60-х годах прошлого века.

К сожалению, реализовать этот алгоритм прямо на stm -ке с генерацией бит и выдачей на SPI у меня не получилось, неободимая частота должна быть около 2.884 Мгц (https://en.wikipedia.org/wiki/Super_Audio_CD) . Выглядит малореальным сгенерировать два канала с семплированием с интерполяцией из usb буфера и вызовом функции sigma_delta для каждого бита и упаковки бит на выдачу в SPI при частоте процессора всего 84 Мгц. Всего 15 клоков на левый и 15 клоков на правый каналы на бит при забитой DMA шине — это нереально…

Облом ?

А что если переписать sigma_delta так, чтобы она выдавала не два уровня сигнала [1 -1] (с одним компаратором около нуля), а больше? Например 4 уровня [3 1 -1 -3] ? Или еще больше, например N+1 (мы заодно сместили входной и выходной сигнал к диапазону от [0 N], так нужно для выдачи ШИМ на таймер):

int sigma_delta(struct sigmaDeltaStorage* st,float x)
{
	st->integral+= x - st->y;
	st->y = floorf(st->integral+0.5f);  // nearest integer
	if(st->y<0) st->y = 0;                    
	if(st->y>N) st->y = N;

	return st->y;
}

Ура — мы изобрели велосипед. Если бы я внимательней читал статью про https://en.wikipedia.org/wiki/Delta-sigma_modulation то обратил бы внимание что один бит — это частный случай, о чем там написано черным по белому :(

Теперь мы можем выбрать удобный для реализации компромисс между частотой отсчетов и количеством дискретных уровней. Простая дискретизация сигнала дает равномерный по частоте шум величиной в половину шага дискретизации. Сигма дельта дает шум обратно пропорциональный частоте сигнала (в первом приближении). То есть энергия спектра шума дискретизации смещается вверх, за пределы слышимых частот! Фокус можно повторить с двойным , тройным и тд интегралом:

float sigma_delta2(struct sigmaDeltaStorage2* st,float x)
{
	st->integral0+= x             - st->y;
	st->integral1+= st->integral0 - st->y;
	st->y = floorf(st->integral1+0.5f);
	if(st->y<0)st->y = 0;
	if(st->y>MAX_VOL)st->y = MAX_VOL;

	return st->y;
}

Есть аналогия с цифровыми фильтрами низкой частоты высоких порядков. Однако дизайн стабильных фильтров высокого порядка оставим профессионалам (по дизайну фильтров) . В коде мы реализуем простой фильтры первого и второго порядка, с реализацией в числах с фиксированной точкой (у нас на STM это самый быстрый тип int32_t)

Для реализации я выбрал частоту 101 Кгц соотвественно количество уровней 828 (84МГц/101КГц) на сигма-дельта второго порядка интегрирования, впрочем при желаниии можно поиграть с опциями. Количество дисплеев лучше задать 0 для конфигурации с сигма- дельта, поскольку помехи от них будут слышны в наушниках, впрочем не очень сильные, слушать можно. Но предпочтительный вместе с индикаторами уровня использовать внешний DAC или организовывать развязку питания для них. Без дисплеев , с RC фильтром низкой частоты сигма-дельта на STM32f401 дает очень хороший звук, лучше большинства встроенных звуковых карточек на недорогих компах и явно лучше дешевых китайских звуковых затычек USB→3.5 мм для наушников.

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

Еще Видео : Снег кружится группы Пламя

Исходный код на гитхабе

© Habrahabr.ru