nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

Разгоняем QuatCore до 25 МГц, часть 1

Когда процессор QuatCore (Quaternion Core, он же ℍ-core) только заработал, его максимально допустимая тактовая частота для относительно медленной ПЛИС 5576ХС4Т составляла около 6 МГц. В принципе, при его разработке скорость действительно шла на последнем месте. Было понятно, что алгоритмы достаточно эффективны: за секунду надо произвести около 10 000 сложений и столько же умножений, и если сложение выполняется за 3 такта, а умножение за 18 тактов, это приводит к потребной тактовой частоте всего 210 кГц (!!!). И действительно, "аффинный алгоритм" (он же алгоритм захвата) при тактовой частоте 4 МГц выполнился за 1,15 мс при допустимых 200 мс.

Более высокими приоритетами был предельно малый объём, занимаемый этим "ядром" на ПЛИС, как в плане логических элементов (менее 500), так и в плане памяти данных и кода (512 байт одного и другого пока что), и если для первого можно было бы предельно урезать команды, возможно даже умножение выкинув, то для второго команды должны быть очень "ёмкими" и заточенными под выполняемую работу. А ещё - простота разработки и отладки, чтобы один человек мог провернуть эту авантюру в течение года, а лучше даже быстрее :)

И всё же, столь низкую рабочую частоту я с самого начала считал "временным явлением". Она вызвана полным отсутствием конвейера в этом процессоре: на каждом такте сигнал PC (Program Counter) поступает в ПЗУ кода, на том же такте, но с задержкой появляется очередная команда, 16-битное слово, состоящее из 8-битного SrcAddr и 8-битного DestAddr (откуда взять и куда положить, а это всё, что процессор умеет :)). В продолжение того же такта по SrcAddr формируется слово на шину данных и, наконец, к фронту тактовой частоты оно "защёлкивается" куда надо, в соответствии с DestAddr. Немудрено, что задержка от появления нового адреса на PC до распространения данных в конкретный регистр, куда их нужно положить, велика. В наихудшем случае нужно пройти через 2 модуля памяти (ПЗУ и ОЗУ!) и ещё целую вереницу мультиплексоров и управляющей логики.

Разделить выполнение на 3 этапа - выборку команды, выборку данных и их запись - давно напрашивалось, и с точки зрения "накладных расходов" (дополнительной логики на ПЛИС) это совсем не страшно. Страшно было МНЕ, всё это отладить. Очень уж давит "программерское" понимание, что вот строчка, мы её выполнили, и моментально содержание памяти и регистров поменялось. А тот факт, что она выполняется в трёх разных местах с разнесением по времени - взрывает мозг :)

Но не так страшен имп, когда есть бензопила! Вроде бы начинает работать!


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

module QuatCoreSrcMux (input [7:0] SRAM, input [15:0] MEM, input [15:0] ALU, input [15:0] IMM, input [15:0] PC, input [15:0] IO, input [7:0] SrcAddr, input clk, output [15:0] Q);

parameter DoLatch = 0;

reg [15:0] rQ;
wire [15:0] combQ;

assign combQ = (~SrcAddr[7])? 	IMM:
               SrcAddr[6]? 	MEM:
               SrcAddr[5]?   	PC:
               (~SrcAddr[4])?	ALU:
               SrcAddr[3]?	SRAM:
				IO;
                                    
always @(posedge clk)
	rQ <= combQ;
	
assign Q = (DoLatch == 1)? rQ : combQ;

endmodule


Параметр DoLatch как раз и нужен был, чтобы ввести конвейер: до сих пор у нас было DoLatch=0. Стоит выставить DoLatch=1 - и данные поступят "на хранение" в этом мультиплексоре, чтобы "с утра" (на новом такте) первым же делом отправить их на запись/обработку!

Введение этой "защёлки" не стоит нам ни одного лишнего ЛЭ: раньше в данном модуле использовали только комбинаторную часть ЛЭ, а теперь и регистровая пригодилась. И именно эта защёлка - наиболее важная, она делит путь прохождения сигнала практически пополам, и от неё одной максимальная частота поднимается до 19,6 МГц. Меня это, честно говоря удивило - я ожидал в лучшем случае удвоения рабочей частоты, с 6 МГц до 12 МГц, путь-то укоротился в лучшем случае вдвое! Видимо, модули ДО защёлки и ПОСЛЕ неё удалось получше сгруппировать, не знаю всех особенностей "внутренней кухни" ПЛИС.

