How to build a Modbus driver in Go?

This article serves as a practical guide into how to build a Modbus driver. I use the Go (Golang) programming language because I like it . However, the principles outlined should apply to any stack.

I happen to be a software engineer who works in SCADA. What does that mean?  it means that a lot of my days involve making various pieces of complex software work in harmony with industrial devices like one big happy family. In order to make software talk to industrial devices, we use protocols that both the devices and the software can understand. Some protocols are fairly complex and some are fairly easy, some are frustrating and some are fun. Amongst them Modbus is the most popular.

So what’s Modbus?

Modbus is an industrial communication protocol that was born in the late 70s. It’s free, it’s fairly efficient , and it’s used everywhere when it comes to industrial devices. Over the years, lots of Modbus flavors were introduced with different variations. However, basic concepts stayed the same.

The building block of the Modbus driver architecture consists of two main components: The Modbus master and the Modbus slave. Usually the Modbus master is like a “client”, the initiator of requests, whereas the Modbus slave acts as a “server”, it accommodates the requests received from the master. How it works is that the Modbus master sends messages to the Modbus slave either asking for data (reads), or sending write requests. The Modbus slave then responds based on the nature of the request. A Modbus Master usually connects to multiple Modbus slaves. Each slave must have a unique ID to identify it, this is simply termed as the “Slave ID” in Modbus RTU or “Unit ID” in Modbus TCP. When building a Modbus software driver, the software is typically the Modbus master and the industrial devices are the Modbus slaves.

How does the Modbus protocol work?

The Modbus slave in the standard Modbus specifications has tables that store the values contained in the Modbus slave. Both the datatype of the value and the access rights to it (read-only or read\write) depend on the type of the table that hosts the value. The two main types of tables are called coils and registers. Coils store discrete (boolean) data , whereas registers store numerical values. We then have tables that allow reads only and tables that allow both reads and writes. Each table type has an allocated list of unique addresses that represent the data it contains. Here are the basic table types in the Modbus specifications.

Modbus driver
Modbus driver

 

The typical mission of the Modbus master is to read values from the tables stored at the Modbus slave, and/or issue write commands to the tables that allow writes. This is performed by sending binary messages in specific formats, the message structure depends on the request type. The typical format of a Modbus message includes what is called a function code which is the operation requested from the Modbus slave followed by the addresses of the values involved. Function codes are predefined and are usually documented in the Modbus device manual. So for example, say we want to read three values from an Analog output register in Modbus slave 1 starting from the address 4: the message must contain the following information:

  1. Slave ID 1
  2. Function code 0x03, which is the typical function code used to request reads from output registers
  3. The address of the first value we seek which is 4
  4. Number 3, to make it known to the Modbus slave that we need 3 consecutive values

The above is basically the building block of most of the Modbus read messages. Then from there, based on your Modbus flavor, you add more blocks like CRC, TCP info, session ID..etc

Modbus flavors

Modbus was initially built as a serial protocol (popular approach in the 70s). But then as the years progressed, ethernet emerged a very powerful means of communications. Most of the Modbus drivers I come across today are TCP related. There are also flavors of Modbus that define how the binary data in the message is presented, the two most common data formats are Modbus ASCII and Modbus RTU. A good article that describes the two can be found here

So how to build a Modbus driver?

With that out of the way, let’s dive into how to write a functional Modbus driver, the steps are fairly straight once you have good grasp of the basics:

  1. You identify the model and make of industrial devices you need your software to interact with
  2. If they use standard Modbus, you use a standard Modbus specification document. Otherwise, you utilize the special modbus manual that accompany the equipment. The function code descriptions section is where the message formats are explained. The standard Modbus document could be found here. With this, you can identify the core Modbus messages (also called Protocol Data Unit PDU )
  3. Identify the Modbus flavor that the device supports (RTU, ASCII, TCP,  Serial…etc). Additional fields are added to the message to support the needed Modbus flavor. Here is an example of how a Modbus TCP packet will look like 
  4. Use your program to build up binary messages that adheres to the format described in the specs, send it to the address (IP address for TCP Modbus) of the slave device
  5. The Modbus driver then needs to write the device response and translate the provided byte array to the response message format described in the specs
  6. Use  a Modbus simulator to test your code.  Here is a nice cross platform simulator : ModbusPal

Most of the special Modbus spec flavors involve new function codes that don’t exist in the standard specs to allow unique functionality , however, the message format is usually the same.

