Русский
Русский
English
Статистика
Реклама

Redd

Разработка простейшего логического анализатора на базе комплекса Redd

16.06.2020 14:11:48 | Автор: admin
В прошлой статье цикла мы потренировались сохранять данные из потокового интерфейса в память средствами DMA. Пришла пора сделать какую-то полезную поделку, используя полученные навыки. Очень полезная при удалённой отладке вещь анализатор. Вообще, при работе с комплексом скорее нужны специализированные шинные анализаторы, но начинать лучше с чего-то попроще. Поэтому сейчас мы сделаем простейший логический анализатор на 32 канала. Понятно, что он будет совсем-совсем примитивным, но зато мы сделаем его своими руками. У кого ещё нет комплекса Redd, могут повторить опыт, используя любую макетную плату с ПЛИС фирмы Altera (Intel) и микросхемой ОЗУ. Итак, приступаем.



Предыдущие статьи цикла
  1. Разработка простейшей прошивки для ПЛИС, установленной в Redd, и отладка на примере теста памяти.
  2. Разработка простейшей прошивки для ПЛИС, установленной в Redd. Часть 2. Программный код.
  3. Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС.
  4. Разработка программ для центрального процессора Redd на примере доступа к ПЛИС.
  5. Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd.
  6. Веселая Квартусель, или как процессор докатился до такой жизни.
  7. Методы оптимизации кода для Redd. Часть 1: влияние кэша.
  8. Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин.
  9. Экстенсивная оптимизация кода: замена генератора тактовой частоты для повышения быстродействия системы.
  10. Доступ к шинам комплекса Redd, реализованным на контроллерах FTDI
  11. Работа с нестандартными шинами комплекса Redd
  12. Практика в работе с нестандартными шинами комплекса Redd
  13. Проброс USB-портов из Windows 10 для удалённой работы
  14. Использование процессорной системы Nios II без процессорного ядра Nios II
  15. Практическая работа с ПЛИС в комплекте Redd. Осваиваем DMA для шины Avalon-ST и коммутацию между шинами Avalon-MM



Определяем функциональность анализатора


На самом деле, самый-самый простейший анализатор мы сделали ещё в прошлый раз. Напомню, как выглядит шина AVALON_ST, скопировав рисунок из старой статьи:



То есть пробросили внешние линии на шину data, взвели сигнал valid, и началось запоминание по принципу отсюда и до обеда. Ну, то есть, пока память не закончится. Так работал мой осциллограф смешанных сигналов RIGOL, так работал логический анализатор HANTEK. Если для осциллографа смешанных сигналов по-другому нельзя, ведь аналоговый сигнал всё время изменяется, а он сохраняется вместе с цифрой, то для логического анализатора такой подход более, чем странен. Зачем сохранять данные без сжатия? В далёком 2007-м году добыл я китайский анализатор LA5034. Он был настолько китайским, что даже программа к нему сначала не имела английского интерфейса! Так вот, даже он уже не расходовал память на сохранение одних и тех же данных. Имея всего несколько килобайт ОЗУ (встроенного в ПЛИС), он позволял делать намного больше, чем дурацкий HANTEK с многомегабайтными микросхемами памяти.

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

Методика сжатия потока


Я выбрал самую простейшую методику сжатия. Линия задержки и компаратор прямого и задержанного на один такт сигнала.



Первый регистр на схеме выполняет чрезвычайно важную функцию. Нельзя работать с недискретизированными данными! Много лет назад я на этом обжёгся. У меня в проекте автомат переходил из одного состояния в другое. Всё бы ничего, но на графе переходов не было такой стрелки. В чём же дело? А я анализировал как раз сырые, а не дискретизированные данные. В результате, они могли изменить своё состояние в любой момент. Как известно, внутри ПЛИС у линий GCK скорость распространения более-менее единая, у остальных же линий совершенно произвольная. А состояние автомата задавалось в двоичном виде. То есть, для его хранения использовалось несколько битов, хранящихся в нескольких триггерах. В отличие от процессора, новое содержимое, которое защёлкнется в каждый бит, вычисляется независимо. И время прохождения сигнала в процессе этих вычислений от входа до триггера тоже для каждого бита своё.

И вот. Надо нам, скажем, перейти из состояния 0000 в зависимости от условий, или в 0001 или в 0110. И вот условие перехода изменилось очень близко к тактовому импульсу. Давайте я обозначу красными те биты, до которых данные успеют добежать, поэтому они примут новые значения для перехода в 0001, а синими те, до которых не успеют, и они примут значение для перехода в 0110. Итак: 0000.

В итоге, получаем состояние 0011. А на графе такого перехода не было! Кодирование методом OneHot не решит проблему, просто она станет очевидной (а так пока я отловил врага, пока понял, кто виноват 4 дня убил, ведь проявлялась беда очень редко, да и сначала я грешил на неверную реализацию логики).

Чтобы избежать этого, дискретизируем всё и вся! Что бы там ни защёлкнулось на входе, на выходе оно будет иметь стабильное состояние на протяжении такта. Поэтому на вход компаратора попадут уже стабильные данные!

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

У такой системы сжатия только один недостаток, но он делает её в таком виде совершенно неприемлемой. Мы не знаем, как долго держалось каждое стабильное состояние. Чтобы устранить данную проблему, добавим таймер. Если сейчас данные 32 бита, то имеет смысл добавить ещё 32-битный таймер, так как суммарная шина должна удваиваться, а после 32 идёт разрядность 64. Просто будем защёлкивать натикавшие показания. Зная значение таймера для прошлой и текущей записи, мы всегда поймём, как долго держалось прошлое значение. Правда, таймер имеет свойство переполняться. На частоте 100 МГц он переполнится через 42.9 секунды. Но ничто не мешает нам при нулевом значении таймера также произвести защёлкивание данных. Накладные расходы памяти будут не так велики, а программа догадается, что произошло переполнение и надо начать отмерять значения с начала. В итоге, получаем такую блок-схему:



Производительность анализатора


64-битная шина данных при 16-битной микросхеме SDRAM это не совсем хорошо. Допустим, мы тактируем ОЗУшку частотой 100 Мгц. Тогда, чисто теоретически, мы не можем использовать частоту дискретизации выше 25 МГц, ведь фактически каждое 64-битное слово будет уходить в ОЗУ в виде четырёх 16-битных слов. А практически, с поправкой на подачу команд микросхеме ОЗУ и циклы регенерации, предельная рабочая частота будет и того меньше. Может, даже 20 МГц.

Что на это можно сказать? Да, при разработке комплекса Redd не стояло задачи сделать супер производительный анализатор. Давайте взглянем на фирменные анализаторы, имеющиеся у меня под рукой. Вот простенький 16-битный. В нём стоит целых две ОЗУшины. Все ножки ПЛИС обслуживают каналы и ОЗУ. Ну, ещё на стык с USB уходят. А в Redd они ещё и для других целей используются.



Вот тот самый многострадальный абсолютно бесполезный HANTEK. Сжатия нет, но тоже две ОЗУшины. Причём, насколько я помню DDR. В своё время, неплохо его изучил: несколько лет назад хотел сделать прошивку со сжатием, даже выпросил у производителя UCF-файл, но так и не освоил работу с ОЗУ у Xilinx. Но с тех пор я мог подзабыть детали схемы.



А вот так изнутри выглядит туловище анализатора LeCroy через отверстие под установку головы:



Там целых четыре модуля памяти с кучей микросхем каждый. Мне не хочется его сейчас вскрывать, но когда я отчищал его от пыли, сильно проникся внешним видом той ПЛИС, которая стоит внутри. Столько модулей памяти в параллель обслуживать много ножек и ресурсов ПЛИСине требуется. И цена такого анализатора (разумеется, нового, а не с eBay) десятки тысяч долларов. Насколько я помню, даже больше полусотни тысяч.

В целом, если кому-то позарез нужна производительность, он может или приобрести макетную плату с 32-битной ОЗУ, или разработать свою, установив туда две 32-битные ОЗУшины. Или даже модули DIMM. Но это уже будет BGA ПЛИС, у неё будет уже другая цена, и всё (включая класс печатной платы) другое. А теория будет та же, что и сейчас, просто надо будет выкинуть преобразователь разрядности шины. Так что продолжаем рассуждения.

Вообще, на самом деле, и у нас всё не так плохо. Если данные идут небольшими пачками, то необходимо и достаточно установить блок FIFO. Пришла пачка она попала в очередь. Дальше на входе тишина, а данные из очереди постепенно уходят в ОЗУ. Таким образом, мгновенная производительность анализатора будет 100 МГц Но в целом всё будет хорошо при условии, что не переполняется FIFO. Именно поэтому я сделал целую статью, которая помогает оставить как можно больше памяти для нужд этого самого блока FIFO. Самое главное блок должен быть установлен там, где шина данных ещё 64-битная. Итого, получаем блок-схему анализатора:



Разработка головы анализатора


Ну что ж, приступаем к разработке головы. Я как-то привык к терминологии мощных шинных анализаторов, у которых имеется универсальное туловище, а уже к нему подключаются проблемно ориентированные головы. Поэтому и у нас будет туловище и голова. Для реализации выбранной схемы не нужно даже делать никаких автоматов.
Интерфейс модуля будет таким:
module AnalyzerHead (    input                   clk,    input                   reset,    input  logic            source_ready,    output logic            source_valid,    output logic[63:0]      source_data,    input logic [31:0]      channels);

Вот так мы реализуем процесс, который защёлкивает данные в регистрах и увеличивает счётчик:
    logic [31:0] counter = 0;    logic [31:0] channels_D1 = 0;    logic [31:0] channels_D2 = 0;    always @ (posedge clk, posedge reset)    if (reset == 1)    begin        counter <= 0;        channels_D1 <= 0;        channels_D2 <= 0;    end else    begin        channels_D1 <= channels;        channels_D2 <= channels_D1;        counter <= counter + 1;    end

Первое условие записи:
    logic valid1;  // Вариант срабатывания 1 - разные данные    assign valid1 = (channels_D1==channels_D2)?0:1;

Второе условие записи:
    logic valid2;    // Вариант срабатывания 2 - переполнение счётчика    assign valid2 = (counter == 0)?1:0;

Результирующее условие записи:
    // Сводим оба варианта воедино    // Если FIFO не готово - увы, данные пропадут    // В полноценном анализаторе надо зажигать аварию при этом    // тут - ну пропадут и пропадут...    assign source_valid = (valid1 | valid2) & source_ready;

Ну, и из опытов ясно, что байты на шине надо немного перекрутить:
 // Данные вот так вот вывернуты    assign source_data [63:56] = counter [7:0];    assign source_data [55:48] = counter [15:7];    assign source_data [47:40] = counter [23:16];    assign source_data [39:32] = counter [31:24];     assign source_data [31:24] = channels_D1 [7:0];    assign source_data [23:16] = channels_D1 [15:7];    assign source_data [15:8] = channels_D1 [23:16];    assign source_data [7:0] = channels_D1 [31:24];

Собственно, всё. Давайте для полноты картины я вставлю полный текст модуля в слитном варианте.
Полный текст модуля
module AnalyzerHead (    input                   clk,    input                   reset,    input  logic            source_ready,    output logic            source_valid,    output logic[63:0]      source_data,    input logic [31:0]      channels);    logic [31:0] counter = 0;    logic [31:0] channels_D1 = 0;    logic [31:0] channels_D2 = 0;    logic valid1;    logic valid2;    always @ (posedge clk, posedge reset)    if (reset == 1)    begin        counter <= 0;        channels_D1 <= 0;        channels_D2 <= 0;    end else    begin        channels_D1 <= channels;        channels_D2 <= channels_D1;        counter <= counter + 1;    end // Вариант срабатывания 1 - разные данные    assign valid1 = (channels_D1==channels_D2)?0:1; // Вариант срабатывания 2 - переполнение счётчика assign valid2 = (counter == 0)?1:0; // Сводим оба варианта воедино // Если FIFO не готово - увы, данные пропадут // В полноценном анализаторе надо зажигать аварию при этом // тут - ну пропадут и пропадут...    assign source_valid = (valid1 | valid2) & source_ready;  // Данные вот так вот вывернуты    assign source_data [63:56] = counter [7:0];    assign source_data [55:48] = counter [15:7];    assign source_data [47:40] = counter [23:16];    assign source_data [39:32] = counter [31:24];     assign source_data [31:24] = channels_D1 [7:0];    assign source_data [23:16] = channels_D1 [15:7];    assign source_data [15:8] = channels_D1 [23:16];    assign source_data [7:0] = channels_D1 [31:24]; endmodule


Упаковка головы в компонент для процессорной системы


Как-то зловеще звучит заголовок Но как бы там ни было, а упаковать всё в компонент нам надо. Мы тренировались делать подобное в этой статье.

У меня получилась шина AVALON_ST, штатные линии тактирования и сброса, и Но сначала рисунок с типовыми вещами:



Из нетиповых: для будущей задумки линии conduit пришлось дать осознанное имя типу сигнала. Оно нам ещё пригодится.



В остальном вроде, всё понятно.

Проектируем процессорную систему


Как мы уже рассматривали в этой статье, мы не станем добавлять в систему процессорное ядро Nios II, а воспользуемся блоком Altera JTAG-to-Avalon-MM.

Работать с контроллером SDRAM мы учились в этой статье, а в этой разбирались, как при помощи блока PLL разогнать систему до 100 Мгц. Экспериментировали с FIFO и изменением ширины шины AVALON_ST при помощи блока AVALON_ST_ADAPTER мы в этой статье. Наконец, с DMA мы экспериментировали буквально в прошлой статье.

Пришла пора собрать все эти знания в едином проекте! Вот такая у меня получилась навёрнутая структурная схема.



Страшно? Ничуть. Давайте пройдёмся по ней сверху вниз. Сначала идёт блок тактирования и сброса. Как всегда, для комплекса Redd, чтобы не мучиться, физическую ножку Reset я не использую (я её всегда виртуальной делаю). Так удобнее для данной конкретной аппаратуры, хоть и не совсем правильно. Тактирование же идёт на блок PLL. Как его настраивать, мы уже подробно рассматривали раньше. Если я вставлю сюда массу скриншотов, то сильно перегружу статью. С выхода c0 мы берём тактовый сигнал для всей нашей системы, а выход c1 экспортируем и подключаем к тактовому входу микросхемы SDRAM.

Master0 это тот самый компонент Altera JTAG-to-Avalon-MM, через который мы будем достукиваться до шины AVALON_MM. Он в настройках не нуждается. Доступ к шине нам нужен, чтобы управлять блоком DMA и чтобы считывать содержимое SDRAM с накопленными результатами.

Дальше идёт наш компонент Голова. А уже из неё растекается поток через цепочку шин AVALON_ST. Сначала он затекает в блок FIFO. Это первый блок, настройки которого стоит показать особо:



8 символов на слово, каждый символ 8 бит. Итого 8*8=64 бита. Ёмкость 4 килослова. Все остальные вещи протокола AVALON_ST отключены. Двойное тактирование сделано для того, чтобы в будущем голова могла работать на частоте, отличной от частоты работы туловища. Это нам пригодится, когда мы будем делать шинный анализатор USB.

Дальше данные перетекают в преобразователь разрядности. Вот его настройки:



Собственно, 8 символов на слово на входе, 4 символа на слово на выходе. 8 бит на символ. Тоже всё просто. Наконец, поток входит в блок DMA. Ему я только типы шин и максимальную длину передачи поправил, да выставил режим доступа только в режиме полного слова, чтобы поднять Fmax. По уму, такой огромный объём памяти дескрипторов не нужен (хотя, нутром чую, что через них мы можем реализовать кольцевой буфер для анализатора). Размер входного FIFO тоже можно уменьшить до минимума, ведь у нас есть FIFO до этого блока. Но, честно говоря, работа над статьёй и так уже затянулась, так что оставим эту оптимизацию для читателей в качестве самостоятельной работы.



Всё. Потоковая часть завершена. Дальше данные попадают в контроллер SDRAM. Напомню его настройки





Из функциональной части всё. Но кто следит за рассказом не по диагонали, а внимательно, наверное, заметил ещё один странный блок DataGen_0. Что это такое? Мы раньше такого не применяли!

Дело в том, что мне же как-то надо проверить работу головы. А это надо все 32 линии назначить на какие-то ножки ПЛИС, подключить к ним какой-то источник А все должны будут поверить мне на слово, что я это сделал. И потом думать, как это повторить у себя. Зачем? Давайте добавим тестовый генератор данных и подключим его не проводами, а через трассировочные ресурсы ПЛИС. Я сделал самый простой счётчик, который увеличивает своё значение в случайные моменты времени. В качестве генератора случайных чисел я взял 32-разрядную M-последовательность, а увеличиваю счётчик, когда в младших восьми битах появляется константа 0x12. Вот такой получился SystemVerilog код, реализующий эту функциональность (обратите внимание, что я по-прежнему не использую сигнал reset, хотя, здесь бы он пригодился):
module DataGen(   input clk,   output logic [31:0] data = 0);// Генератор случайных чиселlogic [31:0]shift_reg = 0;logic next_bit;assign next_bit = shift_reg[31] ^ shift_reg[30]   ^ shift_reg[29] ^ shift_reg[27] ^ shift_reg[25]   ^ shift_reg[ 0];always @(posedge clk)  if(shift_reg == 0)    shift_reg <= 32'h12345678;  else    shift_reg <= { next_bit, shift_reg[31:1] };// Целевой счётчикalways @(posedge clk)begin    if (shift_reg [7:0] == 8'h12)        data <= data + 1;endendmodule

В настройках компонента самая важная деталь это имя параметра Signal Type у conduit шины. Он должен быть таким же, какой я заполнил у соответствующего параметра головы. В остальном всё просто, здесь же нет никаких специальных шин, только conduit



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

Финал работ


Делаем линию reset виртуальной. Всем остальным ножкам я предпочёл сделать назначение не в GUI, а скопировал фрагмент файла *.qsf из проекта, сделанного в самой первой статье.
Вот этот фрагмент:
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to clk_clkset_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[12]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[11]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[10]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[9]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[8]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[7]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[6]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[5]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[4]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[3]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[2]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[1]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[0]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_ba[1]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_ba[0]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_cas_nset_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_clk_clkset_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_cs_nset_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[15]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[14]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[13]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[12]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[11]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[10]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[9]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[8]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[7]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[6]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[5]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[4]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[3]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[2]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[1]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[0]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dqm[1]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dqm[0]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_ras_nset_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_we_nset_location_assignment PIN_64 -to sdram_addr[12]set_location_assignment PIN_60 -to sdram_addr[11]set_location_assignment PIN_44 -to sdram_addr[10]set_location_assignment PIN_59 -to sdram_addr[9]set_location_assignment PIN_58 -to sdram_addr[8]set_location_assignment PIN_55 -to sdram_addr[7]set_location_assignment PIN_54 -to sdram_addr[6]set_location_assignment PIN_53 -to sdram_addr[5]set_location_assignment PIN_52 -to sdram_addr[4]set_location_assignment PIN_51 -to sdram_addr[3]set_location_assignment PIN_50 -to sdram_addr[2]set_location_assignment PIN_49 -to sdram_addr[1]set_location_assignment PIN_46 -to sdram_addr[0]set_location_assignment PIN_73 -to sdram_dq[15]set_location_assignment PIN_72 -to sdram_dq[14]set_location_assignment PIN_71 -to sdram_dq[13]set_location_assignment PIN_70 -to sdram_dq[12]set_location_assignment PIN_69 -to sdram_dq[11]set_location_assignment PIN_68 -to sdram_dq[10]set_location_assignment PIN_67 -to sdram_dq[9]set_location_assignment PIN_66 -to sdram_dq[8]set_location_assignment PIN_30 -to sdram_dq[7]set_location_assignment PIN_28 -to sdram_dq[6]set_location_assignment PIN_11 -to sdram_dq[5]set_location_assignment PIN_10 -to sdram_dq[4]set_location_assignment PIN_7 -to sdram_dq[3]set_location_assignment PIN_3 -to sdram_dq[2]set_location_assignment PIN_2 -to sdram_dq[1]set_location_assignment PIN_1 -to sdram_dq[0]set_location_assignment PIN_65 -to sdram_dqm[1]set_location_assignment PIN_31 -to sdram_dqm[0]set_location_assignment PIN_34 -to sdram_ras_nset_location_assignment PIN_32 -to sdram_we_nset_location_assignment PIN_42 -to sdram_cs_nset_location_assignment PIN_33 -to sdram_cas_nset_location_assignment PIN_38 -to sdram_ba[0]set_location_assignment PIN_39 -to sdram_ba[1]set_location_assignment PIN_25 -to clk_clkset_location_assignment PIN_43 -to sdram_clk_clkset_instance_assignment -name VIRTUAL_PIN ON -to sdram_cke


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

Разработка логического анализатора на базе Redd проверяем его работу на практике

