Вывод звука на STM32 + библиотека

Озадачился выводом звука (мелодий) на stm32 для одной поделки. Стал изучать материалы…
Задача: с минимальными ресурсами по CPU и памяти (а так-же с минимальным объема работ по подключению в коде) — выводить звук.
Итоги изысканий и результат:

Детали под катом…
UPDATE: Суть решения не выводе как таковом, а в формировании первоначального сигнала в коде. Я у себя использовал усилитель D-класса на рассыпухе. Но это не обязательно. Вывод можно делать и через DAC. Можно приделать любой усилитель. Можно использовать внешний DAC. Суть решения в пункте 2 и 3: генерация данных с минимум нагрузки на МК.
З.Ы. А вы обсуждаете первый пункт, который я применил в конкретном месте в конктерных условия, потому что мне так было проще…
З.З.Ы. Удалил часть про PWM, как не главную :)


За основу взяты 3 идеи:
1) Вывод музыки на ATtiny13 от Aterlux с переработками от меня
2) Использование PWM Timer1 для выводя звука из статьи отсюда
3) Для формата мелодии взял за основу формат команды PLAY от ZX Spectrum 128K
Каждая из идей самостоятельная, просто я их объединил в одну систему.

В ролике я не стал городить драйвер, и просто повесил динамик через резистор на верхний PWM (SND_0+SND_PWM0), отсюда дребезг (нет фильтров и половины сигнала).

Формирование звука
Если Вам не интересна теория — можно сразу переходить к 2-й части — формат мелодий.
Эта часть самая сложная. Тут сразу несколько тонких моментов, которые я курил очень долго (или я просто такой тупой?), втыкая в код Aterlux. Вам даю готовое описание.
1) В таблице сохранена предрасчитанная синусоида с равномерным распределением значений вдоль оси времени.
const short wave1[256] = { // Форманта (синусоида)
	  0, 7, 15, 22, 29, 37, 44, 51, 59, 66, 73, 80, 87, 94, 101, 108,
	115, 122, 128, 135, 141, 148, 154, 160, 167, 173, 179, 185, 190, 196, 201, 207,
	212, 217, 222, 227, 232, 237, 241, 245, 249, 253, 257, 261, 265, 268, 271, 274,
	277, 280, 282, 285, 287, 289, 291, 293, 294, 296, 297, 298, 299, 299, 300, 300,
	300, 300, 300, 299, 299, 298, 297, 296, 294, 293, 291, 289, 287, 285, 282, 280,
	277, 274, 271, 268, 265, 261, 257, 253, 249, 245, 241, 237, 232, 227, 222, 217,
	212, 207, 201, 196, 190, 185, 179, 173, 167, 160, 154, 148, 141, 135, 128, 122,
	115, 108, 101, 94, 87, 80, 73, 66, 59, 51, 44, 37, 29, 22, 15, 7,
	0, -7, -15, -22, -29, -37, -44, -51, -59, -66, -73, -80, -87, -94, -101, -108,
	-115, -122, -128, -135, -141, -148, -154, -160, -167, -173, -179, -185, -190, -196, -201, -207,
	-212, -217, -222, -227, -232, -237, -241, -245, -249, -253, -257, -261, -265, -268, -271, -274,
	-277, -280, -282, -285, -287, -289, -291, -293, -294, -296, -297, -298, -299, -299, -300, -300,
	-300, -300, -300, -299, -299, -298, -297, -296, -294, -293, -291, -289, -287, -285, -282, -280,
	-277, -274, -271, -268, -265, -261, -257, -253, -249, -245, -241, -237, -232, -227, -222, -217,
	-212, -207, -201, -196, -190, -185, -179, -173, -167, -160, -154, -148, -141, -135, -128, -122,
	-115, -108, -101, -94, -87, -80, -73, -66, -59, -51, -44, -37, -29, -22, -15, -7
};

