Bare Metal Code

The first step will be to configure the microcontroller to disable the Watchdog Timer and to enable Low-Voltage Programming (LVP).

/* WDT operating mode->WDT Disabled */
#pragma config WDTE = OFF
/* Low voltage programming enabled, RE3 pin is MCLR */
#pragma config LVP = ON  

The internal oscillator has to be set to the desired value. This example uses the HFINTOSC with a frequency of 4 MHz. This translates in the following function:

static void CLK_init(void)
{
    OSCCON1 = 0x60;             /* set HFINTOSC Oscillator */
    OSCFRQ  = 0x02;             /* set HFFRQ to 4 MHz */
}

The following function initializes the Timer2 peripheral with the HFINTOSC clock:

static void TMR2_Initialize(void)
{
    /* TMR2 Clock source, HFINTOSC (00011) */
    T2CLKCON = 0x03;
    /* T2PSYNC Not Synchronized, T2MODE Software control, T2CKPOL Rising Edge */
    T2HLT = 0x00;
    /* TMR2ON on; T2CKPS Prescaler 1:1; T2OUTPS Postscaler 1:1 */
    T2CON = 0x80;
    /* Set TMR2 period, PR2 to 199 (50us) */
    T2PR = Timer2Period;
    /* Clear the TMR2 interrupt flag */
    PIR4bits.TMR2IF = 0;
}

The SPI1_Initialize function will configure the SPI clock source to be TMR2 Output/2:

static void SPI1_Initialize(void)
{  
    /* SSP1ADD = 1 */
    SSP1ADD = 0x01;
    /* Enable module, SPI Master Mode, TMR2 as clock source */
    SSP1CON1 = 0x23;        
}

Therefore, the SPI pins can be relocated using the SSPxCLKPPS, SSPxDATPPS, SSPxSSPPS registers for the input channels and by using the RxyPPS registers for output channels.

The method to configure the location of the pins is independent of the application purpose and the SPI mode. Each microcontroller has its own default physical pin position for peripherals, but they can be changed using the Peripheral Pin Select (PPS).

For SPI1 in Master mode, only the SDI pin needs to be input so it is used with its default location RC4. SCK was mapped to RC3 and SDO was mapped to RC5. This translates into the following code:

static void PPS_Initialize(void)
{  
    RC3PPS = 0x0F;              /* SCK channel on RC3 */
    SSP1DATPPS = 0x14;          /* SDI channel on RC4 */
    RC5PPS = 0x10;              /* SDO channel on RC5 */
}

Since this example has the Master sending data to two Slave devices, two SS pins are needed (SS1 and SS2). For both, a General Purpose Input/Output (GPIO) pin was used (RC6 for SS1 and RC7 for SS2).

Table 1. SPI Pin Locations
Channel Pin
SCK RC3
SDI RC4
SDO RC5
SS1 RC6
SS2 RC7

Since the Master devices control and initiate transmissions, the SDO, SCK and SS pins must be configured as output while the SDI channel will keep its default direction as input. The following example is based on the relocation of the SPI1 pins made above:

static void PORT_Initialize(void)
{
    ANSELC = 0x07;      /* Set RC6 and RC7 pins as digital */
    TRISC  = 0x17;      /* Set SCK, SDO, SS1, SS2 as output and SDI as input */
}

A Master will control a Slave by pulling low the SS pin. If the Slave has set the direction of its SDO pin to output (when the SS pin is low), the SPI driver of the Slave will take control of the SDI pin of the Master, shifting data out from its Transmit Buffer register.

All Slave devices can receive a message, but only those with the SS pin pulled low can send data back. It is not recommended to enable more than one Slave in a typical connection since all of them will try to respond to the message and the Master has only one SDI channel. Therefore, the transmission will result in a write collision.

Before sending data, the user must pull low one of the configured SS signals to let the correspondent Slave device know it is the recipient of the message.

static void SPI1_slave1Select(void)
{
    LATCbits.LATC6 = 0;          /* Set SS1 pin value to LOW */
}

Once the user writes new data into the Buffer register, the hardware starts a new transfer, generating the clock on the line and shifting out the bits. The bits are shifted out starting with the Most Significant bit (MSb).

When the hardware finishes shifting all the bits, it sets the Buffer Full Status bit. The user must check the state of the flag before writing new data into the register by constantly reading the value of the bit (or polling), or else a write collision will occur.

static uint8_t SPI1_exchangeByte(uint8_t data)
{
    SSP1BUF = data;
    
    while(!PIR3bits.SSP1IF) /* Wait until data is exchanged */
    {
        ;
    }   
    PIR3bits.SSP1IF = 0;
    
    return SSP1BUF;
}

The user can pull the SS channel high if there is nothing left to transmit.

static void SPI1_slave1Deselect(void)
{
    LATCbits.LATC6 = 1;          /* Set SS1 pin value to HIGH */
}
The following function is the int_main(void) and begins peripheral initialization before the SPI commands are run in a infinite loop while(1):
int main(void)
{
    CLK_Initialize();
    PPS_Initialize();
    PORT_Initialize();
    TMR2_Initialize();
    SPI1_Initialize();
    
    while(1)
    {
        SPI1_slave1Select();
        receiveData = SPI1_exchangeByte(writeData);
        SPI1_slave1Deselect();
        
        SPI1_slave2Select();
        receiveData = SPI1_exchangeByte(writeData);
        SPI1_slave2Deselect();
    }
}