12. SPI Driver

Let's design the SPI driver API. The following functions are required:

1. Peripheral Clock Setup

void SPI_PeriClockControl(SPI_RegDef_t *pSPIx, uint8_t EnorDi)
{
    if (EnorDi == ENABLE)
    {
        if (pSPIx == SPI1)
        {
            SPI1_PCLK_EN();
        }
        else if (pSPIx == SPI2)
        {
            SPI2_PCLK_EN();
        }
        else if (pSPIx == SPI3)
        {
            SPI3_PCLK_EN();
        }
    }
    else
    {
        if (pSPIx == SPI1)
        {
            SPI1_PCLK_DI();
        }
        else if (pSPIx == SPI2)
        {
            SPI2_PCLK_DI();
        }
        else if (pSPIx == SPI3)
        {
            SPI3_PCLK_DI();
        }
    }
}

2. Initialization (SPI_Init Function)

The initialization of the Serial Peripheral Interface (SPI) is a critical step in embedded systems programming, as it sets up the hardware to correctly transmit and receive data according to a predefined set of rules and configurations. SPI is a synchronous serial communication interface widely used for short-distance communication in embedded systems. It is especially favored for its simplicity and high data transfer speeds compared to other communication protocols like I2C.

The initialization process involves configuring several parameters of the SPI peripheral to ensure proper communication with other SPI devices. These parameters include the SPI mode of operation (master or slave), the bus configuration (full-duplex, half-duplex, or simplex), clock speed, data frame format, clock polarity (CPOL), clock phase (CPHA), and the management of the Slave Select (SS) signal.

Each of these settings corresponds to bits in the SPI peripheral's control registers. Writing to these registers typically involves bitwise operations that set specific bits without altering others. The SPI peripheral on the STM32 microcontroller, like on many other microcontrollers, has a set of registers for this purpose: Control Register 1 (CR1) and Control Register 2 (CR2), among others. Properly setting up these registers dictates how the SPI peripheral will behave during data transmission and reception, ensuring that it interfaces correctly with other SPI-enabled devices in the system.

SPI_CR1

void SPI_Init(SPI_Handle_t *pSPIHandle);

The SPI_Init function takes a pointer to a structure that contains all the user-defined configurations and applies them to the registers of the SPI peripheral. This structured approach abstracts away the hardware-specific details, allowing for cleaner and more readable application code. Moreover, initializing the SPI in a structured manner allows for easy changes and maintenance, which is essential for robust and scalable embedded software development.

Here's a breakdown of what each part of the function SPI_Init does:

// Enable the peripheral clock for the SPI
SPI_PeriClockControl(pSPIHandle->pSPIx, ENABLE);

This line calls a function that enables the clock for the SPI peripheral to make sure that the SPI registers are powered and can be configured.

// First, let's configure the SPI_CR1 register
uint32_t tempreg = 0;

A temporary register tempreg is created and initialized to zero. This variable will accumulate all the settings for the CR1 register and will be written to the CR1 register at the end of the initialization process.

// 1. Configure the device mode (master or slave)
tempreg |= pSPIHandle->SPIConfig.SPI_DeviceMode << SPI_CR1_MSTR;

where

#define SPI_DEVICE_MODE_MASTER 1
#define SPI_DEVICE_MODE_SLAVE 0

This sets the master/slave mode for the SPI by shifting the configured value left by the position of the MSTR bit in the CR1 register.

// 2. Configure the bus config (full duplex, half duplex, or simplex)
if (pSPIHandle->SPIConfig.SPI_BusConfig == SPI_BUS_CONFIG_FD)
{
    // bidi mode should be cleared
    tempreg &= ~(1 << SPI_CR1_BIDIMODE);
}
else if (pSPIHandle->SPIConfig.SPI_BusConfig == SPI_BUS_CONFIG_HD)
{
    // bidi mode should be set
    tempreg |= (1 << SPI_CR1_BIDIMODE);
}
else if (pSPIHandle->SPIConfig.SPI_BusConfig == SPI_BUS_CONFIG_SIMPLEX_RXONLY)
{
    // BIDI mode should be cleared
    tempreg &= ~(1 << SPI_CR1_BIDIMODE);
    // RXONLY bit must be set
    tempreg |= (1 << SPI_CR1_RXONLY);
}

