
В команде Автономного транспорта Яндекса мы занимаемся разработкой лидаров. Когда мы были готовы перейти от прототипа к опытному производству, перед нами встала задача организовать приёмочные испытания его компонентов, в том числе проверку качества пайки. Для этого мы решили объединить ПЛИС и микроконтроллер в одну JTAG‑цепь. Оставалось выяснить, как такое объединение повлияет на прошивку и отладку обоих устройств.
Меня зовут Дмитрий, я из команды разработки лидаров. Расскажу, как мы собирали тестовую цепь из STM32 и Xilinx Artix-7, настраивали прошивку через OpenOCD и openFPGALoader и пробовали совместную отладку через Vivado. В статье собрал рабочие конфигурации, ограничения инструментов и решения, которые пришлось искать на форумах и проверять экспериментами.
JTAG — швейцарский нож мира микросхем
Если вы имели дело с микроконтроллерами или ПЛИС, то сталкивались с интерфейсом JTAG. О его особенностях и внутреннем устройстве можно почитать в цикле статей, из которых я и сам черпал информацию в процессе работы.
Нам JTAG был интересен по двум главным причинам:
Для ПЛИС это основное, если не единственное решение для прошивки. А для микроконтроллеров (в частности, для STM32, о которых пойдёт речь дальше) он делит нишу интерфейса прошивки и дебага с проприетарным SerialWire.
Помимо простой передачи информации, у протокола есть функции для управления ведомой микросхемой.
Также свою роль сыграет то, что JTAG реализует довольно необычный для этого уровня интерфейс: ведомые устройства соединяются друг с другом по цепочке «выход ко входу» и последовательно передают данные от ведущего.
Но изначально у нас был вопрос контроля серийных изделий: какое отношение к нему имеет JTAG? Ответ в одной из дополнительных функций — доступе к управлению выводами микросхемы. Представьте: вы вообще не прошиваете микросхему (возможно, её и вовсе нельзя прошить), но при этом можете устанавливать логические 1 и 0 на её функциональные ноги, а также считывать приходящие извне значения.
Это хороший и довольно бюджетный способ выявления дефектов пайки: специальный скрипт подаёт значения на ноги одной микросхемы и читает их с ног другой, проверяя наличие корректных электрических контактов и отсутствие некорректных. А если объединить микросхемы в общую цепь, можно делать это с помощью минимального числа внешних выводов платы.
Чтобы проверить, как прошивка и совместная отладка будут работать в единой JTAG‑цепи, мы собрали тестовый стенд.
Тестовый стенд: собираем STM32F4 и Xilinx Artix-7 в единую JTAG‑цепь
Для исследования проблемы коллеги раздобыли две отладочные платы — NUCLEO с контроллером STM32F446RE и безымянную плату с чипом Xilinx семейства Artix-7(XC7A200T). Я соединил их в цепь с помощью специализированного высокотехнологичного жгута и подключился программатором Digilent HS-3.

Соединить устройства в цепочку можно двумя способами — в зависимости от порядка сигналов TDI и TDO. Чуть позже это сыграет свою роль:


На этом стенде мы:
поработаем с JTAG с помощью UrJTAG;
прошьём микроконтроллер и ПЛИС с помощью OpenOCD;
прошьём ПЛИС с помощью openFPGALoader;
опробуем возможности совместной отладки ПЛИС и микроконтроллера.
UrJTAG: проверка связи
UrJTAG — это многофункциональная утилита для работы с JTAG. В будущем мы планируем использовать её для управления вводами и выводами микросхем, но пока наша задача — получить первую обратную связь от собранной цепи.
Утилиту нужно собирать из исходников (в папке doc лежит подробная инструкция по установке). После сборки программа доступна по команде jtag — только не забудьте выполнить ldconfig, чтобы динамический линковщик увидел новые библиотеки.
При запуске jtag открывается собственный интерактивный терминал. Сначала связываемся с адаптером с помощью команды cable. В моём случае это выглядит так:
cable DigilentHS1 vid=0x0403 pid=0x6014
Теперь мы можем вызвать команду detect и увидеть результат:
IR length: 15 Chain length: 3 Device Id: 01001011101000000000010001110111 (0x4BA00477) Unknown manufacturer! (01000111011) (/usr/local/share/urjtag/MANUFACTURERS) Device Id: 00000110010000100001000001000001 (0x06421041) Unknown manufacturer! (00000100000) (/usr/local/share/urjtag/MANUFACTURERS) Device Id: 00010011011000110110000010010011 (0x13636093) Manufacturer: Xilinx (0x093) Part(2): xc7a200t (0x3636) error: Unable to open file '/usr/local/share/urjtag/xilinx/xc7a200t/STEPPINGS' Unknown stepping! (0001) (/usr/local/share/urjtag/xilinx/xc7a200t/STEPPINGS)
Видно всё, что нам нужно: в цепи мы обнаружили три устройства, одно из них — ПЛИС от Xilinx, остальные нам неизвестны, поэтому адекватно связаться с ними мы сейчас не можем. Почему устройств оказалось три, а не два — разберём чуть позже. Если передать UrJTAG нужные описания компонентов, дальше можно делать с устройствами всё, что нужно. Но сейчас остановимся на этом и перейдём к прошивке и отладке.
Цепочки OpenOCD и JTAG
OpenOCD — это открытый инструмент для прошивки и отладки микросхем. В основном его используют, чтобы поднять GDB‑сервер при отладке кода на микроконтроллерах, но может он гораздо больше.
Сначала настроим корректную работу OpenOCD. И пока что буду использовать версию 0.12.0, которая по умолчанию ставится пакетным менеджером Ubuntu 24. Чуть позже это окажется важным.
Подключаем микроконтроллер
Для работы с микроконтроллером OpenOCD поднимает GDB‑сервер, к которому потом подключается отладчик — чтобы загружать бинарники, ходить по шагам и расставлять точки останова.
Попробуем запустить простую команду OpenOCD для конфигурации «TDO STM32 в TDI ПЛИС»:
openocd -f interface/ftdi/digilent_jtag_hs3.cfg -f target/stm32f4x.cfg
В ответ получаем ошибку:
openocd -f interface/ftdi/digilent_jtag_hs3.cfg -f target/stm32f4x.cfg Open On-Chip Debugger 0.12.0 Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'. Info : Listening on port 6666 for tcl connections Info : Listening on port 4444 for telnet connections Info : clock speed 2000 kHz Info : JTAG tap: stm32f4x.cpu tap/device found: 0x13636093 (mfg: 0x049 (Xilinx), part: 0x3636, ver: 0x1) Warn : JTAG tap: stm32f4x.cpu UNEXPECTED: 0x13636093 (mfg: 0x049 (Xilinx), part: 0x3636, ver: 0x1) Error: JTAG tap: stm32f4x.cpu expected 1 of 1: 0x4ba00477 (mfg: 0x23b (ARM Ltd), part: 0xba00, ver: 0x4) Info : JTAG tap: stm32f4x.bs tap/device found: 0x4ba00477 (mfg: 0x23b (ARM Ltd), part: 0xba00, ver: 0x4) Info : JTAG tap: auto0.tap tap/device found: 0x06421041 (mfg: 0x020 (STMicroelectronics), part: 0x6421, ver: 0x0) Error: Trying to use configured scan chain anyway... Warn : AUTO auto0.tap - use "jtag newtap auto0 tap -irlen 2 -expected-id 0x06421041" Error: auto0.tap: IR capture error; saw 0x0002 not 0x0001 Warn : Bypassing JTAG setup events due to errors Error: Invalid ACK (6) in DAP response
Сущность этого лога в следующем: OpenOCD пошёл сканировать цепь и нашёл Xilinx‑чип с id 0×13636093, которого встретить не ожидал.
Исправим эту ситуацию. Напишем свой конфигурационный файл для OpenOCD такого вида:
source [find interface/ftdi/digilent_jtag_hs3.cfg] source [find cpld/xilinx-xc7.cfg] source [find target/stm32f4x.cfg]
Запускаем OpenOCD с нашим конфигом через флаг ‑f и видим в логе заветные строчки:
Info : starting gdb server for stm32f4x.cpu on 3333 Info : Listening on port 3333 for gdb connections
Порядок, в котором подключаются конфиги Xilinx и STM32, принципиален: если поменять их местами, то устройства в цепи, называемые здесь тапами, будут расположены неправильно, в итоге мы снова получим ошибку о не совпавших идентификаторах. Соответственно, для конфигурации TDI STM32 в TDO ПЛИС эти файлы нужно подключать в обратном порядке.
Теперь, когда GDB‑сервер поднялся, мы можем подключаться к нему с помощью клиента или с помощью IDE. Launch‑конфигурация для отладки при помощи расширения Cortex‑debug для VS Code будет выглядеть примерно так:
"name": "Chain test", "type": "cortex-debug", "request": "launch", "servertype": "openocd", "cwd": "${workspaceRoot}", "executable": "build/Debug/JTAGChainTest.elf", "device": "STM32F446RE", "configFiles": [ "stm_tdo_config.cfg" ], "svdFile": "./thirdparty/lib/stm32/svd/STM32F446.svd", "runToEntryPoint": "main", "showDevDebugOutput": "raw", "liveWatch": true,
Эта конструкция свободно зашивает в чип прошивки, а также позволяет пользоваться всеми привычными инструментами отладки.
OpenOCD и ПЛИС
С ПЛИС всё обстоит иначе: основное средство отладки — ILA (Internal Logic Analyzer), с помощью которого можно смотреть временные диаграммы сигналов внутри кристалла. Однако этот инструмент проприетарный, и работать с ним получится только через ПО от производителя (в нашем случае Vivado). Поэтому ограничимся тем, что научимся прошивать ПЛИС с помощью OpenOCD.
Здесь стоит пояснить важный нюанс. Процесс, который я для простоты называю прошивкой ПЛИС, можно проводить по двум сценариям:
Прямая конфигурация ячеек ПЛИС. Прошивка сразу начинает работать, но сбрасывается при сбросе питания.
Запись во FLASH‑память. Конфигурация сохраняется в энергонезависимой памяти, и ПЛИС сама считывает её оттуда при каждой перезагрузке.
Нам нужно реализовать оба варианта. Начнём с первого — он проще в реализации.
Вся нужная нам информация лежит в конфиге cpld/xilinx-xc7.cfg. Мы уже задействовали его выше, когда настраивали микроконтроллер, поэтому давайте заглянем внутрь. Найти этот файл можно в папке scripts у OpenOCD (например, по пути /usr/share/openocd/scripts).
Конфиг
# SPDX-License-Identifier: GPL-2.0-or-later # xilinx series 7 (artix, kintex, virtex) # http://www.xilinx.com/support/documentation/user_guides/ug470_7Series_Config.pdf if { [info exists CHIPNAME] } { set _CHIPNAME $CHIPNAME } else { set _CHIPNAME xc7 } # the 4 top bits (28:31) are the die stepping/revisions. ignore it. jtag newtap $_CHIPNAME tap -irlen 6 -ignore-version \ -expected-id 0x03622093 \ -expected-id 0x03620093 \ -expected-id 0x037C4093 \ -expected-id 0x0362F093 \ -expected-id 0x037C8093 \ -expected-id 0x037C7093 \ -expected-id 0x037C3093 \ -expected-id 0x0362E093 \ -expected-id 0x037C2093 \ -expected-id 0x0362D093 \ -expected-id 0x0362C093 \ -expected-id 0x03632093 \ -expected-id 0x03631093 \ -expected-id 0x03636093 \ -expected-id 0x03647093 \ -expected-id 0x0364C093 \ -expected-id 0x03651093 \ -expected-id 0x03747093 \ -expected-id 0x03656093 \ -expected-id 0x03752093 \ -expected-id 0x03751093 \ -expected-id 0x03671093 \ -expected-id 0x036B3093 \ -expected-id 0x036B7093 \ -expected-id 0x036BB093 \ -expected-id 0x036BF093 \ -expected-id 0x03667093 \ -expected-id 0x03682093 \ -expected-id 0x03687093 \ -expected-id 0x03692093 \ -expected-id 0x03691093 \ -expected-id 0x03696093 \ -expected-id 0x036D5093 \ -expected-id 0x036D9093 \ -expected-id 0x036DB093 pld device virtex2 $_CHIPNAME.tap 1 set XC7_JSHUTDOWN 0x0d set XC7_JPROGRAM 0x0b set XC7_JSTART 0x0c set XC7_BYPASS 0x3f proc xc7_program {tap} { global XC7_JSHUTDOWN XC7_JPROGRAM XC7_JSTART XC7_BYPASS irscan $tap $XC7_JSHUTDOWN irscan $tap $XC7_JPROGRAM runtest 60000 #JSTART prevents this from working... #irscan $tap $XC7_JSTART runtest 2000 irscan $tap $XC7_BYPASS runtest 2000 }
В глаза бросается процедура xc7_program. Не дайте себя обмануть, она ничего не программирует — на это тонко намекает тот факт, что процедура не принимает путь к файлу. Гораздо важнее строчка pld device virtex2 $_CHIPNAME.tap 1 — регистрация программируемого логического устройства на тап ПЛИС. В документации OpenOCD есть и другие pld‑команды, в том числе pld load, которую и будем использовать.
Итак, подготовив bit‑файл для загрузки в ПЛИС, используем команду:
openocd -f stm_tdo_config.cfg -c "adapter speed 6000; init; pld load 0 file.bit; exit"
В случае успеха команда отрабатывает молча, но я верифицирую успешность процесса с помощью Vivado Lab: в Hardware Manager у ПЛИС можно увидеть пометку Programmed. Там же можно заметить, что тапов в цепи не два, а три: ПЛИС, arm‑ядро микроконтроллера и ещё один тап, неизвестный Vivado, — периферия контроллера, которая выполнена отдельным звеном JTAG‑цепочки.

