nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Мучаем 5576ХС4Т - часть 'h38 - передатчик байтов SPI

[Оглавление (ссылки на остальные части)]Часть 0 - покупаем, паяем, ставим драйвера и софт
Часть 1 - что это вообще за зверь?
Часть 2 - наша первая схема!
Часть 3 - кнопочки и лампочки
Часть 4 - делитель частоты
Часть 5 - подавление дребезга кнопки
Часть 6 - заканчиваем кнопочки и лампочки
Часть 7 - счетчики и жаба
Часть 8 - передатчик UART
Часть 9 - Hello, wolf!
Часть 'hA - приёмник UART
Часть 'hB - UART и жаба
Часть 'hC - полудуплексный UART.
Часть 'hD - МКО (МКИО, Mil-Std 1553) для бедных, введение.
Часть 'hE - приёмопередатчик МКО "из подручных материалов" (в процессе)
Часть 'hF - модуль передатчика МКО
Часть 'h10 - передатчик сообщений МКО
Часть 'h20 - работа с АЦП ADC124s051
Часть 'h21 - преобразование двоичного кода в двоично-десятичный (BCD)
Часть 'h22 - Bin2Bcd с последовательной выдачей данных
Часть 'h23 - перемножитель беззнаковых чисел с округлением
Часть 'h24 - перемножитель беззнаковых чисел, реализация
Часть 'h25 - передаём показания АЦП на компьютер
Часть 'h26 - работа над ошибками (быстрый UART)
Часть 'h27 - PNG и коды коррекции ошибок CRC32
Часть 'h28 - передатчик изображения PNG
Часть 'h29 - принимаем с ПЛИС изображение PNG
Часть 'h2A - ZLIB и коды коррекции ошибок Adler32
Часть 'h2B - ускоряем Adler32
Часть 'h2C - формирователь потока Zlib
Часть 'h2D - передаём сгенерированное PNG-изображение
Часть 'h2E - делим отрезок на равные части
Часть 'h2F - знаковые умножители, тысячи их!
Часть 'h30 - вычислитель множества Мандельброта
Часть 'h31 - ускоренные сумматоры
Часть 'h32 - ускоренные счётчики (делаем часы)
Часть 'h33 - ускоряем ВСЁ
Часть 'h34 - ускоренные перемножители
Часть 'h35 - умножители совсем просто
Часть 'h36 - уравновешенный четверичный умножитель
Часть 'h37 - ускоренные счётчики, работа над ошибками
Часть 'h38 - передатчик байтов SPI


Последние 8 частей были посвящены "ускорению" всевозможных модулей, чтобы они могли работать на тактовой частоте 80 МГц на нашей ПЛИС, которая эквивалентна кристаллу EPF10K200SRC240-3. Мне казалось, что ещё чуть-чуть - и у нас будет набор быстрых "кубиков", собирая которые, мы будем получать столь же быстрые схемы, и проблема уйдёт раз и навсегда.

Увы, проблема и не думала уходить - когда мы худо-бедно разобрались, как реализовывать "быстрые" счётчики (поскольку библиотечный lpm_counter "сдувается" довольно быстро, особенно, если считать надо не по степеням двойки), быстрые многоразрядные сумматоры (lpm_add_sub тоже не блещет) и прочие вычислители и передатчики, обнаружилось, что встроенная память LPM_RAM_DP+ (Library of Parametrized Modules - Random Access Memory - Dual Port 'улучшенная'), когда выбираешь большой объём, 4-8 килобайт (т.е львиную долю того, что есть на кристалле), тоже начинает нарушать тайминги совершенно "на ровном месте" - вроде выход памяти напрямую подключил к регистру, а всё равно сигнал дойти не успевает!

Мы знаем, что реально на кристалле расположено 24 блока памяти по 512 байт каждый, а когда мы просим дать блок памяти в 4 килобайта, задействуется 8 блоков, причём их выходы должны мультиплексироваться (как это ни удивительно, но внутри ПЛИС выходы с третьим высокоимпедансным "Z" состоянием отсутствуют как класс, не считая некоторых совсем древних ПЛИС), а старшие биты адреса - декодироваться, чтобы подать WR_EN (разрешение записи) только на один, нужный нам блок.

Видать, на этой логике и возникают дополнительные задержки, которых не было, пока мы обходились 128 байтами (как в передатчике показаний АЦП)!

