nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

"16-битный" приёмник UART - окончание

Где-то неделю назад мы попробовали сделать приёмник UART, который, получив один стартовый бит, пытался получить 2 байта подряд без дополнительной синхронизации. Когда передатчиком RS485 выступал дешёвый USB-"свисток" на основе CH340C без кварца, "не взлетело", на один бит мы "съехали" на втором байте.

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


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

Вернём исходный "командоаппарат" из 11 состояний: sIdle ("ждём"), sStart (стартовый бит), sB0..sB7 (биты данных) и sStop (стоповый бит).

Но добавим ещё один бит isSecondByte, который изначально нулевой, и по sStop он переключается. Сигнал процессору "данные получены" должен будет выдаваться только когда isSecondByte = 1.

Изменений даже меньше, чем в прошлом варианте, как-то так:
`include "math.v"

//более "толерантный" приёмник по 16 бит, допускающий большой разброс по скорости передачи

module UART16bitReceiverTol(input clk, input rxd, input RxReq,
			output [15:0] Q, output busy,
			output HasOutput, output FrameError, output LineBreak,
			output [3:0] DebugState);

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

	localparam Quotient = (CLKfreq + BAUDrate/2) / BAUDrate;
	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...
	
	reg isSecondByte = 1'b0;	//чтобы и превратить обычный 8-битный в "16-битный"
	
	wire [DividerBits-1:0] FD; //Frequency Divider
	
	wire ce = ((FD & Limit) == Limit);
	
	assign HasOutput = isStopState & ce & rxd & isSecondByte; //т.е приняты уже все данные, а стоповый бит ожидаемо высокий.
	//у нас как раз есть 1 такт, чтобы занести данные из Q, т.к. на следующий такт они уже пропадут!
	
	assign busy = ~HasOutput;	//запросили выход, а его пока нет!
	//assign busy = RxReq & ~ (isStopState & ce & rxd);
	
	assign FrameError = isStopState & ce & (~rxd);
	assign LineBreak = FrameError & (Q == 0);
	
	wire ZeroFreqDivider = ((isIdle & rxd) | 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)),
				.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;


	assign DebugState = State;
	
	reg [17:0] SR;
							
	always @(posedge clk) if (ce) begin
		SR <= {rxd, SR[17:1]}; //всегда заносим, пофиг, старт, стоп или данные			
		isSecondByte <= isSecondByte ^ isStopState;
	end
		
	assign Q = {SR[17:10], SR[7:0]};
	
	
endmodule


Как и в первой версии, сдвиговый регистр сделали 18-битным, т.е наши полезные 16 бит плюс стартовый и стоповый биты, попавшие "посерединке". По мне, это проще, чем городить логику, останавливающую сдвиговый регистр, и очень вероятно по ЛЭ получится столько же, а то и меньше.

На симуляции всё работает как надо:


Надо только предусмотреть некий "механизм", чтобы единичная ошибка не вызвала сдвиг на 1 байт, из-за которой вполне себе правильный поток будет "запарываться", пока чудесным образом не возникнет вторая такая ошибка.

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

Хочется воспользоваться уже имеющимися счётчиками, это делитель частоты Divider и "командоаппарат" StateMachine. Сейчас получается, что в состоянии sIdle делитель частоты непрерывно сбрасывается, поэтому время никоим образом "не отмеряется". Попробуем сделать, чтобы он продолжил считать и в sIdle, если мы уже приняли первый байт и ждём второй, т.е isSecondByte = 1:

wire ZeroFreqDivider = (isIdle & rxd & (~isSecondByte)) | ce;


До сих пор получалось, что из состояния sIdle = 0 наш "командоаппарат" может попасть только в sStart = 6 по появлению нуля на линии UART. Просто "прибавить единичку" он не способен, поскольку это требует ce=1, он устанавливается, когда делитель досчитает период одного бита на UART, но он этого не сделает, т.к делитель частоты постоянно сбрасывается.

Теперь же, если после первого байта (середины стопового бита) последовала пауза по крайней мере в 1 бит UART, за время которой на линии так и осталось значение 1, всё-таки произойдёт ce=1, и мы перейдём в "недокументированное" состояние 1.

Но поскольку мы использовали "упрощённую" логику для декодирования состояний sIdle и sStart:
	wire isIdle = (~State[3]) & (~State[2]); //shortcut as not all states are used
	wire isStart = (~State[3]) & State[2] & (~State[0]);	//small shortcut...


то и состояния 1, 2, 3 всё ещё определятся как Idle. Так что всё ещё даётся время для появления "нолика" на линии, т.е стартового бита. Как только он придёт - мы как ни в чём не бывало перейдём в состояние sStart, и дальше начнётся нормальный приём байта.

Правда, чтобы не получить лишних сдвигов на сдвиговом регистре, надо чуть усилить условие: сдвиг происходит не просто по ce=1, но только если состояние не Idle.

Если же стартовый бит так и не пришёл по окончании State=3, мы перейдём в State=4, а это состояние уже будет интерпретировано как Start. И если в этот момент у нас по-прежнему нет стартового бита (нуля на линии), мы тут же вернёмся в sIdle, решив, что словили какой-то "случайный импульс", за которым не последовало нормальной передачи. Вот в этот момент мы должны сбросить isSecondByte в ноль!

Остаётся ещё одна практически невероятная ситуация, когда ровно при переходе из State=3 в State=4 и начался стартовый бит, так что возврата в sIdle не произошло. Мы пошли, как ни в чём не бывало, в State=5, он уже не числится ни как Idle, ни как Start, затем в State=6, это наш "штатный" sStart. Там мы снова проверяем ноль на линии, и либо здесь вернёмся в sIdle, либо и тут нам повезло и пойдём дальше. Ничего страшного, получим какое-то "мусорное" значение, и его уже надо будет отбраковать по какой-нибудь контрольной сумме.

После всех изменений получаем такой код:

`include "math.v"

//более "толерантный" приёмник по 16 бит, допускающий большой разброс по скорости передачи

module UART16bitReceiverTol(input clk, input rxd, input RxReq,
							output [15:0] Q, output busy,
							output HasOutput, output FrameError, output LineBreak,
							output [3:0] DebugState, output DisSecondByte);

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

	localparam Quotient = (CLKfreq + BAUDrate/2) / BAUDrate;
	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...
	
	reg isSecondByte = 1'b0;	//чтобы и превратить обычный 8-битный в "16-битный"
	
	wire [DividerBits-1:0] FD; //Frequency Divider
	
	wire ce = ((FD & Limit) == Limit);
	
	assign HasOutput = isStopState & ce & rxd & isSecondByte; //т.е приняты уже все данные, а стоповый бит ожидаемо высокий.
	//у нас как раз есть 1 такт, чтобы занести данные из Q, т.к. на следующий такт они уже пропадут!
	
	assign busy = ~HasOutput;	//запросили выход, а его пока нет!
	//assign busy = RxReq & ~ (isStopState & ce & rxd);
	
	assign FrameError = isStopState & ce & (~rxd);
	assign LineBreak = FrameError & (Q == 0);
	
	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)),
				.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;


	assign DebugState = State;
	assign DisSecondByte = isSecondByte;
	
	reg [17:0] SR;
							
	always @(posedge clk) begin 
		if (ce & (~isIdle))
			SR <= {rxd, SR[17:1]};
		isSecondByte <= (isStart & rxd)? 1'b0 : isSecondByte ^ (isStopState & ce);
	end
		
	assign Q = {SR[17:10], SR[7:0]};
	
endmodule


Чтобы проверить "сброс" isSecondByte после достаточно длинной паузы, решил на симуляции сначала передать одиночный байт (выставить is16bit=0) и обнаружил, что в этом режиме передатчик после успешной передачи одного байта начинает выдавать "0" на линию, что совсем нехорошо! Проблема была в этой строке (см. 16-битный передатчик UART, окончание)

ShiftReg <= DoStart? {DataBus[15:8],2'b01, DataBus[7:0], 1'b0} : {1'b1, ShiftReg[18:1]};


Я не учёл, что при переходе на стоповый бит (sStop) мы передаём единичку, следующую сразу за данными, но затем происходит ещё один сдвиг, когда из sStop мы возвращаемся в sIdle, и в этот момент у нас уже пошёл нолик, и идёт до победного конца, т.к выдача r_ce прекратилась. Надо вот так написать:

ShiftReg <= DoStart?  {DataBus[15:8], ~is16bit, 1'b1, DataBus[7:0], 1'b0} : {1'b1, ShiftReg[18:1]};


Ну, по крайней мере, один из вариантов борьбы с этим.

Получаем такую картину:


Всё, как мы предполагали: по окончании приёма байта стоит DisSecondByte=1, но спустя 3 "бита" состояние возвращается в 0, и DisSecondByte туда же. И далее приёмник совершенно нормально принимает 16-битную посылку.

Ну и пора всё это опробовать "в железе".




Работает!

Сначала передаёт в байтовом режиме "ПрЮвет", затем начинает давать "дамп памяти" в 16-битном режиме (см 16-битный передатчик UART-окончание), а потом просто ждёт 16-битной посылки и тут же ретранслирует её назад.

Как видно, если я действительно посылаю 2 символа, ровно они и возвращаются. Попытался послать 3 символа - получил мусор, это нормально, передатчик макета начал посылку, пока ещё не окончилась передача с компьютера, вот и вышла коллизия.

А вот когда я послал 1 байт - не получил никакого ответа, зато на следующие 2 байта получил ровно их, т.е "восстановление" от случайно переданного байта действительно работает.

Неплохо: теперь я смогу "малой кровью" посылать на свой макет "исходные данные", матрицу аффинного преобразования и пр., не заморачиваясь на "склеивание" байтов в слова.

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

  • Так ли страшно 0,99969 вместо 1?

    В размышлениях о DMA, о возможных последствиях "смешивания" старых и новых значений при выдаче целевой информации, опять выполз вопрос: насколько…

  • DMA для QuatCore

    Вот фрагмент схемы нашего "процессорного ядра" QuatCore: Справа сверху "притаилась" оперативная память. На той ПЛИС, что у меня есть сейчас…

  • "МКО через UART" в железе - 2

    Продолжим проверять этот модулёк. "Для закрепления пройденного", снова запрашиваем телеметрию, сначала 1 слово данных (командное слово 0x3701, и…

  • "МКО через UART" в железе

    Долго мучали эту штуковину на симуляции, пора уже в макет это прошить и посмотреть, как оно себя поведёт. Оставлю "основную схему" (процессор, SPI,…

  • "Ошибка на единицу" при передаче CRC

    В прошлый раз обнаружили нехорошую вещь: при передаче данных от нашего устройства, CRC начинает работать одним словом раньше, чем надо, и результат…

  • "МКО" с CRC, работа над ошибками

    В кои-то веки всё соединили вместе, начали тестировать и обнаружили две проблемы: - не обеспечивается паузы между принятым сообщением и…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 1 comment