1. Разглядывая JTAG: идентификация

  2. Разглядывая JTAG: *.bsdl своими руками

  3. Разглядывая JTAG: что внутри?

Ознакомившись с работой JTAG в общих чертах и написав файл BSDL для воображаемой микросхемы в предыдущей статье, можно рассмотреть работу модуля JTAG внутри микросхем более детально. Для этого мы напишем прошивку для микроконтроллера и для ПЛИС (на «Си» и на «SystemVerilog»), которые позволят считывать/устанавливать логические уровни на отдельных выводах микросхемы через данный интерфейс.

Прежде чем продолжать разговор непосредственно об интерфейсе JTAG, обратим внимание на особенности работы базового элемента JTAG — сдвигового регистра — вне контекста самого интерфейса.

Представим, что у нас имеется сдвиговый регистр, предназначенный для последовательного приёма данных на вход и параллельной выдачи этих данных во вне. Пускай он состоит из 4-х отдельных триггеров (битов), и изначально в регистре находится значение «1100». Если нам необходимо, чтобы регистр выдал значение «0101», то мы будем должны «вдвинуть» эту последовательность в регистр. На это уйдёт 4 такта. Если при этом значения из вне снимаются непосредственно с выходов триггеров сдвигового регистра, то в течение этих четырёх тактов они будут меняться следующим образом: 1100→1110→0111→1011→0101.

Подобное, квази-произвольное изменение значений на выходе может быть не только неудобно, но, в ряде случаев даже катастрофично. Поэтому в микросхемах сдвиговых регистров имеется не один, а два регистра (секции). При тактировании первой секции происходит сдвиг данных, а когда все необходимые данные сдвинуты в сдвиговую (последовательную) секцию, то при помощи дополнительного вывода возможно осуществить копирование (обновление) данных из последовательной секции в параллельную секцию. В результате, данные на выходе поменяются с «1100» на «0101» за время порядка длительности фронта тактирования.

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

Если предполагается, что сдвиговый регистр должен работать как в режиме «параллельный вход, последовательный выход», так и наоборот — «последовательный вход, параллельный выход», то в работе с таким регистром будет три характерные режима: захват («Capture») данных в последовательную секцию, сдвиг («Shift») последовательной секции и обновление («Update») данных в параллельной секции.

Как видно на схеме конечного автомата JTAG, все три данные состояния имеются в наличие.

Сдвиг регистра в модуле JTAG происходит по восходящему фронту TCK в состоянии «SHIFT XX». Загрузка данных в сдвиговый регистр также происходит по восходящему фронту TCK, но в состоянии «CAPTURE XX».

Здесь есть тонкий момент. Согласно схеме конечного автомата JTAG, в состоянии «CAPTURE XX» невозможно задержаться дольше одного такта. Причём соответствующий бит, сигнализирующий о необходимости перехода, устанавливается на линии TMS на нисходящем фронте, а вход в это состояние (как и в любое другое) происходит на ближайшем восходящем фронте. Таким образом в непосредственной окрестности от состояния «CAPTURE XX» имеется два восходящих фронта — в начале и в конце данного состояния, если смотреть на временную диаграмму. Так вот срабатывание «CAPTURE XX» происходит по второму восходящему фронту.

Если в качестве сдвигового регистра подключен «DEVICE_ID», то в состоянии «CAPTURE DR» по второму восходящему фронту TCK в него будет скопирован идентификационный номер микросхемы. Если подключен «BYPASS», то в том же состоянии по второму восходящему фронту TCK в его единственный бит будет помещён ноль. То же самое произойдёт с регистром инструкций в состоянии «CAPTURE IR» — в него по второму восходящему фронту будет загружено значение, которое указывают в атрибуте «INSTRUCTION_CAPTURE» файла BSDL.

В отличие от «CAPTURE XX», «UPDATE XX» выполняется на нисходящем фронте. Это может вызвать вопрос — зачем так было сделано? Почему не сделать единообразно? Ведь гонку фронтов здесь, на первый взгляд, никак не устроить ввиду того, что у всех микросхем цепи JTAG общая линия TMS, а следовательно все микросхемы всегда находятся в одном и том же состоянии.

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

Схема из стандарта IEEE1149.1-2001
Схема из стандарта IEEE1149.1-2001

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

Мы не будем рассматривать особенности работы с подобной экзотической цепочкой JTAG, но отметим для себя причину, по которой выполнение «UPDATE XX» и «CAPTURE XX» происходит на разных фронтах.

Поговорим теперь о подключаемом регистре «BOUNDARY» и инструкциях, связанных с ним.

Наиболее простой инструкцией является «SAMPLE». Если была выбрана эта инструкция, то в состоянии «CAPTURE DR» на восходящем фронте TCK в соответствующие биты подключенного регистра «BOUNDARY» будет записано состояние всех доступных для контроля входов микросхемы. Перейдя затем в состояние «SHIFT DR», мы сможем последовательно сдвинуть все биты регистра «BOUNDARY» через TDO и распознать, какие логические значения были на выводах микросхемы.

Инструкция «EXTEST» способна как считывать состояния на входах микросхемы, так и устанавливать их на выходах.

Здесь есть тонкий момент. Предположим, была выбрана инструкция «EXTEST». Если мы поместим через состояние «SHIFT DR» в последовательную секцию регистра «BOUNDARY» некоторые значения, то они не появятся тут же автоматически на выходах микросхемы, так как в параллельной секции регистра «BOUNDARY» находятся предыдущие значения. Для обновления параллельной секции ранее введёнными данными, мы должны перевести конечный автомат в состояние «UPDATE DR». Логические уровни на выводах микросхемы поменяются в момент прохождения нисходящего фронта в состоянии «UPDATE DR». Однако, значения выходов микросхемы будут поставлены в зависимость от битов регистра «BOUNDARY» как только в регистр инструкций попадёт код инструкции «EXTEST».

Произойдёт это в момент прохождения нисходящего фронта в состоянии «UPDATE IR». То есть до этого момента мы ещё ничего не сбрасывали в параллельную секцию регистра «BOUNDARY», а сразу после — данные из неё уже используются. Эти данные могут быть весьма произвольны! Чтобы не допустить попадания на выходы микросхемы произвольных значений, неплохо было бы загрузить в параллельную секцию заранее известные данные, но так, чтобы при этом эта секция была отключена от выходов. Для этой цели нужна инструкция «PRELOAD».

