Каждый электронщик, работающий (или отдыхающий) с цифровыми микросхемами рано или поздно обязательно сталкивается с протоколом JTAG. Значительное количество материалов о данном протоколе содержит три раздела:

  1. Обширный экскурс в историю и рассказ о том, как стенд с летающими щупами и рентгеновская установка легко могут быть заменены отладчиком на 2-3 порядка дешевле их.

  2. Достаточно сжатое описание протокола JTAG (с картинкой его конечного автомата).

  3. Рассказ о том, что фирменный отладчик, а также программное обеспечение компании <COMPANY NAME> позволят почти без усилий протестировать почти любое устройство почти любой сложности и конфигурации.

Проблема второго раздела подобных материалов в том, что в протоколе JTAG можно выделить более одного уровня абстракции. И за один раз, причём без практических примеров, осознать JTAG целиком может быть весьма непросто. В отсутствие же целостного понимания, для инженера, использующего JTAG, данный протокол будет представлять чёрный ящик, который либо работает (и всё хорошо), либо не работает (и непонятно, что делать). Представляется, что, исключив из рассмотрения историю JTAG и рекламу бренда, а также разделив задачу на отдельные составляющие, возможно осознать JTAG полностью, обретя способность устранять проблемы в работе данного протокола на любом уровне. И первым шагом к такому пониманию будет чтение идентификационного номера микросхемы.

Итак. Любая микросхема с модулем JTAG в обязательном порядке имеет идентификационный номер. Попробуем считать его. Для этого нам потребуется:

  1. Микросхема с JTAG. Есть незначительное количество цифровых микросхем (в основном таким страдают процессоры), у которых JTAG не работает, пока ядро процессора не затактировано. Однако большинство микросхем, у которых вы сможете как-либо присоединиться к линиям TMS, TCK, TDI, TDO (если есть nTRST, то и к нему) подойдёт для данного исследования без дополнительных ограничений. У меня это будет небольшая ПЛИС «5M80ZE64C5N», знакомая некоторым читателям по предыдущему циклу.

  2. Программное обеспечение. Я буду рассказывать про «TopJTAG Probe». Он не перегружен высокоуровневым функционалом и имеет триальную версию.

  3. Отладчик JTAG. «TopJTAG Probe» позволяет использовать порядка 10 различных и достаточно распространённых отладчиков. Данный список несколько отличается от одного ПО к другому. Но базово, во многих пакетах, работающих с JTAG-ом, предусмотрена работа с микросхемой «FT2232». Эта популярная микросхема — мост между USB и широкой номенклатурой протоколов, в том числе и JTAG. В данном цикле я буду использовать отладчик на основе микросхемы «FT2232» и отладчик «Intel(Altera) USB-Blaster».

Соединим микросхему с отладчиком JTAG и подадим питание. Делая это, обратим внимание на несколько ключевых моментов:

  • В ряде демоплат соединение с отладчиком выполнено в виде разъёма с ключом. Если у вас так — то всё отлично. Если нет — придётся соединять порт JTAG с отладчиком отдельными проводами.

  • Если у порта JTAG микросхемы имеется линия nTRST (она может и отсутствовать), то для работы с JTAG её следует подтянуть к питанию.

  • Зачастую питание выходных трансляторов уровней отладчика JTAG выводят на отдельный контакт разъёма отладчика. Таким образом появляется возможность объединить шину питания микросхемы с шиной питания трансляторов. И при отсутствующем питание микросхемы гарантировать отсутствие напряжения на линиях JTAG, а при наличие питания — гарантировать одинаковый уровень напряжения сигналов на всех линиях интерфейса. Поэтому, помимо линий TMS/TCK/TDI/TDO/nTRST и земли, к разъёму отладчика скорее всего потребуется подключить и линию питания.

Считаем при помощи «TopJTAG Probe» идентификационный номер. Для этого:

  1. Выберем пункт меню «Scan → JTAG Connection...»

  2. В открывшемся окне выберем из списка подходящий программатор.

  3. Если список доступных программаторов данной модели неактивен (допустим, мы подключили программатор после запуска ПО), то его следует обновить.

  4. Выберем пункт меню «Scan→Examine the Chain»

  5. В открывшемся окне появится идентификационный номер микросхемы. Для «5M80ZE64C5N» это будет h020A50DD.

Разберёмся, что есть что в этом номере. Согласно стандарту JTAG, в начале номера всегда должна стоять единица. Затем идут 11 бит кода производителя. Коды производителей указаны в таблицах стандарта JEP106 «Standard Manufacturer’s Identification Code». В стандарте есть 8 таблиц, по 256 строк каждая. «Altera», к примеру, находится в первой (b000) таблице. Похоже, что JEDEC убрал публичный доступ для этого стандарта, но его старые версии имеются на сторонних ресурсах. Оставшаяся часть номера содержит код конкретной марки микросхемы. Строго говоря, верхние 4 бита оставшейся части указывают на версию, но для подавляющего большинства микросхем в них стоят нули. Данный код марки указывается в технической документации на микросхему. В нашем случае — это стр.97 документа «MAX V Device Handbook».

