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

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

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

  4. Разглядывая JTAG: самый быстрый программный JTAG на Arduino

В предыдущих статьях цикла был приведен пример реализации ведомого модуля JTAG на Verilog. Я предположил, что количество инженеров, знающих Verilog, меньше, чем количество инженеров, которым требуется понимание принципов работы JTAG. Поэтому, помимо реализации на Verilog, модуль JTAG был также реализован на Си.

Так как реализация на Си преследовала исключительно образовательную цель, то скорость её работы была принесена в жертву некоторой унифицированности подходов с реализацией на Verilog. Поэтому я был несколько удивлён, когда в личном сообщении @Sergei2405 спросил, нет ли способа ускорить работу примера для микроконтроллера, чтобы применить этот код в промышленном изделии.

Субъективно, практическое применение программного JTAG мне по‑прежнему видится не вполне оправданным.
Но, во‑первых, это хороший повод рассмотреть предельные возможности микроконтроллеров.
А во‑вторых, есть формальная причина сказать, что в данной статье предлагается Решение Прикладной Задачи :)

Итак, сегодня мы поговорим про прерывания, поллинг и прочее. А протокол JTAG станет фоном для повествования.

У ряда читателей по ходу статьи может возникнуть вопрос: «так ли уж надо считать машинные циклы в середине третьего десятилетия XXI века?» Такая потребность не является частой, но есть класс задач, где критична скорость отклика. Причём этот отклик должен быть, скажем так, интеллектуальным.

В данном классе задач альтернативой подсчёту циклов является применение ПЛИС. Субъективно, я бы сказал, что их применение является предпочтительным. Но «интеллектуальность» ПЛИС даётся дорогой ценой. Каждый логический элемент — это сотни и тысячи транзисторов. Например, Stratix 10 содержит до 10,2 миллионов логических элементов, и состоит при этом из 43300 миллионов транзисторов.

В тоже время «интеллектуальность» любой процессорной архитектуры напрямую зависит от сложности программы. А она — во многом от объёма памяти. Элементарная ячейка статической оперативной памяти — это всего шесть транзисторов.
Поэтому не удивительно, что, например, Texas Instruments выпустили линейку процессоров Sitara AM437x, ориентированных на задачи реального времени. В этих процессорах помимо основного ядра, работающего на частоте в 1000 МГц, есть два дополнительных ядра PRU (Programmable Real‑time Unit). Они работают на сравнительно невысокой частоте 200 МГц, но гарантированно выполняют ровно одну инструкцию за каждый цикл. Чтобы не было иллюзий в плане концепции, статья Ensuring real‑time predictability, выпущенная Texas Instruments, как раз про данные процессоры.

В качестве платформы я решил использовать самую распространённую демо‑плату в мире — Arduino. Соответственно, рассматриваемым микроконтроллером станет ATmega328P, не имеющий аппаратного интерфейса JTAG.

Так как для получения максимального быстродействия мы будем писать код на языке ассемблера, то средой разработки будет не Arduino IDE, а Microchip Studio.

Для работы с Arduino среду Microchip Studio нужно будет слегка настроить

Для начала нам надо определить номер COM‑порта нашего Arduino. У меня это COM38:

Кроме этого, нам потребуется консольная утилита avrdude. На текущий момент она позволяет программировать микроконтроллеры AVR при помощи примерно сотни различных программаторов. Её актуальную версию можно скачать здесь. В архиве с релизом находятся три файла, их надо скопировать в какую‑нибудь папку. У меня это C:\Program Files (x86)\Atmel\avrdude\

Теперь в среде Microchip Studio создадим проект на ассемблере.
Для этого выберем в главном меню Файл→Создать→Проект и в открывшемся окне укажем тип проекта Assembler:

Нажмём ОК и в следующем окне укажем семейство и марку микроконтроллера:

Затем выберем в главном меню пункт Сервис→Внешние инструменты:

В открывшемся окне укажем:

  • название инструмента

  • путь к *.exe. У меня это C:\Program Files (x86)\Atmel\avrdude\avrdude.exe

  • аргументы (марка программатора, марка микроконтроллера, имя COM‑порта, битрейт, откуда брать прошивку). У меня это «-c arduino -p atmega328p -P COM38 -b 115200 -U flash:w:$(TargetName).hex»

  • путь к папке с проектом — $(TargetDir)

  • «Использовать окно вывода» — ставим галку

Нажимаем ОК. Над пунктом «Внешние инструменты» в меню появится пункт «Arduino».

Собираем проект, нажав Сборка→Собрать решение (либо нажимаем F7). И прошиваем микроконтроллер, нажав на пункт главного меню Сервис→Arduino:

Если мы всё сделали верно и сам Arduino подключен к компьютеру, то по окончании (моментального) программирования в окне вывода будет написано «avrdude done. Thank you.»

Для того чтобы убедиться в работоспособности оборудования и ПО, не лишним будет помигать светодиодом. У Arduino светодиод подключен к 13‑му выводу гребёнки Digital или к 5‑му выводу порта «B» микроконтроллера. Поэтому базовым будет следующий код:

.equ LED_POS  = 5
.equ LED_MASK = (1 << LED_POS)
reset:
  sbi DDRB, LED_POS
  ldi r16,  LED_MASK
  ldi r17,  0
loop:
  out  PORTB, r16
  out  PORTB, r17
rjmp loop

Данный код инициализирует пятый вывод порта «B» как выход, загружает в регистры общего назначения константы (ноль и единицу в пятом бите), а затем начинает выдавать импульсы. Причём длительность каждого импульса будет равна одному (машинному) циклу.

Если у вас нет осциллографа, логического анализатора или даже самого Arduino, но хочется попрактиковаться с приведёнными в статье примерами, вы можете сделать это при помощи симулятора Proteus

Создадим проект: выберем пункт главного меню File→New Project, в открывшемся окне введём название проекта и выберем путь, нажмём Next‑Next‑Next‑Next‑Finish.

Откроется поле для построения схемы. Добавим на него модель микроконтроллера ATmega328P:

  1. Нажмём на боковой панели Component Mode

  2. Нажмём кнопку Pick Device, откроется окно поиска компонентов

  3. В строке для поиска введём ATmega328P

  4. Два раза щёлкнем по единственному результату поиска

Добавим микроконтроллер на схему:

Для этого щёлкнем по названию микроконтроллера на боковой панели, а затем — по полю схемы. Тогда указатель из карандаша превратится в розовый контур будущего компонента. Щёлкнем в понравившемся месте схемы ещё раз — компонент размещён.

Затем дважды щёлкнем на изображении микроконтроллера на нашей схеме и в открывшемся окне впишем в поле Program File полный путь к файлу *.hex, сгенерированному из кода на ассемблере. Файл лежит в папке Debug внутри папки проекта.
А еще в поле PCB Package поменяем тип корпуса на SPDIL28.

Добавим щуп тем же способом, что и микроконтроллер, предварительно выбрав на боковой панели раздел Probe Mode:

Установив щуп на 5‑й вывод порта «B» (19‑й вывод на схеме), поменяем его название на Probe1. Для этого щёлкнем дважды по изображению щупа:

Аналогичным образом добавим источник сигнала. Точнее, генератор одиночного фронта из раздела Generator Mode. Установив источник на 2‑й вывод порта «D» (4‑й вывод на схеме). Поменяем ему название на Edge1, а также установим время срабатывания 100.02m:

Затем из раздела Graph Mode добавим два графика типа Analogue:

Назначим сигналы каждому графику. Для этого щёлкнем по изображению щупа на схеме и просто перетащим его на график:

А затем щёлкнем по графику, чтобы он обновился:

Перенесём таким же образом на другой график источник сигнала Edge1, а затем дважды щёлкнем по каждому графику и настроим временной диапазон, введя в поле Start time число 100m и в поле Stop time 100.1m:

Теперь запустим симуляцию, выбрав пункт главного меню Debug→Run Simulation (timed breakpoint). Откроется небольшое окно для ввода времени окончания симуляции. Введём туда 110m:

Когда Proteus просимулирует 0,11 секунды и встанет на паузу, нажмём на кнопку Stop в нижней панели:

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

Прерывания

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

Когда речь заходит о максимально быстрой реакции микроконтроллера на внешнее событие, первым на ум приходит термин «прерывание».
Поэтому будет разумным рассмотреть худший по времени случай реакции на фронт тактового сигнала по линии TCK при использовании прерывания.

Настроим работу внешнего прерывания.

.set LED_POS  = 5
.set EXT_POS  = 2
.set LED_MASK = (1 << LED_POS)
.set EXT_MASK = (1 << EXT_POS)

.org 0x0000 
  rjmp main
.org 0x0002 
  rjmp external_interrupt
.org INT_VECTORS_SIZE     ; адрес конца вектора прерываний

main:
  cli                     ; запрещаем все прерывания
              
  ldi r17,    LOW(RAMEND) ; устанавливаем счётчик стека
  out SPL,    r17
  ldi r17,    HIGH(RAMEND)
  out SPH,    r17           

  sbi DDRB,   LED_POS     ; устанавливаем PORTB5 как выход (контакт 13 гребёнки Digital)
  cbi DDRD,   EXT_POS     ; устанавливаем PORTD2 как вход  (контакт  2 гребёнки Digital)

  ldi r16,    0x01        ; разрешаем внешнее прерывание
  out EIMSK,  r16
  ldi r16,    0b00001111  ; устанавливаем срабатывание только от восходящего фронта
  sts EICRA,  r16           

  ldi r16,    LED_MASK
  ldi r17,    0

  sei                     ; разрешаем все прерывания
   
loop:          
  rjmp loop

external_interrupt:    
  out PORTB,  r16
  out PORTB,  r17
reti                      ; возвращаемся из внешнего прерывания

В данном примере мы подготавливаем константы для работы с пятым выводом порта «B» (выход тестового импульса) и вторым выводом порта «D» (вход сигнала для внешнего прерывания).
После метки main мы при помощи инструкции cli полностью отключаем возможность любых прерываний.
Затем мы заносим в специальную пару регистров (в счётчик стека) адрес вершины стека. Это действие необходимо для работы прерываний, точнее, для возврата из них — чуть дальше будут подробности.
Затем мы конфигурируем пятый вывод порта «B» как выход и второй вывод порта «D» как вход.
После этого мы разрешаем внешнее прерывание и конфигурируем его срабатывание только по восходящему фронту (0→1).
Наконец, мы заносим в регистры общего назначения константы для нуля и единицы и разрешаем все прерывания командой sei.

Далее основное выполнение программы попадает в бесконечный цикл.

Но кроме конфигурации в данном примере есть блок, помещающий пару инструкций в адреса 0x0000 и 0x0002 памяти программ.

В любой процессорной архитектуре есть специальный регистр — это счётчик инструкций. Он хранит адрес инструкции, которую процессор будет выполнять следующей. Переход по программе — это изменение содержимого данного счётчика.
При запуске ATmega328P в данный счётчик автоматически записывается ноль. А при срабатывании внешнего прерывания, которое мы сконфигурировали, в этот счётчик также автоматически записывается значение 0x0002.

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

То есть, после запуска в счётчик инструкций помещается ноль, срабатывает инструкция rjmp main, совершается переход на метку main, происходит инициализация и затем программа попадает в бесконечный цикл loop, пока не произойдёт прерывание.

В этом случае, однако, процесс перехода в вектор прерываний начнётся только после завершения текущей инструкции. Текущая инструкция — это безусловный переход rjmp, «вращающий» бесконечный цикл. Эта инструкция занимает два цикла. И в худшем случае на второй вывод порта «D» фронт придёт именно тогда, когда выполнение данной инструкции только началось.

Затем начнётся переход в вектор прерываний. Так как после выполнения подпрограммы внешнего прерывания мы должны вернуться в то же место основной программы, на котором прерывание случилось, переход в вектор прерываний будет неявно предусматривать выполнение четырёх действий:

  • чтение текущего значения счётчика инструкций

  • копирование этого значения в оперативную память, на которую указывает счётчик стека

  • уменьшение счётчика стека на два (так как у ATmega328P двухбайтные инструкции)

  • изменение значения счётчика инструкций на адрес в векторе прерываний, соответствующий конкретному типу прерывания

Всё это происходит автоматически, но занимает четыре цикла.

Затем происходит безусловный переход из вектора прерываний в обработчик прерывания. Теоретически, в нашей задаче можно написать обработчик прямо поверх вектора прерываний. Это позволило бы не производить данный переход и сэкономило бы нам два цикла. Но с точки зрения чистоты кода так делать крайне нежелательно.

Итак, мы находимся в обработчике прерывания и готовы выполнить полезный код. Но есть проблема. После выполнения кода нам необходимо вернуться из прерывания при помощи инструкции reti, которая выполнит все те же операции со счётчиком инструкций, счётчиком стека и обменом данными между регистрами и оперативной памятью, только в обратном порядке.
Соответственно, команда reti занимает также четыре цикла.

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

 Минимальное время реакции прерывания
Минимальное время реакции прерывания

В общем случае, при работе с прерываниями также может понадобится сохранить в стек значений регистра флагов и регистров общего назначения при входе в обработчик. Это делается, чтобы вызов прерывания не портил значения регистров в основном потоке выполнения.
Делается это примерно так:

external_interrupt:
  push r16 ; сохраняем по порядку в стек значения регистров
  push r17
  push r18

  in   r18, SREG ; копируем регистр флагов (SREG) в регистр r18
  push r18       ; и тоже сохраняем в стек

; === начало обработчика прерывания ===
; можем как угодно задействовать регистры r16, r17, r18 и не беспокоиться за SREG
; === конец обработчика прерывания ===

  pop  r18       ; восстанавливаем значение регистра флагов в регистр r18
  out SREG, r18  ; копируем сохранённое значение в регистр флагов

  pop  r18 ; восстанавливаем из стека значения регистров в обратном порядке
  pop  r17
  pop  r16
reti 

Здесь инструкция push копирует значение заданного регистра в оперативную память по адресу из счётчика стека и затем уменьшает значение этого счётчика на единицу.
А инструкция pop, соответственно, делает наоборот: копирует содержимое оперативной памяти из адреса, указанного в счётчике стека, в заданный регистр и затем увеличивает значение этого счётчика на единицу.

Хотя для нашей задачи мы можем предположить, что регистров общего назначения достаточно много, чтобы не заниматься сохранением их значений.

Однако в случае программной реализации протокола JTAG мы неизбежно столкнёмся ещё с парой проблем.

Первая заключается в том, что протокол предусматривает различные действия, как по восходящему фронту по линии TCK, так и по нисходящему. Регистр EICRA позволяет настроить условие срабатывания внешнего прерывания следующим образом:

  • по восходящему фронту

  • по нисходящему фронту

  • по любому фронту

  • при наличии логического нуля

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

Чтобы определить полярность мы можем проверить текущее состояние на линии при помощи инструкции sbis (Skip if Bit in I/O register is Set). Она проверяет указанный бит в указанном регистре ввода‑вывода, и если бит содержит логическую единицу, то следующая инструкция пропускается.

Обработчик прерывания в результате примет следующий вид:

external_interrupt:
  sbis PIND, EXT_POS
  rjmp fall_edge
  rise_edge:
    ; реакция на восходящий фронт
  reti
  fall_edge:
    ; реакция на нисходящий фронт
  reti

