nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Ввод-вывод для быстрого QuatCore

Первый QuatCore у нас работал на 4 МГц (на ПЛИС 5576ХС4Т) - достаточно для выполнения целевой задачи, но страшно неудобно его соединять с видеообработчиком, который просто обязан иметь тактовую частоту хотя бы 25 МГц, иначе просто не будет успевать получать пиксели из видеопотока "высокой чёткости".

В итоге мы его успешно ускорили до 25 МГц, после чего доработали компилятор, чтобы он автоматически обнаруживал так называемые Hazard'ы (конфликт соседних команд, из-за того, что они на самом деле выполняются "параллельно", одна записывает данные, а другая считывает "устаревшие") и исправлял их. Кроме того, доработали программу алгоритма захвата ("аффинного алгоритма"), чтобы она хорошо работала на этом процессоре. Как результат, она сократилась в размере (благодаря возможности пересылок "память-память" и упрощения кода в нескольких местах) и ускорилась в 5,5 раз.

Дальше я зачем-то попробовал поставить двунаправленную шину, и оно получилось ещё лучше (в теории. Для такого варианта надо было бы опять всю логику конвейера перекраивать, и Hazard'ы впридачу, поэтому провести симуляцию и получить конкретные результаты - это очень муторно), но сейчас я решил: остановлюсь на том, что есть. В дальнейшем, будет смысл ещё сильнее ускориться, до 50..80 МГц, для того, чтобы максимально быстро получать картинку с фотоприёмной матрицы 1205ХВ014. В этом один-единственный смысл: убрать эффект Rolling Shutter, насколько это вообще возможно. А "укороченный конвейер" на 25 МГц - это в любом случае полумера, нам главное, чтобы в принципе на 25 МГц работало, а выигрыш в несколько микросекунд за счёт более совершенной архитектуры совсем неинтересен (хотя всё равно червь грызёт, это уже психиатрическое).

Так что пока "фиксируем" имеющуюся архитектуру. Мы испытали "голое" ядро, теперь нужно присоединить к нему периферию: UART, SPI и контроллер ЖК-экранчика. Их нужно встроить в нашу "конвейерную логику", а именно, правильно прицепить к ним сигналы SrcDiscard, SrcStall, DestStall, SrcStallReq и DestStallReq...

А первым делом всё-таки оформим "ядро" QuatCore в отдельный модуль:



Все входы нужны: тактовая частота, сброс, входы для внешней статической памяти и для остальной периферии, и входы "запроса на остановку конвейера". Если активна команда на получение данных откуда-то "издалека" (UART, SPI), и ей требуется более одного такта, она должна подать единичку на DestStallReq (запрос на остановку и "получателя данных", и конвейера в целом). Если же активна команда на передачу данных, и она также пока "застряла", то она должна подать единичку на SrcStallReq. Тогда, если она использовала источником данных, к примеру, [--SP], то всё то время ожидания, пока нем не дали передать данные, на шине данных будет лежать именно [SP-1], и только ОДИН РАЗ будет вычитаться единица.

Мы пока оставили два отладочных выхода: PC (Program Counter) и MemAddr, они никуда подключаться не должны, просто чтобы на симуляции понимать, что там вообще происходит.

А все остальные очень важны. SrcAddr и DestAddr задают текущую команду "на запись" и "на чтение". Они должны поступать на каждый внешний модуль. DataBus - "выход" шины данных, после мультиплексирования и задержки на один такт. Это те данные, которые мы можем захотеть куда-то отправить, будь то статическая память, UART, SPI или ЖК-экранчик (и что там ещё добавится в процессе).

DestStall - сигнал для временного отключения всех команд "на запись", либо из-за того, что был произведён прыжок, а в конвейер "по инерции" попала команда, которую исполнять не надо, либо потому что команда "на чтение" выполняется больше одного такта, и можно наломать дров, если наша команда "на запись" хоть чуточку сложнее простой записи в регистр/в память. Т.е инкременты i++,j++,k++, [SP++], [--SP], а также арифметические действия нужно выполнить РОВНО ОДИН РАЗ, иначе жди беды.

SrcStall и SrcDiscard - то же самое для команд "на чтение", но мы как всегда жадные. SrcStall означает - "именно эту команду и надо исполнить, но не торопись". SrcDiscard означает - "это вообще неправильная команда, сделай вид, что её нет". Разница у нас возникла при чтении из памяти: увы, один такт уходит на формирование эффективного адреса и его отправки в блок внутренней памяти, и только к следующему такту мы получаем результат. И если совместно с этой командой выполняется команда умножения (к примеру), то если бы мы выдавали SrcDiscard, то сначала пришлось бы ждать окончания умножения, а потом ещё ждать, пока сформируется эффективный адрес и мы получим правильное значение из памяти, т.к всё время выполнения умножения наш модуль памяти "страдал фигнёй". А так он уже запросит нужный адрес, и будет готов выдать его "мгновенно", но если это был адрес [SP++], то с прибавлением единички он повременит.

Также у нас "выведено наружу" множество параметров: ширина адреса ПЗУ/ROM (где хранится программа), ОЗУ/RAM (где хранятся текущие данные, но при включении ПЛИС они также инициализируются "как пожелаем", что позволяет элементарно хранить там строки и константы, не придумывая хитрючие "загрузчики" или целую операционку). Можно поиграться с шириной аккумулятора - от 19 до 32 бит, чем больше - тем точнее, но занимает много ЛЭ. ijkEnabled - позволяет включить или отключить многострадальную команду ijk (как на запись, так и на чтение), которая собирает в 16 бит регистры i,j,k (каждый по 5 бит) и Inv (1 бит), чтобы можно было их дружно инициализировать, а также одним махом сохранить в стек и извлечь из стека. Казалось, что хорошая идея, но выходной мультиплексор QuatCorePC от этого неплохо "раздувается".

И наконец, EnableIO и EnableSRAM позволяет чуть-чуть упростить "ядро" процессора, мультиплексор шины данных, если известно, что один из входов будет незадействован.

Теперь приделаем хотя бы UART "на передачу", увидеть сообщение Hello, World! на компьютере. Но сразу с перспективой подключить всё остальное. Первым делом размещаем модуль QuatCoreIOselector, слегка доработанный, чисто "из жадности":



А вот код этого модуля:
//DestAddr==000x_xxxx : 'OUT'
//DestAddr==001x_xxxx : select output device

//xxxx_xxxx_xxxx_xx00 - UART
//xxxx_xxxx_xxxx_xx01 - LCD
//xxxx_xxxx_xxxx_xx1x - SPI

//also selecting SPI device
//xxxx_xxxx_xxxx_0xxx - Ethernet
//xxxx_xxxx_xxxx_10xx - SD
//xxxx_xxxx_xxxx_11xx - ADC


//IN command is 
//SrcAddr == 1001_0xxx
//(ALU Src was 100x_xxxx, now it is 1000_xxxx)

module QuatCoreIOselector (	input clk, input [7:0] SrcAddr, input [7:0] DestAddr, input [15:0] DataBus, input SPI_busy,
				output UARTtxEN, output LCD_EN, output SPItxEN, output UARTrxEN, output SPIrxEN,
				output reg [1:0] SPIdevice = 2'b0);

parameter enableLCD = 1'b0;
parameter enableUART = 1'b1;
parameter enableSPI = 1'b0;

localparam HasChoice = enableSPI | ((enableLCD + enableUART + enableSPI) > 1);

wire isSelection = (~DestAddr[7]) & (~DestAddr[6]) & DestAddr[5] & HasChoice;
wire isIO_out = (~DestAddr[7])&(~DestAddr[6])&((~DestAddr[5]) | (~HasChoice));
wire isIO_in = (SrcAddr[7:3] == 5'b1001_0);

reg [1:0] sel = enableUART? 	2'b00:
		enableLCD?	2'b01:
				2'b10;

reg [1:0] shadowSPI = 2'b0;

always @(posedge clk) if (isSelection) begin
	sel <= DataBus[1:0];
	shadowSPI <= DataBus[3:2];
end

always @(posedge clk) if (~SPI_busy)
	SPIdevice <= shadowSPI;
	
assign UARTtxEN = (sel==2'b00) & isIO_out & enableUART;
assign LCD_EN = (sel==2'b01) & isIO_out & enableLCD;
assign SPItxEN = sel[1] & isIO_out & enableSPI;

assign UARTrxEN = (sel==2'b00) & isIO_in & enableUART;
assign SPIrxEN = sel[1] & isIO_in & enableSPI;

endmodule



Если помните, мы решили (как всегда, это решение не единственно возможное и наверняка не лучшее), что будут команды IN и OUT, которые будут принимать данные с устройства ввода-вывода или выдавать данные на него, а устройство будет выбираться с помощью команды SIO (Select I/O), и при текущей реализации QuatCoreIOSelector может быть выбрано UART, либо ЖК-экранчик, либо SPI, и дополнительно можно выбрать одно из 3 устройств, подключённых по SPI: медленный АЦП (присутствует на отладочной плате для 5576ХС4Т), Ethernet-контроллер (также присутствует, и является одним из двух "генераторов тактовой частоты", причём его тактовую частоту можно настроить) и SD-карточка (её я туда подпаял и учился с ней работать по SPI). Позже часть устройств может стать не нужна, а другие добавятся, навроде приёмопередатчика МКО (он же МКИО, он же ГОСТ Р 52070-2003, он же MIL-STD 1553).

А очередная жадность состоит в том, что теперь этот QuatCoreIOSelector не просто заведомо отключит те модули ввода-вывода, которые мы заявим как ненужные (с помощью параметров EnableUART, EnableSPI, EnableLCD), но и в целом "упразднит" команду SIO и свои регистры, на неё завязанные, если окажется, что устройство ввода-вывода всего одно. А именно, в этом случае локальный параметр HasChoice станет равен нулю, и тогда isSelection тоже заведомо будет нулевым, и в этом случае регистры sel и ShadowSPI никогда не будут обновляться, и синтезатор их выкинет, заменив начальными значениями. В итоге, если нам нужен только UART, то размер QuatCoreIOselector уменьшится с 15 ЛЭ до 3 ЛЭ, не считая того, что и незадействованные модули автоматически "самоустранятся".

И теперь подключим передатчик UART, вот его код:

`include "math.v"

module QuatCoreUARTtx (input clk, input st, input [15:0] DataBus, output busy, output txd);

	parameter CLKfreq = 4_000_000;
	parameter BAUDrate = 1_000_000;

	localparam sIdle 	=	4'b0000;
	localparam sStart	=	4'b0110;
	localparam sB1		=	4'b0111;
	localparam sB2		=	4'b1000;
	localparam sB3		=	4'b1001;
	localparam sB4		=	4'b1010;
	localparam sB5		=	4'b1011;
	localparam sB6		=	4'b1100;
	localparam sB7		=	4'b1101;
	localparam sB8		=	4'b1110;
	localparam sStop	= 	4'b1111;

	wire [3:0] State;
	wire isStopState;
	wire isIdle = (~State[3]) & (~State[2]); //shortcut as not all states are used
	
	wire DoStart = st & isIdle;
	wire [7:0] Data = DataBus[7:0];
	wire ce; 	//count enable
	reg r_ce = 1'b0;
	reg r_set = 1'b0;
	always @(posedge clk) begin
		r_ce <= ce;
		r_set <= ce | DoStart | isIdle;
	end
	
	lpm_counter StateMachine (
					.clock (clk),
					.cnt_en (r_ce),
					.sset (DoStart),
					.q (State),
					.cout (isStopState) );
	defparam
		StateMachine.lpm_direction = "UP",
		StateMachine.lpm_port_updown = "PORT_UNUSED",
		StateMachine.lpm_type = "LPM_COUNTER",
		StateMachine.lpm_width = 4,
		StateMachine.lpm_svalue = sStart;

	localparam DividerBits = `CLOG2(((CLKfreq + BAUDrate / 2) / BAUDrate - 1));
	localparam Limit = (CLKfreq + BAUDrate / 2) / BAUDrate - 2;

	lpm_counter Divider (
				.clock (clk),
				.sset (r_set),
				.cout (ce) );
  defparam
    Divider.lpm_direction = "DOWN",
    Divider.lpm_port_updown = "PORT_UNUSED",
    Divider.lpm_type = "LPM_COUNTER",
    Divider.lpm_width = DividerBits,
    Divider.lpm_svalue = Limit;
							
	reg [8:0] ShiftReg = 9'b1111_1111_1;
	assign txd = ShiftReg[0];

	assign busy = st & (~isIdle);

always @(posedge clk) if (DoStart | r_ce)
		ShiftReg <= DoStart? {Data, 1'b0} : {1'b1, ShiftReg[8:1]};

endmodule


И теперь чуть-чуть вспомним, что такое выход busy этого модуля. Когда мы просим его передать байт данных (st=1), busy=0 на этом такте. А затем, пока этот байт передаётся, получится busy=1, если снова st=1, т.е мы уже запросили передачу ещё одного байта, хотя и этот передать не успели! Это хорошее решение, т.к модуль UART может работать вообще без перерывов: мы отдали ему первый байт на передачу, затем процессор двинулся на байт вперёд, проверил условия окончания цикла, перешёл в начало цикла, отдал следующий байт на передачу - и вот только сейчас остановился, чтобы дождаться, пока предыдущий байт будет отправлен. Получается какое-никакое распараллеливание работ: модуль UART работает сам по себе, процессор работает сам по себе, а если он пытается "перегрузить" UART, то "автоматически" останавливается - очень приятный подход.

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



Увы, "философия" busy этого модуля отличается от того, что мы делали с АЛУ и другими устройствами "ядра". Если помните, при обращении к АЛУ мы всегда могли защёлкнуть в него новые данные СРАЗУ ЖЕ, и только после этого он нам останавливал выполнение, пока всё не сделает. Так уж "исторически сложилось", хотя правильнее было бы сделать так же, как с UART: пусть мы даём АЛУ команду "перемножить" - и переходим к следующим командам, а АЛУ перемножает себе потихоньку. И только если мы запросим результат этого умножения, будь то непосредственно значение Acc / UAC, или делая условный переход по JO/JNO, JL/JGE, или если мы дадим очередную арифметическую операцию - лишь тогда мы остановимся и подождём, пока АЛУ завершит свои дела. Как ни странно, один шаг "навстречу" нам уже пришлось сделать - добавить интерлок (блокировку), которая дожидается полного окончания работы, прежде чем отдать значение Acc / UAC.

Разница заключается в том, что для АЛУ и вообще для всех модулей "ядра" до сих пор хватало "защёлки" на мультиплексоре, которая всегда задерживает поступление данных на шину НА ОДИН ТАКТ. Первый такт выполнения новой команды - данные там лежат. Ещё такт спустя - туда приходит что-то другое, так что "хватать" надо очень быстро. Но UART не может по первому требованию загрузить новый байт - у него сдвиговый регистр, который активно в работе по передаче предыдущего байта! Так что давайте в кои-то веки "продлим" PipeStall и до мультиплексора. Его код становится таким:

module QuatCoreSrcMux (input [15: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, input stall, output [15:0] Q);

parameter DoLatch = 1'b1;
parameter EnableIO = 1'b0;
parameter EnableSRAM = 1'b0;

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

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

endmodule


Присоединяем вход stall к проводу PipeStall:


И теперь всё это вместе вполне успешно синтезируется в 516 ЛЭ, fitter срабатывает на удивление быстро (наверное, я пока не присоединил отладочных пинов, а 3 пина, clk, reset и UART_TX - это суперхалява!), тайминги выдерживаются, 25,71 МГц.


Самое время сдуть пыль с программы HelloWorld.asm и попробовать запустить, сначала на симуляторе, а потом и "в железе"...
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • Нахождение двух самых отдалённых точек

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

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Балансируем конвейер QuatCore

    В пятницу у нас всё замечательно сработало на симуляции, первые 16 миллисекунд полёт нормальный. А вот прошить весь проект на ПЛИС и попробовать "в…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 3 comments