Digital Points — программный осциллограф, так и не ставший стартапом

от автора

История о том, как внутренний программный инструмент для отладки вырос в проект, так и не ставший коммерческим, и почему в итоге мы выложили его в open‑source в надежде вдохнуть в него вторую жизнь. Заодно расскажу, что умеет этот инструмент.

TL;DR Digital Points — «программный осциллограф», инструмент для отладки систем реального времени. Может считывать переменные микроконтроллеров в реальном времени и по триггеру, математически обрабатывать их, измерять частотные характеристики, рассчитывать программные регуляторы. Пытались его коммерциализировать, не вышло, и теперь выкладываем в open‑source.

Как всё начиналось

От идеи до прототипа

2020 год. Я работал embedded‑программистом в новосибирской компании, которая разрабатывает и производит промышленные электроприводы большой мощности (сотни кВт и единицы МВт). Параллельно учился в магистратуре и работал младшим научным сотрудником в университете.

В тот период программировал систему управления реверсивным тиристорным преобразователем для электропривода постоянного тока мощностью до 1 МВт с управлением на STM32F746. Медленные процессы (скорость вращения, потребляемая мощность) отлаживал через Modbus RTU/TCP, быстрые (токи, напряжения, углы коммутации) — по сигналам на ЦАП или ШИМ с последующей фильтрацией. Отдельный вид спорта отлаживать в таких условиях, особенно когда при срабатывании автоматических защит или при переключении комплектов тиристоров нужно понять, что произошло за несколько миллисекунд до и после этого, и захватить сигналы хотя бы с частотой дискретизации системы управления (20–100 кГц). А ещё как‑то нужно оценивать быстродействие, запасы устойчивости и работу программных контуров регулирования, цифровых фильтров, наблюдателей и прочих динамических подсистем. Без достаточного объёма информации о внутренней работе системы остаются только косвенные оценки и догадки.

Тогда возникла идея написать программу, которая бы читала внутренние переменные МК подобно осциллографу. Обмен данными только через штатные интерфейсы связи (RS-485, CAN, Ethernet…), потому что отлаживать через JTAG‑эмуляторы под напряжением 6 кВ или килоамперными токами — затея не для слабонервных. На идею меня вдохновила STM Studio, которая сейчас уже скорее мертва, чем жива, и с тех пор фактически вытеснена STM32CubeMonitor.

Первую версию написал в том же году. Рабочее название — Plotter. Ключевые особенности унаследовал от STM Studio:

  • Архитектурное разделение на клиент с GUI для ПК (далее — клиент) и библиотеку для ПО микроконтроллера (далее — сервер). Клиент на C++/Qt, сервер на чистом C без привязки к периферии МК — только логика. Со стороны клиента поддерживались последовательный COM‑порт и Ethernet.

  • Загрузка списка глобальных переменных МК с именами, типами и адресами в памяти. Тогда просто парсился txt‑файл, который генерировал ARM Compiler 6 (clang).

  • Чтение сервером значений переменных из памяти по адресам и отправка их по интерфейсу клиенту.

Интерфейс Plotter

Интерфейс Plotter

Это позволило не ограничиваться заранее выбранными переменными и на ходу, без перепрошивки, выбирать переменные для чтения, тем самым экономя время.

В Plotter было два основных режима.

Режим реального времени: клиент периодически запрашивал выбранные переменные, замерял время между запросами и отображал значения на графике. Частота запросов ограничивалась только битрейтом интерфейса, количеством переменных и загруженностью МК.

Режим триггера: клиент отправлял условия триггера (переменная, порог срабатывания, тип фронта) и адреса переменных для чтения. Сервер начинал циклически складывать в буфер значения переменных, пока не сработает триггер. После этого буфер с доступной скоростью отправлялся клиенту и отображался на графике. Длительность окна буферизации = n / f_s, где n — размер буфера, f_s — частота дискретизации (частота добавления значений в буфер).

