Modbus is a common protocol used for communication between industrial devices. It’s a data layer used over an RS-485 physical interface, which is a differential signal designed to be tolerant to noise, an important criterion in industrial environments. To learn more about Modbus, check out this Wikipedia entry, or you can read the specification. For Modbus message formats, I like to refer to this page.
Note that my work has been focused on Modbus RTU, which is over RS-485 lines, but the same concepts should work with Modbus over Ethernet. Also, I will generally use hex numbers throughout this article.
How does RS-485 Addressing Normally Work?
At the simplest level, a Modbus message consists of 1 address byte, 1 function code byte, a data payload, and a CRC. In the image above, the address is 1, and the function code is 3 (read a holding register). For holding register reads (which means reading a 16-bit memory location), the data payload would also specify the first register offset to read, and the number of registers to read. See https://www.simplymodbus.ca/FC03.htm for specifics.
What we want to focus on here is the address. RS-485 is a master/slave protocol, where there’s one master who initiates all communication and multiple slaves that can respond. If the address is 0, this is a broadcast to all slaves and no one responds. If it’s non-zero, it’s specifying a particular slave, and only that slave responds. We cannot have multiple responses because they all share the same serial lines and will corrupt each other’s messages.
What Problem are we Solving?
It’s clear from this explanation that each slave needs to have a unique address within a system. There are a few ways this is done:
- Use jumpers to specify the address on each slave, making sure they are unique. Similarly, you could hard-code an address in each slave’s firmware, but this would require a separate program for each device, which would be logistically difficult.
- Have each slave default to a known address, and write to a register to set a new address. This requires that each device is connected and re-addressed one-at-a-time. This can be done in-system, or separately on a tool that implements a Modbus Master protocol to write to the device before it’s place in the system. QModMaster is one tool that can help do this.
What we wanted to do at Link 4 was automate the installation by wiring up the controllers without specifying any address, and using firmware to automatically set each slave’s address.
Because we want to adhere to the Modbus protocol, we looked for a way to do this within the Modbus specification. Fortunately, Modbus allows users to define their own message formats using function code 8, Diagnostics messages.
The Auto-Addressing Protocol
All slaves and the master are wired up. At this point, the slaves can have any address, from 1 to 255. We send diagnostic messages to perform the auto-addressing. Note that this requires special code on both the master and slave controllers.
- The master broadcasts a a notification of the desired address range.
- We could allow all viable addresses, from 1 to 247, but this will increase the time to complete auto-addressing. If you’re addressing 30 controllers, this is a waste of time.
- Addresses 248-255 are reserved, per the Modbus spec, although I don’t know why.,
- You want more available addresses than you have controllers, or you’ll increase the chance of address conflicts. I found having an address range double the number of controllers gives us good performance. I didn’t get excessive conflicts until we had only a few additional addresses.
- The master broadcasts a message to tell each slave to randomize their address.
- Each slave needs the ability to randomize it’s address within the desired range. Some controllers have a built-in RNG. Others will need a software-based RNG.
- The master queries each address in the desired range.
- If there’s only 1 slave at this address, it responds with a unique ID. It’s required that each slave have some way to respond with a unique value, either a serial number, or a unique ID built into the controller (Many STM32 ARM devices have a built-in unique ID). If the master receives a message with an intact CRC, it logs this address as used.
- If there are multiple slaves at this address, they’ll respond and corrupt each other’s message. Because they each have a different unique ID, each message will be different, so any corruption will result in a CRC error. The master detects a response with an invalid CRC as a likely conflict of address, which it will try and resolve next.
- If there is no response, this isn’t a slave at this address. It’s available for resolving any conflicts.
- The master maintains a list of which addresses had possible conflicts, and repeats this sequence for those addresses, until no remaining conflicts are detected. For each address on which a conflict was detected:
- Send a randomize address command again. To speed up auto-addressing, I included in this command a list of addresses that are unavailable. The slaves will randomize their address repeatedly until they have an address that’s in the valid range and not in this list of unavailable addresses.
- The master again queries all addresses in the range that are not used, and handling the cases as we did in step 3.
- This continues until the master has found no conflicts in the desired address range. Auto-addressing is complete.
- It would also be good to have a timeout. If you don’t have enough addresses for all of the slaves, this algorithm will not finish. Locking up an embedded device is never acceptable.
Message Formats
The above Modbus exchanges can be implemented in various ways. This is how we implemented them.
Set Addresses Range (Sub function code 0x20)
This command is always a broadcast message (address 0).
0x00 | 0x08 | 0x0020 | 2 | 1 | 100 | |
Address | Function | Sub Function | Length | Min Address | Max Address | CRC |
Figure 2 – A diagnostic Modbus Message for Auto-Addressing
In this example, we set the minimum address to 1, and the maximum to 100. The length is the number of bytes that follow the length field, not including the CRC, per Modbus protocol.
Randomize Addresses (Sub function code 0x21)
This command can be either a broadcast message (address 0) or sent to specific addresses when trying to resolve conflicts. If we’re resolving conflicts at a specific address, we’ll include a list of currently used addresses so the slave doesn’t choose an address already used.
0x?? | 0x08 | 0x0021 | n | ||
Address | Function | Sub Function | Length | Excluded Addresses | CRC |
Retrieve Unique Identifier (Sub function code 0x22)
This command is sent to specific controllers to retrieve a unique identifier. We don’t really have to have a unique ID; a response with a valid CRC should be enough to show there’s a single slave at this address, but:
- There’s a change 2 (or more) slaves respond with the same timing such that they don’t corrupt each other, because the message contents are identical. Requiring a unique ID removes this risk.
- It’s a good practice to be able to track a slave if it’s relocated or switched with a new unit.
Because we used STM32 microcontrollers with a 96-bit unique ID built-in, we sized our unique ID field to be 12 bytes.
0x?? | 0x08 | 0x0022 | 0 | |
Address | Function | Sub Function | Length | CRC |
A Python Simulation
This looks like it should work in theory, but before implementing this in our controllers, I wanted to verify it worked, and collect estimates of how long it would take.
This python program spawns a thread to simulate each slave. By default, there are 20 slaves. To simulate addressing, we send a message to each slave thread, whether it’s a broadcast message or directed at a single address. A slave only responds if it has its address.
Timing also needs to be simulated. We run our Modbus at 19.2 Kbps. This page gives some guide to calculating Modbus transaction time. I figured about 10 ms to send a message on each side, and 30 ms “turn around” time, the time it takes a slave to determine a response, so 50 ms to exchange messages. if no slave responds, the master needs to time out; I set this to 300 ms.
Simulating with the full addressable range (1 to 247):
1) Set address range
2) Start auto-addressing
q) exit/quit.
Select menu item> 2
Randomization took 2 cycles, 24 Msg transactions; 457 Timeouts
Time: 138.29999999999998 secs, at 0.05 secs/msg.
Final dictionary (Address:SlaveID): {24: 3, 38: 9, 60: 2, 64: 15, 69: 10, 79: 7, 85: 17, 97: 4, 127: 1, 169: 12, 170: 11, 180: 0, 206: 8, 219: 16, 230: 5, 237: 18, 107: 19, 149: 13, 175: 6, 234: 14}
The output shows that it took 2 cycles and 138 seconds to auto-address 20 slaves.
There’s no need to allow the full address range if we only have 20 slaves. A range of 1 to 20 would be enough, but then we may get many repeated address clashes.
1) Set address range
2) Start auto-addressing
q) exit/quit.
Select menu item> 2
Randomization took 5 cycles, 49 Msg transactions; 13 Timeouts
Time: 6.35 secs, at 0.05 secs/msg.
Final dictionary (Address:SlaveID): {1: 5, 2: 9, 5: 7, 7: 18, 9: 0, 14: 1, 17: 2, 18: 3, 3: 19, 4: 12, 13: 8, 16: 15, 15: 14, 6: 4, 8: 6, 10: 16, 11: 11, 20: 17, 12: 13, 19: 10}
We see that this took 5 cycles to complete because of the address clashes but still was able to complete in 6.3 seconds. One of the reasons this works as well as it does, with no extra addresses available, is due to slaves being notified of what addresses are not available. When they randomize on later cycles, they check to see if they’ve chosen an unavailable address and if so, increment to a valid address. Otherwise, I would expect the clashes to happen for more cycles.
Having no addressing overhead scares me a bit, so let’s add 10 extra addresses, hopefully reducing the number of cycles needed. We’ll set the range from 1 to 30.
1) Set address range
2) Start auto-addressing
q) exit/quit.
Select menu item> 2
Randomization took 3 cycles, 30 Msg transactions; 36 Timeouts
Time: 12.299999999999999 secs, at 0.05 secs/msg.
Final dictionary (Address:SlaveID): {1: 12, 2: 15, 3: 17, 9: 5, 11: 3, 16: 4, 17: 18, 20: 7, 21: 2, 22: 13, 25: 8, 26: 6, 4: 19, 5: 10, 10: 16, 12: 11, 14: 0, 29: 14, 23: 1, 28: 9}
We did reduce the cycles, but not the overall time. We went from 6.4 seconds to 12.3. The overhead of checking more addresses that have no slave means we have more 300-millisecond timeouts. Optimally, we want just a few more addresses than we have slaves.
There’s really no point trying to fine-tune this too precisely. We have applications that range from a few slaves to 20 or so. The installer may not be aware of adjusting the address range; they want to “push a button” to auto-address, and it works. Since we only do this during an install, shaving 10 seconds off is much less valuable than simplicity.
Possible Improvements
User Unreserved Modbus Registers
We’re using function code 8, subfunctions 0x20, 0x21 and 0x22. According to the Modbus specification, Annex A, function code 8, subfunctions 21-65535, are reserved. We could use our own unreserved function code instead of 8. This isn’t likely to cause a problem in the field, but it’s best to adhere to the spec.
Other Solutions
Modbus-A: Automated Slave ID Allocation Enabling Architecture for Modbus Devices
on RS485/232
B. Sudev, I. Kinghorn, D. Gu, D. Gower propose to do auto-addressing by daisy-chaining the slaves such that each slave must forward messages to each other, rather than let them all listen to the same communication lines. In this way, they can prevent multiple slaves from seeing a message and responding since each slave implements logic on when it forwards messages and enforces having unique addresses. See this paper.
I found this implementation overly complicated and requires slave firmware that deviates from the Modbus protocol, preventing the use of any Modbus libraries.
Permalink //
This is a very interesting approach to solve Modbus auto-addressing, thanks for showing a path!
Can you PM me? I have some more questions.
Malte
Permalink //
Hello Malte. I’m sorry for not seeing your message. I’ve stopped watching these emails since almost all of them are spam.
Were you able to use any of the ideas yourself?