Как же именно происходит передача идентификационного номера и какую именно последовательность сигналов необходимо подать на линии JTAG, чтобы считать этот номер?

Модуль JTAG внутри микросхемы на базовом уровне представляет из себя конечный автомат — систему, которая имеет 16 состояний. Переход между состояниями может произойти при восходящем фронте на линии TCK. А совершится ли он из одного конкретного состояния в другое, либо в третье, либо перехода не будет и система останется в прежнем состоянии — определяется логическим состоянием линии TMS в момент прохождения по линии TCK восходящего фронта, текущим состоянием автомата и схемой переходов.

Переходы между состояниями конечного автомата JTAG
Переходы между состояниями конечного автомата JTAG

Не углубляясь сейчас в детали, отметим три аспекта:

  1. Конечный автомат JTAG сделан таким образом, что из любого состояния возможно перейти в состояние «TEST LOGIC RESET», если пять раз подряд передать лог.1 по линии TMS (подавая при этом тактирующие импульсы по линии TCK). Выберете наугад состояние на схеме и посчитайте количество переходов по веткам с TMS=1 до состояния «TEST LOGIC RESET» :)

  2. Если модуль JTAG и был определённым образом настроен (каким — пока не важно), то переход в состояние «TEST LOGIC RESET» сбрасывает все настройки модуля в начальное состояние.

  3. Начальное состояние предусматривает готовность модуля JTAG выдать идентификационный номер.

Для старта передачи идентификационного номера нам нужно перевести конечный автомат в состояние «SHIFT DR» (до перехода в «SHIFT DR» модуль JTAG будет держать линию TDO в высокоимпедансном состоянии). Затем нужно удерживать конечный автомат в состоянии «SHIFT DR» 32 (или более) тактов TCK. В течение этих тактов идентификационный номер бит-за-битом будет передаваться по линии TDO из специального 32-х битного регистра модуля JTAG. При этом, в этот регистр бит-за-битом будут вдвигаться данные с линии TDI. Если мы с первого тактирующего импульса в состоянии «SHIFT DR» начнём последовательно подавать некий характерный паттерн по линии TDI, то через 32 импульса данный паттерн начнёт выходить через линию TDO.

Имеется тонкий момент, связанный с фронтами тактового сигнала, а также совместной работой модуля JTAG и отладчика. По стандарту, модуль JTAG считывает данные с линий TMS и TDI при восходящем фронте на линии TCK. Соответственно, отладчик должен заранее установить эти значения, а именно — на предшествующем нисходящем фронте TCK. Но так как микросхемы с JTAG могут собираться в цепочку, и каждая микросхема в цепи не знает, подключен ли её вход TDI непосредственно к отладчику или же к выходу TDO другой микросхемы, то модуль JTAG в любой микросхеме также обязан изменять логическое значение на линии TDO по нисходящему фронту TCK.

В связи с вышенаписанным, последовательность событий при начале выдачи данных из внутреннего регистра модуля JTAG в выход TDO будет такой:

  1. Очередной восходящий фронт по TCK приводит к изменению состояния конечного автомата на «SHIFT XX» внутри модуля JTAG.

  2. Синхронно со следующим нисходящим фронтом TCK модуль JTAG выдаст (скопирует) младший бит регистра в линию TDO. Одновременно с этим отладчик JTAG обязан выставить на линии TDI модуля JTAG бит, который предполагается поместить в регистр.

  3. Следующий восходящий фронт (если на TMS был установлен лог.0) приведёт к сдвигу регистра и «втягиванию» в него значения с TDI. Но так как значение младшего бита регистра уже выведено на линию TDO, то на этой стадии при сдвиге младший бит регистра просто исчезает.

Можно было бы взять микроконтроллер, присоединить его к некой тестовой микросхеме, подать с микроконтроллера необходимую последовательность сигналов и посмотреть осциллографом/логическим анализатором/самим микроконтроллером/светодиодом, как по линии TDO из тестовой микросхемы выйдет её идентификационный номер.

Но мы поступим иначе, вернее — совсем наоборот! Попробуем написать такую программу, которая позволила бы микроконтроллеру имитировать устройство с модулем JTAG. А затем считаем при помощи отладчика JTAG и «TopJTAG Probe» идентификационный номер, который мы сами и зададим.

В качестве стенда будет использована плата «NUCLEO-F103RB» с микроконтроллером «STM32F103RBT6» и встроенным программатором «ST-Link».Сам проект написан в среде «STM32CubeIDE-1.7.0». А вся логика будет написана непосредственно в бесконечном цикле «while(1)» с использованием «bit-bang» («ногодрыга»).

Цвета навесных проводов соответствуют цветам линий JTAG с предыдущей картинки
Цвета навесных проводов соответствуют цветам линий JTAG с предыдущей картинки

Возможно, на этом месте у некоторых читателей возникла пара мыслей:

  1. «Пффф! Такие вещи надо делать на ПЛИС!»

  2. «Пффф! Такие вещи надо делать на прерываниях!»

По поводу первого комментария отмечу, что все инженеры, которым может пригодиться знание JTAG, знают «C/C++». Но не все из них также знают «Verilog/VHDL». Было бы неразумной дискриминацией большинства — не предоставить ему инструмент, которым оно привыкло пользоваться.