where

#define SPI_BUS_CONFIG_FD 1
#define SPI_BUS_CONFIG_HD 2
#define SPI_BUS_CONFIG_SIMPLEX_RXONLY 3

This part checks the bus configuration. In full-duplex mode, it clears the bidirectional mode bit (BIDIMODE). In half-duplex mode, it sets the BIDIMODE bit. In simplex receive-only mode, it clears the BIDIMODE bit and sets the RXONLY bit. RXONLY needs to be set, otherwise the SPI CLK will not be generated when there is not data to transmit.

// 3. Configure the SPI serial clock speed (baud rate)
tempreg |= pSPIHandle->SPIConfig.SPI_SclkSpeed << SPI_CR1_BR;

This sets the SPI clock speed by shifting the configured baud rate value left by the position of the BR bits in the CR1 register.

// 4. Configure the DFF (data frame format, 8bit or 16bit)
tempreg |= pSPIHandle->SPIConfig.SPI_DFF << SPI_CR1_DFF;

This sets the data frame format (8-bit or 16-bit) for the SPI communication.

// 5. Configure the CPOL (clock polarity)
tempreg |= pSPIHandle->SPIConfig.SPI_CPOL << SPI_CR1_CPOL;

// 6. Configure the CPHA (clock phase)
tempreg |= pSPIHandle->SPIConfig.SPI_CPHA << SPI_CR1_CPHA;

These lines configure the clock polarity (CPOL) and clock phase (CPHA), which are essential in determining the edge of the clock signal on which the data is captured.

tempreg |= pSPIHandle->SPIConfig.SPI_SSM << SPI_CR1_SSM;

This line sets the Software Slave Management (SSM) bit, which determines if the slave select is handled in hardware or software.

pSPIHandle->pSPIx->CR1 = tempreg;

Finally, all the configured settings accumulated in tempreg are written to the actual CR1 register of the SPI peripheral.


3. SPI Send and Receive Data (Blocking Version)

SPI Status Register (SR)

SPI_SR

/*
 * Bit position definitions SPI_SR
 */
#define SPI_SR_RXNE     0
#define SPI_SR_TXE      1
#define SPI_SR_CHSIDE   2
#define SPI_SR_UDR      3
#define SPI_SR_CRCERR   4
#define SPI_SR_MODF     5
#define SPI_SR_OVR      6
#define SPI_SR_BSY      7
#define SPI_SR_FRE      8

/*
 * SPI related status flags definitions
 */
#define SPI_TXE_FLAG    (1 << SPI_SR_TXE)
#define SPI_RXNE_FLAG   (1 << SPI_SR_RXNE)
#define SPI_BUSY_FLAG   (1 << SPI_SR_BSY)

SPI_GetFlagStatus()

uint8_t SPI_GetFlagStatus(SPI_RegDef_t *pSPIx, uint32_t FlagName)
{
    if (pSPIx->SR & FlagName)
    {
        return FLAG_SET;
    }
    return FLAG_RESET;
}

SPI_SendData()

This function is used to send data via SPI. It writes data to the SPI data register (DR), which is then transmitted serially via the SPI peripheral.

