; modbus_server.vnm ; example code for MODBUS/TCP system ; ; This program is a skeleton MODBUS/TCP server application, using ; the Micro-Robotics ethernet interface ; ; By adding application specific functions you can turn it into a ; full MODBUS/TCP server application. ; ; It has several distinct sections: ; (1) definitions for your system (IP addresses etc) ; These must be changed to suit your network. ; There is also a debug flag you can set here which will give ; verbose runtime information on the serial port ; ; (2) MODBUS TCP interface driver ; The mainstay of this code is the server function, one or more instances ; of which which is run as a separate task. ; You shouldn't need to change anything in this section, which handles ; getting and sending messages at the TCP level ; ; (3) MODBUS Functions ; These process messages and generate replies in the correct message format ; They will work unchanged but you might want to add more error handling ; capability or more functions. ; ; (4) Application functions ; You must rewrite these! ; They are nominal I/O functions with dummy test code in them. They implement ; the mapping of MODBUS numbered inputs and outputs to application specific ; values and controls. ; There is also an application_background function which runs as the program's ; main task. You can put any code you like in here, that may or may not have ; to do with the MODBUS servers and will run asynchronously with MODBUS activity. ; ; (5) Test code. ; This was written to test the rest of the code. In a real application you ; would remove it and the statement in application_background that ; starts the tester. ; ; (c) 2007 Micro-Robotics Limited ; This code comes with no warranty and you use it at you own risk. ; You are free to copy and modify it however you like. ; Please report bugs or suggestions for improvement ; to techsupport@microrobotics.co.uk ; ; (A) ported to Venom 2 2007-07-29 ; ***************** APPLICATION DEPENDENT SETTINGS ********** ; these settings MUST be customised for your system #Define USE_DHCP True #Define my_ip_address "172.16.1.150" ; not used if we use DHCP ; only used with DHCP, and then if only if your network DHCP/DNS supports it #Define my_hostname "modbus_server" ; the next three settings are only relevant if your network ; is connected to the internet and you want to set the time and date ; from the internet or use the internet for any other purpose #Define use_internet True ; set TRUE or FALSE #Define my_gateway "172.16.1.199" ; if connected to the internet #Define my_dns "172.16.1.146" ; Name server address if you have one ; number of simultaneous server processes. 2 - 8 is a good range. ; Increase if your clients get "connection refused" ; Reduce if you get "RAM FULL" run time errors #Define number_of_server_tasks 2 ; tcp timeout value in milliseconds (for receiving data) #Define tcp_timeout_ms 1000 #Define debug_mode False ;***************** MODBUS/TCP DRIVER CODE ******************** ; This section contains code you shouldn't need to modify ; standard port number for MODBUS #Define MODBUS_PORT 502 #Define x_illegal_function 1 #Define x_illegal_address 2 #Define x_illegal_datavalue 3 #Define x_illegal_responselength 4 To init init_modbus ; init MODBUS driver init_application ; put all your inits in this function End ; set up ethernet To init_modbus ; set up ethernet If USE_DHCP Make eth Ethernet(12, 0, my_hostname) Else Make eth Ethernet(12, my_ip_address) If use_internet AndAlso USE_DHCP IsFalse [ eth.Address('N', my_dns) eth.Address('D', my_gateway) ] End ; The main function. Edit To main Repeat number_of_server_tasks Start server application_background ; application function End ; this does the MODBUS/TCP stuff ; you shouldn't need to change this To server Local id := 0 ; request ID Local pid := 0 ; Protocol Identifier (always 0) Local reqlength := 0 ; length of MODBUS function request Local uid := 0 ; Unit ID AutoDestruct Local tcp := New TCProt Local request := New Buffer(Int 8) ; request message Local response := New Buffer(Int 8) ; response message tcp.TimeOut := tcp_timeout_ms Forever [ If debug_mode Print "server listening", CR tcp.Open(MODBUS_PORT) While tcp.Open = 0 Wait 1 If debug_mode Print "connection from ", tcp.Open:"IP", CR While tcp.Open <> -1 [ request.Empty response.Empty ; get the modbus header. While (tcp.Queue = 0) Wait 1 If tcp.Queue < 0 ; disconnected [ debugstr("remote end closed; no data") tcp.Close tcp.Reset Break ] ; collect header debugstr("collect header") id := get16(tcp) pid := get16(tcp) reqlength := get16(tcp) If debug_mode Print "HEADER: id ", id:1, " pid ", pid:1, " len ", reqlength:1, CR If (reqlength > 255) Or (pid <> 0) [ debugstr("invalid header, closing") tcp.Close tcp.Reset Break ] ; We should have the rest of the request in the TCP buffer If tcp.Queue < reqlength [ If debug_mode Print "too short: len=", reqlength:1, "tcp.queue=", tcp.Queue:1, CR tcp.Close tcp.Reset Break ] uid := tcp.Get fcode := tcp.Get request.Put(fcode) Repeat reqlength - 2 request.Put(tcp.Get) If debug_mode [ Print "uid=", ~uid:1, " fcode=", ~fcode:1, CR showmsg("request", request) ] Select Case fcode Case 2 read_input_discretes(request, response) Case 3 read_multiple_regs(request, response) Case 5 write_coil(request, response) Case 16 write_multiple_regs(request, response) Case Else [ ; create exception response response.Put(fcode + $80) response.Put(x_illegal_function) ] If debug_mode showmsg("response",response) ; send the response message put16(tcp, id) put16(tcp, 0) ; PID put16(tcp, response.Length + 1) tcp.Put(uid) tcp.Put(response) tcp.Flush ] If debug_mode Print "connection closed", CR ] End ; get a 16 bit big-endian value from tcp or buffer ; Valid values 0 - 65535 ; caller should check data is avaialable To get16(obj) Local v := obj.Get Return v * 256 + obj.Get End ; put a 16 bit value to TCP or buffer, big-endian To put16(obj, val) obj.Put(val Div 256) obj.Put(val And $ff) End ; MODBUS Functions ; In each of these, the request buffer contains the request data ; following the function code. ; The response buffer must be filled in starting with the ; function code (may be normal or exception response) ; ***** NB you may want to add code to detect invalid values and send ; ***** an exception response ; MODBUS function 2 To read_input_discretes(request, response) Local fcode := request.Get Local ref := get16(request) Local bitcount := get16(request) Local bytecount := (bitcount + 7) Div 8 Local b := 0 ; byte temp holder for bits Local mask := 1 Local bitnum := ref If bytecount > 254 ; exception: result too long [ response.Put($82) response.Put(x_illegal_responselength) Return 0 ] response.Put(2) ; function code response.Put(bytecount) If debug_mode Print "ref=", ref:1, " bitcount=", bitcount:1, " bytecount=", bytecount:1, CR While bitnum < (ref + bitcount) [ If read_bit(bitnum) b := b Or mask mask := mask * 2 If mask = $100 [ response.Put(b) b := 0 mask := 1 ] bitnum := bitnum + 1 ] If mask > 1 response.Put(b) End ; read_input_discretes ; read 16 bit registers To read_multiple_regs(request, response) Local fcode := request.Get Local ref := get16(request) Local wcount := get16(request) Local bytecount := wcount * 2 If debug_mode Print "read_multiple_regs: ref=", ref:1, " wcount=", wcount:1, CR response.Put(fcode) response.Put(bytecount) Repeat wcount put16(response, read_register(ref + Index0)) End ; MODBUS function code 5 (write coil) To write_coil(request, response) Local fcode := request.Get Local ref := get16(request) Local coilstate := request.Get ; 0 or $ff If debug_mode Print "write_coil ref=", ref:1, " state=", coilstate:1, CR set_relay(ref, coilstate) response.Put(fcode) put16(response, ref) response.Put(coilstate) response.Put(0) End ; write_coil ; MODBUS function code 16 To write_multiple_regs(request, response) Local fcode := request.Get Local ref := get16(request) Local wcount := get16(request) Local bcount := request.Get If debug_mode Print "write_multiple_regs (", ref:1, ", ", wcount:1, ")", CR Repeat wcount set_register(ref + Index0, get16(request)) response.Put(fcode) put16(response, ref) End ; ************** debug stuff ********** ; show the contents of a MODBUS message ; msg is expected to be buffer(Int 8) type To showmsg(id, msg) serial.Lock Print id, ": " Repeat msg.Length Print ~msg.(Index0):1, " " Print CR serial.UnLock End ; print string if in debug mode To debugstr(s) If debug_mode Print "dbg:", s, CR End ;***************** END OF MODBUS DRIVER CODE ********** ;***************** APPLICATION CODE ******************* ; Below here is code you need to modify or write to suit your ; application To init_application ; code to create objects here ; global variables are initialised here End ; set a 16 bit register. ; reg is the MODBUS reference for the register ; val is the 16 bit value to write to the register To set_register(reg, val) ; dummy test code Print "set_register ", reg:1, " = ", val:1, CR End ; read a 16 bit register ; reg is the modbus reference for the register ; returns 16 bit value ; the registers correspond to ADC channels ; pulse counter/timer inputs and similar To read_register(reg) ; test code returns a dummy value = ref + 1 Return reg + 1 End ; read a single bit value equivalent to MODBUS input discrete ; ref is the MODBUS reference number ; these will map to digital inputs ; returns zero or non-zero To read_bit(ref) ; dummy test code returns lsb of ref Return ref And 1 End To set_relay(ref, state) ; code to set relay(ref) on if state non-zero, off if zero ; test code: Print "set relay ", ref:1 If state Print " on", CR Else Print " off", CR End ; this does any background tasks that aren't driven directly by ; MODBUS requests. The function should loop and never return. To application_background Start tester ; only need this if we're testing! ; currently a dummy loop that does nothing Forever [ Wait 100 ; replace with your code if needed ] End ;**************************** TEST CODE *********************** ; not needed for real applications ; Uses loopback address to send test messages To tester AutoDestruct Local tcp := New TCProt Local msg := New Buffer(Int 8) If tcp.Open("localhost", modbus_port) [ msg.Put(2) put16(msg, 0) put16(msg, 12) send_msg(msg, tcp) msg.Empty ; 2nd test on same connection msg.Put(2) put16(msg, 2) put16(msg, 33) send_msg(msg, tcp) tcp.Close tcp.Reset ; more messages sent on new connection If tcp.Open(my_ip_address, modbus_port) [ msg.Empty msg.Put(2) put16(msg, 2) put16(msg, 33) send_msg(msg, tcp) msg.Empty msg.Put(3) ; read multiple regs(0, 2) put16(msg, 0) put16(msg, 2) send_msg(msg, tcp) msg.Empty msg.Put(3) ; read multiple regs(5, 6) put16(msg, 5) put16(msg, 6) send_msg(msg, tcp) msg.Empty msg.Put(5) ; write coil 0 on put16(msg, 0) msg.Put($ff) send_msg(msg, tcp) msg.Empty msg.Put(5) ; write coil 1 off put16(msg, 1) msg.Put(0) send_msg(msg, tcp) msg.Empty msg.Put(16) ; write multiple registers put16(msg, 2) put16(msg, 2) ; word count msg.Put(4) ; byte count put16(msg, 1234) put16(msg, 5678) send_msg(msg, tcp) ] ] Else Print "tester: couldn't open tcp connection", CR End ; test: send a request message and print response To send_msg(msg, tcp) Local c := 0 AutoDestruct Local resp := New Buffer(Int 8) If debug_mode [ showmsg("tester tx", msg) ] put16(tcp, 0) ; id put16(tcp, 0) ; pid put16(tcp, msg.Length + 1) ; length of following data tcp.Put(0) ; id tcp.Put(msg) ; rest of msg starting with fcode tcp.Flush tcp.TimeOut := 500 c := tcp.Get ; get 1st char or timeout If c >= 0 [ resp.Put(c) While tcp.Queue > 0 resp.Put(tcp.Get) showmsg("tester rx ", resp) ] End