nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

QuatCore: подключаем статическую память

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

Задача концептуально простая, но на ровном месте возникают проблемы. Взять и "на лету" передать кадр по UART не выйдет, битрейта не хватает катастрофически. Максимум, что можно получить: 921600 бит/с, но с учётом стартовых и стоповых бит это реально выходит 92160 байт/с (чуть менее 100 кбайт/с), а нужно, простите, 25 МБайт/с. Та же история с SD-карточкой: по интерфейсу SPI можно надеяться максимум на 25 МБит/с, без учёта служебной информации, то есть и здесь В ВОСЕМЬ РАЗ не укладываемся!

Заказал себе новую платку для microSD, где на шлейф выведены все 4 вывода данных, что позволяет опробовать нормальный интерфейс SD, но она пока где-то застряла. Так что с microSD пока без вариантов (та ардуинская платка, что у меня, только по SPI позволяет подключить, а часть выводов не подключена и утоплена внутри гнезда).

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





Попробуем теперь к ней обратиться - что-нибудь записать и потом это что-то прочитать назад...

В кои-то веки я буду приводить "раскрашенный" код, нашёл сайтик http://hilite.me, который делает это в полпинка!


Как водится, добавим в QuatCore модуль для работы с внешней, статической памятью. Он будет гораздо проще, чем QuatCoreMem, поскольку столько адресных регистров и вариантов адресации нам здесь не надо, равно как и работы со стеком. Сделаем лишь самую базовую функциональность.

А именно: будет 20-битный адресный регистр, чтобы можно было обратиться к индивидуальному байту при объёме памяти 1 МБайт. Мы сможем устанавливать младшие 16 бит этого регистра, отправляя данные по одному адресу, и старшие 4 бита - по другому (так сказать, "сегменты"). И затем будет ещё один адрес в DestAddr для записи данных в память, и адрес в SrcAddr для чтения из этой памяти. При каждом чтении и записи будет производиться автоинкремент адресного регистра, что очень удобно для последовательной записи и чтения.

При работе с этой памятью мы перешли на "слово" в 8 бит, поскольку именно столько сейчас надо для представления одного пикселя. И это хоть немножко упростило подключение памяти - можно было ножки данных соединить параллельно, а выбор чипа осуществлять каждый раз с помощью CE (chip enable) - в каждый момент времени мы выбираем только один из двух. Второй "самоустраняется", как будто бы и нет его на этих шинах.

Приведём код этого модуля:

//обращение к внешней памяти
//чуть потеснили IOselector в плане адресов.
//наши DestAddr: 01xx_xxxx
//наши SrcAddr:  1001_1xxx

//DestAddr:
//0100_xxxx - задать младшие 16 бит адреса, ERL (External memory Register Low)
//0101_xxxx - задать старшие 16 бит адреса, ERH (External memory Register High)
//011x_xxxx - записать в память и сделать инкремент, [ER++]

//SrcAddr:
//1001_1xxx - чтение из памяти и инкремент, [ER++]
module QuatCoreSRAM (	input clk, input [7:0] DestAddr, input [7:0] SrcAddr, input stall, input [15:0] D,
			output [18:0] RAMaddr, output RAM_CE0, output RAM_CE1, output RAM_RW,
			inout [7:0] RAM_data);
						
