Page 1 of 1

STM32 Programming. Part 6: SPI

Posted: 16 Oct 2023, 02:59
by Oleg
image.png
image.png (7.22 KiB)
Viewed 3620 times
learn how to work with SPI module in STM32F103C8 microcontroller in Master mode using interrupts and without them.

SPI is the most popular serial synchronous data transfer interface between the microcontroller and peripherals. There are two SPI modules in the STM32F103C8. This interface can operate in Master (bus master) or Slave (bus slave) mode. Generally speaking, the SPI interface is quite a tricky thing. Specifically in STM32 SPI can calculate the checksum of received and transmitted frames by a given polynomial, work in Multimaster mode, hardware to work with the output NSS, as well as communicate in half-duplex mode (MOSI and MISO go on the same wire). Therefore, to properly configure SPI you need to carefully study all the registers of this module. Well and one more thing: SPI in STM32 can work in I2S mode (not to be confused with I2C!!!). I2S is an SPI-like interface for data transfer between digital audio devices. Don't let this confuse you, by default this module works in SPI mode and we will not consider registers that are needed only for I2S mode.

To connect two or more devices, you need 4 wires (+ground, where without it):
  • MOSI (Master Out / Slave In) - this wire carries data from master to slave device
  • MISO (Master In / Slave Out) - and here it is the other way around: data goes from the slave to the master.
  • SCK (Serial Clock) - a clock signal that goes from the master to the slave. At each new period of the clock signal, the bus Master sends a new data bit to the Slave, and the Slave in turn sends a data bit to the Master.
  • NSS (Slave select) is an optional wire that is needed if we have several slaves on the SPI bus. Thus, with the help of NSS we can select with which Slave we want to exchange data.
It should be noted that if we only read data from the slave device, we need only SCK and MISO wires, and if we only write to the slave device, we need SCK and MOSI wires.

SPI registers

Here I will describe the registers that apply only to SPI. Everything that relates to I2S has been thrown out.

SPI control register 1 (SPI_CR1)
Figure 1. SPI_CR1 register
image.png (9.17 KiB)
Figure 1. SPI_CR1 register Viewed 3620 times
BIDIMODE: Enable bidirectional data output operation mode.
  • 0: 2-wire operation mode with unidirectional data line transmission
  • 1: 1-wire operation mode with bidirectional data line transmission
BIDIOE: Enable bidirectional mode output operation

This bit, in conjunction with the BIDIMODE bit, selects the direction of transmission in bidirectional mode. In master mode, the MOSI pin is used for data transmission, while in slave mode, the MISO pin is used.
  • 0: Output disabled (receive only)
  • 1: Output enabled (transmit only)
CRCEN: Enable hardware CRC counting
  • 0: CRC calculation disabled
  • 1: CRC calculation enabled
CRCNEXT: The next data transmission will be terminated with a CRC code.
  • 0: Data transfer stage
  • 1: The next transmission will be completed by RCR transmission.
DFF: Data Frame Format
  • 0: Transmission frame size 8 bits
  • 1: Transmission frame size 16 bits
RXONLY: This bit, in conjunction with BIDIMODE, selects the transmission direction in 2-wire (MISO and MISO) mode.
  • 0: Full duplex - transmit and receive
  • 1: Output disabled - receive only
SSM: Software Slave Management. When the SSM bit is set, the NSS signal is replaced by the SSI bit value.
  • 0: Software Slave Management disabled
  • 1: Software Slave Management enabled
SSI: Internal Slave Selection. This bit operates only when the SSM bit is set. The value of this bit is forced on the NSS and the IO pin value of the NSS is ignored.

LSBFIRST: Frame Format
  • 0: MSB transmitted first
  • 1: LSB is transmitted first
SPE: Enable SPI
  • 0: SPI disabled
  • 1: SPI enabled
BR[2:0]: Selects the baud rate
  • 000: fPCLK/2
  • 001: fPCLK/4
  • 010: fPCLK/8
  • 011: fPCLK/16
  • 100: fPCLK/32
  • 101: fPCLK/64
  • 110: fPCLK/128
  • 111: fPCLK/256
