E7 Простая шина и SoC

Вы уже спроектировали процессор NPC и понимаете, как работают устройства.

Однако раньше мы позволяли среде симуляции предоставлять поведенческие модели устройств. В реальном железе контроллеры устройств — это RTL-модули, и процессор должен взаимодействовать с ними через чисто аппаратный механизм.

Этот механизм называется шиной.

Базовые понятия о шине

Суть шины — это протокол взаимодействия. Он может существовать в коммуникации между различными модулями.

Например, когда процессор обращается к памяти, между ними также есть протокол взаимодействия.

+-----+         +-----+
| CPU | <-----> | MEM |
+-----+         +-----+

Обычно модуль, который активно инициирует взаимодействие, называется ведущим устройством (master), а модуль, который отвечает на взаимодействие, — ведомым устройством (slave). Например, в примере выше процессор — это ведущее устройство (master), а память — ведомое устройство (slave).

На самом деле у проектирования шины есть два уровня смысла:

  • На уровне протокола master и slave должны договориться о том, «как они взаимодействуют друг с другом».
  • На уровне реализации, проектируя интерфейсы между master и slave, нам нужно с помощью схем реализовать детали, определённые протоколом.

Системная шина

Шина, соединяющая процессор с памятью и устройствами, обычно называется системной шиной. На самом деле мы ранее использовали DPI-C, чтобы симулировать функциональность системной шины. NPC обращался к памяти через DPI-C и такие интерфейсы, как pmem_read(), предоставляемые средой симуляции. В этом процессе не было задержки чтения: прочитанные данные могли быть возвращены в том же такте, в котором был получен запрос на чтение. Однако это лишь удобный способ реализации взаимодействия в NPC. В реальности такого устройства памяти не существует.

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

  1. Процессору нужно определять, когда становятся доступны данные, возвращённые памятью.
  2. Пока ответ памяти не пришёл, процессор должен ждать. Процессор может продолжить выполнение текущей инструкции только после получения возвращённых данных. Это означает, что процессор больше не может сохранять свойство выполнения одной инструкции за такт.
  3. Кроме того, выборка инструкции не обязана происходить каждый такт. Например, пока процессор ожидает ответа памяти, он не должен выполнять выборку инструкции. Поэтому процессору нужно уведомлять память о том, когда действительно должна происходить операция выборки инструкции.

Однако реальные чипы обычно используют промышленные протоколы шины, похожие на AXI, детали которых очень сложны. Например, протокол шины AXI в общей сложности содержит более 30 сигналов. Это означает, что в принципе, если мы хотим, чтобы реальный чип взаимодействовал с устройствами, нам нужно реализовать эти протоколы шины внутри процессора. Чтобы снизить нагрузку реализации, мы сначала представим простой собственный протокол шины под названием SimpleBus.

Прежде чем реализовывать шину, мы сначала кратко обсудим оценку производительности процессора.

Измерьте IPC процессора

IPC (Instruction Per Cycle) обозначает среднее число инструкций, выполняемых процессором за каждый такт. Он отражает способность процессора выполнять инструкции и является важной метрикой для оценки производительности процессора.

Чтобы измерить IPC, вам сначала нужно измерить:

  • число тактов, затраченных на выполнение программы процессором;
  • число инструкций, выполненных за время выполнения программы.

Получив эти два значения, просто разделите число выполненных инструкций на число тактов. Для простоты вы можете использовать для измерения программу hello.

Сейчас IPC должен быть равен 1. Процессор, который выполняет одну инструкцию за каждый такт, называется однотактным процессором. Вы измерите IPC снова после реализации шины.

Обращение к памяти только для чтения

Поскольку чтение данных из памяти — самое базовое требование, мы сначала рассмотрим, как процессор выполняет операции чтения через системную шину.

Предположим, что процессор имеет фиксированную спецификацию N*32, то есть память содержит N адресуемых 32-битных ячеек. Также предположим, что задержка чтения этой памяти фиксирована и равна 1 такту. По сути это синхронная память, в которой задержка между получением запроса на чтение и возвратом данных фиксирована. Такое поведение соответствует характеристикам доступа SRAM.

Если не учитывать операции записи, нам нужна только ROM (постоянная память только для чтения). Чтобы прочитать данные из ROM, соответствующей шине нужно всего два сигнала: адрес и данные. Их разрядности — log2(N) и 32 соответственно.

+-----+ raddr[log2(N)-1:0] ---> +-----+
| CPU | <---        rdata[31:0] | MEM |
+-----+                         +-----+

Протокол взаимодействия следующий:

  • Master (CPU) отправляет адрес чтения raddr slave (MEM).
  • В следующем такте slave возвращает данные rdata master.
  • Описанное выше поведение происходит каждый такт.

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

output [31:0] ifu_raddr,
input  [31:0] ifu_rdata,

Тайминг этих сигналов показан на следующей диаграмме:

           --\ /----------\ /----------\ /----------\ /----------\ /--
 ifu_raddr    X 0x80000000 X     (1)    X 0x80000004 X            X
           --/ \----------/ \----------/ \----------/ \----------/ \--
           --\ /----------\ /----------\ /----------\ /----------\ /--
 ifu_rdata    X            X 0x00000413 X     (2)    X 0x80051137 X
           --/ \----------/ \----------/ \----------/ \----------/ \--

Чтобы позволить NPC «ждать инструкции, возвращаемые памятью», нам сначала нужно, чтобы IFU знал, на какой стадии процесса выборки инструкции он сейчас находится, и предпринимал разные действия на разных стадиях. Такое поведение, при котором в разное время выполняются разные действия, можно реализовать в цифровых схемах с помощью автомата состояний (FSM).

А именно, мы можем реализовать для IFU два состояния:

  • Состояние idle: установить ifu_raddr равным pc. Затем перейти в состояние wait.
  • Состояние wait: использовать ifu_rdata как действительную инструкцию и продолжить выполнение. Затем вернуться в состояние idle.

Согласно описанному выше протоколу шины, поскольку взаимодействие происходит каждый такт, память возвращает данные, соответствующие запросу предыдущего такта, одновременно с получением запроса адреса в текущем такте. Например, на рисунке выше, когда NPC отправляет запрос на выборку по адресу 0x80000004, память одновременно возвращает данные, отмеченные как (2). Эти данные соответствуют запросу адреса, отмеченному как (1) в предыдущем такте. Если NPC воспримет (2) как действительную инструкцию и сразу же выполнит её, он либо снова выполнит инструкцию по адресу 0x80000000, либо выполнит инструкцию по другому адресу. В любом случае поведение NPC перестанет соответствовать спецификации ISA. Поэтому в состоянии idle NPC не должен воспринимать ifu_rdata как действительную инструкцию. Вместо этого он не должен выполнять никакую инструкцию.

Ранее мы предполагали, что NPC может выполнять одну инструкцию за каждый такт. Однако после введения SimpleBus у NPC нет действительной инструкции для выполнения, пока он ожидает ответа на запрос инструкции. Поэтому нам нужно рассмотреть вопрос: «Как заставить NPC прекратить выполнение инструкций?» Вспомните модель автомата состояний компьютерных систем: процесс выполнения инструкции по сути является процессом изменения состояния процессора. Другими словами, если мы можем сохранить состояние процессора неизменным, мы можем добиться эффекта «невыполнения инструкции». Вы наверняка уже догадались о решении: состояние процессора хранится в последовательностных логических элементах. Пока их сигналы разрешения записи отключены, значения этих последовательностных логических элементов не изменятся. Поэтому вам также нужно правильно настроить сигналы разрешения записи различных последовательностных логических элементов в NPC в соответствующих состояниях.

Реализуйте поддержку SimpleBus в IFU

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

