puts "ПЛИС-культ привет, FPGA / RTL / Verification ХАБ!"
Последние несколько месяцев я плотно сижу в Vivado и Tcl и вот решил с вами поделиться своими "открытиями" в области отладки Tcl скриптов, которые вероятно не многие из вас вообще пишут или используют в своей работе.
Несмотря на свою архаичность, Tool Command Language все же остается самым востребованным языком управления средой проектирования для ПЛИС и ASIC. ЕМНИП все эти инструменты управляются через Tcl, Vivado так уж точно, поэтому было бы не плохо научиться или хотя бы посмотреть какие инструменты отладки этих самых пресловутых Tcl-сценариев вообще существуют или существуют ли они вообще, кроме православного puts
В этой заметке, я постараюсь вам показать, что advanced tcl debugging - это не миф, а вполне реальная сущность, которая была создана еще на заре двухтысячных и не то что бы эта сущность как то изменилась за эти 20 с небольшим лет.
Открывайте ваши консольки, погнали!
А в чем собственно проблема та?
Основная загвоздка в том, что если вы используете просто нативный Tcl для разработки приложений, то отладчики есть: например тоже самое Komodo+ActiveState или DLTK на базе Eclipse. В них отладку производить в целом можно. Но вот когда речь заходит про тулы FPGA / ASIC разработки, то их набор команд или консоль мне так и не удалось прокинуть в эти и другие среды с графическим интерфейсом, поддерживающие отладку. К тому же даже обычное прокидывание Tk из Vivado для реализации кастомных Tk-based интерфейсов тоже не самая простая задача, хотя мной была успешно сделана благодаря tk_tunnel, входящего в состав Xilinx Tcl Store.
ОГРОМНАЯ ПРОСЬБА! если вы знаете или умеете подключать отладчики Tcl к Vivado, свяжитесь со мной в телеге @KeisN13 или в личке хабра или напишите об этом в комментариях, потому что я уже себе весь мозг сломал в попытках это сделать.
Зачем вообще изучать Tcl в 2025?
Не буду тут рассусоливать этот философский вопрос, а просто приведу выдержку из документации Vivado:
Tcl is a standard language in the semiconductor industry for application programming interfaces, and is used by Synopsys® Design Constraints (SDC).
Думаю, на этом мы можем этот вопрос закрыть.
Три дороги, три пути, выбирай куда идти
Еще один философский вопрос: а зачем мне Tcl, если я могу все сделать по кнопкам в GUI?
Ответ достаточно простой: всегда пожалуйста, если это вас устраивает и вам нравится тыкать кнопочки и ждать пока Vivado соберет вам отчет, построит схему, прошьет кристалл и тд. Всегда можно тыкнуть кнопочку в менюшке, настроить кучу полей, а потом нажать OK
.
А можно поступить иначе: ввести одну строчку в консоли для получения точно такого же отчета.
А можно сделать отдельную процедурку, которая будет вызываться одним словом в консоли и которая будет делать все, что вы делаете руками, тем самым освобождая вам кучу времени, которое можно провести за чтением документации :)
В общем тут на вкус и цвет, но если вы хотите погрузиться в advanced fpga, то уж простите, но без Tcl там делать нечего.
Примечание: на практике используются все три подхода, но в разной своей степени, так как иногда действительно намного проще и нагляднее сделать что-то в gui чем писать в консоли, например банальная сборка проекта процессорной системы в Block Design Vivado.

Ну закончим с нытьем и прелюдиями и перейдем к теме статьи.
Простые методы отладки и поиска ошибок в Tcl-сценариях
Я начну с самых простых методов, которые можно использовать, чтобы понять что же пошло не так как планировалось и постепенно дойду до самых интересных и тяжело воспринимаемых на первый взгляд методов отладки.
Команда history
history
– встроенная команда Tcl, которая позволяет просматривать историю выполненных команд.
По умолчанию хранится всего 20 команд, поэтому перед началом работы пропишите history keep 100000
и теперь вся ваша история текущей сессии будет сохранена, до момента пока вы не закроете Vivado. Это действительно иногда очень удобная штука, которая к тому же позволяет запустить команду из истории используя !номер_команды_в_истории
. Например:
history
# тут появится история
!42
#повторится команда под номером 42 из текущей истории
Подробнее с командой history
можно ознакомиться на этой странице