MSTR: Select SPI operation mode: Master/Slave
  • 0: Slave mode
  • 1: Master mode
CPOL: Clock signal polarity
  • 0: CK to 0 at idle
  • 1: CK to 1 when idle
CPHA: Clock signal phase
  • 0: The first clock transition is a data capture edge
  • 1: The second clock transition is the edge of data capture
SPI control register 2 (SPI_CR2)
Figure 2. SPI_CR2 register
image.png (7.39 KiB)
Figure 2. SPI_CR2 register Viewed 3620 times
TXEIE: Interrupt emptying of the Tx data transfer buffer
  • 0: TXE interrupt prohibited
  • 1: TXE interrupt enabled. Used to generate an interrupt when the TXE flag is set.
RXNEIE: Interrupt of non-empty Rx receive buffer
  • 0: RXNE interrupt disabled
  • 1: RXNE interrupt is enabled. Used to generate an interrupt when the RXNE flag is set.
ERRIE: Interrupt when transmission errors occur. This bit controls the generation of an interrupt when one of the SPI errors (CRCERR, OVR, MODF) occurs.
  • 0: Interrupt on error occurrence prohibited
  • 1: Error interrupts are enabled.
SSOE: Enable SS output
  • 0: SS output is disabled in master mode and it is possible to work in multimaster mode.
  • 1: SS output is enabled in master mode and no multimaster operation is possible.
TXDMAEN: When this bit is set, a DMA request occurs when the TXE flag is set.

RXDMAEN: When this bit is set, a DMA request occurs when the RXNE flag is set.

SPI status register (SPI_SR)
Figure 3. SPI_SR register
image.png (7.27 KiB)
Figure 3. SPI_SR register Viewed 3620 times
BSY: Busy Flag. This flag is set and reset by hardware
  • 0: SPI is not busy
  • 1: SPI is busy with communication or the Tx transmit buffer is not empty.
OVR: Overflow flag.
  • 0: No overflow occurred
  • 1: Overflow occurred
CRCERR: CRC checksum error flag. This flag is set by hardware and reset by software by writing zero.
  • 0: The received CRC value matched the value of the SPI_RXCRCR register.
  • 1: The received CRC value did not match the value of the register SPI_RXCRCR
TXE: Transmitter buffer is empty
  • 0: Tx buffer is not empty
  • 1: Tx buffer empty
RXNE: Receiver buffer is not empty
  • 0: Rx buffer empty
  • 1: Rx buffer not empty
SPI data register (SPI_DR)
Figure 4. SPI_DR register
image.png (4.38 KiB)
Figure 4. SPI_DR register Viewed 3620 times
DR[15:0]: Data register. This register is divided into two buffers, one for writing (transmitter buffer) and one for reading (receiver buffer). A write operation to the SPI_DR register writes data to the transmitter buffer, and a read operation from SPI_DR returns a value from the receiver buffer.

SPI CRC polynomial register (SPI_CRCPR)
Figure 5. SPI_CRCPR register
image.png (4.65 KiB)
Figure 5. SPI_CRCPR register Viewed 3620 times
CRCPOLY[15:0]: CRC polynomial register, default is 0007h

SPI RX CRC register (SPI_RXCRCR)
Figure 6. SPI_RXCRCR register
image.png (4.25 KiB)
Figure 6. SPI_RXCRCR register Viewed 3620 times
RXCRC[15:0]: The CRC value of the received data. When CRC calculation is enabled, RXCRC contains the calculated CRC value of the received data. This register is reset to zero when the CRCEN bit in the SPI_CR1 register is set to one.

SPI TX CRC register (SPI_TXCRCR)
Figure 7. SPI_TXCRCR register
image.png (4.25 KiB)
Figure 7. SPI_TXCRCR register Viewed 3620 times
TXCRC[15:0]: The CRC value of the transmitted data. When CRC calculation is enabled, TXCRC contains the calculated CRC value of the transmitted data. This register is reset to zero when the CRCEN bit in the SPI_CR1 register is set to one.