always @(posedge clock) begin
  ifu_rdata <= pmem_read(ifu_raddr);
end

Для части LSU, отвечающей за доступ к данным, пока изменения не требуются. Мы сделаем так, чтобы она поддерживала SimpleBus, позже.

После реализации попробуйте запустить несколько тестовых программ. Одновременно изучите временную диаграмму, чтобы убедиться, что процесс взаимодействия между NPC и памятью соответствует ожидаемому поведению. В принципе, протокол шины должен быть прозрачен для программ верхнего уровня. Поэтому программы, которые успешно запускались раньше, должны продолжать успешно запускаться и после реализации SimpleBus.

Однако, поскольку теперь памяти требуется один такт, чтобы вернуть данные инструкции, NPC больше не является строго однотактным процессором. Вместо этого он становится простым многотактным процессором:

  1. В первом такте IFU выдаёт запрос на выборку инструкции.
  2. Во втором такте IFU получает инструкцию и передаёт её следующим модулям для декодирования и выполнения.

Адаптируйте DiffTest под многотактный процессор

После превращения NPC в многотактный процессор NPC больше не выполняет одну инструкцию каждый такт. Чтобы механизм DiffTest работал корректно, вам нужно немного скорректировать тайминг процесса проверки: DiffTest следует выполнять только тогда, когда инструкция завершила выполнение. Для этого вам может понадобиться считывать через DPI-C некоторые сигналы статуса из RTL, чтобы понять, когда следует запускать DiffTest.

Измерьте производительность процессора

После того как IFU станет поддерживать SimpleBus, выполните следующие задания:

  1. Заново измерьте IPC программы hello.
  2. Оцените производительность процессора с помощью archbench.

Обращение к памяти для чтения и записи

Поскольку LSU нужно выполнять инструкции store, нам нужно добавить в шину новые сигналы для поддержки операций записи:

  • Во-первых, нам, естественно, нужны адрес записи waddr и данные записи wdata.

  • Поскольку операции записи происходят не каждый такт, нам также нужно добавить сигнал разрешения записи wen.

    • Хотя операции чтения тоже происходят не каждый такт (например, в описанном выше многотактном NPC в состоянии wait запрос на выборку инструкции не отправляется), разрешение чтения теоретически не требуется, поскольку операции чтения не изменяют состояние схемы.
    • Однако на практике сигнал разрешения чтения ren обычно всё же включают. Если запроса на чтение нет, памяти не нужно выполнять операцию чтения, что может снизить энергопотребление.
  • Операция записи может изменять лишь некоторые байты внутри слова (например, инструкция sb пишет только 1 байт). Поэтому нам также нужно добавить сигнал маски записи wmask, который определяет, какие байты в записываемых данных нужно записать.

+-----+ addr[log2(N)-1:0]  ---> +-----+
|     | wen                ---> |     |
| CPU | wdata[31:0]        ---> | MEM |
|     | wmask[3:0]         ---> |     |
|     | <---        rdata[31:0] |     |
+-----+                         +-----+

Поскольку LSU не будет одновременно выполнять инструкции load и store, raddr и waddr никогда не будут использоваться одновременно. Поэтому мы можем объединить raddr и waddr в один сигнал addr.

Одновременно протоколу взаимодействия нужно определить поведение операций записи. Опишем это псевдокодом:

if (wen) {
  // wmask_full is the bit-expanded version of wmask
  M[waddr] = (wdata & wmask_full) | M[waddr] & ~wmask_full;
}

Аналогично IFU, мы также сделаем так, чтобы LSU обращался к памяти согласно протоколу SimpleBus. Для LSU, поскольку инструкциям store нужно записывать данные в память, SimpleBus включает следующие сигналы:

output [31:0] lsu_addr,
output        lsu_wen,
output [31:0] lsu_wdata,
output [ 3:0] lsu_wmask,
input  [31:0] lsu_rdata,

Для операций чтения wdata и wmask могут принимать произвольные значения. Тайминг соответствующих сигналов показан на следующем рисунке:

           --\ /----------\ /------------------------
 lsu_addr     X 0x80400000 X
           --/ \----------/ \------------------------
           ---+            +-------------------------
 lsu_wen      |            |
              +------------+
           ------------------------------------------
 lsu_wdata
           ------------------------------------------
           ------------------------------------------
 lsu_wmask
           ------------------------------------------
           ---------------\ /----------\ /-----------
 lsu_rdata                 X 0x12345678 X
           ---------------/ \----------/ \-----------

Для операций записи rdata — безразличный сигнал. Тайминг соответствующих сигналов показан на следующем рисунке:

           --\ /----------\ /------------------------
 lsu_addr     X 0x80400000 X
           --/ \----------/ \------------------------
              +------------+
 lsu_wen      |            |
           ---+            +-------------------------
           --\ /----------\ /------------------------
 lsu_wdata    X 0x12345678 X
           --/ \----------/ \------------------------
           --\ /----------\ /------------------------
 lsu_wmask    X     0xf    X
           --/ \----------/ \------------------------
           ------------------------------------------
 lsu_rdata
           ------------------------------------------

Реализуйте поддержку SimpleBus в LSU

Согласно обсуждению выше, сделайте так, чтобы LSU поддерживал протокол SimpleBus.

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

always @(posedge clock) begin
  lsu_rdata <= (!lsu_wen) ? pmem_read(lsu_addr) : 32'b0;
  if (lsu_wen) begin
    pmem_write(lsu_addr, lsu_wdata, lsu_wmask);
  end
end

Пока вы можете оставить функциональность обращения к устройствам внутри pmem_read()/pmem_write(). Мы расскажем, как обращаться к периферии через шину, на более поздних лекциях.

После того как LSU станет поддерживать SimpleBus, инструкции load требуется 3 такта для завершения на NPC:

  1. В первом такте IFU выдаёт запрос на выборку инструкции.
  2. Во втором такте IFU получает инструкцию и передаёт её следующим модулям для декодирования. Обнаружив, что это инструкция load, он выдаёт запрос обращения к памяти через LSU.
  3. В третьем такте LSU получает данные и передаёт их WBU для записи обратно в регистр.

Поэтому вам также нужно изменить IFU так, чтобы он выбирал следующую инструкцию только после того, как инструкция load завершит выполнение. После реализации попробуйте запустить несколько тестовых программ. Одновременно изучите временную диаграмму, чтобы убедиться, что процесс взаимодействия между NPC и памятью соответствует ожидаемому поведению. Аналогично, программы, которые успешно запускались раньше, должны продолжать успешно запускаться и после реализации SimpleBus.

Измерьте производительность процессора (2)

После того как LSU станет поддерживать SimpleBus, выполните следующие задания:

  1. Заново измерьте IPC программы hello.
  2. Оцените производительность процессора с помощью archbench.

Более общая память

На самом деле свойство фиксированной задержки чтения в один такт обычно может обеспечить только SRAM. Это потому, что SRAM можно изготавливать по тому же технологическому процессу, что и процессор. Однако SRAM очень дорога. Чтобы построить более дешёвую память, обычно используют другие технологии с более высокой плотностью хранения, такие как DRAM. Из-за их электрических характеристик задержка чтения таких видов памяти обычно больше, чем однотактная задержка процессора.

В такой ситуации процессор не может непрерывно отправлять запросы на чтение. Иначе, поскольку скорость запросов процессора выше скорости обслуживания памяти, память будет постоянно занята ненужными запросами, что значительно снизит эффективность всей системы. Чтобы решить эту проблему, процессору нужно сообщать памяти, когда запрос действителен. Поэтому мы можем добавить новый сигнал reqValid к сигналам, отправляемым процессором в память. Память может использовать этот сигнал, чтобы определить, когда есть настоящий запрос. Когда процессору нужно обратиться к памяти, он устанавливает addr и другие соответствующие сигналы и устанавливает reqValid в действительное значение. Когда процессору не нужно обращаться к памяти, он устанавливает reqValid в недействительное значение. Для памяти: Когда reqValid действителен, ей нужно обратиться к данным, соответствующим addr. Когда reqValid недействителен, память не выполняет никакой операции обращения.

