Simple MODBUS RTU Slave

General discussion on mikroPascal.
Post Reply
Author
Message
Boz
Posts: 6
Joined: 13 Jun 2007 22:45
Location: New Zealand
Contact:

Simple MODBUS RTU Slave

#1 Post by Boz » 11 Dec 2008 07:06

In reply to a PM I thought I would make this working prototype for a simple RTU slave generally available.

It was developed on a18F8250 (BIGPIC) at 10Mhz, but modified very easy to my prototype PCB which uses a 28pin 18f2520 with a few configuration byte and register changes.

It only supports the MODBUS functions my client requested, 0x04 (Read multiple registers), 0x06 (write single register) and some 0x08 diagnostics also there are a few bits I did not finish (such as parity) which my client did not need.

Writing the MODBUS functions is actually really easy once you have the basic RS-485 MODBUS frame receive and transmit parts working, and you will also find (as I did) that the standard (rightly or wrongly) is actually interpreted pretty loosely by various manufacturers and I have seen a lot of slaves in the real world that only support two or three MODBUS functions so do not think you have to try and emulate them all unless you are creating a very generic device.

Ive also included a PC MODBUS master simulator program to test the PIC with, hopefully you will find it useful to see the hex bytes going out and in.

Here is the link http://www.boznz.com/misc/modbus-stuff.zip

Good luck and don't bother asking any questions as I probably wont remember exactly why I did it that way and anyway I'm fully occupied on a large (Delphi programming) project and not checking the electronics development forums too much at this time.

Boz
www.boznz.com
Simple solutions to complex problems

apsoft
Posts: 2
Joined: 09 Jun 2005 16:59
Contact:

Thank You

#2 Post by apsoft » 11 Dec 2008 23:54

Thank You

Bye Julio Antolin

chimimic
Posts: 178
Joined: 29 Sep 2007 14:35
Location: France
Contact:

#3 Post by chimimic » 12 Dec 2008 14:18

Thanks for this, Boz.

giladn
Posts: 3
Joined: 23 Aug 2006 09:40

#4 Post by giladn » 17 May 2009 07:45

thanks

Richie800
Posts: 50
Joined: 09 Jul 2007 00:31

Re: Simple MODBUS RTU Slave

#5 Post by Richie800 » 03 Mar 2011 03:29

Hello. I know this post is old but was wondering if some could help me convert a code. I translated the code as best as I could but it is not working with the software provided in zip file. I'm not sure what to do.

This is the code dl from the Zip above

Code: Select all

program modbus

'BOZNZ (C)2007 (www.boznz.com)
'
'Uses BIGPIC PIC18F8520 with XTAL Clock running at 10Mhz.
'
'To change to prototype 18F2520 you need to change configuration word (edit project) and change the USART registers removing the '1' at the end
'
'The device will emulate only the MODBUS serial protocol functions listed below using RTU mode over serial line (www.modbus.org):
'
'0x03 - read holding registerss (See 0x04)
'
'0x04 - read multiple registers. Either functions 0x03 or 0x04 will read multiple registers
'       there is only one set of 50 read-only registers
'
'    0-39 = <Reserved for sensor data read as 0xFFFF if not set>
'    40    = STATUSWORD
'              0 = <reserved>
'              1 = <reserved>
'              2 = <reserved>
'              3 = <reserved>
'              4 = <reserved>
'              5 = Transmit buffer overrun this is a programming error as the system should only be able to tx upto 104 chars and it has been clipped
'              6 = MODBUS Frame counter has counted more than 64K and overflowed back to 0
'              7 = MODBUS CRC counter has counted more than 64K and overflowed back to 0
'              8 = MODBUS Exception counter has counted more than 64K and overflowed back to 0
'              9 = MODBUS processed message counter has counted more than 64K and overflowed back to 0
'             10 = <Reserved>
'             11 = <Reserved>
'             12 = <Reserved>
'             13 = <Reserved>
'             14 = <Reserved>
'             15 = <Reserved>
'    41    = MODBUS Frame count
'    42    = MODBUS CRC Error count
'    43    = MODBUS Exception counter
'    44    = MODBUS Processed message count
'    45..49= <Reserved for whatever read as 0x0000>
'
'0x06 - Write single register - Supports writing the following register
'
'   0x0100 = Write modbus address (valid values 0x0001 to 0x00F2) Address is
'   preserved in non-volatile
'            memory and change will only take affect on Power-on-reset.
'
'0x08 - Disgnostics. supports following sub-functions:
'
'    0x01 - COMMS RESET - Forces software reset of device clearing counters
'           (If the comms ever get screwed up of course it wont receive this
'           command!)
'    0x04 - Force listen-only mode (no further transmissions will be made until
'    power on reset or COMMS RESET command above)
'
'    0x0A - Clear Message error count
'    0x0B - Returns message count (incl counting messages not addressed to it)
'    - will overflow at 0xffff
'
'    0c0C - Returns CRC error count - will overflow at 0xffff
'    0x0D - Returns modbus exceptions returned by device (number of times it
'    has returned a modbus exception eg command not supported) - will overflow
'    at 0xffff
'    0x0E - Returns message count that this device has processed - will
'    overflow at 0xffff
'
'
'ALL other MODBUS functions will return Exception code 0x01 (Not supported) as
'they are not applicable to this device
'
'Modbus default address of unit is 0x05 (use fn 0x06 to change)
'
'Default comms same as default modbus:        9600baud, 1 start, 8 data, no
'parity, 1 stop
'
'PORT RC5 is set high to Transmit on the RS-485 and then returned to low to
'receive
'
'****** ALL PROGRAM VALUES ARE HARD-CODED FOR 10MHZ XTAL AND 9600 BAUD *********
 const MaxReg = 49      ' Number of registers (count from 0)
 const TxBufSize = 104  ' Max buffer size should include all registers + CRC + overhead
 const RxBufSize = 20   ' RX Buffer size does not really matter as we are only interested in first 7 chars constituting a MB command
 const DefMBAddress = 5 ' Default MODBUS Address if system is uninitialised
 const BaudRate = 9600  ' Baud rate (9600 or 19200)
 const Parity = "N"     ' Parity (N, E, O, M or S) { just for transmit, we ignore received Parity errors as we use CRC }
                    '{ Note that the Parity on tranmit is not yet working/tested so use 'N' }
Dim BufPtr, MBaddr, MBFrameTimeout as byte
    CRC as word                     ' calculated 16 bit CRC word
    ReadOnlyMode, OPERATE_LED as boolean
    RxBuf as string[RxBufSize] '// Array where USART Chars are received (in interrupt)
    TxBuf as string[TxBufSize] '// Array from which USART response chars are tranmitted
    MBregister as word[0..MaxReg]' // MODBUS REGISTERS in RAM (See description above)
    HexStr as string[2] '// Used for LCD Display (Dump in production version)

' the high priority interrupt was used by my pulse width detectors so i have deleted the code as it is not relevent
' best to use the high priority for your interrupts so they wont interfere with the modbus comms and timeout timer
' which use the low priority interrupt
'}
sub procedure interrupt_low
  if PIR1.5 = 1 then ' // USART has received a char to process
    OPERATE_LED = false  '// OPERATE LED OFF (to user it will appear to flash off as data is rxd)
    rxbuf[bufptr] = RCREG  ' // read the received data from USART 1 into cmd buffer
    if bufptr < RxBufSize then
       inc(bufptr)  ' // only interested in first few chars containing command others can be biffed
    end if
    if (RCSTA.1 = 0) AND (RCSTA.2 = 0) then '// log/clear errors. We use CRC for error checking so we basically ignore these errors
      RCSTA.CREN =0 '; // clear the error
      RCSTA.CREN = 1'; // enabl       e receiving again
    end if
    MBFrameTimeout =0 '; // clear down frame timeout TIMER0 on char reception (>3.5 chars without a char is a frame timeout)
    PIR1.5 = 0  ' // ack the interrupt
  end if

  if INTCON.TMR0IF = 1 then '// timer 0 has overflowed (1.6384mS) this is the approx length of a character at 9600 baud
    if MBFrameTimeout < 200 then
      inc(MBFrameTimeout)
    end if  '// increment the counter (main loop actually does timeout)
    INTCON.TMR0IF = 0 ' // ack interrupt until next time
  end if

end sub

Sub procedure resetrxbuf ' // Clears receive variables on completion of good modbus frame or period of no comms activity
  OPERATE_LED = 1  '  // OPERATE LED ON
  bufptr = 1  ' // init bufptr back to first position in array
  MBFrameTimeout = 0  '// clr down any USART Receive timeouts
end sub

sub procedure InitPic  ' // initialisation of PIC registers, timers, USART and Interrupts

 ' MEMCON.EBDIS = 1   '  // BIGPIC Development system (Delete line for 18F2520)

  ADCON0 = 0        '// Turn of ADC
 ' CMCON  = 0x07    ' // turn off comparators
  ADCON1 = 0x0F     '// turn off analog inputs

  TRISB =0  '// used for output
  TRISC = 0xdf ' // bits 7 and 8 used by USART, bit 5 used to control RS-485 chip tri-state
  PORTC = 0

  UART1_init(BaudRate)'  // initialize USART (Takes out PORTC bits 7 & 6 !!)
  RCSTA.0 =1 '           // 9 bit mode enabled all the time
  TXSTA.6 =1 '

  Option_Reg = 0xc3'   // Enable 8 bit timer0  interrupt occurs every 4x256x16/Fosc Seconds (1.6384mS @ 10Mhz)
'
'  'RCON = $80'     // ENABLE priority Interrupts - INT0/INT1 (TMP05 timing) high priority, USART1/TIMER0 (comms) low priority
'
'  PIE1 = 0x20 '  // enable USART1 interrupt (we do not enable TIMER1 interrupt we will just poll it for overflows)
'
''  IPR1 =0 ' // All peripheral interrupts are low priority (just in case)
''  IPR2 = 0
'
'  INTCON2 = 0x48' // PortB pullups disabled, INT0 rising edge, INT1 trailing edge, TIMER0 low priority    01001000
'
'  INTCON3.6  = 1'  // INT1 High priority
'
'  INTCON  = 0xe0   // enable timer0, INT0 and Peripheral interrupts and TURN ON GLOBAL INTERRUPTS
'
'  '// INT0 and INT1 enabled seperately by tmp05 routines and interupts when required

end sub

sub procedure ClearCounters' // MB Statistics counters and other counters
  Dim i as byte

    for i = 40 to MaxReg
      MBregister[i] = 0
    next i
  
end sub

sub procedure _Hex(Dim _input as byte) ' // converts input to Hex and writes to
                                       'HexStr variable for display on LCD
                                       ' (Dump in production version)
  hexstr[0] = (_input div 16)+48
  if hexstr[0] > 57 then
     hexstr[0] = hexstr[0]+7     ' // a..f
  end if
  hexstr[1] = (_input mod 16)+48
  if hexstr[1] > 57 then
     hexstr[1] = hexstr[1]+7
  end if
  HexStr[2] = 32
  
end sub

sub procedure InitProg'; // initialisation of program variables
 Dim i as byte
   for i = 0 to MaxReg'// init the registers
      MBregister[i] = 0xffff
   Next I

  ResetRxBuf
  ClearCounters
  ReadOnlyMode = false  '// Enable transmit (cmd 0x08 sub-fn 0x04 can set this flag and stop transmit working)

'        // read our modbus address stored in non-volatile EEPROM or set default
   MBaddr = EEProm_Read(0)
  if MBaddr = 0xff then
     MBaddr = DefMBAddress
  end if

  '// DEBUG message to LCD (delete for 18F2520 as we do not have an LCD)

  'Lcd_Init(PORTH)'                  //  initialize LCD
'  Lcd_Cmd(LCD_CLEAR)'              //  send command to LCD "clear display"
'  Lcd_Cmd(LCD_CURSOR_OFF)'         //  send command cursor off
'  Lcd_Out(1, 1, "--- BOZNZ.COM ---")'    //  print txt to LCD, 1st row, 1st column
'  Lcd_Out(2, 1, "Modbus addr =")'   //  print txt to LCD, 2nd row, 1st column
'  Hex(MBaddr)
'  Lcd_Out(2, 15, HexStr)'          //  print modbus addr to LCD
end sub

'sub procedure SendByte( dim ch as char) ' // add Parity (if required) and send char
'  Dim par as byte
'
'  par = ch.0 + ch.1 + ch.2 + ch.3 + ch.4 + ch.5+ ch.6 + ch.7 ' // LSB=1 = odd number, LSB=0 = even number
'  while PIR1.4 = 0
'     NOP ' // wait for tx transmit reg to become free
'  wend
''    Note i am still working on this bit so use Parity = 'N' until i figure out how to do it
'  case Parity of
'    'E': TXSTA1.TX9D := par.0;
'    'O': TXSTA1.TX9D := NOT par.0;
'    'M': TXSTA1.TX9D := 1;
'    'S': TXSTA1.TX9D := 0;
'  end;
'  TXREG1 := ch; // send char
'  while TXSTA1.TRMT=0 do NOP; // wait for character to be sent
'end;
procedure MB_Send(len:byte); // Send out len chars in the TxBuf and then send a CRC
var
  n,i: integer;
begin
  if len>TxBufSize then // (this will not happen but we check anyway!) clip the transmitted message if it overruns buffer and light ERROR LED
  begin
    len:=TxBufSize;
    MBregister[40].5:=1; // flag error
  end;

  if RxBuf[1]=0 then exit; // Broadcast so dont respond
  if readOnlyMode then exit; // readonly mode so dont respond

  PIE1.5:=0; // turn off receive interrupt during tranmission

  PORTC.5:= 1;  // PULL RC5 High to initiate RS-485 transmission mode and wait for settle
  delay_ms(2);  // 1 will work just as good but dont set it lower than 1

  crc := 0xFFFF;
  for i:=1 to len do // tx each char of buffer and update the CRC as you go
        begin
    crc := crc xor word(TxBuf[i]);
    for n:=1 to 8 do
                begin
      if (crc and 0x0001)<>0 then
                          crc:=(crc shr 1) xor 0xA001
      else
                          crc:=crc shr 1;
    end;
    USART_write(TxBuf[i]); // write the response byte by byte
  end;

  USART_Write(lo(crc)); // write crc LSB
  USART_Write(hi(crc));  // write crc MSB

  delay_ms(2); // settle time as above
  PORTC.5:=0; // Drop RC5 line to put RS-485 back into receive mode

  PIR1.5:=0;  // clr any spurious rx interrupts
  PIE1.5:=1;  // enable receive interrupt
end;
main:
'   Main program 
end.
This is my MikroBasic Code:

Code: Select all

program Modio

include all_digital
' This is my attempt at Modbus RTU. This was designed to be used with Mach3.

symbol OPERATE_LED = PortA.1
 
' Dim RESULT as byte
'   dim i,j as short
'       acc,tmp as word'
'
'dim TX_Buffer as byte[8]
'dim RX_Buffer as byte[9]

'****** ALL PROGRAM VALUES ARE HARD-CODED FOR 10MHZ XTAL AND 9600 BAUD *********
 const MaxReg = 49      ' Number of registers (count from 0)
 const TxBufSize = 104  ' Max buffer size should include all registers + CRC + overhead
 const RxBufSize = 20   ' RX Buffer size does not really matter as we are only interested in first 7 chars constituting a MB command
 const DefMBAddress = 5 ' Default MODBUS Address if system is uninitialised
 const BaudRate = 9600  ' Baud rate (9600 or 19200)
 const Parity = "N"     ' Parity (N, E, O, M or S) { just for transmit, we ignore received Parity errors as we use CRC }
                        '{ Note that the Parity on tranmit is not yet working/tested so use 'N' }
Dim BufPtr, MBaddr, MBFrameTimeout, I as byte
    CRC as word                             ' calculated 16 bit CRC word
    ReadOnlyMode as boolean
    RxBuf as string[RxBufSize]              ' Array where USART Chars are received (in interrupt)
    TxBuf as string[TxBufSize]              ' Array from which USART response chars are tranmitted
    MBregister as word[0..MaxReg]           ' MODBUS REGISTERS in RAM (See description above)
    HexStr as string[2]                     ' Used for LCD Display (Dump in production version)
    
sub procedure interrupt

  if PIR1.5 = 1 then                        ' USART has received a char to process
    OPERATE_LED = false                     ' OPERATE LED OFF (to user it will appear to flash off as data is rxd)
    rxbuf[bufptr] = RCREG                   ' read the received data from USART 1 into cmd buffer
    if bufptr < RxBufSize then
       inc(bufptr)                          ' only interested in first few chars containing command others can be biffed
    end if
    if (RCSTA.1 = 0) AND (RCSTA.2 = 0) then ' log/clear errors. We use CRC for error checking so we basically ignore these errors
      RCSTA.CREN = 0                        ' clear the error
      RCSTA.CREN = 1                        ' enable receiving again
    end if
    MBFrameTimeout = 0                      ' clear down frame timeout TIMER0 on char reception (>3.5 chars without a char is a frame timeout)
    PIR1.5 = 0                              ' ack the interrupt
  end if

  if INTCON.T0IF = 1 then                   ' timer 0 has overflowed (1.6384mS) this is the approx length of a character at 9600 baud
    if MBFrameTimeout < 200 then
      inc(MBFrameTimeout)
    end if                                  ' increment the counter (main loop actually does timeout)
    INTCON.T0IF = 0                         ' ack interrupt until next time
  end if

end sub
'
Sub procedure resetrxbuf       ' Clears receive variables on completion of good modbus frame or period of no comms activity
  OPERATE_LED = 1              ' OPERATE LED ON
  bufptr = 1                   ' init bufptr back to first position in array
  MBFrameTimeout = 0           ' clr down any USART Receive timeouts
end sub


