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:
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.
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.
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.
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.
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.
GNAT Bus connection
There are two ways to connect a device to GNAT Emulator
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
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:
Register |
Offset |
---|---|
UART Control |
0x0 |
UART Data |
0x4 |
The following tables describe the fields of each 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 |
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