PDA

View Full Version : Modbus RTU Slave for 16F876A or 18F2620



emmeci85
- 27th December 2012, 17:32
Hi! Recently I've built a modbus RTU slave program for PIC.
For my project I've managed only modbus function 03, 06, 16 and modbus exception. Also I've used Darrel Taylor interrupts for RX,TX, TMR0 and TMR2

A simple one second clock on PORTC.0 give me the assurance that the interrupts works fine.

any advice on how to improve or simplify it? Thanks

PS. Comments are in Italian, if necessary I translate them



'************************************************* ***************
'* Name : MODBUS_SALVE.BAS *
'* Author : CALLEGHER MARCO *
'* Notice : Copyright (c) 2012 CALLEGHER MARCO *
'* : All Rights Reserved *
'* Date : 10/12/2012 *
'* Version : 1.0 *
'* Notes : *
'* : *
'************************************************* ***************


'Definizioni di Versione e Data
'Salvati su EEPROM interna
VERSION con 1 'Versione: Indirizzo EEPROM 00h
GIORNO con 14 'Giorno: Indirizzo EEPROM 01h
MESE con 12 'Mese: Indirizzo EEPROM 02h
ANNO con 12 'Anno: Indirizzo EEPROM 03h

DATA @0, VERSION, GIORNO, MESE, ANNO,2,1 'Scrive nella eeprom interna la versione e la data del programma
'indirizzo modbus di default e i baud seriali di default


SLAVE_ID VAR BYTE 'Indirizzo Modbus: EEPROM 04h
BAUDRATE VAR BYTE 'Baudrate seriale: EEPROM 05h





'****** Definizioni ******


define LOADER_USED 1 ' riserva spazio di memoria per il loader
define OSC 20 ' setta clock a 20MHz


read $5,BAUDRATE 'legge baudrate seriale default 19200 baud

select case BAUDRATE 'Imposta velocità della porta in base a quello che c'è in eeprom
case 1 'default
SPBRG = 64 ' 19200 @20MHz
case else
SPBRG = 129 ' 9600 @20MHz
end select

RCSTA = $90 'imposta i registri per la comunicazione seriale
TXSTA = $24


define HSER_CLROERR 1 ' auto-clear over-run flag



'****** Variabili ****************
REGISTRI_MEMORIA CON 20 'quantità di memoria holding register(word)
LENGHT_SER_BUF CON 30 'dimensione byte buffer

MEMORIA VAR WORD [REGISTRI_MEMORIA] 'memoria holding register
SER_BUF VAR BYTE [LENGHT_SER_BUF] 'buffer seriale
lenght var byte 'lunghezza dati ricevuti/da spedire
w0 var word 'var temp
qnt var word 'qnt dati da trasmettere
MODBUS_ERR VAR BYTE 'riporta codice errore modbus
CRC16 var word 'word crc16
i var byte 'contatore
j var byte 'contatore
RISPONDI var bit 'abilita riposta (tx)
RX_BUF_FULL VAR BIT 'superato limite byte buffer
MB_READY VAR BIT 'dati seriali ricevuti e pronti
Tmr2Tick VAR BYTE 'var per timer 2

R_clk_1s VAR BIT 'clock 1 secondo


'****** Temporizzatori ******


' Tempo ON Clock lento
T_ON_clk1s VAR WORD
T_ON_clk1s_Start VAR BIT
T_ON_clk1s_Out VAR BIT
T_ON_clk1s_Timer con 100 '1 secondo


' Tempo OFF Clock lento
T_OFF_clk1s VAR WORD
T_OFF_clk1s_Start VAR BIT
T_OFF_clk1s_Out VAR BIT
T_OFF_clk1s_Timer con 100 '1 secondo




'variables for saving state in interrupt handlers:
wsave var byte $70 System
wsave1 var byte $A0 System
wsave2 var byte $120 System
wsave3 var byte $1A0 System


CLEAR 'porta a 0 tutte le variabili


'legge in numero di stazione modbus
read $4,SLAVE_ID 'legge Id modbus default slave id=2