Выбор инструкции «PRELOAD» и загрузка начального значения в регистр «BOUNDARY»
Выбор инструкции «PRELOAD» и загрузка начального значения в регистр «BOUNDARY»
Выбор инструкции «EXTEST» и загрузка следующего значения в регистр «BOUNDARY»
Выбор инструкции «EXTEST» и загрузка следующего значения в регистр «BOUNDARY»

Так как инструкция «SAMPLE» предусматривает сброс данных в последовательную секцию регистра «BOUNDARY», а затем их выдвижение через TDO, а «PRELOAD» — задвигание данных в последовательную секцию через TDI, а затем сброс в параллельную секцию (к тому же «выдвижение» и «задвигание» это, по сути, один и тот же процесс сдвига), то эти две инструкции можно объединить в одну — «SAMPLE/PRELOAD». В версии стандарта 1990 года эти инструкции были жёстко объединены, но в версии 2001 их разделили с возможностью задания им одного и того же кода инструкции, либо различных кодов.

Работа с регистром инструкций отличается от работы с регистром «BOUNDARY» только тем, что используется ветка «IR» конечного автомата, а не ветка «DR». В остальном — по второму восходящему фронту в состоянии «CAPTURE IR» в параллельную секцию регистра инструкций закладывается значение по умолчанию (прописываемое в атрибуте «INSTRUCTION_CAPTURE» файла BSDL), а по нисходящему фронту в состоянии «UPDATE IR» происходит копирование данных из последовательной секции в параллельную и новая инструкция вступает в силу.

Пример ведомого устройства JTAG на Си

Зная основные механизмы работы модуля JTAG, перейдём к написанию кода. Этот код будет запускаться на демоплате «NUCLEO-F103RB», у которой есть один пользовательский светодиод (вывод PA5) и одна пользовательская кнопка (вывод PC13). Часть элементов кода будет повторять пример из первой статьи цикла. Но в целом, кода будет заметно больше.

Итак.

Определим задействуемые выводы демоплаты:

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

#define LED0_PIN GPIO_PIN_5
#define BTN0_PIN GPIO_PIN_13

И проведём инициализацию и настройку этих выводов, дополнив функцию «MX_GPIO_Init»:

//==== 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 ====

//==== I/O PINS INIT BEGIN ====
GPIO_InitStruct.Pin  = BTN0_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

GPIO_InitStruct.Pin  = LED0_PIN;
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);
//==== I/O PINS INIT END ====

Возьмём «очищенную» функцию «main»:

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  while (1)
  {
    //Код
  }
}

Объявим перед функцией «main» пару массивов («led» и «btn») из одного элемента (по количеству светодиодов и кнопок), а в самой функции пачку переменных, отвечающих за отдельные линии интерфейса JTAG. Кроме этого добавим код для определения фронтов на линии TCK:

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();

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

  while (1)
  {
    //Код для «моментального» изменения чего-либо

    if(tck_curr != tck_prev)
    {
      //Код, выполняемый при прохождении фронта
    }
    tck_prev = tck_curr;
	}
}

Теперь добавим в бесконечный цикл (перед детектором фронтов) код, позволяющий в любой (ну, почти любой) момент считать/установить по значению переменных, логические значения на линиях микросхемы:

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;

btn[0] = (GPIOC->IDR & BTN0_PIN ) ? 1 : 0;
if(led[0]) GPIOA->ODR |=  LED0_PIN;
else       GPIOA->ODR &= ~LED0_PIN;

Добавим в детектор фронтов код, определяющий восходящий и нисходящий фронты:

if(tck_curr)    tck_rise(tms_curr, tdi_curr);
else tdo_curr = tck_fall(tms_curr, tdi_curr);

...и создадим две дополнительные функции:

void tck_rise(char tms, char tdi)
{
}

char tck_fall(char tms, char tdi)
{
  char tdo = 0; // HiZ
  return tdo;
}

Функции несколько отличаются, так как значения на линии TDO меняются только по нисходящему фронту, соответственно «tck_rise» не нуждается в «tdo».

Добавим чуть ниже массивов «led» и «btn» глобальное перечисление с состояниями конечного автомата:

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;

Нам потребуется функция, целиком отвечающая за переключение состояний автомата. Структура «switch-case» в этой функции идентична такой же структуре из первой статьи цикла:

void fsm_state_change(char tms)
{
  switch(jtag_fsm){
  case TEST_LOGIC_RESET:
    if(tms) jtag_fsm = TEST_LOGIC_RESET;
    else    jtag_fsm = RUN_TEST_IDLE;
    break;
  case RUN_TEST_IDLE:
    if(tms) jtag_fsm = SELECT_DR_SCAN;
    else    jtag_fsm = RUN_TEST_IDLE;
    break;
  <...>
  case UPDATE_IR:
    if(tms) jtag_fsm = SELECT_DR_SCAN;
    else    jtag_fsm = RUN_TEST_IDLE;
    break;
  }
}
Полный код функции
void fsm_state_change(char tms)
{
  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;
  }
}

Добавим в «tck_rise» функцию «fsm_state_change», а также структуру «switch-case», которая будет отвечать за все действия, происходящие по восходящему фронту:

void tck_rise(char tms, char tdi)
{
  switch(jtag_fsm){
  case CAPTURE_DR:
    //
    break;
  case SHIFT_DR:
    //
    break;
  case CAPTURE_IR:
    //
    break;
  case SHIFT_IR:
    //
    break;
  }
  
	fsm_state_change(tms);
}

Добавим похожую структуру «switch-case» и в функцию «tck_fall»:

char tck_fall(char tms, char tdi)
{
  char tdo = 0; // HiZ

  switch(jtag_fsm){
  case TEST_LOGIC_RESET:
    //
    break;
  case SHIFT_DR:
    //
    break;
  case EXIT1_DR:
    tdo = 0; // HiZ
    break;
  case UPDATE_DR:
    //
    break;
  case SHIFT_IR:
    //
    break;
  case EXIT1_IR:
    tdo = 0; // HiZ
    break;
  case UPDATE_IR:
    //
    break;
  }

  return tdo;
}

Добавим к глобальным переменным четыре регистра, вместе с указанием длины, значениями по умолчанию, параллельными секциями (там, где это нужно) и перечислением кодов операций (для регистра инструкций):

//Регистр инструкций
uint8_t        instruction_reg;
uint8_t        instruction_reg_par;
const uint8_t  INSTRUCTION_LENGTH  = 3;
const uint8_t  INSTRUCTION_DEFAULT = 0x01;
enum Instructions{
  INST_IDCODE = 0x01,
  INST_EXTEST = 0x02,
  INST_BYPASS = 0x03,
  INST_SAMPLE = 0x04
};

