nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Мучаем 5576ХС4Т - часть 'h3A - передатчик сообщений 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
Часть 'h39 - приказываю: спи! (по SPI)
Мучаем 5576ХС4Т - часть 'h3A - передатчик сообщений SPI


В прошлой части, основательно накурившись мануалов, мы пришли к следующей последовательности:
04 22 54 00 01 04 22 66 00 18 03 22 6F 01 00,


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

Сначала идёт количество байтов (04), затем эти самые байты (22 54 00 01), которые передаются одной посылкой, затем следующее количество байтов (04), очередные байты (22 66 00 18), которые передаются следующей посылкой (если бы они сидели все в одной посылке, смысл бы изменился, мы бы продолжили записывать в память с адреса 54), снова количество байтов (03) и снова посылка (22 6F 01) и, наконец, нолик, на котором работа завершается.

Напишем передатчик, который сможет правильно воспринять эту последовательность и передать необходимые посылки по SPI, используя передатчик байтов из части 'h38.





Схема уже достаточно "стандартная": у нас есть передатчик байтов (справа на схеме), модуль памяти (в данном случае ПЗУ, она же ROM) и контроллер, который заведует передачей на уровне "чуть выше". Мы подаём на него импульс start и начальный адрес в памяти. В ответ он запрашивает байт по этому адресу. Если это ноль, работа прекращается. В противном случае запрашивается следующий адрес и посылается по SPI. А за ним - ещё и ещё, пока не отправим сколько надо. Затем снова считываем количество байтов - и так далее.

Также этот контроллер принимает сигналы FinalBit и Finished, без этого никуда.

