Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32F407 · уровень средний · 14 мин чтения
Открыть в интерактивном справочнике

198п. Мини-проект: MIDI — вход по UART 31250 и синтезатор на ЦАП

MIDI — это UART. Медленный (31 250 бит/с), с токовой петлёй вместо уровней и с парой хитростей в потоке байтов, но именно UART: старт, 8 бит, стоп. Клавиатура шлёт по нему «нота 69 нажата со скоростью 100», а прибор на другом конце должен её сыграть. Отсюда мини-проект: вход MIDI на USART, разбор потока и синтезатор на восемь голосов, который через DMA кормит ЦАП 32 000 отсчётами в секунду.

Проверено без платы. Ядро (synth_core.c) — разбор и синтезатор — прогнано в опыте sim_midi.c: поток с бегущим статусом, байтом синхронизации посреди данных и SysEx, аккорд A4 + C#5 + E5, затухание, десять нот на восемь голосов. Опыт собран gcc на ПК и arm-none-eabi-gcc для Cortex-M4, в qemu-system-arm -M netduinoplus2 вывод совпал с ПК строка в строку. Модель на Python (midi_host.py) своим кодом повторила все отсчёты (CRC-32 совпали), записала аккорд в WAV и нашла частоты нот по спектру. Прошивка synth_f407.c собрана armclang по заголовкам пака F4 без предупреждений; на плате не запускалась.

Опоры: спецификация MIDI здесь не цитируется — используются только общеизвестные вещи: 31 250 бит/с, формат сообщений, бегущий статус, токовая петля с оптроном; чисел электрики нет. RM0090 — таблица 42 запросов DMA1, поле TSEL1 в DAC_CR; DS8626 — PA3 (USART2_RX, AF7) и PA4 (DAC_OUT1). UART — раздел 16, ЦАП и генератор на таблице — раздел 15, фазовый аккумулятор — 198л. Проверка — _verify/midi198p_check.py.

Как это называется в документации

Оригинал Перевод
current loop, opto-isolator токовая петля, оптрон
status byte, data byte байт статуса (старший бит 1), байт данных (старший бит 0)
running status бегущий статус: статус не повторяется, пока не сменился
Note On, Note Off, velocity нажата, отпущена, скорость нажатия
System Real-Time, System Exclusive системные сообщения реального времени; SysEx — «свои» сообщения
wavetable, phase accumulator таблица волны, фазовый аккумулятор
voice stealing отнять голос, когда свободных нет

198п.1 Что делает прибор и из чего он

Функция Как
вход MIDI оптрон → USART2_RX (PA3, AF7), 31 250 бит/с, прерывание на байт
разбор бегущий статус, Note On / Off, CC 123, реальное время и SysEx пропускаются
синтез 8 голосов: таблица синуса 256 точек, 32-битный фазовый аккумулятор, огибающая 5 / 50 мс
звук TIM6 → 32 000 Гц → ЦАП 1 (PA4), отсчёты по DMA1 Stream5 канал 7, две половины по 64

MIDI-синтезатор на STM32F407: клавиатура гонит ток через петлю в разъёме, оптрон отдаёт байты на USART2_RX (PA3) на 31 250 бит/с; разбор с бегущим статусом кладёт события в очередь; прерывание DMA забирает события и считает половину буфера — восемь голосов по таблице синуса с фазовым аккумулятором; TIM6 на 84 МГц / 2625 даёт 32 000 Гц, ЦАП на PA4 берёт отсчёт по DMA1 Stream5 канал 7; задержка половины буфера — 2 мс

Выводы — по таблицам DS8626: PA3 — AF7 USART2_RX, PA4 — аналоговый выход DAC_OUT1. Такт — кварц 8 МГц, как на F4-Discovery (198л): ядро 168 МГц, APB1 — 42 МГц, таймеры на APB1 — 84 МГц.


198п.2 Порт MIDI: токовая петля и оптрон

Выход MIDI — не уровень напряжения, а ток по петле из двух проводов в кабеле: передатчик его пускает или нет. На входе ток идёт через светодиод оптрона, а фототранзистор тянет вход UART к земле. Земли двух приборов при этом не соединяются — вход развязан, и петли по земле, от которых гудит звук, нет. Ток есть — логический 0, тока нет — 1, поэтому линия в покое, как и положено UART, — единица.

Скорость 31 250 бит/с — это 1 МГц / 32. Кадр — 10 бит, байт — 320 мкс. Сообщение Note On — 3 байта, 960 мкс. Для USART2 F407 делитель выходит ровно: 42 МГц / 31 250 = 1344, ошибки скорости нет; на G474 от 170 МГц — тоже ровно, 5440.