//Регистр идентификационного кода
uint32_t       device_id_reg;
const uint8_t  DEVICE_ID_LENGTH  = 32;
const uint32_t DEVICE_ID_DEFAULT = 0x0AA55003;

//Регистр BYPASS из одного бита
uint8_t        bypass_reg;
const uint8_t  BYPASS_LENGTH = 1;
const uint8_t  BYPASS_DEFAULT = 0;

//Регистр граничного сканирования
uint32_t       boundary_reg;
uint32_t       boundary_reg_par;
const uint8_t  BOUNDARY_LENGTH = 2;

В функции «tck_rise» заполним состояния для регистра инструкций. А именно, добавим заполнение последовательной секции регистра инструкций значением по умолчанию в состоянии «CAPTURE IR», а также реализуем сдвиг последовательной секции в состоянии «SHIFT IR»:

case CAPTURE_IR:
  instruction_reg = INSTRUCTION_DEFAULT;
  break;
case SHIFT_IR:
  instruction_reg = (instruction_reg >> 1) | (uint32_t)(tdi << (INSTRUCTION_LENGTH  - 1));
  break;

Заполним также состояния для регистра инструкций в функции «tck_fall». В состоянии «TEST LOGIC RESET» будет происходить инициализация обеих секций регистра значением по умолчанию, в состоянии «SHIFT IR» будет происходить установка значений на выводе TDO, в состоянии «UPDATE IR» будет происходить копирование данных из последовательной секции в параллельную:

case TEST_LOGIC_RESET:
  instruction_reg     = INSTRUCTION_DEFAULT;
  instruction_reg_par = INSTRUCTION_DEFAULT;
  tdo = 0; // HiZ
  break;
<...>
case SHIFT_IR:
  tdo = instruction_reg & ((uint8_t)0x1);
  break;
<...>
case UPDATE_IR:
  instruction_reg_par = instruction_reg;
  break;

Вернёмся к функции «tck_rise» и заполним состояния для регистра данных. В каждом состоянии, относящемуся к регистру данных необходимо будет рассмотреть возможность подключения любого из подключаемых регистров. А значит нам потребуется ещё одна, вложенная структура «switch-case». Зависеть она будет от содержимого параллельной секции регистра инструкций, а её отдельными случаями будут коды инструкций. Программный код в состоянии «SHIFT DR» для каждого подключаемого регистра будет полностью аналогичен коду в состоянии «SHIFT IR», за исключением названия регистров (и их длины). В «CAPTURE DR» принципиальные отличия будут только для регистра «BOUNDARY» — в его последовательную секцию будет загружаться не значение по умолчанию, а значения соответствующих входов-выходов микросхемы:

case CAPTURE_DR:
  switch(instruction_reg_par){
  case INST_IDCODE:
    device_id_reg = DEVICE_ID_DEFAULT;
    break;
  case INST_BYPASS:
    bypass_reg    = BYPASS_DEFAULT;
    break;
  case INST_SAMPLE:
    boundary_reg  = (led[0] << 0) | (btn[0] << 1);
    break;
  case INST_EXTEST:
    boundary_reg  = (led[0] << 0) | (btn[0] << 1);
    break;
  }
  break;
case SHIFT_DR:
  switch(instruction_reg_par){
  case INST_IDCODE:
    device_id_reg = (device_id_reg >> 1) | (uint32_t)(tdi << (DEVICE_ID_LENGTH - 1));
    break;
  case INST_BYPASS:
    bypass_reg    = (bypass_reg    >> 1) | (uint32_t)(tdi << (BYPASS_LENGTH    - 1));
    break;
  case INST_SAMPLE:
    boundary_reg  = (boundary_reg  >> 1) | (uint32_t)(tdi << (BOUNDARY_LENGTH  - 1));
    break;
  case INST_EXTEST:
    boundary_reg  = (boundary_reg  >> 1) | (uint32_t)(tdi << (BOUNDARY_LENGTH  - 1));
    break;
  }
  break;

В функции «tck_fall» состояние «SHIFT DR» полностью аналогично состоянию «SHIFT IR» (с поправкой на названия регистров):

case SHIFT_DR:
  switch(instruction_reg_par){
  case INST_IDCODE:
    tdo = device_id_reg & ((uint32_t)0x0000001);
    break;
  case INST_BYPASS:
    tdo = bypass_reg    & ((uint8_t)0x01);
    break;
  case INST_SAMPLE:
    tdo = boundary_reg  & ((uint8_t)0x01);
    break;
  case INST_EXTEST:
    tdo = boundary_reg  & ((uint8_t)0x01);
    break;
  }
  break;

Так как подключаемые регистры «DEVICE ID» и «BYPASS» не нуждаются в параллельной секции, то состояние «UPDATE DR» будет описывать только копирование из секции в секцию для регистра «BOUNDARY»:

case UPDATE_DR:
  switch(instruction_reg){
  case INST_SAMPLE:
    boundary_reg_par = boundary_reg;
    break;
  case INST_EXTEST:
    boundary_reg_par = boundary_reg;
    break;
  }
  break;

Остался последний штрих — выдача значений из регистра «BOUNDARY» на выходы микросхемы в случае, если в регистре инструкций находится код инструкции «EXTEST». Данный код мы впишем в бесконечный цикл функции «main» перед детектором фронтов:

if(instruction_reg_par == INST_EXTEST)
{
  led[0] = boundary_reg_par & (uint8_t)(0x01 << 0);
}
else
{
  led[0] = 0; //HiZ
}
Код на Си полностью
#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

#define LED0_PIN GPIO_PIN_5
#define BTN0_PIN GPIO_PIN_13

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

uint8_t        instruction_reg;
uint8_t        instruction_reg_par;
const uint8_t  INSTRUCTION_LENGTH  = 3;
const uint8_t  INSTRUCTION_DEFAULT = 0x01;
enum Instructions{
	INST_IDCODE = 0x01,
	INST_EXTEST = 0x02,
	INST_BYPASS = 0x03,
	INST_SAMPLE = 0x04
};

uint32_t       device_id_reg;
const uint8_t  DEVICE_ID_LENGTH  = 32;
const uint32_t DEVICE_ID_DEFAULT = 0x0AA55003;

