ПЛИС внутри ПЛИС

Матрешка: ПЛИС внутри ПЛИС

Я хочу рассказать о своем новом проекте для платы Марсоход.

Я попытался сделать проект своей маленькой ПЛИС, написать этот проект на Verilog  и реально запустить его внутри настоящей ПЛИС. Наверное такие проекты лучше делать в больших FPGA, но чем наша плата Марсоход хуже? Она так же годится для таких экспериментов, ну может масштаб не большой, но мне важен сам принцип.

Вообще-то, конечно, многим читателям этот проект покажется бестолковым: «Зачем все это?».

Во-первых, меня интересует эта тема с «академической» точки зрения. Могу ли я сам разработать микросхему ПЛИС? И как их разрабатывают? Во-вторых, я постараюсь рассказать зачем это может оказаться нужным.

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

Чтобы избежать путаницы в терминах я предлагаю в этой статье настоящую реальную микросхему CPLD называть «ПЛИС», а свое разрабатываемое программируемое логическое устройство называть «мПЛИС» - тоесть «моя ПЛИС» или «маленькая ПЛИС».

1. Внутреннее устройство.

Наверное вы уже знакомы с внутренним устройством микросхем ПЛИС (CPLD и FPGA) компании Altera. Сама компания Альтера достаточно подробно все описывает в своей документации. Например, микросхемы CPLD серии MAX II описаны здесь:

Конечно, я не смогу «побыстрому» повторить весь функционал известных микросхем, но какие-то базовые принципы я думаю получится воплотить. Самое главное - это реализовать  программируемый логический элемент LogicElement (LE). Второе – это программируемая коммутация соединений между логическими элементами. Оба эти компонента при включении ПЛИС должны быть инициализированы нужным образом, чтобы моя мПЛИС начала работать. В общем прямая аналогия с настоящими микросхемами. Например, FPGA при включении пуста, но потом в нее заливается образ проекта из специальной внешней микросхемы флеш памяти и вот она уже работает. Микросхемы серии MAX II компании Альтера реализованы примерно так же, только сама флеш находится внутри корпуса ПЛИС.

На плате Марсоход у нас установлена микросхема MAX II EPM240T100C5. В ней всего 240 логических элемента и есть еще User Flash Memory (UFM) – флеш память с последовательным интерфейсом. Я реализую свою маленькую мПЛИС с четырьмя (!) логическими элементами, у нее будет 4 входа и 4 выхода, а инициализироваться она будет из UFM микросхемы MAX II.

Пожалуй начну с описания логического элемента.

2. Логический Элемент.

Логический элемент ПЛИС
Рисунок 1.

Логический элемент состоит из Look-Up Table (LUT) и одного регистра-триггера. Как вы знаете, цифровые схемы работают синхронно с тактовой частотой. С каждым импульсом тактовой частоты схема переходит в следующее состояние. Все состояние схемы хранится в регистрах, а следующее состояние для каждого регистра вычисляется логическими функциями на входе данных этих триггеров. Вот собственно логическая функция для триггера в ПЛИС реализуется с помощью таблицы истинности Look-Up Table (LUT).

Логическая функция для каждого триггера у нас будет четырехвходовая – как функция от четырех возможных сигналов. Значит сама таблица истинности должна иметь 2^4=16 ячеек памяти и четыре входных сигнала LUT являются адресом для чтения результата из нашей таблицы. Не забудьте, что мы делаем программируемую логику, значит мы должны реализовать начальную загрузку таблицы Look-Up Table.  Как же это сделать?

Внутренне устройство LUT (Look-Up Table) в ПЛИС
Рисунок 2.

Посмотрите на эту схему Рисунок 2. Здесь я нарисовал 16 регистров, соединенных последовательно – это и есть таблица Look-Up Table. Если установлен сигнал lds, и подавать тактовую частоту ldclk, то данные из входного сигнала ldi будут последовательно распространяться через все регистры на выход ldo. Таким образом, можно через последовательный интерфейс за 16 тактов загрузить всю таблицу. Входные сигналы in0, in1, in2, in3 все вместе являются селекторами для мультиплексора, который и выбирает результат из таблицы Look-Up, реализуя любую логическую функцию от четырех аргументов.

