I’m really glad to see that other fellow embedded software developers reach similar conclusions about software tracing (a.k.a. logging).
At Quantum Leaps (state-machine.com), we’ve been doing software tracing of embedded code for over 17 years. We call it QP/Spy:
https://www.state-machine.com/qtools/qpspy.html
you can also see how our software tracing works in a YouTube video:
It’s interesting to see and compare other approaches to the problem. Also, because we’ve been doing this for so long, perhaps it will be useful to share some lessons we’ve learned along the way.
But first, you’re absolutely right that clogging the embedded code with printf’s is extremely inefficient. It puts expensive ASCII formatting in time-critical paths through the code – exactly where you can afford it the least. Also, in the formatting process you significantly increase the amount of data. For example, a single byte sent as a hex string (e.g., “0xXX”) takes 4 bytes.
So, the obvious solution is to send the tracing data out of the embedded target in binary and do the formatting on the powerful host computer, where the overhead doesn’t matter.
Now, of course the devil is in the detail of HOW you do this.
Your approach of eliminating the format strings and replacing them with binary IDs (apparently some form of hash function) seems novel and interesting. Although it seems to require a pre-processing step for the source code to generate the IDs. I can imagine a whole set of problems with managing the additional build step and with versioning the various harvested format strings on the host. Of course, it also requires access to the source code when tracing an already deployed target systems.
In QP/Spy we handle this differently. We don’t have format strings. Instead, we have application-specific trace records (packets), see:
https://www.state-machine.com/qtools/qs.html#qs_app
Here is an example of code inside the target:
QS_BEGIN_ID(MY_STATUS, local_filter) /* app-specific record begin */
QS_U8(1, n); /* app-specific uint8_t element */
QS_STR("fine"); /* app-specific string element */
QS_END() /* app-specific record end */
The QS_xxx() statements are macros that generate a tracing packet in the buffer when software tracing is enabled. They compile to nothing when software tracing is disabled.
In our approach each data element within a packet is generated with one additional byte of format, which describes both the type (u8 vs string vs u32, etc.) and the number of characters to format the quantity (for numerical values). For example, in the trace record above, the format overhead will be 2 bytes (for the two data elements).
The app-specific trace record wound produce the following (ASCII-formatted) output on the host:
3136059740 MY_STATUS 3 fine
All these binary logging schemes have the problem of preserving the symbolic information about the tracing data. In your case it seems that you use the format-IDs and the mapping to the actual strings harvested from the target code.
In our case, we use “dictionaries”, which are generated by the target:
https://www.state-machine.com/qtools/qs.html#qs_dict
Our “dictionaries” are mappings between the binary addresses within the embedded target (which are unique) and symbolic names of the objects or functions. This information is analogous to the symbolic information added by the compiler when building for debugging.
The “dictionaries” are produced by the target only during the initialization, so there is no overhead during normal execution. The advantage is that the “dictionaries” are always in synch with the specific target code.
Now, regarding the binary transport protocol, we’ve selected HDLC-type encoding:
https://www.state-machine.com/qtools/qpspy_proto.html
Similar to your COBS encoding, HDLC also has a “flag” byte and transparency (a.k.a. escaping), such that the “flag” byte does not occur in the byte stream. However, the HDLC “flag” byte is 0x7E, in contrast to COBS “flag” being 0x00. From our experience, 0x00 occurs in natural data much more frequently than 0x7E, so much more “escaping” is needed with COBS.
Our QP/Spy protocol has been specifically designed to simplify the data management overhead in the target, yet to allow detection of any data dropouts due to the trace buffer overruns. The protocol has not only provisions for detecting gaps in the data and other errors but allows for instantaneous re-synchronization after any buffering or transmission error to minimize loss of useful data.
We also wanted to easily implement the “last is best” policy, so our trace ring-buffer (typically of 1K bytes) wraps around naturally when the data production outpaces data sending. This is fine, because our protocol detects any gaps in the data (sequence byte).
Regarding other features, we don’t use “tracing levels”. Instead, we have flexible data filtering based on the data-ID (“global filter”) and on the specific object (“local filter”) that produced the data record.
Of course, I cannot describe all the features of QP/Spy and directions in which we’ve taken the software tracing (e.g., testing). As I said, we’ve been doing this and refining it for a very long time…