С другой стороны, заранее нельзя определить, когда память сможет вернуть данные. Например, DRAM периодически обновляет конденсаторы в своих ячейках хранения. Если запрос на чтение приходит во время процесса обновления, фактическая операция чтения данных произойдёт только после завершения обновления. Поэтому памяти также нужно сообщать процессору, когда можно вернуть действительные данные. Аналогично, чтобы определить, когда приходит ответ памяти, мы можем добавить новый сигнал respValid к сигналам, получаемым процессором от памяти. Процессор может использовать этот сигнал, чтобы определить, когда ответ памяти действителен.

Возьмём операцию чтения в качестве примера: Когда память прочитала данные, она устанавливает rdata и активирует respValid. Когда память ещё не закончила чтение данных, она деактивирует respValid.

Для процессора: Когда respValid действителен, он считает, что rdata содержит действительные данные. Когда respValid недействителен, процессор считает, что данные ещё не вернулись, и продолжает ждать.

+-----+ reqValid           ---> +-----+
|     | addr[log2(N)-1:0]  ---> |     |
|     | wen                ---> |     |
| CPU | wdata[31:0]        ---> | MEM |
|     | wmask[3:0]         ---> |     |
|     | <---          respValid |     |
|     | <---        rdata[31:0] |     |
+-----+                         +-----+

Возьмём LSU в качестве примера: расширенный SimpleBus включает следующие сигналы:

output        lsu_reqValid,
output [31:0] lsu_addr,
output        lsu_wen,
output [31:0] lsu_wdata,
output [ 3:0] lsu_wmask,
input         lsu_respValid,
input  [31:0] lsu_rdata,

Для операций чтения wdata и wmask могут принимать произвольные значения. Тайминг соответствующих сигналов показан на следующем рисунке:

                  +------------+
 lsu_reqValid     |            |
               ---+            +-------------------------------------------------
               --\ /----------\ /------------------------------------------------
 lsu_addr         X 0x80400000 X
               --/ \----------/ \------------------------------------------------
               --\              /------------------------------------------------
 lsu_wen          X            X
               --/ ------------ \------------------------------------------------
               ------------------------------------------------------------------
 lsu_wdata
               ------------------------------------------------------------------
               ------------------------------------------------------------------
 lsu_wmask
               ------------------------------------------------------------------
                                                       +------------+
 lsu_respValid                                         |            |
               ----------------------------------------+            +------------
               ---------------------------------------\ /----------\ /-----------
 lsu_rdata                                             X 0x12345678 X
               ---------------------------------------/ \----------/ \-----------

Для операций записи, хотя rdata — безразличный сигнал, SimpleBus всё равно использует respValid, чтобы указать на завершение операции записи. Тайминг соответствующих сигналов показан на следующем рисунке:

                  +------------+
 lsu_reqValid     |            |
               ---+            +-------------------------------------------------
               --\ /----------\ /------------------------------------------------
 lsu_addr         X 0x80400000 X
               --/ \----------/ \------------------------------------------------
               --\ ------------ /------------------------------------------------
 lsu_wen          X            X
               --/              \------------------------------------------------
               --\ /----------\ /------------------------------------------------
 lsu_wdata        X 0x12345678 X
               --/ \----------/ \------------------------------------------------
               --\ /----------\ /------------------------------------------------
 lsu_wmask        X     0xf    X
               --/ \----------/ \------------------------------------------------
                                                       +------------+
 lsu_respValid                                         |            |
               ----------------------------------------+            +------------
               ------------------------------------------------------------------
 lsu_rdata
               ------------------------------------------------------------------

После добавления сигналов valid в SimpleBus нам также нужно расширить автомат состояний:

  • В состоянии idle: Если сейчас требуется обращение к памяти, установить reqValid, addr и другие соответствующие сигналы, затем перейти в состояние wait. Если обращение к памяти не требуется, остаться в состоянии idle.

  • В состоянии wait: Если respValid действителен, использовать rdata как возвращённые данные и продолжить выполнение, затем вернуться в состояние idle. Если respValid недействителен, остаться в состоянии wait и ждать ответа.

  • Описанный выше процесс описывает операцию чтения. Процесс для операции записи аналогичен.

Реализуйте протокол SimpleBus с сигналами valid

Согласно обсуждению выше, сделайте так, чтобы IFU и LSU обращались к памяти согласно протоколу SimpleBus с сигналами valid. Для части памяти, отвечающей за доступ к данным, вы можете обратиться к следующему коду:

always @(posedge clock) begin
  lsu_rdata <= (lsu_reqValid && !lsu_wen) ? pmem_read(lsu_addr) : 32'b0;
  if (lsu_reqValid && lsu_wen) begin
    pmem_write(lsu_addr, lsu_wdata, lsu_wmask);
  end
  lsu_respValid <= lsu_reqValid;
end

Часть памяти, отвечающую за выборку инструкции, можно изменить аналогичным образом.

После реализации попробуйте запустить несколько тестовых программ. Одновременно изучите временную диаграмму, чтобы убедиться, что процесс взаимодействия между NPC и памятью соответствует ожидаемому поведению. Аналогично, программы, которые успешно запускались раньше, должны продолжать успешно запускаться и после расширения SimpleBus.

Измерьте производительность процессора (3)

После реализации протокола SimpleBus с сигналами valid выполните следующие задания:

  1. Заново измерьте IPC программы hello.
  2. Оцените производительность процессора с помощью archbench.

Протестируйте реализацию SimpleBus

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

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

  1. Измените задержку обращения к памяти на фиксированные значения, такие как 5, 10 и 20 тактов.
  2. Добавьте в модуль памяти LFSR и используйте сгенерированные псевдослучайные числа, чтобы определять задержку текущего запроса.
  3. Добавьте LFSR также в IFU и LSU и используйте их для определения задержки соответствующих сигналов valid.

Если NPC по-прежнему может корректно выполнять программы при случайных задержках, генерируемых несколькими LFSR, это значительно повысит вашу уверенность в корректности вашей реализации.

Интеграция с SoC

Вы уже реализовали протокол шины SimpleBus в NPC, но функциональность устройств по-прежнему предоставляется поведенческими моделями в среде симуляции.

С SimpleBus мы можем подключить NPC к SoC и взаимодействовать с настоящими контроллерами устройств.

Получите исходный код ysyxSoC

Вам нужно клонировать проект ysyxSoCоткрыть в новом окне:

cd ysyx-workbench
bash init.sh ysyxSoC

Обратите внимание, что ysyxSoC всё ещё несколько отличается от итогового SoC, используемого для тейп-аута. Поэтому прохождение тестов в ysyxSoC не означает, что проект также пройдёт тесты в итоговой среде симуляции SoC для тейп-аута. Однако, даже несмотря на это, проект ysyxSoC может помочь заранее выявить некоторые потенциальные проблемы.

Проект ysyxSoC содержит множество деталей. Подробнее о темах, связанных с SoC, вы узнаете в дальнейшем обучении. Пока мы подготовили упрощённую версию кода SoC. Архитектурная диаграмма этого SoC показана ниже (некоторые устройства опущены):

                                                        +------+