void SPI_SendData(SPI_RegDef_t *pSPIx, uint8_t *pTxBuffer, uint32_t Len)
{
    while (Len > 0)
    {
        // 1. Wait until TXE is set
        while (SPI_GetFlagStatus(pSPIx, SPI_TXE_FLAG) == FLAG_RESET)
            ;

        // 2. Check the DFF bit in CR1
        if ((pSPIx->CR1 & (1 << SPI_CR1_DFF)))
        {
            // 16 bit DFF
            pSPIx->DR = *((uint16_t *)pTxBuffer);
            Len -= 2;
            (uint16_t *)pTxBuffer++;
        }
        else
        {
            // 8 bit DFF
            pSPIx->DR = *pTxBuffer;
            Len--;
            pTxBuffer++;
        }
    }
}
  • The loop while (SPI_GetFlagStatus(pSPIx, SPI_TXE_FLAG) == FLAG_RESET); is a blocking wait. It ensures that the SPI's transmit buffer is empty before sending new data. This is crucial because writing data to the DR when the transmit buffer is not empty could lead to data corruption or loss.

  • Checking the DFF (Data Frame Format) Bit in CR1:

    • The SPI CR1 register's DFF bit determines the data format for transmission (8-bit or 16-bit). The function checks this bit to decide how much data to send in each operation.
    • If the DFF bit is set, it indicates a 16-bit data frame format, and thus, the function sends 16 bits (2 bytes) of data at a time.
    • If the DFF bit is not set, it indicates an 8-bit data frame format, and the function sends 8 bits (1 byte) of data.
  • Type Casting (uint16_t*):

    • This type casting is done to handle the data correctly according to the DFF bit setting.
    • When sending 16-bit data, (uint16_t *)pTxBuffer casts the pointer to 16-bit, allowing the function to treat pTxBuffer as a pointer to 16-bit data. This is important because the default type of pTxBuffer is uint8_t* (pointer to 8-bit data).

SPI_ReceiveData()

This function is used to receive data via SPI. It reads data from the SPI data register (DR), which has been received serially via the SPI peripheral.

void SPI_ReceiveData(SPI_RegDef_t *pSPIx, uint8_t *pRxBuffer, uint32_t Len)
{
    while (Len > 0)
    {
        // 1. wait until RXNE is set
        while (SPI_GetFlagStatus(pSPIx, SPI_RXNE_FLAG) == (uint8_t)FLAG_RESET)
            ;

        // 2. check the DFF bit in CR1
        if ((pSPIx->CR1 & (1 << SPI_CR1_DFF)))
        {
            // 16 bit DFF
            // 1. load the data from DR to Rxbuffer address
            *((uint16_t *)pRxBuffer) = pSPIx->DR;
            Len--;
            Len--;
            (uint16_t *)pRxBuffer++;
        }
        else
        {
            // 8 bit DFF
            *(pRxBuffer) = pSPIx->DR;
            Len--;
            pRxBuffer++;
        }
    }
}
  • Similar to SPI_SendData(), this function waits (while (SPI_GetFlagStatus(pSPIx, SPI_RXNE_FLAG) == (uint8_t)FLAG_RESET);) until there is data to be read in the SPI's receive buffer. This is indicated by the RXNE flag.
  • Checking the DFF Bit in CR1:
    • Again, the function checks the DFF bit in the CR1 register to determine the data format (8-bit or 16-bit) for receiving data.
    • Depending on the DFF bit setting, the function reads either 16 bits or 8 bits from the DR register.

4. Other Helper Functions

SPI2 GPIO Pin Initialization

We want to use SPI2 for this project. Check out the alternate function mapping table for STM32F446RE (this one only displays Port B):

Alternate_Function_PBx

We can see that the four pins we need are: - SCLK: PB13 - MOSI: PB15 - MISO: PB14 - NSS: PB12 And the alternate function number for SPI2 is 5.

So, let's create a function to initialize these GPIO pins for SPI2. We first create a GPIO_Handle_t structure and fill in the required parameters (see GPIO Pin Configuration Structure). We can then initialize all SPI pins using just this GPIO_Handle_t structure (see GPIO Initialization and De-initialization). Just need to change the GPIO_PinNumber parameter one by one.