'INCLUDE "DT_INTS-18.bas" ' Base Interrupt System
'INCLUDE "ReEnterPBP-18.bas" ' Include if using PBP interrupts
INCLUDE "DT_INTS-14.bas" ' Base Interrupt System
INCLUDE "ReEnterPBP.bas" ' Include if using PBP interrupts






'******* Settaggi per 16F876A *********************
ADCON0 = 0 ' usa i pin analogici come digitali nella PORTA
ADCON1 = $0F ' usa i pin analogici come digitali nella PORTA
CMCON = 7 ' disabilita i comparatori nella PORTA
TMR0 = 61 ' precarica 61 al registro del TMR0
TRISA = %00000000 ' setta uscite pin portA
TRISB = %11111111 ' setta ingressi pin portB
TRISC = %10000000 ' setta uscite pin portC

OPTION_REG = %11010111 ' Set TMR0 configuration
' Prescaler 1:256
' Overflow TMR0 = 9,9840 mS, precaricando 61 nel registro
' Disabilita resistenze pull-up

INTCON = %11100000 ' Enable TMR0 interrupts, genera l'interrupt ogni overflow del TMR0
PIE1.5 = 1
PIE1.1 = 1 ' Abilita interrupt on TMR2
T2CON.1 = 1 ' TMR2 prescaler 1:16
T2CON.2 = 1 ' Abilita Timer2
TMR2 = 0 ' TMR2 Holding register = 0




'****** Settaggi per 18F2620 ***********
'ADCON0 = 0 ' usa i pin analogici come digitali nella PORTA
'ADCON1 = $0F ' usa i pin analogici come digitali nella PORTA
'CMCON = 7 ' disabilita i comparatori nella PORTA
'TMR0L = 61 ' precarica 61 al registro del TMR0
'TRISA = %00000000 ' setta uscite pin portA
'TRISB = %11111111 ' setta ingressi pin portB
'TRISC = %10000000 ' setta uscite pin portC

'T0CON = %11000111 ' Set TMR0 configuration
' Prescaler 1:256
' Overflow TMR0 = 9,9840 mS, precaricando 61 nel registro

'INTCON = %10100000 ' Enable TMR0 interrupts, genera l'interrupt ogni overflow del TMR0
'INTCON2 = %10000100 ' Disabilita resistenze pull-up
'PIE1.5 = 1
'PIE1.1 = 1 ' Enable interrupt on TMR2
'T2CON.1 = 1 ' TMR2 prescaler 1:16
'T2CON.2 = 1 ' Enable Timer2
'TMR2 = 0 ' TMR2 Holding register = 0




'************ Inizializza interrupt ************************
;-----------------------------------------------------------------------------
ASM
INT_LIST macro ; IntSource, Label, Type, ResetFlag?
INT_Handler TMR0_INT, _TickInterrupt, PBP, yes
INT_Handler TMR2_INT, _Tmr2Interrupt, PBP, yes
INT_Handler RX_INT, _RicezioneSeriale, PBP, yes
INT_Handler TX_INT, _SendChar, PBP, yes
endm
INT_CREATE ; Creates the interrupt processor
ENDASM
PAUSE 1000


;-----------------------------------------------------------------------------
@ INT_ENABLE TMR0_INT ; enable Timer 0 interrupts
;-----------------------------------------------------------------------------





'****** Programma principale *********


MainProgram:
'clock lento (1 secondo)
if T_OFF_clk1s_Out = 0 then T_ON_clk1s_Start = 1
if T_ON_clk1s_Out = 1 then R_clk_1s = 1
if R_clk_1s = 1 then T_OFF_clk1s_Start = 1
if T_OFF_clk1s_Out = 1 then
R_clk_1s = 0
T_ON_clk1s_Start = 0
T_OFF_clk1s_Start = 0
endif



PORTC.0 = R_clk_1s



'abilita interrupt ricezione seriale se non è finita la ricezione
IF (MB_READY==0)THEN
@ INT_ENABLE RX_INT ; enable interrupt ricezione seriale
ENDIF

'abilita il controllo dei dati ricevuti
IF (MB_READY==1) AND (RISPONDI == 0) THEN ' se è presente un frame modbus e non si sta rispondendo ad una richiesta

if RX_BUF_FULL==1 THEN 'controlla se in ricezione c'è stato overflow del buffer
MODBUS_ERR=$04 'componi la risposta di errore. Errore 04
gosub Modbus_Exception
RISPONDI = 1
else

