|
| 1 | +# Device Abstraction |
| 2 | + |
| 3 | +Status: Draft |
| 4 | + |
| 5 | +The OpenPRoT Driver Development Kit (Device Development Kit) provides a set of |
| 6 | +generic Rust traits and types for interacting with I/O peripherals and |
| 7 | +cryptographic algorithm accelerators encountered in the class of devices that |
| 8 | +perform Root of Trust (RoT) functions. |
| 9 | + |
| 10 | +The DDK isolates the OpenPRoT developer from the underlying embedded processor |
| 11 | +and operating system. |
| 12 | + |
| 13 | +## Scope |
| 14 | + |
| 15 | +This section provides a non-exhaustive list of peripherals that fall within the |
| 16 | +scope of the Device Driver Kit (DDK). |
| 17 | + |
| 18 | +### I/O Peripherals |
| 19 | + |
| 20 | +| Device | Description | |
| 21 | +| :--------------------------- | :----------------------------------------- | |
| 22 | +| **SMBus/I2C Monitor/Filter** | | |
| 23 | +| **Delay** | Delay execution for specified durations in microseconds or milliseconds. | |
| 24 | + |
| 25 | +### Cryptographic Functions |
| 26 | + |
| 27 | +Cryptographic Algorithm | Description |
| 28 | +:---------------------- | :---------------------------------- |
| 29 | +**AES** | Symmetric encryption and decryption |
| 30 | +**ECC** | ECDSA signature and verification |
| 31 | +**digest** | Cryptographic hash functions |
| 32 | +**RSA** | RSA signature and verification |
| 33 | + |
| 34 | +We will refer to the collection of I/O peripherals and cryptographic algorithm |
| 35 | +accelerators as peripherals from now on. |
| 36 | + |
| 37 | +## Design Goals |
| 38 | + |
| 39 | +### Platform Agnostic |
| 40 | + |
| 41 | +The goal of the DDK is to provide a consistent and flexible interface for |
| 42 | +applications to invoke peripheral functionality, regardless of whether the |
| 43 | +interaction with the underlying peripheral driver is through system calls to a |
| 44 | +kernel mode device driver, inter-task communication or direct access to |
| 45 | +memory-mapped peripheral registers. |
| 46 | + |
| 47 | +### Execution Model Agnostic |
| 48 | + |
| 49 | +The DDK should be agnostic of the execution model and provide flexibility for |
| 50 | +its users. |
| 51 | + |
| 52 | +The collection of traits in the DDK is to be segregated in different crates |
| 53 | +according to the APIs it exposes. i.e. synchronous, asynchronous, and |
| 54 | +non-blocking APIs. |
| 55 | + |
| 56 | +These crates ensure that DDK can cater to various execution models, making it |
| 57 | +versatile for different application requirements. |
| 58 | + |
| 59 | +* Synchronous APIs: The main open-prot-ddk crate contains blocking traits |
| 60 | + where operations are performed synchronously before returning. |
| 61 | +* Asynchronous APIs: The open-prot-ddk-async crate provides traits for |
| 62 | + asynchronous operations using Rust's async/await model. |
| 63 | +* Non-blocking APIs: The open-prot-ddk-nb crate offers traits for non-blocking |
| 64 | + operations which allows for polling-based execution. |
| 65 | + |
| 66 | +## Design Principles |
| 67 | + |
| 68 | +### Minimalism |
| 69 | + |
| 70 | +The design of the DDK prioritizes simplicity, making it straightforward for |
| 71 | +developers to implement. By avoiding unnecessary complexity, it ensures that the |
| 72 | +core traits and functionalities remain clear and easy to understand. |
| 73 | + |
| 74 | +### Zero Cost |
| 75 | + |
| 76 | +This principle ensures that using the DDK introduces no additional overhead. In |
| 77 | +other words, the abstraction layer should neither slow down the system nor |
| 78 | +consume more resources than direct hardware access. |
| 79 | + |
| 80 | +### Composability |
| 81 | + |
| 82 | +The HAL shall be designed to be modular and flexible, allowing developers to |
| 83 | +easily combine different components. This composability means that various |
| 84 | +drivers and peripherals can work together seamlessly, making it easier to build |
| 85 | +complex systems from simple, reusable parts. |
| 86 | + |
| 87 | +### Robust Error Handling |
| 88 | + |
| 89 | +Trait methods must be designed to handle potential failures, as hardware |
| 90 | +interactions can be unpredictable. This means **that methods invoking hardware |
| 91 | +should return a Result type to account for various failure scenarios,** |
| 92 | +including misconfiguration, power issues, or disabled hardware. |
| 93 | + |
| 94 | +```rust |
| 95 | +pub trait SpiRead<W> { type Error; |
| 96 | + |
| 97 | + |
| 98 | +fn read(&mut self, words: &mut [W]) -> Result<(), Self::Error>; |
| 99 | + |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +While the default approach should be to use fallible methods, HAL |
| 104 | +implementations can also provide infallible versions if the hardware guarantees |
| 105 | +no failure. This ensures that generic code can rely on robust error handling, |
| 106 | +while platform-specific code can avoid unnecessary boilerplate when appropriate. |
| 107 | + |
| 108 | +```rust |
| 109 | +use core::convert::Infallible; |
| 110 | + |
| 111 | +pub struct MyInfallibleSpi; |
| 112 | + |
| 113 | +impl SpiRead<u8> for MyInfallibleSpi { |
| 114 | + type Error = Infallible; |
| 115 | + |
| 116 | + fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { |
| 117 | + // Perform the read operation |
| 118 | + Ok(()) |
| 119 | + } |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +### Separate Control and Data Path Operations {#separate-control-and-data-path-operations} |
| 124 | + |
| 125 | +* Clarity: By separating configuration (control path) from data transfer (data |
| 126 | + path), each part of the code has a clear responsibility. This makes the code |
| 127 | + easier to understand and maintain. |
| 128 | +* Modularity: It allows for more modular design, where control and data |
| 129 | + handling can be developed and tested independently. |
| 130 | + |
| 131 | +Example |
| 132 | + |
| 133 | +This example is extracted from Tock's TRD3 design document. Uart functionality |
| 134 | +is decomposed into fine grained traits defined for control path (Configure) and |
| 135 | +data path operations (Transmit and Receive). |
| 136 | + |
| 137 | +```rust |
| 138 | +pub trait Configure { |
| 139 | + fn configure(&self, params: Parameters) -> ReturnCode; |
| 140 | +} |
| 141 | +pub trait Transmit<'a> { |
| 142 | + fn set_transmit_client(&self, client: &'a dyn TransmitClient); |
| 143 | + fn transmit_buffer( |
| 144 | + &self, |
| 145 | + tx_buffer: &'static mut [u8], |
| 146 | + tx_len: usize, |
| 147 | + ) -> (ReturnCode, Option<&'static mut [u8]>); |
| 148 | + fn transmit_word(&self, word: u32) -> ReturnCode; |
| 149 | + fn transmit_abort(&self) -> ReturnCode; |
| 150 | +} |
| 151 | +pub trait Receive<'a> { |
| 152 | + fn set_receive_client(&self, client: &'a dyn ReceiveClient); |
| 153 | + fn receive_buffer( |
| 154 | + &self, |
| 155 | + rx_buffer: &'static mut [u8], |
| 156 | + rx_len: usize, |
| 157 | + ) -> (ReturnCode, Option<&'static mut [u8]>); |
| 158 | + fn receive_word(&self) -> ReturnCode; |
| 159 | + fn receive_abort(&self) -> ReturnCode; |
| 160 | +} |
| 161 | +pub trait Uart<'a>: Configure + Transmit<'a> + Receive<'a> {} |
| 162 | +pub trait UartData<'a>: Transmit<'a> + Receive<'a> {} |
| 163 | +``` |
| 164 | + |
| 165 | +#### Use Case : Device Sharing |
| 166 | + |
| 167 | +* **Peripheral Client Task**: This task is only exposed to data path |
| 168 | + operations, such as reading from or writing to the peripheral. It interacts |
| 169 | + with the peripheral server to perform these operations without having direct |
| 170 | + access to the configuration settings. |
| 171 | +* **Peripheral Server Task**: This task is responsible for managing and |
| 172 | + sharing the peripheral functionality across multiple client tasks. It has |
| 173 | + the exclusive role of configuring the peripheral for data transfer |
| 174 | + operations, ensuring that all configuration changes are centralized and |
| 175 | + controlled. This separation allows for robust access control and simplifies |
| 176 | + the management of peripheral settings. |
| 177 | + |
| 178 | +## Methodology |
| 179 | + |
| 180 | +In order to accomplish this goal in an efficient fashion the DDK should not try |
| 181 | +to reinvent the wheel but leverage existing work in the Rust community such as |
| 182 | +the Embedded Rust Workgroup's embedded-hal or the RustCrypto projects. |
| 183 | + |
| 184 | +As much as possible, the OpenPRoT workgroup should evaluate, curate, and |
| 185 | +recommend existing abstractions that have already gained wide adoption. |
| 186 | + |
| 187 | +By leveraging well-established and widely accepted abstractions, the DDK can |
| 188 | +ensure compatibility, reliability, and ease of integration across various |
| 189 | +platforms and applications. This approach not only saves development time and |
| 190 | +resources but also promotes standardization and interoperability within the |
| 191 | +ecosystem. |
| 192 | + |
| 193 | +When abstractions need to be invented, as is the case for the I3C protocol, for |
| 194 | +instance the OpenPRot workgroup will design it according to the community |
| 195 | +guidelines for the project it is curating from and make contributions upstream. |
| 196 | + |
| 197 | +## Use Cases |
| 198 | + |
| 199 | +This section illustrates the contexts where the DDK can be used. |
| 200 | + |
| 201 | +### Low Level Driver |
| 202 | + |
| 203 | +A low level driver implements a peripheral (or a cryptographic algorithm) driver |
| 204 | +trait by accessing memory mapped registers directly and it is distributed as a |
| 205 | +`no_std` crate. |
| 206 | + |
| 207 | +A `no_std` crate like the one depicted below would be linked directly into a |
| 208 | +user mode task with exclusive peripheral ownership. This use case is encountered |
| 209 | +in microkernel-based embedded O/S such as Oxide HUBRIS where drivers run in |
| 210 | +unprivileged mode. |
| 211 | + |
| 212 | +<img src="figure1.png" alt="figure1" width="300"/> |
| 213 | + |
| 214 | +### Proxy for a Kernel Mode Device Driver |
| 215 | + |
| 216 | +In this section, we explore how a trait from the Device Development Kit (DDK) |
| 217 | +can enhance portability by decoupling the application writer from the underlying |
| 218 | +embedded stack. |
| 219 | + |
| 220 | +The user of the peripheral is an application that is interacting with a kernel |
| 221 | +mode device driver via system calls, but is completely isolated from the |
| 222 | +underlying implementation. |
| 223 | + |
| 224 | +This is applicable to any O/S with device drivers living in the kernel, like the |
| 225 | +Tock O/S. |
| 226 | + |
| 227 | +<img src="figure2.png" alt="figure2" width="500"/> |
| 228 | + |
| 229 | +### Proxy for a Peripheral Server Task |
| 230 | + |
| 231 | +In this section, we will explore once more how traits from the Device |
| 232 | +Development Kit (DDK) can enhance portability by decoupling the application |
| 233 | +writer from the underlying Operating System architecture. This scenario is |
| 234 | +applicable to any microkernel-based O/S |
| 235 | + |
| 236 | +The xyz-i2c-ipc-impl depicted below is distributed as a `no_std` driver crate |
| 237 | +and is linked to a I2C client task. The I2C client task is an application that |
| 238 | +is interacting with a user mode device driver, named the I2C server task via |
| 239 | +message passing. |
| 240 | + |
| 241 | +The I2C Server task owns the actual peripheral and is linked to a |
| 242 | +xyz-i2c-drv-imp driver crate, which is a low-level driver. . |
| 243 | + |
| 244 | +<img src="figure3.png" alt="figure3" width="600"/> |
| 245 | + |
| 246 | +The I2C client task sends requests to the I2C peripheral owned by the server |
| 247 | +task via message passing, completely oblivious to the underlying implementation. |
| 248 | + |
| 249 | +<img src="figure4.png" alt="figure4" width="600"/> |
0 commit comments