nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Мучаем 5576ХС4Т - часть 'h32 - ускоренные счётчики (делаем часы)

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


                    //Говнокод #411 - как узнать дату завтрашнего дня
                    public Calendar getTomorrow() {
                       Thread.sleep(1000*60*60*24);
                       return Calendar.getInstance();
                    }


Когда работаешь с медленным кристаллом ("-3"), мучением становятся даже счётчики сколько-нибудь большой разрядности. Хочешь получить импульсы с частотой 1 кГц из тактовой частоты 80 МГц (деление в 80 000 раз) - выбираешь библиотечный счётчик lpm_counter, задаёшь в Wizard'e все необходимые значения: ширина 17 бит, счёт "вверх" (up), по модулю (modulus) 80 000, дополнительный выход cout, затем жмёшь next-next-next-finish и надеешься: уж наверное авторы софта позаботились о том, чтобы максимально эффективно задействовать возможности ПЛИС.

Не тут-то было: полученный драндулет может работать максимум на 73 МГц, и это всего-то при ширине в 17 бит! Страшно подумать, что будет, если мы попытаемся сделать часы (миллисекунды для развёртки, затем секунды - десятки секунд - минуты - десятки минут - часы - десятки часов), это потребует счётчика с общей шириной 17 + 10 + 4 + 3 + 4 + 3 + 4 + 2 = 47 бит.

Смеху ради нарисуем это безобразие:

(делать часы на "счётчиках пульсаций" не предлагать - мы хотим именно СИНХРОННУЮ работу всей схемы, чтобы мы могли, к примеру, ставить метки времени куда-нибудь в телеметрию, зная, что не запишем уже обновившиеся минуты (00) и ещё не успевшие обновиться часы, после чего получим ошибку на целый час. Возмущаться, что этим должен заниматься RTC с микроамперным потреблением, работающий от MEMS-резонатора на 32768 Гц, тоже не надо - часы здесь лишь для примера - мы хотим понять КАК СОБИРАТЬ ШИРОКИЕ СИНХРОННЫЕ СЧЁТЧИКИ на медленных кристаллах)

Результат немножко ожидаем: задержка распространения на кристалле "-3" составляет 27,1 нс (36,9 МГц), более чем вдвое медленнее, чем надо! На кристалле "-2" задержка: 18,2 нс (54,95 МГц), а на кристаллах "-1", как ни странно, схема по-прежнему способна работать: получается задержка 11,9 нс (84,03 МГц) - по бровке, но жить можно.

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

Когда мы объединяем несколько счётчиков - мы только усугбляем ситуацию! Теперь уже сигнал разрешения работы следующего (более старшего) счётчика будет сформирован лишь после того, как установится правильное значение на выходе cout предыдущего.

И ещё больше ситуацию усугбляет счёт не до степеней двойки, а до произвольных значений (до 80 000, до 1000, до 10 и так далее) - это значит, что вместо более-менее шустрых цепей переноса мы применяем дополнительный компаратор, что иногда требует каскадирования большого числа логических элементов (скажем, 5 штук для 17-битного счётчика).

Но в отличие от сумматоров, здесь у нас есть очень простое решение, позволяющее наращивать разрядность практически неограниченно!


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

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

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

Напишем модуль "убыстрённого" счётчика со всеми входами и выходами, которые могут нам понадобиться:

`include "math.v"

module FastCounter (	input clk, 	//clock
			input ce,  	//count enable
			input sclr, //synchronous clear
			output [Width - 1 : 0] Q,
			output reg TC = 1'b0,  //Terminal Count
			output ceo); //count enable - output

	parameter Fin = 80_000_000;
	parameter Fout = 8_000_000;
	parameter Width = 4;
	
	localparam DivideBy = (Fin + Fout / 2) / Fout; //rounding to nearest integer
	localparam Limit = DivideBy - 2; //when dividing by 10, Limit is 8
	localparam IntWidth = `CLOG2(DivideBy - 1);
	localparam MaxInt = 1 << IntWidth;

	wire cout; //carry-out of lpm_counter
	wire [IntWidth - 1 : 0] IntQ;
	wire combTC = (Limit == MaxInt)? cout : ((IntQ & Limit) == Limit) & (IntQ[0] == (Limit & 1'b1)); //Terminal Count

	lpm_counter	lpm_counter_component (
				.clock (clk),
				.cnt_en (ce),
				.sclr (ceo | sclr),
				.q (IntQ),
				.cout (cout));
	defparam
		lpm_counter_component.lpm_direction = "UP",
		lpm_counter_component.lpm_port_updown = "PORT_UNUSED",
		lpm_counter_component.lpm_type = "LPM_COUNTER",
		lpm_counter_component.lpm_width = Width;

	
	always @(posedge clk) if (ce)
		TC <= combTC;
		
	assign ceo = TC & ce;
	assign Q = IntQ;
	
endmodule


Объясним назначение входов и выходов:
clk - тактовая частота,
ce - count enable, разрешение счёта. В счётчике lpm_counter есть входы clk_en (clock enable) и cnt_en (count enable) - их различие в том, что синхронный сброс будет воспринят только когда clk_en = 1, тогда как cnt_en лишь запрещает счёт, но не запрещает синхронный сброс или установку. Мне пока ни разу не понадобился clk_en, поэтому пока что я подсократил cnt_en до ce - оно как-то привычнее.
sclr - synchronous clear, синхронный сброс. Он срабатывает по положительному фронту тактовой частоты, причём сигнал ce не оказывает влияния на его работу.
Q - двоичный выход счётчика.
TC - Terminal Count - здесь всегда единица, если мы досчитали до последнего значения.
ceo - count enable output - здесь появляется единичный импульс, когда происходит переключение с последнего значения опять на ноль. Именно этим импульсом удобно запускать следующий счётчик в цепочке.

Для симуляции работы этих штук на медленном кристалле мы просто обязаны "защёлкнуть" все входные цепи, иначе симулятор умудряется сдвинуть их почти что на полтакта (если не 3/4 такта), напрочь исказив их работу. Так что мы "оборачиваем" наш счётчик вот в такую схему:


Сразу покажем, как происходит счёт до 10:


Хочется подчеркнуть разницу между TC и ceo. Как видно, TC может длиться довольно долго - здесь появляется единица, как только мы переключились на последнее значение счётчика (в нашем случае - девятка), и сбрасывается в ноль вместе со сбросом самого счётчика в ноль. ceo занимает один период тактовой частоты и формируется под самый конец "действия" TC.

Сигнал TC (Terminal Count) соответствует сигналу cout (carry out) счётчика lpm_counter. Он просто необходим в таком виде, когда мы хотим реализовать более сложную логику сброса, задействующую сразу несколько счётчиков. Только с его помощью мы можем заставить счётчик часов переключиться с 23 на 0, когда наступит 23:59:59. Ведь ни значение 2 в десятках, ни значение 3 в единицах часов не располагают к сбросу.

Но во всех остальных случаях куда удобнее пользоваться сигналом ceo. Он формируется очень легко:
assign ceo = TC & ce;


но поскольку у счётчика lpm_counter такого выхода нет, приходится "навешивать" толпу элементов И, что мы и наблюдаем на схеме в начале поста.

Наверное, самая загадочная строка в этом модуле вот эта:
	wire combTC = ((IntQ & Limit) == Limit) & (IntQ[0] == (Limit & 1'b1)); //Terminal Count


Первую половину этого выражения мы уже встречали в части 7: это более "дешёвая" проверка на равенство. Когда мы вместо (A == B) используем выражение (A & B) == B, мы по сути проверяем, что там, где в числе B стоят единицы, в числе A также стоят единицы. Остальные разряды попросту игнорируются.

Это выражение хорошо тем, что если A < B, мы всегда получим ноль (так и надо); если A == B, мы всегда получим единицу (так тоже надо!), а когда A > B, то будем получать попеременно то ноль, то единицу (а на это нам глубоко плевать, мы до таких значений никогда не досчитаем, поскольку сбросимся в ноль раньше!).

Но теперь, с введением регистра, это выражение может подложить нам свинью. В данном конкретном примере счёта до 10 (т.е от 0 до 9), нам нужно среагировать на восьмёрку - занести в регистр TC единичку, которая к следующему такту поступит в нашу логику и подскажет: пора закругляться! Левая скобка ((IntQ & Limit) == Limit) превращается в выражение IntQ[3], т.е мы попросту смотрим на старший бит, и если он единичный, значит, мы успешно досчитали до восьми. Всё верно - появится единичка, которая в следующий раз "сбросит" нас в ноль.

Но вот незадача - когда мы досчитаем до девяти, выражение IntQ[3] снова будет равно единице. И когда мы уже сбросимся в ноль, до нас дойдёт эта единичка и заставит схему "сброситься" ещё раз! Чтобы не допустить этого, мы проверяем самый младший бит - он также должен совпадать. В случае восьмёрки мы должны иметь нулевой младший бит. Поскольку мы досчитываем лишь одно "лишнее" число, большее Limit, то такой проверки точно будет достаточно.

Наконец, узнаем, чего мы достигли. Во-первых, данная схема, если задать деление в 80 000 раз, синтезируется в 25 ЛЭ, тогда как стандартный lpm_counter при данных параметрах занимает 27 ЛЭ. В этом заслуга "загадочной" строки: вместо точного сравнения 17-битного числа с константой, что требует 17 входов, а значит, 5 ЛЭ, мы проверяем лишь наличие десяти единиц на своих местах, а также младший разряд, всего 11 входов, или 3 ЛЭ.

Гораздо важнее другое: Timing Analyzer показывает, что на кристалле "-3" (самом медленном) максимальная задержка распространения составит 8,6 нс (116,28 МГц), вместо 73 МГц у библиотечного элемента! Ускорение составило 60%.

Нарисуем схему "часов" на новых счётчиках FastCounter:



Благодаря наличию выхода ceo мы убрали львиную долю элементов "И" (они просто вошли внутрь самих модулей), что упростило восприятие. В кои-то веки у нас практически нет пересечений проводов :)

Идущие по цепочке провода ce - ceo немного пугают: опять у нас "последовательный перенос"! Но он не так уж страшен: на самом деле мы просто объединяем по "И" все предыдущие выходы TC, и умный синтезатор не станет делать это по цепочке, суммируя задержки. Вместо этого, на 2-й счётчик в цепочке придёт сигнал TC_0, на 3-й счётчик - TC_0 & TC_1, на 4-й - TC_0 & TC_1 & TC2, и так далее, требуя N/4 логических элементов на этом входе, скаскадированных между собой.

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

Поэтому мы написали ещё один небольшой модуль:
module clock24h_reset_signal (input clk, input [3:0] hours, input tensTC, output reg rstout = 1'b0);

always @(posedge clk)
	rstout <= tensTC & hours[1] & hours[0];
endmodule


Вместо сравнения hours == 3 мы проверяем лишь два младших разряда - если они оба единичные - пора сбрасываться. В этот раз мы проверяем именно на тройку, а не на двойку, поскольку наш регистр не использует входа ce, который "тикает" раз в час. То есть, когда поступает сигнал ce, переводящий 22 в 23, мы ещё посчитали rstout = 0. В первый такт, когда часы переключились в 23, в rstout "защёлкивается" значение 1, и появляется там к следующему такту, и удерживается там целых 287999999998 тактов. Только тогда поступает следующий сигнал ce, который должен был переключить 23 в 24, но теперь он объединяется по "И" с выходом этого модуля и заставляет переключить 23 в 00.

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

Но в последующих счётчиках такой вариант очень неплох - он позволяет заранее вычислить и занести в регистр некоторые промежуточные выражения наподобие TC_1 & TC_2 (зная, что следующее переключение не может произойти сразу, будет пауза хотя бы в такт. Включать сюда TC_0 не нужно, уж слишком он "узкий", тут мы реально ошибёмся), благодаря чему ширина элементов "И" будет сохраняться небольшой. Таким образом можно наращивать счётчики практически неограниченно. Оно, в принципе, и понятно: каждому разряду надо лишь сообразить "должен ли я переключаться по следующему такту?" И у него есть десятки, сотни и тысячи тактов (от предыдущего импульса), чтобы принять это "нелёгкое решение"!

Схема часов на быстрых счётчиках (показанная выше) успешно синтезируется в 75 ЛЭ (предыдущая синтезировалась в 89 ЛЭ, сыграли свою роль упрощённые компараторы, а главное, синтезатор вздохнул с облегчением, не пытаясь "распараллелить" вычисление сигналов cnt_en на каждый счётчик по всей совокупности предыдущих бит), и теперь тайминги обеспечиваются даже на кристалле "-3": мы имеем задержку распространения 12,3 нс (81,3 МГц) - опять "на краю", но если вдруг захочется обеспечить бОльший запас, мы знаем, что делать.

Наконец, посмотрим на симуляцию (мы подключили вход ce 3-го счётчика, счётчика секунд, к "1", чтобы не убить симулятор, пытаясь изобразить 6 912 000 000 000 тактов! т.е у нас секунда теперь прибавляется на каждом такте)



На скриншоте изображён момент, когда 23:59:59 переключается в 00:00:00. Как видно, всё работает, причём переключение происходит очень "ровно".


В следующей части мы поставим в наш "генератор множества Мандельброта" быстрые счётчики и сумматоры и попытаемся всё-таки заставить его работать на ПЛИС...

UPD. Приведённый в этой части модуль содержит ошибку, исправленный код приведён в части 'h37.
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 

  • 6 comments