Writing a Modbus driver in Go

Let’s observe how to build a binary Modbus message in Go, the following implementation covers the TCP Modbus flavor which I happen to encounter quite a bit.

Let’s start by a holding register. Say, we want to read some values from a holding register in a Modbus slave which supports Modbus RTU TCP. We first investigate the proper Modbus binary message format, it looks like this:

[Transaction ID: 2 bytes][protocol ID: 2 bytes][Message length:2 bytes][Slave ID: 1 byte][Function code: 1 byte][first register address:2 bytes][number of registers requested:2 bytes]

Here is a detailed description from where I got this message format.

Here’s how that translates in Go:

func ConstructReadMessage(unitAddr byte, regAddr uint16, length uint16, transactionid uint16) (b []byte){
    // the initial address of the requested data as well as the number of values can be represented in two bytes each
	addrSlice := make([]byte,2)
	lenSlice := make([]byte, 2)
    transSlice := make([]byte,2)

	// we assume our device takes BigEndian , this shows Go built-in functions to produce bytes in the correct order from uint16
	binary.BigEndian.PutUint16(addrSlice, regAddr)
	binary.BigEndian.PutUint16(lenSlice, length)
    binary.BigEndian.PutUint16(transSlice, transactionid)

	//creating a byte array with the needed format that includes all the required information
    //size will be 6
    //function code for reads from holding registers is 0x03
	ReadMsg := []byte{transSlice[0],transSlice[1],0x00,0x00,0x00,0x06, unitAddr, 0x03 ,addrSlice[0], addrSlice[1],lenSlice[0], lenSlice[1]}

	return ReadMsg
}

Fairly simple!! The function took on the task of converting all the arguments to byte slices that can then be merged together to form our message.

Now let’s try another example, how about we try to write a value to a single output coil? The function code used in this case as per the Modbus specs should be 0x05. Message format should look like this:

[Transaction ID: 2 bytes][protocol ID: 2 bytes][Message length:2 bytes][Slave ID: 1 byte][Function code: 1 byte][register address for write:2 bytes][value to write:2 bytes]

And the code:

func(R *OutputCoil)ConstructWriteMessage(unitAddr byte, regAddr uint16, value uint16, transactionid uint16)(b []byte) {
    addrSlice := make([]byte,2)
    // this is the value used for the write command
	valSlice := make([]byte, 2)
    
    trasnSlice := make([]byte, 2)
	
    binary.BigEndian.PutUint16(addrSlice, regAddr)
	binary.BigEndian.PutUint16(valSlice, value)
    binary.BigEndian.PutUint16(trasnSlice, transactionid)
	
    //function code this time is 0x05
    writeMsg := []byte{trasnSlice[0],trasnSlice[1],0x00,0x00,0x00,0x06, unitAddr, 0x05 ,addrSlice[0], addrSlice[1],valSlice[0], valSlice[1]}

	return  writeMsg
}

From here, all what you have to do is to author functions to construct the remaining messages, that correspond to your needs for your Modbus driver. In case of RTU TCP, we send the byte array we constructed to the Modbus device via TCP, then listen to the device response. If it’s a read operation, the device will respond with the requested values. If it’s a write operation, the device replies indicating whether it succeeded or not.

func handleMessage(dst string , data []byte) (response []byte){
    //sanity check
	if data == nil{
		return
	}
	conn, err := net.Dial("tcp", dst)
	checkError(err)
	defer conn.Close()
	fmt.Println("Send data Message \n", data)
	_, err = conn.Write(data)
	checkError(err)
	fmt.Println("Received data Message")
	response = make([]byte,512)
	n,err := conn.Read(response)
	fmt.Println(response[1:n])
	checkError(err)
	return
}