Но нам такая частота - не рыба не мясо. Хочется-то сравняться с частотой оцифровки видеосигнала, 25 МГц. Казалось бы, ещё всего 25% прибавить, вообще халява! А вот нет - как оказывается, теперь у нас нет очевидного "нарушителя", работающего слишком медленно. Медленно работает всё. И нужно ввести ещё две новые защёлки.

Одна защёлка - на выходе ПЗУ кода. Если посмотреть на "схему" QuatCore, мы увидим, что и там у нас был "задел":


К ПЗУ подходит провод тактовой частоты clk, но подключать его некуда. Да и ПЗУ называется интересно: QuatCoreCodeROMlat0.
lat0 означает "latency 0", т.е когда мы задаём адрес, то данные получаем НА ТОМ ЖЕ ТАКТЕ. В загашнике также лежал другой модуль с романтическим названием QuatCoreCodeROMlat1. Причём я пробовал ставить "защёлку" как на входе памяти, так и на выходе. Оказалось, что по входу никакого эффекта на общее быстродействие нет, что и понятно - память подключена НАПРЯМУЮ к выходу регистра PC. А вот наличие защёлки на выходе очень даже неплохо.

Но есть одна важная особенность: это должна быть не просто защёлка, всегда сохраняющая поступающие в неё данные и подавая их на выход в начале следующего такта. У нас есть несколько команд, которые просто ОБЯЗАНЫ выполняться более 1 такта. И чтобы начисто не пропустить одну команду, и не мучаться очень серьёзно с откручиванием PC НАЗАД, нам нужен вход разрешения.

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

В техническом описании на ПЛИС 5576ХС4Т показано, что входы разрешения на выходных "защёлках" действительно есть (обведены красным):


И эквивалентная картина приведена в даташите на Flex10k. По логике вещей, раз эта логика присутствует, её можно где-то включить в блоках lpm_rom, altdpram или в чём-то таком.

Увы, если мы создаём именно ПЗУ, ROM, то подобного выбора нам не предоставят. И действительно, на картинке ниже (эквивалентная схема для однопортового режима) данный вход исчез:


Ну ладно, поставим сюда двухпортовую ОЗУ, просто не будем использовать её входы. Если выбрать вариант Dual clock, то затем можно настроить, чтобы одна тактовая частота была на вход, а другая на выход. Затем можно выбрать, чтобы к каждым "часам" прилагался вход разрешения, inclocken и outclocken (представляется злобный монстр оутклокен, это как кракен, но стимпанковско-механический, из бронзовых "часовых" шестерёнок!).