puts
99,9999% разработчиков используют только одну команду для отладки Tcl-скриптов - это команда puts
. Это самый простой вариант отладки, которым я тоже очень часто пользуюсь.
...
...
# Много много Tcl
...
...
# Место, где отваливается скрипт
puts "debug $something“
...
...
...
Ну и как вы понимаете, у этого есть и свои недостатки:
Надо вручную заносить переменные, которые хотим посмотреть
После отладки, команды
puts
нужно или закомментировать или удалить, чтобы их выхлоп не появлялся в консоли при работе скрипта и не мешал
Ну в целом хватит и двух пунктов
Улучшенный puts
Включив немного фантазии, можем улучшить puts
добавив в него возможность включения и отключения. Сделать это очень просто
proc dputs {msg} {
global debugMode
if { $::debugMode } {
puts $msg
}
}
Теперь устанавливая значение глобальной переменной debugMode
Можно управлять выводом отладочных сообщений. Рассмотрим пример
proc p1 {} {
variable a
set a 1
dputs "The value of a is $a"
p2
}
proc p2 {} {
variable b
set b 2
dputs "The value of b is $b"
}
set ::debugMode 1
p1
set ::debugMode 0
p1

Совет: если переменная глобальная, то лучше указывать ::
в её имени, поскольку при работе с пространствами имен бог его знает в каком именно вы сейчас находитесь, хотя это можно и посмотреть :)
Команда info и субкоманды level и frame
Позволяет получить информацию о состоянии интерпретатора, а также:
Имена параметров процедуры
Имя вызываемой процедуры
Тело процедуры
Список доступных процедур, команд и переменных в конкретном пространстве имен
Просмотреть стек вызова
И многое многое другое, подробнее здесь
Субкоманды level
и frame
позволяют получить информацию по стеку вызова переменной, имя скрипта, номер строки и др. Здесь хороший пример разницы между info level
и info frame
Давайте доработаем процедуру dputs
так, что бы она показывала нам полный путь до переменной:
proc dputs2 args {
global debugMode
if { $::debugMode } {
set res [list]
foreach i $args {
if [uplevel info exists $i] {
lappend res "from [info level -1] $i = [uplevel set $i]"
# полный путь имя значение
} else {
lappend res $i
}
}
puts stderr $res
}
}
Пример Работы dputs2
:
proc p1 {} {
set a 1
dputs2 a
p2
}
proc p2 {} {
set b 2
set c 3
dputs2 b c
nsA::p3
nsB::p3
nsB::nsC::p3
}
namespace eval nsA {
proc p3 {} {
set d 4
dputs2 d
}
}
namespace eval nsB {
namespace eval nsC {
proc p3 {} {
set g 5
dputs2 g
}
}
proc p3 {} {
set e 4
dputs2 e
}
}
set ::debugMode 1
p1
set ::debugMode 0
p1

Как вы можете заметить, мы получили вывод значений переменных с их полным расположением.
Не совсем простые методы отладки Tcl-сценариев
Повышаем градус
Библиотека Tcllib
Это стандартная библиотека для языка программирования Tcl (Tool Command Language). Она представляет собой набор модулей и пакетов, расширяющих функциональность базового интерпретатора Tcl. Библиотека включает различные модули для работы с файлами, сетью, XML, JSON, регулярными выражениями, шифрованием, криптографическими функциями и многим другим.
В Tcllib включены несколько пакетов, которые могут помочь с отладкой Tcl-сценариев:
debug (tcllib 1.21 и старше)
log
logger
Tcllib обычно по умолчанию поставляется со средой разработки, но ее можно скачать отдельно
Например, библиотека установлена по умолчанию в Vivado

