Новая игрушка: отладочная плата с ПЛИС 5576ХС6Т, это функциональный аналог Flex10k, на 2880 ЛЭ, 5 килобайт встроенной памяти, но при этом совершенно невообразимая радиационная стойкость.
Начать освоение хотелось с чего-то простого, с кнопочек и лампочек. Но в то же время хотелось чего-то необычного, поэтому рычажки взял трёхпозиционные, "On-Off-On", и реализовал декодирование троичного кода с преобразованием его в двоичный :)
Хотя дело не просто в "желании странного", такой троичный код позволит мне снизить число ног в разъёме адресной заглушки, с помощью которой задаётся адрес устройства на мультиплексном канале обмена, и при этом сохранить избыточность, чтобы неконтакт в одной из ножек разъёма был бы обнаружен...
У меня в "изделии" применяется Мультиплексный Канал Обмена, он же МКО, он же ГОСТ Р 52070-2003, он же MilStd1553, причём изделие является оконечным устройством (ОУ, оно же Remote Terminal, RT). Так вот, в ГОСТе, в пункте 4.4.1.2 есть фраза: "Адрес ОУ должен устанавливаться через внешний соединитель, который является частью монтажа системы. Изменение собственного адреса ОУ не должно требовать физической модификации или воздействия на любую часть оборудования ОУ".
Далее, имееются ОСТы (отраслевые стандарты) по монтажу разъёмов, из которых мы узнаём: в разъёме можно впаивать перемычки между выводами (это разрешено), а вот какие-нибудь резисторы и другие детальки туда запихивать нельзя, ни один нормоконтроль и ВП не пропустят! Так что кодировать один из множества уровней с помощью резистора (как это сделано, например, на джойстике камеры наблюдения, на некоторых гарнитурах и много где ещё) - не вариант.
Поэтому, не мудрствуя лукаво, мы в своё время сделали адресную заглушку на 8 ногах: два "общих провода" ("ноль вольт", GND, но с корпусом он не связан), 5 проводов адреса и 1 провод чётности, чтобы обнаружить одно нарушенное соединение. В самом изделии провода адреса подтянуты на плюс питания. Лог "1" - это когда контакт "висит в воздухе" в адресной заглушке, а лог "0" - когда там впаяна перемычка на общий провод. Так удаётся задать один из 32 адресов (точнее, из 31, поскольку адрес "все единицы" = 0x1F является групповым/широковещательным, ни одно ОУ не может его иметь!) и заведомо обнаружить один обрыв. Также нужно предусмотреть вариант, что адресную заглушку вообще не воткнули. Здесь мы получим адрес 11111, тот самый групповой/широковещательный. Впрочем, тут ещё и бит чётности не совпадёт, он тоже будет единичным, так что сумма всех бит окажется чётной, а должна быть нечётной!
В общем-то, мы руководствовались РЭ на 1895ВА2Т, специализированную микросхему контроллера информационного обмена по резервированному МКО.
Но теперь, традиционно, захотелось этот разъём, куда втыкается адресная заглушка, задействовать по-другому: вместо "глухих" заглушек в каждый комплект протянуть кабель между комплектами, по которому они будут обмениваться данными для обеспечения стереоскопического режима работы. Увеличивать размер разъёма не хочется, значит, надо потесниться!
Использование "троичной системы счисления" позволяет освободить две ножки под UART, и на оставшихся четырёх (не считая общего провода, GND) вполне спокойно представить один из 32 адресов с даже большей избыточностью, чем до этого!
До этого у нас были биты адреса A0..A4 и бит чётности, PB (Parity Bit). Теперь у нас остаются только провода (триты) адреса A0..A3. Каждый из них мы можем оставить "висеть в воздухе", либо замкнуть на общий провод (GND), ЛИБО замкнуть на выход UART-передатчика!
Три этих уровня можно обозначать по-разному. В обычной троичной системе это было бы 0, 1 и 2. Нам безусловно нравится
уравновешенная троичная система с цифрами "0", "1" и "-1", но здесь она без особой надобности, адреса нам нужны положительные, от 0 до 30, так что останемся в обычной троичной системе. Ещё и дадим ей старые добрые значения:
0 = "НЕТ",
1 = "НЕ ЗНАЮ",
2 = "ДА".
При включении комплекта, он может детектировать, куда именно ведёт перемычка. Если во время передачи произвольного байта по UART какой-то из входов A0..A3 переключается синхронно с выходом UART, значит, перемычка подключена именно к нему. Это мы можем записать как "2". Если вход остаётся с нулевым потенциалом, значит, перемычка подключена к общему проводу, и это мы записываем как "0". Это, в некотором смысле, активные уровни. И наконец, если возобладал резистор подтяжки к плюсу питания, значит, вход "болтается в воздухе" - нет перемычки ни туда, ни сюда. Это мы запишем как "1" (НЕ ЗНАЮ), и именно это значение мы считаем наиболее "подозрительным", поскольку наиболее вероятная неисправность в такой адресной заглушке - это плохой контакт. Когда срок службы должен составлять десятки лет, из которых львиную долю прибор может провести где-то на складе, в ящике, на такое нужно закладываться, и своевременно обнаружить.
Разберёмся, какие значения можно закодировать таким образом, и какие из них придётся отбросить из условий помехозащищённости. Начнём с одного трита:
0 - годится,
1 - нельзя использовать, т.к при нарушении контакта можно спутать с 0 либо 2,
2 - годится.
Из 3 возможных значений, мы смогли использовать 2. Теперь добавим второй трит:
003=0 - годится,
013=1 - нельзя использовать, можно спутать с 00 либо 02,
023=2 - годится,
103=3 - нельзя использовать, можно спутать с 00 либо 20,
113=4 - нельзя использовать, можно спутать с 01, либо 21, либо 10, либо 12,
123=5 - нельзя использовать, можно спутать с 02 либо 22,
203=6 - годится,
213=7 - нелья использовать, можно спутать с 20 либо 22,
223=8 - годится.
Получилось, что из доступных 9 значений, в ход можно пустить лишь 4. Причём выглядит так, будто бы вариант "НЕ ЗНАЮ" в любом трите совершенно недопустим - его явно можно спутать с чем-то ещё, и мы просто пришли к обычной двоичной логике.
Если так, то у нас большие проблемы: при использовании 4 тритов, несмотря на 81 разное значение, реально мы сможем использовать только 24=16 "надёжных" значений! Маловато, нам нужно 31 значение вообще-то!
Так что надо подумать ещё немного... Если у нас происходит обрыв провода, то навредить это нам может только если этот провод куда-то вёл, т.е была перемычка на выход UART ("2"), либо перемычка на общий провод ("0"). Вместо этих значений мы получаем значение "1", т.е ошибаемся ровно на единичку в ту или иную сторону. Закодированное число от этого изменяется на 3n в ту или иную сторону, где n - номер разряда (нулевой разряд - самый младший), а это заведомо НЕЧЁТНОЕ ЧИСЛО. То есть, каждый раз, допуская ошибку, мы будем чётное число превращать в нечётное и наоборот. Напомним, при возне в двоичном коде под чётностью (parity) зачастую понимают совсем другое: чётность суммы всех бит числа, тогда как чётность в обычном математическом смысле попросту задаётся младшим битом числа. Но здесь, в троичном коде, именно обычная математическая чётность нам и нужна!
Поглядим ещё раз на список из 9 чисел выше. Как видно, чётных чисел в нём больше, их 5 штук. И ещё 4 штуки нечётных. Поэтому потребуем использование только чётных чисел! При этом, как и раньше, остаются разрешены числа 0, 2,6,8, но к ним добавляется ещё и число 4 = 113. Казалось бы, уж его точно использовать нельзя, как мы написали выше, его можно спутать с 4 другими! Да вот только ровно эти 4 других мы уже и так забраковали!
Получается, что используя 4 трита, мы можем помехоустойчиво закодировать 41 различное значение. Ещё 40 значений получаются "запрещены".
По сути, мы должны прочитать троичный код, представить его в двоичном виде, после чего младший бит укажет, корректен ли наш ответ, а все остальные укажут закодированный адрес.
Не забываем и об "особом случае", когда мы в разъём так ничего и не воткнули! В данном случае это будет соответствовать числу 11113 = 27+9+3+1 = 40, т.е ЧЁТНОМУ числу, которое мы разрешили. Что ж, по такому случаю, пожалуй, мы выберем нечётные числа, поскольку РАЗРЕШЁННЫХ (не совпадающих с отключённым разъёмом) и тех и других по 40, но если сразу выбрать нечётные числа, не придётся проверять "особый случай".
Давайте попробуем эту штуку реализовать!
Так выглядит коробочка с рычажками:
Сама коробочка - из под перегоревшей нахрен ёлочной гирлянды. Саму гирлянду уже давно не видел, только коробочка и осталась. Вырезал в ней лобзиком прямоугольное отверстие на 4 рычажка - и запаял провода. Центральный у каждого свой, а крайние соединены вместе, "влево" земля (общий провод), "вправо" - провод, также подключённый к ПЛИС, чтобы она могла давать ему попеременно положительный и отрицательный потенциал.
Для начала "рисуем" такую схемку в квартусе:
Внизу не подключённые ни к чему ножки - чтобы не пожечь их по чём зря. В квартусе по умолчанию все неиспользуемые ноги конфигурируются как выход GND. Но DIP-переключатели на плате (их я назвал SW1..SW8) замыкают ножки ПЛИС напрямую на +3,3 вольта, может приличный ток пойти, под сотню мА. Уж не знаю, выгорит-не выгорит, но проверять не хочется! Кнопки по традиции коммутируют ноги на GND, и подтяжка стоит на +3,3 вольта, так что всё в порядке, но для порядку и их разместил. И наконец, два приёмопередатчика UART: один под RS232 на основе микросхемы преобразователя уровней Maxim MAX232CWE, и второй - виртуальный COM-порт на основе FTDI FT232RL. Маркировка у них, что интересно, ПРОТИВОПОЛОЖНАЯ. Т.е ножка RX у Максима - это выход приёмника, и ножка TX - вход передатчика. А у FT232, RX - это то, КУДА НУЖНО ПОДАВАТЬ СИГНАЛ, т.е FT232 считает себя уже принимающей стороной. Типа, весь канал UART оказался длиной всего в 5 сантиметров на плате, ПЛИС передала данные, а FT232 через вход RX их получает, и уже дальше через USB отсылает их на компьютер. Соответственно, из TX данные отправляются на ПЛИС. Но в проекте Quartus я, чтобы не запутаться, две ноги переименовал, у меня здесь FT232Rx - приёмник в ПЛИС, FT232Tx - передатчик в ПЛИС.
Входы Trinar - это как раз центральные контакты наших трёхпозиционных рычажков. Не знаю, что меня сейчас на слове Trinary заклинило, а не Ternary, ну так уж вышло, пущай будет.
clk - это вход, к которому подключён генератор тактовой частоты 50 МГц. В ТУ на ПЛИС даётся некий параметр, "Длительность тактового интервала межрегистровой пересылки", и его значение: 25 нс. Взять обратное значение - получится 40 МГц, на основании чего у меня коллеги хором говорили: больше 40 МГц на эту ПЛИС подавать вообще нельзя! Тем не менее, работает оно без проблем. Я это так понимаю: этот параметр в ТУ появился оттого, что он был прописан в ТЗ на разработку данной ПЛИС. В реальности в ПЛИС этих длительностей межрегистровой пересылки пара десятков, всё зависит, какой регистр к какому подключён. Если это внутри одного LAB (Logic Array Block) - это одно. А если оно соединяется через глобальные интерконнекторы и идёт в противоположный угол - это другое. Если один из регистров сидит уже в IO - это третье. Так что указанные 25 нс - это "наихудший случай", вероятно из угла в угол при пониженных напряжениях питания и на самом глубоком минусе, притом С ЗАПАСОМ, чтобы во время приёмосдаточных испытаний этой ПЛИС уж точно она эти испытания не провалила! Т.е сюда же вносят все возможные погрешности при измерениях, например, допустимое смещение по времени между двумя каналами осциллографа. Поэтому я буду руководствоваться инструкцией по программированию для этой ПЛИС. А там всё просто: проект делаем в Квартусе, под ПЛИС EPF10K50EQC240-3. Просто, да не совсем: такой ПЛИС в Квартусе нет в списке! На компакт-диске, который прилагался к отладочной плате, сказано было выбрать EPF10K50SQC240-3. Что ж, пока всё получается при таком выборе, все ножки как надо назначаются. Ну а что касается быстродействия - если Timing Analyzer считает, что конкретный проект заработает без проблем - значит, так оно и будет, даже в наихудшем случае.
Выход TrinarPulse - тот, что подключён к правым контактам наших рычажков. И наконец, LEDS[7..0] - это 8 светодиодов на плате.
Два модуля из трёх, изображённых здесь - библиотечные. Вверху сидит lpm_counter (я его добавляю из папки /libraries/megafunctions/arithmetic/lpm_counter), самый простой двоичный. Из дополнительных выходов только cout (carry out), на котором появляется единичка ровно на 1 такт, когда счётчик досчитал "до конца" и со следующего такта снова сбросится в ноль. Частота 50 МГц делится в 216 раз, что даёт примерно 763 Гц. Т.е импульсы будут поступать каждые 1,3 мс. Почему я его назвал gen10ms - а кто его знает...
Latch8bit, сидящий справа - это lpm_dff (его добавил из папки /libraries/megafunctions/storage/lpm_dff), просто регистр, который защёлкивается, когда придёт сигнал enable, и держит это значение. Да,
flip flop и latch - это штуки разные, назвал его Latch просто исходя из его назначения в этой "схеме". Ему лишь нужно защёлкнуть декодированное значение и держать его, пока мы не декодируем новое, иначе светодиоды будут неприятно мерцать, реагируя на процесс опроса.
И наконец, самописный модуль DecodeTrinary, вот его код:
module DecodeTrinary( input clk, input ce, input [3:0] Trinary,
output [7:0] Q, output DV, output reg TrinaryPulse);
reg [3:0] Prev;
assign Q[0] = Trinary[0];
assign Q[1] = Prev[0];
assign Q[2] = Trinary[1];
assign Q[3] = Prev[1];
assign Q[4] = Trinary[2];
assign Q[5] = Prev[2];
assign Q[6] = Trinary[3];
assign Q[7] = Prev[3];
always @(posedge clk) if (ce)
TrinaryPulse <= ~TrinaryPulse;
assign DV = ce & (~TrinaryPulse);
always @(posedge clk) if (ce)
Prev <= TrinaryPulse? Prev^Trinary : Trinary;
endmodule
Первое, что видим: на выходе TrinaryPulse формируется меандр: по каждому приходу Clock Enable из gen10ms логический уровень переключается на противоположный.
За один период TrinaryPulse мы успеваем "декодировать" состояние наших рычажков. Давайте начнём с состояния TrinaryPulse=0. Рычажки, установленные в "0", выдадут лог. 0 (центральный контакт замкнут на GND), установленные в "1" выдадут лог. 1 (центральный контакт висит в воздухе, но в ПЛИС включена подтяжка к +3,3 вольтам), а рычажки, установленные в "2", сейчас выдадут лог. 0, поскольку центральный контакт замкнут на TrinaryPulse, а он сейчас нулевой.
К приходу ce=1, напряжения на рычажках должны уже установиться (на это им даётся целая миллисекунда!), и поскольку TrinaryPulse=0, в регистры Prev будут защёлкнуты "текущие показания" этих рычажков, 0 в случае "0" или "2", и 1 в случае "1". Уже возникает импульс DV, т.е Data Valid, но в этот, самый первый раз, он ошибочный (полагаем, это не страшно).
К следующему такту TrinaryPulse переключается в единицу, и мы снова ничего не делаем, давая время установиться напряжениям. Сейчас рычажки, установленные в "2", выдадут лог. 1. Когда придёт ce=1, мы сделаем XOR между значениями в регистре Prev (т.е предыдущее напряжение на рычажке) и текущими значениями.
Чтобы XOR выдал единичку, надо, чтобы напряжения поменялись во время переключения TrinaryPulse, т.е либо в прошлый раз было 0, а стало 1, либо было 1, а стало 0. Если же напряжение стоит "как вкопанное", ответ будет нулевым.
И наконец, когда снова становится TrinaryPulse=0, и приходит ce=1, у нас оказывается сформирован выход. Каждый трит мы представляем как два бита. Младший идёт напрямую с рычажка, а старший - это результат того самого XOR, сохранённый в регистре PREV. По сути, мы закодировали следующее:
(меняется ли напряжение в такт TrinaryPulse?) (текущее напряжение на рычажке, т.е при TrinaryPulse=0)
В итоге, если рычажок установлен в "0", на его выходе неизменный ноль, что даёт на выходе нашего "декодера" 00.
Если рычажок установлен в "1", на его выходе неизменная единица, что даёт на выходе 01.
Если рычажок установлен в "2", то его выход меняется в такт с TrinaryPulse (стало быть, старший бит единица), а при TrinaryPulse=0 на его выходе будет 0, поэтому получаем 10.
Значение 11 может получиться, если мы каким-то непостижимым образом начнём получать импульсы В ПРОТИВОФАЗЕ относительно TrinaryPulse. Его можно интерпретировать как ошибочное.
Этот модулёк синтезируется в 6 ЛЭ, что я полагаю абсолютным минимумом:
- 4 ЛЭ - это регистр Prev, надо же запоминать предыдущее значение каждого "рычажка", а у нас их 4,
- 1 ЛЭ - регистр TrinaryPulse. Разумеется,
- 1 ЛЭ - формирование сигнала DV, т.е Data Valid.
Вот как всё это работает:
Click to view
Увы, иногда при переключении рычажков у нас кратковременно зажигаются "ошибочные" значения. Ну да: дребезг контактов, который иногда оказывается в фазе с TrinaryPulse, а иногда и в противофазе. Поскольку мы не ведём длительного накопления, проводя декодирование всего ПО ДВУМ СЭМПЛАМ, нас можно вот так "обдурить" дребезгом. Не считаю это проблемой: рычажки-то у меня для отладки. В реальности там будут перемычки в разъёме, которые "на ходу" переключаться, разумеется, не будут.
Но теперь нам надо преобразовать число в троичной системе в двоичную!
Не часто такая задачка возникает :) Опишу свой подход, он, конечно, не единственно возможный. По сути, мы должны найти следующее:
B = 27t3+9t2+3t1+t0,
здесь B - итоговое число, t0..t3 - его представление в троичном виде. Перепишем это выражение так:
B = ((t3·3+t2)·3+t1)·3+t0
Если мы никуда не торопимся (а так и есть), преобразование можно осуществить в 4 шага. На каждом шаге мы должны умножить предыдущий результат на 3 и тут же прибавить очередной трит. Умножение на три в двоичном исполнении - это прибавление к числу его сдвинутой влево на одну позицию копии. То есть, наш драндулет должен считать следующее выражение:
Q6 Q5 Q4 Q3 Q2 Q1 Q0
+ Q5 Q4 Q3 Q2 Q1 Q0 0
+ t1 t0
Q - это текущий результат, t - очередной трит. Чтобы представить числа от 0 до 80, нам нужно 7 бит. При этом мы уверены, что никакого переполнения не случится, поэтому не переживаем.
Как видно, младший разряд считается элементарно, на нём получается Q0 XOR t0, с формированием переноса C0 = Q0 AND t0 в следующий разряд.
А вот следующий разряд кажется очень "перенаселённым"! Как будто нужно посчитать Q1+Q0+t1+C0, и если все эти значения будут единицами, у нас будет перенос не в следующий разряд, а ЧЕРЕЗ ОДИН! Но на самом деле такого никогда не случится: перенос из младшего разряда может случиться только при t0=1, а тогда заведомо t1=0, это же трит, для которого допустимые значения - это 00, 01 и 10. Иными словами, мы имеем право написать: t1+C0 = t1|C0. И в таком случае все разряды, кроме самого младшего, сводятся к обычному сумматору, с входом cin (Carry In), на который мы и подаём t1 OR C0.
Наконец, напишем модулёк, который всё это осуществляет:
module TrinaryToBinaryConvertor(input clk, input [1:0] Digit, input start, output reg [6:0] Q, output DV);
wire Carry0to1 = Q[0]&Digit[0]; //будет ли перенос в разряд двоек
wire cin = Carry0to1 | Digit[1]; //мы уверены, что будет либо Carry0to1=1, либо Digit[1]=1, но не оба сразу
//так происходит, поскольку Digit может принимать только 00, 01 либо 10.
wire [5:0] Result;
lpm_add_sub Adder (
.dataa (Q[6:1]),
.datab (Q[5:0]),
.cin (cin),
.result (Result));
defparam
Adder.lpm_direction = "ADD",
Adder.lpm_hint = "ONE_INPUT_IS_CONSTANT=NO,CIN_USED=YES",
Adder.lpm_representation = "UNSIGNED",
Adder.lpm_type = "LPM_ADD_SUB",
Adder.lpm_width = 6;
always @(posedge clk) begin
Q[0] <= start? 1'b0 : Q[0]^Digit[0]; //тут всё элементарно
Q[6:1] <= start? 6'b000000 : Result;
end
//по-хорошему, нужно ещё где-то сформировать сигнал, что у нас получилось корректное значение.
wire [2:0] CounterQ;
wire CounterNonZero = CounterQ[2]|CounterQ[1]; //состояние 001 не используется, так что младш бит не проверяем
lpm_counter Counter (
.clock(clk),
.sset(start),
.cnt_en(CounterNonZero),
.Q (CounterQ),
.cout(DV));
defparam
Counter.lpm_width = 3,
Counter.lpm_direction = "UP",
Counter.lpm_svalue = 3;
endmodule
Если вслед за импульсом start начать подавать на вход Digit один трит за другим, начиная СО СТАРШЕГО (чтобы он успел сколько нужно раз умножиться на 3), то в какой-то момент на выходе Q появится результат. Этот момент мы "отловим" с помощью счётчика, который сформирует сигнал DV=1, т.е Data Valid. Синтезируется это всё в 20 ЛЭ, что считаю неплохим результатом. 7 ЛЭ - регистр, где хранится результат. 6 ЛЭ - сумматор, 3 ЛЭ - счётчик, выдающий Data Valid. Ещё 1 ЛЭ: вычисление C0 (переноса из младшего разряда) и 1 ЛЭ: формирование сигнала CounterNonZero, разрешающего счётчику вести счёт, он нужен, чтобы сидящий "без дела" конвертор не генерил DV=1 самопроизвольно. Итого, 18 ЛЭ. Сойдёт.
Теперь для счастья нам не хватает сдвигового регистра. Для удобной симуляции я написал такую хреновину:
module TrinaryShiftRegForTest(
input clk, input start, input [1:0] D0, input [1:0] D1, input [1:0] D2, input [1:0] D3,
output [1:0] Q);
reg [7:0] SR;
always @(posedge clk)
SR <= start? {D3,D2,D1,D0} : {SR[5:0], 2'b00};
assign Q = SR[7:6];
endmodule
При подаче start=1, он защёлкнет в себя 4 трита, и затем начнёт каждый такт сдвигать их влево. Это очень хорошо согласуется с модулем TrinaryToBinaryConvertor: сигнал start подаём на них одновременно! Конвертор как раз успеет обнулиться! Синтезируется, внезапно, в 8 ЛЭ, тут никуда не денешься.
Чтобы не вбивать все возможные троичные значения ручками, нарисовал ещё "троичный счётчик" на 4 трита, опять из библиотечных элементов lpm_counter, каждый 2-битный, причём modulus=3:
И теперь мы можем проверить работу конвертора:
Первый раз мы запускаем процесс сами, подав единичку на start. Тем самым мы говорим преобразовать в двоичный вид текущее значение счётчика (ноль), а самому счётчику - к следующему разу прибавить единичку.
Далее, когда преобразование будет завершено, будет выдано DV=1, сигнализируя появление нового числа, а заодно запуская цикл по-новой, для следующего значения. Посмотрим "осциллограмму":
Видим, как идёт троичный счёт, и как на шине Binary появляется окончательный результат. Честно просмотрел "до конца", когда выдаётся число 80, и вслед за ним всё начинается опять с нуля.
Осталось сделать это "в железе". Для этого чуточку меняю сдвиговый регистр, просто для удобства (вместо 4 шин по 2 бита каждая, одна 8-битная шина):
1
2
3
4
5
6
7
8
9
10
11
12
module TrinaryShiftReg(
input clk, input [7:0] D, input start,
output [1:0] Q);
reg [7:0] SR;
always @(posedge clk)
SR <= start? D : {SR[5:0], 2'b00};
assign Q = SR[7:6];
endmodule
И финальая "схема":
И её работа:
Click to view
Фурычит! Ох, как непривычно с троичными значениями обращаться!
Отсинтезировалось оно в 60 ЛЭ, из которых 17 - это делитель частоты (гнать на рычажки десятки мегагерц почему-то совсем не хочется!), 7 ЛЭ - декодер троичных значений, 8-сдвиговый регистр, 20-конвертор, и ещё 7 - выходная защёлка, чтобы светодиоды не мерцали в процессе преобразования, а отображали последнее получившееся значение. Разумеется, можно это и посильнее оптимизировать, постаравшись избавиться от лишних защёлок, например, переделать конвертор так, что, закончив работу, он будет держать результат на выходе неограниченно долго. Но в качестве Proof of concept, и как "ПрЮвет ВОЛКУ!" для данной ПЛИС - вполне сойдёт.
Может, кому пригодится для джамперов, если ног вдруг категорически перестанет хватать.