DIY project: Raspberry Pi Serial Programming

 

 

Introduction

In this Raspberry Pi serial programming DIY project, I attempted in writing Raspbian c++ for single Master device to communicate with multiple Slave devices using RS485-ModBus message format. No ModBus libraries are used in this DIY program.

Master device and multiple slave devices are wired together in daisy chain connection / multidrop bus. Each time Master device will request data from a single Slave device by sending out RS485-ModBus query message with stated targeted Slave ID. The targeted Slave device responses by sending ModBus response message. Other Slave devices will ignore the query message when Slave ID mismatched. Afterward, Master device sends query message to another Slave device, and so on.

In this DIY project, I only using the basic commands I get from WaveShare sample code and built-out the rest.

The RS-485 library I am using in this DIY project is Raspberry default libraries ‘<wiringSerial.h>’ & ‘<wiringPi.h>’. Remember to update ‘<wiringSerial.h>’ & ‘<wiringPi.h>’ libraries to avoid communication errors.

http://wiringpi.com/wiringpi-updated-to-2-52-for-the-raspberry-pi-4b/

In the Master device, declare a separate thread to run DateTime process to produce timestamp for the ModBus message.

 

Message Format

In my understandings, ModBus is a message format for communicating devices to follow. So that all communicating devices know the meanings of the messages it received.

ModBus communication can be either of two transmission modes: ASCII or RTU.

In ModBus ASCII mode, each eight-bit byte in a message is sent as two ASCII characters.

In ModBus RTU mode, each eight-bit byte in a message sent comprises of two four-bit hexadecimal characters. Therefore, it sends half the total data bytes size compare to ModBus ASCII mode. Standard RS485 in ‘<wiringSerial.h>’ has 1 stop bit. When a byte in the ModBus RTU message value is 0, RS485 will send out value 0 (ASCII null or ‘\0’) with 1 stop bit.

Each time Master device will request data from a single Slave device by sending RS485-ModBus query message. The Master query message frame contains targeted Slave device ID, Function code, query data starting address and number of registers and CRC16 (16 bits) Error Check. Optional stop byte ‘char(26)’.

The targeted Slave device will response by sending ModBus response message. ModBus response message frame contains the targeted Slave device ID, Function code, Byte Count, Data and CRC (16 bits) Error Check. Optional stop byte ‘char(26)’.

Another query & response messages format method are message without stop byte ‘char(26)’. Query message is always 8 bytes fixed length. Therefore, we can stop reading-in new characters when the received query message reach 8 bytes length. Response message carry total data ‘Byte Count’ information, hence we know when to stop reading-in new characters when receiving response message. Stop byte is needed if the recipient devices don’t know the message length.

Each character on the serial device are 8 bits length. ‘unsigned char’ is 8 data bits; ‘char’ (or ‘signed char’) most significant bit, bit-8 represents +/-.

Slave devices listen for its query message. When received a new character, set software timeout counter to 100 milliseconds.

CRC16 Checksum Error Check is checking whether a device is receiving a complete and non-corrupted message. Both sending and receiving devices will generate CRC16 using the raw message. Transmitting device will attach CRC16 (4 ASCII characters / 16 bits or unsigned short) at the end of the message for the recipient device to verify the correctness of the received message.

In ModBus ASCII mode, I filters all the received characters excepted hexadecimal data characters ‘1~9’, char(48) to char(57); and ‘A~F’, char(65) to char(70). Found in ASCII table.

 

Delays

There are some time delays in Raspberry RS485_CAN_HAT hardware programming.

I observe Master device take 20~30 milliseconds to switch from Send Data mode to Read Data mode, another ~20 milliseconds to receive the data.

In this ‘RS485_CAN_HAT by WaveShare’ hardware, RS485 port is 2 wires Data+ and Data-. Hence, in software we need to switch between RS485 Send Data mode and Read Data mode.

‘<wiringSerial.h>’ Serial Library ‘int serialOpen (char *device, int baud)’ opens and initializes the serial device, set the default read timeout to 10 seconds.

‘int serialGetchar (int fd)’ returns the next character available on the serial device. It will block for up to 10 seconds if no data is available (when it will return -1).

If no Slave devices are responding by sending out any response message, Master device will have to wait 10 seconds due to ‘int serialGetchar (int fd)’ command before sending the next query message. We can change the default 10 seconds ‘<wiringSerial.h>’ read timeout to 100 milliseconds or any, by changing ‘<wiringSerial.h> Advanced Serial Port Control’ options to cut down the waiting time. Every time the Master device received a new character, read timeout counter will reset back to 100 milliseconds.

#include <termios.h>

 

//Advanced Serial Port Control

//Change serialGetchar(int fd) block call for upto 1000ms (existing 10 seconds) if no data is available (when it will return -1)

struct termios options;

tcgetattr(fd, &options);   //Read current options

options.c_cc [VTIME] = 10; //1000ms

tcsetattr(fd, TCSANOW, &options); //Set new options

 

‘<wiringSerial.h>’ Serial Library webpage: www.wiringpi.com/reference/serial-library/

 

Raspbian c++ Source Code

ModBus ASCII mode

Query message & response message with stop byte ‘char(26)’

Version 0:

·       Master device:    485ModBusASCII_Master_StopByte.cpp     Makefile

·       Slave device:       485ModBusASCII_Slave_StopByte.cpp         Makefile

 

Version 1 improvement: If the message received is not intended for the recipient device, ignores it by remain in Read mode & re-initialize ‘RecMessage’ variables. Instead of exiting the Read mode then return back to Read mode again in Version 0.

·       Master device:    485ModBusASCII_Master_StopByte_v1.cpp     Makefile

·       Slave device:       485ModBusASCII_Slave_StopByte_v1.cpp         Makefile

 

Query message & response message with no stop byte attached. Recipient devices know when to stop reading in new character from message’s ‘Byte Count’.

·       Master device:    485ModBusASCII_Master.cpp     Makefile

·       Slave device:       485ModBusASCII_Slave.cpp         Makefile

Using throw error & try-catch error commands to return error messages

·       Master device:    485ModBusASCII_Master_ThrowError.cpp     Makefile

·       Slave device:       485ModBusASCII_Slave_ThrowError.cpp         Makefile

 

ModBus RTU mode

·       Master device:    485ModBusRTU_Master.cpp     Makefile

·       Slave device:       485ModBusRTU_Slave.cpp         Makefile

 

Hardware

·       2* Raspberry Pi 4

·       2* RS485_CAN_HAT by WaveShare

In this DIY attempt, I only using 3 Raspberry Pi and RS485_CAN_HAT. One Raspberry Pi I set as Master device; one Raspberry Pi I defined as Slave device #6; one Raspberry Pi I defined as Slave device #2;

 

References

·       WaveShare RS485_CAN_HAT

o   RS-485 sample code: https://www.waveshare.com/wiki/File:RS485_CAN_HAT_Code.7z

o   RS485_CAN_HAT manual: https://www.waveshare.com/wiki/File:RS485-CAN-HAT-user-manuakl-en.pdf

·       RS485 communication based on wiringPi library: http://wiringpi.com/reference/serial-library/

·       ModBus introduction

·       Calculating ModBus CRC16 in Excel: http://www.simplymodbus.ca/crc.xls  /  crc.xls

CRC16 checksum calculation in c++

 

Visual c++: CRC16 checksum calculation in ModBus ASCII mode

#include <iostream>

 

using namespace std;

 

// Compute the ModBus ASCII mode CRC16

bool get_ModBusASCII_CRC16(const char* ModBusASCIIMessage, char* CRC16_result)

{

    if (strlen(ModBusASCIIMessage) % 2 != 0)

        return false;

 

    unsigned short crc = 0xFFFF;

    int len = strlen(ModBusASCIIMessage) / 2;

    char oneByte[3];

 

    for (int pos = 0; pos < len; pos++) {

        strncpy_s(oneByte, ModBusASCIIMessage + 2 * pos, 2);

        oneByte[2] = '\0';

 

        int nHexNumber;

        sscanf_s(oneByte, "%x", &nHexNumber);

 

        crc ^= nHexNumber;          // XOR byte into least sig. byte of crc

 

        for (int i = 8; i != 0; i--) {    // Loop over each bit

            if ((crc & 0x0001) != 0) {      // If the LSB is set

                crc >>= 1;                    // Shift right and XOR 0xA001

                crc ^= 0xA001;

            }

            else                            // Else LSB is not set

                crc >>= 1;                    // Just shift right

        }

    }

   

    //swap low byte and high byte position

    crc = ((crc & 0xFF00) >> 8) | ((crc & 0x00FF) << 8);

 

    sprintf_s(CRC16_result, 5, "%X", crc);

 

    //For ModBus ASCII mode, if CRC16_result are not 4 digits, add '0' in front to make it to 4 digits

    if (strlen(CRC16_result) == 3)

    {

        CRC16_result[4] = '\0';

        CRC16_result[3] = CRC16_result[2];

        CRC16_result[2] = CRC16_result[1];

        CRC16_result[1] = CRC16_result[0];

        CRC16_result[0] = '0';

    }

    else if (strlen(CRC16_result) == 2)

    {

        CRC16_result[4] = '\0';

        CRC16_result[3] = CRC16_result[1];

        CRC16_result[2] = CRC16_result[0];

        CRC16_result[1] = '0';

        CRC16_result[0] = '0';

    }

    else if (strlen(CRC16_result) == 1)

    {

        CRC16_result[4] = '\0';

        CRC16_result[3] = CRC16_result[0];

        CRC16_result[2] = '0';

        CRC16_result[1] = '0';

        CRC16_result[0] = '0';

    }

    else

        ;

 

    return true;

}

 

int main()

{

    char ModBusASCII_message[] = "1103006B0003";         //CRC = '0x7687'

    //char ModBusASCII_message[] = "0204027F58";       //CRC = '0xDCFA'

    //char ModBusASCII_message[] = "F70302640008";       //CRC = '0x10FD'

 

    char CRC16[5];

 

    if(get_ModBusASCII_CRC16(ModBusASCII_message, CRC16))

        cout << "\"" << ModBusASCII_message << "\" CRC = 0x" << CRC16 << endl;

 

    return 0;

}

 

 


 

Optional Topic: Rectify single-bit error from data received

Modern day processors are generally much faster than the data transmission speed. When data received by a recipient device contains a single error bit, the recipient device can rectify the single-bit error itself without need to request sender to re-send the data using the below program.

In some scenarios, data transmission takes long time due to extensively long traveling distance or slow transmission speed. Scenarios such as transmitting data from one place to another place in opposite earth, transmit signal from Earth to Mars and etc.

I come out this data error rectification idea & program myself. This program can only able to rectify single-bit error and will not able to rectify data errors that are more than one bit. There might be better ways to do this.

Single-bit error can be ON bit accidentally become OFF bit; or OFF bit accidentally become ON bit.

In my program, in order to perform the error rectification, the transmission data need to contain CRC / LRC, parity bit for each byte (either even or odd parity, bit-8).

Program steps:

1.     Compute every received data byte parity and compare them with the received parity bits to pinpoint which received data byte contains the single-bit error.

2.     If found errors are more than 1 bit, stop the rectification function.

3.     If data only single bit error. perform try-and-error method to check if OFF bit accidentally become ON bit. Turn off single-bit from the targeted byte, compute the data’s CRC / LRC and compare it with the received CRC / LRC. If computed CRC / LRC doesn’t match the received CRC / LRC, turn off the next bit from the targeted byte and so on. Until the computed CRC / LRC matching the received CRC / LRC.

4.     If computed CRC / LRC does not match the received CRC / LRC after step ‘3’, check if ON bit accidentally become OFF bit. Turn on single-bit from the targeted byte, compute the data’s CRC / LRC and compare it with the received CRC / LRC. If computed CRC / LRC doesn’t match the received CRC / LRC, turn on the next bit from the targeted byte and so on. Until the computed CRC / LRC matching the received CRC / LRC.

5.     If after step ‘4’, the computed data’s CRC / LRC does not match the received CRC / LRC. Possible causes:

a.     Received CRC / LRC is wrong

b.     LRC / CRC data size is short, sometimes unable to detect data error. Received data contains errors even though computed LRC / CRC matched the received LRC / CRC.

c.      A byte in the received data contains 2 or multiples of two error bits. Hence, this error byte doesn’t not reflex on the parity bit.

d.     If the error byte reflexes on the parity bit, it might contain more than 2 error bits.

 

This method I think might not be practical. When transmitting data size is big, likely the errors are more than one bit.

 

Visual c++ Console App

Version 0: RectifySingleBitError.cpp

Version 1: RectifySingleBitError_v1.cpp

 

 

Optional Topic: Rectify multiple single-bit errors from data received

The below program rectifies multiple single-bit errors from data received with some constraints:

·       In data received, each bytes should only contains maximum 1 error bit.

·       Odd / Even parity should be included together with received data to identify which bytes are having single bit errors

·       The received data should include Longitudinal Redundancy Check (LRC, 1 byte) or Cyclic Redundancy Check (CRC). Small LRC / CRC data size or small received-data size sometimes resulted in received wrong data but computed LRC / CRC is matched, wrongly indicating received error-free data. For example, data ‘0x1133007F0003’ and data ‘0x1103006B0003’ both data are different but CRC16 value are the same.

·       Total single-bit error bytes count should less than 10~20% of total data bytes count?

This program uses tries-and-errors approach. Trying to rectify the error bits in the received data, by flipping every single-bit from each error bytes. Then compute the new data’s LRC / CRC and see if it matches the received LRC / CRC value. If LRC / CRC not matched, flip the next single-bit from each error bytes until LRC / CRC values are matched; or until all the possible combinations are tried.

Again, this program does not always able to rectify errors successfully. Possible causes same as above.

This program sometimes produces wrong data after tries-and-errors bits-flip but with matched LRC / CRC value. Reasons may be due to small received data size, small LRC / CRC size or insufficient LRC / CRC calculation. Big received data size will cause longer rectification runtimes.

Moreover, apparently the more error bytes the less likely this program able to rectify these single-bit errors. Total single-bit error bytes count should less than 10~20% of total data bytes?

 

 

 

Visual c++ Console App

Version 0: RectifyMultipleSingleBitErrors.cpp

 

 

 

 


 

 

 

 

 

 

 

Edit date: 29 June 2020