You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/how-to/develop-device-driver.md
+61-28Lines changed: 61 additions & 28 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -61,15 +61,25 @@ Before starting development, obtain the datasheet and any relevant documentation
61
61
62
62
### Step 2 - Define the Device Manager Component
63
63
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.
65
65
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.
67
70
68
71
```python
72
+
# In: ImuManager.fpp
69
73
@ComponentemittingtelemetryreadfromanMpuImu
70
-
active component ImuManager {
74
+
passive component ImuManager {
75
+
@ Output port allowing to connect to an I2c bus driver for writeRead operations
71
76
output port busWriteRead: Drv.I2cWriteRead
77
+
78
+
@ Output port allowing to connect to an I2c bus driver for write operations
72
79
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
73
83
}
74
84
```
75
85
@@ -78,28 +88,36 @@ active component ImuManager {
78
88
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.
79
89
80
90
```cpp
81
-
// ------------- Snippet from Component.hpp -------------
82
-
// Register addresses (from datasheet)
83
-
staticconstexpr U8 RESET_REG = 0x00;
84
-
staticconstexpr U8 CONFIG_REG = 0x01;
85
-
staticconstexpr U8 DATA_REG = 0x10;
86
-
87
-
// Register values
88
-
staticconstexpr U8 RESET_VAL = 0x80;
89
-
staticconstexpr U8 DEFAULT_ADDR = 0x48;
90
-
staticconstexpr 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
+
93
111
// Reset device
94
-
Drv::I2cStatus MyDeviceManager::reset() {
112
+
Drv::I2cStatus ImuManager::reset() {
95
113
U8 cmd[] = {RESET_REG, RESET_VAL}; // From your datasheet
@@ -123,16 +141,20 @@ Once the device-specific helper functions are implemented, integrate them into y
123
141
- a) Emit telemetry on a schedule by connecting to a RateGroup
124
142
- b) Expose data to the application layer through additional ports
125
143
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
+
126
147
First, let's represent our ImuData in FPP so we can use it in telemetry and ports:
127
148
128
149
```python
150
+
# In: MyProject/Types/ImuTypes.fpp
129
151
@ Struct representing X, Y, Z data
130
152
struct GeometricVector3 {
131
153
x: F32
132
154
y: F32
133
155
z: F32
134
156
}
135
-
157
+
@ Struct representing IMU data
136
158
struct ImuData {
137
159
acceleration: GeometricVector3
138
160
rotation: GeometricVector3
@@ -144,19 +166,23 @@ struct ImuData {
144
166
145
167
Add a run port to connect to a RateGroup, and implement the run handler to read data and emit telemetry on a regular cadence:
146
168
```python
147
-
active component ImuManager {
148
-
...
169
+
# In: Components/ImuManager/ImuManager.fpp
170
+
passive component ImuManager {
171
+
[... other code ...]
149
172
150
173
@ Telemetry channel forIMU data (struct of acceleration, rotation, temperature)
151
174
telemetry ImuData: ImuData
152
175
153
176
@ Scheduling port for reading fromIMUand writing to telemetry
154
177
sync input port run: Svc.Sched
155
178
179
+
@ Event for logging I2C read errors
180
+
event ImuReadError(status: Drv.I2cStatus) severity warning high format"I2C read error with status {}"
Wire your device manager to the bus driver in a topology:
200
228
201
-
```fpp
229
+
```python
230
+
# In: topology.fpp
202
231
instance imuManager: MyProject.ImuManager base id0x1000
203
232
instance busDriver: Drv.LinuxI2cDriver base id0x2000
204
233
@@ -212,7 +241,7 @@ topology MyTopology {
212
241
Then configure the bus driver to open the correct device. This is platform specific. On Linux, this may look like the following:
213
242
214
243
```cpp
215
-
// In Topology.cpp
244
+
// In: Topology.cpp
216
245
void configureTopology() {
217
246
...
218
247
@@ -232,7 +261,7 @@ void configureTopology() {
232
261
233
262
## How-To Develop a Bus Driver
234
263
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.
236
265
237
266
In this section, we will keep working with our example MPU6050IMU sensor connected over I2C. Our goal will be to implement a bus driver forI2C communication on [Zephyr RTOS](https://zephyrproject.org/) instead of Linux. The methodology generalizes to other buses and platforms.
238
267
@@ -252,6 +281,7 @@ We learn the following:
252
281
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)).
253
282
254
283
```python
284
+
# In: ZephyrI2cDriver.fpp
255
285
@I2C bus driver interface
256
286
passive component ZephyrI2cDriver {
257
287
# 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
271
301
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.
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 I2CAPI, this may look like the following:
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:
335
367
336
368
```diff
369
+
# In: topology.fpp
337
370
- instance i2cDriver: LinuxI2cDriver base id0x10015000
338
371
+ instance i2cDriver: Zephyr.ZephyrI2cDriver base id0x10015000
339
372
```
340
373
341
374
And update the configuration code in`configureTopology()` to use the Zephyr-specific device opening method shown in Step 3.
0 commit comments