wire IsOurDest = (~stall)&(~DestAddr[7])&DestAddr[6];
wire IsOurSrc = (~stall)&(SrcAddr[7:3] == 5'b1001_1);

wire LoadERL = IsOurDest & (~DestAddr[5]) & (~DestAddr[4]);
wire LoadERH = IsOurDest & (~DestAddr[5]) & DestAddr[4];
wire WriteMem = IsOurDest & DestAddr[5];

wire DoIncrement = WriteMem | IsOurSrc;

wire [19:0] ER; //External memory Register
wire TC; //Terminal Count

lpm_counter ERL (	.clock (clk),
			.cnt_en (DoIncrement),
			.sload (LoadERL),
			.data (D),
			.Q (ER[15:0]),
			.cout (TC));
defparam
	ERL.lpm_direction = "UP",
	ERL.lpm_port_updown = "PORT_UNUSED",
	ERL.lpm_type = "LPM_COUNTER",
	ERL.lpm_width = 16;
	
lpm_counter ERH (	.clock (clk),
			.cnt_en (TC & DoIncrement),
			.sload (LoadERH),
			.data (D[3:0]),
			.Q (ER[19:16]));
defparam
	ERH.lpm_direction = "UP",
	ERH.lpm_port_updown = "PORT_UNUSED",
	ERH.lpm_type = "LPM_COUNTER",
	ERH.lpm_width = 4;
	
assign RAMaddr = ER[19:1];
assign RAM_CE0 = ER[0];
assign RAM_CE1 = ~ER[0];
assign RAM_RW = ~WriteMem;

assign RAM_data = WriteMem? D[7:0] : 8'bzzzz_zzzz;

endmodule


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

Далее мы видим, как старшие 19 бит этого регистра отправляются на шину адреса внешней памяти, а самый младший бит управляет выбором одного из двух чипов. На первый взгляд кажется, что всё нужно было сделать наоборот - чтобы САМЫЙ СТАРШИЙ бит управлял выбором чипа. Как бы одну микросхему набили до упора - и принялись за следующую!

Пока мы работаем на частоте 4 МГц - разницы действительно нет. Но по мере ускорения до 25 МГц, разница появится. Дело в том, что согласно таймингам на эту память, адрес надо задать за 55 нс до записи, а непосредственно данные можно подать за 25 нс до записи. Заставив меняться чипы в шахматном порядке, можно их хоть сколько-нибудь разогнать...

Давайте взглянем на эти тайминги поподробнее... В первую очередь, на чтение:


Провод OE# (Output Enable) у нас вообще заземлён, т.е выход всегда разрешён. Управление производится с помощью сигналов CE (Chip Enable) и шиной адреса. После того, как и то, и другое будет выставлено правильно, должно пройти 55 нс, и только после этого мы получаем корректные данные.

Для тактовой частоты процессора вплоть до 18 МГц нас это полностью устраивает: и адрес, и CE устанавливаются по фронту тактовой частоты на ПРЕДЫДУЩЕЙ КОМАНДЕ, будь то установка адреса или чтение/запись с инкрементом адреса. А нам данные нужны чуточку перед фронтом тактовой частоты на ТЕКУЩЕЙ КОМАНДЕ, чтобы сигнал успел распространиться через мультиплексор на входы всех регистров. В любом случае, при 4 МГц особенно беспокоится не стоит. Также не стоит беспокоится, что мы раньше времени переключим адрес или выбор чипа - это произойдёт лишь тогда, когда мы будем "защёлкивать" данные, так что накладок не будет.

А вообще, если вдруг захочется производить чтение из памяти на бешеной скорости, надо будет вывести провод OE# на ПЛИС. Как видно, выход включается всего через 5 нс после подачи OE#=0. Так что можно сначала выбрать все чипы и задать им один и тот же адрес, а потом опросить один за другим через OE#. Но нам этого не нужно - читать мы будем для передачи куда-нибудь по UART, там больших скоростей не требуется. Так что пока запоминаем, что на частотах 4..9 МГц, а может и выше, вплоть до 18 МГц, чтение будет работать как надо.

Теперь глянем, что у нас с записью:


Довольно неплохо "бьётся" с логикой QuatCore: как и при чтении, адрес и CE# формируется ещё на предыдущем такте. А вот данные для записи поступают уже на текущем, но им достаточно прийти за 25 нс до начала записи. Нужно будет посмотреть Classic timing analyzer, но в целом для 4 МГц особенной проблемы быть не должно.

В общем, должно всё получиться...

Вот как новый модуль присоединяется к QuatCore:


Но пришлось ещё нарастить мультиплексор шины данных:
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


Мы ещё немножко "обворовали" селектор ввода-вывода на предмет адресов. Когда-то все адреса 100x_xxx на шине SrcAddr были отданы для АЛУ, но потом для АЛУ остались 1000_xxxx, тогда как для ввода-вывода были выделены 1001_xxxx. Теперь и этот диапазон был разделён на два: 1001_0xxx остался для ввода-вывода, а 1001_1xxx - для статической памяти.

И на DestAddr селектор ввода-вывода чувствует себя чересчур вольготно: ему принадлежали адреса 0xxx_xxx - аж 128 штук!
Половину он тоже отдал: теперь у него 00xx_xxxx (64 штуки, реально используются 2), а для статической памяти отдаётся 01xx_xxxx.

Чтобы избежать конфликтов, нужно модуль 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] DestAddr, input [7:0] SrcAddr, 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'b1;
parameter enableUART = 1'b1;
parameter enableSPI = 1'b1;

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

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

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


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

Сначала просто отправим сообщение по UART, чтобы убедиться, что "ничего не сломали":

;проверяем работу статической памяти, подключённой к ПЛИС

%include "QuatCoreConsts.inc"
%include "Win1251.inc"
.rodata
	Hello	Int16 'Привет лунатикам! (через СОЗУ)',0x0D,0x800A
.data
	Stack	dw	?,?
.code
	main proc
				SP	Stack
				SIO	UART
				X	Hello
				CALL	print
		@@endless: 	JMP 	@@endless
	main endp
	
	print proc
				[SP++]	i
				i	0
		@@start:	OUT	[X+i]
				Acc	[X+i]
				i++	0
				SUB	0
				JGE	@@start
				i	[--SP]
				JMP	[--SP]
	print endp


Подключаем к QuatCore всю-всю периферию:


Запускаем:


Фух, работает. Кроме того, производим небольшую проверку на вшивость - тыкаемся тестером в микросхемы памяти. Убеждаемся, что WE#=1 (выбрано чтение, а не запись), CE#=1 на одном из чипов и CE#=0 на другом, так и должно быть. Ну и в целом - куда надо поступает питание, куда надо общий. Адреса все нулевые, а на шине данных пока что "мусор" - где-то нули, где-то единички. Так и должно быть, ячейки СОЗУ приняли случайные значения при включении. Появляется некоторая надежда, что запаяли и отметили пины на ПЛИС правильно. Собственно, главное WE, CE, и адреса с данными не перепутать. А вот путать порядок адресных проводов и проводов шины данных вообще НЕ ВОЗБРАНЯЕТСЯ: по какому адресу записали - по тому же и считаем потом. И то же верно насчёт расположения битов в байте :) Впрочем, мы всё-таки попытались соблюсти тот порядок, что указан в даташите - если вдруг захочется туда осциллографом или тестером залезть, очень упростит жизнь!

Ну а теперь смертельный номер, изменяем программу так, чтобы она записала строку в память, а только потом считала из неё:
;проверяем работу статической памяти, подключённой к ПЛИС

%include "QuatCoreConsts.inc"
%include "Win1251.inc"
.rodata
	Hello	Int16 'Привет лунатикам! (через СОЗУ)',0x0D,0x800A
.data
	Stack	dw	?,?
.code
	main proc
				SP		Stack
				SIO		UART
				CALL		SetInitAdr
				X		Hello
				CALL		print
				
				CALL		SetInitAdr
				i		31
		@@out:		OUT		[ER++]
				iLOOP		@@out
		@@endless: 	JMP 		@@endless
	main endp
	
	print proc
				[SP++]		i
				i		0
		@@start:	[ER++]		[X+i]	;отправляем во внешнюю статическую память
				Acc		[X+i]
				i++		0
				SUB		0
				JGE		@@start
				i		[--SP]
				JMP		[--SP]
	print endp
	
	SetInitAdr proc
		ERL		0
		ERH		0
		JMP		[--SP]
	SetInitAdr endp


Запускаем, сначала с отсоединённой платой памяти. Вот что получается:


И снова чудеса КМОП-технологии: последнее отправленное на шину значение 0x0A (LF, Line Feed, "подача бумаги") там сохранилось за счёт паразитных емкостей, по крайней мере, на те 700 микросекунд, что потребовались для передачи сообщения по UART.

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


Заработало!

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


UPD. Ещё раз перечитал пост - и понял, что мне крупно повезло. В описанных модулях сигнал на запись (WE#) комбинаторно формируется из DestAddr, т.е пока у нас выполняется команда "запись в память", WE#=0. Проблема в том, что никто не даст нам гарантий, что переходе к следующей команде WE# установится в единицу быстрее, чем начнёт меняться значение на шине данных. Если это не так, мы можем записать искажённые данные. Всё-таки, согласно тайминга, правильные данные должны идти на вход до самого фронта WE#. Чтобы гарантировать это, в модуле QuatCoreSRAM вместо строки:

assign RAM_RW = ~WriteMem;


напишем:
assign RAM_RW = (~WriteMem) | clk;

По фронту тактового импульса у нас уже ОБЯЗАНЫ присутствовать правильные данные на шине. А вот выборка новой команды начнётся только после фронта тактового импульса, и к этому моменту уже теперь заведомо RAM_RW = 1.

По-прежнему работает. Теперь есть надежда, что так оно и продолжится :)
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