STM32 + HAL + FreeRTOS Part III: SPI (blocking)

Serial Peripheral Interface (SPI) is quite widely used in embedded systems for connecting all kinds of ICs - sensors, memories, screens, you name it. It seems to be somewhat less popular among the beginners/Arduino crowd than I2C, because of relatively more complicated setup. But it does provide higher speeds and possibility to have more of the same type sensors on a single bus without addressing issues.

STM32F429I-Discovery board has an L3GD20 3-axis gyroscope onboard connected to SPI channel 5. In this article we'll try to get it up and running. I will continue where we left off last time - a working example with blinking LEDs and UART.

So, let us look at the devkit connections, as defined in Discovery board documentation:
  • MOSI is on PF9
  • MISO is on PF8
  • SCK is on PF7
  • Chip-select (CS) is in PC1
  • Interrupt 1 is on PA1
  • Interrupt 2 is on PA2
When we initially generated the code in STM32Cube MX in the first part of this series, I told to include SPI5 in peripheral list, for which to generate initialization code. If we take a look in main.h generated header file, we can find SPI5_XXX_Pin and SPI5_XXX_GPIO_PORT definitions are already there and defined as the ones we need:

#define SPI5_SCK_Pin GPIO_PIN_7
#define SPI5_SCK_GPIO_Port GPIOF
#define SPI5_MISO_Pin GPIO_PIN_8
#define SPI5_MISO_GPIO_Port GPIOF
#define SPI5_MOSI_Pin GPIO_PIN_9
#define SPI5_MOSI_GPIO_Port GPIOF
..
#define NCS_MEMS_SPI_Pin GPIO_PIN_1
#define NCS_MEMS_SPI_GPIO_Port GPIOC
..
#define MEMS_INT1_Pin GPIO_PIN_1
#define MEMS_INT1_GPIO_Port GPIOA
#define MEMS_INT2_Pin GPIO_PIN_2
#define MEMS_INT2_GPIO_Port GPIOA
In addition to that, NCS_MEMS_SPI has been defined. That would be our CS. And last bunch are interrupt pins. Neat.

Another thing we should worry about (it's a pretty common source of headache) are SPI bus setup. Generated setup in spi.c states:

  hspi5.Instance = SPI5;
  hspi5.Init.Mode = SPI_MODE_MASTER;
  hspi5.Init.Direction = SPI_DIRECTION_2LINES;
  hspi5.Init.DataSize = SPI_DATASIZE_8BIT;
  hspi5.Init.CLKPolarity = SPI_POLARITY_HIGH;
  hspi5.Init.CLKPhase = SPI_PHASE_2EDGE;
  hspi5.Init.NSS = SPI_NSS_SOFT;
  hspi5.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
  hspi5.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi5.Init.TIMode = SPI_TIMODE_DISABLE;
Let's review it and compare to the data in datasheet. Sadly, they don't just tell you mode SPI is working in, so you have to read the signal time chart and figure it out on your own:
  1. Instance: SPI5 is what we want, yes
  2. Mode: Master mode - we will be initiating the communications
  3. Direction: 2 Lines. That's what we have and want. Apparently, there's a support for single-wire half-duplex comms as well, but that's not for us
  4. Datasize: 8bit. True. All the registers are 8bit, wider words are sent as pairs of high/low bytes
  5. Clock polarity (CPOL): Idle state is high, so polarity is high
  6. Clock phase (CPHA): Sampling seems to be done on the trailing edge, so this should be SPI_PHASE_2EDGE
  7. NSS (Slave select) will be done in software for now, by manually pulling CS pin low before transfer, with subsequent pull back up once transfer is done. It is possible to use hardware NSS, but it requires disabling the SPI master after transfer to pull it back up (just an implementation in STM32 HAL)
  8. Baud Rate Prescaler is a clock divider, which sets the transfer speed. Leave it be for now
  9. First Bit should be the most significant bit (MSB) as per gyro datasheet
  10. We are not interested in TI mode for now, so leave it disabled
Now that we have set up our comms with the chip, we can start abusing it. I created a small single-shot task, which reads the gyroscope WHO_AM_I register and terminates itself. Let's add it to our freertos.c, where other tasks are already defined:
void vGyroTesterTask(void const * argument) {
 HAL_StatusTypeDef response = HAL_ERROR; // default to error 
 // 0x0F is WHO_AM_I register, 0x80 read bit, should return 0b11010100 or 0xD4
 uint8_t txbuf[3] = {0x0F | 0x80, 0x00}; 
 uint8_t rxbuf[3] = {0x00, 0x00};

 HAL_GPIO_WritePin(NCS_MEMS_SPI_GPIO_Port, NCS_MEMS_SPI_Pin, GPIO_PIN_RESET);
 response = HAL_SPI_TransmitReceive(&hspi5, txbuf, rxbuf, 2, 1000);
 HAL_GPIO_WritePin(NCS_MEMS_SPI_GPIO_Port, NCS_MEMS_SPI_Pin, GPIO_PIN_SET);
 if (response == HAL_OK) {
  printf("Sent: %02x %02x Got: %02x %02x\r\n", txbuf[0], txbuf[1], rxbuf[0], rxbuf[1]);
 } else {
  printf("Got error response as %d\r\n", response);
 }
 osThreadTerminate(gyroTaskHandle);
}
What is happening here, is:
  1. We define default response as error, just to be sure, that it gets changed to HAL_OK
  2. define a 2 byte wide transmit buffer (with last spot as NULL terminator) First is the byte (command) we send. We want to read (0x80) register 0x0F, then we send bunch of zeros as a dummy payload to give chance to the chip to respond. 
  3. define a 2 byte wide receive buffer (with last spot as NULL terminator). First byte is dummy receive, normally filled with 0xFF (no data), the second one will contain the response from the chip
  4. pull down our CS pin, to indicate to the slave, that we are talking to it
  5. Do the actual transceiving of the data
  6. pull back up the CS pin, to tell the chip, that we are done with it
  7. check the response status code and print out the data or code received
  8. kill the thread, for we are done with it
We could (and should) clean up resources as well, by de-initializing the SPI bus, but that's not necessary right now.

All that's remaining, is just starting the task and let it run. I will add a new handle and reduce the priorities of blinky tasks to avoid them interrupting our transmit, since it is in blocking mode, that might mess things up:

osThreadId defaultTaskHandle, blinkyTaskHandle, gyroTaskHandle;

..

void MX_FREERTOS_Init(void) {

 osThreadDef(defaultTask, StartDefaultTask, osPriorityLow, 0, 1000);
 defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

 osThreadDef(blinkyTask, vBlinkyTask, osPriorityLow, 0, 1000);
 blinkyTaskHandle = osThreadCreate(osThread(blinkyTask), NULL);

 osThreadDef(gyroTask, vGyroTesterTask, osPriorityHigh, 0, 1000);
 gyroTaskHandle = osThreadCreate(osThread(gyroTask), NULL);
}
And voila:


As expected, we get our notification, that system is up. First goes high-priority SPI task and then it continues with low priority blinking. The printout shows, that MCU sent 0x8F to the gyro and got back it's chip identification byte 0xD4, just as expected.

I'm not particularly interested in implementing full driver for the gyro, that can be left as an exercise for the reader. There should be plenty of those already available online. 

As usual, sources on GitHub have been updated to include all of the above. Next up: Part V: SPI with DMA

No comments:

Post a Comment