Инструмент прижился: применяли его при запуске уже упоминавшегося тиристорного электропривода, а также для отладки фильтр‑компенсирующего устройства и компенсатора реактивной мощности. К 2022 году, когда я уходил из компании, в Plotter появились зачатки ещё одной функции — измерения ЧХ отдельных динамических звеньев: например, ЧХ «напряжение → скорость вращения» электродвигателя или ЧХ замкнутого контура регулирования тока «задание → регулируемый ток». Появилась возможность оценивать поведение и качество настройки контуров регулирования не на глаз, а по объективным данным.

От прототипа до стартапа

Уволившись, я сосредоточился на научной работе и диссертации. После защиты в 2024 году возник банальный финансовый вопрос: на что дальше жить. К тому моменту я уже занимался фрилансом, рассчитывал и программировал системы управления силовыми преобразователями на заказ, проводил курсы повышения квалификации вместе со своим научным руководителем, а ныне вторым соучредителем стартапа (@romangorbunov91).

Однако хотелось попробовать что‑нибудь новое, и пришла мысль коммерциализировать Plotter (на предыдущей работе NDA не было).

Под эту задачу переписали клиент на Python 3, переработали сервер и зарегистрировали ООО. Новое название — Digital Points. После этого начался типичный квест для стартапов длиною в 1,5 года: бизнес‑акселераторы, бизнес‑инкубаторы, венчурные ярмарки, конференции, выставки. Периодически пересчитывали заветные TAM/SAM/SOM, получая каждый раз разные цифры (так себе финансисты).

Несмотря на все усилия, дело шло туго. Инвесторы и бизнес‑эксперты говорили, что проект крайне интересен и стартап скоро улетит в стратосферу, а потенциальные пользователи — что они такое могут и сами сделать с двумя часами свободного времени и кружкой кофе (может, так и есть). Вместе с тем пришло понимание, что нам просто нравится решать технические задачи, а не искать лидов, упаковывать продукт, делать пивоты и питчиться. По сути для нас это стало хобби. К тому же «нулевая» ООО, благодаря недавним изменениям от 28.11.2025 № 425-ФЗ, стала обходиться нам примерно в 100 тыс. руб./год на теперь обязательные страховые взносы даже при отсутствии выплат и доходов, а с бухгалтерией доходит и до 150 тыс. руб./год на пустом месте. Две среднемесячные зарплаты в Новосибирске. Так что после нескольких месяцев неудачных переговоров о полной продаже проекта или его перезапуске мы решили пустить его в свободное плавание и выложить в open‑source.

Далее — описание основных возможностей Digital Points.

Внутри Digital Points

Клиент Digital Points (далее — DP) написан на Python 3. Основные зависимости:

  • PySide6 для GUI.

  • pyqtgraph для отрисовки графиков.

  • pyserial и python-can соответственно для последовательного интерфейса и CAN. В сервере нет привязки к периферии МК, поэтому поддержку новых интерфейсов достаточно реализовать только на стороне клиента.

  • numpy и sympy для математики.

  • pyelftools для парсинга отладочной информации из ELF‑файлов.

Сервер написан на чистом C без зависимостей.

База осталась та же: реальное время, триггер, загрузка списка переменных и измерение ЧХ. Добавились: БПФ, инструменты ЦОС, автоматический дамп данных в реальном времени, расчёт ПИД‑регулятора. Расширился список поддерживаемых архитектур: ARM Cortex‑M4/M7, TI C2000, RISC‑V (MIK32).

Главный экран Digital Points.

Главный экран Digital Points.

Загрузка списка переменных

В Plotter всё было просто: clang генерировал txt‑файл со списком глобальных переменных, их адресов и типов. Для внутреннего использования этого хватало, но подход крайне неуниверсальный. Пришлось разбираться с парсингом отладочной информации в формате DWARF из ELF‑файла.

В отличие от txt‑файла, уникального для конкретного компилятора, ELF‑файлы генерируются компиляторами (gcc, clang, TI C2000 C/C++ Compiler) в стандартном формате. Именно эти файлы используют отладчики. Готового решения для поиска переменных в ELF‑файлах на Python мы тогда не нашли или плохо искали, и в итоге написали его сами. Возможно, не самое рациональное решение, но что сделано, то сделано.

