Extending GNATemulator

Introduction

GNAT Emulator provides a powerful interface to emulate your own devices and create a rich simulation environment.

With native simulation code communicating with the target through a socket, you will be able to emulate any piece of hardware to make GNAT Emulator an exact representation of your target platform.

GNAT Bus

Overview

GNAT Bus is the link between your simulation environment and the emulator. You can regard GNAT Bus as the simulation of an internal bus (such as AMBA or PCI) connected to the emulated platform through a bridge.

From the guest-executable point of view, the GNAT Bus devices are just like any other emulated peripheral.

GNAT Bus provides four main features:

  1. Memory mapped IO

    Devices can register memory mapped IO areas in the emulated address space. Each load/store instruction executed by the CPU in an IO area will result in a call to the read/write callback of the corresponding device.

    This can be used to share data structure between guest executable and the host environment.

  2. Direct Memory Access

    With Direct Memory Access (DMA) a device can read/write directly from/to the emulated memory.

    This is useful to transfer large amount of data from/to the guest program.

  3. Host Shared Memory (Available on Linux only)

    Devices can register a shared host memory and map it in the emulated address space. Each load/store instruction executed by the CPU in that area will be directly written in the shared memory. The device is able to use those data. This allows faster communications between the virtual machine and the device.

  4. Interrupt

    From the device you can trigger interrupts on the emulated system.

    • Raise interrupt line

    • Lower interrupt line

    • Pulse i.e. quickly raise and lower interrupt line

    Using the interrupt is thread safe, which means that the device can trigger an asynchronous IRQ at any time.

  5. Event

    You can also create a timer running in the emulation time. When the timer expires, the emulation stops and an callback is executed in the device code.

|GNATBus|

GNAT Bus connection

There are two ways to connect a device to GNAT Emulator

  1. Named connection

    In this mode the communication between the device and GNAT Emulator is done through a named connection (Unix Domain socket on Linux and Named pipe on Windows).

    On the device side use:

    register_device_named(dev, "@my_device");
    

    On command line:

    $ gnatemu --gnatbus=@my_device guest_uart
    
  2. TCP connection

    In this mode the communication between the device and GNAT Emulator is done trough a TCP socket.

    On the device side use:

    register_device_tcp(dev, 8032);
    

    On command line:

    $ gnatemu --gnatbus=localhost:8032 guest_uart
    

Tutorial: Create A GNAT Bus Device

To show how to use GNAT Bus, we will define and emulate a UART controller. For simplicity, the controller will only be able to receive data.

You can write device code in C or Ada. This tutorial uses an Ada example but you can find the equivalent C example in <PATH_TO_GNATEMULATOR>/share/examples/gnatemu/gnatbus/).

Interface definition

First, we have to define the interface of our device.

The registers implemented in the UART controller are listed in the following table. The address of each register is defined as an offset to the base address:

UART registers

Register

Offset

UART Control

0x0

UART Data

0x4

The following tables describe the fields of each register:

UART Control register

Bit number(s)

Field name

Reset state

Access

Description

0

Enable_Interrupt

0

R/W

If set an interrupt will be triggered for each character received

1

Data_To_Read

0

R

Set if there is at least one character to read

2 - 31

Reserved

undefined

UART Data register

Bit number(s)

Field name

Reset state

Access

Description

0 - 7

Data

0

R

Read received character when Data_To_Read is set, 0 otherwise.

8 - 31

Reserved

undefined

Project environment setup

In order to explicit the separation, it’s easier to split the project into two directory:

  • native/ containing the project for GNATBus device, run on the host.

  • guest_code/ containing the project for the guest part, run inside GNATEmulator.

Our project directory tree will look like

