In Anbetracht der aktuellen Entwicklungen auf dem Weltmarkt, insbesondere der weltweiten Chip-Knappheit, gibt es einige Probleme, die bei der Bereitstellung von Software auf einer Embedded-Plattform zu lösen sind, aber es fehlt die Plattform selbst.
Das ist kein leichtes Unterfangen, insbesondere für Unternehmen, die Software für verschiedene Plattformen einsetzen und nicht die Möglichkeit haben, die Vielzahl der auf dem Markt erhältlichen SoCs auf Lager zu haben. Es gibt auch die Situation, dass ein neuer Chip in der Entwicklung ist und die Software dafür aus Effizienzgründen parallel entwickelt werden muss.
In Anbetracht all dessen muss es praktisch sein, Hardware auf unseren Standard-x64-Maschinen zu emulieren. Aber was tun wir, wenn unterschiedliche Speicherkarten oder sogar ISAs (Instruction Set Architectures) beteiligt sind? Bei dieser Aufgabe kommen die Fähigkeiten von QEMU zur Hilfe.
Dieser Artikel dokumentiert die Schritte, die erforderlich sind, um eine benutzerdefinierte Maschine zu QEMUs bestehenden Maschinen hinzuzufügen und die etablierten Konstrukte in QEMU zu nutzen und sogar die Open-Source-Natur des Projekts zu nutzen.
Eine kurze QEMU-Beschreibung
QEMU als Open-Source-Maschinenemulator und Virtualisierer. Das bedeutet, dass er sowohl als VM Ihrer Wahl als auch als Maschinenvirtualisierer fungieren kann, indem er Speicherabbildungen der tatsächlichen Hardware nachahmt und (für die als Gast ausgeführte Software sichtbare) Effekte erzeugt, die dem entsprechenden Hardware-Gegenstück sehr ähnlich sind.
Es unterstützt derzeit eine Vielzahl von ARM-Gästen und bietet Unterstützung für etwa fünfzig verschiedene Maschinen. Es werden sowohl „A-Profil"-CPUs als auch „M-Profil"-CPUs unterstützt, wenn auch in einer numerisch begrenzteren Form, was die Maschinen betrifft.
Obwohl Unterstützung für eine Vielzahl von spezifischen Maschinen und SoCs angeboten wird, wird auch eine virtuelle Maschine angeboten. Diese Maschine, virt genannt, ist eine allgemeine ARM-Systememulationsvariante. Virt bietet eine Vielzahl von unterstützten „cpus" (z. B. cortex-a15, cortex-a53 usw.) und verschiedene Peripheriegeräte an. Die Verwendung dieser Maschine ist nur dann zu empfehlen, wenn die Besonderheiten der spezifischen Hardware und ihre Beschränkungen bei der Softwareentwicklung ignoriert werden.
Systemanforderungen
QEMU kann auf den folgenden Host-Plattformen ausgeführt werden:
- Linux
- Microsoft Windows
- macOS
- einige andere UNIX-Plattformen
Initialisierung
Für die Ausführung von QEMU wird die Verwendung eines Linux-basierten Systems als Host empfohlen, zumindest für den speziellen Fall dieses Artikels. In diesem Fall läuft QEMU auf einer Ubuntu-Version 18.04.
Die für dieses Beispiel verwendete QEMU-Version ist auf GitHub mit dem Tag v.7.0.0 gekennzeichnet.
DISCLAIMER: Dies ist kein vollwertiges Tutorial, wenn Sie erwarten, dass Sie am Ende eine spezifische Maschine implementiert haben, mit genauen Adressen und anderen Parametern einer spezifischen Maschine, würde ich das Tutorial empfehlen, das in den Referenzen von S. Sopha erwähnt wird (mit dem Hinweis, dass das FreeRTOS-Tutorial, das benötigt wird, um die Maschinenimplementierung zu überprüfen, nicht vollständig ist). Die vorliegende Arbeit dokumentiert eine andere Sichtweise auf die Implementierung einer Maschine in QEMU und sie zielt darauf ab, mehr erklärend als praktisch zu sein, beginnend mit dem Teil „Klonen bereits existierender Quell- und Header-Dateien". Zuvor können Sie dem Tutorial Schritt für Schritt folgen und die praktischen Anforderungen erfüllen.
Klonen des Git-Repositorys
Um die aktuellste Version von QEMU zu klonen, sollten Sie den folgenden Befehl ausführen:
git clone https://github.com/qemu/qemu
Vorbereitungen für den Aufbau
Es gibt eine Reihe von Abhängigkeiten, die für den erfolgreichen Betrieb von QEMU erforderlich sind, aber das folgende Skript sollte zumindest für die Version 7.0.0 von QEMU ausreichend sein:
# Set up locales
apt-get update && apt-get install -y
apt-utils locales sudo &&
dpkg-reconfigure locales &&
locale-gen en_US.UTF-8 &&
update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 &&
apt-get clean &&
rm -rf /var/lib/apt/lists/*
apt-get update && apt-get install -y
git
build-essential
zlib1g-dev
pkg-config
libglib2.0-dev
binutils-dev
libboost-all-dev
autoconf
libtool
libssl-dev
libpixman-1-dev
libpython-dev
python-pip
python-capstone
virtualenv &&
apt-get clean &&
rm -rf /var/lib/apt/lists/*
apt update -y
apt upgrade -y
apt install ninja-build
Konfigurieren und Erstellen
Für die Konfiguration und den Build muss sichergestellt werden, dass ein Build-Verzeichnis innerhalb des QEMU-Repositorys existiert. Die ausgewählte Konfiguration ist in Zeile 4 markiert und gilt für eine Reihe von 32-Bit- und 64-Bit-ARM-unterstützten Maschinen. Diese Konfiguration umfasst auch die virt generic machine.
! Warnung: Wenn Sie Änderungen an Quell- oder Headerdateien vornehmen, die bereits von configure eingebunden wurden, müssen Sie configure beim Kompilieren nicht erneut ausführen. Sie sollten nur make ausführen, da es sehr zeitaufwendig ist, diese zahlreichen Header- und Quelldateien von Grund auf zu kompilieren.
cd qemu
mkdir build
cd build
../configure --target-list=aarch64-softmmu # preparing the build for ARM machines running on 64 and 32 bits
make
Prüfen der Maschinenliste
Um die Maschinenliste zu überprüfen, verwenden Sie die folgenden Befehle:
cd qemu
cd build
cd aarch64-softmmu
./qemu-system-aarch64 -M help
Dieser Befehl ist nützlich, um zu überprüfen, ob der Kompilierungsprozess korrekt durchgeführt wurde und er wird nützlich sein, wenn Sie versuchen, die benutzerdefinierte Maschine zu QEMU hinzuzufügen.
Betrieb eines Rechners
Zum Betrieb eines Rechners sind möglicherweise ein Kernel-Image und eine Gerätebaumdatei (.dtb - device tree blob file) erforderlich. Das Kernel-Image kann ein von buildroot kompiliertes Linux-Kernel-Image sein oder auch Yocto kann für diese Aufgabe verwendet werden. Die Kompilierung eines Linux-Kernel-Images liegt außerhalb des Rahmens dieses Tutorials. Die .dtb-Datei beschreibt die Speicherzuordnungen der darstellenden Hardware.
# Follow the steps from the pervious code snippet to get to the qemu-system-aarch64 file, or use make install instead of just make
./qemu-system-aarch64 -M
-kernel
-dtb
Hinzufügen eines neuen Rechners zu QEMU
Nennen wir den neuen Rechner arm-example oder ARM_EXAMPLE, um allgemein genug zu sein. Daher werden wir im weiteren Verlauf dieses Abschnitts diese Namenskonvention verwenden.
Damit der Rechner beim Aufruf von ./qemu-system-aarch64 -M help erscheint, müssen Sie ihn zu den Dateien Kconfig, meson.build hinzufügen.
Nach dem Hinzufügen der genannten Teile werden die Quell- und Headerdateien von einem Rechner kopiert, der ähnlich genug ist, um weiter implementiert zu werden.
An den Variablen, die die Adressen des Rechners darstellen, müssen Änderungen vorgenommen werden. Außerdem könnte es erforderlich sein, Konstanten zu ändern, die den Maschinennamen, den Typ, die Uhren und die Unterbrechungsroutinen darstellen.
Hinzufügen eines Rechners zur Konfiguration
Da es sich um einen ARM 64-Bit-Rechner handelt, muss zunächst das Verzeichnis hw/arm innerhalb des qemu-Verzeichnisses gefunden werden. Dort werden die Dateien meson.build und Kconfig wie in den folgenden Schnipseln geändert:
- Kconfig:
config RASPI
bool
select FRAMEBUFFER
select PL011 # UART
select SDHCI
select USB_DWC2
# beginning of added arm-example/ARM_EXAMPLE
config ARM_EXAMPLE
bool
select PL011 # UART for a latter implementation, it can even be commented
# end of added arm-example/ARM_EXAMPLE
config STM32F100_SOC
bool
select ARM_V7M
select STM32F2XX_USART
select STM32F2XX_SPI
- meson.build:
arm_ss.add(when: 'CONFIG_RASPI', if_true: files('bcm2835_peripherals.c', 'bcm2836.c', 'raspi.c'))
# beginning of added arm-example/ARM_EXAMPLE file(s)
arm_ss.add(when: 'CONFIG_ARM_EXAMPLE', if_true: files('arm-example.c'))
# end of added arm-example/ARM_EXAMPLE file(s)
arm_ss.add(when: 'CONFIG_STM32F100_SOC', if_true: files('stm32f100_soc.c'))
Im Verzeichnis configs/devices/ müssen Sie die Datei arm-softmmu.mak wie folgt ändern.
arm-softmmu.mak:
CONFIG_TOSA=y
# beginning of added arm-example/ARM_EXAMPLE
CONFIG_ARM_EXAMPLE=y
# end of added arm-example/ARM_EXAMPLE
CONFIG_Z2=y
Klonen bereits vorhandener Quell- und Header-Dateien
Die Hauptquelldatei raspi.c und die entsprechenden Header- und Abhängigkeitsdateien werden entweder geklont oder in ihrer ursprünglichen Form beibehalten, damit dieses Beispiel funktioniert.
Kopieren Sie die Datei raspi.c und benennen Sie sie in arm-example.c um.
In der Datei arm-example.c wird die folgende Struktur verwendet:
- Header-Dateien;
- Konstanten, die den Namen, die Firmware-Adresse, die Makroinitialisierung des Rechnernamens usw. darstellen;
- Funktion board_ram_size - zum Abrufen der Größe des verfügbaren RAM-Speichers auf der Karte;
- Funktion setup_boot - prüft, ob Firmware vorhanden ist und lädt den Kernel;
- Funktion arm_example_machine_init - Initialisierung der verschiedenen Geräte, die von diesem Rechner verwendet werden (z. B. SoC);
- Funktion arm_example_machine_class_common_init - deklariert die Namen und einige Variablen, die in der MachineClass Datenstruktur vorhanden sind;
- Funktion arm_example_machine_class_init - deklariert allgemeine Namen und Variablen in der MachineClass Datenstruktur;
- Constant arm_example_machine_types - deklariert Felder, die für die Registrierung der Maschine in der QEMU-Liste der Maschinen verwendet werden und verknüpft die Initialisierungsfunktionen von oben mit dem Maschinentyp;
- Aufruf der durch das DEFINE_TYPES-Makro definierten Funktion - wird verwendet, um die Maschine durch Aufruf der oben beschriebenen TypeInfo-Konstante an die Liste der Maschinen anzuhängen.
Im Folgenden werden die Schnipsel für diese Teile des Codes vorgestellt. Die Reihenfolge ist von unten nach oben.
- DEFINE_TYPES MACRO
DEFINE_TYPES(arm_example_machine_types)
- CONSTANT arm_example_machine_types
static const TypeInfo arm_example_machine_types[] = {
{
.name = MACHINE_TYPE_NAME("arm-example"),
.parent = TYPE_ARM_EXAMPLE_MACHINE,
.class_init = arm_example_machine_class_init,
},
{
.name = TYPE_ARM_EXAMPLE_MACHINE,
.parent = TYPE_MACHINE,
.instance_size = sizeof(ARMExampleMachineState),
.class_size = sizeof(ARMExampleMachineClass),
.abstract = true,
}
};
- FUNKTION arm_example_machine_class_init
static void arm_example_machine_class_init(ObjectClass *oc, void *data)
{
MachineClass *mc = MACHINE_CLASS(oc);
ARMExampleMachineClass *smc = ARM_EXAMPLE_MACHINE_CLASS(oc);
smc->board_rev = 0x920092;
arm_example_machine_class_common_init(mc, smc->board_rev);
};
- FUNKTION arm_example_machine_class_common_init
static void arm_example_machine_class_common_init (MachineClass *mc, uint32_t board_rev) {
mc->desc = "ARM EXAMPLE MACHINE (64-bit)";
mc->init = arm_example_machine_init;
mc->default_ram_size = board_ram_size(board_rev);
mc->default_ram_id = "ram";
};
- FUNKTION arm_example_machine_init
static void arm_example_machine_init(MachineState *machine) {
ARMExampleMachineClass *mc = ARM_EXAMPLE_MACHINE_GET_CLASS(machine);
ARMExampleMachineState *s = ARM_EXAMPLE_MACHINE(machine);
uint32_t board_rev = mc->board_rev;
uint32_t vcram_size;
uint64_t ram_size = board_ram_size(board_rev);
if (machine->ram_size != ram_size) {
char *size_str = size_to_str(ram_size);
error_report("Invalid RAM size, should be %s", size_str);
g_free(size_str);
exit(1);
}
memory_region_add_subregion_overlap(get_system_memory(), 0,
machine->ram, 0);
/* Setup the SoC */
object_initialize_child(OBJECT(machine), "soc", &s->soc, "arm-example-soc");
object_property_add_const_link(OBJECT(&s->soc), "ram", OBJECT(machine->ram));
object_property_set_int(OBJECT(&s->soc), "board-rev", board_rev, &error_abort);
qdev_realize(DEVICE(&s->soc), NULL, &error_fatal);
vcram_size = object_property_get_uint(OBJECT(&s->soc), "vcram-size",
&error_abort);
setup_boot(machine, 0,
machine->ram_size - vcram_size);
}
- FUNKTION setup_boot
static void setup_boot(MachineState *machine, int processor_id,
size_t ram_size)
{
ARMExampleMachineState *s = ARM_EXAMPLE_MACHINE(machine);
s->bdinfo.board_id = 0; // TODO: Change this, if required
s->bdinfo.ram_size = ram_size;
if (machine->firmware) {
int r = load_image_targphys(machine->firmware, FIRMWARE_ADDR, ram_size - FIRMWARE_ADDR);
if (r < 0) {
error_report("Failed to load firmware from %s", machine->firmware);
exit(1);
}
s->bdinfo.entry = FIRMWARE_ADDR;
s->bdinfo.firmware_loaded = true;
}
arm_load_kernel(&s->soc.cpu[0].core, machine, &s->bdinfo);
}
- FUNKTION board_ram_size
static uint64_t board_ram_size(uint32_t board_rev)
{
assert(FIELD_EX32(board_rev, REV_CODE, STYLE)); /* Only new style */
return 256 * MiB << FIELD_EX32(board_rev, REV_CODE, MEMORY_SIZE);
}
KONSTANTEN
#define BOARDSETUP_ADDR (ARM_EXAMPLE_RAM_START_ADDR + 0x20)
#define FIRMWARE_ADDR 0x8000 // TODO: Change this if needed
#define NAME_SIZE 20
FIELD(REV_CODE, REVISION, 0, 4);
FIELD(REV_CODE, TYPE, 4, 8);
FIELD(REV_CODE, PROCESSOR, 12, 4);
FIELD(REV_CODE, MANUFACTURER, 16, 4);
FIELD(REV_CODE, MEMORY_SIZE, 20, 3);
FIELD(REV_CODE, STYLE, 23, 1);
struct ARMExampleMachineState {
/*< private >*/
MachineState parent_obj;
/*< public >*/
ARMExample_SoCState soc;
struct arm_boot_info bdinfo;
// MemoryRegion ram;
};
typedef struct ARMExampleMachineState ARMExampleMachineState;
struct ARMExampleMachineClass {
/*< private >*/
MachineClass parent_obj;
/*< public >*/
uint32_t board_rev;
// MemoryRegion ram;
};
typedef struct ARMExampleMachineClass ARMExampleMachineClass;
#define TYPE_ARM_EXAMPLE_MACHINE MACHINE_TYPE_NAME("arm-example-common")
DECLARE_OBJ_CHECKERS(ARMExampleMachineState, ARMExampleMachineClass,
ARM_EXAMPLE_MACHINE, TYPE_ARM_EXAMPLE_MACHINE)
HEADER-DATEIEN
#include "qemu/osdep.h"
#include "qapi/error.h"
#include "hw/arm/arm-example.h"
#include "qemu/cutils.h"
#include "qemu/units.h"
#include "hw/registerfields.h"
#include "qemu/error-report.h"
#include "hw/arm/boot.h"
#include "hw/boards.h"
#include "qemu/module.h"
#include "qom/object.h"
#include "hw/loader.h"
Andere entsprechende Dateien sind folgende:
arm_example.h
arm_example_soc.h
arm_example_soc.c
arm_example_peripherals.h
arm_example_peripherals.c
- MODIFY arm_example.h
Es ist erforderlich, eine enum zu implementieren, das die Anzahl der CPUs, Geräte, Zeitgeber und anderer maschinenspezifischer Geräte enthält. Wir können es ARMExampleConfiguration nennen.
Es besteht auch Bedarf an einer enum, die die gesamte Speicherkarte des zu implementierenden Geräts enthält. Es wird empfohlen, die gesamte Memory Map zu implementieren, die im Referenzhandbuch der zu implementierenden Maschine oder Entwicklungsplatine angegeben ist. Wir können sie ARMExampleMemoryMap nennen.
Die letzte zu implementierende enum kann die IRQs darstellen. Wie bereits erwähnt, können die IRQs im Referenzhandbuch des jeweiligen Geräts nachgelesen werden. Wir können es ARMExampleIRQs nennen.
- MODIFY arm_example_soc.h
Diese Header-Datei enthält die Header-Dateien der Peripheriegeräte sowie der Steuerteile. Die Konstanten für den Namen der Maschine werden deklariert, und eine Struktur stellt den Zustand von SoC dar.
Sie können die oben erwähnte Struktur im nachstehenden Codeschnipsel sehen:
struct ARMExample_SoCState {
/*< private >*/
DeviceState parent_obj;
/*< public >*/
uint32_t enabled_cpus;
struct {
ARMCPU core;
} cpu[ARM_EXAMPLE_SOC_NCPUS];
BCM2835PeripheralState peripherals; // TODO: Change this implementation later
BCM2836ControlState control; // TODO: Change this implementation later
};
- MODIFY arm_example_soc.c
Diese Quelldatei konzentriert sich auf das Hinzufügen des SoC-Geräts zum QEMU-Gerätebaum.
Wenn es einem ähnlichen Pfad wie arm_example.c folgt und Typen, eine Typenstruktur, Initialisierungs- und Implementierungsfunktionen definiert sowie die Deklaration von Konstanten. Wie in dem anderen Beispiel für die Quelldatei wird der Durchgang von unten nach oben erfolgen.
DEFINE_TYPES macro:
DEFINE_TYPES(arm_example_types)
Constant arm_example_types:
static const TypeInfo arm_example_types[] = {
{
.name = TYPE_ARM_EXAMPLE_SOC,
.parent = TYPE_DEVICE,
.instance_size = sizeof(ARMExample_SoCState),
.instance_init = s32g_init,
.class_size = sizeof(ARMExample_SOCClass),
.class_init = s32g_class_init,
.abstract = false,
}
};
Funktion arm_example_class_init:
static void arm_example_class_init(ObjectClass *oc, void *data)
{
DeviceClass *dc = DEVICE_CLASS(oc);
ARMExample_SOCClass *sc = ARM_EXAMPLE_SOC_CLASS(oc);
// dc->user_creatable = false; // TODO: Eliminate this one, if needed
sc->cpu_type = ARM_CPU_TYPE_NAME("cortex-a53");
sc->core_count = ARM_EXAMPLE_NCPUS;
sc->peri_base = ARM_EXAMPLE_PERIPH_GR_0_ADDR; // peripheral base address
sc->clusterid = 0xf; // No idea from where is this one.
dc->realize = arm_example_soc_realize;
}
Funktion arm_example_soc_realize:
static void s32g_soc_realize(DeviceState *dev, Error **errp)
{
ARMExample_SoCState *s = ARM_EXAMPLE_SOC(dev);
if (!arm_example_soc_common_realize(dev, errp)) {
return;
}
if (!qdev_realize(DEVICE(&s->cpu[0].core), NULL, errp)) {
return;
}
/* Connect irq/fiq outputs from the interrupt controller. */
sysbus_connect_irq(SYS_BUS_DEVICE(&s->peripherals), 0,
qdev_get_gpio_in(DEVICE(&s->cpu[0].core), ARM_CPU_IRQ));
sysbus_connect_irq(SYS_BUS_DEVICE(&s->peripherals), 1,
qdev_get_gpio_in(DEVICE(&s->cpu[0].core), ARM_CPU_FIQ));
}
Funktion arm_example_soc_common_realize:
static bool arm_example_soc_common_realize(DeviceState *dev, Error **errp)
{
ARMExample_SoCState *s = ARM_EXAMPLE_SOC(dev);
// ARMExample_SOCClass *sc = ARM_EXAMPLE_SOC_GET_CLASS(dev);
Object *obj;
obj = object_property_get_link(OBJECT(dev), "ram", &error_abort);
/* START: UNCOMMENT WHEN PERIPHERALS IMPLEMENTED */
object_property_add_const_link(OBJECT(&s->peripherals), "ram", obj);
if (!sysbus_realize(SYS_BUS_DEVICE(&s->peripherals), errp)) {
return false;
}
// sysbus_mmio_map_overlap(SYS_BUS_DEVICE(&s->peripherals), 0,
// sc->peri_base, 1);
/* END: UNCOMMENT WHEN PERIPHERALS IMPLEMENTED */
return true;
}
Funktion arm_example_init:
static void arm_example_init(Object *obj)
{
ARMExample_SoCState *s = ARM_EXAMPLE_SOC(obj);
ARMExample_SOCClass *sc = ARM_EXAMPLE_SOC_GET_CLASS(obj);
int n;
for (n = 0; n < sc->core_count; n++)
{
object_initialize_child(obj, "cpu[*]", &s->cpu[n].core, sc->cpu_type);
}
if (sc->core_count > 1) {
qdev_property_add_static(DEVICE(obj), &arm_example_soc_enabled_cores_property);
qdev_prop_set_uint32(DEVICE(obj), "enabled-cpus", sc->core_count);
}
if (sc->ctrl_base) {
object_initialize_child(obj, "control", &s->control,
TYPE_BCM2836_CONTROL); // TODO: Change this, if required
}
object_initialize_child(obj, "peripherals", &s->peripherals,
TYPE_BCM2835_PERIPHERALS); // TODO: Change this, if required
object_property_add_alias(obj, "board-rev", OBJECT(&s->peripherals),
"board-rev");
object_property_add_alias(obj, "vcram-size", OBJECT(&s->peripherals),
"vcram-size");
}
Konstanten:
typedef struct ARMExample_SOCClass {
/*< private >*/
DeviceClass parent_class;
/*< public >*/
const char *name;
const char *cpu_type;
unsigned core_count;
hwaddr peri_base;
hwaddr ctrl_base;
int clusterid;
} ARMExample_SOCClass;
#define ARM_EXAMPLE_SOC_CLASS(klass)
OBJECT_CLASS_CHECK(ARMExample_SOCClass, (klass), TYPE_ARM_EXAMPLE_SOC)
#define ARM_EXAMPLE_SOC_GET_CLASS(obj)
OBJECT_GET_CLASS(ARMExample_SOCClass, (obj), TYPE_ARM_EXAMPLE_SOC)
static Property arm_example_soc_enabled_cores_property =
DEFINE_PROP_UINT32("enabled-cpus", ARMExample_SoCState, enabled_cpus, 0);
Header-Dateien:
#include "qemu/osdep.h"
#include "qapi/error.h"
#include "qemu/module.h"
#include "hw/arm/arm_example_soc.h"
#include "hw/arm/arm_example.h" // Maybe this one could and should be replaced
#include "hw/sysbus.h"
#include "hw/qdev-properties.h"
- MODIFY arm_example_peripherals.h
In der folgenden Datei haben wir uns für eine vereinfachte Implementierung der Datei include/hw/arm/bcm2835_peripherals.h entschieden, vor allem wenn es um die enthaltenen Header geht Es besteht die Möglichkeit, entweder eigene arm_example_ic.h, arm_example_systmr.h, arm_example_fb.h und arm_example_mbox.h zu implementieren oder die ursprünglichen Header der eingangs erwähnten Originalimplementierung zu verwenden.
- MODIFY arm_example.h
ARMExamplePeripheralState struct:
struct ARMExamplePeripheralState {
/* < private > */
SysBusDevice parent_obj;
/* < public > */
MemoryRegion peri_mr, peri_mr_alias, gpu_bus_mr, mbow_mr;
MemoryRegion ram_alias[4];
qemu_irq irq, fiq;
ARMExampleICState ic;
ARMExampleSystemTimerState systmr;
ARMExampleMboxState mboxes;
ARMExampleFBState fb;
};
Konstanten:
#define TYPE_ARM_EXAMPLE_PERIPHERALS "arm-example-peripherals"
OBJECT_DECLARE_SIMPLE_TYPE(ARMExamplePeripheralState, ARM_EXAMPLE_PERIPHERALS)
Headers:
#include "hw/sysbus.h"
#include "exec/memory.h"
#include "qom/object.h"
#include "hw/intc/arm_example_ic.h"
#include "hw/timer/arm_example_systmr.h"
#include "hw/misc/unimp.h"
#include "hw/display/arm_example_fb.h"
#include "hw/misc/arm_example_mbox.h"
- MODIFY arm_example_peripherals.c
- type_init macro:
type_init(arm_example_peripherals_register_types)
- Funktion arm_example_peripherals_register_types:
static void arm_example_peripherals_register_types(void)
{
type_register_static(&arm_example_peripherals_type_info);
}
- Constant arm_example_peripherals_type_info:
static const TypeInfo arm_example_peripherals_type_info = {
.name = TYPE_ARM_EXAMPLE_PERIPHERALS,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(ARMExamplePeripheralState),
.instance_init = arm_example_peripherals_init,
.class_init = arm_example_peripherals_class_init,
};
- Funktion arm_example_peripherals_class_init:
static void arm_example_peripherals_class_init (ObjectClass *oc, void *data)
{
DeviceClass *dc = DEVICE_CLASS(oc);
dc->realize = arm_example_peripherals_realize;
}
- Funktion arm_example_peripherals_class_init:
static void arm_example_peripherals_realize (DeviceState *dev, Error **errp)
{
ARMExamplePeripheralState *s = ARM_EXAMPLE_PERIPHERALS(dev);
Object *obj;
MemoryRegion *ram;
Error *err = NULL;
uint64_t ram_size, vcram_size;
int n;
obj = object_property_get_link(OBJECT(dev), "ram", &error_abort);
ram = MEMORY_REGION(obj);
ram_size = memory_region_size(ram);
/* Interrupt Controller */
if (!sysbus_realize(SYS_BUS_DEVICE(&s->ic), errp)) {
return;
}
/* Sys Timer */
if (!sysbus_realize(SYS_BUS_DEVICE(&s->systmr), errp)) {
return;
}
/* Framebuffer */
vcram_size = object_property_get_uint(OBJECT(s), "vcram-size", &err);
if (err) {
error_propagate(errp, err);
return;
}
if (!object_property_set_uint(OBJECT(&s->fb), "vcram-base",
ram_size - vcram_size, errp)) {
return;
}
if (!sysbus_realize(SYS_BUS_DEVICE(&s->fb), errp)) {
return;
}
memory_region_add_subregion(&s->mbox_mr, MBOX_CHAN_FB << MBOX_AS_CHAN_SHIFT,
sysbus_mmio_get_region(SYS_BUS_DEVICE(&s->fb), 0));
sysbus_connect_irq(SYS_BUS_DEVICE(&s->fb), 0,
qdev_get_gpio_in(DEVICE(&s->mboxes), MBOX_CHAN_FB));
}
- Function arm_example_peripherals_init:
static void arm_example_peripherals_init (Object *obj)
{
ARMExamplePeripheralState *s = ARM_EXAMPLE_PERIPHERALS(dev);
/* Memory region for peripheral devices, which we export to our parent */
memory_region_init(&s->peri_mr, obj,"arm-example-peripherals", 0x1000000);
// TODO: Change this line above, if needed
sysbus_init_mmio(SYS_BUS_DEVICE(s), &s->peri_mr);
/* Interrupt Controller */
object_initialize_child(obj, "ic", &s->ic, TYPE_ARM_EXAMPLE_IC);
/* SYS Timer */
object_initialize_child(obj, "systimer", &s->systmr,
TYPE_ARM_EXAMPLE_SYSTIMER);
}
#include "qemu/osdep.h"
#include "qapi/error.h"
#include "qemu/module.h"
#include "hw/arm/arm_example_peripherals.h"
#include "hw/arm/arm_example.h"
#include "sysemu/sysemu.h"
Header-Dateien:
#include "qemu/osdep.h"
#include "qapi/error.h"
#include "qemu/module.h"
#include "hw/arm/arm_example_peripherals.h"
#include "hw/arm/arm_example.h"
#include "sysemu/sysemu.h"
Auftretende Probleme
Wenn Sie ein erfahrener Programmierer sind, sind Sie wahrscheinlich von Zeit zu Zeit auf Inkompatibilitäten bei den Zeilenenden gestoßen. Das Problem tritt auf, wenn Sie Code auf einem Windows-Host in Visual Studio-Code ändern und den QEMU-Code in einem Linux-Container ausführen möchten. Es wird einige Fehlermeldungen über '/r'-Inkompatibilitäten geben.
Tests und Ergebnisse
Testen
Wir können die Tests entweder mit GDB oder mit einem Kernel durchführen und QEMU normal ausführen, wobei der spezifische Rechner aktiviert ist. Bei der Verwendung eines Kernel-Images ist es wichtig, das richtig kompilierte Image zu verwenden, das die Speicherzuordnung und die IRQs der implementierten Geräte berücksichtigt.
Ergebnisse
Wenn Sie die zuvor vorgestellten Schritte befolgen und die zugehörigen Materialien verwenden, sollte ein erfahrener Programmierer in der Lage sein, eine voll funktionsfähige ARM-basierte Maschine in QEMU zu implementieren.
Schlussfolgerung
Auch wenn es nicht ideal ist, gibt es eine Alternative zu echter Hardware, insbesondere in Zeiten, in denen die Lieferkette für Chips schwierig ist oder sogar das Problem auftritt, dass das physische Gerät nicht in der Produktion vorhanden ist.
Zusammenfassend lässt sich sagen, dass die Aufgabe, die vor uns liegt, einschüchternd erscheinen mag: die Virtualisierung einer tatsächlichen Maschine zu implementieren und ihre realen Auswirkungen in einer virtuellen Umgebung zu emulieren, aber diese Abstraktion sollte den Weg für eine noch größere Anerkennung von QEMU als wertvolles Werkzeug ebnen.
Wenn die Idee der Hardware-Emulation populär genug ist, könnte es nützlich sein, ein generisches Tool zu entwickeln, das die Möglichkeit bietet, zumindest die gängigen Teile von ARM-Maschinen auf einfachere Weise zu implementieren als durch Kodierung oder mit einer Code-als-Dokumentation-Situation wie im Fall des QEMU-Quellcodes umzugehen. Es besteht die Möglichkeit, dass sogar eine GUI-Lösung die Innovation im Embedded-Bereich noch weiter beschleunigen könnte.
Referenzen
S. Sopha, Adding a custom ARM platform to QEMU 5.2.0, 2021. Available: Adding a custom ARM platform to QEMU 5.2.0