func checkError(err error){
	if(err!=nil){
		fmt.Println("Error occured", err)
		os.Exit(1)
	}

Now let’s say the Modbus device provided a response to our read request. The response will be pretty much a bunch of bytes that are provided to us in an array. We need to write some intelligent logic to cut out the array in a way to find out two things: First, if the request was successful or not , and second the values of the registers or coils we are trying to read. Go provides some very nice capabilities in this arena.

For a holding register, the read response looks like this as per the Modbus specs combined with the Modbus TCP message format:

[Transaction ID: 2 bytes][protocol ID: 2 bytes][Message length:2 bytes][Slave ID: 1 byte][Function code: 1 byte][number of bytes that represents the requested values (nx2) : 1 byte][values requested: n bytes]

The above packet for a device that provides each value as two bytes, here’s how the code will look like:

//the parameter data is the response received from the device
//the return type is an interface{} because the calling function doesn't need to worry about the types returned
func Readmessage(data []byte)(interface {}, error){
    fmt.Println("Processing response message... ")
    //if the length of the byte array is less than 6, then the message is invalid
	if len(data) < 6{
		return nil, errors.New("Invalid response message size")
	}
    
	//Could panic here, in a production environment more safegaurds will be needed
    //the message size will be stored in two bytes, in our case it's BigEndian
	length := binary.BigEndian.Uint16([]byte{data[4],data[5]})
	fmt.Println("Message size: " , length)
    
    //byte 6 of the message should be the unit or slave ID associated with 
	fmt.Println("Slave ID: ", data[6])
    
    //byte 7 should be the function code, since this is a read operation for a holding register, fc must be 0x03
	if data[7] == 0x03{
		fmt.Println("Read response confirmed, proceeding with the read operation... ")
	} else {
		return nil, errors.New("Invalid function type in reponse message ")
	}
    
    //the number of values contained in the message is the number of bytes left divided by 2
    //we divide by 2 because each value lives in two bytes or a uint16 datatype
	nValues := int(data[8]/2)
	fmt.Println("Number of values available: ", nValues)
    //we create a slice to host the the values to retrieve
	values := make([]uint16, nValues)
    //open up a reader for the remaining data
    valuesbuf := bytes.NewReader(data[9:])
    //now we use the binary package to read the remaining data into our values slice
	err := binary.Read(valuesbuf, binary.BigEndian, &values)
    //either the needed values or an error will be returned
	return values,err
}

The reason why I return an interface{} is because I was testing out the factory design pattern in Go. The Readmessage() return types need to be as generic as possible, because different Modbus table types will return different datatype results when a read operation is performed. If you are not familiar with interface{} in Go, it’s suffice to say that it is the closest thing Go has to generics.

And with that, we conclude the tutorial. More of my Modbus code can be found here . For more information about the Modbus protocol, http://www.simplymodbus.ca is a nice resource.

 

 

 

2 thoughts on “How to build a Modbus driver in Go?

  1. MODBUS FLAVOURS

    HTTP Error 404.0 – Not Found
    The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.

  2. Hi bro ..

    Incidentally, I have an Modbus Slave Server application for testing your Modbus Driver. HINET Modbus Slave Server, it can simulate Modbus Slave for Standard Modbus and Enron Modbus.

    Unlike the existing Modbus Slave Simulator, Enron Modbus Slave Simulator that I have developed, can utilize the address Number 1 to 999 to be used as an Address ShortCut.

    For example, if we want to request the current exiting temperature and pressure in the Gas Stream #1 and Gas Stream #2 at Address Register 7105,7106,7205,7206, so we must make a request to the slave as much as two times, because the maximum Number Of Point that we can do is only 52 point.

    However, by using the Address ShortCut, you can make requests to the slave just once, because the one ShortCut Address can represent to Address like 7105,7106,7205 and 7206.

    Examples Standard Modbus Request:
    00 00 00 00 00 06 01 03 00 02 1B C1 For Data 7105 & 7106
    00 00 00 00 00 06 01 03 1C 25 00 02 For Data 7205 & 7206

    But with Address ShortCut we have defined it registered 50 would be like this :
    00 00 00 00 00 06 01 03 00 32 00 04 For Data 7105,7106,7205,7206

    Of course, in this way, The information at gas stream #1 and gas stream #2 you can read faster than using other Standard Modbus request.

    In addition, the issue of lack of security that exist in Modbus Protocol, with this Modbus Slave Simulator can be minimized. Because, now you can open a Modbus port with Secure Connection mode.

    With Secure Connection mode, any master who connect to slave then it must have a Client Access License key. And its key number must be equal to the key number that has been defined in the slave server, if not the same, then the Execption Status 06 will be sent to the master.

    Secure Connection mode is optional, you can use it or not use.

    If you are curious to try Modbus Slave I have this, please download the program here https://dl.dropboxusercontent.com/u/3351822/HINETSlaveServer2015.rar

    Thanks…

Leave a Reply

Your email address will not be published. Required fields are marked *