SPI using Registers in STM32

This is another tutorial in the register based series for STM32, and today we will cover the SPI. I will cover both sending and receiving data in the master mode. Also I am going to use the ADXL345 for the demonstration

SPI communication uses 4 Pins i.e

  • MOSI Master Out Slave In is used to send the data to the Slave
  • MISO Master in Slave out is used to receive the data from the slave
  • SCK Serial Clock is used to keep the master and slave in sync
  • SS Slave select is used to select and unselect the slave
  • In Full-duplex mode, all four pins are used
  • There are separate pins for send (MOSI) and for receive (MISO)
  • When we write data into the Data Register of the master, this data goes into the Tx Shift Register of the master
  • Which then gets shifted to the Rx Shift Register of the Slave
  • And finally goes into the data register of the slave, from where it is read by the slave device.

Slave select (NSS) pin management

In Master mode, Slave Select management can be done using two different ways

  • Software NSS management : This means that we want to manage the slave using the software. So we don’t have to rely on the physical NSS pin, and instead we can use any pin from the mcu as the Slave Select pin.
  • Hardware NSS management : Here we have to use the actual NSS pin (fixed pin) to control the slave device. The NSS signal is driven low as soon as the SPI is enabled in master mode, and is kept low until the SPI is disabled.

We will be using the software NSS management in this tutorial.

SPI Configuration

The configuration for the SPI is relatively simpler than what we have seen in other peripherals. Here we only have to modify 1 register. Let’s see the steps to configure the SPI

