Skip to content

Commit 4c978ce

Browse files
committed
Adding documentation for threadsafe Serial.
1 parent 55b23a1 commit 4c978ce

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed

docs/03-threadsafe-serial.md

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<img src="https://content.arduino.cc/website/Arduino_logo_teal.svg" height="100" align="right"/>
2+
3+
Threadsafe `Serial`
4+
===================
5+
## Introduction
6+
While both `SPI` and `Wire` are communication protocols which explicitly exist to facilitate communication between one server device and multiple client devices there are no such considerations for the `Serial` interface. `Serial` communication usually exists in a one-to-one mapping between two communication partners of equal power (both can initiate communication on their own right, there is no server/client relationship).
7+
8+
One example would be an Arduino sketch sending AT commands to a modem and interpreting the result of those commands. Another example would be a GPS unit sending NMEA encoded location data to the Arduino for parsing. In both cases the only sensible software representation for such functionality (AT command module or NMEA message parser) is a single thread. Also in both cases it is undesirable for other threads to inject other kind of data into the serial communication as this would only confuse i.e. the AT controlled modem. A good example for multiple threads writing to serial would be logging. A plausible example for multiple threads reading from `Serial` would be to i.e. split the NMEA parser across multiple threads, i.e. one thread only parses RMC-messages, another parses GGA-messages, etc. In any case the threadsafe `Serial` has to both support single-writer/single-reader and multiple-write/multiple-reader scenarios.
9+
10+
## Initialize Serial with `begin()`
11+
In order to initialize the serial interface for any given thread `Serial.begin(baudrate)` needs to be called in any thread's `setup()` which desires to have **writing** access to the serial interface. Since it does not make sense to support multiple different baudrates (i.e. Thread_A writing with 9600 baud and Thread_B writing with 115200 baud - if you really need this, spread the attached serial devices to different serial ports), the first thread to call `Serial.begin()` locks in the configuration for all other threads. A sensible approach is to call `Serial.begin()` within the main `*.ino`-file and only then start the other threads, i.e.
12+
```C++
13+
/* MyThreadsafeSerialDemo.ino */
14+
void setup()
15+
{
16+
Serial.begin(9600);
17+
while (!Serial) { }
18+
/* ... */
19+
Thread_1.start();
20+
Thread_2.start();
21+
/* ... */
22+
}
23+
```
24+
```C++
25+
/* Thread_1.inot */
26+
void setup() {
27+
Serial.begin(9600);
28+
}
29+
```
30+
```C++
31+
/* Thread_2.inot */
32+
void setup() {
33+
Serial.begin(9600);
34+
}
35+
```
36+
37+
## Write to Serial with `print()`/`println()`
38+
([`examples/Threadsafe_IO/Serial_Writer`](../examples/Threadsafe_IO/Serial_Writer))
39+
40+
Since the threadsafe `Serial` is derived from [`arduino::HardwareSerial`](https://github.com/arduino/ArduinoCore-API/blob/master/api/HardwareSerial.h) it does support the full range of the usual `Serial` API. This means that you can simply write to the `Serial` interface you've been using with the single threaded application.
41+
```C++
42+
Serial.print("This is a test message #");
43+
Serial.println(counter);
44+
```
45+
46+
### Prevent message break-up using `block()`/`unblock()`
47+
([`examples/Threadsafe_IO/Serial_Writer`](../examples/Threadsafe_IO/Serial_Writer))
48+
49+
Due to the pre-emptive nature of the underlying mbed-os a multi-line `Serial.print/println()` could be interrupted at any point in time. When multiple threads are writing to the Serial interface, this can lead to jumbled-up messages.
50+
```C++
51+
/* Thread_1.inot */
52+
void loop() {
53+
Serial.print("This ");
54+
Serial.print("is ");
55+
Serial.print("a ");
56+
Serial.print("multi-line ");
57+
Serial.print("log ");
58+
/* Interruption by scheduler and context switch. */
59+
Serial.print("message ");
60+
Serial.print("from ");
61+
Serial.print("thread #1.");
62+
Serial.println();
63+
}
64+
```
65+
```C++
66+
/* Thread_2.inot */
67+
void loop() {
68+
Serial.print("This ");
69+
Serial.print("is ");
70+
Serial.print("a ");
71+
Serial.print("multi-line ");
72+
Serial.print("log ");
73+
Serial.print("message ");
74+
Serial.print("from ");
75+
Serial.print("thread #2.");
76+
Serial.println();
77+
}
78+
```
79+
The resulting serial output of `Thread_1` being interrupted at the marked spot and `Thread_2` being scheduled can be seen below:
80+
```bash
81+
This is a multi-line log This is a multi-line log message from thread #2.
82+
message from thread #1.
83+
84+
```
85+
In order to prevent such break-ups a `block()`/`unblock()` API is introduced which ensures that the messages are printed in the intended order, i.e.
86+
```C++
87+
/* Thread_1.inot */
88+
void loop() {
89+
Serial.block();
90+
Serial.print("This ");
91+
Serial.print("is ");
92+
/* ... */
93+
Serial.print("thread #1.");
94+
Serial.println();
95+
Serial.unblock();
96+
}
97+
```
98+
```C++
99+
/* Thread_2.inot */
100+
void loop() {
101+
Serial.block();
102+
Serial.print("This ");
103+
Serial.print("is ");
104+
/* ... */
105+
Serial.print("thread #2.");
106+
Serial.println();
107+
Serial.unblock();
108+
}
109+
```
110+
Now the thread messages are printed in the order one would expect:
111+
```bash
112+
This is a multi-line log message from thread #2.
113+
This is a multi-line log message from thread #1.
114+
```
115+
116+
## Read from `Serial`
117+
([`examples/Threadsafe_IO/Serial_Reader`](../examples/Threadsafe_IO/Serial_Reader))
118+
119+
Reading from the `Serial` interface can be accomplished using the `read()`, `peek()` and `available()` APIs.
120+
```C++
121+
/* Thread_1.inot */
122+
void loop()
123+
{
124+
/* Read data from the serial interface into a String. */
125+
String serial_msg;
126+
while (Serial.available())
127+
serial_msg += (char)Serial.read();
128+
/* ... */
129+
}
130+
```
131+
Whenever a thread first call any of those three APIs a thread-dedicated receive ringbuffer is created and any incoming serial communication from that point on is copied in the threads dedicated receive ringbuffer. Having a dedicated receive ringbuffer per thread prevents "data stealing" from other threads in a multiple reader scenario, where the first thread to call `read()` would in fact receive the data and all other threads would miss out on it.

docs/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ Threading on Arduino can be achieved by leveraging the [Arduino_Threads](https:/
1414
### Table of Contents
1515
* [Threading Basics](01-threading-basics.md)
1616
* [Data exchange between threads](02-data-exchange.md)
17+
* [Threadsafe `Serial`](03-threadsafe-serial.md)

0 commit comments

Comments
 (0)