nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Мучаем 5576ХС4Т - часть 'h22 - Bin2Bcd с последовательной выдачей данных

Часть 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 - уравновешенный четверичный умножитель


В прошлой части мы разработали довольно компактный модуль для преобразования 16-битного целого беззнакового числа в двоично-десятичную (BCD) форму. Через 15 тактов работы, все 5 десятичных разрядов выдавались параллельно. Но ситуация, когда они нам действительно нужны "все сразу" - очень редки. Куда бы дальше ни пошли эти данные, они скорее всего понадобятся по одной цифре за раз! Если мы подключаем 7-сегментный индикатор, мы как правило используем "динамическую индикацию", когда цифры зажигаются одна за другой - это позволяет сэкономить на количестве транзисторов и проводов, идущих к контроллеру (будь то ПЛИС или МК). Также "по одной цифре" (максимум по две) мы будем заносить в память, либо отправлять через UART.

Поскольку наш преобразователь двоичного кода в BCD - по своей сути сдвиговый регистр (плюс цепи коррекции), то почему бы не применить его и для последовательной выдачи цифр, от самой старшей к самой младшей?

Но сначала разберёмся, а какой интерфейс нам вообще нужен? Как оказывается, если мы планируем результаты работы занести в оперативную память, нам удаётся влезть всего в 49 логических элементов!



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

Нам нужно нечто такое:
module Bin2Bcd16for7seg (input clk,
                           input [15:0] D,
                           input start,
                           input NextPlease,
	                   output [3:0] Q,
                           output [2:0] DigitPosition);


Когда мы получаем новое значение от АЦП, мы подаём импульс start и новое двоичное число в D. В это же самое время, независимо от АЦП, на NextPlease подаются импульсы для развёртки 7-сегментного дисплея, с некоторой достаточно низкой частотой, но чтобы глаз не шибко замечал мерцания. Скажем, частота развёртки может составлять 50 Гц, а поскольку на один цикл приходится 5 позиций, то импульс NextPlease будет приходить с частотой 250 Гц.

В Q появляется очередная цифра, а DigitPosition показывает, какая она по счёту. К примеру, 0002 соответствует старшему разряду, 0012 - следующему, 1002 - младшему. Останется только подключить Q к дешифратору для 7-сегментного индикатора, а DigitPosition - к простому дешифратору, который будет подавать питание на один из 5 разрядов.

В данном случае мы не шибко заботимся о тех 15 тактах, во время которых на выходе будут некорректные данные, ведь 15 тактов - это всего 190 нс, они даже отобразиться не успеют!

По-другому обстоят дела, если мы хотим отправить наши данные в оперативную память, скорее всего в виде строки. Тогда нам нужен примерно такой интерфейс:
module Bin2Bcd16forMem (input clk,
                        input [15:0] D,
                        input start,
                        output [7:0] char,
                        output DigitReady,
                        output [2:0] DigitPosition,
                        output Finished);


В данном случае достаточно отобразить каждую цифру на выходе ровно один раз, в это время подать единичку на DigitReady, чтобы разрешить запись в оперативную память. Чтобы записать её куда надо, может пригодиться выход DigitPosition, а по окончании всех работ мы выдадим единичный импульс на Finished, и на этом выдача прекращается.

Наконец, при работе напрямую с UART нам тоже нужно выдать каждую цифру ровно один раз, но сделать мы это должны весьма неторопливо! К примеру, при тактовой частоте 80 МГц и при скорости передачи данных 921600 бод - (максимальная, поддерживаемая м/сх cp2102, примерно 740 кбит/с "чистыми", т.е без стартовых и стоповых битов), между отправкой соседних байт проходит 10,85 мкс, или 868 тактов! Поэтому, выдав одну цифру, мы должны "замереть" и терпеливо ждать, когда эта цифра наконец-то будет отправлена. Другое дело, что в отличие от 7-сегментного индикатора, где нам неплохо бы держать на выходе цифру "непрерывно", тут у нас гораздо больше свободы. Интерфейс выглядит примерно так:
module Bin2Bcd16forUART (input clk,
                        input [15:0] D,
                        input start,
                        input NextPlease,
                        output [7:0] char,
                        output DigitReady,
                        output Finished);


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

