Skip to content

Commit f3b191b

Browse files
authored
Address student review of Device Driver docs (#4509)
* First pass at addressing student review * nits and pieces
1 parent 59e1baa commit f3b191b

File tree

1 file changed

+61
-28
lines changed

1 file changed

+61
-28
lines changed

docs/how-to/develop-device-driver.md

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,25 @@ Before starting development, obtain the datasheet and any relevant documentation
6161

6262
### Step 2 - Define the Device Manager Component
6363

64-
Use `fprime-util new --component` to create a new component for your device manager. This component will translate device-specific operations into bus transactions. Identify the bus type (I2C, SPI, UART, etc.) and the operations needed (read, write, configure, etc.). These should be reflected in the component's ports by mirroring the bus driver's interface.
64+
Use `fprime-util new --component` to create a new component for your device manager. It is often useful (although not required) to use Events and Telemetry. It is often sufficient to start with a `passive` component, and upgrade to `active` or `queued` later if needed.
6565

66-
For our example `ImuManager` component, we are using an I2C bus, therefore we need to define ports that mirror the `Drv.I2c` interface (see [Drv/Interfaces/I2c.fpp](../../Drv/Interfaces/I2c.fpp)). A `Drv.I2c` provides an input port of type `Drv.I2cWriteRead`, so we need to define an output port of that type in our component:
66+
This component will translate device-specific operations into bus transactions. Identify the bus type (I2C, SPI, UART, etc.) and the operations needed (read, write, configure, etc.). These should be reflected in the component's ports by mirroring the bus driver's interface.
67+
68+
For our `ImuManager` component, we are using an I2C bus, therefore we need to define ports that mirror the `Drv.I2c` interface (see [Drv/Interfaces/I2c.fpp](../../Drv/Interfaces/I2c.fpp)).
69+
For example, a `Drv.I2c` component provides an ***input*** port of type `Drv.I2cWriteRead`, so we need to define an ***output*** port of that type in our component in order to connect to a bus driver component. We mirror each Bus Driver port that we need to use in our Device Manager.
6770

6871
```python
72+
# In: ImuManager.fpp
6973
@ Component emitting telemetry read from an MpuImu
70-
active component ImuManager {
74+
passive component ImuManager {
75+
@ Output port allowing to connect to an I2c bus driver for writeRead operations
7176
output port busWriteRead: Drv.I2cWriteRead
77+
78+
@ Output port allowing to connect to an I2c bus driver for write operations
7279
output port busWrite: Drv.I2c
80+
81+
# We could also mirror the Drv.I2c 'read' port if needed
82+
# but we do not need them for this example
7383
}
7484
```
7585

@@ -78,28 +88,36 @@ active component ImuManager {
7888
It is good practice to create helper functions for device operations, based on your datasheet. These helpers will then be called from your component's port handlers to respond to requests, or for example update telemetry on a schedule.
7989

8090
```cpp
81-
// ------------- Snippet from Component.hpp -------------
82-
// Register addresses (from datasheet)
83-
static constexpr U8 RESET_REG = 0x00;
84-
static constexpr U8 CONFIG_REG = 0x01;
85-
static constexpr U8 DATA_REG = 0x10;
86-
87-
// Register values
88-
static constexpr U8 RESET_VAL = 0x80;
89-
static constexpr U8 DEFAULT_ADDR = 0x48;
90-
static constexpr U8 DATA_SIZE = 6;
91-
92-
// ------------- Snippet from Component.cpp -------------
91+
// In: ImuManager.hpp
92+
class ImuManager final : public ImuManagerComponentBase {
93+
public:
94+
// Register addresses (from datasheet)
95+
static constexpr U8 RESET_REG = 0x00;
96+
static constexpr U8 CONFIG_REG = 0x01;
97+
static constexpr U8 DATA_REG = 0x10;
98+
99+
// Register values
100+
static constexpr U8 RESET_VAL = 0x80;
101+
static constexpr U8 DEFAULT_ADDR = 0x48;
102+
static constexpr U8 DATA_SIZE = 6;
103+
104+
// [... other component code ...]
105+
};
106+
```
107+
108+
```cpp
109+
// In: ImuManager.cpp
110+
93111
// Reset device
94-
Drv::I2cStatus MyDeviceManager::reset() {
112+
Drv::I2cStatus ImuManager::reset() {
95113
U8 cmd[] = {RESET_REG, RESET_VAL}; // From your datasheet
96114
Fw::Buffer writeBuffer(cmd, sizeof(cmd));
97115
// Port call to bus driver to write the buffer
98116
return this->busWrite_out(0, m_address, writeBuffer);
99117
}
100118

101119
// Read sensor data
102-
Drv::I2cStatus MyDeviceManager::read(ImuData& output_data) {
120+
Drv::I2cStatus ImuManager::read(ImuData& output_data) {
103121
U8 regAddr = DATA_REG;
104122
U8 rawData[DATA_SIZE];
105123
Fw::Buffer writeBuffer(&regAddr, 1);
@@ -123,16 +141,20 @@ Once the device-specific helper functions are implemented, integrate them into y
123141
- a) Emit telemetry on a schedule by connecting to a RateGroup
124142
- b) Expose data to the application layer through additional ports
125143

144+
>[!NOTE]
145+
> Functionalities (a) and (b) are shown for illustrative purposes. You may not need to implement both telemetry and data ports depending on your requirements and use case.
146+
126147
First, let's represent our ImuData in FPP so we can use it in telemetry and ports:
127148

128149
```python
150+
# In: MyProject/Types/ImuTypes.fpp
129151
@ Struct representing X, Y, Z data
130152
struct GeometricVector3 {
131153
x: F32
132154
y: F32
133155
z: F32
134156
}
135-
157+
@ Struct representing IMU data
136158
struct ImuData {
137159
acceleration: GeometricVector3
138160
rotation: GeometricVector3
@@ -144,19 +166,23 @@ struct ImuData {
144166

145167
Add a run port to connect to a RateGroup, and implement the run handler to read data and emit telemetry on a regular cadence:
146168
```python
147-
active component ImuManager {
148-
...
169+
# In: Components/ImuManager/ImuManager.fpp
170+
passive component ImuManager {
171+
[... other code ...]
149172

150173
@ Telemetry channel for IMU data (struct of acceleration, rotation, temperature)
151174
telemetry ImuData: ImuData
152175

153176
@ Scheduling port for reading from IMU and writing to telemetry
154177
sync input port run: Svc.Sched
155178

179+
@ Event for logging I2C read errors
180+
event ImuReadError(status: Drv.I2cStatus) severity warning high format "I2C read error with status {}"
156181
}
157182
```
158183

159184
```cpp
185+
// In: Components/ImuManager/ImuManager.cpp
160186
void ImuManager::run_handler(FwIndexType portNum, U32 context) {
161187
ImuData data;
162188
Drv::I2cStatus status = this->read(data);
@@ -174,17 +200,19 @@ void ImuManager::run_handler(FwIndexType portNum, U32 context) {
174200
Add a port that returns data on request:
175201

176202
```python
203+
# In: Components/ImuManager/ImuManager.fpp
177204
@ Port to read IMU data on request. Update data reference and return status
178205
port ImuDataRead(ref data: ImuData) -> Fw.Success
179206

180-
active component ImuManager {
181-
...
207+
passive component ImuManager {
208+
[... other code ...]
182209

183210
sync input port getData: ImuDataRead
184211
}
185212
```
186213

187214
```cpp
215+
// In: Components/ImuManager/ImuManager.cpp
188216
Fw::Success ImuManager::getData_handler(FwIndexType portNum, ImuData& data) {
189217
Drv::I2cStatus status = this->read(data);
190218
return (status == Drv::I2cStatus::I2C_OK) ? Fw::SUCCESS : Fw::FAILURE;
@@ -198,7 +226,8 @@ Fw::Success ImuManager::getData_handler(FwIndexType portNum, ImuData& data) {
198226

199227
Wire your device manager to the bus driver in a topology:
200228

201-
```fpp
229+
```python
230+
# In: topology.fpp
202231
instance imuManager: MyProject.ImuManager base id 0x1000
203232
instance busDriver: Drv.LinuxI2cDriver base id 0x2000
204233

@@ -212,7 +241,7 @@ topology MyTopology {
212241
Then configure the bus driver to open the correct device. This is platform specific. On Linux, this may look like the following:
213242

214243
```cpp
215-
// In Topology.cpp
244+
// In: Topology.cpp
216245
void configureTopology() {
217246
...
218247

@@ -232,7 +261,7 @@ void configureTopology() {
232261

233262
## How-To Develop a Bus Driver
234263

235-
This section focuses on the bus driver component. The bus driver handles communication over a specific bus (I2C, SPI, UART, etc.). If a suitable bus driver already exists in F Prime (this is the case for most common buses on Linux, see inside the `fprime/Drv/` package), you can skip this section. As mentioned earlier, the bus driver's role is to provide a generic interface for read/write operations over a specific bus that a device manager can use. By splitting the bus driver into its own component, we can reuse (a) re-use the same bus driver implementation for multiple device managers, and (b) swap out bus drivers when porting to different platforms, but re-using the same device manager logic.
264+
This section focuses on the bus driver component. The bus driver handles communication over a specific bus (I2C, SPI, UART, etc.). If a suitable bus driver already exists in F Prime (this is the case for most common buses on Linux, see inside the `fprime/Drv/` package), you can skip this section. As mentioned earlier, the bus driver's role is to provide a generic interface for read/write operations over a specific bus that a device manager can use. By splitting the bus driver into its own component, we can (a) re-use the same bus driver implementation for multiple device managers, and (b) swap out bus drivers when porting to different platforms, but re-using the same device manager logic.
236265

237266
In this section, we will keep working with our example MPU6050 IMU sensor connected over I2C. Our goal will be to implement a bus driver for I2C communication on [Zephyr RTOS](https://zephyrproject.org/) instead of Linux. The methodology generalizes to other buses and platforms.
238267

@@ -252,6 +281,7 @@ We learn the following:
252281
Use `fprime-util new --component` to create a new component for your bus driver. The set of ports that a bus driver needs to expose depends on the bus communication protocol (I2C, SPI, UART, etc.). F Prime provides standard interfaces for common bus types in the `Drv/Interfaces/` directory. For I2C, we can use the existing `Drv.I2c` interface (see [Drv/Interfaces/I2c.fpp](../../Drv/Interfaces/I2c.fpp)).
253282

254283
```python
284+
# In: ZephyrI2cDriver.fpp
255285
@ I2C bus driver interface
256286
passive component ZephyrI2cDriver {
257287
# This imports the Drv.I2c interface, adding the required ports to this component
@@ -271,6 +301,7 @@ Bus drivers will most likely require configuration on startup, usually done by t
271301
For our ZephyrI2cDriver, we will implement a public `open()` method that takes an `device` structure to identify the I2C device. This method will store the `device` as a member variable for later use in read/write operations.
272302

273303
```cpp
304+
// In: ZephyrI2cDriver.cpp
274305
Drv::I2cStatus ZephyrI2cDriver::open(const struct device* i2c_device) {
275306
this->m_device = i2c_device;
276307
if (!device_is_ready(this->m_device)) {
@@ -283,7 +314,7 @@ Drv::I2cStatus ZephyrI2cDriver::open(const struct device* i2c_device) {
283314
With this method, projects can now configure the bus driver in `configureTopology()`:
284315

285316
```c++
286-
// In Topology.cpp
317+
// In: Topology.cpp
287318
#include <zephyr/device.h>
288319
static const struct device *i2c_dev = DEVICE_DT_GET(DT_NODELABEL(i2c0));
289320

@@ -303,6 +334,7 @@ void configureTopology() {
303334
Implement the port calls that are part of the bus driver interface. In our case, `Drv.I2c` contains `write`, `read`, and `writeRead` port handlers, for which the function signatures are autocoded by `fprime-util impl`. With the Zephyr I2C API, this may look like the following:
304335

305336
```cpp
337+
// In: ZephyrI2cDriver.cpp
306338
Drv::I2cStatus ZephyrI2cDriver ::read_handler(FwIndexType portNum, U32 addr, Fw::Buffer& buffer) {
307339
int status = i2c_read(this->m_device, buffer.getData(), buffer.getSize(), addr);
308340
if (status != 0) {
@@ -334,19 +366,20 @@ Drv::I2cStatus ZephyrI2cDriver ::writeRead_handler(FwIndexType portNum, U32 addr
334366
Once a different bus driver is implemented, you can use it in your deployment topology. If you were testing your deployment in Linux, you can simply replace the LinuxI2cDriver with our ZephyrI2cDriver:
335367

336368
```diff
369+
# In: topology.fpp
337370
- instance i2cDriver: LinuxI2cDriver base id 0x10015000
338371
+ instance i2cDriver: Zephyr.ZephyrI2cDriver base id 0x10015000
339372
```
340373

341374
And update the configuration code in `configureTopology()` to use the Zephyr-specific device opening method shown in Step 3.
342375

343376
```diff
344-
// In Topology.cpp
377+
// In: Topology.cpp
345378
+ #include <zephyr/device.h>
346379
+ static const struct device *i2c_dev = DEVICE_DT_GET(DT_NODELABEL(i2c0));
347380

348381
void configureTopology() {
349-
- Drv::I2cStatus status = i2cDriver.open("/dev/i2c-1"); // Linux open() call
382+
- bool status = i2cDriver.open("/dev/i2c-1"); // Linux open() call
350383
+ Drv::I2cStatus status = i2cDriver.open(i2c_dev); // Zephyr open() call
351384
if (status != Drv::I2cStatus::I2C_OK) {
352385
Fw::Logger::log("[I2C] Failed to open I2C device\n");

0 commit comments

Comments
 (0)