+-------+                                           +-> | UART |
| +---+ |     +--------+                            |   +------+
| |IFU| | <-> |        |     +-----+     +------+   |
| +---+ |     | Memory |     | AXI |     | APB  | <-+   +-----+     +-------+
|  CPU  |     |        | <-> | to  | <-> |      | <---> | SPI | <-> | Flash |
| +---+ |     | Bridge | AXI | APB | APB | XBar | <-+   +-----+     +-------+
| |LSU| | <-> |        |     +-----+     +------+   |
| +---+ |     +--------+                            |   +-------+
+-------+                                           +-> | PSRAM |
       SimpleBus                                        +-------+
                                                        +------+
+-------+                                           +-> | UART |
| +---+ |     +--------+                            |   +------+
| |IFU| | <-> |        |     +-----+     +------+   |
| +---+ |     | Память |     | AXI |     | APB  | <-+   +-----+     +-------+
|  CPU  |     |        | <-> |  в  | <-> |      | <---> | SPI | <-> | Flash |
| +---+ |     |  Мост  | AXI | APB | APB | XBar | <-+   +-----+     +-------+
| |LSU| | <-> |        |     +-----+     +------+   |
| +---+ |     +--------+                            |   +-------+
+-------+                                           +-> | PSRAM |
       SimpleBus                                        +-------+

Этот упрощённый код SoC также содержит модуль моста, преобразующий SimpleBus в AXI.

С помощью этого моста NPC можно подключить к SoC с интерфейсом AXI и взаимодействовать с различными устройствами.

На этом этапе вам не нужно понимать детали AXI, APB или различных устройств.

Текущий SoC содержит следующие ключевые устройства (некоторые устройства опущены):

УстройствоАдресное пространство
UART165500x1000_0000 ~ 0x1000_0fff
SPI master0x1000_1000 ~ 0x1000_1fff
Flash0x3000_0000 ~ 0x3fff_ffff
PSRAM0x8000_0000 ~ 0x807f_ffff
ЗарезервированоОстальное

Интегрируйте с ysyxSoC

Выполните следующие шаги, чтобы интегрировать NPC в ysyxSoC:

  1. Скорректируйте интерфейс верхнего уровня NPC так, чтобы он точно соответствовал соглашениям об именовании интерфейса, указанным в ysyxSoC/ready-to-run/minirv/cpu-interface.md, включая направления сигналов, имена и разрядности данных.

    • Сигнал io_lsu_size используется для взаимодействия с периферией. Его должен устанавливать LSU в соответствии с разрядностью данных инструкции обращения к памяти:

      • Устанавливать в 2'b00 для обращений в 1 байт.
      • Устанавливать в 2'b01 для обращений в 2 байта.
      • Устанавливать в 2'b10 для обращений в 4 байта.
  2. Измените значение сброса регистра PC у NPC на 0x3000_0000, чтобы после сброса выборка инструкции начиналась из Flash.

  3. Добавьте все файлы .v и .sv из каталога ysyxSoC/perip и его подкаталогов в список файлов Verilog для Verilator.

  4. Добавьте следующие два каталога в путь поиска включаемых файлов (include) Verilator:

    • ysyxSoC/perip/uart16550/rtl

    • ysyxSoC/perip/spi/rtl

    • За конкретным способом обратитесь к RTFM (man verilator или официальное руководство Verilator).

      • Если вы никогда раньше не просматривали доступные опции Verilator, на этот раз мы рекомендуем внимательно прочитать раздел argument summary в руководстве. Вы можете обнаружить полезные опции, с которыми раньше не сталкивались.
  5. Добавьте следующие опции к опциям компиляции Verilator:

    • --timescale "1ns/1ns"
    • --no-timing
    • -D__VERILOG__
    • -DPDK_BEHAV
    • -D__UART_TO_CONSOLE__
  6. Добавьте ysyxSoC/ready-to-run/minirv/ElaborateTop.v в список файлов Verilog для Verilator.

  7. Модуль SimTop содержит несколько портов, которые в среде симуляции нужно обработать следующим образом:

    • Во время сброса сигнал reset должен оставаться активным не менее 100 тактов.

    • Что касается тактовых сигналов clock и cpuClock: первый используется для тактирования SoC, а второй — для тактирования NPC. Пока вам следует подавать на оба одинаковый вход, например:

      void single_cycle() {
        top->clock = 0; top->cpuClock = 0; top->eval();
        top->clock = 1; top->cpuClock = 1; top->eval();
      }
      
    • Остальные сигналы портов пока можно игнорировать.

  8. Задайте модуль SimTop (определён в ysyxSoC/ready-to-run/minirv/ElaborateTop.v) как модуль верхнего уровня для симуляции в Verilator.

  9. Измените имя модуля ysyx_00000000 в ysyxSoC/ready-to-run/minirv/ElaborateTop.v на имя модуля вашего процессора.

  10. Добавьте следующий код в файл симуляции C++, чтобы решить ошибку компоновки, вызванную отсутствующей функцией flash_read:

extern "C" void flash_read(int32_t addr, int32_t *data) { assert(0); }
  1. Скомпилируйте исполняемый файл симуляции с помощью Verilator.

    • Если вы столкнётесь с ошибкой комбинационной петли, соответствующим образом измените свой RTL-код.
  2. Запустите симуляцию. Вы должны увидеть, что код вызывает ошибку assert(0) внутри flash_read(). Эту проблему мы решим далее.

    • Если эта ошибка не срабатывает, проверьте вашу реализацию шины.

Запуск первой программы на SoC

Для ysyxSoC мы предоставляем программу hello, расположенную по пути: ysyxSoC/ready-to-run/minirv/hello-minirv-ysyxsoc.bin

Запустите программу hello на ysyxSoC

Чтобы запустить эту программу hello, вам нужно, чтобы среда симуляции загрузила программу в память Flash. В частности, вам нужно определить в файле симуляции C++ 16-мегабайтный массив для моделирования памяти Flash, а затем реализовать функцию flash_read(). По адресу addr эта функция должна возвращать соответствующие данные из памяти Flash. Затем загрузите .bin-файл, который нужно выполнить, в массив памяти Flash, чтобы NPC мог выбрать первую инструкцию программы из Flash.

Используя описанный выше способ, заставьте NPC запустить программу hello. Эта программа выполняется около 30 секунд. Если ваша реализация верна, вы увидите, что программа выводит Hello, а затем входит в бесконечный цикл.

Файл .bin вышеуказанной программы hello предоставлен нами напрямую. Далее вам нужно скомпилировать собственные программы для ysyxSoC и запустить их.

В реальных компьютерах обычные устройства памяти, как правило, энергозависимы, например SRAM и DRAM. После включения питания они не содержат действительных данных. Если CPU сразу после включения питания читает инструкции напрямую из памяти, данные, возвращённые памятью, не определены. Следовательно, поведение всей системы также не определено, и CPU не может выполнить ожидаемую программу. Поэтому нужен тип энергонезависимой памяти для хранения начальной программы. Её содержимое может сохраняться после потери питания, что позволяет CPU сразу же выбирать из неё инструкции после включения питания. В ysyxSoC энергонезависимая память реализована с помощью Flash, а энергозависимая память — с помощью PSRAM. PSRAM — это особый тип DRAM. После интеграции с ysyxSoC NPC после сброса выбирает свою первую инструкцию из Flash по адресу 0x30000000. Однако с точки зрения процессора Flash нельзя напрямую записать инструкциями обращения к памяти. Поэтому программы обычно нужно загружать из Flash в записываемую память (например, PSRAM) перед выполнением.

Вышеописанная программа hello уже включает этот процесс. На самом деле предоставленный файл .bin также содержит программу-загрузчик. Она расположена по адресу 0x30000000. Когда NPC выполняет этот файл .bin, он сначала выполняет загрузчик. Задача загрузчика — загрузить настоящую программу hello из Flash в PSRAM по адресу 0x80000000. Поэтому вы увидите вывод программы через последовательный порт, похожий на: loading to memory region [0x80000000, 0x8004896c) После завершения загрузки NPC переходит к точке входа программы hello, которая находится по адресу 0x80000000, а затем выбирает и выполняет инструкции программы hello.

Поскольку загрузчик выполняется во Flash, а Flash нельзя записать инструкциями обращения к памяти со стороны NPC, при компоновке загрузчика требуется дополнительная обработка. Чтобы снизить нагрузку реализации, сейчас мы не требуем от вас реализовывать загрузчик. Вместо этого мы повторно используем загрузчик, содержащийся в предоставленном файле .bin. Мы предоставляем скрипт, расположенный по пути: ysyxSoC/ready-to-run/minirv/gen.sh, который может объединить другие программы с этим загрузчиком, чтобы сгенерировать новый файл .bin. Таким образом вы можете загружать и запускать на ysyxSoC другие программы.

Чтобы использовать этот скрипт, вам нужно создать файл с именем minirv-ysyxsoc.mk в каталоге abstract-machine/scripts/ со следующим содержимым:

include $(AM_HOME)/scripts/minirv-npc.mk
image: image-dep
	cd $(AM_HOME)/../ysyxSoC/ready-to-run/minirv && bash gen.sh $(IMAGE).elf $(IMAGE).bin

Затем с помощью следующей команды сделайте копию klib для minirv-ysyxsoc:

cd abstract-machine/klib/build
cp klib-minirv-npc.a klib-minirv-ysyxsoc.a

После этого вы можете скомпилировать программу с ARCH=minirv-ysyxsoc и запустить её на ysyxSoC.

Запустите самостоятельно скомпилированную программу на SoC

Согласно описанию выше, попробуйте запустить программу dummy на minirv-ysyxsoc.

Поскольку программа dummy не производит никакого вывода, после того как загрузчик перейдёт к программе dummy, будет выведено сообщение: HIT GOOD TRAP

Запустите самостоятельно скомпилированные программы на SoC (2)

Попробуйте запустить cpu-tests и riscv-tests на minirv-ysyxsoc.

Обращение к настоящему контроллеру UART

Поскольку мы интегрировали ysyxSoC, содержащий настоящий контроллер UART — UART16550, некоторые детали этого контроллера UART отличаются от простой поведенческой модели UART, которую мы реализовали ранее. В результате ранее написанная функция putch() не может корректно обращаться к настоящему UART16550. Это значит, что если мы хотим компилировать и запускать собственные программы, способные выводить символы, нам нужно глубже разобраться в деталях UART16550 и изменить putch(), чтобы поддержать его.

В реальных чипах, прежде чем программа выведет символы через последовательный порт, ей нужно сначала инициализировать последовательный порт. В частности, процессу инициализации нужно настроить параметры передачи последовательного порта, включая: скорость передачи (baud rate); длину символа; используется ли бит чётности; разрядность стоп-бита. Среди этих параметров baud rate означает число символов, передаваемых в секунду.

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

Набор параметров последовательного порта обычно описывается в формате вроде 115200 8N1. Это означает: baud rate равен 115200; длина символа — 8 бит; бит чётности не используется; разрядность стоп-бита — 1 бит.

Процесс передачи данных 8N1 показан ниже:

                                      стоп ---+
                                              V
--------+   +---+---+---+---+---+---+---+---+---+--------
 покой  |   | D0| D1| D2| D3| D4| D5| D6| D7|      покой
        +---+---+---+---+---+---+---+---+---+    
          ^
 старт ---+

В периоды простоя последовательная линия остаётся на высоком уровне. Когда нужно передать символ, UART сначала отправляет старт-бит длительностью 1 бит на низком уровне, затем по порядку отправляет каждый бит символа (биты D0~D7, показанные на рисунке), и наконец отправляет стоп-бит длительностью 1 бит на высоком уровне.

Последовательный порт также можно настроить как 9600 7E2, что означает: baud rate равен 9600; длина символа — 7 бит; используется один бит чётности (чётный); используются два стоп-бита. Если бит чётности включён, он располагается между битами символа и стоп-битами.

Из-за электрических характеристик в условиях больших расстояний передачи и сильных шумовых помех вероятность успешной передачи символа снижается. Чтобы повысить устойчивость передачи к помехам, протоколы UART обычно используют технику передискретизации (oversampling). Например, при использовании 16-кратной передискретизации для обнаружения старт-бита сигнал нужно выбрать 16 раз подряд. Только когда 8 или более выборок подряд определены как низкий уровень, UART сочтёт старт-бит успешно обнаруженным.

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

Поскольку контроллер UART не может заранее знать свою будущую рабочую частоту, baud rate обычно настраивается программно в соответствии с фактической рабочей частотой. Между рабочей частотой и baud rate есть соотношение, которое показывает, сколько тактов требуется для передачи одного символа. Предположим, что контроллер UART работает на частоте 50 МГц: Если baud rate установлен в 115200, один символ занимает: 50*1000000/115200 = 434 такта. Если baud rate установлен в 9600, один символ занимает: 50*1000000/9600 = 5208 тактов.

Учитывая, что UART использует передискретизацию, контроллеру UART нужно определить частоту тактового сигнала выборки. Этот тактовый сигнал выборки обычно генерируется делением тактового сигнала контроллера UART. Поэтому программному обеспечению нужно настраивать коэффициент деления тактового сигнала (divisor), чтобы косвенно задать baud rate. Например, когда контроллер UART работает на частоте 50 МГц и использует 16-кратную передискретизацию, для установки baud rate в 115200 нужно настроить регистр divisor как: 50*1000000/115200/16 = 27.13. Это означает, что выборка происходит примерно каждые 27.13 такта. Подробнее о соотношении между divisor и baud rate можно узнать, обратившись к документации UART (RTFM). Руководство UART расположено по пути: ysyxSoC/perip/uart16550/doc/UART_spec.pdf

Однако в аппаратных реализациях регистр divisor обычно поддерживает только целые значения. Например, в рассмотренном выше случае значение, записываемое в регистр divisor, обычно равно 27. Тогда фактический интервал выборки становится равным 27/50MHz = 540ns. Когда передатчик и приёмник работают на разных частотах, это может вносить некоторые ошибки передачи. В таких случаях изменение baud rate может помочь устранить эти ошибки.

Вычислите divisor для UART

Предположим, что передающий UART использует описанную выше конфигурацию. Принимающий контроллер UART работает на частоте 25 МГц и использует 16-кратную передискретизацию. Какое значение нужно настроить в регистре divisor приёмника? Как часто приёмник фактически выполняет выборку сигнала? Будут ли у обеих сторон ошибки передачи при такой конфигурации? Что произойдёт, если вместо этого установить baud rate в 57600?

Правильно реализуйте инициализацию и передачу UART

Предположим, что контроллер UART работает на частоте 25 МГц с целевым baud rate 115200. Вам нужно изменить код в abstract-machine/am/src/riscv/npc/trm.c в соответствии с этой конфигурацией, чтобы реализовать следующую функциональность:

  1. Перед вызовом main() в trm_init() настройте регистр divisor UART.
  2. Измените реализацию putch(). Перед выводом символа сначала проверьте состояние передающего FIFO UART. Символ следует записывать в передающий FIFO, только когда он не заполнен.

Подробности о том, как настроить divisor, как проверить состояние передающего FIFO и как записывать символы в передающий FIFO, вы можете найти в документации UART (RTFM). Вы также можете обратиться к RTL-реализации регистров UART16550 (RTFSC), чтобы лучше разобраться в соответствующей функциональности.

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

Интеграция с NVBoard

Модуль asicTop в ysyxSoC определяет модуль верхнего уровня чипа. В реальном чипе выводы чипа нужно соединять с другими устройствами через печатную плату. Хотя у вас пока нет настоящего чипа, мы можем использовать ранее представленную NVBoard, чтобы помочь вам разобраться в этом процессе.

UART

Мы уже тестировали последовательный вывод с помощью контроллера UART16550. Однако раньше передающая сторона использовала внутри реализации контроллера UART16550 только системную задачу $write для вывода символов. Это не задействовало процесс кодирования символов и их последовательной передачи по кабелю приёмнику. NVBoard интегрирует последовательный терминал. С NVBoard мы можем испытать этот полный процесс.

Терминал UART в NVBoard очень прост. Он поддерживает только конфигурацию последовательной передачи 8N1. Что касается baud rate, поскольку у NVBoard нет понятия тактовой частоты, вместо этого она использует описание через divisor. То есть divisor определяет, сколько тактов используется между выборками каждого бита. Поскольку NVBoard — это среда программной симуляции, она не сталкивается с битовыми ошибками, вызванными электрическими характеристиками. Поэтому, в отличие от настоящего UART, она не использует техники передискретизации. Divisor в NVBoard нельзя настроить во время выполнения, но его можно изменить, отредактировав исходный код.

Изменить его можно в конструкторе UART в nvboard/src/uart.cpp. Есть два возможных способа:

  1. Изменить начальное значение поля divisor.
  2. Вызвать функцию set_divisor(), чтобы настроить его.

Подключите вывод UART TX к NVBoard

Вам нужно всего лишь изменить файл ограничений NVBoard, чтобы привязать вывод UART TX к терминалу UART в NVBoard. Как выполнить привязку, можно узнать из примеров, предоставленных NVBoard. Поскольку контроллер UART уже интегрирован в ysyxSoC, вам не нужно снова изменять RTL-код.

Вы уже настроили регистр divisor UART в trm_init() согласно рабочей частоте 25 МГц и baud rate 115200. Поэтому вам также нужно соответствующим образом настроить divisor UART в NVBoard. Обратите внимание, что divisor в NVBoard не совсем совпадает с divisor, настроенным в регистре divisor UART16550. Между ними есть определённая связь, которую вам нужно выяснить через RTFSC или RTFM. Это также проверка того, насколько хорошо вы понимаете, как работает UART.

Затем снова запустите программу hello. Вы увидите, что последовательный вывод появляется не только в терминале командной строки, но и в терминале UART в правом верхнем углу NVBoard.

Подсказка: Если вы обнаружите, что сообщение loading to memory region появляется только в терминале командной строки, но не в последовательном терминале в правом верхнем углу NVBoard, это потому, что загрузчик уже настроил divisor UART согласно рабочей частоте 25 МГц и baud rate 115200, а divisor UART, настроенный в NVBoard, не соответствует этой настройке. В результате NVBoard не может корректно декодировать символы, выводимые загрузчиком.

Чтобы исправить эту проблему, перепроверьте регистр divisor в trm_init() и divisor UART, настроенный в NVBoard, и убедитесь, что их значения соответствуют настройке рабочей частоты 25 МГц и baud rate 115200.

GPIO

NVBoard также предоставляет такие устройства, как светодиоды и DIP-переключатели, с которыми мы уже встречались на этапе F. Теперь мы можем воспользоваться и ими. Поскольку состояние этих устройств можно передать по одной сигнальной линии, их функциональность относительно проста. Обычно их можно подключить через выводы общего назначения на чипе. Эти выводы называются GPIO. С помощью GPIO чип может напрямую выводить внутренние сигналы за пределы чипа, например, чтобы управлять простыми устройствами вроде светодиодов на плате разработки. Чип также может использовать GPIO, чтобы получать простые внешние состояния, такие как состояние DIP-переключателей и кнопок на плате.

ysyxSoC интегрирует контроллер GPIO с интерфейсом шины APB и отображает его на адресное пространство CPU 0x2000_1000 ~ 0x2000_100f. Мы выделяем контроллеру GPIO всего 16 байт адресного пространства, что поддерживает до 128 выводов. Этого обычно достаточно для типичных приложений.

Учитывая периферию, предоставляемую NVBoard, к устройствам, подходящим для GPIO, относятся 16 светодиодов, 16 DIP-переключателей и 8 семисегментных индикаторов. Поэтому мы распределяем регистровое пространство контроллера GPIO следующим образом:

АдресФункция
0x016-битные данные, каждый бит управляет одним из 16 светодиодов
0x416-битные данные, каждый бит представляет состояние одного из 16 DIP-переключателей
0x832-битные данные, каждые 4 бита управляют одним семисегментным индикатором
0xcЗарезервировано

ysyxSoC не предоставляет внутреннюю реализацию контроллера GPIO. Мы оставляем реализацию этого контроллера в качестве задания. Однако, чтобы реализовать контроллер GPIO, вам сначала нужно разобраться в протоколе шины APBоткрыть в новом окне. Шина APB очень похожа на представленный выше протокол SimpleBus, но детали взаимодействия отличаются. Вы можете изучить эти детали, прочитав соответствующую документацию.

Реализуйте эффект бегущих огней с помощью программы

Вам нужно выполнить следующие задания:

  1. Реализуйте в контроллере GPIO регистр, используемый для управления светодиодами. А именно: Если вы выбрали Verilog, реализуйте соответствующий код в ysyxSoC/perip/gpio/mygpio_top_apb.v. Если вы выбрали Chisel, вы можете вставить код Verilog, сгенерированный Chisel, в mygpio_top_apb.v.

  2. Подключите NVBoard и привяжите выходные выводы GPIO в модуле верхнего уровня SimTop к светодиодам.

  3. Напишите тестовую программу, которая периодически записывает данные в упомянутый выше регистр, чтобы создать эффект бегущих огней.

    • Обратите внимание, что хотя регистр управления светодиодами имеет разрядность всего 16 бит, поскольку инструкция lh в minirv будет раскрыта в несколько инструкций lb, мы всё же рекомендуем использовать 32-битный указатель данных для чтения этого регистра.

Реализуйте кодовый замок

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

Выведите регистрационный номер на семисегментные индикаторы с помощью программы

Преобразуйте свой регистрационный номер в 8 шестнадцатеричных цифр и используйте их для управления 8 семисегментными индикаторами. Например, если ваш регистрационный номер 123456789, преобразовав его в шестнадцатеричный текст, получим 0x75bcd15. Вы можете настроить семисегментные индикаторы так, чтобы они показывали это шестнадцатеричное число.

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

Добавление простых регистров управления и статуса

В RISC-V есть класс системных регистров, используемых для описания состояния процессора, называемых регистрами управления и статуса (Control and Status Registers, CSR). С помощью CSR мы можем добавить в аппаратуру произвольную информацию и позволить программам считывать эту информацию во время выполнения.

В отличие от регистров общего назначения, обычные вычислительные инструкции не могут напрямую обращаться к CSR. Поэтому RISC-V предоставляет инструкции CSR для обмена данными между CSR и регистрами общего назначения. В отличие от обычных инструкций, инструкции CSR атомарно читают и записывают один и тот же CSR.

Инструкции CSR используют формат инструкции I-типа, поэтому адресное пространство CSR имеет разрядность 12 бит и содержит 4096 возможных адресов. Подробнее о деталях CSR можно узнать в RISC-V Privileged Architecture Manualоткрыть в новом окне. Однако нам не нужно реализовывать все CSR. Нам нужно реализовать только необходимые CSR и выполнять операции чтения/записи согласно их адресам CSR. Поэтому вам не нужно разбираться в каждом CSR, описанном в руководстве. Вам нужно обращаться к соответствующим частям только по мере необходимости.

CSR номера студента

