Мучаем 5576ХС4Т - часть 7 - счетчики и жаба

Oct 30, 2018 02:11

Часть 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 и МКО (Mil-Std 1553), немножко ещё поиграемся с реализацией счетчиков на ПЛИС. Счетчик - один из основных "строительных блоков" подобных схем. Поделить частоту до требуемой, посчитать количество переданных бит, чтобы вовремя остановиться, последовательно перебирать состояния конечного автомата - всё это счетчики.

И хочется, разумеется, чтобы счетчики занимали настолько мало логических ячеек, насколько это возможно. Хочется не только нам, поэтому у каждой логической ячейки 5576ХС4Т, равно как у её "родоначальника" flex10k, да и у львиной доли различных ПЛИС, есть специальный "счетный" режим работы.




Если удаётся его задействовать, требуемое количество логических элементов резко сокращается, вплоть до N штук на N-битный счетчик! В части 5, на 27-битный счетчик у нас ушло 62 логические ячейки - явный перебор! Можно сказать: на фоне 9984 доступных, это капля в море и крохоборство, а когда хватать не будет - что это отсталые российские технологии, на которых ничего нормального сделать нельзя, а вот на Virtex вообще никаких проблем! А можно немножко уважить свою жабу и разобраться, как реализовать счетчики наиболее эффективно.

Заодно познакомимся с другими "хитростями" verilog'а.


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

`include "math.v"

module CustomCounterPureVerilog (input clk, output ceo);

parameter Fclk = 80_000_000;
parameter Fout = 1000;

localparam Width = `CLOG2(Fclk / Fout);
localparam Limit = Fclk / Fout - 1;

reg [Width - 1 : 0] Q;

assign ceo = (Q == Limit);

always @(posedge clk)
Q <= ceo? 1'b0 : Q + 1'b1;

endmodule

Заметим, что в этот раз мы написали в параметре число 80_000_000, хотя раньше писали просто: 80000000. Что же означает знак нижнего подчеркивания? Для компилятора - ровным счётом НИЧЕГО. Он игнорируется, даже вот так посреди числа. Зато человеку прочитать такие значения куда проще, чем пальцем по экрану вести, подсчитывая количество ноликов.

Данный счетчик должен "делить частоту" в 80 000 раз, с 80 МГц до 1 кГц, на что требуется 17 бит, т.е 17 регистров.

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

Inferred 1 megafunctions from design logic

Сама по себе строка очень приятная - обычно это и означает как раз, что Quartus нашел в нашем коде определённые элементы, под которые есть специализированная конфигурация логических ячеек. Увы, если мы посмотрим на подробности, то увидим: Quartus немножко обознался:

Info: Inferred adder/subtractor megafunction ("lpm_add_sub") from the following logic: "Add0"

Вместо счётчика, синтезатор углядел сумматор! Да, такой режим у логических ячеек тоже есть:




и он незаменим, когда мы будем реализовывать на ПЛИС своё собственное вычислительное ядро, но здесь этот режим, увы, плохо подходит. К 17 логическим ячейкам, только выполняющим прибавление единицы, придётся присовокупить ещё 17, которые будут подавать на вход правильное значение - либо выход счётного регистра, либо константу. Итого уже получится 34 ячейки, а ведь ещё нужно выполнить сравнение выхода со значением 79 999. Вот как-то так и приходим к получившимся 38 ячейкам.

Странная вещь, но если УСЛОЖНИТЬ модуль, добавив вход синхронного сброса, то Quartus наконец-то очухается и признает в данном модуле счетчик:

`include "math.v"

module CustomCounterPureVerilog (input clk, input sclr, output ceo);

parameter Fclk = 80_000_000;
parameter Fout = 1000;

localparam Width = `CLOG2(Fclk / Fout);
localparam Limit = Fclk / Fout - 1;

reg [Width - 1 : 0] Q;

assign ceo = (Q == Limit);

always @(posedge clk)
Q <= (sclr | ceo)? 1'b0 : Q + 1'b1;

endmodule

Теперь уже при синтезе мы получим следующую строку:
Info: Inferred lpm_counter megafunction (LPM_WIDTH=17) from the following logic: "Q[0]~18"

lpm означает Library of Parameterized Macros.

И в итоге, количество логических ячеек: 23 - уже гораздо лучше!

Следующий кандидат на сокращение - строка Q==Limit. В нашем случае Limit = 79 999, и проверка условия превращается в вычисление выражения (поскольку Limit = 100111000011111112):

assign ceo = Q[16]&(~Q[15])&(~Q[14])&Q[13]&Q[12]&Q[11]&(~Q[10])&(~Q[9])&(~Q[8])&(~Q[7])&Q[6]&Q[5]&Q[4]&Q[3]&Q[2]&Q[1]&Q[0];

Ясное дело, для вычисления этого выражения необходимы все 17 бит. В каждой логической ячейке 4 входа, так что только на входе понадобится 5 ячеек. Их выходы могут объединяться через цепи каскадирования, что позволит не городить дополнительных ячеек для выполнения операции "И" между промежуточными значениями:




Цепи каскадирования позволяют очень эффективно вычислять громоздкие выражения в конъюнктивной нормальной форме (КНФ), к которым относится и наше :)

Как видно, к 17-битному счетчику и добавляется 5 ячеек для проверки равенства, это уже 22, и ещё один объединяет входы синхронного сброса sclr и ce, вот и получаем 23 - всё чётко.

Можно ли компактнее? Да.

Равенство Q == Limit - слишком строгое условие. По сути, нам нужно выражение, которое равно нулю для Q < Limit, равно единице при Q == Limit, и может принимать любое значение при Q > Limit, поскольку выше мы всё равно забираться не будем - счётчик по достижении Limit сбрасывается в ноль.

Такому требованию удовлетворяет следующее условие:

assign ceo = Q[16]&Q[13]&Q[12]&Q[11]&Q[6]&Q[5]&Q[4]&Q[3]&Q[2]&Q[1]&Q[0];

то есть, мы лишь проверяем, что единицы стоят на своих местах, а стоят ли нули на оставшихся - нас не волнует. Ведь если вместо нуля стоит единица, значит Q больше, чем Limit, ну и ладно!

Как ни странно, нам вовсе не нужно каждый раз выписывать такое условие "ручками", достаточно записать его так:

assign ceo = ((Q&Limit) == Limit);

Умный компилятор сообразит, что на ответ влияют только те разряды, где мы ожидаем встретить единицы, и упростит схему. Таких разрядов у нас 11, так что теперь хватит 3 логических ячеек. Запуская синтез усовершенствованной схемы, мы и правда получим 21 LE вместо 23.

Пожалуй, эти 21 логические ячейки - это минимально возможное количество, которого мы добьёмся на "чистом" Verilog'е.

Добавление lpm_counter в явном виде

Если же идти до конца, то можно получить наиболее компактный в плане реализации код, но, к сожалению, он будет привязан конкретно к Альтеровским/Интеловским/Воронежским ПЛИС, и в целом будет уже не столь выразителен.

Для этого заметим две вещи:

- присвоить регистрам счетчика некоторую константу - ничуть не сложнее, чем обнулить его;
- на самом верхнем рисунке в этом посте (рисунок 11 из технического описания ПЛИС) показан выход переноса, он же cout (carry out). Именно когда мы досчитали до всех "единиц", здесь появится единичка, поэтому именно сюда можно присоединить выход ceo, а заодно и вход синхронной загрузки, который заставит счетчик по следующему тактовому импульсу не сброситься в ноль, а выставить некоторую константу.

К примеру, если мы хотим считать до 10 (делить частоту в 10 раз), мы можем идти не от 0 до 9, а от 6 до 15. Таким образом, сигнал окончания счёта мы получаем "нахаляву".

Приведём код:

`include "math.v"

module CustomCounter ( input clk, //clock
output ceo); //clock enable - output

parameter Fclk = 80_000_000;
parameter Fout = 1000;

localparam Width = `CLOG2(Fclk / Fout);
localparam Limit = Fclk / Fout ;
localparam MaxInt = 1 << Width;

wire TC;

assign ceo = TC;

