How to use SPI in STM32
I have written many posts about interfacing I2C devices with STM32 but there are some devices which require only SPI to work i.e. SD card reader, TFT display etc. So today in this post, we are going to learn how to use SPI with STM32.
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.
I am going to show the working with an actual hardware. Because of lack of many SPI devices, I will work with whatever I have and that is ADXL345 accelerometer sensor. I have already covered a tutorial about How to use ADXL345 device with I2C. Do check it out because I am not going to explain the register part but only focus on How to read and write data using SPI.
Connection & Configuration
Below is the image showing the connection between the ADXL345 and the Nucleo F446.
The sensor is powered with 3.3V from the Nucleo board itself. The pin connections are as follows:
- The SCK (Clock) pin from the Nucleo is connected to the SCL (Clock) pin of the sensor.
- The MISO pin from the Nucleo is connected to the SDO (Serial Data Out) pin of the sensor.
- The MOSI pin from the Nulceo is connected to the SDA (Serial Data) pin of the sensor. This SDA pin acts as the SDI (Serial Data Input) when the SPI mode is used.
- The CS pin from the Nucleo is connected to the CS pin of the sensor.
CubeMX Configuration
Below is the image showing the clock configuration for the Nucleo F446.
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.
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.
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.
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).
Some Insight into the CODE
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.
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.
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.
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.
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
Next SET the MEASURE bit, RESET the SLEEP bit and SET the frequency in the WAKE UP bits
Next, in the DATA_FORMAT Register, Set the RANGE using D0 and D1.
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.
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.
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.
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.
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.