uint8_t        bypass_reg;
const uint8_t  BYPASS_LENGTH = 1;
const uint8_t  BYPASS_DEFAULT = 0;

uint32_t       boundary_reg;
uint32_t       boundary_reg_par;
const uint8_t  BOUNDARY_LENGTH = 2;

uint8_t led[1];
uint8_t btn[1];

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;

void fsm_state_change(char tms)
{
	switch(jtag_fsm){
	case TEST_LOGIC_RESET:
		if(tms)	jtag_fsm = TEST_LOGIC_RESET;
		else    jtag_fsm = RUN_TEST_IDLE;
		break;
	case RUN_TEST_IDLE:
		if(tms)	jtag_fsm = SELECT_DR_SCAN;
		else    jtag_fsm = RUN_TEST_IDLE;
		break;
	case SELECT_DR_SCAN:
		if(tms)	jtag_fsm = SELECT_IR_SCAN;
		else    jtag_fsm = CAPTURE_DR;
		break;
	case CAPTURE_DR:
		if(tms)	jtag_fsm = EXIT1_DR;
		else    jtag_fsm = SHIFT_DR;
		break;
	case SHIFT_DR:
		if(tms)	jtag_fsm = EXIT1_DR;
		else    jtag_fsm = SHIFT_DR;
		break;
	case EXIT1_DR:
		if(tms)	jtag_fsm = UPDATE_DR;
		else    jtag_fsm = PAUSE_DR;
		break;
	case PAUSE_DR:
		if(tms)	jtag_fsm = EXIT2_DR;
		else    jtag_fsm = PAUSE_DR;
		break;
	case EXIT2_DR:
		if(tms)	jtag_fsm = UPDATE_DR;
		else    jtag_fsm = SHIFT_DR;
		break;
	case UPDATE_DR:
		if(tms)	jtag_fsm = SELECT_DR_SCAN;
		else    jtag_fsm = RUN_TEST_IDLE;
		break;
	case SELECT_IR_SCAN:
		if(tms)	jtag_fsm = TEST_LOGIC_RESET;
		else    jtag_fsm = CAPTURE_IR;
		break;
	case CAPTURE_IR:
		if(tms)	jtag_fsm = EXIT1_IR;
		else    jtag_fsm = SHIFT_IR;
		break;
	case SHIFT_IR:
		if(tms)	jtag_fsm = EXIT1_IR;
		else    jtag_fsm = SHIFT_IR;
		break;
	case EXIT1_IR:
		if(tms)	jtag_fsm = UPDATE_IR;
		else    jtag_fsm = PAUSE_IR;
		break;
	case PAUSE_IR:
		if(tms)	jtag_fsm = EXIT2_IR;
		else    jtag_fsm = PAUSE_IR;
		break;
	case EXIT2_IR:
		if(tms)	jtag_fsm = UPDATE_IR;
		else    jtag_fsm = SHIFT_IR;
		break;
	case UPDATE_IR:
		if(tms)	jtag_fsm = SELECT_DR_SCAN;
		else    jtag_fsm = RUN_TEST_IDLE;
		break;
	}
}

void tck_rise(char tms, char tdi)
{
	switch(jtag_fsm){
	case CAPTURE_DR:
		switch(instruction_reg_par){
		case INST_IDCODE:
			device_id_reg = DEVICE_ID_DEFAULT;
			break;
		case INST_BYPASS:
			bypass_reg    = BYPASS_DEFAULT;
			break;
		case INST_SAMPLE:
			boundary_reg  = (led[0] << 0) | (btn[0] << 1);
			break;
		case INST_EXTEST:
			boundary_reg  = (led[0] << 0) | (btn[0] << 1);
			break;
		}
		break;
	case SHIFT_DR:
		switch(instruction_reg_par){
		case INST_IDCODE:
			device_id_reg = (device_id_reg >> 1) | (uint32_t)(tdi << (DEVICE_ID_LENGTH - 1));
			break;
		case INST_BYPASS:
			bypass_reg    = (bypass_reg    >> 1) | (uint32_t)(tdi << (BYPASS_LENGTH    - 1));
			break;
		case INST_SAMPLE:
			boundary_reg  = (boundary_reg  >> 1) | (uint32_t)(tdi << (BOUNDARY_LENGTH  - 1));
			break;
		case INST_EXTEST:
			boundary_reg  = (boundary_reg  >> 1) | (uint32_t)(tdi << (BOUNDARY_LENGTH  - 1));
			break;
		}
		break;
	case CAPTURE_IR:
		instruction_reg = INSTRUCTION_DEFAULT;
		break;
	case SHIFT_IR:
		instruction_reg = (instruction_reg >> 1) | (uint32_t)(tdi << (INSTRUCTION_LENGTH  - 1));
		break;
	}

	fsm_state_change(tms);
}

char tck_fall(char tms, char tdi)
{
	char tdo = 0; // HiZ

	switch(jtag_fsm){
	case TEST_LOGIC_RESET:
		instruction_reg     = INSTRUCTION_DEFAULT;
		instruction_reg_par = INSTRUCTION_DEFAULT;
		tdo = 0; // HiZ
		break;
	case SHIFT_DR:
		switch(instruction_reg_par){
		case INST_IDCODE:
			tdo = device_id_reg & ((uint32_t)0x0000001);
			break;
		case INST_BYPASS:
			tdo = bypass_reg    & ((uint8_t)0x01);
			break;
		case INST_SAMPLE:
			tdo = boundary_reg  & ((uint8_t)0x01);
			break;
		case INST_EXTEST:
			tdo = boundary_reg  & ((uint8_t)0x01);
			break;
		}
		break;
	case EXIT1_DR:
		tdo = 0; // HiZ
		break;
	case UPDATE_DR:
		switch(instruction_reg){
		case INST_SAMPLE:
			boundary_reg_par = boundary_reg;
			break;
		case INST_EXTEST:
			boundary_reg_par = boundary_reg;
			break;
		}
		break;
	case SHIFT_IR:
		tdo = instruction_reg & ((uint8_t)0x1);
		break;
	case EXIT1_IR:
		tdo = 0; // HiZ
		break;
	case UPDATE_IR:
		instruction_reg_par = instruction_reg;
		break;
	}

	return tdo;
}

