Существующее множество архитектур микроконтроллеров и микропроцессоров способно удовлетворить много задач во встраиваемых системах. Но существуют задачи, для которых классическая процессорная архитектура либо не подходит вообще, либо подходит в качестве Ъ-решения. Например это могут быть задачи высокоскоростных/многопоточных вычислений, задачи реализации многоканальных интерфейсов, аппаратные сопроцессоры и тому подобное. Для их решения обычно приходится самому разрабатывать микросхемы. Одним из дешевых и доступных вариантов такой разработки является ПЛИС (программируемая логическая интегральная схема). Для описания конфигурации ПЛИС может быть использован язык VHDL. Во многом язык VHDL похож на языки программирования - в нем есть операторы ветвления, циклы, типы данных, и даже подпрограммы, но при этом всем он не является языком программирования! И это самое главное, что нужно понять если вы начинаете его осваивать. Как известно, язык программирования - это язык, операторы которого превращаются в машинный код, который последовательно (инструкции одна за одной) выполняется процессором или микроконтроллером. В ПЛИС выполнять подобные инструкции просто некому, так что всё, что написано на VHDL не выполняется, а описывает устройство. Что подразумевается под словом "описывает" я и попытаюсь на конкретном примере объяснить в этой и последующих статьях.

Применение VHDL для описания устройств я буду показывать на примере ПЛИС фирмы Xilinx семейства Spartan 6(FPGA). Самые дешевые чипы данного семейства поштучно стоят порядка 9$ в Китае, например модель xc6slx9-3tqg144, которую я укажу в настройках проекта. Разрабатывать конфигурацию для ПЛИС я буду в среде Xilinx ISE WebPACK 14.3, она кросс-платформенная и без проблем установилась на Arch linux. Чтобы создать проект надо выполнить File -> New Project -> придумать Name -> Next -> выбрать Family: Spartan6, Device:XC6SLX9, Package:TQG144, Speed: -3, Preffered Language: VHDL -> Next -> Finish. Теперь можно добавлять в проект файлы с описанием. Для этого делаем Project -> New Source -> VHDL Module, File Name: например vhdl_dev -> Next -> Next -> Finish. Вот и все - открылся текстовый редактор, там будет что-то вроде:

----------------------------------------------------------------------------------
-- Company: 
-- Engineer: 
-- 
-- Create Date:    23:42:52 05/08/2014 
-- Design Name: 
-- Module Name:    vhdl_dev - Behavioral 
-- Project Name: 
-- Target Devices: 
-- Tool versions: 
-- Description: 
--
-- Dependencies: 
--
-- Revision: 
-- Revision 0.01 - File Created
-- Additional Comments: 
--
----------------------------------------------------------------------------------
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
-- Uncomment the following library declaration if using
-- arithmetic functions with Signed or Unsigned values
--use IEEE.NUMERIC_STD.ALL;
-- Uncomment the following library declaration if instantiating
-- any Xilinx primitives in this code.
--library UNISIM;
--use UNISIM.VComponents.all;
entity vhdl_dev is
end vhdl_dev;
architecture Behavioral of vhdl_dev is
begin
end Behavioral;

Все строки, которые начинаются с двух дефисов (--) - это комментарии, их можно удалить чтобы не мешали. Операторы library и use используются для подключения стандартной библиотеки и пакета из неё. Между строками entity <name> is и end <name> нужно поместить перечень сигналов (грубо говоря проводов), по которым будет осуществляться ввод/вывод в устройство (то есть описать порт устройства). Пока что там пусто. Имя <name> должно совпадать с именем файла (без расширения). Наконец между architecute ... of ... is и end ... нужно поместить само описание устройства. 

Простой пример: XOR на VHDL

Давайте, допустим реализуем самое простое устройство, которое должно будет выполнять операцию "исключающего ИЛИ" (Exclusive or, XOR) над двумя сигналами. Например у нас есть две кнопки, ПЛИС и светодиод, соединенные по схеме показанной справа. И допустим нам нужно сделать так, что бы светодиод горел, только когда нажата одна из кнопок. При этом когда нажаты обе кнопки, или не нажата ни одна, он гореть не должен. Такое поведение описывается операцией XOR.

