MODbus/RTU on a PIC Microcontroller

By request one of my long lost blogs was a MODbus RTU application for a PIC 18F27J53. You could easily plop this onto many of the 18F series micros. It doesn’t do much but it’s a great stepping stone for throwing in your own IO. I’ll probably follow up with an Arduino version in a while.. I’ll put it on my “todo” list.

The code…

/*
 File:   main.c
 Author: Charles M Ihler
 Contact at: http://iradan.com
 *
 Created on November 8, 2014, 2:37 PM
 *
 Target Device:
 18F27J53 Development Board by AtomSoft
 *
 Project: MODbus/RTU slave
 *
 Notes
 We will ignore both query high bytes for now
 TODO:   Pull all hard coded IO out eventually and set it up via USB?
 Maybe a terminal application for enabling points?
 *
 *
 Version:
 0.02     Controlled sends caned reply on request to address on RS-232
 0.03     CRC works on RX
 0.04     No actual IO yet..
 Function 0x02 (Read Input Status) Started
 0.05     Ported to 18F17J53 for expanded IO/USB options.
 12MHz XTAL with CPUDIV set to 16MHz
 *
 / //#ifndef _XTAL_FREQ //#define _XTAL_FREQ 16000000 // //#define __delay_us(x) _delay((unsigned long)((x)(_XTAL_FREQ/4000000.0)))
 //#define __delay_ms(x) _delay((unsigned long)((x)*(_XTAL_FREQ/4000.0)))
 //#endif 
 // PIC18F27J53 Configuration Bit Settings
 // 'C' source line config statements
 include 
 // #pragma config statements should precede project file includes.
 // Use project enums instead of #define for ON and OFF.
 // CONFIG1L
 pragma config WDTEN = OFF      // Watchdog Timer (Disabled - Controlled by SWDTEN bit)
 pragma config PLLDIV = 3       // PLL Prescaler Selection (Divide by 3 (12 MHz oscillator input))
 pragma config CFGPLLEN = ON    // PLL Enable Configuration Bit (PLL Enabled)
 pragma config STVREN = ON      // Stack Overflow/Underflow Reset (Enabled)
 pragma config XINST = OFF       // Extended Instruction Set (Enabled)
 // CONFIG1H
 pragma config CPUDIV = OSC3_PLL3// CPU System Clock Postscaler (CPU system clock divide by 3 from 48MHz)
 pragma config CP0 = OFF        // Code Protect (Program memory is not code-protected)
 // CONFIG2L
 pragma config OSC = HSPLL      // Oscillator (HS+PLL, USB-HS+PLL)
 pragma config SOSCSEL = HIGH   // T1OSC/SOSC Power Selection Bits (High Power T1OSC/SOSC circuit selected)
 pragma config CLKOEC = OFF      // EC Clock Out Enable Bit  (CLKO output enabled on the RA6 pin)
 pragma config FCMEN = OFF      // Fail-Safe Clock Monitor (Disabled)
 pragma config IESO = OFF       // Internal External Oscillator Switch Over Mode (Disabled)
 define _XTAL_FREQ 16000000 //defined for delay
 char    ctxt[10], buf1, testcrc[4];    //buff serial string volatile unsigned int     ping, isrcall, index, reading, new_rx; int address, crc_high, crc_low, i, querylength; unsigned int result; //these variables are for fuctions to be enabled/disabled and pushed into application code via later int frame, z, bufferbits, query_reg_address, y; char data_out[64]; char reg_address[255];
 /*
  *
 Interrupt Service
 *
 UART RX and Timer 1
 *
 */
 void interrupt ISR() {
 if (PIR1bits.RCIF)          // see if interrupt caused by incoming data
 {
     isrcall = 0x01;
     char temp;
     temp = RCREG;     // read the incoming data
     buf1 = temp;
     if(temp==address && reading==0)      //if my address..
     {
         index = 0;                  //reset index
         reading = 1;                //from now on go to else if
         LATCbits.LATC1 = 1;
         new_rx = 0;
         ping = 1;
     }
     else if(reading == 1)           //in middle of GPS sentence
     {
         //TODO reset timeout timer
         ctxt[index] = temp;         //load it up
         index++;                    //increment index
         ping = 1;                   //this is for debugging
         if(index > 6)              //1+7 = master frame.
             {
             index = 0;              //reset index
             reading = 0;            //no longer storing the string
             new_rx = 1;             //"ding"
             T1CONbits.TMR1ON = 0;
             }
     }
 }
 //RCSTA2bits.FERR = 0;    //Clear errors
 //RCSTA2bits.OERR = 0;
 //time out timer, if tripped new_rx=0;
 if (PIR1bits.TMR1IF)
 {
     new_rx=0;
     ping = 0;
     T1CONbits.TMR1ON = 0;
     PIR1bits.TMR1IF = 0;
     if (reading) {
     reading = 0;
     LATCbits.LATC0 = 1;
     }
 }
 } 
 void init_io(void) {
 INTCONbits.GIE = 0;         //no interruptions please ADCON0 = 0b00000000;        //don't need any ADC ADCON1 = 0b00000000;        //speed Vref=AVdd, VssRef=AVss LATA = 0x00; LATB = 0x00; LATC = 0x00; TRISAbits.TRISA0 = 1; // address 1 TRISAbits.TRISA1 = 1; // address 2 TRISAbits.TRISA2 = 1; // address 3 TRISAbits.TRISA3 = 0; // output TRISAbits.TRISA5 = 0; // output TRISBbits.TRISB0 = 0; // TRISBbits.TRISB1 = 1; // TRISBbits.TRISB2 = 0; // TRISBbits.TRISB3 = 0; // TRISBbits.TRISB4 = 0; //  TRISBbits.TRISB5 = 1; //  TRISBbits.TRISB6 = 0; //  TRISBbits.TRISB7 = 0; //  TRISCbits.TRISC0 = 0; // timer LED output TRISCbits.TRISC1 = 0; // output TRISCbits.TRISC2 = 0; // LED test output TRISCbits.TRISC3 = 0; // output //TRISCbits.TRISC4 = 1; // USB //TRISCbits.TRISC5 = 1; // USB TRISCbits.TRISC6 = 0; // output TX1 TRISCbits.TRISC7 = 1; // input  RX1
 }
 void __delay_10ms(unsigned char n)     //__delay functions built-in can't be used for much at this speed… so!
  {
      while (n-- != 0) {
          __delay_ms(10);
      }
  }
 void uart_xmit(unsigned int mydata_byte) {
 while(!TXSTAbits.TRMT);    // make sure buffer full bit is high before transmitting TXREG = mydata_byte;       // transmit data
 }
 void write_uart(const char txt) {                                 //this send a string to the TX buffer                                 //one character at a time        while(txt)
        uart_xmit(*txt++);
 }
 void serial_init(void)
 {
     //9600 8N1
 TXSTA1bits.SYNC = 0; TXSTA1bits.BRGH=1;       // select low speed Baud Rate (see baud rate calcs below) TXSTA1bits.TX9=0;        // select 8 data bits TXSTA1bits.TXEN = 1;     // enable transmit RCSTA1bits.SPEN=1;       // serial port is enabled RCSTA1bits.RX9=0;        // select 8 data bits RCSTA1bits.CREN=1;       // receive enabled SPBRG1=102; SPBRGH1=0; PIR1bits.RCIF=0;        // make sure receive interrupt flag is clear PIE1bits.RCIE=1;        // enable UART Receive interrupt INTCONbits.PEIE = 1;    // Enable peripheral interrupt INTCONbits.GIE = 1;     // enable global interrupt  __delay_10ms(5);        // give time for voltage levels on board to settle
 }
 void run_timer (void) {
     T1CONbits.TMR1ON = 0;
     TMR1=0;
     TMR1L=0x00;
     TMR1H=0xAA;
     T1CONbits.TMR1CS = 0x01;
     T1CONbits.T1CKPS = 0x01;
     PIE1bits.TMR1IE = 1;
     T1CONbits.TMR1ON = 1;
 //        PIR1bits.TMR1IF=0;
 }
 void check_my_address(void) {
     /*
      *  Determine what address the unit is based on DIP switches
      *  PORTAbits.RA0   Switch 1
      *  PORTAbits.RA1   Switch 2
     */
 if (PORTAbits.RA0 & PORTAbits.RA1) address = 0x04; if (!PORTAbits.RA0 & PORTAbits.RA1) address = 0x03; if (PORTAbits.RA0 & !PORTAbits.RA1) address = 0x02; if (!PORTAbits.RA0 & !PORTAbits.RA1) address = 0x01;
 }
 /*
 TODO
 Poll the inputs
 *
 I will probably use a 74138 to address banks with 74HC245 for byte/banks of inputs
 I will likely use the same set up for "Coils"
 For input registers (analog inputs) I will consider perhaps some kind of A/D input with CD4066+74HC138
 *
 */ 
 void poll_my_inputs(void) {
 //just stuff it with something for now for (z = 0; z < 64; ++z) { reg_address[z] = z; }
 }
 /*
 did the chicken come before the egg?
 this MODbus CRC bit was gently borrowed from the internet..
 I can't determine who wrote it.. it shows up in an Ardiuno Sketch for MODbus
 Modbus over serial line - RTU Slave Arduino Sketch
 with referenced of Juan Pablo Zometa, Samuel Marco, Andras Tucsni, Philip Costigan
 It also shows up on a number of websites and forums uncredited… many PIC or C based ..
 so who knows, but I didn't write it
 The MODbus CRC-16 function is outlined on the Modicon site for reference
 */
 unsigned int modbus_crc(unsigned char buf[], int start, int cnt)
 {
 int     i,j;
 unsigned temp, temp2, flag;
 temp=0xFFFF;
 for (i=start; i<cnt; i++){
    temp=temp ^ buf[i];
 for (j=1; j<=8; j++){       flag=temp & 0x0001;      temp=temp >> 1;
      if (flag) temp=temp ^ 0xA001;
      }
   }
 /*** Reverse byte order. ***/
 temp2=temp >> 8;
 temp= (temp << 8) | temp2;
 return(temp);
 } 
 int check_master_crc (void) {
 int diff, diff1, diff2;       //what we send back char data[6]; result = 0x0000; crc_high = 0x00; crc_low = 0x00; data[0] = address;  //this is ugly but all master queries are 6bytes+CRC so it'll do data[1] = ctxt[0];  //function data[2] = ctxt[1];  //start_addressH data[3] = ctxt[2];  //start_addressL data[4] = ctxt[3];  //# of regH data[5] = ctxt[4];  //# of regL result = modbus_crc(data,0,6); crc_high = result >> 8; crc_low = result & 0x00FF; diff1 = ctxt[5] ^ crc_high; diff2 = ctxt[6] ^ crc_low; diff = diff1 + diff2; //this spits back an XORed value between calculated and received CRC return(diff);
 }
 void respond_input_status(void) {
         //ctxt[1] start_addressH
         //ctxt[2] start_addressL
         //ctxt[3] # of regH
         //ctxt[4] # of regL
     querylength = ctxt[4];  //ignoring upper byte for now (TODO)
     query_reg_address = ctxt[2]; //ignoring upper address byte (TODO)
 frame = 0;     //data frame counter do {     data_out[frame] = reg_address[query_reg_address];     //TODO  make this do somethng     querylength--;     frame++; } while (querylength>0); querylength = ctxt[4];  //reload so we can count time transmitting data result = modbus_crc(data_out,0,2); crc_high = result >> 8; crc_low = result & 0x00FF; //uart_xmit response address, function, data_out[frames], CRC uart_xmit(address); uart_xmit(ctxt[0]); frame = 0; do {     uart_xmit(data_out[frame]);     //TODO  make this do somethng     querylength--;     frame++; } while (querylength>0); uart_xmit(crc_high); uart_xmit(crc_low);
 }
 int main(void) {
     OSCCONbits.SCS = 0x00;
 new_rx=0; init_io(); serial_init(); run_timer(); check_my_address(); y += address; write_uart("MODBus "); uart_xmit(y); LATCbits.LATC0 = 1; LATCbits.LATC1 = 1; __delay_10ms(50); LATCbits.LATC0 = 0; LATCbits.LATC1 = 0; if (address == 0x01){     LATCbits.LATC0 = 1; } if (address == 0x02) {     LATCbits.LATC1 = 1; } __delay_10ms(50); LATCbits.LATC0 = 0; LATCbits.LATC1 = 0; INTCONbits.GIE = 1; INTCONbits.PEIE = 1; while (1) {     //If read data, not me and then if no 232 traffic for >800us then any xmit over     //104 us (9600) x 8 quiet periods minus some for wiggle.     if (ping) {         run_timer();     }     if (new_rx) {       //this device was polled         //testing response         i = check_master_crc();     //check master query crc         if (i==0x00) {              //CRC Ok?             if (ctxt[0] == 2) {       //coil status query                 respond_input_status();             }             //TODO other types of functions go here         }         //all done, get ready for new query         new_rx = 0;     }     //testcrc[0] = 0xFF;     //testcrc[1] = 0x02;      //right now this is only thing processed     //result = modbus_crc(testcrc,0,2);     //crc_high = result >> 8;     //crc_low = result & 0x00FF;     __delay_10ms(10);     LATCbits.LATC2 = 1; //LED blinky test     __delay_10ms(10);     LATCbits.LATC0 = 0;     LATCbits.LATC1 = 0;     LATCbits.LATC2 = 0; }
 }