
История о том, как внутренний программный инструмент для отладки вырос в проект, так и не ставший коммерческим, и почему в итоге мы выложили его в 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 было два основных режима.
Режим реального времени: клиент периодически запрашивал выбранные переменные, замерял время между запросами и отображал значения на графике. Частота запросов ограничивалась только битрейтом интерфейса, количеством переменных и загруженностью МК.
Режим триггера: клиент отправлял условия триггера (переменная, порог срабатывания, тип фронта) и адреса переменных для чтения. Сервер начинал циклически складывать в буфер значения переменных, пока не сработает триггер. После этого буфер с доступной скоростью отправлялся клиенту и отображался на графике. Длительность окна буферизации = 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).

Загрузка списка переменных
В 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 — треугольный сигнал. Снизу — треугольный сигнал.Измерение ЧХ
Самая интересная часть Digital Points — измерение частотных характеристик. Этот инструмент позволяет программно измерить ЧХ практически любых передаточных функций системы: объект, регулятор, обратная связь, фильтры, функции чувствительности и др.
В общем случае принцип измерения следующий:
Клиент формирует массив значений возмущающего воздействия (сигнал инжекции) размера
nи отправляет на сервер.Сервер на каждом такте (с частотой дискретизации
и с учётом коэффициента децимации
) выдаёт очередное значение из массива инжекции, которое пользователь должен добавить к нужной переменной внутри программного контура регулирования.
Одновременно с инжекцией сервер читает временные «снимки» двух переменных, условно
и
(реакция системы), массивами по
nзначений и отправляет клиенту.Клиент выполняет БПФ над «снимками», извлекает комплексные амплитуды
,
одной или нескольких гармоник с номерами
h() и вычисляет их модули
,
и фазы
,
.
Частота и амплитуда инжекции меняются (при моногармоническом измерении), и цикл повторяется.
Время одного измерения без оверхеда: (n*d)/f_s.
В результате получаются массивы амплитуд и фаз, по которым строятся графики ЛАЧХ и ЛФЧХ. Отношение модулей даёт ЛАЧХ, разность фаз
— ЛФЧХ.
Именно так работают векторные анализаторы цепей вроде AP Instruments Model 310 или OMICRON Lab Bode 100, только DP делает это программно.
В DP реализованы два типа измерения ЧХ в зависимости от типа сигнала инжекции: моногармонический и полигармонический. Для обоих формируется массив интересующих частот гармоник в количестве n_freq в диапазоне от f_min до f_max, которые задаются пользователем.
Моногармонический
Массив частот и длительность окна измерения формируются двумя способами на выбор, при этом сам принцип измерения одинаков для обоих.
Способ h = 1
Инжекция и измерения проводятся только на одном периоде частоты, которая в данный момент измеряется. Буфер недоиспользуется, но время измерения минимально.
Python‑скрипт для расчёта и результат
import numpy as np f_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 np f_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_max inv_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]
В обоих способах частоты получаются максимально близкими к исходным. Разница в коэффициентах децимации и количестве точек заметна в основном на высоких частотах, и, вероятно, влияет на точность измерений, хотя это мы не исследовали.

Полигармонический
Спектр полигармонического сигнала инжекции должен содержать только гармоники, кратные первой гармонике с частотой f_min, поэтому массив частот формируется единственным способом.
Python‑скрипт для расчёта и результат
import numpy as np f_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]. Минимизация крест‑фактора снижает амплитуду сигнала и делает его более симметричным относительно оси абсцисс, при этом сохраняя спектральный состав.


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.

Расчёт цифрового ПИД‑регулятора
Коэффициенты цифрового ПИД‑регулятора рассчитываются одним из частотных методов на основе измеренных ЧХ. Метод подробно описан в книге [Corradini2015] (раздел «4.1 System‑Level Compensator Design»).
Вкратце:
Пользователь подгружает измеренные ЧХ или импортирует их из файла.
При необходимости указывает корректирующие коэффициенты для ЧХ, например, если цепь обратной связи имеет не единичный коэффициент усиления, а обычно так и есть.
Задаёт желаемые частоту единичного усиления (частоту среза) и запас устойчивости по фазе.
Нажимает кнопку «Синтезировать», и клиент выдаёт готовые коэффициенты ПИД‑регулятора в формате
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 можно найти анимацию работы программы в различных режимах.