Наверное, и здесь можно проявить инициативу - создать свой собственный модуль памяти на 8 килобайт (или даже 12 килобайт - вся, что есть!), в котором будет тот же самый мультиплексор, и тот же самый дешифратор, но с дополнительными "защёлками", чтобы все тайминги были выдержаны. Да, время доступа увеличится с 2 тактов до 3-4 тактов, но мы всё ещё сможем записывать одно значение каждый такт или считывать по одному значению каждый такт (т.е стоит запустить конвейер, и нам становится пофиг, что ступеней много, результат мы получаем НА КАЖДОМ ШАГУ), что для работы с изображениями очень важно.

Но честно говоря, этот overclocking уже порядком подзадолбал, поэтому сейчас мы научимся хоть самую малость управлять Ethernet-контроллером ENC624J600. А именно, попросим его перейти в режим пониженного потребления, а также выдать нам тактовую частоту 33,3 МГц.

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




На нашей отладочной плате ПЛИС подключена к Ethernet-контроллеру через SPI:
- ETH_MISO - Master In Slave Out - вход для ПЛИС, выход для Ethernet
- ETH_MOSI - Master Out Slave In - выход для ПЛИС, вход для Ethernet
- ETH_SCLK - Serial CLocK - сюда мы подаём тактовую частоту с ПЛИС (в документации на контроллер просят от 0 до 14 МГц)
- ETH_nCS - negative Chip Select - здесь мы должны держать единичку, и только когда захотим "поговорить" с контроллером - выставлять нолик, затем запускать тактовую частоту, передать и принять всё, что надо, и снова выставить "единичку".

Есть и ещё два провода:
- ETH_nINT - negative INTerrupt - по этому проводу контроллер может давать нам прерывания. Они нам пока не нужны
- ETH_CLKout - CLocK out - сюда контроллер Ethernet подаёт тактовую частоту. При подаче питания это 4 МГц, но затем мы можем по SPI настроить встроенный делитель частоты, и либо совсем отключить этот выход, либо задать тактовую частоту от 50 кГц до 33,3 МГц.

Последний провод приходит нам на "специально обученный" вход тактовой частоты (Dedicated clock input), что очень хорошо: с этого входа он может распространяться на огромное количество элементов на всём кристалле, с минимальными задержками.

Как именно управлять этим контроллером по SPI, очень подробно описано в Datasheet на него. Интерфейс довольно-таки мудрёный: есть команды, занимающие 1 байт, 2 байта, 3 байта и произвольное число байт. В первых 3 случаях мы можем передавать команду за командой, удерживая nCS на нуле, т.е контроллер понимает, что одна команда закончилась, и начинается следующая. Но в случае команды на произвольное число байт, сначала идёт код команды, а затем данные, и когда данные заканчиваются, мы должны выставить nCS в единицу, чтобы контроллер понял, что команда окончилась. Затем снова устанавливаем в нолик - и даём следующую команду.

В первую очередь, нам нужен байтовый передатчик SPI, вот его заголовок:

module Better_SPI (input clk, input ce, input [7:0] D, input start,
                   inout MISO,
		   output SCK, output MOSI, output nCS, output FinalBit, output Finished);


clk - clock, тактовая частота,

ce - clock enable, сюда приходят импульсы длительностью в 1 период clk, и с частотой, равной удвоенной частоте SPI, на которой мы хотим работать. Мы могли бы генератор этих импульсов "упрятать" внутрь, но как-то "исторически" сложилось, что они генерируются снаружи, и используются и для других вещей.

D - байт входных данных

start - сюда подаём импульс, чтобы запустить передачу очередного байта

SCK - Serial ClocK - здесь во время передачи байта появится меандр. По отрицательному фронту мы будем "защёлкивать" новый бит на выходе MOSI, а по положительному фронту его будет "защёлкивать" к себе Ethernet-контроллер.

MISO - Master In Slave Out - вход нашего пока что отсутствующего приёмника. Этот провод отмечен как inout (вход/выход), поскольку когда nCS=1, выход Ethernet-контроллера, который должен подавать на этот провод данные, переходит в Z-состояние. Чтобы вывод не "болтался в воздухе", мы его притягиваем к земле.

MOSI - Master Out Slave In - выход нашего передатчика

nCS - negative Chip Select - выход "выбора чипа"

FinalBit - здесь появится единичка, когда мы приступаем к передаче последнего бита. В этот момент, если мы имеем дело с многобайтовой посылкой, уже можно подать на D следующий байт и единичный импульс на Start. Как результат - последняя посылка будет корректна завершена, и мы тут же приступим к передаче следующей, не сбрасывая nCS в единицу, поскольку в противном случае контроллер решит, что мы закончили передачу, и воспримет данные некорректно.

Finished - здесь появится единичка, когда весь байт будет передан, а nCS установлен в единицу. Если мы хотим сразу за многобайтовой командой передать ещё одну, то именно по сигналу Finished мы можем это сделать - теперь уже контроллер воспримет эти две команды как "не связанные между собой".

Данный модуль был написан в конце марта 2019 года, когда я был совсем молодым и неопытным. Он изобилует комбинаторной логикой, что несколько упрощало его разработку (мы сразу определяем, сколько бит нам требуется для хранения состояния - и вводим именно столько регистров. Всё остальное "выводится" из них) и скорее всего уменьшало необходимое число ЛЭ, но негативно сказывается на максимально допустимой тактовой частоте. Даже этот простенький модуль умудряется "провалить" Classic Timing Analyzer на 80 МГц.

Впрочем, ему это и не требуется, т.к его мы запустим вообще на 4 МГц (именно такая частота доступна с выхода Ethernet-контроллера, пока мы не переправим её на 33 МГц), так что пусть будет :)

Объявляем параметр:
parameter DoPauseBetweenBytes = 1;

он позволяет немножко "заглушить" SCK после передачи каждого байта. Это вполне допустимо по стандарту (SCK - это и есть ВРЕМЯ, наши устройства понятия не имеют, сколько реально проходит времени между "Тиком" и "Таком"), чуть-чуть замедляет передачу, зато на осциллограмме мы отчётливо видим отдельные байты, что может быть полезно при отладке.

Сразу же разбираемся с входом/выходом MISO:

assign MISO = nCS? 1'b0 : 1'bz;

Притягиваем его в ноль, когда обмена информацией не идёт, и Ethernet-контроллер "отпускает" его, делая вид, что его тут вообще нет. И сами "отпускаем" его, когда обмен начинается.

Входные данные мы будем "защёлкивать" в сдвиговый регистр, как обычно:
reg [8:0] SR = 1'b0;

Но он на один бит "шире", чем наши данные. Это нужно, если мы хотим передавать данные "без пауз" - ещё не ушёл последний бит, а мы уже "заблаговременно" защёлкиваем следующий. Чтобы не стереть последний бит, ему и выделено дополнительное место.

Ещё нам нужен регистр, чтобы запомнить, что у нас "запросили перезапуск", т.е при передаче последнего бита мы приняли сигнал Start и уже "защёлкнули" новые данные, но должны были завершить передачу, и только потом проверить - надо ли продолжать:
reg DoRestart = 1'b0;


Нужен также меандр, который будет поступать на SCK, для этого объявим регистр, и по каждому импульсу ce будем менять его значение на противоположное:
reg rSCK = 1'b0;


Ну и разумеется, куда же мы денемся без нашего любимого "командоаппарата"!
localparam sIdle =  4'b0000;
localparam sStart = 4'b0001;
localparam sB0 =    4'b0111;
localparam sB1 =    4'b1000;
localparam sB2 =    4'b1001;
localparam sB3 =    4'b1010;
localparam sB4 =    4'b1011;
localparam sB5 =    4'b1100;
localparam sB6 =    4'b1101;
localparam sB7 =    4'b1110; 
localparam sFinish =4'b1111;
	
wire [3:0] State;


Как всегда, у нас есть состояние Idle (ожидание), в котором мы сидим до тех пор, пока не придёт сигнал Start. Тогда мы переходим в состояние sStart, в котором nCS переключился с единицы на ноль, но меандр на SCK пока ещё не появился, там сидит ноль. Только отсчитав один период SCK, мы переходим в sB0, где мы передаём нулевой бит, затем, первый, и т.д. Наконец, в состоянии sFinish мы опять "обнуляем" SCK, и если сигнал Start к этому времени так и не пришёл, возвращаемся в состояние sIdle, при этом nCS опять устанавливается в единичку.

Делаем некоторые логические функции от этих битов состояния:
wire comb_nCS = (~State[3]) & (~State[0]); //State == sIdle

wire isStart = (~State[3]) & (~State[1]) & State[0];
	
wire slow_ce = ce & rSCK;

wire EnableShiftReg = slow_ce | start;

wire IsFinalState;


Итак, провод comb_nCS равен единице только в состоянии sIdle (поскольку из 16 возможных состояний мы используем только 11, то можно не проверять все 4 бита).

isStart равен единице только в состоянии sStart,

slow_ce подаёт единичные импульсы, когда мы должны "защёлкивать" данные на выход, а именно, по отрицательному фронту SCK.

EnableShiftReg управляет сдвиговым регистром. Он должен "защёлкиваться" при поступлении сигнала start и сдвигаться при каждом переходе в следующее состояние.

Наконец, IsFinalState равно единичке только в состоянии sFinish, этот провод мы подключаем к выходу cout (Carry-out) нашего счётчика.

Далее, в зависимости от параметра DoPauseBetweenBytes определим ещё одно значение:
	generate
		if (DoPauseBetweenBytes == 1) begin 
			wire IsFinalDataBit = IsFinalState; 
		end
		else begin
			wire IsFinalDataBit = (State == sB7);
		end			
	endgenerate


Вообще, здесь синтаксис generate / endgenerate не особенно нужен, можно было бы то же самое написать так:
   wire IsFinalDataBit = (DoPauseBetweenBytes == 1)? IsFinalState : (State == sB7);


а синтезатор уже сам сообразил бы, какую из частей "выкинуть"! Но с синтаксисом generate / endgenerate мы сразу понимаем, что это compile-time логика, а не runtime. Не знаю, как лучше, наверное по-всякому можно, главное хоть сколько-нибудь описать логику работы всего этого безобразия, иначе спустя два месяца глянешь - и вообще ничего не поймёшь...

Провод IsFinalDataBit определяет, когда именно мы должны "перепрыгнуть" к началу передачи данных, если нас попросили передать ещё один байт (DoRestart = 1). Либо мы это делаем по окончании состояния sFinish, и тогда между байтами будет образовываться пауза, либо сразу по окончании состояния sB7, тогда никакой паузы не будет, сплошная "стена битов".


Пора определить сам счётчик состояний ("командоаппарат"):
	lpm_counter StateCounter (.clock (clk),
				              .cnt_en (slow_ce | (start & (~IsFinalDataBit))),
				              .sset ((isStart | (IsFinalDataBit & DoRestart)) & slow_ce),
				              .Q (State),
				              .cout (IsFinalState));
	defparam
		StateCounter.lpm_direction = "UP",
		StateCounter.lpm_port_updown = "PORT_UNUSED",
		StateCounter.lpm_type = "LPM_COUNTER",
		StateCounter.lpm_width = 4,
		StateCounter.lpm_svalue = sB0;			                    


Итак, это счётчик, который прибавляет к номеру состояния единичку, но только когда ему "разрешён счёт", то есть присутствует единица на входе cnt_en (count enable). А именно, единица будет прибавляться, когда приходит импульс start, но только не в конце работы, когда мы должны окончить передачу и не реагировать на него раньше времени! Также единица прибавляется по импульсам slow_ce, то есть по отрицательному фронту SCK. Поскольку данный меандр начинает функционировать только при выходе из состояния sIdle, то если уж мы сидим в этом самом sIdle, мы никуда не денемся, пока не придёт сигнал start.

Но кроме прибавления единицы, мы хотим иногда "перепрыгивать" в состояние sB0 (передача нулевого бита). Либо мы "безусловно" перепрыгиваем в sB0 из состояния sStart (между ними длинная прореха), либо мы перепрыгиваем туда же, если закончили передачу и получили запрос на передачу следующего байта (т.е DoRestart = 1). Этим заведует вход sset (synchronous set).

Как мы и обещали, провод IsFinalState мы подключили к выходу cout (Carry Out) этого счётчика - чего добру пропадать!?

Рассмотрим, как работают введённые нами регистры:
	always @(posedge clk) begin
		rSCK <= nCS? 1'b0 : ce? ~rSCK : rSCK;
		DoRestart <= ~IsFinalDataBit? 1'b0 : start? 1'b1 : DoRestart;
		if (EnableShiftReg)
			SR[7:0] <= start? D: SR[7:0]<<1;
		if (slow_ce)
			SR[8] <= SR[7];
	end


Как и обещалось, rSCK сидит в нуле, пока мы в режиме ожидания (и соотв. nCS = 1), а когда мы начинаем работу - переключается с ноля на единицу и с единицы на ноль по каждому импульсу ce, образуя меандр тактовой частоты SPI.

DoRestart почти всё время работы "притянут" к нулю, и лишь при передаче последнего бита он может установиться в единичку, если придёт сигнал start. По сути, он работает как RS-триггер.

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

Осталось за малым - определить выходы:
always @(posedge clk) begin
  nCS <= comb_nCS;
  SCK <= rSCK & IsDataBit;
  Finished <= IsFinalState & slow_ce;
  FinalBit <= IsFinalDataBit & ce & (~rSCK);
end

assign MOSI = SR[8];


Вот, собственно, и всё.

Приведём код всего модуля целиком:
module Better_SPI (input clk, input ce, input [7:0] D, input start,
		   inout MISO,
		   output reg SCK = 1'b0, output reg MOSI = 1'b0, output reg nCS = 1'b1, output reg FinalBit = 1'b0,
                   output reg Finished = 1'b0, output [3:0] DState);
				
	parameter DoPauseBetweenBytes = 1;
	
	assign MISO = nCS? 1'b0 : 1'bz;
	
	reg [8:0] SR = 1'b0;
	
	reg DoRestart = 1'b0;
	
	reg rSCK = 1'b0;
	
	localparam sIdle =  4'b0000;
	localparam sStart = 4'b0001;
	localparam sB0 =    4'b0111;
	localparam sB1 =    4'b1000;
	localparam sB2 =    4'b1001;
	localparam sB3 =    4'b1010;
	localparam sB4 =    4'b1011;
	localparam sB5 =    4'b1100;
	localparam sB6 =    4'b1101;
	localparam sB7 =    4'b1110; 
	localparam sFinish =4'b1111;
	
	wire [3:0] State;
	assign DState = State;
	
	wire comb_nCS = (~State[3]) & (~State[0]);
	wire isStart = (~State[3]) & (~State[1]) & State[0];

	wire slow_ce = ce & rSCK;

	wire EnableShiftReg = slow_ce | start;

	wire IsFinalState;
	
	generate
		if (DoPauseBetweenBytes == 1) begin
			wire IsFinalDataBit = IsFinalState; 
			wire comb_FinalBit = (State == sB7) & slow_ce;
		end
		else begin
			wire IsFinalDataBit = (State == sB7);
			wire comb_FinalBit = (State == sB6) & slow_ce;
		end			
	endgenerate
			
	lpm_counter StateCounter (.clock (clk),
		                  .cnt_en (slow_ce | (start & (~IsFinalDataBit))),
		                  .sset ((isStart | (IsFinalDataBit & DoRestart)) & slow_ce),
				  .Q (State),														
				  .cout (IsFinalState));
	defparam
		StateCounter.lpm_direction = "UP",
		StateCounter.lpm_port_updown = "PORT_UNUSED",
		StateCounter.lpm_type = "LPM_COUNTER",
		StateCounter.lpm_width = 4,
		StateCounter.lpm_svalue = sB0;			                    
	
	always @(posedge clk) begin
		rSCK <= nCS? 1'b0 : ce? ~rSCK : rSCK; 
		DoRestart <= ~IsFinalDataBit? 1'b0 : start? 1'b1 : DoRestart;
		if (EnableShiftReg)
			SR[7:0] <= start? D: SR[7:0]<<1;
		if (slow_ce)
			SR[8] <= SR[7];
		Finished <= IsFinalState & slow_ce;
		FinalBit <= comb_FinalBit;
		SCK <= rSCK & (State > sStart) & (State < sFinish);						
		nCS <= comb_nCS;
		MOSI <= SR[8];
	end
endmodule


Результат работы этого модуля при DoPauseBetweenBytes = 1 приведён в начале поста, а при DoPauseBetweenBytes = 0 - ниже по тексту:



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

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

Как видно, если мы работаем "без пауз", то все биты похожи друг на друга, передаются сплошным потоком. Если же включить паузы, то возникает дополнительная пауза между байтами, величиной один период SCK, поэтому длинные посылки более "читаемы" на осциллограммах.

Наконец, когда мы передали очередной байт, и "заявки" на отправку следующего не получили, мы сначала "отключаем" SCK, и лишь спустя ещё один период SCK выставляем nCS = 1 - жизненно необходимо выдержать эту паузу, иначе Ethernet-контроллер считает, что передача не была окончена, и игнорирует её целиком! Дело в том, что и интервал между последним фронтом SCK и положительным фронтом nCS (когда он возвращается в единицу) также должен составлять не менее 50 нс. И действительно, без этого интервала у меня как-то совсем ничего не работало...

Модуль занимает 30 .. 32 ЛЭ - по сравнению с недавними PNG-монстрами, это уже кажется совсем немного.
Tags: ПЛИС, работа, странные девайсы
Subscribe

Recent Posts from This Journal

  • Ремонт лыжных мостиков

    Вернулся с сегодняшнего субботника. Очень продуктивно: отремонтировали все ТРИ мостика! Правда, для этого надо было разделиться, благо народу…

  • Гетто-байк

    В субботу во время Великой Октябрьской резни бензопилой умудрился петуха сломать в велосипеде. По счастью, уже на следующий день удалось купить…

  • А всё-таки есть польза от ковариаций

    Вчера опробовал "сценарий", когда варьируем дальность от 1 метра до 11 метров. Получилось, что грамотное усреднение - это взять с огромными весами…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 1 comment