Чтобы идентифицировать реализации NPC разных студентов, мы можем использовать идентификационные регистры в CSR. А именно, RISC-V определяет два CSR, mvendorid и marchid, которые можно использовать для хранения идентификационной информации. Впоследствии программы могут с помощью инструкций CSR считывать эти идентификационные значения в регистры общего назначения и выводить их.

Добавьте CSR номера студента и выведите их

А именно, вам нужно реализовать инструкцию csrrs в NPC и добавить следующие два CSR:

  • mvendorid — при чтении этот CSR должен возвращать ASCII-код ysyx, то есть 0x79737978.

  • marchid — при чтении этот CSR должен возвращать десятичное представление числовой части номера студента.

    Например, предположим, что ваш номер студента ysyx_22068888 — тогда, интерпретируя возвращённое значение как целое число, должно получиться 22068888, то есть 0x150be98. Однако номер студента «One Student One Chip» можно получить только после защиты допуска. На этом этапе вы можете использовать номер вашей заявки или другой произвольный идентификатор, например дату рождения. После получения официального номера студента «One Student One Chip» вы сможете изменить его позже.

Вам нужно прочитать RISC-V Privileged Architecture Manual и найти номера CSR и другую информацию для этих двух CSR.

После реализации используйте в программе встроенную ассемблерную вставку, чтобы прочитать значения этих двух CSR, а затем выведите их с помощью printf(). В частности, вы также можете вывести номер студента на семисегментные индикаторы NVBoard.

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

asm volatile ("csrr %0, mvendorid" : "=r"(val));

чтобы прочитать значение mvendorid в переменную C val. Здесь csrr — это псевдоинструкция. При генерации последовательности инструкций компилятор раскрывает её в реализованную вами инструкцию csrrs.

CSR счётчика тактов

Ранее мы реализовали функциональность часов с помощью поведенческой модели. Однако такой поведенческой модели не существует в реальных чипах. RISC-V определяет CSR mcycle — счётчик, который увеличивается на 1 каждый такт. С помощью этого CSR мы можем переводить число тактов в прошедшее время согласно частоте процессора, что позволяет реализовать функции, связанные с часами, на реальных чипах. Согласно спецификации RISC-V, mcycle — это 64-битный счётчик.

В RV32, поскольку разрядность регистров общего назначения — 32 бита, этот счётчик разделён на два 32-битных CSR: mcycle и mcycleh.

Добавьте mcycle

А именно, вам нужно добавить mcycle и mcycleh в NPC. Для этого вам нужно найти в руководстве номера CSR и другую информацию для mcycle и mcycleh.

После реализации попробуйте несколько раз прочитать регистр mcycle через встроенную ассемблерную вставку и проверьте, увеличивается ли его значение автоматически.

Наконец, вам также нужно изменить реализацию часов в AM. А именно, измените функцию __am_timer_uptime() в abstract-machine/am/src/riscv/npc/timer.c, чтобы она читала mcycle и переводила значение счётчика во время с помощью подходящего коэффициента. При запуске программ на реальном процессорном чипе в будущем этот коэффициент будет зависеть от рабочей частоты процессора. Однако в среде симуляции нет понятия реальной тактовой частоты, и скорость течения времени в симуляции не совпадает с реальным временем. Поэтому вы можете выбрать подходящий коэффициент так, чтобы скорость часов, наблюдаемая программой, была близка к реальному времени. Поскольку это преобразование выполняется программно, при запуске на реальном процессорном чипе в будущем вы сможете скорректировать коэффициент согласно частоте процессора и перекомпилировать программу.

Запустите тест часов

Измените реализацию __am_timer_uptime(). Затем запустите real-time clock test в am-tests и добейтесь того, чтобы интервал между сообщениями, выводимыми тестовой программой, был близок к 1 секунде.

Оценка производительности на SoC

Завершив описанные выше задания по разработке на основе ysyxSoC, проведём некоторую оценку производительности.

Измерение IPC и производительности

Измерьте IPC процессора, интегрированного в ysyxSoC

Запустите программу hello на minirv-ysyxsoc и измерьте её IPC. Попробуйте с помощью временной диаграммы понаблюдать, примерно сколько тактов занимает выполнение одной инструкции, чтобы проверить измеренный IPC.

По IPC программы hello можно увидеть, что после интеграции процессора в SoC, хотя поведение обращения к памяти становится более реалистичным, его эффективность также снижается. Можно ожидать, что производительность archbench тоже снизится, а симуляция будет занимать значительно больше времени.

Чтобы быстрее получить результаты оценки производительности, вы можете сначала оценить тестовые программы в archbench, которым требуется меньше времени симуляции. В частности, вы можете начать с программы 100.blockchain. Однако перед этим вам нужно изменить реализацию функции __am_timer_uptime() и скорректировать коэффициент перевода времени согласно синтезированной частоте NPC. Пока вам нужно синтезировать только сам NPC; синтезировать весь ysyxSoC не требуется.

Исправление проблемы с передачей `mainargs` программам, запущенным на ysyxSoC

Мы обновили использование скрипта gen.sh в хендауте в 19:30:00 16 августа 2026 года, исправив проблему, из-за которой mainargs не передавался программам, запущенным на ysyxSoC.

Если вы ещё не создали minirv-ysyxsoc.mk в AM, пожалуйста, перечитайте раздел Запуск вашей первой программы на SoC выше, особенно часть про использование скрипта gen.sh.

Измерьте производительность процессора, интегрированного в ysyxSoC

После изменения реализации __am_timer_uptime() запустите программу 100.blockchain на minirv-ysyxsoc. Это должно занять примерно 5 минут.

После завершения запишите время выполнения программы (min time) и счёт, и сравните их с предыдущими результатами.

Измерьте производительность процессора, интегрированного в ysyxSoC (2)

Если у вас хватит терпения, вы можете запустить все тестовые программы в archbench на minirv-ysyxsoc.

Это может потребовать ожидания около 10 часов, но итоговый счёт бенчмарка, скорее всего, вас не порадует.

Калибровка соотношения частот NPC и устройств

Для симуляции: чем ближе поведение среды симуляции к настоящему чипу, тем меньше погрешность результатов оценки и тем полезнее измеренные данные о производительности. Начиная с модели памяти с нулевой задержкой, реализованной с помощью DPI-C, затем переходя к памяти с однотактной задержкой, доступной через SimpleBus, и, наконец, к Flash и PSRAM в SoC, счёт бенчмарков постепенно снижался. Однако эти более низкие показатели всё точнее отражают производительность настоящего чипа.

В реальном чипе периферийные устройства обычно работают на относительно низких частотах из-за своих электрических характеристик. Например, для IS66WVS4M8ALL PSRAMоткрыть в новом окне максимальная рабочая частота для операций чтения в обычном режиме составляет 33 МГц. На более высоких частотах могут возникать нарушения таймингов, из-за которых PSRAM не сможет корректно читать данные. С другой стороны, процессоры, изготовленные по современным техпроцессам, обычно могут работать на гораздо более высоких частотах.

Чтобы полностью задействовать производительность процессора, SoC обычно включает PLL (Phase-Locked Loop, петля фазовой автоподстройки частоты) для умножения низкочастотного тактового сигнала, подаваемого извне чипа. Высокочастотный тактовый сигнал, генерируемый PLL, используется для тактирования процессора, а низкочастотный тактовый сигнал до умножения частоты используется для тактирования SoC и его периферии. Предположим, что SoC и периферия работают на частоте 25 МГц, а PLL выдаёт тактовый сигнал 500 МГц. Если предположить, что этот высокочастотный тактовый сигнал находится в пределах максимальной рабочей частоты NPC, за один такт контроллера PSRAM NPC должен проходить 20 тактов.

Обновление ysyxSoC для предоставления дополнительного `cpuClock`

