nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

Мучаем 5576ХС4Т, часть 'hB - UART и жаба

Часть 0 - покупаем, паяем, ставим драйвера и софт
Часть 1 - что это вообще за зверь?
Часть 2 - наша первая схема!
Часть 3 - кнопочки и лампочки
Часть 4 - делитель частоты
Часть 5 - подавление дребезга кнопки
Часть 6 - заканчиваем кнопочки и лампочки
Часть 7 - счетчики и жаба
Часть 8 - передатчик UART
Часть 9 - Hello, wolf!
Часть 'hA - приёмник UART
Часть 'hB - UART и жаба
Часть 'hC - полудуплексный UART.
Часть 'hD - МКО (МКИО, Mil-Std 1553) для бедных, введение.
Часть 'hE - приёмопередатчик МКО "из подручных материалов" (в процессе)
Часть 'hF - модуль передатчика МКО
Часть 'h10 - передатчик сообщений МКО
Часть 'h20 - работа с АЦП ADC124s051
Часть 'h21 - преобразование двоичного кода в двоично-десятичный (BCD)
Часть 'h22 - Bin2Bcd с последовательной выдачей данных
Часть 'h23 - перемножитель беззнаковых чисел с округлением
Часть 'h24 - перемножитель беззнаковых чисел, реализация
Часть 'h25 - передаём показания АЦП на компьютер
Часть 'h26 - работа над ошибками (быстрый UART)
Часть 'h27 - PNG и коды коррекции ошибок CRC32
Часть 'h28 - передатчик изображения PNG
Часть 'h29 - принимаем с ПЛИС изображение PNG
Часть 'h2A - ZLIB и коды коррекции ошибок Adler32
Часть 'h2B - ускоряем Adler32
Часть 'h2C - формирователь потока Zlib
Часть 'h2D - передаём сгенерированное PNG-изображение
Часть 'h2E - делим отрезок на равные части
Часть 'h2F - знаковые умножители, тысячи их!
Часть 'h30 - вычислитель множества Мандельброта
Часть 'h31 - ускоренные сумматоры
Часть 'h32 - ускоренные счётчики (делаем часы)
Часть 'h33 - ускоряем ВСЁ
Часть 'h34 - ускоренные перемножители
Часть 'h35 - умножители совсем просто
Часть 'h36 - уравновешенный четверичный умножитель




Посмотрим, к чему приведёт использование счётчиков в явном виде для приёмника UART, после чего наконец-то прошьём в ПЛИС приёмопередатчик и испытаем его.


Всё, что нам нужно - два счётчика. Первый - делитель частоты. Мы должны уметь обнулять его, либо заносить в него значение LimitDiv2, чтобы он сформировал половину периода. Выхода достаточно всего одного, на котором каждый период, по окончании счёта, будет формироваться единичный импульс.

Но если уж мы всё равно напрямую применим lpm_counter, можно воспользоваться чрезвычайно полезным выходом carry-out, на котором появится импульс, когда мы досчитали до всех единиц, и по следующему такту он сбросится во все нули. Благодаря этому выходу нам не нужен компаратор, но вместо обнуления мы должны записать определённое число на вход. То есть, это получится счётчик с синхронной загрузкой данных. Напишем его отдельным модулем:

module UARTFreqDivider (input clk, input sclr, input DoHalf, output cout);

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

	localparam DividerBits = `CLOG2(CLKfreq / BAUDrate);
	localparam Limit = CLKfreq / BAUDrate;
	localparam LimitDiv2 = CLKfreq / BAUDrate / 2;
	localparam MaxInt = 1 << DividerBits;
	
	wire [DividerBits - 1 : 0] DividerInput = DoHalf?  (MaxInt - LimitDiv2) : (MaxInt - Limit);

	lpm_counter	counter (
				.clock (clk),
				.cout (cout), //carry out, =1 when count is over
				.data (DividerInput),	//either half of period, or full period
				.sload (sclr | cout) ); //count or just load value
	defparam
		counter.lpm_direction = "UP",
		counter.lpm_port_updown = "PORT_UNUSED",
		counter.lpm_type = "LPM_COUNTER",
		counter.lpm_width = DividerBits;
endmodule


Происходит здесь совсем немного. Мы создали "экземпляр класса" lpm_counter, который назвали просто counter и задали его ширину, направление счёта - "вверх", вывели наружу выход cout. Единственная дополнительная "логика" - мы прямо здесь на этапе компиляции определяем, какое значение загружать в счётчик, если нам нужен полный период, и какое - если половинка периода, и устроили автоматический сброс (выражением sclr | cout на входе синхронной загрузки), когда он досчитывает до конца.

При 8-битном счётчике (который мы берём за пример, чтобы сравнивать свои "поделия" с чужими, для единообразия), данный модуль синтезируется в 12 логических ячеек. При 14-битном (из 80 МГц - 9600 Гц, на меньшей частоте UART мы вряд ли будем когда-либо работать!) - в 17 логических ячеек.

Проверим, как оно работает, не ошиблись ли мы на единичку где-нибудь? Как всегда на этом примере будем делить всего в 10 раз (или в 5 раз, если выбрали половинный период).



Всё в порядке.

Второй счётчик будет выполнять роль состояния конечного автомата. Вспоминая, что при первом включении счётчик lpm_counter инициализируется в ноль, назначим состояния следующим образом:

0000 - ожидание
0110 - стартовый бит
0111 - первый бит данных
1000 - второй бит данных
...
1110 - восьмой бит данных
1111 - стоповый бит

Все состояния, кроме ожидания, мы "передвинули", чтобы из стопового бита "своим ходом" возращаться в режим ожидания, а также чтобы полезно применить выход Carry-out.

У нас возможны два "прыжка": из режима ожидания в режим приёма стартового бита, а из него - снова в режим ожидания, если стартовый бит не подтвердился. Всё остальное время ведётся счёт, по сигналу ce (clock enable), вырабатываемому счётчиком-делителем частоты. На этот раз мы применим счётчик с входом синхронного сброса (для возвращения в режим ожидания) и входом установки константы 0110, для запуска.

Вот что получается:
module RXstateCounter (input clk, input ce, input sclr, input sset, output [3:0] Q, output isStop);

	lpm_counter	counter (
				.clock (clk),
				.cnt_en (ce),
				.sclr (sclr),
				.sset (sset),
				.q (Q),
				.cout (isStop) );
	defparam
		counter.lpm_direction = "UP",
		counter.lpm_port_updown = "PORT_UNUSED",
		counter.lpm_type = "LPM_COUNTER",
		counter.lpm_width = 4,
		counter.lpm_svalue = 4'b0110;
endmodule


Здесь вообще никакой дополнительной логики не понадобилось. Синтезируется данная штука в 8 логических ячеек. Проверять уж не будем - "ломаться тут нечему".

А теперь соберём из этих модулей приёмник UART, в этот раз - на verilog'е (а не в виде принципиальной схемы):

`include "math.v"

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

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

	localparam rxIdle = 	4'b0000;
	localparam rxStart = 	4'b0110;
	localparam rxB1 =  	4'b0111;
	localparam rxB2	=	4'b1000;
	localparam rxB3	=	4'b1001;
	localparam rxB4	=	4'b1010;
	localparam rxB5	=	4'b1011;
	localparam rxB6	=	4'b1100;
	localparam rxB7	=	4'b1101;
	localparam rxB8	=	4'b1110;
	localparam rxStop = 	4'b1111;
	
	wire [3:0] State;

	wire isIdle = (~State[3]) & (~State[2]);
	wire isStart = (~State[3]) & State[2] & (~State[0]);
	wire isStop;

	wire ce;
	
	UARTFreqDivider divider (	.clk (clk),
					.sclr (isIdle),
					.DoHalf (isIdle & (~rxd) ),
					.cout (ce));
						
	defparam
		divider.CLKfreq = CLKfreq,
		divider.BAUDrate = BAUDrate;
						
	RXstateCounter FSM (		.clk (clk),
					.ce (ce),
					.sclr (ce & isStart & rxd),
					.sset (isIdle & (~rxd)),
					.Q (State),
					.isStop (isStop) );
						
	assign HasOutput = isStop & ce & rxd; 
	assign FrameError = isStop & ce & (~rxd);
	assign LineBreak = FrameError & (Q == 0);
	
	always @(posedge clk)		
		Q <= ce? {rxd, Q[7:1]} : Q;
	
endmodule

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

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

В этот раз синтез модуля даёт 33 логических ячейки, вместо 42 для "чистого верилога", без использования lpm_counter, тогда как построение логики "в лоб" через конечный автомат требовало 69 логических ячеек, и это без выходов для обработки ошибок (FrameError и LineBreak). Регистров мы используем на 20 бит, у остальных 13 ячеек задействуется только комбинаторная часть. Только для формирования FrameError и LineBreak нужно 4 ячейки, ещё 1 - для формирования HasOutput, оставшиеся 8 - на прочую "связующую логику".

Заменим прошлый модуль MyOwnUARTreceiver на этот, SimpleUARTreceiver, благо интерфейс полностью совпадает. Проверяем:


В отличие от модуля передатчика, где Quartus подсунул нам свинью с инициализацией конечного автомата, здесь у нас всё работает как надо, никаких лишних импульсов при подаче питания не возникает.

Пора опробовать это дело непосредственно на ПЛИС. Первое, что можно сделать - принятый байт отображать на восьми светодиодах.

Кроме собственно приёмника, нам нужна 8-битная "защёлка", которая по приходу сигнала HasOutput запоминает принятый байт и отображает его на выходе, пока не придёт следующий. Можно написать её на verilog, дело нехитрое:

module Latch8 (input clk, input ce, input [7:0] D, output reg [7:0] Q);
  always @(posedge clk) if (ce) Q <= D;
endmodule


а можно за то же самое время (или чуть дольше) поместить его из библиотеки lpm (Library of Parameterized Macros). Для этого ставим на схему модуль lpm_ff (flip-flop, из вкладки libraries/megafunctions/storage), в диалогах выбираем количество бит - 8, тип триггера - D-триггер, ставим галочку Create a Clock Enable input, снимаем все галочки с входов сброса и установки, Finish - и получаем тот же самый результат.



Вот как выглядит схема целиком:



В этот раз для разнообразия поставим скорость 115200 бит/с.

Добавляем в pin planner вход RXD, это для нашей отладочной платы Pin_184.

Вот как бы и всё, прошиваем и любуемся миганием светодиодов.

Ещё немного развлечёмся: будем принимать символы, приводить их в верхний регистр (не считая буквы ё), отсылать их назад на компьютер, а светодиодами на плате показывать ошибки в передаче данных.

Напишем модуль для преобразования символа в верхний регистр, за который фанаты unicode и UTF просто руки оторвут, ибо это грязный битовый хак, понятия не имеющий о кодировках и разных способах представления символов. Ну, если у нас будет ПЛИС с мегабайтами встроенной памяти, сотнями мегагерц тактовой частоты и десятками и сотнями тысяч (если не миллионов) логических ячеек - сделаем и на другие кодировки, а пока вот так:

module ToUppercase (input [7:0] D, output [7:0] Q);

	assign Q = (D[7] & D[6]) | ((D > 96) & (D < 123))? (D & 8'b1101_1111) : D;
	
endmodule


всё равно большого практического смысла в этом нет, просто мне совсем грустно возращать в компьютер те же самые символы, ведь того же эффекта можно было бы достичь, просто соединив TXD и RXD :)

И теперь собираем такую схему:


Мы принимаем байт. Если всё в порядке, он будет приведен к верхнему регистру и отправлен назад. В случае ошибки, код ошибки будет высвечен с помощью двух светодиодов. Мы применили библиотечный элемент SRFF (Set-Reset Flip-Flop) из libraries/primitives/storage, чтобы одиночный импульс ошибки фиксировался, пока мы не сбросим его нажатием на кнопку. Инвертор для кнопки (не забываем, что кнопки традиционно замыкают вход на общий провод!) берём из libraries/primitives/logic.

Прошиваем, пробуем отправить строку, и в моём случае ответная посылка вышла что-то не очень:



Первый символ возвращается как надо, и ещё сколько-то символов посередине на своих местах, а другие - искажаются или вовсе пропадают!

Причина тому - наш передатчик оказывается самую малость медленнее передатчика в cp2102 - хоть их скорости должны совпадать, но в действительности есть небольшое расхождение, ну а наш передатчик оказался плохо подготовленным к приходу стартового импульса, если он ещё не закончил передавать предыдущую посылку!

Чтобы поскорее получить правильное ответное сообщение, самый простой способ - вместо скорости 115200 нашего передатчика задать ему чуточку бОльшую скорость, например 116 000. Превышение составляет не более процента, вполне в пределах спецификации, но проблема снимается. Кривое решение, очевидный костыль, в следующей части предложим более корректный вариант.

Стоит так сделать, и мы получаем то, что изображено в первом скриншоте к этому посту - всё хорошо, кроме буквы Ё, как мы и обещали.

Чтобы проверить обнаружение ошибок приёма, проще всего настроить COM-порт на другую скорость передачи, к примеру, 57600 вместо 115200. Теперь, если мы будем передавать символы, где первые или последние 4 бита все нулевые, получим "обрыв линии", в противном случае - просто Frame Error, а иногда и вовсе ничего. Если совсем "уронить" скорость, до тех же 9600, индикация "обрыв линии" будет возникать на любом символе - уже стартовый бит займёт все 10 позиций!


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

Poll #2086785 Стиль кода на verilog

Какой стиль кода вам нравится больше всего?

Последовательная логика в явном виде (какие значения присвоить регистрам к следующему такту)
0(0.0%)
Конечный автомат: набор состояний и простыня case / if /else
2(50.0%)
"Схемотехника": соединяем между собой отдельные модули
1(25.0%)
Всё плохо
1(25.0%)
Tags: ПЛИС, работа, странные девайсы
Subscribe

Recent Posts from This Journal

  • Так есть ли толк в ковариационной матрице?

    Задался этим вопросом применительно к своему прибору чуть более 2 недель назад. Рыл носом землю с попеременным успехом ( раз, два, три, четыре),…

  • Big Data, чтоб их ... (4)

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

  • Потёмкинская деревня - 2

    В ноябре 2020 года нужно было сделать скриншот несуществующей программы рабочего места под несуществующий прибор, чтобы добавить его в документацию.…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 6 comments