Preamble
From time to time here at Technologic Systems we round up a bunch of our newest products and send them to a lab for HALT testing. Recently we decided to re-think how this testing is done and add more comprehensive logging of events going on from the SBC's perspective. Now, we are testing not just the "how long does this board live", but actually utilizing every available sensor on the SBC under test. This way we can gain a more thorough understanding of how the SBC might fail under various conditions and hopefully what some of the earlier symptoms of that failure might look like.
To that end, we decided we needed a couple different methods of off-board data collection as well as on-board logging.
The first method was almost a no-brainer, using rsyslog our sensor and test application can log data over Ethernet to a remote host, but what if the Ethernet port is the first to fail? A second communication method was deemed necessary to ensure we had multiple lines of communication available to the devices located in the chamber.
If not Ethernet, then what? Wifi and Bluetooth were both out because the walls of the test chamber will not pass radio signals reliably, so we needed a robust wired communication standard to run along side our Ethernet network.
Enter libmodbus. RS-485 is a known and solid serial communication standard available on all of our products, and Modbus is an industrial standard communication protocol that can operate comfortably on that medium. Thus it was tasked to me to implement an example RS-485 Modbus RTU slave program that our engineers could build into their respective product tests.
We have implemented several examples of how to communicate over Modbus as a client (master) device on an RTU Modbus network before. This is a relatively common practice. Creating a slave device, on the other hand, proved a different challenge. While Libmodbus provides all of the needed functionality, we discovered there is surprisingly little -- if not none at all -- documentation on how one might go about implementing a Modbus slave using Libmodbus. So, armed with nearly nothing, we jumped into the code and decided perhaps we should create our own documentation.
I should note that Modbus (being a very old protocol) has some rather upside-down terminology sometimes. A modbus Master is also referred to as a modbus Client, while a modbus Slave is referred to as a modbus Server. Master is Client. Server is slave.
How to make a libmodbus slave
So, beyond connecting to a Modbus RTU, which is relatively well documented elsewhere, there are three things you need to know about Libmodbus once the serial port connection is established:
1. The Modbus Map.
This is the structure that holds all of the traditional "registers" you would find on a traditional Modbus slave. I never actually found this documented anywhere, so I'll quote the definition straight out of the code here. This is defined in modbus.h:
typedef struct {
int nb_bits;
int nb_input_bits;
int nb_input_registers;
int nb_registers;
uint8_t *tab_bits;
uint8_t *tab_input_bits;
uint16_t *tab_input_registers;
uint16_t *tab_registers;
} modbus_mapping_t;
This struct is created by modbus_mapping_new(), then passed in and out of your modbus_receive() and modbus_reply() functions. See the Libmodbus documentation for those:
http://libmodbus.org/docs/v3.0.6/
2. modbus_receive():
This function will block on the modbus port until data is received, or until a specified timeout has been reached (see modbus_set_response_timeout() in the libmodbus documentation). The default behavior is to block until a packet has been received, but a timeout is desirable if your slave device has other things to do and isn't otherwise thread-friendly. It should be noted here that modbus_receive() does NOT process the modbus packet. It just receives it and tucks it away into a receive buffer.
3. modbus_reply():
This is where the Modbus Magic actually happens for a slave device. The modbus_reply() function takes the request received in the modbus_receive() function, parses it to decide whether a response is needed, makes any applicable changes to the modbus mapping struct, and sends off a response to the Modbus master. Basically modbus_receive() and modbus_reply() need to be run back to back, as the modbus_mapping struct won't reflect any new data from the received request until the modbus_reply() function has run.
With those bits of knowledge in place, it's probably time to give a couple small sample programs. I should note this code does belong to Technologic Systems, but we don't particularly care if you use, modify, share, or keep it as your secret. It would be nice to give credit where it's due, or even nicer if you thought about buying our products though! Here's the common header:
// modbus_example.h // c. 2018 Technologic Systems // Written by Michael D. Peters #define BAUD_RATE 115200 #define MESSAGE 0X0 #define NEW_SEQUENCE 0X1 #define CURRENT_SEQUENCE 0X2 #define DA_REGISTER 0x14 #define REBOOT_ORDER 0XFFFF
That out of the way, here's the slave side of this example:
// modbus_slave_example.c // c. 2018 Technologic Systems // written by Michael Peters // This example sets up a modbus slave node that listens for modbus commands. // The modbus_map structure is documented literally nowhere. I dug it out of the source // code in modbus.h: /* THIS STRUCT IS HERE FOR REFERENCE. IT IS DEFINED IN modbus.h. typedef struct { int nb_bits; int nb_input_bits; int nb_input_registers; int nb_registers; uint8_t *tab_bits; uint8_t *tab_input_bits; uint16_t *tab_input_registers; uint16_t *tab_registers; } modbus_mapping_t; */ // Compile instructions: // Install package pkg-config. // gcc `pkg-config --cflags --libs libmodbus` <filename.c> -o <binaryname> -Wall #include <stdio.h> #include <stdlib.h> #include <modbus.h> #include <errno.h> #include "modbus_example.h" #define SERIAL_PORT "/dev/ttts3" #define CONFIG_FILE "slaveid.conf" modbus_t* comm_setup(); // sets up modbus port. int get_slave_id_from_file(); // Grabs slave ID from config file. int main(int argc, char **argv) { int err; // set up the modbus data registers structure. modbus_mapping_t *modbus_map; modbus_map = modbus_mapping_new(0, 0, 0x15, 0); // all values init to zero. if(modbus_map == NULL) return 1; // Stop. NULL modbus_map means ENOMEM. modbus_map->tab_registers[DA_REGISTER] = 0x00DA; // This is a test register. Always returns 0xDA. modbus_t *mb = comm_setup(); // turn on the serial port & set up *mb. if(!mb){ modbus_mapping_free(modbus_map); return 1; } int slave_id = get_slave_id_from_file(); // Grab slave ID from file. if(slave_id <= 0){ fprintf(stderr, "slave_id = %d\n", slave_id); fprintf(stderr, "Error opening slave ID config file.\n"); modbus_mapping_free(modbus_map); return 1; // quit if error opening slave ID file. } else modbus_set_slave(mb, slave_id); modbus_set_debug(mb,TRUE); // This causes libmodbus to output all packet data to stdout. uint8_t req[180]; // The actual packet. Should be larger than any inbound packet. int num_reqs; int sequence_number; while(TRUE){ // listen for modbus requests. num_reqs = modbus_receive(mb, req); // default blocking request will not time out. if(num_reqs == -1){ fprintf(stderr, "modbus_receive() returned -1 status.\nerrno=%d\n%s\n", errno, modbus_strerror(errno)); } // Respond to modbus request. This must be run immediately after modbus_receive. err = modbus_reply(mb, req, num_reqs, modbus_map); if(err == -1) printf("Error sending reply. %s\n", modbus_strerror(errno)); else printf("Sent reply. Length %d.\n", err); // if err is not -1 then err=length. // Any new data should now be in the modbus map for the slave to process. // Check for messages in the message register. if(modbus_map->tab_registers[MESSAGE] != 0){ printf("Message from Modbus Master is 0x%04X.\n", modbus_map->tab_registers[MESSAGE]); if(modbus_map->tab_registers[MESSAGE] == REBOOT_ORDER) printf("Message contained reboot command. Replace this printf with a safe shutdown and reboot.\n"); modbus_map->tab_registers[MESSAGE] = 0; // Reset the messages register after acting on it. } else printf("Nothing in the message register.\n"); // check to see if there is a new sequence number being requested. if(modbus_map->tab_registers[NEW_SEQUENCE] != 0){ sequence_number = modbus_map->tab_registers[NEW_SEQUENCE]; modbus_map->tab_registers[CURRENT_SEQUENCE] = sequence_number; // reset the new_sequence register. modbus_map->tab_registers[NEW_SEQUENCE] = 0; } // This is the end of the packet processing loop. } modbus_mapping_free(modbus_map); modbus_close(mb); modbus_free(mb); return 0; } // Opens CONFIG_FILE and reads in a text value. // If there's a problem, returns -1. Otherwise returns a number. int get_slave_id_from_file() { int the_id; FILE *fp = fopen(CONFIG_FILE, "r"); int numbytes; if(fp == NULL){ the_id=-1; } else{ numbytes = fscanf(fp, "%d", &the_id); if(numbytes==0) the_id=-1; } return the_id; } // Creates serial port connection and allocates modbus port. // Returns a pointer to the modbus context (ctx). modbus_t* comm_setup() { modbus_t *mb; mb = modbus_new_rtu(SERIAL_PORT, BAUD_RATE, 'N', 8, 1); if(!mb){ fprintf(stderr, "unable to open bus at port %s.\n", SERIAL_PORT); } modbus_connect(mb); return mb; }
In a nutshell, the example sets up the modbus port and map struct, then sits and listens for commands on Modbus, respond if appropriate, then process any changes made to the modbus mapping as needed. Note, the modbus map can be modified either when a packet is addressed to the slave's ID or when the packet is addressed to broadcast (ID = 0). An actual response will only be sent to the serial port if the modbus slave's ID was used. All ID values will still kick out of modbus_receive() but won't be acted on by modbus_reply().
So, to accompany the aforementioned modbus server example, here is a modbus client that will exercise the server appropriately:
// controller_modbus_example.c // c. 2018 Technologic Systems // written by Michael Peters // This example sests up an example modbus master that sends example commands to the example slave. #include <stdio.h> #include <stdlib.h> #include <stddef.h> #include <stdint.h> #include <modbus/modbus.h> #include <errno.h> #include "modbus-example.h" #define SERIAL_PORT "/dev/ttts3" #define TARGET_SLAVE 78 #define BROADCAST 0 // err = 1 if something bad happened. // err = 0 if everything went according to plan. modbus_t* comm_setup() { modbus_t *mb; mb = modbus_new_rtu(SERIAL_PORT, BAUD_RATE, 'N', 8, 1); if(!mb){ fprintf(stderr, "unable to open bus at port %s.\n", SERIAL_PORT); } else modbus_connect(mb); return mb; } // Get the value of a specific register, populate ret_valule with it. // returns 1 on read error. int get_specific_register(modbus_t *mb, uint8_t target_id, uint16_t the_register, uint16_t *ret_value) { int error; modbus_set_slave(mb, target_id); error = modbus_read_registers(mb, the_register, 1, ret_value); if(error < 0) return 1; else return 0; } // sends a number to the target device. // remember ID 0 is broadcast. // returns 1 if something went wrong, else returns 0. int set_sequence_register(modbus_t *mb, uint8_t target_id, uint16_t sequence) { modbus_set_slave(mb, target_id); if(modbus_write_register(mb, NEW_SEQUENCE, sequence) == -1) return 1; else return 0; } // Sets an arbitrary register on the target device. int set_specific_register(modbus_t *mb, uint8_t target_id, uint8_t the_register, uint16_t the_message) { modbus_set_slave(mb, target_id); if(modbus_write_register(mb, MESSAGE, the_message) == -1) return 1; else return 0; } // ******************************ENTRY POINT HERE*************************************** int main(int argc, char **argv) { int error; uint16_t return_value; modbus_t *mb = comm_setup(); if(mb == NULL){ printf("%s\n", modbus_strerror(errno)); return 1; } else { // Test get_register. error = get_specific_register(mb, TARGET_SLAVE, DA_REGISTER, &return_value); if(error>0) printf("%s\n", modbus_strerror(errno)); else printf("Requested 0xDA register, received 0x%2X.\n", return_value); // Test set sequence register. error = set_sequence_register(mb, BROADCAST, 10); // broadcast new sequence is 10. if(error>0) printf("Unable to set sequence number. %s\n", modbus_strerror(errno)); else printf("Set new sequence number to 10.\n"); // Read back the sequence number to make sure it got set properly. error = get_specific_register(mb, TARGET_SLAVE, CURRENT_SEQUENCE, &return_value); if(error > 0) printf("Unable to read back sequence number. %s\n", modbus_strerror(errno)); else printf("Target's current sequence number is %d.\n", return_value); // Broadcast the reboot command. error = set_specific_register(mb, BROADCAST, MESSAGE, REBOOT_ORDER); if(error > 0) printf("Unable to send broadcast reset. %s\n", modbus_strerror(errno)); else printf("Reset message has been broadcast.\n"); } modbus_close(mb); modbus_free(mb); return error; }
Finally, this client just exercises the server to demonstrate functionality. It sends off an "are you alive?", grabbing 0xDA out of the test register, then issues a broadcast register write followed by a device register read to make sure the write was properly acted upon (because CURRENT SEQUENCE is expected to be what we wrote in NEW SEQUENCE in our last operation), then a demonstration of how message passing might be implemented, for example if we wanted all of the devices on the RS-485 network to reboot.