int main(void)
{
	HAL_Init();
	SystemClock_Config();
	MX_GPIO_Init();

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

	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;

		btn[0] = (GPIOC->IDR & BTN0_PIN ) ? 1 : 0;
		if(led[0]) GPIOA->ODR |=  LED0_PIN;
		else       GPIOA->ODR &= ~LED0_PIN;

		if(instruction_reg_par == INST_EXTEST)
		{
			led[0] = boundary_reg_par & (uint8_t)(0x01 << 0);
		}
		else
		{
			led[0] = 0; //HiZ
		}

		if(tck_curr != tck_prev)
		{
			if(tck_curr)    tck_rise(tms_curr, tdi_curr);
			else tdo_curr = tck_fall(tms_curr, tdi_curr);
		}

	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 ====

  //==== I/O PINS INIT BEGIN ====
  GPIO_InitStruct.Pin  = BTN0_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  GPIO_InitStruct.Pin  = LED0_PIN;
  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);
  //==== I/O PINS INIT END ====

}

Тестирование «в железе»

Скомпилируем и пошьём данный код в «STM32F103RB». Затем соединим его навесными проводами с отладчиком JTAG на базе «FT2232» и запустим «TopJTAG Probe». По аналогии с первой статьёй проверим подключение отладчика к компьютеру и выставим в нём частоту TCK 10кГц. После этого проверим соединение с микросхемой — должен считаться идентификационный номер «0AA55003». Если номер считан, всё подключено правильно и можно создать в «TopJTAG Probe» при помощи мастера проект. Очень странная концепция, но в «TopJTAG Probe» уже созданный проект возможно открыть без физического подключения программатора и микросхемы с модулем JTAG, а вот создать новый без физического подключения — не получится.

На последней, третей странице мастера будет предложено подключить файл BSDL. Немного подправив проект файла BSDL (количество выводов и атрибуты регистра «BOUNDARY») из прошлой статьи, мы сможем предложить «TopJTAG Probe» достойный файл BSDL :)

Текст файла BSDL
entity MY_IC is
	generic (PHYSICAL_PIN_MAP : string);
	port  (
		LED: out bit;
		BTN: in	 bit;
		TMS: in	 bit;
		TDI: in	 bit;
		TCK: in	 bit;
		TDO: out bit;
		VDD: linkage bit;
		VSS: linkage bit;
		NC:  linkage bit_vector(0 to 12)
		);

	use STD_1149_1_2001.all;

	attribute COMPONENT_CONFORMANCE of MY_IC : entity is "STD_1149_1_2001";
	attribute PIN_MAP of MY_IC : entity is PHYSICAL_PIN_MAP;

	constant  PLCC20:PIN_MAP_STRING:=
		"LED:	1, " &
		"BTN:	2, " &
		"TMS:	3, " &
		"TDI:	4, " &
		"TCK:	5, " &
		"TDO:	6, " &
		"VDD:	7, " &
		"VSS:	8, " &
		"NC:	(9,10,11,12,13,14,15,16,17,18,19,20) ";

	attribute TAP_SCAN_MODE	 of TMS : signal is true;
	attribute TAP_SCAN_IN    of TDI : signal is true;
	attribute TAP_SCAN_CLOCK of TCK : signal is (10.0e3, BOTH);
	attribute TAP_SCAN_OUT   of TDO : signal is true;

	attribute INSTRUCTION_LENGTH of MY_IC : entity is 3;
	attribute INSTRUCTION_OPCODE of MY_IC : entity is
		"IDCODE (001)," &
		"EXTEST (010)," &
		"BYPASS (011)," &
		"SAMPLE (100) ";
	attribute INSTRUCTION_CAPTURE of MY_IC : entity is "00000001";

	attribute IDCODE_REGISTER of MY_IC : entity is
		"0000" &			-- код ревизии или чего-нибудь типа того
		"1010101001010101" &	-- код модели микросхемы hAA55
		"00000000001" &		-- код производителя (соответствует AMD)
		"1"; 				-- единица по стандарту IEEE1149.1

	attribute REGISTER_ACCESS of MY_IC : entity is
		"DEVICE_ID (IDCODE)," &
		"BYPASS    (BYPASS)," &	
		"BOUNDARY  (EXTEST, SAMPLE)";

	attribute BOUNDARY_LENGTH of MY_IC : entity is 2;
	attribute BOUNDARY_REGISTER of MY_IC : entity is
		"0 (BC_1, LED, output2, X)," &
		"1 (BC_1, BTN, input,   X)" ;
end MY_IC;

После создания проекта остаётся только запустить коммуникацию по протоколу JTAG. Для этого:

  1. Добавим линию «BTN» к списку для отображения временны́х диаграмм.

  2. Откроем окно выбора инструкций.

  3. Выберем режим работы с инструкцией «EXTEST»

  4. Нажмём кнопку запуска коммуникации.

  1. Теперь, нажимая кнопку на демоплате мы будем видеть в реальном времени изменение на временной диаграмме и мигание соответствующего вывода на схеме.

  2. А также, выбирая в контекстном меню (или комбинацией клавиш) пункт «Toggle Value» на выделенном выходе микросхемы (в таблице слева), мы сможем мигать пользовательским светодиодом.

Пример ведомого устройства JTAG на SystemVerilog

Напишем теперь тот же функционал на «SystemVerilog». В качестве демоплаты будет использоваться плата «DE-Nano». Она имеет 2 пользовательские тактовые кнопки и 8 светодиодов, и мы задействуем их всех.

Создадим сперва модуль верхнего уровня и зададим ему входы и выходы:

module main (
  input      tms,
  input      tck,
  input      tdi,
  output reg tdo,

  output reg [7:0]led,
  input      [1:0]btn
  );
  //Код
endmodule
Список используемых выводов в DE0-Nano
btn[0] -> J15
btn[1] -> E1
led[0] -> A15
led[1] -> A13
led[2] -> B13
led[3] -> A11
led[4] -> D1
led[5] -> F3
led[6] -> B1
led[7] -> L3
tms    -> E10
tck    -> B11
tdi    -> D11
tdo    -> B12

Далее создадим необходимые нам регистры, с указанием их длины, значений по умолчанию, с последовательными и параллельными секциями и так далее:

//Регистр инструкций
parameter INSTRUCTION_LENGTH = 3;
parameter INSTRUCTION_DEFAULT = 3'b001;
reg [INSTRUCTION_LENGTH-1:0]instruction_ser;
reg [INSTRUCTION_LENGTH-1:0]instruction_par;	
enum reg[INSTRUCTION_LENGTH-1:0]{
  INST_IDCODE = 3'b001,
  INST_EXTEST = 3'b010,
  INST_BYPASS = 3'b011,
  INST_SAMPLE = 3'b100
} instructions;

