Шауэрман Александр А . shamrel@yandex.ru
Первая статья цикла: Архитектура ПЛИС. Часть 1. Логический элемент
Для многих проектов на ПЛИС значительную часть используемых логических ячеек занимают мультиплексоры (Multiplexer – MUX). Всем известно, что мультиплексор представляет собой комбинационное устройство, коммутирующее несколько входов на один выход. Выбор входа определяют управляющие сигналы. При оптимизации логики вашего мультиплексора, вы сможете максимально эффективно реализовать его в ПЛИС Altera. В этом разделе рассмотрим основные проблемы и дадим решения для достижения оптимального использования ресурсов.
Для тех, кто перешел на язык описания схем после проектирования на дискретной логике, мультиплексор представляется неким фиксированным электронным компонентом: такой микросхемой, переключающей цифровые потоки. Однако при HDL описании схемы, особенно если это Verilog HDL , роль мультиплексора значительно шире и его можно встретить в не всегда ожидаемых местах. Если вы в своем коде используете оператор case
, if
или тернарный оператор выбора, будьте уверены, вы используете мультиплексор. Поэтому разберемся с основными типами мультиплексоров, встречающихся в Verilog описании схем и некоторыми особенностями их синтеза.
При аппаратном синтезе в ПЛИС выделяют несколько типов мультиплексоров: бинарные мультиплексоры, мультиплексоры селекторы и приоритетные мультиплексоры. Понимание того, как мультиплексоры создаются из HDL кода, и как они могут быть реализованы в ходе синтеза, является первым шагом на пути к оптимизации структуры мультиплексора для получения наилучших результатов.
Б инарные мульт и плексоры
Бинарные мультиплексоры – это мультиплексоры, которые содержат дешифратор; они выбирают вход, основываясь на значении управляющих сигналов, представленных в виде двоичного позиционного кода. Простейший такой мультиплексор имеет два информационных входа и один управляющий.
Рисунок 1 – Таблица истинности и обозначение мультиплексора 2:1 на функциональных схемах
Аналитически такой мультиплексор описывается формулой:
(1)
Такой мультиплексор создается при однократном использовании оператора if
или тернарного оператора выбора:
always @ * // Если y – это reg
if ( c0) y = d1;
else y = d0;
Или так:
assign y = c0 ? d1 : d0; // Если y – это wire
Для синтеза этого мультиплексора понадобится одна ячейка 4-х входового LUT . Однако бинарный мультиплексор 4 в 1 имеет уже 6 входов (4 информационных и два управляющих) и не может быть синтезирован непосредственно на 4-LUT. В этом отношении серии ПЛИС с 6-и входовыми LUT находятся в более выигрышном положении, но в нашем Cyclone IV для увеличения коммутируемых каналов (входов) придется использовать каскадную структуру.
Рисунок 2 – Каскадное объединение мультиплексоров
Если реализовывать каскадное включение из трех мультиплексоров "в лоб", то для этого понадобится 3 LUT, при этом у каждого LUT останется неиспользованный вход. На практике для полного использования ресурса применяется взаимное вложение мультиплексоров. Такой подход интересен тем, что на выходе первой LUT (сигнал z, рисунок 3) в зависимости от значения c1 коммутируются либо информационные входы d0, d1, либо управляющий сигнал c0. В результате достигается полная утилизация ресурсов.
Рисунок 3 – Взаимное вложение мультиплексоров для синтеза на LUT
(2)
(3)
Таким образом, для ПЛИС, у которых LE основан на 4-LUT, мультиплексор 4:1 эффективно строится на двух LUT. Мультиплексор с большим количеством входов при синтезе разбивается на блоки 4-х входовых мультиплексоров, при необходимости в завершении ставится мультиплексор 2:1.
Пример Verilog модуля бинарного мультиплексора 4:1 (занимает две LUT):
Бинарный мультиплексор 4:1. Verilog
module multiplexer
(
input d3, d2, d1, d0, // data
input c1, c0, // control
output reg y
) ;
always @*
case ( { c1, c0} )
2'b00 : y = d0;
2'b01 : y = d1;
2'b10 : y = d2;
2'b11 : y = d3;
endcase
endmodule
Язык Verilog позволяет обратится отдельно к каждому биту в векторе по его номеру. Эта возможность даст еще одну реализацию бинарного мультиплексора.
Бинарный мультиплексор 4:1. Verilog. Реализация с объединением входов
module binary_mux
(
input [ 3 : 0 ] din, // Вход
input [ 1 : 0 ] sel, // Управление
output dout // Выход
) ;
assign dout = din[ sel] ;
endmodule
В отличии от предыдущих реализаций информационные и управляющие входы объединены в вектор. При использовании такого модуля в экземпляре используется конкатенация (объединение):
binary_mux mux_inst
(
.din( { d3, d2, d1, d0} ) ,
.sel( { c1, c0} ) ,
.dout( y)
) ;
На практике удобно использовать универсальные модули, где основные свойства задаются через параметр. Это позволяет использовать одну реализацию в различных задачах. В последнем примере бинарного мультиплексора легко перейти к параметризируемой модели и управлять числом коммутируемых входов:
Бинарный мультиплексор 4:1. Verilog. Параметризируемая модель
module binary_mux
# (
parameter SEL_WIDTH = 3 , // разрядность управляющего вектора
parameter TOTAL_DAT = 1 << SEL_WIDTH // максимальное число входов
)
(
input [ TOTAL_DAT- 1 : 0 ] din,
input [ SEL_WIDTH- 1 : 0 ] sel,
output dout
) ;
assign dout = din[ sel] ;
endmodule
При использовании модуля параметр SEL_WIDTH
можно установить в заголовке экземпляра модуля:
binary_mux # ( .SEL_WIDTH( 2 ) ) mux_inst
(
.din( { d3, d2, d1, d0} ) ,
.sel( { c1, c0} ) ,
.dout( y)
) ;
Легко убедится, что при одинаковом числе коммутируемых входов на уровне RTL все варианты описания мультиплексора дают одинаковую схему.
Мультиплексор-селектор
Мультиплексор-селектор не имеет дешифратора: для каждого входа предназначена отдельная управляющая линия. Одновременно только на одной линии управления может быть установлена единица. Несмотря на простоту реализации на дискретных элементах на каждый вход ставится элемент 2-И, с выхода которых сигнал подается на элемент ИЛИ. При реализации в ПЛИС такой мультиплексор намного менее эффективен, чем двоичный. Из-за того, что на каждый вход требуется управляющая линия, для 4-х канального мультиплексора понадобится уже три 4-LUT.
Мультиплексор-селектор 4:1. Verilog
module selector_multiplexer
(
input d3, d2, d1, d0, // data
input c3, c2, c1, c0, // control
output reg y
) ;
always @*
case ( { c3, c2, c1, c0} )
2'b0001 : y = d0;
2'b0010 : y = d1;
2'b0100 : y = d2;
2'b1000 : y = d3;
default : y = 0 ;
endcase
endmodule
Однако в некоторых случаях управляющие сигналы – это выход декодера, тогда компилятор попытается объединить селектор и дешифратор в двоичный мультиплексор.
Приоритетный мультиплексор
Судя по названию, в приоритетном мультиплексоре логика выбора подразумевает приоритет. Варианты для выбора правильного элемента должна быть проверена в определенном порядке на основе приоритета сигнала. Такая структура в Verilog в основном определяется операторами if
, else
, casex
и ?:
.
Например, такой модуль:
Приоритетный мультиплексор 4:1. Verilog. Реализация на if
module priority_multiplexer
(
input d3, d2, d1, d0, // data
input c2, c1, c0, // control
output reg y
) ;
always @*
if ( c0)
y = d0;
else if ( c1)
y = d1;
else if ( c2)
y = d2;
else
y = d3;
endmodule
на уровне RTL (Register transfer level – уровень регистровых взаимодействий) будет иметь вид:
Рисунок 4 – Структура приоритетного мультиплексора на уровне RTL
Очевидно, что путь прохождения сигнала со входа d3
на выход y
будет отличаться от пути с входа d0
на выход y
. И в зависимости от числа мультиплексоров в цепи временная задержка при прохождении через такую цепь может стать слишком большой, особенно для ПЛИС с 4-LUT. Однако все не настолько плохо: при компиляции Quartus II стремится преобразовать структуру к древовидному виду, при этом использует прием взаимного вложения, как это было при синтезе бинарного мультиплексора 4:1. В результате технологическая карта (Technology Map) модуля из примера будет иметь вид:
Рисунок 5 – Структура приоритетного мультиплексора 4:1 на уровне Technology Map
Из рисунка 5 видно, что по прежнему самые длинные пути это d3→y
и d2→y
, но включают в себя не три, а два LUT. В этом случае, быстродействие схемы будет таким же, как и в аналогичном бинарном мультиплексоре, но при этом используется три LUT против двух у бинарного. Очевидно, что при увеличении числа коммутируемых входов у бинарного мультиплексора выигрыш в быстродействии и в занимаемых ресурсах будет увеличиваться, поэтому стоит избегать приоритетных мультиплексоров там, где приоритет не требуется. Если порядок выбора не важен, можно использовать case
для построения бинарного мультиплексора. Если в проекте избежать приоритетного выбора нельзя, а задержки становятся критичными, можно постараться изменить логику работы, что бы уменьшить число логических уровней, особенно вдоль критического пути.
И напоследок, точно такой же по функционалу и реализации модуль может быть записан с использованием оператора casex
следующим образом:
Приоритетный мультиплексор 4:1. Verilog. Реализация на casex
module priority_multiplexer
(
input d3, d2, d1, d0, // data
input c2, c1, c0, // control
output reg y
) ;
always @*
casex ( { c2, c1, c0} )
3 'b?? 1 : y = d0;
3 'b? 10 : y = d1;
3'b100 : y = d2;
3'b000 : y = d3;
endcase
endmodule
У такой записи помимо наглядности есть еще одно маленькое преимущество, в конструкции casex
легко определить значение по умолчанию и тем самым избежать ряд неприятностей, но об этом речь пойдет в следующей главе.
Неявный default
Часто бывает, что применение оператора if
удобнее чем case
. Однако, использование if
может привести в результате к сложному дереву мультиплексоров, которые не могут быть легко оптимизированы инструментами синтеза. В частности, каждый if
имеет неявный else
, даже когда он не указан и компилятор будет обязан отработать эту ветвь. Такое неявное умолчание может стать причиной дополнительного усложнения синтеза мультиплексора, а в худших случаях привезти к синтезу Latch .
Рассмотрим пример приоритетного мультиплексора, но "упростим" схему, "забудем" последнюю ветвь else
:
Приоритетный мультиплексор 4:1 с ошибкой. Verilog
module priority_multiplexer_bad
(
input d3, d2, d1, d0, // data
input c2, c1, c0, // control
output reg y
) ;
always @*
if ( c0)
y = d0;
else if ( c1)
y = d1;
else if ( c2)
y = d2;
// else
// y = d3;
endmodule
При компиляции такой модуль вместо 3 LUT занимает уже 4 LUT, а Quartus выдал следующие предупреждения:
Warning (332060): Node: c0 was determined to be a clock but was found without an associated clock assignment.
Warning (335093): TimeQuest Timing Analyzer is analyzing 1 combinational loops as latches.
Что-то пошло не так. Вход c0
компилятор воспринял как вход тактирования (это в комбинационной-то схеме!) и добавил петлю обратной связи, создав Latch. При этом компилятор реализовал лишь то, что хотел от него программист. Если значение переменной c0
равно единице, то на вход поступает d0
; если c0
равно нулю, а c1
равно единице, то на выходе будет d1
; если и c0
, и c1
– нули, а c1
– единица, то на выходе – d2
. Но что должно быть на выходе, если все управляющие сигналы равны нулю? Компилятор делает единственно верный вывод – нужно сохранить предыдущее значение, а для этого синтезировать асинхронный (одноступенчатый) триггер, или, как его по другому называют, защелку (Latch).
Рисунок 6 – Структура полученного мультиплексора на уровне RTL
Рисунок 7 – Структура полученного мультиплексора на уровне Technology Map
На технологической карте последняя (крайняя правая) LUT охвачена петлей обратной связи – это и есть Latch. В отечественной литературе выделяют целое семейство таких устройств, их называют асинхронными триггерами, одноступенчатыми триггерами , или триггерами-защелками. Такие устройства имеют ограниченную поддержку в инструментах проверки и анализа, например, TimeQuest (встроенная в Quartus утилита для временного анализа, оценивает задержки распространения) интерпретирует Latch, как синхронный триггер, работающий по спадающему фронту, что является весьма грубым допущением. Поэтому схем с защелками нужно избегать, а использование такого мультиплексора является крайне неудачным решением.
При описании сложного мультиплексора с разветвленной иерархией на операторах if
-else
бывает трудно отследить все ветви, а код становится трудным для восприятия. В этом случае можно рекомендовать изменить структуру мультиплексора и попытаться реализовать его в качестве бинарного, используя оператор case
. Однако и в бинарном мультиплексоре легко допустить ошибку. Достаточно "забыть" указать один из вариантов выбора.
Например так:
Бинарный мультиплексор с ошибкой. Verilog
module multiplexer_bad
(
input d3, d2, d1, d0, // data
input c1, c0, // control
output reg y
) ;
always @*
case ( { c1, c0} )
2'b00 : y = d0;
2'b01 : y = d1;
2'b10 : y = d2;
// 2'b11: y = d3;
endcase
endmodule
Как результат, при синтезе получим увеличение числа логических элементов, Latch и плохую предсказуемость при временном и функциональном анализе. Когда в мультиплексоре используется всего 4 варианта выбора, то, в принципе, легко проследить все варианты, но задача усложняется экспоненциально при увеличении числа коммутируемых входов, тем более, если не все сочетания управляющих сигналов функционально возможны или достаточно редко происходят. Спецификация Verilog имеет решение на этот случай, оператор case
включает в себя обработку значения по умолчанию, для этого используется ключевое слово default
. Перепишем предыдущий модуль следующим способом:
Бинарный мультиплексор с default. Verilog
module multiplexer_bad
(
input d3, d2, d1, d0, // data
input c1, c0, // control
output reg y
) ;
always @*
case ( { c1, c0} )
2'b00 : y = d0;
2'b01 : y = d1;
2'b10 : y = d2;
// 2'b11: y = d3;
default : y = 0 ;
endcase
endmodule
Даже если будет закомментирован любой другой случай, или два случая, компилятор реализует корректную схему без защелок. Поэтому хорошим тоном программирования является добавления default
даже для бинарного мультиплексора с полным набором описанных вариантов.
Описание ветви default
является особенно важным в мультиплексора-селекторах, где по определению в векторе управляющих сигналов заложена избыточность. Определяя явно значение default
, мы даем компилятору информацию о том, как синтезировать эти случаи.
Многоразрядный мультиплексор
Под многоразрядными мультиплексорами мы будем подразумевать мультиплексоры, коммутирующие многоразрядные данные (вектора). Так как в подобном устройстве все разряды не связаны между собой, при построении в ПЛИС на уровне логического блока не будут задействованы цепи переноса, а устройство в целом представляет собой несколько одноразрядных мультиплексоров, включенных параллельно, у которых управляющие входы объединены. Поэтому все показанные выше методы описания мультиплексоров актуальны и при многоразрядных значениях входных данных. Например, для того, чтобы превратить бинарный мультиплексор в N-разрядный, достаточно информационные входы и выход объявить как N-разрядные вектора, управляющие сигналы останутся без изменения:
N-разрядный бинарный мультиплексор. Verilog
module multiplexer
# ( parameter N = 8 )
(
input [ N- 1 : 0 ] d3, d2, d1, d0, // data
input c1, c0, // control
output reg [ N- 1 : 0 ] y
) ;
always @*
case ( { c1, c0} )
2'b00 : y = d0;
2'b01 : y = d1;
2'b10 : y = d2;
2'b11 : y = d3;
endcase
endmodule
При такой реализации единственный настраиваемый параметр – это N, разрядность входных и выходных данных. Количество входов остается фиксированным, что неудобно, в сложном проекте придется держать множество модулей. Гораздо большим потенциалом в плане параметризации обладает модель бинарного мультиплексора с объединением входов. Ниже приведен модуль многоразрядного настраиваемого мультиплексора, построенный в соответствии с рекомендациями Altera.
Настраиваемый модуль мультиплексора. Verilog
module bus_mux
# (
parameter DAT_WIDTH = 2 ,
parameter SEL_WIDTH = 3 ,
parameter TOTAL_DAT = DAT_WIDTH << SEL_WIDTH
)
(
input [ TOTAL_DAT- 1 : 0 ] din,
input [ SEL_WIDTH- 1 : 0 ] sel,
output [ DAT_WIDTH- 1 : 0 ] dout
) ;
localparam NUM_WORDS = ( 1 << SEL_WIDTH) ;
genvar i, k;
generate
for ( k= 0 ; k < DAT_WIDTH; k= k+ 1 )
begin : out
wire [ NUM_WORDS- 1 : 0 ] tmp;
for ( i= 0 ; i < NUM_WORDS; i= i+ 1 )
begin : mx
assign tmp [ i] = din[ k+ i* DAT_WIDTH] ;
end
assign dout[ k] = tmp[ sel] ;
end
endgenerate
endmodule
Рисунок поясняет принцип работы модуля. Для примера выбрана ширина входных данных 4, количество входов 4, а ширина управляющего вектора – 2. В цикле генерации формируем временный вектор tmp
, из которого одноразрядным мультиплексором выбираем нужное значение.
Рисунок 8 – Принцип работы модуля многоразрядного коммутатора
Экспериментальное исследование
Несмотря на то, что рассмотренные в этой статье модели достаточно простые и для того, чтобы оценить корректность работы достаточно взглянуть на схему в RTL Viewer и Technology Map , можно провести экспериментальное исследование непосредственно на ПЛИС. С некоторого времени у меня под рукой отладочная плата LESO2 , версия с Cyclone IV на борту. Восьми тумблеров хватит для формирования информационных и управляющих сигналов одноразрядного мультиплексора. Ниже приведен листинг модуля верхнего уровня.
Модуль верхнего уровня. Verilog
module demo_mux
(
( * chip_pin = "65" * ) input d0, //sb8 тумблер
( * chip_pin = "64" * ) input d1, //sb7 тумблер
( * chip_pin = "60" * ) input d2, //sb6 тумблер
( * chip_pin = "59" * ) input d3, //sb5 тумблер
( * chip_pin = "52" * ) input d4, //sw кнопка (с инверсией)
( * chip_pin = "58" * ) input c0, //sb4
( * chip_pin = "55" * ) input c1, //sb3
( * chip_pin = "54" * ) input c2, //sb2
( * chip_pin = "53" * ) input c3, //sb1
( * chip_pin = "11, 10, 8, 7, 6, 3, 2, 1" * )
output [ 7 : 0 ] led_o
) ;
wire y;
// Экземпляр исследуемого модуля:
multiplexer mult_inst
(
.d3( d3) , .d2( d2) , .d1( d1) , .d0( d0) , // data
.c1( c1) , .c0( c0) , // control
.y( y)
) ;
// назначаем выход модуля на светодиод
assign led_o [ 0 ] = y;
endmodule
В листинге для примера установлен экземпляр одноразрядного бинарного мультиплексора. При исследовании многоразрядных мультиплексоров на информационные входы можно подать константы. В качестве дополнительного информационного входа (в листинге обозначен как d4
) можно использовать тактовую кнопку.
Создаем проект в Quartus II, настраиваем на генерацию rbf-файла. Подробнее о создании и настройки проекта можно узнать из статьи "Пишем "демку" для LESO2 на Verilog ". Использовать Pin Planer или Assigment Editor нет необходимости, все порты модуля верхнего уровня назначены непосредственно в тексте программы при объявлении портов с помощью специальной директивы. Компилируем проект, с помощью утилиты l2flash загружаем rbf-файл в ПЛИС. Исследуем схему.
Рекомендации и выводы
1. На ПЛИС с 4-х входовыми LUT оптимальным образом строятся бинарные мультиплексоры 4:1. При описании мультиплексоров с большим числом входов для сохранения общего быстродействия схемы рекомендуется использовать конвейер.
2. Мультиплексор-селектор не дает преимущества ни по скорости, ни по занимаемым ресурсам. Можно рекомендовать использовать его только совместно с дешифратором.
3. Приоритетный мультиплексор нужно использовать с осторожностью и только там, где он действительно нужен. При увеличении количества входов задержка распространения сигнала вдоль критического пути значительно возрастает.
4. Все ветви мультиплексора должны быть описаны. Для каждого if
должен быть свой else
, если использование case
(или casex
) сложно, то следует использовать default
для ветви, исполняемой по умолчанию.
Литература
Quartus Prime Standard Edition Handbook Volume 1: Design and Synthesis
Advanced Synthesis Cookbook
Первая статья цикла: Архитектура ПЛИС. Часть 1. Логический элемент