sub procedure InitPic  ' // initialisation of PIC registers, timers, USART and Interrupts

 ' MEMCON.EBDIS = 1   '  // BIGPIC Development system (Delete line for 18F2520)

  ADCON0 = 0        '// Turn of ADC
 ' CMCON  = 0x07    ' // turn off comparators
  ADCON1 = 0x04     '// turn off analog inputs

  TRISB = $FF  '// used for input
  TRISC = 0xdf ' // bits 7 and 8 used by USART, bit 5 used to control RS-485 chip tri-state
  PORTC = 0
  TRISC = $00
  PortA = $00   ' Output
  TrisA = $00
  PortD = $00
  TrisD = $00

  UART1_init(9600)'  // initialize USART (Takes out PORTC bits 7 & 6 !!)
  RCSTA.0 =1 '           // 9 bit mode enabled all the time
  TXSTA.6 =1 '

  T0con = 0xc3'   // Enable 8 bit timer0  interrupt occurs every 4x256x16/Fosc Seconds (1.6384mS @ 10Mhz)

  RCON = $80'     // ENABLE priority Interrupts - INT0/INT1 (TMP05 timing) high priority, USART1/TIMER0 (comms) low priority

  PIE1 = 0x20 '  // enable USART1 interrupt (we do not enable TIMER1 interrupt we will just poll it for overflows)

  IPR1 =0 ' // All peripheral interrupts are low priority (just in case)
  IPR2 = 0

  INTCON2 = 0x48' // PortB pullups disabled, INT0 rising edge, INT1 trailing edge, TIMER0 low priority    01001000

  INTCON3.6  = 1'  // INT1 High priority

  INTCON  = 0xe0   '// enable timer0, INT0 and Peripheral interrupts and TURN ON GLOBAL INTERRUPTS
'
'  '// INT0 and INT1 enabled seperately by tmp05 routines and interupts when required

end sub

sub procedure ClearCounters  ' // MB Statistics counters and other counters
  Dim i as byte

    for i = 40 to MaxReg
      MBregister[i] = 0
    next i

end sub

sub procedure _Hex(Dim _input as byte)
' // converts input to Hex and writes to HexStr variable for display on LCD
' (Dump in production version)

  hexstr[0] = (_input div 16)+48
  if hexstr[0] > 57 then
     hexstr[0] = hexstr[0]+7     ' // a..f
  end if
  hexstr[1] = (_input mod 16)+48
  if hexstr[1] > 57 then
     hexstr[1] = hexstr[1]+7
  end if
  HexStr[2] = 32

end sub

sub procedure InitProg  '; // initialisation of program variables
 Dim i as byte
   for i = 0 to MaxReg   '// init the registers
      MBregister[i] = 0xffff
   Next I

  ResetRxBuf
  ClearCounters
  ReadOnlyMode = false  '// Enable transmit (cmd 0x08 sub-fn 0x04 can set this flag and stop transmit working)

'                       // read our modbus address stored in non-volatile EEPROM or set default
   MBaddr = EEProm_Read(0)
  if MBaddr = 0xff then
     MBaddr = DefMBAddress
  end if
end sub

Sub procedure MB_Send(dim ln as byte)    ' Send out len chars in the TxBuf and then send a CRC
  Dim  n,i as integer
  
  if ln > TxBufSize then         ' (this will not happen but we check anyway!) clip the transmitted message if it overruns buffer and light ERROR LED
     ln = TxBufSize
     MBregister[40].5 = 1         ' flag error
  end if

  if RxBuf[1] = 0 then            ' Broadcast so dont respond
     exit
  end if
  if readOnlyMode then            ' readonly mode so dont respond
     exit
  end if

  PIE1.5 = 0                      ' turn off receive interrupt during tranmission

  PORTC.5 = 1                     ' PULL RC5 High to initiate RS-485 transmission mode and wait for settle
  delay_ms(2)                     ' 1 will work just as good but dont set it lower than 1

  crc = 0xFFFF
  for I = 1 to ln                 'tx each char of buffer and update the CRC as you go
      crc = crc xor word(TxBuf[i])
      for n = 1 to 8
          if (crc and 0x0001) <> 0 then
             CRC = (crc >> 1) xor 0xA001
          else
             CRC = crc >> 1
          end if
      next n
    UART1_write(TxBuf[i])       ' write the response byte by byte
  Next I

  UART1_Write(lo(crc))          ' // write crc LSB
  UART1_Write(hi(crc))          ' // write crc MSB

  delay_ms(2)                   ' settle time as above
  PORTC.5 = 0                   ' Drop RC5 line to put RS-485 back into receive mode

  PIR1.5 = 0                    ' clr any spurious rx interrupts
  PIE1.5 = 1                    ' enable receive interrupt
end sub

sub procedure MB_SendExceptionCode(Dim cde as byte)

  dec(MBRegister[44])             ' dec number of preocessed mesages
  inc(MBRegister[43])             ' inc number of MB exceptions
  if MBregister[43] = 0 then
     MBregister[40].8 = 1
  end if
  TxBuf[1] = RxBuf[2] OR 0x80     ' set MSB of function requested to indicate error
  TxBuf[2] = cde                  ' return error code
  MB_Send(2)                      ' send error message
  
end sub

sub procedure MB_Function0x04    'Function 0x04 read multiple registers
  Dim
     i,n,Rs,Rc as byte
  '{   Remember: RxBuf[]   [1]=addr, [2]=cmd, [3]=AddrHi, [4]=AddrLo, [5]=CountHi, [6]=CountLo  }

  if (RxBuf[3]<>0) or (RxBuf[4]>MaxReg) then  'Starting address valid range is 0 to MaxReg
      MB_SendExceptionCode(0x02)              ' invalid starting address
      exit
  end if

  Rs = RxBuf[4]                               ' Register start address ok in range = 0..MaxReg

  if (RxBuf[5]<>0) or (RxBuf[6]=0) or (RxBuf[6]>MaxReg) then ' too few/many registers
      MB_SendExceptionCode(0x03)              ' too many/too few registers
      exit
  end if

  Rc = RxBuf[6]                               ' register count

  if Rs+Rc>MaxReg then                        ' quantity + start is not ok
     MB_SendExceptionCode(0x02)
     exit
  end if

'  {
'    User has requested a valid start reg and count so now load registers
'    all ok so send user what they requested
'  }

'//  TxBuf[1]:=RxBuf[1]; // Note First bit in winetec prototype is slave address but this is modbus over tcp not  modbus over serial standard

  TxBuf[1] = RxBuf[2]    ' First bit of response transmission Set to 0x03 or 0x04 depending upon what called it
  TxBuf[2] = Rc*2        ' Second byte is byte count

  n = 3                  ' initialise index to TxBuf at 3rd char

  For I = 0 to Rc - 1
    TxBuf[n] = hi(MBregister[rs+i])   ' Send hi byte first
    inc(n)
    TxBuf[n] = lo(MBregister[rs+i])   ' send lo byte next
    inc(n)
  Next I

  MB_Send(n-1)                       ' Send txbuffer

end sub

