|
Comms CCF
This is a simple communication layer based on COBS, CBOR and FNV-1A
|
Doxygen docs at https://kovirobi.github.io/comms-ccf/.
This is a simple communication layer based on COBS, CBOR and FNV-1A.
Can be used to implement other channels on top, e.g. RPC, logging, tracing etc.
None of the ideas are new here, but it's still useful to have them together like this. And it's more focused on embedded than some other libraries. (At the same time, the code size could perhaps be smaller, I used C++ templates and they can expand to a fair amount of code. Especially when using no optimisation [-O0] instead of debug optimisation [-Og].)
Currently there is a working demo in FreeRTOS-Demo/ which should be possible to port to other platforms. There is some Python support in python/comms_ccf/ which can connect to the demo and issue RPC commands.
This is proof-of-concept because not all of the layers below are filled in properly:
There is a demo in the FreeRTOS-Demo/ folder, with detailed information about where the flash/memory use comes from.
A summary of the resurce use is copied below from the FreeRTOS-Demo/README.md file.
| Summary | .text | .bss | .data |
|---|---|---|---|
| No Comms-CCF | 11.54KiB | 3.54KiB | 356B |
| Add Comms-CCF, no RPC handlers | 13.26KiB (+1.72Ki) | 5.62KiB (+2.09Ki) | 356B (no change) |
| Add version RPC call | 14.16KiB (+920) | 5.64KiB (+24) | 356B (no change) |
| Add add call | 14.89KiB (+752) | 5.67KiB (+28) | 356B (no change) |
| Add sub call, similar to add | 14.95KiB (+56) | 5.70KiB (+28) | 356B (no change) |
| Add greet call | 15.25KiB (+312) | 5.72KiB (+24) | 356B (no change) |
| Add readm_mem and write_mem calls | 16.64KiB (+1.39Ki) | 5.78KiB (+56) | 356B (no change) |
| Add test log task | 20.78KiB (+4.14Ki) | 6.86KiB (+1.09Ki) | 356B (no change) |
| Inline vtable | 20.75KiB (-28) | 6.89KiB (+24) | 356B (no change) |
| Deferred formatting | 19.85KiB (-920) | 6.86KiB (-24) | 356B (no change) |
With much gratitude to Robert McGregor and Jamie Wood, who have shown me that writing something like this can be easy and fun. And showing the utility in writing something like this compared to continuing on with the standard "write a CLI over UART" method.
Their method was different, using a Python program to parse and compile a DSL for describing the RPC calls. This allowed that to be used easily with C, and also support more features and fewer workarounds to the template limitations. I used C++ templates to continue my learning of them.
The expected use of this is between a microcontroller and a host-machine, communicating over e.g. UART or similar (byte) serial channel. The aim is to make a few things easy:
This impacts design in a couple of ways:
CPU is much faster than the transport, so we can do checksumming as we copy the received byte
Note: This breaks down with SEGGER_RTT, specifically I found that using JLink + SEGGER_RTT doesn't do flow control on the host TX side (where it could/should), so for now I recommend setting the down buffer size to be the maximum packet size (e.g. -DBUFFER_SIZE_DOWN=256 from the default 16). On the other hand, the up buffer size has a default of 1024B, you could reduce that to 256B so still saving overall space.
Packets are received from an interrupt handler, that needs to queue them for deferred processing.
Specifically it might need to enqueue a packet while some packets are being processed. Not seeing the newly added packet is fine (the interrupt will notify it to try again), but it cannot use a mutex because that would leave the interrupt being blocked on the thread (probably a crash but at least a priority inversion);
The format on the wire is the following, little endian network byte order for the checksum because that is what I'm used to writing.
| ID: u8 | data: u8[]/CBOR | checksum: u32 |
|---|---|---|
The ID is a channel ID/tag, the data is a variable length of bytes (the length is given by the framing layer), and the checksum is the FNV-1A checksum, over both the ID and data in that order.
This isn't the traditional CRC-32 (Ethernet variant) because FNV-1A uses fewer bytes for the same throughput (no precomputed table), is very easy to implement and is pretty good. There are better algorithms for absolute throughput and randomness (say SipHash, Murmur2), but these have usually been written for throughput on a desktop with vector extensions, and their code does end up being a little bigger.
Given the use case here of communicating between two trusted parties, the choice of FNV-1A seems good. It does operate on a byte byte basis, which does slow it down but also means we don't impose length/padding requirements on the messages.
Further, the data might be encoded as CBOR (depending on the ID). For example if the ID is for a serial channel, unstructured bytes are fine. But for logs, structured data can be useful (and not just for the metatdata). For RPC, structured data is necessary.
I chose CBOR because it is easy to encode/decode into a compact format, supports many types (more than I end up using).