Отладочную информацию в виде дерева с узлами DIE извлекаем из секции .debug_info ELF‑файла с помощью pyelftools. Дополнительно достаём список имён глобальных переменных из секции .symtab. После этого обходим все узлы дерева в поисках информации о переменных из списка. Более подробно о формате DWARF можно почитать в этой статье или на официальном сайте стандарта.

В итоге собирается информация обо всех глобальных переменных МК с учётом потери отладочной информации при оптимизации компилятора (register allocation): имена, адреса в памяти, типы данных и размеры в байтах. В будущем в этот список можно добавить статические переменные с фиксированными адресами.

Пример списка переменных различных типов.

Пример списка переменных различных типов.

Выделение памяти на стороне МК

В Plotter количество одновременно считываемых переменных было жёстко зашито в коде сервера. Опять же, для внутреннего использования этого вполне хватало. В DP же хотелось больше гибкости, но с возможностью явно ограничивать объём занимаемой памяти в МК.

При инициализации сервера пользователь объявляет массив ограниченного размера, и DP распределяет в нём свои объекты, не выходя за его пределы. Похожим образом устроен heap_1.c во FreeRTOS. Буферы приёма и передачи данных, буферы для режима триггера — всё располагается в выделенном массиве, по максимуму используя доступную память. Исключение составляют только глобальные структуры внутренней реализации сервера.

Режим реального времени + авто‑дампы

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

Тепловые и нагрузочные испытания, испытания на отказ электрооборудования могут длиться часами и даже днями. На скорости Ethernet 100 Мбит/с за 1 час непрерывного измерения накопится до 40 ГБ данных. Такой объём данных значительно превышает объём ОЗУ обычного рабочего ноутбука.

Периодический сброс дампов на диск спасает от переполнения памяти. В DP можно настроить порог сброса в МБ. Как только объём данных превысит порог, они сбрасываются на диск, удаляются из ОЗУ, а на графиках фиксируется имя соответствующего файла с дампом.

Пример выгрузки дампов в режиме реального времени.

Пример выгрузки дампов в режиме реального времени.

Режим триггера

Основу этого режима также оставили без изменений, но добавили полезные настройки, многие из которых пришли из обычных осциллографов:

  • Количество точек до и после триггера.

  • Задержка срабатывания — время после запуска, в течение которого триггер игнорирует события.

  • Пропуск триггеров, например, если нужно поймать каждый n‑й триггер.

  • Режим однократного срабатывания (one‑shot).

  • Запуск без триггера, как будто триггер срабатывает сразу же после запуска измерения.

БПФ и инструменты ЦОС

Над сигналами, полученными в режиме триггера или в реальном времени, можно выполнить БПФ. Для этого достаточно только указать временной интервал для преобразования. Без экспорта в сторонние системы можно здесь и сейчас оценить спектральный состав сигналов.

Также над сигналами можно производить математические преобразования по формулам: арифметика, математические функции, фильтры, интегральные показатели. Формулы можно применять как к отдельной переменной, так и сразу к нескольким.

Окно выбора переменных и задание формул для их пересчёта.

Окно выбора переменных и задание формул для их пересчёта.
Сверху - полигармонический сигнал и полученные из него сигналы по формулам (x+2)*1.5 и cos(x) + x4, где x - исходный сигнал, x4 - треугольный сигнал. Снизу - треугольный сигнал.

Сверху — полигармонический сигнал и полученные из него сигналы по формулам (x+2)*1.5 и cos(x) + x4, где x — исходный сигнал, x4 — треугольный сигнал. Снизу — треугольный сигнал.

Измерение ЧХ

Самая интересная часть Digital Points — измерение частотных характеристик. Этот инструмент позволяет программно измерить ЧХ практически любых передаточных функций системы: объект, регулятор, обратная связь, фильтры, функции чувствительности и др.