Начнём с самого простого варианта, который выдаёт строку в память, байт за байтом.

Чтобы заставить предыдущий вариант преобразователя выдавать данные последовательно, цифру за цифрой, мы должны "научить" его не всегда применять цепи коррекции, а лишь "на первом этапе", этапе преобразования. Затем сдвиг влево будет осуществляться без коррекции, просто чтобы место старшего разряда последовательно заняли бы все цифры, вплоть до разряда единиц.

Модуль, отвечающий за один десятичный разряд, будет выглядеть так:
module SeqBinToBcdAtomicBlock (input clk,
                               input sclr,
                               input DoCorrection,
                               input bIn,
                               output reg [3:0] Q = 1'b0,
                               output bOut);

	wire gt4 = (Q > 4) & DoCorrection; 

	assign bOut = gt4 | Q[3] ; //that's senior digit (carry-out)
	
	wire [3:0] Qmod = gt4? Q + 2'b11 : Q;

	always @(posedge clk)
		Q <= sclr? 1'b0 : {Qmod[2:0], bIn}; 
endmodule


Модуль немножко усложнился - раньше мы отправляли "дальше по цепочке" непосредственно результат условия (Q > 4), поскольку было понятно, что при умножении 5 .. 9 на два у нас будет перенос в следующий разряд, в противном случае - не будет. Теперь, поскольку коррекция может не делаться, надо рассмотреть и вариант "обычного сдвига". В итоге, теперь такой модуль занимает 7 логических элементов (ЛЭ), вместо шести - не так уж и плохо. Попытки упростить этот модуль "ручками", зная, что при коррекции на входе могут быть только числа 0 .. 9, но никак не 10..15 - не приводят к положительным результатам, всё равно остаётся 7 ЛЭ.

Также нам надо усложнить "конечный автомат", а на самом деле тупо счётчик (нам понравилось называть его командоаппаратом) - сначала он должен отсчитать нам 15 тактов на преобразование, а затем ещё 4 раза по 4 такта, чтобы показать на выходе все 5 десятичных разрядов. Понятное дело, теперь нам нужен 5-битный счётчик, идущий от 0 до 31:
//first, state machine
localparam sIdle  =       5'b00000;
localparam sStart =       5'b00001;
localparam sDigit0Ready = 5'b10000; //senior, tens of thousands
localparam sShift1_1    = 5'b10001;
localparam sShift1_2    = 5'b10010;
localparam sShift1_3    = 5'b10011;
localparam sDigit1Ready = 5'b10100; //thousands
localparam sShift2_1    = 5'b10101;
localparam sShift2_2    = 5'b10110;
localparam sShift2_3    = 5'b10111;
localparam sDigit2Ready = 5'b11000; //hundreds
localparam sShift3_1    = 5'b11001;
localparam sShift3_2    = 5'b11010;
localparam sShift3_3    = 5'b11011;
localparam sDigit3Ready = 5'b11100; //tens
localparam sShift4_1    = 5'b11101;
localparam sShift4_2    = 5'b11110;
localparam sShift4_3    = 5'b11111; //Digit4 will be ready exactly when we're back to Idle!

wire [4:0] State;

wire IsIdle = (State == sIdle); //no shortcuts, all states are used!
wire TC; //Terminal Count
always @(posedge clk)
	Finished <= TC;

wire DoCorrection = ~State[4]; //we do correction on conversion steps, but not when showing results

assign DigitPosition = {Finished, State[3:2]};
assign DigitReady = Finished | (State[4] & (~State[1]) & (~State[0]));

