nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

16-битный полудуплексный UART

Ну, то есть на линии RS485 ничего не меняется: точно так же идут стартовый бит, 8 бит данных и стоповый бит. Но "внутренний интерфейс" меняется, он заточен под 16-битный процессор и 16-битный "протокольный контроллер МКО" (которому пока что вместо настоящего МКО/Mil-Std 1553 подсовывают UART). На передачу запускается 16 бит за раз, и на приём приходит сразу два байта. А если один байт пришёл, а потом возникла длиннющая пауза, то "возвращаемся в исходное состояние". Таким образом, если штатно отправлять чётное количество байт, то один раз возникшая ошибка не собьёт нам все последующие сообщения.

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


В первую очередь выписываем заголовок:
module HalfDuplexUART16bit(input clk, input [15:0] D, input startTX, input isCRC, input CRC_bit,
			output [15:0] Q, output HasOutput, output FrameError, output LineBreak,
			output txd, output reg RW = 1'b0, output isDataBit,
			inout rxd,
			output [3:0] DebugState);


По сравнению с 8-битным вариантом изменилась только ширина входа D и выхода Q. В общем-то, код из 8-битного варианта и берём, надо самую чуточку его доработать.

Добавляем признак второго байта:
reg isSecondByte = 1'b0;	//чтобы и превратить обычный 8-битный в "16-битный"


В 16-битном приёмнике он работал следующим образом:
always @(posedge clk)
	isSecondByte <= (isStart & rxd)? 1'b0 : isSecondByte ^ (isStopState & ce);


По окончании приёма одного байта (а теперь будет и по окончании передачи) этот бит переключается на противоположный, а чтобы однажды допущенная ошибка ("пропавший" байт) не сбила нам это признак навсегда, введено первое условие: если во время приёма стартового бита (он всегда должен быть нулевой) мы внезапно приняли единичный, то isSecondByte устанавливается в ноль. Самая частая причина такого события: передача сообщения окончилась, на входе приёмника стабильно единица, но у нас таким способом "подкручен" (это ещё впереди) делитель частоты, что и в состоянии sIdle при ожидании поступления второго байта продолжает "тикать", и по сигналу "ce" конечный автомат начинает двигаться к состоянию sStart, и оказавшись там - уже сбрасывает isSecondByte в ноль, и состояние в sIdle.

Этот код взят из приёмника, теперь нужно сюда ещё включить логику передатчика. Второе условие можно не трогать, где значение переключается на противоположное, тут всё правильно. Но вот первое надо "ужать" только для приёма:

always @(posedge clk)
	isSecondByte <= (isStart & rxd & ~RW)? 1'b0 : isSecondByte ^ (isStopState & ce);			


На передаче волноваться за ошибку не стоит: начинаем мы с isSecondByte = 0. По окончании передачи 1-го байта, мы обязаны будем переключиться в состояние sStart, чтобы продолжить передачу, при этом isSecondByte установится в единицу. А там мы всё-таки вернёмся в sIdle, при этом isSecondByte снова установится в ноль.

Наше выражение для RW (выбор между приёмом и передачей) требует небольшой коррекции. Так было изначально (для 8 бит):
always @(posedge clk)
	RW <= (startTX & isIdle)? 1'b1 : (isStopState & ce)? 1'b0 : RW;


Изначально работаем на приём. Если нам пришёл startTX, когда мы свободны (ничего не принимаем и не передаём), начинаем передачу, и по окончании стопового бита прерываем её. В условии перехода в единицу, возможно, стоит добавить также условие isSecondByte = 0. В этом случае, если приёмник уже успел принять один байт, и ждёт поступления второго (т.е стоит в sIdle, но при isSecondByte=1), может стоит дать ему получить этот второй байт - и только тогда начать передачу. Но вообще такой ситуации возникать не должно, у нас всё детерминировано: начинаем отвечать лишь тогда, когда всё сообщение было получено. Сами голоса не подаём, пока не попросят. Так что может и лишняя забота. Пока оставим.

