J-Run: Automating performance tests on real hardware

One of the things that irritates me a lot is manual work that should be automated by machines. Automation always trumps the error-prone human and, in my case, offered the opportunity to get to use some of SEGGER”s software I’d never used before to develop a useful tool.

The problem

What I wanted to do was automate gathering of on-target benchmark and selftest results for emCrypt, our extensive cryptography library. Customers are always asking how big things are, how much RAM they use, and how fast they run. I had always run the on-target emCrypt selftests and benchmarks by manually loading them onto hardware, running the code in Embedded Studio using RTT to output the results, and then copy-pasting the results into the emCrypt manual. And repeating the whole process for the next benchmark or selftest.

With so many selftest and benchmark applications, it became clear that the process was both cumbersome and error prone, not to mention boring, so I looked for a solution that I could use to automate the simple per-application process:

  • Create a project for the benchmark or selftest application
  • Build that application for a selected target processor
  • Program that application onto a target board using the J-Link
  • Capture the application’s output and write a results file
  • …Done!

I built the solution from existing and new parts bound together in a small batch file.

Creating the project

This is the easy part. I had already developed and used a Python script that enables me to create Embedded Studio (and other) projects for our middleware. It happens to be very flexible, supporting a wide range of boards and target processors, and also ensures that the resulting projects are correctly configured with the minimum set of sources required. It creates configurations for optimized for size and optimized for speed using different compilers, debug versions using RTT, and instrumented versions using SystemView, like this:

Managing your project with configurations to select compilation options, preprocessor defines, and application-dependent features makes development so much more productive. You can change configuration to an instrumented debug build in an instant when faced with a problem, or into a minimum size release build when unexpectedly requested for some metrics: click the dropdown, select the appropriate configuration, build and run. This is much simpler than editing your configuration settings in the plain Debug and Release configurations because it’s almost inevitable that a quick-and-dirty setting change in one of those configurations gets forgotten. Using Embedded Studio’s configuration inheritance capability can really streamline your development process!

Continuing on, I modified the script so I could easily choose the target processor and application I wanted to build as the existing script indiscriminantly created all projects for every board and every application it supported. And I chose to use the “Clang – MaxSpeed” configuration for benchmarking, but could equally have chosen some other configuration.

Building the application

With the project created, time to build. Embedded Studio ships with a command-line application, emBuild, that loads a solution file and builds a particular project in one or more configurations: just what I need! No coding necessary, just a few lines of batch script to get the correct spell when building.

Running the project

So this is where I ran into trouble. SEGGER has a set of utilities to program targets (e.g. J-Flash) and capture RTT (RTT Client and RTT Logger), but they are not easily automated and they are GUI-based rather than being command-line oriented, which I needed for my script. After canvassing the J-Link and Embedded Studio teams, we came to realize that no single utility would do what I needed and no utility is easily modified to do what I want.

So, what does an engineer do when presented with such a conundrum?  Simple: turn it into a programming task!

Developing J-Run, the unit test and benchmarking utility

It seems obvious that SEGGER has everything I need from a quick inspection of our products: Ozone and Embedded Studio can both read ELF files, program targets, and capture RTT output.

The best option for J-Run was to take what we already have as proven components and do what a customer would do: write a utility that exactly fits the intended purpose.

Therefore I selected two SEGGER products to build J-Run:

I used only the SEGGER documentation and distributed libraries. That is, I didn’t use any internal knowledge and I didn’t contact the teams for assistance when writing J-Run.

SEGGER’s ELFLib and J-Link SDK

ELFLib gets me everything I need to program an ELF file into the target: I can find section and program information and I can look up symbols. The J-Link SDK gets me everything I need to program the target and interact with a running program.

Using ELFLib and the J-LinK SDK for this task was super easy.  A simple sketch of the application is:

  • Download program:
    • Load the ELF file into PC memory using ELFLib
    • Open a connection to the J-Link using J-Link SDK
    • Iterate over each program header using ELFLib
      • Extract section’s binary data using ELFLib
      • Program binary data into target using J-Link SDK
  • Prepare RTT:
    • Locate the RTT control block symbol by direct lookup using ELFLib
    • Translate the symbol into a target processor address using ELFLib
    • Pass the RTT control block address to J-Link SDK
  • Execute application and capture output:
    • Start capturing RTT output using J-Link SDK
    • Reset and run the application using J-Link SDK
    • Capture any RTT output and display it on stdout using J-Link SDK
    • Finish and close down when a wildcard “Sentinel” is found

It’s all very straightforward, a few hours of work to make the application look nice and add command line options for full flexibility.

In the end, the application is about 400 lines of code, with headers and comments, so is far from complex. As an executable it’s only 53K.

J-Run in action

To demonstrate J-Run, here’s a simple application that times how long it takes, in processor cycles, to compute 10,000 single-precision sines:

/*********************************************************************
*                   (c) SEGGER Microcontroller GmbH                  *
*                        The Embedded Experts                        *
*                           www.segger.com                           *
**********************************************************************

-------------------------- END-OF-HEADER -----------------------------

File        : SEGGER_RTT_JRun_Demo.c
Purpose     : Show how J-Run can capture benchmark or unit test output.

*/

#include "SEGGER_RTT.h"
#include <math.h>

/*********************************************************************
*
*       Static data
*
**********************************************************************
*/

static const volatile unsigned * const _pCYCCNT = (void *)0xE0001004;

/*********************************************************************
*
*       Global functions
*
**********************************************************************
*/

/*********************************************************************
*
*       MainTask()
*
* Function description
*   Main task executed by the RTOS to create further resources and
*   running the main application.
*/
void MainTask(void);
void MainTask(void) {
  unsigned i;
  unsigned T0;
  //
  SEGGER_RTT_printf(0, "SEGGER J-Run demo.\n\n");
  //
  T0 = *_pCYCCNT;
  for (i = 0; i < 10000; ++i) {
    (void)sinf(i / 10000.0f);
  }
  T0 = *_pCYCCNT - T0;
  //
  SEGGER_RTT_printf(0, "Took %u cycles\n\n", T0);
  SEGGER_RTT_printf(0, "\nSTOP.\n\n");
  for (;;) {
    /* Benchmark done */
  }
}

/*************************** End of file ****************************/

Running this with J-Run from the command line is easy:

C:> jrun SEGGER_RTT_JRun_Demo.elf
(c) 2018 SEGGER Microcontroller GmbH    www.segger.com
J-Run compiled Mar 19 2018 10:31:28

Open application...OK
Set target device to MK66FN2M0xxx18...OK
Select SWD interface...OK
Set interface speed to 4000 kHz...OK
Reset target...OK
Download 00000000-0000234F...OK
Download 00002350-00002397...OK
Set RTT control block at 0x20002090...OK
Start target application...OK
Start RTT...OK
Read terminal data...

SEGGER J-Run demo.

Took 2582433 cycles

C:>

To reduce it to the bare minimum and just capture the application’s output, use the --silent switch:

C:> jrun --silent SEGGER_RTT_JRun_Demo.elf
SEGGER J-Run demo.

Took 2582433 cycles

C:>

Conclusion

Anybody can easily write a custom application for J-Link using SEGGER components such as ELFLib and the J-Link SDK. What’s more, because these are documented and tested components, you can be confident that your custom application will be both reliable and efficient.

J-Run application source code

/*********************************************************************
*                   (c) SEGGER Microcontroller GmbH                  *
*                        The Embedded Experts                        *
*                           www.segger.com                           *
**********************************************************************

-------------------------- END-OF-HEADER -----------------------------

File        : JRun.c
Purpose     : Load, run, and capture application output through J-Link.

*/

/*********************************************************************
*
*       #include Section
*
**********************************************************************
*/

#include "ELF.h"
#include "JLINK.h"
#include "JLinkARMDLL.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