Sub procedure MB_Function0x06        ' Write descrete register
'  { Command = 0x06
'    [1]=addr, [2]=cmd, [3]=AddrHi, [4]=AddrLo, [5]=ValueHi, [6]=ValueLo
'    Allows MODBUS Master to write to PIC EEPROM The following registers are supported
'    0x0100 = Modbus address (Value 0x0001 to 0x00fe)
'
'    * Note we can map any MODBUS address to any register or variable in ram or eeprom you will probably want to modify this but the principle is really simple
'  }

  if (RxBuf[3]=1) and (RxBuf[4]=0) and (RxBuf[5]=0) and (RxBuf[6]<>0) and (RxBuf[6]<>0xff) then ' write register 0x0100
    ' write new modbus address value to eeprom
          EEProm_write(0,RxBuf[6])
          ' return OK to master
    TxBuf[1] = RxBuf[2]
    TxBuf[2] = RxBuf[3]
    TxBuf[3] = RxBuf[4]
    TxBuf[4] = RxBuf[5]
    TxBuf[5] = RxBuf[6]
    MB_Send(5)
    ' may also want to issue a RESET command here or change the MBaddr variable to take effect, my customer does not want this
    exit
  end if

  if (RxBuf[3]=1) and (RxBuf[4]=0) then ' addrok so value wrong
    MB_SendExceptionCode(0x03)          ' value not supported
  else
    MB_SendExceptionCode(0x02)          ' address not supported
  end if
end sub

Sub procedure MB_Function0x08           ' Returns MB diagnostic functions from command 0x08
    if RxBuf[3]<>0 then
      MB_SendExceptionCode(0x03)          ' Data value not supported (command not supported)
      exit
    end if

   Select case RxBuf[4]
       Case 0x01                          ' RETURN OK AND SOFT RESET THE DEVICE
             TxBuf[1] = 0x08
             MB_Send(1)
             Reset
      Case 0x04                           ' FORCE LISTEN ONLY NO RETURN
             ReadOnlyMode = true
      Case 0x0a                           ' RETURN OK AND Clear the counters
             TxBuf[1] = 0x08
             MB_Send(1)
             ClearCounters
      Case 0x0b                          ' RETURN MB MessageCount
             TxBuf[1] = 0x08
             TxBuf[2] = hi(MBregister[41])
             TxBuf[3] = lo(MBregister[41])
             MB_Send(3)
      Case 0x0c                          ' RETURN MB CRC ERror count
             TxBuf[1] = 0x08
             TxBuf[2] = hi(MBregister[42])
             TxBuf[3] = lo(MBregister[43])
             MB_Send(3)
      Case 0x0d                          ' RETURN MB Exception count
             TxBuf[1] = 0x08
             TxBuf[2] = hi(MBregister[43])
             TxBuf[3] = lo(MBregister[43])
             MB_Send(3)
      Case 0x0e                         ' RETURN MB Slave message count
             TxBuf[1] = 0x08
             TxBuf[2] = hi(MBregister[44])
             TxBuf[3] = lo(MBregister[44])
             MB_Send(3)

      Case else
            MB_SendExceptionCode(0x03) ' MB data value not supported (command not supported)
   end select

end Sub

sub function MB_CRC_OK as boolean     ' check the crc of the rx buffer. lo=[BufPtr-1] and hi=[BufPtr]
  Dim
     n,i as byte
  result = false
  crc = 0xFFFF
  for I = 1 to bufptr-2                ' dont incl checksum bits in check (obviously!)
    crc  = crc xor word(RxBuf[i])
    for n = 1 to 8
      if (crc and 0x0001)<>0 then
          CRC = (crc >> 1) xor 0xA001
      else
          CRC = crc >> 1
      end if
    Next N
  Next I
  if (rxbuf[BufPtr-1] = lo(crc)) and (rxbuf[BufPtr] = hi(crc)) then 
      result = true                  ' return good
  end if
end sub


' For development kit with LCD display then include this to show first 8 bytes of received message (really helps!!)
Sub procedure RxDebug
  Dim
     I as byte
  for I = 1 to 8
    Hex(RxBuf[i])
    Lcd_Out(2, i*2-1, HexStr)
  Next I
end Sub


Sub procedure MB_DecodeFrame  ' decode the mb command in rxbuf

  dec(bufPtr)                 ' Adjust BufPtr down 1 to reflect the no of chars actually received

'  { Minimum frame size is 5 but maximum frame size can be upto 255. For commands we support the largest frame will be 7
'
'    MB FRAME = [1] = Slave Address
'               [2] = Command
'               [3]...[BufPtr-2] = MESSAGE
'               [BufPtr-1]=CRC HI byte
'               [BufPtr]=CRC LO byte
'  }

  RxDebug                   ' Show message in HEX on LCD screen can take out in production version when we do not have an LCD!!!!!!!!!

  if MB_CRC_OK then         ' good CRC so command is valid
    inc(MBRegister[41])     ' inc MB 'all messages' counter
    if MBregister[41] = 0 then
       MBregister[40].6 = 1
    end if
    if (rxbuf[1] = MBaddr) or (rxbuf[1] = 0) then ' command was addressed to this device or is a broadcast (if broadcast we dont respond)
       inc(MBRegister[44])   ' // inc counter to say message was processed, we may decement it again if exception occurs
       if MBregister[44]=0 then
         MBregister[40].9 = 1
       end if

       select case rxbuf[2]
           Case 0x04
                MB_Function0x04
           Case 0x06
                MB_Function0x06
           Case 0x08
                MB_function0x08
'        { Other modbus commands just add here if required }
          Case else
               MB_SendExceptionCode(0x01)' // Modbus command NOT supported
       end select

    end if
 ' end
  else ' MB bad crc
    inc(MBRegister[42])
    if MBregister[42] = 0 then 
       MBregister[40].7 = 1             ' // Overflow error
    end if
  end if

  ResetRxBuf                            ' reset rxbuffer for next message

main:
  '// ######################    Start of main prog    #############################
  InitPic
  InitProg

  while true ' //loop forever
    OPERATE_LED = True ' show operate LED

''    { Do some work here writing your data to the modbus registers.. whatever, anything... }
'
'    MBregister[2] = 1234   ' for example i set register 2 to 1234
'
''    { End of work }

    if (bufptr > 4) and (MBFrameTimeout > 5) then ' received a MODBUS frame (probably)
      MB_DecodeFrame
    end if
    if MBFrameTimeout > 199 then                ' after 300mS without comms its also safe to reset
      resetrxbuf
    end if

  wend