uart/
 |-- guest_code/
      |-- src/
 `-- native/
      |-- src/

We create a project file uart/native/uart.gpr, with the following content (see the GPRBuild documentation for detailed information on project files):

with "gnatbus_ada.gpr";

project UART is

  for Languages    use ("Ada");
  for Source_Dirs  use ("src", "../../helpers");
  for Object_Dir   use "obj";
  for Exec_Dir     use ".";
  for Main         use ("main.adb");

  package Naming is
     for Spec("Board_Parameters") use
       "board_parameters-" & external ("GNATEMU_BOARD") & ".ads";
  end Naming;

  package Compiler is
     for Default_Switches ("Ada") use ("-gnaty", "-gnatwa");
  end Compiler;

  package Builder is
     for Executable ("main.adb") use "gnatbus_uart";
  end Builder;

  package Linker is
     for Required_Switches use GnatBus_Ada.Required_Linker_Switches;
  end Linker;
end UART;

gnatbus_ada.gpr is a project distributed with GNAT Emulator, it contains the low-level circuitery (connection and communication with GNAT Emulator) and provides an abstaction layer so you just have to focus on the simulation code.

helpers/, available under GNATBus example directory, contains a set of default values in order to ease the portability across boards.

GNATEMU_BOARD is the board you wish to target. This will fetch the correct helper files stored above.

package UART_Controller

uart/native/src/uart_controller.ads
uart/native/src/uart_controller.adb

This package implements a UART_Control protected object that contains the logic of our device (receive characters, manage the FIFO list, set the Data_To_Read flag, trigger interrupt when needed).

We will not go through the details of the UART_Controller since those are outside the scope of this tutorial. But you can find sources of this package in GNAT Emulator’s examples directory (<PATH_TO_GNATEMULATOR>/share/examples/gnatemu/gnatbus/uart/native).

package UART_Device

uart/native/src/uart.ads
uart/native/src/uart.adb

To implement our UART device we create a class that inherits from the Bus_Device abstract class.

type UART_Device (Vendor_Id, Device_Id : Id;
                  Base_Address         : Bus_Address;
                  Port                 : Integer)
   is new Bus_Device (Vendor_Id, Device_Id, Port, Native_Endian) with record

   UC : UART_Control;
   --  The UART_Control protected object described earlier
   Connected : Boolean := False;
   --  A boolean storing the status of the connection

end record;

The Vendor_Id, Device_Id and Port discriminants are required by the Bus_Device abstract type. Base_Address will be used latter as the address of our I/O area.

The device will have to implement six subprograms to provide the required interface:

  • Device_Setup

  • Device_Init

  • Device_Reset

  • Device_Exit

  • IO_Read

  • IO_Write

Let’s look in detail how these are used by GNAT Bus and how they are implemented in our UART example.

Device_Setup
overriding procedure Device_Setup (Self : in out UART_Device);

This subprogram has to register the I/O area(s) and perform any other initialization needed before the device is started.

Body of Device_Setup procedure for UART_Device:

------------------
-- Device_Setup --
------------------

procedure Device_Setup (Self : in out UART_Device) is
begin
   Ada.Text_IO.Put_Line ("Device_Setup");

   --  Register the only I/O area: 8 bytes at base address to match the two
   --  registers.

   Self.Register_IO_Memory (Self.Base_Address, 8);

   --  Set UART_Device access in the UART_Control protected object

   Self.UC.Set_Device (Self'Unchecked_Access);
end Device_Setup;
Device_Init
overriding procedure Device_Init (Self : in out UART_Device);

As implied by its name, this subprogram has to perform device initialization. It will be called only once, at the beginning of emulation, when the emulator have been connected. Therefore, we can use it to update our Connected field.

Body of Device_Init procedure for UART_Device:

-----------------
-- Device_Init --
-----------------

procedure Device_Init (Self : in out UART_Device) is
   pragma Unreferenced (Self);
begin
   Ada.Text_IO.Put_Line ("Device_Init");
   Self.Connected := True;
end Device_Init;
Device_Reset
overriding procedure Device_Reset (Self : in out UART_Device);

This procedure will be called each time a CPU reset occurs in the emulator. A reset is also triggered at the beginning of emulation (after Device_Init).

In our example, we have to flush the FIFO queue and set the registers to their reset value (this is handled by UART_Control).

Body of Device_Reset procedure for UART_Device:

------------------
-- Device_Reset --
------------------

procedure Device_Reset (Self : in out UART_Device) is
begin
   Ada.Text_IO.Put_Line ("Device_Reset");

   --  Send the reset signal to the UART_Control

   Self.UC.Reset;
end Device_Reset;
Device_Exit
overriding procedure Device_Exit (Self : in out UART_Device);

Device_Exit is called one time, at the end of emulation.

In our example there is nothing to do.

Body of Device_Exit procedure for UART_Device:

-----------------
-- Device_Exit --
-----------------

procedure Device_Exit (Self : in out UART_Device) is
   pragma Unreferenced (Self);
begin
   Ada.Text_IO.Put_Line ("Device_Exit");
end Device_Exit;
IO_Read
overriding procedure IO_Read (Self    : in out UART_Device;
                              Address : Bus_Address;
                              Length  : Bus_Address;
                              Value   : out Bus_Data);
--  Address : Bus_Address
--     Absolute address of the first byte targeted by this read operation.
--
--  Length : Bus_Address
--     Number of bytes targeted by this read operation (1, 2 or 4).

This procedure will be called when the CPU executes a load instruction in any of the I/O areas registered by the device. The procedure must set Value according to the specification of the emulated device.

The procedure is usually implemented with a case statement with branches for each register.

Body of IO_Read procedure for UART_Device:

-------------
-- IO_Read --
-------------

procedure IO_Read (Self    : in out UART_Device;
                   Address : Bus_Address;
                   Length  : Bus_Address;
                   Value   : out Bus_Data) is

   pragma Unreferenced (Length);
begin
   --  Ada.Text_IO.Put_Line ("Read @ " & Address'Img);

   --  case statement on the relative address

   case Address - Self.Base_Address is
      when 0 =>
         --  Return value of the control register
         Value := Self.UC.Get_CTRL;

      when 4 =>
         --  Pop a byte from FIFO queue
         Self.UC.Pop_DATA (Value);

      when others =>
         Ada.Text_IO.Put_Line ("Read unknown register:" & Address'Img);
         Value := 0;
   end case;
end IO_Read;
IO_Write
overriding procedure IO_Write (Self    : in out UART_Device;
                               Address : Bus_Address;
                               Length  : Bus_Address;
                               Value   : Bus_Data);
--  Address : Bus_Address
--     Absolute address of the first byte targeted by this write operation.
--
--  Length : Bus_Address
--     Number of bytes targeted by this write operation (1, 2 or 4).

This procedure is the equivalent of Read_IO when store instructions are executed.

Body of IO_Write procedure for UART_Device:

--------------
-- IO_Write --
--------------

procedure IO_Write (Self    : in out UART_Device;
                    Address : Bus_Address;
                    Length  : Bus_Address;
                    Value   : Bus_Data) is

   pragma Unreferenced (Length);
begin
   Ada.Text_IO.Put_Line ("Write @ " & Address'Img);

   --  case statement on the relative address

   case Address - Self.Base_Address is
      when 0 =>
         --  Set Control register value
         Self.UC.Set_CTRL (Value);

      when others =>
         Ada.Text_IO.Put_Line ("Write unknown register:" & Address'Img);
   end case;
end IO_Write;

Main procedure

uart/native/src/main.adb

Finally, we need a main procedure to allocate and start our device. We also include a loop that sends a message to the UART every second.

with UART; use UART;
with Ada.Text_IO;

procedure Main is
   My_UART : UART.UART_Ref;
begin
   My_UART := new UART.Uart_Device
     (16#ffff_ffff#,                --  Vendor_Id
      16#aaaa_aaaa#,                --  Device_Id
      Board_Parameters.Addr,        --  Base Address
      8032);                        --  TCP Port

   --  Start the Device loop

   My_UART.Start;

   --  Wait for the connection

   while not My_Uart.Connected loop
       delay 1.0;
   end loop;

   Ada.Text_IO.Put_Line ("Start Simulation");

   --  Send three messages

   For Cnt in 1 .. 3 loop
       My_UART.UC.Put ("Send Message: " & Cnt'Img & ASCII.LF);
       delay 1.0;
   end loop;

   --  Request the target to shutdown

   Ada.Text_IO.Put_Line ("Stop Simulation");

   My_Uart.Shutdown_Request;

   -- Stop the device and exit

   My_Uart.Wait_Termination;

end Main;

The device’s TCP port is hardcoded to 8032 while the base address is retrieved inside the helper files.

Compilation

With all the source files prepared (main.adb, uart.adb, uart.ads, uart_controller.adb and uart_controller.ads) we can build build the UART device program.

# Add GNATBus's project files directory in GPR_PROJECT_PATH
$ export GPR_PROJECT_PATH=<PATH_TO_GNATEMULATOR>/share/gpr:$GPR_PROJECT_PATH
# And run gprbuild
$ cd native
$ gprbuild -Puart.gpr -XGNATEMU_BOARD=<board>

We also have to build the guest executable.

$ cd guest_code
$ gprbuild --target=<target> --RTS=<runtime>
    -Pguest_uart.gpr -XGNATEMU_BOARD=<board>

Device connection and execution

To set up your simulation environment, you first have to start the device

$ ./gnatbus_uart

and then in another terminal, start GNAT Emulator with the GNAT Bus switch and a comma-separated list of “hostname:port” items.

Our device uses port 8032.

$ <target>-gnatemu --gnatbus=localhost:8032 guest_uart

Sharing some host memory with the guest

Simulating some devices might require a lot of data to be loaded / stored. To improve performance GNATBus allow to map in the guest address space some host memory. Hence there are less operations to share large chunk of memory.

|Shared Memory|

Modifying the uart to output 1K of data in one shot

Mapping the memory is quite easy in the above GNATBus Device’s Device_Setup it only requires to register a shared memory.

Body of Device_Setup procedure for UART_Device:

------------------
-- Device_Setup --
------------------

procedure Device_Setup (Self : in out UART_Device) is
begin
   Ada.Text_IO.Put_Line ("Device_Setup");

   --  Register the only I/O area: 8 bytes at base address to match the two
   --  registers.

   Self.Register_IO_Memory (Self.Base_Address, 8);

   --  Register a 1K shared memory area called "/foo": it is directly
   --  mapped at 0x80002000 in the guest address space.

   Self.Register_Shared_Memory (16#80002000#, 1024, "foo");

   --  Set UART_Device access in the UART_Control protected object

   Self.UC.Set_Device (Self'Unchecked_Access);
end Device_Setup;

The data is accessible on the device side as well. for example on a register write:

Body of IO_Write procedure for UART_Device:

--------------
-- IO_Write --
--------------

procedure IO_Write (Self    : in out UART_Device;
                    Address : Bus_Address;
                    Length  : Bus_Address;
                    Value   : Bus_Data) is

   --  Reading / Writing to Shm write to the host memory directly.
   type Shm_Array is array (1 .. 1024) of aliased Interfaces.Unsigned_8;
   Shm : Shm_Array;
   pragma Suppress_Initialization (Shm);
   -- Calling this synchronize the Shm.
   for Shm'Address use Self.Shm_Map (Id => 0);

   pragma Unreferenced (Length);
begin
   --  Ada.Text_IO.Put_Line ("Write @ " & Address'Img);

   --  case statement on the relative address

   case Address - Self.Base_Address is
      when 0 =>
         --  Set Control register value
         Self.UC.Set_CTRL (Value);
      when 4 =>
         --  Synchronize the Shm to ensure that all the write from the
         --  simulator are synchronized. This is implemented as no-op on
         --  modern OS.. but is not guaranted to be optional by the
         --  documentation.
         Self.Shm_Sync(Id => 0);

         for U in Shm'First .. Shm'Last loop
             --  Do something with the data..
         end loop;
      when others =>
         Ada.Text_IO.Put_Line ("Write unknown register:" & Address'Img);
   end case;
end IO_Write;