//Регистр идентификационного кода
parameter DEVICE_ID_LENGTH  = 32;
parameter DEVICE_ID_DEFAULT = 32'h0AA55003;
reg [DEVICE_ID_LENGTH-1:0]device_id_ser;

//Регистр BYPASS из одного бита
parameter BYPASS_DEFAULT = 0;
reg bypass_ser;

//Регистр граничного сканирования
parameter BOUNDARY_LENGTH = 10;
reg [BOUNDARY_LENGTH-1:0]boundary_ser;
reg [BOUNDARY_LENGTH-1:0]boundary_par;

Добавим после этого перечисление состояний конечного автомата:

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». Один будет срабатывать по восходящему фронту TCK, другой — по нисходящему:

always @(posedge tck) begin
  //Код
end

always @(negedge tck) begin
  //Код
end

В блок восходящего фронта впишем структуру «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
  <...>
  UPDATE_IR:begin
    if(tms) jtag_fsm <= SELECT_DR_SCAN;
    else    jtag_fsm <= RUN_TEST_IDLE;
  end
endcase

В том же блоке «always» добавим структуру «case» для управления регистрами и вставим в ветвь «CAPTURE IR» код, инициализирующий регистр инструкций, а в ветвь «SHIFT IR» код, осуществляющий сдвиг регистра инструкций:

case(jtag_fsm)
  CAPTURE_DR:begin
    //
  end
  SHIFT_DR:begin
    //
  end
  CAPTURE_IR:begin
    instruction_ser <= INSTRUCTION_DEFAULT;
  end
  SHIFT_IR:begin
    instruction_ser <= {tdi,instruction_ser[INSTRUCTION_LENGTH-1:1]};
  end
endcase

Не углубляясь в синтаксис языков HDL в общем и SystemVerilog-а в частности, отмечу, что существует два типа присваивание: блокирующие «=» и неблокирующее «<=». Во всём данном примере будут использоваться неблокирующие присваивания.

Подобно примеру на «Си», в ветвях «CAPTURE DR» и «SHIFT DR» потребуется вложенная структура «case», которая бы задействовала тот или иной подключаемый регистр в зависимости выбранной инструкции. Для написанного выше кода — это будет выглядеть так:

CAPTURE_DR:begin
  case(instruction_par)
    INST_IDCODE:begin
      device_id_ser <= DEVICE_ID_DEFAULT;
    end
    INST_BYPASS:begin
      bypass_ser    <= BYPASS_DEFAULT;
    end
    INST_SAMPLE:begin
      boundary_ser[7:0] <= led[7:0];
      boundary_ser[9:8] <= btn[1:0];
    end
    INST_EXTEST:begin
      boundary_ser[7:0] <= led[7:0];
      boundary_ser[9:8] <= btn[1:0];
    end
  endcase
end
SHIFT_DR:begin
  case(instruction_par)
    INST_IDCODE:begin
      device_id_ser <= {tdi,device_id_ser[DEVICE_ID_LENGTH-1:1]};
    end
    INST_BYPASS:begin
      bypass_ser    <= tdi;
    end
    INST_SAMPLE:begin
      boundary_ser  <= {tdi,boundary_ser[BOUNDARY_LENGTH-1:1]};
    end
    INST_EXTEST:begin
      boundary_ser  <= {tdi,boundary_ser[BOUNDARY_LENGTH-1:1]};
    end
  endcase
end

Теперь перейдём в блок «always», отвечающий за реакцию на нисходящий фронт, добавим туда структуру «case» и ветвь «SHIFT IR» выдачей бита из регистра инструкций на выход TDO, ветвь «UPDATE IR» — копированием данных последовательной секции регистра инструкций в параллельную секцию, а также инициализацией параллельной секции регистра инструкций в состоянии «TEST LOGIC RESET»:

case(jtag_fsm)
  TEST_LOGIC_RESET:begin
    instruction_par <= INSTRUCTION_DEFAULT;
    tdo <= 1'bz;//Переключение TDO в состояние HiZ
  end
  SHIFT_DR:begin
    //
  end
  EXIT1_IR:begin
    tdo <= 1'bz;//Переключение TDO в состояние HiZ
  end
  UPDATE_DR:begin
    //
  end
  SHIFT_IR:begin
    tdo <= instruction_ser[0];
  end
  EXIT1_IR:begin
    tdo <= 1'bz;//Переключение TDO в состояние HiZ
  end
  UPDATE_IR:begin
    instruction_par <= instruction_ser;
  end
endcase

Дополним ветви «SHIFT DR» и «UPDATE DR» кодом, осуществляющим выдачу данных в линию TDO из подключаемых регистров, а также кодом, производящим обновление данных в параллельной секции регистра граничного сканирования:

SHIFT_DR:begin
  case(instruction_par)
    INST_IDCODE:begin
      tdo <= device_id_ser[0];
    end
    INST_BYPASS:begin
      tdo <= bypass_ser;
    end
    INST_SAMPLE:begin
      tdo <= boundary_ser[0];
    end
    INST_EXTEST:begin
      tdo <= boundary_ser[0];
    end
  endcase
end
<...>
UPDATE_DR:begin
  case(instruction_par)
    INST_SAMPLE:begin
      boundary_par <= boundary_ser;
    end
    INST_EXTEST:begin
      boundary_par <= boundary_ser;
    end
  endcase
end

Наконец, установим связь между битами регистра граничного сканирования и выводами, управляющими светодиодами. Для этого вне блоков «always» следует написать код непрерывного присваивания с условным оператором «X ? Y : Z»:

assign led = (instruction_par == INST_EXTEST) ? {boundary_par[7:0]} : 8'bzzzz_zzzz;
Код на SystemVerilog полностью
module main (
	input      tms,
	input      tck,
	input      tdi,
	output reg tdo,

	output reg [7:0]led,
	input      [1:0]btn
	);
	
	parameter INSTRUCTION_LENGTH = 3;
	parameter INSTRUCTION_DEFAULT = 3'b001;
	reg [INSTRUCTION_LENGTH-1:0]instruction_ser;
	reg [INSTRUCTION_LENGTH-1:0]instruction_par;	
	enum reg[INSTRUCTION_LENGTH-1:0]{
		INST_IDCODE = 3'b001,
		INST_EXTEST = 3'b010,
		INST_BYPASS = 3'b011,
		INST_SAMPLE = 3'b100
	} instructions;
	
	parameter DEVICE_ID_LENGTH  = 32;
	parameter DEVICE_ID_DEFAULT = 32'h0AA55003;
	reg [DEVICE_ID_LENGTH-1:0]device_id_ser;

	parameter BYPASS_DEFAULT = 0;
	reg bypass_ser;
	
	parameter BOUNDARY_LENGTH = 10;
	reg [BOUNDARY_LENGTH-1:0]boundary_ser;
	reg [BOUNDARY_LENGTH-1:0]boundary_par;


	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)
		CAPTURE_DR:begin
			case(instruction_par)
				INST_IDCODE:begin
					device_id_ser <= DEVICE_ID_DEFAULT;
				end
				INST_BYPASS:begin
					bypass_ser    <= BYPASS_DEFAULT;
				end
				INST_SAMPLE:begin
					boundary_ser[7:0] <= led[7:0];
					boundary_ser[9:8] <= btn[1:0];
				end
				INST_EXTEST:begin
					boundary_ser[7:0] <= led[7:0];
					boundary_ser[9:8] <= btn[1:0];
				end
			endcase
		end
		SHIFT_DR:begin
			case(instruction_par)
				INST_IDCODE:begin
					device_id_ser <= {tdi,device_id_ser[DEVICE_ID_LENGTH-1:1]};
				end
				INST_BYPASS:begin
					bypass_ser    <= tdi;
				end
				INST_SAMPLE:begin
					boundary_ser  <= {tdi,boundary_ser[BOUNDARY_LENGTH-1:1]};
				end
				INST_EXTEST:begin
					boundary_ser  <= {tdi,boundary_ser[BOUNDARY_LENGTH-1:1]};
				end
			endcase
		end
			CAPTURE_IR:begin
				instruction_ser <= INSTRUCTION_DEFAULT;
			end
			SHIFT_IR:begin
				instruction_ser <= {tdi,instruction_ser[INSTRUCTION_LENGTH-1:1]};
			end
		endcase


		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 @(negedge tck) begin
		case(jtag_fsm)
			TEST_LOGIC_RESET:begin
				instruction_par <= INSTRUCTION_DEFAULT;
				tdo <= 1'bz;
			end
			SHIFT_DR:begin
				case(instruction_par)
					INST_IDCODE:begin
						tdo <= device_id_ser[0];
					end
					INST_BYPASS:begin
						tdo <= bypass_ser;
					end
					INST_SAMPLE:begin
						tdo <= boundary_ser[0];
					end
					INST_EXTEST:begin
						tdo <= boundary_ser[0];
					end
				endcase
			end
			EXIT1_IR:begin
				tdo <= 1'bz;
			end
			UPDATE_DR:begin
				case(instruction_par)
					INST_SAMPLE:begin
						boundary_par <= boundary_ser;
					end
					INST_EXTEST:begin
						boundary_par <= boundary_ser;
					end
				endcase
			end
			SHIFT_IR:begin
				tdo <= instruction_ser[0];
			end
			EXIT1_IR:begin
				tdo <= 1'bz;
			end
			UPDATE_IR:begin
				instruction_par <= instruction_ser;
			end
		endcase
	end

	
	assign led = (instruction_par == INST_EXTEST) ? {boundary_par[7:0]} : 8'bzzzz_zzzz;

endmodule

Для тестирования «в железе» данного кода следует слегка видоизменить файл BSDL, увеличив количество входов и выходов в заголовке, а также в описании корпуса, увеличить длину регистра «BOUNDARY» и дополнить описание данного регистра.

Текст файла BSDL
entity MY_IC is
	generic (PHYSICAL_PIN_MAP : string);
	port  (
		LED: out bit_vector(0 to 7);
		BTN: in	 bit_vector(0 to 1);
		TMS: in	 bit;
		TDI: in	 bit;
		TCK: in	 bit;
		TDO: out bit;
		VDD: linkage bit;
		VSS: linkage bit;
		NC:  linkage bit_vector(0 to 3)
		);

	use STD_1149_1_2001.all;

	attribute COMPONENT_CONFORMANCE of MY_IC : entity is "STD_1149_1_2001";
	attribute PIN_MAP of MY_IC : entity is PHYSICAL_PIN_MAP;

	constant  PLCC20:PIN_MAP_STRING:=
		"LED:	(1,2,3,4,5,6,7,8), " &
		"BTN:	(9,10),  " &
		"TMS:	11, " &
		"TDI:	12, " &
		"TCK:	13, " &
		"TDO:	14, " &
		"VDD:	15, " &
		"VSS:	16, " &
		"NC:	(17,18,19,20) ";

	attribute TAP_SCAN_MODE	 of TMS : signal is true;
	attribute TAP_SCAN_IN    of TDI : signal is true;
	attribute TAP_SCAN_CLOCK of TCK : signal is (10.0e3, BOTH);
	attribute TAP_SCAN_OUT   of TDO : signal is true;

	attribute INSTRUCTION_LENGTH of MY_IC : entity is 3;
	attribute INSTRUCTION_OPCODE of MY_IC : entity is
		"IDCODE (001)," &
		"EXTEST (010)," &
		"BYPASS (011)," &
		"SAMPLE (100) ";
	attribute INSTRUCTION_CAPTURE of MY_IC : entity is "00000001";

	attribute IDCODE_REGISTER of MY_IC : entity is
		"0000" &			-- код ревизии или чего-нибудь типа того
		"1010101001010101" &	-- код модели микросхемы hAA55
		"00000000001" &		-- код производителя (соответствует AMD)
		"1"; 				-- единица по стандарту IEEE1149.1

	attribute REGISTER_ACCESS of MY_IC : entity is
		"DEVICE_ID (IDCODE)," &
		"BYPASS    (BYPASS)," &	
		"BOUNDARY  (EXTEST, SAMPLE)";

	attribute BOUNDARY_LENGTH of MY_IC : entity is 10;
	attribute BOUNDARY_REGISTER of MY_IC : entity is
		"0 (BC_1, LED(0), output2, X)," &
		"1 (BC_1, LED(1), output2, X)," &
		"2 (BC_1, LED(2), output2, X)," &
		"3 (BC_1, LED(3), output2, X)," &
		"4 (BC_1, LED(4), output2, X)," &
		"5 (BC_1, LED(5), output2, X)," &
		"6 (BC_1, LED(6), output2, X)," &
		"7 (BC_1, LED(7), output2, X)," &
		"8 (BC_1, BTN(0), input,   X)," &
		"9 (BC_1, BTN(1), input,   X)" ;
end MY_IC;

После соединения «DE0-Nano» с отладчиком JTAG, созданием проекта в «TopJTAG Probe» и подключения файла BSDL, мы сможем приблизительно вот так управлять светодиодами и считывать состояния кнопок на демонстрационной плате:

Заключение

Как и любая крупная область знаний, как и любой популярный инструмент, JTAG не является герметичной сущностью и имеет рад ветвящихся направлений.

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

Другой широкой областью является программирование и отладка через JTAG. Дополнительные подключаемые регистры, а также нестандартные и специфические для каждого производителя микросхем дополнительные инструкции модулей JTAG создают достаточно большой пласт информации, выходящий за рамки базового стандарта IEEE1149.1.

Помимо этого, сам стандарт непрерывно совершенствуется, одни его части отмирают, другие — привносятся. В частности, есть версия двухпроводного JTAG, полная своими особенностями и подводными камнями.

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

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


  1. alinkagalichina
    28.04.2022 20:37
    +5

    Все статьи определённо в закладки. Спасибо огромное.

    Есть ли планы написать про двухпроводный jtag? Читала про него, но пощупать вживую так и не удалось.


    1. Flammmable Автор
      28.04.2022 20:48
      +1

      Спасибо, что читаете )

      По поводу именно двухпроводного JTAG пока планов нет, так как из "ствола" классического JTAG вырастает огромное количество "ветвей" смежных тем, о которых можно написать очень много. Мне ближе "ветвь" про SVF, но освещение мною данной темы если и случится, то весьма нескоро.


  1. Khort
    28.04.2022 22:42
    +2

    Как я уже писал на электрониксе - стандарту JTAG почти 40 лет, его жевали-разжевали уже все кто только мог. RTL на него весь давно написан, BSDL много кем разжеван, даже книги есть. Хотя картинки у вас конечно красивые, респект.

    Дам совет. Если хотите сбацать пиар-бомбу в стиле Панчула, вида: как проектируются супер-современные микропроцессоры, то есть очень интересный потомок житага - стандарт ieee 1500. Это уже не про граничное сканирование, а про сканирование логики. И не просто контроллер, а целая архитектура, на порядок более сложная и развитая. Но в целом, это развитие житага. Так вот, без этого (ieee 1500) стандарта действительно ни один современный SoC не обходится, а между тем в РФ это знает (и использует) очень мало кто. Такая статья была бы интересна и профессионалам, а не только начинающим.


    1. Flammmable Автор
      28.04.2022 23:49
      +3

      RTL на него весь давно написан, BSDL много кем разжеван

      Прям даже любопытно стало, если всё так здорово, как вы говорите, то 1) где же взять готовое IP-ядро на JTAG slave (а то на OpenCores единственный проект на тему - jtag_slave - содержит в своём архиве единственный файл "readme.txt", содержащий в себе текст "closed project at the moment ;-(" )? 2) ...и где на русском языке почитать про BSDL (ну так, что бы всё было "разжевано")?


      1. Khort
        29.04.2022 08:52

        Загуглите строку "BSDL" boundary scan description language book Получите первой же ссылкой книгу 2003 года. Тогда, 20 лет назад, это было еще актуально

        https://link.springer.com/chapter/10.1007/978-1-4615-0367-5_2

        Если нужны айпи, добро пожаловать в мир Синопсиса. У них эти ядра лежат уже лет 15 как минимум в каждом дистрибутиве DC. С описанием, все разжевано, все что есть в вашей статье и еще сверху раз в 10 больше. Синтезатор DC автоматически (по подготовленному скрипту) вставит Tap контроллер в ваш код, соединит все IO цепочками boundary scan и выпишет получившийся BSDL. А до кучи сгенерит тестбенч, который дергает интерфейс, читает и пишет, да еще и проверяет. Лет 15 это все там есть как минимум, отлажено настолько что никто уже давно в этот код и не лезет внутрь. Нет, можно конечно и самому все написать, но я бы клеил шилдик "РЕТРО" к такому материалу.


        1. Flammmable Автор
          29.04.2022 09:33
          +3

          Здесь уместно пояснить, что стоимость лицензии Synopsys Design Compiller в 2004 году начиналась от 29.000$ (подобные компании не любят выставлять ценник на ПО в открытый доступ, поэтому информация весьма старая). Без учёта инфляции доллара - это, на настоящий момент 2 миллиона рублей. У Синопсиса имеется и академическая программа, позволяющая использовать его продукты в целях обучения сильно дешевле, но она сопряжена с рядом действий административно-бюрократического характера. На сколько известно мне, частным лицам триальная версия продуктов Синопсис не предоставляется.

          Я позволю себе несколько переформулировать свои вопросы и ваши ответы:
          - Где взять готовое IP-ядро JTAG?
          - В платных, закрытых программных продуктах.
          - Где на русском языке почитать про BSDL?
          - В англоязычной книжке.

          Конечно тут можно пуститься в банальности про то, что работа с FPGA/ASIC подразумевает использование дорогого программного инструментария и знание английского языка. А также пуститься в софистику и сказать, что формально язык BSDL был полностью разжёван непосредственно в стандарте IEEE1149.1-1994B, а весь RTL был написан и отлажен к 1993 году, так как JTAG присутствовал уже в процессорах AM486 (сам этот RTL сугубо конфиденциален, однако формально он существует).

          Но тут возникает встречный вопрос: к чему тогда ваше предложение писать про IEEE1500? Он же также "давно разжёван" в самом стандарте, а RTL к нему "давно написан" (раз имеются микросхемы, поддерживающие IEEE1500).


  1. vip100
    30.04.2022 23:08
    +1

    А в тексте все правильно ? Я так понял и на рисунках так написано, что обновление регистров происходит по нисходящему фронту. Или я что-то не так понял ?

    Логические уровни на выводах микросхемы поменяются в момент прохождения восходящего фронта в состоянии «UPDATE DR». Однако, значения выходов микросхемы будут поставлены в зависимость от битов регистра «BOUNDARY» как только в регистр инструкций попадёт код инструкции «EXTEST». Произойдёт это в момент прохождения восходящего фронта в состоянии «UPDATE IR».


    1. Flammmable Автор
      30.04.2022 23:13

      Это гнусная опечатка, которую я уже поправил. Спасибо, что заметили! Отмечу, однако, что сообщения об опечатках (или подозрении на них) по местному этикету следует присылать в личном сообщении.