nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

Полудуплексный приёмопередатчик UART, 3 года спустя :)

Когда я только взялся за ПЛИС 3 года назад, соорудил передатчик UART на 27 ЛЭ (если делитель частоты на 8 бит), или 28 ЛЭ, если st=1 во время передачи предыдущего байта не должен перезапустить передачу, а просто должен игнорироваться.

Год назад вернулся к этому делу, и, как ни странно, меньше этот модуль не стал, при том же 8-битном делителе частоты получилось 30 ЛЭ, зато выход в кои-то веки стал регистровым, а не комбинаторным, гарантируя, что в линию не пойдёт высокочастотный "мусор" в некоторых местах (а он реально мешал передаче на 921 600 бод, и всё исправилось, когда выход стал напрямую с регистра). И в целом схема стала чрезвычайно "скоростной", даже на нашей медленной ПЛИС 5576ХС4Т и максимальном делении частоты с 80 МГц до 300 бод, предельная частота указывается в 96,15 МГц. (чем толще делитель частоты - тем ниже становится его предельная частота, особенно, если использовать компаратор для счёта "не до конца", и не защёлкивать его выход в регистр, прежде чем сбросить). Ну и нехорошая багофича, когда при первом же включении этот модуль начинает передавать, правда, сплошные единицы, была исправлена. В принципе, можно попытаться пару ЛЭ отыграть назад, если тактовая частота невелика, а потому делать схему "скоростной" не требуется, но есть риск, что в крупных проектах это усложнит жизнь фиттеру. А так он этот несчастный UART может "по остаточному принципу" впихнуть куда угодно, "размазать тонким слоем" по всему кристаллу :)

Похожая история с приёмником UART на 33 ЛЭ (3 года назад), и столько же (при делителе в 8 бит) занимает куда более новый приёмник UART для QuatCore.

Но тогда, 3 года назад, я ещё одного монстра сделал, полудуплексный приёмопередатчик, это "сиамские близнецы" из приёмника и передатчика, использующие общий делитель частоты, сдвиговый регистр и конечный автомат, что оправданно, если одновременный приём и передача невозможны. А именно так и есть на RS485! За счёт этого "совместного использования" ресурсов получилось сделать его размер 46 ЛЭ (при 8-битном делителе), что несколько меньше, чем 60 ЛЭ, которые получались при двух отдельных модулях.

Впрочем, мне этот модуль много крови попил, и я к нему не возращался, ДО СЕГОДНЯШНЕГО ДНЯ.

А сейчас стало интересно ещё разок попробовать его сделать, причём с "заделом" под CRC, и посмотреть, что из этого получится...


Давайте выпишем заголовок этой хреновины для начала:

module HalfDuplexUART(	input clk, input rxd, input [7:0] D, input startTX, input isCRC, input CRC_bit,
			output [7:0] Q, output HasOutput, output FrameError, output LineBreak,
			output txd, output RW, output isDataBit);	

	parameter CLKfreq = 80_000_000;
	parameter BAUDrate = 512_000;


Приёмник RS485 подключён к входу rxd. Когда надо передать байт, нужно его поместить в D и выдать startTX=1. Но если isCRC=1, то вместо данных из D будет бит за битом отправляться значение с входа CRC_bit.

Когда приёмник успешно получает очередной байт, он появляется на Q, и выдаётся HasOutput=1. FrameError и LineBreak - сообщения об ошибках (FrameError: вовремя не получен стоповый бит, LineBreak: при этом все данные были нулевыми, т.е 9 ноликов подряд, скорее всего обрыв), которые мы пока не особо приспособили куда-нибудь.

Передатчик RS485 подключается к выходу txd, а переключение между приёмом и передачей - к выходу RW (единица когда на передачу, ноль когда на приём). Это же значение RW=1 используется, чтобы проконтролировать занятость передатчика. И наконец, isDataBit=1 в тот момент, когда передаются именно полезные данные, а не стартовые или стоповые биты - это нужно для вычисления CRC. Причём, isDataBit=1 длится ровно один такт для каждого бита.

Параметры задали "левые", лишь бы получился 8-битный делитель частоты, просто для честного сравнения с другими схемами. Потом зададим свои 25 МГц тактовую и 921600 бод скорость передачи.

Попробуем применить те же 11 состояний конечного автомата, что использовались в недавних модулях приёмника и передатчика:
	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;


Мотивация такая: когда используешь в качестве конечного автомата обычный счётчик lpm_counter, он всегда инициализируется нулём, поэтому sIdle хорошо бы поставить в нуле. Когда запуск произведён, переход между состояниями удобно делать как прибавление единички, поэтому пущай они стоят друг за другом, причём по окончании мы должны "своим ходом" возвращаться в sIdle. Вот и всё, как будто бы без вариантов! А запуск делается с помощью sset (синхронная установка константы), так что "прыгаем" из sIdle в sStart.

3 года назад я пытался действовать более хитро, хотел, чтобы у всех 8 состояний, где передаются данные, должна быть единичка в старшем бите, этот старший бит управлял выходом (либо подключался сдвиговый регистр, либо выбирался ноль, либо единица). Потом оказалось, что этот признак "передаются данные" и не нужен особо - просто сдвиговый регистр расширяем на один бит, а при каждом сдвиге вдвигаем туда единицы, вот и дело с концом.

Сейчас признак "передаются данные" всё-таки пригодится для управления CRC, чтобы в него вдвигались только данные, а не стартовые/стоповые биты. Но он не так уж и страшен и здесь:

	assign isDataBit = (State[3] | State[0])&(~isStopState)&ce;	


Как видно из "простыни" выше, когда старший бит состояния нулевой, данные передаются только в одном случае, когда младший бит состояния единичный. Затем с помощью единичного старшего бита мы "захватываем" оставшиеся 7 бит данных, но ещё нужно исключить стоповый бит. Это делается проводом isStopBit, который подключён к выходу cout (carry out) счётчика StateMachine.

Ну и для того, чтобы при передаче каждого бита провод isDataBit "зажёгся" ровно на один такт, сюда ещё добавлен ce (clock enable).

Разберёмся быстренько с делителем частоты. Его мы нагло копируем из недавнего приёмника UART. Сначала величины, вычисляемые на этапе синтеза:

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


Определяем, во сколько раз делить частоту, поделив входную (CLKfreq) на выходную (BAUDrate) с округлением до ближайшего целого.

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

Ну и сам делитель частоты опишем:
wire [DividerBits-1:0] FD; //Frequency Divider
wire ce = ((FD & Limit) == Limit);

	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;


Такое исполнение получается компактнее, чем если он всегда будет досчитывать до упора (и ce подключено к выходу cout), но можно будет выбрать загрузку двух разных начальных значений. А так мы либо его сбрасываем, либо загружаем константой, а досчитывает он до конкретного места. Благодаря тому, что дальше он считать никогда не может, проверку на точное равенство (где надо нули, где надо единицы) можно заменить на более мягкую проверку, что все единицы на месте, именно это и делает строка (FD & Limit) == Limit, см. счетчики и жаба. Управляющие сигналы HalfFreqDivider и ZeroFreqDivider мы пока не сформировали.

Давайте выход RW сделаем сразу регистровым, именно он и будет "помнить", передаём ли мы байт или принимаем его:
module HalfDuplexUART(	input clk, input rxd, input [7:0] D, input startTX, input isCRC, input CRC_bit,
			output [7:0] Q, output HasOutput, output FrameError, output LineBreak,
			output txd, output reg RW = 1'b0, output isDataBit);


Начать передачу мы должны, если были в состоянии sIdle, и пришло StartTX=1. Тогда установится RW=1. А вернуться в нолик оно должно, когда передадим стоповый бит и готовы будем вернуться в sIdle. То есть, можно записать вот так:

always @(posedge clk)
	RW <= (startTX & isIdle)? 1'b1 : (isStopState & ce)? 1'b0 : RW;


Определим вообще состояние State и несколько полезных "логических функций" от него:
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...


Ну и поразмыслим о самом конечном автомате, т.е по каким воздействиям мы переходим из одного состояния в другое. Начинаем, вестимо, с sIdle. Из него можем перейти только в sStart, и произойдёт это либо при поступлении StartTX (запрос на передачу байта), либо если на входе rxd появится нолик, т.е стартовый бит. Но делитель частоты в этих двух случаях должен запуститься по-разному: при передаче просто "сняться с тормозов", а во время приёма перепрыгнуть на LimitDiv2, чтобы отсчитать лишь половину стартового бита.

Далее, мы сидим в sStart, пока не придёт ce=1, т.е не будет отсчитан выбранный интервал. В режиме передачи мы просто перешагиваем в следующее состояние, sB1. В режиме приёма, в нормальной ситуации, когда снова принят нолик (в центре стартового бита) - тоже. Но если там оказалась единица (не было нормального стартового бита, вместо него какая-то импульсная помеха) - возвращаемся в sIdle.

В состояниях от sB1 до sB8, как ни странно, ведём себя одинаково и на приёме, и на передаче: дожидаемся ce=1 для перехода к следующему состоянию.

И наконец, из sStop мы тоже по ce=1 уходим в sIdle.

Как ни странно, особых затруднений тут нет, записываем конечный автомат:
	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;


Нужно ещё сформировать сигналы, управляющие делителем частоты:
	wire ZeroFreqDivider = isIdle & rxd | ce;
	wire HalfFreqDivider = isIdle & ~rxd & ~RW;


В состоянии sIdle, пока на вход rxd (физический приёмник RS485) поступает единичка, делитель частоты непрерывно обнуляется, а значит и ce=1 сформироваться не может. Если поступит startTX=1, состояние переключится в sStart, и обнуление прекратится, счётчик заработает. Также он заработает, если на вход rxd придёт нолик (стартовый бит). В таком случае мы обязаны выставить ZeroFreqDivider=0, поскольку вход сброса самый приоритетный. И в этот же момент поставить HalfFreqDivider=1. Возможно, (~RW) можно оттуда убрать, т.к я делал ножку rxd двунаправленной в ПЛИС, чтобы во время работы передатчика, когда микросхемка ADM3485ARZ переводит выход своего приёмника в Z-состояние, мы её "перехватили" и установили в лог. "1".

Кстати, давайте прямо сюда включим эту логику, ещё разок переписав заголовок модуля:

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

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


Вот теперь можно убрать ~RW в условии для HalfFreqDivider :)

Управляющую часть как будто бы и закончили! Теперь нужен Data Path, не знаю устоявшегося русского термина. Википедия считает: операционный автомат, ну допустим.

Хочу снова применить 9-битный сдвиговый регистр, но сейчас он чуточку усложнится. 8 бит из него будут "выведены наружу" как Q - принятые данные. Самый старший бит в передатчике "вдвигал" в себя единицы, либо зашёлкивал D[7], когда начиналась очередная передача байта. А в приёмнике он "вдвигал" в себя rxd. Учитывая, что когда RW=1, rxd также мы жёстко приравняли к единице, два этих варианта элементарно объединяются:

SR[8] <= (StartTX & isIdle)? D[7] : rxd;


Следующие 7 бит - самые простые. Они работают в двух режимах, либо параллельная загрузка D[6:0], либо сдвиг:
SR[7:1] <= (StartTX & isIdle)? D[6:0] : SR[8:2];


И, наконец, самый младший бит. Если пока что проигнорировать входы для CRC, тоже всё просто:
SR[0] <= (StartTX & isIdle)? 1'b0 : SR[1];


Любые телодвижения осуществляются только по сигналам ce=1, либо по сигналу startTX=1 в состоянии sIdle. Так что можно выписать весь код для сдвигового регистра:

	reg [8:0] SR = 1'b0;
							
	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 : SR[1];
	end
		
	assign Q = SR[8:1];
	assign txd = SR[0];


Данный приёмопередатчик заточен конкретно под RS485, где при подаче RW=0 активен приёмник и выключен передатчик, никак не реагируя на биты, идущие по txd, и наоборот. Если этот модуль поставить в дуплекс (например, к микросхемке CP2102), надо будет чуть усложнить цепи rxd и txd - заменить двунаправленный вывод rxd на вход, но с элементов ИЛИ (чтобы при передаче мы игнорировали идущее по нему), и ещё один элемент ИЛИ поставить на выход, чтобы во время приёма никакой "мусор" не шёл на передачу.

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


Пока что получается вот так:
`include "math.v"

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

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

	parameter CLKfreq = 80_000_000;
	parameter BAUDrate = 512_000;

	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...
	
	wire [DividerBits-1:0] FD; //Frequency Divider
	
	wire ce = ((FD & Limit) == Limit);
	//assign isDataBit = (State[3] | State[0])&(~isStopState)&ce;	
	
	always @(posedge clk)
		RW <= (startTX & isIdle)? 1'b1 : (isStopState & ce)? 1'b0 : RW;
	
	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) | 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;
	
	reg [8:0] SR = 1'b0;
							
	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 : SR[1];
	end
		
	assign Q = 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


Я даже убрал (закомментировал) выражение isDataBit, чтобы сравнивать "один к одному" новую реализацию со старой, трёхлетней давности.

Запускаем синтез - и получаем 40 ЛЭ, тогда как раньше выходило 45. Всё-таки чему-то научился, похоже :)

Правда, предельная частота 78,13 МГц. Пока я играюсь с 25 МГц, этого "за глаза", но вдруг когда-нибудь захочу запитать ПЛИС от генератора 80 МГц, который там присутствует на отладочной плате?

Сделаем ce (clock enable) регистром:

reg ce = 1'b0;
always @(posedge clk)
	ce <= ((FD & Limit) == Limit);


И ещё нужно заменить выражение для Quotient, вычесть единичку (т.к сигнал о достижении лимита задерживается на один такт):
localparam Quotient = ((CLKfreq + BAUDrate/2) / BAUDrate) - 1;


Теперь синтез даёт 39 ЛЭ (!) И предельная частота 100 МГц - это меня более чем устраивает :)

Осталось только узнать: а оно вообще работает как положено? Для этого выставим наши параметры CLKfreq = 25 МГц, BAUDrate = 921600 и отсинтезируем ещё разок, это даёт 36 ЛЭ. И ещё я всё-таки инииализирую сдвиговый регистр всеми единицами. Это не играет особой роли: пока RW=0, передатчик всё равно отключён, но так приятнее, иначе неспокойно :) В общем, на передачу всё хорошо:


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


Короткий и совсем безобидный комбинаторный выброс.

А чтобы проверить приём, давайте два таких драндулета соединим друг с другом:


Ну и поглядим, что из этого вышло:


Да, что отправили - то и пришло. И тут HasOutput - уже не комбинаторный пичок, а вполне себе сигнал на один такт:


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


Неплохое начало: побили свой собственный рекорд в 46 ЛЭ (при 8-битном делителе), теперь всё то же самое умещается в 39 ЛЭ, имеет предельную частоту 100 МГц, регистровый выход (без "комбинаторных" помех). А когда тактовая частота 25 МГц и скорость передачи 921 600 бод, и вовсе 36 ЛЭ. А убрать выходы FrameError и LineBreak - будет 32 ЛЭ (!)

Теперь попробовать подсоединить CRC, и достроить с 8 до 16 бит. Но это уже в понедельник.
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 

  • 1 comment