Tcllib – пакет debug (tcllib-1.21 и старше)
Представляет собой улучшенную систему включения / отключения вывода отладочной информации
Управление осуществляется путем включения / отключения определенных тегов, связанных с выводом отладочной информации
Возможно установить разные уровни срабатывания для тегов: число больше или равное 0
Можно добавить общий заголовок, индивидуальные префиксы и суффиксы для отладочного сообщения каждого тега
Зарегистрировать любой пакет из Tcllib в сеcсии можно простой командой
source {путь_до_tcclib/modules/debug/debug.tcl}
или воспользоваться auto_path
и pkgIndex.tcl
или прописать путь в init.tcl
Vivado если вы это умеете :)
Документация по работе с пакетом
Рассмотрим небольшой пример работы с пакетом debug
# 1. Создаем и настраиваем теги
debug define tagA
debug define tagB
# для этого тега установим вывод сообщения если уровень вызова 2 и выше
debug level tagB 2
# Добавим префикс к выводу сообщения
debug prefix tagB " DEBUG:B "
debug define tagC
# Добавим глобальный префикс перед выводом сообщений, действует на все теги
debug header "==> "
proc p1 {} {
set a 5
debug.tagA "Value a is $a"
set a 6
debug.tagB "Value a is $a" 1
set a 7
debug.tagB "Value a is $a" 2
set a 8
# Сообщение не покажется,
# потому что у этого тега настроен уровень срабатывания 2,
# а в сообщении он 3
debug.tagB "Value a is $a" 3
}
# включаем тег
debug on tagA
p1
# выключаем тег
debug off tagA
p1
debug on tagB
p1
debug off tagB
p1

Этот метод несколько сложнее чем просто dputs
с глобальным вкл/выкл, но при этом предоставляет более гибкий функционал управления
Tcllib – пакет log
Пакет log предоставляет команды, которые позволяют библиотекам и приложениям выборочно регистрировать информацию об их внутренней работе и состоянии.
По принципу работы схож с пакетом debug, но имеет несколько расширенную систему управления включением / отключением отладочных сообщений и более строгим уровнем срабатываний: emergency, alert, critical, error, warning, notice, info, debug
Есть в Vivado, подключается через package require log

Рассмотрим пример работы пакета log
# подключаем пакет
package require log
# включаем определенные уровни для вывода сообщений
::log::lvSuppress debug 0
::log::lvSuppress error 0
proc p1 {} {
set a 5
set a 6
# выводим сообщение с уровнем debug
::log::log debug "~~log::log Value a is $a"
set a 7
# выводим сообщения с уровнем error
::log::log error "~~log::log Value a is $a"
# см ниже про разницу ::log::log и ::log::Puts
::log::Puts error "~~log::Puts Value a is $a"
}
p1
# Отключим уровень error
::log::lvSuppress error 1
p1

