Непрерывная интеграция при разработке RTL-модулей

Автор: https://github.com/VSHEV92

Оглавление

  • Введение

  • GitHub Flow

  • GitHub Actions

  • Workflow для AXI-Stream сумматора

  • Запускаем Workflow

  • Улучшаем Workflow

  • Заключение

Введение

Создание цифровых устройств, как правило, представляет из себя итеративный процесс. Требования к устройству частично могут измениться уже на этапе его разработки. Также часто приходится модифицировать RTL-код после получения отчетов от инструментов синтеза и имплементации. По этой причине желательно предпринять определенные шаги для облегчения поддержки кода и внесения возможных изменений. Иными словами, нужно настроить процесс непрерывной интеграции. В этой статье на примере Github Actions и разработанного нами ранее сумматора с AXI-Stream интерфейсами мы поговорим о том, как может выглядеть процесс непрерывной интеграции при создании цифровых устройств.

GitHub Flow

Для начала кратко рассмотрим, каким образом обычно ведется современная разработка RTL-модулей. Так как сейчас цифровое устройство представляет из себя код на одном из языков описания аппаратуры, для его сопровождения необходимо пользоваться системой контроля версий. На текущий момент наиболее популярной системой является Git. Есть несколько подходов ее использования, но мы остановимся, наверное, на самом простом, который называется GitHub Flow. Давайте рассмотрим, что он из себя представляет.

Как правило, основной код модуля находится в ветке main. Если нам требуется добавить какие-либо модификации, например, описать новый функционал или исправить баг, мы создает новую ветку. В этой ветке в код вносятся все необходимые изменения и выполняются проверки. Когда мы посчитаем, что работа завершена, мы сливаем нашу рабочую ветку с основной веткой main. Над одним и тем же модулем одновременно могут работать несколько разработчиков, поэтому рабочих веток так же может быть много. В виде диаграммы данный подход представлен ниже:

a0rnwydwkaimioopmeipvdzzxfc.png

Перед переносом изменений в основную ветку, их лучше показать коллегам, чтобы получить обратную связь, советы или замечания. Если мы занимаемся разработкой через GitHub, то для этих целей используется pull request. С его помощью мы уведомляем всех участников проекта о модификациях, которые хотим добавить. Мы также можем назначить одного или нескольких reviewers, без одобрения которых невозможно выполнить слияние веток.

Однако, из-за человеческого фактора проведение review не гарантирует, что неработающий и плохой код попадет в основную ветку. Желательно иметь некоторый автоматизированный процесс проверки перед слиянием веток. Этот процесс и называется непрерывной интеграцией (Continuous Integration — CI).

GitHub Actions

Для настройки непрерывной интеграции GitHub предоставляет инструмент, который называется GitHub Actions. В его терминологии процесс, который будет запущен после определенного события, например запроса на слиние веток, называется workflow. Для создания workflow необходимо добавить в репозиторий файл на языке YAML, который описывает выполняемые действия и события (триггеры), которые их запускают.

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

В свою очередь jobs делятся на шаги (steps), выполняющие отдельную небольшую задачу в рамка всего процесса непрерывной интеграции. Часто один step соответствует одной bash-команде или одному tcl-скрипту.

При настройке непрерывной интеграции для разных проектов очень часто появляются одни и те же последовательности шагов. Для удобства их можно сгруппировать в единую сущность, которая называется action, и затем многократно переиспользовать. У GitHub есть отличный инструмент под названием GitHub Marketplace, который предоставляет доступ к огромному количеству готовых actions.

Кратко разобравшись с тем, что представляет из себя непрерывная интеграция, давайте перейдем к практике и выполним ее настройку для нашего сумматора с AXI-Stream интерфейсами.

Workflow для AXI-Stream сумматора

Репозиторий сумматора с AXI-Stream интерфейсами можно найти по ссылке. Он содержит папку src, в которой расположены исходники сумматора, а также папку tests, где находится тестовое окружение. В качестве симулятора для запуска тестов используется Icarus Verilog.