IF (SER_BUF[0]<>SLAVE_ID) Then 'controlle se la richiesta è indirizzata a questo dispositivo
MB_READY = 0 'non considera la richiesta
lenght = 0

ELSE

GoSub Calcul_CRC16 'calcolo crc della risposta ricevuta

IF (CRC16.LowByte<>SER_BUF[lenght-2]) OR (CRC16.HighByte<>SER_BUF[lenght-1]) Then 'controlla che il crc sia corretto
'CRC ERRATO, non considera i dati ricevuti
MB_READY=0
lenght = 0


ELSE
'CRC OK, SLAVE_ID OK, BUFF not OVERFLOW
SELECT CASE SER_BUF[1] ' seleziona la funzione modbus

CASE 3 'Lettura holding registers funz. h03

qnt=SER_BUF[4]*256 + SER_BUF[5] 'quantità di registri da leggere

if qnt*2 + 5 > LENGHT_SER_BUF then 'controlla che la quantità di registri da leggere non sia superiore alla dimensione del buffer seriale
MODBUS_ERR=$04 'componi la risposta di errore. Errore 04
gosub Modbus_Exception

else

if (SER_BUF[2]*256+SER_BUF[3]+qnt) > REGISTRI_MEMORIA THEN 'se la quantità di registri richiestra supera la memoria
MODBUS_ERR=$02 'componi la risposta di errore. Errore 02
gosub Modbus_Exception
ELSE
gosub Funz_03_Lettura 'componi la risposta di lettura
ENDIF

endif

RISPONDI = 1 'flag, ok invio risposta



CASE 6 'Scrittura holding register singolo. h06

if (SER_BUF[2]*256+SER_BUF[3]) > REGISTRI_MEMORIA THEN 'controlla che il registro sia un indirizzo della memoria valido
MODBUS_ERR=$02 'componi la risposta di errore. Errore 02
gosub Modbus_Exception
ELSE

gosub Funz_06_Scrittura 'componi la risposta di lettura
ENDIF

RISPONDI = 1

case $10 'Scrittura holding register multipli. (h16)

qnt=SER_BUF[4]*256 + SER_BUF[5] 'quantità di registri da leggere

if (SER_BUF[2]*256+SER_BUF[3]+qnt) > REGISTRI_MEMORIA THEN 'controlla che il registro sia un indirizzo della memoria valido
MODBUS_ERR=$02 'componi la risposta di errore. Errore 02
gosub Modbus_Exception
ELSE

gosub Funz_10_Scrittura 'componi la risposta di lettura
ENDIF

RISPONDI = 1


CASE ELSE 'Se non è una funzione gestita
MODBUS_ERR=$01 'componi la risposta di errore. Errore 01
gosub Modbus_Exception

RISPONDI = 1

END SELECT
endif
ENDIF
ENDIF
ENDIF




'Risponde se la porta seriale è libera e se è richiesta la risposta
'abilita l'interrupt
if TXSTA.1 == 1 and RISPONDI == 1 then
@ INT_ENABLE TX_INT ;- enable the TX interrupt
if (lenght == qnt) then
MB_READY=0
lenght=0
RISPONDI=0
RX_BUF_FULL=0
endif
endif





PAUSEus 100


goto MainProgram




'******** MODBUS FUNCTION **************


Modbus_Exception:
SER_BUF[1]=SER_BUF[1]+$80
SER_BUF[2]=MODBUS_ERR
lenght=5 'lunghezza totale risposta compreso crc e header

GoSub Calcul_CRC16 'calcolo crc16
SER_BUF[lenght-2] = CRC16.LowByte
SER_BUF[lenght-1] = CRC16.HighByte
qnt=0 'resetta variabile
RETURN




Funz_03_Lettura: 'funzione lettura holding register

lenght=qnt*2 'quantità di byte del frame di risposta

repeat
qnt=qnt-1 'compone la risposta con i registri
w0=MEMORIA[SER_BUF[2]*256+SER_BUF[3]+qnt]
SER_BUF[3+(qnt*2)]=w0.byte1
SER_BUF[4+(qnt*2)]=w0.byte0
until qnt=0

