nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

DMA для QuatCore

Вот фрагмент схемы нашего "процессорного ядра" QuatCore:


Справа сверху "притаилась" оперативная память. На той ПЛИС, что у меня есть сейчас (5576ХС4Т) и той, что хотел бы применить в лётном изделии (5576ХС6Т) полноценной двухпортовой памяти нет. Максимум - можно одновременно осуществлять чтение на один адрес и запись на другой адрес, при условии, что адреса не совпадают (если совпадают, то на выход пойдёт только что записанное значение, конкретно в этих ПЛИС, если они ведут себя аналогично Flex10k до последних мелочей). Но я пока и эту возможность не использовал из-за своей исключительной жадности: это бы потребовало два отдельных формирователя эффективного адреса, а они довольно толстые (выбрать базовый регистр, выбрать первый индексный, второй индексный регистры - и всё сложить).

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

Сейчас адрес используется ровно один. Кроме него, на оперативную память поступают 16 бит для записи, напрямую из шины данных, сигнал WREN (WRite ENable), а выход отправляется в модуль QuatCoreMem, где стоит мультиплексор, срабатывающий на "квадратные скобки" в мнемонике команды. Если они стоят - значит нам нужно значение из памяти, коммутируем его. Если нет - значение одного из регистров (X,Y,Z,SP), выдадим его.

Теперь бы как-нибудь к этой же памяти подключить "провода" D,Q, MemWrReq, MemRdReq и MemReady из протокольного контроллера МКО, желательно малой кровью... Обеспечить ему пресловутый Прямой Доступ к Памяти, или Direct Memory Access (DMA)...


Как говорилось ранее, меня вполне устраивает абсолютный приоритет процессора по доступу к памяти. Протокольный контроллер вполне может подождать десяток тактов, т.к происходит там всё даже по нашим меркам неспешно. Пока процессор "перемалывает" 16 бит за 1 такт, 40 нс, на шине МКО один бит передаётся за 1 мкс, т.е 25 тактов. Такой вариант, где DMA не мешает работать процессору, "выискивая" свободные такты, вовсе не нов, это так называемый "прозрачный" режим (transparent mode) работы.

Насчёт "взаимных блокировок" ещё подумаю на досуге. Могу предположить нехорошую ситуацию, что пока процессор формирует новую целевую информацию, нежданно-негаданно пришёл запрос на её получение - и протокольный контроллер начал её выдавать, из-за чего возникает "винегрет" из старых и новых данных. По крайней мере, у нас обеспечивается атомарность по 16-битным словам: каждое из них будет либо "новое", либо "старое", но никак не смесь. Поэтому мы можем хотя бы обезопасить себя: прежде чем работать над этими целевыми данными, мы вешаем "статусные биты" (они все умещаются в одно слово, первое по счёту, сразу после заголовка), где все признаки корректности установлены в ноль, затем спокойненько заливаем новые данные, и после этого уже устанавливаем правильные "статусные биты". Этого вполне должно хватить, тем более что я всё-таки надеюсь, что после нескольких запросов мы выйдем в "синхронную работу", где начало экспозиции будем подгадывать так, чтобы как раз за 10-100 мкс до запроса заготовить все данные - так они будут наиболее "актуальными". Если так, то коллизия возможна только в самом начале работы, а там она простительна. Поэтому пока городить огород не хочу! На самом деле, я даже не уверен, что смешивание старых и новых данных может сильно навредить. От алгоритмов будет зависеть - может ли в них кватернион ступенькой скакнуть, например, от 0,707+0,707i до -0,706-0,708i - оба кватерниона выражают практически ту же ориентацию, но если мы возьмём одно значение "старое", а другое "новое", 0,707-0,708i, вот это будет фиаско! Когда-то у меня была затея выражать кватернион малого поворота как -1-ai-bj-ck, потому что минус единица вполне себе представима, а плюс единица - нет, и ставить 0,9999847+ai+bj+ck как-то некрасиво... Может, лучше так не делать...

Ладно, поехали!

Самое простое - вход D для протокольного контроллера. Он спокойненько подключается к выходу Q оперативной памяти, дешёво и сердито.

Остальные входы надо будет подключать через модуль DMA, он встроится вот так:


Внутри модуля будет находиться два мультиплексора "2 к 1", на данные (16 бит) и на адрес (пока что 9 бит, возможно и больше).