Чтобы настроить новый workflow, в корне репозитория необходимо создать папку .github, которая будет содержать еще одну папку с именем workflows. Все YAML-файлы должны находиться в папке workflows. Мы создали файл pre-commit.yml с описанием workflow, который будет запускаться перед слиянием веток. Давайте последовательно рассмотрим его содержимое.

Сначала указываем имя workflow:

name: AXIS-Adder Pre-Commit Workflow

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

on: 
  workflow_dispatch:
  pull_request:
    branches: main

Мы установили два триггера. Первый из них — это workflow_dispatch, позволяющий запускать workflow вручную. Второй триггер указывает, что workflow будет запускаться при создании pull_request для внесения изменений в ветку main.

Далее указываем, что наш workflow содержит job с именем AXIS-Adder-Pre-Commit, которая будет выполняться на виртуальной машине с последней версией операционной системы Ubuntu:

AXIS-Adder-Pre-Commit:
    runs-on: ubuntu-latest

По умолчанию для запуска нашего workflow GitHub самостоятельно выделит для нас ресурсы в Azure Cloud.

Теперь зададим шаги, которые будут выполнены в рамках нашей job. Для описания каждого шага мы будем указывать его имя и действия, которые он совершает. Так как мы получаем абсолютно новую виртуальную машину, первым делом необходимо установить Icarus Verilog. Также дополнительно установим утилиту rand для генерации случайных целых чисел. Присвоим шагу имя Install tools:

- name: Install tools
  run: |
    sudo apt update
    sudo apt install iverilog
    sudo apt install rand

Ключевое слово run означает, что шаг будет состоять из одной или нескольких bash-команд. Вертикальная черта | после слова run указывает, что для описания команд будет использовано более одной строки текста.

Далее нам требуется перенести наш репозиторий из GitHub на полученную виртуальную машину. Мы создаем step с именем Check out repository code. Так как этот шаг необходимо выполнять в любом проекте, для него есть готовый action. Ключевое слово uses указывает, что на данном шаге мы будем использовать action, который называется actions/checkout@v4.

 steps:
   - name: Check out repository code
     uses: actions/checkout@v4

На текущем этапе наша виртуальная машина готова для запуска тестов. Сначала с помощью команды iverilog мы собираем исходники сумматора и тестового окружения в snapshot c именем adder_snap:

- name: Build
  run: |
  	iverilog src/adder_comb.v \
             src/adder_axis_cu.v \
             src/axis_inf_cu.v \
             src/adder_axis_pipe.v \
             tests/adder_axis_tb.v \
             -I tests \
             -o adder_snap

Если во время сборки произойдут какие-либо ошибки, то симулятор завершится с ненулевым кодом возврата, что остановит всю job.

На следующем шаге мы запускаем моделирование:

- name: Simulation
  run: |
    SEED=`rand`
    echo $SEED
    vvp adder_snap +seed=$SEED | tee log.txt

Сначала с помощью команды rand получаем случайное целое число, сохраняем его в переменную SEED и выводим его значение на экран. Далее выполняя команду vvp, запускаем моделирование. С помощью +arg и переменной SEED мы инициализируем начальное состояние генераторов случайных чисел в тестовом окружении. Если в процессе выполения тестов буду обнаружены ошибки, то мы сможем воспроизвести проблему локально у себя на компьютере, используя выведенное ранее на экран значение переменной SEED.

После завершения моделирования, если не случилось ничего экстраординарного, симулятор всегда вернет нулевой код возврата независимо от того, были ли обнаружены ошибки во время выполнения тестов или нет. Поэтому, чтобы убедиться в успешном прохождении тестов, нам необходимо вручную проверить сообщения симулятора. Для этого с помощью команды tee мы осуществляем запись всех сообщений в файл log.txt.

На следующем шаге выполняется анализ содержимого полученного файла:

- name: Check Results
  run: |
    if grep -q PASSED log.txt; then
      exit 0
    else
      exit 1
    fi

Мы знаем, что при отсутствии ошибок тестовое окружение выведет итоговое сообщение TEST PASSED. Иначе будет выведено TEST FAILED. Поэтому с помощью команды grep будем искать в сообщениях симулятора слово PASSED и, если оно обнаружено, то мы завершаем job с нулевым кодом возврата (exit 0). Если же это слово не найдено, то считается, что тесты не пройдены, и выдается единичный код возврата (exit 1).

