-
Разглядывая 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)Atmelavrdude
Теперь в среде Microchip Studio создадим проект на ассемблере.
Для этого выберем в главном меню Файл→Создать→Проект и в открывшемся окне укажем тип проекта Assembler:
Нажмём ОК и в следующем окне укажем семейство и марку микроконтроллера:
Затем выберем в главном меню пункт Сервис→Внешние инструменты:
В открывшемся окне укажем:
-
название инструмента
-
путь к *.exe. У меня это C:Program Files (x86)Atmelavrdudeavrdude.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:
-
Нажмём на боковой панели Component Mode
-
Нажмём кнопку Pick Device, откроется окно поиска компонентов
-
В строке для поиска введём ATmega328P
-
Два раза щёлкнем по единственному результату поиска
Добавим микроконтроллер на схему:
Для этого щёлкнем по названию микроконтроллера на боковой панели, а затем — по полю схемы. Тогда указатель из карандаша превратится в розовый контур будущего компонента. Щёлкнем в понравившемся месте схемы ещё раз — компонент размещён.
Затем дважды щёлкнем на изображении микроконтроллера на нашей схеме и в открывшемся окне впишем в поле 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
; === начало обработчика прерывания ===
; можем как угодно задействовать регистры r16, r17 и 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.
Автор: Flammmable