Configuring SPI in Master mode without interrupts

After we have studied the SPI registers, let's start practicing. The task is to configure SPI1 in Master mode and start continuous sending of one byte without using interrupts. Well, let's go!

Let's call the initialization function SPI1_Init():

Code: Select all

void SPI1_Init(void)
{
}
Now we need to determine which pins of the microcontroller SPI1 is connected to. Open Reference manual, go to the section about GPIO, find 9.3.10 SPI1 alternate function remapping. There is a table like this:
Fig. 8 Table 56 in the Reference manual
image.png (21.74 KiB)
Fig. 8 Table 56 in the Reference manual Viewed 3620 times
Let's not bother with Remap for now. From Fig. 7 shows that SPI1 is connected to the GPIOA port to the following pins:
  • NSS - PA4
  • SCK - PA5
  • MISO - PA6
  • MOSI - PA7
And how can these pins be properly configured to work with SPI? In 9.1.11 GPIO configurations for device peripherals there is such a table:
Figure 9. Setting of port pins for work with SPI1
image.png (59.07 KiB)
Figure 9. Setting of port pins for work with SPI1 Viewed 3620 times
The red rectangles highlight the settings for our case. So, we have the necessary information, now we can customize. As we already know, before we start working with any peripheral, it is necessary to turn on the clock signal:

Code: Select all

//Enable SPI1 and GPIOA clocking
  RCC->APB2ENR |= RCC_APB2ENR_SPI1EN | RCC_APB2ENR_IOPAEN;
Next, configure the GPIO.

Code: Select all

 //First reset all configuration bits to zeros
  GPIOA->CRL &= ~(GPIO_CRL_CNF5_Msk | GPIO_CRL_MODE5_Msk
                | GPIO_CRL_CNF6_Msk | GPIO_CRL_MODE6_Msk
                | GPIO_CRL_CNF7_Msk | GPIO_CRL_MODE7_Msk);
  
  //Customize
  //SCK: MODE5 = 0x03 (11b); CNF5 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF5_Pos) | (0x03<<GPIO_CRL_MODE5_Pos);
  
  //MISO: MODE6 = 0x00 (00b); CNF6 = 0x01 (01b)
  GPIOA->CRL |= (0x01<<GPIO_CRL_CNF6_Pos) | (0x00<<GPIO_CRL_MODE6_Pos);
  
  //MOSI: MODE7 = 0x03 (11b); CNF7 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF7_Pos) | (0x03<<GPIO_CRL_MODE7_Pos);
Do not touch the NSS output as we will not use it. Next, the SPI setting:

Code: Select all

  SPI1->CR1 = 0<<SPI_CR1_DFF_Pos //Frame size 8 bits
    | 0<<SPI_CR1_LSBFIRST_Pos //MSB first
    | 1<<SPI_CR1_SSM_Pos //SSS program control
    | 1<<SPI_CR1_SSI_Pos //SS in high state
    | 0x04<<SPI_CR1_BR_Pos //Baud rate: F_PCLK/32
    | 1<<SPI_CR1_MSTR_Pos //Master mode (master)
    | 0<<SPI_CR1_CPOL_Pos | 0<<SPI_CR1_CPHA_Pos; // SPI operating mode: 0
We set it up like this: 8 bits, MSB first, CPOL/CPHA 00. It is worth paying special attention to SSM and SSI. Initialization of SPI module in Master mode is possible only with SS signal equal to one. I will not explain why it is so, I will only say that it comes from Multimaster mode. The SS signal can be received either from the NSS pin or the SSI bit of the CR1 register. If SSM is set to zero (default value), it will do a status check on NSS when SPI is enabled, and NSS is set as Input floating by default. Thus, if the NSS pin is a logic one, the initialization will complete successfully, otherwise nothing will happen and the MODF bit will be set in the SR register, which indicates a mode error. In addition, even after successful initialization, a low level on the NSS will disable SPI and reset the MSTR bit (from master mode will switch to slave). And if NSS is just hanging in the air, the system will not work at all. Therefore, set SSM and SSI to one.