Медленная линия чувствуется на аккордах. Десять нот подряд — 30 байтов, 9,60 мс: последняя нота звучит почти на 10 мс позже первой. Бегущий статус экономит байт статуса у всех сообщений, кроме первого: 21 байт, 6,72 мс.


198п.3 Разбор потока

Байт со старшей единицей — статус, с нулём — данные. У Note On (0x9n) и Note Off (0x8n) два байта данных: нота и скорость, n — канал 0…15. Три правила, которые ломают наивный разбор:

SysEx (0xF0 … 0xF7) и остальные системные сообщения бегущий статус сбрасывают: данные после них без нового статуса выбрасываются.

    m->data[m->n++] = b;
    if (m->n < m->need)
        return 0;
    m->n = 0;                                                  /* статус остаётся: бегущий */

Опыт — 29 байтов: три Note On одним статусом (между байтами второй ноты — 0xF8), Active Sensing, Note On на чужом канале, SysEx, два байта данных без статуса, Note On со скоростью 0, Note Off и CC 123. Разбор выдал шесть событий:

Событие Нота Скорость
нажата 69 100
нажата 73 100
нажата 76 100
отпущена (Note On, скорость 0) 69 0
отпущена 73 0
все ноты выключить (CC 123) — —

Пропущены два байта реального времени и один SysEx; выброшено три: сообщение чужого канала и два байта данных после SysEx.


198п.4 Синтезатор: таблица, фаза, голоса

Голос — это 32-битный фазовый аккумулятор: каждый отсчёт к фазе прибавляется шаг ноты, старшие 8 бит — номер точки в таблице синуса на 256 точек, следующие 16 — доля для линейной интерполяции (как тон в 198л). Шаг ноты n — round(440 · 2^((n − 69)/12) · 2³² / 32 000); таблица 128 шагов посчитана заранее. Для A4 это 59 055 800. Ошибка настройки из-за округления шага — не больше 0,000437 цента; ноты от 8,176 Гц до 12 543,9 Гц — все ниже половины частоты отсчётов, 16 000 Гц.

            uint32_t idx = v->phase >> 24, frac = v->phase >> 8 & 0xFFFFu;
            int32_t a = SYN_SINE[idx], b = SYN_SINE[idx + 1u];
            int32_t smp = a + (int32_t)(((b - a) * (int32_t)frac) >> 16);   /* линейная интерполяция */
            mix += (((smp * (int32_t)v->env) >> 16) * v->vel) >> 7;

Огибающая — атака 5 мс (шаг 410 в Q16, 160 отсчётов) и затухание 50 мс (шаг 41): в опыте после CC 123 голоса звучат 1598 отсчётов и замолкают на 1599-м, через 49,97 мс. Без огибающей нота начиналась бы и кончалась ступенькой — щелчком.

Голосов восемь. Новая нота берёт тот же голос, если эта нота уже звучит; иначе свободный; иначе самый тихий из затухающих; иначе самый старый. Опыт — десять нот подряд со скоростью 127: отнято два голоса, ноты 60 и 61 заменились на 68 и 69.

Смесь сдвигается так, чтобы восемь голосов на полной громкости поместились в 12 бит: 8 · 32 767 · 127/128 / 128 = 2031 ≤ 2047 — это проверяется при сборке. За это платит один голос: при скорости 100 он даёт всего ±200 кодов. В опыте аккорд из трёх нот лёг в 1448…2646, десять нот — в 646…3377, обрезок — ноль.


198п.5 ЦАП, DMA и таймер

TIM6 считает 84 МГц: период 2625 тактов (ARR = 2624) — ровно 32 000 Гц, и по обновлению выдаёт TRGO (MMS = 010). ЦАП канал 1 берёт по нему отсчёт: TSEL1 = 000 — это «Timer 6 TRGO event» по RM0090. Отсчёт в DHR12R1 кладёт DMA1 Stream5 канал 7 — запрос DAC1 по таблице 42 RM0090, память → периферия, 16 бит, по кругу.

Буфер — две половины по 64 отсчёта, 2 мс каждая. Прерывание DMA на половине и в конце считает ту часть, которую ЦАП только что отыграл. Это и есть задержка синтезатора: до 2 мс плюс время байтов MIDI.

События с USART в синтезатор напрямую не идут. Прерывание USART (выше приоритетом) кладёт их в очередь на 32 события, а прерывание DMA забирает очередь перед расчётом половины. Иначе нота могла бы смениться посреди расчёта отсчёта — голос наполовину старый, наполовину новый.

