
История о том, как внутренний программный инструмент для отладки вырос в проект, так и не ставший коммерческим, и почему в итоге мы выложили его в 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 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]
В обоих способах частоты получаются максимально близкими к исходным. Разница в коэффициентах децимации и количестве точек заметна в основном на высоких частотах, и, вероятно, влияет на точность измерений, хотя это мы не исследовали.
Полигармонический
Спектр полигармонического сигнала инжекции должен содержать только гармоники, кратные первой гармонике с частотой 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]. Минимизация крест‑фактора снижает амплитуду сигнала и делает его более симметричным относительно оси абсцисс, при этом сохраняя спектральный состав.
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 можно найти анимацию работы программы в различных режимах.
ссылка на оригинал статьи https://habr.com/ru/articles/1058864/