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
Simple MODBUS RTU Slave
Simple MODBUS RTU Slave
www.boznz.com
Simple solutions to complex problems
Simple solutions to complex problems
Thank You
Thank You
Bye Julio Antolin
Bye Julio Antolin
Re: Simple MODBUS RTU Slave
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
This is my MikroBasic Code:
Please help.
Rick
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.
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.
Rick
Re: Simple MODBUS RTU Slave
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
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.
Re: Simple MODBUS RTU Slave
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!
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.gr