На языке описания аппаратуры Verilog можно описать такую Look-Up Table вот так:


//serially programmable look-up table
module lut(
  input wire  ldclk, //clock used for image loading
  input wire  ldi,   //lut load data input
  output wire ldo,   //lut load data output
  input wire  lds,   //lut load data signal
  input wire [3:0]in,
  output wire out
);

reg [15:0]lut_reg;
always @(posedge ldclk)
 if(lds)
  lut_reg <= { lut_reg[14:0],ldi };

assign ldo = lut_reg[15];
assign out = lut_reg[in];

endmodule


Если посмотрите на Рисунок 1, то увидите, что выход LUT подключен ко входу данных триггера логического элемента. Однако, мой логический элемент имеет три выхода: один выход это выход триггера, который фиксирует значение LUT, второй – это выход непосредственно LUT и третий – это выход который идет на выходной пин моей мПЛИС. Для чего делать выход LUT? Если нужная логическая функция сложная, имеет больше 4х аргументов, то несколько LUT могут быть соединены каскадно, чтобы реализовать ее.

Небольшое замечание по поводу выходного пина. Видите мультиплексор, который выбирает источник сигнала для выходного пина? На выходной пин можно вывести либо выход LUT либо выход регистра. Этот мультиплексор так же программируемый и инициализируется при включении мПЛИС. Вообще в проекте много мультиплексоров и все они «программируемые». Давайте рассмотрим их подробнее.

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

Вот, например, четырехвходовый программируемый мультиплексор / коммутатор:

Программируемый мультиплексор 
Рисунок 3.

Селектор для мультиплексора берется с выходов двух триггеров, которые загружаются последовательно с помощью сигналов ldi, ldclk и lds за 2 такта. Вход ldi и выход ldo используются для последовательного соединения всех модулей проекта. Вы помните, как сделана инициализыция LUT этими же сигналами? Здесь используется та же методика.

Реализация такого мультиплексора на Verilog может выглядеть вот так:


//serially programmable multiplexor
module pmux4(
  input wire  ldclk, //clock used for image loading
  input wire  ldi, //load data input
  output wire ldo, //load data output
  input wire  lds, //load data signal
  input wire  [3:0]in,
  output wire out
);

reg [1:0]sel_reg;
always @(posedge ldclk)
 if(lds)
  sel_reg <= { sel_reg[0],ldi };

assign ldo = sel_reg[1];
assign out = (in[sel_reg] & (~lds));

endmodule


В моем проекте чрезвычайно простой мПЛИС из четырех логических элементов используются 4-х и 8-ми входовые программируемые коммутаторы.

Так, например, каждый вход LUT имеет 8ми входовый коммутатор, который позволяет подключить к нему либо любой из выходов любого триггера любого Логического Элемента, либо выход LUT любого логического элемента (кроме выхода своего LUT) либо один из входов ПЛИС.  Вот посмотрите на этот ужасный Рисунок 4:

Соединение Логических элементов в ПЛИС

Рисунок 4.

Зелеными линиями показаны все возможные связи, которые уже как бы реализованы. Только одно соединение из восьми возможных для каждого входа каждого LUT станет активным после загрузки пользовательского проекта благодаря программируемым мультиплексорам.

Кроме этого, каждый Логический Элемент имеет вход тактовой частоты и к нему, с помощью программируемого 4х входового коммутатора/мультиплексора можно подключить либо «глобальный» сигнал тактовой частоты  (есть у меня в мПЛИС и такой) либо один из выходов триггеров любого логического элемента, кроме выхода регистра своего Логического Элемента.

Точно так же 4х входовым программируемым мультиплексором можно комутировать входы Enable каждого логического элемента.

3. Загрузка пользовательского проекта в мПЛИС.

Еще раз про загрузку пользователького проекта в мою мПЛИС. Я уже говорил, что каждая LUT и каждый программируемый мультиплексор используют для загрузки (начальной инициализации) сигналы ldi, ldclk, lds и ldo. Выход ldo одного модуля соединен со входом ldi следующего и так далее. С помощью этих сигналов все компонены ПЛИС объединены в один очень длинный последовательный регистр, содержащий образ пользовательского проекта – «прошивку».

Чтобы проект стартовал в нашей ПЛИС нужно на вход lds подать единицу, на вход ldi последовательно задвигать образ проекта синхронно с тактовой частотой ldclk. Образ проекта пройдет «насквозь» все модули и выйдет на выход ldo последнего модуля.
Вот так инициализируется один логический элемент и его связи (а таких элементов у меня всего четыре):

Загрузка проекта в ПЛИС

Рисунок 5.

В исходном состоянии все триггера в которые загружается образ проекта содержат нули. Первый загружаемый бит в нашей прошивке должен быть всегда единицей – это будет признаком окончания загрузки. Как только на выходе ldo последнего модуля появляется единица – значит пора останавливать загрузку проекта и мПЛИС переходит в рабочее состояние.
 
Еще здесь нужно сделать одно очень важное замечание. Я столкнулся с одной важной проблемой природу которого я не сразу понял. В каком бы состоянии не находились триггера, содержащие образ проекта, его «прошивку» - это уже новая схема, загруженная в мПЛИС. Причем во время загрузки проекта в мПЛИС с каждым тактом загруженная схема меняется. Даже не полностью загруженный образ является уже работающей схемой и не всегда «правильной». Что такое «не правильная» схема? Это когда выход логики оказывается окольными путями подключен к ее же входу. Так делать нельзя. Пример такой неправильной логики, например, вот:

Неправильная логика

Эта схема имеет обратную связь. Altera Quartus II на такое безобразие конечно выдаст критическое сообщение «Design contains combinational loop of <number> nodes» и настоятельно порекомендует исправлять его.

Так и у меня в моей мПЛИС может получиться. Вход LUT1 может оказаться подключенным к выходу LUT2, а выход LUT1 подключенным ко входу LUT2.

А действительно, чем страшны эти обратные связи? Да просто схема может не работать, она может, например возбуждаться или оказываться в каком-то неизвестном состоянии. Такие обратные связи могут плохо симулироваться.

Ну конечно, если пользователь разработал устройство с такими обратными связями, то это остается на его совести. А что делать мне, разработчику мПЛИС, когда такие неправильные связи будут самопроизвольно образовываться где-то в процессе последовательной загрузки мПЛИС?

Я собственно понял, что в моем проекте что-то не правильно, когда iverilog отказывался симулировать мой проект. Причем некоторые проекты-образы я тестбенчем спокойно загружал в мою мПЛИС и симулировал, а некоторые образы (без изменения собственно схемы мПЛИС) приводили к краху симулятора iverilog в процессе загрузки образа.

Пришлось предпринять дополнительные меры, чтобы во время загрузки проекта он не работал. Я добился этого путем принудительного подавления выходных сигналов мультиплексоров в ноль во время активного сигнала lds.

Весь код на Verilog, описывающий мПЛИС можно посмотреть здесь.

4. Тестбенч.

После того, как код мПЛИС был написан нужно было как-то начинать его тестировать. Лучше всего начинать тесты с симуляции модулей. Для этих целей я написал специальный testbech на Verilog. Для симуляции я использую чрезвычайно простое средство iverilog (Icarus Verilog).

С помощью тестбенча я хотел посмотреть как будет загружаться образ пользовательского проекта в мПЛИС. Тут сразу возникает вопрос, а как я буду создавать эти образы, в каком виде их хранить? У меня естественно нет никакого компилятора для моей мПЛИС. Значит образы проектов придется создавать как-то ручным способом, например, редактируя некий текстовый файл.

Формат текстового файла для проектов мПЛИС я выбрал / придумал вот такой:


00011000 //config done bit
00011000 //>>>pin3 out select (lut or reg)
01000110 //lut3
01001010
01001010
01001010
00110000 //lut input muxes
00110010
00110100
00110110
00110000 //ena mux
00100000 //clk mux
00011000 //>>>pin2 out select (lut or reg)
01000011 //lut2
01001100
01001100
01001100
00110000 //lut input muxes
00110010
00110100
00110110
00110010 //ena mux
00100100 //clk mux
……


Это текстовый файл.

В начале каждой его строки в двоичном виде описан байт. Мне предстоит «побитное ручное программирование», типа как раньше в перфокартах дырочки пробивали Smile

Каждый байт в строке я мысленно делю на 2 половины по 4 бита. Старшая тетрада обозначает сколько бит из младшей тетрады являются значимыми.
В мПЛИС будет загружаться младшая тетрада старшими битами вперед, но только несколько бит. Например, первый байт всегда «00011000» - это значит взять и записать в мПЛИС один бит из младшей тетрады, в данном примере это единица. Этот бит, когда последовательно насквозь пройдет все триггера содержащие прошивку в мПЛИС, выйдет на последнем сигнале ldo и обозначает конец загрузки проекта – это как сигнал CONFIG_DONE.

Этот формат мне кажется удобным, потому, что я могу сразу четко выделить строки, который отвечают за разные компоненты проекта мПЛИС. Так, следующий одиночный бит (описываемый как 00011000, тоесть 0001 бит из потока 1000) конфигурирует выходной пин мПЛИС. Он определяет пин подключен к LUT или триггеру логического элемента. Дальше идут 4 строки по 4 бита – это значение таблицы LUT. Потом 4 строки по 3 бита – программируют мультиплексоры на входах LUT. Ну и так далее. Все вместе должно позволить мне мысленно создавать прошивку и описывать ее битами в файле.

Такой текстовый файл можно легко считать с помощью тестбенча на языке Verilog:


reg [7:0] value[0:63]; //64 bytes array used for cpld image
initial
begin
 //load "cpld image" into temporary array
 $readmemb("image.dat",value);

 $display("mini_cpld image file");
 for(i=0; i<64; i=i+1)
  $display("%d %b",i,value[i]);
end


После того, как файл считан в массив value, тестбенч загружает образ проекта в мПЛИС. Для этого я написал задачу sendbits. Ей нужны параметры num_bits (это старшая тетрада каждого байта массива) и val – это младшая тетрада байта массива.


task sendbits;
begin
  for
(j=0; j<num_bits; j=j+1)
  begin
    ldi = val[3];
    @(posedge ldclk)
    #0;
    val = val << 1;
  end
end
endtask

always
begin

  #5 ldclk = ~ldclk;
end


Еще одна задача, которую решает мой тестбенч – создание файла, описывающего содержимое flash памяти UFM реальной ПЛИС, в которой я потом собираюсь пробовать мой мПЛИС. Файл с расширением *.MIF – Memory Initialization File нужен квартусу для компиляции проекта для платы Марсоход и ее чипа MAX II.
 
Весь текст программы тестбенча можно посмотреть вот здесь.

Интересно, что получается при симуляции проекта. Сперва я компилирую тестбенч и мПЛИС с помощью iverilog (запуск из командной строки)

> iverilog –o qqq test.v mini_cpld.v

Потом вручную готовлю несколько разных текстовых файлов - прошивок для мПЛИС image.dat и с ними запускаю симулятор:

> vvp qqq

Просмотр получившихся временных диаграмм  делаем с помощью утилиты gtkwave из комплекта iverilog.

> gtkwave out.vcd

При симуляции проекта мПЛИС ведет себя по-разному в зависимости какой image.dat дашь симулятору. Тестбенч считывает его и загружает его в мПЛИС. Иными словами – что загружается в мПЛИС, так она и работает!

Вот теперь самое интересное – несколько проектов для мПЛИС, которые я разработал.

5. Тестовые проекты для мПЛИС.

5.1. Четырехбитный битный двоичный счетчик

У меня нет компилятора для моей мПЛИС, но первое, что я хотел сделать для нее на Verilog выглядит вот так:


reg [3:0]counter;
always @(posedge clk)
    counter <= counter+1;


Я ручками «скомпилировал» этот код в вот такой файл для мПЛИС:

00011000 //config done bit
00011000 //>>>pin3 out select (lut or reg)
01000110 //lut3
01001010
01001010
01001010
00110000 //lut input muxes
00110010
00110100
00110110
00110000 //ena mux
00100000 //clk mux
00011000 //>>>pin2 out select (lut or reg)
01000011 //lut2
01001100
01001100
01001100
00110000 //lut input muxes
00110010
00110100
00110110
00110010 //ena mux
00100100 //clk mux
00011000 //>>>pin1 out select (lut or reg)
01000000 //lut1
01001111
01001111
01000000
00110000 //lut input muxes
00110010
00110100
00110110
00110100 //ena mux
00101000 //clk mux
00011000 //>>>pin0 out select (lut or reg)
01000000 //lut0
01000000
01001111
01001111
00110000 //lut input muxes
00110010
00110100
00110110
00110110 //ena mux
00101100 //clk mux
00000000
00000000
00000000
00000000
00000000

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

Двоичный счетчик в мПЛИС

5.2. Четырехбитный счетчик по модулю 10

На языке Verilog такой счетчик можно описать вот так:


reg [3:0]counter;
always @(posedge clk)
  if(counter==9)
    counter <= 0;
  else
    counter <= counter+1;


Для того, чтобы сделать счетчик по модулю 10 нужно всего-навсего взять мой предыдущий проект двоичного счетчика и заменить таблицы LUT для трех старших регистров счетчика. Это я и сделал:

00011000 //config done bit
00011000 //>>>pin3 out select (lut or reg)
01000110 //lut3
01000000
01001010
01001010
00110000 //lut input muxes
00110010
00110100
00110110
00110000 //ena mux
00100000 //clk mux
00011000 //>>>pin2 out select (lut or reg)
01000011 //lut2
01001100
01001100
01001100
00110000 //lut input muxes
00110010
00110100
00110110
00110010 //ena mux
00100100 //clk mux
00011000 //>>>pin1 out select (lut or reg)
01000000 //lut1
01000101
01001111
01000000
00110000 //lut input muxes
00110010
00110100
00110110
00110100 //ena mux
00101000 //clk mux
00011000 //>>>pin0 out select (lut or reg)
01000000 //lut0
01000000
01001111
01001111
00110000 //lut input muxes
00110010
00110100
00110110
00110110 //ena mux
00101100 //clk mux
00000000
00000000
00000000
00000000
00000000

Результат симуляции получается вот такой:

Счетчик по модулю 10

Максимальное число на счетчике получается 9, что и требовалось.

5.3. Трехбитный счетчик с выходом COUT.

Вот код счетчика с дополнительным выходом COUT:


reg [2:0]counter;
always @(posedge clk)
  counter<=counter+1;

wire cout;
assign cout = (counter == 3’b111);


Для реализации такого кода требуется три регистра со своими LUT и еще одна просто LUT нужна для вычисления COUT.
Обратите внимание, во второй строчке «pin3 out select» выбирает для выходного пина выход LUT.

00011000 //config done bit
00010000 //>>>pin3 out select (lut or reg), here select LUT
01000100 //lut3
01000000
01000000
01000000
00110000 //lut input muxes
00110010
00110100
00110110
00110000 //ena mux
00100000 //clk mux
00011000 //>>>pin2 out select (lut or reg), here select REG
01000011 //lut2
01001100
01001100
01001100
00110000 //lut input muxes
00110010
00110100
00110110
00110010 //ena mux
00100100 //clk mux
00011000 //>>>pin1 out select (lut or reg)
01000000 //lut1
01001111
01001111
01000000
00110000 //lut input muxes
00110010
00110100
00110110
00110100 //ena mux
00101000 //clk mux
00011000 //>>>pin0 out select (lut or reg)
01000000 //lut0
01000000
01001111
01001111
00110000 //lut input muxes
00110010
00110100
00110110
00110110 //ena mux
00101100 //clk mux
00000000
00000000
00000000
00000000
00000000

cnt8cout

5.4. Сдвиговый регистр.

Значения сигналов на входних пинах мПЛИС объединяются по «ИЛИ» и заносится в младший разряд сдвигового регистра.
Все четыре триггера сдвигового регистра выведены на выходные пины.

Код проекта, который я хочу сделать на Verilog:


wire in;
assign in = ipin[0] | ipin[1] | ipin[2] | ipin[3];
always @(posedge clk)
  out_pin[3:0] <= { out_pin[2:0], in };


Я вручную «скомпилировал» этот код в вот такой файл для своей мПЛИС:

@0
00011000 //config done bit
00011000 //>>>pin3 out select (lut or reg)
01001000 //lut3
01000000
01000000
01000000
00110010 //lut input muxes
00110010
00110010
00110010
00110000 //ena mux
00100000 //clk mux
00011000 //>>>pin2 out select (lut or reg)
01001000 //lut2
01000000
01000000
01000000
00110100 //lut input muxes
00110100
00110100
00110100
00110010 //ena mux
00100100 //clk mux
00011000 //>>>pin1 out select (lut or reg)
01001000 //lut1
01000000
01000000
01000000
00110110 //lut input muxes
00110110
00110110
00110110
00110100 //ena mux
00101000 //clk mux
00011000 //>>>pin0 out select (lut or reg)
01001111 //lut0
01001111
01001111
01001110
00111110 //lut input muxes
00111110
00111110
00111110
00110110 //ena mux
00101100 //clk mux
00000000
00000000
00000000
00000000
00000000
     
При симуляции видим, что сдвиговый регистр работает и реагирует на входные сигналы:

Симуляция сдвигового регистра

Итак, у меня получилось сделать простейшую программируемую «маленькую ПЛИС» из четырех элементов!

Однако все это была теория. А сможет мой код описывающий мПЛИС работать внутри настойщей ПЛИС? Может!

6. Проект мПЛИС для платы Марсоход 

После того, как код был протестирован на симуляторе, я решил испытать его в реальной ПЛИС. Был сделан проект для платы Марсоход. Схема проекта (самый верхний уровень) выглядит вот так:

Проект ПЛИС в ПЛИС

В этом проекте есть модуль моей мПЛИС mini_cpld, модуль ALTUFM_NONE, который хранит образ пользовательского проекта и еще модуль загрузчика, который читает содержимое UFM и таким образом загружается мПЛИС.

Еще несколько замечаний по сигналам проекта.

  • Если проект загрузился, то светодиод на led[7] загорится.
  • led[6] подключен к внутреннему сигналу медленной тактовой частоты - это будет системная тактовая частота для моей мПЛИС. Она "медленная", где-то пара герц, но это исключительно для того, чтобы визуально по светодиодам можно было бы увидеть как работают мои проекты внутри мПЛИС. Конечно, частота может быть подана и большая.
  • выходы ПЛИС led[3:0] подключены к четырем выходам мПЛИС
  • входы ПЛИС key[3:0] подключены к четырем входам мПЛИС
  • выход ldo теоретически можно было бы тут же в проекте соединить проводом с lds, это сигналы обеспечивающие загрузку мПЛИС из UFM. Есть однако здесь какая-то странная странность. Если я соединяю эти два сигнала здесь же в проекте, то компилятор Quartus II каким-то немыслимым образом оптимизирует схему и выбрасывает из нее ВСЕ! Это очень странно. Что бы избежать этой "оптимизации" пришлось замыкать на плате Марсоход эти сигналы джампером снаружи (сигналы F0 и F1 по схеме платы Марсоход).

Вообще-то конечно нужно признать, что этот проект пришелся не по душе компилятору Quartus II. Он выдает массу нехороших предупреждений типа Combinational loops или Gated clocks. Ну да, я все это знаю, но мне для мПЛИС так надо и по другому никак не сделать.

Весь проект можно взять здесь: 

Выше я показывал 4 проектика для мПЛИС - это двоичный счетчик, счетчик по модулю 10, счетчик с сигналом COUT и сдвиговый регистр.

Все эти проекты реально заработали в мПЛИС! Вот демонстрационное видео:

 7. Кому это надо?

Я обещал рассказать, зачем все это может понадобиться.

Конечно, это сугубо мое мнение, мне кажется, что в ближайшем будущем логические элементы ПЛИС станут неотъемлимой частью очень многих микросхем: процессоров, систем на кристалле, image sensors и прочих. Этот процесс уже потихоньку идет. 

Может и нам нужно подумать в эту сторону?

 

 


Добавить комментарий