Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ODP Embedded Controller Track Overview

The Embedded Controller (EC) track is designed for individuals who are interested in developing and integrating embedded controller systems within the Open Device Partnership (ODP) framework. This track focuses on the design, implementation, and testing of embedded controllers, which are essential components in modern computing devices. Readers will learn how ODP supports creating robust EC solutions that enhance device functionality, improve power management, and ensure system reliability.

Embedded Controller

ODP Architecture

An Embedded Controller is typically a single SOC (System on Chip) design capable of managing a number of low-level tasks.

These individual tasked components of the SOC are represented by the gold boxes in the diagram. The ODP Support for Embedded Controller development is represented in the diagram in the green boxes, whereas third party support libraries are depicted in blue.

Component modularity

A Component can be thought of as a stack of functionality defined by traits (A trait in Rust is analogous to an interface in other common languages). For the functionality defined by the trait definition to interact with the hardware, there must be a HAL (hardware abstraction layer) defined that implements key actions required by the hardware to conduct these tasks. These HAL actions are then controlled by the functional interface of the component definition.
The component definition is part of a Subsystem of functionality that belongs to a Service. For example, a Power Policy Service may host several related Subsystems for Battery, Charger, etc. Each of these Subsystems have Controllers to interact with their corresponding components. These Controllers are commanded by the Service their Subsystem belongs to, so for example, the power policy service may interrogate the current charge state of the battery. It does so by interrogating the Subsystem Controller which in turn relies upon the interface defined by the component Trait, which finally calls upon the hardware HAL to retrieve the necessary data from the hardware. This chain of stacked concerns forms a common pattern that allows for agile modularity and flexible portability of components between target contexts.

flowchart TD
    A[e.g. Power Policy Service<br><i>Service initiates query</i>]
    B[Subsystem Controller<br><i>Orchestrates component behavior</i>]
    C[Component Trait Interface<br><i>Defines the functional contract</i>]
    D[HAL Implementation<br><i>Implements trait using hardware-specific logic</i>]
    E[EC / Hardware Access<br><i>Performs actual I/O operations</i>]

    A --> B
    B --> C
    C --> D
    D --> E

    subgraph Service Layer
        A
    end

    subgraph Subsystem Layer
        B
    end

    subgraph Component Layer
        C
        D
    end

    subgraph Hardware Layer
        E
    end

Secure vs Non-Secure

Communication between Subsystems may be considered to be either a "Secure" channel for data communication or a "Non-Secure" channel. An implementation may use more than one transport for different controller and controller service needs.

Data communication with the embedded controller can be considered an owned interface because it is implemented within the EC architecture itself. It may also tie into an external communication bus such as SPI or I2C for data exhanges between other MCUs or the host, but for purposes of communicating between its own subsystems, it is an internally implemented construct.

A "Secure" transport is one that can validate and trust the data from the channel, using cryptographic signatures and hypervisor isolation to insure the integrity of the data exchanged between subsystems. Not all such channels must necessarily be secure, and indeed in some cases depending upon the components used it may not even be possible to secure a channel. The ODP approach is agnostic to these decisions, and can support either or both patterns of implementation.

Depending upon the hardware architecture and available supporting features, a secure channel may incorporate strong isolation between individual component subsystems through memory access and paging mechanisms and/or hypervisor control.

Two similar sounding, but different models become known here. One is SMM, or "System Management Mode". SMM is a high-privilege CPU mode for x86 microcontrollers that EC services can utilize to gain access. To facilitate this, the SMM itself must be secured. This is done as part of the boot time validation and attestation of SMM access policies. With this in place, EC Services may be accessed by employing a SMM interrupt.

For A deeper dive into what SMM is, see How SMM isolation hardens the platform

Another term seen about will be "SMC", or "Secure Memory Control", which is a technology often found in ARM-based architectures. In this scheme, memory is divided into secure and non-secure areas that are mutally exclusive of each other, as well as a narrow section known as "Non-Secure Callable" which is able to call into the "Secure" area from the "Non-Secure" side.

Secure Memory Control concepts are discussed in detail with this document: TrustZone Technology for Armv8-M Architecture

SMM or SMC adoption has design ramifications for EC Services exchanges, but also affects the decisions made around boot firmware, and we'll see these terms again when we look at ODP Patina implementations.

Hypervisor context multiplexing

Another component of a Secure EC design is the use of a hypervisor to constrain the scope of any given component service to a walled-off virtualization context. One such discussion of such use is detailed in this article

The Open Device Partnership defines:

  • An "owned interface" that communicates with the underlying hardware via the available data transport .
  • We can think of this transport as being a channel that is considered either "Secure" or "Non-Secure".
  • This interface supports business logic for operational abstractions and concrete implementations to manipulate or interrogate the connected hardware component.
  • The business logic code may rely upon other crates to perform its functions. There are several excellent crates available in the Rust community that may be leveraged, such as Embassy.
  • Synchronous and asynchronous patterns are supported.
  • No runtime or RTOS dependencies.

An implementation may look a little like this:

ODP Arch

Embedded Controller Architecture

The construction of a typical component under the control of a service subsystem looks as follows:

flowchart LR
    A[Some Service<br><i>Service initiates query</i>]
    B[Subsystem Controller<br><i>Orchestrates component behavior</i>]
    C[Component Trait Interface<br><i>Defines the functional contract</i>]
    D[HAL Implementation<br><i>Implements trait using hardware-specific logic</i>]
    E[EC / Hardware Access<br><i>Performs actual I/O operations</i>]

    A --> B
    B --> C
    C --> D
    D --> E

    subgraph Service Layer
        A
    end

    subgraph Subsystem Layer
        B
    end

    subgraph Component Layer
        C
        D
    end

    subgraph Hardware Layer
        E
    end

When in operation, it conducts its operations in response to message events

sequenceDiagram
    participant Service as Some Service
    participant Controller as Subsystem Controller
    participant Component as Component (Trait)
    participant HAL as HAL (Hardware or Mock)

    Service->>Controller: query_state()
    Note right of Controller: Subsystem logic directs call via trait
    Controller->>Component: get_state()
    Note right of Component: Trait implementation calls into HAL
    Component->>HAL: read_some_level()
    HAL-->>Component: Ok(0)
    Component-->>Controller: Ok(State { value: 0 })
    Controller-->>Service: Ok(State)

    alt HAL returns error
        HAL-->>Component: Err(ReadError)
        Component-->>Controller: Err(SomeError)
        Controller-->>Service: Err(SomeUnavailable)
    end

A core pattern of the ODP architecture is one of Dependency Injection. The service and subsystem Traits define the functional contract of the component, while the HAL implementation provides the hardware-specific logic. This allows for a clear separation of concerns and enables the component to be easily tested and reused across different platforms. Components are eligible to be registered for their subservice if they match the required traits.

flowchart TD
    subgraph Component
        A[Needs Logger and Config]
    end

    subgraph Framework
        B[Provides ConsoleLogger]
        C[Provides NameConfig]
        D[Injects Dependencies]
    end

    B --> D
    C --> D
    D --> A

EC Component Model

A component that implements a specification and depends upon a HAL interface.

flowchart TD
    A[Component]
    B[Specification Trait]
    C[HAL Trait]

    A --> B
    A --> C

A component is housed within a subsystem, which is controlled by a service. The service orchestrates the component's behavior and manages its lifecycle.

flowchart TD
    A[__Controller__ <br/> Implements Service Interface Trait]
    B[__Device__ <br/> Implements Component Type Trait]
    C[__Component__ <br/> Implements Specification Trait]

    A --> B
    B --> C

Component interactions are generally initiated in response to message events. The controller receives a message, which it routes to the component. The component then calls into the HAL to perform the requested operation.

flowchart TD
    A[__Service Layer__ <br/> e.g. _Controller_]
    B[__Device Layer__ <br/> _Wrapped Component_]
    C[__Component Layer__ <br/> _Handles Message_]

    M["Incoming Message"] --> A
    A -->|calls _handle_| B
    B -->|calls _handle_| C

Runtime Behaviors of an Embedded Controller

Component code is typically executed as an asynchronous task invoked by Embassy Executor. A component reacts to events that are produced at higher levels of the Embedded Controller logic, through one or more policy manager tasks.

Tasks are generally event-driven actions dispatched in response to messages.

Messages are conveyed through a comms implementation. Events may be handled exclusively be a single component or reacted to by more than one component.

Messages are queued and dispatched in order but are handled asynchronously. Signaling may be required to enforce an ordered flow.

A component is wrapped within a defined Device wrapper that implements the traits that identify and control the implemented device type. The Controller for the device reacts to various events issued at the direction of policy manager logic, and in turn invokes the component hardware in appropriate response.

sequenceDiagram
    participant Policy as Policy Manager Task
    participant Comms as Comms Dispatcher
    participant Ctrl as Controller
    participant Dev as Device Wrapper
    participant Comp as Component (Implements Trait)

    Policy->>Comms: Emit Message (e.g. BatteryEvent)
    Comms->>Ctrl: dispatch_message()
    Ctrl->>Dev: handle()
    Dev->>Comp: perform_action()
    Comp-->>Dev: Result
    Dev-->>Ctrl: Response
    Ctrl-->>Comms: Ack or follow-up message

Embedded Controller Service Registration

Embedded Controller components and services in ODP are statically composed at build time but must be registered with the service infrastructure to become discoverable and operational during execution.

This registration model allows each policy domain (e.g. power, thermal, charging) to own and manage the devices associated with it.


Registration Pattern

Pseudocode (concept only)
The registration flow is: construct → init services → register.

let battery = BatteryDevice::new(DeviceId(1));
// ...
// in an async task function
embedded_services::init().await;
embedded_services::power::policy::register_device(&battery).await.unwrap();

This omits static allocation (StaticCell), executor wiring for async tasks, and controller setup, for clarity.

Realistic skeleton (matches the sample projects)

Refer to the Battery implementation example or the examples in the embedded-services repository for more concrete examples.

#![allow(unused)]
fn main() {
// statically allocate single ownership
static BATTERY: StaticCell<MockBatteryDevice> = StaticCell::new();

// Construct a device handle
let battery = BATTERY.init(MockBatteryDevice::new(DeviceId(1)));

// In an async context, initialize services once, then register the device
embedded_services::init().await;
embedded_services::power::policy::register_device(&battery).await.unwrap();

// Controller setup is covered in the next section.
}

Semantics note: In ODP, “Device” types are handles to a single underlying component; the service runtime serializes access. Introducing the controller simply gives policy logic a dedicated handle to act on; it does not create a second owner of the hardware.

Bringing in the Controller

#![allow(unused)]
fn main() {
let controller = CONTROLLER.init(
    MockBatteryController::<&'static mut MockBattery>::new(battery.inner_battery())
);
}

The controller is given a handle to the inner MockBattery (inner_battery()), not a second owner of the hardware. All access is serialized through the service runtime.

What Registration Enables

FeatureEnabled by Registration
Message RoutingThe comms system delivers events to services
Task SpawningServices are polled and run by the executor
Feature ExposureSubfeatures (e.g. fuel_gauge) declared via trait contracts
Test VisibilityServices and devices can be observed in tests
flowchart TD
    A[Component<br/>_Implements Trait_] --> B[Device Wrapper]
    B --> C[Controller<br/>_Implements Service Trait_]
    C --> D[SERVICE_REGISTRY]
    D --> E[Policy Manager<br/>_via Comms_]
    D -->|_Provides_| F[Async Task Execution]

Figure: Service Registration and Runtime Execution

Devices are wrapped and managed by controllers. These are registered into the service registry, which exposes them to both the message dispatcher and the async runtime for polling and task orchestration.

Message Dispatch and Service Binding

Once a controller is registered, the service registry allows the comms system to route incoming events to the correct service based on:

  • The device ID
  • The message type
  • The controller's implementation of the handle() function (as defined by ServiceTraits)

When a message is emitted (e.g. BatteryEvent::UpdateStatus), the comms channel looks up the appropriate service and dispatches the message.

Where ServiceTraits represent the service traits that define a Controller action, implementation may look something like this (in this case, ServiceTraits defines a function named handle, and it calls upon a local function defined in the device implementation):

#![allow(unused)]
fn main() {
impl ServiceTraits for BatteryController {
    async fn handle(&mut self, msg: Message) -> Result<()> {
        match msg {
            Message::Battery(BatteryEvent::UpdateStatus) => {
                self.device.update().await
            }
            _ => Ok(()),
        }
    }
}
}

This provides a flexible pattern where services are matched to message types through trait implementations and static dispatch. No dynamic routing or introspection is used — behavior is known at compile time.

Static Composition, Dynamic Coordination

While all services and components are statically bound into the final binary:

  • Message routing and task polling occur dynamically
  • Controllers only receive messages for devices they were registered to manage
  • Multiple services can be registered independently and coexist without conflict

This pattern supports:

  • Easy testing with mocks or alternate HALs
  • Additive subsystem design (battery, charger, thermal)
  • Isolated debugging of service behavior

EC Services

The Embedded Controller is responsible for an increasing number of tasks that are meant to be always available, independent of the main CPU. The scope of these EC services often goes beyond hardware device concerns alone. These services often need to be exposed to the Operating System and Application layers so that higher-level monitoring and control designs can interact to inspect conditions or configure operating parameters.

Conceptually, any number of services could be exposed to the Operating System in this way. The Windows Operating System specifies a particular set of EC Services that it requires.

These Windows services are discussed in the Embedded Controller Interface Specification

Windows-specific management features such as the Microsoft Power Thermal Framework (MPTF) implementation notes are relevant to this discussion also.

EC Services Architecture

Communication Pathways

flowchart TD
    A[Host OS or Firmware]
    B[ACPI Interface / Mailbox / HID]
    C[EC Service Dispatcher]
    D[Subsystem Controller - Battery, Thermal, etc.]

    A --> B
    B --> C
    C --> D

Figure: EC Service Entry Points Host platforms interact with EC services through one or more communication pathways. These may include ACPI-defined regions, mailbox protocols, or vendor-defined HID messages. The EC processes these via service dispatch logic.

Messaging Exchange Format (Conceptual)

sequenceDiagram
    participant Host
    participant EC

    Host->>EC: Request {Service ID, Command, Payload}
    EC-->>Host: Response {Status, Data}
  

Figure: Message Exchange

The diagram above illustrates the basic message handshake.

This table explains the field data exchanged:

FieldDescription
Service IDIdentifies target subsystem
CommandSpecific operation to perform
PayloadData required for operation
StatusResult of operation
DataOptional result values

Secure and Non-Secure Implementations

In the diagram below, the dark blue sections are those elements that are part of normal (non-secure) memory space and may be called from a service interface directly. As we can see on the Non-Secure side, the ACPI transport channel has access to the EC component implementations either directly or through the FF-A (Firmware Framework Memory Management Protocol).

Secure implementation architecture can be seen in the upcoming Security discussion.

flowchart TD
    A[Untrusted Host OS]
    B[Trusted Runtime Services]
    C[EC Service Gateway]
    D[EC Subsystems]

    A -.->|Filtered Access| C
    B -->|Secure Channel| C
    C --> D

Embedded Controller Security

Naturally, as the scope of concerns that fall to the Embedded Controller has grown, security for these features becomes paramount.

ODP provides support for hardware-enforced isolation of the Embedded Controller Service Interface enforced with Hafnium for hypervisor control.

EC Services

Embedded controller services are available for the operating system to call for various higher-level purposes dictated by specification. The Windows Operating system defines some of these standard services for its platform.

These service interfaces include those for:

  • debug services
  • firmware management services
  • input management services
  • oem services
  • power services
  • time services

Services may be available for operating systems other than Windows.

OEMs may wish to implement their own services as part of their product differentiation.

EC Service communication protocols

With a communication channel protocol established between OS and EC, operating system agents and applications are able to monitor and operate peripheral controllers from application space.

This scope comes with some obvious security ramifications that must be recognized.

Implementations of ODP may be architected for both Secure and Non-Secure system firmware designs, as previously discussed.

Secure Architecture

In the diagram above, the dark blue sections are those elements that are part of normal (non-secure) memory space and may be called from a service interface directly. As we can see on the Non-Secure side, the ACPI transport channel has access to the EC component implementations either directly or through the FF-A (Firmware Framework Memory Management Protocol).

FF-A

The Firmware Framework Memory Management Protocol (Spec) describes the relationship of a hypervisor controlling a set of secure memory partitions with configurable access and ownership attributes and the protocol for exchanging information between these virtualized contexts.

FF-A is available for Arm devices only. A common solution for x64 is still in development. For x64 implementations, use of SMM is employed to orchestrate hypervisor access using the [Hafnium] Rust product.

In a Non-Secure implementation without a hypervisor, the ACPI connected components can potentially change the state within any accessible memory space. An implementation with a hypervisor cannot. It may still be considered a "Non-Secure" implementation, however, as the ACPI data itself is unable to be verified for trust.

In a fully "Secure" implementation, controller code is validated at boot time to insure the trust of the data it provides. Additionally, for certain types of data, digital signing and/or encryption may be used on the data exchanged to provide an additional level of trust.

Secure EC Example

Consult the Secure EC Services Specification for more explanation and a sample implementation discussion.

Embedded Controller Exercises

For a hands-on walkthrough for creating key Embedded Controller components, such as Battery, Charger, and Thermal devices, please see the Component Examples