Обратите внимание, что в log есть две процедуры вывода сообщений: ::log::log
и ::log::Puts
Разница между ними в том, что ::log::log
учитывает включение и отключение уровня, а ::log::Puts
работает просто как обычный puts
, т.е. игнорирует вкл/откл уровня. См. внимательнее лог консоли и код
Встроенная команда trace
Осуществляет мониторинг изменения выбранных переменных, отслеживает вход/выход, зарегистрированных в трасcировщике процедур и команд
Для переменных, процедур и команд задаются определенные триггеры, по которым происходит формирования лога, с выводом в указанный канал (консоль, файл, сокет и тд.)
Присутствует в версии Tcl 8.4 и старше (которая вышла больше 20 лет назад xD)
Нюанс: мониторинг триггеров осуществляется вручную, то есть если вы внесли переменную на отслеживание и скрипт упал, то при новом запуске скрипта произойдет добавление, а не перезапись триггера и фактически у вас их будет просто несколько. Это не критично, просто сообщения, генерируемые триггером будут повторяться несколько раз. Чтобы этого не случалось необходимо очищать список триггеров каждый раз перед вызовом скрипта, процедуры и тд.
Если переменная в неймспейсе, то она должна быть объявлена как variable
, а не через set
Алгоритм работы команды trace
Добавить переменные, процедуры и команды через trace add
Для переменных указать триггер срабатывания: запись, чтение или unset
Для процедур и команд указать момент срабатывания триггера: непосредственно перед запуском, после выполнения или после входа в тело процедуры
Указать процедуру обработчик события: именно она будет формировать отчет. Процедура принимает на вход три операнда, значение которых содержит всю необходимую информацию, но ее надо достать из этих переменных. Имя процедуры может быть любым
Включить трассировщик с указанными триггерами
После окончания работы или если случилась ошибка выполнения обязательно сотрите триггеры перед следующим запуском
Пример работы команды trace для переменных
# Эта процедура, которая будет запущена, когда сработает триггер по переменной
# Для переменной триггеры это запись, чтение или unset
# Они задаются индивидуально для каждой переменной
# код примера взял тут
# https://stackoverflow.com/questions/32743204/using-tcl-trace-on-a-local-variable-of-a-proc
proc trackMyVar {name element op} {
# In case of array variable tracing, the 'element' variable will specify the array index
# For scalar variables, it will be empty
if {$element != ""} {
set name ${name}($element)
}
upvar $name x
if {$op eq "r"} {
puts "Variable $name is read now. It's value : $x"
} elseif { $op eq "w"} {
puts "Variable $name is written now. New value : $x"
} elseif {$op eq "u"} {
puts "Variable $name is unset" } else {
# Only remaining possible value is "a" which is for array variables
# For array variables, tracing will work only if they have
# accessed/modified with array commands
}
}
proc foo {} {
# Adding tracing for variable 'k’
# здесь объявляется переменная, триггеры запуска и процедура обработчик триггера
trace add variable k rwu trackMyVar
set k {0}
foreach a { 1 2 3 4 } {
lappend k [ expr { [lindex $k end ] + $a } ]
}
unset k; # Just added this to demonstrate 'unset' operation
# Здесь стираем трассировкщик по переменной с заданными триггерами
trace remove variable k {read write unset} trackMyVar
}
foo

Что тут происходит:
Согласно алгоритму, который я описал выше, мы создали трассировщик по переменной k
, задали триггеры, когда переменная читается или в нее происходит запись значения записывается или когда делается unset, затем указали процедуру обработчик trackMyVar
, которая запускается при срабатывании триггера, и после окончания работы мы стерли трассировщик и триггеры по переменной k
Сложно? Сложно, но это еще не все, перейдем к примеру с процедурами
Пример работы команды trace для процедур
Это все работает примерно также, как и с переменными, но вывод будет такой же как и для пакета logger, поэтому переходим к нему.
Tcllib – пакет logger
Мощный инструмент отладки, логирования и трассировки процедур, сочетающий в себе функционал пакета log и команды trace. Позволяет:
создавать неограниченное число логов, со своими уникальными уровнями срабатывания
отслеживать стек вызова процедур с отображением передаваемых аргументов и возвращаемым результатом
выбирать процедуры для логирования и их трассировки
требует определенного навыка владения и понимания архитектуры построения
Как это работает:
Подключить пакет logger:
package require logger
Создать сервис – коллекцию отдельных логов
Создать процедуру, которая вызывается каждый раз, когда происходит вход/выход в зарегистрированные процедуры
Зарегистрировать процедуры в трассировщике
Включить трассировку зарегистрированных процедур
Пример работы пакета logger для процедур
# Подключаем пакет
package require logger
# простая процедура-обработчик
proc tracecmd { dict } {
puts $dict
}
# Создаем сервис example
set log [::logger::init example]
# Регистрируем процедуру обработчик для сервиса example
${log}::logproc trace tracecmd
# процедуры, которую будем отслеживать
proc foo { args } {
puts "In foo"
bar 1
return "foo_result"
}
proc bar { x } {
puts "In bar"
return "bar_result"
}
# Указываем процедуры, которые хотим отслеживать
${log}::trace add foo bar
# Включаем обработчик
${log}::trace on
# Погнали!
foo