Если условие оказалось ложно, то инструкция sbis занимает один цикл. И ещё два цикла занимает инструкция rjmp.
Если условие оказалось истинным, то в данном случае инструкция sbis займёт два цикла, а rjmp окажется пропущен.

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

Сделать это возможно при помощи конструкции, похожей на switch‑case.

Switch‑case

Напомню, как работает конструкция switch‑case в Си:

switch(value){
    case 0:
	//код для значения 0
	//break
    case 1:
	//код для значения 1
	//break
    case 2:
	//код для значения 2
	//break
    case 3:
	//код для значения 3
	//break
    default:
	//код для иных случаев
}

В скобках после switch пишется выражение, которое даёт целочисленный результат. Этот результат поочерёдно сравнивается со значениями, прописанными после каждого блока case. Если в ходе очередного сравнения есть совпадение, то выполняется блок кода для этого случая, а также все блоки нижеследующих случаев.
Необязательным является блок default. Он срабатывает, если не произошло ни одного совпадения.
Зачастую существует необходимость исполнить в случае совпадения только соответствующий блок кода и не исполнять остальные. Тогда после него необходимо написать команду break. Она прервёт выполнение последующих блоков, если таковые имеются.

На ассемблере аналогичную структуру можно сделать следующими инструкциями:

  • cpi (Compare with Immediate), сравнивает значение регистра с константой и выставляет бит равенства в регистре флагов. Выполняется за один цикл.

  • breq (Branch if Equal), производит переход выполнения программы на указанную метку, если бит равенства в регистре флагов установлен в состояние логической единицы. В этом случае выполняется за два цикла. Если бит равенства сброшен в ноль, то тогда данная инструкция занимает один цикл. При этом никаких переходов не происходит.

Код структуры switch‑case на ассемблере будет следующим:

cpi  r18, 0           
breq case_0
cpi  r18, 1
breq case_1
cpi  r18, 2
breq case_2
cpi  r18, 3
breq case_3
rjmp case_default
case_0:
  ; код для значения 0
  ; опционально rjmp break_switch
case_1:
  ; код для значения 1
  ; опционально rjmp break_switch
case_2:
  ; код для значения 2
  ; опционально rjmp break_switch
case_3:
  ; код для значения 3
  ; опционально rjmp break_switch
case_default:
    ; код для иных случаев
break_switch:

Если значение тестируемого регистра соответствует самому последнему case, то при прочих равных, этот код будет исполняться максимально долго. Причина в последовательной проверке всех возможных значений.
В случае если в конкретном case не было выявлено равенство значения регистра и константы, длительность выполнения пары инструкций…

cpi  rXX, Y
breq case_Y

...будет равно двум циклам.
А если равенство было выявлено, то трём.

Учитывая, что конечный автомат модуля JTAG может находиться в одном из 16 состояний, можно сказать, что если мы задействуем обсуждаемую структуру, то к худшему случаю прибавится 15×2+1×3=33 цикла.

Есть, однако, гораздо более быстрый способ, если все возможные значения проверяемого регистра идут по порядку (ну или имеют незначительные пробелы в этом порядке).
Мы можем вместо проверки совершить переход вперёд на то количество инструкций, которое указано в тестируемом регистре. А затем из нового места произвести переход в обработчик соответствующего case.

Для этого нам придётся воспользоваться инструкцией ijmp (Indirect Jump). Она производит переход по 16‑битному адресу, указанному в паре регистров r30:r31, иначе называемой регистром Z:

ldi r19, 0
ldi ZL, low(switch_label)
ldi ZH, high(switch_label)
add ZL, r18
adc ZH, r19
ijmp
switch_label:
rjmp case_0
rjmp case_1
rjmp case_2
rjmp case_3
case_0:
  ; код для значения 0
  ; опционально rjmp break_switch
case_1:
  ; код для значения 1
  ; опционально rjmp break_switch
case_2:
  ; код для значения 2
  ; опционально rjmp break_switch
case_3:
  ; код для значения 3
  ; опционально rjmp break_switch
break_switch:

В приведённом примере мы загружаем в пару регистров ZL и ZH значение метки switch_label (адреса меток — 16‑битные).
Затем прибавляем к нему значение тестируемого регистра.
Так как регистр Z является 16‑битным, необходимо предусмотреть ситуацию, когда при добавлении 8‑битного значения к младшему байту регистра Z результат окажется больше, чем 255. То есть произойдёт переполнение (которое приведёт к автоматической установке флага переноса в регистре флагов). По этой причине применяется пара инструкций add/adc: простое сложение, и сложение с учётом флага переноса.

Мы можем заранее (например, в ходе инициализации) выделить регистр под хранение нуля и пару регистров под хранение значения метки switch_label. А в самой конструкции switch‑case использовать инструкцию movw, которая способна копировать значения из одной пары регистров в другую пару за один цикл.

ldi r23, 0
ldi r24, low(switch_label)
ldi r25, high(switch_label)
<...>
movw ZH:ZL, r24:r25
add ZL, r18
adc ZH, r23
ijmp
switch_label:
rjmp case_0
rjmp case_1
rjmp case_2
rjmp case_3
case_0:
  ; код для значения 0
  ; опционально rjmp break_switch
case_1:
  ; код для значения 1
  ; опционально rjmp break_switch
case_2:
  ; код для значения 2
  ; опционально rjmp break_switch
case_3:
  ; код для значения 3
  ; опционально rjmp break_switch
break_switch:

Тогда movw, add и adc займут по одному циклу, а ijmp и rjmp по два цикла. Итого семь циклов. Причём абсолютно независимо от количества case.

Как уже говорилось, конструкция switch‑case применяется для определения состояния конечного автомата. Но помимо неё, каждая реакция на восходящий фронт должна предусматривать изменение этого состояния.
Если мы сохраняем код/номер состояния, то на его изменение уйдёт три дополнительных цикла:

ldi  r16, NEXT_STATE_IF_TMS1 ; следующее состояние, если TMS=1
sbis TMS_REG, TMS_PIN        ; пропускаем инструкцию, если TMS=1
ldi  r16, NEXT_STATE_IF_TMS0 ; если TMS=0, инструкция не пропущена, переписываем состояние

Однако можно обойтись и вовсе без конструкции switch‑case. Для этого в конце каждого обработчика состояния следует сохранять не код состояния, а адрес следующего обработчика, применяя затем инструкцию ijmp для перехода по этому адресу.

Хотя этот метод и дешевле по циклам, чем switch‑case, но он не бесплатен. Адреса обработчиков являются 16‑битными. Поэтому выполнение кода займёт шесть циклов:

ldi ZL, low(ADDR_IF_TMS1)
ldi ZH, high(ADDR_IF_TMS1)
sbis TMS_REG, TMS_PIN
ldi ZL, low(ADDR_IF_TMS0)
sbis TMS_REG, TMS_PIN
ldi ZH, high(ADDR_IF_TMS0)

Поллинг

Суммируя всё вышесказанное, если мы планируем использовать прерывания, то неизбежные накладные расходы для худшего случая будут следующими:

  • 4 цикла инструкции reti от предыдущего прерывания (для худшего случая)

  • 4 цикла на уход в прерывание

  • 3 цикла на определение полярности фронта (для худшего случая)

  • 6 цикла на обработку состояния конечного автомата

Итого 17 циклов.

Есть способ делать всё то же самое более чем в три раза быстрее:

loop:
  rise_edge:
    sbis PIND, JTCK_POS
    rjmp rise_edge
      ; реакция на восходящий фронт
  fall_edge:
    sbic PIND, JTCK_POS
    rjmp fall_edge;
      ; реакция на нисходящий фронт
rjmp loop

Здесь пара sbis(sbic)/rjmp вращает цикл в ожидании фронта. При обнаружении фронта происходит выход из цикла и попадание в обработчик. После выполнения обработчика начинается ожидание фронта противоположной полярности. Подобный механизм, предусматривающий постоянный опрос чего‑либо в ожидании изменения называется, поллинг.

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

 Минимальное время реакции поллинга
