Ну там никто не мешает и как просто ПЛИС без процессора использовать на самом деле.
Но вообще — позволяет выполнить и интегрировать realtime-периферию гораздо ближе к ядру процессора, чем при отдельных кристаллах, не ограничиваясь при этом softcore-процессором внутри самой ПЛИС.
Мы тут как-то по работе недавно покупали Zynq-модули у Trenz, на выходе получается ARM-процессор + ПЛИС довольно большой ёмкости (это в одном кристалле), + SPI Flash + пара модулей на плате, выводы и отдельный Xilinx-совместимый программатор на CPLD от Lattice, и всё это чуть меньше $100.
Variable, используемые как де-факто регистры, — это один из верных способов выстрелить себе в ногу в VHDL. Их поведение сложнее контролировать в асинхронных (описывающих логику, а не триггер) процессах, в целом есть довольно высокий шанс получить на выходе т.н. защёлки (latch), которых не должно быть в проекте, если только не ставишь их туда вручную с конкретной целью.
Опять же, разные инструменты синтеза дружат с переменными очень по-разному и могут создавать из них регистры даже там, где это не требуется, после чего схема работает неправильно.
Добро пожаловать в мир HDL! Несколько придирок, на которые хотелось бы обратить внимание. :)
1. Строго говоря, код на HDL не является «программой». На это же напрямую намекает само название hardware description language — «язык описания аппаратуры». HDL скорее ближе к схемотехнике, чем к программированию. Хотя конечно ПЛИС дают некоторое впечатление программирования, т.к. результат можно увидеть буквально через пару минут, а не через три месяца как в случае со СБИС.
2. Первый процесс даст вам деление на 6, а не на 5 (отсчёты с 0 до 5, 0-1-2-3-4-5).
3. Никогда раньше не сталкивался с
shared variable
, в любом случае даже просто переменные использовать в VHDL лучше по минимуму и для промежуточных значений в процессах; основу, а тем более 100% выходов синхронных элементов, должны составлять сигналы
signal
. Если возникают сложности с использованием значений сигналов внутри процессов — следует подробнее ознакомиться к параллельными и последовательными блоками в VHDL (в Verilog логика похожа) и присвоением значений сигналам и переменным.
4. С т.з. пользователя управление с требованием зажатия нескольких кнопок выглядит не очень приятным. Я бы предложил, например, отвести одну кнопку под выбор режима (редактирование одного или другого параметра), две под инкремент/декремент и одну под включение/выключение как есть.
5. Если уж мы выполняем проверку «длина импульса должна быть меньше периода» при изменении импульса, то стоило бы проверять и обратный случай при изменении периода.
6. В целом получается, что duty/period и dig/di — это живущие своей независимой жизнью одинаковые величины, отличающиеся лишь в представлении. В общем случае более логично было бы выполнять двоично-десятичное преобразование от исходных значений. Но в данном примере подозреваю, что стоял вопрос жёсткого ограничения по площади ПЛИС.
7.
if cnt > period then
elsif cnt = duty then
Здесь опять «ошибка +1» — счётчик работает на диапазоне от 0 до периода, что приводит к заданию числом period реального значения в period+1 тактов.
8.
variable cntX : integer range 0 to 1000 := 0;
variable cnt : integer range 0 to 1000 := 0;
Тут опять максимальное значение 1000, что даст нам 1001 отсчётов, а еще — возможно данное описание корректно работает с обработкой переполнения счётчика (переход из 1000 в 0), но описано несколько страшновато. В самом первом процессе счётчик более корректен — там данная проверка описания переполнения выполняется явно:
t := t + 1;
if t = 5 then
t := 0;
tact <= not tact;
end if;
… и тут я понял, что пункт №2 как раз правильный, а у меня нет, всё дело в переменных, где сначала выполняется t := t + 1, получаем 5, а потом сразу меняем на 0. Но такое использование переменных может привести к избыточной логике. При использовании сигналов, новое значение устанавливается в сигнале только по выходу из процесса, т.о. если процесс сихронный, то результат t <= t + 1 мы «увидим» в этом процессе только на следующем такте (так же это работает и в реальности для триггеров). Совсем хорошо было бы так:
signal t: integer;
process(clk)
begin
if rising_edge(clk) then
if t = 4 then
t <= 0;
else
t <= t + 1;
end if;
end if;
end process;
Возвращаясь к счётчикам — для cntX переход через 0 не проверяется, а для cnt, как уже выше было сказано, посчитается как +1.
Название проекта конечно более-менее запоминающееся, но при этом уж очень плохо индексируемое, распространить это не удастся уже только по данной причине.
Немного добавить — в Европе почему-то ярлык «картофельной нации» закрепился за латышами, и если искать шутки про латышей или про картофель — обязательно найдётся пересечение, а вот про беларусов и картофель мало кто знает.
La tortilla de patatas это не только «не совсем драник», но и «совсем не драник», потому что это жареная картошка с яйцом и луком. Драники, как и бабка, делаются из тёртого картофеля, в то время как в тортилье картофель медальонами, возможно слегка «помятый».
Все думают, что нужно заставить всех перейти на единый банк и валюту, но почему-то каждый видит в качестве единой свою собственную валюту и собственный банк ;)
Стоит заметить, что для складского учёта NFC-метки так себе — в таких случаях используют UHF-метки, работающие на частотах в районе 900МГц (±, несущая зависит от региона использования). UHF RFID работает на расстояниях до 10 метров, протокол специально рассчитан для быстрой инвентаризации и позволяет достаточно быстро «прозвонить» целый грузовик товара с расстояния.
NFC конечно более удобен для домашнего использования, т.к. достаточно только смартфона под рукой.
Лично у меня этот «нормальный лаунчер» вёл себя примерно так же, как описано в статье. Сама игра весит 50-70 ГБ, при этом для нормального обновления еще требуется свободных столько же (но это наугад), ничего про это лаунчер не говорит и может потратить несколько часов времени на обновление, после чего сказать «у меня тут закончилось место, обновление прервано, жмите Repair». На починке игра по сути перескачивалась заново. По итогу почти все обновления у меня проходили с третьего-четвёртого раза.
Честно говоря, почти ничего не понял из написанного. Блокирующее присваивание выполняется мгновенно, неблокирующее выполняется после дельта-задержки. Если все always-блоки выполняются в пределах одной дельта-задержки, то действительно возможно появление результата блокирующего присваивания ранее, чем это требовалось. Поэтому выше уже упоминалось, что с этой точки зрения разделение в VHDL на сигналы и переменные удобнее и адекватнее, чем "=" и "<=" в Verilog. Но наличие возможности выстрелить себе в ногу не является достаточным для «это нельзя делать, это всё неправильно, никогда так не пишите».
По поводу частоты семплирования:
UART является асинхронным интерфейсом — без передачи синхросигнала и без восстановления частоты из данных. Скорость передачи данных по сути «оговаривается» заранее, определить её из потока данных довольно проблематично.
Но даже если оба устройства знают на какой частоте они обмениваются данными, данные частоты всё равно не будут идентичными. Во-первых, в каждом устройстве в общем случае будет свой источник тактирования (кварц, генератор, что угодно), имеющий внутренний дрифт. Во-вторых, часто невозможно получить целочисленное деление тактовой частоты для подходящего baud rate в UART: контроллер например работает на 24МГц, а чтобы получить 115200, нужно 24МГц разделить на 208.(3).
В общем к чему это я всё — семплировать входящие данные лучше на как можно большей частоте. К примеру, распространённым механизмом является работа приёмника на частоте x16: по заднему фронту входного сигнала (начало старт-бита) отсчитываем 8 тактов, попадая таким образом на середину бита; далее через каждые 16 тактов читаем значение входного бита. С точки зрения частотной ошибки мы можем себе позволить расхождение по частоте на ±8 периодов нашей частоты семплирования (т.к. точка семплирования в таком случае выпадет за пределы бита), и соответственно на ±1 период частоты семплирования на бит.
На первом такте при начале старт-бита мы заведомо не знаем попал ли наш отчёт ровно на задний фронт или нет, точность этого — как раз один период частоты семплирования. Соответственно при более низкой частоте (как х4 в вашем случае) стартовая ошибка может быть больше, а запас по отклонению частот — меньше.
По поводу использования блокирующих и неблокирующих присваиваний:
«Идеологически правильный» код потенциально может привести к формированию защёлок (latch) в синтезе: блок always @(*) комбинаторный, следовательно для каждого из сигналов, используемых в левой части присвоений, должны быть описаны все условия ветвления. В данном примере это обходится (возможно, по счастливой случайности) наличием блокирующих присваиваний сигналов перед ветвлениями, что по сути станет неявным присваиванием во всех ветвлениях, где это не описано явно. Примерно так развернутся эти присваивания:
always @(*)
begin
if(nreset) begin
if(div4 == 2'd0) begin
case(s)
4'd0: begin
sendstart = 1'b1;
canfetch = 1'b0;
end
4'd9: begin
sendstart = 1'b0;
canfetch = wr;
end
4'd10:
begin
sendstart = 1'b0;
canfetch = wr;
end
default:
begin
sendstart = 1'b0;
canfetch = 1'b0;
end
endcase
end else begin
sendstart = 1'b0;
if(s < 4'd9) begin
canfetch = 1'b0;
end
else begin
canfetch = wr;
end
end
if(canfetch && idle) begin
sendstart = 1'b1;
end
else begin
sendstart = 1'b0;
end
end else begin
sendstart = 1'b0;
canfetch = 1'b0;
end
end
Собственно, если бы присваивания были неблокирующими, ваш вариант скорее всего работал бы некорректно (не помню как в Verilog, но в VHDL повторное неблокирующее присваивание в блоке считается ошибкой).
Вне зависимости от того, блокирующие у нас присваивания или неблокирующие, пропуск одной из веток приведёт к тому, что синтезатор не будет знать, что ему делать с сигналом в данном условии, и примет решение создать элемент памяти. Но т.к. в блоке не указаны фронты (а то и вообще комбинаторный список чувствительности), будет установлена защёлка. Мета-пример подобной ситуации:
always @(*)
begin
if(nreset) begin
if(canfetch) begin
sendstart = 1'b1;
end
end else begin
sendstart = 1'b0;
end
end
Что должно происходить с сигналом sendstart при nreset == 1 и canfetch == 0? Выходит, что он должен сохранить своё текущее состояние. Но для этого нужен элемент памяти, т.к. ситуация невозможна, если использовать только комбинаторные элементы.
Во втором процессе нет состояния гонки и отсутствует неопределённое поведение, т.к. if проверяется лишь однажды. На выходе синтеза получится T-триггер.
поведение в данном случае полностью определённое и будет просинтезировано аналогично
always @(posedge clk) begin
a <= b;
b <= c;
end
На выходе вы получите сдвиговый регистр a < — b < — c.
Если же строчки присвоения поменять местами, то поведение данных блоков будет отличаться — в случае с неблокирующими присваиваниями по-прежнему будет трёхбитовый сдвиговый регистр, а вот в случае с блокирующими — двухбитовый a < — c, при этом сигнал b скорее всего будет исключен в процессе оптимизации синтеза.
Но никаких неопределённостей в данных конструкциях нет, просто нужно понимать как именно они «отображаются» в реальную схему.
Вообще использование блокирующих присваиваний внутри always вполне себе допустимо, и даже допустимо чтение сигнала в том же блоке «ниже по коду», в котором он присваивается. Просто это выльется в более длинную цепочку логики, подобные операции будут выполнены в пределах одного такта. Пока это писал, увидел, что ниже вам то же самое уже написали ранее.
Если вы добавляете задержку во всех операциях, то суммарная задержка по асинхронной логике тоже может переполниться.
Ну и опять же если у вас присвоения задержек есть на синхросигналах и на сигналах сброса, то добавление задержек не будет соответствовать результатам синтеза, т.к. величины задержек будут разными. Синтез строит clock tree и reset tree, добавляя буферы так, чтобы итоговая задержка по цепям сброса и синхронизации соответствовала распространению данных. В системах с большим количеством (да в общем-то просто больше одного) асинхронных друг другу частотных доменов это может быть сложно отмоделировать правильно, а внесённая руками задержка может изменить работу схемы.
Вы проигнорировали всё, что old_bear вам написал. Описанная проблема заключается в том, что из-за особенностей моделирования у присвоения «clk2 <= clk1» нет прямого эквивалента в синтезе, т.к. в моделировании такая конструкция создаёт задержку (пусть и дельта), а в синтезе — нет.
То, что вы в итоге добавляете задержки в присвоения, по сути имеет ту же заложенную проблему — подобные присвоения не будут эквивалентными результатам синтеза.
Но вообще — позволяет выполнить и интегрировать realtime-периферию гораздо ближе к ядру процессора, чем при отдельных кристаллах, не ограничиваясь при этом softcore-процессором внутри самой ПЛИС.
Опять же, разные инструменты синтеза дружат с переменными очень по-разному и могут создавать из них регистры даже там, где это не требуется, после чего схема работает неправильно.
1. Строго говоря, код на HDL не является «программой». На это же напрямую намекает само название hardware description language — «язык описания аппаратуры». HDL скорее ближе к схемотехнике, чем к программированию. Хотя конечно ПЛИС дают некоторое впечатление программирования, т.к. результат можно увидеть буквально через пару минут, а не через три месяца как в случае со СБИС.
2. Первый процесс даст вам деление на 6, а не на 5 (отсчёты с 0 до 5, 0-1-2-3-4-5).
3. Никогда раньше не сталкивался с , в любом случае даже просто переменные использовать в VHDL лучше по минимуму и для промежуточных значений в процессах; основу, а тем более 100% выходов синхронных элементов, должны составлять сигналы . Если возникают сложности с использованием значений сигналов внутри процессов — следует подробнее ознакомиться к параллельными и последовательными блоками в VHDL (в Verilog логика похожа) и присвоением значений сигналам и переменным.
4. С т.з. пользователя управление с требованием зажатия нескольких кнопок выглядит не очень приятным. Я бы предложил, например, отвести одну кнопку под выбор режима (редактирование одного или другого параметра), две под инкремент/декремент и одну под включение/выключение как есть.
5. Если уж мы выполняем проверку «длина импульса должна быть меньше периода» при изменении импульса, то стоило бы проверять и обратный случай при изменении периода.
6. В целом получается, что duty/period и dig/di — это живущие своей независимой жизнью одинаковые величины, отличающиеся лишь в представлении. В общем случае более логично было бы выполнять двоично-десятичное преобразование от исходных значений. Но в данном примере подозреваю, что стоял вопрос жёсткого ограничения по площади ПЛИС.
7.
Здесь опять «ошибка +1» — счётчик работает на диапазоне от 0 до периода, что приводит к заданию числом period реального значения в period+1 тактов.
8.
Тут опять максимальное значение 1000, что даст нам 1001 отсчётов, а еще — возможно данное описание корректно работает с обработкой переполнения счётчика (переход из 1000 в 0), но описано несколько страшновато. В самом первом процессе счётчик более корректен — там данная проверка описания переполнения выполняется явно:
… и тут я понял, что пункт №2 как раз правильный, а у меня нет, всё дело в переменных, где сначала выполняется t := t + 1, получаем 5, а потом сразу меняем на 0. Но такое использование переменных может привести к избыточной логике. При использовании сигналов, новое значение устанавливается в сигнале только по выходу из процесса, т.о. если процесс сихронный, то результат t <= t + 1 мы «увидим» в этом процессе только на следующем такте (так же это работает и в реальности для триггеров). Совсем хорошо было бы так:
Возвращаясь к счётчикам — для cntX переход через 0 не проверяется, а для cnt, как уже выше было сказано, посчитается как +1.
NFC конечно более удобен для домашнего использования, т.к. достаточно только смартфона под рукой.
По поводу частоты семплирования:
UART является асинхронным интерфейсом — без передачи синхросигнала и без восстановления частоты из данных. Скорость передачи данных по сути «оговаривается» заранее, определить её из потока данных довольно проблематично.
Но даже если оба устройства знают на какой частоте они обмениваются данными, данные частоты всё равно не будут идентичными. Во-первых, в каждом устройстве в общем случае будет свой источник тактирования (кварц, генератор, что угодно), имеющий внутренний дрифт. Во-вторых, часто невозможно получить целочисленное деление тактовой частоты для подходящего baud rate в UART: контроллер например работает на 24МГц, а чтобы получить 115200, нужно 24МГц разделить на 208.(3).
В общем к чему это я всё — семплировать входящие данные лучше на как можно большей частоте. К примеру, распространённым механизмом является работа приёмника на частоте x16: по заднему фронту входного сигнала (начало старт-бита) отсчитываем 8 тактов, попадая таким образом на середину бита; далее через каждые 16 тактов читаем значение входного бита. С точки зрения частотной ошибки мы можем себе позволить расхождение по частоте на ±8 периодов нашей частоты семплирования (т.к. точка семплирования в таком случае выпадет за пределы бита), и соответственно на ±1 период частоты семплирования на бит.
На первом такте при начале старт-бита мы заведомо не знаем попал ли наш отчёт ровно на задний фронт или нет, точность этого — как раз один период частоты семплирования. Соответственно при более низкой частоте (как х4 в вашем случае) стартовая ошибка может быть больше, а запас по отклонению частот — меньше.
По поводу использования блокирующих и неблокирующих присваиваний:
«Идеологически правильный» код потенциально может привести к формированию защёлок (latch) в синтезе: блок always @(*) комбинаторный, следовательно для каждого из сигналов, используемых в левой части присвоений, должны быть описаны все условия ветвления. В данном примере это обходится (возможно, по счастливой случайности) наличием блокирующих присваиваний сигналов перед ветвлениями, что по сути станет неявным присваиванием во всех ветвлениях, где это не описано явно. Примерно так развернутся эти присваивания:
Собственно, если бы присваивания были неблокирующими, ваш вариант скорее всего работал бы некорректно (не помню как в Verilog, но в VHDL повторное неблокирующее присваивание в блоке считается ошибкой).
Вне зависимости от того, блокирующие у нас присваивания или неблокирующие, пропуск одной из веток приведёт к тому, что синтезатор не будет знать, что ему делать с сигналом в данном условии, и примет решение создать элемент памяти. Но т.к. в блоке не указаны фронты (а то и вообще комбинаторный список чувствительности), будет установлена защёлка. Мета-пример подобной ситуации:
Что должно происходить с сигналом sendstart при nreset == 1 и canfetch == 0? Выходит, что он должен сохранить своё текущее состояние. Но для этого нужен элемент памяти, т.к. ситуация невозможна, если использовать только комбинаторные элементы.
поведение в данном случае полностью определённое и будет просинтезировано аналогично
На выходе вы получите сдвиговый регистр a < — b < — c.
Если же строчки присвоения поменять местами, то поведение данных блоков будет отличаться — в случае с неблокирующими присваиваниями по-прежнему будет трёхбитовый сдвиговый регистр, а вот в случае с блокирующими — двухбитовый a < — c, при этом сигнал b скорее всего будет исключен в процессе оптимизации синтеза.
Но никаких неопределённостей в данных конструкциях нет, просто нужно понимать как именно они «отображаются» в реальную схему.
Нет, потому что я левша
Ну и опять же если у вас присвоения задержек есть на синхросигналах и на сигналах сброса, то добавление задержек не будет соответствовать результатам синтеза, т.к. величины задержек будут разными. Синтез строит clock tree и reset tree, добавляя буферы так, чтобы итоговая задержка по цепям сброса и синхронизации соответствовала распространению данных. В системах с большим количеством (да в общем-то просто больше одного) асинхронных друг другу частотных доменов это может быть сложно отмоделировать правильно, а внесённая руками задержка может изменить работу схемы.
То, что вы в итоге добавляете задержки в присвоения, по сути имеет ту же заложенную проблему — подобные присвоения не будут эквивалентными результатам синтеза.