lpm_counter	counter (
				.clock (clk),
				.cnt_en (~IsIdle | start),
				.q (State),
				.cout (TC) );
	defparam
		counter.lpm_direction = "UP",
		counter.lpm_port_updown = "PORT_UNUSED",
		counter.lpm_type = "LPM_COUNTER",
		counter.lpm_width = 5;


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

Мы решили чуть сдвинуть все шаги, чтобы преобразование оканчивалось к состоянию 10000, а не 01111, как раньше. Благодаря этому упрощается логика управления сдвигами (если старший бит стал единичным, мы отключаем коррекцию!), но сигнал окончания работы приходится на состояние sIdle, поэтому приходится сделать Finished регистром и задерживать его на 1 шаг. Ничего страшного.

Также мы сразу формируем сигналы готовности очередного разряда: это происходит либо по окончании работы (именно тогда на выходе появляется последняя цифра, разряд единиц), либо при единице в старшем бите состояния и двух нулях в младших.

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

Из-за того, что мы решили затянуть процесс ещё на один шаг, то теперь в разряд единиц при начале работы заносится всего лишь один бит входных данных. Сделаем соответствующий модуль:
module SeqBinToBcdLeastSignBlock (input clk,
                                  input sclr,
                                  input DoCorrection,
                                  input bIn,
                                  input D,
                                  output reg [3:0] Q = 1'b0,
                                  output bOut);

	wire gt4 = (Q > 4) & DoCorrection; 

	assign bOut = gt4 | Q[3] ; //that's senior digit (carry-out)
	
	wire [3:0] Qmod = gt4? Q + 2'b11 : Q;

	always @(posedge clk)
		Q <= sclr? {3'b000, D} : {Qmod[2:0], bIn}; 
endmodule


По сравнению с прошлой схемой, мы удлиняем сдвиговый регистр на единичку, а также указываем, как формировать 8-битный символ из 4-битной "десятичной цифры":
assign char = {4'h3, senior};


Приведём код всего преобразователя целиком:
module Bin2Bcd16forMem (input clk,
                        input [15:0] D,
                        input start,
                        output [7:0] char,
                        output DigitReady,
                        output [2:0] DigitPosition,
                        output reg Finished = 1'b0);
//first, state machine
localparam sIdle  =       5'b00000;
localparam sStart =       5'b00001;
localparam sDigit0Ready = 5'b10000; //senior, tens of thousands
localparam sShift1_1    = 5'b10001;
localparam sShift1_2    = 5'b10010;
localparam sShift1_3    = 5'b10011;
localparam sDigit1Ready = 5'b10100; //thousands
localparam sShift2_1    = 5'b10101;
localparam sShift2_2    = 5'b10110;
localparam sShift2_3    = 5'b10111;
localparam sDigit2Ready = 5'b11000; //hundreds
localparam sShift3_1    = 5'b11001;
localparam sShift3_2    = 5'b11010;
localparam sShift3_3    = 5'b11011;
localparam sDigit3Ready = 5'b11100; //tens
localparam sShift4_1    = 5'b11101;
localparam sShift4_2    = 5'b11110;
localparam sShift4_3    = 5'b11111; //Digit4 will be ready exactly when we're back to Idle!

wire [4:0] State;

wire IsIdle = (State == sIdle); //no shortcuts, all states are used!
wire TC; //Terminal Count
always @(posedge clk)
	Finished <= TC;

wire DoCorrection = ~State[4]; //we do correction on conversion steps, but not when showing results
assign DigitPosition = {Finished, State[3:2]};

assign DigitReady = Finished | (State[4] & (~State[1]) & (~State[0]));

lpm_counter	counter (
				.clock (clk),
				.cnt_en (~IsIdle | start),
				.q (State),
				.cout (TC) );
	defparam
		counter.lpm_direction = "UP",
		counter.lpm_port_updown = "PORT_UNUSED",
		counter.lpm_type = "LPM_COUNTER",
		counter.lpm_width = 5;

//now our datapath
wire [19:0] Q;
reg [10:0] SR = 1'b0; //shift register

reg [3:0] senior = 1'b0; //[19:16]
assign char = {4'h3, senior};


wire B0toB1;
SeqBinToBcdLeastSignBlock Block0 (
	.clk (clk),
	.sclr (start),
	.D (D[15]),
	.DoCorrection (DoCorrection),
	.bIn (SR[10]),
    .Q (Q[3:0]),
    .bOut (B0toB1));

wire B1toB2;
SeqBinToBcdAtomicBlock Block1 (
	.clk (clk),
	.sclr (start),
	.DoCorrection (DoCorrection),
	.bIn (B0toB1),
    .Q (Q[7:4]),
    .bOut (B1toB2));

wire B2toB3;
SeqBinToBcdAtomicBlock Block2 (
	.clk (clk),
	.sclr (start),
	.DoCorrection (DoCorrection),
	.bIn (B1toB2),
    .Q (Q[11:8]),
    .bOut (B2toB3));
    
wire B3toB4;
SeqBinToBcdAtomicBlock Block3 (
	.clk (clk),
	.sclr (start),
	.DoCorrection (DoCorrection),
	.bIn (B2toB3),
    .Q (Q[15:12]),
    .bOut (B3toB4));

always @(posedge clk) begin
	senior <= start? D[3:0] : {senior[2:0], B3toB4};
	SR     <= start? D[14:4]: {SR[9:0], senior[3]};
end
endmodule

module SeqBinToBcdAtomicBlock (input clk, input sclr, input DoCorrection, input bIn, output reg [3:0] Q = 1'b0, output bOut);

	wire gt4 = (Q > 4) & DoCorrection; 

	assign bOut = gt4 | Q[3] ; //that's senior digit (carry-out)
	
	wire [3:0] Qmod = gt4? Q + 2'b11 : Q;

	always @(posedge clk)
		Q <= sclr? 1'b0 : {Qmod[2:0], bIn}; 
endmodule

module SeqBinToBcdLeastSignBlock (input clk,
                                  input sclr,
                                  input DoCorrection,
                                  input bIn,
                                  input D,
                                  output reg [3:0] Q = 1'b0,
                                  output bOut);

	wire gt4 = (Q > 4) & DoCorrection; 

	assign bOut = gt4 | Q[3] ; //that's senior digit (carry-out)
	
	wire [3:0] Qmod = gt4? Q + 2'b11 : Q;

	always @(posedge clk)
		Q <= sclr? {3'b000, D} : {Qmod[2:0], bIn}; 
endmodule


Результаты моделирования работы показаны в начале поста. Данная штуковина синтезируется в 49 логических элементов, то есть, по сравнению с "параллельным" исполнением, добавилось всего 4 новых ЛЭ. Нам удалось достичь таких результатов благодаря тому, что мы позволили этому модулю "самому определять", когда выдавать данные наружу, считая, что память в любой момент сможет занести эти значения куда следует. Поэтому опции "стоять на месте" у нашего сдвигового регистра вообще нет!

Все прочие варианты реализации уже будут заметно "толще", поскольку добавление "разрешения работы" сдвигового регистра делает управляющую логику слишком сложной, чтобы уместиться в одном ГФ, и на каждый бит добавляется ещё минимум по логическому элементу. Ещё сложнее обеспечить циклическое переключение цифр - заметьте, что здесь мы обошлись 16 дополнительными состояниями при 20-битном сдвиговом регистре, поскольку как только последняя цифра добралась до позиции старшего разряда, нам уже стало глубоко пофиг, что будет дальше. Если же мы должны аккуратно "вернуть всё как было", то только для осуществления циклических сдвигов нам нужно 20 состояний, плюс хотя бы 15 состояний для преобразования, т.е счётчик "утолщается" до 6 бит, появляется усложнённая логика работы с ним (хождение по кругу по NextPlease и дополнительный круг по start).
Tags: ПЛИС, математика, работа, странные девайсы
Subscribe

Recent Posts from This Journal

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

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

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

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

  • DMA для QuatCore

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

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 2 comments