Identifying the frame length for an unknown serial protocol

Let’s suppose you’re reverse-engineering a serial protocol. You already know the correct configuration for the serial port (baudrate etc.) and you assume the protocol is built from frames of equal length.

For simplicity we will also assume that the device sends the data without the necessity to request data from it first. If this is not the case, you can use a different and much simpler approach (just send the request character and se

The next step is to determine the the frame length of the protocol. This post not only details two variants of one of the algorithms you can use in order to do this, but also provides a ready-to-use Python script you can use for your own protocols.

Approach 1: Autocorrelation with argmax

We will use a simple mathematical approach in order to find out what the most likely frame length will be. This is based on the assumption that frames will have a high degree of self similarity, i.e. many of the bytes in a single frame will match the corresponding bytes in the next frame.

It is not required that all bytes are the same in every frame, but if you have entirely different bytes in every frame, the approach will likely not deduce the correct frame length.

This approach is based on autocorrelation. Although it sounds complicated, it means nothing more Compare a sequence by a delayed/shifted version of itself.

This means we will perform the following steps:

  • Read a set of characters from the serial device
  • Correlate the set of characters with shifted versions of itself
  • The framelength is the shift where the maximum similarity occurs (using np.argmax)

As similiarity score, we’ll use 1 if the bytes equal or 0 else. For specific protocols, it might be a more viable approach to introduce individual bit matching, but this will also introduce noise into the process. 

For most simple protocols I’ve seen, this approach works very well for both ASCII and binary.

Plotting the correlation looks like this:

Approach 2: Multiple-shift aware Autocorrelation

This modified algorithms works well for protocols where there is insignificant similarity between any two frames or if there is a lot of noise. For such protocol, the maximum score approach does not yield the correct result.

However, we can use the property of constant-framelength protocols that we get high matching scores by shifting a frame by an integer multiple of the (unknown) framelength. Instead of just taking one maximum peak, we multiply all the scores for the integer-multiples of any length.

While this approach doesn’t sound too complicated compared to the first one, it has more caveats and pitfalls, e.g. that there are no integer multiples within the data array for the second half of the correlation result array, and the second quarter is not very significant as there are not many multiples to multiply.

The script (see below) works around these issues by only computing the first quarter of the possible result space. Use the -n parameter in order to increase the number of characters read by the script.

After computing the multiple-shift aware correlation, we can use argmax just like in the first approach to find the best correlation. Sometimes this identifies a multiple of the frame length due to noise. You can look at the plot (use -p) and manually determine the frame length in order to find the correct frame length.

As you can see from the result, the “noise” (in between the frame-length shift matches, caused by random matches between characters) is mostly gone.

In many real usecases, this algorithm will produce a more distinct signal in the plot, but the automatically calculated frame size will not be correct as several effects tend to increase the lobe height for multiples of the frame height. Therefore, it is adviseable to have a look at the plot (-p in the script) before taking the result as granted.

Automating the algorithm

Here’s the Python3 script to this article which works well without modification, but for some protocols you might need to adjust it to fit your needs:

#!/usr/bin/env python3
"""
ProtocolFrameLength.py

Determine the frame length of an unknown serial protocol
with constant-length frames containing similar bytes in every frame.

For an explanation, see
Identifying the frame length for an unknown serial protocol
Example usage: $ python3 ProtocolFrameLength.py -b 115200 /dev/ttyACM0 """ import serial import numpy as np import math from functools import reduce import operator __author__ = "Uli Köhler" __license__ = "Apache License v2.0" __version__ = "1.0" __email__ = "ukoehler@techoverflow.net" def match_score(c1, c2): """ Correlation score for two characters, c1 and c2. Uses simple binary score """ if c1 is None or c2 is None: # Fill chars return 0 return 1 if c1 == c2 else 0 def string_match_score(s1, s2): assert len(s1) == len(s2) ln = len(s1) return sum(match_score(s1[i], s2[i]) for i in range(ln)) def compute_correlation_scores(chars, nomit=-1): # Omit the last nomit characters as single-char matches would be over-valued if nomit == -1: # Auto-value nomit = len(chars) // 10 corr = np.zeros(len(chars) - nomit) # Note: autocorrelation for zero shift is always 1, left out intentionally! for i in range(1, corr.size): # build prefix by Nones prefix = [None] * i s2 = prefix + list(chars[:-i]) # Normalize by max score attainable due to Nones and the sequence length (there are i Nones) corr[i] = string_match_score(chars, s2) / (len(chars) - i) return corr def print_most_likely_frame_length(correlations): # Find the largest correlation coefficient. This model does not require a threshold idx = np.argmax(correlations) print("Frame length is likely {} bytes".format(idx)) def plot_correlations(correlations): from matplotlib import pyplot as plt plt.style.use("ggplot") plt.title("Correlation scores for a protocol with 16-byte frames") plt.gcf().set_size_inches(20,10) plt.plot(correlations) plt.title("Correlation scores") plt.ylabel("Normalized correlation score") plt.xlabel("Shift") plt.show() def multishift_adjust(correlations): """ Multi-shift aware algorithm """ corr_multishift = np.zeros(correlations.size // 4) for i in range(1, corr_multishift.size): # Iterate multiples of i (including i itself) corr_multishift[i] = reduce(operator.mul, (correlations[j] for j in range(i, correlations.size, i)), 1) return corr_multishift if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument('port', help='The serial port to use') parser.add_argument('-b', '--baudrate', type=int, default=9600, help='The baudrate to use') parser.add_argument('-n', '--num-bytes', type=int, default=200, help='The number of characters to read') parser.add_argument('-m', '--multishift', action="store_true", help='Use the multi-shift aware autocorrelation algorithm') parser.add_argument('-p', '--plot', action="store_true", help='Plot the resulting correlation matrix') args = parser.parse_args() ser = serial.Serial(args.port, args.baudrate) ser.reset_input_buffer() chars = ser.read(args.num_bytes) corr = compute_correlation_scores(chars) if args.multishift: corr = multishift_adjust(corr) print_most_likely_frame_length(corr) if args.plot: plot_correlations(corr)

 

Usage example:

python3 ProtocolFrameLength.py -b 115200 /dev/ttyACM0

You can use -m to use the multiple-shift aware approach.

Use -p to plot the results as shown above. If one of the approaches does not work, it is advisable to plot the result in order to see if there is something visible in the plot which has not been detected by the

As the results depend on the actual data read, it is advisable to perform multiple runs and see if the results vary.

Accurate calculation of PT100/PT1000 temperature from resistance

TL;DR for impatient readers

PT100/PT1000 temperatures calculation suffers from accuracy issues for large sub-zero temperatures. UliEngineering implements a polynomial-fit based algorithm to provide 58.6 \mu{\degree}C peak-error over the full defined temperature range from -200 {\degree}C to +850 °C.

Use this code snippet (replace pt1000_ by pt100- to use PT100 coefficients) to compute an accurate temperature (in degrees celsius) e.g. for a resistane of 829.91 Ω of a PT1000 sensor.

from UliEngineering.Physics.RTD import pt1000_temperature
# The following calls are equivalent and print -43.2316359463
print(pt1000_temperature("829.91 Ω"))
print(pt1000_temperature(829.91))

You install the library (compatible to Python 3.2+) using

$ pip3 install git+https://github.com/ulikoehler/UliEngineering.git

Read more

Accurate short & long delays on microcontrollers using ChibiOS

How system ticks work

In order to understand how delays work, we’ll first need to have a look at system ticks. Although ChibiOS 3.x supports a feature called tickless mode, we’ll stick to a simple periodic tick model for simplicity reasons.

A system tick is simply a timer that interrupts the microcontroller periodically and performs some kernel management tasks. For example, with a 1 kHz system tick (systick) frequency, the program flow is interrupted every millisecond. When being interrupted, one of the things the kernel does is to check if a thread that is currently asleep needs to be woken up. In other words, if your thread has some code like this:

// [...]
chThdSleepMilliseconds(5);
// [...]

and the kernel has a 1 kHz systick frequency, the kernel will set your thread to sleep, wait for 5 system ticks (i.e. 5 ms) and then wake up the

Read more

Using burnout current sources for Wheatstone bridge detection

Many recent high-performance ADCs like the AD7190 include a builtin so-called burnout current source that can allegedly be used to detect an open circuit in the sensor. However, most vendors don’t provide an easy explanation on how this can be done.

In this blogpost I will attempt to explain how those current sources can be useful for practical applications. For this example, we will assume the ADC has one idealized differential channel and is connected to a simple wheatstone bridge strain gauge:

Read more

Normalizing electronics engineering value notations using Python

In electronics engineering there is a wide variety of notations for values that need to be recognized by intuitive user interfaces. Examples include:

  • 1fA
  • 0.1A
  • 0.00001
  • 1e-6
  • 4,5nA
  • 4,500.123 A
  • 4A5
  • 4k0 A

The wide variety of options, including thousands separators, comma-as-decimal-separator and suffix-as-decimal-separator, optional whitespace and scientific notations makes it difficult to normalize values without using specialized libraries. Read more