Приоритетная структура кода
В разработке электронных устройств грань между разработчиком-схемотехником и разработчиком-программистом очень размыта. Что уж говорит о том, кто должен писать RTL под FPGA.
С одной стороны, RTL — это территория схем, с другой стороны, ресурсы FPGA дешевеют, синтезаторы умнеют. Цена ошибки RTL дизайнера для FPGA не превышает цены ошибки программиста, а созданные схемы можно также обновлять и наращивать по функциональности, как обычную прошивку процессора.
Производители микросхем тоже не отстают, стали паковать ПЛИС в один корпус с процессором, даже Intel выпустил процессор для PC с FPGA внутри, купив для этого известного производителя ПЛИС Altera.
Думаю всем истинным программистам Вселенная шлет сигналы, что им просто необходимо изучить RTL и начать писать “код” для FPGA не хуже, чем под их привычные процессоры.
Когда-то давно, я проходил этот путь и позволю себе дать несколько советов для ускорения.
Для начала, нужно выбрать язык описания. На текущий момент использование языков типа System Verilog, SystemC и т.п., именно для создания схем, больше похоже на сделку с дьяволом, чем на работу. Поэтому еще в строю старинные и базовые VHDL и Verilog. Я попробовал оба и советую использовать последний. Verilog более дружествен по синтаксису программистам, да и в целом как-то посовременней.
Если вы твердо решили пройти этот путь, то я полагаю, что вы уже знаете ключевые слова и стандартные конструкции Verilog. Вы потратили какое-то время и понимаете, что в описании аппаратуры все происходит одновременно, а не по очереди, как в программах.
Мы пока оставим вопрос мета-стабильности и гонки сигналов, для этого ограничимся только синхронными схемах с синхронным сбросом, а всякую комбинаторику и асинхроньщину оставим старой школе.
В описаниях схем очень важна структура кода, об организации которой и пойдет речь далее. Структура не только повышает читаемость и поддерживаемость кода, но также влияет на результат работы итоговой схемы.
Настоящие RTL-дизайнеры мыслят “схемно”, они организуют код в блоки и этим определяют его структуру. Мы не будем сразу менять образ мышления, а будем создавать “программисткие” описания. Мы сосредоточимся на том, что хотим получить, а создание подходящей для этого схемы, оставим синтезатору. Все как с языками высокого уровня, пишем код, а оптимизацию и перевод в машинные коды вешаем на компилятор.
Плата за такой подход примерно та же, чуть менее оптимальный с точки зрения ресурсов результат, но как сказано выше цена ресурсов снижается, поэтому не будем жалеть патроны. Cинтезаторы к текущему моменту здорово поумнели, но все же некоторые проблемы имеются, рассмотрим пример:
input clk; //тактовый сигнал
input data_we; //сигнал разрешения записи от внешнего модуля
input [7:0] data; //данные от внешнего модуля
reg [7:0] Data; //данные
reg DataRdy; //флаг готовности данных
reg [7:0] ProcessedData; //обработанные данные
//прием данных по фронту клока ----------------------
always @(posedge clk)
begin
if(data_we == 1’b1) //если есть сигнал записи
begin
Data <= data; //сохраняем данные
DataRdy <= 1’b1; //ставим флаг готовности данных
end
end
пример кода №1
Пока никаких проблем нет, прием выделили в отдельный блок, все удобно и понятно. Теперь допустим, дальше у нас идет работа с полученными данными, и нам хочется снять флаг DataRdy по окончанию обработки данных, чтобы понимать, когда придут новые данные.
//обработка по фронту клока ----------------------
always @(posedge clk)
begin
if(DataRdy == 1’b1) //если есть новые данные
begin
//обработка данных
ProcessedData <= Data;
DataRdy <= 1’b0; //снимаем флаг, данные обработаны
end
end
пример кода №2
Вот теперь начинаются проблемы, у любителей Xilinx точно, но думаю, что и другие синтезаторы будут солидарны. Cинтезатор скажет, что у сигнала DataRdy два источника меняющих его значения, он меняется по фронту сигнала в 2 блоках и неважно, что тактовый сигнал один.
Может показаться, что синтезатор не знает какое значение задать, если выполняются условия смены в обоих блока одновременно, когда DataRdy имеет значение 1
//в первом блоке
if(data_we == 1’b1)
DataRdy <= 1’b1;
...
//во втором блоке
if(DataRdy == 1’b1)
DataRdy <= 1’b0;
пример кода №3
Но модификация кода решающая этот конфликт не поможет.
//прием данных по фронту клока ----------------------
always @(posedge clk)
begin
//если есть сигнал записи, и снят флаг данных
if((data_we == 1’b1)&&(DataRdy == 1’b0))
begin
Data <= data; //сохраняем данные
DataRdy <= 1’b1; //ставим флаг готовности данных
end
end
пример кода №4
Логически все верно, никаких конфликтов нет, но синтезатор настойчиво будет жаловаться на двойной источник сигнала, и договориться с ним не получиться. Нельзя в разных блоках менять один сигнал, чтобы все получилось, надо и прием, и обработку поместить в один блок.
И тут первое предложение, а давайте у нас в модуле будет вообще всего 1 блок always, и все, что делает наш модуль, мы разместим в этом блоке, наш пример станет выглядеть так
input clk; //тактовый сигнал
input data_we; //сигнал разрешения записи от внешнего модуля
input [7:0] data; //данные от внешнего модуля
reg [7:0] Data; //данные
reg DataRdy; //флаг готовности данных
reg [7:0] ProcessedData; //обработанные данные
//---------------------------------------------------
// обработка основного клока
//---------------------------------------------------
always @(posedge clk)
begin
//если есть сигнал записи, и снят флаг данных
if((data_we == 1’b1)&&(DataRdy == 1’b0))
begin
Data <= data; //сохраняем данные
DataRdy <= 1’b1; //ставим флаг готовности данных
end
else if(DataRdy == 1’b1) //если есть флаг данных
begin
//обработка данных
ProcessedData <= Data;
DataRdy <= 1’b0; //снимаем флаг, данные обработаны
end
end
пример кода №5
Теперь все работает, но модуль стал уже не таким понятным, явного разделения на прием и обработку нет, все в одну кучу. Тут нам на помощь приходить одно очень приятное свойство языка Verilog. Если в одном блоке вы совершаете несколько присвоений одной переменной (говорим о неблокирующих присвоениях), то выполнится последние из них (Стандарт Verilog HDL IEEE Std 1364-2001). Правильнее сказать, что выполняются они все в описанном порядке, но так как все такие присвоения происходят одновременно, то переменная примет последние присвоенное значение.
То есть, если написать так:
input B;
reg [2:0] A;
always @(posedge clk)
begin
A <= 1;
A <= 2;
A <= 3;
if(B) A <= 4;
end
пример кода №6
То A примет значение 3 в случае, если В ложь, а если все же В истина, то А примет значение 4, в этом можно убедиться на следующем изображении
Рис1. Временная диаграмма симуляции поведения описания №6
Это полностью описанная стандартом и синтезируемая конструкция, что дает нам интересные возможности, нет необходимости делать сложные цепочки конструкции if — else if разделяя, когда переменной присвоить одно значение, а когда другое. Вы просто можете написать условие и значение переменной, написать это не думая о других условиях и присвоениях этой переменной, написать это как бы изолированно от другого кода.
Далее останется расположить такие присвоения в правильном порядки, тем самым задать их приоритеты на случай одновременного выполнения, и все получится само. Это очень удобный способ управления кодом, при этом контролируемый синтезатором, а не человеком.
В следующем примере показано, как это может выглядеть
//---------------------------------------------------
// обработка основного клока
//---------------------------------------------------
always @(posedge clk)
begin
//прием данных, наименьший приоритет -----
if(...) Data <= data;
//обработка данных, средний приоритет ----
if(...) Data <= Func(Data);
//сброс, наивысший приоритет -------------
if(reset_n == 1’b0)
Data <= 0;
end
пример кода №7
Куда бы вас не завела кривая создания модуля, вы можете быть уверенными, что состояние сброса, перекроет все что вы натворили выше, вы можете сделать сколько угодно ошибок в логике, сброс произойдет и задаст переменной описанное в блоке сброса значение.
Также вы можете быть уверенными, что если у вас вдруг совпадут в один момент времени условия обработки и приема данных, то вы обработаете данные, а не затрете их новыми пришедшими. Это произойдет потому, что обработка в нашем коде стоит ниже, она более приоритетная. Если вдруг в какой-то момент вы поймете, что важнее не потерять приходящие данные, поменяйте блоки местами и тем самым измените приоритеты.
Если у вас несколько интерфейсов, которые могут изменить данные, вы опять же просто располагаете участки кода реализующие интерфейс друг за другом, и тем самым расставляете приоритеты доступа к данным.
//---------------------------------------------------
// обработка основного клока
//---------------------------------------------------
always @(posedge clk)
begin
//прием данных по 1 интерфейсу,
//наименьший приоритет -------------------
if(master1_we)
Data <= data1;
//прием данных по 2 интерфейсу,
//приоритет выше первого -----------------
if(master2_we)
Data <= data2;
//обработка данных, средний приоритет ----
if(need_process)
Data <= (Data << 1);
//сброс, наивысший приоритет -------------
if(reset_n == 1’b0)
Data <= 0;
end
пример кода №8
Симуляция работы описания можно видеть на рисунке ниже
Рис2. Временная диаграмма симуляции поведения описания №8
Эта система управляется несколькими ведущими устройствами и арбитраж между ними получился автоматически. Когда мастера управляют схемой по очереди (фаза 1 и 2, рис. 2), она получает данные от каждого из них, но если вдруг несколько мастеров выдадут данные одновременно (фаза 3, рис. 2), то схема использует данные от более приоритетного мастера, интерфейс которого описан ниже, от второго в нашем примере.
При этом сброс схемы перекрывает все сигналы (фаза 5, рис. 2), а обработка выше по приоритету любого из мастеров, но ниже сброса (фаза 4, рис. 2).
Вернемся к начальному примеру, и покажем его конечный вариант описания:
input clk; //тактовый сигнал
input data_we; //сигнал разрешения записи от внешнего модуля
input [7:0] data; //данные от внешнего модуля
reg [7:0] Data; //данные
reg DataRdy; //флаг готовности данных
reg [7:0] ProcessedData; //обработанные данные
//---------------------------------------------------
// обработка основного клока
//---------------------------------------------------
always @(posedge clk)
begin
//прием данных -------------------------------
//приоритет 0
if(data_we == 1’b1)//если есть сигнал записи
begin
Data <= data; //сохраняем данные
DataRdy <= 1’b1; //ставим флаг готовности данных
end
//обработка данных --------------------------
//приоритет 1
if(DataRdy == 1’b1) //если есть флаг данных
begin
//обработка данных
ProcessedData <= Data;
DataRdy <= 1’b0; //снимаем флаг, данные обработаны
end
end
пример кода №9
Даже не нужно в блоке приема проверять, что DataRdy имеет нулевое значение, блок обработки перекроет по приоритету блок приема, и сбросит флаг DataRdy, даже если во время обработки поступят новые данные. А поменяв блоки местами, мы не пропустим никаких новых данных.
input clk; //тактовый сигнал
input data_we; //сигнал разрешения записи от внешнего модуля
input [7:0] data; //данные от внешнего модуля
reg [7:0] Data; //данные
reg DataRdy; //флаг готовности данных
reg [7:0] ProcessedData; //обработанные данные
//---------------------------------------------------
// обработка основного клока
//---------------------------------------------------
always @(posedge clk)
begin
//обработка данных --------------------------
//приоритет 0
if(DataRdy == 1’b1) //если есть флаг данных
begin
//обработка данных
ProcessedData <= Data;
DataRdy <= 1’b0; //снимаем флаг, данные обработаны
end
//прием данных -------------------------------
//приоритет 1
if(data_we == 1’b1)//если есть сигнал записи
begin
Data <= data; //сохраняем данные
DataRdy <= 1’b1; //ставим флаг готовности данных
end
end
пример кода №10
После обработки данных сбрасывается флаг DataRdy, но если одновременно с этим моментом к нам приходят новые данные, блок приема перекроет приоритет сброса и опять поставит флаг DataRdy, и данные (факт их обновления) не потеряются, данные будут обработаны в следующем цикле.
Что дает такая организация кода?
Код разделен на понятные блоки, перед ними можно дать пространные комментарии, что делает каждый блок. Мы имеем возможно задавать приоритеты блокам, перекрывая присвоения одного блока другим, при это не связываем их в огромные неудобные списки if — else if — else if. Можно удалить или “закомментировать” блок, вставить между любыми блоками еще один, остальная часть кода продолжит работать без правок.
Поскольку у нас единый always, то нет конфликтов двойных источников сигнала, если в какой-то момент мы решим изменять сигналы в разных структурных блоках. Мы просто меняем сигнал где и когда нам надо. Не надо организовывать никаких “хэндшейков” и “пробрасывать” дополнительные сигналы, как в случае отдельных always.
Код управляем, читаем, изменяем, существует по понятным законам, вам не надо собирать приоритетные шифраторы и подавать их на мультиплексоры интерфейсов, собирая все сигналы в шины и прикидывать все условия изменения сигнала.
Все что вам надо, просто описать поведение схемы, что вы от нее хотите, задать приоритеты расположением блоков описания и отдать все это на обработку синтезатору. Можете быть уверены, он прекрасно справится с поставленной задачей и выдаст схему с желаемым поведением, уж синтезатор Xilinx точно, но думаю и другие будут солидарны.