Как видим, в логе показываются:
стек вызова процедур
аргументы процедур со значениями
моменты входа и выхода из процедур
возвращаемый результат
статус завершения работы процедур
Миша! может уже хватит, мы перестали понимать уже на втором Чаке!!!
Не останавливаемся, финиш уже скоро.
Понятно, что стек вызова процедур виден, а что со значением переменных? Их можно посмотреть?
Да, значения всех переменных в процедуре, которые они приняли до объявления триггера можно вывести, но есть нюансы:
В процедуре должно быть объявление хотя бы одной переменной через
variable
. Если все переменные объявлены черезset
, то их не будет видно-
Переменная
log
, инициализируется в глобальном пространстве имен, и для получения к ней доступа есть несколько вариантов:объявить ее как
global
в процедуреиспользовать
${::log}
, вместо${log}
использовать полный путь вызова триггера, например:
::logger::tree::service_name::debug
,::logger::tree::service_name::error
и тдиспользовать
[set ::log]
в общем случае, нужно указать путь до инициализации
log
, если он создан не в глобальном пространстве имен
Пример работы пакета logger для переменных
# Процедура обработчик
proc log_local_var {txt} {
set caller [info level -1]
set vars [uplevel 1 info vars]
foreach var [lsort $vars] {
if {[uplevel 1 [list array exists $var]] == 1} {
lappend val $var <Array>
} else {
lappend val $var [uplevel 1 [list set $var]] }
}
puts "$txt"
puts "Caller: $caller"
puts "Variables in callers scope:"
foreach {var value} $val {
puts "$var = $value"
}
}
# Подключаем пакет
package require logger
# Регистрируем сервис
set log [logger::init myservice]
#Регистрируем процедуру,
#которая вызывается при срабатывании уровня debug
${log}::logproc debug log_local_var
proc p1 { aa bb cc } {
variable c
set c $cc
set a $aa
set e [expr $a + $c]
set b $bb
${::log}::debug "Crashed here !!!"
#ещё какой-то код
}
p1 10 20 30

Как вы можете заметить, в логе отобразились все переменные с их значениями, которые они приняли к моменты вызова триггера ${::log}::debug "Crashed here !!!"
Глобальные переменные для ошибок
Немножко выдохнем, и вернемся к уровню сложности 0
В Tcl существует ряд глобальных переменных, получение значения которых может помочь при отладке. Рассмотрим две из них errorCode
и errorInfo
В общем случае, когда происходит исключение (exception) информация об ошибке и код ошибки, сохраняются в эти переменные.
В Tcl существует механизм, когда пользовательские процедуры и команды могут персонализировать информационное сообщение и код ошибки. Это можно сделать несколькими способами:
использовать
return –code code
при выходе из процедурприменить обработчик исключений
catch
Более подробно расписано здесь
Пример использования errorCode и errorInfo
proc a {} { b }
proc b {} { c }
proc c {} { d }
proc d {} { some_command }
a
Вывод, конечно будет зависеть от интерпретатора, но тем не менее.
Это то, что показала консоль после того как обнаружилось, что some_command
не объявлена

Если после получения сообщения об ошибке ввести в консоль $::errorInfo
получим более расширенный лог/историю до момента ошибки

А если ввести $::errorCode
, то

