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
|