nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

Мучаем 5576ХС4Т - часть 4 - делитель частоты

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


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

Сделаем делитель с 80 МГц до 1 кГц и 1 Гц.


Как водится, продолжим тот же проект - не придётся заново вводить модель ПЛИС, назначать выводы I/O, да и дешифратор двоичного кода нам пригодится :)

Напишем следующий модуль на verilog'e:

module GenHID (input clk,
		  output ce_fast,
		  output ce_slow);

	parameter Fclk = 80000000 ; //80 МГц
	parameter Ffast = 8000000 ; //8 МГц для симуляции
	parameter Fslow = 1000000; //1 МГц для симуляции

	reg [16:0] ct_fast = 0; //Первый счетчик
	reg [9:0] ct_slow = 0; //второй счетчик
	assign ce_fast = (ct_fast == (Fclk/Ffast) - 1) ;
	assign ce_slow = ce_fast & (ct_slow == (Ffast/Fslow) - 1) ;

        always @(posedge clk) begin
	  ct_fast <= ce_fast? 1'b0 : ct_fast + 1'b1 ;
	  ct_slow <= ce_slow? 1'b0 : ce_fast? ct_slow + 1'b1 : ct_slow ;
        end
endmodule


Мы назвали его GenHID, поскольку выдаваемые им частоты понадобятся нам именно при "непосредственном взаимодействии с человеком" через кнопочки, лампочки и 7-сегментные индикаторы. 1 кГц или близко к тому - частота, удобная как для развёртки 7-сегментных индикаторов, так и для подавления дребезга контактов. 1-2 Гц понадобятся, если мы запустим какие-нибудь часы, ну и просто для мигающих штук. Поэтому генератор для Human Interface Device :)

Мы определяем параметры Fclk, Ffast, Fslow. Они существуют на этапе компиляции, "внутрь ПЛИС" они не попадают. Когда мы начинаем создавать "экземпляры" (instances), то есть либо размещать этот модуль на принципиальной схеме, либо запихивать его внутрь других модулей, мы можем для каждого экземпляра переопределить эти параметры. Именно так мы и сделаем - сейчас выбрано деление частоты в 10 раз, а затем ещё в 8 раз - при таких величинах мы сможем просимулировать работу этой схемы. Если бы мы попытались запустить симуляцию с реальными параметрами - делении в 40 млн. раз - компьютер конечно посчитал бы всё и даже нарисовал на экране, но нам самим пришлось бы долго и упорно выискивать среди 80 млн тактовых импульсов те самые 2 выходных - удовольствие ниже среднего.

Как мы видим, параметры можно складывать, вычитать, умножать и делить - никаких проблем, ведь всё это делается на этапе компиляции, индивидуально для каждого экземпляра модуля. Поэтому при синтезировании схемы, строка
assign ce_fast = (ct_fast == (Fclk/Ffast) - 1) ;


превратится в

assign ce_fast = (ct_fast == 9) ;


Что удивительно, при сравнении величин, Quartus не выдаёт предупреждения о несовпадении ширины данных. Если, к примеру, мы сравним 4-битную шину со значением 42, компилятор сразу же сообразит, что результат сравнения всегда false, и проведёт масштабную оптимизацию!

Так что если мы ошибёмся в этом модуле и не зададим сколько надо бит под счетчик, с предупреждением вы всё-таки столкнёмся: Output pins are stuck at VCC or GND. Очень важное предупреждение, всегда надо к нему прислушиваться!

Вслед за параметрами мы определяем регистры, используя ключевое слово reg. Мы задали две штуки: один 17-битный ct_fast, второй - 10-битный ct_slow.

Это - наши элементы памяти. Делать для них непрерывное присваивание командой assign - недопустимо (пропадает весь смысл!). Их можно инициализировать, что мы и сделали при объявлении регистров.

Тогда, сразу после конфигурации ПЛИС, этот регистр будет содержать нулевое значение. Для нашего счетчика это особенной роли не играет, и мы обнулили их, чтобы "ублажить" программу симуляции, иначе там как начнутся неопределённые значения, так и останутся. (Ну и к тому же, нам это ничего не стоит - каждый из 9984 регистров ПЛИС, а также каждый из 96 кбит данных может быть инициализирован как мы пожелаем). А вот какой-нибудь program counter или регистр состояния UART передатчика просто необходимо правильно инициализировать, чтобы сразу по включении не наломать дров.