Теперь перейдём к вопросу, как писать программу во FLASH. Под капотом у этого процесса две операции:
Запись на саму ПЛИС специальной прошивки, которая умеет работать с FLASH по SPI.
Передача этой самой прошивке уже настоящего файла, который она запишет во FLASH.
Работа с FLASH в OpenOCD осуществляется с помощью специальных драйверов. Нас интересует jtagspi‑драйвер. В его описании в документации нет ничего подходящего нам, зато в конфигах OpenOCD есть файл cpld/jtagspi.cfg.
# SPDX-License-Identifier: GPL-2.0-or-later set _USER1 0x02 if { [info exists JTAGSPI_IR] } { set _JTAGSPI_IR $JTAGSPI_IR } else { set _JTAGSPI_IR $_USER1 } if { [info exists TARGETNAME] } { set _TARGETNAME $TARGETNAME } else { set _TARGETNAME $_CHIPNAME.proxy } if { [info exists FLASHNAME] } { set _FLASHNAME $FLASHNAME } else { set _FLASHNAME $_CHIPNAME.spi } target create $_TARGETNAME testee -chain-position $_CHIPNAME.tap flash bank $_FLASHNAME jtagspi 0 0 0 0 $_TARGETNAME $_JTAGSPI_IR proc jtagspi_init {chain_id proxy_bit} { # load proxy bitstream $proxy_bit and probe spi flash global _FLASHNAME pld load $chain_id $proxy_bit reset halt flash probe $_FLASHNAME } proc jtagspi_program {bin addr} { # write and verify binary file $bin at offset $addr global _FLASHNAME flash write_image erase $bin $addr flash verify_bank $_FLASHNAME $bin $addr }
В целом всё работает так, как описано выше:
есть процедура
jtagspi_init, которая загружает в ПЛИС proxy‑прошивку и регистрирует FLASH. Эту прошивку можно сгенерировать с помощью скриптаcontrib/loaders/flash/fpga/xilinx_bscan_spi.py, как написано в документации к драйверу jtagspi, или найти готовый файл в интернете;есть процедура
jtagspi_program, которая затем записывает в этот FLASH прошивку и верифицирует.
Напишем и запустим вот такой конфиг для случая TDO STM32 в TDI ПЛИС:
reset_config none source [find interface/ftdi/digilent_jtag_hs3.cfg] source [find cpld/xilinx-xc7.cfg] source [find cpld/jtagspi.cfg] # source [find target/stm32f4x.cfg] adapter speed 4000 init jtagspi_init 0 bscan_spi_xc7a200t.bit jtagspi_program test_jtag.bin 0x0 shutdown
Мы увидим длинный лог о записи данных в разные секторы FLASH и успешное завершение процедуры. Перезагружаем ПЛИС, потому что без специальных действий с нашей стороны прошивка из FLASH подхватывается только после перезагрузки, открываем Vivado и видим... статус Not programmed, который сигнализирует о том, что мы что‑то сделали неправильно.
Начнём диагностику с прошивки FLASH с помощью Vivado. И — о чудо! — ПЛИС свободно загружается и отлично работает. Значит, с файлом всё в порядке, и остаётся понять, что мы делаем не так. Очевидно, Vivado производит с файлом или флешкой какие‑то манипуляции, которых не делает OpenOCD.
Ответ на этот вопрос узнать легко, как нам сначала казалось, — и в Vivado, и в OpenOCD есть функция верификации: программа считывает содержимое FLASH, сравнивает его с заданным файлом и сообщает о любых несовпадениях байтов. В OpenOCD за это отвечает команда flash verify_bank $_FLASHNAME $bin $addr, которая как раз выполняется внутри jtagspi_program.
Прошьём FLASH с помощью Vivado и верифицируем с помощью OpenOCD — верификация успешна. Окей, похоже, что OpenOCD не только не пишет эти участки FLASH, но и не верифицирует их. Но Vivado должна нам показать, что сделал не так OpenOCD. Прошиваем FLASH с помощью OpenOCD и верифицируем при помощи Vivado, и... верификация успешна. Почему при одинаковых результатах проверки ПЛИС загружается только в одном случае, по‑прежнему непонятно.
Но у нас есть последний рубеж — с помощью Vivado сдампить FLASH после двух прошивок и сравнить полученные файлы. Впечатление, которое на меня произвело абсолютное совпадение этих файлов, сравнимо с тем, что чувствовал Пильман из «Пикника на обочине», когда думал о людях, переживших Посещение: следов воздействия нет, а катастрофы есть. У нас есть два одинаковых файла во FLASH, загруженных из разных источников. С одним из них ПЛИС грузится, с другим — нет.
Но, как это часто бывает, мистическая, на первый взгляд, картина скрывает под собой довольно тривиальную действительность: оказалось, что если не очищать FLASH с помощью Vivado предварительно, а сразу писать свой файл с помощью OpenOCD поверх другого, записанного корректно, то проблем с загрузкой с неё не будет. Похоже, что дело не в файле, а в состоянии FLASH.
FLASH‑память, помимо самого массива памяти, обычно содержит набор регистров для управления: конфигурация, чтение статуса и так далее. Если заглянуть в даташит на наш FLASH‑чип (S25FL256S в моём случае — его, кстати, умеет опознавать proxy‑прошивка ПЛИС), то там найдётся конфигурационный регистр CR1 с битом QUAD. Он отвечает за включение QUAD‑режима, который необходим для того, чтобы ПЛИС смогла успешно загрузиться из FLASH.
Теперь, когда мы разобрались, что нужно сделать с FLASH, осталось понять — как. Согласно официальной документации OpenOCD, jtagspi‑драйвер умеет выполнять команду jtagspi cmd bank_id resp_num cmd_byte ..., с помощью которой можно посылать и принимать байты с FLASH по SPI. В итоге наш конфиг выглядит примерно так (для верификации добавил ещё и чтение регистра, который мы писали):
reset_config none source [find interface/ftdi/digilent_jtag_hs3.cfg] source [find cpld/xilinx-xc7.cfg] source [find cpld/jtagspi.cfg] # source [find target/stm32f4x.cfg] adapter speed 4000 init jtagspi_init 0 bscan_spi_xc7a200t.bit jtagspi_program test_jtag.bin 0x0 jtagspi cmd 0 0 0x06 jtagspi cmd 0 0 0x01 0x00 0x02 echo [jtagspi cmd 0 1 0x35] shutdown
И вот наконец мы запускаем OpenOCD, перезагружаем устройство и видим заветный статус Programmed.
Правда, и здесь есть несколько подводных камней. Начнём с того, что, если вы работаете на Ubuntu® 22, где по умолчанию из пакетного менеджера установлен OpenOCD 0.11, который вы не обновляли, этот скрипт упадёт с ошибкой, так как команда jtagspi ему неизвестна. С OpenOCD 0.12 скрипт работает корректно. Однако здесь нас ждёт ещё одно испытание: если поменять конфигурацию на TDI STM32 в TDO ПЛИС и попытаться прошиться, новый скрипт будет выглядеть так:
reset_config none source [find interface/ftdi/digilent_jtag_hs3.cfg] source [find target/stm32f4x.cfg] source [find cpld/xilinx-xc7.cfg] source [find cpld/jtagspi.cfg] adapter speed 4000 init jtagspi_init 0 bscan_spi_xc7a200t.bit jtagspi_program test_jtag.bin 0x0 jtagspi cmd 2 0 0x06 jtagspi cmd 2 0 0x01 0x00 0x02 echo [jtagspi cmd 2 1 0x35] shutdown
Обратите внимание: первый аргумент команды jtagspi cmd изменился на 2. Это индекс нужного банка FLASH. Его изменение связано с тем, что в системе добавились ещё два банка памяти от STM32 (список всех зарегистрированных банков можно посмотреть с помощью команды echo [flash banks]):
#0 : stm32f4x.flash (stm32f2x) at 0x00000000, size 0x00000000, buswidth 0, chipwidth 0 #1 : stm32f4x.otp (stm32f2x) at 0x1fff7800, size 0x00000000, buswidth 0, chipwidth 0 #2 : xc7.spi (jtagspi) at 0x00000000, size 0x02000000, buswidth 0, chipwidth 0
Этот скрипт не работает даже на версии 0.12: регистр не переходит в нужное состояние. Причину этой проблемы мне не удалось выяснить, зато я узнал, что баг уже исправлен в более свежих коммитах в основной ветке, для которых ещё не вышел официальный релиз.
Я проверял работу на коммите e6752ecbcf72efe4e213e8418e381ff2e0ffdf54 из репозитория. В нём есть подробная инструкция по сборке из исходников, а сообщения об ошибках в установочных скриптах довольно понятные. От себя добавлю только, что digilent_jtag_hs3-драйвер использует ftdi, который не включён в сборку по умолчанию, поэтому при конфигурации не забывайте указать нужный флаг (./configure --enable-ftdi). На этом коммите прошивка во FLASH работает в обеих конфигурациях.
Хотя OpenOCD справился со своей задачей, для прошивки ПЛИС хочется иметь инструмент попроще, который работает без сложных манипуляций с конфигурационными файлами. Посмотрим, насколько удобен в этом плане openFPGALoader.
OpenFPGALoader
OpenFPGALoader — это CLI‑утилита с открытым исходным кодом, которая предназначена для прошивки различных ПЛИС. В отличие от OpenOCD, здесь всё должно запускаться из коробки. Соберём утилиту из исходников и попробуем запустить простенькую команду
openFPGALoader --cable digilent_hs3 --detect
В ней указан кабель, то есть наш JTAG‑адаптер, а также флаг --detect, который позволит посмотреть найденные тапы. Однако выполнение завершится ошибкой:
JTAG init failed with Unknown device with IDCODE: 0x06421041 (manufacturer: 0x020 (), part: 0x32 vers: 0x0
Это говорит о том, что openFPGALoader не смог разобраться с периферией STM32 и ему нужна наша помощь. Флаг --misc-device <STM_ID_CODE>,<STM_IR_LEN>,stmperiph позволяет указать недостающую информацию о неизвестном тапе.
В моём случае STM_ID_CODE = 0x06421041, STM_IR_LEN = 5, значения я посмотрел в Vivado, они указаны в описании Unknown‑тапа. В итоге для TDO STM32 в TDI ПЛИС получаем такой результат:
$ openFPGALoader --cable digilent_hs3 --detect --misc-device 0x06421041,5,stmperiph Jtag frequency : requested 6.00MHz -> real 6.00MHz index 0: idcode 0x6421041 type stmperiph irlength 5 index 1: idcode 0x4ba00477 type ARM cortex A9 irlength 4 index 2: idcode 0x3636093 manufacturer xilinx family artix a7 200t model xc7a200 irlength 6
Теперь попробуем прошить FLASH‑память. Итоговая команда для первой конфигурации TDO STM32 в TDI ПЛИС выглядит так:
openFPGALoader --cable digilent_hs3 --write-flash --enable-quad --fpga-part xc7a200t --verify ~/test_jtag.bin --misc-device 0x06421041,5,stmperiph --index-chain 2
И... она не работает. Не буду держать интригу: заставить утилиту работать при таком порядке устройств в цепи у меня так и не получилось (проверял на коммите 63c519c3c57ded2d8cc412197d8b781115b1922a).
Чтобы заставить утилиту работать, нужно собрать цепь в конфигурацию TDI STM32 в TDO ПЛИС, индекс ПЛИС в цепи станет 0, и тогда команда openFPGALoader --cable digilent_hs3 --write-flash --enable-quad --fpga-part xc7a200t --verify ~/test_jtag.bin --misc-device 0x06421041,5,stmperiph --index-chain 0 прошьёт FLASH с таким логом:
write to flash Jtag frequency : requested 6.00MHz -> real 6.00MHz Open file /home/user/test_jtag.bin DONE Parse file DONE Use: /usr/local/share/openFPGALoader/spiOverJtag_xc7a200t.bit.gz load program Load SRAM: [==================================================] 100.00% Done Shift IR 35 ir: 1 isc_done 1 isc_ena 0 init 1 done 1 jtag_chain_len: 3 SOJ version: 2.000000 JEDEC ID: 0x010219 Detected: spansion S25FL256S 512 sectors size: 256Mb 00000000 00000000 00000000 00 start addr: 00000000, end_addr: 00170000 Erasing: [==================================================] 100.00% Done Writing: [==================================================] 100.00% Done Verifying write (May take time) Reading: [==================================================] 100.00% Done Use: /usr/local/share/openFPGALoader/spiOverJtag_xc7a200t.bit.gz load program Load SRAM: [==================================================] 100.00% Done Shift IR 35 ir: 1 isc_done 1 isc_ena 0 init 1 done 1 jtag_chain_len: 3 SOJ version: 2.000000 JEDEC ID: 0x010219 Detected: spansion S25FL256S 512 sectors size: 256Mb set_quad_bit: Done
Совместная отладка микроконтроллера и ПЛИС
Мы неплохо поработали — научились прошивать и отлаживать микроконтроллер и прошивать ПЛИС. Остался ещё один вопрос. Если микроконтроллер и ПЛИС работают в рамках одного устройства, то с высокой долей вероятности нам потребуется совместно их отлаживать. Как это сделать, если они в единой JTAG‑цепочке?
Объединение устройств в единую цепь накладывает свои условия: мы больше не можем использовать два независимых адаптера, нам нужен единый сервер, который будет общаться со всеми устройствами в цепи, а также осуществлять дистрибуцию сообщений на шине. В теории что‑то подобное может делать OpenOCD, но он не поддерживает инструментарий отладки ПЛИС (в основном речь идёт об ILA — внутреннем логическом анализаторе).
К счастью, помимо ПЛИС, Xilinx производит также SoC, например серию ZYNQ, где в едином устройстве также собраны ПЛИС и микроконтроллер, и, соответственно, предоставляет инструменты для отладки подобных устройств. И Vivado, которую мы уже неоднократно использовали, как раз такой инструмент: когда помимо ПЛИС она встречает в цепи ARM‑ядро, она подключается к нему и поднимает GDB‑сервер, что нам и нужно.
Проверяем отладку через CLI
Запустим Vivado и проверим работу сервера вручную. В консоли запускаем gdb-multiarch и подключаемся к серверу с помощью target extended-remote localhost:3000.
Моргающий светодиод из тестовой прошивки, оставшейся на плате от последних экспериментов, замирает в одном из положений. Вводим в терминал continue, после чего светодиод снова моргает.
В ходе этого процесса мы выясним, что группа команд monitor, с помощью которой, например, делается перезагрузка контроллера в случае обычной отладки, урезана. Команда run сообщает, что она не может убить процесс, чтобы перезагрузиться. Однако можно ставить точки останова и ходить по шагам, и если бы мы подтянули elf‑файл прошивки, то могли бы всё это посмотреть.
Настройка отладки в VS Code
Перейдём к более привычному мне формату — к отладке в VS Code. В целом нужно просто повторить то, что мы уже сделали, но с помощью расширения на C++. Напишем вот такой таргет в launch‑файл:
{ "name": "Remote gdbserver :3000 (multiarch-gdb)", "type": "cppdbg", "request": "launch", "program": "JTAG_test.elf", // Path to elf "cwd": "${workspaceFolder}", "MIMode": "gdb", "miDebuggerPath": "/usr/bin/gdb-multiarch", "miDebuggerServerAddress": "localhost:3000", "stopAtEntry": false, }
Запускаем отладку на уже прошитую STM и подключаемся к исполняющейся программе, где мы можем ходить по шагам, ставить точки останова и в целом пользоваться обычным набором инструментов, кроме перезагрузки чипа и его прошивки, — способа сделать это с помощью данного сервера я не нашёл. Предполагаю, это связано с тем, что сервер видит только ARM‑ядро, а о существовании FLASH и всего остального он просто не знает.
Заключение
В этой статье я постарался обобщить опыт, полученный в ходе исследования нестандартной JTAG‑цепочки, и собрать в одно руководство то, что обычно приходится искать по разрозненным даташитам и коммитам.
Краткое резюме всей этой истории:
OpenOCD достаточно спокойно чувствует себя в цепочке и свой функционал выполняет свободно.
OpenFPGALoader может подкинуть сюрпризов, однако и его удалось заставить работать в каком‑то варианте.
Vivado работает без проблем и даже открывает новые возможности — отладку микроконтроллеров.
Объединение ПЛИС и микроконтроллера в общую цепочку практически не ущемляет наши возможности прошивки и отладки.
Надеюсь, эта информация и готовые конфигурационные файлы сэкономят вам время при разработке и прошивке собственных устройств.
Если вы сталкивались с подобными JTAG‑цепями, решали другими способами вставшие перед нами проблемы или используете альтернативный софт для тех же целей — поделитесь опытом в комментариях!
Комментарии (7)

IvUyr
03.11.2026 07:57А старый добрый народный J-Link не пробовали с комплектным софтом для такого? В процессе вялой разработки у меня девайс, мелая наколенная серия, там связка из STM32, FPGA Altera (и ещё несколько микросхем), и думал как раз, что неплохо бы собрать их в TAP-цепочку и шить сразу и то и то, плюс отладка. У меня есть конечно и USB-Blaster и ST-Link 2.1 самодельный, но не хочется плодить количество “служебных” портов, даже пришлось доказывать необходимость в отладочном порту (свой собственный интерфейс и протокол, не uart или что-то подобное).

old_bear
03.11.2026 07:57Если вы сталкивались с подобными JTAG‑цепями, решали другими способами вставшие перед нами проблемы или используете альтернативный софт для тех же целей — поделитесь опытом в комментариях!
Как как.
Читаешь спек на JTAG. Куришь маны Xilinx-а. Соединяешь несколько десятков FPGA большой промышленной вундервафли в цепочку. Подключаешь эту цепочку к EZ-USB от тогда ещё Cypress-а. Пишешь на С/asm-е прошивку для этого EZ-USB чтобы дёргать JTAG по командам с хоста, пишешь приложуху для хоста чтобы слать команды и данные. Проводишь пару увлекательных недель тыкаясь логическим анализатором в разные места... Profit!
nerudo
Не сильно ли ограничивает ли STM частоту jtag по сравнению с fpga? Вообще, насколько мне видно, для целей отладки стараются делать отдельно JTAG на FPGA и SWD на микроконтроллерах, чтобы инструменты не зависели друг от друга.
SeaJackal Автор
Про частоту JTAG интересный вопрос. В рамках этой работы частоту не анализировал, если честно, заводил всё на 4 МГц. У меня есть подозрение, что для тестового стенда ограничителем частоты будет соединительный кабель, а не конечные устройства, но это просто предположение. Касательно отдельных JTAG и SWD: для отладок и ранних стадий цикла разработки - полностью согласен, а вот для серийных решений кажется, что удобнее сделать весь setup устройства через один разъём и там же сделать тестирование, о котором я писал в начале, в него захочется добавить как можно больше устройств (JTAG есть много у каких микросхем помимо ПЛИС и STM), не добавляя при этом новых разъёмов
nerudo
Нюанс в том, что jtag был создан во времена БИС, которые связывались друг с другом широкими (по тем временам), но относительно медленными шинами. Ну и вполне разумно придумали способ тестирования. Сейчас же таких межмикросхемных шин практически не осталось. Уверен, что ваш stm подключен к FPGA проводами пятью-шестью: uart, i2c/spi. Может быть несколько gpio. Тестировать особо нечего. Большие микросхемы соединяются гигабитными линками и там jtag'а нет уже по техническим причинам. Казалось бы, есть шины памяти, но вот сюрприз - на них jtag не делают тоже, видимо надежнее проверять штатными тестами памяти.