All that remains now is to enable SPI1:

Code: Select all

SPI1->CR1 |= 1<<SPI_CR1_SPE_Pos; //Enable SPI
That's it, initialization in Master mode is complete! Here is the full code of the function:

Code: Select all

void SPI1_Init(void)
{
  //Turn on SPI1 and GPIOA clocking
  RCC->APB2ENR |= RCC_APB2ENR_SPI1EN | RCC_APB2ENR_IOPAEN;
  
  /**********************************************************/
  /*** Configuring GPIOA pins to work together with SPI1 ***/
  /**********************************************************/
  //PA7 - MOSI
  //PA6 - MISO
  //PA5 - SCK
  
  //First reset all configuration bits to zeros
  GPIOA->CRL &= ~(GPIO_CRL_CNF5_Msk | GPIO_CRL_MODE5_Msk
                | GPIO_CRL_CNF6_Msk | GPIO_CRL_MODE6_Msk
                | GPIO_CRL_CNF7_Msk | GPIO_CRL_MODE7_Msk);
  
  //Customize
  //SCK: MODE5 = 0x03 (11b); CNF5 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF5_Pos) | (0x03<<GPIO_CRL_MODE5_Pos);
  
  //MISO: MODE6 = 0x00 (00b); CNF6 = 0x01 (01b)
  GPIOA->CRL |= (0x01<<GPIO_CRL_CNF6_Pos) | (0x00<<GPIO_CRL_MODE6_Pos);
  
  //MOSI: MODE7 = 0x03 (11b); CNF7 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF7_Pos) | (0x03<<GPIO_CRL_MODE7_Pos);
  
  
  /**********************/
  /*** Setting SPI1 ***/
  /**********************/
  
  SPI1->CR1 = 0<<SPI_CR1_DFF_Pos //Frame size 8 bits
    | 0<<SPI_CR1_LSBFIRST_Pos //MSB first
    | 1<<SPI_CR1_SSM_Pos //SSS program control
    | 1<<SPI_CR1_SSI_Pos //SS in high state
    | 0x04<<SPI_CR1_BR_Pos //Baud rate: F_PCLK/32
    | 1<<SPI_CR1_MSTR_Pos //Master mode (master)
    | 0<<SPI_CR1_CPOL_Pos | 0<<SPI_CR1_CPHA_Pos; // SPI operating mode: 0
  
  SPI1->CR1 |= 1<<SPI_CR1_SPE_Pos; //Enable SPI
}
Now let's move on to data exchange. But first we need to touch a little bit on the SPI module device.
Fig. 10. SPI module block diagram
image.png (57.79 KiB)
Fig. 10. SPI module block diagram Viewed 3620 times
SPI has a Shift register, a transmit buffer (Tx buffer) and a receiver buffer (Rx buffer). There are three very interesting flags in the SR register: BSY, TXE and RXNE. The TXE flag is set if the transmitter buffer (Tx buffer) is empty and the next value can be loaded into it, RXNE is set to one if a new value has arrived in the receiver buffer (Rx buffer) and can be read. BSY is set if the SPI module is busy with a communication operation or if the transmitter buffer is not empty.

The logic of operation is as follows: the operation of writing to the DR register fills the transmitter buffer with a data frame (8 or 16 bits, depending on the setting), and the BSY flag is set, and TXE is reset. The value from the transmitter buffer is then loaded into the shift register and the SPI data transfer process is started, and the TXE flag is set to one, indicating that a new value can be loaded into the Tx buffer. If another value is loaded into the Tx buffer, TXE is reset to zero until the current data frame transfer is completed and the next Tx buffer value is loaded into the shift register.

With each new period of the SCK synchronization signal, the shift register spits out another bit into MOSI and pops a new data bit from MISO into its tail (this is true for Master mode, vice versa for Slave). After the last bit has been received, the shift register value is loaded from the receiver buffer (Rx buffer) and the RXNE flag is set. If no new value has been loaded into the Tx buffer, the data transfer is terminated and the BSY flag is reset to zero.