Амплитуда значений — половина от максимальной скважности PWM. В моем случае, при частоте 48МГц и частоте таймера 80000 — скважность == 600/2 = 300.
2) Если мы будем выводить их со скоростью 256 в секунду, очевидно мы получим частоту 1Гц. Если 25600 — 100Гц. Т.е. для вывода звука нужной частоты, нам нужно просто менять скорость вывода этих значений. Или, если у нас фиксированная скорость вывода — скорость прохода по массиву. Т.е. если мы берем каждое значение — то получаем частоту Х. Если же мы будем брать через одно значение — то очевидно частота увеличиться в 2 раза. Поэтому создана таблица скорости прохода по синусоиде в зависимости от нужной частоты для 12-ти нот первой актавы (TIMER_FREQUENCY — задает частоту таймера. В коде она задана, как мы помним, в 80000):

// Тип с фиксированной точкой - для позиции в вейв-форме и скорости
// старшие 8 бит кодируют целую часть, младшие 24 - дробную
typedef uint fixed_8_24;

// Определяем скорость, для частоты в 1 герц
#define SOUND_ONE_HERTZ_SPEED (4294967296.0 / TIMER_FREQUENCY * 4.0)

const fixed_8_24 notes[] = { // Значения полей waveSpeed для разных нот. Интервал - полутон.
  // нота "ДО" первой октавы и далее
  (fixed_8_24)(0.5 + 261.63 * SOUND_ONE_HERTZ_SPEED), (fixed_8_24)(0.5 + 277.18 * SOUND_ONE_HERTZ_SPEED),
  (fixed_8_24)(0.5 + 293.66 * SOUND_ONE_HERTZ_SPEED), (fixed_8_24)(0.5 + 311.13 * SOUND_ONE_HERTZ_SPEED),
  (fixed_8_24)(0.5 + 329.63 * SOUND_ONE_HERTZ_SPEED), (fixed_8_24)(0.5 + 349.23 * SOUND_ONE_HERTZ_SPEED),
  (fixed_8_24)(0.5 + 369.99 * SOUND_ONE_HERTZ_SPEED), (fixed_8_24)(0.5 + 392.00 * SOUND_ONE_HERTZ_SPEED),
  (fixed_8_24)(0.5 + 415.30 * SOUND_ONE_HERTZ_SPEED), (fixed_8_24)(0.5 + 440.00 * SOUND_ONE_HERTZ_SPEED),
  (fixed_8_24)(0.5 + 466.16 * SOUND_ONE_HERTZ_SPEED), (fixed_8_24)(0.5 + 493.88 * SOUND_ONE_HERTZ_SPEED)
};

3) Почему только одна октава? Дело в том, что частоты октав отличаются на степень 2. Т.е. ноты предыдущей октавы (малой) в 2 раза «медленнее», а ноты следующей (второй) — в 2 раза «бастрее» (см. Wiki: Октавная система). Т.е. нам достаточно делить или умножать скорость на степень 2 и мы получим требуемую октаву. А как мы помним, умножение или деление на степень 2 — это сдвиг, что не может не радовать по скорости работы.
4) Каналы. У нас будет 3-х канальная система (или 3-х голосая). Каждый канал проигрывает одну ноту или паузу в единицу времени. Итоговый звук, есть сумма всех каналов. Часть каналов может молчать (играть паузу/ничего не играть, т.к. мелодия простая и не требует всех каналов/иметь громкость 0(ноль)).
Каждый канал хранит данные в структуре:
typedef struct {
  //Данные текущей ноты
  fixed_8_24 wavePos;   // Позиция в массивах звука.
  fixed_8_24 waveSpeed; // Скорость приращения позиции, кодируется также (фактически задает ноту).
  uint  ticksToNext;    // Длительность текущей ноты
  uint8_t wave1amp;     // Множитель громкости звука (форманты)
  //Данные канала "глобальные"
  uint8_t volMark;      // Маркировка громкости по каналу.
  const char *melody;   // Мелодия (NULL - нет мелодии)
  uchar melodyPos;      // текущая позиция в мелодии
  uchar len;		// Длительность ноты (если не указана у ноты)
  char  shift;		// На сколько сдвинута октава от первой
} CHAN_INFO;

Данные по текущему состоянию хранятся в специальной структуре:
typedef union {
	struct{
		short amp;
		short delta;
	};
	struct {
		ushort uamp;
		ushort udelta;
	};
	uint vecAmp;
} VEC_AMP;

