В предыдущих главах Программное обеспечение создает оборудование была представлена ​​лестница абстракции оборудования, включая физический, структурный и поведенческий уровни. Здесь мы рассмотрим общие шаблоны на поведенческом уровне, которые покажутся удивительно знакомыми пользователям современных асинхронных параллельных сред, таких как NodeJs или asyncio Python.

Поведенческое аппаратное программирование - или, как его называют люди, занимающиеся микросхемами, - аппаратное * описание * - вошло в моду примерно в середине 1980-х годов. Два самых популярных языка описания оборудования (HDL), Verilog и VHDL, были представлены в первые дни HDL. Хотя Verilog и VHDL поддерживают нижний структурный уровень, их новым вкладом стало добавление поведенческих функций, описанных здесь. Общая идея заключается в том, что вместо непосредственного программирования * структурного содержимого * части оборудования - по сути, иерархического списка компонентов и соединений - разработчики могут описать, что делает аппаратный блок * * , на чуть более высоком уровне абстракции.

Почти каждое сложное цифровое оборудование, которое вы используете, было написано либо на Verilog, либо на VHDL. Сюда входят практически все микросхемы, необходимые для чтения этой статьи - ЦП вашего компьютера (или смартфона), сетевой интерфейс, передающий и принимающий этот документ, и графический процессор, отображающий его на вашем экране.

Эти языки в целом напоминают С ++ - ишний уровень абстракции и производительности. Но HDL централизованно объединяют несколько парадигм программирования, которые выглядят, ну, ужасно блестящими и новыми для многих программистов в 2019 году. Поведенческий код HDL, в частности, включает управляемый событиями и реактивный шаблоны, широко используемые в современных асинхронных средах.

Для этого есть веская причина. Оборудование по своей природе работает параллельно; поэтому аппаратные программы должны были описывать параллелизм с первого дня. Что еще хуже, сложное оборудование выполняет тонны операций параллельно - не десятки или сотни, а тысячи или миллионы. Для имитации этого уровня параллелизма обычно требовалось моделировать его с помощью параллелизма. (Если это утверждение кажется противоречивым, ознакомьтесь с популярным выступлением Роба Пайка из Google Параллелизм - это не параллелизм.)

Поскольку все больше программного обеспечения - и все больше * действительно популярного * программного обеспечения - становится все более параллельным, эти шаблоны параллелизма проникают в умы все большего числа инженеров.

Шаблон, управляемый событиями

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

  • Всякое случается
  • Код запускается в ответ

Происходящие «вещи» можно описать как серию событий. В веб-среде типичные события, доступные программисту, могут включать в себя действия пользователя (щелчки, нажатия клавиш), таймеры или завершение медленных асинхронных операций, таких как файловый ввод-вывод или работа в сети. Под поверхностью более естественные события запускают такие вещи, как таймеры и фоновое обслуживание.

Популярные асинхронные фреймворки, такие как NodeJS и asyncio Python (среди действительно популярного программного обеспечения, о котором упоминалось ранее), выполняют асинхронный код в так называемом цикле событий. Код приложения определяет, какие события должны быть добавлены, а фоновый механизм выполнения определяет, когда они должны выполняться, и запускает их по одному. Обратите внимание, что, хотя эти библиотеки могут отображаться для использования параллелизма, в большинстве из них нет. Вместо этого конструкция цикла событий использует параллелизм. NodeJS особенно откровенен в своей однопоточности. Обратите внимание, что для использования Node не требуются никакие блокировки, семафоры или другие признаки параллельного программирования. (Это, вероятно, было важным источником его огромной популярности.)

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

Управляемый событиями шаблон обычно используется для описания последовательной логики, в которой выходы аппаратного модуля являются функциями как текущих входов, так и текущего состояния модуля. Большая часть цифрового оборудования является как последовательным, так и синхронным в том смысле, что его операции запускаются фронтами широко распространенного сигнала синхронизации, обычно называемого часами. Конечные автоматы (FSM) - одни из наиболее распространенных примеров.

module fsm ( /* ... */ ) 
  always @ (posedge clock or negedge reset)
    next_state = state;
    out = 0;
    case (state)
      A : 
        if (in) next_state = C;
        else next_state = B;
      B : 
        if (in) begin
          out = 1;
          next_state = C;
        end
      C : 
        if (~in) begin
          out = 1;
          next_state = B;
        end
      default : begin
          out = 1’bX;
          next_state = 3’bX;
        end
    endcase
  end
endmodule

Даже самые сложные части цифрового кремния обычно сводятся к следующему шаблону: чувствительность плюс соответствующий код обратного вызова. В качестве примера возьмем (отрывок) реализацию ЦП RISC-V с открытым исходным кодом.

module RISCVCPU (clock);        // A RISC-V module excerpt 
 parameter LD = 7'b000_0011,  // Instruction opcodes
  SD = 7'b010_0011, 
  BEQ = 7'b110_0011, 
  NOP = 32'h0000_0013, 
  ALUop = 7'b001_0011; 
 input clock;
 reg [63:0] PC; 
 // ...
integer i; 
 initial begin
  for (i=0; i<=31; i=i+1) Regs[i] = i;
 end
always @(posedge clock) begin
  // Fetch & increment PC
  IFIDIR <= IMemory[PC >> 2];
  PC <= PC + 4;
  IDEXA <= Regs[IFIDrs1]; 
  IDEXB <= Regs[IFIDrs2]; 
  // ...
    begin
      if ((opcode == LD) || (opcode == SD))
      begin
        ALUOut <= A + ImmGen; // compute effective address
        state <= 4;
      end
    else if (opcode == ALUop) begin
      case (IR[31:25]) // case for the various R-type instructions
        0: ALUOut <= A + B; // add operation
        default: ; // other R-type operations
      state <= 4;
    end
    else if (opcode == BEQ) begin
      if (A == B) begin
        PC <= ALUOut; // branch taken--update PC
        state <= 1;
      end
      else
      // ...
    end
 end
 // ...
endmodule

Основной код выполнения инструкции начинается сразу после аннотации always @(posedge clock) и простирается от последующего begin до парного end (ограничителей области видимости Verilog). Это ядро, более или менее, является большим оператором переключения. Разберите тип инструкции, которую нужно выполнить, и запустите ее. Это очень похоже на распространенные исполнители команд виртуальных машин, такие как цикл оценки CPython:

PyObject * PyEval_EvalFrame(PyFrameObject *f) {
    PyObject **stack_pointer;  /* Next free slot in value stack */
    int opcode;        /* Current opcode */
    switch (opcode) {
      case TARGET(BINARY_OR): {
          PyObject *right = POP();
          PyObject *left = TOP();
          PyObject *res = PyNumber_Or(left, right);
          Py_DECREF(left);
          Py_DECREF(right);
          SET_TOP(res);
          if (res == NULL) goto error;
          DISPATCH();
      }
      case TARGET(LIST_APPEND): {
          PyObject *v = POP();
          PyObject *list = PEEK(oparg);
          int err;
          err = PyList_Append(list, v);
          Py_DECREF(v);
          if (err != 0) goto error;
          PREDICT(JUMP_ABSOLUTE);
          DISPATCH();
      }
      // ...

Два времени выполнения

К настоящему времени вам может быть интересно, когда какой-либо из этого кода HDL запускается? В отличие от типичного программного обеспечения, поведенческий код HDL работает (по крайней мере) в двух основных средах выполнения:

  • Логический синтез - это компиляция поведенческого кода HDL в представление более низкого уровня, обычно в логические элементы.
    Считайте это компилятором. Хотя обратите внимание, что язык вывода этого компилятора часто совпадает с его вводом, то есть Verilog. Выходные данные просто используют конструкции более низкого уровня и избегают конструкции более высокого уровня.
  • Моделирование - это среда выполнения, которая предсказывает поведение оборудования.
    Большинство описанных здесь поведенческих конструкций - чувствительность, циклы событий, реактивные обновления и т. д. - применимы к этой среде выполнения моделирования. Модель цифрового моделирования в сочетании с этими описаниями поведения называется дискретным моделированием. Он используется в ряде областей, помимо электроники, но особенно распространен здесь.

Хотя мы кратко коснемся логического синтеза в следующем разделе, посвященном реактивному оборудованию, наше основное внимание уделяется времени выполнения моделирования дискретных событий.

Дискретно-событийное моделирование

Независимо от того, имеют ли эти груды кода Verilog и C много смысла, мы можем довольно быстро ввести время выполнения симуляции дискретных событий, используя более удобный для разработчиков язык. EventSim (https://github.com/HW21/EventSim) - это реализация этих идей на JavaScript с открытым исходным кодом.



События

Во-первых, нам нужно определить, что мы подразумеваем под Events. Для аппаратного моделирования Событие минимально состоит из:

  • (a) task (объект-функция), который должен быть запущен, и
  • (b) Смоделированный time для запуска на

Мы будем использовать ECMAScript 2015+ class, чтобы определить эти Events. На данный момент класс Event не будет иметь никакого поведения, только два члена данных time и task.

class Event {
    /* Event "class" 
     * Mostly a two-tuple or structure of 
     * (a) A `task` (function-object) to be run, and 
     * (b) A simulated `time` to run it at */
    constructor (time, task) {
        this.time = time;
        this.task = task;
    }
}

Модель оборудования

Далее нам понадобится какая-то модель оборудования. Мы не будем выполнять тяжелую работу по синтаксическому анализу существующего HDL - вместо этого мы создадим модель, встроенную в тот же язык, что и наша среда выполнения моделирования: современный Javascript.

Оглядываясь назад на Verilog, который мы уже видели, modules, как правило, состоит из двух категорий:

  • Структурный контент, включая сигналы, порты, параметры и экземпляры других модулей.
  • Процедурные блоки, которые описывают поведение модуля, в коде, который выполняется процедурно от начала до конца. Они очень похожи на функции или методы класса. Аннотации к каждому процедурному блоку (always, initial и др.) Описывают чувствительность, которая заставит их запускаться.

Эти две категории очень похожи на классы объектно-ориентированных языков, таких как Python или C ++, или объектно-ориентированных языков, таких как Javascript. Мы будем использовать такой класс JavaScript для представления образца аппаратного модуля.

class Module {
    /* 
     * A Simple System-Model, 
     * Using the sorts of event-driven constructs common in Verilog or VHDL, 
     * But calling `sim.add_event` instead of using special syntax. 
     */
    constructor () {
        // This "this binding" can go away if using arrow-function-enabled Javascript
        this.kickStart = this.kickStart.bind(this);
        this.keepGoing = this.keepGoing.bind(this);
        
        // Create our Sim object.  This would be "behind the scenes" in a dedicated HDL. 
        this.sim = new Sim();
        this.sim.add_event(new Event(0, this.kickStart));
    }
    kickStart () {
        console.log(this.sim.time.toString() + " KICK STARTING!");
        const e = new Event(11, this.keepGoing);
        this.sim.add_event(e);
        const e2 = new Event(3, this.keepGoing);
        this.sim.add_event(e2);
    }
    keepGoing () {
        console.log(this.sim.time.toString() + " STILL GOING!");
        const e = new Event(this.sim.time + 10, this.keepGoing);
        this.sim.add_event(e);
    }
}

Наш начальный класс аппаратных модулей пропускает многие функции и тонкости выделенного HDL. Нет сигналов, портов или экземпляров модулей (на данный момент). И нет специального синтаксиса для генерации событий. Вместо этого Module вызывает метод add_event для this.sim, его ссылку на время выполнения симуляции.

Время выполнения моделирования

При наличии всей этой основы базовое моделирование во время выполнения оказывается довольно простым. Самая простая симуляция состоит из двух элементов данных:

  • Текущее моделирование time
  • (Своего рода) хранилище будущего events, часто называемое очередью событий

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

  • Во-первых, причинно-следственное ограничение времени выполнения моделирования заключается в том, что события выполняются в неубывающем порядке времени моделирования. События могут быть добавлены в очередь в произвольном порядке, но должны быть выполнены в порядке их времени моделирования. Другими словами, мы можем думать о цикле событий как о выполнении событий по приоритету, а приоритет события определяется минимальным временем.
  • Во-вторых, хотя нам нужно найти событие с минимальным временем, порядок оставшихся событий не имеет значения. В принципе, мы можем поддерживать полностью отсортированный запас будущих событий, но большая часть работы, необходимой для сортировки, тратится впустую. Нас не очень заботит, в порядке ли, скажем, будущие события с 499-м и 500-м приоритетами, особенно зная, что выполнение предыдущих 498 может добавить больше событий до или между ними.

Удобно, что существует хорошо известная, быстрая, широко популярная структура данных именно с такими характеристиками: минимальная куча. (В некоторых введениях в кучу этот вариант использования даже используется в качестве вводного примера.) Практически все популярные языки имеют надежную реализацию, доступную либо во встроенных, либо во широко популярных внешних библиотеках. Для нашей реализации JavaScript мы будем использовать пакет collections, доступный в NPM.

Со всеми этими реализациями класс Sim, реализующий эту среду выполнения, нуждается только в трех простых методах:

  • Конструктор, который инициализирует свои первичные данные-члены.
  • add_event, который, как мы видели, используется Module, добавляет (будущее) событие в очередь
  • run выполняет тяжелую работу - запускает моделирование до указанного времени tstop
var Heap = require("collections/heap");
class Sim {
    /* Discrete-Event Simulation Class */
    constructor () {
        // Our primary attribute is a min-heap, sorted by event-time 
        this.events = new Heap(null, null, function (a, b)  { return b.time - a.time; });
        this.time = 0;
    }
    add_event(e) {
        /* Add new Event `e` */
        this.events.push(e);
    }
    run (tstop) {
        /* Run simulation, up to time `tstop` */
        while(this.events.length) {
            // Grab the next event
            const e = this.events.pop();  
            // If it's after tstop, put it back and bail
            if (e.time > tstop) {
                this.events.push(e);
                break;
            }
            // Run it!
            this.time = e.time;
            e.task(); 
        }
    }
}

Вот и все! Мы готовы создать экземпляр модуля и смоделировать его.

// Create our model instance 
const model = new Model();
const sim = model.sim;
// Run for a while
sim.run(40);
// Pause, look around a second
console.log("PAUSED!");
// Now run for a while longer
sim.run(100);

Запуск сценария EventSim’smain.js, который включает весь этот код, генерирует вывод в следующих строках:

yarn run v1.15.2
$ node --use_strict main.js
0 KICK STARTING!
3 STILL GOING!
11 STILL GOING!
13 STILL GOING!
21 STILL GOING!
23 STILL GOING!
31 STILL GOING!
33 STILL GOING!
PAUSED!
41 STILL GOING!
43 STILL GOING!
51 STILL GOING!
53 STILL GOING!
61 STILL GOING!
63 STILL GOING!
71 STILL GOING!
73 STILL GOING!
81 STILL GOING!
83 STILL GOING!
91 STILL GOING!
93 STILL GOING!
✨  Done in 0.12s.

Реактивный паттерн

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

a = 1
b = 2
c = a + b
c
## 3

Императивный и реактивный стили различаются при изменении значений a или b - после присвоения c. В типичной императивной парадигме значение c не меняется:

a = 11
c
## 3

Оператор императивного присваивания c = a + b относится к значениям a и b, сейчас - во время присваивания.

В реактивном шаблоне присвоение означает нечто совершенно иное - по сути, c должно быть равным сумме a и b, навсегда.

a = 1
b = 2
c = a + b
c
## 3
a = 11
b = 22
c
## 33

Это совсем другое представление о том, что означает назначение. Отображение реактивного шаблона на типичные императивные языки требует (каким-то образом) обновления значения c «в фоновом режиме» всякий раз, когда его зависимости изменяют значение. Для этого требуется что-то вроде графа зависимостей между переменными и набор методов обновления, вызываемых при изменении значения входной переменной. Удобно, что эти процедуры обновления хорошо подходят для запуска из чего-то вроде цикла событий, представленного в нашем предыдущем разделе.

Популярные реализации реактивного паттерна включают Observable Notebook, в котором используется JavaScript для сочетания веб-программирования и визуализации. Возможно, наименее любимые всеми реактивными программами являются формулы электронных таблиц Excel. Данные могут изменяться даже после ввода формул; собственно говоря, это вообще суть.

Реактивное оборудование

Хотя для реализации реактивного шаблона в императивных языках требуется множество фоновых машин, он очень четко отображается на оборудование. Спаривание настолько естественное, что реактивное назначение встроено непосредственно в самые популярные HDL. Verilog использует ключевое слово assign, чтобы сделать реактивные, или, на его жаргоне, непрерывные назначения.

module combinational (  
  input a, b, c, d, output  o);
 
  assign o = ~((a & b) | c ^ d);
endmodule

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

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

  • программа ввода для компиляции, и
  • целевая среда для ее компиляции

Для типичного программного компилятора целевой средой обычно является базовая платформа: инструкции ARM или x86, байт-код виртуальной машины Java или Python. Для аппаратного синтеза, напротив, целевая среда содержит библиотеку логических ячеек, обычно предназначенную для конкретной технологии процесса. В отличие от программного компилятора, он часто также включает набор ограничений реализации, например, по целевой скорости или размеру.

Реактивные назначения HDL позволяют компилировать независимое от цели описание аппаратной «программы». Мы можем представить себе модуль combinational, который можно использовать в TSMC 16 нм FinFET, в устаревшем 1 мкм и во всех промежуточных технологических процессах.

Хотя реактивный паттерн является мощным для этого независимого от платформы описания комбинационной логики, он остается довольно низкоуровневым поведенческим описанием. Реактивные назначения могут фиксировать комбинационные отношения между сигналами оборудования, но не имеют средств включения состояния или истории системы. Здесь обычно и появляются описания, основанные на событиях.

Описание оборудования «высокого уровня»

Стили аппаратного программирования, которые мы представили до сих пор, в целом напоминают схожий с C ++ уровень абстракции и производительности. Наиболее сложные из этих средств (поведенческие процедуры, платформенно-независимые модули) были введены с основными отраслевыми HDL (Verilog и VHDL) в середине 1980-х годов. В 2019 году почти каждое сложное цифровое оборудование по-прежнему написано на Verilog или VHDL. За более чем три десятилетия их жизни в аппаратном сообществе так и не появилось единого мнения о том, как повысить уровень продуктивности проектировщиков до уровня современных языков программирования, ориентированных на программистов.

Первоначальные и, возможно, все еще наиболее распространенные попытки используют набор языков сценариев (Perl, Bash или TCL) для генерации Verilog или VHDL. (Особенно популярный и SAD! такой карточный домик построен из макросов emacs.)

Совсем недавно появились два конкурирующих подхода к повышению производительности оборудования. Первый, обычно называемый синтезом высокого уровня (HLS), относится к набору инструментов и методов, которые пытаются описать аппаратные функциональные возможности в программном обеспечении относительно низкого уровня. язык, такой как C ++, удалив все аппаратные абстракции, такие как модули, порты, сигналы и провода. HLS значительно повышает уровень абстракции оборудования - фактически полностью удаляя его - без существенного изменения уровня абстракции языка описания.

Более новый и контрастный подход, в основном заимствованный из академических кругов, использует современные библиотеки описания оборудования, построенные на основе существующих языков программирования. Эти современные HDL почти повсеместно избегают HLS и во многих случаях отказываются от поведенческой парадигмы, управляемой событиями. Вместо этого современные HDL сосредоточены на использовании средств высокопроизводительного языка программирования для управления структурными описаниями оборудования, возможно, включая реактивные, непрерывные назначения в нижней части спектра поведенческого моделирования оборудования.

На сегодняшний день наиболее совершенным современным HDL является Chisel, разработанный в Калифорнийском университете в Беркли. Chisel встроен в Scala и использует комбинацию объектно-ориентированного и функционального программирования для создания оборудования структурного уровня.

import chisel3._
class GCD extends Module {
  val io = IO(new Bundle {
    val a  = Input(UInt(32.W))
    val b  = Input(UInt(32.W))
    val e  = Input(Bool())
    val z  = Output(UInt(32.W))
    val v  = Output(Bool())
  })
  val x = Reg(UInt(32.W))
  val y = Reg(UInt(32.W))
  when (x > y)   { x := x -% y }
  .otherwise     { y := y -% x }
  when (io.e) { x := io.a; y := io.b }
  io.z := x
  io.v := y === 0.U
}

Сообщается, что Chisel использовался в Google при разработке своего чипа Edge TPU. Другие современные HDL включают MiGen, PyMtl Корнелла, Magma Стэнфорда, все основанные на Python, и SpinalHDL, основанные на Scala. Все они еще раньше, и их коммерческое развертывание было незначительным.

Должен ли я заботиться?

У немногих разработчиков программного обеспечения будет веская причина изучать Verilog или VHDL. (Надеюсь, что скоро у немногих инженеров по аппаратному обеспечению также будет причина изучать их.) Но несколько тенденций делают программирование оборудования актуальным для более широких кругов программного сообщества.

Облачные сервисы делают развертывание и использование специального оборудования более доступным. У каждого из крупных поставщиков облачных услуг уже есть специализированное аппаратное решение для ускорения машинного обучения. Эти трое используют совершенно разные подходы к управлению своим оборудованием. Инстансы F1 от Amazon представляют собой наиболее гибкий пример, предлагая практически полную настройку инстансов FPGA, как правило, в Verilog. Появление HLS и современных HDL сделает ускорители, работающие на этих экземплярах, гораздо более доступными для разработки и использования.

Меняется и универсальное оборудование. Растущая популярность архитектуры набора команд RISC-V, также разработанная Калифорнийским университетом в Беркли, указывает на будущее, в котором самая популярная облачная инфраструктура будет с открытым исходным кодом - вплоть до транзисторов. Такое будущее с открытой инфраструктурой потребует вклада инженеров в области аппаратного и программного обеспечения, в том числе для всех поддерживающих программных библиотек, инструментов и языков.

Программное обеспечение создает оборудование - это подробное описание того, как проектируются кремний и электроника, для тех, кто склонен к разработке программного обеспечения. (Оказывается, мы все-таки не такие уж разные.)

Если вы относитесь к числу тех, кто склонен к разработке программного обеспечения, мы будем рады вашим отзывам о том, что здесь имеет смысл, а что нет. Электронное письмо [email protected], ответ на Medium или комментарий в Twitter.

Первоначально опубликовано на https://fritch.mn