pw_system#

Warning

This module is an early work-in-progress towards an opinionated framework for new projects built on Pigweed. It is under active development, so stay tuned!

pw_system is quite different from typical Pigweed modules. Rather than providing a single slice of vertical functionality, pw_system pulls together many modules across Pigweed to construct a working system with RPC, Logging, an OS Abstraction layer, and more. pw_system exists to greatly simplify the process of starting a new project using Pigweed by drastically reducing the required configuration space required to go from first signs of on-device life to a more sophisticated production-ready system.

Trying out pw_system#

If you’d like to give pw_system a spin and have a STM32F429I Discovery board, refer to the board’s target documentation for instructions on how to build the demo and try things out

If you don’t have a discovery board, there’s a simulated device variation that you can run on your local machine with no additional hardware. Check out the steps for trying this out here.

Target Bringup#

Bringing up a new device is as easy as 1-2-3! (Kidding, this is a work in progress)

  1. Configure the build. How exactly to do this depends on the build system.

    • GN: Create a pw_system_target in your GN build. This is what will control the configuration of your target from a build system level. This includes which compiler will be used, what architecture flags will be used, which backends will be used, and more. A large quantity of configuration will be pre-set to work with pw_system after you select the CPU and scheduler your target will use, but your target will likely need to set a few other things to get to a fully working state.

    • Bazel: Add a dependency on @pigweed//pw_system to your cc_binary, and set one label flag, @pigweed//pw_system:extra_platform_libs. Point it to a cc_library containing any platform-dependent dependencies of your pw_system instantiation. In particular, this should include platform-specific initialization code (see next point) and the custom pw_linker_script (if any) to use when linking the pw_system binary.

      Warning

      You should always add the alwayslink = 1 attribute to the target you point @pigweed//pw_system:extra_platform_libs to. This is because Bazel links files in topological order, but the dependencies from extra_platform_libs may appear before the objects they are used in. The alwayslink = 1 will prevent the linker from erroneously garbage-collecting them.

  2. Write target-specific initialization. Most embedded devices require a linker script, manual initialization of memory, and some clock initialization. pw_system leaves this to users to implement as the exact initialization sequence can be very project-specific. All that’s required is that after early memory initialization and clock configuration is complete, your target initialization should call pw::system::Init() and then start the RTOS scheduler (e.g. vTaskStartScheduler()).

  3. Implement ``pw::system::UserAppInit()`` in your application. This is where most of your project’s application-specific logic goes. This could be starting threads, registering RPC services, turning on Bluetooth, or more. In UserAppInit(), the RTOS will be running so you’re free to use OS primitives and use features that rely on threading (e.g. RPC, logging).

Pigweed’s stm32f429i_disc1_stm32cube target demonstrates what’s required by the first two steps. The third step is where you get to decide how to turn your new platform into a project that does something cool! It might be as simple as a blinking LED, or something more complex like a Bluetooth device that brews you a cup of coffee whenever pw watch kicks off a new build.

Note

Because of the nature of the hard-coded conditions in pw_system_target, you may find that some options are missing for various RTOSes and architectures. The design of the GN integration is still a work-in-progress to improve the scalability of this, but in the meantime the Pigweed team welcomes contributions to expand the breadth of RTOSes and architectures supported as pw_system_targets.

GN Target Toolchain Template#

This module includes a target toolchain template called pw_system_target that reduces the amount of work required to declare a target toolchain with pre-selected backends for pw_log, pw_assert, pw_malloc, pw_thread, and more. The configurability and extensibility of this template is relatively limited, as this template serves as a “one-size-fits-all” starting point rather than being foundational infrastructure.

# Declare a toolchain with suggested, compiler, compiler flags, and default
# backends.
pw_system_target("stm32f429i_disc1_stm32cube_size_optimized") {
  # These options drive the logic for automatic configuration by this
  # template.
  cpu = PW_SYSTEM_CPU.CORTEX_M4F
  scheduler = PW_SYSTEM_SCHEDULER.FREERTOS

  # Optionally, override pw_system's defaults to build with clang.
  system_toolchain = pw_toolchain_arm_clang

  # The pre_init source set provides things like the interrupt vector table,
  # pre-main init, and provision of FreeRTOS hooks.
  link_deps = [ "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:pre_init" ]

  # These are hardware-specific options that set up this particular board.
  # These are declared in ``declare_args()`` blocks throughout Pigweed. Any
  # build arguments set by the user will be overridden by these settings.
  build_args = {
    pw_third_party_freertos_CONFIG = "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f4xx_freertos_config"
    pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm4f"
    pw_sys_io_BACKEND = dir_pw_sys_io_stm32cube
    dir_pw_third_party_stm32cube = dir_pw_third_party_stm32cube_f4
    pw_third_party_stm32cube_PRODUCT = "STM32F429xx"
    pw_third_party_stm32cube_CONFIG =
        "//targets/stm32f429i_disc1_stm32cube:stm32f4xx_hal_config"
    pw_third_party_stm32cube_CORE_INIT = ""
    pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
      "PW_BOOT_FLASH_BEGIN=0x08000200",
      "PW_BOOT_FLASH_SIZE=2048K",
      "PW_BOOT_HEAP_SIZE=7K",
      "PW_BOOT_MIN_STACK_SIZE=1K",
      "PW_BOOT_RAM_BEGIN=0x20000000",
      "PW_BOOT_RAM_SIZE=192K",
      "PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
      "PW_BOOT_VECTOR_TABLE_SIZE=512",
    ]
  }
}

# Example for the Emcraft SmartFusion2 system-on-module
pw_system_target("emcraft_sf2_som_size_optimized") {
  cpu = PW_SYSTEM_CPU.CORTEX_M3
  scheduler = PW_SYSTEM_SCHEDULER.FREERTOS

  link_deps = [ "$dir_pigweed/targets/emcraft_sf2_som:pre_init" ]
  build_args = {
    pw_log_BACKEND = dir_pw_log_basic #dir_pw_log_tokenized
    pw_log_tokenized_HANDLER_BACKEND = "//pw_system:log"
    pw_third_party_freertos_CONFIG = "$dir_pigweed/targets/emcraft_sf2_som:sf2_freertos_config"
    pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm3"
    pw_sys_io_BACKEND = dir_pw_sys_io_emcraft_sf2
    dir_pw_third_party_smartfusion_mss = dir_pw_third_party_smartfusion_mss_exported
    pw_third_party_stm32cube_CONFIG =
        "//targets/emcraft_sf2_som:sf2_mss_hal_config"
    pw_third_party_stm32cube_CORE_INIT = ""
    pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
      "PW_BOOT_FLASH_BEGIN=0x00000200",
      "PW_BOOT_FLASH_SIZE=200K",

      # TODO: b/235348465 - Currently "pw_tokenizer/detokenize_test" requires at
      # least 6K bytes in heap when using pw_malloc:bucket_block_allocator.
      # The heap size required for tests should be investigated.
      "PW_BOOT_HEAP_SIZE=7K",
      "PW_BOOT_MIN_STACK_SIZE=1K",
      "PW_BOOT_RAM_BEGIN=0x20000000",
      "PW_BOOT_RAM_SIZE=64K",
      "PW_BOOT_VECTOR_TABLE_BEGIN=0x00000000",
      "PW_BOOT_VECTOR_TABLE_SIZE=512",
    ]
  }
}

Metrics#

The log backend is tracking metrics to illustrate how to use pw_metric and retrieve them using Device.get_and_log_metrics().

Console#

The pw-system-console can be used to interact with the targets. See pw_system console CLI reference for detailed CLI usage information.

Multi-endpoint mode#

The default configuration serves all its traffic with the same channel ID and RPC address. There is an alternative mode that assigns a separate channel ID and address for logging. This can be useful if you want to separate logging and primary RPC to pw_system among multiple clients.

To use this mode, add the following to gn args out:

pw_system_USE_MULTI_ENDPOINT_CONFIG = true

The settings for the channel ID and address can be found in the target //pw_system:multi_endpoint_rpc_overrides.

Extra logging channel#

In multi-processor devices, logs are typically forwarded to a primary application-class core. By default, pw_system assumes a simpler device architecture where a single processor is communicating with an external host system (e.g. a Linux workstation) for developer debugging. This means that logging and RPCs are expected to coexist on the same channel. It is possible to redirect the logs to a different RPC channel output by configuring PW_SYSTEM_LOGGING_CHANNEL_ID to a different channel ID, but this would still mean that logs would inaccessible from either the application-class processor, or the host system.

The logging multisink can be leveraged to address this completely by forwarding a copy of the logs to the application-class core without impacting the behavior of the debug communication channel. This allows pw-system-console work as usual, while also ensuring logs are available from the larger application-class processor. Additionally, this allows the debug channel to easily be disabled in production environments while maintaining the log forwarding path through the larger processor.

An example configuration is provided below:

config("extra_logging_channel") {
  defines = [ "PW_SYSTEM_EXTRA_LOGGING_CHANNEL_ID=2" ]
}

pw_system_target("my_system") {
  global_configs = [ ":extra_logging_channel" ]
}

Once you have configured pw_system as shown in the example above, you will still need to define an RPC channel for the channel ID that you selected so the logs can be routed to the appropriate destination.

pw_system:async#

pw_system:async is a new version of pw_system based on pw_async2 and pw_channel. It provides an async dispatcher, which may be used to run async tasks, including with C++20 coroutines.

To use pw_system:async, add a dependency on @pigweed//pw_system:async in Bazel. Then, from your main function, invoke pw::SystemStart() with a pw::channel::ByteReaderWriter to use for IO.

 1int main() {
 2  // First, do any required low-level platform initialization.
 3
 4  // Initialize a channel to handle pw_system communications, including for
 5  // pw_rpc. This example uses LoopbackByteChannel, but a channel that actually
 6  // transmits data should be used instead.
 7  std::byte channel_buffer[128];
 8  pw::multibuf::SimpleAllocator alloc(channel_buffer, pw::System().allocator());
 9  pw::channel::LoopbackByteChannel channel(alloc);
10
11  // Post any async tasks that should run. These will execute after calling
12  // pw::SystemStart.
13  pw::System().dispatcher().Post(my_task);
14
15  // As needed, start threads to run user code. Or, register a task to start
16  // threads after pw::SystemStart.
17
18  // When ready, start running the pw::System threads and dispatcher. This
19  // function call never returns.
20  pw::SystemStart(channel);
21
22  return 0;
23}

pw_system:async Linux example#

//pw_system/system_async_host_simulator_example is an example app for running pw_system:async on a Linux host. Running the example requires two terminals. In the first terminal, start the pw_system:async instance:

bazelisk run //pw_system/system_async_host_simulator_example

That will wait for a TCP connection from the pw_system console. To connect to it from the console, run the following:

bazelisk run //pw_system/py:pw_system_console -- -s 127.0.0.1:33000

Debugging pw_system_console with VSCode#

When running a python script through bazel, python is run inside a bazel sandbox, which can make re-creating this environment difficult when running the script outside of bazel to attach a debugger.

To simplify debugging setup, Pigweed makes available the debugpy package to ease attaching pw_system_console.

First configure VSCode to add the following to the configuration section of launch.json. This file can be automatically opened by selecting Run -> Open Configurations`, or Run -> Add Configuration if there is no existing launch.json.

 "configurations": [
 {
   "name": "Python Debugger: Remote Attach",
   "type": "debugpy",
   "request": "attach",
   "connect": {
     "host": "localhost",
     "port": 5678
   },
   "pathMappings": [
     {
       "localRoot": "${workspaceFolder}",
       "remoteRoot": "."
     }
   ]
 }

]

Next, run the console through bazel, adding the argument(s) --debugger-listen and optionally --debugger-wait-for-client to pause the console until the debugger attached. For example:

bazelisk run //pw_system/py:pw_system_console -- --debugger-listen

Once the console has been started, simply select Run -> Start Debugging and the VS code debugger will automatically attach to the running python console.

API reference#

void pw::SystemStart(channel::ByteReaderWriter &io_channel)#

Starts running pw_system:async with the provided IO channel. This function never returns.

inline system::AsyncCore &pw::System()#

Returns a reference to the global pw_system instance. pw::System() provides several features for applications: a memory allocator, an async dispatcher, and a RPC server.

class AsyncCore#

The global pw::System() instance. This object is safe to access, whether pw::SystemStart has been called or not.

Public Functions

Allocator &allocator()#

Returns the system pw::Allocator instance.

async2::Dispatcher &dispatcher()#

Returns the system pw::async2::Dispatcher instance.

inline rpc::Server &rpc_server()#

Returns the system pw::rpc::Server instance.

bool RunOnce(Function<void()> &&function)#

Runs a function once on a separate thread. If the function blocks, it may prevent other functions from running.

Returns:

true if the function was enqueued to run, false if the function queue is full