В общем случае принцип измерения следующий:

  1. Клиент формирует массив значений возмущающего воздействия (сигнал инжекции) размера n и отправляет на сервер.

  2. Сервер на каждом такте (с частотой дискретизации f_s и с учётом коэффициента децимации d) выдаёт очередное значение из массива инжекции, которое пользователь должен добавить к нужной переменной внутри программного контура регулирования.

  3. Одновременно с инжекцией сервер читает временные «снимки» двух переменных, условно x(t) и y(t) (реакция системы), массивами по n значений и отправляет клиенту.

  4. Клиент выполняет БПФ над «снимками», извлекает комплексные амплитуды X(j\omega), Y(j\omega) одной или нескольких гармоник с номерами h (\omega = h\omega_{min}) и вычисляет их модули |X(j\omega)|, |Y(j\omega)| и фазы \arg X(j\omega), \arg Y(j\omega).

  5. Частота и амплитуда инжекции меняются (при моногармоническом измерении), и цикл повторяется.

Время одного измерения без оверхеда: (n*d)/f_s.

В результате получаются массивы амплитуд и фаз, по которым строятся графики ЛАЧХ и ЛФЧХ. Отношение модулей 20 \lg \left( |Y(j\omega)| / |X(j\omega)| \right) даёт ЛАЧХ, разность фаз \arg Y(j\omega) - \arg X(j\omega) — ЛФЧХ.

Именно так работают векторные анализаторы цепей вроде AP Instruments Model 310 или OMICRON Lab Bode 100, только DP делает это программно.

В DP реализованы два типа измерения ЧХ в зависимости от типа сигнала инжекции: моногармонический и полигармонический. Для обоих формируется массив интересующих частот гармоник в количестве n_freq в диапазоне от f_min до f_max, которые задаются пользователем.

Моногармонический

Массив частот и длительность окна измерения формируются двумя способами на выбор, при этом сам принцип измерения одинаков для обоих.

Способ h = 1

Инжекция и измерения проводятся только на одном периоде частоты, которая в данный момент измеряется. Буфер недоиспользуется, но время измерения минимально.

Python‑скрипт для расчёта и результат
import numpy as npf_s = ...f_min = ...f_max = ...n_freq = ...n_max = ...# Первичный расчёт кол-ва точек и частот с логарифмическим распределением.f = np.geomspace(f_min, f_max, num=n_freq)n = np.maximum(np.floor(f_s / f + 0.5), 1).astype(np.uint64)# Расчёт коэффициентов децимации.d = np.maximum(np.ceil(n / n_max), 1).astype(np.uint64)# Перерасчёт кол-ва точек с учётом децимации.n = np.maximum(np.floor(n / d.astype(np.float64) + 0.5), 1).astype(np.uint64)# Перерасчёт частот.f = f_s / (n * d)# Расчёт номеров гармоник.# Для данного способа все гармоники единичные.h = np.ones(n_freq).astype(np.uint64)

Пример результата расчёта при f_s = 100e3 Гц, f_min = 10 Гц, f_max = 5000 Гц, n_freq = 10, n_max = 1000:

f = [10.0000, 19.9362, 39.7772, 79.3651, 158.2278, 315.4574, 628.9308, 1250.0000, 2500.0000, 5000.0000]n = [1000, 836, 838, 630, 632, 317, 159, 80, 40, 20]d = [10, 6, 3, 2, 1, 1, 1, 1, 1, 1]h = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

Способ h = var

Инжекция и измерения проводятся на максимальном возможном количестве периодов. Буфер используется по максимуму, но время измерения возрастает.

Python‑скрипт для расчёта и результат
import numpy as npf_s = ...f_min = ...f_max = ...n_freq = ...n_max = ...# Первичный расчёт кол-ва точек и частот с логарифмическим распределением.f = np.geomspace(f_min, f_max, num=n_freq)n_base = np.maximum(np.floor(f_s / f + 0.5), 1).astype(np.uint64)# Маска для кол-ва точек, превышающих максимальное.mask = n_base > n_maxinv_mask = ~mask# Инициализируем коэффициенты децимации и номера гармоник.d = np.ones(n_freq, dtype=np.uint64)h = np.ones(n_freq, dtype=np.uint64)n = n_base.copy()# Если точек больше, чем можно, рассчитываем децимацию.d[mask] = np.ceil(n_base[mask] / n_max).astype(np.uint64)n[mask] = np.ceil(n_base[mask] / d[mask]).astype(np.uint64)f[mask] = f_s / (n[mask] * d[mask])# Если точек меньше, чем можно, рассчитываем номера гармоник.h[inv_mask] = np.maximum(np.floor(n_max / n_base[inv_mask]), 1).astype(np.uint64)n[inv_mask] = np.floor(h[inv_mask] * n_base[inv_mask] + 0.5).astype(np.uint64)f[inv_mask] = (h[inv_mask] * f_s) / n[inv_mask]# Пересчитываем кол-во точек.n = np.maximum(np.floor(n + 0.5), 1).astype(np.uint64)

