nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

QuatCore: багофича SPI

Около 10 месяцев назад я синтезировал схему на ПЛИС, которая получает по SPI показания АЦП, помножает на нужные константы, преобразует в двоично-десятичный код и передаёт по UART в составе строки. Там был специально разработанный SPI для конкретной микросхемки АЦП ADC124s051, умножитель беззнаковых чисел, преобразователь двоичного кода в двоично-десятичный, весьма компактный передатчик UART, и на всё про всё ушло 307 ЛЭ.

Сейчас "с новыми знаниями" реализую практически ту же задачу на QuatCore. Но теперь SPI более универсальный, аппаратного преобразователя в двоично-десятичный код нет, UART стоит, но пока не используется, добавился контроллер ЖК-экранчика и код для его правильной инициализации. А ещё управление подсветкой (минуя процессор, просто кнопочкой) и светодиодик, мигающий раз в секунду. И показания АЦП у нас слегка усредняются, по 8 отсчётам. И на всё про всё: 600 ЛЭ, при том, что данная конструкция способна "между делом" ещё и решить наши задачи технического зрения :)



(на самом деле, можно было и сильнее ужаться - ограничить ширину адресных регистров до 6 бит вместо текущих 8, UART выкинуть за ненадобностью, ширину аккумулятора с 32 бит снизить до приемлемых 22-23 бита или и того меньше, АЦП-то 12-битная и шумит прилично, но нам это не очень интересно)

Я хотел написать полудуплексный модуль SPI, специально для SD-карточек, но немножко ошибся - и он вышел дуплексным, хотя казалось, что это вообще при нашей архитектуре не очень достижимо :)

А сначала - пару слов про подсветку этих МЭЛТовских ЖК-экранчиков.


С подсветкой я тоже немножечко затупил. Вычитал в даташите, что номинальный ток 120 мА, и стал подбирать токоограничивающий резистор. Воткну один - маловато будет! Два в параллель - уже лучше, но малавато! И всё меньше и меньше ставил, боялся сжечь, а оказалось - они уже внутри стоят, 2 штуки по 39 Ом. Собственно, на задней стороне они были видны отчётливо:



Для управления впаял транзистор, смеху ради: КТ608Б, чтобы отвыкать от пластика :)


Коллектор присоединён к минусу подсветки напрямую, эмиттер - на минус питания, база через резистор 1 кОм - на выход ПЛИС. Согласно ТУ, максимально допустимый ток через вывод ПЛИС: 4 мА, а у меня получилось 2,4 мА, можно было бы резистор чуть меньше номиналом, 620..680 Ом. Ну да ладно, и при таком токе транзистор открывается неплохо, падает 305 мВ между эмиттером и коллектором, жить можно.

Теперь про SPI. Изначально я хотел, чтобы командой

OUT   [X+k]

мы передавали по SPI содержимое [X+k] и при этом игнорировали то, что нам пришлют в ответ (т.к SD-карточка нам пришлёт сплошные единицы, мы это и так знаем).

А командой
[Y+k]   IN

мы бы получили содержимое посылки, при этом передавая в ответ сплошные единицы.

Написал вроде бы осмысленный код модуля, затем некую тестовую программу, которая сначала посылает команду, а потом должна принять ответ. И обнаружил: пока идёт передача, процессор успевает дойти до команды приёма, и она "застревает", пока передача не завершится - и тут же отдаёт нам содержание сдвигового регистра. Получилось, что две эти команды "случайно" и обеспечили нам дуплексную работу!

Вот немножко подрихтованный код этого модуля:

