In the first article on low speed serial communications for 8-but microcontrollers, we looked primarily at the design principles and message structure. In this second, long overdue, article, we take a closer look at handling a message.
I am designing a simple, robust protocol for universal serial communications between low-level devices and sensors with very limited resources, whether it is over an RS485 bus, Wi-Fi, PLC or otherwise.
Time to start designing the logic flow of a protocol library to implement this. One of the key elements of such an implementation is the mechanism of a ‘state machine’: a way of keeping track of the type of message bytes we are expecting, based on previously received data. With the help of a 'Finite State Machine' that becomes a relatively easy task.
State of mind
OK, I’ll just throw it at you...
The flowchart presents the different stages of the so called ‘Finite State Machine’ (FSM for short), starting with the initial IDLE state. Whenever a character is received, we check the state we are in and what kind of data we are expecting. We react accordingly (like storing the received data) and update the state as needed. To keep things simple,we don’t show the (error) handling that takes place on the lower level, including the merging of nibbles into a byte. Also we will probably calculate the CRC as we process the bytes of message data. And a third aspect that is not presented in this simplified FSM flowchart is the handling of the address. After all we should only process messages addressed to us and broadcast messages.
The state machine flow
Let's have a look what’s going on in the FSM chart, state by state:
- Initially we are in the IDLE state. All we care about is getting an STX byte to signify the start of the message. Anything else is just ignored.
- Once we have received an STX byte, we enter the STX state. In that state we either receive more STX characters – as explained in this blog post – or the first (high) address byte. In that case we save the address byte and set the state to ADDR state.
- When receiving the address, we must get a second byte to form the complete 16-bit address. So when we receive a byte while in the ADDR state, it must be the low byte of the address word. We assemble the address and set the state to ORG_HI as we are next expecting the first byte of the originator address.
- In the ORG_HI state we will store the byte received as the high byte of the 16-bit originator address and set the state to ORG_LO to wait for the second address byte.
- The byte we receive while in the ORG_LO state is added to the previously received high byte to form a 16-bit address. We will then enter the SEQ state to wait for the sequence number.
- The sequence number is just one byte, so when received it is stored in the message buffer and we will enter the LEN state to wait for the parameter length byte.
- The parameter length byte is expected when we are in the LEN state. It is stored as such in the message buffer and used as a counter for the countdown of the following parameter bytes. But first we enter the PARAM state. If the LEN byte received is valid, otherwise we will set an error code and return to the IDLE state. The rest of the transmission will then be ignored until we receive another STX.
- While in the PARAM state, we keep receiving parameter bytes until the counter reaches 0. Then we will enter the CRC state.
- The received CRC is checked against the calculated CRC (not shown in the FSM chart) and in case of a mismatch an error code is set and we enter the IDLE state. Ignoring the ETX byte(s) that will follow. Otherwise we enter the ETX state.
- In the ETX state we simply check if we receive an ETX byte (if more ETX bytes are sent, they are simply ignored). The assembled message buffer and status code is returned to the caller and we enter the IDLE state again, waiting for another message.
As mentioned, one thing that is missing in this FSM chart is the assembly of bytes from the two separate nibbles that are sent (except for STX and ETX). We will deal with that in a simple sub-FSM: whenever we receive a byte we check if it is the first or the second nibble and merge them, while checking for STX/ETX and verifying if the received values are valid (high nibble should be reverse of low nibble) at the same time. STX and ETX bytes are just passed on without assembly of nibbles into bytes.
Merging the nibbles
The decision was made to split the message bytes into nibbles and complement them with the reverse bits in the high nibble before sending them over the wire. The first solution that came to mind was to implement the nibble merging stuff in the interrupt routine that is triggered when a raw byte is received. That would however involve another mechanism to inform the receive message routine that a message byte is ready. This means either a polling mechanism (undesired) or a hardware specific solution to trigger another interrupt. An alternative way to deal with the nibble merging is within the main flow of the FSM that assembles the message received in a buffer. I am still working out what is the best way, but for now let's assume it is best handled within the main FSM flow. But as we start coding, we will see.
In the next part of this series we will do some coding to implement the core of the FSM and see how it turns out in terms of CPU and memory usage. BTW, the ‘standard’ way of implementing an FSM is by using a ‘state table’ - an array of values to check and set the state. In the case of an Atmel 8-bit MCU that would consume too much precious RAM, so we will use a 'state register'. More on that in the next blog post on this topic.