По поводу второго комментария отмечу, что не у всех читателей под рукой имеется именно «STM32F103RBT6». Вместо него может оказаться, к примеру, Ардуино. При портировании данного проекта на другое ядро, базовая настройка среды и проекта будет подразумевать лишь запуск примера «blinking led». Если же применять прерывания или какой-либо аппаратный блок периферии, то это создаст абсолютно ненужную (ввиду того, что подобные вещи действительно следует «делать на ПЛИС») привязку к конкретному микроконтроллеру. И, забегая вперёд, в данной статье после написания примера на «Си», будет продемонстрировано, как данный пример переписать на «SystemVerilog».

Прежде всего мы определим задействуемые линии GPIO. Для этого в начале файла «main.c» (также можно и в «main.h») запишем четыре директивы «define»:

#define JTMS_PIN GPIO_PIN_1
#define JTCK_PIN GPIO_PIN_15
#define JTDI_PIN GPIO_PIN_14
#define JTDO_PIN GPIO_PIN_13

Затем нам потребуется сконфигурировать данные выводы должным образом. Для этого нужно дописать в функцию «MX_GPIO_Init» следующий код:

Код инициализации GPIO для STM32
GPIO_InitStruct.Pin = JTMS_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

GPIO_InitStruct.Pin = JTCK_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

GPIO_InitStruct.Pin = JTDI_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

GPIO_InitStruct.Pin = JTDO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

Теперь перейдём непосредственно к логике программы. Добавим в функцию «main» перечисление состояний конечного автомата («finite state machine» — FSM) модуля JTAG, просто переписав их из схемы:

enum Jtag_fsm{
	TEST_LOGIC_RESET,
	RUN_TEST_IDLE,

	SELECT_DR_SCAN,
	CAPTURE_DR,
	SHIFT_DR,
	EXIT1_DR,
	PAUSE_DR,
	EXIT2_DR,
	UPDATE_DR,

	SELECT_IR_SCAN,
	CAPTURE_IR,
	SHIFT_IR,
	EXIT1_IR,
	PAUSE_IR,
	EXIT2_IR,
	UPDATE_IR
} jtag_fsm = TEST_LOGIC_RESET;

Затем добавим переменные, отвечающие за состояние того или иного вывода:

char tms_curr = 1;
char tck_curr = 0;
char tdi_curr = 0;
char tdo_curr = 0;

Добавим в бесконечный цикл код, позволяющий оперативно узнавать состояние на линиях TMS/TCK/TDI через соответствующие переменные и столь же оперативно выдавать на линию TDO значение переменной «tdo_curr»:

while (1)
{
	tms_curr = (GPIOB->IDR & JTMS_PIN ) ? 1 : 0;
	tck_curr = (GPIOB->IDR & JTCK_PIN)  ? 1 : 0;
	tdi_curr = (GPIOB->IDR & JTDI_PIN)  ? 1 : 0;
	if(tdo_curr)	GPIOB->ODR |=  JTDO_PIN;
	else			GPIOB->ODR &= ~JTDO_PIN;
}

В идеале, для TDO можно написать функцию, обеспечивающую (при необходимости) перевод данной линии в высокоимпедансное состояние. Но в первом приближении будет достаточно и приведённого кода.

Добавим в бесконечный цикл конструкцию, позволяющую обнаружить фронты на линии TCK. Для этого нам потребуется дополнительная переменная «tck_prev» (её нужно добавить к прочим переменным), содержащая значение «tck_curr» с предыдущей итерации бесконечного цикла:

if(tck_curr != tck_prev)
{
	if(tck_curr)
	{
		//Реакция на восходящий фронт TCK
	}
	else 
	{
		//Реакция на нисходящий фронт TCK
	}
}
tck_prev = tck_curr;

Опишем переходы конечного автомата при помощи конструкции «switch-case», помещённой в ветвь восходящего фронта TCK. В нашем случае при помощи «switch» мы будет находить текущее состояние конечного автомата. Найдя его в определённом «case», мы будем однократно изменять (ну, либо оставим прежним) состояние конечного автомата в зависимости от состояния на линии TMS, то есть в зависимости от значения переменной «tms_curr»:

switch(jtag_fsm){
case TEST_LOGIC_RESET:
	if(tms_curr)	jtag_fsm = TEST_LOGIC_RESET;
	else			jtag_fsm = RUN_TEST_IDLE;
	break;
<...>
case UPDATE_IR:
	if(tms_curr)	jtag_fsm = SELECT_DR_SCAN;
	else			jtag_fsm = RUN_TEST_IDLE;
	break;
}
Полный код всех переходов
switch(jtag_fsm){
case TEST_LOGIC_RESET:
	if(tms_curr)	jtag_fsm = TEST_LOGIC_RESET;
	else			jtag_fsm = RUN_TEST_IDLE;
	break;
case RUN_TEST_IDLE:
	if(tms_curr)	jtag_fsm = SELECT_DR_SCAN;
	else			jtag_fsm = RUN_TEST_IDLE;
	break;
case SELECT_DR_SCAN:
	if(tms_curr)	jtag_fsm = SELECT_IR_SCAN;
	else			jtag_fsm = CAPTURE_DR;
	break;
case CAPTURE_DR:
	if(tms_curr)	jtag_fsm = EXIT1_DR;
	else			jtag_fsm = SHIFT_DR;
	break;
case SHIFT_DR:
	if(tms_curr)	jtag_fsm = EXIT1_DR;
	else			jtag_fsm = SHIFT_DR;
	break;
case EXIT1_DR:
	if(tms_curr)	jtag_fsm = UPDATE_DR;
	else			jtag_fsm = PAUSE_DR;
	break;
case PAUSE_DR:
	if(tms_curr)	jtag_fsm = EXIT2_DR;
	else			jtag_fsm = PAUSE_DR;
	break;
case EXIT2_DR:
	if(tms_curr)	jtag_fsm = UPDATE_DR;
	else			jtag_fsm = SHIFT_DR;
	break;
case UPDATE_DR:
	if(tms_curr)	jtag_fsm = SELECT_DR_SCAN;
	else			jtag_fsm = RUN_TEST_IDLE;
	break;
case SELECT_IR_SCAN:
	if(tms_curr)	jtag_fsm = TEST_LOGIC_RESET;
	else			jtag_fsm = CAPTURE_IR;
	break;
case CAPTURE_IR:
	if(tms_curr)	jtag_fsm = EXIT1_IR;
	else			jtag_fsm = SHIFT_IR;
	break;
case SHIFT_IR:
	if(tms_curr)	jtag_fsm = EXIT1_IR;
	else			jtag_fsm = SHIFT_IR;
	break;
case EXIT1_IR:
	if(tms_curr)	jtag_fsm = UPDATE_IR;
	else			jtag_fsm = PAUSE_IR;
	break;
case PAUSE_IR:
	if(tms_curr)	jtag_fsm = EXIT2_IR;
	else			jtag_fsm = PAUSE_IR;
	break;
case EXIT2_IR:
	if(tms_curr)	jtag_fsm = UPDATE_IR;
	else			jtag_fsm = SHIFT_IR;
	break;
case UPDATE_IR:
	if(tms_curr)	jtag_fsm = SELECT_DR_SCAN;
	else			jtag_fsm = RUN_TEST_IDLE;
	break;
}

Для операций с идентификационным номером нам потребуется 32-битная константа с данным номером и 32-битная переменная (регистр):

const uint32_t ID_CODE = 0x0AA55003;//Придуманный нами номер
uint32_t data_reg;

Добавим в ветвь восходящего фронта ещё одну конструкцию «switch-case» с выбором по jtag_fsm, в которой опишем логику работы регистров. Эту новую конструкцию необходимо вставить перед конструкцией «switch-case», отвечающей за переходы состояний конечного автомата. В новом «switch-case» создадим ветвь «TEST LOGIC RESET» в которой будем производить инициализацию регистров всякий раз, когда конечный автомат попадает в данное состояние:

switch(jtag_fsm){
case TEST_LOGIC_RESET:
	data_reg = ID_CODE;
  break;
}

Также добавим в этот «switch-case» сдвиг регистра в состоянии «SHIFT DR»:

case SHIFT_DR:
	data_reg = (data_reg >> 1) | ((uint32_t)tdi_curr << 31);
	break;

Теперь опишем логику работы с выходом TDO. Для этого создадим «switch-case» в ветке нисходящего фронта:

switch(jtag_fsm){
case TEST_LOGIC_RESET:
	tdo_curr = 0;
	break;
case SHIFT_DR:
	tdo_curr = data_reg & ((uint32_t)0x1);
	break;
case EXIT1_DR:
	tdo_curr = 0;
	break;
}

В этом «switch-case», в ветке «SHIFT DR», происходит выдача младшего бита регистра в линию TDO. Линия TDO, по-хорошему должна переходить в высокоимпедансное состояние (HiZ) всегда, когда конечный автомат не пребывает в «SHIFT XX». Так как единственный выход из «SHIFT DR» — это «EXIT1 DR», то здесь и следует переключить линию в HiZ. Но ввиду того, что функционал высокоимпеданмного состояния TDO у нас не предусмотрен, оставим здесь некий компромиссный вариант в виде перевода линии в лог.0. Также следует на всякий случай перевести линию TDO в HiZ (у нас — в лог.0) при сбросе в состояние «TEST LOGIC RESET».

Мы обеспечили выдачу идентификационного номера через регистр данных — «Data Register» («DR»). Но программа «TopJTAG Probe» при идентификации проверяет также и наличие регистра инструкций — «Instruction Register» («IR»). Что это за регистр и зачем он нужен — пока не важно. На текущем этапе нам просто нужна ещё одна переменная-регистр длинной, скажем, 8 бит, инициализируемая нулём и способная к пробросу через себя данных с TDI в TDO. Для этого нужно объявить данную переменную:

uint8_t  inst_reg;

А затем дополнить конечные автоматы в ветке восходящего...

switch(jtag_fsm){
case TEST_LOGIC_RESET:
	data_reg = ID_CODE;
	inst_reg = 0;
	break;
case SHIFT_DR:
	data_reg = (data_reg >> 1) | ((uint32_t)tdi_curr << 31);
	break;
case SHIFT_IR:
	inst_reg = (inst_reg >> 1) | ((uint8_t)tdi_curr << 7);
	break;
}

...и нисходящего фронта

switch(jtag_fsm){
case TEST_LOGIC_RESET:
	tdo_curr = 0;
	break;
case SHIFT_DR:
	tdo_curr = data_reg & ((uint32_t)0x1);
	break;
case EXIT1_DR:
	tdo_curr = 0;
	break;
case SHIFT_IR:
	tdo_curr = inst_reg & ((uint8_t)0x1);
	break;
case EXIT1_IR:
	tdo_curr = 0;
	break;
}

Всё! Наш симулятор JTAG готов!

Полный код «main.c»
#include "main.h"

#define JTMS_PIN GPIO_PIN_1
#define JTCK_PIN GPIO_PIN_15
#define JTDI_PIN GPIO_PIN_14
#define JTDO_PIN GPIO_PIN_13

void SystemClock_Config(void);
static void MX_GPIO_Init(void);

int main(void)
{
	const uint32_t ID_CODE = 0x0AA55003;//Придуманный нами номер
	uint32_t data_reg;
	uint8_t  inst_reg;

	char tms_curr = 1;
	char tck_curr = 0;
	char tck_prev = 0;
	char tdi_curr = 0;
	char tdo_curr = 0;

	enum Jtag_fsm{
		TEST_LOGIC_RESET,
		RUN_TEST_IDLE,

		SELECT_DR_SCAN,
		CAPTURE_DR,
		SHIFT_DR,
		EXIT1_DR,
		PAUSE_DR,
		EXIT2_DR,
		UPDATE_DR,

		SELECT_IR_SCAN,
		CAPTURE_IR,
		SHIFT_IR,
		EXIT1_IR,
		PAUSE_IR,
		EXIT2_IR,
		UPDATE_IR
	} jtag_fsm = TEST_LOGIC_RESET;

	HAL_Init();
	SystemClock_Config();
	MX_GPIO_Init();

	while (1)
	{
		tms_curr = (GPIOB->IDR & JTMS_PIN ) ? 1 : 0;
		tck_curr = (GPIOB->IDR & JTCK_PIN)  ? 1 : 0;
		tdi_curr = (GPIOB->IDR & JTDI_PIN)  ? 1 : 0;
		if(tdo_curr)	GPIOB->ODR |=  JTDO_PIN;
		else					GPIOB->ODR &= ~JTDO_PIN;

		if(tck_curr != tck_prev)
		{
			if(tck_curr)
			{
				switch(jtag_fsm){
				case TEST_LOGIC_RESET:
					data_reg = ID_CODE;
					inst_reg = 0;
					break;
				case SHIFT_DR:
					data_reg = (data_reg >> 1) | ((uint32_t)tdi_curr << 31);
					break;
				case SHIFT_IR:
					inst_reg = (inst_reg >> 1) | ((uint8_t)tdi_curr << 7);
					break;
				}

				switch(jtag_fsm){
				case TEST_LOGIC_RESET:
					if(tms_curr)	jtag_fsm = TEST_LOGIC_RESET;
					else			jtag_fsm = RUN_TEST_IDLE;
					break;
				case RUN_TEST_IDLE:
					if(tms_curr)	jtag_fsm = SELECT_DR_SCAN;
					else			jtag_fsm = RUN_TEST_IDLE;
					break;
				case SELECT_DR_SCAN:
					if(tms_curr)	jtag_fsm = SELECT_IR_SCAN;
					else			jtag_fsm = CAPTURE_DR;
					break;
				case CAPTURE_DR:
					if(tms_curr)	jtag_fsm = EXIT1_DR;
					else			jtag_fsm = SHIFT_DR;
					break;
				case SHIFT_DR:
					if(tms_curr)	jtag_fsm = EXIT1_DR;
					else			jtag_fsm = SHIFT_DR;
					break;
				case EXIT1_DR:
					if(tms_curr)	jtag_fsm = UPDATE_DR;
					else			jtag_fsm = PAUSE_DR;
					break;
				case PAUSE_DR:
					if(tms_curr)	jtag_fsm = EXIT2_DR;
					else			jtag_fsm = PAUSE_DR;
					break;
				case EXIT2_DR:
					if(tms_curr)	jtag_fsm = UPDATE_DR;
					else			jtag_fsm = SHIFT_DR;
					break;
				case UPDATE_DR:
					if(tms_curr)	jtag_fsm = SELECT_DR_SCAN;
					else			jtag_fsm = RUN_TEST_IDLE;
					break;
				case SELECT_IR_SCAN:
					if(tms_curr)	jtag_fsm = TEST_LOGIC_RESET;
					else			jtag_fsm = CAPTURE_IR;
					break;
				case CAPTURE_IR:
					if(tms_curr)	jtag_fsm = EXIT1_IR;
					else			jtag_fsm = SHIFT_IR;
					break;
				case SHIFT_IR:
					if(tms_curr)	jtag_fsm = EXIT1_IR;
					else			jtag_fsm = SHIFT_IR;
					break;
				case EXIT1_IR:
					if(tms_curr)	jtag_fsm = UPDATE_IR;
					else			jtag_fsm = PAUSE_IR;
					break;
				case PAUSE_IR:
					if(tms_curr)	jtag_fsm = EXIT2_IR;
					else			jtag_fsm = PAUSE_IR;
					break;
				case EXIT2_IR:
					if(tms_curr)	jtag_fsm = UPDATE_IR;
					else			jtag_fsm = SHIFT_IR;
					break;
				case UPDATE_IR:
					if(tms_curr)	jtag_fsm = SELECT_DR_SCAN;
					else			jtag_fsm = RUN_TEST_IDLE;
					break;
				}
		}
		else
		{
			switch(jtag_fsm){
			case TEST_LOGIC_RESET:
				tdo_curr = 0;
				break;
			case SHIFT_DR:
				tdo_curr = data_reg & ((uint32_t)0x1);
				break;
			case EXIT1_DR:
				tdo_curr = 0;
				break;
			case SHIFT_IR:
				tdo_curr = inst_reg & ((uint8_t)0x1);
				break;
			case EXIT1_IR:
				tdo_curr = 0;
				break;
			}
		}
	}
	if(tdo_curr)	GPIOB->ODR |=  JTDO_PIN;
	else			GPIOB->ODR &= ~JTDO_PIN;
	tck_prev = tck_curr;
  }
}