`include "math.v"
module QuatCoreDuplexSPI (	input clk, input ce, input [15:0] D, input TXen, input RXen,
				inout MISO,
				output [15:0] Q,
				output reg SCK=1'b0, output MOSI, output nCS, output busy, output ceo);

	assign MISO = nCS? 1'b1 : 1'bz; //"неактивным" здесь считается лог. "1", на манер UART
	
	//нужно более 16 состояний (все 16 бит, а ещё ожидание
	localparam sIdle =  5'h00;
	localparam sB0 =    5'h10;
	localparam sB1 =    5'h11;
	localparam sB2 =    5'h12; //и так далее
	localparam sB3 =    5'h13;
	localparam sB4 =    5'h14;
	localparam sB5 =    5'h15;
	localparam sB6 =    5'h16; 
	localparam sB7 =    5'h17;
	localparam sB8 =    5'h18;
	localparam sB9 =    5'h19;
	localparam sBA =    5'h1A;
	localparam sBB =    5'h1B;
	localparam sBC =    5'h1C;
	localparam sBD =    5'h1D;
	localparam sBE =    5'h1E;
	localparam sBF =    5'h1F;
	
	wire [4:0] State; //вставим его отдельным модулем, чтобы задействовать режим счётчика ЛЭ

	reg [16:0] SR = 17'h1FFFF; //нужно одним битом больше, потому что пока защёлкиваем по MISO, не успеваем сдвинуть младший и затираем его нахрен
	//могли бы, учитывая полудуплекс, иногда защёлкивать, а иногда нет, но нужен регистр для определения - приём или передача - то на то и выходит.
	
	assign nCS = (~State[4]) & (~State[0]); //по сути, "isIdle". За счёт "прорехи" в 5 состояний, удаётся обойтись малой кровью

	wire NEG_E = ce & SCK; //спад SCK, когда мы переключаем состояния и сдвигаем данные в регистре
	wire POS_E = ce & (~SCK); //фронт SCK, когда мы защёлкиваем входной бит

	wire IsFinalBit; //генерируется логикой переноса (cout)
	
	wire TXbusy = TXen & (~(NEG_E & (nCS | IsFinalBit))); //т.е ждём, пока не будет спада SCK в режиме Idle или на передаче последнего бита
	wire RXbusy = RXen & (~(POS_E & (nCS | IsFinalBit))); //т.е ждём, пока не будет фронта SCK в режиме Idle или на передаче последнего бита
	assign busy = TXbusy | RXbusy;
	wire start = TXen & NEG_E & (nCS | IsFinalBit); //то есть, старт всегда по TXen, а по RXen можно получить ответ, что же там было!
				
	lpm_counter StateCounter (	.clock (clk),
			              	.cnt_en (NEG_E & (~nCS)),
			              	.sset (start), 
			              	.Q (State),																		
					.cout (IsFinalBit));
	defparam
		StateCounter.lpm_direction = "UP",
		StateCounter.lpm_port_updown = "PORT_UNUSED",
		StateCounter.lpm_type = "LPM_COUNTER",
		StateCounter.lpm_width = 5,
		StateCounter.lpm_svalue = sB0;			                    
	

	assign MOSI = SR[16];
	assign Q = {SR[15:1], MISO}; //экономит один такт, позволяет не накладывать огр 
				
	always @(posedge clk) begin
		SCK <= ce? ~SCK : SCK;
		if (NEG_E)
			SR[16:1] <= start? D: SR[15:0];
		if (POS_E)
			SR[0] <= MISO;
	end
	
	assign ceo = ce & SCK; //просто сэкономили 1 бит в делителях частоты, поскольку мы ОЧЕНЬ жадные.
endmodule


Как ни странно, он устроен проще, чем наши первые SPI-контроллеры (для АЦП, для Ethernet). Если в тех мы очень аккуратно сначала выставляли nCS в нолик, потом запускали тактовую частоту по SCK, начинали передавать биты, останавливали SCK и только потом выставляли nCS в единицу, то здесь SCK фигачит постоянно, nCS мы ставим в нолик по отрицательному фронту SCK, для большинства устройств это должно подойти, по крайней мере на какой-то тактовой частоте (Ethernet-контроллер хотел 50 нс задержку между спадом nCS и фронтом SCK, это получится при частоте не более 10 МГц, хотя контроллер поддерживает вплоть до 14 МГц). Когда nCS=1, мы даже не останавливаем сдвиговый регистр - пущай выдаёт что хочет, "никто этого не увидит".

Приведём очередную схему QuatCore, теперь уже с 3 устройствами ввода-вывода: передатчик UART, контроллер ЖК и дуплексный SPI.



Stall всё больше и больше жиреет: теперь по OR объединяется аж 4 провода. Селектор ввода-вывода QuatCoreIOselector задействовали практически полностью, не хватает только приёмника UART.

И чтобы посадить на шину выход SPI, пришлось ещё разок доработать входной мультиплексор шины данных, QuatCoreSrcMux, вот его код:

module QuatCoreSrcMux (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]?	IO:
				ALU;
                                    
always @(posedge clk)
	rQ <= combQ;
	
assign Q = (DoLatch == 1)? rQ : combQ;

endmodule


Как видно,
адреса 0xxx_xxxx (0x00..0x7F), 128 штук, отданы модулю IMM (Immediate, непосредственное значение),
адреса 11xx_xxxx (0xC0..0xFF), 64 штуки, отданы модулю MEM (Memory, адресные регистры и обращение к памяти через них),
адреса 101x_xxxx (0xA0..0xBF), 32 штуки, отданы модулю PC (Program Counter, счётчик инструкций, индексные регистры, условные и безусловные переходы),
адреса 1001_xxxx (0x90..0x9F), 16 штук, отданы на ввод-вывод, и наконец
адреса 1000_xxxx (0x80..0x8F), 16 штук, отданы модулю ALU (Arithmetic-logic unit, арифметическо-логическое устройство).

Забавное такое деревце.

Модуль QuatCoreSchemMem (управление памятью, реализованное в виде схемы, а не верилоговского модуля) был заменён на QuatCoreSchemMemV2, он больше на 16 ЛЭ, но позволяет всё-таки заносить адресные регистры в память, в первую очередь в стек.


Далее, нам нужна тестовая программа, которая будет обращаться по SPI к АЦП, немножко обрабатывать данные и показывать их на ЖК-экранчике. Вот она:

;учимся возиться с 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-й канал)
	BCDtable	dw	1,10,100,1000,10000	
	Volts		dw	' ','В',f' '
.data
	Stack		dw	?,?,?,?,?	;ладно, тут уже получается тяжело без стека. Адрес возврата, сохранённый рег. j,Y и 2 локальные переменные!
.code
	main proc
				SP		Stack
				SIO		LCD
				X		InitLCD
				CALL		print
				;экран инициализировали
				i		4	;положение десятичной запятой, меняться не будет!
				Z		AdcAddr
				Y		AdcConsts
				;теперь начинается цикл опроса АЦП
		@@MainLoop:	k		1	;два канала опрашиваем
		@@k_loop:	SIO		SPI
				OUT		[Z+k]	;задаём в АЦП номер канала
				j		7
				Acc		0
		@@ave:		OUT		[Z+k]	;снова номер канала, но теперь точно знаем, что ответ какой надо
				C		IN	;получаем соотв. значение
				ADD		C
				jLOOP		@@ave
				
				C		UAC
				;но сначала печатаем правильную строчку
				X		Rows
				X		[X+k]
				SIO		LCD
				CALL		print
				;а вот теперь умножаем, иначе у нас Acc затирался!
				MULU		[Y+k]	;помножаем на нужную константу
				;показываем нужную строку
				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]
				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


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

Процедура print у нас "старая добрая", способная вывести до 32 символов, сейчас больше не требуется. (если потребуется, можно взять более кощунственную реализацию отсюда)

И смотрим теперь на основной код программы. Первым делом инициализируем стек, это как зубы с утра почистить. Потом инициализируем ЖК-экранчик, в этот раз строка инициализации очень короткая, т.к мы не вводим свои, пользовательские символы.

Сразу же задаём положение десятичной запятой, она для обеих чисел стоит в одном месте, "одна цифра перед запятой". И также заносим в адресные регистры адрес констант, на которые надо домножить полученное из АЦП число, и адрес, где лежит два числа для отправки по SPI для выбора первого и второго канала АЦП, соответственно.

Весь последующий код зациклен и работает "до скончания времён", то бишь, пока не вырубим питание. Каждый раз мы делаем цикл с k=1..0, k-это номер канала.

Выбираем устройство ввода-вывода: SPI (командой SIO, Select I/O), и отправляем туда команду по выбору канала АЦП. Уже сейчас АЦП пришлёт ответ с оцифрованным ПРЕДЫДУЩИМ каналом, но в данном случае мы тупо проигнорируем это значение. Обнулим аккумулятор и 8 раз опросим АЦП, каждый новый отсчёт прибавляя к аккумулятору. Тут уже видно, как правильно работать "в дуплексе" - мы сначала запускаем передачу, а затем - приём, и тогда процессор как раз остановится у самого окончания посылки, чтобы поместить нам в регистр C полученное значение.

Пока мы не можем написать напрямую:

ADD   IN

(прибавить значение, полученное со входа)
поскольку АЛУ пока что не имеет входа stall, который заставил бы его подождать, пока данные на входе будут правильными - и только тогда начинать работу. В принципе, можно и добавить, но пока лень, всё-таки вот так "с нахрапу" производить арифметические действия с только что поступившими данными - это редкость :)

Затем получившуюся сумму мы "скидываем" в регистр C, а сами начинаем выводить строку, либо "Шина 5 В: ", либо "Шина 1,8 В: ". Чтобы вывести нужную, мы применяем косвенную адресацию, "многоходовочку":

				X		Rows
				X		[X+k]

По адресу Rows как раз лежит два адреса соответствующих строк. Наконец-то наш модуль QuatCoreMem не запрещает таких вещей :)

Не забываем переключить селектор с SPI на ЖК, выводим строку.

Затем умножаем значение с АЦП (накопленное за 8 отсчётов) на константу, чтобы получить ответ в вольтах. Из-за этой строки,

MULU		[Y+k]


я целый день потерял!

Главным виновником была строка
[SP++]   Y

которая заносила адрес стека, 0x2E, вместо регистра Y.
Но сказывалось всё во время умножения: вместо констант 30695,55298 нам подсовывали какую-то мелочь пузатую, домножение на которую давало НОЛЬ. То есть, программа В ОБЩЕМ И ЦЕЛОМ РАБОТАЕТ - всё выводит, что-то опрашивает, вот только ДАННЫЕ НУЛЕВЫЕ. И учитывая, что последний раз я возился с этим АЦП 10 месяцев назад, а сейчас запустил его через новый модуль SPI, то все мои подозрения падали именно на этот участок. Я проверял, те ли выбрал выводы ПЛИС, уже даже расчехлил осциллограф и стал наблюдать информационный обмен по SPI. И обнаружил, что там всё правильно - запрашиваем мы что надо, и ответ приходит какой надо, вот только до экрана почему-то не доходит...

А всё потому, что мне лень было добавить устройства ввода-вывода в свой эмулятор, и ещё больше лень ждать на симуляции, когда же он инициализирует ЖК-экранчик и возьмётся наконец за АЦП. Думал, сейчас с нахрапу прямо "в железе" всё отлажу. Ну ладно, всё хорошо, что хорошо кончается...

Дальше мы выводим на экран получившиеся напряжения в десятичном виде, добавляем в конце пробел и буковку "В" (вольт), и остаётся только сделать задержку на полсекунды.

Вот код, который обеспечивает такую задержку:
			Acc		63
			C		4
		@@wait:	FMS		4
			JGE		@@wait

Здесь мы заносим в аккумулятор значение 63, которое командами умножения интерпретируется как 63/32768.
В регистр C заносим значение 4, то бишь 4/32768.
И наконец, внутри цикла начинаем вычитать из аккумулятора (4/32768)2 с помощью одной из команд "умножения с накоплением", Fused Multiply-Subtract. При этом задействуется весь 32-битный аккумулятор, что нам и надо. Да и сама команда выполняется неторопливо, свыше 16 тактов, так что для задержки самое то что надо.

Всего происходит 129024 итерации, каждая занимает 19 тактов, что при частоте 4 МГц даёт задержку в 613 мс.
Выбирая разные значения для команды умножения и разное начальное значение аккумулятора, мы можем варьировать задержку в ОЧЕНЬ ШИРОКИХ ПРЕДЕЛАХ, от микросекунд до пары часов!

Вот видео работы этого безобразия "в железе":


В отладочной плате предполагается несколько вариантов подачи 5 вольт - от MicroUSB или от нескольких других входов, и каждый подключён через свой диод. Из-за этого, а также из-за падения напряжения на довольно длинном (и не очень качественном) кабеле USB, у нас вместо 5 вольт выходит примерно 4,3 вольта.

Далее мы видим, как включение подсветки просаживает ещё примерно на 100 мВ, а если закоротить входной диод пинцетом - напряжение повышается до 4,7 вольт.

Всё это время напряжение ядра, +1,8 вольта, стоит "как вкопанное".

И посмотрим всё-таки на симуляции, как именно работает модуль SPI в составе QuatCore. Подключим к нему дурацкий модуль:

module SPI_dummy_slave (input MOSI, inout MISO, input nCS);

assign MISO = nCS? 1'bz : MOSI;

endmodule


Просто соединить провода MOSI и MISO оказалось нельзя - при синтезе ругается на несколько выходов, соединённых вместе. А так всё равно при симуляции указывает на logic contention, но результаты даёт правильные. Куда именно воткнуть этот модуль - указано на схеме QuatCore, зелёным справа снизу.

В программе мы закомментируем первый вызов процедуры print для инициализации ЖК-экрана, чтобы не ждать свыше 1,5 мс, когда он очищает экран. Вот её листинг:

    main proc
00  FD2E                  SP      Stack
01  4001                  SIO     LCD
02  CD00                  X       InitLCD
03  A004                  i       4   ;положение десятичной запятой, меняться не будет!
04  ED24                  Z       AdcAddr
05  DD22                  Y       AdcConsts
06  A201          @@MainLoop: k       1   ;два канала опрашиваем
07  4002          @@k_loop:   SIO     SPI
08  00E8                  OUT     [Z+k]   ;задаём в АЦП номер канала
09  A107                  j       7
0A  8000                  Acc     0
0B  00E8          @@ave:  OUT     [Z+k]   ;снова номер канала, но теперь точно знаем, что ответ какой надо
0C  8A90                  C       IN  ;получаем соотв. значение
0D  8283                  ADD     C
0E  A97D                  jLOOP       @@ave
0F  8A82                  C       UAC
10  CD20                  X       Rows
11  CDC8                  X       [X+k]
12  4001                  SIO     LCD
13  F3B0                  CALL        print
14  98D8                  MULU        [Y+k]   ;помножаем на нужную константу
15  F3B1                  CALL        UnsignedFixed
16  CD2B                  X       Volts
17  F3B0                  CALL        print
18  AA6F                  kLOOP       @@k_loop
19  803F                  Acc     63
1A  8A04                  C       4
1B  9304          @@wait: FMS     4
1C  B17F                  JGE     @@wait
1D  B806                  JMP     @@MainLoop
    main endp
    print proc
1E  F3A0                  [SP++]  i
1F  A000                  i       0
20  00C4          @@start:    OUT     [X+i]   ;хитрая проверка на -32768
21  80C4                  Acc     [X+i]
22  A400                  i++     0
23  8300                  SUB     0
24  B17C                  JGE     @@start
25  A0FF                  i       [--SP]
26  B8FF                  JMP     [--SP]
    print endp
    UnsignedFixed proc
27  F3A2                  [SP++]  k
28  F3DD                  [SP++]  Y
29  A204                  k       4   ;номер обрабатываемого разряда
2A  F07F                  [SP+1]  -1  ;-1 означает, что значащих цифр ещё не было, 0-что появились
2B  DD26                  Y       BCDtable                
2C  AF02      @@start:        JNik        @@skipP0    ;уже здесь нужно проверить на запятую, поскольку её наличие заставит поставить нолик!
2D  F000                  [SP+1]  0           ;указали на значимость :)
2E  A1F0      @@skipP0:       j       [SP+1]
2F  83D8      @@sub:      SUB     [Y+k]
30  A500                  j++     0
31  B17E                  JGE     @@sub
32  82D8                  ADD     [Y+k]
33  A903                  jLOOP       @@proceed   ;прыжка не будет, если [SP+1]=-1 и текущий разряд нулевой
34  AA78                  kloop       @@start
35  F000                  [SP+1]  0
36  FC80      @@proceed:      [SP]        Acc     ;сейчас аккумулятор пригодится...
37  8030                  Acc     '0'
38  82A1                  ADD     j
39  83F0                  SUB     [SP+1]
3A  0080                  OUT     Acc
3B  F000                  [SP+1]  0
3C  80FC                  Acc     [SP]
3D  AF02                  JNik        @@finish
3E  002C                  OUT     ','
3F  AA6D      @@finish:       kloop       @@start 
40  DDFF                  Y       [--SP]
41  A2FF                  k       [--SP]
42  B8FF                  JMP     [--SP]
    UnsignedFixed endp  


Ну и "осциллограмма":


Тут интересна команда OUT. Первый раз, когда она вызывается, процессор "застревает" на один такт, а именно ждёт спада по SCK, чтобы именно по нему выставить nCS в ноль и начать передачу. Можно было бы избежать такой задержки ценой усложнения модуля, но не хочется. Так мы, по сути, отрабатываем ожидание передачи предыдущей посылки единообразно с ожиданием спада SCK. А дальше, как только передача началась, процессор "отпускает", и он двигается дальше.

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

Посмотрим повнимательнее:


Как и положено, отсчитывается 16 фронтов SCK. Мы заносим значения в MOSI на "полтакта" раньше, чтобы дать им время распространиться до входа и обустроиться там со всеми удобствами.

Когда за командой OUT следует ещё одна команда OUT, передача продолжается "бесшовно". Как и при первом запуске, процессор дожидается спада SCK, и только вслед за этим "трогается с места" и попадает на команду
C   IN


И снова мы видим остановку, пока не окончится передача 16 бит. На шину данных в данный момент скоммутирован именно выход модуля SPI, и мы видим, как там формируется значение. Поскольку наше "устройство SPI" попросту пересылает MOSI в MISO, то при корректной работе модуля мы должны получить ровно то же значение, которое мы отправили. Так оно и есть.

И ещё один интересный момент: сейчас мы "трогаемся с места" одним тактом ранее, не по спаду SCK, а ПО ФРОНТУ. Именно в этот момент мы считываем последний бит, причём подаём его напрямую на шину данных, не дожидаясь "защёлкивания" в сдвиговый регистр.

Это даёт нам время "бесшовно" начать передачу следующей посылки, если сразу вслед за командой IN последует команда OUT.
Сейчас мы написали программу немного неправильно - она осуществляет прыжок после команды IN, поэтому "момент упущен" - nCS уже уходит в единицу, правда, ненадолго. Для АЦП это совершенно всё равно, штука всеядная.

А вот если нам нельзя прерывать длинную передачу (зачастую тогда нас "не поймут"), то лучше начинать цикл с IN, затем ТУТ ЖЕ за ним OUT, а вот потом спокойно можно делать свои дела!


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

Recent Posts from This Journal

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

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

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

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: 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 

  • 0 comments