Разработка процессора и вообще программируемых микросхем — процесс сложный и длительный. От старта проектирования до получения первых образцов в кремнии проходит больше года. При этом ПО желательно писать и отлаживать параллельно процессу производства, чтобы оптимизировать сроки выхода продукта. Но как это делать, если железо еще не на руках или оно есть в очень ограниченном количестве, а нужно многим?
Спойлер: делать имитацию. О том, какие подходы существуют и как выжать из них максимум эффективности для имитации сложных многоядерных систем, рассказали инженеры-программисты отдела разработки системного ПО YADRO Светлана @sita5174Бурлака и Александр Солдатов.
Как можно имитировать железо
Когда программист не может проверить работу кода в проде, он делает тестовый стенд, который «имитирует» кодовое окружение. Но что делать инженеру, который разрабатывает ПО под железо, которого нет? Как сымитировать целый процессор?
На самом деле методы, с помощью которых можно имитировать аппаратуру частично или целиком, существуют. Бывают разные виды, их называют по-разному, но в этой статье, для упрощения, мы будем использовать два термина — симуляцию и эмуляцию.
Между понятиями довольно тонкая грань, но условно объяснить и разделить их можно так.
При симуляции имитируются свойства объекта, поведение, функции. При этом его логика и принципы работы могут игнорироваться.
При эмуляции объект воспроизводится целиком (или частично) вместе со всеми принципами и логикой работы.
Воспользуемся примером для лучшего объяснения. Допустим, есть задача: нужно, чтобы в регистре процессора появилось определенное значение. В случае симуляции мы лишь обеспечиваем появление этого значения. Какой логикой мы этого добились — неважно. При эмуляции мы воспроизводим не только появление нужного значения в регистре, но и механизм — почему оно там появилось.
Для каких задач мы имитируем оборудование
Все программное обеспечение, которое пишется для процессоров, требует отладки и тестирования. Для создания различных имитаций есть ряд инструментов: эмулятор QEMU, потактовые симуляторы, FPGA-платформы. Подробнее о них мы еще поговорим.
Методы имитации абстрагированы от аппаратных средств. Например, мы можем сделать программную имитацию процессора на открытой архитектуре и запустить ее на физическом компьютере с x86-процессором. В этом контексте физический компьютер называется хостом, а имитируемая система — гостем.
Выделим несколько конкретных направлений, где можно работать с имитациями процессора и ПО:
тестирование функциональных моделей IP-блоков с помощью функциональных тестов, написанных на С и Assembler,
проверка корректности написания тестов: они должны давать одинаковые результаты и на модели устройства в QEMU, и на RTL-модели,
разработка и отладка BSP (Board Support Package),
проверка работы аппаратной части совместно с ПО — например, драйвера и устройства.
Грань между применением эмуляций и симуляций (в обозначенном выше понимании) довольно тонкая. Выбор метода зависит от конкретной задачи.
Для системных тестов и тестов ПО, отладки драйверов чаще используют эмуляцию. Здесь нужно воспроизвести работу «железа» целиком — ровно так, как оно работает. Иначе, когда мы запустим ПО на реальном процессоре, оно может начать работать непредсказуемо или не работать совсем. А вот в случае отладки пользовательских программ — например, прикладного ПО — симуляции может быть вполне достаточно.
Способы и инструменты имитации оборудования, которые мы используем
Рассмотрим три подхода к имитации процессоров и сопутствующих систем, чтобы отлаживать ПО и верифицировать IP-блоки. Каждый имеет свои достоинства и недостатки.
Эмулятор QEMU
QEMU — самый быстрый и понятный способ. Это open source-программа с активным комьюнити. На ней можно легко запустить операционную систему класса Linux и оперативно выполнить многие задачи, но есть минусы.
Как только задачи выходят за рамки стандартных, функциональности QEMU «из коробки» уже недостаточно. В составе чипа могут быть достаточно сложные устройства — например, кодеки или графические ускорители. Готовых моделей в QEMU нет — нужно писать свои, а это сложно и не всегда целесообразно.
Второй минус — сложность подключения нестандартных аппаратных устройств к QEMU. Представим, что вам нужно пробрасывать такие устройства, как, например, дисплей с LVDS-интерфейсом, камера с MIPI-интерфейсом, чтобы проверить работу написанного кода. Напрямую к компьютеру камеру c MIPI не подключить — нужны аппаратные и программные доработки. А на них может уйти от нескольких месяцев.
В таких случаях можно расширить функционал QEMU — как именно расскажем чуть позже.
Потактовый симулятор
Если у вас есть описание аппаратного блока чипа на языке RTL, вы можете организовать точную потактовую симуляцию. Для этого можно использовать такие решения, как VCS, SystemC и другие вплоть до Verilator.
Если компьютер мы эмулируем на уровне регистров, памяти, то в случае потактовой симуляции спускаемся на уровень электрических сигналов. Это позволяет точнее воспроизводить поведение оборудования, но сама эмуляция довольно долгая. Linux на такой конфигурации не запустить — он будет неделю стартовать и в итоге вообще не запустится.
FPGA-платформа
Третий возможный вариант — это FPGA-прототип, когда весь чип реализуется внутри одной или нескольких FPGA.
В этом случае можно подключить необходимое оборудование (например, камеру, экран), можно все промоделировать. Но собирать bitstream для FPGA — дорого и долго. Если в дизайн закрадется ошибка, придется пересобирать прошивку. В среднем на это может уйти до суток и более.
И чем сложнее чип, тем сложнее прототип. Большой чип может не «влезть» в одну FPGA — нужно разделять на несколько, что сопровождается ростом накладных расходов.
Использовать только один из перечисленных способов — неэффективно. Мы рекомендуем применять все три в разных комбинациях. Так можно компенсировать те или иные недостатки методов.
Хорошая практика — первым этапом обкатывать решения на QEMU. Далее расскажем, как прокачать известный многим инструмент.
Эмуляция с помощью QEMU — процессоры и IP-блоки
Коротко об архитектуре QEMU
QEMU может эмулировать полностью функционирующий компьютер со всем оборудованием. Программа поддерживает разные архитектуры и запуск десятков операционных систем.
Схема эмуляции системы в QEMU.
Эмуляция процессора осуществляется с помощью динамической двоичной трансляции и кодогенератора TCG (Tiny Code Generator). TCG берет блоки кода, переводит их сначала в машинно-независимый код, а потом в инструкции для хостовой машины. Последняя выделяет определенный объем памяти для работы QEMU.
В QEMU устройства и доступ к ним реализованы с помощью MMIO (Memory-mapped Input/Ouput) — ввод-вывод с отображением в памяти (это один из вариантов). Также в их реализации прописано, как реагировать на попытки обращения к памяти. Устройства регистрируются на системной шине (System bus), которая моделируется с помощью объектной модели QEMU — для работы с ней есть API.
Устройства могут запускаться на этапе инициализации виртуальной машины, либо их можно подключать динамически с помощью QEMU Monitor, когда машина уже запущена.
Когда нужно кастомизировать QEMU
В работе с эмуляцией процессоров в ряде случаев стоит расширить возможности QEMU.
Причина 1. Если процессор работает на ядрах разной битности
Представим, что у вас есть чип — система на кристалле, которую вы эмулируете. При этом вы используете процессорные ядра различных конфигураций:
System (SYS) — группа процессорных ядер, принимающих всю нагрузку; на них запускается Linux, пользовательские приложения, они 64-битные.
System Control Unit (SCU) — процессорное ядро, среди прочих функций которого — управление системой питания и загрузкой ОС. Оно 32-битное.
Ваша задача — организовать общение между двумя системами, этого требует полноценная эмуляция. На примере питания: SYS не может себя включить и выключить самостоятельно, это делает SCU, а SYS, в свою очередь, должен попросить SCU выключить питание.
При этом QEMU позволяет эмулировать только системы одной архитектуры. Так как у ядер чипа разная битность — читай, разная архитектура, вы не сможете запустить их в рамках одного процесса. Поэтому нужно развернуть их как две отдельные виртуальные машины QEMU и организовать связность между ними.
Как решить задачу
Разработать механизм (для статьи назовем его Interconnect), благодаря которому будут общаться модели IP-блоков в системе.
Как может работать такой механизм, расскажем на примере модели IP-блока, представляющей собой почтовый ящик, с помощью которого две системы могут обмениваться сообщениями. В тексте назовем эту модель Mailbox. Обмен происходит через shared memory — участок памяти, расшаренный между системами, каждая может в него писать или его читать.
Когда одна система что-то пишет в shared memory, другая система об этом не знает. Чтобы сообщить о событии из одной системы в другую, мы используем файловые дескрипторы EventFD. Они отправляют сообщения, сигнализирующие о том, что произошло какое-то событие или прерывание.
В Linux файловый дескриптор представляет собой число — своего рода идентификатор — и контекст. Одинаковые номера EventFD не подразумевают один контекст. Так, файловый дескриптор с номером 42 в SYS может быть никак не связан с дескриптором 42 в SCU — в разных системах у них разные контексты. Таким образом, системам необходимо обмениваться файловыми дескрипторами и контекстами. Для этого можно использовать сокет, который позволяет передавать файловые дескрипторы из одной системы в другую. Этот процесс и поддерживается механизмом Interconnect.
Вся схема может выглядеть так:
На этапе инициализации системы MailBox (SCU) отправляет файловые дескрипторы в Interconnect (SCU). Затем Interconnect передает эти файловые дескрипторы в Interconnect (SYS), где они отдаются в Mailbox (SYS). Системы обменялись файловыми дескрипторами — далее Interconnect не используется.
Дальнейшее общение между SCU и SYS происходит следующим образом:
одна система пишет в shared memory и через EventFD отправляет сообщение о событии,
другая система получает сообщение о событии и идет читать shared memory.
Все это регулируется обработчиком в главном цикле QEMU — Main Loop. Можно создать свой EventFD, зарегистрировать его в этом цикле, и QEMU будет сообщать, что сейчас пришло уведомление о каком-то событии. Далее это событие обрабатывается.
Причина 2. IP-блоков QEMU может быть недостаточно
Помимо процессорных ядер, вы можете эмулировать IP-блоки чипа и периферию платформы. Часть моделей можно найти в QEMU — они разработаны сообществом. Но в более сложных случаях некоторые модели нужно будет создавать самостоятельно.
Функционал, который нужно внедрять в модели, может различаться. Он зависит от требований целевого программного обеспечения, которое будет взаимодействовать с IP-блоком. Иногда достаточно удовлетворить интерфейс: чтобы драйвер обнаружил устройство, зарегистрировал его, убедился, что оно включено, прочитал версию, проверил цифровую подпись. Но есть и более сложные случаи.
Когда вы будете писать код модели и реализовывать устройство в QEMU, рекомендуем выполнить следующие действия:
Привяжите устройство к участку памяти.
Реализуйте callback-функции чтения и записи (read/write), которые будут вызываться при обращении к данному участку памяти.
Определите смещения и размеры областей MMIO, чтобы при обращении к участку памяти можно было обратиться к конкретному регистру.
Интерпретируйте чтение или запись в регистр — что именно нужно сделать. Возможна как передача данных и изменение состояния устройства, так и влияние на состояние других устройств. Последнее, например, происходит в случае с описанным ранее Interconnect, когда одна система запускает процессы в другой.
В этой статье мы не будем подробно останавливаться на разработке IP-блоков. Если вам интересен процесс, пишите в комментариях. Можем развить эту тему в отдельном тексте.
Метод косимуляции, или когда одного способа эмуляции недостаточно
Ранее мы перечислили три метода эмуляции процессора и уточнили, что для большей эффективности лучше использовать их все — в разных комбинациях.
Например, как можно проверить работу аппаратной части совместно с ПО: высокоскоростные ядра запускаются на быстром эмуляторе, а тестируемый контроллер — на потактовом симуляторе или FPGA-платформе. Таким образом можно эффективно проверить работу драйвера с устройством.
Использование нескольких симуляторов в одной системе называется косимуляцией. Она помогает частично избавиться от недостатков различных типов симуляторов.
Можно выделить два вида косимуляции.
Программная: QEMU + потактовый симулятор. В рамках такой косимуляции можно, запустив Linux на QEMU, отлаживать драйверы IP-блоков, которые реализованы на RTL и выполняются внутри потактового симулятора.
Программно-аппаратная: QEMU + FPGA-платформа. В этом случае можно подключить к системе внутри QEMU внешнее устройство — например, дисплей. Как это выглядит: на QEMU запускаем Linux, а видеоконтроллер реализуем внутри FPGA, к которой подключен экран.
Аппаратно-программная косимуляция — тема для отдельной подробной статьи. В этом тексте мы больше расскажем, как реализовать программную косимуляцию.
Разработка программной косимуляции
Итак, наша задача — связать два симулятора, то есть через какую-то «магию» заставить их общаться между собой. Эту «магию» можно реализовать через открытую технологию Remote Port TLM — создана компанией Xilinx и доступна под лицензией MIT.
Разработка ориентирована на применение языка SystemC и связанной с ним технологии TLM. Фактически она позволяет на языке С++ описывать взаимодействие QEMU и потактового симулятора.
Как работает SystemC Transaction Level Modeling (TLM)
Есть QEMU с добавленным псевдоустройством для того, чтобы общаться с Remote Port. В оригинальной технологии, описанной на картинке выше, используется связка SystemC TLM, где есть:
бридж, преобразующий Remote Port-вызов в транзакции SystemC TLM,
TLM Interconnect, который адресует эти транзакции в тот или иной design under test. В основном DUT касается AXI-шины и производных — все, что можно объединить под общим названием Advanced Microcontroller Bus Architecture (AMBA).
Взаимодействие между симуляторами реализовано через Unix- или TCP-сокет. По нему происходит обмен пакетами: один из симуляторов отправляет пакет, а второй на него отвечает.
Есть различные типы пакетов. Часть пакетов соответствует транзакциям на шине AXI: чтение и запись одиночного значения в память, чтение и запись пакетов данных (AXI bursts), прерывания. Также есть пакеты для handshake и синхронизации времени между симуляторами. Каждый пакет содержит заголовок, содержимое которого позволяет корректно определить тип пакета и выполнить его дальнейший разбор, и переменную часть, зависящую от типа пакета. Каждый пакет имеет уникальный идентификатор, позволяющий отправлять несколько запросов, не дожидаясь ответов на каждый.
Как можно адаптировать и реализовать технологию
Например, вместо SystemC можно использовать проприетарный симулятор и testbench, написанный на языке SystemVerilog. Чтобы привязать Remote Port к симулятору, можно использовать System Verilog DPI.
Технология входит в стандарт System Verilog и обеспечивает взаимодействие между кодом С и SystemVerilog. Код на С собирается в виде библиотеки, к нему подключается header, который генерируется на этапе сборки SystemVerilog. Затем эта библиотека подается потактовому симулятору вместе с результатами сборки SystemVerilog — производится совместная симуляция.
В тестовой реализации можно организовать взаимодействие QEMU через механизм RemotePort с кодом SystemVerilog внутри симулятора.
Обычно QEMU выступает в роли мастера: отправляет AXI-транзакции до модели периферии в потактовый симулятор и дает ей возможность должным образом ответить. Для этого на стороне QEMU есть псевдоустройство — RP Master, который переадресует обращения в память устройства, преобразовывая их в транзакции Remote Port. Когда QEMU является мастером на AXI-шине, организовать общение с потактовым симулятором намного проще.
Однако существуют ситуации, когда эмулятор работает как slave — мастером выступает модель, выполняемая в потактовом симуляторе. Ей нужна возможность отправлять транзакции в QEMU. Когда такое может произойти? Например, внутри потактового симулятора у нас выполняется модель видеоконтроллера, которая должна самостоятельно ходить в память QEMU (прямой доступ в память DMA) и забирать изображения, которые она будет показывать. В таком случае устройство одновременно и slave (мастер — процессор), и мастер (slave для него — оперативная память).
Когда QEMU — slave, общение организовать сложнее. Для этого на версию эмулятора нужно портировать псевдоустройства RP slave и RP wire (на иллюстрации выше) — последнее участвует в работе Interrupt controller, который обеспечивает прерывание работы процессора. Оба псевдоустройства осуществляют преобразование пакетов в транзакции в память гостевой системы в QEMU — все это, соответственно, передается в сокет.
Для взаимодействия QEMU и потактового симулятора можно создать библиотеку на базе стандарта SystemVerilog DPI. Она преобразует транзакции RemotePort в вызовы SystemVerilog DPI и обратно, то есть реализует Remote Port to DPI.
Заключение
Эмуляция сложной системы — всегда задача со звездочкой. Просто взять доступное решение на GitHub и применить его не получится. Нужна значительная доработка, которая сопровождается исследованием существующих инструментов и экспериментами с ними.
Для имитации сложных систем лучше всего работает метод косимуляции, благодаря которому можно нивелировать недостатки отдельных подходов к симуляции процессора и его окружения. Несмотря не сложность реализации, косимуляция окупается тем, что экономит ресурсы и время команды. Можно не развертывать всю систему внутри FPGA-платформы, а раскрутить в ней один или несколько IP-блоков и просимулировать их совместно с основной системой, которая работает на х86-хосте в QEMU.
Приходилось ли вам имитировать железо и другие системы? Как справлялись с задачей? Пишите в комментариях.
Комментарии (14)
juvescorp
04.12.2023 10:42Как организована общая память для виртуальных машин? (shared memory)
Как по ссылке ниже или как-то по-другому?
https://www.qemu.org/docs/master/system/devices/ivshmem.html
sita5174
04.12.2023 10:42+1Мы используем POSIX mmap, но не напрямую, а через QEMU API: qemu_ram_mmap().
nckma
04.12.2023 10:42"Linux на такой конфигурации не запустить — он будет неделю стартовать и в итоге вообще не запустится."
Смотря какой Linux. Простой консольный Линукс на CPU без MMU даже на Icarus Verilog за пол часа стартует. А с верилатором думаю 10 секунд будет.
sita5174
04.12.2023 10:42Для ответа на этот вопрос нужен отдельный эксперимент. Возможно, вы такой проводили? Было бы интересно ознакомиться с результатами.
nckma
04.12.2023 10:42Когда-то давным давно я пытался запустить Amber ARM v2a SoC в ПЛИС.
Как часть той работы была симуляция работы процессора и запуск какого-то усеченного Линукс.
https://marsohod.org/projects/marsohod2/amber-arm-soc/396-amber-arm-v2a-soc-verilator
Там в конце моей статьи написано такое:
Самое интересное, что если с Icarus Verilog на подобную симуляцию загрузки ядра Linux уходило минут сорок пять, то теперь, с Verilator, полный старт симулируемой системы происходит секунд за 30! Невероятно. Я честно говоря очень впечатлен получившимся быстродействием. Очень рекомендую присмотреться к Verilator. В некоторых случаях - отличный инструмент.
vaxxabait
04.12.2023 10:42А чем Synopsys Virtualizer не угодил ? Ну кроме цены (либо сложности поиска китайского кряка учитывая политику). Или аналоги от Cadence/Siemens/Imperas/Windriver....Там хоть сразу десяток разных архитектур можно в одном прототипе. И анализ узких мест в интерконнекте из коробки. И RTL косимуляция, и с HAPS оно замечательно косуществует.
checkpoint
04.12.2023 10:42+1Я пришел к выводу, что санкции это хороший повод и шанс послать к чертям всю проприетарщину и начать работу с СПО, постепенно расширяя его возможности. От офиса до симуляции вычислительных ядер.
sita5174
04.12.2023 10:42+1Короткий ответ: проблема в сложности закупки лицензированного продукта, а использование неофициального ПО или его взлом противоречат позиции компании. А вы используете какой-то из перечисленных инструментов? Расскажите, какие задачи решаете.
checkpoint
04.12.2023 10:42+2Спасибо за статью, очень интересно. Мне доводилось симулировать RISC-V (VexRiscV без MMU) + небольшой кусочек своего железа (Verilog) с помощью Verilotor-а, потактово исполняя программу в ПЗУ. При отсутствии опыта задача оказалась не из легких, не смотря на то, что в Сети много материала на эту тему.
SIISII
Причина 1. Если роцессор работает на ядрах разной битности
Во-первых, очепятка в этом подзаголовке :)
А во-вторых, правильней говорить всё ж о разных архитектурах, а не "битности" (разрядности). Например, в STM32MP15x имеется одно или два 32-разрядных ядра Cortex-A7 и одно 32-разрядное ядро Cortex-M4. "Битности" совпадают, но архитектуры-то реально разные (ARMv7-A и ARMv7-M).
maquefel
Неверно, тут речь идет именно о "битности":
Архитектура у них одинаковая RISC-V. Есть AMP с одной разрядностью, но разным функционалом например Sifive Unmatched, там и разрядность одинаковая, но например на 0 харте отсутвует MMU, и они как раз эмулируются в размках одной машины QEMU.
byman
И все же в данном примере лучше говорить о разных архитектурах :) Да и сам автор пишет:"Так как у ядер чипа разная битность — читай, разная архитектура ".
SIISII
Ну, вообще-то самое понятие "архитектура" подразумевает программную совместимость -- по меньшей мере, "снизу вверх". Если прогу, написанную для 32-разрядного RISC-V, можно без перекомпиляции запустить на 64-разрядном, тогда можно считать их одной архитектурой, если нет -- то нет. (Я с RISC-V реально не знаком, поэтому и говорю про "если")
Ну а если сравнить с тем же ARMом одинаковой разрядности, то с одной стороны имеем M-профиль, а с другой -- A- и R-. Хотя почти все собственно команды из M-профиля будут нормально выполняться на процах A- или R-профиля, считать эти разновидности одной архитектурой, как по мне, всё же неправильно: у них кардинально отличается, так сказать, системная архитектура -- в первую очередь, обработка прерываний, и соответствующий код абсолютно непереносим. С другой стороны, A- и R-профили можно считать одной архитектурой: основная разница -- наличие MMU (A) или MPU (R). Ну а 64-разрядная архитектура ARM, понятно, имеет к M-профилю ещё меньшее отношение.
В общем, подозреваю, что с RISC-V примерно так же, как и с ARM: типа одна архитектура, а реально -- несколько, имеющих как общие, так и кардинально различающиеся элементы. Но, как уже сказал, пока в этом вопросе некомпетентен.
sita5174
Мы привели пример, когда мы не можем запустить ядра с разной битностью внутри одной виртуальной машины, так как QEMU не позволяет это сделать.
Однако, Вы правы, что и системы с разными архитектурами также нельзя запустить в рамках одного процесса.