Skip to content

Commit ceb80a7

Browse files
authored
[spec] Initial DDK draft. (#39)
This document is still WIP but it is being open sourced to request feedback from the community. Signed-off-by: Miguel Osorio <[email protected]>
1 parent 6dc787f commit ceb80a7

File tree

5 files changed

+249
-0
lines changed

5 files changed

+249
-0
lines changed
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
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"/>
167 KB
Loading
308 KB
Loading
284 KB
Loading
70.2 KB
Loading

0 commit comments

Comments
 (0)