Минимальное время реакции поллинга

Это, собственно, всё.
Никаких уходов в подпрограммы‑обработчики и возвратов из них.
Никакой проверки полярности — она обеспечивается сама собой.
И даже любые выяснения текущего состояния можно исключить.
В отличие от обработчика прерывания, который может быть только один, детекторов фронта sbis(sbic)/rjmp может быть сколько угодно. А поэтому мы можем отождествить состояние конечного автомата JTAG с текущим местонахождением в программе. То есть в неявной форме хранить состояние конечного автомата в счётчике инструкций.

Для улучшения структуры кода имеет смысл вынести пары sbis(sbic)/rjmp в отдельные макросы. Метки внутри макросов являются локальными. То есть адреса, соответствующие меткам макроса, будут пересчитаны именно для того места, куда макрос будет подставлен.

.macro WAIT_JTCK_RISE
  rise_edge:
  sbis JTCK_PORT, JTCK_POS
  rjmp rise_edge
.endm
.macro WAIT_JTCK_FALL
  fall_edge:
  sbic JTCK_PORT, JTCK_POS
  rjmp fall_edge
.endm
<...>
SELECT_DR:
  WAIT_JTCK_FALL
    ; код для нисходящего фронта в состоянии select-dr
  WAIT_JTCK_RISE
    ; код для восходящего фронта в состоянии select-dr
CAPTURE_DR:
  WAIT_JTCK_FALL
    ; код для нисходящего фронта в состоянии capture-dr
  WAIT_JTCK_RISE
    ; код для восходящего фронта в состоянии capture-dr
SHIFT_DR:
  WAIT_JTCK_FALL
    ; код для нисходящего фронта в состоянии shift-dr
  WAIT_JTCK_RISE
    ; код для восходящего фронта в состоянии shift-dr
<...>

Внутри состояний

Модуль JTAG запоминает данные в наборе сдвиговых регистров. Их заполнение подразумевает… сдвиг.

В рамках рассматриваемой архитектуры имеется инструкция ror (Rotate Right through Carry), которая предоставляет возможность формировать достаточно большой сдвиговый регистр из отдельных 8‑битных регистров.
Данная инструкция запоминает младший бит 8‑битного регистра и сдвигает содержимое регистра в его сторону. Затем (но в том же цикле) помещает содержимое флага переноса в старший бит регистра, а потом заносит во флаг переноса тот запомненный младший бит.

Таким образом можно представить, например, 32‑битный регистр DATA_REG, состоящий из четырёх 8‑битных регистров, и сдвинуть его при помощи такой последовательности инструкций:

#define DATA_REG0  r19
#define DATA_REG1  r20
#define DATA_REG2  r21
#define DATA_REG3  r22
<...>
ror DATA_REG3
ror DATA_REG2
ror DATA_REG1
ror DATA_REG0

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

Если нам необходимо при помощи JTAG лишь помигать светодиодом на порте, который дан нам в безраздельное пользование, то копирование из регистра периферийного сканирования в порт займёт один цикл:

out PORTB, DATA_REG0

Если же на порте есть линии, менять состояние которых недопустимо, то придётся сделать заметно больше действий:

  • сохранить текущее значение выходных битов порта в регистр

  • в сохранённой копии сбросить в ноль состояние всех битов, которые допустимо менять в порте

  • в регистре‑источнике сбросить в ноль состояние всех битов, которые нельзя менять в порте

  • совместить значения неизменяемых и изменяемых бит в одном регистре при помощи побитовой операции ИЛИ

  • обновить значения выходных битов в порте

.equ MASK    = 0b00000111
<...>
ldi r17,   MASK
<...>
in  r16,   PORTB
cbr r16,   MASK
and DATA_REG0, r17 
or  r16,   DATA_REG0
out PORTB, r16

Это уже пять циклов.

Если нам требуется также устанавливать на выводах высокоимпедансное состояние (например, для двунаправленных шин), то все те же операции придётся совершить для регистра DDRB.

У микроконтроллера ATmega328P всего три порта: B, C и D. Поэтому для максимально полной работы потребуется регистр периферийного сканирования длиной в девять байт: PORTx, PINx и DDRx для каждого порта. И 9×5=45 циклов после нисходящего фронта в состоянии UPDATE‑DR.

Кроме этого, после выполнения копирования потребуется ещё 3‑4 цикла на переход в другое состояние.

Таким образом, для наиболее канонического варианта реализации JTAG, худший случай (с учётом задержки на обнаружение фронтов, задержки на переход в другое состояние и 50% заполняемости тактового сигнала TCK) будет составлять (45+5+4)×2=108 циклов. При тактовой частоте микроконтроллера в 16 МГц максимальная тактовая частота JTAG составит 148 кГц.

Если же ситуация такова, что время переноса данных из регистра периферийного сканирования в порты можно сократить до минимума, то следующим лимитирующим фактором становится структура switch‑case. Она должна направить выполнение кода на обработку того или иного регистра данных в зависимости от содержимого регистра инструкций.

Решение о выборе регистра данных принимается в состоянии SELECT‑DR. Само выполнение ветвления при помощи структуры switch‑case осуществляется, как уже говорилось, за семь циклов.
Но здесь есть тонкий момент. В каждом состоянии конечного автомата JTAG есть пара детекторов фронтов: сперва нисходящего, затем восходящего. После восходящего фронта выполняется проверка линии TMS и переход в новое состояние, например так:

#define JUMP_TO rjmp
.macro CHECK_JTMS
   sbic JTMS_PORT, JTMS_POS
.endm
<...>
PAUSE_DR_IDCODE:
  WAIT_JTCK_FALL
  WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO EXIT2_DR_IDCODE
      JUMP_TO PAUSE_DR_IDCODE

Этот переход занимает три‑четыре цикла, которые прибавятся к семи циклам switch‑case. Поэтому, чтобы уменьшить длительность худшего случая, имеет смысл выполнить switch‑case после нисходящего фронта. А затем, после восходящего фронта, либо продолжить выполнение по определённой ветке, либо уйти в состояние SELECT‑IR:

.macro REG_INIT
  ldi ZERO_REG, 0
  ldi SW_DR_LREG, low(switch_dr_label)
  ldi SW_DR_HREG, high(switch_dr_label)
.endm
.macro INSTR_SWITCH_DR
  movw ZH:ZL, SW_DR_HREG:SW_DR_LREG
  add ZL, INSTR_REG
  adc ZH, ZERO_REG
  ijmp
.endm
<...>
REG_INIT
<...>
SELECT_DR:
  WAIT_JTCK_FALL
    INSTR_SWITCH_DR
    switch_dr_label:
      JUMP_TO SELECT_DR_BYPASS ; действие по умолчанию (код 0)
      JUMP_TO SELECT_DR_IDCODE ; код инструкции 1 (IDCODE)
      JUMP_TO SELECT_DR_BYPASS ; код инструкции 2 (BYPASS)
      JUMP_TO SELECT_DR_SAMPLE ; код инструкции 3 (SAMPLE)
      JUMP_TO SELECT_DR_EXTEST ; код инструкции 4 (EXTEST)
      JUMP_TO SELECT_DR_BYPASS ; действие по умолчанию (код 5)
      JUMP_TO SELECT_DR_BYPASS ; действие по умолчанию (код 6)
      JUMP_TO SELECT_DR_BYPASS ; действие по умолчанию (код 7)
SELECT_DR_IDCODE:
  WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_IR
      JUMP_TO CAPTURE_DR_IDCODE
SELECT_DR_BYPASS:
  WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_IR
      JUMP_TO CAPTURE_DR_BYPASS
SELECT_DR_SAMPLE:
  WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_IR
      JUMP_TO CAPTURE_DR_SAMPLE
SELECT_DR_EXTEST:
  WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_IR
      JUMP_TO CAPTURE_DR_EXTEST

Стоит отметить, что при использовании приведённой конструкции switch‑case нам следует определить действия для всех возможных значений регистра инструкций. Пункт «d» раздела 8.1.1 стандарта IEEE1149.1 говорит следующее:

Instruction binary codes that are not otherwise required to provide control of test logic shall be equivalent to the BYPASS instruction.

То есть, если код в регистре инструкций не совпадает ни с одной инструкцией, то такое состояние должно обрабатываться так же, как если бы это была инструкция BYPASS.

После оптимизации, узким местом станет состояние SHIFT‑DR. Даже если регистр периферийного сканирования будет мал, регистр идентификационного кода всё равно займёт 32 бита (четыре регистра). Непосредственно сдвиг 32‑битного регистра займёт 4+3 цикла.
Проблема в том, что это происходит на нисходящем фронте, а значит, к семи циклам прибавляются ещё три‑четыре цикла потенциального перехода и пять циклов детектора фронта. Итого 16×2=32 цикла для худшего случая на один такт TCK:

.macro JTDO_DR_OUT
  bst DATA_REG0, 0
  in  r16, JTDO_PORT
  bld r16, JTDO_POS
  out JTDO_PORT, r16
.endm
.macro SHIFT_IDCODE
  in  r16, JTDI_PORT
  bst r16, JTDI_POS 
  ror DATA_REG3
  ror DATA_REG2
  ror DATA_REG1
  ror DATA_REG0
  bld DATA_REG3, 7
.endm
.macro JTDO_HIZ
   cbi JTDO_DDR,  JTDO_POS
   cbi JTDO_PORT, JTDO_POS
.endm
.macro JTDO_PP
   sbi JTDO_DDR, JTDO_POS
.endm
<...>
SHIFT_DR_IDCODE:
    JTDO_PP ; переключение выхода TDO в состояние push‑pull
  WAIT_JTCK_FALL
    JTDO_DR_OUT
  WAIT_JTCK_RISE
    SHIFT_IDCODE
    CHECK_JTMS
      JUMP_TO EXIT1_DR_IDCODE
      JUMP_TO SHIFT_DR_IDCODE
  EXIT1_IR:
	WAIT_JTCK_FALL
	  JTDO_HIZ ; переключение выхода TDO в состояние HiZ
<...>

Технически, можно выгадать пару циклов, перераспределив операции между обработкой нисходящего фронта и восходящего фронта. Однако радикальным действием будет удаление детектора восходящего фронта из состояния SHIFT‑DR. Тогда программная реализация не сможет работать на низких частотах, но самый длинный такт TCK сократится до 20 циклов — как за счёт отсутствия детектора фронта, так и за счёт перераспределения времени между полутактами.

Полный код

;        ====== MACRO, CONST and DEF ======

#define JUMP_TO rjmp

#define INSTR_REG  r18
#define DATA_REG0  r19
#define DATA_REG1  r20
#define DATA_REG2  r21
#define DATA_REG3  r22

#define BOUNDARY0  r23
#define BOUNDARY1  r24

#define ZERO_REG   r25
#define SW_DR_LREG r26
#define SW_DR_HREG r27

.equ JTMS_POS  = 2
.equ JTMS_MASK = (1 << JTMS_POS)
.equ JTMS_PORT = PIND
.equ JTMS_DDR  = DDRD

.equ JTCK_POS  = 3
.equ JTCK_MASK = (1 << JTCK_POS)
.equ JTCK_PORT = PIND
.equ JTCK_DDR  = DDRD

.equ JTDI_POS  = 4
.equ JTDI_MASK = (1 << JTDI_POS)
.equ JTDI_PORT = PIND
.equ JTDI_DDR  = DDRD

.equ JTDO_POS  = 5
.equ JTDO_MASK = (1 << JTDO_POS)
.equ JTDO_PORT = PORTD
.equ JTDO_DDR  = DDRD

.equ INSTR_LENGTH = 3
.equ INSTR_IDCODE = 0x01
.equ INSTR_EXTEST = 0x02
.equ INSTR_BYPASS = 0x07
.equ INSTR_SAMPLE = 0x04

.equ IDCODE0 = 0x3F
.equ IDCODE1 = 0x00
.equ IDCODE2 = 0xFE
.equ IDCODE3 = 0xCA

.macro INIT_MAIN_CLK
   ;
.endm
.macro INIT_REG
  ldi ZERO_REG, 0
  ldi SW_DR_LREG, low(switch_dr_label)
  ldi SW_DR_HREG, high(switch_dr_label)
.endm
.macro INIT_JTMS
   cbi JTMS_DDR, JTMS_POS
.endm
.macro INIT_JTCK
   cbi JTCK_DDR, JTCK_POS
.endm
.macro INIT_JTDI
   cbi JTDI_DDR, JTDI_POS
.endm
.macro INIT_JTDO
   cbi JTDO_DDR, JTDO_POS ; TDO starts in HiZ 
.endm
.macro INIT_GPIO
   cbi DDRB,  4
   cbi PORTB, 4
   sbi DDRB,  5
   cbi PORTB, 5
.endm
.macro WAIT_JTCK_RISE
  rise_edge:
  sbis JTCK_PORT, JTCK_POS
  rjmp rise_edge
.endm
.macro WAIT_JTCK_FALL
  fall_edge:
  sbic JTCK_PORT, JTCK_POS
  rjmp fall_edge
.endm
.macro JTDO_HIZ
   cbi JTDO_DDR,  JTDO_POS
   cbi JTDO_PORT, JTDO_POS
.endm
.macro JTDO_PP
   sbi JTDO_DDR, JTDO_POS
.endm
.macro JTDO_DR_OUT
  bst DATA_REG0, 0
  in  r16, JTDO_PORT
  bld r16, JTDO_POS
  out JTDO_PORT, r16
.endm
.macro SHIFT_IDCODE
  in  r16, JTDI_PORT
  bst r16, JTDI_POS 
  ror DATA_REG3
  ror DATA_REG2
  ror DATA_REG1
  ror DATA_REG0
  bld DATA_REG3, 7
.endm
.macro SHIFT_BYPASS
  in  r16, JTDI_PORT
  bst r16, JTDI_POS 
  ror DATA_REG0
  bld DATA_REG0, 0
.endm
.macro SHIFT_SAMPLE
  in  r16, JTDI_PORT
  bst r16, JTDI_POS 
  ror DATA_REG2
  ror DATA_REG1
  ror DATA_REG0
  bld DATA_REG2, 7
.endm
.macro SHIFT_EXTEST
  in  r16, JTDI_PORT
  bst r16, JTDI_POS 
  ror DATA_REG2
  ror DATA_REG1
  ror DATA_REG0
  bld DATA_REG2, 7
.endm
.macro JTDO_IR_OUT
  bst INSTR_REG, 0 
  in  r16, JTDO_PORT
  bld r16, JTDO_POS
  out JTDO_PORT, r16
.endm
.macro SHIFT_INSTR
  in  r16, JTDI_PORT
  bst r16, JTDI_POS 
  ror INSTR_REG
  bld INSTR_REG, (INSTR_LENGTH - 1)
.endm
.macro UPDATE_INSTR
  ldi r16, 0b00000111
  and INSTR_REG, r16
.endm
.macro CHECK_JTMS
   sbic JTMS_PORT, JTMS_POS
.endm
.macro LOAD_IDCODE
  ldi DATA_REG0, IDCODE0
  ldi DATA_REG1, IDCODE1
  ldi DATA_REG2, IDCODE2
  ldi DATA_REG3, IDCODE3
.endm
.macro LOAD_BYPASS
  ldi DATA_REG0, 0
