Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса REDD +9




В предыдущих статьях мы уже познакомились с шиной Avalon-MM, где MM означает Memory Mapped, то есть проецируемая на память. Эта шина вполне себе универсальная. К ней может быть подключено несколько ведущих (Master) и несколько ведомых (Slave) устройств. Мы уже подключали сразу два ведущих устройства (Instruction Master и Data Master), потому что у процессора NIOS II гарвардская архитектура, так что шины команд и данных у него разные, но многие авторы для упрощения разработки ПО снаружи подключают их к одной и той же общей шине.

Если какой-то блок на шине имеет функциональность прямого доступа к памяти (DMA), то он также будет содержать ведущее устройство для шины.

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



Все статьи цикла:

  1. Разработка простейшей «прошивки» для ПЛИС, установленной в Redd, и отладка на примере теста памяти
  2. Разработка простейшей «прошивки» для ПЛИС, установленной в Redd. Часть 2. Программный код
  3. Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС
  4. Разработка программ для центрального процессора Redd на примере доступа к ПЛИС

Уже известный нам документ Avalon Interface Specifications (вообще, я не даю прямых ссылок, так как они вечно меняются, поэтому вся сеть завалена статьями с мёртвыми ссылками, проще найти актуальное положение, вбив название в поисковик) сообщает, что кроме шины Avalon-MM, имеется ещё и шина Avalon-ST, где ST означает Stream, то есть, потоковая. Дело в том, что очень часто передаваемые данные имеют потоковую структуру. Да хоть классический сектор жёсткого диска. У него есть фиксированный размер. Его следует передать от начала до конца. Даже если и рассматривать его в адресуемой области, то адреса будут линейно нарастать. А если использовать для хранения блок FIFO, то адреса внутри него полностью скрыты от нас. Они есть, но работа с ними — не наша забота.

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



А вот типичные сигналы этой шины:



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

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



В общем, шина очень интересная, и сегодня мы начнём эксперименты с нею. Как мы уже знаем, ПЛИС подключена к шине USB комплекса Redd через мост FT2232H, работающий в режиме FT245-SYNC. Строго говоря, данные, проходящие через этот интерфейс, вполне себе потоковые данные. Сегодня мы научимся эти данные передавать в нашу процессорную систему на базе NIOS II. Жаль только, что протокол FT245-SYNC хоть и потоковый, но не полностью соответствует шине Avalon-ST. Для экономии ножек микросхемы он имеет двунаправленную шину данных, а шина Avalon-ST однонаправленная. Значит, нам предстоит сделать блок, согласующий близкие, но не совпадающие протоколы.

С протоколом FT245-SYNC мы уже знакомились в одной из предыдущих статей. Напомню, что его описание можно найти в документе AN_130 FT2232H Used in an FT245 Style Synchronous FIFO Mode. Вот типичная временная диаграмма передачи от моста в ПЛИС



Вообще, как программист я очень заинтересован в том, чтобы у передаваемого пакета были бы чётко отмеченные начало и конец. Ну, чтобы это было больше похоже по логике на протокол UDP, так как если передача будет идти в стиле TCP, то придётся добавлять в поток специальные реперные данные, на что будут тратиться мои, программистские, усилия и процессорные такты… С виду, линия RXF может помочь нам в этом. Проверяем… Заливаем в ПЛИС «прошивку» для измерения производительности, сделанную в прошлой статье, а к линии RXF подключаем щуп осциллографа. В качестве тестовой программы для центрального процессора Redd используем базу, также использовавшуюся для измерения производительности, просто вместо посылки больших объёмов данных, посылаем монолитный блок из 0x400 байт.

	uint8_t temp [maxBlockSize];
	memset (temp,0,sizeof (temp));

	uint32_t dwWritten;
	FT_Write(ftHandle0, temp, 0x400, &dwWritten);

Получаем следующую картинку на линии RXF:



Понятно, что микросхема принимает 0x200 байт буфера (именно столько может прийти в одном пакете USB2.0 HS), затем отдаёт их в канал. Вообще, это странно, так как в документации заявлено, что в каждом направлении используется два буфера. За время передачи второй буфер вполне должен был успеть наполниться. Увы. Конец его заполнения явно запаздывает. Собственно, отсюда видно, почему производительность не выходит на теоретические 52 мегабайта в секунду: большой процент времени (хоть и не 50%) передача просто не идёт.