А дальше изменение значений регистров производится только внутри always-блоков.

"собачка" всего навсего означает "at", то есть мы прочитываем строку

always @(posedge clk)


"Всегда при положительном фронте clk:"

Далее, примерно как в паскале: если мы описываем лишь одну операцию, можем написать её сразу. Если больше одной - нужно написать begin .. end

Ровно так здесь и сделано. Поэтому не пугайтесь и такой строчки (иногда можно встретить):

always @(posedge clk) if (ce) begin


if(ce) уже не относится к заголовку always-блока. По смыслу, оно должно было бы изображаться так:

always @(posedge clk)
       if (ce) begin


       end


posedge - положительный фронт. Есть также ключевое слово negedge - отрицательный фронт, но он используется гораздо реже, например, в ситуациях, когда у нас данные приходят по обеим фронтам (это и называется DDR - Double Data Rate).

Наконец, можно встретить такую строку:
always @(posedge clk or posedge aclr) begin


Это означает: действия внутри блока могут быть выполнены как по положительному фронту clk, так и по положительному фронту aclr, т.е aclr имеет право "нагрянуть" в любой момент, это асинхронный вход (при названии aclr, если только разработчик над нами не издевается, это асинхронный сброс - Asynchronous CLeaR). Нужны ли они - отдельный вопрос, отчасти религиозный, пока не будем сильно углубляться.

Две строки assign нам вполне понятны: оба выхода модуля, ce_fast и ce_slow - комбинаторные, на них появляется единичка только в момент, когда соответствующий счётчик досчитал "до конца". Длительность этой единички составит ровно один такт.

И наконец, разберёмся в двух строках, заключённых внутри Always-блока. Там применён ещё один тип присваивания - "неблокирующее присваивание" (non-blocking assignment), обозначаемое как <=

Пока внутри always-блока стоят только такие присваивания, его строки можно тасовать между собой в любом порядке, поскольку ВСЕ ЗНАЧЕНИЯ в правых частях оператора присваивания имеют значение с прошлого шага. Мы не торопимся изменить эти значения - сначала посчитали результат каждой строки, и лишь под самый конец, одновременно, поменяем содержимое регистров.

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



К моменту прихода положительного фронта все новые значения уже должны быть подготовлены и находиться на входе триггеров (регистра), чтобы потом просто "защёлкнуться".

Обычный знак = внутри always-блока называется блокирующим присваиванием, это значит, что во всех последующих строках будет использоваться уже обновлённое значение регистра. Иногда можно так и поступить, но надо понимать - это не приведёт к какому бы то ни было упрощению итоговой схемы. Кроме того, больше возможностей для ошибки. Мы имеем полное право написать внутри always-блока:

X = X + 1;
X = X + 1;
X = X + 1;

В действительности, присвоение будет сделано ровно одно - последнее, а что именно присвоить - умный компилятор посчитает заранее, превратив это в:

X <= X + 3;


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

Наконец, в строках внутри always-блока мы видим условный оператор: если выражение перед вопросиком истинно, выбирается первое значение, если ложно - значение после двоеточия.

Итак, разберёмся наконец, что происходит в этом модуле.

На каждом фронте тактового импульса к "быстрому счётчику" прибавляется единичка, а если он досчитал до финального значения (до 80 тысяч, к примеру, если мы хотим из 80 МГц получить 1 кГц), он обнуляется и начинает считать заново.

"Медленный" счётчик ведёт себя почти также, но он прибавляет единичку лишь в те моменты, когда "быстрый" счетчик дошёл до конца и обнулился.

Пора промоделировать его работу, создав ещё один Vector Waveform File. Но для начала нам надо проинформировать Quartus, какую тактовую частоту вообще ожидать по входу clk. Это нужно, чтобы он смог во время синтеза схемы убедиться - все задержки распространения сигнала позволяют нашей схеме работать корректно.

Лезем в Assignments - classic timer analyzer settings. Можно просто ввести значение 80 МГц в default required fmax, но тогда при компиляции мы увидим предупреждение: Found pins functioning as undefined clocks and/or memory enables. Если его раскрыть, будет
info: assuming node clk80MHz is an undefined clock.

Как видим, quartus достаточно умён, сам до этого догадается. Но чтобы лишний warning не мозолил глаза, мы можем в том же classic timer analyzer settings тыкнуть по кнопке individual clocks и уже там определить новые "часы" - дать им какое-нибудь красивое название, например clk80MHz (автор никогда не считал себя развитым на 4π, имеет право не проявлять креатива!), указать, на какой пин оно идёт и какую частоту имеет - 80 МГц.

Запускаем компиляцию, видим в summary, что Met timing requirements: yes, это хорошо. Можно зайти отдельно в Compilation report - timing analyzer - summary и увидеть:



Slack означает - какой у нас имеется запас, т.е сколько времени проходит от полного установления всех входов регистров до поступления положительного фронта. Видно, что мы работаем уже близко к пределу. 120 МГц должна ещё потянуть, а хоть мегагерцем больше - ни-ни!

Причём этот результат получен, когда блок genHID уже был включён в общую схему с обозначенными выводами, так что Quartus знает, что тактовая частота поступает по специально обученному тактовому входу. Если выставить наш GenHID как top-level entity и запустить на компиляцию, мы получим следующий результат:



Здесь у нас остался лишь 10-процентный запас по частоте, а ведь речь о весьма простом модуле, при совершенно свободной ПЛИС, когда можно распихивать элементы как угодно! По мере увеличения заполнения могут возникать дополнительные задержки на передачу с конца в конец. Мы видим, насколько важно пользоваться глобальными тактовыми входами.

Ещё заглянем в compilation summary, где увидим, total logic elements: 62 / 9984 (<1%).
Вообще-то, многовато, эту схему можно реализовать и на меньшем количестве элементов (надеюсь рассказать как-нибудь попозже), но на первый раз сойдёт!

Промоделируем, наконец, эту штуковину!

Времени нам нужно чуточку побольше (всё-таки делим в 80 раз). Вот здесь просто чудеса интерфейса. Открыв Vector Waveform File, мы должны заглянуть в меню Edit и выбрать там End time... Вместо 1 us зададим 20 us - должно хватить.

В этот раз в нашем Vector Waveform File для строки clk мы выбираем в контекстном меню value - clock... (да кто бы мог подумать!!), и там вместо вбивания ручками длительности одного такта, можно выбрать Base waveform on clock settings, и там уже установлен наш clk80MHz - замечательно!

В simulator settings заменяем старый файл Decoder3to8test.vwf на новенький, который автор обозвал романтично: GenHIDtest.vwf, и запускаем симуляцию.

Получается следующая картина:
Seq_logic.gif

Мы видим, что импульс ce_fast формируется ровно как надо. На каждые 10 тактов приходится ровно один импульс ce_fast, он занимает один такт. "ce" расшифровывается как clock enable.

Именно таким образом мы должны "замедлять" процессы, а не путём непосредственного деления частоты! Желательно, чтобы ВСЕ наши модули работали от единой частоты, от 80 МГц, поскольку количество глобальных соединений, пригодных для "часов", не так уж много, и потому что если мы делим частоты "рабоче-крестьянски", с помощью триггеров, без применения ФАПЧ (фазовая автоподстройка частоты, она же PLL, Phase Locked Loop), у нас будут фазовые сдвиги между различными "часами", что может приводить к очень интересным эффектам, когда мы пытаемся перенести сигнал из одной части схемы в другую, работающую на другой частоте. В общем, тематике crossing clock domains посвящены целые талмуды и многочасовые лекции на ютубе. Поэтому рекомендуется не создавать новые меандры, которые мы будем подавать на тактовые входы отдельных цепей, а вырабатывать импульсы clock enable, благо в логических ячейках ПЛИС такой вход изначально присутствует в триггере!

Увы, импульс ce_slow немножко "подкачал" - он затянут относительно ce_fast и имеет меньшую длительность (а за один такт приходит ложный импульс, но ещё меньшей длительности).

Но как ни странно, даже с таким импульсом схема работает как следует!

Если же у нас возникают сомнения в этих импульсах, мы можем их "защёлкнуть"! Они будут задержаны на один такт, но здесь это вообще не принципиально. Добавим в нашу схему "улучшенные" выходы:

module GenHID (input clk,
		  output ce_fast,
		  output ce_slow,
                  output reg ce_reg_fast,
                  output reg ce_reg_slow);

	parameter Fclk = 80000000 ; //80 МГц
	parameter Ffast = 8000000 ; //8 МГц для симуляции
	parameter Fslow = 1000000; //1 МГц для симуляции

	reg [16:0] ct_fast = 0; //Первый счетчик
	reg [9:0] ct_slow = 0; //второй счетчик
	assign ce_fast = (ct_fast == (Fclk/Ffast) - 1) ;
	assign ce_slow = ce_fast & (ct_slow == (Ffast/Fslow) - 1) ;

        always @(posedge clk) begin
	  ct_fast <= ce_fast? 1'b0 : ct_fast + 1'b1 ;
	  ct_slow <= ce_slow? 1'b0 : ce_fast? ct_slow + 1'b1 : ct_slow ;
          ce_reg_fast <= ce_fast;
          ce_reg_slow <= ce_slow;
        end
endmodule


Мы пока оставили "старые" выводы, чтобы можно было сравнить их между собой. Вот, что получается, вблизи:



Мы видим, как "облагородились" наши сигналы - все выбросы ушли, в том числе "ложный импульс" по ce_slow, который выглядел весьма неприятно. Длительность импульсов стала почти в точности равна одному такту, они выходят почти ноздря в ноздрю.

Наконец, если мы захотим "выкинуть" исходные выводы ce_fast и ce_slow, можно изменить модуль следующим образом:

module GenHID (   input clk,
                  output reg ce_reg_fast,
                  output reg ce_reg_slow);

	parameter Fclk = 80000000 ; //80 МГц
	parameter Ffast = 8000000 ; //8 МГц для симуляции
	parameter Fslow = 1000000; //1 МГц для симуляции

        wire ce_fast, ce_slow;

	reg [16:0] ct_fast = 0; //Первый счетчик
	reg [9:0] ct_slow = 0; //второй счетчик
	assign ce_fast = (ct_fast == (Fclk/Ffast) - 1) ;
	assign ce_slow = ce_fast & (ct_slow == (Ffast/Fslow) - 1) ;

        always @(posedge clk) begin
	  ct_fast <= ce_fast? 1'b0 : ct_fast + 1'b1 ;
	  ct_slow <= ce_slow? 1'b0 : ce_fast? ct_slow + 1'b1 : ct_slow ;
          ce_reg_fast <= ce_fast;
          ce_reg_slow <= ce_slow;
        end
endmodule


Тут мы применили ещё одно ключевое слово - wire. В "классическом" verilog (тот, что не system verilog), это практически противопоставление ключевому слову reg. Как wire мы обозначаем переменные, которые жестко определяются входными сигналами и значениями регистров в данный момент времени.

В system Verilog появляется новое ключевое слово - logic, и его рекомендуют применять вместо wire, когда у нас используются только два состояния - чёткая "единица" и чёткий "ноль". А wire должен использоваться в более сложных случаях - для двунаправленной передачи (выходной каскад должен уметь переходить в Z-состояние с высоким выходным сопротивлением), либо когда устройства имеют выход с открытым коллектором (скорее - открытым стоком) и объединены по принципу "проводного И" либо "проводного ИЛИ", и всё в этом духе.

Можно чуть-чуть сократить код: если при объявлении регистра мы можем его инициализировать, то при объявлении wire мы можем в той же строке объявить, к чему он "присоединён", чтобы не писать потом отдельный assign:

module GenHID (   input clk,
                  output reg ce_reg_fast,
                  output reg ce_reg_slow);

	parameter Fclk = 80000000 ; //80 МГц
	parameter Ffast = 8000000 ; //8 МГц для симуляции
	parameter Fslow = 1000000; //1 МГц для симуляции

	reg [16:0] ct_fast = 0; //Первый счетчик
	reg [9:0] ct_slow = 0; //второй счетчик
	wire ce_fast = (ct_fast == (Fclk/Ffast) - 1) ;
	wire ce_slow = ce_fast & (ct_slow == (Ffast/Fslow) - 1) ;

        always @(posedge clk) begin
	  ct_fast <= ce_fast? 1'b0 : ct_fast + 1'b1 ;
	  ct_slow <= ce_slow? 1'b0 : ce_fast? ct_slow + 1'b1 : ct_slow ;
          ce_reg_fast <= ce_fast;
          ce_reg_slow <= ce_slow;
        end
endmodule


Отдельный модуль мы проверили, теперь надо с его помощью чем-нибудь помигать. Увы, если мы просто подключим светодиод к одному из выходов ce_fast или ce_slow - мы ничего не увидим, одиночные импульсы длиной 12,5 нс, между которыми интервал 1 мс .. 1 с - невозможно обнаружить!

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

module toggle (clk, ce, Q);

	input clk, ce;
	output reg Q;
	
	always @(posedge clk) if (ce)
		Q <= ~Q;

endmodule


Снова мы немножко "играемся" с синтаксисом, смотрим, как ещё можно описать то же самое. Здесь мы в списке выводов модуля toggle не стали описывать, какие из них входные, а какие - на выход, и какого именно типа (wire или reg) - это мы описали чуточку позже. Возможно, такой вариант более предпочтителен, когда мы в начале работы над модулем выводим "наружу" множество его внутренних цепей, для отладки, а потом хотим быстренько их оттуда убрать. Тогда мы просто удаляем ключевое слово output из объявления переменных, и "вычеркиваем" эти цепи из списка.

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

module ParamOut (clk, Q);

	parameter Width = 5;

	input clk;

	output reg [Width - 1 : 0] Q;

        //описание модуля

endmodule


Тут подобный синтаксис становится осмысленным: мы сначала определяем параметр (а он просто ОБЯЗАН быть определён внутри модуля!), а потом используем его для определения ширины входных и выходных шин.

Впрочем, если мы напишем так:

module ParamOut (input clk, output reg [Width - 1 : 0] Q);

	parameter Width = 5;
        
        //описание модуля

endmodule


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

Но человек всё-таки читает эти тексты сверху вниз, и ему более длинный синтаксис становится более понятным, сущности не возникают "из ниоткуда".

Создаём схемотехнический символ как для GenHID, так и для toggle, и размещаем всё это на нашей схеме:



Первое, что мы видим - рядом с модулем GenHID появилась таблица его параметров. Когда его первоначально размещаешь, задаются параметры, которые мы вписали в коде, но можно теперь тыкнуть по ним дважды и задать любые значения. Так мы и сделаем, когда решим, наконец, зашить это дело в ПЛИС.

Второе - мы уже вводили шину LED[7..0] - наши 8 светодиодиков. Как подключить к ней одиночный выход Q модуля toggle? А очень просто: вытаскиваем из него провод, выделяем его, вызываем правой кнопкой мыши контекстное меню, а в нём - properties. И там можно задать имя проводу. Мы пишем: LED[0], и теперь этот провод будет "подсоединён" к шине. Не очень красивый способ, было бы приятнее рисовать "жгуты" с заходящими в них проводами, но уж что есть.

Запускаем моделирование этой штуки:


Вот он, долгожданный "медленный" меандр! Напоминаем - не надо такие меандры подавать на тактовые входы. А вот на светодиод - всегда пожалуйста.

Ну и последний шаг - меняем параметры, пусть теперь
Ffast = 1000
Fslow = 2

ну и прошиваем это дело в нашу ПЛИС.

Ещё одно напоминание: мы определили два входа для тактовой частоты. Один назывался Clk80MHz, хотя удобнее будет переименовать просто в clk (чтобы при симуляции отдельных модулей их тактовая частота сразу поступала откуда надо, с глобального тактового входа), второй - YetAnotherClk80MHz, который на схеме задействован не будет, но важно, чтобы он был определён - только тогда он сконфигурируется на вход и не будет закорачивать наш генератор тактовой частоты. Это особенность отладочной платы LDM-Systems. Автор так акцентирует на этом внимание, поскольку провёл долгие часы, пытаясь понять - почему ПЛИС не желает корректно работать. Уже расчехлил осциллограф, стал смотреть входы, но вот ведь незадача - даже с делителем 1:10 осциллограф прилично влияет на вход 80 МГц - стоит туда прикоснуться щупом, как работа ПЛИС с тактовой частотой и вовсе прекращается, она замирает на одном такте. Поэтому даже тогда виновник в явном виде не проглядывался, казалось - либо генератор слабоват, либо ПЛИС тормозная, либо и то, и другое сразу.

Как видим, мигает:


Tags: ПЛИС, работа, странные девайсы
Subscribe

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

    Задался этим вопросом применительно к своему прибору чуть более 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 

  • 4 comments