nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

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

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

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

Прежде чем двигаться дальше, надо это дело исправить, по возможности малой кровью.


Начнём со второй проблемы. "вывели" наружу сигналы transmitCRC и computeCRC, которые формирует протокольный контроллер (две нижние строки), и crc_error, которую формирует приёмопередатчик (третья снизу). Ещё 19-битный сдвиговый регистр приёмопередатчика я вывел, но понапрасну, там всё правильно.


Сигналы transmitCRC и computeCRC работают, как ожидалось. В состоянии sIdle оба нулевые. Когда computeCRC=0, регистры CRC начинают работать как самый простой сдвиговый регистр (без обратных связей с XOR), выталкивая наружу бит за битом. И в то же самое время идёт проверка, совпадают ли входные биты (идущие с приёмника) с выходом CRC, и если хоть один бит не совпал - "зажигается" crc_error=1.

Таким способом удалось совместить две задачи, которые "не пересекаются", не мешают друг другу. Режим простого сдвигового регистра нужен, чтобы обнулить CRC, а также чтобы выдать его на линию, в конце сообщения. Сравнение нужно, чтобы определить целостность принятого сообщения.

У нас в CRC лежали нули, и "выталкиваться" стали нули, поэтому crc_error = 1 наступил на первой же принятой "единичке". Это нормально, так и было задумано.

Но вот чего я не рассчитал - это переходного процесса из sIdle в sReceive. Сигнал computeCRC комбинаторно зависит от State[0], поэтому только когда состояние переключится в sReceive, выставится computeCRC=1. Это приведёт к СИНХРОННОМУ сбросу регистра crc_error, т.е он сбросится лишь К СЛЕДУЮЩЕМУ ТАКТУ. А вот код для MessageError:

always @(posedge clk)
	MessageError <= isIdle? isCWerror : (isFirstDataWord & DataValid & RXisData & (D != TxMux)) | CRCerror | MessageError;


На первом же такте в состоянии sReceive, если crcError=1, то и MessageError тут же установится в единицу!

Впрочем, "осциллограмма" показывает ещё более забавное поведение crc_error: ошибка не сбрасывается к следующему такту, как только computeCRC переключилось в единицу. Сбрасывается она сильно дольше. Если отследить, становится ясно: она сбрасывается по первому поступившему биту данных из следующего слова!

И если ещё разок глянуть в код CRC_for_VIPS, станет ясно, почему:

  always @(posedge clk)	if (clk_en) begin
		SR[0]  <= FB;
		SR[1]  <= SR[0];
		SR[2]  <= SR[1];
		SR[3]  <= SR[2];
		SR[4]  <= SR[3];
		SR[5]  <= SR[4] ^ FB;
		SR[6]  <= SR[5];
		SR[7]  <= SR[6];
		SR[8]  <= SR[7];
		SR[9]  <= SR[8];
		SR[10] <= SR[9];
		SR[11] <= SR[10];
		SR[12] <= SR[11] ^ FB;
		SR[13] <= SR[12];
		SR[14] <= SR[13];
		SR[15] <= SR[14];
		crc_error <= crc_en? 1'b0 : (D ^ SR[15]) | crc_error;
	end


Ага, crc_error, как и все прочие регистры этого модуля, может изменяться только по прибытии очередного бита данных!

В чём проблема - понятно. Как её исправить "малой кровью" - не вполне. Можно усложнить выражение computeCRC, чтобы оно не просто оставалось нулём в режиме sIdle, но в момент прихода слова переключалось в единицу. А ещё разрешить модулю CRC сбрасывать регистр crc_error в любой момент, а не только по прибытии бита данных.

Либо оставить всё "как есть", но поставить дополнительное условие на crc_error. К примеру, crc_error & (~isFirstDataWord). Здесь isFirstDataWord - однобитный регистр, который мы уже ввели, чтобы отслеживать первое слово данных (для проверки заголовков массивов). Тогда crc_error заведомо успеет установиться в ноль, а мы в любом случае ожидаем хоть одно слово данных перед CRC, так что успеет и туда установиться, и сюда. Давайте действительно попробуем:

MessageError <= isIdle? isCWerror : (isFirstDataWord & DataValid & RXisData & (D != TxMux)) | CRCerror & ~isFirstDataWord | MessageError;