Настройте свой текстовый редактор
Это просто еще один совет, который читатель скорее всего не оценит, но я его использую максимально часто при отладке :)
Во многих тестовых редакторах есть замечательная функция: выделенный фрагмент или всю строку на которой находится курсор скопировать в буфер обмена и исполнить в консоли.
Если отладка проекта ведется в tcl-mode без GUI, то можно запустить Vivado, скажем в терминале VS Code и, назначив себе горячую клавишу на описанный функционал, сэкономить себе кучу времени.
Например, для VS Code это хоткей workbench.action.terminal.runSelectedText
После назначения горячей клавиши на это действие, выделенный фрагмент или вся строка будут переданы в терминал, где запущена Vivado в tcl-mode, и выполнена.
Мы не хотим логи и путсы, мы хотим пошаговую отладку с брекпойнтами!
Действительно, для Tcl/Tk существуют и IDE и просто скрипты, которые позволяют выполнять отладку в пошаговом режиме, с просмотром состояния переменных, точками останова и всем остальным.
НО ЕСТЬ НЮАНС! За какми-то гуем нам запретили gets
в гуи!

Вот что пишут в доках на Vivado
It is recommended that you provide custom values to a script through a configuration file that can be easily read by the Tcl script
Но мы оказались хитрее

Если запускать Vivado, как это делают взрослые дядьки, то дорога к пользовательскому вводу через gets stdin
нам будет открыта, и это даёт нам огромные возможности возможности в отладке.
Консольные отладчики
На просторах всемирной паутины вы можете найти несколько консольных отладчиков, я укажу на два из них, которыми сам пользовался.
stepsource.tcl
Это скрипт, придуманный не мной, который позволяет выполнять пошаговую отладку Tcl-сценариев, но в консоли vivado. Имеет следующий функционал
<line#> run until line number
<return> run next line
a list array values
b run until next breakpoint
b ? list breakpoints
b <line#> set breakpoint
b -<line#> unset breakpoint
b - unset all breakpoints
c list changed variable values
e run to end of current procedure
g list global variables
h help
l list all instrumented lines
l <line#> [<line#>] list line numbers
v list variable values
x abort execution
<anything else> execute as tcl command
Скачивается здесь
Как с ним работать?
-
В консоли Vivado в non-GUI режиме выполнить
source stepsource.tcl
stepsource::StepSource путь_до_вашего_скрипта.tcl
Зажать enter до полной прогрузки скрипта (пока опять не появится
vivado%
)Теперь объявленные в вашем скрипте процедуры видны командой ::ss и могут быть отлажены в пошаговом режиме
Пример:
proc p { x y z } {
set a $x
set b $y
set c $z
set d [expr $a + $b]
puts "End of proc"
}

При работе с stepsource.tcl я обнаружил его некорректное поведение, если в процедуре используется вызов другого .tcl скрипта. Не стал пока с этим разбираться, оставил этот коммент здесь, чтобы вы просто имели это в виду.
debug.tcl
Этот скрипт, который скачивается здесь, мне показался несколько удобнее и проще, но его пришлось немного допилить, поскольку как и указано на его странице - он не совсем готовый.
Ради спортивного интереса, я просто оставлю о нем упоминание в надежде на то, что кто-нибудь когда-нибудь попробует его самостоятельно запустить и доработать.
Отмечу один баг который я нашел. Он связан с проблемой синтаксического сахара в виде $var
вместо [set var]
, меняющей указатель на файл на 1 после команды [read $fp]
. Если вы пользуетесь этим скриптом для отладки и в процедуре есть чтение контента файла через read
просто замените $fp
на [set fp]
.
Все, можно выдыхать
На этом, товарищи камрады, у меня все. Идея для этой статьи у меня висела в голове более 7 лет и я рад, что смог систематизировать свои наработки в этом материале.
Надеюсь, вы найдете её полезной и сможете применить описанные здесь подходы в своей работе. Если у вас появятся вопросы и предложения буду рад их рассмотреть и помочь в поиске ответа на них.
Tcl штука достаточно простая и понятная, но когда баг уводит тебя в долгие часы поиска проблем, хотелось бы иметь хоть какой-то адекватный инструмент помимо puts
, чтобы его устранить.
foreach reader $all {
puts "Thanks and good luck"
}