Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions docs/src/specification/device_abstraction/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# Device Abstraction

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.

## Scope

This section provides a non-exhaustive list of peripherals that fall within the
scope of the Device Driver Kit (DDK).

### I/O Peripherals

| Device | Description |
| :--------------------------- | :----------------------------------------- |
| **SMBus/I2C Monitor/Filter** | |
| **Delay** | Delay execution for specified durations in microseconds or milliseconds. |

### Cryptographic Functions

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.

## Design Goals

### Platform Agnostic

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.

### Execution Model Agnostic

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.

* Synchronous APIs: The main open-prot-ddk crate contains blocking traits
where operations are performed synchronously before returning.
* Asynchronous APIs: The open-prot-ddk-async crate provides traits for
asynchronous operations using Rust's async/await model.
* Non-blocking APIs: The open-prot-ddk-nb crate offers traits for non-blocking
operations which allows for polling-based execution.

## Design Principles

### Minimalism

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.

### Zero Cost

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.

### Composability

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.

### Robust Error Handling

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.

```rust
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.

```rust
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(())
}
}
```

### Separate Control and Data Path Operations {#separate-control-and-data-path-operations}

* Clarity: By separating configuration (control path) from data transfer (data
path), each part of the code has a clear responsibility. This makes the code
easier to understand and maintain.
* Modularity: It allows for more modular design, where control and data
handling can be developed and tested independently.

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).

```rust
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> {}
```

#### Use Case : Device Sharing

* **Peripheral Client Task**: This task is only exposed to data path
operations, such as reading from or writing to the peripheral. It interacts
with the peripheral server to perform these operations without having direct
access to the configuration settings.
* **Peripheral Server Task**: This task is responsible for managing and
sharing the peripheral functionality across multiple client tasks. It has
the exclusive role of configuring the peripheral for data transfer
operations, ensuring that all configuration changes are centralized and
controlled. This separation allows for robust access control and simplifies
the management of peripheral settings.

## Methodology

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.

## Use Cases

This section illustrates the contexts where the DDK can be used.

### Low Level Driver

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.

<img src="figure1.png" alt="figure1" width="300"/>

### Proxy for a Kernel Mode Device Driver

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.

<img src="figure2.png" alt="figure2" width="500"/>

### Proxy for a Peripheral Server Task

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. .

<img src="figure3.png" alt="figure3" width="600"/>

The I2C client task sends requests to the I2C peripheral owned by the server
task via message passing, completely oblivious to the underlying implementation.

<img src="figure4.png" alt="figure4" width="600"/>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.