Формирование звука идет в 2 этапа:
1) На каждом канале расчитывается очередная нота и ее громкость. Это происходит: TIMER_FREQUENCY/4/512 = 39 раз в секунду в методе void sound::_doTick(CHAN_INFO *ch) для каждого канала не зависимо.
2) TIMER_FREQUENCY/4 =20000 раз в секунду вычисляется текущий семпл для PWM на основе данных всех каналов:
// Просчитывает один сэмпл и выводит его. Выполняется TIMER_FREQUENCY/4 раз в секунду.
void sound::_doSample()
{
	int sum = 0;
	for (CHAN_INFO * ch = &chans[0]; ch < &chans[NUM_CHANNELS]; ch++) 
	{
		uint8_t wp = ch->wavePos >> 24;
		ch->wavePos += ch->waveSpeed;
		sum += (wave1[wp] * ch->wave1amp);
	}

	VEC_AMP tmpAmp; tmpAmp.vecAmp = vecAmp.vecAmp;

	tmpAmp.delta = (sum - tmpAmp.amp) >> 2;

	vecAmp.vecAmp = tmpAmp.vecAmp;
	
	tickCounter = 4;
	
	if (runTick--)
		return;

	runTick = 512;
	
	// Каждые 512 сэмплов
	_doTick(&chans[0]);
	_doTick(&chans[1]);
	_doTick(&chans[2]);

	if (chans[0].ticksToNext & chans[1].ticksToNext & chans[2].ticksToNext & 0x80000000)
		stop();
}

  • В цикле проходим по всем каналам
  • Берем очередное значение синусоиды
  • Сдвигаем позицию в синусоиде на следующую (величина этого сдвига задает скорость проигрывания синусоиды, т.е. частоту ноты)
  • Умножаем величину на громкость и складываем в общий результат.
  • Результат кладем в данные для PWM. Тут такой сложный алгоритм, т.к. мы работает с разными по длине типами (int, short). Чтобы не потерять значения — приходиться писать много кода. Компилятор потом это умещает в 3-4 инструкции. Главное шаманство: объединение VEC_AMP. Тут происходит объединение разных типов в один uint. Без этого не работало...
  • Через каждые 512 семплов, пересчитываем данные каждого канала
  • Если при этом все каналы начинают играть «бесконечную» ноту, значит мелодия закончилась.

