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“
... 
... 
... 

Ну и как вы понимаете, у этого есть и свои недостатки:

  1. Надо вручную заносить переменные, которые хотим посмотреть

  2. После отладки, команды 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

Рассмотрим пример работы пакета 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)

  • Документация команды trace

Нюанс: мониторинг триггеров осуществляется вручную, то есть если вы внесли переменную на отслеживание и скрипт упал, то при новом запуске скрипта произойдет добавление, а не перезапись триггера и фактически у вас их будет просто несколько. Это не критично, просто сообщения, генерируемые триггером будут повторяться несколько раз. Чтобы этого не случалось необходимо очищать список триггеров каждый раз перед вызовом скрипта, процедуры и тд.
Если переменная в неймспейсе, то она должна быть объявлена как 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

Пример работы пакета 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"
}

Комментарии (0)