“Сигма дельта” или как сделать хорошую звуковую карту из 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
Сформулируем тоже в виде кода (входной сигнал от -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 с лишним лет я много забыл и сейчас в этой области я просто любитель. Во вторых не хотелось переусложнять математикой статью тут ее реально нужно много для детального описания …
Еще Видео : Снег кружится группы Пламя
Исходный код на гитхабе