Но так или иначе, а мы выяснили, что выявлять начало пакета по отрицательному фронту RXF можно только в том случае, если размер пакета не превышает 0x200 байт. Если мы шлём в устройство только команды с малым объёмом данных, это вполне достижимо. Но если мы шлём большие потоки данных, придётся использовать непрерывный канал, похожий по своей логике на UART (или, скажем, на канал TCP), выделяя границы пакетов чисто программным путём.

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



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

Со стороны шины Avalon-ST я выбрал следующий режим работы (в документе их приведено великое множество, но для стыка с FT245-SYNC ближе всего именно этот):



Напомню направление сигналов:



То есть мы просто дожидаемся подтверждения на шине (valid), защёлкиваем данные и стробируем этот факт линией ready.

Со стороны FT245_FIFO протокол выглядит так:



Получается, что мы должны дождаться сигнала TXE и стробировать данные сигналом WR# (у обоих указанных сигналов полярность инверсная).

TXE# по своей функциональности очень похож на ready, а WR# — на valid. Детали чуть разные, но логика сходная.

Выходит, что мы можем выделить одно единственное состояние toPC, в котором будут производиться простейшие коммутации некоторых линий. Условием входа в это состояние будет готовность обеих сторон к передаче, то есть (TXE#==0) И (valid==1). Как только какая-то из готовностей пропала, выходим обратно в idle.

Граф переходов автомата пока получается простой:



А таблица коммутации — такой (где имена сигналов имеют неоднозначность, им добавлены индексы, где имена однозначны — индексов нет):

Сигнал Состояние toPC Прочие состояния
WR# NOT (validSink) 1
readySink NOT (TXE#) 0
DATAFT245_FIFO DataSink Z


Переходим к чуть более сложной передаче от Source к FT245_FIFO. Как мы уже видели в предыдущей статье, усложнение состоит в переключении направления при помощи сигнала OE#:



Для шины Avalon_ST всё то же самое, что и ранее, поэтому рисунки второй раз не приводятся, но теперь мы находимся на позиции Source.

Здесь линия RXF# соответствует линии valid, а линия RD# — линии ready. Ну, прекрасно, добавляем пару состояний автомату:



и следующую логику для активных в данном состоянии сигналов:

Сигнал dropOE fromPC Прочие состояния
OE# 0 0 1
RD# 1 NOT(readySource) 1
dataSource Любое значение DATAFT245_FIFO Любое значение
valid Source 0 NOT(RXF#) 0

Понятно, что схема получилась не самая идеальная. Здесь возможны различные нюансы, связанные с переполнением или опустошением буферов. Но вроде потери данных возникать не должно, а что касается оптимальности — надо же с чего-то начинать!

Приступаем к переносу наработанной теории в код SystemVerilog. Правда, воспользоваться всеми возможностями SystemVerilog у нас не получится. Было дело, я писал большую статью, где проверял практическую синтезируемость красивых возможностей данного языка реальной средой разработки. Здесь у нас просто просится использование интерфейсов, ведь в системе будет два экземпляра типа Avalon-ST. Увы и ах. Вот тестовый код:
interface AvalonST #(parameter width=8)(input clk);
logic [width-1:0] data;
logic 		  ready;
logic 		  valid;
modport source (input clk, ready, output data,valid);
modport sink (input clk, data, valid, output ready);
endinterface

module FT245toAvalonST
(
    AvalonST.source source,
    AvalonST.sink sink
);

//assign source.ready = sink.valid;
assign sink.ready = source.valid;
endmodule

Он прекрасно синтезируется в основном компиляторе (закомментированная строка при удалении комментария провоцирует ошибку, чтобы убедиться, что синтезатор всё верно трактует), но при проверке по кнопке Analyze Synthesis Files для компонента для такого кода выдаётся ошибка, что тип AvalonST неизвестен. То есть там идёт анализ не на SystemVerilog, а на чистый Verilog. А жаль.



Причём язык-то определяется верно, просто анализатор именно интерфейсы среди портов не понимает.



В общем, придётся использовать некрасивый старый синтаксис.

С этим синтаксисом получаем такой интерфейс модуля:
module FT245toAvalonST
(
	input			clk,
	input			reset,
	
	inout	  		[7:0] ft245_data,
	input	 logic		ft245_rxf,
	input	 logic		ft245_txe,
	output logic		ft245_rd,
	output logic		ft245_wr,
	output logic		ft245_oe,
	output logic		ft245_siwu,
	
	input	 logic		source_ready,
	output logic		source_valid,
	output logic[7:0] source_data,
	
	output logic		sink_ready,
	input	 logic		sink_valid,
	input  logic[7:0]	sink_data
	
);


Грубо, винтажно, но что ж поделать.

Граф переходов автомата реализуем без особых изысков:
// Состояния нашего автомата
enum {idle, toPC, dropOE, fromPC} state = idle;

// Реализация графа переходов автомата
always_ff @(posedge clk,posedge reset)
begin
	if (reset == 1)
	begin
		state <= idle;
	end else
	begin
		case (state)
		idle: begin
				if ((ft245_txe == 0) && (sink_valid == 1))
					state <= toPC;
				else if ((ft245_rxf == 0)&&(source_ready == 1))
					state <= dropOE;
				end
		toPC: begin
				if (!((ft245_txe == 0) && (sink_valid == 1)))
					state <= idle;
				end
		dropOE: begin
					state <= fromPC;
				end
		fromPC: begin
				if (!((ft245_rxf == 0)&&(source_ready == 1)))
							state <= idle;
				end
		endcase
	end
end


Управление выходами же требует некоторых пояснений.

Часть установок делается «в лоб»:
// Реализация выходов автомата
// на самом деле, её можно было бы целиком сделать через мультиплексоры,
// но как-то традиционно принято делать так.
always_comb
begin
	ft245_oe <= 1;
	ft245_rd <= 1;
	ft245_wr <= 1;
	source_valid <= 0;
	sink_ready <= 0;
	
	// Шина данных здесь не присваивается,
	// это делается во внешних assign-ах

	case (state)
		idle: begin
				end
		toPC: begin
					ft245_wr <= !(sink_valid);
					sink_ready <= !(ft245_txe);
				end
		dropOE: begin
					ft245_oe <= 0;
				end
		fromPC: begin
					ft245_oe <= 0;
					ft245_rd <= !(source_ready);
					source_valid <= !(ft245_rxf);
				end
		endcase
	
end


Но, скажем, для двунаправленной шины данных следует применить типовое решение. Как мы помним, она объявлена в интерфейсной части так:

	inout	  		[7:0] ft245_data,

и чтение из неё может вестись обычным образом. Для нашего случая, мы просто заворачиваем все данные на данные исходящей шины Avalon-ST:

// А это мы входящие данные заворачиваем на нужную шину
assign source_data = ft245_data;

Но в целом, читать из шины можно всегда и каким угодно способом. А вот писать в неё следует при помощи мультиплексора. Когда мы пишем в шину данные, эти данные должны поступать из любой другой заранее подготовленной шины. Обычно в модуле заводят переменную типа reg (или новомодный logic). В нашем же случае такая шина уже имеется. Это шина sink_data. В остальных случаях на выход подаётся состояние Z. Кто знаком со схемотехникой, тот прекрасно себе представляет типичный выходной буфер. Он тоже либо пропускает какие-либо входные данные, либо переходит в Z-состояние. В нашем коде этот мультиплексор выглядит так:

// Так традиционно назначается выходное значение inout-линии
assign ft245_data = (state == toPC) ? sink_data : 8'hzz;

И ещё сигнал ft245_siwu. У нас он никогда не используется, поэтому согласно документации на FT2232H, подтянем его к единице:

// Документ от FTDI гласит:
// Tie this pin to VCCIO if not used.
assign ft245_siwu = 1;

Собственно, всё.

Полностью модуль выглядит так:
module FT245toAvalonST
(
	input			clk,
	input			reset,
	
	inout	  		[7:0] ft245_data,
	input	 logic		ft245_rxf,
	input	 logic		ft245_txe,
	output logic		ft245_rd,
	output logic		ft245_wr,
	output logic		ft245_oe,
	output logic		ft245_siwu,
	
	input	 logic		source_ready,
	output logic		source_valid,
	output logic[7:0] source_data,
	
	output logic		sink_ready,
	input	 logic		sink_valid,
	input  logic[7:0]	sink_data
	
);

// Состояния нашего автомата
enum {idle, toPC, dropOE, fromPC} state = idle;

// Реализация графа переходов автомата
always_ff @(posedge clk,posedge reset)
begin
	if (reset == 1)
	begin
		state <= idle;
	end else
	begin
		case (state)
		idle: begin
				if ((ft245_txe == 0) && (sink_valid == 1))
					state <= toPC;
				else if ((ft245_rxf == 0)&&(source_ready == 1))
					state <= dropOE;
				end
		toPC: begin
				if (!((ft245_txe == 0) && (sink_valid == 1)))
					state <= idle;
				end
		dropOE: begin
					state <= fromPC;
				end
		fromPC: begin
				if (!((ft245_rxf == 0)&&(source_ready == 1)))
							state <= idle;
				end
		endcase
	end
end

// Реализация выходов автомата
// на самом деле, её можно было бы целиком сделать через мультиплексоры,
// но как-то традиционно принято делать так.
always_comb
begin
	ft245_oe <= 1;
	ft245_rd <= 1;
	ft245_wr <= 1;
	source_valid <= 0;
	sink_ready <= 0;
	
	// Шина данных здесь не присваивается,
	// это делается во внешних assign-ах

	case (state)
		idle: begin
				end
		toPC: begin
					ft245_wr <= !(sink_valid);
					sink_ready <= !(ft245_txe);
				end
		dropOE: begin
					ft245_oe <= 0;
				end
		fromPC: begin
					ft245_oe <= 0;
					ft245_rd <= !(source_ready);
					source_valid <= !(ft245_rxf);
				end
		endcase
	
end

// Кое-что придётся cделать особым способом, вне описания автомата...

// Документ от FTDI гласит:
// Tie this pin to VCCIO if not used.
assign ft245_siwu = 1;

// Так традиционно назначается выходное значение inout-линии
assign ft245_data = (state == toPC) ? sink_data : 8'hzz;

// А это мы входящие данные заворачиваем на нужную шину
assign source_data = ft245_data;


endmodule


Как включить модуль в список доступных для применения в процессорной системе, мы подробно разбирали в одной из предыдущих статей, так что я просто покажу результат на рисунке. Упомяну, что для его достижения мне пришлось добавить две шины AVALON-ST, одну шину Conduit, растащить сигналы из ошибочно определённой шины AVALON-MM, а когда в той шине не осталось ни одного сигнала, просто удалить её. Попутно на рисунке показаны выбранные мною настройки шин AVALON-ST (8 бит на символ, нет ошибок, максимальный канал нулевой, латентность нулевая).



С разработкой модуля для стыковки шин — всё. Но увы и ах. Разработать — это только начало работы. Внедрить намного сложнее. Как видно из положения скроллера на экране, до конца статьи ещё далеко. Итак, приступаем к созданию простейшего проекта, в котором используется стык шины FT245-SYNC с шинами AVALON-ST. Именно простейшего. Серьёзный проект не уложится в рамки ни одной статьи разумного размера. Я сейчас буду делать упрощение за упрощением просто для того, чтобы внимания читателей хватило до конца текста, чтобы они не бросили чтение на полуслове. Первое упрощение состоит в том, что тактовые импульсы с частотой 60 МГц для FT245_SYNC вырабатывает сама микросхема FT2232H. Я мог бы добавить в систему две тактовых линии, но как скоро все увидят, у нас будут такие паутины из проводов, что мама не горюй. Если я ещё буду обращать внимание на разные тактовые линии, мы все запутаемся. Поэтому я просто объявляю, что сегодня наша процессорная система будет тактироваться от микросхемы FT2232H, а не от штатного генератора.

Почему нельзя всегда так делать? Очень просто: пока FT2232H не переведена в режим 245_SYNC, на выходе у неё нет этих импульсов. То есть надо сначала запустить программу для центрального процессора, а уже затем грузить всё в ПЛИС. Если бы мы делали систему для внешнего Заказчика, такое решение породило бы массу проблем. Из опыта знаю, что нам бы регулярно звонили и говорили, что ничего не работает, мы бы напоминали про такты, но это помогало бы ненадолго. Но мы делаем внутреннюю вещь, причём будем пользоваться ею только в лабораторных условиях. То есть, в рамках данной задачи такое допустимо.

Но это тянет новые проблемы. Частота у нас 60 МГц, а блок тактирования SDRAM, которым мы сейчас пользуемся, намертво завязан на частоту 50 МГц. Да, я проверял, 60 подавать можно, но давайте сделаем вид, что мы стараемся не выходить за допустимые режимы. В последующих статьях я постараюсь показать, как заменить этот жёсткий блок, но сегодня мы просто скажем, что раз наш блок тактирования от SDRAM не может работать от используемой частоты, мы исключаем из процессорной системы SDRAM. Программа и её данные будут полностью размещаться во внутренней памяти ПЛИС. Экспериментально выяснено, что в сегодняшней конфигурации у ПЛИС можно под это дело отнять максимум 28 килобайт ОЗУ. Оказывается, можно брать объёмы и не кратные степеням двойки…

Кроме того, мы будем пользоваться штатным блоком тактирования и сброса. Он сбрасывается чуть иначе, чем тот, который мы использовали для SDRAM. Чтобы не усложнять статью, я воспользуюсь тем, что разрабатываемая система всегда будет работать под управлением отладчика, поэтому заведу сброс от подсистемы JTAG отладки.

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



где для блока тактирования и сброса была скорректирована частота:



а для ОЗУ – объём:



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



С этим блоком у нас появится возможность вызывать функции, аналогичные printf. У него, кроме шины AVALON_MM, следует подключить также и выход запроса на прерывание.



Всё, заготовка для процессорной системы завершена. Пора встраивать наш блок. Куда он будет направлять данные? Среди доступных нам блоков имеется очень интересная двухпортовая память FIFO. Её прелесть состоит в том, что один порт можно настроить на режим AVALON-ST и подключить его к нашему блоку, а второй — на режим AVALON_MM и работать с ним силами процессора NIOS II. Этот замечательный блок располагается здесь:



Шин Avalon-ST у нас две (одна на чтение, другая на запись), поэтому блоков FIFO надо тоже два. Сейчас я очень подробно пройдусь по одному из них, мы намотаем пару километров паутины (и кучу экранов текста с рисунками), а про второй скажем, что «его можно сделать по аналогии», указав только отличия. Поэтому пока что добавляем только один блок в систему и осматриваемся в его настройках. Настроек много. Можно было бы просто обозначить требуемые значения, чтобы все обращались к статье, как к справочнику, но вдруг кто-то попадёт в ситуацию, что надо настраивать, а доступа к сети (а значит, и статье) нет. Поэтому я буду добавлять настройки итерационно. Сначала очевидные, затем — по мере требований системы бегать по диалогу вновь и вновь. Так все прочувствуют процесс и смогут повторить его в любой момент. Итак. По умолчанию, нам выдали такие настройки:



Я сейчас буду делать FIFO, которое набирает данные из Avalon-ST, а выгружает в Avalon-MM. Получается, что первая правка будет такой:



Мне выдали вот такое интересное предупреждение:



Оказывается, когда хотя бы один из портов проецируется на память, ширина шины Avalon-ST должна быть строго 32 бита. А у нас шина 8-битная. Как согласовать разрядности, расскажу чуть ниже, а пока делаем 32-разрядную шину при восьмибитном символе здесь. Ну, и пакетный режим отключаем, как было решено в теоретической части.



Дальше — ёмкость. Допустим, я сделаю очередь на 256 слов (то есть, 1024 байта):



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



и отхватываем ошибку:



Ну что ж. Добавляем двойное тактирование. Просто подключим оба входа к одной и той же тактовой линии, благо у нас она единая.
Уффф. Итого имеем:



Но подключать это дело к общей системе ещё рано. Как мы выяснили, из разработанного нами блока выходит 8-битная шина Avalon-ST, а сюда должна входить 32-битная. Как нам быть? Переделывать свой блок? Нет! Всё уже сделано до нас. Вот то, что нам поможет:



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



Настройки же у него делаем такие: на вход у нас шина 8-битная, на выход 32-битная. Сигналы Packet не используются, сигналы ready и valid используются.



Пора плести паутину. Сначала я проложу потоковые линии (на рисунке они обе подсвечены, маркеры стоят на приёмниках данных):



То есть сигнал от Source нашего блока идёт на вход адаптера. А с выхода адаптера — на вход FIFO. Как я и говорил, все соединения у потокового протокола идут по схеме точка-точка.
Ну, а теперь навешиваем линии сброса, тактирования, а также подключаем всё к системной шине и к прерываниям…



Ну вот… А теперь по тому же принципу добавляем FIFO для выдачи данных в FT245SYNC. Только там данные попадают в FIFO из Avalon-MM в 32-битном виде. Проходят через адаптер 32-в-8 и затем попадают на вход SINK нашего блока, который в текущей схеме не подключён… Получаем следующий фрагмент итоговой схемы (память там получилась с одиночным тактированием):



Дальше формальности, которые мы уже хорошо отработали при опытах, описанных в прошлых статьях (по большей части — в этой). У процессора назначаем вектора. Для системы вызываем автоматическое назначение номеров прерываний и адресов. Сохраняем систему… Все помнят, что имя сохраняемой системы должно совпадать с именем проекта, чтобы система оказалась на верхнем уровне иерархии? Добавляем систему в проект, делаем черновую сборку проекта, назначаем ножки. Лично я схитрил: скопировал назначения из файла *.qsf чернового проекта в текущий чистовой (а вы можете взять мой проект и скопировать соответствующие строки *.qsf в свой, но можете и просто назначить все ножки через GUI). Особое внимание обращаю на то, что сигнал clk связан с ножкой 23, а не 25, как в предыдущих проектах. Напоминаю, что здесь мы тактируемся от выхода FT2232.



Прекрасно! Аппаратная часть готова. Переходим к программной. С чего начнём? Сегодня этот вопрос не стоит. Если мы начнём с программы, которая исполняется на процессоре NIOS II, у нас ничего не заработает. Сначала мы должны перевести FT2232 в режим 245-SYNC, только тогда наша процессорная система получит тактовые импульсы. Поэтому начинаем с кода для центрального процессора.

Получаем что-то такое:
#include <cstdio>

#include <sys/time.h>
#include <unistd.h>
#include "ftd2xx.h"

FT_HANDLE OpenFT2232H()
{
	FT_HANDLE	ftHandle0;
	static FT_DEVICE ftDevice;

	// Перебираем все устройства с нулевого
	int nDevice = 0;
	while (true)
	{
		// Если устройство не открылось
		if (FT_Open(nDevice, &ftHandle0) != FT_OK)
		{
			printf("No FT2232 found\n");
			// То считаем, что перед нами конец списка
			return 0;
		}

		// А что за устройство открылось?
		if (FT_GetDeviceInfo(ftHandle0,
			&ftDevice,
			NULL,
			NULL,
			NULL,
			NULL) == FT_OK)
		{
			// То, что мы ищем
			if (ftDevice == FT_DEVICE_2232H)
			{
				// Действия, которые я позаимствовал в AN130

				FT_SetBitMode(ftHandle0, 0xff, 0x00);

				usleep(1000000);

				//Sync FIFO mode 
				FT_SetBitMode(ftHandle0, 0xff, 0x40);

				FT_SetLatencyTimer(ftHandle0, 2);

				FT_SetUSBParameters(ftHandle0, 0x10000, 0x10000);

				return ftHandle0;

			}
		}
		// Закрыли это устройство
		FT_Close(ftHandle0);
		// Переходим к следующему
		nDevice += 1;
	}
	printf("No FT2232 found\n");
}

int main()
{
	FT_HANDLE	ftHandle0 = OpenFT2232H();

	if (ftHandle0 == 0)
	{
		printf("Cannot open device\n");
		return -1;
	}

	int item;

	bool bWork = true;
	while (bWork)
	{
		printf("1 - Send 16 bytes\n");
		printf("2 - Send 256 bytes\n");
		printf("3 - Receive loop\n");
		printf("0 - Exit\n");

		scanf("%d", &item);

		switch (item)
		{
		case 0:
			bWork = false;
			break;
		case 1:
		{
			static const unsigned char data[0x10] = { 0x00,0x01,0x02,0x03,
									0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f };
			DWORD dwWritten;
			FT_Write(ftHandle0, (void*)data, sizeof(data), &dwWritten);
		}
		break;
		case 2:
		{
			unsigned char data[0x100];
			for (size_t i = 0; i < sizeof(data); i++)
			{
				data[i] = (unsigned char)i;
			}
			DWORD dwWritten;
			FT_Write(ftHandle0, (void*)data, sizeof(data), &dwWritten);
		}
		break;
		case 3:
		{
			DWORD dwRxBytes;
			DWORD dwRead;
			DWORD buf[0x100];
			while (true)
			{
				FT_GetQueueStatus(ftHandle0, &dwRxBytes);
				if (dwRxBytes != 0)
				{
					printf("Received %d bytes (%d DWORDs)\n", dwRxBytes, dwRxBytes / sizeof(buf[0]));
					if (dwRxBytes > sizeof(buf))
					{
						dwRxBytes = sizeof(buf);
					}
					FT_Read(ftHandle0, buf, dwRxBytes, &dwRead);
					for (DWORD i = 0; i < dwRxBytes / sizeof(buf[0]);i++)
					{
						printf("0x%X, ",buf[i]);
					}
					printf("\n");
				}
			}
		}
			break;
		}
	}
	// Всё, устройство можно закрыть
	FT_Close(ftHandle0);
	return 0;
}


Функция OpenFT2232H() знакома нам из прошлой статьи. Именно она открывает устройство FT2232 и переводит его в требуемый нам режим. Сразу после успешного запуска программы мы получаем тактовые импульсы, а вместе с ними и возможность отлаживать программу для NIOS II. Ну, а функциональность основной функции проста, как табуретка. Послать немного данных (1), послать много данных (2), принять данные (3). Обратите внимание лишь на то, что все данные посылаются блоками, кратными четырём байтам. Это всё потому, что у нас стоит адаптер 8-в-32. На выходе из него данные должны идти двойными словами. В остальном — всё очевидно.

При разработке программы для NIOS II в первую очередь следует настроить BSP. Напоминаю, что саму программу я создаю согласно шаблону Hello World Small. Изменённые поля в BSP отмечены красным на приведённом ниже рисунке (как создаётся программа из шаблона и как правится BSP, было подробно рассмотрено в одной из предыдущих статей). Напомню, что я выбираю корень дерева, то есть, элемент Settings, чтобы справа были видны сразу все настройки.



Дальше Generate BSP и, в силу своей привычки, я меняю имя файла hello_world_small.c на hello_world_small.cpp, после чего делаю очистку проекта, чтобы не было никаких наведённых ошибок от этого переименования.

Проверку работы я буду вести довольно поверхностно (настоящий тестер обязательно бы тщательно протестировал перекачку больших объёмов данных, превышающих размер FIFO, но цель статьи — показать основные принципы, а не добиться того, чтобы её бросили читать в силу безумного размера). И показывать основные принципы я буду в два этапа. Первый этап — проверка передачи данных от центрального процессора к NIOS II. Для этого я разработал такой код:

extern "C"
{
#include "sys/alt_stdio.h"
#include <system.h>
#include <altera_avalon_fifo_util.h>
}
#include <stdint.h>
int main()
{ 
	while (1)
	{
		int level = IORD_ALTERA_AVALON_FIFO_LEVEL(FIFO_0_OUT_CSR_BASE);
		if (level != 0)
		{
			alt_printf("0x%x words received:\n",level);
			for (int i=0;i<level;i++)
			{
				alt_printf("0x%x,",IORD_ALTERA_AVALON_FIFO_DATA (FIFO_0_OUT_BASE));
			}
			alt_printf("\n");
		}
	}

  /* Event loop never exits. */
  while (1);

  return 0;
}

Эта программа ждёт появления данных в FIFO. Если они там появились, отображает их.
Приступаем к испытаниям. Сначала я сделаю вид, что забыл запустить тактирование. Поэтому после включения Redd я загружаю «прошивку» ПЛИС, затем пытаюсь запустить на отладку программу для NIOS II. Получаю такое сообщение:



Если у вас появилось оно же, значит вы действительно забыли запустить тактирование процессорной системы. Но теперь вы знаете, как это быстро выявить. А чтобы устранить, необходимо и достаточно запустить программу, которую мы написали для центрального процессора. Как только она запустится и проинициализирует мост FT2232, тактовые импульсы пойдут на наш процессор, и можно будет повторять процесс старта отладки. Причём программа для центрального процессора к тому времени может быть и завершена. Тактовые импульсы уже никуда не денутся: мост уже настроен на режим FT245-SYNC.

В программе для центрального процессора нажимаем 1. В зависимости от ситуации, в терминале появляется либо:

0x2 words received:
0x3020100,0x7060504,
0x2 words received:
0xb0a0908,0xf0e0d0c,


либо:

0x3 words received:
0x3020100,0x7060504,0xb0a0908,
0x1 words received:
0xf0e0d0c,


В принципе, может быть и 1, затем 3 двойных слов, но у меня такого не возникало. Всё зависит от того, сколько байт успеет пробежать по шине, до того как начнётся первое отображение. А уж если оно началось, то к его завершению все остальные байты точно успеют добежать, так как передача данных через JTAG — процесс не быстрый. Если бы у шины использовались пакетные сигналы, программа смогла бы увидеть данные только по завершении приёма пакета. В некоторых случаях, это хорошо (пока нет пакета, зачем его видеть? Особенно если пакет транзитный), в некоторых плохо (FIFO — это чёрный ящик, для конечной обработки данные следует скопировать в адресуемое ОЗУ, а это лучше делать в параллель с приёмом данных).

Передаваемые данные помещаются в двойные слова в нотации Little Endian. Напомню, что передаётся следующий массив:

static const unsigned char data[0x10] = { 0x00,0x01,0x02,0x03,
	0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f };

Всё верно. Если в программе для центрального процессора выбрать пункт 2, то появится сообщение (для облегчения чтения, строки отформатированы при подготовке статьи):

0x3 words received:
0x3020100,0x7060504,0xb0a0908,
0x3d words received:
0xf0e0d0c,
0x13121110,0x17161514,0x1b1a1918,0x1f1e1d1c,
0x23222120,0x27262524,0x2b2a2928,0x2f2e2d2c,
0x33323130,0x37363534,0x3b3a3938,0x3f3e3d3c,
0x43424140,0x47464544,0x4b4a4948,0x4f4e4d4c,
0x53525150,0x57565554,0x5b5a5958,0x5f5e5d5c,
0x63626160,0x67666564,0x6b6a6968,0x6f6e6d6c,
0x73727170,0x77767574,0x7b7a7978,0x7f7e7d7c,
0x83828180,0x87868584,0x8b8a8988,0x8f8e8d8c,
0x93929190,0x97969594,0x9b9a9998,0x9f9e9d9c,
0xa3a2a1a0,0xa7a6a5a4,0xabaaa9a8,0xafaeadac,
0xb3b2b1b0,0xb7b6b5b4,0xbbbab9b8,0xbfbebdbc,
0xc3c2c1c0,0xc7c6c5c4,0xcbcac9c8,0xcfcecdcc,
0xd3d2d1d0,0xd7d6d5d4,0xdbdad9d8,0xdfdedddc,
0xe3e2e1e0,0xe7e6e5e4,0xebeae9e8,0xefeeedec,
0xf3f2f1f0,0xf7f6f5f4,0xfbfaf9f8,0xfffefdfc,

Тоже всё верно. Переходим к проверке обратной передачи. Заменяем программу для NIOS II на такую:

	/* Это - тест 2 */
	uint32_t buf[] = {0x11223344,0x55667788,0x99aabbcc,0xddeeff00};
	for (uint32_t i=0;i<sizeof(buf)/sizeof(buf[0]);i++)
	{
		IOWR_ALTERA_AVALON_FIFO_DATA (FIFO_1_IN_BASE,buf[i]);
	}

Выбираем в программе для центрального процессора пункт 3 и запускаем этот вариант программы для NIOS II. Получаем:

Received 16 bytes (4 DWORDs)

0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00,


Оба канала начерно работают. А начисто проверим как-нибудь в другой раз.

Заключение


В статье рассмотрены основы потокового протокола шины Avalon-ST. Через этот протокол организована связь центрального процессора Redd с процессорной системой, реализованной в ПЛИС. Читатели получили представление о простейшем методе взаимодействия центрального и вспомогательного процессоров. Скачать созданные в процессе разработки проекты можно здесь.

Однако полученные знания о потоковых протоколах и их использовании очень начальные. В последующих статьях будет показано, как через эти протоколы эффективно сохранять данные в динамическом ОЗУ, размещённом на плате Redd.




К сожалению, не доступен сервер mySQL