Skip to content

Commit 2342b26

Browse files
committed
Improved compatibility
External libraries have been replaced to improve compatibility. Sketch is now compatible with Arduino Nano, Uno and Mega. Ethernet chips W5100, W5200 and W5500.
1 parent a587ba4 commit 2342b26

File tree

13 files changed

+372
-176
lines changed

13 files changed

+372
-176
lines changed

README.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ Screenshots of the web interface:
4343
## How can I build it myself?
4444
Get the hardware. Cheap clones from China are sufficient:
4545

46-
* Arduino (Nano, Uno, possibly other)
47-
* W5500-based Ethernet shield (for Nano, I recommend W5500 Ethernet Shield from RobotDyn)
46+
* Arduino Nano, Uno or Mega (and possibly other)
47+
* W5100, W5200 or W5500 based Ethernet shield (for Nano, I recommend W5500 Ethernet Shield from RobotDyn)
4848
* MAX485 module
4949

50-
Connect the hardware:
50+
Connect the hardware (on Nano and Uno, the sketch uses HW Serial, on Mega you have to configure Serial in ADVANCED SETTINGS in the sketch):
5151

5252
* Arduino <-> MAX485
5353

@@ -57,7 +57,7 @@ Connect the hardware:
5757

5858
* Pin 6 <-> DE,RE
5959

60-
Download this repository (all *.ino files) and open arduino-modbus-rtu-tcp-gateway.ino in Arduino IDE. Download all required libraries (some of them are available in "library manager", other have to be manually downloaded from github). If you want, you can check the default factory settings (can be later changed via web interface) and advanced settings (can only be changed in sketch). Compile and upload your program to Arduino. Connect your Arduino to ethernet, connect your Modbus RTU slaves to MAX485 module. Use your web browser to access the web interface on default IP http://192.168.1.254 Enjoy :-)
60+
Download this repository (all *.ino files) and open arduino-modbus-rtu-tcp-gateway.ino in Arduino IDE. Download all required libraries (both are available in "library manager"). If you want, you can check the default factory settings (can be later changed via web interface) and advanced settings (can only be changed in sketch). Compile and upload your program to Arduino. Connect your Arduino to ethernet, connect your Modbus RTU slaves to MAX485 module. Use your web browser to access the web interface on default IP http://192.168.1.254 Enjoy :-)
6161

6262
## Where can I learn more about Modbus protocols?
6363

@@ -74,15 +74,32 @@ The key to success is:
7474

7575
* use StreamLib https://github.com/jandrassy/StreamLib
7676
* use F macros for your HTML code
77-
* for W5500 ethernet modules, use Ethernet3 https://github.com/sstaub/Ethernet3
77+
* use for() loop for repetitive code
7878
* use POST method (rather than GET) for your webforms, see this tutorial https://werner.rothschopf.net/202003_arduino_webserver_post_en.htm
7979

8080
Big thanks to the authors of these libraries and tutorials!
8181

82-
## Background
82+
## Limitations
8383

84-
This project started as a simple "playground" where I learned things. However, it evolved into more serious project: Modbus gateway in full compliance with Modbus standards. Later on, web interface was added as a demonstration of what a simple Arduino Nano is capable of.
84+
#### Portability
8585

86-
Not everything could fit into the limited flash memory of Arduino Nano / Uno. The DHCP client within a ethernet library consumes too much memory. The code for automatic IP address is in the sketch but disabled by default. If you want to use auto IP functionality, you have to use something bigger (such as Arduino Mega) and uncomment #define ENABLE_DHCP. After that, new "Auto IP" setting will appear in the IP settings web interface.
86+
The code was tested on Arduino Nano, Uno and Mega, ethernet chips W5100 and W5500. It may work on other platforms, but:
87+
88+
* The pseudorandom generator (for random MAC) is seeded through watch dog timer interrupt - this will work only on Arduino (credits to https://sites.google.com/site/astudyofentropy/project-definition/timer-jitter-entropy-sources/entropy-library/arduino-random-seed)
89+
* The restart function will also work only on Arduino.
90+
91+
#### Ethernet socket
92+
93+
The default Ethernet.h library determines MAX_SOCK_NUM by microcontroller RAM (not by Ethernet chip type). So if you use W5500 (which has 8 sockets available) on Arduino Nano, only 4 sockets will be used. If you want to force the library to use 8 sockets, edit https://github.com/arduino-libraries/Ethernet/blob/master/src/Ethernet.h#L36
94+
95+
#### Memory
96+
97+
Not everything could fit into the limited flash memory of Arduino Nano / Uno. If you have a microcontroller with more memory (such as Mega), you can enable extra features in the main sketch by uncommenting:
98+
99+
* #define ENABLE_DHCP will allow you to set "Auto IP" via DHCP in the IP settings web interface. Leased IP is automatically renewed.
87100

88101
<img src="/pics/modbus6.png" alt="06" style="zoom:100%;" />
102+
103+
* #define ENABLE_EXTRA_DIAG shows extra info on "Current status" page: per socket diagnostics, run time counter.
104+
105+
<img src="/pics/modbus1x.png" alt="01x" style="zoom:100%;" />

arduino-modbus-rtu-tcp-gateway/01-interfaces.ino

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,29 @@
1313
resetFunc
1414
- well... resets Arduino
1515
16+
maintainDhcp()
17+
- maintain DHCP lease
18+
1619
maintainUptime
1720
- maintains up time in case of millis() overflow
1821
1922
maintainCounters
2023
- synchronizes roll-over of data counters to zero
2124
25+
CreateTrulyRandomSeed
26+
- seed pseudorandom generator using watch dog timer interrupt (works only on AVR)
27+
- see https://sites.google.com/site/astudyofentropy/project-definition/timer-jitter-entropy-sources/entropy-library/arduino-random-seed
28+
29+
generateMac()
30+
- generate random MAC using pseudo random generator (faster and than build-in random())
31+
2232
+ preprocessor code for identifying microcontroller board
2333
2434
***************************************************************** */
2535

36+
2637
void startSerial() {
27-
Serial.begin(localConfig.baud, localConfig.serialConfig);
38+
mySerial.begin(localConfig.baud, localConfig.serialConfig);
2839
// Calculate Modbus RTU character timeout and frame delay
2940
byte bits = // number of bits per character (11 in default Modbus RTU settings)
3041
1 + // start bit
@@ -40,31 +51,56 @@ void startSerial() {
4051
charTimeout = 750;
4152
frameDelay = 1750;
4253
}
43-
pinMode(SerialTxControl, OUTPUT);
44-
digitalWrite(SerialTxControl, RS485Receive); // Init Transceiver
54+
pinMode(rs485ControlPin, OUTPUT);
55+
digitalWrite(rs485ControlPin, RS485_RECEIVE); // Init Transceiver
4556
}
4657

4758
void startEthernet() {
48-
Ethernet.setRstPin(ethResetPin);
59+
if (ethResetPin != 0) {
60+
pinMode(ethResetPin, OUTPUT);
61+
digitalWrite(ethResetPin, LOW);
62+
delay(25);
63+
digitalWrite(ethResetPin, HIGH);
64+
delay(500);
65+
}
4966
#ifdef ENABLE_DHCP
50-
if (!localConfig.enableDhcp || !Ethernet.begin(localConfig.mac)) {
51-
Ethernet.begin(localConfig.mac, localConfig.ip, localConfig.subnet, localConfig.gateway, localConfig.dns);
67+
if (localConfig.enableDhcp) {
68+
dhcpSuccess = Ethernet.begin(localConfig.mac);
69+
}
70+
if (!localConfig.enableDhcp || dhcpSuccess == false) {
71+
Ethernet.begin(localConfig.mac, localConfig.ip, localConfig.dns, localConfig.gateway, localConfig.subnet);
5272
}
5373
#else /* ENABLE_DHCP */
54-
Ethernet.begin(localConfig.mac, localConfig.ip, localConfig.subnet, localConfig.gateway, localConfig.dns);
55-
localConfig.enableDhcp = false; // Ensure Dhcp is disabled in config
74+
Ethernet.begin(localConfig.mac, localConfig.ip, localConfig.dns, localConfig.gateway, localConfig.subnet);
75+
localConfig.enableDhcp = false; // Make sure Dhcp is disabled in config
5676
#endif /* ENABLE_DHCP */
5777
modbusServer = EthernetServer(localConfig.tcpPort);
5878
webServer = EthernetServer(localConfig.webPort);
5979
Udp.begin(localConfig.udpPort);
6080
modbusServer.begin();
6181
webServer.begin();
82+
// Ethernet library determines MAX_SOCK_NUM by Microcontroller RAM (not by Ethernet chip type).
83+
// Therefore, for Arduino Mega + W5100, MAX_SOCK_NUM is wrongly set to 8
84+
if (Ethernet.hardwareStatus() == EthernetW5100) {
85+
maxSockNum = 4;
86+
}
6287
dbg(F("[arduino] Server available at http://"));
6388
dbgln(Ethernet.localIP());
6489
}
6590

6691
void(* resetFunc) (void) = 0; //declare reset function at address 0
6792

93+
void maintainDhcp()
94+
{
95+
if (localConfig.enableDhcp && dhcpSuccess == true) { // only call maintain if initial DHCP request by startEthernet was successfull
96+
uint8_t maintainResult = Ethernet.maintain();
97+
if (maintainResult == 1 || maintainResult == 3) { // renew failed or rebind failed
98+
dhcpSuccess = false;
99+
startEthernet(); // another DHCP request, fallback to static IP
100+
}
101+
}
102+
}
103+
68104
void maintainUptime()
69105
{
70106
unsigned long milliseconds = millis();
@@ -82,15 +118,65 @@ void maintainUptime()
82118

83119
void maintainCounters()
84120
{
85-
// synchronize roll-over of data counters to zero, at 0xFFFFF000
86-
if (serialTxCount > 0xFFFFF000 || serialRxCount > 0xFFFFF000 || ethTxCount > 0xFFFFF000 || ethRxCount > 0xFFFFF000) {
121+
// synchronize roll-over of data counters to zero, at 0xFFFFFF00 or 0xFF00 respectively
122+
#ifdef ENABLE_EXTRA_DIAG
123+
const unsigned long rollover = 0xFFFFFF00;
124+
#else
125+
const unsigned int rollover = 0xFF00;
126+
#endif /* ENABLE_EXTRA_DIAG */
127+
if (serialTxCount > rollover || serialRxCount > rollover || ethTxCount > rollover || ethRxCount > rollover) {
87128
serialRxCount = 0;
88129
serialTxCount = 0;
89130
ethRxCount = 0;
90131
ethTxCount = 0;
91132
}
92133
}
93134

135+
136+
void generateMac()
137+
{
138+
// Marsaglia algorithm from https://github.com/RobTillaart/randomHelpers
139+
seed1 = 36969L * (seed1 & 65535L) + (seed1 >> 16);
140+
seed2 = 18000L * (seed2 & 65535L) + (seed2 >> 16);
141+
uint32_t randomBuffer = (seed1 << 16) + seed2; /* 32-bit random */
142+
143+
for (byte i = 0; i < 3; i++) {
144+
localConfig.mac[i + 3] = randomBuffer & 0xFF;
145+
randomBuffer >>= 8;
146+
}
147+
}
148+
149+
void CreateTrulyRandomSeed()
150+
{
151+
seed1 = 0;
152+
nrot = 32; // Must be at least 4, but more increased the uniformity of the produced
153+
// seeds entropy.
154+
155+
// The following five lines of code turn on the watch dog timer interrupt to create
156+
// the seed value
157+
cli();
158+
MCUSR = 0;
159+
_WD_CONTROL_REG |= (1 << _WD_CHANGE_BIT) | (1 << WDE);
160+
_WD_CONTROL_REG = (1 << WDIE);
161+
sei();
162+
163+
while (nrot > 0); // wait here until seed is created
164+
165+
// The following five lines turn off the watch dog timer interrupt
166+
cli();
167+
MCUSR = 0;
168+
_WD_CONTROL_REG |= (1 << _WD_CHANGE_BIT) | (0 << WDE);
169+
_WD_CONTROL_REG = (0 << WDIE);
170+
sei();
171+
}
172+
173+
ISR(WDT_vect)
174+
{
175+
nrot--;
176+
seed1 = seed1 << 8;
177+
seed1 = seed1 ^ TCNT1L;
178+
}
179+
94180
// Board definitions
95181
#if defined(TEENSYDUINO)
96182

arduino-modbus-rtu-tcp-gateway/02-modbus-tcp.ino

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@
2121
2222
deleteRequest
2323
- deletes requests from queue
24-
25-
***************************************************************** */
2624
25+
getSlaveResponding, setSlaveResponding
26+
- read from and write to bool array
27+
28+
***************************************************************** */
2729

28-
BitBool<maxSlaves> slavesResponding;
30+
// bool array for storing Modbus RTU status (responging or not responding). Array index corresponds to slave address.
31+
uint8_t slavesResponding[(maxSlaves + 1 + 7) / 8];
32+
uint8_t masks[8] = {1, 2, 4, 8, 16, 32, 64, 128};
2933

3034
typedef struct {
3135
byte tid[2]; // MBAP Transaction ID
@@ -43,7 +47,7 @@ CircularBuffer<byte, reqQueueCount> queueRetries; // queue of retry
4347

4448
void recvUdp()
4549
{
46-
int packetSize = Udp.parsePacket();
50+
unsigned int packetSize = Udp.parsePacket();
4751
if (packetSize)
4852
{
4953
ethRxCount += packetSize;
@@ -53,18 +57,18 @@ void recvUdp()
5357
Udp.read(udpInBuffer, sizeof(udpInBuffer));
5458
Udp.flush();
5559

56-
int errorCode = checkRequest(udpInBuffer, packetSize);
60+
byte errorCode = checkRequest(udpInBuffer, packetSize);
5761
byte pduStart; // first byte of Protocol Data Unit (i.e. Function code)
5862
if (localConfig.enableRtuOverTcp) pduStart = 1; // In Modbus RTU, Function code is second byte (after address)
5963
else pduStart = 7; // In Modbus TCP/UDP, Function code is 8th byte (after address)
6064
if (errorCode == 0) {
6165
// Store in request queue: 2 bytes MBAP Transaction ID (ignored in Modbus RTU over TCP); MBAP Unit ID (address); PDUlen (func + data);remote IP; remote port; TCP client Number (socket) - 0xFF for UDP
62-
queueHeaders.push(header {{udpInBuffer[0], udpInBuffer[1]}, udpInBuffer[pduStart - 1], packetSize - pduStart, Udp.remoteIP(), Udp.remotePort(), UDP_REQUEST});
66+
queueHeaders.push(header {{udpInBuffer[0], udpInBuffer[1]}, udpInBuffer[pduStart - 1], (byte)(packetSize - pduStart), Udp.remoteIP(), Udp.remotePort(), UDP_REQUEST});
6367
queueRetries.push(0);
64-
for (byte i = 0; i < packetSize - pduStart; i++) {
68+
for (byte i = 0; i < (byte)(packetSize - pduStart); i++) {
6569
queuePDUs.push(udpInBuffer[i + pduStart]);
6670
}
67-
} else if (errorCode > 0) {
71+
} else if (errorCode != 0xFF) {
6872
// send back message with error code
6973
Udp.beginPacket(Udp.remoteIP(), Udp.remotePort());
7074
if (!localConfig.enableRtuOverTcp) {
@@ -94,25 +98,25 @@ void recvTcp()
9498
{
9599
EthernetClient client = modbusServer.available();
96100
if (client) {
97-
int packetSize = client.available();
101+
unsigned int packetSize = client.available();
98102
ethRxCount += packetSize;
99103
byte tcpInBuffer[modbusSize + 4]; // Modbus TCP frame is 4 bytes longer than Modbus RTU frame
100104
// Modbus TCP/UDP frame: [0][1] transaction ID, [2][3] protocol ID, [4][5] length and [6] unit ID (address).....
101105
// Modbus RTU frame: [0] address.....
102106
client.read(tcpInBuffer, sizeof(tcpInBuffer));
103107
client.flush();
104-
int errorCode = checkRequest(tcpInBuffer, packetSize);
108+
byte errorCode = checkRequest(tcpInBuffer, packetSize);
105109
byte pduStart; // first byte of Protocol Data Unit (i.e. Function code)
106110
if (localConfig.enableRtuOverTcp) pduStart = 1; // In Modbus RTU, Function code is second byte (after address)
107111
else pduStart = 7; // In Modbus TCP/UDP, Function code is 8th byte (after address)
108112
if (errorCode == 0) {
109113
// Store in request queue: 2 bytes MBAP Transaction ID (ignored in Modbus RTU over TCP); MBAP Unit ID (address); PDUlen (func + data);remote IP; remote port; TCP client Number (socket) - 0xFF for UDP
110-
queueHeaders.push(header {{tcpInBuffer[0], tcpInBuffer[1]}, tcpInBuffer[pduStart - 1], packetSize - pduStart, {}, 0, client.getSocketNumber()});
114+
queueHeaders.push(header {{tcpInBuffer[0], tcpInBuffer[1]}, tcpInBuffer[pduStart - 1], (byte)(packetSize - pduStart), {}, 0, client.getSocketNumber()});
111115
queueRetries.push(0);
112116
for (byte i = 0; i < packetSize - pduStart; i++) {
113117
queuePDUs.push(tcpInBuffer[i + pduStart]);
114118
}
115-
} else if (errorCode > 0) {
119+
} else if (errorCode != 0xFF) {
116120
// send back message with error code
117121
if (!localConfig.enableRtuOverTcp) {
118122
client.write(tcpInBuffer, 5);
@@ -155,14 +159,14 @@ void processRequests()
155159
if (!queueHeaders.isEmpty()) {
156160
boolean queueHasRespondingSlaves; // true if queue holds at least one request to responding slaves
157161
for (byte i = 0; i < queueHeaders.size(); i++) {
158-
if (slavesResponding[queueHeaders[i].uid] == true) {
162+
if (getSlaveResponding(queueHeaders[i].uid) == true) {
159163
queueHasRespondingSlaves = true;
160164
break;
161165
} else {
162166
queueHasRespondingSlaves = false;
163167
}
164168
}
165-
while (queueHasRespondingSlaves == true && slavesResponding[queueHeaders.first().uid] == false) {
169+
while (queueHasRespondingSlaves == true && getSlaveResponding(queueHeaders.first().uid) == false) {
166170
// move requests to non responding slaves to the tail of the queue
167171
for (byte i = 0; i < queueHeaders.first().PDUlen; i++) {
168172
queuePDUs.push(queuePDUs.shift());
@@ -175,22 +179,21 @@ void processRequests()
175179
}
176180
}
177181

178-
int checkRequest(byte buffer[], int bufferSize) {
182+
byte checkRequest(byte buffer[], unsigned int bufferSize) {
179183
byte address;
180184
if (localConfig.enableRtuOverTcp) address = buffer[0];
181185
else address = buffer[6];
182186

183187
if (localConfig.enableRtuOverTcp) { // check CRC for Modbus RTU over TCP/UDP
184188
if (checkCRC(buffer, bufferSize) == false) {
185-
return -1; // reject: do nothing and return no error code
189+
return 0xFF; // reject: do nothing and return no error code
186190
}
187191
} else { // check MBAP header structure for Modbus TCP/UDP
188-
if (buffer[2] != 0x00 || buffer[3] != 0x00 || buffer[4] != 0x00 || buffer[5] + 6 != bufferSize) {
189-
return -1; // reject: do nothing and return no error code
192+
if (buffer[2] != 0x00 || buffer[3] != 0x00 || buffer[4] != 0x00 || buffer[5] != bufferSize - 6) {
193+
return 0xFF; // reject: do nothing and return no error code
190194
}
191195
}
192-
193-
if (queueHeaders.isEmpty() == false && slavesResponding[address] == false) { // allow only one request to non responding slaves
196+
if (queueHeaders.isEmpty() == false && getSlaveResponding(address) == false) { // allow only one request to non responding slaves
194197
for (byte j = queueHeaders.size(); j > 0 ; j--) { // start searching from tail because requests to non-responsive slaves are usually towards the tail of the queue
195198
if (queueHeaders[j - 1].uid == address) {
196199
return 0x0B; // return modbus error 11 (Gateway Target Device Failed to Respond) - usually means that target device (address) is not present
@@ -213,3 +216,18 @@ void deleteRequest() // delete request from queue
213216
queueHeaders.shift();
214217
queueRetries.shift();
215218
}
219+
220+
221+
bool getSlaveResponding(const uint8_t index)
222+
{
223+
if (index >= maxSlaves) return false; // error
224+
return (slavesResponding[index / 8] & masks[index & 7]) > 0;
225+
}
226+
227+
228+
void setSlaveResponding(const uint8_t index, const bool value)
229+
{
230+
if (index >= maxSlaves) return; // error
231+
if (value == 0) slavesResponding[index / 8] &= ~masks[index & 7];
232+
else slavesResponding[index / 8] |= masks[index & 7];
233+
}

0 commit comments

Comments
 (0)