30.06.2020 18:10:31 | Автор: admin
В прошлой статье мы сделали аппаратуру, реализующую логический анализатор на базе комплекса Redd. Статья разрослась так, что рассмотрение программной поддержки мы отложили на потом. Пришла пора разобраться с тем, как мы будем получать и отображать данные, которые анализатор копит в ОЗУ.



Предыдущие статьи цикла
  1. Разработка простейшей прошивки для ПЛИС, установленной в Redd, и отладка на примере теста памяти.
  2. Разработка простейшей прошивки для ПЛИС, установленной в Redd. Часть 2. Программный код.
  3. Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС.
  4. Разработка программ для центрального процессора Redd на примере доступа к ПЛИС.
  5. Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd.
  6. Веселая Квартусель, или как процессор докатился до такой жизни.
  7. Методы оптимизации кода для Redd. Часть 1: влияние кэша.
  8. Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин.
  9. Экстенсивная оптимизация кода: замена генератора тактовой частоты для повышения быстродействия системы.
  10. Доступ к шинам комплекса Redd, реализованным на контроллерах FTDI
  11. Работа с нестандартными шинами комплекса Redd
  12. Практика в работе с нестандартными шинами комплекса Redd
  13. Проброс USB-портов из Windows 10 для удалённой работы
  14. Использование процессорной системы Nios II без процессорного ядра Nios II
  15. Практическая работа с ПЛИС в комплекте Redd. Осваиваем DMA для шины Avalon-ST и коммутацию между шинами Avalon-MM
  16. Разработка простейшего логического анализатора на базе комплекса Redd


Сегодня нам очень понадобится опыт, который мы получили в в одной из недавних статей. Там я говорил, что в идеале было бы полезно выучить язык Tcl, но в целом, можно программировать на дикой смеси высокоуровневой логики, описанной на C++ и низкоуровневых запросах на Tcl. Если бы я вёл разработку для себя, то так бы и сделал. Но когда пишешь статью, приходится стараться умять всё в как можно меньшее количество файлов. Одна из задуманных статей так никогда и не была написана именно по этой причине. Мы договорились с коллегой, что он сделает код в рамках проекта, а я потом опишу его. Но он построил код так, как это принято в жизни раскидал его по огромному количеству файлов. Потом код оброс бешеным количеством технологических проверок, которые нужны в жизни, но за которыми не видно сути. И как мне было описывать всё это? Посмотрите налево, посмотрите направо Здесь играй, здесь не играй, а здесь коллега рыбу заворачивал? В итоге, полезный материал не пошёл в публикацию (правда, статьи по той тематике всё равно не набирали рейтинга, так что полезный-то он полезный, но мало кому интересный).

Вывод из всего этого прост. Чтобы в статье не бегать от модуля к модулю, пришлось разобраться, как сделать на чистом Tcl. И знаете, не такой это оказался страшный язык. Вообще, всегда можно перейти в каталог C:\intelFPGA_lite и задать поиск интересующих слов по файлам *.tcl. Решения почти всегда найдутся. Так что он мне нравится всё больше и больше. Ещё и ещё раз советую приглядеться к этому языку.

Я уже говорил, что при запуске Квартусового Tcl скрипта под Windows и под Linux, тексты должны немного различаться в районе инициализации. Запуск под Linux непосредственно на центральном процессоре Reddа должен давать большее быстродействие. Но зато запуск под Windows во время опытов более удобен для меня, так как я могу редактировать файлы в удобной для себя среде. Поэтому все дальнейшие файлы я писал под запуск из System Console в Windows. Как их переделать в Линуксовый вариант, мы разбирались в той самой статье по ссылке выше.

Обычно я даю фрагменты кода с пояснениями, а затем уже справочно полный код. Но это хорошо, когда во фрагментах каждый видит что-то знакомое. Так как для многих, кто читает эти строки, язык Tcl считается экзотикой, сначала я приведу для справки полный текст своего первого пробного скрипта.
Полный текст находится здесь.
variable DMA_BASE 0x2000000variable DMA_DESCR_BASE 0x2000020# Чтение регистра блока DMA.#proc dma_reg_read { address } {  variable DMA_BASE  variable m_path  set address [expr {$address * 4 + $DMA_BASE}]  return [master_read_32 $m_path $address 1]}# Запись регистра блока DMAproc dma_reg_write { address data } {  variable DMA_BASE  variable m_path  set address [expr {$address * 4 + $DMA_BASE}]  master_write_32 $m_path $address $data}# Запись регистра дескрипторов блока DMAproc dma_descr_reg_write { address data } {  variable DMA_DESCR_BASE  variable m_path  set address [expr {$address * 4 + $DMA_DESCR_BASE}]  master_write_32 $m_path $address $data}proc prepare_dma {sdram_addr sdram_size} {# Остановили процесс, чтобы всё понастраивать# Да, мне лень описывать все константы, # я делаю всё на скорую рукуdma_reg_write 1 0x20# На самом деле, тут должно быть ожидание фактической остановки,# но в рамках теста, оно не нужно. Точно остановимся.# Добавляем дескриптор в FIFO# Адрес источника (вообще, это AVALON_ST, но я всё# с примеров списывал, а там он зануляется)dma_descr_reg_write 0 0# Адрес приёмника. dma_descr_reg_write 1 $sdram_addr# Длинаdma_descr_reg_write 2 $sdram_size# Управляющий регистр (взводим бит GO)dma_descr_reg_write 3 0x80000000# Запустили процесс, не забыв отключить прерыванияdma_reg_write 1 4}puts "running"set m_path [lindex [get_service_paths master] 0]open_service master $m_pathprepare_dma 0 0x100puts [master_read_32 $m_path 0 16]



Разбираем фрагменты скрипта


При работе со скриптом нам надо знать базовые адреса блоков. Обычно я их беру из заголовочных файлов, сделанных для BSP, но сегодня мы не создали никаких проектов. Однако адреса всегда можно посмотреть в Platform Designer хоть на привычной нам структурной схеме (я покажу только пример одного адреса):



Хоть там же на специальной вкладке, где всё собрано в виде таблиц:



Нужные мне адреса я вписал в начало скрипта (а нулевой начальный адрес буферного ОЗУ я буду подразумевать всегда, чтобы облегчить код):
variable DMA_BASE 0x2000000variable DMA_DESCR_BASE 0x2000020


Функции доступа к DMA в позапрошлой статье я специально написал в простейшем виде. Сегодня же я просто взял и аккуратно перенёс этот код из C++ в Tcl. Эти функции требуют доступа к аппаратуре, а именно чтения и записи порта самого блока DMA и функция записи порта блока дескрипторов DMA. Читать дескрипторы пока не нужно. Но понадобится допишем по аналогии. Мы уже тренировались работать с аппаратурой здесь, поэтому делаем такие функции:
# Чтение регистра блока DMA.#proc dma_reg_read { address } {  variable DMA_BASE  variable m_path  set address [expr {$address * 4 + $DMA_BASE}]  return [master_read_32 $m_path $address 1]}# Запись регистра блока DMAproc dma_reg_write { address data } {  variable DMA_BASE  variable m_path  set address [expr {$address * 4 + $DMA_BASE}]  master_write_32 $m_path $address $data}# Запись регистра дескрипторов блока DMAproc dma_descr_reg_write { address data } {  variable DMA_DESCR_BASE  variable m_path  set address [expr {$address * 4 + $DMA_DESCR_BASE}]  master_write_32 $m_path $address $data}

