AdaCore Blog

Enhancing Ada Embedded Development: The Power of Real-Time Logging with RTT

by Maxim Reznik

Developing in Ada, like any other programming language, demands diverse tools to ensure convenience and productivity. One crucial aspect of embedded software development is the mechanism for tracking program execution, or logging, which enables real-time monitoring and analysis of application behavior. Logging plays a pivotal role in error detection and resolution, performance optimization, and system security.

There are various logging methods, such as using UART (serial port) and SWO (Serial Wire Output), widely employed in different embedded systems. However, these methods require specific hardware components both on the target device and the debugger. On the other hand, there's a method that doesn't rely on specific hardware capabilities and ensures real-time data transmission with low latency and high efficiency.

The idea behind this method is simple: the protocol data is stored in a ring buffer on the target device and read by the debugger without interrupting the program execution. Since the debugging board is already connected to the debugger, no additional devices or connections are needed. This universal method is platform-independent; all that is required is a debugger capable of reading and writing the content of the target device's memory without halting the CPU.

This method is supported by SEGGER debug probes and software and is known as RTT (Real-Time Transfer). The popular embedded systems debugging tool, OpenOCD, started supporting RTT from version 0.11. Let's explore how RTT can be utilized in Ada programming.

The RTT method allows the simultaneous transmission of multiple data streams. Each stream has its own buffer and transfers data in one direction: either from the target device to the host or from the host to the target device. As long as the host can read data while keeping the buffer from overflowing, the program continues to execute as usual. There are several buffer modes indicating what happens when the buffer overflows. Depending on the settings, the program either ignores new data, overwrites the oldest buffer data, or pauses execution until space becomes available in the buffer. To describe the buffer, the following data types are used:

type Operating_Mode is (No_Block_Skip, No_Block_Trim, Block_If_FIFO_Full);

type Buffer_Flags is record
   Reserved : Natural range 0 .. 0 := 0;
   Mode 	: Operating_Mode := No_Block_Skip;
end record with Size => 32;

for Buffer_Flags use record
   Reserved at 0 range 2 .. 31;
   Mode 	at 0 range 0 .. 1;
end record;

type RTT_Buffer is limited record
   Name   : System.Address := System.Null_Address;
   --  Buffer’s name such as "Terminal" or "SysView".
   Buffer : System.Address := System.Null_Address;
   --  Buffer pointer.
   Size   : Interfaces.C.unsigned := 0;
   --  Size of the buffer in bytes.
   Write_Offset : Interfaces.C.unsigned := 0 with Atomic;
   --  Next byte to be written
   Read_Offset : Interfaces.C.unsigned := 0 with Atomic;
   --  Next byte to be read
   Flags  : Buffer_Flags;
end record;

To inform the debugger about the number of buffers we are using and how to locate them, we gather the corresponding information in a control block. The control block also stores a signature, making it easy to find in memory.

type RTT_Buffer_Array is array (Positive range <>) of RTT_Buffer;

type RTT_Control_Block
  (Max_Up_Buffers   : Natural;
   Max_Down_Buffers : Natural) is
limited record
   ID   : Interfaces.C.char_array (1 .. 16) :=
           "SEGGER RTT" & (1 .. 6 => Interfaces.C.nul);
   --  Predefined control block identifier value
   Up   : RTT_Buffer_Array (1 .. Max_Up_Buffers);
   Down : RTT_Buffer_Array (1 .. Max_Down_Buffers);
end record;

for RTT_Control_Block use record
   ID               at 0 range 0 .. 128 - 1;
   Max_Up_Buffers   at 16 range 0 .. 32 - 1;
   Max_Down_Buffers at 20 range 0 .. 32 - 1;
end record;

Let's create one 256-byte buffer for logging (data transmission from the board to the host):

Terminal : constant Interfaces.C.char_array :=
  ("Terminal" & Interfaces.C.nul);

Terminal_Output : HAL.UInt8_Array (1 .. 256);

Control_Block : aliased RTT_Control_Block :=
  (Max_Up_Buffers   => 1,
   Max_Down_Buffers => 0,
    Up              =>
       (1 => (Name   => Terminal'Address,
              Buffer => Terminal_Output'Address,
              Size   => Terminal_Output'Length,
              others => <>)),
    others => <>);

The simplest version of the write subroutine implementing the overwrite strategy looks like this:

procedure Write
  (Block : in out Control_Block;
   Index : Positive;
   Data  : HAL.UInt8_Array)
is
   use type Interfaces.C.unsigned;

   type Unbounded_UInt8_Array is
      array (0 .. Interfaces.C.unsigned'Last) of HAL.UInt8;
   --  The constrained type to avoid bounds checking

   Buffer : RTT_Buffer renames Block.Up (Index);

   Target : Unbounded_UInt8_Array
       with Import, Address => Buffer.Buffer;

   Left   : Interfaces.C.unsigned;
   From   : Natural := Data'First;
   Length : Natural;

   Write_Offset : Interfaces.C.unsigned := Buffer.Write_Offset;
begin
   while From <= Data'Last loop
      Left := Buffer.Size - Buffer.Write_Offset;
      Length := Natural'Min (Data'Last - From + 1, Natural (Left));

      for J in 1 .. Length loop
         Target (Write_Offset) := Data (From);
         Write_Offset := Write_Offset + 1;
         From := From + 1;
      end loop;

      if Write_Offset >= Buffer.Size then
         Write_Offset := 0;
      end if;

      Buffer.Write_Offset := Write_Offset;
   end loop;
end Write;

And its usage can be recorded as follows:

Write
  (Control_Block,
   Index => 1,
   Data  => (Character'Pos ('H'),
             Character'Pos ('e'),
             Character'Pos ('l'),
             Character'Pos ('l'),
             Character'Pos ('o'),
             16#0D#,
             16#0A#));

Compile the executable file and find the Control_Block address in the map.txt file. This address will be needed for debugger configuration (we use 0x2001234 as an example). In fact, there is no need to specify the exact address of the block. If you specify the entire RAM range, it will work just fine. OpenOCD will find the unique identifier "SEGGER RTT" and discover the Control_Block based on it.

To receive data on the host, OpenOCD 0.11 is used. By connecting to OpenOCD with GDB, then RTT can be enabled using the following commands:

# specify the address and size of the memory area where the control block is located
monitor rtt setup 0x2001234 64 "SEGGER RTT"
# start RTT
monitor rtt start
# set up TCP port 9090 to receive stream 0
monitor rtt server start 9090 0

Now, logging data can be obtained by connecting to port 9090, for example, using the telnet program:

$ telnet localhost 9090
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

Hello

It is important to emphasize that this method allows for the transmission of both textual and binary data. You can flexibly configure the sizes, quantity, and direction of data transmission channels. Since the data transmission occurs by simple memory copying, logging can even be used from interrupt handlers. The method enables the creation of a virtual data transmission channel utilizing any binary protocol, such as MAVLink for drone control. If necessary, you can even create a frame buffer and display graphical information by writing a basic visualization program on the host side.

Cor­tex Debug — VS Code exten­sion for ARM Cortex‑M Micro­con­trollers #

Integrated development environments also support RTT. For example, let's consider Cortex Debug, an extension for VS Code. With rich features and customizable flexibility, Cortex Debug supports RTT when using OpenOCD and J-Link. Data received from the RTT stream can be displayed both in textual form and using the built-in graph plotter.

RTT data in the graph plotter

There is an option to decode non-standard types of transmitted data using a custom JavaScript function. As an example, let's provide a Cortex Debug configuration file for STM32F4X:

{
  "configurations": [
    {
      "executable": "${workspaceFolder}/.obj/main",
      "name": "Debug with OpenOCD",
      "request": "launch",
      "type": "cortex-debug",
      "servertype": "openocd",
      "configFiles": ["interface/stlink.cfg", "target/stm32f4x.cfg"],
      "searchDir": [],
      "runToEntryPoint": "main",
      "showDevDebugOutput": "none",
      "rttConfig": {
        "enabled": true,
        "address": "auto",
        "decoders": [
          {
            "port": 0,
            "type": "console"
          }
        ]
      }
    }
  ]
}

It is worth noting that the extension can independently locate the control block if it has an external name "_SEGGER_RTT" in the debugging information.

In conclusion, SEGGER's RTT method provides a flexible and efficient solution for real-time logging and data transmission during Ada development. It allows for the transmission of textual and binary information with minimal latency, without interrupting the program and without requiring additional hardware. RTT ensures versatility and ease of use, making it a vital tool for debugging, error detection, and security provision in embedded systems development.


PS You can find the complete project with RTT output on GitHub repository.

Posted in #Embedded    #DIY    #vscode   

About Maxim Reznik

Maxim Reznik

Maxim Reznik is a Software Engineer and Consultant at AdaCore. At AdaCore, he works on the GNAT Studio, the Ada VS Code extension and the Language Server Protocol implementation.