lpm_counter lpm_counter_component (
.clock (clk),
.sset (TC),
.cout (TC),
.q (),
.aclr (1'b0),
.aload (1'b0),
.aset (1'b0),
.cin (1'b1),
.clk_en (1'b1),

.cnt_en (1'b1),
.data ({Width{1'b0}}),
.eq (),
.sclr (1'b0),

.sload (1'b0),
.updown (1'b1));
defparam
lpm_counter_component.lpm_direction = "UP",
lpm_counter_component.lpm_port_updown = "PORT_UNUSED",
lpm_counter_component.lpm_svalue = MaxInt - Limit,
lpm_counter_component.lpm_type = "LPM_COUNTER",
lpm_counter_component.lpm_width = Width;

endmodule

Объясним, что здесь происходит. Наконец-то мы начали соединять между собой модули не на принципиальной схеме, как раньше, а непосредственно в verilog'е.

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

Строка

lpm_counter lpm_counter_component

похожа на объявление экземпляра класса в C++: сначала мы указываем название класса, а затем - имя переменной.

В скобках мы сопоставляем входы и выходы этого модуля с другими "проводами" нашего модуля. Вход тактовой частоты в lpm_counter называется clock, а мы говорим - присоединим его к входу clk нашего модуля. Во входы можно также подавать постоянные значения, как мы сделали с aclr, aload, aset, а незадействованные можно просто не указывать, либо оставить пустые скобки, как в нашем примере. Это покажет, что мы помним о существовании таких выходов, но сознательно оставили их неподключёнными, за ненадобностью.

Затем с помощью ключевого слова defparam мы задаём параметры конкретного экземпляра модуля, того, что мы назвали lpm_counter_component.

Это - аналог того, как мы на принципиальной схеме лезли в табличку параметров и назначали их по своему усмотрению.

Смотрим, что же здесь происходит. Параметры заданы так, что по каждому фронту тактового импульса счетчик будет идти "вверх", прибавляя единичку. На выходе cout (carry out) будет появляться единичка только если счёт дошёл до всех единиц, в нашем случае до 217 - 1. Именно этот выход мы жестко соединили с ceo (clock enable - output), а также с входом синхронной установки sset (synchronous set). Когда на него поступает единичка, в регистр счетчика заносится значение, заданное в параметре lpm_counter_component.lpm_svalue. Мы его задали так, чтобы обеспечить деление частоты в нужное количество раз.

Компилируем это дело и получаем шикарный результат: всего 20 логических ячеек! Да, сэкономили ещё ровно одну. По ходу получили и ещё небольшую "головную боль" - когда напрямую используешь lpm_counter, нет никакой возможности инициализировать счётчик - он всегда начинает с нуля, что в нашем случае не есть хорошо. Получится, что в первый раз он отсчитает не 80 000 тактов, а все 217 - 1 тактов, и только после этого "выйдет на режим".

Если мы добавим ещё и вход синхронного сброса:

`include "math.v"

module CustomCounter ( input clk, //clock
input sclr, //synchronous clear
output ceo); //clock enable - output

parameter Fclk = 80_000_000;
parameter Fout = 1000;

localparam Width = `CLOG2(Fclk / Fout);
localparam Limit = Fclk / Fout ;
localparam MaxInt = 1 << Width;

wire [Width - 1 : 0] Q;

wire TC;

assign ceo = TC;

lpm_counter lpm_counter_component (
.clock (clk),
.sset (TC | sclr),
.cout (TC),
.q (Q),
.aclr (1'b0),
.aload (1'b0),
.aset (1'b0),
.cin (1'b1),
.clk_en (1'b1),

.cnt_en (1'b1),
.data ({Width{1'b0}}),
.eq (),
.sclr (1'b0),

.sload (1'b0),
.updown (1'b1));
defparam
lpm_counter_component.lpm_direction = "UP",
lpm_counter_component.lpm_port_updown = "PORT_UNUSED",
lpm_counter_component.lpm_svalue = MaxInt - Limit,
lpm_counter_component.lpm_type = "LPM_COUNTER",
lpm_counter_component.lpm_width = Width;

endmodule

то количество логических ячеек достигнет 21 - как и в реализации "на чистом верилоге".

Счетчики с входом разрешения

Пока что все рассмотренные счетчики работали строго на тактовой частоте, в нашем случае на 80 МГц. Зачастую так быстро считать не надо. Вместо подсчета тактовых импульсов, нужно считать импульсы, поступающие на вход ce (clock enable). Напишем такой модуль с учётом наших наработок:

`include "math.v"

module CustomCounterPureVerilog (input clk, input sclr, input ce, output ceo);

parameter Fclk = 80_000_000;
parameter Fout = 1000;

localparam Width = `CLOG2(Fclk / Fout);
localparam Limit = Fclk / Fout - 1;
localparam MaxInt = 1 << Width;

reg [Width - 1 : 0] Q;

assign ceo = ((Q & Limit) == Limit) & ce;

always @(posedge clk)
Q <= (sclr | ceo)? 1'b0 : ce? Q + 1'b1 : Q;

endmodule

Кажется, что модуль усложнился. Но при компиляции получаем те же самые 21 логические ячейки, как и раньше! Объяснение простое - вход разрешения уже есть в схеме логической ячейки, просто раньше мы его не использовали.

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

2. Quartus II 9.0sp2 почему-то в упор не замечает счетчиков, если только там не поставить вход синхронного сброса!

3. Когда нам нужно считать не до 2N, а до меньшего числа, мы сможем сэкономить весьма прилично на "компараторе", если будем проверять не точное равенство, а наличие "единиц" на своих местах.

4. Можно не ждать у моря погоды (обнаружит, не обнаружит) и добавить модуль счетчика lpm_counter самостоятельно. Большой выгоды это, впрочем, не принесёт. Но если Quartus начнёт тупить и упорно "разворачивать" вашу схему в lpm_adder или просто на логике "общего назначения", то можно прибегнуть и к этому методу.

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

Previous post Next post
Up