Code: Select all

Sending data to [i]SPI [/i]will look like this:

void SPI1_Write(uint16_t data)
{
  //Wait until the transmitter buffer is empty
  while(!(SPI1->SR & SPI_SR_TXE))
    ;
  
  //fill the transmitter buffer
  SPI1->DR = data;
}
It should be understood that the TXE flag only indicates that a new value can be added to the transmitter buffer, while the previous data frame can be transmitted at this time. If you want to make sure that ALL data has already been successfully sent to the slave device, use the BSY flag.

Here is the data reception:

Code: Select all

uint16_t SPI1_Read(void)
{
  SPI1->DR = 0; //start exchange
  
  //Wait until a new value appears
  //in the receiver buffer
  while(!(SPI1->SR & SPI_SR_RXNE))
    ;
  
  //return the value of the receiver buffer
  return SPI1->DR;
}
For the test, here is this main():

Code: Select all

void main()
{
  ClockInit();
  
  SPI1_Init();
  
  
  for(;;)
  {
    SPI1_Write(0x34);
  }
}
ClockInit() - initialization of the clocking system, see this article. Then SPI1 initialization and infinite loop with sending the value 0x34. To prove the correct operation of the program, here is an oscillogram:
Fig. 11. Oscillogram of the program operation, lower graph (blue) signal SCK, upper (yellow) signal on the MOSI pin
image.png (7.89 KiB)
Fig. 11. Oscillogram of the program operation, lower graph (blue) signal SCK, upper (yellow) signal on the MOSI pin Viewed 3620 times
Figure 11 shows that the data is streaming continuously without delay. That's great!

Configuring SPI in Master mode with interrupts

Now let's do the same thing, but only on interrupts. The task is as follows: we have a certain array of bytes, which must be spit out to SPI using interrupts. I will not go into the details of interrupts in STM32, for this will be a separate article, I will limit myself to the necessary minimum.

The peripheral module can have several events that can cause an interrupt, for SPI it is TXEIE, RXNEIE and ERRIE (see Fig. 2). However, the interrupt handler in most cases is only one: SPI1_IRQHandler(). Thus, if we have multiple event interrupts enabled, we need to look at the SR status register in SPI1_IRQHandler() to understand what happened.

In order for the interrupt to be triggered, we need to perform 3 actions:
  1. Enable the interrupt in the SPI module.
  • Enable the interrupt from the SPI in the NVIC. When any SPI enabled interrupt occurs, the SPI1_IRQHandler() handler will be called.
  • Allow interrupts globally (by default, after resetting the microcontroller, they are allowed).
The initialization function is almost no different from the previous example, only the line NVIC_EnableIRQ(SPI1_IRQn) is added. Here is the initialization code:

Code: Select all

void SPI1_Init(void)
{
  //Turn on SPI1 and GPIOA clocking
  RCC->APB2ENR |= RCC_APB2ENR_SPI1EN | RCC_APB2ENR_IOPAEN;
  
  /**********************************************************/
  /*** Configuring GPIOA pins to work together with SPI1 ***/
  /**********************************************************/
  
  //PA7 - MOSI
  //PA6 - MISO
  //PA5 - SCK
  //First reset all configuration bits to zeros
  GPIOA->CRL &= ~(GPIO_CRL_CNF5_Msk | GPIO_CRL_MODE5_Msk
                | GPIO_CRL_CNF6_Msk | GPIO_CRL_MODE6_Msk
                | GPIO_CRL_CNF7_Msk | GPIO_CRL_MODE7_Msk);
  
  //Customize
  //SCK: MODE5 = 0x03 (11b); CNF5 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF5_Pos) | (0x03<<GPIO_CRL_MODE5_Pos);
  
  //MISO: MODE6 = 0x00 (00b); CNF6 = 0x01 (01b)
  GPIOA->CRL |= (0x01<<GPIO_CRL_CNF6_Pos) | (0x00<<GPIO_CRL_MODE6_Pos);
  
  //MOSI: MODE7 = 0x03 (11b); CNF7 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF7_Pos) | (0x03<<GPIO_CRL_MODE7_Pos);
  
  /*
  //SS MODE4 = 0x03 (11b); CNF4 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF4_Pos) | (0x03<<GPIO_CRL_MODE4_Pos);
  */
  
  /**********************/
  /*** Setting SPI1 ***/
  /**********************/
  
  SPI1->CR1 = 0<<SPI_CR1_DFF_Pos //Frame size 8 bits
    | 0<<SPI_CR1_LSBFIRST_Pos //MSB first
    | 1<<SPI_CR1_SSM_Pos //SSS program control
    | 1<<SPI_CR1_SSI_Pos //SS in high state
    | 0x04<<SPI_CR1_BR_Pos //Baud rate: F_PCLK/32
    | 1<<SPI_CR1_MSTR_Pos //Master mode (master)
    | 0<<SPI_CR1_CPOL_Pos | 0<<SPI_CR1_CPHA_Pos; // SPI operating mode: 0
  
  NVIC_EnableIRQ(SPI1_IRQn); //Allow interrupts from SPI1
  
  SPI1->CR1 |= 1<<SPI_CR1_SPE_Pos; //Enable SPI
}
Next, we will need 3 global variables:

