Hardware SPI

This tutorial will discuss the hardware SPI peripheral that is common in most PIC micro controllers. In the PIC, this module is more commonly known as MSSP. Unlike the bit bang version of this that was discussed in the last tutorial, the hardware version is a little cleaner to use and can transmit and receive information on the same SPI clock cycle. This is a must when dealing with modules that both transmit and write on the same cycle, one of those being the RF12 433mhz wireless modules. Like the other tutorials this will be done on a PIC16F18323 and a 74HC595 shift register, compiled using XC8.

/* 
 * File:   main.c
 * Author: Bill Heaster
 * Hardware SPI testing
 * Created on October 10, 2015, 2:46 PM
 */

#include <stdio.h>
#include <stdlib.h>
#include <xc.h>

// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.

// CONFIG1
#pragma config FEXTOSC = HS     // FEXTOSC External Oscillator mode Selection bits (HS (crystal oscillator) above 4 MHz)
#pragma config RSTOSC = EXT1X   // Power-up default value for COSC bits (EXTOSC operating per FEXTOSC bits)
#pragma config CLKOUTEN = OFF   // Clock Out Enable bit (CLKOUT function is disabled; I/O or oscillator function on OSC2)
#pragma config CSWEN = ON       // Clock Switch Enable bit (Writing to NOSC and NDIV is allowed)
#pragma config FCMEN = ON       // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is enabled)

// CONFIG2
#pragma config MCLRE = ON       // Master Clear Enable bit (MCLR/VPP pin function is MCLR; Weak pull-up enabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config WDTE = OFF       // Watchdog Timer Enable bits (WDT disabled; SWDTEN is ignored)
#pragma config LPBOREN = OFF    // Low-power BOR enable bit (ULPBOR disabled)
#pragma config BOREN = ON       // Brown-out Reset Enable bits (Brown-out Reset enabled, SBOREN bit ignored)
#pragma config BORV = LOW       // Brown-out Reset Voltage selection bit (Brown-out voltage (Vbor) set to 2.45V)
#pragma config PPS1WAY = ON     // PPSLOCK bit One-Way Set Enable bit (The PPSLOCK bit can be cleared and set only once; PPS registers remain locked after one clear/set cycle)
#pragma config STVREN = ON      // Stack Overflow/Underflow Reset Enable bit (Stack Overflow or Underflow will cause a Reset)
#pragma config DEBUG = OFF      // Debugger enable bit (Background debugger disabled)

// CONFIG3
#pragma config WRT = OFF        // User NVM self-write protection bits (Write protection off)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (High Voltage on MCLR/VPP must be used for programming.)

// CONFIG4
#pragma config CP = OFF         // User NVM Program Memory Code Protection bit (User NVM code protection disabled)
#pragma config CPD = OFF        // Data NVM Memory Code Protection bit (Data NVM code protection disabled)

#define _XTAL_FREQ 8000000 //let the compiler know what our crystal is so our _delay functions properly.

#define LATCHPIN LATCbits.LATC3 




//setup
void setup()
{
    //disable analog
    ANSELA =0x00;
    ANSELC =0x00;

    //set pin direction
    TRISCbits.TRISC3=0; //used as Latch Pin
    TRISCbits.TRISC2=0; //used as SDO
    TRISCbits.TRISC0=0; //used as SCK
    TRISCbits.TRISC1 = 0; //disable SDI because we don't need it. 
    
    
    //set PPS pins
    RC2PPS = 0b00011001; // Set RC2 to SDO
    RC0PPS = 0b00011000; //set RC0 to SCK
    
    //lock the PPS
    PPSLOCK = 1;
    
    //enable the spi perephial 
    SSPEN =1;
        
    //turn on the perephial interupts 
    GIE = 1;
    PEIE = 1;
    
    //set latch high to start, setting it low will latch the SR and allow unwanted bits to spew into it. 
    LATCHPIN = 1;
    
}

//hacked together delay function
void delaySecond()
{
    int j =0;
    
    for(j=0; j<100; j++)
    {
        __delay_ms(10);
    }   
}



int main(int argc, char** argv) {

    setup();
    
    while(1)
    {
        //swing the latch low to write
        LATCHPIN =0;

        //turn the leds on.
        SSP1BUF = 0xff;

        //without this delay there is a missed bit somewhere. I dont know why, but adding this delay solves the issue. 
        __delay_ms(1);

       //swing the latch high to finish writting
        LATCHPIN = 1;

        delaySecond();


        //swing the latch low to write
        LATCHPIN =0;

        //turn the leds off

        SSP1BUF = 0x00;
         __delay_ms(1);



        //swing the latch high to finish writting
        LATCHPIN = 1;

        delaySecond();
  
    }
    
    
  
    return (EXIT_SUCCESS);
}