То же самое можно ещё так написать:
MessageError <= isIdle? isCWerror : (isFirstDataWord? DataValid & RXisData & (D != TxMux) : CRCerror) | MessageError;


Вроде логичнее выходит: в состоянии sIdle проверяем предполагаемое командное слово (верна ли комбинация признака "приём/передача" и подадреса), на первом слове данных проверяем заголовок, на последующих: CRC.

Синтезируется вся тестовая схема (включая "контроллер шины", Bus Controller) в 297 ЛЭ, на один больше, чем до исправления ошибки. Предельная частота 47,39 МГц, причём "критический путь" - от конечного автомата HalfDuplexUART до регистра MessageError. Немудрено. Если совсем "припрёт", наверняка можно будет это выражение вычислять за несколько тактов, вот совершенно "не горит". А пока устраивает...

Проверяем, что из этого выходит:


Да, в этот раз MessageError остаётся нулевым, пока принимаются слова данных 0xAAAA (заголовок массива), 0x0001 и 0x0002. Команды на запись идут, как ни в чём не бывало. А вот к последнему слову данных 0x0003, как видно включается computeCRC = 0 (уже посчитали, теперь сверяем), transmitCRC = 1 (пофиг, передатчик отключён). И довольно быстро выскакивает crc_error = 1, а следом и MessageError = 1.

Глянем повнимательнее на последнее слово данных и ответное слово:


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

Ответное слово: 0x3400, что означает "адрес 6, ошибка в сообщении". Всё верно!

Давайте теперь попробуем послать правильный CRC, убедиться, что тогда всё сработает.

Переправим transmitCRC на единицу в Vector Waveform File (мы почти что "вручную" входные сообщения генерим, лениво ещё и контроллер шины полноценный писать, для одного лишь этого теста!), и попробуем снова:


Да, в этот раз до самого окончания приёма MessageError так и остался нулевым (в единицу он "зажёгся" только в режиме sIdle, там можно!), в память записалось и последнее слово данных (собственно, CRC), и было отправлено ответное слово 0x3000, т.е тот же адрес 6 (это наш адрес), но уже без признака ошибки.

Кажись, одну проблему исправили. Ну, мы ещё не проверяли отправку информации - правильно ли там CRC сформируется, но на рассмотренном сценарии пока всё хорошо...

Теперь давайте громко подумаем о паузе между сообщениями. Мне страшно не хочется дополнительный счётчик вводить, который отмеряет от 50 до 250 тактов (2-10 мкс). Конечно, не так это и много, 6..8 бит, плюс какая-никакая обвязка. Но красивее было бы задействовать имеющиеся счётчики. В принципе, можно всё те же "часы реального времени" замучать, если младший делитель частоты ещё на две части разделать. У нас там 25 МГц сразу делилось в 4883 раза, чтобы давать импульс примерно каждые 195 мкс, это 13-битный счётчик. А давайте сначала будем делить в 61 раз (импульс каждые 2,44 мкс), а потом ещё в 80 раз (импульс каждые 195,2 мкс). На деле мы хотели 100 мс / 512 = 195,3125 мкс, но так и быть, ошибёмся на 112,5 нс. Это за 100 мс, которые проходят между импульсами "Синхронизация (с СД)", накопится ошибка аж на 58 мкс, то есть меньше половины младшего разряда - мы ничего не почувствуем! Я сейчас игрался с двумя делителями, смотрел, где получится самое близкое к 195,3125 мкс значение. Начал с первого делителя в 50 раз (период 2 мкс), дошёл до 64 (период 2,56 мкс), и наилучшее совпадение было при делении в 61 раз. При этом, на первый делитель нужно 6 бит, на второй: 7 бит, то есть те же самые 13.

