363 lines
11 KiB
ReStructuredText
363 lines
11 KiB
ReStructuredText
.. _module-pw_hdlc:
|
|
|
|
=======
|
|
pw_hdlc
|
|
=======
|
|
`High-Level Data Link Control (HDLC)
|
|
<https://en.wikipedia.org/wiki/High-Level_Data_Link_Control>`_ is a data link
|
|
layer protocol intended for serial communication between devices. HDLC is
|
|
standardized as `ISO/IEC 13239:2002 <https://www.iso.org/standard/37010.html>`_.
|
|
|
|
The ``pw_hdlc`` module provides a simple, robust frame-oriented transport that
|
|
uses a subset of the HDLC protocol. ``pw_hdlc`` supports sending between
|
|
embedded devices or the host. It can be used with :ref:`module-pw_rpc` to enable
|
|
remote procedure calls (RPCs) on embedded on devices.
|
|
|
|
**Why use the pw_hdlc module?**
|
|
|
|
* Enables the transmission of RPCs and other data between devices over serial.
|
|
* Detects corruption and data loss.
|
|
* Light-weight, simple, and easy to use.
|
|
* Supports streaming to transport without buffering, since the length is not
|
|
encoded.
|
|
|
|
.. admonition:: Try it out!
|
|
|
|
For an example of how to use HDLC with :ref:`module-pw_rpc`, see the
|
|
:ref:`module-pw_hdlc-rpc-example`.
|
|
|
|
.. toctree::
|
|
:maxdepth: 1
|
|
:hidden:
|
|
|
|
rpc_example/docs
|
|
|
|
--------------------
|
|
Protocol Description
|
|
--------------------
|
|
|
|
Frames
|
|
======
|
|
The HDLC implementation in ``pw_hdlc`` supports only HDLC unnumbered
|
|
information frames. These frames are encoded as follows:
|
|
|
|
.. code-block:: text
|
|
|
|
_________________________________________
|
|
| | | | | | |...
|
|
| | | | | | |... [More frames]
|
|
|_|_|_|__________________________|____|_|...
|
|
F A C Payload FCS F
|
|
|
|
F = flag byte (0x7e, the ~ character)
|
|
A = address field
|
|
C = control field
|
|
FCS = frame check sequence (CRC-32)
|
|
|
|
|
|
Encoding and sending data
|
|
=========================
|
|
This module first writes an initial frame delimiter byte (0x7E) to indicate the
|
|
beginning of the frame. Before sending any of the payload data through serial,
|
|
the special bytes are escaped:
|
|
|
|
+-------------------------+-----------------------+
|
|
| Unescaped Special Bytes | Escaped Special Bytes |
|
|
+=========================+=======================+
|
|
| 7E | 7D 5E |
|
|
+-------------------------+-----------------------+
|
|
| 7D | 7D 5D |
|
|
+-------------------------+-----------------------+
|
|
|
|
The bytes of the payload are escaped and written in a single pass. The
|
|
frame check sequence is calculated, escaped, and written after. After this, a
|
|
final frame delimiter byte (0x7E) is written to mark the end of the frame.
|
|
|
|
Decoding received bytes
|
|
=======================
|
|
Frames may be received in multiple parts, so we need to store the received data
|
|
in a buffer until the ending frame delimiter (0x7E) is read. When the
|
|
``pw_hdlc`` decoder receives data, it unescapes it and adds it to a buffer.
|
|
When the frame is complete, it calculates and verifies the frame check sequence
|
|
and does the following:
|
|
|
|
* If correctly verified, the decoder returns the decoded frame.
|
|
* If the checksum verification fails, the frame is discarded and an error is
|
|
reported.
|
|
|
|
---------
|
|
API Usage
|
|
---------
|
|
There are two primary functions of the ``pw_hdlc`` module:
|
|
|
|
* **Encoding** data by constructing a frame with the escaped payload bytes and
|
|
frame check sequence.
|
|
* **Decoding** data by unescaping the received bytes, verifying the frame
|
|
check sequence, and returning successfully decoded frames.
|
|
|
|
Encoder
|
|
=======
|
|
The Encoder API provides a single function that encodes data as an HDLC
|
|
unnumbered information frame.
|
|
|
|
C++
|
|
---
|
|
.. cpp:namespace:: pw
|
|
|
|
.. cpp:function:: Status hdlc::WriteUIFrame(uint64_t address, ConstByteSpan data, stream::Writer& writer)
|
|
|
|
Writes a span of data to a :ref:`pw::stream::Writer <module-pw_stream>` and
|
|
returns the status. This implementation uses the :ref:`module-pw_checksum`
|
|
module to compute the CRC-32 frame check sequence.
|
|
|
|
.. code-block:: cpp
|
|
|
|
#include "pw_hdlc/encoder.h"
|
|
#include "pw_hdlc/sys_io_stream.h"
|
|
|
|
int main() {
|
|
pw::stream::SysIoWriter serial_writer;
|
|
Status status = WriteUIFrame(123 /* address */,
|
|
data,
|
|
serial_writer);
|
|
if (!status.ok()) {
|
|
PW_LOG_INFO("Writing frame failed! %s", status.str());
|
|
}
|
|
}
|
|
|
|
Python
|
|
------
|
|
.. automodule:: pw_hdlc.encode
|
|
:members:
|
|
|
|
.. code-block:: python
|
|
|
|
import serial
|
|
from pw_hdlc import encode
|
|
|
|
ser = serial.Serial()
|
|
address = 123
|
|
ser.write(encode.ui_frame(address, b'your data here!'))
|
|
|
|
Typescript
|
|
----------
|
|
|
|
Encoder
|
|
=======
|
|
The Encoder class provides a way to build complete, escaped HDLC UI frames.
|
|
|
|
.. js:method:: Encoder.uiFrame(address, data)
|
|
|
|
:param number address: frame address.
|
|
:param Uint8Array data: frame data.
|
|
:returns: Uint8Array containing a complete HDLC frame.
|
|
|
|
Decoder
|
|
=======
|
|
The decoder class unescapes received bytes and adds them to a buffer. Complete,
|
|
valid HDLC frames are yielded as they are received.
|
|
|
|
.. js:method:: Decoder.process(bytes)
|
|
|
|
:param Uint8Array bytes: bytes received from the medium.
|
|
:yields: Frame complete frames.
|
|
|
|
C++
|
|
---
|
|
.. cpp:class:: pw::hdlc::Decoder
|
|
|
|
.. cpp:function:: pw::Result<Frame> Process(std::byte b)
|
|
|
|
Parses a single byte of an HDLC stream. Returns a Result with the complete
|
|
frame if the byte completes a frame. The status is the following:
|
|
|
|
- OK - A frame was successfully decoded. The Result contains the Frame,
|
|
which is invalidated by the next Process call.
|
|
- UNAVAILABLE - No frame is available.
|
|
- RESOURCE_EXHAUSTED - A frame completed, but it was too large to fit in
|
|
the decoder's buffer.
|
|
- DATA_LOSS - A frame completed, but it was invalid. The frame was
|
|
incomplete or the frame check sequence verification failed.
|
|
|
|
.. cpp:function:: void Process(pw::ConstByteSpan data, F&& callback, Args&&... args)
|
|
|
|
Processes a span of data and calls the provided callback with each frame or
|
|
error.
|
|
|
|
This example demonstrates reading individual bytes from ``pw::sys_io`` and
|
|
decoding HDLC frames:
|
|
|
|
.. code-block:: cpp
|
|
|
|
#include "pw_hdlc/decoder.h"
|
|
#include "pw_sys_io/sys_io.h"
|
|
|
|
int main() {
|
|
std::byte data;
|
|
while (true) {
|
|
if (!pw::sys_io::ReadByte(&data).ok()) {
|
|
// Log serial reading error
|
|
}
|
|
Result<Frame> decoded_frame = decoder.Process(data);
|
|
|
|
if (decoded_frame.ok()) {
|
|
// Handle the decoded frame
|
|
}
|
|
}
|
|
}
|
|
|
|
Python
|
|
------
|
|
.. autoclass:: pw_hdlc.decode.FrameDecoder
|
|
:members:
|
|
|
|
Below is an example using the decoder class to decode data read from serial:
|
|
|
|
.. code-block:: python
|
|
|
|
import serial
|
|
from pw_hdlc import decode
|
|
|
|
ser = serial.Serial()
|
|
decoder = decode.FrameDecoder()
|
|
|
|
while True:
|
|
for frame in decoder.process_valid_frames(ser.read()):
|
|
# Handle the decoded frame
|
|
|
|
Typescript
|
|
----------
|
|
Decodes one or more HDLC frames from a stream of data.
|
|
|
|
.. js:method:: process(data)
|
|
|
|
:param Uint8Array data: bytes to be decoded.
|
|
:yields: HDLC frames, including corrupt frames.
|
|
The Frame.ok() method whether the frame is valid.
|
|
|
|
.. js:method:: processValidFrames(data)
|
|
|
|
:param Uint8Array data: bytes to be decoded.
|
|
:yields: Valid HDLC frames, logging any errors.
|
|
|
|
Allocating buffers
|
|
------------------
|
|
Since HDLC's encoding overhead changes with payload size and what data is being
|
|
encoded, this module provides helper functions that are useful for determining
|
|
the size of buffers by providing worst-case sizes of frames given a certain
|
|
payload size and vice-versa.
|
|
|
|
.. code-block:: cpp
|
|
|
|
#include "pw_assert/check.h"
|
|
#include "pw_bytes/span.h"
|
|
#include "pw_hdlc/encoder"
|
|
#include "pw_hdlc/encoded_size.h"
|
|
#include "pw_status/status.h"
|
|
|
|
// The max on-the-wire size in bytes of a single HDLC frame after encoding.
|
|
constexpr size_t kMtu = 512;
|
|
constexpr size_t kRpcEncodeBufferSize = pw::hdlc::MaxSafePayloadSize(kMtu);
|
|
std::array<std::byte, kRpcEncodeBufferSize> rpc_encode_buffer;
|
|
|
|
// Any data encoded to this buffer is guaranteed to fit in the MTU after
|
|
// HDLC encoding.
|
|
pw::ConstByteSpan GetRpcEncodeBuffer() {
|
|
return rpc_encode_buffer;
|
|
}
|
|
|
|
The HDLC ``Decoder`` has its own helper for allocating a buffer since it doesn't
|
|
need the entire escaped frame in-memory to decode, and therefore has slightly
|
|
lower overhead.
|
|
|
|
.. code-block:: cpp
|
|
|
|
#include "pw_hdlc/decoder.h"
|
|
|
|
// The max on-the-wire size in bytes of a single HDLC frame after encoding.
|
|
constexpr size_t kMtu = 512;
|
|
|
|
// Create a decoder given the MTU constraint.
|
|
constexpr size_t kDecoderBufferSize =
|
|
pw::hdlc::Decoder::RequiredBufferSizeForFrameSize(kMtu);
|
|
pw::hdlc::DecoderBuffer<kDecoderBufferSize> decoder;
|
|
|
|
-------------------
|
|
Additional features
|
|
-------------------
|
|
|
|
Interleaving unstructured data with HDLC
|
|
========================================
|
|
It is possible to decode HDLC frames from a stream using different protocols or
|
|
unstructured data. This is not recommended, but may be necessary when
|
|
introducing HDLC to an existing system.
|
|
|
|
The ``FrameAndNonFrameDecoder`` Python class supports working with raw data and
|
|
HDLC frames in the same stream.
|
|
|
|
.. autoclass:: pw_hdlc.decode.FrameAndNonFrameDecoder
|
|
:members:
|
|
|
|
RpcChannelOutput
|
|
================
|
|
The ``RpcChannelOutput`` implements pw_rpc's ``pw::rpc::ChannelOutput``
|
|
interface, simplifying the process of creating an RPC channel over HDLC. A
|
|
``pw::stream::Writer`` must be provided as the underlying transport
|
|
implementation.
|
|
|
|
If your HDLC routing path has a Maximum Transmission Unit (MTU) limitation,
|
|
using the ``FixedMtuChannelOutput`` is strongly recommended to verify that the
|
|
currently configured max RPC payload size (dictated by pw_rpc's static encode
|
|
buffer) will always fit safely within the limits of the fixed HDLC MTU *after*
|
|
HDLC encoding.
|
|
|
|
HdlcRpcClient
|
|
=============
|
|
.. autoclass:: pw_hdlc.rpc.HdlcRpcClient
|
|
:members:
|
|
|
|
.. autoclass:: pw_hdlc.rpc.HdlcRpcLocalServerAndClient
|
|
:members:
|
|
|
|
Example pw::rpc::system_server backend
|
|
======================================
|
|
This module includes an example implementation of ``pw_rpc``'s ``system_server``
|
|
facade. This implementation sends HDLC encoded RPC packets via ``pw_sys_io``,
|
|
and has blocking sends/reads, so it is hardly performance-oriented and
|
|
unsuitable for performance-sensitive applications. This mostly servers as a
|
|
simplistic example for quickly bringing up RPC over HDLC on bare-metal targets.
|
|
|
|
-----------
|
|
Size report
|
|
-----------
|
|
The HDLC module currently optimizes for robustness and flexibility instead of
|
|
binary size or performance.
|
|
|
|
There are two size reports: the first shows the cost of everything needed to
|
|
use HDLC, including the dependencies on common modules like CRC32 from
|
|
pw_checksum and variable-length integer handling from pw_varint. The other is
|
|
the cost if your application is already linking those functions. pw_varint is
|
|
commonly used since it's necessary for protocol buffer handling, so is often
|
|
already present.
|
|
|
|
.. include:: size_report
|
|
|
|
-------
|
|
Roadmap
|
|
-------
|
|
- **Expanded protocol support** - ``pw_hdlc`` currently only supports
|
|
unnumbered information frames. Support for different frame types and
|
|
extended control fields may be added in the future.
|
|
|
|
-------------
|
|
Compatibility
|
|
-------------
|
|
C++17
|
|
|
|
------
|
|
Zephyr
|
|
------
|
|
To enable ``pw_hdlc.pw_rpc`` for Zephyr add ``CONFIG_PIGWEED_HDLC_RPC=y`` to
|
|
the project's configuration.
|
|
|