/************** STEPS TO FOLLOW *****************
1. Enable SPI clock
2. Configure the Control Register 1
3. Configure the CR2

Like I mentioned, it’s very simple to configure the SPI. See the configuration code below

void SPIConfig (void)
  RCC->APB2ENR |= (1<<12);  // Enable SPI1 CLock
  SPI1->CR1 |= (1<<0)|(1<<1);   // CPOL=1, CPHA=1
  SPI1->CR1 |= (1<<2);  // Master Mode
  SPI1->CR1 |= (3<<3);  // BR[2:0] = 011: fPCLK/16, PCLK2 = 80MHz, SPI clk = 5MHz
  SPI1->CR1 &= ~(1<<7);  // LSBFIRST = 0, MSB first
  SPI1->CR1 |= (1<<8) | (1<<9);  // SSM=1, SSi=1 -> Software Slave Management
  SPI1->CR1 &= ~(1<<10);  // RXONLY = 0, full-duplex
  SPI1->CR1 &= ~(1<<11);  // DFF=0, 8 bit data
  SPI1->CR2 = 0;
  • First of all enable the SPI 1 clock in the RCC_APB2ENR Register
  • Now will modify the CPOL and CPHA bits according to the slave requirement ( watch video if you don’t know what this means)
  • Enable the master mode
  • Next is the prescalar. The slave device can support upto 5 MBits/s, so I am keeping the presclalar of 16
    This will divide the APB2 clock (80 MHz) by 16, and bring it down to 5MHz
  • The data format is selected as MSB first
  • Then we will configure the software slave management. Like I said in the beginning, we will use the software to control the slave, so we need to set these SSM and SSI bits
  • Next we will configure the full duplex mode by resetting the RXONLY (10th) bit
  • Next is the data length bit, and we will keep the 8 bit data.
  • Then reset the entire CR2 register, since we will not be setting up any DMA or Interrupt in this tutorial


  • Before Transmitting the data, we will wait for the TXE (Transmit Register Empty) bit in the Status Register to set. This indicates that the Transmit Register is empty and we can load the data
  • After transmitting the data, we will wait for the BSY (Busy) bit in the Status Register to reset. This will indicate that the SPI is not busy in communication anymore and we can proceed with other things
void SPI_Transmit (uint8_t *data, int size)
	/************** STEPS TO FOLLOW *****************
	1. Wait for the TXE bit to set in the Status Register
	2. Write the data to the Data Register
	3. After the data has been transmitted, wait for the BSY bit to reset in Status Register
	4. Clear the Overrun flag by reading DR and SR
	int i=0;
	while (i<size)
	   while (!((SPI1->SR)&(1<<1))) {};  // wait for TXE bit to set -> This will indicate that the buffer is empty
	   SPI1->DR = data[i];  // load the data into the Data Register
/*During discontinuous communications, there is a 2 APB clock period delay between the
write operation to the SPI_DR register and BSY bit setting. As a consequence it is
mandatory to wait first until TXE is set and then until BSY is cleared after writing the last
	while (!((SPI1->SR)&(1<<1))) {};  // wait for TXE bit to set -> This will indicate that the buffer is empty
	while (((SPI1->SR)&(1<<7))) {};  // wait for BSY bit to Reset -> This will indicate that SPI is not busy in communication	
	//  Clear the Overrun flag by reading DR and SR
	uint8_t temp = SPI1->DR;
					temp = SPI1->SR;
  • Here we are waiting for the TXE bit to set before sending the data
  • To send the data, we have to copy it in the DR (Data Register)
  • After all the data has been transmitted, we will wait for the busy flag to reset
  • Before exiting the Transmit function, we will make a dummy read to the data register and the status register.
  • This is to clear the overrun flag, which gets set when we transfer the data to the device.


The receiving process is as follows:-

  • Before receiving the data, we must send some dummy byte to the device. Since the slave device is in the transmission mode, this dummy byte does not change the registers or the data of the registers
  • On receiving the dummy byte, the device transmits one byte of data.
  • This will set the RXNE (Receive Buffer Not Empty) bit. This will indicate that there is some data in the Data Register, which is ready to be read.
  • And later we can copy the data from the Data Register into our buffer. This clears the RXNE bit
void SPI_Receive (uint8_t *data, int size)
	/************** STEPS TO FOLLOW *****************
	1. Wait for the BSY bit to reset in Status Register
	2. Send some Dummy data before reading the DATA
	3. Wait for the RXNE bit to Set in the status Register
	4. Read data from Data Register

	while (size)
		while (((SPI1->SR)&(1<<7))) {};  // wait for BSY bit to Reset -> This will indicate that SPI is not busy in communication
		SPI1->DR = 0;  // send dummy data
		while (!((SPI1->SR) &(1<<0))){};  // Wait for RXNE to set -> This will indicate that the Rx buffer is not empty
	  *data++ = (SPI1->DR);
  • As you can see above, we wait for the busy flag to reset.
  • Then we send some dummy byte to the device. I am transmitting 0
  • next we wait for the RXNE bit to set
  • And finally copy the data from the DR into our buffer



I am using the following Pins for the SPI 1:

  • PA5 -> CLK
  • PA6 -> MISO
  • PA7 -> MOSI
  • PA9 -> Slave Select

The first three pins need to be set in the alternate function mode, and the PA9 will be set as the general output pin.

The Alternate function mode depends on the microcontroller. For example, let’s see the Alternate function description for F446RE

You can see in the picture above that the SPI 1 is the AF5. Also since I am using Pins PA5, 6 and 7, I am going to use the AFRL Register. In case you are using the Pins 8 to 15, you must use the AFRH register

Let’s see the configuration now

void GPIOConfig (void)
	RCC->AHB1ENR |= (1<<0);  // Enable GPIO Clock
	GPIOA->MODER |= (2<<10)|(2<<12)|(2<<14)|(1<<18);  // Alternate functions for PA5, PA6, PA7 and Output for PA9
	GPIOA->OSPEEDR |= (3<<10)|(3<<12)|(3<<14)|(3<<18);  // HIGH Speed for PA5, PA6, PA7, PA9
	GPIOA->AFR[0] |= (5<<20)|(5<<24)|(5<<28);   // AF5(SPI1) for PA5, PA6, PA7
  • First we will enable the GPIOA clock
  • Then select the Alternate Function mode for PA5, PA6 and PA7 and the output mode for PA9
  • Next we will set the speed for all four pins. The speed is set to HIGH Speed
  • And finally configure the Alternate Function in the AFR[0] (AFRL)


The Configuration in F103 is slightly different. F103 do not have the Alternate Function Registers and therefore we have to use the Control Register to configure the SPI pins

Below is the Picture from the F103 Reference manual, suggesting the configuration for the SPI

Since we are using SPI in Master Full Duplex mode, we will configure the pins accordingly. I have already covered how to configure the GPIO in F103, you can check the article STM32F103 Clock Setup using Registers

void GPIOConfig (void)
	RCC->APB2ENR |=  (1<<2);  // Enable GPIOA clock
	GPIOA->CRL = 0;
	GPIOA->CRL |= (11U<<20);   // PA5 (SCK) AF output Push Pull
	GPIOA->CRL |= (11U<<28);   // PA7 (MOSI) AF output Push Pull
	GPIOA->CRL |= (1<<26);    // PA6 (MISO) Input mode (floating)
	GPIOA->CRL |= (3<<16);    // PA4 used for CS, GPIO Output 
  • Here we will enable the GPIOA clock
  • Then reset the entire Control Register
  • Now set the AF output Push Pull mode for PA5 -> Clock
  • Do the Same for the PA7 -> MOSI
  • We need to set the Alternate Function input mode for the PA6 -> MISO
  • And set the general Output mode for the PA4 -> SS

Some Other Functions

Here are some other function that we will be using in this tutorial.

void SPI_Enable (void)
	SPI1->CR1 |= (1<<6);   // SPE=1, Peripheral enabled

void SPI_Disable (void)
	SPI1->CR1 &= ~(1<<6);   // SPE=0, Peripheral Disabled

void CS_Enable (void)
	GPIOA->BSRR |= (1<<9)<<16;

void CS_Disable (void)
	GPIOA->BSRR |= (1<<9);

float xg, yg, zg;
int16_t x,y,z;
uint8_t RxData[6];
void adxl_write (uint8_t address, uint8_t value)
	uint8_t data[2];
	data[0] = address|0x40;  // multibyte write
	data[1] = value;
	CS_Enable ();  // pull the cs pin low
	SPI_Transmit (data, 2);  // write data to register
	CS_Disable ();  // pull the cs pin high

void adxl_read (uint8_t address)
	address |= 0x80;  // read operation
	address |= 0x40;  // multibyte read
	uint8_t rec;
	CS_Enable ();  // pull the pin low
	SPI_Transmit (&address, 1);  // send address
	SPI_Receive (RxData, 6);  // receive 6 bytes data
	CS_Disable ();;  // pull the pin high

void adxl_init (void)
	adxl_write (0x31, 0x01);  // data_format range= +- 4g
	adxl_write (0x2d, 0x00);  // reset all bits
	adxl_write (0x2d, 0x08);  // power_cntl measure and wake up 8hz

Here I am keeping the focus on the SPI, so we will see only the SPI related part. I am not going to talk about how the ADXL actually works. You can check out the other article I wrote about it ADXL345 Accelerometer and STM32

  • To ENABLE the SPI, we will set the SPE bit (bit 6 in CR1)
  • To DISABLE SPI, we will simply clear that bit
  • The slave can be selected by Pulling the SS pin LOW
  • And to release the Slave, we have to Pull the SS pin HIGH

In order to communicate with the slave device, we have to follow the procedure as mentioned below

To send the data

  • Select the Slave device
  • Send the register address
  • Send the data
  • Release the slave device

To read the data

  • Select the slave device
  • Send the Register address, from where you want to read
  • Read the data
  • Unselect the slave device

The MAIN Function

int main ()
	SysClockConfig ();
	GPIOConfig ();
	TIM6Config ();
	SPIConfig ();
	SPI_Enable ();
	adxl_init ();
	while (1)
		adxl_read (0x32);		
		x = ((RxData[1]<<8)|RxData[0]);
		y = ((RxData[3]<<8)|RxData[2]);
		z = ((RxData[5]<<8)|RxData[4]);

	  xg = x*.0078;
    yg = y*.0078;
   	zg = z*.0078;
		Delay_ms (500);
  • Here we will configure the system, SPI, GPIO etc.
  • Then Initialize the ADXL
  • Read 6 Bytes of data from the ADXL
  • And later convert this data into the acceleration values


You can check out the picture from the keil debugger is shown below

The picture shows the values of the acceleration in the x, y and z axis

Check out the Video Below


You can help with the development by DONATING
To download the code, click DOWNLOAD button and view the Ad. The project will download after the Ad is finished.

3 Comments. Leave new

  • Hello! Could you please help me with this problem?
    ILI9341\ILI9341.axf: Error: L6218E: Undefined symbol __aeabi_assert (referred from touchgfxhal.o).
    ILI9341\ILI9341.axf: Error: L6218E: Undefined symbol __aeabi_vec_ctor_nocookie_nodtor (referred from application.o).
    ILI9341\ILI9341.axf: Error: L6218E: Undefined symbol typeinfo for touchgfx::Font (referred from constfont.o).

  • Cameron Pacileo
    May 19, 2021 1:12 AM

    Thank you so much! Love all of your tutorials. When using an st board as a slave device, I can successfully send data back to the master perfectly at a 1KHz rate. However I cannot check the byte first sent by the master without ruining the synchronization of the spi line. What do I have to do in order to read the byte sent first and then initiate sending the data from the slave? Thanks again


Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.


Adblocker detected! Please consider reading this notice.

We've detected that you are using AdBlock Plus or some other adblocking software which is preventing the page from fully loading.

We don't have any banner, Flash, animation, obnoxious sound, or popup ad. We do not implement these annoying types of ads!

We need money to operate the site, and almost all of it comes from our online advertising.

Please add controllerstech.com to your ad blocking whitelist or disable your adblocking software.