nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

Приёмник UART для QuatCore

Мы уже много периферии подключили к QuatCore: передатчик UART, приёмопередатчик SPI (аж 3 устройства к нему подключали, каждое со своими "тараканами" в голове), передатчик для ЖК-экранчика, начинали ковырять МКО (Mil-Std 1553), но прервались на долгий срок (хотя скоро предстоит!), ну и видеопроцессор сейчас мучали в хвост и в гриву.

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

Пора исправить это недоразумение, благо у нас есть почти готовый модуль для этого. Дата последнего изменения: 8 ноября 2018 года, в 4 часа утра!

Вот он:

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

module MyOwnUARTreceiver(	input rxd,
				input clk,
				output reg [7:0] Q,
				output HasOutput,
				output FrameError,
				output LineBreak,
				output [3:0] DebugState);

	parameter CLKfreq = 80_000_000;
	//parameter BAUDrate = 8_000_000; //debug
	parameter BAUDrate = 512_000;
	//parameter BAUDrate = 9600;

	localparam DividerBits = `CLOG2(CLKfreq / BAUDrate);
	localparam Limit = CLKfreq / BAUDrate - 1;
	localparam LimitDiv2 = CLKfreq / BAUDrate / 2 - 1;
	
	reg [DividerBits-1 : 0] FreqDivider;

	`define rxIdle 	4'b1010
	`define rxStart 4'b0000
	`define rxB1	4'b0001
	`define rxB2	4'b0010
	`define rxB3	4'b0011
	`define rxB4	4'b0100
	`define rxB5	4'b0101
	`define rxB6	4'b0110
	`define rxB7	4'b0111
	`define rxB8	4'b1000
	`define rxStop	4'b1001
	
	reg [3:0] State = `rxIdle;
	
	wire isIdle = State[3] & State[1];
	
	wire ce = ((FreqDivider & Limit) == Limit);
	
	wire IsStart = (State == `rxStart);
	
	wire IsStop = State[3] & State[0];
	
	assign HasOutput = IsStop & ce & rxd; //т.е приняты уже все данные, а стоповый бит ожидаемо высокий.
	//у нас как раз есть 1 такт, чтобы занести данные из Q, т.к. на следующий такт они уже пропадут!
	
	assign FrameError = IsStop & ce & (~rxd);
	assign LineBreak = FrameError & (Q == 0);
	
	wire ZeroFreqDivider = ((isIdle & rxd) | ce);
	wire HalfFreqDivider = (isIdle & ~rxd);
	
	assign DebugState = State;
							
	always @(posedge clk) begin
		FreqDivider <= 	ZeroFreqDivider? 	1'b0:
				HalfFreqDivider? 	LimitDiv2: //transmission began
											FreqDivider + 1'b1;
	
		State <= 	(isIdle & ~rxd)?	`rxStart: //should translate to 'synchronous reset'
				ce?
				(IsStart & rxd?		`rxIdle:
							State + 1'b1):
							State;		
		
		Q <= ce? {rxd, Q[7:1]} : Q; //всегда заносим, пофиг, старт, стоп или данные			
	end
	
endmodule


Нда, поменялся стиль за последние 2 года :) Пожалуй, нужно для начала привести его к "стандартам QuatCore"


Вместо `define мы теперь предпочитаем localparam - иначе в проекте может оказаться несколько файлов с одинаковыми константами, и они начнут конфликтовать. Ведь `define "понятия не имеет" о модулях, файлах, каких-то там областях видимости - у препроцессора "всё общее". Сделаем вот так:

	localparam rxIdle 	= 4'b1010;
	localparam rxStart	= 4'b0000;
	localparam rxB1		= 4'b0001;
	localparam rxB2		= 4'b0010;
	localparam rxB3		= 4'b0011;
	localparam rxB4		= 4'b0100;
	localparam rxB5		= 4'b0101;
	localparam rxB6		= 4'b0110;
	localparam rxB7		= 4'b0111;
	localparam rxB8		= 4'b1000;
	localparam rxStop	= 4'b1001;



Делить мы предпочитаем с округлением "до ближайшего целого". Скажем, если тактовая частота 4 МГц, а скорость передачи 460 800 бод, то 4e6 / 460800 ≈ 8,68. В этом модуле получим 8, в результате реальная скорость составит 4 МГц / 8 = 500 000 бод, отличается от искомой на 8,5%. А вот если округлить как положено, до 9, реальная скорость составит 444 444 бод, отличается на 3,5% - это ещё терпимо.

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

Чтобы получить правильное округление, строки

	localparam Limit = CLKfreq / BAUDrate - 1;
	localparam LimitDiv2 = CLKfreq / BAUDrate / 2 - 1;


заменим на
	localparam Limit = (CLKfreq + BAUDrate/2) / BAUDrate - 1;
	localparam LimitDiv2 = (CLKfreq + BAUDrate/4) / (BAUDrate * 2) - 1;


В таком виде, с тактовой частотой 25 МГц и скоростью передачи 921 600 бод, этот модуль синтезируется в 37 ЛЭ

В общем-то, неплохо, но можно и лучше... Как ни удивительно, здесь вообще не используются счётчики. Они не указаны "в явном виде" (как lpm_counter), а при синтезе Квартус обнаруживает (infer) только 5-битный сумматор - не лучшее решение.

И для окончания счёта мы используем "компараторы", хоть и чуть упрощённые (поскольку знаем, что ещё выше значение не поднимется, т.к мы его сбросим, можно не проверять нули, а проверить только единицы, оттого выражение ((FreqDivider & Limit) == Limit) - оно превращается в длиннющий элемент И между всеми разрядами FreqDivider, где в Limit стоят единицы), хотя эффективнее использовать выход cout (carry-out), а вместо сброса в ноль делать установку в нужное значение. Ещё и направление счёта можно выбрать - вверх или вниз, что повлияет на "начальное положение" счётчика - он всегда инициализируется нулём, но этот ноль будет значить разные вещи.

Разберёмся с делителем частоты FreqDivider
Он управляется двумя сигналами:

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

always @(posedge clk) begin
		FreqDivider <= 	ZeroFreqDivider? 	1'b0:
				HalfFreqDivider? 	LimitDiv2: //transmission began
							FreqDivider + 1'b1;


Поначалу, когда никакое сообщение не посылается, rxd=1, у нас непрерывно устанавливается значение ZeroFreqDivider=1, которое заставляет "счётчик" непрерывно обнуляться.

Когда начинается посылка, стартовый бит (rxd=0), устанавливается ZeroFreqDivider=0, но зато HalfFreqDivider=1. Это заставляет поместить в счётчик "половинное" значение". И к следующему такту HalfFreqDivider=0, поскольку состояние (state) уходит с sIdle.

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

Сделаем эту штуку "настоящим счётчиком", то есть lpm_counter. Считать он будет вверх, потому как независимо от направления счёт lpm_counter ВСЕГДА ИНИЦИАЛИЗИРУЕТСЯ НУЛЁМ, а мы хотим соединить cout с проводом ce (clock enable), по которому происходит переключение состояний. Если бы счётчик считал вниз, то как раз нулевое значение сразу дало бы cout=1, и мы перепрыгнули бы с состояния sIdle и пошли принимать по чём зря. В итоге очень быстро успокоились бы (в середине периода опять rxd=1, хотя должен ещё продолжаться нолик - "стартовый бит"), но не нравятся мне подобные штуки на старте.

Поразмыслим с нашим конкретным примером, счёта до 27 (25 МГц / 921600 Гц). Нужен 5-битный счётчик. Хм, нужно ещё и эту строку переделать:

localparam DividerBits = `CLOG2(CLKfreq / BAUDrate);


А то с предыдущим примером может конфуз выйти, когда мы захотим считать до 9 (4 МГц / 460800, округлить до ближайшего), а нам предоставят 3-битный счётчик, который до 9 досчитать не сможет! Запишем лучше так всё это дело:
	localparam Quotient = (CLKfreq + BAUDrate/2) / BAUDrate;
	localparam DividerBits = `CLOG2(Quotient);
	localparam Limit = Quotient - 1;
	localparam LimitDiv2 = (CLKfreq + BAUDrate/4) / (BAUDrate * 2) - 1;


Теперь, чтобы счётчик делил частоту в 27 раз, а не в "свои родные" 32, он должен стартовать не с нуля, а с числа 5 = 5'b00101. А когда мы один-единственный раз хотим сосчитать половинку периода, это должно быть "деление в 14 раз" (25 МГц / 921600 / 2, округлить до ближайшего целого), и для этого надо начать счёт с числа 18 = 5'b10010. На удивление коряво выходит: видно, что лишь один бит для параллельной загрузки можно оставить нулевым, остальные меняются.

Так что, думаю, действительно есть смысл счётчик обнулять для "полного счёта", делать параллельную загрузку для "половинного счёта", а сигнал "ce" всё-таки генерировать самим, всё-таки сравнение с константой (тупо "равно-не равно") дешевле мультиплексора! Тем более, что мы заранее знаем, что выше лимита число не поднимется, и там можно выдавать Undefined Behaviour :) Короче, если лимит стоит 26 = 5'b11010, то чтобы обнаружить, что мы уже до него дошли, проверяем лишь наличие единиц на своих местах, FreqDivider[1]&FreqDivider[3]&FreqDivider[4]. 3 входа, значит достаточно одного ЛЭ.

Как ни странно, пришли ровно к тому, с чего начали! Разве что счётчик укажем в явном виде, поскольку Квартус его здесь в упор не видит:

	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; 


Теперь весь модуль занимает 32 ЛЭ - уже на 13% подсократили!

Конечный автомат, то бишь регистр State. Как видно, мы и его постарались сделать счётчиком, но Квартус этого почему-то не заметил. Удивительно, что он и конечного автомата здесь не усмотрел. Что-то со зрением у Квартуса.

У нас 11 состояний: ожидание (idle), стартовый бит, затем 8 бит данных и стоповый бит. Поэтому для хранения состояния нужно 4 бита, мы так и сделали, разумеется.

Переходы между состояниями достаточно просты: мы идём от состояния к состоянию, но из стопового бита должны вернуться в ожидание В ЛЮБОМ СЛУЧАЕ, а из стартового бита должны перейти в ожидание, если в этот момент пришла "единичка", т.е наш запуск на самом деле произошёл из-за какой-то случайной помехи.

Уже не помню, чем меня не устроило самое естественное расположение команд: 0000 означает ожидание, 0001: стартовый бит, 0010: первый бит данных, и так далее. Сигналы isStart, isStop, isIdle особенно простым способом всё равно не образовывались.

Давайте попробуем ровно такой же вариант, который уже ввели в передатчике UART для QuatCore:
0000 - Idle
0001..0101 - не используются
0110 - start
0111 - B0
1000 - B1
1001 - B2
1010 - B3
1011 - B4
1100 - B5
1101 - B6
1110 - B7
1111 - stop


При таком варианте мы максимально можем задействовать входы и выходы счётчика: sclr для "досрочного" возвращения в Idle, sset для старта, cout для индикации состояния stop.

По-моему, вот так:
	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...

	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;



Теперь наш модуль синтезируется в 30 ЛЭ, причём 2 ЛЭ занимает формирование сигнала FrameError ("неверная посылка"), а ещё 3 ЛЭ - формирование сигнала LineBreak ("обрыв линии") - он "загорается", когда логический "0" продолжается в течение 10 периодов, т.е сначала мы восприняли его как стартовый бит, затем прошло 8 бит данных - все нули (это допустимо), а вот затем вместо стопового бита тоже был ноль.

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

А без них ("на минималках") и вовсе получается 25 ЛЭ!

Но теперь ещё нужно "подружить" этот приёмник с QuatCore! Один из вариантов - сделать буфер FIFO, благо мы с ними уже возились, и научились делать их как на вход QuatCore (тогда процессор останавливается, если буфер пуст), так и на выход (процесор останавливается, если буфер полон). В общем-то, это общепринятая практика, но иногда её доводят до абсурда. Меня страшно раздражают медиаплееры и "умные телевизоры", когда они серьёзно задумываются в некоторых местах, ты жмёшь кнопки, они не реагируют, но потом спохватываются - и ОТРАБАТЫВАЮТ ВСЁ ТО, ЧТО ТЫ НАЖАЛ! Моё такое мнение: если уж ты слишком долго не опрашиваешь свои входные устройства (те, что взаимодействуют с пользователем) - лучше просто забыть о поступивших за это время сигналах! А если опрашиваешь их хорошо - то большой буфер не нужен :)

Здесь я бы вообще предусмотрел возможность обойтись без буфера. Для этого нужно добавить вход RxReq (запрос принятого значения) и выход busy ("занят"), тогда как HasOutput и не нужен особо (сохраним для прерываний, если вдруг покусает нас эта муха. Или для подключения выходного буфера). И логика довольно проста: если RxReq=1 И HasOutput=0, выдаётся сигнал busy. Это заставляет процессор остановиться, продолжая выдавать RxReq=1, до тех пор пока байт наконец не придёт. Тогда он будет занесён на шину данных - и всё хорошо.

Понимаю, логика весьма специфическая, всё должно замереть и дожидаться пользовательского ввода. Но у нас этот UART для отладочных целей, там это удобнее всего. Как в простеньком консольном приложении есть какой-нибудь ReadLn - и тоже пока он не получит своих данных - всё останавливается. Нормально :)

В итоге, весь наш модуль выглядит так:

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

module BetterUARTreceiver(	input clk, input rxd, input RxReq,
				output reg [7: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 = RxReq & (~HasOutput);	//запросили выход, а его пока нет!
	
	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;
							
	always @(posedge clk) begin	
		Q <= ce? {rxd, Q[7:1]} : Q; //всегда заносим, пофиг, старт, стоп или данные			
	end
	
endmodule


и синтезируется в 31 ЛЭ. Если же вообще не использовать выходы HasOutput, FrameError и LineBreak, выходит и вовсе 25 ЛЭ.

Остаётся подключить этот модуль на своё место в QuatCore с периферией:


Сигнал UARTrxEN уже дожидался своего часа в модуле QuatCoreIOselector, его мы включили к входу RxReq только что добавленного модуля.

А вот к выходу пришлось подключить ещё один мультиплексор, поскольку раньше из устройств ввода у нас был только SPI. Не стал заморачиваться - поставил "библиотечный" BUS_MUX. И ещё объединил по OR2 выходы busy от того же SPI и от UARTrx.

В итоге, всё отсинтезировалось, но опять завалило тайминги, 24,04 МГц вместо штатных 25. Увы, компилятор перестал справляться с созданием таблицы непосредственных значений - да, он умудряется оставить совсем без ЛЭ 10 выходов из 16, и ещё на два выхода дать по одному ЛЭ (т.е они явились функцией от 3..4 входов), но оставшиеся 4 выхода становятся функцией от 7 входов, и отжирают 80 ЛЭ все дружно! И задержка становится нехилой. Сейчас становится выгоднее её сделать в "простейшем виде"...

В общем-то, у меня здесь есть выход из ситуации - поставить дополнительную защёлку, уж как-нибудь переживём, если очередной байт по UART придёт к нам на 40 нс позже, чем надо :)


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

Recent Posts from This Journal

  • Нахождение двух самых отдалённых точек

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

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Балансируем конвейер QuatCore

    В пятницу у нас всё замечательно сработало на симуляции, первые 16 миллисекунд полёт нормальный. А вот прошить весь проект на ПЛИС и попробовать "в…

  • Огари разговаривают

    Сегодня по пути на работу встретил огарей прямо в Лосином острове, на берегу Яузы. Эти были на удивление бесстрашны, занимались своими делами, не…

  • Ковыряемся с сантехникой

    Наконец-то закрыл сколько-нибудь пристойно трубы, подводящие к смесителю, в квартире в Москве: А в воскресенье побывал на даче, там очередная…

  • Мартовское велосипедное

    Продолжаю кататься на работу и с работы на велосипеде, а также в РКК Энергию и на дачу. Хотя на две недели случился перерыв, очередная поломка,…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments