How-to guide#

Emulators frontend

This guide shows you how to do common pw_emu tasks.

Set up emulation targets#

An emulator target can be defined in the top level pigweed.json under pw:pw_emu:target or defined in a separate json file under targets and then included in the top level pigweed.json via pw:pw_emu:target_files.

For example, for the following layout:

├── pigweed.json
└── pw_emu
    └── qemu-lm3s6965evb.json

the following can be added to the pigweed.json file:

{
  "pw": {
    "pw_emu": {
      "target_files": [
        "pw_emu/qemu-lm3s6965evb.json"
      ]
    }
  }
}

The following subsections presents examples of how to define new emulation targets for QEMU and renode. For detailed emulation target configuration please see Configuration.

QEMU targets#

When defining a QEMU emulation target the following keys have to be defined under <target>:qemu:

  • executable: name of the QEMU executable, e.g. qemu-system-arm, qemusystem-riscv64, etc.

  • machine: the QEMU machine name; see qemu-system-<arch> -machine help for a list of supported machines names

Here is an example that defines the qemu-lm3s6965evb target as configuration fragment:

{
  "targets": {
    "qemu-lm3s6965evb": {
      "gdb": [
        "arm-none-eabi-gdb"
      ],
      "qemu": {
        "executable": "qemu-system-arm",
        "machine": "lm3s6965evb",
        "channels": {
          "chardevs": {
            "serial0": {
              "id": "serial0"
            }
          }
        }
      }
    }
  }
}

Since this is an ARM machine note that QEMU executable is defined as qemu-system-arm.

QEMU chardevs can be exposed as host channels under <target>:qemu:channels:chardevs:<chan-name> where chan-name is the name that the channel can be accessed with (e.g. pw emu term <chan-name>). The id option is mandatory and it should match a valid chardev id for the particular QEMU machine.

This target emulates a Stellaris EEVB and is compatible with the lm3s6965evb-qemu Pigweed target. The configuration defines a serial0 channel to be the QEMU chardev with the serial0 id. The default type of the channel is used, which is TCP and which is supported by all platforms. The user can change the type by adding a type key set to the desired type (e.g. pty)

renode targets#

The following configuration fragment defines a target that uses renode:

{
  "targets": {
    "renode-stm32f4_discovery": {
      "gdb": [
        "arm-none-eabi-gdb"
      ],
      "renode": {
        "executable": "renode",
        "machine": "platforms/boards/stm32f4_discovery-kit.repl",
        "channels": {
          "terminals": {
            "serial0": {
              "device-path": "sysbus.usart1"
            }
          }
        }
      }
    }
  }
}

Note that machine is used to identify which renode script to use for the machine definitions and terminals to define which UART devices to expose to the host.

This target emulates the ST 32F429I Discovery kit and is compatible with the stm32f429i-disc1 Pigweed target. The configuration defines a serial0 channel as the serial port identified as sysbus.usart1 in the renode machine definition script.

Run target binaries#

To quickly run target binaries on the host using an emulation target the pw emu run command can be used. It will start an emulator instance, connect to a (given) serial port and load and run the given binary.

$ pw emu run --args=-no-reboot qemu-lm3s6965evb out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_snapshot/test/cpp_compile_test

--- Miniterm on serial0 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
INF  [==========] Running all tests.
INF  [ RUN      ] Status.CompileTest
INF  [       OK ] Status.CompileTest
INF  [==========] Done running all tests.
INF  [  PASSED  ] 1 test(s).
--- exit ---

Note the -no-reboot option is passed directly to QEMU and instructs the emulator to quit instead of rebooting.

Debugging#

Debugging target binaries can be done using the pw emu gdb command. It requires a running emulator instance which can be started with the pw emu start command.

In the following example we are going to debug the status_test from the pw_status module compiled for lm3s6965evb-qemu. First we are going to start an emulator instance using the qemu-lm3s6965evb emulator target and load the test file:

$ pw emu start qemu-lm3s6965evb \
    --file out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test

Next, we will start a gdb session connected to the emulator instance:

$ pw emu gdb -e out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test
GNU gdb (Arm GNU Toolchain 12.2.MPACBTI-Rel1 ...
...
Reading symbols from out/stm32f429i_disc1_debug/obj/pw_status/test/status_test.elf...
Remote debugging using ::1:32979
pw::sys_io::WriteByte (b=(unknown: 0x20)) at pw_sys_io_baremetal_lm3s6965evb/sys_io_baremetal.cc:117
117    uart0.data_register = static_cast<uint32_t>(b);
(gdb) bt
#0  pw::sys_io::WriteByte (b=(unknown: 0x20)) at pw_sys_io_baremetal_lm3s6965evb/sys_io_baremetal.cc:117
#1  0x00002f6a in pw::sys_io::WriteBytes (src=...) at pw_span/public/pw_span/internal/span_impl.h:408
#2  0x00002eca in pw::sys_io::WriteLine (s=...) at pw_span/public/pw_span/internal/span_impl.h:264
#3  0x00002f92 in operator() (log=..., __closure=0x0 <vector_table>) at pw_log_basic/log_basic.cc:87
#4  _FUN () at pw_log_basic/log_basic.cc:89
#5  0x00002fec in pw::log_basic::pw_Log (level=<optimized out>, flags=<optimized out>, module_name=<optimized out>, file_name=<optimized out>, line_number=95,
 function_name=0x6e68 "TestCaseStart", message=0x6e55 "[ RUN      ] %s.%s") at pw_log_basic/log_basic.cc:155
#6  0x00002b0a in pw::unit_test::LoggingEventHandler::TestCaseStart (this=<optimized out>, test_case=...) at pw_unit_test/logging_event_handler.cc:95
#7  0x00000f54 in pw::unit_test::internal::Framework::CreateAndRunTest<pw::(anonymous namespace)::Status_Strings_Test> (test_info=...)
 at pw_unit_test/public/pw_unit_test/internal/framework.h:266
#8  0x0000254a in pw::unit_test::internal::TestInfo::run (this=0x20000280 <_pw_unit_test_Info_Status_Strings>)
 at pw_unit_test/public/pw_unit_test/internal/framework.h:413
#9  pw::unit_test::internal::Framework::RunAllTests (this=0x20000350 <pw::unit_test::internal::Framework::framework_>) at pw_unit_test/framework.cc:64
#10 0x000022b0 in main (argc=<optimized out>, argv=<optimized out>) at pw_unit_test/public/pw_unit_test/internal/framework.h:218

At this point gdb commands can be used to debug the program.

Once the debugging session is over the emulator instance should be stopped:

$ pw emu stop

Boot debugging#

To debug bootstraping code the -p option can be used when the emulator is started:

$ pw emu start -p qemu-lm3s6965evb --file out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test

That will load the given program but the emulator will not start executing. Next, we can start a debugging session using the pw emu gdb commands:

$ pw emu gdb -e out/lm3s6965evb_qemu_gcc_size_optimized//obj/pw_status/test/status_test
GNU gdb (Arm GNU Toolchain 12.2.MPACBTI-Rel1 ...
...
Reading symbols from out/lm3s6965evb_qemu_gcc_size_optimized//obj/pw_status/test/status_test...
Remote debugging using ::1:38723
pw_boot_Entry () at pw_boot_cortex_m/core_init.c:122
122    asm volatile("cpsid i");
(gdb)

Note that the program is stopped at the pw_boot_Entry function. From here you can add breakpoints or step through the program with gdb commands.

Using multiple instances#

To use multiple emulator instances the --instance <instance-id> option can be used. The default pw emu instance id is default.

Note

Internally each emulator instance is identified by a working directory. pw emu’s working directory is $PROJECT_ROOT/.pw_emu/<instance-id>

As an example, we will try to start the same emulator instance twice:

$ pw emu start qemu-lm3s6965evb --file out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test
$ pw emu start qemu-lm3s6965evb --file out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test
pigweed/.pw_emu/default: emulator already started

Note that the second command failed because the default emulator instance is already running. To start another emulator instance we will use the --instance or -i option:

$ pw emu -i my-instance start qemu-lm3s6965evb --file out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test

Note that no error was triggered this time. Finally, lets stop both emulator instances:

$ pw emu stop -i my-instance
$ pw emu stop

Adding new emulator types#

The pw_emu module can be extended to support new emulator types by providing implementations for pw_emu.core.Launcher and pw_emu.core.Connector in a dedicated pw_emu Python module (e.g. pw_emu.myemu) or in an external Python module.

Internal pw_emu modules must register the connector and launcher classes by updating pw_emu.pigweed_emulators.pigweed_emulators. For example, the QEMU implementation sets the following values:

pigweed_emulators: Dict[str, Dict[str, str]] = {
  ...
  'qemu': {
    'connector': 'pw_emu.qemu.QemuConnector',
    'launcher': 'pw_emu.qemu.QemuLauncher',
   },
  ...

For external emulator frontend modules pw_emu is using the Pigweed configuration file to determine the connector and launcher classes, under the following keys: pw_emu:emulators:<emulator_name>:connector and pw_emu:emulators:<emulator_name>:connector. Configuration example:

{
  "pw": {
    "pw_emu": {
      "emulators": [
        "myemu": {
          "launcher": "mypkg.mymod.mylauncher",
          "connector": "mypkg.mymod.myconnector",
        }
      ]
    }
  }
}

The pw_emu.core.Launcher implementation must implement the following methods: pw_emu.core.Launcher._pre_start(), pw_emu.core.Launcher._post_start and pw_emu.core.Launcher._get_connector.

There are several abstract methods that need to be implement for the connector, like pw_emu.core.Connector.reset() or pw_emu.core.Connector.cont(). These are typically implemented using internal channels and pw_emu.core.Connector.get_channel_stream. See pw_emu.core.Connector for a complete list of abstract methods that need to be implemented.