На G474 так ровно не выйдет: 170 МГц / 32 000 = 5312,5. Период 5312 даёт 32 003,01 Гц, 5313 — 31 996,99 Гц, ошибка ±0,0094 %; шаги нот тогда считают под настоящую частоту или ищут делитель ФАПЧ под 32 кГц.


198п.6 Проверка: WAV и спектр

Модель на Python повторила разбор и синтезатор своим кодом и получила те же отсчёты: CRC-32 аккорда (40 960 отсчётов), его хвоста, затухания и многоголосия совпали с прогоном на C. Аккорд записан в WAV — 32 000 Гц, 16 бит, моно, 40 960 кадров — и прочитан обратно без расхождений.

Спектр — 32 768 отсчётов после атаки, окно Ханна, шаг 0,9766 Гц; частота пика уточняется по трём соседним точкам:

Нота По спектру Шаг фазы даёт Равномерный строй
A4 (69) 439,992 Гц 440,0000 Гц 440,0000 Гц
C#5 (73) 554,350 Гц 554,3653 Гц 554,3653 Гц
E5 (76) 659,261 Гц 659,2551 Гц 659,2551 Гц

Все три ноты найдены с ошибкой меньше 0,02 Гц — в пятьдесят раз меньше шага спектра. Самая сильная побочная составляющая вдали от нот — −84,2 дБ (5288 Гц): ошибки таблицы из 256 точек с интерполяцией и округления до 12 бит.

armclang 6.24 собрал ядро и прошивку без предупреждений: 790 и 598 байт кода, 1074 байта таблиц (синус и шаги нот), 560 байт ОЗУ.


198п.7 Грабли

Нет бегущего статуса. Разбор, который ждёт статус перед каждой нотой, теряет все ноты аккорда, кроме первой.

Реальное время посреди ноты. 0xF8 между нотой и скоростью — обычное дело при синхронизации. Его пропускают, не трогая статус и собранные данные.

Note On со скоростью 0. Так отпускают ноту, не меняя бегущий статус Note On. Не понять — ноты зависают.

Данные после SysEx. SysEx сбрасывает бегущий статус; байты данных после него без нового статуса — не ноты.

Потерянный байт. При переполнении USART (ORE) собранное сообщение неверно — разбор начинают заново.

Синтезатор из двух прерываний. События — через очередь, считает голоса одно прерывание DMA.

Запас на восемь голосов. Смесь без запаса обрезается на аккордах, с запасом один голос тих: ±200 кодов из 2048.

Частота отсчётов не делится. На G474 32 000 Гц от 170 МГц не получается ровно — ±0,0094 %.

Земля через MIDI. Вход развязан оптроном: соединять земли приборов через разъём MIDI не нужно — появится петля по земле.


198п.8 Итоги

Полные исходники

synth_core.c — разбор MIDI и синтезатор.

/* synth_core.c — раздел 198п: разбор MIDI и синтезатор (см. synth_core.h). */
#include "synth_core.h"
#include "synth_tab.h"

_Static_assert(SYN_VOICES * 32767u * 127u / 128u / 128u <= 2047u, "8 голосов на полной громкости не выходят за 12 бит");
_Static_assert(sizeof SYN_INC / sizeof SYN_INC[0] == 128u, "шаг фазы на каждую из 128 нот");

/* ── MIDI ──────────────────────────────────────────────────────────────── */
void midi_init(midi_rx *m, int channel)
{
    m->status = 0;
    m->n = m->need = 0;
    m->in_sysex = 0;
    m->channel = (int8_t)channel;
    m->realtime = m->sysex = m->ignored = 0;
}

int midi_feed(midi_rx *m, uint8_t b, midi_ev *e)
{
    if (b >= 0xF8u) {                                          /* реальное время: где угодно, статус не трогает */
        m->realtime++;
        return 0;
    }
    if (b >= 0xF0u) {                                          /* системные: SysEx и общие — сбрасывают статус */
        if (b == 0xF0u) {
            m->in_sysex = 1;
            m->sysex++;
        } else if (b == 0xF7u) {
            m->in_sysex = 0;
        }
        m->status = 0;
        m->n = 0;
        return 0;
    }
    if (b & 0x80u) {                                           /* статус канала */
        m->in_sysex = 0;
        m->status = b;
        m->n = 0;
        m->need = (uint8_t)(((b & 0xE0u) == 0xC0u) ? 1u : 2u);  /* Cx и Dx — один байт данных */
        return 0;
    }
    if (m->in_sysex)
        return 0;
    if (!m->status) {                                          /* данные без статуса */
        m->ignored++;
        return 0;
    }
    m->data[m->n++] = b;
    if (m->n < m->need)
        return 0;
    m->n = 0;                                                  /* статус остаётся: бегущий */
    uint8_t type = m->status & 0xF0u, ch = m->status & 0x0Fu;
    if (m->channel >= 0 && ch != (uint8_t)m->channel) {
        m->ignored++;
        return 0;
    }
    if (type == 0x90u && m->data[1]) {
        e->type = EV_ON;
        e->note = m->data[0];
        e->vel = m->data[1];
        return 1;
    }
    if (type == 0x80u || type == 0x90u) {                      /* Note Off или Note On со скоростью 0 */
        e->type = EV_OFF;
        e->note = m->data[0];
        e->vel = 0;
        return 1;
    }
    if (type == 0xB0u && m->data[0] == 123u) {                 /* CC 123: все ноты выключить */
        e->type = EV_ALL_OFF;
        e->note = e->vel = 0;
        return 1;
    }
    return 0;
}