As before the program starts with configuration of all the control register for the PIC. These are the same as the other tutorials and will remain that way unless otherwise noted. The biggest change between the hardware and bit bang versions of the SPI protocol takes place in the setup() routine. Here we will discuss a new set of registers which are the PPS registers. These are in control of setting which pins attach to which internal peripheral. This was my first time dealing with PPS and as i can see how it would be a nice feature for future board layouts it is also a giant pain in the ass. Once the analog function is disabled using the ANSELA and ANSELC registers the pin directions are set using the TRIS registers. Notice that the pins that is labeled SS and SDI in the data sheet are set to output. By doing this it disables the TRIS override that occurs inside the peripheral and allows us to control the pin directly from software. In this case we will be using the SS pin as the LATCH pin, similar to the previous tutorial.


//setup
void setup()
{
    //disable analog
    ANSELA =0x00;
    ANSELC =0x00;

    //set pin direction
    TRISCbits.TRISC3=0; //used as Latch Pin
    TRISCbits.TRISC2=0; //used as SDO
    TRISCbits.TRISC0=0; //used as SCK
    TRISCbits.TRISC1 = 0; //disable SDI because we don't need it. 
    
    
    //set PPS pins
    RC2PPS = 0b00011001; // Set RC2 to SDO
    RC0PPS = 0b00011000; //set RC0 to SCK
    
    //lock the PPS
    PPSLOCK = 1;
    
    //enable the spi perephial 
    SSPEN =1;
        
    //turn on the perephial interupts 
    GIE = 1;
    PEIE = 1;
    
    //set latch high to start, setting it low will latch the SR and allow unwanted bits to spew into it. 
    LATCHPIN = 1;
    
}

The RC2PPS and RC0PPS are the PPS registers. These will set pins RC2 and RC0 to connect to the internal SPI peripheral. The values to set were taken from the data sheet resulting in RC2 becoming SDO and RC0 becoming the SPI clock signal. Once you set all of the PPS pins that are needed the PPSLOCK is set. This can only be done once during your program, unless you follow the 5 or so instructions that have to be done in assembly to turn PPSLOCK off and re configure the pins. Once the PPS is locked we turn on the SPI peripheral using the SSPEN bit. Because the SPI requires interrupts to run properly we must also turn those on. As before the GIE has to be set to use any interrupts and the PEIE will enable the internal peripheral interrupts to be used. Lastly we set the LATCHPIN high in this case so that we don’t transmit any bits on the SPI bus until the program is up and running.

//hacked together delay function
void delaySecond()
{
    int j =0;
    
    for(j=0; j<100; j++)
    {
        __delay_ms(10);
    }   
}

as the comment states, this function is like the one used in the HelloWorld tutorial to make a small delay so we can visually see the LED’s change on the shift register output.

int main(int argc, char** argv) {

    setup();
    
    while(1)
    {
        //swing the latch low to write
        LATCHPIN =0;

        //turn the leds on.
        SSP1BUF = 0xff;

        //without this delay there is a missed bit somewhere. I dont know why, but adding this delay solves the issue. 
        __delay_ms(1);

       //swing the latch high to finish writting
        LATCHPIN = 1;

        delaySecond();


        //swing the latch low to write
        LATCHPIN =0;

        //turn the leds off

        SSP1BUF = 0x00;
         __delay_ms(1);



        //swing the latch high to finish writting
        LATCHPIN = 1;

        delaySecond();
  
    }
    
    
  
    return (EXIT_SUCCESS);
}

On to the main function. Setup() routine is called first to setup pins, peripherals and registers. The latch pin is then brought low to enable writing to the shift register. The next line puts a character into SSP1BUF. This particular register will begin writing to the SPI bus automatically as soon as we write to it. Think of it like calling the ShiftOut() routine from the last tutorial. Once you call it , it just runs and outputs data onto the SPI bus. The character we are sending is in hex and is equal to 0b11111111 which means that all of the LED’s will flash on once we set the latch pin back high. The delaySecond() routine is called so we can see the led’s light up and then they are all turned off by writing 0x00 or 0b00000000 to the SSP1BUF buffer. Because I am using the SPI in master mode, and also because I am not receiving I do not need to check the buffer before I write to it the second time. Alternatively, if you were doing a two way transfer you could use the overflow interrupt to detect when the buffer is full and then read from it.

Conclusion

using hardware SPI has the advantage of speed over bit banging data in. It also has the ability to transmit and receive in the same SPI clock cycle. If the proper bits are set you can also implement write collision control and various other features that most people would never use. Plus, because the protocol is built into hardware there is more program memory available to the programmer because you don’t have to write functions to accomplish the same task and the buffer is in a rolled out loop that will cut down on processor cycles used to digest and send the data over the SPI bus. Bottom line, use the damn hardware SPI when you can, faster is always better.