void SPI2_GPIOInits(void)
{
    GPIO_Handle_t SPIPins;

    SPIPins.pGPIOx = GPIOB;
    SPIPins.GPIO_PinConfig.GPIO_PinMode = GPIO_MODE_ALTFN;
    SPIPins.GPIO_PinConfig.GPIO_PinAltFunMode = 5;
    SPIPins.GPIO_PinConfig.GPIO_PinOPType = GPIO_OP_TYPE_PP;
    SPIPins.GPIO_PinConfig.GPIO_PinPuPdControl = GPIO_NO_PUPD;
    SPIPins.GPIO_PinConfig.GPIO_PinSpeed = GPIO_SPEED_FAST;

    // SCLK
    SPIPins.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_NO_13;
    GPIO_Init(&SPIPins);

    // MOSI
    SPIPins.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_NO_15;
    GPIO_Init(&SPIPins);

    // MISO (actually not required because we are just sending data)
    SPIPins.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_NO_14;
    GPIO_Init(&SPIPins);

    // NSS (actually not required because there is only one slave)
    SPIPins.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_NO_12;
    GPIO_Init(&SPIPins);
}

SPI2 Peripheral Initialization

We also need to initialize the SPI peripheral. We create a SPI_Handle_t structure and fill in the required parameters (see SPI Configuration Structure). We can then set all SPI parameters using just this SPI_Handle_t structure. Just need to change the SPI_RegDef_t parameter one by one.

void SPI2_Inits(void)
{
    SPI_Handle_t SPI2handle;

    SPI2handle.pSPIx = SPI2;
    SPI2handle.SPIConfig.SPI_BusConfig = SPI_BUS_CONFIG_FD;
    SPI2handle.SPIConfig.SPI_DeviceMode = SPI_DEVICE_MODE_MASTER;
    SPI2handle.SPIConfig.SPI_SclkSpeed = SPI_SCLK_SPEED_DIV2; // generates sclk of 8MHz
    SPI2handle.SPIConfig.SPI_DFF = SPI_DFF_8BITS;
    SPI2handle.SPIConfig.SPI_CPOL = SPI_CPOL_HIGH;
    SPI2handle.SPIConfig.SPI_CPHA = SPI_CPHA_LOW;
    SPI2handle.SPIConfig.SPI_SSM = SPI_SSM_EN; // software slave management enabled for NSS pin

    SPI_Init(&SPI2handle);
}

NSS Signal Internally High

Instead of connecting the NSS pin physically, we can set the SSI bit in the SPI control register to make the NSS signal internally high. This is done for two reasons:

  1. NSS Signal Internally High:

    • In SPI, the NSS pin is used to select a slave device for communication. Typically, when the master wants to communicate with a slave, it pulls the NSS line low (active low configuration).
    • In some SPI configurations, especially when the microcontroller is set as a slave, the NSS pin is not controlled by an external master but is instead managed internally by the SPI hardware of the microcontroller. In such cases, setting the SSI (Slave Select Internal) bit in the SPI control register forces the NSS signal to be high internally. This means the microcontroller's SPI hardware treats the NSS pin as if it is always unselected, or in a non-active state, even if there's no physical NSS pin connected or if the physical state of the pin is different.
  2. Avoiding MODF Error:

    • The MODF (Mode Fault) error is a specific type of error condition in SPI communication. It occurs when the microcontroller is configured as a slave, but the NSS pin goes low unexpectedly, which can happen if the microcontroller is inadvertently driven into master mode while the NSS pin is also being driven low by an external master.
    • By setting the SSI bit and thus forcing the NSS signal high internally, the microcontroller avoids the MODF error. This is because, from the microcontroller's perspective, the NSS pin remains unselected (high), preventing the condition where the microcontroller might think it should be a master while also being selected by an external master.

MODF

void SPI_SSIConfig(SPI_RegDef_t *pSPIx, uint8_t EnOrDi)
{
    if (EnOrDi == ENABLE)
    {
        pSPIx->CR1 |= (1 << SPI_CR1_SSI);
    }
    else
    {
        pSPIx->CR1 &= ~(1 << SPI_CR1_SSI);
    }
}

SPI Peripheral Enable/Disable