Рассмотрим его подробнее. Начинаем с заголовка, хотя по схеме заголовок и так "восстанавливается":
module SPI_controller (input clk,
                       input start,
                       input [AddrWidth-1 : 0] InitAddr,
                       input [7:0] D,
                       input TxFinalBit,
                       input TxFinished,
		       output reg [AddrWidth - 1 : 0] OutAddr = 1'b0,
		       output TxStart,
		       output Finished );
					 
	parameter AddrWidth = 4;
	parameter MaxByteCount = 4; //simplifies logic and counter a little.
	
	localparam ByteCounterWidth = `CLOG2(MaxByteCount + 1);


Мы задаём параметры - ширину адресной шины, а также максимально возможная длина посылки в байтах. Для простоты мы выделили целый байт на это число, но если мы заранее знаем, что самая длинная посылка состоит из 4 байт - так сразу и напишем! И обнаружим, что нам хватит 3 бита на счётчик оставшихся байт.

Задаём наш "командоаппарат", он же счётчик состояний, он же "конечный автомат", хотя простоват для столь громких названий :)
localparam sIdle = 		3'b000;
localparam sFetch = 		3'b001;
localparam sSetup = 		3'b010;
localparam sDecision = 		3'b011;
localparam sTransmission = 	3'b100;
localparam sRestart = 		3'b101;
	
wire [2:0] State;
	
wire isIdle = (State == sIdle); //no shortcuts...
wire isFetch = (~State[2])&(~State[1]); //idle or fetch, actually. But should suffice (will see about it!)
wire isSetup = State[1] & (~State[0]);
wire isDecision = State[1] & State[0]; //another liiitle shortcut
wire isTransmission = State[2] & (~State[0]); //a liiitle shortcut
wire isRestart = State[2] & State[0]; //so many of these states, maybe state machine would be better after all?

wire isStateWithNormalCE = (~State[2])&(~isIdle); //here we step easily: Fetch, Setup, Decision (no need to wait for anybody)


Итак, у нас используется 6 состояний: ожидание (sIdle), выборка памяти (sFetch), начало работы (sSetup), принятие решения (sDecision), непосредственно передача байтов (sTransmission) и перезапуск (sRestart).

Описание работы
Изначально мы находимся в режиме ожидания, до тех пор, пока не придёт импульс запуска start. По нему мы переходим в состояние sFetch, а также "защёлкиваем" в регистр OutAddr исходный адрес.

Только к началу состояния sFetch (на следующем такте) с выхода OutAddr начинает поступать правильный адрес. Но поскольку и память у нас с "защёлкой", то она пока выдаёт "старое" значение. Для того и нужно это состояние - дождаться поступления правильных данных из памяти. Кроме того, мы прибавляем 1 к OutAddr, чтобы заблаговременно получить следующий байт, который нам, возможно, придётся передавать.

К следующему такту мы "безусловно" переходим в состояние sSetup. Теперь мы знаем, сколько байт нам нужно передать - это указано на входе D. Это значение мы "защёлкиваем" в счётчик оставшихся байт, BytesLeft. С OutAddr уже идёт адрес, увеличенный на единицу, т.е он указывает на первый байт для передачи по SPI.

Теперь мы "безусловно" переходим в состояние sDecision. Теперь уже мы смотрим, что же у нас "защёлкнуто" в регистре BytesLeft. Если там ноль, мы возвращаемся в режим ожидания, так ничего и не передав. В противном случае, мы посылаем импульс TxStart на передатчик байтов. К этому моменту на выходе памяти мы уже имеем нужный байт, который и надо передать. Выдача этого импульса также заставляет счётчик OutAddr ещё прибавить единичку, а счётчик BytesLeft - вычесть единичку. И мы переходим в состояние sTransmission.

В этом состоянии мы "замираем" - сидим здесь и ждём импульса TxFinalBit ("началась передача последнего бита текущего байта). Если ещё остался хоть один байт для передачи (BytesLeft не равен нулю), появление этого импульса сразу же запускает следующий байт (благо, именно он идёт с выхода памяти), и снова прибавляет 1 к OutAddr и отнимает 1 от BytesLeft.

Это обеспечивает нам "бесшовную" передачу байт, составляющих одну посылку.

Наконец, когда BytesLeft == 0 (мы передали всё, что надо), появление нового импульса TxFinalBit более не приводит к формированию нового импульса запуска и к прибавлению/отниманию единичек. Вместо этого, мы переходим в состояние sRestart.

Здесь мы снова останавливаемся, но ненадолго - нам всего лишь надо дождаться импульса TxFinished, то есть ПОЛНОГО окончания передачи посылки, с выставлением nCS в единицу. Только заполучив этот импульс, мы перепрыгиваем в состояние sFetch. Поскольку запустив отправку последнего байта, мы уже прибавили единичку к OutAddr, на выходе памяти уже сидит байт, показывающий длину следующей посылки. Но нам ещё надо прибавить единичку к счётчику адреса сразу за этим, потому мы и перешли в sFetch. И дальше всё начинается по-новой, пока в очередной раз мы не получим нулевой байт.

Реализация

Мы выписали, как определить каждое из состояний по 3 битам нашего счётчика, учитывая, что 2 состояния оказались незадействованными. Как ни странно, ВСЕ эти логические функции оказались нам нужны!

Также мы ввели сигнал isStateWithNormalCE, который равен единице, если мы задерживаемся в текущем состоянии всего на 1 такт, и сразу же идём дальше. Это sFetch (мы просто дожидаемся, пока на входе D появится корректное значение), sSetup (мы заносим долгожданное значение D в регистр BytesLeft) и sDecision (мы проверяем это значение на равенство нулю). В остальных состояниях (sIdle, sTransmission, sRestart) мы можем немножко задержаться.

Определяем регистр BytesLeft и ещё пару "проводов":
reg [ByteCounterWidth-1 : 0] BytesLeft = 1'b0;
wire isOver = (BytesLeft == 0);
wire StateCE = (isIdle & start) | (isTransmission & isOver & TxFinalBit) | isStateWithNormalCE; 


Наверное, правильнее было бы описать очередной счётчик lpm_counter, потому что тогда вместо отдельного ЛЭ, проверяющего BytesLeft на равенство нулю, достаточно было бы подключиться к cout (Carry-out), который для вычитающего счётчика срабатывает именно на "все нули". Либо применить нашу концепцию "быстрых счётчиков" и формировать сигнал заблаговременно. Но пока пусть так: "работает - не лезь!".

Когда StateCE = 1, счётчик состояний прибавляет единичку. Как видно, это происходит либо когда мы в режиме ожидания и получили импульс на запуск, либо мы в тех состояниях, которые длятся всего один такт, либо мы были в состоянии sTransmission и только что получили сигнал TxFinalBit для последнего байта из нашей посылки.

Теперь мы можем выписать сам счётчик состояний:
	lpm_counter	counter (
				.clock (clk),
				.cnt_en (StateCE),
				.sclr (isDecision & isOver),
				.sset (isRestart & TxFinished),
				.q (State) );
	defparam
		counter.lpm_direction = "UP",
		counter.lpm_port_updown = "PORT_UNUSED",
		counter.lpm_type = "LPM_COUNTER",
		counter.lpm_width = 3,
		counter.lpm_svalue = sFetch;


Кроме разрешения счёта, мы используем синхронный сброс в sIdle (если оказалось, что нам нужно передать 0 байт), а также синхронную установку в sFetch, когда закончилась передача очередной посылки.

И осталось всего несколько строк:
  always @(posedge clk) begin
    OutAddr <= start? InitAddr : (TxStart | isFetch)? OutAddr + 1'b1 : OutAddr;
    
    BytesLeft <= isSetup? D : TxStart? BytesLeft - 1'b1 : BytesLeft;  
  end
  assign TxStart = (isDecision | TxFinalBit) & (~isOver);
  assign Finished = isDecision & isOver;


Итак, регистр адреса OutAddr либо загружает значение InitAddr на старте, либо стоит на месте, либо прибавляет единичку в состоянии sFetch, или каждый раз, как мы посылаем импульс TxStart, запуская передачу очередного байта.

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

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

Наконец, выпишем модуль целиком.
`include "math.v"

module SPI_controller (input clk,
                       input start,
                       input [AddrWidth-1 : 0] InitAddr,
                       input [7:0] D,
                       input TxFinalBit,
                       input TxFinished,
		       output reg [AddrWidth - 1 : 0] OutAddr = 1'b0,
		       output TxStart,
		       output Finished );
					 
	parameter AddrWidth = 4;
	parameter MaxByteCount = 4; //simplifies logic and counter a little.
	
	localparam ByteCounterWidth = `CLOG2(MaxByteCount + 1);
	
	//state machine
	localparam sIdle = 		3'b000;
	localparam sFetch = 		3'b001;
	localparam sSetup = 		3'b010;
	localparam sDecision = 		3'b011;
	localparam sTransmission = 	3'b100;
	localparam sRestart = 		3'b101;
	
	wire [2:0] State;
	
	wire isIdle = (State == sIdle); //no shortcuts...
	wire isFetch = (~State[2])&(~State[1]); //idle or fetch, actually. But should suffice (will see about it!)
	wire isSetup = State[1] & (~State[0]);
	wire isDecision = State[1] & State[0]; //another liiitle shortcut
	wire isTransmission = State[2] & (~State[0]); //a liiitle shortcut
	wire isRestart = State[2] & State[0]; //so many of these states, maybe state machine would be better after all?
	
	wire isStateWithNormalCE = (~State[2])&(~isIdle); //here we step easily: Fetch, Setup, Decision (no need to wait for anybody)
	

	reg [ByteCounterWidth-1 : 0] BytesLeft = 1'b0;
	wire isOver = (BytesLeft == 0);
	wire StateCE = (isIdle & start) | (isTransmission & isOver & TxFinalBit) | isStateWithNormalCE; 
	
	lpm_counter	counter (
				.clock (clk),
				.cnt_en (StateCE),
				.sclr (isDecision & isOver),
				.sset (isRestart & TxFinished),
				.q (State) );
	defparam
		counter.lpm_direction = "UP",
		counter.lpm_port_updown = "PORT_UNUSED",
		counter.lpm_type = "LPM_COUNTER",
		counter.lpm_width = 3,
		counter.lpm_svalue = sFetch;
			
  always @(posedge clk) begin
    OutAddr <= start? InitAddr : (TxStart | isFetch)? OutAddr + 1'b1 : OutAddr;
    
    BytesLeft <= isSetup? D : TxStart? BytesLeft - 1'b1 : BytesLeft;
   
  end
  assign TxStart = (isDecision | TxFinalBit) & (~isOver);
  assign Finished = isDecision & isOver;
endmodule


Данная штуковина синтезируется в 27 ЛЭ, а схема целиком (контроллер + память + передатчик байтов, но без делителя частоты) - в 64 ЛЭ.

Timing Analyzer показывает максимально допустимую частоту 67,57 МГц. Главный "тормоз" здесь - мы оставили комбинаторный выход TxStart, т.е получается, что выход FinalBit передатчика байтов поступает на вход TxFinalBit контроллера, и тут же, объединившись с другими 5 логическими значениями, должен отправиться обратно, на вход start! Мы уже знаем, как с этим бороться. Эти модули достаточно простые - могли бы заработать и на 80 МГц, но у нас он фактически будет работать от 4 МГц, так что ничего страшного вообще!

Результаты симуляции приведены в начале поста. Благодаря внесению пауз между байтами (когда nCS по-прежнему ноль, но CSK замирает на один такт) картинка получается вполне понятной. Действительно, мы видим передачу 4 байт, затем ещё 4 байт и, наконец, 3 байт.
Прочитав каждый байт по "зелёным точкам" (там где их считывает Ethernet-контроллер, по положительному фронту SCK), мы видим, что передаётся то, что мы и хотели.

Осталось только задать правильно все пины и опробовать этот контроллер "в железе", в смысле, на ПЛИС.
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 

  • 0 comments