.endm
.macro LOAD_SAMPLE
  in DATA_REG2, PINB
.endm
.macro LOAD_EXTEST
  in DATA_REG2, PINB
.endm
.macro LOAD_INSTR
  ldi INSTR_REG, INSTR_IDCODE
.endm
.macro INSTR_SWITCH_DR
  movw ZH:ZL, SW_DR_HREG:SW_DR_LREG
  add ZL, INSTR_REG
  adc ZH, ZERO_REG
  ijmp
.endm
.macro UPDATE_BOUNDARY
  mov BOUNDARY0, DATA_REG0
  mov BOUNDARY1, DATA_REG1
.endm
.macro SET_OUTPUT
  out PORTB, BOUNDARY0
  out DDRB,  BOUNDARY1
.endm
.macro INIT_ALL
  INIT_MAIN_CLK
  INIT_REG
  INIT_JTMS
  INIT_JTCK
  INIT_JTDI
  INIT_JTDO
  INIT_GPIO
.endm

;        ====== JTAG LOGIC ======

reset:
  INIT_ALL

  TEST_LOGIC_RESET:
    WAIT_JTCK_FALL
      LOAD_INSTR
	  JTDO_HIZ
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO TEST_LOGIC_RESET
      JUMP_TO RUN_TEST_IDLE
  RUN_TEST_IDLE:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_DR
      JUMP_TO RUN_TEST_IDLE
  SELECT_DR:
	WAIT_JTCK_FALL
	  INSTR_SWITCH_DR
      switch_dr_label:
	  JUMP_TO SELECT_DR_BYPASS // default    (code = 0)
	  JUMP_TO SELECT_DR_IDCODE // inctruction code = 1 IDCODE
	  JUMP_TO SELECT_DR_EXTEST // inctruction code = 2 EXTEST
	  JUMP_TO SELECT_DR_SAMPLE // inctruction code = 3 SAMPLE
	  JUMP_TO SELECT_DR_BYPASS // default    (code = 4)
 	  JUMP_TO SELECT_DR_BYPASS // default    (code = 5)
	  JUMP_TO SELECT_DR_BYPASS // default    (code = 6)
	  JUMP_TO SELECT_DR_BYPASS // inctruction code = 7 BYPASS
  SELECT_DR_IDCODE:
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_IR
      JUMP_TO CAPTURE_DR_IDCODE
  SELECT_DR_BYPASS:
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_IR
      JUMP_TO CAPTURE_DR_BYPASS
  SELECT_DR_SAMPLE:
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_IR
      JUMP_TO CAPTURE_DR_SAMPLE
  SELECT_DR_EXTEST:
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_IR
      JUMP_TO CAPTURE_DR_EXTEST

// === IDCODE branch ===
  CAPTURE_DR_IDCODE:
	WAIT_JTCK_FALL
	  LOAD_IDCODE
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO EXIT1_DR_IDCODE
      JUMP_TO SHIFT_DR_IDCODE
  SHIFT_DR_IDCODE:
      JTDO_PP
	WAIT_JTCK_FALL
	  JTDO_DR_OUT
	WAIT_JTCK_RISE
	  SHIFT_IDCODE
    CHECK_JTMS
      JUMP_TO EXIT1_DR_IDCODE
      JUMP_TO SHIFT_DR_IDCODE
  EXIT1_DR_IDCODE:
	WAIT_JTCK_FALL
	  JTDO_HIZ
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO UPDATE_DR_IDCODE
      JUMP_TO PAUSE_DR_IDCODE
  PAUSE_DR_IDCODE:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO EXIT2_DR_IDCODE
      JUMP_TO PAUSE_DR_IDCODE
  EXIT2_DR_IDCODE:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO UPDATE_DR_IDCODE
      JUMP_TO SHIFT_DR_IDCODE
  UPDATE_DR_IDCODE:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_DR
      JUMP_TO RUN_TEST_IDLE

// === BYPASS branch ===
  CAPTURE_DR_BYPASS:
	WAIT_JTCK_FALL
	  LOAD_BYPASS
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO EXIT1_DR_BYPASS
      JUMP_TO SHIFT_DR_BYPASS
  SHIFT_DR_BYPASS:
      JTDO_PP
	WAIT_JTCK_FALL
	  JTDO_DR_OUT
	WAIT_JTCK_RISE
	  SHIFT_BYPASS
    CHECK_JTMS
      JUMP_TO EXIT1_DR_BYPASS
      JUMP_TO SHIFT_DR_BYPASS
  EXIT1_DR_BYPASS:
	WAIT_JTCK_FALL
      JTDO_HIZ
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO UPDATE_DR_BYPASS
      JUMP_TO PAUSE_DR_BYPASS
  PAUSE_DR_BYPASS:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO EXIT2_DR_BYPASS
      JUMP_TO PAUSE_DR_BYPASS
  EXIT2_DR_BYPASS:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO UPDATE_DR_BYPASS
      JUMP_TO SHIFT_DR_BYPASS
  UPDATE_DR_BYPASS:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE;
    CHECK_JTMS
      JUMP_TO SELECT_DR
      JUMP_TO RUN_TEST_IDLE

// === SAMPLE branch ===
  CAPTURE_DR_SAMPLE:
	WAIT_JTCK_FALL
	  LOAD_SAMPLE
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO EXIT1_DR_SAMPLE
      JUMP_TO SHIFT_DR_SAMPLE
  SHIFT_DR_SAMPLE:
      JTDO_PP
	WAIT_JTCK_FALL;
	  JTDO_DR_OUT
	WAIT_JTCK_RISE;
	  SHIFT_SAMPLE
    CHECK_JTMS
      JUMP_TO EXIT1_DR_SAMPLE
      JUMP_TO SHIFT_DR_SAMPLE
  EXIT1_DR_SAMPLE:
	WAIT_JTCK_FALL
      JTDO_HIZ
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO UPDATE_DR_SAMPLE
      JUMP_TO PAUSE_DR_SAMPLE
  PAUSE_DR_SAMPLE:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO EXIT2_DR_SAMPLE
      JUMP_TO PAUSE_DR_SAMPLE
  EXIT2_DR_SAMPLE:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO UPDATE_DR_SAMPLE
      JUMP_TO SHIFT_DR_SAMPLE
  UPDATE_DR_SAMPLE:
	WAIT_JTCK_FALL
	  UPDATE_BOUNDARY
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_DR
      JUMP_TO RUN_TEST_IDLE
// === EXTEST branch ===
  CAPTURE_DR_EXTEST:
	WAIT_JTCK_FALL
	  LOAD_EXTEST
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO EXIT1_DR_EXTEST
      JUMP_TO SHIFT_DR_EXTEST
  SHIFT_DR_EXTEST:
      JTDO_PP
	WAIT_JTCK_FALL
	  JTDO_DR_OUT
	WAIT_JTCK_RISE
      SHIFT_EXTEST
    CHECK_JTMS
      JUMP_TO EXIT1_DR_EXTEST
      JUMP_TO SHIFT_DR_EXTEST
  EXIT1_DR_EXTEST:
	WAIT_JTCK_FALL
      JTDO_HIZ
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO UPDATE_DR_EXTEST
      JUMP_TO PAUSE_DR_EXTEST
  PAUSE_DR_EXTEST:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO EXIT2_DR_EXTEST
      JUMP_TO PAUSE_DR_EXTEST
  EXIT2_DR_EXTEST:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO UPDATE_DR_EXTEST
      JUMP_TO SHIFT_DR_EXTEST
  UPDATE_DR_EXTEST:
	WAIT_JTCK_FALL
	  UPDATE_BOUNDARY
	  SET_OUTPUT
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_DR
      JUMP_TO RUN_TEST_IDLE
  SELECT_IR:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO TEST_LOGIC_RESET
      JUMP_TO CAPTURE_IR
  CAPTURE_IR:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
	LOAD_INSTR
    CHECK_JTMS
      JUMP_TO EXIT1_IR
      JUMP_TO SHIFT_IR
  SHIFT_IR:
      JTDO_PP
	WAIT_JTCK_FALL
      JTDO_IR_OUT
	WAIT_JTCK_RISE
	  SHIFT_INSTR
    CHECK_JTMS
      JUMP_TO EXIT1_IR
      JUMP_TO SHIFT_IR
  EXIT1_IR:
	WAIT_JTCK_FALL
	  JTDO_HIZ
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO UPDATE_IR
      JUMP_TO PAUSE_IR
  PAUSE_IR:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO EXIT2_IR
      JUMP_TO PAUSE_IR
  EXIT2_IR:
	WAIT_JTCK_FALL
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO UPDATE_IR
      JUMP_TO SHIFT_IR
  UPDATE_IR:
	WAIT_JTCK_FALL
	  UPDATE_INSTR
	WAIT_JTCK_RISE
    CHECK_JTMS
      JUMP_TO SELECT_DR
      JUMP_TO RUN_TEST_IDLE



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

