Hololink Module, Device Driver Tutorial

View as Markdown

Introduction

Users building their own HSB-IP based devices can use this tutorial to learn how to build an hololink_module driver. Applications use these drivers to access the functionality on that board, both for generic services and for things that are only present on your specific device. A typical device development workflow follows.

Instantiate the HSB-IP block within your device:

  • Generate a new UUID for your device (e.g. uuidgen).
  • Update configuration parameters, e.g. to indicate how many sensor interfaces and network ports your device has.
  • Add or adjust any peripherals that are specific to your device.
  • Ensure that per-device parameters, like MAC IDs and serial numbers, are appropriately configured.

Once the device is deployed, and the host is properly connected, you should see reasonable data using the hololink-enumerate command. For example, an HsbLite device produces these messages, one for each network port:

1# hololink-enumerate
2mac_id=3A:31:1D:1E:24:AA hsb_ip_version=0x2606 fpga_crc=0x0 ip_address=192.168.0.2 fpga_uuid=889b7ce3-65a5-4247-8b05-4ff1904c3359 serial_number=10060032828115 interface=enP5p3s0f0np0 board=hololink-lite
3mac_id=3A:31:1D:1E:24:AB hsb_ip_version=0x2606 fpga_crc=0x0 ip_address=192.168.0.3 fpga_uuid=889b7ce3-65a5-4247-8b05-4ff1904c3359 serial_number=10060032828115 interface=enP5p3s0f1np1 board=hololink-lite

To build a driver, the typical workflow includes:

  • Identify the variations in your device from the standard HsbLite configuration. You’ll leverage the existing HsbLite implementation for everything that doesn’t change.
  • Copy from a module template into a new directory for your device, hololink_module/module/<your-device>. Use the module we’re building in this tutorial, hololink_module/module/tutorial, as your template.
  • Update the build instructions with the name and UUID for your device.
  • Update the software with your configuration parameters (e.g. number of sensor interfaces and network ports).
  • Override any HsbLite service implementations as necessary.
  • Create services for any new features.
  • Build and install your module.
  • If your device supports a sensor configuration that’s already supported by an example application, you probably can use it without modification. You shouldn’t even have to recompile that application code.
  • For your new sensors or features, build an example application that demonstrates that feature. Follow the IMX274 Module Tutorial for instructions on this.
  • Add testing for your device to the tests directory; this way your users can continue to access your validation suite.

Once your device is validated, you can publicly publish your updates for customers to use. As new versions of HSB-IP and host software are released, your module driver should continue to work as is; guidelines on what needs to be updated and when are under Version management, below.

The Tutorial device

hololink_module/module/tutorial is an imaginary implementation used for this tutorial. It is the same as HsbLite, with these variations:

  • UUID d3061b3b-85b0-4096-ba57-296d2418477f.
  • Three sensor ports: 0 and 1 are cameras, 2 is an IMU.
  • One network interface.
  • A single register (TUTORIAL_DEVICE_STATUS, 0x42348000) with a bit (STATUS_LED_BIT) that turns an LED on or off.

We’ll show how you build a module based on HsbLite, apply sensor port configurations, and provide an API for setting that on-board LED.

The Tutorial module

This module is an example you can copy to build your own device: replace the word “tutorial” with your device’s name throughout.

The module’s source is in hololink_module/module/tutorial/. The parent hololink_module/CMakeLists.txt registers it — keep these add_subdirectory calls in alphabetical order:

1add_subdirectory(module/tutorial)

hololink_module/module/tutorial/CMakeLists.txt calls add_hololink_module, which builds the module into a shared object named hololink_<uuid>_<compat-id>.so:

1add_hololink_module(
2 NAME tutorial
3 UUID d3061b3b-85b0-4096-ba57-296d2418477f
4 SOURCES
5 module_entry.cpp
6)

By default the compat-id comes from the framework, so you’re always building a module for that specific version of HSB-IP; see Version management, below.

hololink_module/module/tutorial/module_entry.cpp holds the implementation — refer to the in-tree file for complete details; the key parts follow, top-down.

The module’s one exported entry point, hololink_module_init, builds the publisher. The host calls it to initialize the library when the application loads the module:

1extern "C" hololink_module_services_t
2hololink_module_init(const hololink_module_init_t* init)
3{
4 auto publisher = std::make_shared<TutorialDevicePublisher>();
5 g_publisher = publisher;
6 return publisher->setup(init);
7}

TutorialDevicePublisher satisfies application get_service requests — normally by passing them to its HsbLite superclass. Your device-specific behavior lives here; for the basic module that’s just the board’s name and its sensor layout:

1class TutorialDevicePublisher : public HsbLitePublisher {
2protected:
3 std::string module_name() const override { return "tutorial"; }
4
5 void publish_channel_configuration() override
6 {
7 auto impl = std::make_shared<TutorialDeviceChannelConfigurationV1>();
8 ServicePublisher<HsbLiteChannelConfigurationV1>(shared_from_this())
9 .publish("", impl);
10 }
11};

module_name() is documentation and logging only.

publish_channel_configuration() publishes TutorialDeviceChannelConfigurationV1, which redefines use_sensor — how we hand the sensor and network-port configuration back to the framework. It isn’t unique to one device; the same instance serves every device found:

1class TutorialDeviceChannelConfigurationV1
2 : public HsbLiteChannelConfigurationV1 {
3public:
4 void use_sensor(EnumerationMetadata& metadata, int64_t sensor_number) override
5 {
6 constexpr int64_t TOTAL_SENSORS = 3;
7 if (sensor_number < 0 || sensor_number >= TOTAL_SENSORS) {
8 throw std::runtime_error(
9 "While selecting a Tutorial device sensor: sensor_number "
10 + std::to_string(sensor_number)
11 + " is out of range (Tutorial device supports 0.."
12 + std::to_string(TOTAL_SENSORS - 1) + ")");
13 }
14 // One physical data plane (0) shared by every sensor, one SIF each.
15 hsb_lite_sensor_metadata(metadata, sensor_number,
16 /*data_plane=*/0, /*sifs_per_sensor=*/1);
17 }
18};

That’s all the basic module needs — the functionality is all based on HSB-IP, so no further driver work is necessary. Build the module with cmake into a build directory under /tmp; the module .so is staged under $BUILD_DIR/lib/hololink/modules/, where applications find it:

1BUILD_DIR=/tmp/hololink-build
2cmake -S . -B "$BUILD_DIR" -G Ninja
3cmake --build "$BUILD_DIR" --target tutorial

Applications load hololink module drivers dynamically, so you won’t have to recompile them to work with your new device — the application observes your new UUID during enumeration and uses it to find your module. At this point, running module_imx274_player or python3 examples/module_linux_imx274_player.py should work unmodified.

Board-specific extensions

Now that existing applications run, add functionality that is specific to your device. For the Tutorial device that’s the status LED, exposed through a bespoke service.

TutorialDeviceInterfaceV1 declares the APIs that applications can call. Put it in the module’s public header hololink_module/module/tutorial/include/hololink/module/tutorial/tutorial_device.hpp so applications can include it:

1class TutorialDeviceInterfaceV1
2 : public ConfigurableService<TutorialDeviceInterfaceV1> {
3public:
4 static constexpr const char* type_id = "tutorial_device.v1";
5
6 static std::string locator_id(const EnumerationMetadata& metadata)
7 {
8 return "serial=" + metadata.get<std::string>("serial_number");
9 }
10
11 virtual ~TutorialDeviceInterfaceV1() = default;
12
13 /* Turn the board's status LED on (true) or off (false). */
14 virtual hololink_module_status_t set_status_led(bool on) = 0;
15};

type_id is the identity get_service uses to hand back the right type; it must be unique to a class. locator_id builds the key used to find a cached instance for a device — a ;-separated name=value string (here just serial=<serial_number>).

Having declared the public interface TutorialDeviceInterfaceV1 — visible to any application that includes this header — we now move to its implementation TutorialDeviceV1, which is only visible within the module itself.

configure() sets this instance up for a specific device — the framework keeps a separate instance per serial number, so different devices each get their own. Once it’s set up, calls to set_status_led control the on-board LED. The LED is wired as a pull-down: clearing bit 0 of TUTORIAL_DEVICE_STATUS turns it on, setting it turns it off.

1// device_status block: one status LED on bit 0. The LED is wired as a
2// pull-down, so clearing bit 0 turns it on and setting bit 0 turns it off.
3static constexpr uint32_t TUTORIAL_DEVICE_STATUS = 0x42348000;
4static constexpr uint32_t STATUS_LED_BIT = 1u << 0;
5
6/* Concrete Tutorial device status service. configure() resolves the
7 * board's already-started HololinkInterfaceV1 control plane (the
8 * application fetches and start()s it first); every register access
9 * goes through it. */
10class TutorialDeviceV1
11 : public tutorial::TutorialDeviceInterfaceV1,
12 public Service<TutorialDeviceV1> {
13public:
14 static constexpr const char* type_id = "tutorial_device.impl.v1";
15
16 void configure(const EnumerationMetadata& metadata) override
17 {
18 const std::string hololink_id = HololinkInterfaceV1::locator_id(metadata);
19 hololink_ = HololinkInterfaceV1::get_service(this->module(), hololink_id.c_str());
20 }
21
22 hololink_module_status_t set_status_led(bool on) override
23 {
24 // Pull-down: clear bit 0 to turn the LED on, set it to turn it off.
25 return on ? hololink_->and_uint32(TUTORIAL_DEVICE_STATUS, ~STATUS_LED_BIT)
26 : hololink_->or_uint32(TUTORIAL_DEVICE_STATUS, STATUS_LED_BIT);
27 }
28
29private:
30 std::shared_ptr<HololinkInterfaceV1> hololink_;
31};

TutorialDevicePublisher::construct_overrides is the right place to construct instances of TutorialDeviceV1:

1class TutorialDevicePublisher : public HsbLitePublisher {
2protected:
3 // ...
4
5 bool construct_overrides(
6 const std::string& instance_id,
7 const std::string& type_id) override
8 {
9 if (!Publisher::has_type_id<TutorialDeviceV1>(type_id)) {
10 return false;
11 }
12 auto impl = std::make_shared<TutorialDeviceV1>();
13 ServicePublisher<TutorialDeviceV1>(shared_from_this())
14 .publish(instance_id, impl);
15 return true;
16 }
17};
  • Publisher::has_type_id<TutorialDeviceV1>(type_id) checks whether this is a type we handle.
  • ServicePublisher<TutorialDeviceV1>(shared_from_this()).publish(instance_id, impl) registers one instance per distinct instance_id (usually the device serial number). The same board can appear on multiple network ports, so this maps those interfaces back to one object.
  • Once an object is published, we won’t be asked to build it again unless it’s invalidated (e.g. device disconnect); later calls start from the cache publish maintains.
  • Returning true means the object is built and no further handlers need to be searched.

If your application needs specific functions on your board — like the set_status_led method above — it must fetch a handle to TutorialDeviceInterfaceV1. Your application is then tightly coupled to your device: running it against another device would throw when it tries to get that unsupported TutorialDeviceInterfaceV1 implementation.

Status-LED application

This is a small application with no data plane at all — it just drives the LED via the bespoke service and the reactor. The complete program ships as example_module_tutorial.cpp.

Discover the board, bring up the control plane, and fetch the bespoke service:

1 // Find the board via bootp enumeration and get its metadata.
2 auto& adapter = hololink::module::Adapter::get_adapter();
3 hololink::module::EnumerationMetadata metadata
4 = adapter.wait_for_channel(HOLOLINK_IP, DISCOVERY_TIMEOUT);
5
6 // Bring up the control plane; every register access flows through it.
7 auto hololink = hololink::module::HololinkInterfaceV1::get_service(metadata);
8 if (hololink->start() != HOLOLINK_MODULE_OK) {
9 throw std::runtime_error("HololinkInterface::start failed");
10 }
11 if (hololink->reset() != HOLOLINK_MODULE_OK) {
12 throw std::runtime_error("HololinkInterface::reset failed");
13 }
14
15 // Fetch the Tutorial device's bespoke status-LED service.
16 auto device
17 = hololink::module::tutorial::TutorialDeviceInterfaceV1::get_service(metadata);

Reactor alarms are one-shot, so the callback re-arms itself to blink once per second; the reactor runs it on its own thread:

1 auto reactor = hololink::module::ReactorV1::get_service(
2 adapter.host_publisher()->self_module());
3 auto state = std::make_shared<bool>(false);
4 auto tick = std::make_shared<hololink::module::ReactorV1::Callback>();
5 *tick = [device, state, reactor, tick]() {
6 *state = !*state;
7 device->set_status_led(*state);
8 reactor->add_alarm_s(BLINK_PERIOD_S, tick);
9 };
10 reactor->add_alarm_s(BLINK_PERIOD_S, tick);

The reactor thread drives the blinking, so the main thread just blocks forever:

1 // The reactor thread drives the blinking; this program runs forever.
2 for (;;) {
3 sleep(60);
4 }

Background

  • Module init. The host calls the module’s one exported symbol, hololink_module_init, which returns the module’s service callbacks so the host can route get_service requests to it.
  • Service cache. Each module caches the services it has published. Some are singletons (one per module); others are device-specific, keyed by instance_id — usually the serial number — so the same board seen on multiple ports resolves to one object.
  • HsbLite is a core implementation. It implements the host-side behavior for HSB-IP; a device-specific module can override virtually any part of it.

Functional perspective

When an HSB device is powered on, it periodically publishes an enumeration message. These are typically sent at 1 Hz. This enumeration message includes a UUID that identifies the device and a compat-id field, providing specific information about version compatibility.

The application uses hololink_module::Adapter to listen to these bootp messages. Each enumeration message is deserialized into a structure which is referred to as enumeration metadata.

The received UUID and compat-id fields are used to locate the hololink module associated with this device. This module is a driver (as a shared object) and is typically named hololink_<UUID>_<compat-id>.so. When found, it’s loaded and initialized, and the received enumeration metadata is passed to it. At this point, the module can adjust or enhance enumeration data as necessary.

The resulting enumeration metadata is passed back to the application. If the host doesn’t have a compatible module that it can load, the enumeration metadata from the deserialized network message is sent (unmodified) to the application.

The application uses this enumeration metadata to fetch specific services. For example, to reset the board, the application calls HololinkInterfaceV1::reset. To get a pointer to the HololinkInterfaceV1 object, use HololinkInterfaceV1::get_service(enumeration_metadata):

1 auto hololink = hololink::module::HololinkInterfaceV1::get_service(metadata);

This call to get_service fetches the possibly customized implementation from the device module — so the call to reset does what’s necessary for that specific device. (Note that not all services are found using enumeration metadata; some services are found with other criteria.)

The publisher’s construct_service chain constructs services lazily on first fetch; override any of the methods it calls to substitute your own implementation instead of the HsbLite implementation. You can even override construct_service itself if necessary.

All parts of the application depend on these services. For example, to configure the data plane for the device to use RoCE messaging, RoceReceiverOp fetches RoceDataChannelInterfaceV1 and calls its configuration methods. An application that does not use RoCE signalling wouldn’t fetch this object; so not all devices need to support it. If an application asks for a service that the module doesn’t support, the program would terminate with an error message showing that the service isn’t supported by that device.

Some APIs are device specific, e.g. activating a feature or component that is only instantiated on that device. The device module can define a device specific interface (e.g. HsbLiteInterfaceV1) with APIs that access and control those functions. Applications that want to use those features can get this device-specific service and call its methods — with the recognition that those calls tightly couple the application to that device.

Version management

A compat-id names a specific set of registers, peripherals, and their behaviors; any HSB-IP with the same compat-id is register compatible. If a breaking change occurs in HSB-IP registers, the compat-id is updated, which guarantees that existing deployed systems will not try to use the incompatible version.

The services a module exports are versioned. This lets you build new modules later on (e.g. to support new features); as long as a module still produces services matching the interface version that the application requires, the application will continue to work.