Не много пояснений:
Нужное значение задается в vecAmp.
Оно состоит из 2-х 16-ти битных short, упакованных в 1 uint. Это нужно для атомарности работы с ним.
Одно число это текущее значение для вывода.
Второе — delta, которая прибавляется к текущему значению.
Суть в том, что амплитуда может меняться резко. Чтобы это сгладить, реальный сигнал линейно меняется за 4 тика. Т.е. если у нас частота семплирования звука 20000, то частота вывода (и его таймер) работают на частоте 80000 (#define TIMER_FREQUENCY 80000) — по 4 тика на каждый семпл. Тики от 4 до 0 хранятся в tickCounter. Значение меняется на 4 после вычисления очередного семпла из основного цикла.

Формат мелодий
За основу взят оператор PLAY для ZX Spectrum 128K.
Мелодия для каждого канала задается в виде строки символов, которые кодируют громкость канала, октаву, ноты /паузы и их длительности. + есть специальные коды типа «перемотка на начало» или «разделитель».
static void play(const char *melodyA, const char *melodyB = NULL, const char *melodyC = NULL);

Можно задать мелодию для всех каналов (3-х голосая мелодия), только для 2-х или только для 1-го.
Формат мелодии:
  1. «v» или «V» + число от 0 до 99 — задает громкость на канале. Действует на все ноты до след. изменения громкости. Первоначально стоит «V50».
  2. «o» или «O» + число от 0 до 8 — задает базовую октаву. Действует на все ноты до след. изменения октавы. Первоначально стоит «O4» (первая октава). Не рекомендуется устанавливать менее O2 и более O6.
  3. "<" — знак «перемотки». Мелодия на ВСЕХ КАНАЛАХ начнет играть сначала. Используется для «бесконечного» проигрывания.
  4. "&" — пауза.
  5. «с #c d #d e f #f g #g a #a h» — ноты базовой октавы
  6. «C #C D #D E F #F G #G A #A H» — ноты следующей после базовой октавы. используется для быстрого выбора нот другой октавы, без необходимости переключения базовой октавы
  7. Длительность паузы или ноты можно указать 3-мя способами:
    • Перед нотой или паузой можно указать длительность от 1 до 9. Соответственно 8, 12, 16, 24, 32, 48, 64, 96 и 128 тактов. Это значение будет сохранено как длительность для канала.
    • Длительность можно не указывать, тогда будет использована длительность на канале. Первоначально стоит длительность 5.
    • Можно задать точную длительность в тактах если указать значение от 1 до 999 после ноты или паузы. Это значение не меняет значение на канале.
    • Пример: «4&a 5CF43» Обратите внимание на пробел между «a» и 5: чтобы система не приняла 5 как точную длительность — поставлен разделитель
  8. "<ПРОБЕЛ>" — разделитель. Иногда может возникнуть путаница, при указании числовых параметров. Например указание громкости и следом длительности ноты. Чтобы избежать этого, между числами ставиться пробел. Он не влияет на работу и служит только разделителем чисел. Например: «V30 5aO4 7F»

Пример мелодии из ролика:
sound::play("V60 3aV50 6A5#G4Ea40 3D3E3#C6ha80 7#CE56#C56<",
	"V0 1a V50 4#C4#G3a3A3a4A3D5#C5a#g104 3e7hD72 6h3#g",
	"O3 V0 3a V50 3#f3#C5#F3D3a3E3D3a3E5#C3#g3h5E3#f3#C5#F3D3a5E3a3E5#C3E3h");
Файлы в топике: sound_stm32-lib.zip

Комментарии (23)

RSS свернуть / развернуть
А зачем такие заморочки? У большинства stm32 есть ЦАП.
0
DAC не даст достаточной мощности чтобы раскачать динамик.
0
Эм, окай. УНЧ зачем тогда придумали? :D
0
Что интересно, _YS_ на подобный вопрос отвечал, что двухтактный повторитель (т.е. усилок D-класса) обходится дешевле, чем усиление сигнала с DAC)
+1
И еще. Основные заморочки были с формированием сигнала. Далее перевести его в звук можно многими способами.
0
Не впечатлило. Наберите на ютюбе stm32 wavetable. Вот там реально шедевры музыкальные.
0
Типа такого: www.youtube.com/watch?v=4JkhzH57Je4
А у вас это типа забивания гвоздей микроскопом
0
Вы путаете вывод звука как дополнительный функционал, когда ресурсов на это выделяется минимум и вывод звука как основная работа МК.
Тут речь о том, что с минимальными ресурсами по CPU и памяти можно сделать вывод звука, например, в игрушке, управляемой МК. Я сильно сомневаюсь, что кто-то будет встраивать полноценную систему в такие штуки…
+1
Эм, DAC. Для вывода звука требуется, в худшем случае, 2 таймера(один для своевременного чтения данных с SD-карты, другой для выкидывания данных в DAC) и 1 канал(2, если стерео) DAC. У вас же нужны целых 4 полевика и 4 GPIO.
0
очень давно в эпоху РС-пищалок (как у ТС) народ понял красоту аналогового звука COVOX на 572ПА1 + 174УН4…
потом пошли SoundBlaster для оцифрованного звука и синтезаторы мелодий AWE32 на основе Wave-таблиц.
Сейчас поставить относительно мощный усилитель с минимумом обвязки вообще без проблем — качество звука с ТС не сравнимо с записанным на микроСД карту
-1
Дописал коментарий в начале статьи. Сюда копия:
Суть решения не выводе как таковом, а в формировании первоначального сигнала в коде. Я у себя использовал усилитель D-класса на рассыпухе. Но это не обязательно. Вывод можно делать и через DAC. Можно приделать любой усилитель. Можно использовать внешний DAC. Суть решения в пункте 2 и 3: генерация данных с минимум нагрузки на МК.
З.Ы. А вы обсуждаете первый пункт, который я применил в конкретном месте в конктерных условия, потому что мне так было проще…
0
У Атмела ещё лет *цать был пример вывода звука через ШИМ — AVR335: Digital Sound Recorder
with AVR® and DataFlash
на AT90S8535 с фильтрами на LM324
http://www.atmel.com/Images/doc1456.pdf
0
А почему не заюзать I2S? Вместо шим.
0
Мне было проще запилить PWM в данном конкретном случае.
0
Зря удалили часть про PWM. Осциллограммы были очень наглядные.
+1
Верни инфу про ШИМ в зад!!!
0
Народ, подскажите. Если звук всё равно получился такой отвратительный, имеет-ли смысл заморачиваться с синусами, если можно тоном просто пищать прямоугольником?
0
Ответ будет в зависимости от того какого качества вы хотите получить «звук» — программный синтез или оцифрованный реальный?
послушайте Пингвина и почитайте как он добился такого результата forum.easyelectronics.ru/viewtopic.php?f=17&t=22281&hilit=armada
с внешним DAC