Пример результата расчёта при f_s = 100e3 Гц, f_min = 10 Гц, f_max = 5000 Гц, n_freq = 10, n_max = 1000:

f = [10.0000, 19.9362, 39.7772, 79.3651, 158.2278, 315.4574, 628.9308, 1250.0000, 2500.0000, 5000.0000]n = [1000, 836, 838, 630, 632, 951, 954, 960, 1000, 1000]d = [10, 6, 3, 2, 1, 1, 1, 1, 1, 1]h = [1, 1, 1, 1, 1, 3, 6, 12, 25, 50]

В обоих способах частоты получаются максимально близкими к исходным. Разница в коэффициентах децимации и количестве точек заметна в основном на высоких частотах, и, вероятно, влияет на точность измерений, хотя это мы не исследовали.

Сверху - сформированный массив из 20 частот от 10 Гц до 45 кГц. Снизу - пример сигнала инжекции на минимальной частоте.

Сверху — сформированный массив из 20 частот от 10 Гц до 45 кГц. Снизу — пример сигнала инжекции на минимальной частоте.

Полигармонический

Спектр полигармонического сигнала инжекции должен содержать только гармоники, кратные первой гармонике с частотой f_min, поэтому массив частот формируется единственным способом.

Python‑скрипт для расчёта и результат
import numpy as npf_s = ...f_min = ...f_max = ...n_freq = ...n_max = ...# Расчёт максимальной гармоники.h_max = int(min(n_max / 2, f_max / f_min))# Формирование массива гармоник с логарифмическим распределением.h = np.unique(np.rint(np.geomspace(1, h_max, num=n_freq)).astype(np.uint64))# Расчёт коэффициента децимации.d = np.full(len(h), np.ceil(f_s / f_min / n_max), dtype=np.uint64)# Расчёт кол-ва точек и частот.n = np.floor(f_s / f_min / d + 0.5).astype(np.uint64)f = f_s / (d * n) * h

Пример результата расчёта при f_s = 100e3 Гц, f_min = 10 Гц, f_max = 5000 Гц, n_freq = 10, n_max = 1000:

f = [10.0000, 20.0000, 40.0000, 80.0000, 160.0000, 320.0000, 630.0000, 1260.0000, 2510.0000, 5000.0000]n = [1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000]d = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10]h = [1, 2, 4, 8, 16, 32, 63, 126, 251, 500]

Сигнал инжекции синтезируется как сумма всех целевых гармоник с одинаковыми амплитудами A и различными фазами ph[h]. Точно так же сигнал отправляется на сервер, инжектируется, измеряется реакция, выполняется БПФ и извлекаются модули и фазы гармоник. За один цикл измерения получаются массивы амплитуд и фаз и строятся ЛАЧХ и ЛФЧХ.

Как правило, инжекция добавляется внутри программного контура и воздействует на управляющие органы системы физически, а значит, её амплитуда не может быть любой и должна быть ограничена для предотвращения повреждений устройства.

Если при моногармоническом измерении фаза единственной гармоники инжекции не имеет значения и амплитуда инжекции задаётся напрямую, то при полигармоническом от фаз гармоник ph[h] зависит её итоговая амплитуда. Здесь мы применили алгоритм расстановки фаз гармоник с минимизацией крест‑фактора сигнала инжекции и последующим масштабированием под заданную амплитуду [Guillaume1991]. Минимизация крест‑фактора снижает амплитуду сигнала и делает его более симметричным относительно оси абсцисс, при этом сохраняя спектральный состав.