/* ── синтезатор ────────────────────────────────────────────────────────── */
uint32_t synth_inc(uint8_t note)
{
    return SYN_INC[note & 0x7Fu];
}

void synth_init(synth *s)
{
    for (unsigned i = 0; i < SYN_VOICES; i++) {
        s->v[i].phase = s->v[i].inc = s->v[i].env = s->v[i].age = 0;
        s->v[i].note = s->v[i].vel = s->v[i].state = 0;
    }
    s->clock = s->stolen = s->clipped = 0;
}

static voice *pick_voice(synth *s, uint8_t note)
{
    voice *best = 0;
    for (unsigned i = 0; i < SYN_VOICES; i++)                  /* та же нота звучит — перезапустить её */
        if (s->v[i].state && s->v[i].note == note)
            return &s->v[i];
    for (unsigned i = 0; i < SYN_VOICES; i++)                  /* свободный */
        if (!s->v[i].state)
            return &s->v[i];
    for (unsigned i = 0; i < SYN_VOICES; i++)                  /* самый тихий из затухающих */
        if (s->v[i].state == 3u && (!best || s->v[i].env < best->env))
            best = &s->v[i];
    if (!best) {
        best = &s->v[0];                                       /* самый старый */
        for (unsigned i = 1; i < SYN_VOICES; i++)
            if (s->v[i].age < best->age)
                best = &s->v[i];
    }
    s->stolen++;
    return best;
}

void synth_event(synth *s, const midi_ev *e)
{
    if (e->type == EV_ON) {
        voice *v = pick_voice(s, e->note);
        if (!v->state) {
            v->phase = 0;
            v->env = 0;
        }
        v->note = e->note;
        v->vel = e->vel;
        v->inc = synth_inc(e->note);
        v->state = 1;
        v->age = ++s->clock;
        return;
    }
    for (unsigned i = 0; i < SYN_VOICES; i++) {
        voice *v = &s->v[i];
        if ((v->state == 1u || v->state == 2u) && (e->type == EV_ALL_OFF || v->note == e->note))
            v->state = 3;
    }
}

void synth_render(synth *s, uint16_t *out, uint32_t n)
{
    for (uint32_t k = 0; k < n; k++) {
        int32_t mix = 0;
        for (unsigned i = 0; i < SYN_VOICES; i++) {
            voice *v = &s->v[i];
            if (!v->state)
                continue;
            if (v->state == 1u) {                              /* атака */
                v->env += SYN_ATTACK;
                if (v->env >= 65536u) {
                    v->env = 65536u;
                    v->state = 2;
                }
            } else if (v->state == 3u) {                       /* затухание */
                v->env = v->env > SYN_RELEASE ? v->env - SYN_RELEASE : 0u;
                if (!v->env) {
                    v->state = 0;
                    continue;
                }
            }
            uint32_t idx = v->phase >> 24, frac = v->phase >> 8 & 0xFFFFu;
            int32_t a = SYN_SINE[idx], b = SYN_SINE[idx + 1u];
            int32_t smp = a + (int32_t)(((b - a) * (int32_t)frac) >> 16);   /* линейная интерполяция */
            mix += (((smp * (int32_t)v->env) >> 16) * v->vel) >> 7;
            v->phase += v->inc;
        }
        int32_t o = 2048 + (mix >> 7);
        if (o < 0 || o > 4095) {
            s->clipped++;
            o = o < 0 ? 0 : 4095;
        }
        out[k] = (uint16_t)o;
    }
}

Дальше: раздел 198р «Мини-проект: OBD-II сканер на STM32 — обороты, скорость, температура и коды ошибок».

Открыть в интерактивном справочнике