Code: Select all

int32_t tx_index = 0; //this stores the number of bytes transferred
int32_t tx_len = 0; //how many bytes to transfer
uint8_t *tx_data; //pointer to the array with transferred data
Then comes the function to start the SPI data transfer. It takes as input a pointer to the uint8_t array and the number of bytes to be transferred:

void SPI1_Tx(uint8_t *data, int32_t len)
{
if(len<=0)
return;

//Wait until SPI is free from the previous transfer
while(SPI1->SR & SPI_SR_BSY)
;

//Set the variables that will be
//used in the SPI interrupt handler
tx_index = 0;
tx_len = len;
tx_data = data;

//Resolve TXEIE interrupt and start the exchange
SPI1->CR2 |= (1<<SPI_CR2_TXEIE_Pos);
}

It works like this. In the initial state, the SPI is not transmitting any data and the TXE flag in the SR register is set to one. This means that if the TXEIE interrupt is enabled, it will be triggered immediately. After all the preliminary settings we enable the TXEIE interrupt, thus starting the process of sending data over SPI. The interrupt handler, where all the main work takes place, looks like this:

Code: Select all

void SPI1_IRQHandler(void)
{
  SPI1->DR = tx_data[tx_index]; //Write the new value to DR
  tx_index++; //increase the counter of transferred bytes by one
  
  //if all the bytes have been transferred, then disable the interrupt,
  // thus ending the data transfer
  if(tx_index >= tx_len)
    SPI1->CR2 &= ~(1<<SPI_CR2_TXEIE_Pos);
}
I think it's clear from the comments. Let's sketch a small main() for demonstration:

Code: Select all

uint8_t data[10];
void main()
{
  ClockInit(); //initialization of the clocking system
  
  SPI1_Init(); //initialization of SPI1
  
  //fill the data[] array with data
  for(int i=0; i<sizeof(data); i++)
  {
    data[i] = i+1;
  }
  
  //start data transfer
  SPI1_Tx(data, sizeof(data));
  
  //infinite loop
  //you can do something useful here
  for(;;)
  {
  }
}
And here are the oscillograms of the data transfer process. This is for data[] buffer length of 3 bytes:
Fig. 12: Sending a 3-byte buffer using interrupts
image.png (7.2 KiB)
Fig. 12: Sending a 3-byte buffer using interrupts Viewed 3620 times
Everything works correctly, as much as said - so much and sent Bytes go one after another without delay. That's great.

And this is how sending 10 bytes looks like:
Figure 13: Sending a 10-byte buffer using interrupts
image.png (7.64 KiB)
Figure 13: Sending a 10-byte buffer using interrupts Viewed 3620 times
That's all for now, the article has already turned out to be a big one. I haven't decided yet what will be in the next part, but we should make articles about NVIC interrupt controller and DMA direct memory access controller. And SPI in Slave mode should be considered.