summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--posts/serial-protocols/index.html48
1 files changed, 24 insertions, 24 deletions
diff --git a/posts/serial-protocols/index.html b/posts/serial-protocols/index.html
index 4201275..b5744a5 100644
--- a/posts/serial-protocols/index.html
+++ b/posts/serial-protocols/index.html
@@ -57,16 +57,16 @@
summarize some points on how to design a serial protocol that is simple to implement and works reliably even under error
conditions.</p>
<p>If you have done low-level microcontroller firmware you will regularly have had to stuff some data up a serial port to
-another microcontroller or to a computer. In the age of USB, a serial port is still the simplest and quickest way to get
-communication to a control computer up and running. Integrating a ten thousand-line USB stack into your firmware and
-writing the necessary low-level drivers on the host side might take days. Poking a few registers to set up your UART to
-talk to an external hardware USB to serial converter is a matter of minutes.</p>
+another microcontroller or to a computer. In the age of USB, an old-school serial port is still the simplest and
+quickest way to get communication to a control computer up and running. Integrating a ten thousand-line USB stack into
+your firmware and writing the necessary low-level drivers on the host side might take days. Poking a few registers to
+set up your UART to talk to an external hardware USB to serial converter is a matter of minutes.</p>
<p>This simplicity is treacherous, though. Oftentimes, you start writing your serial protocol as needs arise. Things might
-start harmless with something like <tt class="docutils literal">SET_LED ON\n</tt>, but unless you proceed it is easy to end up in a hot mess of command
+start harmless with something like <tt class="docutils literal">SET_LED ON\n</tt>, but as the code grows it is easy to end up in a hot mess of command
modes, protocol states that breaks under stress. The ways in which serial protocols break are manifold. The simplest one
is that at some point a character is mangled, leading to both ends of the conversation ending up in misaligned protocol
states. With a fragile protocol, you might end up in a state that is hard to recover from. In extreme cases, this leads
-to code such as <a class="reference external" href="https://github.com/juhasch/pyBusPirateLite/blob/master/pyBusPirateLite/BBIO_base.py#L68">this gem</a> performing some sort of arcane ritual to get back to some known state, and all just because
+to code such as <a class="reference external" href="https://github.com/juhasch/pyBusPirateLite/blob/dece35f6e421d4f6a007d1db98d148e2f2126ebb/pyBusPirateLite/base.py#L113">this gem</a> performing some sort of arcane ritual to get back to some known state, and all just because
someone did not do their homework. Below we'll embark on a journey through the lands of protocol design, exploring the
facets of this deceptively simple problem.</p>
<div class="section" id="text-based-serial-protocols">
@@ -83,10 +83,10 @@ to know when you hit one of them.</p>
<div class="section" id="low-information-density">
<h4>Low information density</h4>
<p>Generally, you won't be able to stuff much more than four or five bit of information down a serial port using a
-human-readable protocol. In many cases you will get much less. If you have 10 commands that are only issued a couple
-times a second nobody cares that you spend maybe ten bytes per command on nice, verbose strings such as <tt class="docutils literal">SET LED</tt>. But
-if you're trying to squeeze a half-kilobyte framebuffer down the line you might start to notice the difference between
-hex and base-64 encoding, and a binary protocol might really be more up to the job.</p>
+single byte of a human-readable protocol. In many cases you will get much less. If you have 10 commands that are only
+issued a couple times a second nobody cares that you spend maybe ten bytes per command on nice, verbose strings such as
+<tt class="docutils literal">SET LED</tt>. But if you're trying to squeeze a half-kilobyte framebuffer down the line you might start to notice the
+difference between hex and base-64 encoding, and a binary protocol might really be more up to the job.</p>
</div>
<div class="section" id="complex-parsing-code">
<h4>Complex parsing code</h4>
@@ -97,7 +97,7 @@ an entirely different beast. On a small microcontroller, <a class="reference ext
microcontrollers, you just won't get a regex library even though it would make parsing textual commands <em>so much
simpler</em>. Lacking these resources, you might end up hand-knitting a lot of low-level C code to do something seemingly
simple such as parsing <tt class="docutils literal">set_channel (13, <span class="pre">1.1333)\n</span></tt>. These issues have to be taken into account in the protocol design
-from the beginning. If you don't really need matching parentheses, don't use them.</p>
+from the beginning. For example, you don't really need matching parentheses, don't use them.</p>
</div>
<div class="section" id="fragile-protocol-state">
<h4>Fragile protocol state</h4>
@@ -112,7 +112,7 @@ and <tt class="docutils literal">0x10 '\n'</tt> turns into <tt class="docutils l
<p>You might try to solve the problem of your protocol state machine tangling up with a timeout. &quot;If I don't get a valid
command for more than 200ms I go back to default state.&quot; But consider the above example. Say, your control computer
sends a <tt class="docutils literal">SET_DISPLAY</tt> command every 100ms. If in one of them the state machine tangles up, the parser hangs since the
-timeout is never hit, a new line of text arriving every 100ms.</p>
+timeout is never hit, because a new line of text is arriving every 100ms.</p>
</div>
<div class="section" id="framing-is-hard">
<h4>Framing is hard</h4>
@@ -126,9 +126,9 @@ data you've lost.</p>
<h3>Solutions</h3>
<div class="section" id="keep-the-state-machine-simple">
<h4>Keep the state machine simple</h4>
-<p>Always use a single line of text to represent a single command. Don't do protocol states or modes where you can toggle
-between different interpretations for a line. If you have to send human-readable text as part of a command (such as
-<tt class="docutils literal">SET_DISPLAY</tt>) escape it so it doesn't contain any newlines.</p>
+<p>In a text-based protocol, always use a single line of text to represent a single command. Don't do protocol states or
+modes where you can toggle between different interpretations for a line. If you have to send human-readable text as part
+of a command (such as <tt class="docutils literal">SET_DISPLAY</tt>), escape it so it doesn't contain any newlines.</p>
<p>This way, you keep your protocol state machine simple. If at any time your serial trips and flips a bit or looses a byte
your protocol will recover on the next newline character, returning to its base state.</p>
</div>
@@ -230,19 +230,19 @@ on the receiving side.</p>
<p>Here's your five-step guide to serial bliss:</p>
<ol class="arabic simple">
<li>Unless you have super-special requirements, always use the slowest you can get away with from 9600Bd, 115200Bd or
-1MBd. 8N1 framing if you're talking to anything but another microcontroller on the same board. These settings are
-the most common and cover any use case. You'll inevitably have to guess these at some point in the future.</li>
+1MBd. 8N1 framing if you're talking to anything but another microcontroller on the same board. Using common values
+like these makes it easier when you'll inevitably have to guess these at some point in the future ;)</li>
<li>If you're doing something simple and speed is not a particular concern, use a human-readable text-based protocol. Use
-one command/reply per line, begin each line with some sort of command word and format numbers in hexadecimal. You get
-bonus points if the device replies to unknown commands with a human-readable status message and prints a brief
-protocol overview on boot.</li>
+one command/reply per line, begin each line with some sort of command word and format numbers in hexadecimal. Bonus
+points for the device replying to unknown commands with a human-readable status message and printing a brief protocol
+overview on boot.</li>
<li>If you're doing something even slightly nontrivial or need moderate throughput (&gt;1k commands per second or &gt;20 byte of
-data per command) use a COBS-based protocol. If you don't have a better idea, go for an <tt class="docutils literal">[target <span class="pre">MAC][command</span>
-<span class="pre">ID][command</span> arguments]</tt> packet format for multidrop busses. For single-drop you may decide to drop the MAC address.</li>
+data per command) use a COBS-based protocol. A good starting point is a <tt class="docutils literal">[target <span class="pre">MAC][command</span> <span class="pre">ID][command</span>
+arguments]</tt> packet format for multidrop busses. For single-drop you may decide to drop the MAC address.</li>
<li>Always include some sort of &quot;status&quot; command that prints life stats such as VCC, temperature, serial framing errors
-and uptime. You'll need some sort of ping command anyway and that one might as well do something useful.</li>
+and uptime. You'll need some sort of ping command anyway and that command might as well do something useful.</li>
<li>If at all possible, keep your protocol context-free across packets/lines. That is, a certain command should always be
-self-contained, and no command should change the meaning of the next packet or line that is sent. This is really
+self-contained, and no command should change the meaning of the next packet/line/command that is sent. This is really
important to allow for self-synchronization. If you really need to break up something into multiple commands, say you
want to set a large framebuffer in pieces, do it in a <a class="reference external" href="https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning">idempotent</a> way: Instead of sending something like <tt class="docutils literal">FRAMEBUFFER
<span class="pre">INCOMING:\n[byte</span> <span class="pre">0-16]\n[byte</span> <span class="pre">17-32]\n[...]\nEND</span> OF FRAME</tt> rather send <tt class="docutils literal">FRAMEBUFFER DATA FOR OFFSET 0: [byte