This is a function to turn on or off the SPI peripheral itself, not the GPIO pins. This is a common operation in SPI communication, often performed at the beginning of an SPI operation to enable the peripheral and then disabled once the operation is complete to save power or allow other peripherals to use the same pins.

void SPI_PeripheralControl(SPI_RegDef_t *pSPIx, uint8_t EnOrDi)
{
    if (EnOrDi == ENABLE)
    {
        pSPIx->CR1 |= (1 << SPI_CR1_SPE);
    }
    else
    {
        pSPIx->CR1 &= ~(1 << SPI_CR1_SPE);
    }
}

SPI Flag Status

The SPI peripheral typically takes time to transmit and receive data. During this time, the SPI peripheral is busy, and the user should not be able to take over the SPI peripheral. To prevent this, the SPI peripheral has a busy flag (BSY) in the status register (SR). The BSY flag is set when the SPI peripheral is busy and cleared when it is not busy. The user can check the BSY flag to see if the SPI peripheral is busy or not.

SPI_SR_BSY

uint8_t SPI_GetFlagStatus(SPI_RegDef_t *pSPIx, uint32_t FlagName)
{
    if (pSPIx->SR & FlagName)
    {
        return FLAG_SET;
    }
    return FLAG_RESET;
}

5. Complete Code - Exercise: SPI Send Data (Blocking Version)

int main(void)
{
    char user_data[] = "Hello world";

    // This function is used to initialize the GPIO pins to behave as SPI2 pins
    SPI2_GPIOInits();

    // This function is used to initialize the SPI2 peripheral parameters
    SPI2_Inits();

    // This makes NSS signal internally high and avoids MODF error
    SPI_SSIConfig(SPI2, ENABLE);

    // Enable the SPI2 peripheral
    SPI_PeripheralControl(SPI2, ENABLE);

    // Send data
    SPI_SendData(SPI2, (uint8_t*)user_data, strlen(user_data));

    // Let's confirm SPI is not busy
    while(SPI_GetFlagStatus(SPI2, SPI_BUSY_FLAG));

    // Disable the SPI2 peripheral
    SPI_PeripheralControl(SPI2, DISABLE);

    while(1)
            ;
    return 0;
}

5. SPI Send and Receive Data (Interrupt Version)

uint8_t SPI_SendDataIT(SPI_Handle_t *pSPIHandle, uint8_t *pTxBuffer, uint32_t Len)
{
    uint8_t state = pSPIHandle->TxState;

    if (state != SPI_BUSY_IN_TX)
    {
        // 1 . Save the Tx buffer address and Len information in some global variables
        pSPIHandle->pTxBuffer = pTxBuffer;
        pSPIHandle->TxLen = Len;
        // 2.  Mark the SPI state as busy in transmission so that
        //     no other code can take over same SPI peripheral until transmission is over
        pSPIHandle->TxState = SPI_BUSY_IN_TX;

        // 3. Enable the TXEIE control bit to get interrupt whenever TXE flag is set in SR
        pSPIHandle->pSPIx->CR2 |= (1 << SPI_CR2_TXEIE);
    }

    return state;
}
uint8_t SPI_ReceiveDataIT(SPI_Handle_t *pSPIHandle, uint8_t *pRxBuffer, uint32_t Len)
{
    uint8_t state = pSPIHandle->RxState;

    if (state != SPI_BUSY_IN_RX)
    {
        // 1 . Save the Rx buffer address and Len information in some global variables
        pSPIHandle->pRxBuffer = pRxBuffer;
        pSPIHandle->RxLen = Len;
        // 2.  Mark the SPI state as busy in reception so that
        //     no other code can take over same SPI peripheral until reception is over
        pSPIHandle->RxState = SPI_BUSY_IN_RX;

        // 3. Enable the RXNEIE control bit to get interrupt whenever RXNEIE flag is set in SR
        pSPIHandle->pSPIx->CR2 |= (1 << SPI_CR2_RXNEIE);
    }

    return state;
}