Мы обновили код ysyxSoC в 17:30:00 15 августа 2026 года, добавив поддержку пересечения тактовых доменов (clock-domain crossing) и предоставив cpuClock в качестве порта модуля верхнего уровня симуляции.

Если вы получили код ysyxSoC до этого времени, сделайте резервную копию всех необходимых изменений, удалите каталог ysyxSoC и получите код заново с помощью:

bash init.sh ysyxSoC

Однако текущая симуляция предполагает, что процессор и вся периферия работают на одной и той же частоте. А именно, верхний уровень симуляции ysyxSoC предоставляет два тактовых сигнала, clock и cpuClock. Сейчас мы тактируем их с одинаковой частотой. Другими словами, один такт симуляции Verilator соответствует и одному такту процессора, и одному такту периферии. По сравнению с настоящим чипом результаты оценки производительности, полученные при таком допущении, слишком оптимистичны. Поэтому счёт, полученный ранее при запуске 100.blockchain на minirv-ysyxsoc, всё ещё завышен. Если запустить 100.blockchain на реальном чипе после тейп-аута, итоговый счёт будет ниже.

Чтобы получить в симуляции более точные результаты оценки производительности, нам нужно откалибровать соотношение частот между NPC и SoC: использовать быстрый тактовый сигнал для тактирования процессора и более медленный — для тактирования SoC и его периферии. А именно, в среде симуляции каждый вызов single_cycle() будет тактировать cpuClock на один такт, делая cpuClock быстрым тактовым сигналом. Только после нескольких вызовов single_cycle() clock завершит один такт, делая clock медленным тактовым сигналом.

Например, диаграмма ниже показывает случай, когда частота cpuClock втрое выше частоты clock:

             +---+   +---+   +---+   +---+   +---+   +---+   +---+
cpuClock     |   |   |   |   |   |   |   |   |   |   |   |   |   |
         +---+   +---+   +---+   +---+   +---+   +---+   +---+   +---+

                     +-----------+           +-----------+
  clock              |           |           |           |
         +-----------+           +-----------+           +-----------+

Реализуйте тактовые сигналы с разными частотами

Измените функцию single_cycle() в среде симуляции так, чтобы частота cpuClock была целым кратным частоты clock. Для этого вы можете использовать переменную-счётчик, чтобы управлять частотой переключения clock. clock должен переключаться только тогда, когда счётчик достигает определённого значения.

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

Измерьте производительность процессора, интегрированного в ysyxSoC (3)

Предположим, что SoC и его периферия работают на частоте 25 МГц, и выберите коэффициент умножения частоты k такой, что частота cpuClock в k раз выше частоты clock. В качестве базового ориентира для сравнения производительности сначала выберите k = 1, что означает установку рабочей частоты NPC в 25 МГц.

При k = 1 запустите программу 100.blockchain. После завершения запишите время выполнения программы (min time) и сравните его с предыдущим результатом.

Измерьте производительность процессора, интегрированного в ysyxSoC (4)

Выберите наибольший коэффициент умножения k такой, что k * 25 МГц не превышает синтезированную частоту NPC. Другими словами, установите рабочую частоту NPC равной k * 25 МГц, а не напрямую используйте синтезированную максимальную частоту NPC. Это симулирует процесс ручного выбора коэффициента умножения PLL на реальном чипе в будущем.

С максимальным значением k запустите программу 100.blockchain. Это должно занять примерно от 30 минут до 1 часа. После завершения запишите время выполнения программы (min time) и сравните его с предыдущими результатами. Обратите внимание, что измерения производительности, включая и IPC, и время выполнения, должны основываться на числе тактов cpuClock и частоте k * 25 МГц.

Стоит ли оптимизировать частоту?

Вы можете обнаружить, что производительность программы 100.blockchain почти не меняется между k = 1 и максимальным значением k. Это означает, что увеличение частоты NPC почти не даёт положительного эффекта с точки зрения производительности программы.

Подумайте, почему так происходит. В текущем SoC стоит ли оптимизировать частоту процессора? Если стоит, откуда именно берётся выгода от оптимизации частоты процессора? Если не стоит, какие факторы нивелируют выгоду от оптимизации частоты процессора?

Прочие соображения

Оценка стоимости тейп-аута

Для проектирования чипа стоимость тейп-аута пропорциональна площади чипа. Предположим, что фабрика ICsprout55 берёт за тейп-аут 30 000 юаней за . В общем случае площадь, указанная в отчёте синтеза, не является итоговой площадью чипа, поскольку для процесса бэкенд-проектирования нужно зарезервировать дополнительную площадь. На основе эмпирических оценок можно считать, что площадь, указанная в отчёте синтеза, составляет 60% от итоговой площади для тейп-аута.

Оцените производительность и стоимость по площади

На основе текущего отчёта синтеза NPC оцените стоимость тейп-аута NPC.

Обратите внимание, что текущий отчёт синтеза не включает площадь SoC, а общая стоимость проектирования чипа состоит не только из стоимости тейп-аута.

todo: Оцените производительность и стоимость по площади (2)

Попробуйте использовать стратегию синтеза, ориентированную на площадь, чтобы заново оценить производительность и стоимость тейп-аута NPC.

С общей точки зрения, какая стратегия синтеза больше подходит для текущего проекта?

Эффективность симуляции

Среда minirv-ysyxsoc с откалиброванным соотношением частот хорошо подходит для оценки производительности, но вы также заметите, что её эффективность симуляции значительно ниже, чем у minirv-npc. По времени выполнения программы minirv-npc может симулироваться в десятки, а то и в сотни раз быстрее, чем minirv-ysyxsoc. Это отражает фундаментальный компромисс: чтобы получить более точные данные о производительности, нужно симулировать больше деталей, таких как шина, контроллер PSRAM и различия частот. Это означает, что симуляция каждого такта требует больше реального времени вычислений, что в итоге снижает эффективность симуляции. Соответственно, хотя minirv-npc обладает более высокой эффективностью симуляции, производимые им данные о производительности менее точны.

Означает ли это, что minirv-npc бесполезен? На самом деле мы можем использовать minirv-npc как среду функционального тестирования. Если в minirv-npc есть функциональная ошибка, с высокой вероятностью та же ошибка есть и в minirv-ysyxsoc. Однако отлаживать ошибку в minirv-npc, где симуляция намного быстрее, очевидно более подходящий подход. Таким образом мы можем в полной мере использовать сильные стороны обеих сред симуляции и компенсировать их соответствующие слабые стороны, тем самым повышая общую эффективность разработки и тестирования.

Повысьте эффективность функционального тестирования

Попробуйте изменить соответствующий процесс симуляции так, чтобы среда симуляции могла переключаться между автономным NPC и NPC, интегрированным в ysyxSoC. Вы можете использовать макроопределения, переменные Makefile или любой другой подход для реализации этого процесса. При симуляции автономного NPC по-прежнему используйте 0x8000_0000 в качестве значения сброса PC.

Хотя minirv-npc эффективнее minirv-ysyxsoc, запуск на NPC несколько более крупной программы всё же может занять много времени. В принципе minirv может реализовать всю систему команд RV32I. Однако для этого инструкции, не входящие в minirv, нужно транслировать в несколько, а то и в десятки эквивалентных по поведению инструкций minirv. Другими словами, по сравнению с компиляцией программы под RV32I, компиляция под minirv приводит к эффективности выполнения, которая в несколько, а то и в десятки раз ниже. Поэтому, хотя у minirv есть потенциал запускать игры, игровой процесс был бы в десятки раз медленнее, чем на RV32I.

На этапе D мы расширим NPC от 8 инструкций, поддерживаемых minirv, до нескольких десятков инструкций RV32I. Добавив аппаратуре больше функциональности, мы сможем значительно повысить эффективность выполнения программного обеспечения.