Теперь уже можно реализовывать логику работы с DMA. Ещё и ещё раз говорю, что я специально в позапрошлой статье не обращался к готовому API, а сделал как можно более простые собственные функции. Я знал, что мне придётся эти функции портировать. И вот так я портировал инициализацию DMA (аргументы начальный адрес и длина в байтах для буфера, в который будет идти приём):
proc prepare_dma {sdram_addr sdram_size} {# Остановили процесс, чтобы всё понастраивать# Да, мне лень описывать все константы, # я делаю всё на скорую рукуdma_reg_write 1 0x20# На самом деле, тут должно быть ожидание фактической остановки,# но в рамках теста, оно не нужно. Точно остановимся.# Добавляем дескриптор в FIFO# Адрес источника (вообще, это AVALON_ST, но я всё# с примеров списывал, а там он зануляется)dma_descr_reg_write 0 0# Адрес приёмника. dma_descr_reg_write 1 $sdram_addr# Длинаdma_descr_reg_write 2 $sdram_size# Управляющий регистр (взводим бит GO)dma_descr_reg_write 3 0x80000000# Запустили процесс, не забыв отключить прерыванияdma_reg_write 1 4}

Ну, собственно, всё. Дальше идёт основное тело скрипта. Какое я там сделал допущение? Я не жду окончания работы DMA. Я же знаю, что таймер тикает весьма быстро. Поэтому запрошенные мною 0x100 байт заполнятся весьма шустро. И все мы знаем, что JTAG работает очень неспешно. В реальной жизни, разумеется, надо будет добавить ожидание готовности. А может и отображение текущего адреса. И ещё чего-нибудь И тогда код станет точно непригодным для статьи, из-за того, что все эти мелочи станут закрывать суть. А пока простейший код выглядит так:
puts "running"set m_path [lindex [get_service_paths master] 0]open_service master $m_pathprepare_dma 0 0x100puts [master_read_32 $m_path 0 16]

То есть, настроились на работу с аппаратурой, приняли 0x100 байт в буфер с адресом 0, отобразили первые 16 байт.

Заливаем прошивку, запускаем System Console, исполняем скрипт, наблюдаем такую красоту:



Таймер 0, счётчик 0. Таймер 0x18, счётчик 1. Таймер 0x5EB, счётчик 2. Ну, и так далее. В целом, мы видим то, что хотели. На этом можно было бы и закончить, но текстовое отображение не всегда удобно. Поэтому продолжаем.

Как отобразить результаты в графическом виде


То, что мы получили это вполне себе замечательно, но временные диаграммы обычного логического анализатора часто удобнее смотреть в графическом виде! Надо бы написать программу, чтобы можно было это делать Вообще, написать свою программу всегда полезно, но время ресурс ограниченный. А в рамках работы с комплексом Redd, мы придерживаемся подхода, что все эти работы вспомогательные. Мы должны тратить время на основной проект, а на вспомогательный нам никто его не выделит. Можно этим заняться в свободное от работы время, но лично мне это не даёт делать балалайка. Она тоже много времени отнимает. Так что свободное время лично у меня тоже занято. К счастью, уже существуют решения, которые могут облегчить нам жизнь. Я узнал о нём, когда читал статьи про Icarus Verilog. Там строят графическую визуализацию времянок через некие файлы VCD. Что это такое?

В стандарте Verilog (я нашёл его здесь ) смотрим там раздел 18. Value change dump (VCD) files. Вот так! Оказывается, можно заставить тестовую систему на языке Verilog генерировать подобные файлы. И они будут стандартизированы, то есть, едины, независимо от среды, в которой производится моделирование. А где стандарт, там и единство логики систем отображения. А что, если мы тоже будем формировать такой же файл на основании того, что пришло из анализатора? Причём заставим Tcl скрипт делать это Осталась самая малость: выяснить, что в файл следует записать, а также как это сделать.

Радостный, я попросил Гугля показать мне пример vcd-файла. Вот, что он мне дал. Я выделил всё, что там было в таблице и сохранил в файл tryme.vcd. Если к моменту чтения статьи страничка перестанет существовать, вот содержимое этого файла ниже.
Содержимое файла tryme.vcd
$commentFile created using the following command:vcd files output.vcd $dateFri Jan 12 09:07:17 2000$end$versionModelSim EE/PLUS 5.4$end$timescale1ns$end$scope module shifter_mod $end$var wire 1 ! clk $end$var wire 1 " reset $end$var wire 1 # data_in $end$var wire 1 $ q [8] $end$var wire 1 % q [7] $end$var wire 1 & q [6] $end$var wire 1 ' q [5] $end$var wire 1 ( q [4] $end$var wire 1 ) q [3] $end$var wire 1 * q [2] $end$var wire 1 + q [1] $end$var wire 1 , q [0] $end$upscope $end$enddefinitions $end#0$dumpvars0!1"0#0$0%0& 0'0(0)0*0+0,$end#1001!#1500!#2001!$dumpoffx!x"x#x$x%x&x'x(x)x*x+x,$end#300$dumpon1!0"1#0$0% 0&0'0(0)0*0+1,$end#3500!#4001!1+#4500!#5001!1*#5500!#6001!1)#6500!#7001!1(#7500!#8001!1'#8500!#9001!1&#9500! #10001!1%#10500!#11001!1$#11500!1"0$0%0&0'0(0)0*0+0,#12001!$dumpall1!1"1#0$0%0&0'0(0)0*0+0,$end


Какое-то Юстас Алексу. Можно, конечно, разобраться, глядя в стандарт, но больно там много всего. Попробуем всё сделать на практике, загрузив файл в какую-нибудь среду, визуализирующую его содержимое, и сопоставив текст с полученной картинкой. Несмотря на то, что заголовок web-страницы гласит, что всё это пример от среды ModelSim, мне не удалось открыть данный файл в этой среде. Возможно, в комментариях кто-то подскажет, как это сделать. Однако не МоделСимом единым жив человек. Существует такая кроссплатформенная система с открытым исходным кодом GtkWave. Сборку под Windows я взял тут . Запускаем её, выбираем в меню File->Open New Tab:



Указываем наш файл и получаем такую картинку:



Выделяем все сигналы и через меню правой кнопки Мыши выбираем Recurse Import->Append:



И вот результат:



Видя такое дело, вполне можно разобраться, что в файле зачем добавлено.
$timescale1ns$end

Ну, тут всё понятно. Это в каких единицах время будет задаваться.
$scope module shifter_mod $end$var wire 1 ! clk $end$var wire 1 " reset $end$var wire 1 # data_in $end$var wire 1 $ q [8] $end$var wire 1 % q [7] $end$var wire 1 & q [6] $end

Это объявляются переменные с весьма экзотическими именами Восклицательный знак, Кавычки, решётка и т.п. Шина в найденном примере объявляется побитово. Идём дальше:
#0$dumpvars0!1"0#0+0,$end

Время равно нулю. Дальше значения переменных. Зачем ключевое слово $dumpvars? Придётся заглянуть в стандарт. Как я и думал, какое-то непонятное занудство. Но создаётся впечатление, что нам сообщают, что эти данные получены при помощи директивы языка $dumpvars. Давайте попробуем убрать это слово и соответствующую ему строку $end. Загружаем обновлённый файл и видим результат:



Как говорится, найдите десять отличий Никакой разницы. Значит, нам это добавлять на выход не нужно. Идём дальше.
#1001!#1500!

Мы видим, что в моменты 100 и 150 нс тактовый сигнал переключился, а остальные нет. Поэтому мы можем добавлять только изменившиеся значения сигналов. Идём дальше.
#2001!$dumpoffx!x"x,$end#300$dumpon1!0"

Теперь мы умеем задавать состояние X. Проверяем, нужны ли ключевые слова $dumpoff и $dumpon, выкинув их из файла (не забываем про парные им $end)

Добавлять ещё один рисунок, полностью идентичный предыдущим, я не буду. Но можете проверить это же у себя.

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


То же самое текстом:
vcd_declaration_vars ::=$var var_type size identifier_code reference $endvar_type ::=event | integer | parameter | real | realtime | reg | supply0 | supply1 | time| tri | triand | trior | trireg | tri0 | tri1 | wand | wire | worsize ::=decimal_numberreference ::=identifier| identifier [ bit_select_index ]| identifier [ msb_index : lsb_index ]index ::=decimal_number


Прекрасно! Мы можем задавать и вектора! Пробуем добавить одну переменную, дав ей имя звёздная собака, так как односимвольные варианты уже израсходованы авторами оригинального примера:
$var reg 32 *@ test [31:0] $end

И добавим ей таких присвоений Обратите внимание на пробелы перед именем переменной! Без них не работает, но и стандарт требует их наличия в отличие от однобитных вариантов:
#0b00000001001000110100010101100111 *@#100b00000001XXXX0011010001010110zzzz *@

Смотрим результат (я в нём ради интереса раскрыл вектор)



Ну и замечательно. У нас есть вся теория, чтобы подготовить файл для просмотра в GtkWave. Наверняка, можно будет его посмотреть и в ModelSim, просто я пока не понял, как это сделать. Давайте займёмся формированием данного файла для нашего супер-мега-анализатора, который фиксирует одно единственное число.

Делаем Tcl-скрипт, создающий файл VCD


Если весь предыдущий опыт я брал из файлов *.tcl, идущих в комплекте с Квартусом, то здесь всё плохо. Слово file есть в любом из них. Пришлось спросить у Гугля. Он выдал ссылку на замечательный справочник.

Функции не трогаем, а основное тело скрипта переписываем так.
Основное тело скрипта.
puts "running"set m_path [lindex [get_service_paths master] 0]open_service master $m_pathprepare_dma 0 0x100set fileid [open "ShowMe.vcd" w]puts $fileid "\$timescale"puts $fileid "1ns"puts $fileid "\$end"puts $fileid "\$scope module megaAnalyzer \$end"puts $fileid "\$var reg 32 ! data \[31:0] \$end"puts $fileid "\$upscope \$end"puts $fileid "\$enddefinitions \$end"# А зачем рисовать где-то справа?# Будем рисовать от первой точки# Для этого запомним базуset startTime [lindex [master_read_32 $m_path 0 1] 0]for { set i 0}  {$i < 20} {incr i} {   set cnt [lindex [master_read_32 $m_path [expr {$i * 8}]  1] 0]   set data [lindex [master_read_32 $m_path [expr {$i * 8 + 4}]  1] 0]   # Таймер тикает с частотой 100 МГц   # Один тик таймера - это 10 нс   # поэтому на 10 и умножаем   puts $fileid "#[expr {($cnt - $startTime)* 10}]"   puts $fileid "b[format %b $data] !"}close $fileid


Здесь я не стал учитывать перескоки счётчика через ноль. Перед нами простейший демонстрационный скрипт, так что не будем его перегружать. Запускаем Дааааа. Это именно то, о чём я предупреждал. Оно работает весьма и весьма долго. Сколько оно будет работать в случае заполненных мегабайтных буферов страшно даже предположить. Но зато мы не потратили много времени на разработку! Опять же, кто работал с анализатором BusDoctor, тот не даст соврать: этот фирменный анализатор отдаёт данные больших объёмов тоже очень и очень неспешно.
Итак, получаем файл:
$timescale1ns$end$scope module megaAnalyzer $end$var reg 32 ! data [31:0] $end$upscope $end$enddefinitions $end#0b0 !#240b1 !#15150b10 !#25170b11 !#26510b100 !#26690b101 !#31180b110 !#31830b111 !#35540b1000 !#35630b1001 !#36940b1010 !#39890b1011 !#46130b1100 !#55540b1101 !#60820b1110 !#71270b1111 !#71480b10000 !#76270b10001 !#77990b10010 !#91000b10011 !


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



Счётчик щёлкает! В случайные моменты времени. Что хотели, то и получили

Заключение


Мы сделали простейший логический анализатор, потренировались принимать данные с него и производить их визуализацию. Чтобы довести этот простейший пример до совершенства, разумеется, придётся ещё потрудиться. Но путь, по которому идти, понятен. Мы получили опыт изготовления реальных измерительных устройств на базе ПЛИС, установленной в комплексе Redd. Теперь каждый сможет доработать эту основу так, как сочтёт нужным (и как ему позволит время), а в следующей серии статей я начну рассказ о том, как заменить этому анализатору голову, чтобы сделать шинный анализатор для USB 2.0.

Те, у кого нет настоящего Redd, смогут воспользоваться вот такой свободно доставаемой платой, которая имеется у массы продавцов на Ali Express.


Искать следует по запросу WaveShare ULPI. Подробнее о ней на странице производителя.
Подробнее..

Моделируем поведение Quartus-проекта на Verilog в среде ModelSim

29.07.2020 16:12:53 | Автор: admin
В прошлой статье мы сделали достаточно сложный модуль. Разумеется, я вставил в тело статьи уже отлаженный результат. Но мне показалось, что достаточно странно, когда автор говорит делай, как я, но при этом не показывает очень важного процесса. Давайте я покажу, как вообще проводится отладка системы путём моделирования. Причём в следующей статье будут содержаться сведения, которые ещё неделю назад не знал даже я. Но, чтобы перейти к ним, надо разобраться с базовыми принципами. Итак. Давайте рассмотрим, как быстро подготовить и не менее быстро запустить процесс моделирования в среде ModelSim.



Предыдущие статьи цикла
  1. Разработка простейшей прошивки для ПЛИС, установленной в Redd, и отладка на примере теста памяти
  2. Разработка простейшей прошивки для ПЛИС, установленной в Redd. Часть 2. Программный код
  3. Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС
  4. Разработка программ для центрального процессора Redd на примере доступа к ПЛИС
  5. Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd
  6. Веселая Квартусель, или как процессор докатился до такой жизни
  7. Методы оптимизации кода для Redd. Часть 1: влияние кэша
  8. Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин
  9. Экстенсивная оптимизация кода: замена генератора тактовой частоты для повышения быстродействия системы
  10. Доступ к шинам комплекса Redd, реализованным на контроллерах FTDI
  11. Работа с нестандартными шинами комплекса Redd
  12. Практика в работе с нестандартными шинами комплекса Redd
  13. Проброс USB-портов из Windows 10 для удалённой работы
  14. Использование процессорной системы Nios II без процессорного ядра Nios II
  15. Практическая работа с ПЛИС в комплекте Redd. Осваиваем DMA для шины Avalon-ST и коммутацию между шинами Avalon-MM
  16. Разработка простейшего логического анализатора на базе комплекса Redd
  17. Разработка логического анализатора на базе Redd проверяем его работу на практике
  18. Делаем голову шинного USB-анализатора на базе комплекса Redd


Как работает обычная программа для ЭВМ? Имеется некая внешняя среда (монитор и клавиатура с мышкой самые типичные представители этой самой среды). Программа с ними взаимодействует. При отладке можно производить настоящие воздействия от внешней среды, а можно эмулировать их. У нас тестеры часто пишут всякие скрипты, которые как раз эмулируют внешние воздействия. После чего запускаются анализаторы логов, которые проверяют, чтобы ответы в среду уходили верные.

Что делать, если в этой программе для ЭВМ всё глючит? Можно поставить точки останова и изучать срез системы в момент, когда они сработали. Срез системы это значения переменных. Может, состояния различных мьютексов и прочих объектов синхронизации. В общем, срез внутренних параметров отлаживаемой системы.

При отладке для ПЛИС можно сделать всё то же самое. Правда, если среда будет настоящая, то делать остановку и изучать срез системы проблематично, хоть и возможно. В рамках рассказа о Redd, я всё время продвигаю мысль, что всё должно быть просто и быстро. Мы не проектируем сложных систем. Мы делаем какие-то модули, вроде того, что был сделан в прошлой статье. Он навороченный, но весьма и весьма несложный. В общем, мы будем производить его поведенческое моделирование.

И здесь возникает вопрос о внешней среде. Как сымитировать её? Нам на помощь приходят модели. На языке Verilog (как и VHDL, и других похожих) вполне можно описать поведение чего угодно. Делаем мы систему, которая работает с микросхемой ULPI Значит, чтобы проверить её работу, на том конце должно быть что-то, что ведёт себя именно, как ULPI. То есть, модель ULPI. Но этого мало. Наш блок реагирует на команды от шины ALAVON_MM. Именно эта шина заставляет блок жить. Поэтому надо ещё добавить модель шины AVALON_MM, причём эта модель должна быть активной. Именно она будет подавать тестовые воздействия.



В конечном итоге мы должны сделать именно такую систему. И тогда мы сможем снимать временные диаграммы сигналов на всех её шинах и даже внутри любых её модулей. Если будет возникать ошибка, мы сможем устанавливать точки останова и изучать срезы системы, чтобы найти врага. Хотя, лично я эти точки останова обычно не ставлю, чаще всего хватает анализа временных диаграмм. Дело в том, что сигналы можно смотреть не только интерфейсные, а любые внутренние. Вытянув десяток-другой внутренних сигналов на график, обычно можно догадаться, что в логике реализовано не так.

Цель сегодняшней статьи не рассказать о том, что такое моделирование вообще (это долгая история), а показать, как это моделирование провести быстрее всего. И рассмотрим мы это не на боевой задаче, а на простом примере. Сделаем совсем простенькую тестовую систему, чтобы в следующей статье уже понимать, откуда растут ноги у более сложного её варианта, ведь при чтении удобнее не сидеть и недоумевать: Зачем он это делает?, а знать все базовые принципы, из которых уже вытекают усложнения. Кстати, недавно выяснилось, что один мой знакомый хоть и владеет мастерством моделирования, но не знал, что в среду Quartus встроены механизмы, которые позволяют делать это легко и непринуждённо. Он тратил на это намного больше усилий, чем требуется. Так что может, кто-то тоже сейчас узнает для себя что-то новое о возможностях, заложенных в Quartus. Итак, приступаем.

Создание простейшей модели на языке Verilog


Люди делятся на две категории. Те, кто любит создавать всё с нуля руками и те, кто любит делать это, повозив мышкой. Руками создавать всё правильнее. Можно контролировать каждое действие и делать всё заведомо идеально. Но память штука ненадёжная. Если всё время заниматься одним и тем же делом, она держит детали в уме, а если приходится всё время переключаться между языками, через месяц-другой приходится вспоминать, что же там надо сделать. Поэтому работа через вариант повозить мышкой имеет право на существование хотя бы из-за этого. Опять же, если у отлаживаемого модуля десяток-другой интерфейсных сигналов, мне всегда скучно делать рутинную работу по их переобъявлению и пробросу. Поэтому сейчас мы рассмотрим, как сделать модель при помощи мышки. А дальше каждый для себя решит, достаточно ему этого, или стоит переходить на ручную работу.

Итак, мы хотим промоделировать модуль. Что такое промоделировать выходит за рамки нашего цикла, можно на эту тему написать отдельный большой цикл. То есть, в рамках этого раздела, считаем, что вы знакомы с методикой разработки модели. Но дальше надо всё включить в проект Или нет? Как ни странно, для моделирования модуля совершенно не нужно даже создавать собственный проект. Мы можем прицепиться в качестве паразита к любому проекту, не включая в него ничего нового, а только создав тестовый набор, который никак не будет участвовать в основной сборке.

Давайте ради интереса прицепим к нашему ULPI-проекту вот такой забавный модуль на SystemVerilog, написанный мной специально для иллюстрации и не имеющий никакого отношения к разрабатываемому анализатору. Просто некоторое время назад довелось много возиться с вычислением контрольных сумм, вот он в голову и пришёл.
module sum(input         clk,input [7:0]   data,input         we,input         sof,output [15:0] sum);logic [15:0] temp;always @ (posedge clk)begin     if (we)      begin         if (sof)             temp <= data;         else             temp <= temp + data;     endend// В идеале - так//assign sum = (~temp)+1;// Но контролировать проще так:assign sum = temp;endmodule

Видно, что данные в него поступают по шине, очень отдалённо напоминающей AVALON_MM, а выходят просто в параллельном коде.

Положим получившийся файл в каталог с нашим проектом, но не станем включать его в проект в Quartus. Вместо этого создадим тестовый набор специально под него. Для этого выбираем пункт меню Assignments>Settings:



и в появившемся дереве ищем пункт EDA Tools Settings>Simulation:



Кстати, о типе моделирования, выделенном зелёной рамкой. Возможно, кто-то помнит, в первых статьях я говорил, что при создании проекта чисто по привычке выбираю ModelSim Altera? Это было то самое ружьё на сцене, которое рано или поздно должно было выстрелить. Но если при создании проекта тип моделирования не был выбран, его можно выбрать или изменить здесь.

Продолжаем создавать тестовый набор. Переключаем радиокнопку на Compile test bench (кстати, а как этот термин красиво переводится на русский? Я не могу заставить себя писать тестовый стенд, так как не вижу никакого стенда) и нажимаем кнопку Test Benches:



В открывшемся диалоге нажимаем New:



Если делать тестовый набор вручную, то можно заполнить поля за один проход. Но так как мы делаем всё при помощи мышки, то сейчас заполняем только часть полей, а остальные дозаполним позже. В поле Test bench name я вбил слово Parazit (а как ещё назвать тест, который просто паразитирует на проекте?). Слово Parazit под ним заполнилось автоматически. Сейчас мы не будем его менять, но в будущем нам ещё предстоит это сделать. Также при помощи кнопки ... я выбрал файл sum.sv с кодом отлаживаемого сумматора, после чего, при помощи кнопки Add, затолкнул его в список файлов теста. Пока всё. Закрываем диалог



Дальше мы продолжим формирование теста в среде ModelSim. Для этого выбираем пункт меню Tools>Run Simulation Tools>RTL Simulation:



Открывается окно ModelSim. Возможно, будут найдены ошибки в коде Verilog, тогда надо закрывать ModelSim, править ошибки, открывать вновь. Но рано или поздно, перечень ошибок станет чисто организационным. У меня он выглядит так:



Не найдено модуля верхнего уровня. Это нормально. Мы его ещё не создали просто. Поэтому идём в перечне библиотек к work и раскрываем её. Вот он, наш сумматор.



Наводимся на него, нажимаем правую кнопку Мыши и выбираем пункт меню Create Wave. Это в тексте всё так занудно, если бы я снимал видео, весь процесс занимал бы десятки секунд, так что не пугайтесь, а следите за руками дальше. Итак, Create Wave



Интерфейсные сигналы модуля автоматически переехали на график:



Надо назначить значение какого-нибудь из них. Не важно какого, важно назначить. Очень старая среда моделирования Квартуса умела красиво генерить тактовые сигналы. Увы, её давно изъяли из поставки, так как стали прилагать ModelSim, а тут с подобным всё не так красиво. Проку в формировании генератора здесь, я не увидел, поэтому даже показывать не буду. Так что Ну, давайте линию we нулю присвоим. Наводимся на сигнал, нажимаем правую кнопку, выбираем пункт меню Edit>Wave Editor>Create/Modify WaveForm.



В появившемся диалоге выбираем Constant. И время заодно поменяем, скажем, на 100 микросекунд:



Далее указываем значение 0:



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



Выбираем тип файла Verilog Testbench (кстати, очень жаль, что не SystemVerilog, но в будущем можно будет поправить и ручками). Также задаём имя файла. Я назвал его parazit_tb, по принципу а почему бы и нет?.



Всё, ModelSim можно закрывать, времянку при этом сохранять не нужно.

Что делать с моделью дальше


Вот такой кривоватый, но всё-таки готовый Верилоговский файл нам создала система:
`timescale 1ns / 1nsmodule parazit_tb  ;    reg    sof   ;   reg    we   ;   wire  [15:0]  sum   ;   reg  [7:0]  data   ;   reg    clk   ;   sum     DUT  (        .sof (sof ) ,      .we (we ) ,      .sum (sum ) ,      .data (data ) ,      .clk (clk ) ); // "Constant Pattern"// Start Time = 0 ns, End Time = 100 us, Period = 0 ns  initial  begin  end  initial#0 $stop;endmodule

Автоматика избавила нас от написания стандартных блоков. Причём если бы интерфейсных сигналов было больше, автоматика бы послушно прописала бы и соединила все цепи. Лично меня при ручном создании тестовых наборов удручает именно процесс описания сигналов и их проброса. Теперь в этом файле мы сейчас создадим модель среды, которая будет воздействовать на отлаживаемый модуль sum.

Как видим, толку от задания констант, сделанного автогенератором никакого. Но всё-таки, созданы все цепи, подключён модуль, подлежащий тестированию, даже секция initial создана. Давайте облагородим код. Первое выкинем точку останова, удалив строки:
  initial#0 $stop;

Дальше добавим модель тактового генератора (как же мне не хватает замечательного генератора, который делали старинные Квартусы! Там можно было задать частоту в мегагерцах и не думать о пересчёте её в период, а тем более полупериод).
  always   begin      clk = 0;      #5;      clk = 1;      #5;  end

Теперь нам надо послать несколько байт данных. Проще всего это сделать прямо в секции initial, но если я буду прописывать там каждую фазу доступа к шине, код в этой секции станет запутанным. Поэтому я сделаю такую задачку (именно она выступает в роли модели шины):
task SendByte (input reg[7:0] D);    begin        data = D;        we = 1;        @(posedge clk);        #1        we = 0;   endendtask

Ну, и впишу назначение констант и вызов циклов работы с шиной в блок initial. Напоминаю, что запись типа #123 означает ждать 123 единицы времени. У нас это наносекунды. Также напоминаю, что так как присвоения идут последовательно, используем операцию равно, а не стрелка. Итого, имеем следующий основной код тестирования:
Смотреть здесь
  initial  begin     sof = 0;     we = 0;     data = 0;     #13;     // Первый байт кадра     sof = 1;     SendByte (1);     // Остальные байты     sof = 0;     SendByte (5);     SendByte (1);     // А тут мы промоделируем небольшую задержечку     #20;     SendByte (1);  end


Итого, у нас полный код модуля приобрёл такой вид:
Смотреть полный код модуля.
`timescale 1ns / 1nsmodule parazit_tb  ;    reg    sof   ;   reg    we   ;   wire  [15:0]  sum   ;   reg  [7:0]  data   ;   reg    clk   ;   sum     DUT  (        .sof (sof ) ,      .we (we ) ,      .sum (sum ) ,      .data (data ) ,      .clk (clk ) );   always   begin      clk = 0;      #5;      clk = 1;      #5;  endtask SendByte (input reg[7:0] D);    begin        data = D;        we = 1;        @(posedge clk);        #1        we = 0;   endendtask// "Constant Pattern"// Start Time = 0 ns, End Time = 100 us, Period = 0 ns  initial  begin     sof = 0;     we = 0;     data = 0;     #13;     // Первый байт кадра     sof = 1;     SendByte (1);     // Остальные байты     sof = 0;     SendByte (5);     SendByte (1);     // А тут мы промоделируем небольшую задержечку     #20;     SendByte (1);  endendmodule



Завершение подготовки тестового набора


Пришла пора добавить этот текст к тестовому набору. Для этого идём в уже известный нам диалог



Но теперь наш набор не создаём, а выбираем в списке. В будущем список будет расти по мере добавления наборов Выбрав, нажимаем кнопку Edit. Я внёс в настройки три правки:
  1. Добавил файл parazit_tb.v в список.
  2. Так как файле parazit_tb.v модуль верхнего уровня имеет имя parazit_tb (можете убедиться, глянув исходник из предыдущего раздела), я вписал это имя в строку Top level module in test bench.
  3. Я сказал вести моделирование в течение 10 микросекунд, после чего приостановиться. Если что я домоделирую через нажатие кнопок ручного управления.




Итого


Закрываем всё. Снова запускаем ModelSim. Видим, что всё работает верно. Данные приходят и учитываются в сумме. Если же на такте нет данных (we в нуле) сумма не увеличивается.



Как пользоваться самой средой моделирования это тема на несколько статей. Причём скорее в видеоформате. Но в целом мы познакомились с методикой быстрой подготовки и запуска тестов на языке Verilog из среды Quartus.

Теперь, зная, как быстро запустить моделирование, мы можем набросать модели среды для нашей головы USB-анализатора и проверять её работу. При этом мы не запоминали ни одного заклинания ModelSim, так как Квартус позволяет всё настроить при помощи мышки. Все необходимые скрипты он генерит сам и среду ModelSim вызывает тоже сам. Базу для модели нам также создали в автоматическом режиме, хоть её и пришлось затем доработать вручную.

Увы и ах. Один из элементов внешней среды модуль ULPI. Чтобы разработать его модель самостоятельно, надо, во-первых, тщательно разобраться в логике работы той микросхемы. А в предыдущей статье я говорил, что она очень заковыристая. Ну и, во-вторых, надо затратить уйму времени на разработку кода модели. И устранение ошибок в нём Понятно, что проще найти что-то готовое. Но готовую модельку удалось найти только на языке SystemC. Поэтому в следующей статье мы будем учиться моделировать систему с использованием этого языка.
Подробнее..

Моделирование прошивки в среде ModelSim с использованием моделей на языке SystemC

29.09.2020 14:15:47 | Автор: admin
В прошлой статье мы познакомились с процессом моделирования прошивки в среде ModelSim, где и целевой код, и генератор тестовых воздействий написаны на языке Verilog. Жаль, но для решаемой в цикле цели этого недостаточно. Я уже многократно продвигал идею, что разработка для комплекса Redd должна идти с наименьшими трудозатратами. Если модель устройства пишется быстро, её можно написать с нуля. В прошлый раз мы сделали модель шины, по которой писали байты в сумматор. Но ULPI очень сложная вещь. Написать её модель с нуля ой, как не просто. Если можно найти готовую, лучше это сделать. И я нашёл Увы и ах, она оказалась на языке SystemC. Как начать работать с этим языком, мы сейчас и рассмотрим.




Предыдущие статьи цикла
  1. Разработка простейшей прошивки для ПЛИС, установленной в Redd, и отладка на примере теста памяти
  2. Разработка простейшей прошивки для ПЛИС, установленной в Redd. Часть 2. Программный код
  3. Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС
  4. Разработка программ для центрального процессора Redd на примере доступа к ПЛИС
  5. Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd
  6. Веселая Квартусель, или как процессор докатился до такой жизни
  7. Методы оптимизации кода для Redd. Часть 1: влияние кэша
  8. Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин
  9. Экстенсивная оптимизация кода: замена генератора тактовой частоты для повышения быстродействия системы
  10. Доступ к шинам комплекса Redd, реализованным на контроллерах FTDI
  11. Работа с нестандартными шинами комплекса Redd
  12. Практика в работе с нестандартными шинами комплекса Redd
  13. Проброс USB-портов из Windows 10 для удалённой работы
  14. Использование процессорной системы Nios II без процессорного ядра Nios II
  15. Практическая работа с ПЛИС в комплекте Redd. Осваиваем DMA для шины Avalon-ST и коммутацию между шинами Avalon-MM
  16. Разработка простейшего логического анализатора на базе комплекса Redd
  17. Разработка логического анализатора на базе Redd проверяем его работу на практике
  18. Делаем голову шинного USB-анализатора на базе комплекса Redd
  19. Моделируем поведение Quartus-проекта на Verilog в среде ModelSim


Вообще, эта статья в виде DOC файла появилась ещё в июне. Тогда был написан блок из пяти статей одновременно. Но выгрузить DOC файл на Хабр та ещё задача. Поэтому так вышло, что время именно на неё появилось только сейчас (а ещё две томятся в ожидании). При выгрузке, я заметил, что если не пропитаться духом предыдущих статей, эта выглядит каким-то занудством. Поэтому, если есть такое желание освежите в памяти хотя бы прошлую статью, а лучше эти две (Делаем голову шинного USB-анализатора... и Моделируем поведение Quartus-проекта...).

Введение


Итак, готовая модель, где её взять? Есть проект, решающий точно такую же задачу, что и анализатор, который мы разрабатываем, но имеющий пару особенностей. Первая особенность он для ПЛИС Xilinx. Вторая он совершенно не документирован. Как-то работает. Можно даже купить готовую макетную плату, залить в неё готовый двоичный код И получить какую-то функциональность. Кому нужен прибор любой ценой, может просто пойти по этому пути. Но как его развивать не знает никто. Тот проект лежит тут . В каталоге \ulpi_wrapper\testbench лежит комплект файлов для тестирования подсистемы обёртки вокруг ULPI. Там рекомендуют вести моделирование в среде Icarus Verilog, но я порылся и не нашёл на поверхности путных описаний, как это делать на языке SystemC. Поэтому решил продолжить работу в среде ModelSim. Если бы я знал, чем это кончится Но я не знал. Поэтому начал исследования. По ходу изложения будут показаны как успехи, так и неудачи. Начнём с неудач, чтобы все видели, как не стоит делать.

Неудачная попытка сделать всё в лоб


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



Запускаю ModelSim и не вижу в группе work ничего, что было бы связано с SystemC. Верилоговский код вижу, а Сишный нет.



Если посмотреть на логи, то видно, что его и не пытались собирать. В чём дело?



Полезная информация про настройку файла *.do


Известно, что для запуска ModelSim используется файл *.do. Но как любитель всё делать мышкой, я никогда не заглядывал ему внутрь. Давайте его поищем и откроем! В каталоге проекта находится только один такой файл. Наверное, это то, что нам нужно.



Открываем его. В начале сборка всяких служебных вещей и файлов, входящих в проект.
Смотреть текст
transcript onif ![file isdirectory verilog_libs] {file mkdir verilog_libs}if ![file isdirectory vhdl_libs] {file mkdir vhdl_libs}vlib verilog_libs/altera_vervmap altera_ver ./verilog_libs/altera_vervlog -vlog01compat -work altera_ver {c:/intelfpga_lite/17.1/quartus/eda/sim_lib/altera_primitives.v}vlib verilog_libs/lpm_vervmap lpm_ver ./verilog_libs/lpm_vervlog -vlog01compat -work lpm_ver {c:/intelfpga_lite/17.1/quartus/eda/sim_lib/220model.v}vlib verilog_libs/sgate_vervmap sgate_ver ./verilog_libs/sgate_vervlog -vlog01compat -work sgate_ver {c:/intelfpga_lite/17.1/quartus/eda/sim_lib/sgate.v}


А вот в конце явно сборка нужных нам вещей, это я сужу по имени файла ulpi_wrapper.v:
vlog -vlog01compat -work work +incdir+C:/Work/UsbHead1/SystemCPlay {C:/Work/UsbHead1/SystemCPlay/ulpi_wrapper.v}vsim -t 1ps -L altera_ver -L lpm_ver -L sgate_ver -L altera_mf_ver -L altera_lnsim_ver -L cycloneive_ver -L rtl_work -L work -L UsbHead1 -voptargs="+acc"  lalalaadd wave *view structureview signalsrun 10 us

Действительно. Есть сборка Verilog-овского модуля, и нет никаких намёков на сборку модулей на SystemC. Жаль только, что этот DO-файл автоматически создаётся при каждом запуске моделирования, так что просто взять и отредактировать его не получится. Его создаёт очень сложный TCL-скрипт. Править его нет никакого желания. Но после статьи про весёлую квартусель, наверное, понятно, что такая мелочь не повод опускать руки. Наверняка, всё уже есть. Жаль только, что в документации сказано, что вы можете сделать скрипт так, а можете так, и нет никаких намёков на примеры. Ну что ж, давайте выводить всё экспериментальным путём. Создаём файл C:\Work\UsbHead1\SystemCPlay\myrun.do и пытаемся передать ему управление. Сначала пробуем это сделать так:



Основной DO-файл всё равно продолжает вырабатываться, но его концовка становится такой:
vlog -sv -work UsbHead1 +incdir+C:/Work/UsbHead1/UsbHead1/synthesis/submodules {C:/Work/UsbHead1/UsbHead1/synthesis/submodules/UsbHead1_master_0_b2p_adapter.sv}vlog -sv -work UsbHead1 +incdir+C:/Work/UsbHead1/UsbHead1/synthesis/submodules {C:/Work/UsbHead1/UsbHead1/synthesis/submodules/UsbHead1_master_0_timing_adt.sv}vlog -vlog01compat -work work +incdir+C:/Work/UsbHead1/SystemCPlay {C:/Work/UsbHead1/SystemCPlay/ulpi_wrapper.v}vsim -t 1ps -L altera_ver -L lpm_ver -L sgate_ver -L altera_mf_ver -L altera_lnsim_ver -L cycloneive_ver -L rtl_work -L work -L UsbHead1 -voptargs="+acc"  lalalado C:/Work/UsbHead1/SystemCPlay/myrun.do

Мы видим, что Verilog файл по-прежнему компилится, далее по-прежнему запускается процесс моделирования (правда, я-то это видел при пробных запусках, но теперь могу точно сказать, что команда vsim запускает этот процесс), после чего управление передаётся нашему скрипту. Этот скрипт должен процессом отображения управлять. Но сборкой мы управлять по-прежнему не можем. Если собранных файлов не хватает, система отвалится по ошибке раньше, чем нам дадут что-либо сделать. Ну и отлично, пробуем последний вариант настройки.



И тут начинается самое интересное. Оно настолько важно, что я возьму это в рамочку.

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


Скрипт всё равно формируется, но его концовка стала более удобной для нас.
vlog -sv -work UsbHead1 +incdir+C:/Work/UsbHead1/UsbHead1/synthesis/submodules {C:/Work/UsbHead1/UsbHead1/synthesis/submodules/UsbHead1_master_0_timing_adt.sv}do "C:/Work/UsbHead1/SystemCPlay/myrun.do"

Наконец-то процесс сборки исходников для проекта полностью отдан нам на откуп! Замечательно! На тот момент я смог найти только документ SystemC Verification with ModelSim, написанный для Xilinx. Но ModelSim, он и в Африке ModelSim. Пользуясь примерами из этого документа и образцами DO-файла, созданного на прошлых опытах, я сделал следующий текст скрипта (не пугайтесь обилию ключей, ниже мы почти все выкинем, абсолютные пути тоже потом заменим на относительные, на этом этапе я просто всё дергал из примеров и автоматически сгенерённых образцов).
vlog -vlog01compat -work work +incdir+C:/Work/UsbHead1/SystemCPlay {C:/Work/UsbHead1/SystemCPlay/ulpi_wrapper.v}vlib sc_worksccom g I C:/intelFPGA_lite/17.1/quartus/cusp/systemc/include work sc_work C:/Work/UsbHead1/SystemCPlay/ulpi_driver.cpp

Барабанная дробь И ModelSim нам заявляет:



Если опустить все неприличные слова, то мне и сказать-то нечего Но такой путь пройден! И где взять другую модель ULPI? Разумеется, я договорился с иностранными знакомыми, профессионально занимающимися серьёзными проектами для ПЛИС. Специально для меня они открыли на выходные удалённый доступ к машине с лицензионным ModelSim. Второй блин также оказался комом: 64-битная версия даже в лицензионном виде не работает с SystemC. Но в конце концов, мне удалось поиграть с 32-битной версией лицензионного ModelSim. Поэтому продолжаем рассказ

Пара слов о документации


Итак. Теперь, когда я получил доступ к лицензионному ПО, самое время поговорить о том, где искать информацию и где черпать вдохновение. В сети сведения о языке достаточно обрывочны. Но в поставке системы, имеются следующие полезные каталоги:

C:\modeltech_10.2c\docs\pdfdocs документация, включая файлы в формате PDF. Мне понравились файлы modelsim_se_ref.pdf (ModelSim SE Command Reference Manual), modelsim_se_user.pdf (ModelSim SE Users Manual) и modelsim_se_tut.pdf (ModelSim SE Tutorial). По самому языку там мало что есть, но по тому, как подключать файлы и как решать проблемы диалектов вполне.

Дальше, полезный каталог C:\modeltech_10.2c\examples. Там есть примеры готовых файлов *.do и готовых файлов cpp и h. Самый полезный для нас пример C:\modeltech_10.2c\examples\systemc\vlog_sc. В нём показано, как обращаться из Verilog кода к коду на SystemC. Мы, в итоге, пойдём именно этим путём.

В каталоге C:\modeltech_10.2c\include\systemc содержатся исходные коды библиотеки типов языка. Неплохой справочник. Как говорится, на безрыбье и рак рыба.

Из каталогов всё. Теперь название замечательной книги, из которой можно узнать многое как о языке, так и о методике программирования на нём. SystemC From the Ground Up, Second Edition. Авторы David C. Black, Jack Donovan, Bill Bunton, Anna Keist.

Диалекты языка SystemC


Итак. Получив доступ к работающей системе, я радостный собрал проект, согласно ранее созданному скрипту. Он собрался без ошибок! Первая моделька с ГитХаба согласилась работать с нами! Желая прогнать эталонный тест, я добавил в проект файл ulpi_wrapper_tb.cpp из того же каталога и получил массу ошибок. Допустим, ошибку в строке:
m_vpi_handle = vpi_handle_by_name((const char*)name, NULL);
поправить сложно, но ещё можно. Но строка
        // Update systemC TB        if(sc_pending_activity())            sc_start((int)(time_value-m_last_time),SC_NS);

навевала плохие мысли. Функция sc_pending_activity() в библиотеках отсутствует. Имеется функция sc_pending_activity_at_current_time(), но с нею я даже разбираться не стал. Вместо тысячи слов объяснения, приведу дамп:



И файлов с таким текстом (*.exe, *.dll и т. п.) нашлось 44 штуки.

Можно было попытаться всё переписать Но надо ли оно? Напомню, я вообще-то начал всё это, так как хотел воспользоваться всем готовым. Разработать я всё и в бесплатной среде на чистом SystemVerilog могу, если уж тратить кучу времени Я шёл сюда, чтобы время не потратить, а сэкономить! Но на самом деле Главное не забыть, что мы делаем. Мы хотим воспользоваться моделькой шины ULPI. Она собралась. Проблемы возникли при попытке собрать полную тестовую систему из примера А зачем это? Ну не работает полная система, и ладно. Будем осваивать одну модельку, не глядя на работу системы, методом проб и ошибок.

Устраняем непонимание, основанное на диалектах


Итак. Мы будем делать смешанную систему. Модуль с моделью будет написан на языке SystemC, а тестовые воздействия ему и разрабатываемому модулю я буду подавать на языке Verilog. То есть, надо добиться появления модуля ulpi_driver в группе work.

Осматривая примеры файлов *.do из поставки ModelSim, я сильно упростил скрипт, и в итоге, сделал такой:
vlog +../../SystemCPlay {../../MyCores/ULPIhead.sv}sccom -g ../../SystemCPlay/ulpi_driver.cppsccom -link

Ошибок нет, но и модуль в группе не появился. Осматривая файлы примера (напомню, лучший пример, реализующий именно такое смешивание языков есть в каталоге C:\modeltech_10.2c\examples\systemc\vlog_sc), я понял, что в конец файла ulpi_driver.cpp надо добавить строчку:
SC_MODULE_EXPORT(ulpi_driver);

Документация на ModelSim говорит, что это особенности диалекта. И вуаля! Вот он, наш модуль:



Правда, для него меню Create Wave (это меню мы рассматривали в прошлой статье) недоступно. И портов у него нет. Исторически я сначала разбирался с портами, но методически я отложу рассказ про них на потом. Иначе придётся дважды править код. Чтобы этого не делать, сначала проведём небольшую подготовку.

Делаем тактовый генератор


Оказалось, что у модели есть пара отличий от настоящего ULPI. Первое отличие состоит в том, что тактовый сигнал 66 МГц должен вырабатывать чип. А что мы видим в модели?
    sc_in<bool>             clk_i;

Непорядок! Начинаем переделку! Все работы, если не указано иное, ведём в файле ulpi_driver.h.
Заменяем тип порта. Было:
    sc_in<bool>             clk_i;

стало (я ещё и имя порта поменял):
    sc_inout<bool>             clk;

Из книжки я узнал, что настоящий генератор вставляется путём добавления переменной:
    sc_clock oscillator;

Параметры задаём в конструкторе. В итоге, конструктор приобретает вид:
    //-------------------------------------------------------------    // Constructor    //-------------------------------------------------------------    SC_HAS_PROCESS(ulpi_driver);    ulpi_driver(sc_module_name name): sc_module(name),                                      m_tx_fifo(1024),                                       m_rx_fifo(1024),                                      oscillator ("clk66",sc_time(15,SC_NS))    {

Последняя строка как раз для этого. При желании, можно даже уже запустить моделирование, дважды щёлкнуть по модулю usb_driver, дальше вытянуть clk66 на времянку и немного прогнать процесс моделирования. Мы уже видим, как работает генератор:



Не забудем поменять имя тактового сигнала и в месте, где стартует основной поток. Было:
        SC_CTHREAD(drive, clk_i.pos());

Стало:
        SC_CTHREAD(drive, clk.pos());


Внутренние связи заменены. А вот как красиво вывести сигнал наружу, я не нашёл. Возможно, мне просто не хватает квалификации. Но так или иначе, а все попытки вытянуть порт наружу, не увенчались успехом. Всегда что-то мешало. Я даже нашёл на одном форуме обсуждение, где автору нужно было сделать то же самое. Коллектив решил, что можно пробросить только на входные порты. Но нам же надо на выход! Поэтому делаем так.

Добавляем под конструктором функцию потока:
    void clkThread(void)     {       while (true)       {           wait(oscillator.posedge_event());           clk.write (true);           wait(oscillator.negedge_event());           clk.write (false);       }    }

И добавляем ссылку на неё в конструктор класса:
        SC_THREAD(clkThread);

Давайте я покажу текущий район конструктора, чтобы было целостное видение текущего результата:
    SC_HAS_PROCESS(ulpi_driver);    ulpi_driver(sc_module_name name): sc_module(name),                                      m_tx_fifo(1024),                                       m_rx_fifo(1024),                                      oscillator ("clk66",sc_time(15,SC_NS))    {        SC_CTHREAD(drive,clk.pos());        SC_THREAD(clkThread);        m_reg[ULPI_REG_VIDL]    = 0x24;        m_reg[ULPI_REG_VIDH]    = 0x04;        m_reg[ULPI_REG_PIDL]    = 0x04;        m_reg[ULPI_REG_PIDH]    = 0x00;        m_reg[ULPI_REG_FUNC]    = 0x41;        m_reg[ULPI_REG_OTG]     = 0x06;        m_reg[ULPI_REG_SCRATCH] = 0x00;    }    void clkThread(void)     {       while (true)       {           wait(oscillator.posedge_event());           clk.write (true);           wait(oscillator.negedge_event());           clk.write (false);       }    }

Всё. Первая правка завершена.

Делаем двунаправленную шину данных


У ULPI двунаправленная шина данных. А в модели мы видим следующее её описание:
    sc_out <sc_uint<8> >    ulpi_data_o;    sc_in  <sc_uint<8> >    ulpi_data_i;

Непорядок! Сначала мы сделаем заготовку на базе выходной шины, а затем переключим всё на неё. С чего начать? С того, что шина должна уметь переходить в третье состояние, а тип sc_uint<8> работает только с двоичными данными. Нам поможет тип sc_lv<8>. Поэтому меняем объявление шины на:
    sc_inout <sc_lv<8> >    ulpi_data_o;

Теперь переходим в файл ulpi_driver.cpp и там ищем все обращения к шине ulpi_data_o. Интуитивно я понял, что поправить надо только одно место:


То же самое текстом.
void ulpi_driver::drive_input(void){    // Turnaround    ulpi_dir_o.write(false);    ulpi_nxt_o.write(false);    ulpi_data_o.write(0x00);    wait(oscillator.posedge_event());}



Меняем выделенную строку на
    ulpi_data_o.write("ZZZZZZZZ");

Всё. Теперь можно вместо двух строк:
    sc_inout <sc_lv<8> >    ulpi_data_o;    sc_in  <sc_uint<8> >    ulpi_data_i;

написать одну:
    sc_inout <sc_lv<8> >    ulpi_data;

и заменить все ссылки на старые переменные как в h-нике, так и в cpp-шнике на ссылки на переменную ulpi_data.

Добавляем псевдонимы для портов


Итак. После долгих поисков я пришёл к выводу (возможно, ошибочному), что в среде ModelSim просто взять и увидеть порты для отдельно лежащего модуля на SystemC средствами GUI, не судьба. Однако, если этот модуль вставить в тестовую систему, они появятся. Но пока рылся с теорией, нашёл, как красиво задать псевдонимы для имён портов. Итоговый конструктор класса стал выглядеть так:
    SC_HAS_PROCESS(ulpi_driver);    ulpi_driver(sc_module_name name): sc_module(name),                                      m_tx_fifo(1024),                                       m_rx_fifo(1024),                                      oscillator ("clk66",sc_time(15,SC_NS)),                                      rst_i ("rst"),                                           ulpi_data ("data"),                                      ulpi_dir_o ("dir"),                                      ulpi_nxt_o ("nxt"),                                      ulpi_stp_i ("stp")    {        SC_CTHREAD(drive,clk.pos());        SC_THREAD(clkThread);        m_reg[ULPI_REG_VIDL]    = 0x24;        m_reg[ULPI_REG_VIDH]    = 0x04;        m_reg[ULPI_REG_PIDL]    = 0x04;        m_reg[ULPI_REG_PIDH]    = 0x00;        m_reg[ULPI_REG_FUNC]    = 0x41;        m_reg[ULPI_REG_OTG]     = 0x06;        m_reg[ULPI_REG_SCRATCH] = 0x00;    }

Делаем тестовую систему


Ну что же. Сделать всё на автомате, чтобы сразу два отлаживаемых модуля (голова анализатора и модель шины ULPI) сами запрыгнули в тестовый файл, у меня не получилось. Но сделаем хотя бы тест для головы, а потом добавим к нему ULPI. Пользуясь методикой из прошлой статьи, я сделал тестовую систему для файла ULPIhead.sv. Файл я назвал sim1.v и тут же переименовал его в sim1.sv.

После чего ручками добавил туда модуль ulpi_driver. Итоговый скрипт myrun.do выглядит так:
vlog +../../SystemCPlay {../../MyCores/ULPIhead.sv}sccom -g ../../SystemCPlay/ulpi_driver.cppsccom -linkvlog +../../SystemCPlay {../../SystemCPlay/sim1.sv}vsim -voptargs="+acc" sim1

Последняя строка вымученная. Без неё не было портов у Verilog кода. Изменяя параметры оптимизации, мы устраняем эту беду. Её я подсмотрел в том файле *.do, который был создан для моделирования нашей системы в самом начале, когда всё ещё делалось на автомате. Правда, там строка длиннющая. Я просто нашёл тот ключ, который решает проблему, и скопировал его. А так не люблю длинных строк, всё лишнее я выкинул.

Теперь добавляем в тестовую систему блок ULPI и делаем тест-пустышку. Просто чтобы убедиться, что все тактовые сигналы тикают, а шины устанавливаются в нужные значения.

У меня получился такой тест.
Смотреть текст.
`timescale 1ns / 1nsmodule sim1  ;    reg    ulpi_dir   ;   wire   source_valid   ;   wire    ulpi_stp   ;   reg    ulpi_clk   ;   reg    ulpi_nxt   ;   reg    reset_n   ;   reg    read   ;   reg  [31:0]  writedata   ;   wire    ulpi_rst   ;   reg    clk   ;   wire  [7:0]  source_data   ;   reg    write   ;   wire  [7:0]  ulpi_data   ;   reg    source_ready   ;   reg  [1:0]  address   ;   wire  [31:0]  readdata   ;   always   begin     clk = 1;     #5;     clk = 0;     #5;  end  ULPIhead  DUT    (       .ulpi_dir (ulpi_dir ) ,      .source_valid (source_valid ) ,      .ulpi_stp (ulpi_stp ) ,      .ulpi_clk (ulpi_clk ) ,      .ulpi_nxt (ulpi_nxt ) ,      .reset_n (reset_n ) ,      .read (read ) ,      .writedata (writedata ) ,      .ulpi_rst (ulpi_rst ) ,      .clk (clk ) ,      .source_data (source_data ) ,      .write (write ) ,      .ulpi_data (ulpi_data ) ,      .source_ready (source_ready ) ,      .address (address ) ,      .readdata (readdata ) );   ulpi_driver ULPI  (      .clk (ulpi_clk),      .rst (ulpi_rst),      .data (ulpi_data),      .dir (ulpi_dir),      .nxt (ulpi_nxt),      .stp (ulpi_stp)  );  initial  begin     reset_n  = 1'b0;     source_ready = 1;     writedata = 0;     address = 0;     read = 0;     write = 0;     #20     reset_n  = 1'b1;  endendmodule





Заключение


Худо-бедно, но мы освоили моделирование на языке SystemC с использованием системы ModelSim. Правда, оказалось, что для этого необходимо иметь доступ к лицензионной 32-битной версии. Свободная версия и лицензионная 64-битная версия такой возможности не дают. Как я понял, совершенно бесплатно всё можно сделать в системе Icarus Verilog, но как именно этого достичь, не разобрался. Мне оказалось проще получить доступ к требуемому ModelSim. В следующей статье мы воспользуемся полученными знаниями, чтобы провести моделирование нашей головы.

В ходе работ были произведены достаточно сложные доработки моделей. Получившиеся файлы можно скачать тут.
Подробнее..

Практические опыты с USB-анализатором на базе Redd

02.11.2020 16:19:26 | Автор: admin
Три статьи назад мы сделали голову для шинного анализатора USB, после чего обсуждали вопросы моделирования и гоняли эту голову на моделях. Правда, всё это было не зря. В прошлой статье я показательно нашёл пару сбоев логики, а в реальности их было чуть больше. Поэтому, начни я всё проверять сразу в железе сидел бы и сокрушался: А чего оно так себя ведёт? Теперь же я уверен, что в поведенческой модели вся логика делает именно то, что было задумано. Что дальше? Те, кто делает серьёзные большие проекты, дальше переходят к моделированию синтезированной модели, а в конце модели, привязанной к реальной топологии упаковки в ПЛИС (так называемое Gate-Level моделирование). Там уже учитываются задержки на всех трассировочных ресурсах при именно этом результате компиляции. Но проект нашей сложности этого не требует. Нам было достаточно убедиться, что вся логика реализована верно. И теперь мы можем перейти к сборке реального анализатора и проверке его работы на практике.




Предыдущие статьи цикла
  1. Разработка простейшей прошивки для ПЛИС, установленной в Redd, и отладка на примере теста памяти
  2. Разработка простейшей прошивки для ПЛИС, установленной в Redd. Часть 2. Программный код
  3. Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС
  4. Разработка программ для центрального процессора Redd на примере доступа к ПЛИС
  5. Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd
  6. Веселая Квартусель, или как процессор докатился до такой жизни
  7. Методы оптимизации кода для Redd. Часть 1: влияние кэша
  8. Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин
  9. Экстенсивная оптимизация кода: замена генератора тактовой частоты для повышения быстродействия системы
  10. Доступ к шинам комплекса Redd, реализованным на контроллерах FTDI
  11. Работа с нестандартными шинами комплекса Redd
  12. Практика в работе с нестандартными шинами комплекса Redd
  13. Проброс USB-портов из Windows 10 для удалённой работы
  14. Использование процессорной системы Nios II без процессорного ядра Nios II
  15. Практическая работа с ПЛИС в комплекте Redd. Осваиваем DMA для шины Avalon-ST и коммутацию между шинами Avalon-MM
  16. Разработка простейшего логического анализатора на базе комплекса Redd
  17. Разработка логического анализатора на базе Redd проверяем его работу на практике
  18. Делаем голову шинного USB-анализатора на базе комплекса Redd
  19. Моделируем поведение Quartus-проекта на Verilog в среде ModelSim
  20. Моделирование прошивки в среде ModelSim с использованием моделей на языке SystemC
  21. Проводим моделирование системы для проверки работоспособности головы USB-анализатора


Доработка кода головы


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

Первое тактирование. Сначала я планировал тактировать систему от основного генератора, удвоив его частоту (с 50 до 100 МГц), как мы это делали для логического анализатора, а голову от выходной частоты ULPI (60 МГц). Для этого я предусмотрел два тактовых домена. Жизнь внесла свои коррективы. Эти тактовые домены потребовали таких пространных объяснений, что статья стала не про USB-анализатор, а про них. Поэтому я избавился от двух тактовых частот. Но как всё тактировать? Решено было, что источником тактовых сигналов станет сама голова! Поэтому ей был добавлен новый порт:
   output              clk60,

И добавлена буквально одна строка текста:
assign clk60 = ulpi_clk;

В верилоговской части всё. Хотя, при упаковке в компонент, мы к тактам ещё вернёмся.

Вторая доработка сигнал GO. В логическом анализаторе в том виде, в каком он пошёл в статью, имеется одна весёлая вещь заполнение FIFO. Давайте я покажу фрагмент готового рисунка из статьи, где мы делали логический анализатор (Разработка простейшего логического анализатора на базе комплекса Redd). Детали реализации (ширина шины) там чуть отличаются, но суть проблемы ясна.



Мы включаем DMA, в итоге сначала пробегают не данные из головы, а данные, накопившиеся в FIFO. А когда они там копились, кто же знает? Надо, как минимум, добавить операцию Flush. Здесь же я сделал сигнал, являющийся ключом, запирающим выход данных из самой головы. Пока он не взведён, голова не выдаёт данные в AVALON_ST, а значит, они не попадут и в FIFO. Соответственно, теперь вместо прямого управления сигналом шины source_valid есть внутренний флаг ядра:
logic source_valid_priv;

Именно его взводит или сбрасывает автомат. А внешний сигнал формируется с учётом бита GO:
logic go;assign source_valid = source_valid_priv & go;

Ну, и сам бит Go формируется регистром управления:
// Обслуживание AVALON_MM на записьalways_ff @(posedge ulpi_clk)begin...   if (write == 1)    begin      case (address)...          2 : begin...                // Бит 1 - запуск анализа,                // Без него данные наружу не выйдут                go <= writedata [1];...

Ну, и третья доработка чисто для красоты. Но эта красота была одной из причин, почему пришлось отказаться от двух тактовых доменов. Я хотел отображать текущий адрес DMA на экране PC при работе TCL-скрипта. Возможно, это как-то даже и можно сделать, но я не нашёл правильного решения. Не вижу я порта, который можно читать через шину AVALON_MM на лету, чтобы видеть текущий адрес записи, и всё тут. Запутавшись в документе, описывающем типовые AVALON-устройства (включая и все виды DMA-контроллеров), я решил подойти к решению вопроса кардинально. Если я не могу узнать, какой адрес сейчас обрабатывается, то кто мне мешает сообщать, сколько данных отправлено в него из источника? Пусть будет так (я опять беру схему от логического анализатора, но кроме ширины шин она ничем не отличается):



На рисунке показано, что счётчик (cnt) блока DMA не имеет выхода на шину AVALON_MM. А новый счётчик (cnt) блока Голова подсчитывает число транзакций записи в шину AVALON_ST и позволяет считать своё значение через шину AVALON_MM. Ну, а мы можем получить ему доступ через порт JTAG и блок Altera HTAG-to-Avalon_MM, а дальше отобразить считанное значение на экране пусть в абсолютном виде, пусть в процентах от размера буфера. Так делают все приличные анализаторы!

Приступаем к внедрению. Добавим в ядре головы ещё один порт. Но адресное пространство шины уже всё занято четырьмя портами. Поэтому я расширил шину адреса. Теперь она выглядит так:
input        [2:0]  address,

Читаем счётчик так:
// Обслуживание AVALON_MM на чтениеalways_comb begin   case (address)...      // Счётчик переданных данных для красивого отображения      4: readdata <= transfer_cnt;

а формируем по перепаду GO. Типовое решение для тех, кто собрался ловить перепад добавить в код задержку на шаг и сравнение прямого и задержанного значений. Итого:
// Это - для красоты. Счётчик переданных данныхlogic [31:0] transfer_cnt = 0;// Его красивое формирование:logic go;logic go_prev;always_ff @(posedge ulpi_clk)begin    // Для ловли перепада нам надо знать    // предыдущее значение бита "go"    go_prev <= go;    // Если анализ только что запустился - сбросили счётчик    if ((!go_prev) && (go))         transfer_cnt <= 0;    // Иначе - считаем каждый такт, когда данные уехали в AVALON_ST    else if (go & source_valid_priv)         transfer_cnt <= transfer_cnt + 1;end

У нас ещё будут доработки головы, поэтому справочный полный текст модуля я приведу в конце статьи.

Упаковка головы в компонент


Компонент с USB-головой довольно сложен. В нём имеется целых две шины, плюс шина conduit, плюс ещё источник тактовых импульсов. Давайте потихоньку рассмотрим все особенности. Начнём с источника тактовых сигналов:



Обратите внимание, что я прописал параметр Clock rate и взвёл флажок Clock rate known. Без этого не будет собираться ядро контроллера SDRAM (оно ведь тоже будет тактироваться от этого источника). А вот так выглядит не шина, а единственный её сигнал (выделение в левом списке отличается на один уровень):



С этим понятно. Теперь шина AVALON_MM. Обратите внимание на времянку чтения. Исходно она была другой. Если я ничего не путаю, исходно параметр Read Wait был равен единице. В целом, вы всегда можете сравнивать свои настройки с моими и добиваться идентичности. Ну, и мы видим, что сопоставленным тактовым источником является тот самый clock_source, который мы только что создали.



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



При подготовке шины ULPI типа conduit, возникает одна особенность, которую мы также уже проходили в статье про логический анализатор. Вот настройки шины в целом:



А особенность состоит в том, что для каждого её сигнала надо вручную прописать уникальное имя signal_type. Вот пример для линии dir:



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

Внешний вид процессорной системы


Оказывается, прямо в редакторе Platform Designer можно менять цвета связей. Жаль только, что число контрастных цветов не так велико, поэтому основные линии я подкрасил вновь обнаруженными средствами редактора, а на какие контрастных цветов не хватило по старинке, вручную.

Схема процессорной системы максимально похожа на ту, которую мы делали для логического анализатора. Поэтому рассмотрим только отличия. Первое отличие в тактировании. Как видно, в финальном варианте блок clk_60 вообще не используется. Его можно даже удалить, но у меня просто рука не поднялась, вдруг ещё пригодится. А так, ни один из его выходов не используется, так что его выкинет оптимизатор.

Источником сброса (красная линия) является блок JTAG_TO_AVALON_MM. А источником тактового сигнала (синяя линия) наша USB-голова.



Зелёная линия это AVALON_MM.

Блок PLL хоть и настроен на два выхода c0 и c1, но реально c0 не используется. c1 по-прежнему тактирует микросхему SDRAM. Настройки PLL просты: входная частота 60 МГц, выход c0 60 МГц, сдвиг 0, выход c1 60 МГц, сдвиг минус 60 градусов. Приведу скриншот для настройки именно этого выхода. Вообще, про настройку PLL рассказывается тут Ускорение программы для синтезированного процессора комплекса Redd без оптимизации: замена тактового генератора.



Голова настроек не имеет. Отмечу только, что шина ULPI у нас экспортируется. Из головы начинается поток данных, идущий по жёлтым стрелкам. Поток проходит те же блоки, что и в логическом анализаторе. Можно сказать, что у анализаторов полностью идентичные туловища с поправкой на некоторые параметры.

FIFO, в отличие от логического анализатора, настроено на 16-битную шину (2 символа на слово). Именно такой выход у нашей головы.



Соответственно, преобразователь формата данных работает по схеме 16->32 (2 символа на слово на входе и 4 на выходе):



Ну, а настройки DMA и контроллера DSRAM идентичны таковым из статьи про логический анализатор Разработка простейшего логического анализатора на базе комплекса Redd. Не будем перегружать этот текст дублями скриншотов.

Черновая проверка


Вспомогательные функции


Черновая проверка позволит нам набросать необходимую базу TCL скрипта и убедиться, что он в принципе работает. Функции для доступа к DMA я взял из кода для проверки логического анализатора. Единственно, что заменил базовые адреса на те, которые автоматически назначались для новой схемы:
variable ULPI_BASE 0x1000020variable DMA_BASE 0x1000000variable DMA_DESCR_BASE 0x1000050# Чтение регистра блока DMA.#proc dma_reg_read { address } {  variable DMA_BASE  variable m_path  set address [expr {$address * 4 + $DMA_BASE}]  return [master_read_32 $m_path $address 1]}# Запись регистра блока DMAproc dma_reg_write { address data } {  variable DMA_BASE  variable m_path  set address [expr {$address * 4 + $DMA_BASE}]  master_write_32 $m_path $address $data}# Запись регистра дескрипторов блока DMAproc dma_descr_reg_write { address data } {  variable DMA_DESCR_BASE  variable m_path  set address [expr {$address * 4 + $DMA_DESCR_BASE}]  master_write_32 $m_path $address $data}proc prepare_dma {sdram_addr sdram_size} {# Остановили процесс, чтобы всё понастраивать# Да, мне лень описывать все константы, # я делаю всё на скорую рукуdma_reg_write 1 0x20# На самом деле, тут должно быть ожидание фактической остановки,# но в рамках теста, оно не нужно. Точно остановимся.# Добавляем дескриптор в FIFO# Адрес источника (вообще, это AVALON_ST, но я всё# с примеров списывал, а там он зануляется)dma_descr_reg_write 0 0# Адрес приёмника. dma_descr_reg_write 1 $sdram_addr# Длинаdma_descr_reg_write 2 $sdram_size# Управляющий регистр (взводим бит GO)dma_descr_reg_write 3 0x80000000# Запустили процесс, не забыв отключить прерыванияdma_reg_write 1 4}

Новые функции это функции доступа к регистрам ULPI. Вообще, по уму там нужно ждать снятия сигнала BSY. Но я утверждаю, что шина JTAG настолько медленная, что BSY на ULPI снимется гарантированно медленнее, чем скрипт успеет сделать обращение к регистру. Поэтому я не трачу на эту заведомо бесполезную работу силы и время. Так что при записи просто положили адрес, положили данные, вышли:
proc ulpi_reg_write {reg_addr reg_data} {  variable ULPI_BASE  variable m_path  set port_addr [expr {$ULPI_BASE + 0}]  set port_data [expr {$ULPI_BASE + 4}]  # Задали адрес регистра  master_write_32 $m_path $port_addr $reg_addr   master_write_32 $m_path $port_data $reg_data   # Надо бы дождаться готовности, но JTAG точно медленнее,  # поэтому практического смысла в этом нет.}

При чтении положили адрес, инициировали процесс чтения, считали результат без ожидания:
proc ulpi_reg_read {reg_addr} {  variable ULPI_BASE  variable m_path  set port_addr [expr {$ULPI_BASE + 0}]  set port_data [expr {$ULPI_BASE + 4}]  set port_ctrl [expr {$ULPI_BASE + 8}]  # Задали адрес регистра  master_write_32 $m_path $port_addr $reg_addr   # Запустили процесс чтения регистра  master_write_32 $m_path $port_ctrl 1  # Ждать я не буду, JTAG стопудово медленней работает  return [master_read_32 $m_path $port_data 1]}

Все желающие могут добавить процесс ожидания готовности самостоятельно.

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

Проверка доступности ОЗУ


Первый тест просто позволяет убедиться, что ОЗУ доступно. Основной код предельно прост:
puts "running"set m_path [lindex [get_service_paths master] 0]open_service master $m_pathputs "ID:"master_write_32 $m_path 0x00 0x11111111master_write_32 $m_path 0x04 0x22222222master_write_32 $m_path 0x08 0x33333333master_write_32 $m_path 0x0c 0x44444444puts [master_read_32 $m_path 0 4]

Если результат запуска даёт эталонный результат, значит система начерно работает. Вот такой результат получается у меня:



Проверка чтения регистров


Убедиться, что регистры читаются, удобно на регистрах идентификации. Вот что говорит на эту тему документация:



Делаем простейший скрипт:
puts "running"set m_path [lindex [get_service_paths master] 0]open_service master $m_pathputs "ID:"puts [ulpi_reg_read 0]puts [ulpi_reg_read 1]puts [ulpi_reg_read 2]puts [ulpi_reg_read 3]

Прогоняем, проверяем результат:



Работает! Регистры читаются верно!

Проверка записи регистров


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



Поэтому я буду читать регистр 0x0A, писать в регистр 0x0C, а в итоге при повторном чтении в регистре 0x0A сбросятся некоторые биты!
Вот текст скрипта:
puts "running"set m_path [lindex [get_service_paths master] 0]open_service master $m_pathputs "Otg Control Before: [ulpi_reg_read 0x0a]"# На пробу отключим подтяжку напрочьulpi_reg_write 0x0c 7puts "Otg Control After: [ulpi_reg_read 0x0a]"

Вот результат:



Все кубики системы работают! Можно начинать боевые опыты

Боевая проверка


Масса команд


Ну что ж. Пришла пора провести боевую проверку. Делаем такое основное тело скрипту (там я отключаю все резисторы шины, перевожу работу в режим FS, настраиваю DMA, взвожу GO и начинаю ждать, когда счётчик заполнения превысит запрошенный мною объём, после чего вывожу на экран начало принятого буфера):
puts "running"set m_path [lindex [get_service_paths master] 0]open_service master $m_path# Готовим регистры к анализу# Отключим подтяжки напрочьulpi_reg_write 0x0c 7# Включаем режим Full Speed (биты 1:0 = 01)    # Зануляем TermSelect (бит 2)# Включаем режим Non Driving (биты 4:3 = 01)ulpi_reg_write 0x06 0x1fulpi_reg_write 0x05 0x09# Собственно, всё. Теперь запускаем DMAprepare_dma 0x00 0x1000# Взводим бит GOmaster_write_32 $m_path [expr {$ULPI_BASE + 8}] 2# И начинаем отображать адресаset cur_dma_addr [master_read_32 $m_path [expr {$ULPI_BASE + 0x10}]  1]while {$cur_dma_addr < 0x100} {puts -nonewline "$cur_dma_addr \r"after 1000set cur_dma_addr [master_read_32 $m_path [expr {$ULPI_BASE + 0x10}]  1]}puts -nonewline "$cur_dma_addr \r"# Сняли бит GOmaster_write_32 $m_path [expr {$ULPI_BASE + 8}] 0puts [master_read_16 $m_path 0 128]puts "Finished!"

Запускаем скрипт и начинаем играть в любимую игру ослика Иа Входит-выходит. Аккуратно подключаем какое-нибудь FS-устройство. Лично мне под руку попался китайский клон USB-бластера. Результат меня сильно озадачил. Он был примерно таким:
0x4801 0x4c01 0x4801 0x4c01 0x4d01 0x4c01 0x4d01 0x4c01 0x4d01 0x4c01 0x4d01 0x4c01 0x4d01 0x4c01

Команды, команды, команды А где данные? Но при одном из прогонов, данные промелькнули. Ровно одна посылка! Тогда я решил, что надо смотреть на данные большого объёма. Поэтому переписал скрипт так, чтобы он принимал целый мегабайт и скидывал его в файл. Тело стало выглядеть следующим образом:
puts "running"set m_path [lindex [get_service_paths master] 0]open_service master $m_path# Готовим регистры к анализу# Отключим подтяжки напрочьulpi_reg_write 0x0c 7# Включаем режим Full Speed (биты 1:0 = 01)    # Зануляем TermSelect (бит 2)# Включаем режим Non Driving (биты 4:3 = 01)ulpi_reg_write 0x06 0x1fulpi_reg_write 0x05 0x09# Собственно, всё. Теперь запускаем DMAprepare_dma 0x00 0x100000# Взводим бит GOmaster_write_32 $m_path [expr {$ULPI_BASE + 8}] 2# И начинаем отображать адресаset cur_dma_addr [master_read_32 $m_path [expr {$ULPI_BASE + 0x10}]  1]while {$cur_dma_addr < 0x100000} {puts -nonewline "$cur_dma_addr \r"after 1000set cur_dma_addr [master_read_32 $m_path [expr {$ULPI_BASE + 0x10}]  1]}puts -nonewline "$cur_dma_addr \r"# Сняли бит GOmaster_write_32 $m_path [expr {$ULPI_BASE + 8}] 0set fileid [open "ShowMe.txt" w]puts $fileid [master_read_16 $m_path 0 0x40000]close $fileidputs "Finished!"

И вот так выглядят участки с данными:


То же самое текстом.
... 0x5e01 0x5e01 0x5e01 0xa500 0x5e01 0x5e01 ...... 0x5d01 0x5d01 0x5d01 0x8c00 0x5d01 0x5d01 ...... 0x5c01 0x5c01 0x5c01 0xba00 0x5d01 0x5d01 ...... 0x5d01 0x5d01 0x5d01 0xa500 0x5d01 0x5d01 ...... 0x5e01 0x5e01 0x5e01 0x8d00 0x5e01 0x5d01 ...... 0x5c01 0x5c01 0x5c01 0x4200 0x5c01 0x5d01 ...


Так получилось, что я знаю суть магического числа 0xA5. Это признак PID. То есть, сначала идёт пакет номер 0x8C, затем пакет номер 0x8D Но все они накрыты просто бешеным количеством команд. Зачем эти команды?

И тут меня осенило. Я же работаю с FS-устройством. Оно гонит данные на смешной частоте. А ULPI выдаёт их с той же частотой, с какой бы выдавал и для HS. И чем ему заполнять пустоты? Вот он командами их и заполняет! Хорошо, что я начал проверку с FS-устройства!

Значит, надо производить фильтрацию.

Фильтр номер раз


Сначала я решил, что надо фильтровать поток по факту начала пакета. То есть выводить команду только в этот момент, а остальные команды пакета игнорировать:



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


То же самое текстом.
... 0x4e03 0x4d03 0x5d03 0xa500 0xec00 0x6a00...... 0x4d03 0x4e03 0x5d03 0xa500 0xed00 0x9200...


Здесь всё понятно. Много пакетов со сброшенным RxActive, и только перед самыми данными RxActive взлетает в единицу. Когда команда равна 5X, биты 5:4 равны 01 (RxActive равны единице). 4X же соответствует значению 00 в этих битах. Вот фрагмент из документации:



Переделываем фильтр так, чтобы он брал только значения, где RxActive равно единице. Увы и ах. Вот очень характерный участок дампа:


То же самое текстом.
0x5d03 0xa500 0x1800 0x7600 0x5d03 0xa500 0x1900 0x8e00 0x5d03 0xa500 0x1a00 0xce00 0xa500 0x1b00 0x3600 0x5d03 0xa500 0x1c00 0x4e00 0x5d03 0xa500 0x1d00 0xb600 0x5d03 0xa500 0x1e00 0xf600 0xa500 0x1f00 0x0e00 0x5d03 0xa500 0x2000 0x6e00 0x5d03 0xa500 0x2100 0x9600 0xa500 0x2200 0xd600 0x5d03 0xa500 0x2300 0x2e00 0x5d03 0xa500 0x2400 0x5600 0x5d03 0xa500 0x2500 0xae00 0xa500 0x2600 0xee00 0x5d03 0x2d00 0x0000 0x1000 0x5e03 0xc300 0x8000 0x0600 0x0000 0x0100 0x0000 ...0x5e03 0xd200 0x5d03 0x6900 0x0000 0x1000 0x5d03 0x5a00 0x6900 0x0000 0x1000 0x5e03 0x4b00 0x1200 0x0100 0x1000 0x0100 0x0000 ...0x5e03 0xd200 0x5d03 0xe100 0x0000 0x1000 0x5d03 0x4b00 0x0000 0x0000 0x5e03 0x5a00


Мы видим, что в некоторых местах PIDы склеились, не будучи разделёнными командой (такие приклеившиеся вещи я выделил жёлтым). Значит, решение близкое к верному, но не совсем

Фильтр номер два


Правильный фильтр я подсмотрел в проекте usbsniffer. Я был почти прав. Оказывается, в пределах одного пакета шины ULPI (то есть без падения линии DIR) может пройти несколько USB-запросов. Поэтому состояние wait1 я зря правил. Надо просто сохранять те команды, где RxActive перешёл из нуля в единицу, в каком бы месте пакета это ни случилось. Замечательно. Если мы ловим переход, то нам нужно защёлкивать прошлое значение. То есть добавить в код процесс, который его защёлкивает. Важно только защёлкивать, когда передаётся команда и не защёлкивать в остальных случаях. Признак команды живёт в source_data[8], защёлкиваемое значение в source_data[4].
logic active_prev = 0;always_ff @(posedge ulpi_clk)begin     if (source_data[8])         active_prev <= source_data[4];end

Ну, и теперь, имея текущее и предыдущее значение, мы можем написать:
logic activated;assign activated = (!active_prev) & source_data[4];

Кто заметил, что я здесь не анализирую факт команды? Всё в порядке, это не ошибка. Просто теперь готовность шины AVALON_ST я задаю так:
assign source_valid = source_data[8]?(source_valid_priv & go & activated):(source_valid_priv & go);

Если команда, то анализировать этот флаг. Иначе не анализировать. В итоге, получился такой симпатичный дамп, в котором все пакеты A5 начинаются после персональной команды (то есть, с начала строки):
0x5d01 0xa500 0x4e00 0x1c00 0x5d01 0xa500 0x4f00 0xe400 0x5d01 0xa500 0x5000 0x0c00 0x5d01 0xa500 0x5100 0xf400 0x5d01 0xa500 0x5200 0xb400 0x5d01 0x2d00 0x0000 0x1000 0x5e01 0xc300 0x8000 0x0600 0x0000 0x0100 0x0000 ...0x5e01 0xd200 0x5d01 0x6900 0x0000 0x1000 0x5d01 0x5a00 0x5d01 0x6900 0x0000 0x1000 0x5e01 0x4b00 0x1200 0x0100 0x1000 0x0100 0x0000 ...0x5e01 0xd200

Если что:
  • A5 PID_SOF
  • 2D PID_SETUP
  • C3 PID_DATA0
  • D2 PID_ACK
  • 69 PID_IN
  • 4B PID_DATA1

Вроде, последовательность пакетов вполне себе логичная. То есть, анализатор начерно работает.

Заключение


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

Поэтому в рамках цикла, за эти пять статей мы познакомились с типовой методикой разработки прошивок для Redd. Продумали процессорную систему, выявили недостающие блоки, написали их, отмоделировали, сделали реальную систему, содержащую блоки, отладили, выявили и устранили мелкие недостатки. Всё. Если нужно что-то большее скорее всего, это задача не для Redd, и вам начальство просто не выделит время на самостоятельный проект. Redd это вспомогательный элемент, а прошивки для него помогают отлаживать что-то другое. Так что рекомендуемая последовательность разработки под комплекс именно такая.

Что же касается самого анализатора, то этот блок статей, как я уже упоминал раньше, был написан в июне. Дальше были сделаны заготовки ещё для пары статей (разумеется, продвинулся и сам анализатор), после чего руководство бросило меня в бой по совершенно другим задачам. И завертелось Так что пока в теме будет сделана небольшая пауза. Но, во-первых, в планах стоит развитие анализатора, а во-вторых, рейтинг у статей такой, что видно, что они кому-то нужны (в отличие от статей про Линукс, одна из которых собрала один балл рейтинга и жалкое количество просмотров, но я пингвинов тоже не люблю). Поэтому наверняка будет продолжение. Но пока что перерыв. Однако, все желающие могут начать свои практические исследования, опираясь на полученные знания.

Для справки.
Исходный код головы USB на момент завершения данной статьи выглядит так.
Смотреть исходный код головы USB.
module ULPIhead(   input               reset,   output              clk60,   // AVALON_MM   input        [2:0]  address,   input               write,   input        [31:0] writedata,   input               read,   output logic [31:0] readdata = 0,   // AVALON_ST   input  logic        source_ready,   output logic        source_valid,   output logic [15:0] source_data = 0,   // ULPI   inout        [7:0]  ulpi_data,   output logic        ulpi_stp = 0,   input               ulpi_nxt,   input               ulpi_dir,   input               ulpi_clk,   output              ulpi_rst);logic      have_reg = 0;logic      reg_served = 0;logic      reg_request = 0;logic      read_finished = 0;logic [5:0] addr_to_ulpi;logic [7:0] data_to_ulpi;logic [7:0] data_from_ulpi;logic      write_busy = 0;logic      read_busy = 0;logic [7:0] ulpi_d = 0;logic force_reset = 0;logic active_prev = 0;logic activated;assign activated = (!active_prev) & source_data[4];always_ff @(posedge ulpi_clk)begin     if (source_data[8])         active_prev <= source_data[4];end// Это - для красоты. Счётчик переданных данныхlogic [31:0] transfer_cnt = 0;// Его красивое формирование:logic go;logic go_prev;logic source_valid_priv;assign source_valid = source_data[8]?(source_valid_priv & go & activated):(source_valid_priv & go);always_ff @(posedge ulpi_clk)begin    // Для ловли перепада, нам надо знать    // предыдущее значение бита "go"    go_prev <= go;    // Если анализ только что запустился - сбросили счётчик    if ((!go_prev) && (go))         transfer_cnt <= 0;    // Иначе - считаем каждый такт, когда данные уехали в AVALON_ST    else if (go & source_valid_priv)         transfer_cnt <= transfer_cnt + 1;end// Формирование регистра статусаalways_ff @(posedge ulpi_clk)begin      // Приоритет у сброса выше      if  (reg_served)           write_busy <= 0;      else if (have_reg)           write_busy <= 1;      // Приоритет у сброса выше      if  (read_finished)           read_busy <= 0;      else if (reg_request)           read_busy <= 1;end// Обслуживание AVALON_MM на чтениеalways_comb begin   case (address)      // Регистр адреса (чисто для самоконтроля)      0 : readdata <= {26'b0, addr_to_ulpi};      // Регистр данных      1 : readdata <= {23'b0, data_from_ulpi};      // 2 - регистр управления, а он - только на запись      // Регистр статуса      3 : readdata <= {30'b0, (reg_request | read_busy), (have_reg | write_busy)};      // Счётчик переданных данных для красивого отображения      4: readdata <= transfer_cnt;      default: readdata <= 0;   endcaseend   // Обслуживание AVALON_MM на записьalways_ff @(posedge ulpi_clk)begin   // Назначение вещей по умолчанию, они могут быть перекрыты   // внутри условия сроком на один такт   have_reg    <= 0;   reg_request <= 0;   if (write == 1)    begin      case (address)          0 : addr_to_ulpi <= writedata [5:0];          // Запись в регистр данных требует сложной работы          1 : begin                data_to_ulpi <= writedata [7:0];                have_reg <= 1;              end          2 : begin                // Младший бит регистра инициирует процесс чтения                reg_request <= writedata[0];                // Бит 1 - запуск анализа,                // Без него данные наружу не выйдут                go <= writedata [1];force_reset = writedata [31];              end         default: begin end      endcase   endend   // Самый главный автоматenum {idle,wait1,wr_st,wait_nxt_w,hold_w,wait_nxt_r,wait_dir1,latch,wait_dir0} state = idle;always_ff @ (posedge ulpi_clk)begin   if (reset)   begin       state <= idle;   end else   begin      // Присвоение сигналов по умолчанию      source_valid_priv <= 0;      reg_served  <= 0;      ulpi_stp <= 0;      read_finished <= 0;      case (state)      idle: begin           if (ulpi_dir)               state <= wait1;           else if (have_reg)                 begin                  // Как я и рассуждал в документе, команду                  // мы выставим прямо тут, не будем плодить                  // состояния                  ulpi_d [7:6] <= 2'b10;                  ulpi_d [5:0] <= addr_to_ulpi;                  state <= wait_nxt_w;                end           else if (reg_request)                begin                  // Логика - как для записи                  ulpi_d [7:6] <= 2'b11;                  ulpi_d [5:0] <= addr_to_ulpi;                  state <= wait_nxt_r;                end         end      // Здесь мы просто пропускаем такт TURN_AROUND      wait1 : begin            state <= wr_st;            // Начиная со следующего такта, можно ловить данные            source_valid_priv <= 1;             // Бит 9 в единице отмечает начало пакета            source_data <= {7'h0,!ulpi_nxt,ulpi_data};         end      // Пока не изменится сигнал DIR - гоним данные в AVALON_ST      wr_st : begin            if (ulpi_dir)            begin              // На следующем тактеа, всё ещё ловим данные               source_valid_priv <= 1;               source_data <= {7'h0,!ulpi_nxt,ulpi_data};            end else               // В документе было ещё состояние wait2,               // но я решил, что оно - лишнее.                state <= idle;         end      wait_nxt_w : begin           if (ulpi_nxt)           begin              ulpi_d <= data_to_ulpi;              state <= hold_w;           end         end      hold_w: begin           // при моделировании выяснилось, что ULPI может           // быть не готова принимать данные. и снять NXT           // Добавил условие...           if (ulpi_nxt) begin              // Всё, по AVALON_MM можно принимать следующий байт              reg_served  <= 1;              ulpi_d <= 0;    // Шину в idle              ulpi_stp <= 1;  // На один такт взвели STP              state <= idle;  // А потом - уйдём в состояние idle           end         end       // От состояния STPw я решил отказаться...       // ...      // Это уже начало чтения. Ждём, когда скажут NXT      // И тем самым подтвердят, что наша команда распознана      wait_nxt_r : begin           if (ulpi_nxt)           begin              ulpi_d <= 0;    // Номер регистра можно убирать              state <= wait_dir1;           end         end      // Ждём, когда нам выдадут данные      wait_dir1: begin          if (ulpi_dir)             state <= latch;        end      // Тут мы защёлкиваем данные      // и без каких-либо условий идём дальше      latch: begin          data_from_ulpi <= ulpi_data;          state <= wait_dir0;        end      // Ждём, когда шина вернётся к чтению      wait_dir0: begin          if (!ulpi_dir)          begin             state <= idle;             read_finished <= 1;          end        end         default:begin         state <= idle;         end      endcase    endend// Так традиционно назначается выходное значение inout-линииassign ulpi_data = (ulpi_dir == 0) ? ulpi_d : 8'hzz;// reset мог прийти извне, а могли его и мы сформироватьassign ulpi_rst = reset | force_reset;assign clk60 = ulpi_clk;endmodule

Подробнее..

Добавляем поддержку Vendor-команд к USB3.0 устройству на базе FX3

02.02.2021 12:18:59 | Автор: admin
В предыдущих статьях мы сделали достаточно интересную железку, состоящую из контроллера FX3 и ПЛИС Cyclone IV. Мы научились гонять через шину USB 3.0 потоки данных с достаточно высокой скоростью (я доказал, что поток 120 МБ/с из ULPI будет проходить через эту систему без искажений и потерь). Всё хорошо, но система, которая просто гонит данные, не имеет смысла. Любую систему надо настраивать. То есть, хочешь не хочешь, а кроме скоростных данных надо слать не очень спешные команды.

У шины USB для передачи команд предназначена конечная точка EP0. Сегодня мы потренируемся дорабатывать прошивку FX3 так, чтобы она обрабатывала команды от PC, а также транслировала их через GPIO в сторону ПЛИС. Кстати, именно здесь проявляется преимущество контроллера над готовым мостом. Что меня в текущей реализации Redd сильно удручает я не могу посылать никаких команд. Их можно только упаковать в основной поток. В случае же с контроллером что хочу, то и делаю. Начинаем творить, что хотим



Предыдущие статьи цикла:
  1. Начинаем опыты с интерфейсом USB 3.0 через контроллер семейства FX3 фирмы Cypress
  2. Дорабатываем прошивку USB 3.0, используя анализатор SignalTap, встроенный в среду разработки Quartus
  3. Учимся работать с USB-устройством и испытываем систему, сделанную на базе контроллера FX3
  4. Боремся с таймаутами при использовании USB 3.0 через контроллер FX3, возникающими при определенных условиях

Введение


Осматривая исходники типовой прошивки, я нашёл знакомое имя функции в файле cyfxgpiftousb.c. Функцию зовут:
/* Callback to handle the USB setup requests. */CyBool_tCyFxApplnUSBSetupCB (        uint32_t setupdat0, /* SETUP Data 0 */        uint32_t setupdat1  /* SETUP Data 1 */    )

Имея за плечами опыт работы с кучкой USB-контроллеров, начиная от прямого предка нашего (это был FX2LP), через STM32 и далее со всеми остановками, я уже нутром чую, что нужная нам функциональность начинается здесь. Собственно, код этой функции как раз разбирает команды группы STANDARD Request. Осталось добавить туда свою группу VENDOR COMMANDS. Жаль только, что все команды, которые уже имеются в готовой функции, не передают данных. Они ограничиваются работой с полями wData и wIndex, Мне этого недостаточно. Я хочу передавать в ПЛИС байт и два 32-битных слова (команда, адрес, данные), либо передавать байт и DWORD, после чего принимать DWORD (передали команду и адрес, приняли данные). То есть, без фазы данных точно не обойтись. Начинаем разбираться, где черпать вдохновение и добавлять желаемую функциональность.

Участок в зоне ответственности шины USB


Итак. Добавить фазу данных. Гуглю по слову:
CyU3PUsbAckSetup

И первая же ссылка ответила на все мои вопросы. На всякий случай вот она.

В том коде данные гоняют и туда, и обратно. Хорошо. Начнём с малого. Сначала вставляем только прогон данных через USB, без их передачи в ПЛИС. Будем для самоконтроля отправлять данные в UART, а при приёме, чтобы не тратить время на сложный вспомогательный код, просто будем заполнять память константами 00, 01 02 03

Добавляем в конец функции CyFxApplnUSBSetupCB() такой блок:
    if (bType == CY_U3P_USB_VENDOR_RQT)    {    // Cut size if needif (wLength > sizeof(ep0_buffer)){wLength = sizeof (ep0_buffer);}    // Need send data to PC    if (bReqType & 0x80)    {    int i;    for (i=0;i<wLength;i++)    {    ep0_buffer [i] = (uint8_t) i;    }    CyU3PUsbSendEP0Data (wLength, ep0_buffer);            isHandled = CyTrue;    } else    {    CyU3PUsbGetEP0Data (wLength, ep0_buffer, NULL);    ep0_buffer [wLength] = 0;// Null terminated String            CyU3PDebugPrint (4, (char*)ep0_buffer);    CyU3PUsbAckSetup();            isHandled = CyTrue;    }    }

Волшебная константа 0x80 согласен, что некрасивая, но не нашлось ничего подходящего в заголовках в районе изучаемого участка, а дальше искать не хотелось. Но, наверное, все помнят, что именно старший бит задаёт направление. Мало того, я в терминологии USB вечно путаюсь, что значит IN, что значит OUT. Я просто запомнил, что, когда есть 0x80 данные бегут в PC. Остальное, вроде, всё красиво и понятно получилось, даже не требует комментариев.

Чтобы не писать своей тестовой программы, проверять я сегодня буду в сниффере BusHound. Если в нём дважды щёлкнуть по устройству, то появляется очень полезный диалог. Вот тут щёлкаем:



И вот такую красоту получаем:



Я заполнил тип команды 0xC0 (Vendor Specific, данные из устройства в PC). Код команды я сделал равным 23 просто так, чисто во время экспериментов. Сейчас туда можно вписать всё, что угодно, в функции это поле не проверяется. Не проверяются и поля Value и Index. А вот когда я вбил поле Length, у меня внизу появился дамп. Всё готово к посылке команды. Нажимаем Run, получаем:



Всё верно. Функция CyFxApplnUSBSetupCB() посылает из FX3 в USB инкрементирующиеся байты, мы их видим. Теперь пробуем передавать. Подключаем UART (как это сделать я рассказывал в одной из предыдущих статей), запускаем терминал. Меняем тип запроса на 0x40 (Vendos Specific Command, данные из PC в устройство). Заполняем поля данных ASCII символами:



Жмём Run получаем:



Прекрасно! Эта часть готова! Переходим к работе с аппаратурой.

Работа с GPIO


Грустная теория


В том же примере, который я нашёл на github, идёт и работа с GPIO. Вот как красиво выглядит это в пользовательской части:
CyU3PGpioSetValue (FPGA_SOFT_RESET,  !((ep0_buffer[0] & GPIO_FPGA_SOFT_RESET) > 0)); CyU3PGpioSetValue (FMC_POWER_GOOD_OUT, ((ep0_buffer[0] & GPIO_FMC_POWER_GOOD_OUT) > 0));

Красиво? Ну, конечно же, красиво! Но впору вспомнить, что я писал в одной из статей про нашу ОСРВ МАКС.

Я там рассказывал, что операторы new и delete по факту раскрываются в огромный кусок кода с непредсказуемым временем исполнения. Примерно так и тут. Функция CyU3PGpioSetValue() раскрывается в такую громаду, что я спрячу её под кат.
Смотреть текст функции CyU3PGpioSetValue().
CyU3PReturnStatus_tCyU3PGpioSetValue (                   uint8_t  gpioId,                   CyBool_t value){    uint32_t regVal;    uvint32_t *regPtr;    if (!glIsGpioActive)    {        return CY_U3P_ERROR_NOT_STARTED;    }    /* Check for parameter validity. */    if (!CyU3PIsGpioValid(gpioId))    {        return CY_U3P_ERROR_BAD_ARGUMENT;    }    if (CyU3PIsGpioSimpleIOConfigured(gpioId))    {        regPtr = &GPIO->lpp_gpio_simple[gpioId];    }    else if (CyU3PIsGpioComplexIOConfigured(gpioId))    {        regPtr = &GPIO->lpp_gpio_pin[gpioId % 8].status;    }    else    {        return CY_U3P_ERROR_NOT_CONFIGURED;    }           regVal = (*regPtr & ~CY_U3P_LPP_GPIO_INTR);    if (!(regVal & CY_U3P_LPP_GPIO_ENABLE))    {        return CY_U3P_ERROR_NOT_CONFIGURED;    }    if (value)    {        regVal |= CY_U3P_LPP_GPIO_OUT_VALUE;    }    else    {        regVal &= ~CY_U3P_LPP_GPIO_OUT_VALUE;    }    *regPtr = regVal;    regVal = *regPtr;    return CY_U3P_SUCCESS;}


Какое будет максимальное быстродействие у кода, вызывающего эту функцию в цикле, мне страшно подумать. У неё есть более компактный аналог, но и его я предпочту спрятать под кат.
Более компактный аналог.
CyU3PReturnStatus_tCyU3PGpioSimpleSetValue (                         uint8_t  gpioId,                         CyBool_t value){    uint32_t regVal;    if (!glIsGpioActive)    {        return CY_U3P_ERROR_NOT_STARTED;    }    /* Check for parameter validity. */    if (!CyU3PIsGpioValid(gpioId))    {        return CY_U3P_ERROR_BAD_ARGUMENT;    }    regVal = (GPIO->lpp_gpio_simple[gpioId] &        ~(CY_U3P_LPP_GPIO_INTR | CY_U3P_LPP_GPIO_OUT_VALUE));    if (value)    {        regVal |= CY_U3P_LPP_GPIO_OUT_VALUE;    }    GPIO->lpp_gpio_simple[gpioId] = regVal;    return CY_U3P_SUCCESS;}


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

Чуть более оптимистичная теория


Чтобы не хранить маску записанных в порт данных, а также обеспечить себе максимальную потокобезопасность, мы можем воспользоваться аппаратурой, дающей независимый доступ к каждому биту порта. Вдохновение мы будем искать в разделе 9.2 GPIO Register Interface документа FX3_Programmers_Manual.pdf.

Вот так выглядит блок GPIO:



Мы видим, что кроме классического двоичного представления, есть такое, где каждой линии (а их в контроллере 61 штука) соответствует собственное 32-разрядное слово. Формат его такой:



Собственно, всё ясно. Так как я собираюсь работать с конкретными линиями GPIO, я вполне могу обращаться к битам IN_VALUE и OUT_VALUE в этих регистрах. Больше мне ничего и не надо. Ну, и настройку направления можно произвести здесь же.

С какими линиями мы работаем


Хорошо. Как нам достукиваться до линий, понятно. А как они адресуются? Что за 61 линия GPIO, о которых говорится в документации? С чем предстоит работать мне? Плату для меня разводил знакомый, которому я поставил очень простую задачу: несколько свободных линий от FX3 завести на ПЛИС. Так как конкретные номера не были мною обозначены, он взял те, которые захотел. Вот участок ПЛИС, к которому подходят линии GPIO, именованные в той нотации, какая задана на шелкографии около разъёма макетки:



Я собираюсь программно реализовать шину SPI, значит, мне надо 4 линии (выбор кристалла, тактовый сигнал и данные туда-обратно). Возьмём линии от DQ24 до DQ27 по принципу А почему бы и нет?. В одной из прошлых статей, я уже показывал таблицу, при помощи которой мы можем быстро сопоставить эти имена с реальными линиями GPIO. Смотрим в неё:



Значит, нас интересуют линии GPIO 41, 42, 43 и 44. Вот с ними я и буду работать.

Инициализация GPIO


Все, кто хорошо знаком с архитектурой ARM, знают, что любые порты надо инициализировать. Как это сделать в нашем случае? Мы работаем с демонстрационным приложением, так что часть работы уже сделана за нас. Доработаем кое-что из готового кода. В функции main(), есть такой участок:
io_cfg.isDQ32Bit = CyTrue;
io_cfg.useUart = CyTrue;
io_cfg.useI2C = CyFalse;
io_cfg.useI2S = CyFalse;
io_cfg.useSpi = CyFalse;
io_cfg.lppMode = CY_U3P_IO_MATRIX_LPP_DEFAULT;

/* No GPIOs are enabled. */
io_cfg.gpioSimpleEn[0] = 0;
io_cfg.gpioSimpleEn[1] = 0;
io_cfg.gpioComplexEn[0] = 0;
io_cfg.gpioComplexEn[1] = 0;
status = CyU3PDeviceConfigureIOMatrix (&io_cfg);

Поправим его так:



То же самое текстом.
    io_cfg.isDQ32Bit = CyFalse;    io_cfg.useUart   = CyTrue;    io_cfg.useI2C    = CyFalse;    io_cfg.useI2S    = CyFalse;    io_cfg.useSpi    = CyFalse;    io_cfg.lppMode   = CY_U3P_IO_MATRIX_LPP_UART_ONLY;    /* No GPIOs are enabled. */    io_cfg.gpioSimpleEn[0]  = 0;    io_cfg.gpioSimpleEn[1]  = (1<<9)|(1<<10)|(1<<11)|(1<<12);    io_cfg.gpioComplexEn[0] = 0;    io_cfg.gpioComplexEn[1] = 0;    status = CyU3PDeviceConfigureIOMatrix (&io_cfg);


Биты 9, 10, 11 и 12 в коде это биты старшего слова. Поэтому физически они соответствуют битам GPIO 9+32=41, 10+32=42, 11+32=43 и 12+32=44. Тем самым, с которыми я собираюсь работать.
Зададим ещё им направления. Скажем, я раскидаю их так:

Бит Цепь Направление
41 SS OUT
42 CLK OUT
43 MOSI OUT
44 MOSI IN


Объявим для этого следующие макросы:
#define MY_BIT_SS    41#define MY_BIT_CLK   42#define MY_BIT_MOSI  43#define MY_BIT_MISO  44

А в функцию CyFxApplnInit() добавим такой код:
    CyU3PGpioClock_t     gpioClock;    gpioClock.fastClkDiv = 2;    gpioClock.slowClkDiv = 16;    gpioClock.simpleDiv  = CY_U3P_GPIO_SIMPLE_DIV_BY_2;    gpioClock.clkSrc     = CY_U3P_SYS_CLK;    gpioClock.halfDiv    = 0;    apiRetStatus = CyU3PGpioInit (&gpioClock, NULL);    if (apiRetStatus != CY_U3P_SUCCESS)    {        CyU3PDebugPrint (4, "GPIO Init failed, error code = %d\r\n", apiRetStatus);        CyFxAppErrorHandler (apiRetStatus);    }    GPIO->lpp_gpio_simple[MY_BIT_SS] = CY_U3P_LPP_GPIO_OUT_VALUE | CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE;    GPIO->lpp_gpio_simple[MY_BIT_CLK] = CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE;    GPIO->lpp_gpio_simple[MY_BIT_MOSI] = CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE;    GPIO->lpp_gpio_simple[MY_BIT_MISO] = CY_U3P_LPP_GPIO_INPUT_EN | CY_U3P_LPP_GPIO_ENABLE;

Всё, блок GPIO инициализирован, направления заданы. А линия SS ещё и взведена в единицу. Можно начинать пользоваться GPIO для реализации функциональности.

Участок в зоне ответственности аппаратуры


Запись в SPI я сделаю в виде макросов взвести в 1 и Сбросить в 0 (увы, именно макросов, перед нами же код на чистых Сях, в плюсах я бы сделал на шаблонных функциях) и одной функции, которая обращается к ним. Получилось так:
#define SET_IO_BIT(nBit) GPIO->lpp_gpio_simple[nBit] |= CY_U3P_LPP_GPIO_OUT_VALUE#define CLR_IO_BIT(nBit) GPIO->lpp_gpio_simple[nBit] &= ~CY_U3P_LPP_GPIO_OUT_VALUEvoid SPI_Write (unsigned int data, int nBits){   while (nBits)   {   if (data&1)   {   SET_IO_BIT (MY_BIT_MOSI);   } else   {   CLR_IO_BIT (MY_BIT_MOSI);   }   SET_IO_BIT (MY_BIT_CLK);   data >>= 1;   nBits -= 1;   CLR_IO_BIT (MY_BIT_CLK);   }}

Соответственно, вместо вывода в UART в ранее написанном обработчике USB-команд, я сделаю вывод в SPI, но по очень хитрому алгоритму. Сначала байт USB-команды. Затем слова wData и wIndex, и потом DWORD, пришедший в фазе данных. При такой солянке сборной, удобнее всё передавать младшим битом вперёд (именно так работает функция SPI_Write()).

Чтение я пока делать не буду. Сейчас проверяется сама идея. Чтобы проверить чтение, надо делать прошивку и для ПЛИС, а запись я могу проконтролировать и при помощи осциллографа.

В результате, код обработчика Vendor-команды трансформируется следующим образом:
    // Need send data to PC    if (bReqType & 0x80)    {    int i;    for (i=0;i<wLength;i++)    {    ep0_buffer [i] = (uint8_t) i;    }    CyU3PUsbSendEP0Data (wLength, (uint8_t*)ep0_buffer);            isHandled = CyTrue;    } else    {    CyU3PUsbGetEP0Data (wLength, (uint8_t*)ep0_buffer, NULL);    ep0_buffer [wLength] = 0;// Null terminated String            CyU3PDebugPrint (4, (char*)ep0_buffer);            CLR_IO_BIT(MY_BIT_SS);            SPI_Write(bRequest,8);            SPI_Write(wValue,16);            SPI_Write(wIndex,16);            SPI_Write(ep0_buffer[0],32);            SET_IO_BIT(MY_BIT_SS);    CyU3PUsbAckSetup();            isHandled = CyTrue;    }

Итого


Итого, даём такой запрос:



И получаем такой результат:



Немного оптимизации


Видно, что данные передаются младшим битом вперёд, хорошо видны байт 0x23 и начало байта 0x55. Всё верно. Правда, частота, конечно, не ахти (её можно разглядеть, если кликнуть по рисунку и посмотреть его в увеличенном виде). Примерно 1.2 мегагерца. В целом, меня сейчас это сильно не беспокоит, но здесь скорее важен сам принцип. Не люблю, когда всё совсем медленно, и всё тут! Смотрим, во что превратилась функция записи, в этом нам поможет файл GpifToUsb.lst:
40003404 <SPI_Write>:40003404:ea00000d b40003440 <SPI_Write+0x3c>40003408:e59f303c ldrr3, [pc, #60]; 4000344c <SPI_Write+0x48>4000340c:e3100001 tstr0, #140003410:e59321ac ldrr2, [r3, #428]; 0x1ac40003414:e1a000a0 lsrr0, r0, #140003418:13822001 orrner2, r2, #14000341c:03c22001 biceqr2, r2, #140003420:e58321ac strr2, [r3, #428]; 0x1ac40003424:e59321a8 ldrr2, [r3, #424]; 0x1a840003428:e2411001 subr1, r1, #14000342c:e3822001 orrr2, r2, #140003430:e58321a8 strr2, [r3, #424]; 0x1a840003434:e59321a8 ldrr2, [r3, #424]; 0x1a840003438:e3c22001 bicr2, r2, #14000343c:e58321a8 strr2, [r3, #424]; 0x1a840003440:e3510000 cmpr1, #040003444:1affffef bne40003408 <SPI_Write+0x4>40003448:e12fff1e bxlr4000344c:e0001000 .word0xe0001000

16 строк. Вполне компактно Я уже много раз писал, что не собираюсь становиться гуру FX3. Поэтому решил не вчитываться в километры документов, а поиграть с кодом на практике. Само собой, несколько часов опытов я опущу, и приведу только итоговый результат. Так что немножко младшим учеником старшего помощника второго заместителя гуру побыть пришлось Но так или иначе. Я изучил вопрос настройки тактирования GPIO и пришёл к выводу, что оно вполне оптимальное.

Но напишем такой тестовый блок кода (первый макрос роняет значение в порту, второй взводит, а дальше идёт чреда взлётов и падений):
#define DOWN GPIO->lpp_gpio_simple[MY_BIT_CLK] = CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE#define UP GPIO->lpp_gpio_simple[MY_BIT_CLK] = CY_U3P_LPP_GPIO_OUT_VALUE | CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;

Ему соответствует участок ассемблерного кода, оптимизировать который в целом, невозможно. Он идеален:
400036c4:e58421a8 strr2, [r4, #424]; 0x1a8400036c8:e58431a8 strr3, [r4, #424]; 0x1a8400036cc:e58421a8 strr2, [r4, #424]; 0x1a8400036d0:e58431a8 strr3, [r4, #424]; 0x1a8400036d4:e58421a8 strr2, [r4, #424]; 0x1a8400036d8:e58431a8 strr3, [r4, #424]; 0x1a8

Результат прогона (получаем меандр с частотой 12.5 МГц):



А теперь заменим запись констант с прямой записи на чтение модификацию запись, как это реализовано в моих макросах для SPI:
#define UP GPIO->lpp_gpio_simple[MY_BIT_CLK] |= CY_U3P_LPP_GPIO_OUT_VALUE#define DOWN GPIO->lpp_gpio_simple[MY_BIT_CLK] &= ~CY_U3P_LPP_GPIO_OUT_VALUE    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;    UP;    DOWN;

В ассемблерном коде покажу только одну итерацию вверх-вниз

400036e4: e59431a8 ldr r3, [r4, #424]; 0x1a8
400036e8: e3c33001 bic r3, r3, #1
400036ec: e58431a8 str r3, [r4, #424]; 0x1a8
400036f0: e59431a8 ldr r3, [r4, #424]; 0x1a8
400036f4: e3833001 orr r3, r3, #1
400036f8: e58431a8 str r3, [r4, #424]; 0x1a8

Вместо пары строк получаем шесть. Частота упадёт втрое? Делаем прогон



12.5/1.9=6.6
Более, чем в шесть раз частота упала! Получается, что чтение из порта довольно медленная операция. Значит, чуть переписываем мои макросы записи в порт, убирая из них операции чтения:
#define SET_IO_BIT(nBit) GPIO->lpp_gpio_simple[nBit] = CY_U3P_LPP_GPIO_OUT_VALUE | CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE#define CLR_IO_BIT(nBit) GPIO->lpp_gpio_simple[nBit] = CY_U3P_LPP_GPIO_DRIVE_LO_EN | CY_U3P_LPP_GPIO_DRIVE_HI_EN | CY_U3P_LPP_GPIO_ENABLE

Делаем прогон записи в SPI



4 мегагерца. Ну вот. Не особо напрягаясь, разогнали систему почти вчетверо. Меня не покидает ощущение, что всё можно разогнать ещё сильнее, но оставим это на потом. Сейчас особо это не требуется.

Заключение


Мы освоили механизм добавления VENDOR команд в USB-устройство на базе FX3. При этом мы испытали работу с командами, передающими данные через конечную точку EP0 в обоих направлениях. Также мы освоили работу с GPIO у этого контроллера. Теперь, кроме скоростной передачи через конечные точки типа BULK и GPIF, мы можем передавать команды в свою прошивку ПЛИС.

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

Делаем блок SPI to AVALON_MM для USB-устройства на базе FX3

24.02.2021 16:18:54 | Автор: admin
В предыдущей статье мы научились подавать Vendor команды в устройство USB3.0 на базе контроллера FX3 и реализовали программную шину SPI. Сегодня мы продолжим начатое и сделаем компонент SPI to Avalon_MM. Может возникнуть вопрос: мы же уже умеем работать с шиной Avalon_MM через JTAG средствами TCL-скриптов, зачем нам что-то ещё?

Дело в том, что когда мы работаем на чистом TCL, как делали это здесь и здесь, всё замечательно. Но для задач, гоняющих десятки или даже сотни мегабайт, этот вариант слишком медленный. Поэтому мы вынуждены добавить программу на С++, работающую через USB 3.0.

Вариант с TCL-сервером, к которому обращается плюсовая программа, рассмотренный в этой статье, требует сложной ручной подготовки при каждом запуске. Надо обязательно запустить среду исполнения (для Windows и Linux они разные), запустить серверный скрипт, а затем в программе синхронизировать работу с данными по USB и с командами через TCP. Не люблю такие сложности. Оставим те варианты под случаи, когда они не создают трудностей. Здесь же у нас есть USB-устройство, мы всё равно с ним работаем, вот и будем обращаться к шине AVALON_MM через него. Приступаем.



Предыдущие статьи цикла:

  1. Начинаем опыты с интерфейсом USB 3.0 через контроллер семейства FX3 фирмы Cypress
  2. Дорабатываем прошивку USB 3.0, используя анализатор SignalTap, встроенный в среду разработки Quartus
  3. Учимся работать с USB-устройством и испытываем систему, сделанную на базе контроллера FX3
  4. Боремся с таймаутами при использовании USB 3.0 через контроллер FX3, возникающими при определенных условиях
  5. Добавляем поддержку Vendor-команд к USB3.0 устройству на базе FX3

Введение


Перед тем как заняться разработкой своего блока, я попытался найти что-то готовое. Да, работа с TCL как напрямую, так и через сеть создаёт ряд неудобств для пользователя. Но нельзя ли достучаться до JTAG-адаптера напрямую? Ну, или хотя бы подключиться к JTAG-серверу, как это делают штатные компоненты системы? Я мучил Гугля всё более и более мудрёными запросами. Увы. Есть вариант, когда сервер реализуется на самой плате (только в нашем варианте с платы выкинут процессор, не на чем его там запустить), но нет примеров, как подключиться к JTAG-серверу, запущенному на PC. Были статьи про существующий сервер для проекта Марсоход, запускаемый на Малине, но насколько я понял, там надо подменять DLL. Было ещё несколько статей с явно нужными ключевыми словами, но все они были удалены, а в кэше Гугля лежало что-то, совершенно нечитаемое.

Я даже выдвинул гипотезу, что дело в ядрах с ограниченными правами использования. Тех, которые работают только тогда, когда подключены к PC с запущенным сервером. Возможно, кто поймёт принцип управления JTAG-сервером, тот сможет их всколоть, а правообладатели этого не хотят, поэтому тщательно скрывают протоколы. У нас нет задачи что-то всколоть, а у меня нет желания писать статью, которую быстро удалят. Поэтому я решил просто сделать свой блок. Какой? Решение выплывает из моей текущей рабочей загрузки. Я играю в среду Litex. Там используется шина AXI-Lite или Wishbone. Я работаю со второй из них и вижу в системе массу переходников. Там есть и SPI to Wishbone, и UART to Wishbone и всё, что угодно to Wishbone. Поэтому я и решил сделать переходник SPI to Avalon-MM.

Где черпаем вдохновение


Если вбить Гуглю запрос Avalon Memory-Mapped Master Templates, то мы попадём вот сюда:

Avalon Memory-Mapped Master Templates (intel.com)

Скачиваем имеющийся на этой странице zip-файл, там есть примеры мастеров для шины AVALON-MM практически на все случаи жизни. В документе приводятся примеры, как просто взять и положить файлы из этого архива, после чего начать работать с ними. Приведу пример рисунка для одного из направлений



Мы должны реализовать участок Control Logic и FIFO, после чего всё заработает само. Сначала я гипнотизировал эти примеры, мечтая пойти именно по этому пути. Первое, что не давало покоя: мастер чтения и мастер записи разные блоки, каждый из которых подключается к шине AVALON. Соединить их не так просто. Затем, начав практические опыты, я понял, что иерархия там тоже получается не самая лучшая. Мне бы пришлось сделать очень много транзитных верёвок. Система становилась слишком сложной, а выше я уже писал, что не люблю сложных систем.

Тогда я внимательно посмотрел на исходные тексты и понял, что на самом деле там реализуются действия, сложность которых не выше, чем у блоков AVALON_MM Slave, которые мы уже делаем, как семечки щёлкаем. Там нет ничего страшного. Нет никаких линий GNT, характерных для некоторых взрослых шин. Вообще ничего нет. Знай себе, ставь стробы и удерживай их, пока не получил подтверждения, что данные прокачались. Всё! Остальное за нас сделает логика, являющаяся внешней по отношению к нашему блоку (она спрятана от нас где-то внутри System Interconnect Fabric).

Некоторые небольшие трудности возникли бы при отладке пакетных передач, но я же не собираюсь их гонять! У меня очень медленная шина SPI (в прошлой статье мы видели, что на ней частота тактовых сигналов не превышает 4 МГц, а данных по ней пробегает 8 + 32 + 32 = 72 бита, итого предельная частота следования данных 55,(5) КГц). Так что получили запрос прогнали одно слово по Avalon, отпустили шину. Ждём следующий запрос. Не нужны тут пакеты!

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

Главный автомат


В основу работы модуля положим конечный автомат. Причём сигналом сброса для него я выбрал положительный уровень линии SS. Давайте я покажу типичную посылку по шине SPI, взяв первую попавшуюся времянку с просторов сети:



На этом рисунке линия называется не SS (Slave Select), а CS (Chip Select). Но мы видим, что её высокий уровень можно трактовать как сброс шины. Это очень удобно. Не надо бояться, что произойдёт рассинхронизация. Мы почти уверены, что перед первым битом этот сигнал перейдёт из единицы в ноль. В своём коде для FX3 я сделаю так, чтобы быть уверенным не почти, а стопроцентно.

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



Когда значение bit_cnt достигло сорока (из-за особенностей языков Verilog, да и VHDL тоже, в коде используется константа 39), мы выходим на рабочий участок. Команд может быть две: чтение и запись. За это отвечает нулевой бит команды (из-за той же особенности языка в коде проверяется первый). Вот так выглядит обработчик этого состояния на SystemVerilog:

Смотерть текст.
always_ff @(posedge master_clk, posedge spi_ss)begin      // Это эквивалентно сбросу со стороны SPI      if (spi_ss == 1)      begin          bit_cnt <= 0;          state <= idle;      end else      begin          master_write <= 0;          master_read <= 0;          case (state)            idle: begin                 // Ура! У нас очередной перепад SCK!                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regCmdAndAddr <= {spi_mosi_reg,regCmdAndAddr[39:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 39)                    begin                       // Пишем                       if (regCmdAndAddr[1])                       begin                           state <= write1;                       end else                       // Читаем                       begin                           state <= read1;                       end                    end                 end            end


Чтобы хоть как-то оправдать восьмибитный регистр команды, я использую старшие 4 бита как BYTE_ENABLE для шины AVALON_MM, чтобы дать возможность писать не только по 32 бита. Для этого в конце текста есть такая строка:

assign master_byteenable = regCmdAndAddr [7:4];assign master_address = regCmdAndAddr [39:8];

Вторая строка в этой паре подключает наш регистр адреса к линиям адреса шины AVALON_MM.

Теперь пройдёмся по ветви чтения. Сначала надо считать данные из шины. Я исхожу из предположения, что шина SPI крайне медленная, поэтому не ввожу на неё никакого сигнала готовности. Считаем, что данные из AVALON_MM придут так быстро, что в SPI не успеет убежать ни одного лишнего бита. Нам потребуется два состояния. В первом мы взведём строб чтения и будем удерживать его, пока нам не придёт подтверждения, что данные пришли. Тогда мы защёлкнем эти данные и будем выдавать их в SPI на протяжении тридцати двух тактов. Собственно, всё. Потом мы на всякий случай сбросим счётчик битов (а вдруг начнётся передача новой команды без снятия SS?) и вновь перейдём в состояние idle, где будем копить новую команду. Состояние read1 выделено особым цветом, так как оно не зависит от положительного перепада на линии SCK шины SPI. Оно привязано только к тактовому сигналу шины AVALON_MM.



Вот так реализованы эти состояния в автомате.

Cмотреть текст.
            read1: begin                  master_read <= 1;                  if (master_readdatavalid)                  begin                      state <= read2;                      regData <= master_readdata;                  end            end            read2: begin                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regData <= {1'b0,regData[31:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 71)                    begin                       bit_cnt <= 0;                       state <= idle;                    end                 end            end


И вот так привязан выход регистра сдвига к сигналу MOSI шины SPI:

assign spi_miso = regData [0];

Ветка записи с точностью до наоборот. Сначала в состоянии write1 мы принимаем 32 бита данных, затем выставляем строб записи и висим в состоянии write2 (также не привязанным к SCK, поэтому имеющем особый цвет), пока нам не сообщат, что наши данные ушли.



Получаем такой код для реализации состояний.

Смотреть текст.
            // При записи, надо сначала допринимать данные из SPI            write1: begin                 // Ура! У нас очередной перепад SCK!                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regData <= {spi_mosi_reg,regData[31:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 71)                    begin                       state <= write2;                    end                 end            end            // Всё заполнено. Держим строб записи.             write2: begin               master_write <= 1;               // Если шина нас услышала - ну и прекрасно. Вышли               if (master_waitrequest == 0)               begin                   bit_cnt <= 0;                   state <= idle;               end            end


и такую строку для статического проецирования регистра данных на линии данных AVALON_MM:

assign master_writedata = regData;

Остальной текст модуля необходимая технологическая мишура. Нам надо продискретизировать линии SPI по тактовой частоте шины AVALON_MM, кроме того, надо получить задержанный на один авалоновский такт сигнал SCK шины SPI, чтобы иметь возможность ловить его перепад. Все эти действия мы имеем право выполнять только если уверены, что шина SPI достаточно медленная относительно AVALON_MM. Именно поэтому в прошлой статье я занимался оптимизацией, но не гнался за бешеными показателями.

reg spi_sck_reg, spi_sck_d;reg spi_mosi_reg;always @ (posedge master_clk)begin     spi_sck_reg <= spi_sck;     spi_mosi_reg <= spi_mosi;     spi_sck_d <= spi_sck_reg;end

Cобственно, всё. Модуль готов. Вот его полный текст для справки.

Смотреть полный текст модуля.
module spitoAvalon_mm (input           master_clk,input           master_reset,output  [31:0]    master_address,output  reg       master_write=0,output  [3:0]     master_byteenable,output  [31:0]    master_writedata,output reg        master_read,input             master_readdatavalid,input [31:0]    master_readdata,input             master_waitrequest,input           spi_sck,input           spi_mosi,output          spi_miso,input           spi_ss);// Чтобы не заводить SPI_SCK на линию GCK,// мы ориентируемся на то, что шина - медленная, поэтому// просто ловим перепады по основной тактовой частоте// Да и вообще, отдискретизируем SPI по тактовой. Во избежание...reg spi_sck_reg, spi_sck_d;reg spi_mosi_reg;always @ (posedge master_clk)begin     spi_sck_reg <= spi_sck;     spi_mosi_reg <= spi_mosi;     spi_sck_d <= spi_sck_reg;end// Число битов, принятых из SPIreg [7:0] bit_cnt;// Регистр команд/адреса. Итого 8 + 32 = 40 битreg [39:0] regCmdAndAddr = 0;reg [31:0] regData = 0;enum {idle,        read1, read2,       write1,write2     } state = idle;always_ff @(posedge master_clk, posedge spi_ss)begin      // Это эквивалентно сбросу со стороны SPI      if (spi_ss == 1)      begin          bit_cnt <= 0;          state <= idle;      end else      begin          master_write <= 0;          master_read <= 0;          case (state)            idle: begin                 // Ура! У нас очередной перепад SCK!                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regCmdAndAddr <= {spi_mosi_reg,regCmdAndAddr[39:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 39)                    begin                       // Пишем                       if (regCmdAndAddr[1])                       begin                           state <= write1;                       end else                       // Читаем                       begin                           state <= read1;                       end                    end                 end            end            // При записи, надо сначала допринимать данные из SPI            write1: begin                 // Ура! У нас очередной перепад SCK!                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regData <= {spi_mosi_reg,regData[31:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 71)                    begin                       state <= write2;                    end                 end            end            // Всё заполнено. Держим строб записи.             write2: begin               master_write <= 1;               // Если шина нас услышала - ну и прекрасно. Вышли               if (master_waitrequest == 0)               begin                   bit_cnt <= 0;                   state <= idle;               end            end            // При чтении - наоборот, сначала считали данные            // Для медленной шины, надо бы ещё готовность SPI добавить            // но в этом примере, мы ею пренебрежём            read1: begin                  master_read <= 1;                  if (master_readdatavalid)                  begin                      state <= read2;                      regData <= master_readdata;                  end            end            read2: begin                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regData <= {1'b0,regData[31:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 71)                    begin                       bit_cnt <= 0;                       state <= idle;                    end                 end            end            default:  begin              state <= idle;            end          endcase                endendassign master_byteenable = regCmdAndAddr [7:4];assign master_address = regCmdAndAddr [39:8];assign master_writedata = regData;assign spi_miso = regData [0];endmodule


Можно внедрять его в проект. Как это делается мы детально рассматривали тут и потом постоянно занимались этим в цикле статей про Redd. Но сегодня править придётся намного больше вещей, чем обычно.

Внедряем модуль в проект


Обычно я детально рассказываю про процесс внедрения модуля. Но сегодня потребуется такое количество мелких правок, что мне кажется, рассказ не будет иметь никакого эффекта. Все дочитают до третьего абзаца, затем зевнут и перейдут к следующему разделу. Поэтому сегодня я дам TCL-скрипт, внедряющий новый модуль в систему, и расскажу, как его применить. Будет не так скучно, а главное это будет давать какие-то новые знания. Вот такой скрипт сделал Квартус после всех моих действий:

Смотреть текст Квартуса.
# TCL File Generated by Component Editor 17.1# Wed Dec 30 02:23:32 MSK 2020# DO NOT MODIFY# # SpiToAvalonMM "SpiToAvalonMM" v1.0#  2020.12.30.02:23:32# # # # request TCL package from ACDS 16.1# package require -exact qsys 16.1# # module SpiToAvalonMM# set_module_property DESCRIPTION ""set_module_property NAME SpiToAvalonMMset_module_property VERSION 1.0set_module_property INTERNAL falseset_module_property OPAQUE_ADDRESS_MAP trueset_module_property AUTHOR ""set_module_property DISPLAY_NAME SpiToAvalonMMset_module_property INSTANTIATE_IN_SYSTEM_MODULE trueset_module_property EDITABLE trueset_module_property REPORT_TO_TALKBACK falseset_module_property ALLOW_GREYBOX_GENERATION falseset_module_property REPORT_HIERARCHY false# # file sets# add_fileset QUARTUS_SYNTH QUARTUS_SYNTH "" ""set_fileset_property QUARTUS_SYNTH TOP_LEVEL spitoAvalon_mmset_fileset_property QUARTUS_SYNTH ENABLE_RELATIVE_INCLUDE_PATHS falseset_fileset_property QUARTUS_SYNTH ENABLE_FILE_OVERWRITE_MODE falseadd_fileset_file spitoAvalon_mm.sv SYSTEM_VERILOG PATH MyCores/spitoAvalon_mm.sv TOP_LEVEL_FILE# # parameters# # # display items# # # connection point conduit_end# add_interface conduit_end conduit endset_interface_property conduit_end associatedClock ""set_interface_property conduit_end associatedReset ""set_interface_property conduit_end ENABLED trueset_interface_property conduit_end EXPORT_OF ""set_interface_property conduit_end PORT_NAME_MAP ""set_interface_property conduit_end CMSIS_SVD_VARIABLES ""set_interface_property conduit_end SVD_ADDRESS_GROUP ""add_interface_port conduit_end spi_ss spi_ss Input 1add_interface_port conduit_end spi_sck spi_sck Input 1add_interface_port conduit_end spi_mosi spi_mosi Input 1add_interface_port conduit_end spi_miso spi_miso Output 1# # connection point avalon_master# add_interface avalon_master avalon startset_interface_property avalon_master addressUnits SYMBOLSset_interface_property avalon_master associatedClock clock_sinkset_interface_property avalon_master associatedReset reset_sinkset_interface_property avalon_master bitsPerSymbol 8set_interface_property avalon_master burstOnBurstBoundariesOnly falseset_interface_property avalon_master burstcountUnits WORDSset_interface_property avalon_master doStreamReads falseset_interface_property avalon_master doStreamWrites falseset_interface_property avalon_master holdTime 0set_interface_property avalon_master linewrapBursts falseset_interface_property avalon_master maximumPendingReadTransactions 0set_interface_property avalon_master maximumPendingWriteTransactions 0set_interface_property avalon_master readLatency 0set_interface_property avalon_master readWaitTime 0set_interface_property avalon_master setupTime 0set_interface_property avalon_master timingUnits Cyclesset_interface_property avalon_master writeWaitTime 0set_interface_property avalon_master ENABLED trueset_interface_property avalon_master EXPORT_OF ""set_interface_property avalon_master PORT_NAME_MAP ""set_interface_property avalon_master CMSIS_SVD_VARIABLES ""set_interface_property avalon_master SVD_ADDRESS_GROUP ""add_interface_port avalon_master master_address address Output 32add_interface_port avalon_master master_write write Output 1add_interface_port avalon_master master_byteenable byteenable Output 4add_interface_port avalon_master master_writedata writedata Output 32add_interface_port avalon_master master_read read Output 1add_interface_port avalon_master master_readdatavalid readdatavalid Input 1add_interface_port avalon_master master_readdata readdata Input 32add_interface_port avalon_master master_waitrequest waitrequest Input 1# # connection point clock_sink# add_interface clock_sink clock endset_interface_property clock_sink clockRate 0set_interface_property clock_sink ENABLED trueset_interface_property clock_sink EXPORT_OF ""set_interface_property clock_sink PORT_NAME_MAP ""set_interface_property clock_sink CMSIS_SVD_VARIABLES ""set_interface_property clock_sink SVD_ADDRESS_GROUP ""add_interface_port clock_sink master_clk clk Input 1# # connection point reset_sink# add_interface reset_sink reset endset_interface_property reset_sink associatedClock clock_sinkset_interface_property reset_sink synchronousEdges DEASSERTset_interface_property reset_sink ENABLED trueset_interface_property reset_sink EXPORT_OF ""set_interface_property reset_sink PORT_NAME_MAP ""set_interface_property reset_sink CMSIS_SVD_VARIABLES ""set_interface_property reset_sink SVD_ADDRESS_GROUP ""add_interface_port reset_sink master_reset reset Input 1


Итак. У меня в проекте традиционно имеется каталог MyCores. Туда я кладу СистемВерилоговский файл spitoAvalon_mm.sv.



А теперь на уровень проекта кладу файл SpiToAvalonMM_hw.tcl.



Прекрасно. Открываем Platform Designer и видим, что наш компонент сам запрыгнул в перечень доступных!



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



Обратите внимание, что всем линиям шины SPI пришлось дать осмысленные имена.

Теперь посмотрим настройки шины AVALON_MM



Я выставил адресацию с точностью до байта. Можно было бы переключить точность до слова, но раз у меня есть линии BYTE_ENABLE, то можно же сделать работу с байтами и WORDами. Так что до байта. И какая-то из латентностей, уже не помню какая, была установлена в единицу. Я заменил на ноль. Собственно, на рисунке выше все задержки и латентности равны нулю, так что не перепутаете. Единицу будет хорошо видно.

Добавляем тестовую систему


Собственно, как проверить шину? А давайте через неё подключим небольшую ОЗУшку и попробуем писать и читать данные. Добавляем в систему, доставшуюся нам в наследство от прошлых статей, два элемента: мост SpiToAvalonMM и On Chip Memory. Но ОЗУ мы настроим чуть более сложно, чем обычно. Дело в том, что при отладке зеркальных систем можно получить, что запись и чтение работают. Но если допущена идеологическая ошибка в обоих направлениях, оно неверно запишется, так же неверно считается, но считанные данные совпадут с записанными, и мы увидим ложную работу. Поэтому желательно, чтобы в ОЗУ были заранее известные данные. Сначала мы убедимся, что они читаются верно, а уже затем начнём проводить пару запись-чтение, чтобы проверить уже работу записи при заведомо работающем чтении. Для этого мы взводим флажок Enable non-default initialization file и начинаем разбираться, куда положить и как сделать файл onchip_mem.hex.



Кладём его на тот же уровень, где живут файлы *.qpf и *.qsf. А вот с содержимым придётся немного повеселиться. Генератор такого файла входит в состав NIOS II EDS, то есть, в состав Квартуса. Но увы, это elf2hex. А нам бы bin2hex. Я нашёл замечательный проект SRecord 1.64 (sourceforge.net), который умеет преобразовывать любые файлы в любые. Замечательный проект! Он стоит того, чтобы попасть в записные книжки разработчиков железа Но в классическом HEXе адресация идёт байтами, а Квартус хочет, чтобы шла DWORD-ами, поэтому пришлось писать генератор hex файла самому. Учитывая, что надо было экономить время на разработку, он получился таким.

Смотреть текст.
void SpiToAvalonDemo::on_m_btnCreateMemInitFile_clicked(){    // Create an output stream    QFile file ("onchip_mem.hex");    if (!file.open(QIODevice::WriteOnly))    {     return;    }    QTextStream out (&file);    uint32_t initData [4096/sizeof(uint32_t)];    memset (initData,0,sizeof(initData));    initData [0x00] = 0x12345678;    initData [0x01] = 0x11111111;    initData [0x02] = 0x22222222;    initData [0x03] = 0x33333333;    initData [0x04] = 0x44444444;    initData [0x05] = 0x55555555;    initData [0x06] = 0x66666666;    initData [0x07] = 0x77777777;    initData [0x08] = 0xffffffff;    // Адрес всегда нулевой    out << ":020000020000FC\r\n";    for (size_t i=0;i<sizeof(initData)/sizeof(initData[0]);i+=8)    {        uint8_t cs = 0x20;        out <<":20";        cs += (uint8_t) (i/0x100);        cs += (uint8_t) (i/0x1);        out << QString ("%1").arg(i,4,16,QChar('0'));        out << "00";        uint8_t* ptr = (uint8_t*)(initData+i);        for (int j=0;j<32;j++)        {            cs += ptr[j];            out << QString ("%1").arg(ptr[j],2,16,QChar('0'));        }        cs = -1 * cs;        out << QString ("%1\r\n").arg(cs,2,16,QChar('0'));    }    // EOF    out << ":00000001FF\r\n";//    out.writeRawData((const char*)initData,sizeof(initData));    file.close();}


Просто для справки: вот так выглядят вновь добавленные блоки в процессорной системе:



Conduit выход spi экспортирован наружу, единственная шина AVALON_MM соединяет наш мастер со slave-входом ОЗУ. А больше я даже не знаю, что сказать. Обычно у нас системы были позабористей. Тут всё просто.

Не забываем назначить новые выводы


Не забываем, что у нас появились новые выводы (SPI). Дело в том, что когда я повторял свои подвиги при написании этого текста, я реально забыл это сделать, и очень удивлялся, что постоянно читается FFFFFF, хотя система уже была проверена при черновой подготовке и не должна была дурить. Так что не забываем! Для моей аппаратуры получилось так:



Дорабатываем код FX3


В прошлый раз в прошивке FX3 мы сделали только запись в SPI. В этот раз надо добавить чтение. Для начала чтение бита данных. Если при записи было очень полезно работать с конкретными битами, так как вместо чтения-модификации-записи, можно было только писать, то при чтении такой режим не даёт никакого выигрыша. Всё равно надо принять данные и как-то выделить требуемый бит. Поэтому для чтения данных я сделал такой макрос:

#define GET_IO_BIT(nBit) ((GPIO->lpp_gpio_invalue1 >> (nBit-32))&1)

Из оптимизации там только то, что я заранее знаю, что нужные нам биты находятся в диапазоне 32-63, поэтому сразу обращаюсь к регистру GPIO->lpp_gpio_invalue1, не тратя такты процессора на проверку.

Само чтение тоже особо ничем не приметно. Но кто будет вглядываться в текст, тот заметит, что последовательность действий отличается от той, которая напрашивается сама собой. Я просто постарался раскидать работу, чтобы и в положительном, и в отрицательном полупериоде сигнала SCK задержку вносили какие-то полезные команды. Было бы обидно всё полезное расположить в одной половинке, а в другую добавлять бесполезную задержку при помощи NOPов. И так всё медленно работает!

Смотреть текст.
unsigned int SPI_Read (int nBits){   unsigned int data = 0;   SET_IO_BIT (MY_BIT_MOSI);   while (nBits)   {   data >>= 1;   nBits -= 1;   CLR_IO_BIT (MY_BIT_CLK);   data |= (GET_IO_BIT (MY_BIT_MISO) << 31);   SET_IO_BIT (MY_BIT_CLK);   }   CLR_IO_BIT (MY_BIT_CLK);   return data;}


И, наконец, обработчик Vendor-команды в итоге стал таким:

Смотреть текст.
    if (bType == CY_U3P_USB_VENDOR_RQT)    {    // Cut size if needif (wLength > sizeof(ep0_buffer)){wLength = sizeof (ep0_buffer);}    // Need send data to PC    if (bReqType & 0x80)    {            CLR_IO_BIT(MY_BIT_SS);            SPI_Write(bRequest,8);            SPI_Write(wValue,16);            SPI_Write(wIndex,16);            ep0_buffer [0] = SPI_Read (32);            SET_IO_BIT(MY_BIT_SS);    CyU3PUsbSendEP0Data (wLength, (uint8_t*)ep0_buffer);            isHandled = CyTrue;    } else    {    CyU3PUsbGetEP0Data (wLength, (uint8_t*)ep0_buffer, NULL);    ep0_buffer [wLength] = 0;// Null terminated String            CyU3PDebugPrint (4, (char*)ep0_buffer);            CLR_IO_BIT(MY_BIT_SS);            SPI_Write(bRequest,8);            SPI_Write(wValue,16);            SPI_Write(wIndex,16);            SPI_Write(ep0_buffer[0],32);            SET_IO_BIT(MY_BIT_SS);    CyU3PUsbAckSetup();            isHandled = CyTrue;    }    }


Черновая проверка


Как и в прошлый раз, начерно всё проверяем через какую-нибудь подавалку USB-команд. Лично я предпочитаю BusHound. Как через него выполнять подобные проверки, было рассказано в предыдущей статье. Вот я читаю адрес 0. Длину всегда задаю равную четырём. Читается 12345678, как раз то, что было записано в файле onchip_mem.hex.



С адреса 4 читается 11111111



Ну, и так далее. Теперь пробуем записать. Скажем, по адресу 0x40 значение 0x87654321.



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

Добавляем код для программной работы


Убедившись, что всё работает верно, я добавил в класс, общающийся с библиотекой LibUSB, две функции. Если быть совсем точным, то я создал класс CAvalonViaFX3, унаследовав его от уже известного по одной из прошлых статей CUsbTester, а уже в него добавил эти две функции. Но это уже детали реализации, для нас сейчас важен сам код. Вот он:

bool CAvalonViaFX3::WriteDword(uint32_t addr, uint32_t data){    int res = libusb_control_transfer(m_hUsb,                            0x40,0xf1,                            (uint16_t)addr,(uint16_t)(addr>>16),                            (unsigned char*)&data,4,100);    return (res == 4);}bool CAvalonViaFX3::ReadDword(uint32_t addr, uint32_t& data){    int res = libusb_control_transfer(m_hUsb,                            0xc0,0xf0,                            (uint16_t)addr,(uint16_t)(addr>>16),                            (unsigned char*)&data,4,100);    return (res == 4);}

Ну, и код, тестирующий память, размером 4 килобайта, выглядит так:

static const int memSize = 4096;void SpiToAvalonDemo::on_m_btnMemoryTest_clicked(){    QRandomGenerator genWrite (1234);    uint32_t data [memSize/sizeof(uint32_t)];    for (size_t i=0;i<memSize/sizeof(uint32_t);i++)    {        data[i] = genWrite.generate();    }    for (int i=0;i<memSize;i+=sizeof(uint32_t))    {        if (!m_tester.WriteDword(i,data[i/sizeof(uint32_t)]))        {            QString msg = QString ("Write Error at Addr: 0x%1\n").arg(i,8,16,QChar('0'));            qDebug() << msg;            return;        }    }    for (int i=0;i<memSize;i+=sizeof(uint32_t))    {        uint32_t rd;        if (!m_tester.ReadDword(i,rd))        {            QString msg = QString ("Read Error at Addr: 0x%1\n").arg(i,8,16,QChar('0'));            qDebug() << msg;            return;        }        if (rd!=data[i/sizeof(uint32_t)])        {            QString msg = QString ("Miscompare at Addr: 0x%1 Written: %2 Read: %3\n").arg(i,8,16,QChar('0')).                    arg(data[i],8,16,QChar('0')).arg(rd,8,16,QChar('0'));            qDebug() << msg;            return;        }    }    qDebug()<<"Test Finished\n";}

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

Заключение


Мы освоили методику разработки мастеров, читающих и пишущих в шину AVALON_MM. Набивая руку, мы сделали переходник SPI в AVALON_MM и проверили его работоспособность. При работе с контроллером FX3 это позволит обращаться (с не очень высокой производительностью) к шине без использования каких-либо сторонних средств, так как раньше пришлось бы работать с TCL-командами или скриптами.

Материалы, получившиеся при написании статьи, можно скачать тут.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru