15. Networking and Communications

The topic of this week is network systems in a broad sense. In our group assignment, we showed how to send messages through the internet using UDP, allowing us to send bytes to a microcontroller.

For my own week assignment, I wanted to explore communication between microcontrollers only, while mixing several types of processor architecture.

AFP: an ASCII only protocol with auto-baud

While talking with my fabacademy instructor Nicolas, he mentioned an interesting idea he had while working at the Royal meteorological institute of Belgium. Observatories are often equipped with rather complex weather stations, and keeping track of all the sensors can quickly become a mess when each of them uses a different protocol for communication. Therefore, an open source master/slave architecture would be of great use in this context. It could be implemented by cheap µC acting as interfaces between each sensor and the master.

The main idea is to create a new protocol inspired from two existing ones: SDI-12 and LIN. SDI-12 is an intuitive ASCII-only protocol with up to 62 slave addresses (0-9a-zA-Z), but supports only low baudrates and requires synchronized clocks. LIN, on the other hand, is asynchronous and implements auto-baud adjustment using a synchronization preamble. It is less flexible than SDI-12, as the header size is fixed and the slave response must be kept short.

We propose the following protocol, named AFP for now (Asynchronous FabLab Protocol) and still in search for a better name:

  • ASCII-only: all characters except the break must be printable, and easily understood. They are sent on the wire with standard UART specifications (1 start + 1 stop bit).
  • 1-wire only: can be either implemented on the hardware serial port of the device, or a single arbitrary interrupt-able pin. The impedance is high as long as the device is listening.
  • master header: every request starts with the following header:
    • break sequence: 13 bits at LOW voltage indicating a new request.
    • sync field: a U character (01010101) indicating the baudrate of the master.
    • ID bytes: the ID of the target device in the range [0-9a-zA-Z] or * for a broadcast signal.
    • Message bytes: an optional (short) sequence of bytes indicating the type of request.
    • header terminator: a single ! character as in SDI-12.
  • slave response: sent immediately following the master header, unless the message was a broadcast. The format is .json, starting with { and ending with }\n. the content in-between should be valid .json, and it is suggested to use short field names and values (i.e. {“T”:20.0} could be the response for a temperature sensor). The \n is a terminator character that indicates to all other devices that the master is now allowed to send new requests. The master has a timeout logic, in which a long silence will be interpreted as a missing device.

A complete transaction could for instance be:

<BREAK>U0TH!{"T":20.0,"H":70.5}\n

Which could be a polling for the current temperature (T) and humidity (H).

In the most general case, the network could look like this:

in which the IO pins must toggle between high and low impedance when appropriate (listening and talking, respectively). Pull-up resistors should be used in case there is nothing on the line.

For this week’s assignment, we will focus on a simpler scenario, where we make use of the already present hardware serial pins:

Note that it is possible to merge RX and TX pins using resistors and the appropriate impedance logic, but we neglect this for now.

The auto-baud detection is very similar to LIN, but I want to make it more general. Looking at the documentation from the ATtiny1614, I found the following:

This tells me that LIN was not designed for a full auto-baud detection, but rather calibrating the current baudrate to exactly match the master’s rate. It is not possible to set a completely different rate, because the sync field’s value is checked to be a valid ‘U’ (0x55) in the current baudrate, not the new one. I checked this behavior with a SAMD11C14 and ATtiny1614, sending the following master header at 19200 baud:

With both of these µC, the LIN auto-baud failed when starting from a 9600 baud, but it succeeded when the rate was close enough to 19200, for instance 18000. A flag indicating a corrupted sync field was present in both cases. However, the ATtiny1614 supports a more flexible auto-baud: the GENAUTO mode, activated by setting CTRLB.RXMODE to 0x2. This disables the check for the sync field to be a valid ‘U’, and I succeeded in synchronizing the baudrate with various values.

To test the protocol, I made a simple master/slave configuration with an Arduino Mega as the master, and two slaves: an ATtiny1614 and a SAMD11C14, both designed during my Electronics design week.

I want to propose a useful Arduino library implemeting the AFP protocol described earlier, therefore I picked those devices to make sure the same code would work on all three. I devised the following API, currently based on polling on the slave side:

AFPMaster:
  AFPMaster(unsigned long baud, int txPin);
  void begin();
  int sendRequest(char id, const char* msg);
  void sendBroadcast(const char* msg);
  void setBaud(unsigned long baud);

AFPSlave:
  AFPSlave(char id, int rxPin);
  void begin();
  int pollRequest();
  void sendReply();
  void listenReply();
  int addField(const char* field_name);
  void enableField(int field_id);
  void disableField(int field_id);
  void disableAllFields();
  void updateField(int field_id, const char* txt, bool add_quotes);

You will find the current version of this library at the end of this page. Note that this code is an early version, and does not support all features mentioned earlier. In particular, it will be necessary to detect the type of microcontroller for critical operations such as the auto-baud calibration and the pin impedance change.

Using the following code, I was able to let the Arduino Mega communicate with both slave boards using input characters from the PC:

#include "AFPMaster.h"

#define PIN_TX 16
#define CMD_INPUT_MAX 64

char cmd_input[CMD_INPUT_MAX];

int i = 0;
int button_state = 0;

AFPMaster afp(AFP_BAUD_DEFAULT, PIN_TX);

void setup() {
  afp.begin();
  Serial.begin(9600);
}

void update_input() {
  int result;
  char c = Serial.read();
  cmd_input[i] = c;
  if (c == '!') {
    cmd_input[i] = '\0';
    Serial.print("SEND:");
    Serial.print(cmd_input);
    Serial.println("!");
    // the input should be of the form [ID][MSG]!
    if (cmd_input[0] == '*') {
      afp.sendBroadcast(cmd_input+1);
    } else {
      result = afp.sendRequest(cmd_input[0], cmd_input+1);
      if (result == AFP_OK) {
        Serial.print("RECV:");
        Serial.print(afp.reply);
      } else {
        Serial.println("no reply");
      }
    }
    i=0;
  } else {
    i++;
    // protect against overflow: drop everyting
    if (i == CMD_INPUT_MAX)
      i = 0;
  }
}


void loop() {
  if (Serial.available()) {
    update_input();
  }
  delay(1);
}

The following shows dummy responses from the slaves, pretending to be weather sensors for temperature and humidity:

I’m looking forward to improving the code and build a larger network. One interesting feature is to let the slave µC sleep until the break character is detected through an interrupt on the RX pin. This will also require some architecture-specific code, which I avoided for now to make it as abstract and Arduino-compatible as possible.

Downloads