Ниже представлено полное содержимое YAML-файла:

name: AXIS-Adder Pre-Commit Workflow
on: 
  workflow_dispatch:
  pull_request:
    branches: main
jobs:
  AXIS-Adder-Pre-Commit:
    runs-on: ubuntu-latest
    steps:
      - name: Install tools
        run: |
          sudo apt update
          sudo apt install iverilog
          sudo apt install rand
          
      - name: Check out repository code
        uses: actions/checkout@v4
        
      - name: Build
        run: |
          iverilog src/adder_comb.v \
                   src/adder_axis_cu.v \
                   src/axis_inf_cu.v \
                   src/adder_axis_pipe.v \
                   tests/adder_axis_tb.v \
                   -I tests \
                   -o adder_snap
                   
      - name: Simulation
        run: |
          SEED=`rand`
          echo $SEED
          vvp adder_snap +seed=$SEED | tee log.txt
          
      - name: Check Results
        run: |
          if grep -q PASSED log.txt; then
            exit 0
          else
            exit 1
          fi

Запускаем Workflow

Проверим, как работает наш workflow. Для начала будем запускать его вручную. Для этого в репозитории в разделе Actions нужно выбрать workflow и нажать кнопку Run Workflow. Мы также можем выбрать ветку, на которой запустится workflow. Пока будем выполнять запуски на ветке main.

5flrdlyytalcn35npiyoa2o6lfq.png

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

m5pzqmqfgjgztaczkdrks2biafg.png

Заглянув в подробности, получаем, что job был остановлен на шаге Build из-за синтаксической ошибки в файле adder_axis_cu.v.

lbt0fvxcxl1t6vn4nknvpw0lw0o.png

Далее внесем ошибки в сумматор, чтобы он успешно компилировался, но не проходил тесты. Видим, что теперь job останавливается на шаге Сheck Results. Также можно наблюдать множество сообщений об ошибках от тестового окружения на шаге Simulation.

cqw1n_ihncj3dwny6cld_zfpcx4.png

Исправив все ошибки, получим успешное прохождение workflow.

wt09moxhr1rlc1v8hosaj5_omf0.pngzwlhqvujwmtksgpdz1p3ehggrrg.png

Теперь рассмотрим второй вариант запуска workflow. Для этого добавим ветку, внесем некоторые изменения в исходники и создадим pull request. Можно увидеть, что наш workflow запускается автоматически. При этом, если ветка main является защищенной, то мы не сможем произвести слияние (кнопка Merge pull request неактивна) до тех пор, пока workflow не будет выполнен успешно.

dsxagawlbghfzzfteggch2qvhy0.png

Улучшаем Workflow

Внесем в наш workflow два небольших улучшения. На текущий момент одним из первых шагов является установка симулятора Icarus Verilog. Мы вынуждены это делать, так как GitHub выделяет нам в облаке новую чистую виртуальную машину. Установка требует всего несколько секунд, что не сильно сказывается на длительности выполнения workflow. Если бы мы использовали более серьезный симулятор, то заниматься его установкой перед каждым запуском тестов было бы слишком расточительно.

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

Для примера мы создадим контейнер на основе Ubuntu с уже установленными Icarus Verilog и утилитой rand. Мы не будем останавливаться на том, как создаются и настраиваются контейнеры, так как это отдельная обширная тема. Мы лишь кратко опишем содержимое Dockerfile для создания контейнера:

FROM ubuntu:22.04

RUN apt-get update
RUN apt-get install iverilog -y
RUN apt-get install rand -y

WORKDIR /project

В начале с помощью FROM мы указываем, что контейнер будет собран на основе Ubuntu версии 22.04. Далее с помощью команд RUN обновляем список пакетов и устанавливаем Icarus Verilog и утилиту rand. В завершение указываем, что при запуске контейнера рабочей директорией будет являться папка /project.