Нажатие кнопки в данном случае означает подачу на вход ПЛИС высокого уровня напряжения (что соответствует логической "1"), в отпущенном состоянии на входе присутствует лог. "0". Аналогично светодиод в данной схеме будет гореть при выходном значении лог. "1", и не гореть при лог "0". 

На ПЛИС показаны три вывода IO, собственно к ним мы и подключим наши сигналы, то есть опишем их в entity. У нас будет два входных сигнала с названием I1, I2 а также один выходной сигнал O. В самом коде VHDL мы должны только описать операцию xor. 

Итак введем код:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
entity vhdl_dev is
   port(
      I1, I2 : in std_logic;
      O: out std_logic
   );   
end vhdl_dev;
architecture Behavioral of vhdl_dev is
begin
   O <= I1 xor I2;
end Behavioral;

В entity мы описали порт устройства, в котором прописаны два входных сигнала (in) и один выходной (out). Оба сигнала имеют тип std_logic. Этот тип описывает простой провод, по которому могут передаваться "0" либо "1" (забегая наперед стоит все же отметить, что это не единственные возможные значения, и еще могут быть значения вроде "не определен", "высоко-импедансный" и т.д.). В архитектуре мы описываем присвоение выходному сигналу (знаком '<=') результата xor над двумя входными сигналами.

Теперь выполним Process -> Implement Top module, и дожидаемся пока выполнится реализация. 

Просмотр RTL схемы

Посмотрим во что превратился данный код. Для начала посмотрим как этот код описал "схемотехнику" нашего устройства. Для этого взглянем на так называемую схему уровня регистровых передач (англ. Register transfer level, RTL). Смысл такой схемы в том, что она полностью описывает структуру устройства, элементами которой являются всяческие схемотехнические примитивы (например логические вентели и триггеры) а также связи между ними. 

Схему RTL мы можем увидеть в программе, предназначенной для назначения пинов "Plan Ahead" (на всякий случай скажу, что назначение пинов - это сопоставления сигналов из entity конкретным физическим ножкам микросхемы). Для запуска Plan Ahead, необходимо в окне Processes зайти в User Constraints и даблкликнуть на I/O Pin Planning (Plan Ahead) - Pre Synthesis

После нажатия, может появится диалог Yes/No, надо выбрать Yes, затем должно открыться окно PlanAhead (обычно оно весьма долго загружается). В окне нажимаем Background, Close. Ждем пока все элементы интерфейса загрузится и нажимаем F4, должна появится схема:

xorgate(2).jpg

Бирками обозначены входы и выходы, а желтым цветом единственный логический вентель (gate), имеющий тип RTL_XOR. 

Просмотр схемы в примитивах ПЛИС

Схема RTL показывает нам только логическую структуру устройства. Такая структура не зависит от технологии реализации, то есть может быть реализована в любой элементной базе, будь то ПЛИС (микросхемы, структуру которых можно менять), заказные микросхемы (микросхемы, структуру которых нельзя менять, например микроконтроллеры), или даже просто куча микросхем дискретной логики (например серий К155 или 74HC). Последний способ в реальной жизни уже конечно не встретить, но первые процессоры выполнялись именно так.

Но нас интересует каким образом эта RTL схема будет реализована физически именно в нашей ПЛИС. Для этого необходимо зайти в Processes -> User Constraints и даблкликнуть на I/O Pin Planning (Plan Ahead) - Post Synthesis(сразу под Pre Synthesis). В окне Plan Ahead опять нажимаем F4. После этого во вкладке Schematic должна появится схема:

xorpldlevel.jpg

Первое, что мы здесь видим это примитивы IBUF и OBUF. Это так называемые входные и выходные буфферы, которые заводят сиганл в ПЛИС и выводят её наружу соответственно. Уровни входных и выходных напряжений могут быть весьма разные - например 1.2 В, 2.5 В, 3.3 В (это настраивается разработчиком), но внутри ПЛИС используются одинаковые уровни, по этому задачей буферов является их преобразование.

Далее на схеме показан примитив LUT с названием "Mxor_O_xo<0>1" (Look-UP Table, дословно таблица поиска). Примитив LUT - это базовая единица хранения конфигурации в ПЛИС. LUT описывает однобитную логическую функцию от нескольких однобитных логических входов. В данном случае у нас два входа. Конфигурация хранится в виде последовательности бит, каждый бит для своего набора значений входов. Для N входов всего может быть 2^N наборов/ Например для двух это будет 2^2 = 4:

  1. Первый набор: I0=0, I1=0
  2. Второй набор: I0=0, I1=1
  3. Третий набор: I0=1, I1=0
  4. Четвертый набор: I0=1, I1=1

То есть длинна конфигурационной строки будет равна 2^N двоичных бит. Теперь давайте рассмотрим конфигурационную строку LUT для нашего примера:

Номер 
набора 
I0  I1   Значение выхода
O (бит конфигурации)
 1 0 0  0
 2 0 1  1
 3 1 0  1
 4 1 1  0

Конфигурационная строка записывается в порядке от последнего набора к первому, то есть сначала пишем 4ый бит: "0", потом пишем третий (получаем "01"), потом 2ой ("011"), и наконец 1ый ("0110"). Чтобы проще было запомнить, считайте что младший разряд строки-конфигурации имеет номер один, а самый старший - последний номер. Часто, для сокращения записи, строку конфигурации LUT записывают в виде 16тиричного кода вместо двоичного (в данном случае 0110(2) = 6(16)). Чтобы убедится, что я вам не соврал, вы можете нажать на "Mxor_O_xo<0>1"  и в окне Instance Properties, во вкладке Arttributes, посмотреть значение INIT:

Тут указано значение атрибута INIT длинной в 4 бита, со значением hex 6.

Симуляция проекта

После описания и синтеза устройства, нужно проверить его работоспособность. В простом случае можно проверить прошив конфигурацию в ПЛИС на реальной плате (перед этим назначив пины и уровни напряжений на них в Plan Ahead ), либо можно воспользоваться симулятором.

Для симуляции во первых нужно описать воздействия на наше устройства (то есть прописать интерисующие нас, разные варианты входных данных). Сделать это можно в Test Bench. В ISE делаем Project -> New Source -> VHDL Test Bench -> В File Name: test_vhdl_dev (например) -> Next -> Next -> Finish. Появится шаблон Test Bench, в который нужно вписать свои воздействия. По сути тестбенч описывает виртуальное устройство, которое подключает разрабатываемое устройство в качестве компонента и управляет его входами. 

Предлагаю стереть шаблон и написать вместо него следующее:

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
ENTITY test_vhdl_dev IS
END test_vhdl_dev;
ARCHITECTURE behavior OF test_vhdl_dev IS 
-- Component Declaration for the Unit Under Test (UUT)
COMPONENT vhdl_dev
   PORT(
      I1 : IN  std_logic;
      I2 : IN  std_logic;
      O : OUT  std_logic
   );
END COMPONENT;
--Inputs
signal I1 : std_logic := '0';
signal I2 : std_logic := '0';
--Outputs
signal O : std_logic;
BEGIN
-- Instantiate the Unit Under Test (UUT)
uut: vhdl_dev PORT MAP (
   I1 => I1,
   I2 => I2,
   O => O
); 
-- Stimulus process
stim_proc: process
begin
   I1 <= '0'; 
   I2 <= '0'; 
   wait for 100 ns;
   I1 <= '0'; 
   I2 <= '1';
   wait for 100 ns;
   I1 <= '1'; 
   I2 <= '0';
   wait for 100 ns;
   I1 <= '1'; 
   I2 <= '1';   
   wait;
end process;
END;

В первой половине этого кода (до Stimulus process) происходит подключение нашего устройства в качестве компонента тестбенча и описание всяких сигналов по которым он подключается. Эту часть понимать пока не обязательно, тем более что она была сгенерирована автоматически. А вот в Stimulus process описываются внешние воздействия на устройство (стимулы). Вначале мы подаем на оба входа значения логических нулей, потом ждем 100 наносекунд и подаем на один из входов "1", а на другой "0" и т.д. Таким образом мы перебрали все 4ре возможные варианта.

