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 |
Выводы — по таблицам 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. Три правила, которые ломают наивный разбор:
- бегущий статус — после статуса могут идти несколько пар данных подряд; статус действует, пока не придёт другой;
- реальное время (0xF8…0xFF: синхронизация, Active Sensing) может прийти посреди сообщения, между его байтами данных, и статус не трогает;
- Note On со скоростью 0 — это Note Off.
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 Итоги
- MIDI — UART на 31 250 бит/с: байт 320 мкс, Note On 960 мкс; десять нот — 9,60 мс, с бегущим статусом — 6,72 мс; делитель F407 — 1344 ровно.
- Разбор: бегущий статус, реальное время посреди данных, Note On со скоростью 0, сброс статуса после SysEx — опыт из 29 байтов дал 6 событий.
- Синтез: таблица синуса 256 точек с интерполяцией, 32-битная фаза, 8 голосов, огибающая 5 / 50 мс; настройка точнее 0,000437 цента.
- ЦАП от TIM6 ровно на 32 000 Гц через DMA1 Stream5 канал 7, задержка половины буфера — 2 мс; на G474 частота отсчётов выходит с ошибкой ±0,0094 %.
- Модель на Python: те же отсчёты по CRC-32, WAV, ноты по спектру с ошибкой меньше 0,02 Гц, побочные составляющие не выше −84,2 дБ.
- armclang: 790 + 598 байт кода, 1074 байта таблиц, 560 байт ОЗУ.
Полные исходники
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 — обороты, скорость, температура и коды ошибок».