Skip to main content

ICON custom hardware module guide

Introduction to the hardware abstraction layer

ICON's hardware abstraction layer ("HAL") is responsible for translating commands from ICON's real-time control layer ("RTCL") into hardware-specific commands. It is also responsible for reporting hardware status to the RTCL.

The HAL is extensible by means of custom hardware modules. By writing a custom hardware module, you can add support for new robotic hardware to ICON.

A custom hardware module is an executable binary that runs in a dedicated kubernetes pod on the real-time PC ("RTPC"). The hardware module implementation is a C++ class containing methods that execute in a real-time context.

Communication between the ICON server and the custom hardware module is established through hardware interfaces. These hardware interfaces are flatbuffer objects stored in shared memory. The custom hardware module implementation reads and writes to these objects directly using flatbuffer APIs. Therefore, we recommend that anyone writing custom hardware modules be familiar with flatbuffers.

Prerequisites

Before you can start the development of your custom hardware module this guide expects the following:

  • The hardware you want to control is physically connected to an IPC. In a multi device cluster, it must be connected to the cluster's real-time PC.
  • Your development machine has a direct Ethernet connection to the same IPC (same network as the IPC's uplink port)
  • You're familiar with the name of the real-time PC. In a multi device cluster the name of the real-time PC will be different from the IPC's name. Intrinsic RTPCs are shipped with a label on the front. Contact Intrinsic for help if you don't know the right value to use.

The next step is to setup the development environment on your development machine.

Development environment

This section explains how to setup the local development environment.

Installation requirements

Follow the guide on how to set up the development environment if you haven't already.

Additionally you will need to deploy your custom hardware module as an Intrinsic service so we recommend familiarizing yourself with the creation of custom services.

From now on, you should be running Visual Studio Code with your project folder opened inside the provided Dev Container.

Prepare the Bazel workspace

Use inctl to create a new bazel workspace.

inctl bazel init --sdk_repository=https://github.com/intrinsic-ai/sdk.git --sdk_version latest

This makes all ICON extension APIs available for this folder, including the custom hardware module API.

You can confirm a correct setup by creating a list of all build targets of the SDK:

bazel query @ai_intrinsic_sdks//...

Build and deploy a hardware module

The easiest way to begin working with hardware modules is to compile and deploy an existing one. The SDK contains the loopback hardware module which only reports back the commanded joint position every cycle. When adding support for custom hardware, we recommend using this loopback hardware module as a starting point.

A complete example of the build targets necessary for creating a hardware module can be found in the SDK.

In general to create your own custom hardware module you will need to recreate a similar set of targets. These have the following roles:

  • A cc_library to create a library containing your custom hardware module implementation. This will be an implementation of the `HardwareModuleInterface abstract base class.

  • A hardware_module_binary build rule to link a custom hardware module implementation with the hardware module runtime, creating a binary executable.

  • A hardware_module_image build rule that packages the hardware module binary into a container image, suitable for deployment with an Intrinsic service asset.

  • A hardware_module_manifest build rule that defines the intrinsic_service metadata. This creates a ServiceManifest textproto that is configures the service to be able to communicate with ICON via shared memory and have real-time performance.

  • An intrinsic_service rule that creates the service which packages the hardware module image.

With the above rules you can build and deploy your hardware module as with any other Intrinsic service asset. For this please refer to the latest instructions on installing intrinsic_service assets.

Developer's guide

This section provides information for developers, that want to develop their own hardware module.

The hardware module interface defines a series of methods which may be triggered by the real-time framework. These include non-realtime methods which are triggered to initialize and to clear faults, as well as real-time methods which handle the control loop.

This guide only provides complementary information to the detailed documentation of the hardware module interface provided in the SDK. We recommend thoroughly reading that documentation before implementing a custom hardware module.

Initialization and Fault Handling

Bringing up a hardware module will likely involve various operations to connect to and enable operations according to the robot specific communication interface.

Given the robot specific requirements implementations will vary but here we provide some general suggestions on what code to place in particular methods of the implementation.

  • Init: This is the first method used to initialize the hardware module. It is recommended to put the initialization of hardware interfaces (see the next section for additional details) and other similar operations which are unlikely to fail in this method. In addition this is where the hardware module receives its configuration through the HardwareModuleInitContext. Any failure here is considered fatal and requires a complete service restart to recover from. We recommend validating the configuration here but putting code which may fail but does not require reconfiguration into Prepare.

  • Prepare: This is called after Init from the control framework and after this returns the hardware module should be ready to immediately send and receive real-time method calls such as Activate. A common strategy is to use this to create the real-time threads and activate communication with the robot controller. Similar to Init any failure here is considered fatal and requires a complete service restart to recover from. Thus for failures which can be recovered without a restart it is preferable not to return an error and instead wait till a call to EnableMotion is made. This allows clearing the error with ClearFaults.

  • Activate: This call must return in real-time and triggers the start of the realtime cyclical communication between ICON and the hardware module. If the module is clock-driving then it must begin ticking the clock immediately after this call. After Activate is called ICON will call ReadStatus every cycle.

  • EnableMotion and Enabled: These methods are used to transition to a state in which ICON is actively controlling the module via calls to ApplyCommand. EnableMotion may block but after which the module must be ready to receive a call to Enabled which will immediately precede the ReadStatus and ApplyCommand calls made in the first cycle in which ApplyCommand is called.

  • DisableMotion and Disabled: These methods are used to transition to a state in which ICON is no longer controlling the module via calls to ApplyCommand. DisableMotion may block but after which the module must be ready to receive a call to Disabled after which it will no longer receive any commands.

  • ClearFaults: This method is used to recover from all non-fatal faults. It can block but after a successfuly call to it the module should expect calls to ReadStatus. This is a very useful method since it is the fastest method for recovering from a fault through the Flowstate frontend.

Hardware interface registration

Hardware interfaces are used to communicate and exchange data with the RTCL. Mutable hardware interfaces are used to report hardware status to the RTCL. Read-only hardware interfaces are used to receive commands from the RTCL.

A hardware module must declare and instantiate each of its hardware interfaces. Doing so creates a flatbuffer object in shared memory and returns a handle to it. The hardware module should hold onto this handle.

Our hardware module must report its joint positions. For that, advertise an intrinsic_fbs::JointPositionState hardware interface in our module. As a class member, add a mutable interface handle:

 private:
// HAL interface handles.
xfa::icon::MutableHardwareInterfaceHandle<intrinsic_fbs::JointPositionState>
joint_position_state_;

Then, in your module's Init method, advertise the interface:

absl::Status LoopbackHardwareModule::Init(
HardwareInterfaceRegistry& interface_registry, const ModuleConfig& config) {

ASSIGN_OR_RETURN(
joint_position_state_,
interface_registry.AdvertiseMutableInterface<JointPositionState>(
"position_state", kNumDofs));

The hardware module needs to receive a joint position command, so you advertise an intrinsic_fbs::JointPositionCommand hardware interface. As a class member, add a read-only interface handle:

 private:
xfa::icon::HardwareInterfaceHandle<intrinsic_fbs::JointPositionCommand>
joint_position_command_;

Then, in your module's Init method, advertise the interface:

absl::Status LoopbackHardwareModule::Init(
HardwareInterfaceRegistry& interface_registry, const ModuleConfig& config) {

ASSIGN_OR_RETURN(
joint_position_command_,
interface_registry.AdvertiseInterface<JointPositionCommand>(
"position_command", kNumDofs));

The hardware module can now communicate with the RTCL by writing to joint_position_state and reading from joint_position_command. Note that the arm part expects additional hardware interfaces. See the /intrinsic/icon/hardware_modules/loopback/loopback_hardware_module.cc for a complete example.

Hardware module registration

In the .cc file, register the interfaces of the hardware module. Supported interfaces are listed in icon/hal/default_hardware_interfaces.h.

INTRINSIC_ADD_HARDWARE_INTERFACE(intrinsic_fbs::JointPositionCommand,
intrinsic_fbs::BuildJointPositionCommand,
"intrinsic_fbs.JointPositionCommand")

Finally, to export a hardware module, register it using the REGISTER_HARDWARE_MODULE macro. This should be placed in the outer scope at the bottom of your .cc file.

REGISTER_HARDWARE_MODULE(loopback_module::LoopbackHardwareModule)

The real-time control loop

The Init function of the hardware module gets called immediately when the hardware module starts. This function allows to setup all necessary communication to the actual robot hardware and as described earlier sets up the communication channels to and from ICON. However, given its multi-process architectural setup, ICON and the hardware module may start with a slight delay. When both processes - ICON and the hardware module - are fully setup and ready to enter a real-time loop, ICON calls Activate() on the hardware module.

RealtimeStatus Activate();

This function signals the hardware module that ICON is ready to periodically call ReadStatus(). Equivalently, before ICON stops, it calls Deactivate() on the hardware module. It is important to note that before the call to Activate() occurs, the hardware module is responsible to keep a healthy connection to the hardware as ICON might not have started yet. Only after a successful call to Activate(), ICON has correctly started the real-time loop.

The ICON server runs a real-time control loop. Every control cycle, the following steps are performed in order (bold steps are customizable):

Control cycle sub-steps
Hardware module: ReadStatus
(context switch to ICON server process)

Once the ICON server is enabled, the real-time loop is extended by the controller loop which computes new command values through its respective actions:

Control cycle sub-steps
Hardware module: ReadStatus
(context switch to ICON server process)
Action: Sense
Action: Control
Reaction handling
(context switch to custom hardware module process)
Hardware module: ApplyCommand

The implementations of these steps need to be real-time safe. That is, they must always finish computation within a single control cycle, even in error cases. 1000Hz is a common control frequency, which means all steps should take well under one millisecond to finish, even in exceptional or error cases. Follow the real-time C++ programming style to implement a real-time safe hardware module.

Hardware module update

The hardware module's ReadStatus method is invoked at the beginning of every control cycle. The implementation should update all mutable hardware interfaces with the latest state received from hardware.

The hardware module's ApplyCommand method is invoked at the end of every control cycle. The implementation should read from the read-only hardware interfaces and apply the appropriate commands to hardware.

Because a hardware module's ApplyCommand is followed by the next cycle's ReadStatus, it is possible to write to mutable hardware interfaces during ApplyCommand. For example, this is what the loopback hardware module does:

RealtimeStatus LoopbackHardwareModule::ApplyCommand() {
for (size_t i = 0; i < kNumDofs; ++i) {
joint_position_state_->mutable_position()->Mutate(
i, joint_position_command_->position()->Get(i));
joint_velocity_state_->mutable_velocity()->Mutate(i, 0.0);
joint_acceleration_state_->mutable_acceleration()->Mutate(i, 0.0);
}
return OkStatus();
}

RealtimeStatus LoopbackHardwareModule::ReadStatus() {
// NO-OP, Status is updated already through `ApplyCommand`.
return OkStatus();
}

On the other hand, reading from read-only hardware interface during ReadStatus obtains the previous cycle's commands, which may be stale.

Shared Libraries

For building hardware module binaries and images it is recommended to use statically linked libraries. In cases where this is not possible, shared libraries can be added to the hardware module image via additional container layers. The Intrinsic SDK provides helper build rules for constructing container layers which can be added to the hardware module image.

You can import a shared library through the bazel cc_import rule. This allows you to link the shared library as a dependency to your hardware module library. Then, the compiled shared library files need to be added to the hardware module image as layers constructed using the container_layer rule. For a hypothetical shared library libshared:

cc_import(
name = "libshared",
hdrs = [
"inc/libshared.h",
],
shared_library = "lib/libshared.so",
)

cc_library(
name = "my_hardware_module",
...
deps = [
":libshared",
...
]
)

hardware_module_binary(
name = "my_hardware_module_main",
hardware_module_lib = ":loopback_hardware_module",
)

container_layer(
name = "libshared-runfiles",
directory = "/usr/lib",
files = [
":lib/libshared.so",
],
)

hardware_module_image(
name = "my_hardware_module_image",
hardware_module_binary = "my_hardware_module_main",
layers = [":libshared-runfiles"],
)

Limitations

note

At this time, the HAL only supports a fixed and limited set of hardware interfaces to the RTCL. The list of supported interfaces is in icon/hal/default_hardware_interfaces.h.