Вот старый код модуля QuatCoreRTC:
module QuatCoreRTC (input clk, input sync, input [15:0] D, input Mark, output reg [15:0] Q = 1'b0, output tick);

	
	wire [15:0] Time;
	wire LowCounterCout;
	wire ce_195us;
							
	lpm_counter HighCounter (
				.clock (clk),
				.cnt_en (LowCounterCout & ce_195us),
				.data (D[6:0]),
				.sload (sync),
				.q (Time[15:9]) );
	defparam
		HighCounter.lpm_direction = "UP",
		HighCounter.lpm_port_updown = "PORT_UNUSED",
		HighCounter.lpm_type = "LPM_COUNTER",
		HighCounter.lpm_width = 7;
		
	lpm_counter LowCounter(
				.clock (clk),
				.cnt_en (ce_195us),
				.sclr(sync),
				.q (Time[8:0]),
				.cout (LowCounterCout));
						
	defparam
		LowCounter.lpm_direction = "UP",
		LowCounter.lpm_port_updown = "PORT_UNUSED",
		LowCounter.lpm_type = "LPM_COUNTER",
		LowCounter.lpm_width = 9;
		
	always @(posedge clk) if (Mark)
		Q <= Time;		
		
	lpm_counter Divider (
				.clock (clk),
				.cnt_en (1'b1),
				.sclr (sync),
				.sset (ce_195us),
				.cout (ce_195us) );
	defparam
		Divider.lpm_direction = "UP",
		Divider.lpm_port_updown = "PORT_UNUSED",
		Divider.lpm_type = "LPM_COUNTER",
		Divider.lpm_width = 13,
		Divider.lpm_svalue = 3309;

	assign tick = ce_195us;

endmodule


Ну и делим делитель :) Выходит так:

module QuatCoreRTC (input clk, input sync, input [15:0] D, input Mark, output reg [15:0] Q = 1'b0, output tick, output ce_2_44_us);

	
	wire [15:0] Time;
	wire LowCounterCout;
	wire ce_195us;
							
	lpm_counter HighCounter (
				.clock (clk),
				.cnt_en (LowCounterCout & ce_195us),
				.data (D[6:0]),
				.sload (sync),
				.q (Time[15:9]) );
	defparam
		HighCounter.lpm_direction = "UP",
		HighCounter.lpm_port_updown = "PORT_UNUSED",
		HighCounter.lpm_type = "LPM_COUNTER",
		HighCounter.lpm_width = 7;
		
	lpm_counter LowCounter(
				.clock (clk),
				.cnt_en (ce_195us),
				.sclr(sync),
				.q (Time[8:0]),
				.cout (LowCounterCout));
						
	defparam
		LowCounter.lpm_direction = "UP",
		LowCounter.lpm_port_updown = "PORT_UNUSED",
		LowCounter.lpm_type = "LPM_COUNTER",
		LowCounter.lpm_width = 9;
		
	always @(posedge clk) if (Mark)
		Q <= Time;		

	lpm_counter LowDivider (
				.clock (clk),
				.cnt_en (1'b1),
				.sclr (sync),
				.sset (ce_2_44_us),
				.cout (ce_2_44_us) );
	defparam
		LowDivider.lpm_direction = "UP",
		LowDivider.lpm_port_updown = "PORT_UNUSED",
		LowDivider.lpm_type = "LPM_COUNTER",
		LowDivider.lpm_width = 6,
		LowDivider.lpm_svalue = 3;
		
	wire TC_195us;
	assign ce_195us = TC_195us & ce_2_44_us;
	lpm_counter HighDivider (
				.clock (clk),
				.cnt_en (ce_2_44_us),
				.sclr (sync),
				.sset (ce_195us),
				.cout (TC_195us) );
	defparam
		HighDivider.lpm_direction = "UP",
		HighDivider.lpm_port_updown = "PORT_UNUSED",
		HighDivider.lpm_type = "LPM_COUNTER",
		HighDivider.lpm_width = 7,
		HighDivider.lpm_svalue = 48;
				
	assign tick = ce_195us;

endmodule


Сделали "по классике", введя отдельно TC (Terminal Count) и ce (clock enable). Когда "старший" делитель доходит до значения 127, на его выходе cout появляется единичка, и остаётся единичкой 61 такт подряд, пока "младший" делитель не досчитает снова до 63, выдав импульс ce_2_44_us, который заставит "старший" делитель сдвинуться со значения 127. Именно поэтому выход cout выведен на "провод" TC_195us, и уже он объединяется по "И" с ce_2_44_us, и уже получившийся сигнал мы называем ce_195us. Не знаю, может тут можно и "схитрить", соединив точно так же cout и sset "старшего делителя". Тогда длительность отсчётов от 48 до 126 будет положенные 61 такт, а вот отсчёт 127 продлится всего лишь 1 такт!

А ведь можно ровно так и сделать: мы же "спешили" на 112,5 нс. А так, добавить один дополнительный такт в 40 нс - будем спешить только на 72,5 нс, мелочь, а приятно! Ну и логика самую чуточку упрощается. "По классике" у меня вся эта тестовая схема синтезировалась в 300 ЛЭ, до модификации "часов реального времени": в 296 ЛЭ. А если сделать вот так (привожу только делители):

	lpm_counter LowDivider (
				.clock (clk),
				.cnt_en (1'b1),
				.sclr (sync),
				.sset (ce_2_44_us),
				.cout (ce_2_44_us) );
	defparam
		LowDivider.lpm_direction = "UP",
		LowDivider.lpm_port_updown = "PORT_UNUSED",
		LowDivider.lpm_type = "LPM_COUNTER",
		LowDivider.lpm_width = 6,
		LowDivider.lpm_svalue = 3;
		
	lpm_counter HighDivider (
				.clock (clk),
				.cnt_en (ce_2_44_us),
				.sclr (sync),
				.sset (ce_195us),
				.cout (ce_195us) );
	defparam
		HighDivider.lpm_direction = "UP",
		HighDivider.lpm_port_updown = "PORT_UNUSED",
		HighDivider.lpm_type = "LPM_COUNTER",
		HighDivider.lpm_width = 7,
		HighDivider.lpm_svalue = 47;


Так аж 1 ЛЭ сэкономили, 299 вместо 300 :)

И теперь осталось придумать, как именно сделать эту паузу. В целом, используется тот же принцип с двумя регистрами, как для "таймаута" по сообщению, см. тестируем "МКО" с CRC. Можно продлить состояние sReply: сейчас оно проскакивает ровно за 1 такт, за время которого мы отправляем на передачу сформированное ответное слово. В этом случае дополнительный регистр нужен всего на 1 бит: он сбрасывается при переходе в sReply, устанавливается по приходу ce_2_44_us от "часов реального времени", а критерием перехода из sReply в sTransmit является единичное значение этого регистра и ce_2_44_us=1.

Правда, тогда нужно и выражение для startTX поменять, у нас это просто было State[1] (т.е непрерывно чего-то пересылаем в состоянии sReply и sTransmit), а теперь нужно его "заглушить" на sReply, пока не настанет время.

Старое выражение для конечного автомата и startTX:
always @(posedge clk)
	State <= 	FrameError? 	sIdle :
			isIdle? 	(BeginMessage? sReceive : sIdle):
			isReceive?	((DoWeNeedToTransmit | noWordsLeft)? (rBroadcast? sIdle : sReply) : sReceive):
			isReply?	sTransmit:
					(~DoWeNeedToTransmit | noWordsLeft | MessageError)? sIdle : sTransmit;
							
assign start 	= State[1];


Новый вариант:

reg OneTickLeft = 1'b0;

always @(posedge clk) begin
	OneTickLeft <= ~State[1]? 1'b0 : ce_2us | OneTickLeft;
	State <= 	FrameError? 	sIdle :
			isIdle? 	(BeginMessage? sReceive : sIdle):
			isReceive?	((DoWeNeedToTransmit | noWordsLeft)? (rBroadcast? sIdle : sReply) : sReceive):
			isReply?	((OneTickLeft&ce_2us)? sTransmit: sReply):
					(~DoWeNeedToTransmit | noWordsLeft | MessageError)? sIdle : sTransmit;
end
							
assign start 	= State[1] & (OneTickLeft&ce_2us | State[0]);


Как всегда, стараюсь делать условия "наименее жёсткими", позволяя регистрам "жить своей жизнью" в те моменты, когда необходимости в них нет. Скажем, OneTickLeft непрерывно держится в нуле в состояниях sIdle и sReceive, а в sReply и sTransmit защёлкивается в единицу, как только придёт ce_2us.

Теперь вся наша тестовая схема синтезируется в 302 ЛЭ, т.е в общей сложности добавление паузы раздуло наш проект на 6 ЛЭ. Очевидно, сделай мы отдельный счётчик, который бы эти 2 мкс отсчитал, только на него бы ушло 6 ЛЭ, не считая "обвязки". Считаю, малой кровью обошлись.

Ну и проверим формирование паузы:



Да, всё правильно! Перейдя в состояние 2 (sReply), мы дождались второго импульса от ce_2us, и только тогда начали отвечать.

Наблюдаем странные "махинации" с computeCRC: она "зажглась" на один такт перед тем, как мы перешли в sReply, без всякого толку. Это, я так понимаю, счётчик оставшихся слов дошёл до нуля, а у нас computeCRC включается когда остаётся одно слово. Как будто бы можно было не проверять самый младший бит - пущай и на единице, и на нуле "горит", но мы же уже выкинули самый старший бит 6-битного счётчика. Если ещё и младший выкинем - не сможем 0 от 32 отличить, и всё запорем. Так что хрен с ним.

И давайте ещё для очистки совести проверим передачу данных нашим оконечным устройством

Пусть это будет "телеметрия", подадрес 110002. И давайте запросим аж 3 слова телеметрии. Для этого посылаем командное слово 0x3703 (адрес ОУ 6, K=1, т.е передача от ОУ к КШ, подадрес 110002, число слов 3):


Командное слово передали - и ВООБЩЕ ТИШИНА, состояние как было sIdle, так и осталось... Сейчас громко думал, что же пошло не так? Потом сообразил: всё так! Я же сам таймаут установил, что только после длинной паузы слово будет сочтено командным! А теперь сам же эту паузу не выдержал. Ну да, это слово было сочтено словом данных, которое "случайно" затесалось на шине - и мы его проигнорировали!

Ладно, сделаю здоровенный отступ в 400 мкс и попробую ещё разок. Похоже, с нахрапу традиционно не вышло, будем смотреть внимательно. Сначала приём командного слова:


Пока его получали, CRC обнулили. Получили - сделали паузу, после чего дали на отправку ответное слово. И тут уже чувствуется подлянка: не успело ещё ответное слово уйти, даже один его бит - а уже запустилось computeCRC=1, т.е уже с него начнётся вычисление CRC. Мы так не договаривались!!!

Смотрим дальше:


Ответное слово 0x3000 (адрес 6, без ошибок) успешно отправлено, следующим идёт заголовок, 0xFF00. А пока передатчик отправляет его, на его входе уже готово следующее слово данных, 2-е из трёх.

Заголовок успешно отправлен, начинает передаваться второе слово, а на входе передатчика уже заготовлено последнее слово. Но из-за того, что оно последнее, computeCRC уже переключается в ноль (чтобы уже посчитанный CRC не подпортился), а transmitCRC - в единицу, "наивно полагая", что эти настройки относятся к ТРЕТЬЕМУ слову. Только вот на самом деле только-только начало отправляться ВТОРОЕ. Именно оно по ошибке и станет CRC.

Смотрим дальше:


Да, второе слово получилось 0x89D6. Если на https://crccalc.com ввести последовательность 0030 00FF (т.е ответное слово и заголовок, но младшим битом вперёд), в режиме HEX, и посчитать CRC-16, то на строке CRC16/KERMIT получим то самое значение 0x89D6. CRC действительно посчитался, только вот со сдвигом на одно слово!

И наконец, когда отправилось последнее слово, конечный автомат уже перешёл в состояние sIdle, с computeCRC = transmitCRC = 0, поэтому последнее слово было взято из памяти, и это было попросту нулевое значение...

Мы всё в том же положении: Понятно, в чём проблема, не очень понятно, как её "малой кровью" решить...

У нас как бы перемешались две концепции. С передатчиками мы старались работать по принципу "Fire and forget" - уж если мы отправили ему слово на передачу, и он его успешно принял (о чём свидетельствовало RW=0 в этот самый момент), то за его дальнейшей судьбой можем не следить - для нас "слово уже отправлено", хотя на деле оно ещё свыше 20 мкс будет мариноваться на линии, это же больше 500 тактов!

А сигналы TransmitCRC и ComputeCRC управляют приёмопередатчиком "здесь и сейчас". Вот они и разошлись на одно слово.


Ладно, эти две проблемы устранили, потом обнаружили ещё одну, при передаче данных (CRC сбивается на 1 слово), её сейчас тоже обмозгуем. Должно быть решение, простое до безобразия, главное, его найти...
Tags: ПЛИС, работа, странные девайсы
Subscribe

Recent Posts from This Journal

  • Тестируем 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