Различие сигналов с одинаковым спектром, но отличающимися фазами гармоник. Здесь гармоники от 1 до 15.

Различие сигналов с одинаковым спектром, но отличающимися фазами гармоник. Здесь гармоники от 1 до 15.
Измерение ЧХ апериодического фильтра 1-го порядка.

Измерение ЧХ апериодического фильтра 1-го порядка.

In‑place алгоритм

При измерении ЧХ серверу нужно одновременно хранить в буфере массив значений инжекции s и массивы значений реакций x и y. При количестве точек n понадобится буфер на 3*n записей. Оперативная память целевых встраиваемых платформ часто сильно ограничена, поэтому в DP для работы с буфером используется in‑place алгоритм.

В начале чётные элементы буфера заполняются значениями массива инжекции размера n, нечётные элементы не заполняются. На каждом такте дискретизации сервер извлекает из буфера очередное значение инжекции с индексом 2*i и выдаёт в ПО микроконтроллера. Затем читает реакцию системы по одному значению переменных x и y в элементы с индексами 2*i и 2*i+1, замещая значения инжекции.

Таким образом, in‑place алгоритм позволяет уменьшить размер буфера до 2*n.

Визуализация in-place алгоритма.

Визуализация in‑place алгоритма.

Расчёт цифрового ПИД‑регулятора

Коэффициенты цифрового ПИД‑регулятора рассчитываются одним из частотных методов на основе измеренных ЧХ. Метод подробно описан в книге [Corradini2015] (раздел «4.1 System‑Level Compensator Design»).

Вкратце:

  1. Пользователь подгружает измеренные ЧХ или импортирует их из файла.

  2. При необходимости указывает корректирующие коэффициенты для ЧХ, например, если цепь обратной связи имеет не единичный коэффициент усиления, а обычно так и есть.

  3. Задаёт желаемые частоту единичного усиления (частоту среза) и запас устойчивости по фазе.

  4. Нажимает кнопку «Синтезировать», и клиент выдаёт готовые коэффициенты ПИД‑регулятора в формате K_p, K_i и K_d или в формате коэффициентов цифрового фильтра a и b.

Пример расчёта ПИД-регулятора.

Пример расчёта ПИД‑регулятора.

Прочие функции

Помимо всего прочего добавили некоторые возможности, которые упрощают работу с программой:

  • Экспорт и импорт данных в форматах CSV и PNG.

  • Курсоры с автоматическим расчётом минимального, максимального, среднего и среднеквадратичного значений, периода, частоты и крест‑фактора на выбранном промежутке времени.

  • Чтение численных значений переменных. Иногда вместо графиков информативнее вывести число в различных представлениях.

  • Горячие клавиши: Tab/Ctrl+Tab для переключения страниц на главном окне, Ctrl+Space для запуска/останова измерения, Esc для закрытия второстепенных окон.

Пример работы курсоров.

Пример работы курсоров.

В завершение

Долго выбирали под какой лицензией выкладывать исходный код. С одной стороны, это не библиотека, которую другие будут использовать внутри своих проектов, поэтому можно использовать и copyleft лицензию типа GPL. С другой стороны, если уж открывать код, то делать это с максимальной пользой для пользователей и разработчиков. В итоге выбрали BSD-2-Clause.

Любые вопросы и обсуждения — в комментарии или личку. Баг‑репорты и пулл‑реквесты — в репозиторий. Программа будет развиваться в любом случае, поскольку мы и сами ей пользуемся. Круто, если она будет полезна кому‑то ещё.

В ближайших планах:

  • Поддержка новых архитектур и интерфейсов связи.

  • Исправление багов (куда без них).

  • Работа через JTAG‑эмуляторы, как это делают MCU Viewer или STM32CubeMonitor.

  • Отладка сразу нескольких устройств.

  • Добавление других классических методов синтеза цифровых регуляторов.

Ссылки на репозиторий: GitHub (основной), SourceCraft (зеркало)

В README можно найти анимацию работы программы в различных режимах.

ссылка на оригинал статьи https://habr.com/ru/articles/1058864/