niazlv/fun-open-5012h

GitHub: niazlv/fun-open-5012h

FNIRSI-5012H口袋示波器的开源增强固件,支持20种协议解码、FFT频谱分析,并附带完整的硬件模拟器和主机端测试套件。

Stars: 0 | Forks: 0

# fun-open-5012h — открытая прошивка для FNIRSI-5012H ![платформа](https://img.shields.io/badge/MCU-GD32F407VE-blue) ![ядро](https://img.shields.io/badge/ядро-Cortex--M4%20%40%20250%20МГц-blue) ![выборка](https://img.shields.io/badge/выборка-125%20Мвыб%2Fс-green) ![экран](https://img.shields.io/badge/экран-ST7789%20320×240-green) ![эмулятор](https://img.shields.io/badge/эмулятор-Unicorn%20%2B%20SDL2-orange) ![лицензия](https://img.shields.io/badge/лицензия-BSD--3--Clause,%20DOOM:%20GPL--2.0-lightgrey) Прошивка для карманного осциллографа FNIRSI-5012H (GD32F407VE, LCD 320x240) — [форк](#происхождение) открытой прошивки Алекса Тарадова. Всё, что написано ниже, можно проверить, не имея прибора: он [эмулируется целиком](#эмулятор-железа), алгоритмы покрыты [хостовыми тестами](#сборка), а скриншоты не сняты с экрана — они сгенерированы из того же образа, что заливается в железку. ## Оглавление - **Начать отсюда** - [Чем это интересно](#чем-это-интересно) — пять вещей, ради которых стоит листать дальше - [Что умеет](#что-умеет) — одна таблица, весь список - [Скриншоты](#скриншоты) — спектр, декодер, входной тракт, меню - [Что это за прибор и в чём он врёт](#что-это-за-прибор-и-в-чём-он-врёт) — предыстория железки - **Пользоваться** - [Управление](#управление) · [Особенности](#особенности) - [Доступные приложения](#доступные-приложения): [осциллограф](#1-oscilloscope) · [3D](#2-3d-graphics) · [движок](#3-3d-engine) · [трассировщик](#4-ray-trace-test) · [Flappy](#5-flappy-bird) · [Snake](#6-snake-game) · [2048](#7-2048) · [Тетрис](#8-tetris) · [DOOM](#9-doom) · [БК-0010-01](#10-бк-0010-01) · [дампы](#11-coredump-viewer) · [flash](#12-flash-viewer) - **Как это сделано** - [Эмулятор железа](#эмулятор-железа) — Unicorn, модели периферии, модель тракта - [Архитектура](#архитектура) — стек экранов, виджет меню, как добавить приложение - [Структура проекта](#структура-проекта) — что в каком каталоге и почему - **Собрать и залить** - [Сборка](#сборка) · [Прошивка](#прошивка) · [Что нельзя стирать](#что-нельзя-стирать) - [Отладка в VS Code](#отладка-в-vs-code) - **Справочное** - [Иглы на трассе: что это было](#иглы-на-трассе-что-это-было) — разбор дефекта захвата: как выглядел, почему, чем починили и где предел шума - [Технические характеристики](#технические-характеристики) · [Оптимизации](#оптимизации) - [Происхождение](#происхождение) · [Лицензия](#лицензия) Отдельными файлами: [все декодеры подробно](doc/decoders.md) · [эмулятор](emu/README.md) · [плата](doc/Hardware.md) · [распайка программатора](doc/Programming.md) · [что лежит в attic](attic/README.md) ## Чем это интересно **Эмулятор всей платы.** [`emu/`](emu/) исполняет тот самый `open-5012h.bin`, который заливается в прибор: Unicorn крутит код, вокруг — модели GD32F407, AD9288, ST7789, кнопочных шифраторов и батареи. Исходники прошивки про эмулятор не знают, граница — регистры и выводы. Сигнал на входе и несовершенства тракта (полоса, выброс, апертурный джиттер, DNL АЦП) задаются с командной строки, прогоны детерминированы. **Двадцать протокольных декодеров по одному щупу**, девятнадцать опознают протокол сами — в том числе те, что умеют себя проверить: CAN по сошедшемуся CRC-15, SWD по шести проверкам внутри пакета, 1-Wire по CRC8 у ROM. Двадцатый — SPI без такта — берётся только вручную и прямо называет свои допущения. **1173 хостовые проверки** DSP и декодеров: `make test`, без прибора и без ARM-тулчейна. Список декодеров в тестах — вайлдкард, поэтому новый попадает под них в день, когда его написали. **Три дефекта, которые прибор переживал молча** — [и почему он молчал](#что-это-за-прибор-и-в-чём-он-врёт). Такты ожидания флеша не были выставлены вовсе; CRC записи конфига считался по диапазону, включающему само поле CRC; хранилище конфига перекрывало хвост образа. Каждый выглядел как «настройки почему-то слетели». **Рендерер id Software на настоящем уровне из WAD** — не рейкастер по сетке: обход BSP-дерева, visplanes, перспективная развёртка текстур и оригинальные colormap'ы. Все таблицы проекции считаются на хосте, рабочий набор уложен в 120 КБ SRAM, оставшихся от кольца захвата. Текстуры не влезают в образ и живут на внешней 8-МБ флешке, откуда вычитываются по колонке — [что это стоит по кадрам](#9-doom). Всё запускается из общего лаунчера, переключается на ходу и не мешает друг другу: пока идёт игра, осциллограф остановлен и отдаёт ей свою память. ## Что умеет | | | | --- | --- | | **Осциллограф** | 50 нс/дел … 50 с/дел (медленнее 500 мс — самописец), 50 мВ/дел … 10 В/дел, до 125 Мвыб/с | | **Измерения** | Vpp, частота, скважность, Vrms, Vavg, тип сигнала, КНИ, джиттер | | **Спектр** | FFT с поиском основной частоты по гребёнке гармоник | | **Декодеры** | UART, MIDI, CAN, LIN, SENT, 1-Wire (DS18B20), DHT11/22, WS2812, NEC IR, EV1527, DShot, SPI! (по одному щупу, вручную), SWO/ITM, SWD, Manchester, RC5/RC6, DALI, KNX TP1, Servo PWM, сырые уровни, автоопределение — [как устроен каждый](doc/decoders.md) | | **Отладка** | дампы при Hard Fault, просмотрщик flash, авто-открытие дампа | | **Игры и 3D** | DOOM, Flappy Bird, Snake, 2048, тетрис, куб, RayCast/RayTrace-движок | ## Скриншоты Сняты с [эмулятора](emu/), исполняющего тот же `open-5012h.bin`, что заливается в прибор; кадр — то, что прошивка вывела на шину дисплея. Сигнал на входе синтезированный, числа на экране считает сама прошивка. ### Спектр (FFT) Меандр 200 кГц. F0 найден по гребёнке гармоник: 200.02 кГц, THD 19.9%, шумовая полка −39 дБ. ![Спектр](https://static.pigsec.cn/wp-content/uploads/repos/cas/bc/bc1dd7678f402d42ad78dba63fbf41a25bef7506f6e4702eb4b3433d606abafb.png) ### Декодер протоколов `Hiмир!` в 8N1 @ 115200, кадр на стыке ASCII и UTF-8: `48`/`H` и `69`/`i` с символьными подписями, следом двухбайтовая последовательность `D0 BC` → `U+043C`. Каждый байт подписан под трассой, сетка битов — на самой осциллограмме. Протокол и скорость определены автоматически; кадр, разрезанный краем записи, честно посчитан ошибкой (`err 1`). ![Декодер UART](https://static.pigsec.cn/wp-content/uploads/repos/cas/36/36560e478590ce02ab09f25bde4254dc1014c22774c530e67f628d7e08f9dedb.png)
Лаунчер ![Лаунчер](https://static.pigsec.cn/wp-content/uploads/repos/cas/d4/d4c8ee7609d4fe55a9633aab6b6f2215c6a28916c82929b89d7a8a647adb5a3c.png)
Синус 10 кГц после AUTO Задано 2 Vpp / 10 кГц, измерено 2.03 В / 9.99 кГц, RMS 702 мВ (теория 707). Масштаб 500 мВ/дел, 50 мкс/дел подобран кнопкой `AUTO`. ![Синус](https://static.pigsec.cn/wp-content/uploads/repos/cas/2d/2df3a3f8a9bcf15dc8840c34a5765f667af94c6be78981f27aee0b87129f2acc.png)
Меандр 25 кГц, скважность 30% Измерено 25.00 кГц, d 30.0%. Скважность считается по всей записи. ![Меандр](https://static.pigsec.cn/wp-content/uploads/repos/cas/a7/a70d707bacbf391be591de23e9268c9eedfc5cb52f73a391c909b91ea0d3cfd5.png)
Поиск глитча Меандр 200 кГц с выбросом 80 нс раз в 32 периода. `SHIFT+50%` нашёл самый узкий импульс записи, остановил захват и подвёл развёртку. ![Глитч](https://static.pigsec.cn/wp-content/uploads/repos/cas/3d/3ddd6f8d2faf9b9b175ce80e9e1f1205f6b78ef27ac413f29d5899728e3a89f9.png)
Меню Раздел осциллографа: триггер, измерения, отображение, декодер, калибровка, спектр. Настройки сохраняются во flash. ![Меню](https://static.pigsec.cn/wp-content/uploads/repos/cas/55/55e335e1a371f44b7ff25d0e778694675feb21afb81b109c7656a7e09cb42170.png)
Входной тракт: ideal / 100 МГц / 15 МГц Один и тот же меандр 2 МГц с нулевым временем фронта на источнике, три варианта `--afe`. `--afe ideal` — все несовершенства выключены, Vpp 1.99 В: ![Идеальный тракт](https://static.pigsec.cn/wp-content/uploads/repos/cas/b6/b62323fdca6d5f204c5b05dd21e77f45f9e7bd800f9b4c8aa30b16f27de8ecc3.png) Тракт прибора (100 МГц, выброс 4%, шум и DNL АЦП по даташиту AD9288) — фронты 3.5 нс, выброс поднимает Vpp до 2.15 В: ![Реальный тракт](https://static.pigsec.cn/wp-content/uploads/repos/cas/5e/5ee06b2d51cfa532462473b204f197f6c46e8228d708855793973dfc0de061ce.png) `--afe "bw=15M overshoot=15"` — полосы не хватает: ![Узкая полоса](https://static.pigsec.cn/wp-content/uploads/repos/cas/ee/ee2ce0fb7e6ef78a5f1eb98a21c3cccb3d7dd057dd1d373ff40f4cfd2612d1df.png)
## Особенности - **Стек экранов** — приложения, меню и диалоги живут на одном стеке; экран, закрывший что-то собой, при закрытии сам вызывает перерисовку того, что было под ним - **Один виджет меню** на всю прошивку — меню описываются таблицами `const` - **У каждого приложения свой раздел системного меню** с настройками и свои страницы в общем разделе `Help` - **Текстовые страницы прокручиваются** — со скроллбаром, разделами и переходом по разделам, поэтому у справки нет длины, за которую нельзя выходить - **Единые правила управления** во всех приложениях ## Управление | Кнопка | Действие | | --- | --- | | `UP` / `DOWN` | Навигация | | `LEFT` / `RIGHT` | Изменение значения, вход в подменю, действия приложения | | `MODE` | Выбрать / подтвердить; в приложении — его основное действие | | `MENU` | Системное меню поверх работающего приложения | | `SHIFT+MENU` | Выход в лаунчер из любого места |
Переназначение клавиш: что можно переназначить и чем это ограничено `MENU > General Settings > Remap Keys` открывает таблицу: слева физическая клавиша, справа то, чем она работает. Переключатель в первой строке включает трансляцию целиком, последняя строка возвращает все клавиши к себе. Раскладка лежит в `config.key_mapping[]` и сохраняется во флеш, как и прочие настройки. Переназначаются одиннадцать клавиш: `F1` `F2` `SAVE` `AUTO` `AC/DC` `STOP` `EDGE` `50%` `TRIG` `TRIG_UP` `TRIG_DOWN`. Навигация (`UP` `DOWN` `LEFT` `RIGHT` `MODE` `MENU` `SHIFT`) — нет, и это не осторожность, а условие работоспособности: меню ездят на этих клавишах, поэтому никакая раскладка не может отрезать выход из редактора, который её задал. `1X/10X` в том же списке защищённых, хотя выглядит обычной клавишей: на плате это и есть `SHIFT` — один провод (`PE12`) и один бит под двумя именами. Цель строки ставится двумя способами. `MODE` открывает захват — нажмите ту клавишу, которой строка должна работать. `LEFT`/`RIGHT` перебирают список, включая `(off)`: клавиша перестаёт делать что-либо. Перебор здесь не ради удобства — если клавиша сломана, нажать её в захвате нельзя, а направить на неё живую можно, и это ровно тот случай, ради которого переназначение и нужно. Результат не зависит от порядка таблицы: обмен двух клавиш местами и цепочка (`F1` на кнопку `F2`, `F2` дальше) дают одно и то же при любом наборе одновременно нажатых клавиш. Аккорд с системной клавишей тоже переназначается — `SHIFT+F1` уходит туда же, куда `F1`.
Как собирается системное меню и как читаются текстовые страницы Системное меню собирается при открытии: сначала настройки работающего приложения, затем `General Settings`, `Advanced`, `Help` и `Exit Application`. Раздел `Help` — единственное место, где живут текстовые страницы: сначала страницы работающего приложения (у осциллографа — кнопки, спектр, декодер, калибровка), под разделителем — общие `Key Bindings`. Пока меню открыто, приложение не выполняется и не рисует. Внутри текстовой страницы: `UP`/`DOWN` — строка, `LEFT`/`RIGHT` — экран, `TRIG_UP`/`TRIG_DOWN` — раздел, любая другая кнопка закрывает. Страница, которая помещается целиком, закрывается любой кнопкой, как и раньше.
## Доступные приложения ### 1. Oscilloscope Основное приложение. [Базовая прошивка](#происхождение) даёт сам прибор — захват, триггер, развёртку, аттенюатор, калибровку. Этот форк добавил поверх него весь слой анализа, которого в открытой прошивке не было. **Развёртка и вход** - 28 шагов развёртки: **50 нс/дел … 50 с/дел**; от 1 с/дел и медленнее луч становится самописцем — режим **roll**, описан ниже - 8 диапазонов по вертикали: **50 мВ/дел … 10 В/дел** (7 транзисторов аттенюатора, 50 и 100 мВ отличаются только программным зумом) - Чередование двух АЦП даёт настоящие **125 Мвыб/с**; кольцо захвата — 96 КБ, а экран, измерения, БПФ и декодеры читают его стабильный снимок в 24 КБ - AC/DC, калибровка нуля и усиления хранятся в конфиге
Как дрались за 125 Мвыб/с — и чем эта драка кончилась AD9288 — это **два независимых 8-битных преобразователя в одном корпусе**, у каждого свой такт кодирования (PA8 — АЦП A, PA9 — АЦП B). Здесь оба тактируются от 62.5 МГц, и единственный способ получить из них 125 Мвыб/с — развести такты на полпериода. Иначе оба преобразователя защёлкивают **один и тот же момент**, и каждый 16-битный снимок шины GPIOD несёт одно мгновение дважды. Работает это только благодаря одной особенности АЦП: **data align**. Канал B кодирует на полпериода в стороне, но его выход пересинхронизируется внутри микросхемы к фронту такта канала A. Шина меняется только по фронтам A, поэтому одна 16-битная транзакция DMA безопасно уносит оба канала. В унаследованном коде такты были сведены **в фазу**: одинаковые `CHxCV`, одинаковый режим PWM, нетронутая полярность. Прибор терял половину проектной частоты — 50 МГц складывался ровно в 12.5 (= |50 − 62.5|), а на трассе сидела рябь от отсчёта к отсчёту, потому что дубликат приходил с другого преобразователя, со своим усилением и своим смещением. Правка — одна константа: `ADC_B_CLOCK_MODE` с 6 на 7, режим PWM 1 вместо 0 на CH1 таймера. И вот здесь была цена. **Первая попытка инвертировала не тот канал.** CH0 — это такт канала A, а он опора data align: сдвинув его, мы сдвинули каждый переход шины на 8 нс прямо на момент защёлкивания DMA. Так как перекос старта между TIMER0 и TIMER7 — это то, что получится из двух записей в CEN, каждый `dma_start()` выпадал случайно: то чистый прогон, то метастабильные нечётные байты — иглы, около 600 мВ мусора на закороченном входе. Такт канала A двигать нельзя вообще. Инверсия CH1 оставляет тайминги шины байт в байт такими же, какими они были у всегда работавшей синфазной сборки. Дальше по тракту чередование стоит ещё двух вещей. Нечётный байт кольца приходит с канала A, который **разведён на шину задом наперёд**, — его разворачивает `buffer_reverse()`, и делать это надо ровно один раз и в правильный момент (после снятия окна, до публикации записи). А триггер в двухканальном режиме смотрит только чётные байты: сравнивать перевёрнутые он не может, поэтому ищет фронт по одной фазе, на 62.5 Мвыб/с, даже когда запись идёт на 125. **Чем кончилось.** На эмуляторе всё сходится, но это ничего не доказывает: [`emu/src/board.c`](emu/src/board.c) зашивает чередование жёстко и режимы `TIMER0->CHCTL0` не читает вовсе — он выдаст 125 Мвыб/с и синфазной прошивке тоже. Надпись `125 M` в углу экрана — это `BASE_SAMPLE_RATE`, константа компиляции: она стоит там независимо от того, что делают такты. Записанное наблюдение на живом приборе есть **только у неверного варианта** (те самые иглы); правильный проверяется по списку в комментарии [`capture.c:105`](src/acq/capture.c#L105) — 50 МГц на вход при 50 нс/дел: `~50 МГц` значит работает, `~15 МГц` значит пара легла в обратном порядке (лечится программно), иглы значит плата data align не стратила и надо вернуть 6/6. Ни один пункт пока не отмечен как увиденный.
Кольцо, снимок и что именно замораживает STOP Кольцо захвата 96 КБ. Пока идёт захват, всё (экран, измерения, БПФ, декодеры) читает не кольцо, а его стабильный снимок в 24 КБ — и именно частота этого снимка определяет, что видно, а что свернётся в алиас: - до ~16 мкс/дел снимок это **окно кольца на полной частоте**: 24576 отсчётов подряд, 125 Мвыб/с, Найквист 62.5 МГц - медленнее — снимок это прореживание всей записи 4:1 (31.25 Мвыб/с, Найквист ~15.6 МГц) - `STOP` замораживает **то, что на экране**: в `Normal`/`Single` кадр последнего триггера (снимок) — кольцо к этому моменту уже перезаписано тем, что пришло ПОСЛЕ пойманного, и отдавать его значило бы подменить улов мусором при первом же зуме. В `Auto`, где экран и так живой, достаётся сырое кольцо целиком, если текущий свип успел его обернуть. Автостопы (отлов посылки, поиск глитча) всегда берут кольцо — им нужна самая длинная история. Пока замороженная запись — только снимок, в углу сетки серым написано `rec <длина>`: столько записи есть под зум Текущая частота снимка показана на экране справа сверху — это она, а не частота кольца, решает, увидите вы 50 МГц или его алиас
**Триггер** (`Trigger Settings`) - Режимы `Auto` / `Normal` / `Single`, фронт `Rise` / `Fall` / `Both` - Уровень кнопками `TRIG_UP`/`TRIG_DOWN` или из меню - `50%` — уровень в середину сигнала одной кнопкой - `AUTO` — автонастройка: вертикаль по длинной записи, частота по диапазонам (иначе автонастройка ныряет в милливольты и цепляется за шум)
Как устроен поиск фронта — 139 инструкций ассемблера и ни одной на C Это код Алекса Тарадова, не форка: [`src/acq/trigger.c`](src/acq/trigger.c) не изменился с первого коммита ни на байт. Разбор здесь потому, что это самая плотная часть прибора и понять её стоит. Задача неприятная: фронт надо найти в 96 КБ кольца, которое DMA продолжает переписывать, и найти быстрее, чем оно перепишется. Шесть подпрограмм — `Rise` / `Fall` / `Both` × одноканальный / двухканальный режим — написаны целиком на ассемблере, без вызова C: | подпрограмма | инструкций | | --- | --- | | `trigger_find_rise_single` / `fall_single` | 139 | | `trigger_find_both_single` | 135 | | `trigger_find_rise_dual` / `fall_dual` | 141 | | `trigger_find_both_dual` | 89 | Все шесть плюс `trigger_set_levels` — 2324 байта кода. **Четыре отсчёта за инструкцию.** Уровень триггера размножен по всем четырём байтам слова (`trigger_set_levels`), а сравнение — `uqsub8`, насыщающее вычитание по байтам из набора SIMD Cortex-M4: одна инструкция сравнивает сразу четыре отсчёта с четырьмя копиями уровня. Ненулевой байт результата означает «этот отсчёт перешёл», нулевой — «нет». Ни одной ветки на отсчёт. **Горячий цикл — 19 инструкций на 32 отсчёта.** `ldm` забирает восемь слов одним махом, дальше восемь пар `uqsub8` + `cbnz`, затем `subs`/`bne`. Меньше 0.6 инструкции на отсчёт — с запасом быстрее, чем 125 Мвыб/с, которые подсовывает DMA. В двухканальном режиме к каждому слову добавляется `ands #0x00ff00ff` (27 инструкций на 32 байта): нечётные байты кольца приходят с канала A разведёнными задом наперёд, сравнивать их бессмысленно, поэтому триггер ищет фронт по одной фазе. **Три инструкции, чтобы понять, какой из четырёх.** Когда `uqsub8` дал ненулевой байт, надо узнать его позицию: `rbit`, `clz`, `lsr #3`. Никакого перебора байтов. **Почему он не врёт.** Голое сравнение с уровнем сработало бы мгновенно на сигнале, который уже выше уровня, — и показало бы «фронт» там, где его нет. Поэтому подпрограмма сначала читает первый отсчёт записи, и если он уже выше, уходит в отдельный цикл ожидания: пока сигнал не окажется **ниже** уровня хотя бы на один отсчёт, фронт не ищется вообще. Гистерезис — `0x03030303`, три кода из 256 на каждый байт, вычитается при взведении и прибавляется обратно при поиске, поэтому шум на самом уровне не порождает череду ложных срабатываний. Отдельный кусок разбирает случай, когда спад и подъём попали в одно и то же 4-байтовое слово. **Почему код выглядит скопированным.** Восемь одинаковых блоков `41:`…`47:` и трамплины вроде `59: b 61f` — не небрежность: у `cbnz` дальность прыжка всего +126 байт, и цель приходится подтаскивать поближе. В исходнике это прямо помечено комментарием.
**Измерения** (`MODE`, настройки в `Measurements`) - Vpp, частота, скважность, Vrms, Vavg, тип сигнала, КНИ (THD) - Два независимых места, включаются раздельно: - `Panel over the trace` — полупрозрачная панель поверх луча, мелкий шрифт, показывает весь отмеченный галочками набор - `Status line` — два конкретных значения крупным шрифтом внизу: `Left` и `Right` выбираются по отдельности, ничего не крутится - Слот в положении `Off` возвращает своё место статусной строке: фронт и уровень триггера, горизонтальная позиция. Оба `Off` — строка полностью стоковая - Уровни берутся по перцентилям и с отбраковкой выбросов, поэтому одиночная игла не портит амплитуду **Спектр** (`SHIFT+MODE`) - БПФ по всей записи; разрешение `df = 1/время записи`, то есть его задаёт **развёртка**, а не частота дискретизации — 50 и 100 Гц сети разделяются только на 5 мс/дел и медленнее - Основная частота определяется по гребёнке гармоник, а не по самому высокому пику: сеть читается как 50 Гц даже когда третья гармоника выше - Курсор, переход по пикам, управление шириной обзора **Roll — самописец на медленных развёртках** (сам собой, от 1 с/дел) - Луч перестаёт быть развёрткой и становится лентой: правый столбец — это «сейчас», всё остальное на столбец старше. В слоте состояния вместо `WAIT`/`TRIG` горит `ROLL`, поле позиции показывает, сколько времени помещается на экране: от **12 с** на 1 с/дел до **10 минут** на 50 с/дел - `Display > Roll from` опускает границу до 500, 200 или 100 мс/дел — туда, где развёртка ещё может, но экран уже стоит секундами - Каждый столбец — **пиковая огибающая** всех отсчётов за своё время, а не одна выборка: на 122 квыб/с в неё попадает и выброс шириной 20 мкс - Ни одна ручка **не стирает нарисованное**. `SHIFT+U/D` и `UP`/`DOWN` пересчитывают историю по вертикали, `SHIFT+L/R` — по времени: медленнее — столбцы сливаются, быстрее — один становится несколькими. Частота дискретизации в roll не зависит от шага развёртки, поэтому столбец на любом из них означает одно и то же и пересчёт законен. Разглядеть в подробностях то, что записано грубо, это не даст — несколько новых столбцов несут одну огибающую, — но десять минут записи остаются - Заход в спектр, тренд или декодер ленту тоже **не убивает**: она живёт вне столбцов трассы, которые эти виды забирают под себя. Непрерывной её при этом никто не изображает — время, проведённое в другом виде, рисуется тем разрывом, которым оно и было - Триггер не участвует; захват на время roll удерживается в `AUTO`, чтобы кольцо крутилось, а измерения продолжали приходить. Сохранённый режим возвращается на выходе
Почему развёртки там нет, а не «не сделали» Запись — это всегда 98304 отсчёта, а тактирует их предделитель таймера шириной 16 бит. Отсюда потолок: самая длинная запись, которую железо может взять, — около **51 с**, то есть меньше **одного экрана** на 5 с/дел. Никакой настройкой это не обходится — на медленных развёртках записи просто не существует. Но и там, где она ещё существует, ждать её нечем: на 500 мс/дел запись набирается 6.4 с, и всё это время экран стоит, а потом разом меняется целиком. Ровно это стоковая прошивка и заменяет самописцем — и ровно поэтому `Roll from` позволяет опустить границу. Поэтому roll читает **кольцо на ходу**: `capture_fold_samples()` сворачивает в пару «мин/макс» всё, что DMA записал с прошлого вызова, без копирования и без снимка. Позиция записи берётся из регистров самого канала (`MBS` плюс `CH2CNT`), а не из программных указателей — те верны только внутри обработчика FTF и снаружи отстают на целый блок. Читать их порознь тоже нельзя: блок может закончиться между чтением `MBS` и чтением счётчика, и тогда база старого буфера складывается с полным счётчиком нового — ответ уезжает на блок в прошлое, а на экране это столбец с сотнями миллисекунд сигнала в нём, игла во всю высоту сквозь чистую трассу. Счётчик читается по разу с каждой стороны от адреса, и несовпадение — это ровно та ситуация. Сама лента живёт в буфере отображения и сдвигается влево на столбец: 300 столбцов min/max уже есть, а второй такой набор — это больше килобайта TCM, которого свободно около одного. Отсюда и пересчёт истории при смене вертикали вместо её сброса: строка пикселя знает достаточно, чтобы вернуть вольты и лечь на новое место.
**Послесвечение** (`Display > Persistence`) - `Off` — обычная трасса - `Infinite` — огибающая копится и **не уходит**, и рисуется **областью**: рант, мигнувший один раз полчаса назад, — заметное пятно, а не волосок. Ровно то, ради чего накопление и нужно, когда ищешь редкое событие - `Decay (CRT)` — та же огибающая, но рисуется **двумя кривыми**, верхней и нижней, а не заливкой между ними. Это и есть след луча: на сдвиге сигнала видно форму старой осциллограммы, которая тускнеет и подтягивается к новой. Яркость кончается за ~1.5 с и намеренно ниже цвета луча — луч должен оставаться самым ярким на сетке - на ровной трассе след прячется под ней и не мешает - на шумной — читается как естественное утолщение по шуму - каждый столбец дотягивается до половины пути к соседям: без этого на крутом фронте, где край огибающей за столбец уезжает далеко, получалась не кривая, а пунктир из точек
Почему это по столбцам, а не по пикселям, и что из этого следует Настоящий люминофор гаснет **попиксельно**. Экран — 300x200, то есть 60000 ячеек: даже по 4 бита это 30 КБ, а свободного TCM над резервом стека — около трёх. Поэтому яркость одна на столбец, и накопитель — та же пара min/max, что и раньше, плюс байт, который и так тратился на флаг «в этом столбце что-то есть». Отсюда одно правило, на котором всё держится: свечение обновляется **только там, где огибающая выросла**, а не там, где просто прошла трасса. Иначе каждый столбец с живой трассой был бы вечно на полной яркости — то есть это снова `Infinite`, только сложнее. При таком правиле ровная трасса перестаёт подсвечивать свой столбец сразу, и её собственная огибающая гаснет прямо под ней, а вспышка, дотянувшаяся дальше, загорается и уходит. Одного затухания яркости мало: столбец, который вырос хоть на пиксель, прибит к полной яркости, а на крутом фронте это почти каждый кадр — пиксель джиттера триггера двигает полосу постоянно. Прибитой оказывалась **вся** накопленная полоса, включая след минутной давности. Поэтому края ещё и подтягиваются к живой трассе, каждый сам по себе и только тот, который в этом кадре не рос: столбец на фронте зажигает ту сторону, где сейчас луч, и сбрасывает ту, которую покинул. Первая версия при этом **заливала** всё между краями, и выходил не след, а сплошной оливковый многоугольник: на крутом фронте, где джиттер триггера честно раздвигает огибающую на пол-экрана, заливка занимала треть сетки и светилась почти как луч. Поэтому в режиме затухания рисуются только сами края — та самая пара кривых, которую луч и оставил бы. `Infinite` заливку сохранил: там вопрос другой, «где сигнал вообще когда-либо был», и площадь на него отвечает лучше, чем контур.
**Декодеры** (`SHIFT+EDGE`, настройки в `Decoder`) - Автоопределение протокола или ручной выбор - `TRIG_UP`/`TRIG_DOWN` — переход по байтам, опция остановки на кадре - До 64 байт в результате; посылка длиннее декодируется настолько, насколько влезла в запись, `+` в заголовке панели значит «было больше» - Байты подписаны **на самом луче**: значение и символ под трассой, поле протокола, группы из нескольких байт и сетка бит поверх осциллограммы | протокол | чем опознаётся | что показывает | | --- | --- | --- | | **UART** | перебором скоростей: побеждает та, чьи кадры объясняют запись | текст и UTF-8, ошибки кадра | | **MIDI** | 31250 бод плюс грамматика статусных байт | `MIDI ch1 On C4 v100`, running status, SysEx | | **CAN 2.0A/2.0B** | сошедшимся CRC-15; полярность читается с линии | ID, DLC, данные, `ACK`/`NAK` | | **LIN** | брейком в 13 доминантных бит; скорость — из `SYNC 0x55` | `LIN 19200 ID=2A 4B`, `cls`/`enh`, `no resp` | | **SENT** | sync-импульсом в 56 тиков и ниблами 12…27 тиков | два 12-битных сигнала, CRC | | **1-Wire** | CRC8 у ROM; 55 семейств Далласа по коду | `DS18B20 +25.06C 12b`, `iButton`, `DS2413` | | **DHT11 / DHT22** | контрольной суммой пяти байт | `DHT22 45.3% +23.4C` | | **WS2812** | скважностью бита (28 % и 56 %) | `G0 R0 B0` и цвет пикселя `#221133` | | **NEC IR** | лидом 9 мс и инверсными байтами | адрес и команда, `cut` у обрезанных | | **RC5 / RC5X / RC6** | 1.778 мс bi-phase, у RC6 — двойной toggle | адрес, команда, toggle | | **EV1527** | sync 1:31 и битом ровно в 4T | 20 бит адреса + 4 кнопки, повторы `x4` | | **DShot** | 16 бит с покоем по краям и CRC4 | газ в процентах, телеметрия, `bd` | | **Servo PWM** | постоянной каденцией кадра | ширина и угол: `1.75ms +45d 50Hz` | | **SPI!** | ничем — только выбором вручную | битовый поток на названных допущениях | | **SWO / ITM** | грамматикой пакетов ITM, от 100 кбит | printf прошивки, исключения, `PC=` | | **SWD** | шестью проверками внутри пакета | `DPIDR`, `CSW`, `TAR`, `ACK`, `par!` | | **Manchester** | ранами ровно двух длин 1:2, кадр в тишине | биты кадра, `inv`, нарушения `!b15` | | **DALI** | Manchester 1200 бит/с плюс форма адреса | `DALI a5 lvl 128`, ответ `ans YES` | | **KNX TP1** | импульсным кодом и чётностью каждого символа | `KNX 1.1.5>1/2/3 1B` | | **сырые уровни** | — | уровни, длительности, символы | Как каждый протокол опознаётся, чем подтверждается кадр, что попадает в подписи и в каком порядке идёт каскад — [`doc/decoders.md`](doc/decoders.md). **Стоп-режим** `STOP` замораживает запись, и по ней можно двигаться, менять масштаб, измерять, декодировать и строить спектр — анализ идёт по сырому кольцу. Полное описание слоя анализа — в `README_NEW_FEATURES.md`. ### 2. 3D Graphics Вращающийся куб с заливкой граней и отсечением невидимых поверхностей. Перерисовывается только та область, через которую куб прошёл. - `UP`/`DOWN`, `LEFT`/`RIGHT`, `TRIG_UP`/`TRIG_DOWN` — вращение по X, Y, Z (любая из них переключает в ручной режим) - `STOP` — автовращение вкл/выкл - `MODE` — сбросить вид - Меню: скорость, стиль (заливка / каркас / оба), сброс вида ### 3. 3D Engine Движок с режимами RayCast и RayTrace, сценой из кубов, сфер, треугольников и плоскостей и системой освещения. Кадр строится по две строки за проход главного цикла и начинается заново при любом движении камеры, поэтому на экране всегда один целый кадр, а не склейка из двух. - `UP`/`DOWN` — вперёд/назад, `LEFT`/`RIGHT` — вбок - `TRIG_UP`/`TRIG_DOWN` — вверх/вниз - `SHIFT` + стрелки — поворот камеры - `MODE` — переключить RayCast / RayTrace - Меню: рендерер, сброс камеры ### 4. Ray Trace Test Три сферы, трассировка по одной строке за проход главного цикла. - `LEFT`/`RIGHT` — облёт камерой - `MODE` — пересчитать кадр - Меню: разрешение, авто-облёт, `Render now` ### 5. Flappy Bird Сцена собирается слоями и перерисовывается по частям. `restore_rect()` — примитив, на котором держится всё остальное: ему дают прямоугольник, он кладёт обратно небо с градиентом, облака и трубы — именно в этом порядке. Кадр после этого — полосы, которые трубы освободили, сами трубы, полоса, которую покинула птица, и один блит 16×16 на неё саму. Птица составляется поверх того, мимо чего летит: тайл сначала заполняется небом той полосы градиента, где он оказался, потом колонками трубы, если она за спиной, и только потом спрайтом. Залить тайл одним цветом — значит пробить дырку в трубе ровно тогда, когда на птицу смотрят. Труба рисуется целиком, а не полосой, в которую въехала: затенение поперёк — то, что делает её трубой, но оно же означает, что двух одинаковых колонок у неё нет. Обычный трюк со скроллом («дорисовать колонку спереди, стереть сзади») оставляет блик там, где он был, и тянет за трубой гребёнку полос. - `MODE`, `UP` или `STOP` — взмах / старт / переиграть - Меню: сложность, «Tighten as you score», рестарт, сброс рекорда Просвет закрывается на 4 px каждые 6 очков, а трубы ускоряются — до шести ступеней, после чего хватит. Медали на финальной панели: бронза с 10, серебро с 25, золото с 50; спрайт один, перекрашивается по двум символам палитры. Рекорд лежит в конфиге и переживает выключение. ### 6. Snake Game Поле — сетка 20×13 клеток по 16 px поверх шахматной клетчатки. Каждая клетка собирается в промежуточном буфере 16×16 и уходит на панель одним блитом; за шаг перерисовываются четыре клетки — освободившийся хвост, новый хвост, голова, ставшая шеей, и новая голова, — так что цена шага не зависит от длины змейки. Тело рисуется по маске «какие стороны клетки продолжаются»: прямой участок читается трубой, поворот — коленом, соседние клетки сходятся без шва. Голова смотрит туда, куда идёт змейка, хвост сужается к кончику. Спрайты фруктов и карты уровней лежат в `src/apps/snake_assets.c` текстовой графикой: символ на пиксель у иконки, символ на клетку у карты. Иконка 16×16 занимает те же 272 байта, что и индексная картинка, но правится прямо в исходнике — на приборе нет файловой системы, и то, что не вкомпилировано, не существует. - Стрелки — направление; нажатия ставятся в очередь, поэтому «влево-вверх» между двумя шагами даёт два поворота, а не один и один потерянный - `MODE` (или `STOP`) — старт / пауза / переиграть уровень - `SHIFT`+`MODE` — новый заход с первого уровня - Меню: режим, скорость, переход через стены, бонусный фрукт, рестарт, сброс рекорда **Кампания** — восемь карт: каждая просит на пару фруктов больше предыдущей и идёт на 10 мс быстрее, а после восьмой начинается заново, ещё быстрее и с большими нормами. **Классика** — одно открытое поле без конца. Если край поля убивает, он нарисован: при выключенном переходе через стены игра сама обводит карту рамкой стен. Невидимая смертельная граница — единственное, чего игрок не может узнать, глядя на экран. Золотое яблоко появляется после каждого четвёртого фрукта, стоит 50 очков и последние три секунды мигает — это решение, а не подарок. Рекорд лежит в конфиге и переживает выключение. Логика и отрисовка проверяются на хосте: `make test` гоняет по всем картам жадного бота, сверяя после каждого шага сетку с телом змейки, и заодно проверяет, что каждая свободная клетка достижима (иначе фрукт может лечь туда, куда не доползти) и что ни один блит не вышел за пределы панели — `lcd_draw_buf` не обрезает, и уехавший блит на железе не пропадает, а рисуется в другом месте. ### 7. 2048 Поле настраивается от 3×3 до 6×6 в меню. Приложение занимает 3.6 КБ и не несёт ни одного спрайта: плитки — скруглённые прямоугольники из общего тулкита, цифры — тот же шрифт, что и везде. Мало кода по двум причинам. В клетке лежит **показатель степени**, а не значение: 0 — пусто, 11 — плитка 2048. Это байт, это прямой индекс в таблицу цветов, и «сливаются ли эти двое» превращается в сравнение целых. И сдвиг **один**, а не четыре: направление — это способ обойти поле, поэтому `cell_at()` переводит пару (линия, шаг) в клетку, а сам сдвиг никогда не узнаёт, куда его двигают. - Стрелки — сдвинуть всё поле - `MODE` — отменить последний ход (на один шаг) - `STOP` — новая игра - Меню: размер поля, новая игра, сброс рекорда Ход анимируется в два такта: плитки едут туда, где оказались, а потом то, что из них получилось, вспухает и оседает. Едет не поле целиком — только линии, по которым что-то двигалось, поэтому кадр анимации стоит нескольких миллисекунд, а не сорока. Ход, который ничего не сдвинул и ничего не слил, ходом не считается: новая плитка за него не появляется. Плитка, только что получившаяся из слияния, в этом же ходу больше не сливается — ряд из четырёх двоек даёт две четвёрки, а не восьмёрку. 2048 — веха, а не конец: баннер показывается один раз, игра продолжается. Числа, которым не хватает места на мелком поле, показываются в тысячах (`16k`, `64k`). Правила проверяются на хосте: `make test` разбирает слияния по клеткам на всех четырёх направлениях и всех четырёх размерах поля, проверяет отмену хода, конец игры, и что сетка при любом размере укладывается в свою панель. ### 8. Tetris Экран ландшафтный, а стакан — нет; из этого и вырос макет: стакан 10×20 по 11 px слева, а справа во всю ширину — панель, которая есть у любого тетриса: next, hold, счёт, уровень, линии. Фигура — одно 16-битное число: четыре строки по четыре бита в коробке 4×4, по числу на поворот, 56 байт на весь набор. Повернуть — взять следующее число, проверить позицию — пройти шестнадцать бит. Больше ничему в файле не нужно знать, как выглядит S. - `LEFT`/`RIGHT` — двигать, `UP` — поворот - `DOWN` — мягкое падение, 1 очко за клетку - `MODE` — сброс до дна, 2 очка за клетку - `F1` — отложить фигуру (hold), один раз на фигуру - `STOP` — пауза - Меню: стартовый уровень, призрак, новая игра, сброс рекорда Одна линия — 100, две — 300, три — 500, четыре — 800, всё умножается на уровень: четыре линии разом стоят вдвое больше четырёх одиночных, и в этом вся игра. Уровень растёт каждые десять линий. Фигуры приходят по семь: мешок тасуется и раздаётся целиком, поэтому партия никогда не решается «палкой, которая не пришла». Приземлившуюся фигуру ещё момент можно двигать и крутить — это то, что позволяет подсунуть её под навес. Поворот, который не влезает, сдвигается вбок на пару клеток, прежде чем быть отклонённым, — иначе фигура у стены просто перестаёт слушаться. Контур под фигурой показывает, куда она упадёт. Перерисовываются только изменившиеся клетки: то, что игрок видит, — это стакан плюс фигура плюс её призрак, собранные в теневую сетку и сравнённые с тем, что уже на панели. Кадр стоит горсти клеток, а не двухсот. ### 9. DOOM Не «похожий на DOOM» рейкастер, а рендерер id Software на настоящем уровне из WAD: обход BSP-дерева, список solidsegs, перспективная развёртка текстур из `R_StoreWallRange`, visplanes для пола и потолка, оригинальные colormap'ы, и движение с исходными разгоном, трением и порогом ступеньки в 24 юнита. - `UP`/`DOWN` — идти вперёд/назад, `LEFT`/`RIGHT` — поворот - `SHIFT` + `LEFT`/`RIGHT` — шаг вбок, `F2` — бег - `MODE` — открыть дверь, `STOP` — пауза - Меню: `Always run`, `Show stats`, `Restart level` Клавиатура заведена через два приоритетных шифратора, и одновременно видна только одна кнопка из группы: `LEFT`, `DOWN` и `TRIG` лежат в одной группе, поэтому модификатор обязан быть на одном из двух пинов, заведённых напрямую — это `SHIFT` и `F2`. Иначе шаг вбок в одну из сторон недостижим физически. Экран поделён точно: 200 строк вида, 32 строки полосы статуса из WAD (STBAR с лицом, панелью оружия и настоящими цифрами Doom) и 8 строк служебной строки с FPS и координатами. Здоровье, броня и патроны пока постоянные — менять их нечему, пока нет геймплея.
Сборка и заливка ассетов — три команды от IWAD до игры Уровень, текстуры, палитра и все таблицы проекции считаются на хосте — собрать их на устройстве не из чего. В репозитории их нет (это содержимое чужого WAD), поэтому: # 1. пакет из любого IWAD: шароварный doom1.wad, полный doom.wad, freedoom1.wad python3 tools/wadpack.py doom1.wad -o doom/doom_assets.bin --map E1M1 --split # -> doom/doom_assets.bin 110 КБ, линкуется в образ # -> doom/doom_assets_tex.bin 100 КБ, поедет на SPI-флешку # 2. прошивка cd make && make && make prog # или openocd, см. «Прошивка» # 3. текстуры на чип: открыть на приборе SPI Flash Loader, затем ./tools/spiflash.py add doom/doom_assets_tex.bin --name doom.tex Имя `doom.tex` обязательно — приложение ищет файл именно по нему. Проверить, что он на месте: `./tools/spiflash.py ls`. Без ключа `--split` получится цельный пакет по-старому: он тоже работает, но целиком линкуется в образ и **не влезает** — вместе с прошивкой это ~552 КБ при 512 КБ флеша. Заливка через `SPI Flash Loader` — три секунды. Без него, одним отладчиком, те же 100 КБ идут около пяти минут (`./tools/spiflash.py write`), зато не требуют трогать прибор.
Чем платим за ассеты на SPI — 20–55 fps вместо ровных 50 **С паком во flash было ровных ~50 fps. С текстурами на SPI-флешке — от 20 до 55, в зависимости от того, что на экране.** На простых видах цена почти нулевая, на сложных частота падает вдвое с лишним. Вот из чего это складывается. Внешняя флешка не отображена в адресное пространство: у GD32F407 нет блока QSPI, а EXMC — параллельная шина, чьи выводы заняты ЖК-экраном и АЦП. Значит указатель в пак не сделать, и каждую колонку текстуры надо **вычитать по шине** прежде, чем рисовать. Единица чтения выбрана по замеру ([tests/doom_cache.c](tests/doom_cache.c), 1896 видов E1M1): за 59 374 обращениями к текстурам на кадр стоят всего 557 вызовов `R_DrawColumn`. Проверять кеш на каждое обращение — больше миллисекунды на кадр только на проверках; на каждую колонку — в сто раз дешевле. Поэтому кешируются колонки: 64 слота по 128 байт (колонка максимум 70) в памяти, одолженной у осциллографа. Флэты пола так кешировать нельзя — `R_DrawSpan` прыгает по ним индексом, а не идёт подряд, — поэтому все 21 КБ грузятся резидентно при входе в приложение. Первая работающая версия давала ровные 17–18 fps. Две правки подняли её до нынешних 20–55, и обе стоит знать, потому что обе — про то, как легко потерять кратность на ровном месте: - **индексация кеша.** Слот выбирался как `(offset >> 4) & 63`, а колонки лежат в паке через ~64 байта: соседние попадали в слоты через один, и работала четверть таблицы. Сдвиг `>> 6` — та же таблица, используемая целиком. Померено харнессом по тем же 1896 видам: **270.7 подкачки на кадр против 164.3**, минус 39%; - **такт и команда.** Было PCLK/8 = 15.6 МГц с обычным чтением `03h` — вчетверо с запасом, потому что `03h` эти чипы рассчитывают на 50–80 МГц. Стало Fast Read `0Bh` (с холостым байтом, за который чип успевает выставить данные) на PCLK/4 = **31.25 МГц**: команда рассчитана за 100 МГц, так что предел теперь контроллер, а не флешка. Ровно вдвое. Что осталось нетронутым: **DMA на приёме.** Сейчас каждый байт идёт через опрос флагов в цикле, и на 132 байта подкачки это сотни тактов накладных расходов поверх самой передачи. DMA снял бы их с процессора целиком, и это следующий очевидный шаг, если частота кадров окажется важна. Если нужны ровные 50 без просадок, соберите цельный пакет без `--split` и уберите что-нибудь другое из образа: с паком во flash рендерер работает по указателям и никакой шины между ним и текстурами нет. Правда, освобождать придётся ~100 КБ.
Карта flash — все 512 КБ прошивке, конфиг на внешнем чипе Прошивке доступны **все 512 КБ** внутреннего флеша: с 2026-07-29 настройки и калибровка живут на внешней SPI-флешке (`0x7A0000`), и последний 128-килобайтный сектор МК освободился. Сейчас занято 344 из 512 КБ. Так было не всегда, и история тут поучительная. Конфиг занимал последние 256 КБ, хвост ассетов DOOM попадал внутрь него: прошивка работала, а `config_init` затирал собственные данные на первом же сохранении. Тогда хранилище ужали до одного сектора, а регион в `linker/gd32f407ve.ld` — до 384 КБ, чтобы переполнение было ошибкой сборки. Теперь регион снова 512 КБ, а ассерт в том же скрипте сверяется с `__store_reserved`, который Makefile выставляет по `CONFIG_STORE`: 0 для внешнего хранилища, 128 КБ для внутреннего. Сборка с `CONFIG_STORE=internal` упрётся ровно туда, где упиралась раньше.
Что уже есть и чего ещё нет Есть: геометрия и текстуры уровня, пол/потолок, небо, затухание света, прозрачные средние текстуры, столкновения по linedef'ам через blockmap, двери (`MODE` пускает луч на 64 юнита и открывает ближайшую спецлинию), полоса статуса. Пока нет: спрайтов (вещей и монстров), лифтов, переключателей, линий- триггеров «по проходу», звука. Двери с ключами открываются без ключей — инвентаря ещё нет. Свободной памяти в блоке — около 1 КБ из 120 КБ (`doom_mem_t`), так что спрайтам понадобится пересмотр бюджета; замеры высоких вод по всем массивам печатает `tests/doom_host.c --sweep`.
### 10. БК-0010-01 Электроника БК-0010-01 целиком — советская домашняя машина 1985 года: К1801ВМ1 (а это PDP-11), 32 КБ ОЗУ, 32 КБ ПЗУ, экран из 16 КБ, которые видеоконтроллер читает напрямую, и пять регистров наверху адресного пространства на всю остальную периферию. - `UP` `DOWN` `LEFT` `RIGHT` — стрелки БК (коды 032, 033, 010, 031) - `MODE` — ВВОД, `F1` — пробел, `AUTO` — табуляция - `STOP` — клавиша СТОП, `SAVE` — регистры и дизассемблер - Меню: экран (цвет/ч-б), вписывание 256 строк в 240, скорость, джойстик Статистика (`Status line`) рисуется в правом поле панели — 32 пикселя, до которых картинка БК не достаёт, — и ничего у машины не отнимает. В ч-б поля нет, и там она строкой снизу. Показывает: кадры (50 — успеваем, меньше — панель не тянет), **kHz** эмулируемого процессора (у настоящего БК — 3000), перерисованные строки панели и **vs BK** — то же самое кратностью. Последнее интересно с `Speed: Unlimited`: сколько БК-0010-01 стоит эта железка, одним числом. Клавиатура прибора читается через два приоритетных шифратора, и из группы видна только одна кнопка: `UP` с `RIGHT` вместе не нажать, как и `DOWN` с `LEFT`. Ни одной игре на БК диагональ не нужна — на самой машине стрелки в один ряд. **ПЗУ монитора в репозитории нет и не будет** — это прошивка чужой машины. Кладём свои на SPI-флешку, и они подхватятся: ./tools/spiflash.py add bk10mon.rom # 8 КБ, встаёт на 0100000 ./tools/spiflash.py add bk10bas.rom # Бейсик, встаёт на 0120000 ./tools/spiflash.py add boulderdash.bin # лента, появится в списке Формат `.bin` — тот самый ленточный: два слова заголовка (адрес загрузки и длина), дальше байты. Список файлов показывается при входе в приложение. Без монитора запускается **заглушка** из `bk_load.c`: она ставит стек и регистры экрана так, как их оставляет монитор, наводит все векторы на `RTI`, чистит экран и передаёт управление загруженной программе. Это не монитор — `EMT` возвращается, ничего не сделав. Самодостаточной игре хватает, и она честно об этом говорит, если программа всё-таки позвала прошивку.
Что именно эмулируется — и что нет Процессор — весь базовый набор PDP-11, включая `XOR`, `SOB`, `MARK`, `SXT`, `MTPS`/`MFPS`, ловушки `EMT`/`TRAP`/`BPT`/`IOT`, T-бит и ошибку шины по нечётному адресу. **`MUL`, `DIV`, `ASH`, `ASHC` не реализованы, и это не упрощение**: у К1801ВМ1 их нет, программы для БК умножают сдвигами, и на живой машине `070000` уходит в ловушку по вектору 010. Здесь — тоже. Экран: 256×256 в четырёх цветах и 512×256 в ч-б — это выбор смотрящего, а не режим железа (видеоконтроллер всегда гонит 512 точек и всегда красит парами бит). Все 16 палитр БК-0011 разобраны, регистр рулонного сдвига работает, «малый экран» на 64 строки тоже. Периферия: клавиатура с обоими векторами (060 и 0274) и правильной — инвертированной — маской прерывания, строка 50 Гц по вектору 0100, программируемый таймер 0177706–0177712 с предделителем, джойстик на 0177714, системный регистр 0177716. Нет: звука, магнитофона, дисковода, БК-0011М с его страничной памятью. Такты процессора — форма таймингов 1801, а не измеренный кремний, поэтому в меню есть «Speed»: если игра идёт не в том темпе, её правят там. 256 строк не влезают в 240 точек панели. `Squash` выбрасывает одну строку из шестнадцати и показывает все — он по умолчанию, потому что игра на БК кладёт рамку и счёт туда, где у телевизора был оверскан, и `Crop` срезает именно их (у Boulder Dash — верхнюю и нижнюю стены пещеры). `Crop` держит масштаб и теряет шестнадцать строк, деля их между верхом и низом (`Top line`).
Из чего собран и как собрать поменьше Всё в `src/bk/`, и всё выключается из `src/bk/bk_config.h`. Замерено — разница в прошитом образе, всё остальное неподвижно: | ключ | байт | | --- | --- | | `BK_DEBUGGER=0 BK_DISASM=0` | −2800 | | `BK_VIDEO_DIRTY=0` | −1984, и кадры вместе с ними | | `BK_CPU_TBIT=0 BK_CPU_BUS_ERROR=0` | −1856 | | `BK_IO_TIMER=0` | −528 | | `BK_STUB_ROM=0` | −384, тогда ПЗУ обязательно | | `BK_STATS=0` | −368 | | `BK_VIDEO_PALETTES=0` | −192 | | `BK_VIDEO_MONO=0` / `BK_IO_JOYSTICK=0` | −128 каждый | | `BK_CPU_EIS=1` | +704 — машина, которой никогда не было | cd make && make BK_CFLAGS="-DBK_DISASM=0 -DBK_DEBUGGER=0" Целиком эмулятор — 21 868 байт кода и 164 байта `.bss`; со всеми ключами выше — 17 564. Сами 64 КБ машины ни в одно из этих чисел не входят: они живут в SRAM, которую не занимает кольцо захвата, там же, где DOOM держит свой `doom_mem_t`. Проверяется на хосте настоящим кодом PDP-11 — `tests/bk_cpu_test.c` собирает программы вручную в восьмеричном и смотрит, что вышло, включая умножение сдвигами и запуск через ту самую заглушку ПЗУ: `make test`.
### 11. CoreDump Viewer Просмотр дампов, записанных обработчиком Hard Fault и `error()`. См. `README_NEW_FEATURES.md`. ### 12. Flash Viewer Hex / ASCII / Layout / Thumb по содержимому внутренней flash. См. `README_NEW_FEATURES.md`. ## Эмулятор железа [`emu/`](emu/) — эмулятор прибора: Unicorn (ядро QEMU) исполняет немодифицированный `open-5012h.bin`, вокруг — модели периферии GD32F407, AD9288, ST7789, кнопочных шифраторов и батареи. Исходники прошивки про эмулятор не знают: граница — регистры и выводы, ассемблерные участки (триггер, децимация) исполняются как есть. Сигнал на входе и несовершенства тракта (полоса, выброс, джиттер, DNL, рассогласование каналов АЦП) задаются с командной строки, прогоны детерминированы. Скриншоты выше — `emu/scripts/screenshots.sh`. brew install unicorn sdl2 # apt: libunicorn-dev libsdl2-dev make -C make && make -C emu && make -C emu run ./emu/build/fun5012h-emu make/build/open-5012h.bin \ --signal "square f=1M vpp=2 duty=30 rise=5n noise=10m" \ --afe "bw=20M overshoot=15" Ключи, форматы сигналов, модель тракта, скрипты и watchdog — в [`emu/README.md`](emu/README.md). ## Архитектура
Стек экранов (src/ui/ui.c) — лаунчер, приложение, меню и диалог на одном стеке Всё, что занимает экран — лаунчер, работающее приложение, всплывающее меню, модальный диалог — это экран на одном стеке. Входные события и `tick()` получает только верхний экран. При снятии экрана всё, что он закрывал, перерисовывается вызовом `draw(full=true)`, поэтому ни один экран не должен знать, что находится под ним.
Виджет меню (src/ui/menu_widget.c) — таблицы menu_item_t и прокручиваемые текстовые страницы Меню описываются таблицами `menu_item_t` (`MI_ACTION`, `MI_SUBMENU`, `MI_TOGGLE`, `MI_NUMBER`, `MI_CHOICE`, `MI_SEPARATOR`) и рисуются одним виджетом: навигация, прокрутка, редактирование значений реализованы один раз. Здесь же живёт `menu_open_info()` — модальная текстовая страница, на которой построены все справки и информационные диалоги. Страница длиннее экрана прокручивается: скроллбар показывает, какая её доля видна, а строка, начинающаяся с `INFO_HEAD`, — заголовок раздела. Он рисуется полосой, подписывается рядом с заголовком страницы, пока этот раздел на экране, и служит точкой перехода для `TRIG_UP`/`TRIG_DOWN`. Справка декодера — это 650 строк и два десятка разделов; без прокрутки от неё читался только первый экран, а без разделов до последнего пришлось бы держать стрелку.
Лаунчер (src/ui/launcher.c) — таблица app_desc_t Таблица `app_desc_t`: имя, описание и указатели `init` / `task` / `buttons` / `cleanup` / `redraw` / `menu` / `help`. Приложение выполняется как один экран поверх лаунчера.
Системное меню (src/ui/system_menu.c) — как собирается корень и где живёт Help Собирается при открытии из меню работающего приложения и общих разделов. Ничего специфичного для конкретного приложения здесь нет: пункты осциллографа, например, лежат в `src/scope/scope_menu.c` и показываются только пока работает осциллограф. Корень выглядит одинаково у любого приложения: <настройки приложения> из app_desc_t.menu ------------------ General Settings Advanced Device Info, System Info, Reboot Help <страницы приложения> + Key Bindings ------------------ Exit Application Раздел `Help` — единственное место для страниц, которые только показывают текст (`info_page_t`). Настройки и справка живут в двух разных таблицах именно поэтому: `menu` встраивается в корень как настройки приложения, `help` — в раздел `Help`, поэтому справка любого приложения лежит по одному и тому же пути `MENU > Help`. Внутри настроек текстовых страниц быть не должно.
Структура приложения — шесть точек входа и два меню Каждое приложение предоставляет: - `init()` — инициализация и первая отрисовка - `task()` — работа в главном цикле (вызывается только когда приложение наверху стека) - `buttons_handler()` — обработка кнопок - `cleanup()` — освобождение ресурсов (в том числе `timer_remove()` для своих таймеров) - `redraw()` — полная перерисовка после закрытия наложенного меню - `const menu_def_t _menu` — свой раздел системного меню (только настройки) - `const menu_def_t _help_menu` — свои текстовые страницы, они попадают в общий раздел `Help` Важное правило: обработчики пунктов меню **не рисуют**. Пока меню открыто, оно находится поверх приложения; изменения вступают в силу визуально, когда меню закрывается и вызывается `redraw()`.
Добавление нового приложения — четыре шага 1. Создайте `src/apps/new_app.h` и `src/apps/new_app.c`, реализуйте шесть точек входа выше 2. Экспортируйте `extern const menu_def_t new_app_menu;` и `extern const menu_def_t new_app_help_menu;` из заголовка 3. Добавьте строку в таблицу `g_apps` в `src/ui/launcher.c` 4. Добавьте `../src/apps/new_app.c` в `SRCS` в `make/Makefile`, в блок приложений Заголовок подключается по короткому имени (`#include "new_app.h"`): в `make/Makefile` есть по одному `-I` на каталог, поэтому путь в `#include` писать не нужно. Раздел меню приложения должен помещаться в `MAIN_APP_BUDGET` строк (см. `src/ui/system_menu.c`) — всплывающее меню не прокручивается. Более длинные списки надо раскладывать по подменю, как это сделано у осциллографа.
## Структура проекта Исходники разложены по подсистемам — сверху вниз, от железа к приложениям: src/core/ старт, syscalls, конфиг, таймер, утилиты src/hal/ то, что трогает регистры: LCD, кнопки, батарея, флеш, ввод src/acq/ тракт сбора: захват, синхронизация, буфер src/dsp/ что из выборок считается: измерения, БПФ, классификация, тренд src/decode/ декодеры протоколов; logic_decode.c — диспетчер остальных src/ui/ виджеты и экранная обвязка, без логики осциллографа src/scope/ сам осциллограф src/apps/ всё, что запускается из лаунчера и не осциллограф src/bk/ эмулятор БК-0010-01: процессор, шина, экран, загрузчик src/debug/ посмертный дамп doom/ вендорённый движок DOOM, под своей COPYING include/ вендорные CMSIS/GD32 заголовки linker/ скрипт компоновки make/ сборка прошивки (arm-none-eabi-gcc) emu/ хостовый эмулятор: гоняет тот же .bin через Unicorn tests/ хостовые тесты, без железа и без ARM-тулчейна tools/ wadpack.py, git-хуки, rtt.tcl attic/ код, который ни во что не собирается, — см. attic/README.md Каталоги — это указатель для читателя, а не часть графа включений: в `make/Makefile` есть по одному `-I` на каталог, поэтому заголовок подключается коротким именем (`#include "measure.h"`), и файл можно перенести, не правя каждый `#include`. Цена — имена файлов должны быть уникальны на весь проект: объектные файлы складываются в один плоский `build/`. ## Сборка Нужен `arm-none-eabi-gcc` (на macOS — `brew install --cask gcc-arm-embedded`) и `python3` для отчёта о размере. Из корня репозитория: make # сборка, с DOOM — образ ~468 КБ из 512 КБ make DOOM=0 # без ассетов DOOM, если пакет не собран — образ ~354 КБ make test # хостовые тесты: DSP и декодеры, железо не нужно make emu # хостовый эмулятор make clean # очистка всех трёх сборок make layout # что где лежит в образе make help # список целей Корневой `Makefile` — тонкий переходник, он только вызывает `make -C`. Каждая сборка по-прежнему работает и напрямую: `cd make && make` собирает прошивку, `cd tests && make` гоняет тесты, `cd emu && make` собирает эмулятор. Пакет ассетов DOOM — это ~208 КБ, и целиком в образ он не помещался: вместе с прошивкой выходило ~552 КБ при 512 КБ флеша. Теперь он **режется надвое** по признаку «как к этому обращаются» — подробности в [разделе про DOOM](#9-doom): на SPI-флешку (doom.tex, 100 КБ) в образ (110 КБ) TEXDATA колонки стен таблицы проекции — это математика, а не FLATS текстуры пола и потолка данные id, они одни для любого WAD геометрия уровня, каталоги, HUD Поэтому DOOM собирается по умолчанию и флага для него больше нет. Без пакета (`doom/doom_assets.bin` отсутствует) линкуется пустая заглушка, приложение запускается и показывает экран с инструкцией. Сам движок в образе всегда. Результат — `make/build/open-5012h.bin` (и `.hex`, и `.elf` с отладочной информацией). ## Прошивка Разъём USB на приборе — **не USB**: на линиях данных выведены SWDIO/SWCLK, так что нужен любой SWD-программатор. Подробности распайки — в `doc/Programming.md`. **Режим программирования.** Держите `F2` при включении прибора. Экран останется тёмным и никак не покажет, что устройство включено — это нормально: прошивка останавливается до настройки быстрых тактовых, иначе работающий АЦП мешает SWD.
Вариант 1: OpenOCD — любой CMSIS-DAP программатор Так прошивался этот проект — программатором WCH-Link в ARM-режиме. GD32F4 шьётся как STM32F4. brew install open-ocd cd make openocd -f interface/cmsis-dap.cfg \ -c "transport select swd" -c "adapter speed 2000" \ -f target/stm32f4x.cfg \ -c "program build/open-5012h.bin verify reset exit 0x08000000" Должно закончиться `** Verified OK **`. Проверить уже прошитое, ничего не перезаписывая: openocd -f interface/cmsis-dap.cfg -c "transport select swd" \ -f target/stm32f4x.cfg \ -c "init; verify_image build/open-5012h.bin 0x08000000 bin; shutdown" Для WCH-Link есть отдельный конфиг `interface/wch-cmsis-dap.cfg`, но обычный `cmsis-dap.cfg` тоже его находит.
Вариант 2: edbg — то, что стоит за make prog Так это делает `make prog`. `edbg` нет ни в brew, ни в системе по умолчанию — его надо собрать из исходников ([github.com/ataradov/edbg](https://github.com/ataradov/edbg)): git clone https://github.com/ataradov/edbg && cd edbg && make sudo cp edbg /usr/local/bin/ cd /path/to/project/make && make prog
### Что нельзя стирать Настройки и **калибровка** с 2026-07-29 живут на внешней SPI-флешке, по адресу `0x7A0000` — 64 КБ ротации из 8 МБ чипа. Внутренний флеш МК отдан прошивке целиком, все **512 КБ**. `mass_erase` внутреннего чипа больше калибровку не теряет, зато её теряет стирание внешнего: `spiflash.py` отказывается писать выше `0x7A0000` без явного `--unsafe`, и прошивка отказывает второй раз, уже на приборе. Резервная копия — `./tools/spiflash.py dump 0x7A0000 65536 cal.bin`. Числа калибровки дополнительно показывает страница **MENU → Advanced → Calibration**: флеш для них — кеш, а не единственный экземпляр. **Верхние 320 КБ чипа (`0x7B0000`…`0x7FFFFF`) не наши.** Полное сканирование всех 2048 секторов нашло там данные штатной прошивки FNIRSI: двадцать сохранённых записей по 4 КБ (`0x7BD000`…`0x7CFFFF`, в них отсчёты АЦП) и несколько структур выше. Что они значат — неизвестно, восстановить их неоткуда, поэтому туда не пишет никто. Снять с них копию: `./tools/spiflash.py pull 0x7B0000 327680 stock.bin` (нужен открытый **SPI Flash Loader**, иначе `dump` тем же диапазоном, только медленно).
Зачем переехало и как вернуть обратно Внутренний сектор стоил 128 КБ из 384, доступных прошивке — треть бюджета образа под 416 байт настроек, — и стирался только целиком, останавливая выборку кода на 1–2 с. Это и был «белый экран сторожевого таймера». На внешнем чипе та же ротация стирает 4 КБ за ~45 мс. Цена: калибровка теперь на отдельной микросхеме. Если её нет или она не отвечает, прошивка читает старый внутренний сектор (он никуда не делся, пока образ не перевалит за 384 КБ) и говорит об этом в System Information. Первая загрузка новой прошивки переносит живую запись сама — ничего делать не надо, в строке `Cfg:` появится `(migrated)`. Вернуть старое поведение: cd make && make CONFIG_STORE=internal Сборка снова упрётся в 384 КБ (линкер проверяет это ассертом, а не молча), последний сектор снова станет хранилищем. Флаг меняет и код, и границу образа, поэтому `make` при его смене сам сбрасывает объектные файлы.
## Отладка в VS Code
Расширения, задачи, F5, SVD и что делать, если программатор не находится Всё уже настроено в `.vscode/`. Нужны два расширения: - **cortex-debug** (`marus25.cortex-debug`) — отладка через OpenOCD - **C/C++** (`ms-vscode.cpptools`) — IntelliSense Задачи (`Cmd+Shift+P` → `Tasks: Run Task`): | Задача | Что делает | | --- | --- | | `Build embedded project` | `make` в `make/` (она же по `Cmd+Shift+B`) | | `Clean embedded project` | `make clean` | | `Flash with OpenOCD` | собрать и прошить через CMSIS-DAP | | `Flash binary with edbg` | `make prog` | Отладка — `F5`, конфигурация **Debug (OpenOCD)**: собирает проект, шьёт, останавливается на `main`. Работают точки останова, пошаговое выполнение, просмотр переменных и Live Watch (обновление 4 раза в секунду). Пути к конфигам OpenOCD заданы относительно его собственного каталога скриптов (`interface/cmsis-dap.cfg`), поэтому обновление openocd их не ломает. Просмотр регистров периферии по умолчанию выключен: для него нужен SVD-файл, которого в репозитории нет. Если нужен — положите `STM32F40x.svd` и добавьте в `launch.json`: "svdFile": "${workspaceRoot}/resources/STM32F40x.svd", Если программатор не находится, проверьте, что прибор в режиме программирования (`F2` при включении) и что `openocd` видит его сам: openocd -f interface/cmsis-dap.cfg -c "transport select swd" \ -f target/stm32f4x.cfg -c "init; shutdown" Должно появиться `Cortex-M4 r0p1 processor detected`.
## Что это за прибор и в чём он врёт FNIRSI-5012H — карманный осциллограф из тех, что продаются готовыми приборами: экран, кнопки, щуп, закрытая прошивка и никакой схемы. Всё, что известно о плате, известно потому, что Алекс Тарадов её разобрал и записал — [`doc/Hardware.md`](doc/Hardware.md) целиком его. Открытая прошивка выросла оттуда, этот форк — из неё. Прибор при этом устроен так, что несколько вещей в нём не то, чем кажутся. **Разъём USB — не USB.** На линиях данных выведены SWDIO и SWCLK: это заводской порт программирования. USB на нём не заработает без переделки платы. Приятная сторона — прошить прибор можно, ничего не вскрывая; неприятная — воткнутый кабель не сделает ровно ничего. **АЦП — не AD9288.** Микросхема без маркировки, выводы совпадают с AD9288, но пара ножек, помеченных в оригинале как NC, здесь посажена на землю через конденсаторы, и метка первого вывода другой формы. Скорее всего клон, возможно MXT2088. То есть даташит, по которому приходится работать, — не даташит той детали, которая стоит на плате. **Диапазона 50 мВ/дел не существует.** 50 и 100 мВ/дел — один и тот же транзистор аттенюатора (Q3) и одно и то же положение реле. Разница целиком программная: прошивка домножает. В списке диапазонов их два, в железе — один. **Клавиатура физически не берёт часть комбинаций.** 18 кнопок читаются двумя приоритетными шифраторами SN74HC148 по восемь, и из группы видна ровно одна нажатая кнопка. `LEFT`, `DOWN` и `TRIG` лежат в одной группе — держать их вместе нельзя в принципе. Отдельно на ножки МК заведены только `F2` и `SHIFT`, поэтому все модификаторы в этой прошивке сидят на них: иначе, например, шаг вбок в DOOM в одну из сторон был бы недостижим. **И главное — прибор не измеряет собственную частоту дискретизации.** Надпись `125 M` в углу берётся из `BASE_SAMPLE_RATE` — это `#define`. Ничто в системе не сверяет её с тем, что реально делают такты АЦП. Именно поэтому сведённые в фазу такты (см. [`src/acq/capture.c`](src/acq/capture.c) и раздел [осциллографа](#1-oscilloscope)) прожили так долго: прибор честно показывал 125 Мвыб/с, выдавая 62.5, и единственным симптомом был 50 МГц, свернувшийся в 12.5 — а кто в такой прибор подаёт 50 МГц. Тот же узор — у трёх дефектов из начала этого файла. Такты ожидания флеша не были выставлены вовсе, и настройки просто иногда не сохранялись. CRC записи конфига считался по диапазону, включающему само поле CRC, и не сходился никогда — прибор молча возвращался к умолчаниям. Хранилище конфига перекрывало хвост образа, и прошивка работала ровно до первого сохранения. Ни одна из трёх поломок ничего не сообщала: она выглядела как «настройки почему-то слетели». ## Иглы на трассе: что это было Отдельный разбор, потому что дефект прожил в прошивке долго, маскировался под «шумит развёртка», и цена ошибки в диагнозе тут выше обычного: сигнал портился **до** всего, что его читает. ### Как это выглядело На 200 мкс/дел трасса шла в иглах во всю высоту экрана, и триггер в режиме NORMAL срабатывал на них постоянно — поймать реальное событие было нельзя. Величина иглы — около **128 отсчётов** из 256, ровно полшкалы, и она не менялась при смене аттенюатора: 600 мВ на 100 мВ/дел, 1.05 В на 200 мВ/дел, то есть одно и то же в отсчётах АЦП. Наводка со входа делилась бы аттенюатором вместе с сигналом — здесь этого не происходило, а значит источник лежал уже за ним, в цифре. Сбивало с толку то, что дефект казался привязанным к развёртке. На самом деле он был привязан к **частоте дискретизации**: 15.625 Мвыб/с чисто, 31.25 и выше — иглы. Просто на 200 мкс/дел запись прорежена 4:1 и покрывает всю ширину экрана, поэтому туда попадали разом все иглы записи, а не несколько. ### Почему Такты АЦП делает TIMER0, запрос DMA — TIMER7, и оба живут при `CAR = 1`. Счётчик при этом принимает только 0 или 1, то есть за период есть ровно две точки, где может сработать сравнение, и `CH0CV` выбирает между ними. Стояла та, что срабатывает при счёте 1. Это **тот же самый тактовый момент**, что и фронт кодирования АЦП B, потому что его канал запущен инвертированным (`ADC_B_CLOCK_MODE = 7`). Иначе говоря, чтение шины было назначено ровно на тот миг, когда преобразователь выставляет на неё новое значение. Задержка от запроса до фактического чтения `GPIOD` фиксирована, поэтому чтение стабильно попадало в момент переключения, и байт возвращался смесью двух соседних выборок. На главном переносе 127→128, где меняются все восемь бит сразу, такая смесь даёт ошибку ровно в полшкалы — те самые 128 отсчётов. Порог по частоте отсюда же: пока период короче задержки чтения, уйти от перехода некуда. ### Как починили Двумя шагами, каждый проверен на железе отдельно. **Шаг 1 — увести чтение с перехода.** `CH0CV = 0` вместо 1: сравнение на перевороте счётчика, в полупериоде от перехода. Запрос по-прежнему один за период, частота дискретизации не меняется ни на герц. Какая из двух точек верна, зависит от **периода** — задержка фиксирована в наносекундах, а период нет, — поэтому выбор идёт по делителю: короткие периоды берут счёт 1, длинные переворот. **Шаг 2 — позиционировать точно.** Две точки — это всё, что может назвать `CAR = 1`, и лучшая из двух не есть лучшая вообще. Счётчик TIMER7 переведён на полные 250 МГц, период уехал в `CAR`, и строб стал позиционируемым с шагом **4 нс**: восемь точек на 200 мкс/дел вместо двух. Запрос остался один за период — частота не изменилась. Оптимум пришлось задавать таблицей по делителю: задержка запрос→чтение не одинакова при 62.5 и 31.25 млн запросов в секунду, и одна общая поправка чинила 200 мкс ровно ценой 100 мкс. ### Как это померили Ключевой шаг, без которого перебор был бы гаданием: **иглы считает сам прибор.** Вход закорочен, режим NORMAL, уровень триггера +300 мВ — это около 64 отсчётов, недосягаемо для фона в 1–2 отсчёта и заведомо ниже иглы в 128. Прибор стоит в `WAIT` и дёргается только на игле, а «ловлю одну изредка» превращается в число за 30 секунд. | делитель | период | точек | поправка 0 | поправка +1 | | --- | --- | --- | --- | --- | | sr 1 | 16 нс | 4 | чисто | иглы, >10/с | | sr 2 | 32 нс | 8 | 19–20 за 30 с | **ни одной** | Правильная позиция даёт **ноль**, а не «пореже» — именно поэтому таблицу можно расширять осмысленно. ### Где предел Осталось то, что дефектом уже не является, и упирается это в разные вещи. **1–2 отсчёта на медленных развёртках — это квантователь.** АЦП восьмибитный, один отсчёт равен 1/256 шкалы, и размах в один отсчёт есть физический пол: измеренные 4 мВ на 50 мВ/дел при 1 мс — это 0.85 отсчёта, ниже уже нечему быть. Аппаратно не улучшается никак. **3–4 отсчёта на быстрых — это установление.** Чем короче период, тем меньше времени на установление шины и тем заметнее апертурный джиттер. Зависимость монотонная и ровная: 1 мс — 0.85 отсчёта, 500 мкс — 1.9, 100 мкс — 2.7–3.7. Это характеристика преобразователя, а не ошибка прошивки. **Рябь на 50 мкс/дел и быстрее — это не шум.** Там работает чередование двух преобразователей, и видна их взаимная рассогласованность по усилению и смещению. Она детерминированная, а значит калибруется — см. `calib_channel_delta` в меню калибровки, а не борьбу с шумом. Уменьшить оставшееся можно только усреднением, и тут стоит различать два разных инструмента. Режим усреднения в прошивке (`average_mode`) — это экспоненциальное среднее по кадрам в пространстве экрана: на быстрых развёртках, где столбец и есть одна выборка, он даёт учебные √N, но требует повторяющегося сигнала и устойчивого триггера. На медленных развёртках в столбец попадает несколько десятков выборок записи (на 200 мкс/дел — 62), но берётся из них только пара min/max, а усредняется потом лишь середина полосы. Усреднение **внутри столбца** дало бы там √62 ≈ 8-кратное падение шума, то есть около трёх дополнительных бит по вертикали, и не требовало бы ни повторяемости, ни триггера. Ценой пикового детектора: игла уже, чем столбец, перестала бы быть видна — поэтому это отдельный режим, а не умолчание. Сейчас его нет. ## Технические характеристики - **Микроконтроллер**: GD32F407VE (ARM Cortex-M4F), разогнан до **250 МГц** при штатных 168 — с включением high-drive по процедуре вендора и безопасным откатом, если кристалл не подтвердит готовность - **Память**: 512 КБ flash — вся прошивке, настройки уехали на внешний чип; 64 КБ TCM под весь `.data`/`.bss`/стек, из них 30 КБ приложение одалживает у осциллографа; 128 КБ SRAM под кольцо захвата (его на время занимает DOOM) - **АЦП**: клон AD9288, два канала с чередованием, до 125 Мвыб/с - **Дисплей**: LCD 320x240 ST7789, RGB565, шина дёргается ножками GPIO — EXMC при такой разводке невозможен, поэтому оптимизируют не скорость вывода пикселя, а их количество - **Внешняя flash**: 8 МБ на SPI (на этом экземпляре GigaDevice `C8 40 17`, а не W25Q64 из документации) — настройки и калибровка, текстуры DOOM, файлы - **Управление**: 18 кнопок через два приоритетных шифратора (одновременно видна одна кнопка из группы) плюс `F2` и `SHIFT` на отдельных пинах - **Питание**: Li-Ion, контроль заряда и предупреждение о разряде ## Оптимизации - Прямая отрисовка на LCD без кадрового буфера (экономия RAM) - Инкрементальная перерисовка: приложения трогают только те пиксели, которые изменились - Окно вывода на LCD настраивается один раз на прямоугольник или строку; попиксельный вывод — самый дорогой путь и используется только там, где без него нельзя - Прогрессивный рендеринг тяжёлых сцен по несколько строк за проход, чтобы главный цикл продолжал опрашивать кнопки ## Происхождение Это форк [ataradov/open-5012h](https://github.com/ataradov/open-5012h) — открытой прошивки Алекса Тарадова, с которой начался проект. Оттуда пришёл сам прибор: захват, аттенюатор, калибровка, отрисовка, драйвер LCD, хранилище конфига и PLL на 250 МГц. Оттуда же — весь ассемблер поиска фронта ([`src/acq/trigger.c`](src/acq/trigger.c) не изменился с первого коммита ни на байт) и SIMD-обработка кольца в [`src/acq/buffer.c`](src/acq/buffer.c). Обратная разработка платы, без которой ничего этого не было бы, — тоже его; [`doc/Hardware.md`](doc/Hardware.md) и [`doc/Programming.md`](doc/Programming.md) написаны им. Форк добавил поверх: слой анализа (измерения, БПФ, классификатор, тренд, двадцать декодеров), каркас интерфейса (стек экранов, виджет меню, лаунчер, системное меню), приложения, посмертные дампы, эмулятор, хостовые тесты, безопасный подъём того самого разгона (high-drive и такты ожидания флеша) и починку хранилища конфига. ## Лицензия Проект распространяется под [BSD-3-Clause](LICENSE) — как унаследованный код Алекса Тарадова, так и всё написанное в этом форке. Файлы, добавленные форком, несут SPDX-идентификатор в заголовке; унаследованные — полный текст BSD, как он был. Исключения: - **Движок DOOM** (`doom/`, а также исторические `attic/doom_complete_port.*` и `attic/doom_full_port.*`) — производная работа от исходников DOOM (linuxdoom-1.10), выпущенных id Software под GPL. Эти файлы — под [GPL-2.0-or-later](doom/COPYING). Движок линкуется всегда, поэтому под условия GPL при распространении подпадает **любой** собранный образ. - **Ассеты DOOM** (`doom/doom_assets.bin` и `doom/doom_assets_tex.bin`) в репозиторий не входят: обе половины пакета собираются локально из вашего IWAD, и графика WAD принадлежит её правообладателю. Свободный IWAD — [Freedoom](https://freedoom.github.io/). - **Эмулятор** (`emu/`) линкуется с Unicorn Engine (GPL-2.0): исходники эмулятора — BSD-3-Clause, но его собранный бинарник при распространении подпадает под условия GPL. Протоколы, которые декодирует прошивка (UART, CAN, 1-Wire и другие), — это спецификации: авторское право на сами протоколы не распространяется, копирайт в заголовках декодеров относится к коду реализации.
标签:GD32, 协议解码器, 客户端加密, 嵌入式UI, 嵌入式固件, 硬件仿真, 示波器