То есть чтобы запустить данный код на другой архитектуре надо переписать только макросы.

Мигающий светодиод

Скомпилируем приведённый код, запишем его в микроконтроллер, и создадим проект в TopJTAG Probe, как описано здесь.

Для работы нам также потребуется файл BSDL. О его написании подробно рассказано в этой статье. Здесь мы просто воспользуемся заранее подготовленным кодом, создадим файл BSDL, и загрузим его в проект TopJTAG Probe.

Файл BSDL для Arduino
entity ARDUINO is
	generic (PHYSICAL_PIN_MAP : string);
	port  (
		TMS: in  bit;
                TCK: in  bit;
		TDI: in  bit;		
		TDO: out bit;
		VCC: linkage bit_vector(0 to 1);
		GND: linkage bit_vector(0 to 1);
                LED: out bit;
                BTN: in  bit;
		NC:  linkage bit_vector(0 to 9)
		);
	use STD_1149_1_2001.all;
	attribute COMPONENT_CONFORMANCE of ARDUINO : entity is "STD_1149_1_2001";
	attribute PIN_MAP of ARDUINO : entity is PHYSICAL_PIN_MAP;
	constant  SO28:PIN_MAP_STRING:=
		"TMS:	4, " &
		"TCK:	5, " &
		"TDI:	6, " &
		"TDO:	11," &
		"VCC:	(7,20), " &
		"GND:	(8,22), " &
		"LED:	19," &
		"BTN:	18," &
		"NC:	(1,2,3,9,10,12,13,14,15,16,17,21,23,24,25,26,27,28) ";
	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 ARDUINO : entity is 3;
	attribute INSTRUCTION_OPCODE of ARDUINO : entity is
		"IDCODE (001)," &
		"EXTEST (010)," &
		"SAMPLE (011)," &
		"BYPASS (111)";
	attribute INSTRUCTION_CAPTURE of ARDUINO : entity is "00000001";
	attribute IDCODE_REGISTER of ARDUINO : entity is
		"1100" &
		"1010111111100000" & 
		"00000011111" &      
		"1";
	attribute REGISTER_ACCESS of ARDUINO : entity is
		"DEVICE_ID (IDCODE)," &
		"BYPASS	   (BYPASS)," &	
		"BOUNDARY  (EXTEST, SAMPLE)";
	attribute BOUNDARY_LENGTH of ARDUINO : entity is 24;
	attribute BOUNDARY_REGISTER of ARDUINO : entity is
		"0   (BC_4, *,  internal, X)," &
		"1   (BC_4, *,  internal, X)," &
		"2   (BC_4, *,  internal, X)," &
		"3   (BC_4, *,  internal, X)," &
		"4   (BC_4, *,  internal, X)," &
		"5   (BC_1, LED, output3, X, 13, 0, Z)," & -- out PORTB5 push-pull
		"6   (BC_4, *,  internal, X)," &
		"7   (BC_4, *,  internal, X)," &
		"8   (BC_4, *,  internal, 0)," &
		"9   (BC_4, *,  internal, 0)," &
		"10  (BC_4, *,  internal, 0)," &
		"11  (BC_4, *,  internal, 0)," &
		"12  (BC_4, *,  internal, 0)," &
		"13  (BC_1, *,   control, 1)," &           -- out PORTB5 HiZ
		"14  (BC_4, *,  internal, 0)," &
		"15  (BC_4, *,  internal, 0)," &
		"16  (BC_4, *,  internal, X)," &
		"17  (BC_4, *,  internal, X)," &
		"18  (BC_4, *,  internal, X)," &
		"19  (BC_4, *,  internal, X)," &
		"20  (BC_1, BTN,   input, X)," &           -- in PORTB4
		"21  (BC_4, *,  internal, X)," &
		"22  (BC_4, *,  internal, X)," &
		"23  (BC_4, *,  internal, X)" ;
end ARDUINO;

Для данного примера у файла будет нетипичная, но вполне допустимая особенность. Обычно в описании регистра периферийного сканирования строки отвечающие за работу конкретного вывода группируются вместе (идут друг за другом).

attribute BOUNDARY_REGISTER of ARDUINO : entity is
  <...>
  "16   (BC_1, LED, output3, X, 17, 0, Z)," &  -- выход PORTB5 push-pull 
  "17   (BC_1, *,   control, 1)," &            -- выход PORTB5 HiZ
  "18   (BC_1, BTN,   input, X)," &            -- вход  PORTB4
  <...>

У нас же, ввиду большего удобства копирования данных из регистра периферийного сканирования в регистры PORTB, DDRB и PINB, группировка будет по назначениям:

  • сначала строки, отвечающие за выходные значения каждого вывода в порте

  • затем строки, отвечающие за направления выводов

  • и в конце строки, отвечающие за чтение выводами входных сигналов

Теперь мы можем соединиться через интерфейс JTAG с Arduino на скорости 500 кГц и помигать светодиодом:

Польза подобного действия весьма дискуссионна. Но, пожалуй, это самая быстрая программная реализация JTAG на Arduino.

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


  1. datacompboy
    21.05.2024 15:33

    (картинка с троллейбусом)

    Прекрасная реализация..! :) Правда, как мне кажется, правильный вопрос на предложенный вопрос должен быть "А вам зачем?!" потому что должно быть незачем... :)


    1. Flammmable Автор
      21.05.2024 15:33
      +1

      Размышляя над задачей, я предположил как минимум одно непротиворечивое (но достаточно надуманное) практическое оправдание.

      Однако я считаю, что данный пример может быть неплохим бенчмарком. Вроде задачи восьми ферзей, но для реалтайма. То есть при фиксированной частоте тактирования какой максималтный битрейт сможет выдать микроконтроллер на заданной архитектуре через интерфейс JTAG.

      P.S. Субъективно, я не слишком положительно отношусть к людям, с разбегу спрашивающих "А вам зачем?". На эту тему у меня есть отделтная статья.


      1. datacompboy
        21.05.2024 15:33

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

        А если не секрет, какой непротиворечивый юзкейс может быть? (впрочем, один я знаю -- обязательное соответствие определённому, неизменяемому, чек-листу; вообще он уже достаточно весом чтоб делать "надо так надо").

        От слова "статья" ссылка отвалилась -- можно повторить?

        p.s.: я всегда спрашиваю "а вам зачем?" даже когда есть идеи зачем -- потому, что я ни раз делал что-то, что оказывалось совсем не тем, что требуется; как в том анекдоте про кастрацию...


        1. Flammmable Автор
          21.05.2024 15:33
          +1

          1. datacompboy
            21.05.2024 15:33
            +1

            Контекст важен, да. В прямом-личном общении я всегда стараюсь узнать как можно больше контекста.


  1. VT100
    21.05.2024 15:33
    +2

    В этой статье - всё прекрасно. Пожалуйста, продолжайте.

    Если допустить перерасход памяти программ, то можно "усушить" лишнюю таблицу переходов

    switch_label:

    rjmp case_0

    rjmp case_1

    rjmp case_2

    rjmp case_3

    при использовании ijmp, использовав грануляцию памяти согласно максимальному размеру ветви и используя

    add ZL, r18*case_chunk_size

    adc ZH, zero_register


    1. Flammmable Автор
      21.05.2024 15:33
      +1

      В этой статье - всё прекрасно. Пожалуйста, продолжайте.

      Спасибо :) Я и не собирался останавливаться ;)


  1. JackKatch
    21.05.2024 15:33

    "В скобках после switch пишется выражение, которое даёт целочисленный
    результат. Этот результат поочерёдно сравнивается со значениями,
    прописанными после каждого блока case." Возможно для микроконтроллера это так (я не знаю, нужно проверять). А в общем случае это не так. Конструкция switch предназначена для перехода, за одно сравнение, на соответствующий вариант case. Если вариантов мало (например меньше 4-х), то очевидно последовательное сравнение с каждой меткой case не будет сильно дольше и компилятор не генерирует код перехода за одно сравнение. Вообще статья хорошая, интересная.


    1. Flammmable Автор
      21.05.2024 15:33
      +1

      Конструкция switch предназначена для перехода, за одно сравнение, на соответствующий вариант case

      С точки зрения ассемблерных инструкций, если используются инструкции сравнения, то они производят сравнение только пары операндов. И тут два варианта:
      1) последовательный перебор вариантов
      2) бинарное древо/бинарный поиск

      Бинарное древо имеет определённые накладные расходы и в сравнении с последовательным перебором оно начинает выигрывать начиная лишь с определённого количества case.

      Но. При этом количестве case-ов бинарное древо, выиграв у последовательного перебора, проиграет (по скорости) конструкции на основе косвенного перехода.

      Если же в конструкции switch-case используется инструкция косвенного перехода, то здесь не будет никаких сравнений.

      Именно в приведённом в статье примере switch-case на косвенном переходе работает великолепно. Однако в общем случае у него есть ахиллесова пята - что если у нас совокупность возможных значений тестируемой переменной является сильно разреженной? Что если предполагается, например, три case: 1, 2 и 100500? В таком случае switch-case, конечно, сработает столь же быстро, но таблица переходов получится огромного размера. Причём на 99,999% она будет заполнена пустотой.

      Поэтому я не вполне понимаю написанное вами. Что значит "за одно сравнение" перейти на "соответствующий вариант case"? Что вы имеете ввиду?


      1. datacompboy
        21.05.2024 15:33
        +1

        Когда-то давным давно, когда земля была раскаленным шаром, а C -- макроассемблером на стероидах, switch это был способ задать таблицу переходов в исходнике (а не просто синтаксическим сахаром). Но да, даже тогда, это не было "за одно сравнение", те же проверки на на отрицательные, на больше максимума и так далее необходимы для хотя бы default.

        Сейчас разницы между switch с breakами и if/else if/else if не должно быть, так как оптимизаторы стараются выжать максимум вне зависимости от исходника.


      1. JackKatch
        21.05.2024 15:33

        Товарищ datacompboy прояснил для вас ситуацию, в общих чертах. Дискутировать с ним не буду, достаточно посмотреть исполняемый файл, что бы убедится в том, что switch и if принципиально разные конструкции. Простейший пример это массив меток, результат ключевого выражения это индекс в массиве. Далее выбор метки по индексу и переход. Именно для этого и существует конструкция switch, что бы не перебирать (например) сто ключей. В тривиальных случаях (мало веток case) компилятор считает, что последовательного сравнения хватит и накладные расходы, в виде объёма кода, не нужны. Никакие оптимизации не помогут сделать быстрее чем переход сразу на нужную ветку (если веток много). Ума не приложу, кто вбивает в голову людям, что switch и if это одно и тоже. Вы сами об этом пишете в своём комментарии. А тонкости, что мол сравнения нет... Ну к чему это? Поспорить? Повторюсь , статья хорошая. А тема switch для меня как красная тряпка. Интересуют тонкости компиляции (ну про разряженные таблицы и их огромный размер т.д.), есть интернет, читайте.


        1. Flammmable Автор
          21.05.2024 15:33

          А тонкости, что мол сравнения нет... Ну к чему это?

          В смысле, тонкости? ))))))))) Вы пишете:

          Конструкция switch предназначена для перехода, за одно сравнение, на соответствующий вариант case.

          ...я вас спрашиваю, как такое возможно? Вы выдаёте абзац экзальтированного текста с вкраплениями "Дискутировать не буду".
          Ну такое себе )))) Я бы охарактеризовал это как неконструктивное общение :)

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

          Где бы я был без таких советов )


        1. datacompboy
          21.05.2024 15:33
          +3

          1. Flammmable Автор
            21.05.2024 15:33
            +1

            Тссс ))) Не разрушайте картину ))))

            Если серьёзно, то большое спасибо за столь оперативно составленные примеры. В такие моменты хочется сказать: Хабр - тот!


  1. nixtonixto
    21.05.2024 15:33
    +3

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

    Сохранение регистра в стек - это 2 машинных цикла, а в ячейку ОЗУ - 1. Поэтому я на AVR сохранял в ОЗУ. Для контроллера без вытесняющих прерываний это допустимо. И обязательно надо сохранять регистр с флагами SREG, чтобы вызов прерывания в момент между арифметической/логической операцией и проверкой результата операции (по флагам SREG) не нарушал работу программы.


    1. Flammmable Автор
      21.05.2024 15:33
      +1

      Весьма полезный комментарий! Спасибо. Ваше замечание про SREG я внёс в статью.


    1. redsh0927
      21.05.2024 15:33
      +2

      а в ячейку ОЗУ - 1

      У межек/тинек 2 такта. Только у xmega ST/LD 1/2 такта.


    1. NutsUnderline
      21.05.2024 15:33
      +1

      При грамотном использовании регистров (то что С должен бы делать, но не сделает) вообще ничего можно не сохранять, кроме SREG (и то не всегда): прерывание использует только свои регистры, основной цикл - свои.


  1. NutsUnderline
    21.05.2024 15:33
    +1

    Красивое! Я что то даже не вижу как бы здесь использовать сдвиговый регистр от SPI

    На более новых avr появилась куча интересной периферии - там может еще чего то можно придумать.


    1. Flammmable Автор
      21.05.2024 15:33
      +2

      Я что то даже не вижу как бы здесь использовать сдвиговый регистр от SPI

      Штатный SPI сдвигает только один байт. А в JTAG регистр IDCODE в обязательном порядке 32-битный. Так что да. Но с другой стороны - тем лучше этот пример в качестве бенчмарка :)

      На более новых avr появилась...

      AVR здесь исключительно из-за популярности Ардуино. В дальнейшем я намереваюсь рассмотреть более интересные и практичные, нежели AVR, ядра через призму программного JTAG.


      1. VT100
        21.05.2024 15:33

        Хм-м-м... Возможно, упустил нить. Но что с того, что только один байт? Ну, пусть будет 4 прерывания на 32 бита.


        1. NutsUnderline
          21.05.2024 15:33
          +2

          да там такие скорости что он только и делать что прерываться будет, оверхед на прерывания немалый, и самое главное - ничего особо полезного "в свободное время" делать то и ничего. У новых контроллеров avr есть dma - на полном автомате как минимум 4 байта можно послать, плюс usb аппаратный во "втором потоке" - получаем отладчик.


        1. Flammmable Автор
          21.05.2024 15:33
          +1

          Появилась пара мыслей, которые нуждаются в обдумывании :)