Данные будут поступать либо с шины данных процессора (когда процессор обращается к памяти), либо с выхода приёмопередатчика МКО (на первое время UART), на запись в память. Здесь добавление мультиплексора меня ничуть не волнует: на основном мультиплексоре QuatCore (QuatCoreSrcMux) стоит "защёлка", чтобы разделить получение данных (половинка SrcAddr команды) и их использование (половинка DestAddr) на два соседних такта. Как результат, участок между этим мультиплексором и входом в оперативной памяти был предельно коротким, вообще без ЛЭ на пути! Поэтому здесь хиленький мультиплексор погоды не сделает.

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

И ещё нужна достаточно примитивная логика: если память сейчас нужна процессору (будь то на запись, CPU_wr = 1, или на чтение, CPU_rd = 1), адрес коммутируется с выхода QuatCoreMem. Данные с процессорной шины можно выбирать просто по CPU_wr = 1, на остальные сигналы (CPU_rd, EXT_rd, EXT_wr) наплевать. И также, по CPU_wr = 1 заведомо должно появиться WREN = 1.

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

Заглянем в модуль QuatCoreFasterMemDecoder:

//приготовление эффективного адреса и получение данных по этому адресу - процесс небыстрый,
//поэтому разбиваем его на 2 такта. Т.е если у нас запрашивают ЧТЕНИЕ из памяти, то включаем busy на 1 такт

//на удивление сложное поведение внутри конвейера, нужно различать 3 разных управляющих сигнала:
//DestStall запрещает запись в регистры, память и инкременты/декременты SP, по части DestAddr
//SrcStall запрещает инкременты/декременты SP, но как ни в чём не бывало формирует правильное значение в DataBus. Когда будет SrcStall=0, мы тут же выдаём ответ
//SrcDiscard отличается тем, что после сброса в 0 мы выжидаем ещё 1 такт, прежде чем выдать ответ, т.к не уверены, что адрес команды остался тем же. 

module QuatCoreFasterMemDecoder (input clk, input [7:0] PreDestAddr, input [7:0] SrcAddr, input SrcStall, input PreDestStall, input SrcDiscard, input PipeStall,
				output reg MemWrite = 1'b0, output SrcSquareBrac, output WriteX, output WriteY, output WriteZ, output WriteSP, output CountSP, output SPup,
				output [1:0] BaseAddr, output [1:0] FirstIndex, output [1:0] SecondIndex, output busy	);

reg fetching = 1'b0;

reg RegWrite = 1'b0;
reg DestCountSP = 1'b0;
reg [5:0] DestAddr = 6'h00;
							
wire isDest = (~PreDestStall)&(PreDestAddr[7:6] == 2'b11);
wire DestSquareBrac = (PreDestAddr[3:0] != 4'b1101);

always @(posedge clk) if (~PipeStall) begin
	MemWrite <= DestSquareBrac & isDest;
	RegWrite <= (~DestSquareBrac) & isDest;
	DestCountSP <= isDest & PreDestAddr[5] & PreDestAddr[4] & PreDestAddr[1] & PreDestAddr[0];
	DestAddr <= PreDestAddr[5:0];
end

assign BaseAddr = 	MemWrite? DestAddr[5:4] : SrcAddr[5:4];
assign FirstIndex = 	MemWrite? DestAddr[1:0] : SrcAddr[1:0];
assign SecondIndex = 	MemWrite? DestAddr[3:2] : SrcAddr[3:2];

assign SrcSquareBrac = (SrcAddr[3:0] != 4'b1101);

assign WriteX = RegWrite & (DestAddr[5:4] == 2'b00);
assign WriteY = RegWrite & (DestAddr[5:4] == 2'b01);
assign WriteZ = RegWrite & (DestAddr[5:4] == 2'b10);
assign WriteSP = RegWrite & (DestAddr[5:4] == 2'b11);

wire isSource = (~SrcDiscard)&(~SrcStall)&(SrcAddr[7:6] == 2'b11);
assign CountSP = (DestCountSP | (fetching & isSource & SrcAddr[5] & SrcAddr[4] & SrcAddr[1] & SrcAddr[0]));
assign SPup = ~SecondIndex[0];

assign busy = isSource & SrcSquareBrac & (~fetching);

//осталось с ним сообразить. Когда приходит SrcAddr[7:6] == 2'b11 и SrcDiscard = 0, то невзирая на SrcStall, мы уже начали выборку! 
//и значит, когда нам "позволят" работать, уже получим результат!
//но ещё, когда SrcStall=0 и fetching=1, он должен сброситься опять в ноль,
//чтобы идущая сразу следом команда не могла ошибочно выдать результат СРАЗУ
always @(posedge clk)
	fetching <= (~SrcDiscard)&(SrcAddr[7:6] == 2'b11)&SrcSquareBrac&((~fetching)|SrcStall);
													
endmodule


И держа его перед глазами, подумаем, как правильно сформировать CPU_rd. Скорее всего, неважно, если CPU_rd = 1 чуть чаще, чем действительно надо (например, данные действительно собираемся брать из QuatCoreMem, но не из памяти как таковой, а из регистра), главное только, чтобы случайно не выдать CPU_rd = 0 там, где обращение к памяти реально идёт! Это прямой путь к неверной работе, когда процессор получит результат, адресованный протокольному контроллеру.

У меня первая идея была взять сигнал isSource из модуля, приведённого выше. Действительно, там проверяется, что половинка команды SrcAddr относится именно к QuatCoreMem, и при этом SrcDiscard = 0 (т.е данную команду не "выкинули" из конвейера из-за прыжка) и SrcStall = 0 (не ждём выполнения второй половинки команды).

Но это как раз могло выйти боком! Логика модуля такова, что при SrcStall = 1 выборка памяти всё равно начинается, хоть при этом isSource=0. Так что нужно чуть "ослабить" выражение:

assign CPU_rd = (~SrcDiscard) & (SrcAddr[7:6] == 2'b11);


По идее, можно и SrcDiscard отсюда выкинуть, поскольку это всё-таки довольно редкая ситуация, погоды не сделает.

Не нарваться бы здесь на другую "засаду": процессор пытается отправить задание видеообработчику, но у того забит входной буфер, поэтому процессор ждёт, как раз-таки с SrcStall = 1. И если ему так не повезло в этот момент пытаться прочитать данные из памяти, то память окажется "заблокирована", и возможно ОЧЕНЬ НАДОЛГО. Например, мы забили задания "ожидание кадрового синхроимпульса" и ещё 30 штук "ожидание строчного синхроимпульса" - и теперь ничего не будет происходить ближайшие десятки-сотни микросекунд, если не единицы миллисекунд! И если в это время придёт запрос по шине МКО (пока что UART), контроллер у нас самую малость сбрендит, выдаст что угодно, только не целевую информацию. Да и записать он её не сможет толком.

Есть ли такое сочетание у меня в коде? ДА ПОЛНО:
TaskPending proc
		ABS	C
		JNO	[SP]	;вот в чём прелесть стека без инкремента!
		;если дошли до сюда, заказываем отрезки на обработку
AcqNoCheck:	Acc	[X+2j+k]
AcqAfterMerge:	DIV2S	[X+1]	;теперь в аккмуляторе у нас X - Ceil(D/2)
		DIV2A	1	;чтобы всё-таки было X-Floor(D/2)
		ACQ	Acc	;первый отрезок
		ADD	[X+1]	;а вот теперь X + Floor(D/2)
		ACQ	Acc	;второй отрезок
		JMP	[SP]
TaskPending endp


Напомним, левую часть мы как бы сдвигаем на строку вниз - это и будет показывать, какие две половинки команды исполняются одновременно. Тут вместе с ACQ (Acquire) будут исполняться и [X+1], и [SP]...

Впрочем, здесь заполнение входного буфера поставит крест и на обнаружении точек, так что "уж если облажаемся - то облажаемся вдвойне", а это уже не важно.

"Красивого" способа сформировать CPU_rd, чтобы при "застревании" процессора была доступна память я не вижу. Проблема, что когда "зажигается" SrcStall, мы понятия не имеем, сколько он продлится, один такт или 1000 тактов. Любая логика вида "вот, ничего не происходит, давайте мы по-быстренькому к памяти обратимся" может привести к тому, что именно в следующий такт SrcStall погаснет, и процессор наивно полагал что "уж точно на выходе памяти уже правильное значение лежит, его-то я и возьму", а на деле там значение, заказанное не им! Либо можно отказаться от выборки памяти во время SrcStall, но это замедлит процессор.

Так что предлагаю пока смириться ещё и с такой "особенностью". Если совсем припрёт, можно будет в программу внести NOPы в нужных местах, чтобы такое предотвратить, хотя сейчас у меня ощущение, что при правильной работе видеообработчика такой блокировки на деле не будет.

Ладно, продолжим... Выводим из QuatCoreMem этот новый сигнал, CPU_rd:


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

И остаётся написать этот несчастный DMA. Вот его заголовок (по нему и был сформирован символ "на схеме"):

module QuatCoreDMA (input clk, input [AddrWidth-1:0] CPU_addr, input [8:0] Ext_addr,
		input CPU_wr, input CPU_rd, input Ext_wr, input Ext_rd,
		input [15:0] CPU_data, input [15:0] Ext_data,
		output [15:0] Q, output WREN, output [AddrWidth-1:0] Addr, output MemReady);
parameter AddrWidth = 9;


Как управлять мультиплексорами - мы фактически разобрались, халява сэр:
assign Q = CPU_wr? CPU_data : Ext_data;
assign Addr = (CPU_wr | CPU_rd)? CPU_addr : Ext_addr;


Хотя и здесь неявным образом мы полагаем, что данные на входе Ext_data "не протухнут", если мы их возьмём не во время такта, где Ext_wr = 1, а сколькими-то тактами позже. Подозреваю, что это не совсем так: при работе нашего полудуплексного приёмопередатчика на приём данные на выходе оставались верными ровно в течение 1 такта, а потом сдвиговый регистр срабатывал вновь, делая их некорректными. Надо будет немножко это дело подправить - запретить сдвиговому регистру работать в этот момент, тогда у нас будет "передышка" эдак на 25 тактов. Давайте глянем сразу на это безобразие:

always @(posedge clk) if (isIdle? startTX : ce) begin
	SR[18] <= (startTX & isIdle)? D[15] : rxd;
	SR[17:1] <= (startTX & isIdle)? {D[14:8],2'b01,D[7:0]} : SR[18:2];
	//SR[0] <= (startTX & isIdle)? 1'b0 : (isCRC & (State[3:1] != 3'b111))? CRC_bit : SR[1];
	SR[0] <= (startTX & isIdle)? 1'b0 : (isCRC &~& State[3:1])? CRC_bit : SR[1];
end
		
assign Q = {SR[18:11],SR[8:1]};


Интересует нас работа SR[18] и SR[17:1], т.к именно оттуда мы получаем результат. В состоянии, отличном от sIdle, сдвиг производится по ce=1.

Как вариант, можно HasOutput задержать на 1 такт, позволив сдвиговому регистру сделать своё "чёрное дело", и данные снять из SR[17:10] и SR[7:0]. Заодно и фиттеру задачу упростим, разбив комбинаторные цепи "пополам" защёлкой:

wire cHasOutput = isStopState & ce & rxd & ~RW & isSecondByte; //т.е приняты уже все данные, а стоповый бит ожидаемо высокий.
always @(posedge clk)
	HasOutput <= cHasOutput;


Глянем, ничего ли не поломали:


Хорошо! Импульс HasOutput приходит в самом начале "правильных данных", и они там ещё надолго сохраняются.

И опять возвращаемся к несчастному DMA!

Да, с мультиплексорами всё хорошо, теперь ещё немного логики туда добавить. Для начала, на запись. Если приходит Ext_wr = 1 (запрос на запись в память из протокольного контроллера), и в это время CPU_wr = 0 и CPU_rd = 0 (процессору память не нужна), нужно подать WREN = 1 (разрешить запись). Если же процессор работал с памятью - нам нужно запомнить, что заявка поступала - и при первой возможности её выполнить.

Вот как-то так:
wire AllowExt = ~CPU_rd & ~CPU_wr; //процессору память не нужна, можно ей распоряжаться
					
reg ExtWrReq = 1'b0;

//когда поступает Ext_wr=1, нужно запомнить "запрос на запись", если только мы его сразу же не удовлетворили
//а сброситься он должен, когда мы его удовлетворим, а произойдёт это, когда память освободится
always @(posedge clk)
	//ExtWrReq <= Ext_wr & (~AllowExt) ? 1'b1 : AllowExt: 1'b0 : ExtWrReq;
	ExtWrReq <= (ExtWrReq | Ext_wr) & (~AllowExt);
	
assign WREN = CPU_wr | (Ext_wr | ExtWrReq) & AllowExt;


Ввели сигнал AllowExt - "память отдали внешнему устройству". Далее, регистр ExtWrReq, в котором будет храниться "запрос", если он не был исполнен сразу же.

Логику для него я сначала записал "в стиле RS-триггера": устанавливаем в единицу, когда поступил запрос, но память была занята, сбрасываем в ноль, когда память освобождается, всё остальное время сохраняем состояние.

Потом увидел, что то же самое можно записать в виде более простого выражения, "для D-триггера".

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

Осталась логика чтения из памяти.

Для процессора уже всё готово - как только CPU_rd = 1, запрашивается именно адрес, данный процессором, и на следующий такт поступят данные.

Для внешнего устройства (протокольного контроллера) с самими данными тоже разобрались: как только процессору память не нужна, включится адрес из внешнего устройства, и такт спустя мы получим необходимые данные.

Нужно лишь отметить их получение, подав в этот момент MemReady = 1.

Похоже, для этого нужно два 1-битных регистра. Первый, как и раньше, "зафиксирует" запрос, и снимет его, как только память будет успешно запрошена. Второй нужен попросту для задержки на один такт, поскольку информация также будет выдана один такт спустя. Как-то так:
reg ExtRdReq = 1'b0;

always @(posedge clk) begin
	ExtRdReq <= (ExtRdReq | Ext_rd) & (~AllowExt);
	MemReady <= (ExtRdReq | Ext_rd) & AllowExt;
end


Как будто бы всё. Так выглядит модуль целиком:
module QuatCoreDMA (input clk, input [AddrWidth-1:0] CPU_addr, input [8:0] Ext_addr,
		input CPU_wr, input CPU_rd, input Ext_wr, input Ext_rd,
		input [15:0] CPU_data, input [15:0] Ext_data,
		output [15:0] Q, output WREN, output [AddrWidth-1:0] Addr, output reg MemReady = 1'b0);
parameter AddrWidth = 9;
					
assign Q = CPU_wr? CPU_data : Ext_data;
assign Addr = (CPU_wr | CPU_rd)? CPU_addr : Ext_addr;

wire AllowExt = ~CPU_rd & ~CPU_wr; //процессору память не нужна, можно ей распоряжаться
					
reg ExtWrReq = 1'b0;

//когда поступает Ext_wr=1, нужно запомнить "запрос на запись", если только мы его сразу же не удовлетворили
//а сброситься он должен, когда мы его удовлетворим, а произойдёт это, когда память освободится
always @(posedge clk)
	//ExtWrReq <= Ext_wr & (~AllowExt) ? 1'b1 : AllowExt: 1'b0 : ExtWrReq;
	ExtWrReq <= (ExtWrReq | Ext_wr) & (~AllowExt);
	
assign WREN = CPU_wr | (Ext_wr | ExtWrReq) & AllowExt;
					
reg ExtRdReq = 1'b0;

always @(posedge clk) begin
	ExtRdReq <= (ExtRdReq | Ext_rd) & (~AllowExt);
	MemReady <= (ExtRdReq | Ext_rd) & AllowExt;
end
								
endmodule


Синтезируется в 29 ЛЭ, что и понятно: 16-битный мультиплексор, 9-битный мультиплексор, 3 регистра и логика WREN. Меньше никак.

И собираем всё вместе!
В первую очередь, "протокольный контроллер":


Не очень красиво: тут прямо внутри стояла "своя собственная" оперативная память и две защёлки, чтобы MemRd задержать на 2 такта и выдать в качестве MemReady. Сейчас это всё выкинули, и соответствующие ножки "вывели наружу".


Схема "верхнего уровня":


Барабанная дробь... Синтезируется в 1704 ЛЭ (перед фиттером), 1732 ЛЭ (после фиттера). Timing Analyzer: все требования выполнены, предельная частота 26,04 МГц!

Как-то слишком просто... Я ожидал, что будет эдак 20 МГц, и ещё неделю-другую надо будет "конвейер перебирать", чтобы вернуться к родным 25 МГц. Какая-то жуткая подлянка затаилась и ждёт своего часу.

Ну ладно, с завтрашнего дня начинаю отладку. Разрозненные части, наконец-то, собираются в целое.
Tags: ПЛИС, работа, странные девайсы
Subscribe

Recent Posts from This Journal

  • Тестируем atan1 на QuatCore

    Пора уже перебираться на "железо" потихоньку. Решил начать с самого первого алгоритма, поскольку он уже был написан на ассемблере. В программу внёс…

  • Формулы приведения, что б их... (и atan на ТРЁХ умножениях)

    Формулу арктангенса на 4 умножениях ещё немножко оптимизировал с помощью алгоритма Ремеза: Ошибка уменьшилась с 4,9 до 4,65 угловой секунды, и…

  • Алгоритм Ремеза в экселе

    Вот и до него руки дошли, причина станет ясна в следующем посте. Изучать чужие библиотеки было лениво (в том же BOOSTе сам чёрт ногу сломит), писать…

  • atan на ЧЕТЫРЁХ умножениях

    Мишка такой человек — ему обязательно надо, чтоб от всего была польза. Когда у него бывают лишние деньги, он идёт в магазин и покупает какую-нибудь…

  • Ай да Пафнутий Львович!

    Решил ещё немного поковыряться со своим арктангенсом. Хотел применить алгоритм Ремеза, но начал с узлов Чебышёва. И для начала со своего "линейного…

  • atan(y/x) на двух умножениях!

    Чего-то никак меня не отпустит эта тема, всё кажется, что есть очень простой и эффективный метод, надо только его найти! Сейчас вот такое…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 4 comments