/*********************************************************************
*
*       Defines, fixed
*
**********************************************************************
*/

/*********************************************************************
*
*       Program header types
*/
#define PT_LOAD           1

/*********************************************************************
*
*       Static data
*
**********************************************************************
*/

static ELF_FILE_HANDLE   _ElfHandle;
static char              _aSectionData[16*1024*1024];
static int               _FlagSilent = 0;
static int               _NumStreams = 1;

/*********************************************************************
*
*       Static code
*
**********************************************************************
*/

/*********************************************************************
*
*       _AppExit()
*
*  Function description
*    Exit application.
*
*  Parameters
*    sFormat - Optional error message format string.
*/
static void _AppExit(const char *sFormat, ...) {
  va_list ap;
  //
  if (sFormat) {
    va_start(ap, sFormat);
    printf("\nFATAL ERROR: ");
    vprintf(sFormat, ap);
    printf("\n");
    va_end(ap);
  }
  //
  if (_ElfHandle != INVALID_ELF_FILE_HANDLE) {
    ELF_Close(_ElfHandle);
  }
  if (JLINK_IsOpen()) {
    JLINK_Close();
  }
  exit(sFormat ? 100 : 0);
}

/*********************************************************************
*
*       _Log()
*
*  Function description
*    Log message.
*
*  Parameters
*    sFormat - Log message format string.
*/
static void _Log(const char *sFormat, ...) {
  va_list ap;
  //
  if (!_FlagSilent) {
    va_start(ap, sFormat);
    vprintf(sFormat, ap);
    va_end(ap);
  }
}

/*********************************************************************
*
*       _WildcardMatch()
*
*  Function description
*    Match string against wildcard.
*
*  Parameters
*    sPat - Wildcard pattern to match against.
*    sStr - String to match against wildcard.
*
*  Return value
*    == 0 - No match.
*    != 0 - Match.
*/
static int _WildcardMatch(const char *sPat, const char *sStr) {
  const char * s;
  const char * p;
  int          Star;
  //
  Star = 0;
  //
Again:
  for (s = sStr, p = sPat; *s; ++s, ++p) {
    switch (*p) {
    case '?':
      break;
      //
    case '*':
      Star = 1;
      sStr = s;
      sPat = p;
      if (*++sPat == 0) {
        return 1;
      }
      goto Again;
      //
    default:
      if (*s != *p) {
        if (!Star) {
          return 0;
        }
        ++sStr;
        goto Again;
      }
      break;
    }
  }
  if (*p == '*') {
    ++p;
  }
  return *p == 0;
}

/*********************************************************************
*
*       Public code
*
**********************************************************************
*/

/*********************************************************************
*
*       main()
*
*  Function description
*    Main application.
*
*  Parameters
*    argc - Argument count.
*    argv - Argument vector.
*
*  Return value
*    OS exit status.
*/
int main(int argc, char **argv) {
  ELF_ITEM_LOCATION   Loc;
  ELF_PROGRAM_INFO    ProgramInfo;
  ELF_DIE_HANDLE      RTTCB_DIE;
  U64                 AddrU64;
  char                acIn[256];
  char                acOut[256];
  int                 Index;
  int                 Status;
  const char        * sText;
  const char        * sInputFileName;
  const char        * sDeviceName;
  const char        * sExitWild;
  int                 Interface;
  int                 Speed;
  //
  sInputFileName = NULL;
  sDeviceName    = "MK66FN2M0xxx18";
  Interface      = JLINKARM_TIF_SWD;
  Speed          = 4000;
  sExitWild      = "*STOP.*";
  //
  if (argc == 1) {
    printf("(c) 2018 SEGGER Microcontroller GmbH    www.segger.com\n");
    printf("J-Run compiled " __DATE__ " " __TIME__ "\n");
    printf("\n");
    printf("Syntax:\n");
    printf("  JRun [option, option...] elf-file\n");
    printf("\n");
    printf("Options:\n");
    printf("  -d str , --device str  Set device name to 'str'   [default %s]\n", sDeviceName);
    printf("  -s, --swd              Select SWD interface       [default]\n");
    printf("  -j, --jtag             Select JTAG interface\n");
    printf("  -f n, --speed n        Set interface to 'n' kHz   [default %u]\n", Speed);
    printf("  -v, --verbose          Show progress              [default]\n");
    printf("  -q, --silent           Work silently\n");
    printf("  -x str, --exit str     Set exit wildcard to 'str' [default %s]\n", sExitWild);
    printf("  -1                     Send RTT to stdout         [default]\n");
    printf("  -2                     Send RTT to stdout+stderr\n");
    _AppExit(NULL);
  }
  ++argv;
  --argc;
  while (argc > 0) {
    sText = *argv++;
    --argc;
    if (sText[0] == '-') {
      if (strcmp(sText, "--device") == 0) {
        if (argc-- > 0 && argv[0][0] != '-') {
          sDeviceName = *argv++;
        } else {
          _AppExit("--device requires an argument");
        }
      } else if (strcmp(sText, "--silent") == 0 || strcmp(sText, "-q") == 0) {
        _FlagSilent = 1;
      } else if (strcmp(sText, "--verbose") == 0 || strcmp(sText, "-v") == 0) {
        _FlagSilent = 0;
      } else if (strcmp(sText, "--swd") == 0 || strcmp(sText, "-s") == 0) {
        Interface = JLINKARM_TIF_SWD;
      } else if (strcmp(sText, "--jtag") == 0 || strcmp(sText, "-j") == 0) {
        Interface = JLINKARM_TIF_JTAG;
      } else if (strcmp(sText, "--speed") == 0 || strcmp(sText, "-f") == 0) {
        if (argc-- > 0 && argv[0][0] != '-') {
          Speed = atoi(*argv++);
        } else {
          _AppExit("--speed requires an argument");
        }
      } else if (strcmp(sText, "--exit") == 0 || strcmp(sText, "-x") == 0) {
        if (argc-- > 0 && argv[0][0] != '-') {
          sExitWild = *argv++;
        } else {
          _AppExit("--exit requires an argument");
        }
      } else if (strcmp(sText, "-1") == 0) {
        _NumStreams = 1;
      } else if (strcmp(sText, "-2") == 0) {
        _NumStreams = 2;
      } else {
        _AppExit("unknown option '%s'", sText);
      }
    } else {
      sInputFileName = sText;
    }
  }
  //
  if (sInputFileName == NULL) {
    _AppExit("no ELF input file provided");
  }
  //
  _Log("(c) 2018 SEGGER Microcontroller GmbH    www.segger.com\n");
  _Log("J-Run compiled " __DATE__ " " __TIME__ "\n\n");
  //
  _Log("Open application...");
  _ElfHandle = ELF_Open(sInputFileName, NULL, NULL);
  if (_ElfHandle == INVALID_ELF_FILE_HANDLE) {
    _AppExit("cannot open '%s' for reading.", sInputFileName);
  } else {
    _Log("OK\n");
  }
  //
  // Find the RTT control block.
  //
  RTTCB_DIE = ELF_GetGlobalVarByName(_ElfHandle, "_SEGGER_RTT");
  if (RTTCB_DIE == INVALID_ELF_DIE_HANDLE) {
    _AppExit("SEGGER RTT control block not found in Dwarf information.");
  }
  if (ELF_GetItemStartLoc(_ElfHandle, RTTCB_DIE, &Loc) < ELF_OK) {
    _AppExit("Can't determine location of SEGGER RTT control block.");
  }
  if (ELF_GetItemLocAddr(_ElfHandle, &Loc, 0, &AddrU64) < ELF_OK) {
    _AppExit("Can't determine location of SEGGER RTT control block.");
  }
  //
  // Connect to J-Link.
  //
  sText = JLINK_OpenEx(NULL, NULL);
  if (sText != NULL) {
    _AppExit("%s", sText);
  }
  //
  // Set device, interface, speed.
  //
  _Log("Set target device to %s...", sDeviceName);
  sprintf(acIn, "device = %s", sDeviceName);
  JLINKARM_ExecCommand(acIn, &acOut[0], sizeof(acOut));
  if (acOut[0]) {
    _AppExit("%s\n", acOut);
  } else {
    _Log("OK\n");
  }
  //
  _Log("Select %s interface...", Interface == JLINKARM_TIF_SWD ? "SWD" : "JTAG");
  if (JLINKARM_TIF_Select(Interface) < 0) {
    _AppExit("cannot select interface");
  } else {
    _Log("OK\n");
  }
  //
  _Log("Set interface speed to %d kHz...", Speed);
  JLINKARM_SetSpeed(Speed);
  _Log("OK\n");
  //
  // Get connected.
  //
  Status = JLINKARM_Connect();
  if (Status != 0) {
    _AppExit("could not connect to target.", argv[0]);
  }
  //
  // Program device from ELF file.
  //
  _Log("Reset target...");
  Status = JLINKARM_Reset();
  if (Status < 0) {
    _AppExit("error resetting target.");
  }
  _Log("OK\n");
  JLINKARM_BeginDownload(0);
  for (Index = 0; Index < ELF_GetNumPrograms(_ElfHandle); ++Index) { ELF_GetProgramInfoByIndex(_ElfHandle, Index, &ProgramInfo); if (ProgramInfo.FileSize > sizeof(_aSectionData)) {
      _AppExit("program section too big.");
    } else if (ProgramInfo.Type == PT_LOAD && ProgramInfo.FileSize > 0) {
      _Log("Download %08X-%08X...", ProgramInfo.PAddr, ProgramInfo.PAddr + ProgramInfo.MemorySize - 1);
      ELF_GetProgramData(_ElfHandle, Index, &_aSectionData[0], 0, ProgramInfo.MemorySize);
      Status = JLINKARM_WriteMem(ProgramInfo.PAddr, ProgramInfo.MemorySize, _aSectionData);
      if (Status < 0) {
        _AppExit("error programming target.");
      } else {
        _Log("OK\n");
      }
    }
  }
  JLINK_EndDownload();
  //
  // Make RTT ready.
  //
  _Log("Set RTT control block at 0x%08X...", (U32)AddrU64);
  sprintf(acIn, "setrttaddr = 0x%08x", (U32)AddrU64);
  JLINKARM_ExecCommand(acIn, &acOut[0], sizeof(acOut));
  if (acOut[0]) {
    _AppExit("%s", acOut);
  } else {
    _Log("OK\n");
  }
  //
  _Log("Start target application...");
  Status = JLINKARM_Reset();
  if (Status < 0) {
    _AppExit("error resetting target.");
  }
  JLINKARM_Go();
  _Log("OK\n");
  //
  _Log("Start RTT...");
  Status = JLINK_RTTERMINAL_Control(JLINKARM_RTTERMINAL_CMD_START, NULL);
  if (Status < 0) {
    _AppExit("error starting RTT.");
  } else {
    _Log("OK\n");
  }
  //
  // Dump terminal data until termination wildcard matched.
  //
  _Log("Read terminal data...\n\n");
  Index = 0;
  for (;;) {
    Status = JLINK_RTTERMINAL_Read(0, &_aSectionData[Index], 1);
    if (Status < 0) { _AppExit("error reading RTT data."); } else if (Status > 0) {
      if (_aSectionData[Index] == '\n') {
        _aSectionData[Index+1] = 0;
        if (_WildcardMatch(sExitWild, _aSectionData)) {
          _AppExit(NULL);
        }
        printf("%s", &_aSectionData[0]);
        if (_NumStreams == 2) {
          fprintf(stderr, "%s", &_aSectionData[0]);
        }
        Index = 0;
      } else {
        ++Index;
      }
    }
  }
}

/*************************** End of file ****************************/