.. _extending-gnatemulator: Extending GNATemulator ********************** .. |GNATemulator| replace:: **GNAT Emulator** .. |GNATBus| replace:: **GNAT Bus** Introduction ============ |GNATemulator| 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 |GNATemulator| an exact representation of your target platform. |GNATBus| ========= Overview -------- |GNATBus| is the link between your simulation environment and the emulator. You can regard |GNATBus| 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 |GNATBus| devices are just like any other emulated peripheral. |GNATBus| 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. .. image:: gnatbus_graphs/gnatbus_sim_env.png :alt: |GNATBus| :align: center |GNATBus| connection -------------------- There are two ways to connect a device to |GNATemulator| #. **Named connection** In this mode the communication between the device and |GNATemulator| is done through a named connection (Unix Domain socket on Linux and Named pipe on Windows). On the device side use: .. code-block:: c register_device_named(dev, "@my_device"); On command line: .. code-block:: bash $ gnatemu --gnatbus=@my_device guest_uart #. **TCP connection** In this mode the communication between the device and |GNATemulator| is done trough a TCP socket. On the device side use: .. code-block:: c register_device_tcp(dev, 8032); On command line: .. code-block:: bash $ gnatemu --gnatbus=localhost:8032 guest_uart Tutorial: Create A |GNATBus| Device ----------------------------------- To show how to use |GNATBus|, 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 ``/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: .. csv-table:: UART registers :header: "Register", "Offset" "UART Control", "0x0" "UART Data", "0x4" The following tables describe the fields of each register: .. csv-table:: UART Control register :header: "Bit number(s)", "Field name", "Reset state", "Access", "Description" :widths: 15 17 13 10 45 "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", "", "" .. csv-table:: UART Data register :header: "Bit number(s)", "Field name", "Reset state", "Access", "Description" :widths: 15 17 13 10 45 "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 |GNATemulator|, it contains the low-level circuitery (connection and communication with |GNATemulator|) 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`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: c 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 |GNATemulator|'s examples directory (``/share/examples/gnatemu/gnatbus/uart/native``). ``package UART_Device`` ^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: c 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. .. code-block:: ada 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 |GNATBus| and how they are implemented in our UART example. Device_Setup ```````````` .. code-block:: ada 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``: .. code-block:: ada ------------------ -- 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 ``````````` .. code-block:: ada 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``: .. code-block:: ada ----------------- -- 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 ```````````` .. code-block:: ada 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``: .. code-block:: ada ------------------ -- 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 ``````````` .. code-block:: ada 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``: .. code-block:: ada ----------------- -- 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 ``````` .. code-block:: ada 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``: .. code-block:: ada ------------- -- 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 ```````` .. code-block:: ada 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``: .. code-block:: ada -------------- -- 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 ^^^^^^^^^^^^^^ .. code-block:: c 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. .. code-block:: ada 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. .. code-block:: bash # Add GNATBus's project files directory in GPR_PROJECT_PATH $ export GPR_PROJECT_PATH=/share/gpr:$GPR_PROJECT_PATH # And run gprbuild $ cd native $ gprbuild -Puart.gpr -XGNATEMU_BOARD= We also have to build the guest executable. .. code-block:: bash $ cd guest_code $ gprbuild --target= --RTS= -Pguest_uart.gpr -XGNATEMU_BOARD= Device connection and execution ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To set up your simulation environment, you first have to start the device .. code-block:: bash $ ./gnatbus_uart and then in another terminal, start |GNATemulator| with the |GNATBus| switch and a comma-separated list of "hostname:port" items. Our device uses port 8032. .. code-block:: bash $ -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. .. image:: gnatbus_graphs/gnatbus_shared_memory.png :alt: |Shared Memory| :align: center 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``: .. code-block:: ada ------------------ -- 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``: .. code-block:: ada -------------- -- 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;