void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL16;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

void Error_Handler(void)
{
  __disable_irq();
  while (1)
  {
  }
}

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);

  GPIO_InitStruct.Pin = B1_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = GPIO_PIN_5;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);


  //==== JTAG PINS INIT BEGIN ====
  GPIO_InitStruct.Pin = JTMS_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = JTCK_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = JTDI_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = JTDO_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
  //==== JTAG PINS INIT END ====

}

Есть, правда, одна существенная проблема. Частота синхроимпульсов по линии TCK с использованием отладчика JTAG «Intel(Altera) USB-Blaster» и ПО «TopJTAG Probe» составляет 6,25МГц. То есть период следования фронтов синхроимпульсов составляет 0,16мкс, а время между восходящим и нисходящим фронтом составляет 0,08мкс. При этом, задержка от фронта по TCK до выдачи данных с TDO для «NUCLEO-F103RB» и написанной выше программы составляет порядка 1мкс.

Однако, при использовании в «TopJTAG Probe» отладчика на основе микросхемы «FT2232H» появляется возможность ограничить максимальную частоту синхроимпульсов, скажем, десятью килогерцами, для которых период составляет гигантские 100мкс.

Скомпилируем получившуюся программу, запишем прошивку в микроконтроллер, соединим навесными проводами отладчик JTAG с демонстрационной платой. И наконец считаем код при помощи «TopJTAG Probe»:

Теперь попробуем сделать тоже самое, только на «SystemVerilog» для ПЛИС. Я использую демоплату «DE0-Nano» на базе ПЛИС «Cyclone IV» («EP4CE22F17C6N»).

Для начала создадим модуль верхнего уровня с тремя входами и одним выходом:

module main (
	input		tms,
	input		tck,
	input		tdi,
	output reg	tdo);
		//Логика
endmodule

После сборки проекта в планировщике выводов появится список из четырёх строк с названиями выводов модуля и возможностью их назначения выводам микросхемы ПЛИС. Распределив выводы, создадим в модуле параметр (нечто вроде константы в терминологии «SystemVerilog») и два регистра (нечто вроде переменных в терминологии «SystemVerilog»). В «SystemVerilog» при объявлении регистра, содержащего более чем один бит, необходимо явно указать общее количество бит. Установим такие же размеры регистров, как в примере на «Си»:

parameter ID_CODE	= 32'h0AA55003;
reg	[31:0]data_reg;
reg	[ 7:0]inst_reg;

Затем создадим перечисление со списком состояний конечного автомата (почти как в «Си»):

enum {TEST_LOGIC_RESET,
		RUN_TEST_IDLE,

		SELECT_DR_SCAN,
		CAPTURE_DR,
		SHIFT_DR,
		EXIT1_DR,
		PAUSE_DR,
		EXIT2_DR,
		UPDATE_DR,

		SELECT_IR_SCAN,
		CAPTURE_IR,
		SHIFT_IR,
		EXIT1_IR,
		PAUSE_IR,
		EXIT2_IR,
		UPDATE_IR
} jtag_fsm = TEST_LOGIC_RESET;

Для написания реакций на изменения уровней отдельных выводов в «SystemVerilog» применяется специальный блок «always». Создадим такой блок:

always @(posedge tck) begin
	//Логика
end

Та логика, что будет описана внутри данного блока, будет однократно срабатывать каждый раз, когда по линии TCK в ПЛИС будет приходить восходящий фронт. Добавим в данный блок структуру «case»:

always @(posedge tck) begin
	case(jtag_fsm)
		XXX:begin
			if(tms)	jtag_fsm = YYY;
			else		jtag_fsm = ZZZ;
		end
	endcase
end

Как видно, синтаксическая конструкция «case» языка «SystemVerilog» позволяет в данном случае перенести код с «Си» на «SystemVerilog» практически без изменений:

Полный код структуры «case», описывающий логику переходов конечного автомата
case(jtag_fsm)
	TEST_LOGIC_RESET:begin
		if(tms)	jtag_fsm = TEST_LOGIC_RESET;
		else		jtag_fsm = RUN_TEST_IDLE;
	end
	RUN_TEST_IDLE:begin
		if(tms)	jtag_fsm = SELECT_DR_SCAN;
		else		jtag_fsm = RUN_TEST_IDLE;
	end
	SELECT_DR_SCAN:begin
		if(tms)	jtag_fsm = SELECT_IR_SCAN;
		else		jtag_fsm = CAPTURE_DR;
	end
	CAPTURE_DR:begin
		if(tms)	jtag_fsm = EXIT1_DR;
		else		jtag_fsm = SHIFT_DR;
	end
	SHIFT_DR:begin
		if(tms)	jtag_fsm = EXIT1_DR;
		else		jtag_fsm = SHIFT_DR;
	end
	EXIT1_DR:begin
		if(tms)	jtag_fsm = UPDATE_DR;
		else		jtag_fsm = PAUSE_DR;
	end
	PAUSE_DR:begin
		if(tms)	jtag_fsm = EXIT2_DR;
		else		jtag_fsm = PAUSE_DR;
	end
	EXIT2_DR:begin
		if(tms)	jtag_fsm = UPDATE_DR;
		else		jtag_fsm = SHIFT_DR;
	end
	UPDATE_DR:begin
		if(tms)	jtag_fsm = SELECT_DR_SCAN;
		else		jtag_fsm = RUN_TEST_IDLE;
	end
	SELECT_IR_SCAN:begin
		if(tms)	jtag_fsm = TEST_LOGIC_RESET;
		else		jtag_fsm = CAPTURE_IR;
	end
	CAPTURE_IR:begin
		if(tms)	jtag_fsm = EXIT1_IR;
		else		jtag_fsm = SHIFT_IR;
	end
	SHIFT_IR:begin
		if(tms)	jtag_fsm = EXIT1_IR;
		else		jtag_fsm = SHIFT_IR;
	end
	EXIT1_IR:begin
		if(tms)	jtag_fsm = UPDATE_IR;
		else		jtag_fsm = PAUSE_IR;
	end
	PAUSE_IR:begin
		if(tms)	jtag_fsm = EXIT2_IR;
		else		jtag_fsm = PAUSE_IR;
	end
	EXIT2_IR:begin
		if(tms)	jtag_fsm = UPDATE_IR;
		else		jtag_fsm = SHIFT_IR;
	end
	UPDATE_IR:begin
		if(tms)	jtag_fsm = SELECT_DR_SCAN;
		else		jtag_fsm = RUN_TEST_IDLE;
	end
endcase

В «SystemVerilog» удобнее обращаться к отдельным битам регистра (по сути, регистр является массивом бит), удобнее производить конкатенацию («склеивание») отдельных регистров в один и удобнее устанавливать вывод в высокоимпедансное состояние. Описание реакции на нисходящий фронт TCK будет следующей:

always @(negedge tck) begin
	case(jtag_fsm)
		TEST_LOGIC_RESET:begin
			tdo		= 1'bz;//Перевод tdo в HiZ
		end
		SHIFT_DR:begin
			tdo		= data_reg[0];
		end
		EXIT1_DR:begin
			tdo		= 1'bz;//Перевод tdo в HiZ
		end
		SHIFT_IR:begin
			tdo		= inst_reg[0];
		end
		EXIT1_IR:begin
			tdo		= 1'bz;//Перевод tdo в HiZ
		end
	endcase	
end

Остаётся добавить логику, осуществляющую сдвиг регистров. Здесь стоит сказать, что в языках описания аппаратуры имеются два типа присваивания. Не углубляясь в детали, отметим, что в данном примере используется блокирующее присваивание. И чтобы данный код работал с именно таким присваиванием, нам потребуется создать ещё один, дополнительный блок «always», реагирующий на восходящий фронт:

always @(posedge tck) begin
	case(jtag_fsm)
		TEST_LOGIC_RESET:begin
			data_reg	= ID_CODE;
			inst_reg	= 0;
		end
		SHIFT_DR:begin;
			data_reg	= {tdi,data_reg[31:1]};//конкатенация tdi и сдвинутого регистра
		end
		SHIFT_IR:begin
			inst_reg	= {tdi,inst_reg[7:1]};//конкатенация tdi и сдвинутого регистра
		end
	endcase