end.
Please help.

Rick

ICYE
Posts: 51
Joined: 02 Aug 2006 16:47

Re: Simple MODBUS RTU Slave

#6 Post by ICYE » 15 Jul 2011 09:20

Hi all!

I made small modification at previous code and it works with RS232 as I expected . However, when I try with RS485 (to use a rs232-485 converter) I've been received wrong response. Could someone check and test this code for me?
Best Regards,


EasyPic6- PIC18f452- 8Mhz

Code: Select all

program rtu


symbol OPERATE_LED = PortA.1


'****** ALL PROGRAM VALUES ARE HARD-CODED FOR 8MHZ XTAL AND 9600 BAUD *********
const MaxReg = 49      ' Number of registers (count from 0)
const TxBufSize = 104  ' Max buffer size should include all registers + CRC + overhead
const RxBufSize = 20   ' RX Buffer size does not really matter as we are only interested in first 7 chars constituting a MB command
const DefMBAddress = 0x05 ' Default MODBUS Address if system is uninitialised
const BaudRate = 9600  ' Baud rate (9600 or 19200)
const Parity = "N"     ' Parity (N, E, O, M or S) { just for transmit, we ignore received Parity errors as we use CRC }
                        '{ Note that the Parity on tranmit is not yet working/tested so use 'N' }
Dim BufPtr, MBaddr, MBFrameTimeout, I as byte
    CRC as word                             ' calculated 16 bit CRC word
    ReadOnlyMode as boolean
    RxBuf as string[RxBufSize]              ' Array where USART Chars are received (in interrupt)
    TxBuf as string[TxBufSize]              ' Array from which USART response chars are tranmitted
    MBregister as word[0..MaxReg]           ' MODBUS REGISTERS in RAM (See description above)
    HexStr as string[2]                     ' Used for LCD Display (Dump in production version)

dim
  LCD_RS as sbit at RB4_bit
  LCD_EN as sbit at RB5_bit
  LCD_D7 as sbit at RB3_bit
  LCD_D6 as sbit at RB2_bit
  LCD_D5 as sbit at RB1_bit
  LCD_D4 as sbit at RB0_bit

dim
  LCD_RS_Direction as sbit at TRISB4_bit
  LCD_EN_Direction as sbit at TRISB5_bit
  LCD_D7_Direction as sbit at TRISB3_bit
  LCD_D6_Direction as sbit at TRISB2_bit
  LCD_D5_Direction as sbit at TRISB1_bit
  LCD_D4_Direction as sbit at TRISB0_bit
sub procedure interrupt

  if PIR1.5 = 1 then                        ' USART has received a char to process
    OPERATE_LED = false                     ' OPERATE LED OFF (to user it will appear to flash off as data is rxd)
    rxbuf[bufptr] = RCREG                   ' read the received data from USART 1 into cmd buffer
    if bufptr < RxBufSize then
       inc(bufptr)                          ' only interested in first few chars containing command others can be biffed
    end if
    if (RCSTA.1 = 0) AND (RCSTA.2 = 0) then ' log/clear errors. We use CRC for error checking so we basically ignore these errors
      RCSTA.CREN = 0                        ' clear the error
      RCSTA.CREN = 1                        ' enable receiving again
    end if
    MBFrameTimeout = 0                      ' clear down frame timeout TIMER0 on char reception (>3.5 chars without a char is a frame timeout)
    PIR1.5 = 0                              ' ack the interrupt
  end if

  if INTCON.T0IF = 1 then                   ' timer 0 has overflowed (1.6384mS) this is the approx length of a character at 9600 baud
    if MBFrameTimeout < 160 then
      inc(MBFrameTimeout)
    end if                                  ' increment the counter (main loop actually does timeout)
    INTCON.T0IF = 0                         ' ack interrupt until next time
  end if

end sub
'

Sub procedure resetrxbuf       ' Clears receive variables on completion of good modbus frame or period of no comms activity
  OPERATE_LED = 1              ' OPERATE LED ON
  bufptr = 1                   ' init bufptr back to first position in array
  MBFrameTimeout = 0           ' clr down any USART Receive timeouts
end sub


sub procedure InitPic  ' // initialisation of PIC registers, timers, USART and Interrupts

    Lcd_Init()
LCD_CMD(_LCD_CLEAR)

' MEMCON.EBDIS = 1   '  // BIGPIC Development system (Delete line for 18F2520)

  ADCON0 = 0        '// Turn of ADC
' CMCON  = 0x07    ' // turn off comparators
  ADCON1 = $82      '// turn off analog inputs

  TRISD = $FF  '// used for input
  TRISC = 0xdf ' // bits 7 and 8 used by USART, bit 5 used to control RS-485 chip tri-state
  PORTC = 0
  TRISC = $00
  PortA = $00   ' Output
  TrisA = $00
  PortB = $00
  TrisB = $00

  UART1_Init(9600)'  // initialize USART (Takes out PORTC bits 7 & 6 !!)
  RCSTA.0 = 1 '           // 9 bit mode enabled all the time

  TXSTA.6 = 1 '

  T0con = 0xc3'   // Enable 8 bit timer0  interrupt occurs every 4x256x16/Fosc Seconds (2.048mS @ 8Mhz)

  RCON = $80'     // ENABLE priority Interrupts - INT0/INT1 (TMP05 timing) high priority, USART1/TIMER0 (comms) low priority

  PIE1 = 0x20 '  // enable USART1 interrupt (we do not enable TIMER1 interrupt we will just poll it for overflows)

  IPR1 =0 ' // All peripheral interrupts are low priority (just in case)
  IPR2 = 0

  'INTCON2 = 0x84' // PortB pullups disabled, INT0 rising edge, INT1 trailing edge, TIMER0 low priority    01001000

  INTCON3.6  = 1'  // INT1 High priority

  INTCON  = 0xe0   '// enable timer0, INT0 and Peripheral interrupts and TURN ON GLOBAL INTERRUPTS
'
'  '// INT0 and INT1 enabled seperately by tmp05 routines and interupts when required

end sub