Теперь нужно запустить симулятор. Для этого в окне Design выбираем Simulation, нажимаем на файл с тестом (на test_vhdl_dev ...) и в окне Processes два раза нажимаем на "Simulate Behevioral Model ...":

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

isimbehevioral.jpg

Думаю все понятно - верхние линии показывают лог. "1", а нижние - лог "0". В колонке Value показаны значения сигналов на желтом маркере. На картинке маркер стоит на времени в 400 ns (когда оба входа равны "1"), по этому но выходе показан "0".

Симуляция с учетом временных задержек

Выполненная ранее симуляция проходит на так называемом поведенческом (behevioral) уровне, когда результат на выходе изменяется мгновенно при изменении сигналов на входе. Такая симуляция удобна для отладки алгоритмов работы устройства, но она весьма идеализирована и следственно может отличатся от реальной работы устройства. В реальности между изменением входных и выходных значений на каждом примитиве ПЛИС присутствует определенная временная задержка. Конечно, это задержка составляет от десятков пикосекунд до десятков наносекунд, но при высоких частотах изменения входных сигналах (порядка десятков МГц, эти задержки могут повлиять на работоспособность).

Для того, чтобы увидеть результат работы устройства в более реальном виде, можно воспользоватся симуляцией с "учетом временных задержек" ( также называемой Post - Fit). Такая симуляция проводится с учетом задержки конкретных примитивов ПЛИС (указаны поставшиками ПЛИС для каждого примитива, в нашем случае это будет задержка на выходе LUT), по этому работоспособность в железе будет не хуже чем в такой симуляции.

Post-Fit симуляция запускается аналогично Behevioral, только в окне Design в списке сверху нужно выбрать Post-Route:

Далее все точно также. В окне симулятора теперь должны появится вышеописанные задержки. Измерить их можно потянув желтый маркер от фронта одного сигнала до фронта другого (фронт - это перепад сигнала, то есть место, где линии меняются с низкой на высокую - передний фронт, либо с высокой на низкую - задний фронт) :

Видно, что при установке сигнала I2 в "1" сигнал на выходе устанавливается в "1" только после 6.403 наносекунд! О такой задержки нужно помнить, и для того что бы убедится в работоспособности устройства, после выполнения поведенческой симуляции нужно  выполнять симуляцию с учетом временных задержек. В некоторых случаях задержки даже преднамеренно используются разработчиками, и их устройства в поведенческой симуляции может не работать.

Определение использованных ресурсов

Последнее, о чем хотелось бы написать в данной статье, это просмотр отчета о имплементации устройства. Такой отчет после выполнения Implement, доступен в меню Project -> Design Summery. 

Что тут можно увидеть:

В строках показаны существующие примитивы ПЛИС, а в колонках: их использованное количество (Used), количество доступное в выбранной модели ПЛИС (Availiable), а также процент использования (Utilization). Процент округляется до 1. Первая ненулевая характеристика это Number of Slice LUTs - количество использованных LUT-ов. Как мы уже видели это всего один LUT. Тут стоит сказать, что двух-входовых LUT-ов в нашей ПЛИС Spartan6 на самом деле нету, тут есть только 5и-входовые(O5) и 6и-входовые(O6), причем 6и-входовой на самом деле состоит из двух 5и-входовых. Поэтому увиденный ранее LUT2 в самом деле был преобразован в LUT6, в котором использованы только 2 входа из 6и.

Конечный вид занятых ресурсов можно посмотреть воспользовавшись процессом Analyze Timing (в окне процессов):

При этом будет запущен планировщик пинов, но уже в режиме окончательного размещения примитивов на кристале ПЛИС. Используя зум можно увидеть следующее:

destsliceview.jpg:

Голубым подсвечен использованный LUT6, а зелеными линиями показаны связи, которые дальше идут к IBUF/OBUF.

Следующая характеристика это Number of occupied slices - количество занятых слайсов. Слайс это несколько елементарных ресурсов, объединенных во едино. На картинке выше показан именно слайс. Слайсы бывают разных типов, в нашем случае использован слайс типа "L"(SLICEL). В нем мы види 4 6и-входовых лута, три однобитных мультиплексора, какуюто непонятную логику CARRY (видимо что-то для переноса) и 8 разных триггеров(Flip Flop).