Home STM32 STM32 HAL STM32 SPI Communication Example – HAL Read/Write with ADXL345

How to use SPI in STM32  – Read/Write Using HAL (ADXL345 Sensor)

This tutorial explains how to configure and use the STM32 SPI peripheral with HAL libraries to communicate with SPI-based sensors like the ADXL345 accelerometer. You’ll learn to set up SPI master mode in STM32CubeMX, understand SPI wiring, configure CPOL/CPHA, and write HAL functions for SPI read/write operations. This guide is beginner-friendly and demonstrates real-time data exchange with a live hardware setup using STM32 and SPI.

How to use SPI in STM32

In this tutorial, I will demonstrate the implementation on actual hardware. Due to the limited availability of SPI devices, I will be using the ADXL345 accelerometer sensor for this example. I have already created a separate tutorial on interfacing the ADXL345 using the I²C protocol, which you can check out for a detailed explanation of the sensor’s register configuration. Here, our primary focus will be on how to perform data read and write operations using SPI.

VIDEO TUTORIAL

You can check the video to see the complete explanation and working of this project.

Check out the Video Below

Introducing the STM32 SPI Peripheral

The SPI (Serial Peripheral Interface) peripheral in STM32 microcontrollers is a high-speed, full-duplex communication interface designed for efficient data exchange between the MCU and external devices such as sensors, displays, memory chips, and other microcontrollers. It supports master and slave modes, allowing the MCU to either control peripheral devices or be controlled by another master. STM32 SPI offers flexible configuration options such as data frame size, clock polarity and phase, and multiple baud rate settings, making it suitable for a wide range of applications.

Some of its important features are:

  • Full-Duplex and Half-Duplex Communication – Allows simultaneous data transmission and reception or single-direction communication when required.
  • Configurable Clock Settings – Supports multiple clock polarity (CPOL) and clock phase (CPHA) configurations to ensure compatibility with various SPI devices.
  • DMA and Interrupt Support – Can be integrated with DMA or interrupt-driven modes for efficient, high-speed data transfers with minimal CPU load.
  • Multi-Slave Device Support – Can control multiple slave devices using dedicated chip-select (NSS) pins or software-managed NSS.
SPI Master Slave connection

SPI (Serial Peripheral Interface) generally requires 4 wires as shown above. The names are as follows:-

  • SCK –> Serial Clock.
  • MOSI –> Master out Slave In is used to send data to slave.
  • MISO –> Master In Slave Out is used to receive data from slave.
  • CE/CS –> Chip Select is used for selecting the slave.

SPI is not very different from I2C. It just require more wires and the process of selecting the slave is a little different. In order to enable a slave device, we need to pull the CS pin low and after our read or write is complete, just pull the pin high again. This will disable the slave device.

We can connect as many slaves as we want, but only 1 can be selected at a time. This is one of the major advantages SPI have over the I2C peripheral, where we can connect a maximum of 128 (27) devices to the same bus.

What Is SPI in STM32 and Why Use It?

SPI (Serial Peripheral Interface) is a high-speed, full-duplex protocol ideal for connecting STM32 to:

  • Accelerometers, gyros, magnetometers (e.g., ADXL345, MPU6050 in SPI mode)
  • Flash memory or EEPROM
  • SD cards (SPI mode)
  • Display modules (TFT, OLED)

Compared to I2C, SPI is faster, supports multiple slaves with dedicated CS lines, and is easier to manage in timing-sensitive applications.

WIRING DIAGRAM

Below is the image showing the connection between the ADXL345 and the Nucleo F446.

ADXL345 connection with STM32

The sensor is powered with 3.3V from the Nucleo board itself. The pin connections are as follows:

Nucleo PinSensor PinDescription
SCK (Clock)SCL (Clock)Provides the clock signal for SPI communication.
MISOSDO (Serial Data Out)Transfers data from the sensor to the Nucleo.
MOSISDA (Serial Data)Transfers data from the Nucleo to the sensor. In SPI mode, this SDA pin acts as SDI (Serial Data Input).
CSCSChip Select signal to enable or disable the sensor during communication.
ADXL345 pin description

CubeMX CONFIGURATION

Clock Configuration

Below is the image showing the clock configuration for the Nucleo-F446.

STM32 Clock Configuration

I have enabled the External Crystal to provide the clock. The Nucleo F446RE has 8MHz crystal on board and we will use the PLL to run the system at maximum 180MHz.

SPI Configuration

STM32 supports different SPI modes. The mostly used modes are Half Duplex & Full Duplex with Master or Slave modes.

In Half Duplex mode, the SPI uses only 3 wires, CS, SCLK and SDIO. The data is transmitted and received on the same line, therefore the STM32 can either send or receive data at a time.
On the other hand, in Full Duplex mode the SPI uses 4 wires, CS, SCLK, MOSI and MISO. The data is sent by the STM32 on the MOSI line and it is received on the MISO line. Full Duplex mode is used more widely in SPI communication.

As per the ADXL345 datasheet, the maximum SPI clock can be set to 5MHz, so we will keep our SPI clock below this value. Also the sensor follows the SPI MODE 3, CPOL=1, CPHA=1.

ADXL345 SPI details

The SPI Modes decides on which edge of the clock signal, the data will be sampled and on which edge it will be shifted out. Both Master and slave should be configured to use the same SPI Mode for the transmission to work.

Below is the image showing the SPI configuration. I am using SPI1 for the project.

STM32 SPI Configuration

The SPI is configured in Full Duplex mode, so the STM32 as master can send and receive data at the same time. The Data size is set to 8 Bits because the ADXL345 only supports 8 bit data transfer. The data will be arranged in MSB first format.

I have used the Prescaler of 16 to reduce the SPI clock to 2.8MB/s. This is to make sure that our SPI clock remain lower than 5MB/s (MAX for ADXL345). Also note that the CPOL is HIGH (1) and CPHA is set to 2 edge (CPHA=1).

STM32 SPI Master-Slave Setup and Clock Settings

When using STM32 as SPI Master:

  • Set SPI mode according to the slave’s CPOL/CPHA
  • Use MSB-first or LSB-first based on sensor requirements
  • Ensure SPI clock (SCK) frequency is within the slave’s limit
  • Always toggle CS (chip select) LOW before sending and HIGH after

For the ADXL345 sensor, use SPI mode 3 with CPOL = 1 and CPHA = 1. Set prescaler to stay under 5 MHz clock speed.

STM32 SPI Code Example – Read & Write Functions Using HAL

ADXL WRITE FUNCTION

Unlike I2C, the SPI does not have different slave addresses to identify the read and write operations. Therefore the master itself needs to inform the slave whether it wants to perform a write operation or a read operation. Along with that the master also need to inform whether it is performing a multibyte read or write.

Below is the image showing the write operation on the ADXL345 using SPI.

SPI 4-Wire Write

As you can see in the image above, there are 2 additional bits attached with the Address bits. The Address is only 6bit long and then we have the Multibyte (MB) bit and Read Write (R/W) bit. The R/W bit must be set to 0 for the write operation. Also the MB bit should be set to 1 for writing multiple bytes to the device.

Below is the function to write the data to the slave.

void adxl_write (uint8_t Reg, uint8_t data)
{
	uint8_t writeBuf[2];
	writeBuf[0] = Reg|0x40;  // multibyte write enabled
	writeBuf[1] = data;
	HAL_GPIO_WritePin (GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); // pull the cs pin low to enable the slave
	HAL_SPI_Transmit (&hspi1, writeBuf, 2, 100);  // transmit the address and data
	HAL_GPIO_WritePin (GPIOB, GPIO_PIN_6, GPIO_PIN_SET); // pull the cs pin high to disable the slave
}

The parameter of the function adxl_write are:

  • @Reg is the register address inside the slave device, where the master wants to write the data to.
  • @data is the data, the master wants to store in the above address.

Since we are writing 2 bytes, register address and data, we need to store both the bytes in an array. Then set the R/W bit to 0 for write operation and MB bit to 1 for multibyte operation. Our final data for the first byte transfer will be b01xxxxxx. Here the ‘x’ represents the 5 bit register address.

To send the data via the SPI, we need to follow the procedure as mentioned below.

  • Pull the CS low to select the slave.
  • Call the function HAL_SPI_Transmit to transmit the 2 byte array we just defined. The timeout for the transfer operation is set to 100ms.
  • Pull the CS pin high to disable the slave.

ADXL READ FUNCTION

Just like write operation, the master needs to inform the slave that this is a read operation. To do so, the R/W but must be set to 1, indicating the Read command.

Below is the image showing the write operation on the ADXL345 using SPI.

SPI 4-Wire read

As you can see in the image above, there are 2 additional bits attached with the Address bits. The Address is only 6bit long and then we have the Multibyte (MB) bit and Read Write (R/W) bit. The R/W bit must be set to 1 for the read operation. Also the MB bit should be set to 1 for reading multiple bytes from the device.

Below is the function to read the data from the slave.

void adxl_read (uint8_t Reg, uint8_t *Buffer, size_t len)
{
	Reg |= 0x80;  // read operation
	Reg |= 0x40;  // multibyte read
	HAL_GPIO_WritePin (GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);  // pull the cs pin low to enable the slave
	HAL_SPI_Transmit (&hspi1, &Reg, 1, 100);  // send the address from where you want to read data
	HAL_SPI_Receive (&hspi1, Buffer, len, 100);  // read 6 BYTES of data
	HAL_GPIO_WritePin (GPIOB, GPIO_PIN_6, GPIO_PIN_SET);  // pull the cs pin high to disable the slave
}

The parameter of the function adxl_write are:

  • @Reg is the register address inside the slave device, where the master wants to read the data from.
  • @Buffer is the pointer to the buffer where the the received data will be stored.
  • @len is the number of bytes, the master wants to read.

The R/W bit must be set to 1 for read operation and MB bit to 1 for multibyte operation. Our final data for the Register Address will be b11xxxxxx. Here the ‘x’ represents the 5 bit register address.

To read the data via the SPI, we need to follow the procedure as mentioned below.

  • Pull the CS low to select the slave.
  • Call the function HAL_SPI_Transmit to transmit the modified register address, where we want to read the data from. The timeout for the transfer operation is set to 100ms.
  • Call the function HAL_SPI_Receive to receive the data from the slave device. The 6 bytes of data will be stored in the Buffer array.
  • Pull the CS pin high to disable the slave.

ADXL INITIALIZATION

We will first check if the slave device is responding, by reading the ID Register (0x00). The ADXL should respond with the value 0xE5.

ADXL345 ID
void adxl_init (void)
{
	uint8_t chipID=0;
	adxl_read(0x00, &chipID, 1);

If the DEV_ID returned is 0xE5, we will proceed with the initialisation.

Now we will modify POWER_CTL Register (0x2D) and DATA_FORMAT Register (0x31). First RESET all bits of POWER_CTL register by writing 0 to them.

	if (chipID == 0xE5)
	{
		adxl_write (0x2d, 0x00);  // reset all bits; standby
		adxl_write (0x2d, 0x08);  // measure=1 and wake up 8hz

Set the MEASURE bit, RESET the SLEEP bit and SET the frequency in the WAKE UP bits

ADXL POWER_CTL

Next, in the DATA_FORMAT Register, Set the RANGE using D0 and D1.

ADXL DATA_FORMAT
ADXL Wakeup Bits
		adxl_write (0x31, 0x01);  // 10bit data, range= +- 4g
	}
}

The main function

Inside the main function, we will first initialise the ADXL.

int main()
{
  ....
  adxl_init();  // initialize adxl

We will write the rest of the code in the while loop.

  while (1)
  {
	  adxl_read (0x32, RxData, 6);

Here we will first read 6 bytes starting from the Register 0x32. The data is stored in the Registers 0x32 to 0x37 in the form of DATA X0, DATA X1, DATA Y0, DATA Y1, DATA Z0, DATA Z1.

ADXL Data Registers

Now we need to combine the DATA X0, DATA X1 into single 10 bit value and this can be done by

	  int16_t x = ((RxData[1]<<8)|RxData[0]);
          int16_t y = ((RxData[3]<<8)|RxData[2]);
          int16_t z = ((RxData[5]<<8)|RxData[4]);

Next we will convert this data into the g form in order to check for the acceleration in specific axis. As you can check above in the initialisation part, we have set the range of ±4 g. According to the datasheet, for the range of ±4 g, the sensitivity is 128LSB/g.

ADXL345 Sensitivity Configuration

So to convert into g, we need to divide the value by 128.

      xg = (float)x/128;
      yg = (float)y/128;
      zg = (float)z/128;

      HAL_Delay(1000);
  }
}

RESULT

You can check the result in the live expression of the debugger console. The image below shows the output on the console.

adxl outputs acceleration data

The image above shows the acceleration in all 3 axes when the sensor is placed normally on the table. The Acc in z axis is 1g, while in other axes, it is close to 0.

adxl outputs acceleration data

The image above shows the acceleration when the sensor is tilted in the y axis. The Acc in y-axis is -1g, while the other axes are close to 0.

You can watch the video below to see the complete working.

With this setup, STM32 communicates efficiently with SPI sensors using full-duplex data transfer. You’ve learned how to configure CubeMX, write HAL_SPI_Transmit() and HAL_SPI_Receive() code, and handle multi-byte SPI reads. This approach works not only for ADXL345 but for any SPI-compatible sensor, memory, or display device. The HAL_SPI functions are flexible and scalable for all STM32 series.

PROJECT DOWNLOAD

Info

You can help with the development by DONATING Below.
To download the project, click the DOWNLOAD button.

STM32 SPI Project FAQs

You May Also Like

Subscribe
Notify of

7 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
sekanan
6 months ago
#tle5012b
How do I continue to use it?
Can I use this code?
ansh bhatnagar
3 years ago

hi, i try the above step to implement spi1 in stm32f3 register all are same but i am not 100% during the transmission and reception i am getting some garbage data in the receiver buffer when check the spi->dr i found it showing 0xffff please suggest me what could i have done wrong so that spi1 work correctly.

Anh
5 years ago

can you use SPI in PL1167?

fengchongren
6 years ago

YOU can use the AD7705 , it also use the SPI to communication to the MCU , but if you use AD7705 ,you must modification the parameter

Lara
6 years ago

Hello, I have tried your same code for ADXL375 using a STM32F401 and truestudio. I have put the right values for the cpol and the cpha and my speed is less than 5MHz. However, when i try getting the device ID i get rubbish (the value changes each time i run the program) and when i read the different axes I get really strange values especially for the z axis. Basically, nothing is working. While debugging it seems that only MOSI line is working (MISO no). I have measured with oscilloscope as well but got no clear information. I really dont know what else to do. if you know what it could be it would help a lot.

Lara
Reply to  admin
6 years ago

I am using SPI because I am programming various ADXL375, so I would like to be able to at least gather measurements from one

keyboard_arrow_up