и с встроенным


повторю ещё раз — очень давно в эпоху РС-пищалок (как у ТС) народ понял красоту аналогового звука COVOX на 572ПА1 + 174УН4…
потом пошли SoundBlaster для оцифрованного звука и синтезаторы мелодий AWE32 на основе Wave-таблиц.
Сейчас поставить относительно мощный усилитель с минимумом обвязки вообще без проблем — качество звука с ТС не сравнимо с записанным на микроСД карту
0
Ну по моему у ТС просто получилась некая отвратного качества пищалка, хотя сама идеология (ну и способ получения синуса) мне очень даже понравилась.
Просто есть мысль сделать к часикам «музыкальный» будильник, и думаю имеет ли смысл заморачиваться. Звук планируется получить аналогичный тому, который был в ручном ЧБ тетрисе, ну или первых монофонических мобильниках.
0
Звук планируется получить аналогичный тому, который был в ручном ЧБ тетрисе, ну или первых монофонических мобильниках.
Для такого звука достаточно меандр гнать. Но если ресурсов в избытке — то можно сделать и синус, и более сложный синтез (скажем, имитировать какой-нить из старых звуковых чипов, применявшихся в приставках и играть чиптюны) с выводом через ШИМ.
У ТС прежде всего отвратный динамик, по отзывам тех, кто делал нормально — уже при 8-10 битах PWM звучит достаточно чисто, при этом усиливать его можно просто двухтактным ключевым каскадом.
В двух видосиках комментом выше, кстати, можно наблюдать сравнение колонки (первое видео) против мелкого динамика без акустического оформления (второе). Во втором еще очень хорошо слышно разницу между просто лежащим на столе динамиком, как у ТС, и имеющим даже минимальное оформление (когда автор его на стол бросает в конце).
0
Синтез самый обычный — аддитивный с аккумулятором фазы. А звук паршивый потому что в этом месте:
sum += (wave1[wp] * ch->wave1amp);
происходит переполнение разрядной сетки.
В свете этого танцы с бубном в виде VEC_AMP вызывают недоумение, поскольку проблему в корне не решают.
0
Извини, а если более менее для дилетантов?
Я просто немного не понял что значит «переполнение разрядной сетки».
0
Допустим разрядность PWM 10 бит. Судя по таблице синуса, для отображения максимальных значений потребуются те самые 10 бит с учетом знака. При совпадении пучностей во всех 3-х каналах результат потребует 11 бит, т.е. старший бит вылезает за разрядность PWM и отсекается. Что приводит к жутким нелинейным искажениям, и это при условии что wave1amp лежит в диапазоне от 0 до 1. Если wave1amp больше 1, то ситуация становится ещё хуже. Кроме того PWM не понимает отрицательных значений, из-за чего отрицательная полуволна становится по уровню выше положительной. На осциллограмме это выглядит примерно так:

Как видно, синусоидой тут и не пахнет.
Чтобы этого избежать нужно приводить значение отсчета к беззнаковому виду, прибавляя к знаковому число с 1 в знаковом разряде (0х80 для 8 бит, 0х100 для 9 бит и т.д.).
Ну и сами манипуляции с VEC_AMP создают нелинейные искажения. Интересно, автор пытался смотреть сигнал на динамике осциллографом? Если бы он это сделал, то понял бы что именно пошло не так.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.