Не чувствуя никаких подлянок, я написал вот такой модуль:
module QuatCoreROMwENA (input clk, input SrcENA, DestENA, input [RomWidth-1:0] Addr, output [7:0] SrcAddr, output reg [7:0] DestAddr = 1'b0);

parameter RomWidth = 6;

	wire [15:0] Q; //непосредственно выход памяти

	altdpram	altdpram_component (
				.outclock (clk),
				.wren (1'b0),	//это на самом деле ROM, но почему-то в однопортовом режиме нам не хотят давать ENA
				.outclocken (SrcENA),
				.data (16'b0),	//вообще, вариант с заменой 512-байтового блока на новый откуда-нибудь из SD-карты - весьма неплох, позволяет крутые вещи делать. Но потом...
				.rdaddress (Addr),
				.wraddress (Addr),	//пока не надо :)
				.q (Q));
	defparam
		altdpram_component.indata_aclr = "OFF",
		altdpram_component.indata_reg = "UNREGISTERED",
		altdpram_component.intended_device_family = "FLEX10KE",
		altdpram_component.lpm_file = "QuatCoreCode.mif",
		altdpram_component.lpm_type = "altdpram",
		altdpram_component.outdata_aclr = "OFF",
		altdpram_component.outdata_reg = "OUTCLOCK",
		altdpram_component.rdaddress_aclr = "OFF",
		altdpram_component.rdaddress_reg = "UNREGISTERED",
		altdpram_component.rdcontrol_aclr = "OFF",
		altdpram_component.rdcontrol_reg = "UNREGISTERED",
		altdpram_component.width = 16,
		altdpram_component.widthad = RomWidth,
		altdpram_component.wraddress_aclr = "OFF",
		altdpram_component.wraddress_reg = "UNREGISTERED",
		altdpram_component.wrcontrol_aclr = "OFF",
		altdpram_component.wrcontrol_reg = "UNREGISTERED";

	assign SrcAddr = Q[7:0];
	
	always @(posedge clk) if (DestENA)
		DestAddr <= Q[15:8];

endmodule


То есть, ввёл двухпортовую память, задал ей файл инициализации, и затем входы записи "заземлил". Увы, результат вышел неожиданный:


По первому же фронту выход переходит в неопределённое состояние, и остаётся в таковом. Чтобы понять, в чём тут проблема, попробовал всё-таки вывести входы на запись, we (Write Enable) и D (data). Наткнулся на такую ошибку:



Чтобы допустить инициализацию памяти, он хочет, чтобы все сигналы на запись "защёлкивались". Но увы, если мы добавим inclock, то он будет относиться и к адресу, и мы удлиним конвейер ещё на одно звено, причём безо всякой пользы для быстродействия!

Не знаю пока, как побороть "блок внутренней памяти" (БВП, он же EAB), в итоге сделал проще - защёлки поставил СВОИ:
module QuatCoreCodeROMwSTALL (	input [RomAddrWidth-1:0] Addr, input clk, input stall,
				output [7:0] SrcAddr, output reg [7:0] DestAddr = 1'b0);

	parameter RomAddrWidth = 8;

	wire [15:0] OP;
	reg [15:0] rOP = 1'b0;
	always @(posedge clk) if (~stall) begin
		rOP <= OP;
		DestAddr <= rOP[15:8];
	end
		
	assign SrcAddr =  rOP[7:0];

	lpm_rom	lpm_rom_component (
				.address (Addr),
				.q (OP),
				.memenab (1'b1));
	defparam
		lpm_rom_component.intended_device_family = "FLEX10KE",
		lpm_rom_component.lpm_address_control = "UNREGISTERED",
		lpm_rom_component.lpm_file = "QuatCoreCode.mif",
		lpm_rom_component.lpm_outdata = "UNREGISTERED",
		lpm_rom_component.lpm_type = "LPM_ROM",
		lpm_rom_component.lpm_width = 16,
		lpm_rom_component.lpm_widthad = RomAddrWidth;
		
endmodule


Наверняка потерял сколько-то в быстродействии на этом, и ещё и 16 ЛЭ лишних задействовал, но попробуем пока так.

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

Теперь максимально допустимая частота составит 23,64 МГц, и главным нарушителем становится модуль QuatCoreMem в целом и оперативная память в частности. Именно путь от выхода ПЗУ в декодер адреса, в мультиплексоры базовых и индексных регистров и сумматоры, дающие эффективный адрес, затем этот адрес поступает в ОЗУ, и уже оттуда на шину данных через ещё один мультиплексор идут данные!

И это цепочку надо где-то "разрубить", желательно на равные половинки. Опытным путём было установлено, что лучшее место - это вход ОЗУ. Там надо поставить "защёлку" - и тогда можно рассчитывать на максимальную частоту в 28,74 МГц.

Но теперь модуль памяти надо доработать, чтобы он останавливал работу процессора на 1 такт, пока из памяти не придут корректные данные.


Итак, мы поставили дополнительные регистры где надо и увидели, что теперь процессор мог бы работать на частоте 25 МГц и даже выше. Но чтобы он заработал КОРРЕКТНО, нужно ввести логику "управления конвейером" - научиться останавливать его и "выбрасывать" из него неверные команды. Похоже, что логика эта выйдет весьма и весьма небольшой - десяток ЛЭ, не больше, но её нужно делать очень аккуратно - она так и норовит "самозаблокироваться"...
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

Recent Posts from This Journal

  • Ремонт лыжных мостиков

    Вернулся с сегодняшнего субботника. Очень продуктивно: отремонтировали все ТРИ мостика! Правда, для этого надо было разделиться, благо народу…

  • Гетто-байк

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

  • А всё-таки есть польза от ковариаций

    Вчера опробовал "сценарий", когда варьируем дальность от 1 метра до 11 метров. Получилось, что грамотное усреднение - это взять с огромными весами…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments