nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

"16-битный" приёмник UART

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

Сейчас немножко посмотрел на осциллографе, как работает USB-RS485 хренька за 300 рублей, на основе чипа CH341. Как оказалось, он НИКАКИХ ПАУЗ между байтами НЕ ДЕЛАЕТ. Его просишь передать "посылку" - и она идёт сплошняком. Кажется, что и здесь можно не заморачиваться и представить два байта как одну посылку, посередине которой располагаются стоповый и стартовый биты. Но здесь это чревато: скорость передачи должна довольно точно поддерживаться и соответствовать номиналу, иначе получим сбои синхронизации...


Вот код модуля приёмника UART, который сейчас используется в QuatCore:

//приёмник UART, самый простой
//который ловит первый нолик, после чего отсчитывает +1/2 периода, чтобы натыкаться на серединки импульсов.
`include "math.v"

module BetterUARTreceiver(	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...
	
	wire [DividerBits-1:0] FD; //Frequency Divider
	
	wire ce = ((FD & Limit) == Limit);
	
	assign HasOutput = isStopState & ce & rxd; //т.е приняты уже все данные, а стоповый бит ожидаемо высокий.
	//у нас как раз есть 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 [7:0] SR;
							
	always @(posedge clk) if (ce)
		SR <= {rxd, SR[7:1]}; //всегда заносим, пофиг, старт, стоп или данные			
		
	assign Q = SR;
	
	
endmodule


Как водится, приёмник устроен сложнее, чем передатчик. Пока на входе поддерживается лог. "1", сохраняется состояние Idle. Когда первый раз появляется нолик, переходим в состояние Start. Там мы такт за тактом проверяем, что нолик продолжается, это не была какая-то случайная помеха. Если снова проскакивает "единица", возвращаемся в Idle.

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

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

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

Давайте, ради эксперимента, быстренько превратим его в 16-битный драндулет, также, как превратили передатчик. Сдвиговый регистр был 8-битным, чтобы вместить ровно биты данных, а теперь должен стать 18-битным, чтобы вместить 16 бит данных и ещё два бита посередине - стоповый и стартовый. Можно пытаться сделать логику, запрещающую работу сдвигового регистра в нужный момент, но она выйдет куда сложнее 2 ЛЭ, и сложнее отлаживать. А так - дёшево и сердито.

Расширяем размер выхода до 16 бит, и соединяем их с нужными 16 битами регистра.

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

Получается такая хреновина:
//приёмник UART, самый простой
//который ловит первый нолик, после чего отсчитывает +1/2 периода, чтобы натыкаться на серединки импульсов.
`include "math.v"

module UART16bitReceiver(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 	=	5'b00000;
	localparam sStart0	=	5'b01100;
	localparam sB0	    	=	5'b01101;
	localparam sB1	    	=	5'b01110;
	localparam sB2		=	5'b01111;
	localparam sB3		=	5'b10000;
	localparam sB4		=	5'b10001;
	localparam sB5		=	5'b10010;
	localparam sB6		=	5'b10011;
	localparam sB7		=	5'b10100;
	localparam sStop0	=	5'b10101;
	localparam sStart1	=	5'b10110;
	localparam sB8		=	5'b10111;
	localparam sB9		=	5'b11000;
	localparam sBA		=	5'b11001;
	localparam sBB		=	5'b11010;
	localparam sBC		=	5'b11011;
	localparam sBD		=	5'b11100;
	localparam sBE		=	5'b11101;
	localparam sBF		=	5'b11110;
	localparam sStop1	= 	5'b11111;

	wire [4:0] State;
	wire isStopState;
	wire isIdle = (~State[4]) & (~State[3]); //shortcut as not all states are used
	wire isStart = (~State[4]) & (~State[1]) & (~State[0]);	//small shortcut...
	
	wire [DividerBits-1:0] FD; //Frequency Divider
	
	wire ce = ((FD & Limit) == Limit);
	
	assign HasOutput = isStopState & ce & rxd; //т.е приняты уже все данные, а стоповый бит ожидаемо высокий.
	//у нас как раз есть 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 = 5,
		StateMachine.lpm_svalue = sStart0;


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


Для очистки совести проверим его на симуляции, соединив с 16-битным передатчиком:


Передаём слово 0x1234. Смотрим на симуляции:


Да, здесь, когда частоты передатчика и приёмника строго совпадают, и никаких помех нет, всё чётко. Как можно видеть, новое значение в сдвиговый регистр "защёлкивается" ровно на середине очередного передаваемого бита, endian совпадает. Может несколько удивить, что значение 0x1234 продержалось на выходе всего полбита, после чего заместилось "мусором", но это нормально: ровно в момент выдачи HasOutput = 1 оно "защёлкнется" куда-то дальше, и хранить его больше не надо.

Самое время проверить эту хреновину "в железе".

Для этого нужно написать программку для QuatCore. Для начала, пусть она принимает одно 16-битное слово и тут же переправляет его назад, эдакое "эхо". Вот так как-то:

@@TxRxLoop:  C    IN
             OUT  C
             JMP  @@TxRxLoop


Но делать программу в 3 строки мне совсем не хочется - компилятор всерьёз решит, что нужно сделать 2-битную шину адреса ROM и вообще убрать шину RAM (здесь мы память не используем), сформирует соответствующие файлы QuatCoreCode.mif и QuatCoreData.mif, и разбирайся, что там квартусу в этом не понравится, не любит он "вырожденные случаи". Поэтому я их просто добавил в конец наших "алгоритмов захвата". Сначала оно считается, потом выдаёт "ПрЮвет" в 8-битном режиме, переключается в 16-битный режим, выдаёт результаты работы - а потом уже эти 3 строки, чтобы проверить UART. Почему бы и нет...

Компилятор предусмотрительно вставляет NOP между двумя строками, чтобы не было RAW Hazard (чтение из IN в рег. C одновременно с записью его значения в OUT, СТАРОГО значения), это хорошо.

Запускаем - и видим что-то совсем нехорошее. На сообщения в 2 байта ответа нет. На более длинное, в 3 байта - ответ всегда один и тот же, 7FFF.

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

Вот блин... Тогда пробую запустить старую добрую программу HelloUserName.asm, может хоть она заработает? Сообщение "Как вас зовут?" она вывела, на мой ответ не отреагировала. Приплыли.

С этой программой у меня опять параметры не обновились. Вижу несколько Critical Warnings, что по проекту объём памяти 512 слов, а в файле .mif - всего 64. И точно так же, только про 256/64. А это значит: я "на верхнем уровне" задал RomWidth и RamWidth, а "вглубь" они так и не проникли, остались старые! Глянул и другие параметры после компиляции, а там EnableIO=0. Ага, а я думаю - чего ж он не реагирует??? Ладно, выставляю параметры "ручками" и уровнем ниже, синтезирую - опять ни в какую. Но это я вспомнил, что данная конкретная программка у меня принимала байты, пока не встретит "спецсимвол" от 0 до 13, какой-нибудь CR или LF или просто ноль. А я в терминале отключил добавление CR, LF либо CR+LF. Включил - РАБОТАЕТ. Ну хоть так.

Теперь возвращаемся к текущей программе, пока что с 8-битным, "старым" приёмником. Проверил все параметры, всё нормально, но уходит в бесконечный цикл.

Возникает версия: у нас сейчас приёмник и передатчик UART - два независимых устройства, подсоединённые к приёмопередатчику RS485. Приёмник как-то неадекватно реагирует на нашу же незавершённую передачу! Вспомним, как оно всё соединено:


И самое главное, что эта микросхема ADM3485ARZ подаёт на RO (Receiver Output) во время работы передатчика?

В даташите находим, что при nRE=1 (выключили приёмник) этот выход переходит в Z-состояние. И я даже когда-то сделал соответствующую ножку ПЛИС двунаправленной: на вход, когда у нас передатчик отключён, и на выход ("чтобы не висела в воздухе"), когда включён. Только вот за каким-то лешим я его тащил "на ноль":


Ну привычка такая, любые висящие провода замыкать на ноль... Вот и выходило, что всё то время, пока работает передатчик, у нас на приёмнике появляется сплошной ноль. Приёмник считает, что началось сообщение (стартовый бит "0"), честно его получает, проверяет стоповый бит - он тоже нулевой (а должна быть 1) - выбрасывает нафиг. И так несколько раз.

А В САМОМ КОНЦЕ ПЕРЕДАЧИ ему-таки может повезти, прочитать вполне себе разумное сообщение. Это тут же снова запустит передатчик, и тут же начнём ждать следующее сообщение, и оно опять появится по окончании работы передатчика, ну и пошло-поехало.

Что ж, по аналогии со злым модулем SinkToGround:

module SinkToGround (input DoSink, output Q);

	assign Q = DoSink? 1'b0 : 1'bz;
	
endmodule


сделаем SinkToVcc:
module SinkToVcc (input DoSink, output Q);

	assign Q = DoSink? 1'b1 : 1'bz;
	
endmodule


и поставим его взамен. Запускаем:


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

Ну и теперь пробуем всё-таки 16-битный вариант приёмника:


Когда передаю два байта, ответа не получаю, т.е такую посылку приёмник "бракует". Это намекает на заниженную скорость передачи, из-за чего мы при проверке стопового бита (после 2 байт) попадаем ещё на последний бит данных, который в этих посылках был нулевым. В UART последним передаётся СТАРШИЙ бит, и при передаче латинских букв он всегда нулевой.

Давайте попробуем русские буквы:


Теперь приём происходит. Как видим, с первым байтом всё в порядке, а второй приходит со сдвижкой в один бит.

Отправлен: DF = 1101_1111
Получен:   BE =  101_11110

Отправлен: A8 = 1010_1000
Получен:   50 =  010_10000

Отправлен: D5 = 1101_0101
Получен:   AA =  101_01010


Как вариант, на большой скорости передачи всё-таки вводится пауза между байтами, оттого всё и отстаёт.

Приём будет стабилен до тех пор, пока к стоповому биту мы не уедем более чем на половину длительности бита в любую сторону. Вся посылка - это 10 бит (стартовый бит, 8 бит данных, стоповый бит), поэтому разница в 0,5 длительности бита - это аккурат 5%, "стандартно". Если же воспринять первый стоповый и последующий стартовый биты как просто "биты данных", и продолжить точно так же получать следующие биты - это потребует точности передачи уже в 2,5%. Причём она будет складываться как из точности работы самого передатчика, так и из точности измерения нами этих длительностей.

До сих пор я делил тактовую частоту "нацело". Например, тактовая частота 25 МГц, хочу 921600 бод, одно делим на второе - получаем 27,12. Округляем до 27 - и получаем на деле 925926 бод, разница 0,5%. Но ещё читаем технические характеристики Ethernet-контроллера ENC624J600 (я с него беру тактовую частоту для ПЛИС), и там находим CLKout stability ±0,25%. Итого, по наихудшему случаю, выходит ошибка 0,75%, и на передатчик остаётся 1,85%, практически без запаса. Я честно прочитал datasheet на микросхемку CH341, которая применена в USB-RS485 хреньке за 300 рублей:


и НЕ НАШЁЛ НИКАКОЙ ИНФОРМАЦИИ О ТОЧНОСТИ ПОДДЕРЖАНИЯ СКОРОСТИ. "Хуже 1,85%" - догадался Штирлиц!

Можно как-нибудь прикупить или спаять конвертер RS485 - RS232, попробовать к "железному" COM-порту подключиться, есть он у меня в компьютере:


Зелёные - это COM, большой чёрный - это LPT :)


Что ж, по крайней мере с дешёвым переходником USB-RS485 эта хрень правильно работать не желает. Придётся сделать чуточку сложнее. Продолжение следует...
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

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

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

  • Как продлить агонию велотрансмиссии на 1500+ км

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

  • DMA для QuatCore

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

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

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

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

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

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

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

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 5 comments