А вот условие установки RW в ноль надо усилить требованием isSecondByte=1. То есть, передав первый байт и оказавшись в состоянии sStop, нужно оставить передатчик включённым и дать ему передать второй байт. В итоге получаем так:
always @(posedge clk)
	RW <= (startTX & isIdle)? 1'b1 : (isStopState & ce & isSecondByte)? 1'b0 : RW;


Ещё надо переделать сигнал ZeroFreqDivider (обнуление делителя частоты). В нашем 8-битном приёмопередатчике он формируется так:
wire ZeroFreqDivider = isIdle & rxd | ce;


Т.е в состоянии sIdle делитель непрерывно обнуляется, не считая момента, когда на вход приёмника придёт нолик (предположительно стартовый бит). И ещё он обнуляется, когда делитель досчитывает полную длительность одного бита данных.

В 16-битном приёмнике этот же сигнал формируется так:
wire ZeroFreqDivider = (isIdle & rxd & (~isSecondByte)) | ce;


"непрерывное обнуление" отключено на втором байте сообщения, в состоянии sIdle, чтобы после некоторого ожидания мы "теряли надежду" дождаться окончания 16-битной посылки (из которой приняли только половину) и начали бы сначала.

Как ни странно, этот код вполне устраивает нас и в 16-битном полудуплексном устройстве, т.к в режиме передачи, при установке isSecondByte=1 мы сразу же должны будем прыгнуть в sStart, вообще не заходя в sIdle. Оставим ровно такой.

Вот мы и дошли до самого конечного автомата, он тоже нуждается в доработке. В 8-битном полудуплексном устройстве он выглядит так:
lpm_counter StateMachine (
			.clock (clk),
			.cnt_en (ce),
			.sset (isIdle & ((~rxd) | startTX)),
			.sclr (isStart & rxd & (~RW)),
			.q (State),
			.cout (isStopState) );
defparam
	StateMachine.lpm_direction = "UP",
	StateMachine.lpm_port_updown = "PORT_UNUSED",
	StateMachine.lpm_type = "LPM_COUNTER",
	StateMachine.lpm_width = 4,
	StateMachine.lpm_svalue = sStart;


Мы начинаем работу (переходим в состояние sStart) только из состояния sIdle, по одному из воздействий: либо пришёл startTX (запрос на передачу), либо rxd=0 (приёмник "поймал" стартовый бит сообщения). "Досрочно" вернуться в sIdle мы можем в одном случае: во время приёма, в состоянии sStart, в середине бита (по времени, т.е момент, когда никаких переходных процессов, каких-то заваленных фронтов быть уже не должно), если там на линии оказалась единичка, вместо ожидаемого нами стартового бита 0. И наконец, переходить на следующее состояние мы всегда будем по сигналу ce. дёшево и сердито :)

Теперь поглядим на конечный автомат 16-битного приёмника:
lpm_counter StateMachine (
			.clock (clk),
			.cnt_en (ce),
			.sset (isIdle & (~rxd)),
			.sclr (isStart & rxd),
			.q (State),
			.cout (isStopState) );
defparam
	StateMachine.lpm_direction = "UP",
	StateMachine.lpm_port_updown = "PORT_UNUSED",
	StateMachine.lpm_type = "LPM_COUNTER",
	StateMachine.lpm_width = 4,
	StateMachine.lpm_svalue = sStart;


Никакой новой информации он нам не добавляет... Из него "убрано" условие старта по запросу на передачу (startTX=1), и проверка, что мы находимся в режиме приёма (RW=0), поскольку это его единственный режим :) А именно каких-то добавок к конечному автомату из-за 16 бит вместо 8, как ни странно, не было. Да, приняли байт - и точно так же, как и всегда вернулись в sIdle, ждать, когда нам соизволят передать следующий. Ну, разве что у нас есть возможность из этого sIdle "выкарабкаться" своим ходом, но это реализовано не здесь :)

Но для нашего нового устройства, 16-битного полудуплексного, нужно одно дополнительное условие: если мы находимся в режиме передачи (RW=1), в состоянии sStop (передача стопового бита), ce=1 (эта передача только что закончилась), и при этом isSecondByte=0 (передали только первый байт), нужно вместо "естественного" перехода в sIdle (т.е из состояния 15 в состояние 0) перейти в sStart, т.е нужно подать единичку на sSet:
lpm_counter StateMachine (
			.clock (clk),
			.cnt_en (ce),
			.sset (isIdle & ((~rxd) | startTX) | isStop & RW & ce & ~isSecondByte),
			.sclr (isStart & rxd & (~RW)),
			.q (State),
			.cout (isStopState) );
defparam
	StateMachine.lpm_direction = "UP",
	StateMachine.lpm_port_updown = "PORT_UNUSED",
	StateMachine.lpm_type = "LPM_COUNTER",
	StateMachine.lpm_width = 4,
	StateMachine.lpm_svalue = sStart;


Суровое выражение... А что делать, здесь разница между приёмом и передачей становится сильнее, чем в 8-битном варианте. Но вроде бы не так всё страшно.

Добрались до сдвигового регистра. Так он выглядит в 8-битном полудуплексном агрегате:
reg [8:0] SR = 9'b1_1111_1111;
							
always @(posedge clk) if (ce | startTX & isIdle) begin
	SR[8] <= (startTX & isIdle)? D[7] : rxd;
	SR[7:1] <= (startTX & isIdle)? D[6:0] : SR[8:2];
	//SR[0] <= (startTX & isIdle)? 1'b0 : (isCRC & (State[3:1] != 3'b111))? CRC_bit : SR[1];
	SR[0] <= (startTX & isIdle)? 1'b0 : (isCRC &~& State[3:1])? CRC_bit : SR[1];
end


Регистр 9-битный. При подаче startTX=1 происходит параллельная загрузка: в младший бит (прямо сейчас идущий на выход txd) защёлкивается 0 (стартовый бит), во все остальные - 8 бит данных.

Во всё остальное время, по сигналу ce=1, происходит сдвиг вправо, при этом в старший бит вдвигается значение из rxd (вход от приёмника RS485). Выход rxd на микросхемке ADM3485ARZ переходит в Z-состояние (с высоким выходным импедансом), когда RW=1 (режим передачи), и чтобы провод не "висел в воздухе", мы притягиваем его к единице. Поэтому, в режиме передачи, закончив с битами данных, мы "автоматом" дальше передадим единичку, стоповый бит.

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

Теперь глянем сдвиговый регистр в 16-битном приёмнике:
reg [17:0] SR;
							
always @(posedge clk) if (ce & (~isIdle))
	SR <= {rxd, SR[17:1]};
		
assign Q = {SR[17:10], SR[7:0]};


Регистр 18-битный, т.е 16 бит данных + стартовый и стоповый биты, которые оказались "посередине". И ещё дополнительная "хитрость": ce=1 вызывает сдвиг только если isIdle=0. Это позволяет корректно получить наши 2 байта, если между ними есть "зазор" длительностью в несколько бит, т.е когда мы из состояния sIdle=0, в ожидании второго байта, успели перейти в State=1, затем 2, 3, каждый переход происходит по ce=1, но все эти состояния определяются как isIdle, благодаря упрощённой логике.

Пора написать сдвиговый регистр для 16-битной полудуплексной хреновины:
reg [18:0] SR = 19'b111_1111_1111_1111_1111;
							
always @(posedge clk) if (isIdle? startTX : ce) begin
	SR[18] <= (startTX & isIdle)? D[15] : rxd;
	SR[17:1] <= (startTX & isIdle)? {D[14:8],2'b01,D[7:0]} : SR[17:2];
	//SR[0] <= (startTX & isIdle)? 1'b0 : (isCRC & (State[3:1] != 3'b111))? CRC_bit : SR[1];
	SR[0] <= (startTX & isIdle)? 1'b0 : (isCRC &~& State[3:1])? CRC_bit : SR[1];
end
		
assign Q = {SR[18:11],SR[8:1]};
assign txd = SR[0];


Тут регистр 19-битный, чтобы не только ввести стартовый и стоповый биты "посередине" между байтами, но и самый первый стартовый бит, по аналогии с 8-битным передатчиком.

Ну и остаётся сформировать несколько сигналов:
assign HasOutput = isStopState & ce & rxd & ~RW; //т.е приняты уже все данные, а стоповый бит ожидаемо высокий.
//у нас как раз есть 1 такт, чтобы занести данные из Q, т.к. на следующий такт они уже пропадут!
	
//assign FrameError = isStopState & ce & (~rxd) & ~RW;
//assign LineBreak = FrameError & (Q == 0) & ~RW;


Пока используем только HasOutput (сообщение, что получили 16-битное слово). Когда-нибудь, глядишь, раскомментируем FrameError (сначала увидели стартовый бит, приняли честно 8 бит данных, но вместо единицы в стоповом бите опять был ноль - что-то очень подозрительно!), он мог бы сбрасывать логику протокольного контроллера по получению сообщения, дескать "тут явно какая-то хрень пришла, но мы её не распознали, теперь уж точно 32 слова данных корректно не получим - пошли отдыхать до следующего командного слова". И может быть, где-то пригодилось бы LineBreak ("10 нулевых бит подряд, напоминает оборванную линию связи"), чтобы вывести её как прерывание, если, конечно, есть куда "выругаться".

Что ж, приводим весь код целиком:
`include "math.v"

module HalfDuplexUART16bit(	input clk, input [15:0] D, input startTX, input isCRC, input CRC_bit,
				output [15:0] Q, output HasOutput, output FrameError, output LineBreak,
				output txd, output reg RW = 1'b0, output isDataBit,
				inout rxd,
				output [3:0] DebugState);

	assign rxd = RW? 1'b1 : 1'bz;

	parameter CLKfreq = 25_000_000;
	parameter BAUDrate = 921_600;
//921_600

	localparam Quotient = ((CLKfreq + BAUDrate/2) / BAUDrate) - 1;
	localparam DividerBits = `CLOG2(Quotient);
	localparam Limit = Quotient - 1;
	localparam LimitDiv2 = (CLKfreq + BAUDrate/4) / (BAUDrate * 2) - 1;

	localparam sIdle 	=	4'b0000;
	localparam sStart	=	4'b0110;
	localparam sB1	 	=	4'b0111;
	localparam sB2	    	=	4'b1000;
	localparam sB3		=	4'b1001;
	localparam sB4		=	4'b1010;
	localparam sB5		=	4'b1011;
	localparam sB6		=	4'b1100;
	localparam sB7		=	4'b1101;
	localparam sB8		=	4'b1110;
	localparam sStop	= 	4'b1111;

	wire [3:0] State;
	wire isStopState;
	wire isIdle = (~State[3]) & (~State[2]); //shortcut as not all states are used
	wire isStart = (~State[3]) & State[2] & (~State[0]);	//small shortcut...
	
	wire [DividerBits-1:0] FD; //Frequency Divider
	
	reg isSecondByte = 1'b0;	//чтобы и превратить обычный 8-битный в "16-битный"	
	always @(posedge clk)
		isSecondByte <= (isStart & rxd & ~RW)? 1'b0 : isSecondByte ^ (isStopState & ce);			
		
	
	wire comb_ce = (FD & Limit) == Limit;
	reg ce = 1'b0;
	always @(posedge clk)
		ce <= comb_ce;
	assign isDataBit = (State[3] | State[0])&(~isStopState)&comb_ce;	
	
	always @(posedge clk)
		RW <= (startTX & isIdle)? 1'b1 : (isStopState & ce & isSecondByte)? 1'b0 : RW;

	wire ZeroFreqDivider = (isIdle & rxd & (~isSecondByte)) | ce;
	wire HalfFreqDivider = isIdle & ~rxd;
	
	lpm_counter Divider (
			.clock (clk),
			.sset (HalfFreqDivider),
			.sclr (ZeroFreqDivider),
			.Q (FD) );
  defparam
    Divider.lpm_direction = "UP",
    Divider.lpm_port_updown = "PORT_UNUSED",
    Divider.lpm_type = "LPM_COUNTER",
    Divider.lpm_width = DividerBits,
    Divider.lpm_svalue = LimitDiv2;   
	

	lpm_counter StateMachine (
				.clock (clk),
				.cnt_en (ce),
				.sset (isIdle & ((~rxd) | startTX) | isStopState & RW & ce & ~isSecondByte),
				.sclr (isStart & rxd & (~RW)),
				.q (State),
				.cout (isStopState) );
	defparam
		StateMachine.lpm_direction = "UP",
		StateMachine.lpm_port_updown = "PORT_UNUSED",
		StateMachine.lpm_type = "LPM_COUNTER",
		StateMachine.lpm_width = 4,
		StateMachine.lpm_svalue = sStart;


	assign DebugState = State;
	
	
	reg [18:0] SR = 19'b111_1111_1111_1111_1111;
							
	always @(posedge clk) if (isIdle? startTX : ce) begin
		SR[18] <= (startTX & isIdle)? D[15] : rxd;
		SR[17:1] <= (startTX & isIdle)? {D[14:8],2'b01,D[7:0]} : SR[17:2];
		//SR[0] <= (startTX & isIdle)? 1'b0 : (isCRC & (State[3:1] != 3'b111))? CRC_bit : SR[1];
		SR[0] <= (startTX & isIdle)? 1'b0 : (isCRC &~& State[3:1])? CRC_bit : SR[1];
	end
		
	assign Q = {SR[18:11],SR[8:1]};
	assign txd = SR[0];
	
	assign HasOutput = isStopState & ce & rxd & ~RW; //т.е приняты уже все данные, а стоповый бит ожидаемо высокий.
	//у нас как раз есть 1 такт, чтобы занести данные из Q, т.к. на следующий такт они уже пропадут!
	
	//assign FrameError = isStopState & ce & (~rxd) & ~RW;
	//assign LineBreak = FrameError & (Q == 0) & ~RW;	
	
endmodule



Эта хреновина синтезируется ровно в 50 ЛЭ (когда тактовая 25 МГц, скорость передачи 921600 бод), и максимально допустимая частота составляет 81,97 МГц - неплохо.

Осталось поглядеть - а оно вообще работает?

Продолжение следует...
Tags: ПЛИС, работа, странные девайсы
Subscribe

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

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

  • Формулы приведения, что б их... (и atan на ТРЁХ умножениях)

    Формулу арктангенса на 4 умножениях ещё немножко оптимизировал с помощью алгоритма Ремеза: Ошибка уменьшилась с 4,9 до 4,65 угловой секунды, и…

  • Алгоритм Ремеза в экселе

    Вот и до него руки дошли, причина станет ясна в следующем посте. Изучать чужие библиотеки было лениво (в том же BOOSTе сам чёрт ногу сломит), писать…

  • atan на ЧЕТЫРЁХ умножениях

    Мишка такой человек — ему обязательно надо, чтоб от всего была польза. Когда у него бывают лишние деньги, он идёт в магазин и покупает какую-нибудь…

  • Ай да Пафнутий Львович!

    Решил ещё немного поковыряться со своим арктангенсом. Хотел применить алгоритм Ремеза, но начал с узлов Чебышёва. И для начала со своего "линейного…

  • atan(y/x) на двух умножениях!

    Чего-то никак меня не отпустит эта тема, всё кажется, что есть очень простой и эффективный метод, надо только его найти! Сейчас вот такое…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments