Status: Draft
The OpenPRoT Driver Development Kit (Device Development Kit) provides a set of generic Rust traits and types for interacting with I/O peripherals and cryptographic algorithm accelerators encountered in the class of devices that perform Root of Trust (RoT) functions.
The DDK isolates the OpenPRoT developer from the underlying embedded processor and operating system.
This section provides a non-exhaustive list of peripherals that fall within the scope of the Device Driver Kit (DDK).
| Device | Description |
|---|---|
| SMBus/I2C Monitor/Filter | |
| Delay | Delay execution for specified durations in microseconds or milliseconds. |
| Cryptographic Algorithm | Description |
|---|---|
| AES | Symmetric encryption and decryption |
| ECC | ECDSA signature and verification |
| digest | Cryptographic hash functions |
| RSA | RSA signature and verification |
We will refer to the collection of I/O peripherals and cryptographic algorithm accelerators as peripherals from now on.
The goal of the DDK is to provide a consistent and flexible interface for applications to invoke peripheral functionality, regardless of whether the interaction with the underlying peripheral driver is through system calls to a kernel mode device driver, inter-task communication or direct access to memory-mapped peripheral registers.
The DDK should be agnostic of the execution model and provide flexibility for its users.
The collection of traits in the DDK is to be segregated in different crates according to the APIs it exposes. i.e. synchronous, asynchronous, and non-blocking APIs.
These crates ensure that DDK can cater to various execution models, making it versatile for different application requirements.
The design of the DDK prioritizes simplicity, making it straightforward for developers to implement. By avoiding unnecessary complexity, it ensures that the core traits and functionalities remain clear and easy to understand.
This principle ensures that using the DDK introduces no additional overhead. In other words, the abstraction layer should neither slow down the system nor consume more resources than direct hardware access.
The HAL shall be designed to be modular and flexible, allowing developers to easily combine different components. This composability means that various drivers and peripherals can work together seamlessly, making it easier to build complex systems from simple, reusable parts.
Trait methods must be designed to handle potential failures, as hardware interactions can be unpredictable. This means that methods invoking hardware should return a Result type to account for various failure scenarios, including misconfiguration, power issues, or disabled hardware.
pub trait SpiRead<W> { type Error; fn read(&mut self, words: &mut [W]) -> Result<(), Self::Error>; }
While the default approach should be to use fallible methods, HAL implementations can also provide infallible versions if the hardware guarantees no failure. This ensures that generic code can rely on robust error handling, while platform-specific code can avoid unnecessary boilerplate when appropriate.
use core::convert::Infallible; pub struct MyInfallibleSpi; impl SpiRead<u8> for MyInfallibleSpi { type Error = Infallible; fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { // Perform the read operation Ok(()) } }
Example
This example is extracted from Tock's TRD3 design document. Uart functionality is decomposed into fine grained traits defined for control path (Configure) and data path operations (Transmit and Receive).
pub trait Configure { fn configure(&self, params: Parameters) -> ReturnCode; } pub trait Transmit<'a> { fn set_transmit_client(&self, client: &'a dyn TransmitClient); fn transmit_buffer( &self, tx_buffer: &'static mut [u8], tx_len: usize, ) -> (ReturnCode, Option<&'static mut [u8]>); fn transmit_word(&self, word: u32) -> ReturnCode; fn transmit_abort(&self) -> ReturnCode; } pub trait Receive<'a> { fn set_receive_client(&self, client: &'a dyn ReceiveClient); fn receive_buffer( &self, rx_buffer: &'static mut [u8], rx_len: usize, ) -> (ReturnCode, Option<&'static mut [u8]>); fn receive_word(&self) -> ReturnCode; fn receive_abort(&self) -> ReturnCode; } pub trait Uart<'a>: Configure + Transmit<'a> + Receive<'a> {} pub trait UartData<'a>: Transmit<'a> + Receive<'a> {}
In order to accomplish this goal in an efficient fashion the DDK should not try to reinvent the wheel but leverage existing work in the Rust community such as the Embedded Rust Workgroup's embedded-hal or the RustCrypto projects.
As much as possible, the OpenPRoT workgroup should evaluate, curate, and recommend existing abstractions that have already gained wide adoption.
By leveraging well-established and widely accepted abstractions, the DDK can ensure compatibility, reliability, and ease of integration across various platforms and applications. This approach not only saves development time and resources but also promotes standardization and interoperability within the ecosystem.
When abstractions need to be invented, as is the case for the I3C protocol, for instance the OpenPRot workgroup will design it according to the community guidelines for the project it is curating from and make contributions upstream.
This section illustrates the contexts where the DDK can be used.
A low level driver implements a peripheral (or a cryptographic algorithm) driver trait by accessing memory mapped registers directly and it is distributed as a no_std crate.
A no_std crate like the one depicted below would be linked directly into a user mode task with exclusive peripheral ownership. This use case is encountered in microkernel-based embedded O/S such as Oxide HUBRIS where drivers run in unprivileged mode.
In this section, we explore how a trait from the Device Development Kit (DDK) can enhance portability by decoupling the application writer from the underlying embedded stack.
The user of the peripheral is an application that is interacting with a kernel mode device driver via system calls, but is completely isolated from the underlying implementation.
This is applicable to any O/S with device drivers living in the kernel, like the Tock O/S.
In this section, we will explore once more how traits from the Device Development Kit (DDK) can enhance portability by decoupling the application writer from the underlying Operating System architecture. This scenario is applicable to any microkernel-based O/S
The xyz-i2c-ipc-impl depicted below is distributed as a no_std driver crate and is linked to a I2C client task. The I2C client task is an application that is interacting with a user mode device driver, named the I2C server task via message passing.
The I2C Server task owns the actual peripheral and is linked to a xyz-i2c-drv-imp driver crate, which is a low-level driver. .
The I2C client task sends requests to the I2C peripheral owned by the server task via message passing, completely oblivious to the underlying implementation.