На основе этого файла мы собираем image c именем vshev92/iverilog и отправляем ее в DockerHub. Чтобы использовать этот контейнер нам необходимо в описании workflow после указания операционной системы добавить всего одну строку:

container: vshev92/iverilog:latest

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

qybt-dv_6orhz0bypu8tqkibmdm.png

Обратите внимание на два новых новый шага. На шаге Initialize containers происходит получение с DockerHub и запуск нашего контейнера, а на шаге Stop containers — его остановка.

Еще одно улучшение нашего workflow будет заключаться в использовании матрицы параметров. В описании job мы можем создать некоторые параметры и указать списки их возможных значений. Тогда при старте workflow будет запущено множество jobs со всеми возможными комбинациями этих параметров.

Эта возможность может быть удобна, если RTL-модуль является параметризируемым. Например, для нашего сумматора можно регулировать ширину входных слагаемых с помощью параметра WIDTH. Его значение задается на этапе компиляции, вызывая команду iverilog с ключом -D WIDTH=«значение». Таким образом, определив матрицу параметров, мы запустим сразу множестов тестов для различной ширины входных слагаемых.

Чтобы это сделать, после имени job необходимо добавить следующие строки:

strategy:
  matrix:
    WIDTH: [4, 7, 12]

Ключевые слова strategy и matrix создают матрицу из одного параметра с именем WIDTH, который может принимать значения 4, 7 и 12. Чтобы использовать матрицу нам нужно также изменить шаг Build, в котором выполняется компиляция исходников сумматора и тестового окружения:

- name: Build
  run: |
    iverilog src/adder_comb.v \
             src/adder_axis_cu.v \
             src/axis_inf_cu.v \
             src/adder_axis_pipe.v \
             tests/adder_axis_tb.v \
             -I tests \
             -D WIDTH=${{ matrix.WIDTH }} \
             -o adder_snap

Мы добавили в команду запуска симулятора ключ -D WIDTH, которому присвоили значение ${{ matrix.WIDTH }} из матрицы параметров. После запуска workflow можно увидеть, что параллельно было запущено три jobs, каждая со своим значение параметра WIDTH.

px4qxrmtjuh1lofajqzpk_ruvs8.png

Раскрыв подробности первой job, видим, что в ключу -D WIDTH действительно было присвоено значение 12.

6cfvkiu3uqczc8gtujobaq6nrvk.png

Полное содержимое YAML-файла после внесения всех изменений представлено ниже:

name: AXIS-Adder Pre-Commit Workflow
on: 
  workflow_dispatch:
  pull_request:
    branches: main
jobs:
  AXIS-Adder-Pre-Commit:
    strategy:
      matrix:
        WIDTH: [4, 7, 12]
    runs-on: ubuntu-latest
    container: vshev92/iverilog:latest
    steps:
      - name: Check out repository code
        uses: actions/checkout@v4
        
      - name: Build
        run: |
          iverilog src/adder_comb.v \
                   src/adder_axis_cu.v \
                   src/axis_inf_cu.v \
                   src/adder_axis_pipe.v \
                   tests/adder_axis_tb.v \
                   -I tests \
                   -D WIDTH=${{ matrix.WIDTH }} \
                   -o adder_snap
                   
      - name: Simulation
        run: |
          SEED=`rand`
          echo $SEED
          vvp adder_snap +seed=$SEED | tee log.txt
          
      - name: Check Results
        run: |
          if grep -q PASSED log.txt; then
            exit 0
          else
            exit 1
          fi

Заключение

В данной статье мы познакомились с тем, что такое непрерывная интеграция, и как она может применяться при разработке RTL-модулей. Мы собрали небольшой workflow, который запускает тесты при получении pull request на внесение изменений в основную ветку. Конечно, это далеко не единственный пример использования GitHub Actions. Например, можно настроить периодический запуск workflow по таймеру для выполнения длинных ночных тестов. Таким образом, без участия разработчиков каждую ночь будет запускаться множество тестов с разными начальными условиями и исследоваться различные части пространства состояний устройства. У GutHub Actions очень хорошая документация, поэтому всем заинтересовавшимся читателям рекомендуем с ней ознакомиться.

© Habrahabr.ru