sub procedure ClearCounters  ' // MB Statistics counters and other counters
  Dim i as byte

    for i = 40 to MaxReg
      MBregister[i] = 0
    next i

end sub

sub procedure _Hex(Dim _input as byte)
' // converts input to Hex and writes to HexStr variable for display on LCD
' (Dump in production version)

  hexstr[0] = (_input div 16)+48
  if hexstr[0] > 57 then
     hexstr[0] = hexstr[0]+7     ' // a..f
  end if
  hexstr[1] = (_input mod 16)+48
  if hexstr[1] > 57 then
     hexstr[1] = hexstr[1]+7
  end if
  HexStr[2] = 32

end sub

sub procedure InitProg  '; // initialisation of program variables

Dim i as byte
   for i = 0 to MaxReg   '// init the registers
      MBregister[i] = 0xffff
   Next I
  '
    EEPROM_Write(0, 0x05)
  ResetRxBuf
  ClearCounters
  ReadOnlyMode = false  '// Enable transmit (cmd 0x08 sub-fn 0x04 can set this flag and stop transmit working)

'                       // read our modbus address stored in non-volatile EEPROM or set default

   MBaddr = EEProm_Read(0)
  if MBaddr = 0xff then
  MBaddr =  DefMBAddress
  end if

end sub

Sub procedure MB_Send(dim ln as byte)    ' Send out len chars in the TxBuf and then send a CRC
  Dim  n,i as integer

  if ln > TxBufSize then         ' (this will not happen but we check anyway!) clip the transmitted message if it overruns buffer and light ERROR LED
     ln = TxBufSize
     MBregister[40].5 = 1         ' flag error
  end if

  if RxBuf[1] = 0 then            ' Broadcast so dont respond
     exit
  end if
  if readOnlyMode then            ' readonly mode so dont respond
     exit
  end if

  PIE1.5 = 0                      ' turn off receive interrupt during tranmission

  PORTC.5 = 1                     ' PULL RC5 High to initiate RS-485 transmission mode and wait for settle
  delay_ms(2)                     ' 1 will work just as good but dont set it lower than 1

  crc = 0xFFFF
  for i = 1 to ln                 'tx each char of buffer and update the CRC as you go
      crc = crc xor word(TxBuf[i])
      for n = 1 to 8
          if (crc and 0x0001) <> 0 then
             CRC = (crc >> 1) xor 0xA001
          else
             CRC = crc >> 1
          end if
      next n

    UART1_write(TxBuf[i])       ' write the response byte by byte

  Next i

  UART1_Write(lo(crc))          ' // write crc LSB


  UART1_Write(hi(crc))          ' // write crc MSB


  delay_ms(3)                   ' settle time as above
  PORTC.5 = 0                   ' Drop RC5 line to put RS-485 back into receive mode

  PIR1.5 = 0                    ' clr any spurious rx interrupts
  PIE1.5 = 1                    ' enable receive interrupt

end sub

sub procedure MB_SendExceptionCode(Dim cde as byte)

  dec(MBRegister[44])             ' dec number of preocessed mesages
  inc(MBRegister[43])             ' inc number of MB exceptions
  if MBregister[43] = 0 then
     MBregister[40].8 = 1
  end if
  TxBuf[1] = RxBuf[2] OR 0x80     ' set MSB of function requested to indicate error
  TxBuf[2] = cde                  ' return error code
  MB_Send(2)                      ' send error message

end sub
             '*******************03 read holding register******************************
sub procedure MB_Function0x04    'Function 0x04 read multiple registers
  Dim
     i,n,Rs,Rc as byte
  '{   Remember: RxBuf[]   [1]=addr, [2]=cmd, [3]=AddrHi, [4]=AddrLo, [5]=CountHi, [6]=CountLo  }

  if (RxBuf[3]<>0) or (RxBuf[4]>MaxReg) then  'Starting address valid range is 0 to MaxReg
      MB_SendExceptionCode(0x02)              ' invalid starting address
      exit
  end if

  Rs = (RxBuf[4])                               ' Register start address ok in range = 0..MaxReg

  if (RxBuf[5]<>0) or (RxBuf[6]=0) or (RxBuf[6]>MaxReg) then ' too few/many registers
      MB_SendExceptionCode(0x03)              ' too many/too few registers
      exit
  end if

  Rc = RxBuf[6]                               ' register count

  if Rs+Rc>MaxReg then                        ' quantity + start is not ok
     MB_SendExceptionCode(0x02)
     exit
  end if

'  {
'    User has requested a valid start reg and count so now load registers
'    all ok so send user what they requested
'  }

'//  TxBuf[1]:=RxBuf[1]; // Note First bit in winetec prototype is slave address but this is modbus over tcp not  modbus over serial standard

  TxBuf[1] = RxBuf[1]    ' First bit of response transmission Set to 0x03 or 0x04 depending upon what called it
  TxBuf[2]=  RxBuf[2]
  TxBuf[3] = Rc*2      ' Second byte is byte count
  
  n = 4                  ' initialise index to TxBuf at 3rd char

  For i = 0 to Rc  - 1
    TxBuf[n] = hi(MBregister[rs+i])   ' Send hi byte first
    inc(n)
    TxBuf[n] = lo(MBregister[rs+i])   ' send lo byte next
    inc(n)
  Next i

  MB_Send(n-1)                       ' Send txbuffer

end sub

Sub procedure MB_Function0x06        ' Write descrete register
'  { Command = 0x06
'    [1]=addr, [2]=cmd, [3]=AddrHi, [4]=AddrLo, [5]=ValueHi, [6]=ValueLo
'    Allows MODBUS Master to write to PIC EEPROM The following registers are supported
'    0x0100 = Modbus address (Value 0x0001 to 0x00fe)
'
'    * Note we can map any MODBUS address to any register or variable in ram or eeprom you will probably want to modify this but the principle is really simple
'  }

  if (RxBuf[3]=1) and (RxBuf[4]=0) and (RxBuf[5]=0) and (RxBuf[6]<>0) and (RxBuf[6]<>0xff) then ' write register 0x0100
    ' write new modbus address value to eeprom
          Eeprom_write(0,RxBuf[6])
          ' return OK to master  The normal response is an echo of the query, returned after the register contents have been written.
    TxBuf[1] = RxBuf[1]
    TxBuf[2] = RxBuf[2]
    TxBuf[3] = RxBuf[3]
    TxBuf[4] = RxBuf[4]
    TxBuf[5] = RxBuf[5]
    TxBuf[6] = RxBuf[6]
    MB_Send(6)  'lenght of the data for CRC
    ' may also want to issue a RESET command here or change the MBaddr variable to take effect, my customer does not want this
    exit
  end if

  if (RxBuf[3]=1) and (RxBuf[4]=0) then ' addrok so value wrong
    MB_SendExceptionCode(0x03)          ' value not supported
  else
    MB_SendExceptionCode(0x02)          ' address not supported
  end if