SER_BUF[2] = lenght 'viene scritto sul frame di risposta
lenght=lenght+5 'lunghezza totale risposta compreso crc e header

GoSub Calcul_CRC16 'calcolo crc16
SER_BUF[lenght-2] = CRC16.LowByte
SER_BUF[lenght-1] = CRC16.HighByte

return


Funz_06_Scrittura: 'scrive il byte ricevuto all'indirizzo di memoria
w0.lowbyte=SER_BUF[5]
w0.HIGHBYTE=SER_BUF[4]
MEMORIA[SER_BUF[2]*256+SER_BUF[3]]=W0
qnt=0 'resetta variabile
return


Funz_10_Scrittura: 'scrittura multipla registri
repeat
qnt=qnt-1
w0.lowbyte=SER_BUF[8+(qnt*2)]
w0.HIGHBYTE=SER_BUF[7+(qnt*2)]
MEMORIA[SER_BUF[2]*256+SER_BUF[3]+qnt]=W0
until qnt=0

lenght=8 'quantità di byte del frame di risposta compreso crc e header

GoSub Calcul_CRC16 'calcolo crc16
SER_BUF[lenght-2] = CRC16.LowByte
SER_BUF[lenght-1] = CRC16.HighByte

RETURN




Calcul_CRC16: 'calcolo CRC
CRC16=$FFFF
For i=0 to lenght-3
CRC16=CRC16^SER_BUF[i]
For j=1 to 8
IF CRC16.Bit0=1 Then
CRC16=$A001^(CRC16>>1)
Else
CRC16=CRC16>>1
EndIF
Next j
Next i


Return




'************************************************* *******************


'****** Interrupts Temporizzatori *****
'****** esegue l'interrupt ogni overflow di TMR0, cioè ogni 9,9840 mS




TickInterrupt:


'Timer clock 1 secondo ON


if T_ON_clk1s_Start = 1 then
if T_ON_clk1s < T_ON_clk1s_Timer then
T_ON_clk1s = T_ON_clk1s + 1
else
T_ON_clk1s_Out = 1
endif
else
T_ON_clk1s = 0
T_ON_clk1s_Out = 0
endif

'Timer clock 1 secondo OFF


if T_OFF_clk1s_Start = 1 then
if T_OFF_clk1s < T_OFF_clk1s_Timer then
T_OFF_clk1s = T_OFF_clk1s + 1
else
T_OFF_clk1s_Out = 1
endif
else
T_OFF_clk1s = 0
T_OFF_clk1s_Out = 0
endif





ExitInterrupt:
TMR0 = 61 ' precarica 61 nel registro TMR0
'TMR0L = 61 ' precarica 61 nel registro TMR0
INTCON.2 = 0 ' Reset timer interrupt flag
@ INT_RETURN




'************************************************* *******************
'**** TIMER2 interrupt
'**** Verifica se non è stato ricevuto dati su seriale entro 10ms
'**** Considera terminata la richiesta
Tmr2Interrupt:
PIR1.1 = 0
Tmr2Tick=Tmr2Tick+1


IF Tmr2Tick>120 THEN 'CIRCA 10ms
MB_READY = 1

@ INT_DISABLE RX_INT ;- disable the RX interrupt
@ INT_DISABLE TMR2_INT ;- disable Timer 2 interrupts


ELSE
MB_READY = 0
ENDIF





@ INT_RETURN




'************************************************* *******************
'****** Interrupts Ricezione / Trasmissione Seriale *****


RicezioneSeriale:


IF MB_READY == 0 THEN


HSERIN [SER_BUF[lenght]] ' take it

if lenght < LENGHT_SER_BUF then 'controlla che il buffer non sia pieno
lenght=lenght+1 'incrementa
ELSE
RX_BUF_FULL=1 'buffer pieno, errore.

ENDIF

Tmr2Tick=0 'resetta tmr2

ENDIF

@ INT_ENABLE TMR2_INT ; enable Timer 2 interrupts
@ INT_RETURN






SendChar:
hserout [SER_BUF[qnt]] ' send it
qnt=qnt+1
@ INT_DISABLE TX_INT ;- disable the TX interrupt
@ INT_RETURN