end
Полный код модуля «main»
module main (
	input			tms,
	input			tck,
	input			tdi,
	output reg 	tdo);

	parameter ID_CODE	= 32'h0AA55003;

	reg	[31:0]data_reg;
	reg	[ 7:0]inst_reg;

	enum {TEST_LOGIC_RESET,
		   RUN_TEST_IDLE,

		   SELECT_DR_SCAN,
		   CAPTURE_DR,
		   SHIFT_DR,
		   EXIT1_DR,
		   PAUSE_DR,
		   EXIT2_DR,
		   UPDATE_DR,

		   SELECT_IR_SCAN,
		   CAPTURE_IR,
		   SHIFT_IR,
		   EXIT1_IR,
		   PAUSE_IR,
		   EXIT2_IR,
		   UPDATE_IR
	} jtag_fsm = TEST_LOGIC_RESET;
	
	always @(posedge tck) begin
		case(jtag_fsm)
			TEST_LOGIC_RESET:begin
				if(tms)	jtag_fsm = TEST_LOGIC_RESET;
				else		jtag_fsm = RUN_TEST_IDLE;
			end
			RUN_TEST_IDLE:begin
				if(tms)	jtag_fsm = SELECT_DR_SCAN;
				else		jtag_fsm = RUN_TEST_IDLE;
			end
			SELECT_DR_SCAN:begin
				if(tms)	jtag_fsm = SELECT_IR_SCAN;
				else		jtag_fsm = CAPTURE_DR;
			end
			CAPTURE_DR:begin
				if(tms)	jtag_fsm = EXIT1_DR;
				else		jtag_fsm = SHIFT_DR;
			end
			SHIFT_DR:begin
				if(tms)	jtag_fsm = EXIT1_DR;
				else		jtag_fsm = SHIFT_DR;
			end
			EXIT1_DR:begin
				if(tms)	jtag_fsm = UPDATE_DR;
				else		jtag_fsm = PAUSE_DR;
			end
			PAUSE_DR:begin
				if(tms)	jtag_fsm = EXIT2_DR;
				else		jtag_fsm = PAUSE_DR;
			end
			EXIT2_DR:begin
				if(tms)	jtag_fsm = UPDATE_DR;
				else		jtag_fsm = SHIFT_DR;
			end
			UPDATE_DR:begin
				if(tms)	jtag_fsm = SELECT_DR_SCAN;
				else		jtag_fsm = RUN_TEST_IDLE;
			end
			SELECT_IR_SCAN:begin
				if(tms)	jtag_fsm = TEST_LOGIC_RESET;
				else		jtag_fsm = CAPTURE_IR;
			end
			CAPTURE_IR:begin
				if(tms)	jtag_fsm = EXIT1_IR;
				else		jtag_fsm = SHIFT_IR;
			end
			SHIFT_IR:begin
				if(tms)	jtag_fsm = EXIT1_IR;
				else		jtag_fsm = SHIFT_IR;
			end
			EXIT1_IR:begin
				if(tms)	jtag_fsm = UPDATE_IR;
				else		jtag_fsm = PAUSE_IR;
			end
			PAUSE_IR:begin
				if(tms)	jtag_fsm = EXIT2_IR;
				else		jtag_fsm = PAUSE_IR;
			end
			EXIT2_IR:begin
				if(tms)	jtag_fsm = UPDATE_IR;
				else		jtag_fsm = SHIFT_IR;
			end
			UPDATE_IR:begin
				if(tms)	jtag_fsm = SELECT_DR_SCAN;
				else		jtag_fsm = RUN_TEST_IDLE;
			end
		endcase
	end
	
	always @(posedge tck) begin
		case(jtag_fsm)
			TEST_LOGIC_RESET:begin
				data_reg	= ID_CODE;
				inst_reg	= 0;
			end
			SHIFT_DR:begin;
				data_reg	= {tdi,data_reg[31:1]};
			end
			SHIFT_IR:begin
				inst_reg	= {tdi,inst_reg[7:1]};
			end
		endcase
	end
	
	always @(negedge tck) begin
		case(jtag_fsm)
			TEST_LOGIC_RESET:begin
				tdo		= 1'bz;
			end
			SHIFT_DR:begin
				tdo		= data_reg[0];
			end
			EXIT1_DR:begin
				tdo		= 1'bz;
			end
			SHIFT_IR:begin
				tdo		= inst_reg[0];
			end
			EXIT1_IR:begin
				tdo		= 1'bz;
			end
		endcase	
	end	

endmodule

Следует отметить (или напомнить), что код на «SystemVerilog» синтезируется в карту соединений между элементами ПЛИС и выполняется не последовательностью команд, как в «Си», а, по сути, одновременно. Даже без дополнительной оптимизации, данный код на демоплате «DE0-Nano» имеет задержку между приходом фронта по линии TCK и выдачей очередного бита по линии TDO всего 0,008мкс.

Разобравшись с работой конечного автомата JTAG на примере выполнения команды чтения идентификационного номера можно перейти к прочим командам. Их применение напрямую связано с языком BSDL, на котором мы в следующий раз напишем файл описания модуля JTAG для воображаемой микросхемы, а также напишем на «Си» и «SystemVerilog» код, имитирующий работу данной микросхемы.

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


  1. Daniloniks
    01.04.2022 16:32
    +3

    Спасибо, ждём продолжения (уровнем не ниже цикла про ВЧ магию :)).


  1. Costic
    01.04.2022 16:36
    +1

    Любопытно было посмотреть на вашу реализацию. А диаграммы в чём вы рисовали?


    1. Flammmable Автор
      01.04.2022 16:40
      +1

      Спасибо.
      Я их рисовал в InkScape. Включив сетку, подобные вещи там рисовать весьма удобно - как на клетчатой бумаге. А потом можно поиграть с толщиной и цветом.