nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

QuatCore: SPI на 3 устройства

Очень странной траекторией идёт разработка. Что нужно в первую очередь, чтобы оцифровать видеосигнал? Разумеется, обратиться по SPI к Ethernet-контроллеру!


Дело в том, что на моей отладочной плате для 5576ХС4Т на ПЛИС заведено два источника тактовой частоты. Первый - отдельный генератор на 80 МГц, но такая частота для данной ПЛИС изначально была великовата, как по мне. Все схемы должны иметь один "комбинаторный слой", т.е выход регистра присоединяется к входу другого логического элемента, проходит через его ГФ (LUT) и СРАЗУ ЖЕ защёлкивается в регистре этого же элемента. Кое-чего получается и на такой частоте, но всё-таки это издевательство.

Второй источник - это выход CLKOut с Ethernet-контроллера ENC624J600. При подаче питания на контроллер, он начинает выдавать на этот выход 4 МГц, но сконфигурировав поле COCON (Clock Out CONfiguration) в регистре ECON2, можно задать другие тактовые частоты от 50 кГц до 33,33 МГц, или совсем отключить этот выход для энергосбережения.

ФАПЧ (PLL) в нашей ПЛИС отсутствует, простое деление частоты сделать можно, но не рекомендуется, т.к вывести обычный ЛЭ на Dedicated (специально предназначенную) шину для тактовых частот, судя по всему, невозможно, а делать разводку тактовой частоты по обычным соединителям выходит очень ущербно - они не предназначены для столь большой нагрузки (fan-out), и, как результат, маленькие схемки могут и заработать, но в один прекрасный момент оно просто не отсинтезируется - и делай что хочешь!

До сих пор мы эксплуатировали свой QuatCore именно на 4 МГц. Более простые схемы мы вовсю мучали на 80 МГц. Но сейчас мне больше всего хотелось бы раздобыть 33,33 МГц - это вполне подходящая частота для оцифровки нашего аналогового видео.

В принципе, эту задачу мы уже один раз выполнили, безо всяких процессоров, на 64 ЛЭ, что может показаться очень хорошо. Хорошо-то хорошо, но там мы задействовали отдельный Блок Внутренней Памяти (БВП), чтобы сохранить последовательность аж из 15 байт, но по сути это значит, что отхватили все 512 байт, поскольку порты же мы заняли!

Поэтому интереснее может быть другое - от одного процессора и от одного SPI-контроллера в его составе, который мы испытывали и для медленной АЦП (500 киловыборок в секунду - явно не для видео!), и для SD-карточки, запитать ДО КУЧИ ещё и Ethernet, и научиться выбирать устройство, с которым мы работаем.

И это на удивление простая модификация, ведь SPI изначально на это рассчитывался. SCK (Serial ClocK) поступает просто на все устройства, а вместе с ним MOSI (Master Out Slave In). С MISO (Master In Slave Out) "в недрах ПЛИС" чуточку интереснее. Если бы мы подключили все устройства на одну ногу, то никаких проблем. Сама нога будет объявлена как двунаправленная, т.е когда ни одно устройство не выбрано, то мы сами будем притягивать эту ногу на 3,3 вольта. И в любой момент времени мы будем выбирать только одно из устройств - и именно оно будет работать на эту шину.

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

Если мы просто "на схеме" (в Block Diagram) или в верилоге соединим между собой несколько двунаправленных (Bidirectional) выходов, то компилятор будет грязно ругаться - он не может понять, в какой момент какая из этих ног должна становиться входной, а какая-выходной.

Так что надо всё обозначить "ручками", написав модуль на верилоге:

//нужен, когда входы MISO для разных девайсов сидят на РАЗНЫХ НОЖКАХ ПЛИС
//просто соединить их друг с другом нельзя - компилятор не поймёт, какая из них должна активироваться
//так что сделаем свой собственный мультиплексор

//Device 	=	0x: ETH
//		=	10: SD
//		=	11: ADC
module SPI_Mux3 (input nCS, input [1:0] Device, output MISO_MUX, output SD_nCS, output ADC_nCS, output ETH_nCS,
		 inout SD_MISO, inout ADC_MISO, inout ETH_MISO);

assign SD_nCS = nCS | ~(Device == 2'b10);
assign ADC_nCS = nCS | ~(Device == 2'b11);
assign ETH_nCS = nCS | Device[1];

assign SD_MISO = SD_nCS? 1'b1 : 1'bz;
assign ADC_MISO = ADC_nCS? 1'b1 : 1'bz;
assign ETH_MISO = ETH_nCS? 1'b1 : 1'bz;

assign MISO_MUX = 	~Device[1]?	ETH_MISO :
			Device[0]?	ADC_MISO :
					SD_MISO;

endmodule


Этот же модуль правильно управляет выходами nCS (negative Chip Select) - пока интерфейс не активен, на всех выходах единицы. А если мы запускаем обмен, то на одно, выбранное устройство мы подаём нолик, а на оставшиеся - по-прежнему единички.

Дополним "селектор ввода-вывода". До сих пор мы давали ему 2 бита, в которых закодировано одно из 3 устройств, которое в данный момент управляется командами IN и OUT: это могут быть UART, ЖК-экранчик или контроллер SPI.

Теперь мы задействуем следующие 2 бита, чтобы выбрать одно из 3 устройств на шине SPI, к которому собираемся обращаться. Вот его код на верилоге:

//DestAddr==00xx_xxxx : 'OUT'
//DestAddr==01xx_xxxx : select IO device (SIO)

//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_xxxx
//(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];
wire isIO_out = (~DestAddr[7])&(~DestAddr[6]);
wire isIO_in = (SrcAddr[7:4] == 4'b1001);

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


Тут есть одна хитрость: мы ввели не только 2-битный регистр SPIdevice, который напрямую подаётся на мультиплексаор, но и 2-битный shadowSPI. Именно он "защёлкивается" при подаче команды SIO (Select I/O), а SPIdevice "защёлкивается" уже от него, но только когда приёмопередатчик SPI не занят. Это сделано, поскольку приёмопередатчик "возвращает управление" процессору, как только получит новый байт для передачи, и если это был последний байт, предназначенный одному устройству, а дальше по программе мы переключаемся на другое, то нам не хочется "запороть" тот последний байт, на лету выбрав другое устройство на шине SPI. Вполне стандартное решение.

Так всё это выглядит "в сборе" в QuatCore:


Приёмопередатчик SPI тоже пришлось чуть-чуть видоизменить, поменяв ему MISO с двунаправленного вывода, замыкаемого на "1" при nCS=1, в обычный вход. Иначе при синтезе выскакивала ошибка, что этот вывод пытаются "изменить" с нескольких мест. А так всё в порядке, и чуть понятнее, что происходит.

Наконец, посмотрим, как выглядит QuatCore на "общей схеме", где к нему подключены уже реальные ножки ПЛИС:


Вся эта конструкция "в сборе", вместе с ШИМ-контроллером для управления "джойстиком" камеры наблюдения (об этом чуть позже), занимает 597 ЛЭ - растёт, но пока в пределах разумного.

Чтобы теперь можно было использовать в своих программах все эти устройства, транслятор трогать не нужно, как это ни странно. Достаточно дополнить файл QuatCoreConsts.inc, который мы теперь подключаем к своим ассемблерным программам. Сейчас он выглядит так:

;константы для аргументов QuatCore
;можно было бы их в сам компилятор "зашить", но пущай так
.data ;костыль нашего компилятора - он ищет EQU только в сегменте данных

;устройства ввода-вывода. Выбор производится командой SIO
UART	EQU	0
LCD	EQU	1
ETH	EQU	2
SD	EQU	10
ADC	EQU	14

;константы, которыми можно инициализировать аккумулятор, командой ZAcc
MinusThreeHalf	EQU	0
MinusOne	EQU	1
ThreeHalf	EQU	2
RoundZero	EQU	3


Теперь самое время опробовать несколько из ранее написанных программ, чтобы убедиться, что ничего не поломал. Начнём с HelloADC.asm, на которой мы проверяли работу "медленного" АЦП по SPI и вывод показаний на экран в "человеческом" виде. Всё, что нужно поменять - вместо
SIO SPI

написать
SIO ADC


А потом запустить, получить опять абсурдно маленькие значения - и вспомнить, что для работы с АЦП мы использовали другой модуль SPI, выдающий 16 бит за раз, а сейчас у нас 8-битный модуль. С ним тоже можно работать, но сложнее. В итоге, я пришёл к такой программе:

;учимся возиться с SPI. Пока пробуем свой модуль на уже испытанном АЦП
;хотим получить отсчёт напряжения, помножить на что надо
;и вывести на экранчик
;так что нужна инициализация (но без лишних символов наверное) экрана,
;умножение и отображение беззнак числа с фикс. запятой
%include "QuatCoreConsts.inc"
%include "LCD_code_page_1.inc"
.rodata
;адр 0
	InitLCD		dw	0x330		;установить разрядность интерфейса (одиночная посылка, команда, 0011)
	Init1		dw	0x330		;установить разрядность
	Init2		dw	0x330		;установить разрядность
	Init3		dw	0x320		;установить разрядность 4 бита
	Init4		dw	0x12A		;установка параметров (двойная посылка, команда, 0010 1010 = 4 бита, страница 1)
	Init5		dw	0x10C		;включение дисплея (двойная посылка, команда, 0000 1100 = включить, курсора нет, ничего не мигает)
	Init6		dw	0x101 	;очистка дисплея (двойная посылка, команда, 0000 0001)
	Init7		dw	0x8106	;установка режима ввода данных (двойная посылка, команда, 0000 0110 = курсор вправо, дисплей не двигается)
	
	Row0		dw	0x180,'Ш','и','н','а',' ','5',' ','В',':',f' '
	Row1		dw	0x1C0,'Ш','и','н','а',' ','1',',','8',' ','В',':',f' '
	Rows		dw	Row1,Row0	;выбрать адрес строки для данного канала
	AdcConsts	dw	30695,55298	;первый-для +1,8 вольт, второй - для +5 вольт
	;AdcAddr	dw	0x1000,0x0800	;первый - для +1,8 вольт (2-й канал), второй - для +5 вольт (1-й канал)
	AdcAddr		dw	0x0010,0x0008	;для 8-битного SPI.
	BCDtable	dw	1,10,100,1000,10000	
	Volts		dw	' ','В',f' '
	Senior		dw	0x0000,0x0100,0x0200,0x0300,0x0400,0x0500,0x0600,0x0700,0x0800,0x0900,0x0A00,0x0B00,0x0C00,0x0D00,0x0E00,0x0F00
	Senior1		dw	0x0000,0x0100,0x0200,0x0300,0x0400,0x0500,0x0600,0x0700,0x0800,0x0900,0x0A00,0x0B00,0x0C00,0x0D00,0x0E00,0x0F00
.data
	Stack		dw	?,?,?,?,?	;ладно, тут уже получается тяжело без стека. Адрес возврата, сохранённый рег. j,Y и 2 локальные переменные!
.code
	main proc
				SP		Stack
				SIO		LCD
				X		InitLCD
				CALL		print
				;экран инициализировали
				Z		AdcAddr
				;теперь начинается цикл опроса АЦП
		@@MainLoop:	k		1	;два канала опрашиваем
		@@k_loop:	SIO		ADC
				OUT		[Z+k]	;задаём в АЦП номер канала
				j		7
				Acc		0
				X		Senior
		@@ave:		OUT		[Z+k]	;снова номер канала, но теперь точно знаем, что ответ какой надо
				i		IN	;получаем соотв. значение. Если SPI 8-битный, то старшие 4 разряда только... 
				OUT		0	;пофиг что, лишь бы отправить
				Y		IN	;8-битная, съест ненужные единицы
				ADD		Y	;прибавили младшие 8 разрядов
				ADD		[X+i]	;прибавили старшие разряды - целых 32 байта потеряли, ну да ладно...
				jLOOP		@@ave
				

				C		UAC
				;но сначала печатаем правильную строчку
				X		Rows
				X		[X+k]
				SIO		LCD
				CALL		print
				;а вот теперь умножаем, иначе у нас Acc затирался!
				Y		AdcConsts				
				MULU		[Y+k]	;помножаем на нужную константу
				;показываем нужную строку
				i		4	;положение десятичной запятой, меняться не будет!				
				CALL		UnsignedFixed
				X		Volts
				CALL		print
				kLOOP		@@k_loop
				;а теперь бы сделать паузу
				Acc		63
				C		4
		@@wait:		FMS		4
				JGE		@@wait
				JMP		@@MainLoop
	main endp
	
	;X указывает на начало строки текста
	;конец обозначается отрицательным числом (у нас НЕТ ФЛАГА НУЛЯ!!!)
	;меняет значение регистра Acc, остальные сохраняет как есть
	print proc
				[SP++]		i
				i		0
		@@start:	OUT		[X+i]	;хитрая проверка на -32768
				Acc		[X+i]
				i++		0
				SUB		0
				JGE		@@start
				i		[--SP]
				JMP		[--SP]
	print endp
	
	
	;отобразить число без знака из ACC, причём поставить запятую в позицию, заданную в регистре i.
	;меняет регистры j,Acc
	;(k и Y мы сохраняем в стек, чтобы вернуть как было)
	;после выполнения k=0
	UnsignedFixed proc
				[SP++]		k
				[SP++]		Y
				k		4	;номер обрабатываемого разряда
				[SP+1]		-1	;-1 означает, что значащих цифр ещё не было, 0-что появились
				Y		BCDtable				
	@@start:		JNik		@@skipP0	;уже здесь нужно проверить на запятую, поскольку её наличие заставит поставить нолик!
				[SP+1]		0			;указали на значимость :)
	@@skipP0:		j		[SP+1]
	@@sub:			SUB		[Y+k]
				j++		0
				JGE		@@sub
				ADD		[Y+k]
				jLOOP		@@proceed	;прыжка не будет, если [SP+1]=-1 и текущий разряд нулевой
				kloop		@@start
				[SP+1]		0
	@@proceed:		[SP]		Acc		;сейчас аккумулятор пригодится...
				Acc		'0'
				ADD		j
				SUB		[SP+1]
				OUT		Acc
				[SP+1]		0
				Acc		[SP]
				JNik		@@finish
				OUT		','
	@@finish:		kloop		@@start	
				Y		[--SP]
				k		[--SP]
				JMP		[--SP]
	UnsignedFixed endp	


Здесь, мы сначала прочитываем старшие 4 бита результата, и заносим их в 5-битный регистр i. Затем прочитываем младшие 8 бит результата и заносим в 8-битный регистр Y.

Чтобы сдвинуть i на 8 позиций влево, я не нашёл ничего лучше, как ввести таблицу Senior[] в области данных, и обратиться к ней через [X+i]. Там лежат сдвинутые значения. Поначалу у меня какой-то бред получался, и "на всякий случай" я её продублировал, если вдруг в старшем бите i приходит какой-то мусор.

Использование 8-битного регистра Y тоже было необходимо, потому что для удобства обработки ответов SD мы сделали расширение знака тех 8 бит, что приходят по SPI, а здесь нам это расширение как раз очень вредит! Таким вот способом мы от него избавляемся.

В итоге программа заработала, хотя я очень удивился, насколько "прыгают" значения питающих напряжений. Но похоже, так оно и есть - эта программа снимает усреднённое значение за 8 отсчётов, сделанных в течение 64 мкс, а потом выжидает около секунды, прежде чем сделать следующие измерения. Хотя есть ещё одна проблема: данная АЦП просит на входе от 3,2 МГц до 8 МГц, а я подаю всего лишь 2 МГц. Она производит оцифровку с помощью переключаемого конденсатора - как-то хитро там заряды перераспределяются - возможно на уменьшенной частоте там заряды уползти успевают... Ну ладно, по крайней мере чего-то работает.

Опять на отладку ушло больше времени, чем надо. Продолжение (с обращением к Ethernet-контроллеру) следует...
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • Быстрее, меньше, точнее!

    Начинаем тестировать новую процедуру арктангенса. Сначала на симуляции. Получаем дамп памяти: Переписываем числа в таблицу: Первая половина…

  • atan1 "на четырёх умножениях" на ассемблере

    Немножко подправил таблицу результатов для первого алгоритма. В этот раз вместо "правильных углов" 0°, 12°, 24° и т.д (те, которые я ХОТЕЛ взять для…

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

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

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments