Skip to content

Commit 7d9070d

Browse files
committed
[io] Implement blocking IODevice for fibers
1 parent a4bd34a commit 7d9070d

File tree

3 files changed

+80
-39
lines changed

3 files changed

+80
-39
lines changed

src/modm/io/iodevice_wrapper.hpp renamed to src/modm/io/iodevice_wrapper.hpp.in

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
#define MODM_IODEVICE_WRAPPER_HPP
1717

1818
#include <stdint.h>
19-
19+
%% if with_fiber
20+
#include <modm/processing/fiber.hpp>
21+
%% endif
22+
%#
2023
#include "iodevice.hpp"
2124

2225
namespace modm
@@ -39,25 +42,49 @@ IOBuffer
3942
template< class Device, IOBuffer behavior >
4043
class IODeviceWrapper : public IODevice
4144
{
45+
%% if with_fiber
46+
static constexpr fiber::id NoOwner{fiber::id(-1)};
47+
static inline fiber::id id{NoOwner};
48+
%% endif
4249
public:
4350
IODeviceWrapper() = default;
4451
using IODevice::write;
4552

4653
void
4754
write(char c) override
4855
{
49-
bool written;
50-
do
56+
if constexpr (behavior == IOBuffer::BlockIfFull)
57+
%% if with_fiber
5158
{
52-
written = Device::write(uint8_t(c));
59+
// lock this device to one fiber until a newline is received
60+
const auto me = this_fiber::get_id();
61+
this_fiber::poll([&]{ return id == NoOwner or id == me; });
62+
id = me;
63+
64+
this_fiber::poll([&]{ return Device::write(uint8_t(c)); });
65+
66+
if (c == '\n') id = NoOwner;
5367
}
54-
while(behavior == IOBuffer::BlockIfFull and not written);
68+
%% else
69+
while (not Device::write(uint8_t(c))) ;
70+
%% endif
71+
else Device::write(uint8_t(c));
5572
}
5673

5774
void
5875
flush() override
5976
{
77+
%% if with_fiber
78+
const auto me = this_fiber::get_id();
79+
this_fiber::poll([&]{ return id == NoOwner; });
80+
id = me;
81+
82+
this_fiber::poll([&]{ return Device::isWriteFinished(); });
83+
84+
id = NoOwner;
85+
%% else
6086
Device::flushWriteBuffer();
87+
%% endif
6188
}
6289

6390
bool
@@ -71,6 +98,10 @@ class IODeviceWrapper : public IODevice
7198
template< class Device, IOBuffer behavior >
7299
class IODeviceObjectWrapper : public IODevice
73100
{
101+
%% if with_fiber
102+
static constexpr fiber::id NoOwner{fiber::id(-1)};
103+
fiber::id id{NoOwner};
104+
%% endif
74105
Device &device;
75106
public:
76107
IODeviceObjectWrapper(Device& device) : device{device} {}
@@ -79,18 +110,38 @@ class IODeviceObjectWrapper : public IODevice
79110
void
80111
write(char c) override
81112
{
82-
bool written;
83-
do
113+
if constexpr (behavior == IOBuffer::BlockIfFull)
114+
%% if with_fiber
84115
{
85-
written = device.write(uint8_t(c));
116+
// lock this device to one fiber until a newline is received
117+
const auto me = this_fiber::get_id();
118+
this_fiber::poll([&]{ return id == NoOwner or id == me; });
119+
id = me;
120+
121+
this_fiber::poll([&]{ return device.write(uint8_t(c)); });
122+
123+
if (c == '\n') id = NoOwner;
86124
}
87-
while(behavior == IOBuffer::BlockIfFull and not written);
125+
%% else
126+
while (not device.write(uint8_t(c))) ;
127+
%% endif
128+
else device.write(uint8_t(c));
88129
}
89130

90131
void
91132
flush() override
92133
{
134+
%% if with_fiber
135+
const auto me = this_fiber::get_id();
136+
this_fiber::poll([&]{ return id == NoOwner; });
137+
id = me;
138+
139+
this_fiber::poll([&]{ return device.isWriteFinished(); });
140+
141+
id = NoOwner;
142+
%% else
93143
device.flushWriteBuffer();
144+
%% endif
94145
}
95146

96147
bool

src/modm/io/module.lb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def init(module):
1818
def prepare(module, options):
1919
module.depends(
2020
":architecture:accessor",
21+
":architecture:fiber",
2122
":math:utils")
2223

2324
is_avr = options[":target"].identifier.platform in ["avr"]
@@ -45,12 +46,13 @@ def build(env):
4546
env.substitutions = {
4647
"is_hosted": target.platform == "hosted",
4748
"is_avr": target.platform == "avr",
49+
"with_fiber": env.has_module(":processing:fiber"),
4850
"family": target.family,
4951
"core": core,
5052
}
5153
env.outbasepath = "modm/src/modm/io"
5254
env.copy("iodevice.hpp")
53-
env.copy("iodevice_wrapper.hpp")
55+
env.template("iodevice_wrapper.hpp.in")
5456
env.template("iostream_printf.cpp.in")
5557
env.template("iostream.hpp.in")
5658
env.template("iostream_chrono.hpp.in")

src/modm/io/module.md

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,41 +24,14 @@ stream.printf("format number 8: %u or as signed -100: %d", 8, -100);
2424
## Redirecting IOStreams
2525
2626
The `modm::IODeviceWrapper` transforms any peripheral device that provides static
27-
`write()` and `read()` functions into an `IODevice`.
28-
29-
You have to decide what happens when the device buffer is full and you cannot
30-
write to it at the moment. There are two options:
31-
32-
1. busy wait until the buffer is free, or
33-
2. discard the bytes that cannot be written.
34-
35-
Option 1 has the advantage, that none of your data will be lost,
36-
however, busy-waiting can take a long time and can mess up your
37-
program timings.
38-
There is also a **high risk of deadlock**, when writing to a
39-
IODevice inside of an interrupt and then busy-waiting forever
40-
because the IODevice requires interrupts itself to send out
41-
the data.
42-
43-
It is therefore highly recommended to use option 2, where surplus
44-
data will be discarded.
45-
You should increase the IODevice buffer size, if you experience
46-
missing data from your connection.
47-
This behavior is also deadlock safe when called from inside another
48-
interrupt, and your program timing is minimally affected (essentially
49-
only coping data into the buffer).
50-
51-
There is no default template argument, so that you hopefully make
52-
a conscious decision and be aware of this behavior.
53-
54-
Example:
27+
`write()` and `read()` functions into an `IODevice`:
5528
5629
```cpp
5730
// configure a UART
5831
using Uart = Uart0;
5932
6033
// wrap it into an IODevice
61-
modm::IODeviceWrapper<Uart, modm::IOBuffer::DiscardIfFull> device;
34+
modm::IODeviceWrapper<Uart, modm::IOBuffer::BlockIfFull> device;
6235
6336
// use this device to print a message
6437
device.write("Hello");
@@ -67,3 +40,18 @@ device.write("Hello");
6740
modm::IOStream stream(device);
6841
stream << " World!";
6942
```
43+
44+
45+
## IODevice Buffer Behavior
46+
47+
The `modm::IODeviceWrapper` can be configured to discard or block in case the
48+
device is full. Discarding data is always non-blocking, however, blocking on
49+
write involves waiting in a loop until the device has space. This can cause a
50+
deadlock when called inside an interrupt!
51+
52+
If compiled with the `modm:processing:fiber` module, the blocking behavior
53+
allows the fiber to yield while waiting for the device. To prevent interlaced
54+
output from different fibers, the `write(char)` function is protected by a
55+
mutex that releases when a newline character `\n` is written. The `flush()`
56+
function is also mutex protected to allow one fiber to reliably flush the
57+
stream without having other fibers push data into the device.

0 commit comments

Comments
 (0)