end sub

Sub procedure MB_Function0x08           ' Returns MB diagnostic functions from command 0x08
    if RxBuf[3]<>0 then
      MB_SendExceptionCode(0x03)          ' Data value not supported (command not supported)
      exit
    end if

   Select case RxBuf[4]
       Case 0x01                          ' RETURN OK AND SOFT RESET THE DEVICE
             TxBuf[1] = 0x08
             MB_Send(1)
             Reset
      Case 0x04                           ' FORCE LISTEN ONLY NO RETURN
             ReadOnlyMode = true
      Case 0x0a                           ' RETURN OK AND Clear the counters
             TxBuf[1] = 0x08
             MB_Send(1)
             ClearCounters
      Case 0x0b                          ' RETURN MB MessageCount
             TxBuf[1] = 0x08
             TxBuf[2] = hi(MBregister[41])
             TxBuf[3] = lo(MBregister[41])
             MB_Send(3)
      Case 0x0c                          ' RETURN MB CRC ERror count
             TxBuf[1] = 0x08
             TxBuf[2] = hi(MBregister[42])
             TxBuf[3] = lo(MBregister[42])
             MB_Send(3)
      Case 0x0d                          ' RETURN MB Exception count
             TxBuf[1] = 0x08
             TxBuf[2] = hi(MBregister[43])
             TxBuf[3] = lo(MBregister[43])
             MB_Send(3)
      Case 0x0e                         ' RETURN MB Slave message count
             TxBuf[1] = 0x08
             TxBuf[2] = hi(MBregister[44])
             TxBuf[3] = lo(MBregister[44])
             MB_Send(3)

      Case else
            MB_SendExceptionCode(0x03) ' MB data value not supported (command not supported)
   end select

end Sub

sub function MB_CRC_OK as boolean     ' check the crc of the rx buffer. lo=[BufPtr-1] and hi=[BufPtr]
  Dim
     n,i as byte
  result = false
  crc = 0xFFFF
  for I = 1 to bufptr-2                ' dont incl checksum bits in check (obviously!)
    crc  = crc xor word(RxBuf[i])
    for n = 1 to 8
      if (crc and 0x0001)<>0 then
          CRC = (crc >> 1) xor 0xA001
      else
          CRC = crc >> 1
      end if
    Next N
  Next I
  if (rxbuf[BufPtr-1] = lo(crc)) and (rxbuf[BufPtr] = hi(crc)) then
      result = true                  ' return good
  end if
end sub


' For development kit with LCD display then include this to show first 8 bytes of received message (really helps!!)
Sub procedure RxDebug
  Dim
     I as byte
  for I = 1 to 8
    _Hex(RxBuf[i])
    Lcd_Out(2, i*2-1, HexStr)
  Next I
end Sub





Sub procedure MB_DecodeFrame  ' decode the mb command in rxbuf

  dec(bufPtr)                 ' Adjust BufPtr down 1 to reflect the no of chars actually received

'  { Minimum frame size is 5 but maximum frame size can be upto 255. For commands we support the largest frame will be 7
'
'    MB FRAME = [1] = Slave Address
'               [2] = Command
'               [3]...[BufPtr-2] = MESSAGE
'               [BufPtr-1]=CRC HI byte
'               [BufPtr]=CRC LO byte
'  }

  RxDebug                   ' Show message in HEX on LCD screen can take out in production version when we do not have an LCD!!!!!!!!!

  if MB_CRC_OK then         ' good CRC so command is valid
    inc(MBRegister[41])     ' inc MB 'all messages' counter
    if MBregister[41] = 0 then
       MBregister[40].6 = 1
    end if
    if (rxbuf[1] = MBaddr) or (rxbuf[1] = 0) then ' command was addressed to this device or is a broadcast (if broadcast we dont respond)
       inc(MBRegister[44])   ' // inc counter to say message was processed, we may decement it again if exception occurs
       if MBregister[44]=0 then
         MBregister[40].9 = 1
       end if

       select case rxbuf[2]
           Case 0x04
               MB_Function0x04
       Case 0x06
                MB_Function0x06
           Case 0x08
                MB_function0x08
'        { Other modbus commands just add here if required }
          Case else
               MB_SendExceptionCode(0x01)' // Modbus command NOT supported
       end select

    end if
' end
  else ' MB bad crc
    inc(MBRegister[42])
    if MBregister[42] = 0 then
       MBregister[40].7 = 1             ' // Overflow error
    end if
  end if

  ResetRxBuf                            ' reset rxbuffer for next message
   end sub

main:
  '// ######################    Start of main prog    #############################
  InitPic
  InitProg

  while true ' //loop forever
    OPERATE_LED = True ' show operate LED

''    { Do some work here writing your data to the modbus registers.. whatever, anything... }
'
    MBregister[0] = 1234
    MBregister[1] = 1965
    MBregister[2] = 1907
    MBregister[3] = 2008
    MBregister[4] = 14
    MBregister[5] = 7
    MBregister[6] = 2011
    
    
    
''    { End of work }

    if (bufptr > 4) and (MBFrameTimeout > 5) then ' received a MODBUS frame (probably)
      MB_DecodeFrame
    end if
    if MBFrameTimeout > 300 then                ' after 300mS without comms its also safe to reset
      resetrxbuf
    end if

  wend

end.

NuMcA
Posts: 37
Joined: 24 Dec 2009 17:08
Location: Athens, Greece
Contact:

Re: Simple MODBUS RTU Slave

#7 Post by NuMcA » 12 Dec 2014 11:41

Great idea guys! I will try it and come back with questions and maybe answers! :)

Can anyone confirm that the MikroBasic code works? Is there a link for the Project to download?
Thank you!
My simulation cockpit and other projects: www.numca.grImage

Post Reply

Return to “mikroPascal General”