STM32F4 SPI read problem - Page 1
Forum section: electronics / general

author: Ulfberth
author_rank: Newbie
timestamp: February 02, 2023, 12:27:31 pm
content: HiI got problem with receiving data from nrf24l01+First I thought that this is hardware issue, and slave device doesn't send right data, so I plugged logic analyzer which showed me that hardware is ok.After that I launched debugger and found out that this is software issue, but I still didn't managed to find the mistakeSo what I'm trying to do is:1) Write register of slave.2) Read that register.so steps to write address are:1) Select NRF 2) Send register address + write cmd 3) Send register value 4) Release deviceAnd for read:1) Select NRF 2) Send register address + read cmd 3) Send 5 dummy bytes 4) save data to array 5) Release deviceThe problem is, 1st byte in array always wrong, the rest is right, but they are shifted by 1. And after 1 cycle 1st data in array is actually the one which should be lastIt's on screenshot below, instead of rxBuf=0x51 I get 1C then after cycle its 0x55...I assume mistake is in read function, but I can't figure it outCode and screenshots is belowSPI settingsCode:void spi1_init(void){// Enable clock for SPI2RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;// Configure SPI2SPI1->CR1 = 0; // Reset CR1 registerSPI1->CR1 |= SPI_CR1_BR_0; // Baud rate prescaler = 4 => 16/4=4mhzSPI1->CR1 |= SPI_CR1_MSTR; // Master modeSPI1->CR1 |= SPI_CR1_SSM | SPI_CR1_SSI; // Software slave managementSPI1->CR1 |= SPI_CR1_SPE; // Enable SPI2}void spi1_gpio_init(void) // PA4 - SPI1_NSS | PA5-SPI1_SCK | PA6-SPI1_MISO| PA7-SPI1_MOSI{RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; //Enable GPIOA ClockGPIOA->MODER |= GPIO_MODER_MODER4_0; //PA4 GPIO OUT modeGPIOA->MODER |= GPIO_MODER_MODER5_1; //Enable Alternate function mode for GPIOA PA5GPIOA->MODER |= GPIO_MODER_MODER6_1; //Enable Alternate function mode for GPIOA PA6GPIOA->MODER |= GPIO_MODER_MODER7_1; //Enable Alternate function mode for GPIOA PA7GPIOA->AFR |= 0x5UL<<GPIO_AFRL_AFSEL5_Pos; //GPIO alternate function set 0101 AF5 SPI PA5GPIOA->AFR |= 0x5UL<<GPIO_AFRL_AFSEL6_Pos; //GPIO alternate function set 0101 AF5 SPI PA6GPIOA->AFR |= 0x5UL<<GPIO_AFRL_AFSEL7_Pos; //GPIO alternate function set 0101 AF5 SPI PA7}code for read and write functionsCode:void test_read(void){uint8_t rxBuf={0};GPIOA->BSRR = GPIO_BSRR_BR4;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = NRF24_REG_TX_ADDR | NRF24_CMD_R_REGISTER;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = 0xFF;while (!(SPI1->SR & SPI_SR_RXNE));rxBuf=SPI1->DR;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = 0xFF;while (!(SPI1->SR & SPI_SR_RXNE));rxBuf=SPI1->DR;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = 0xFF;while (!(SPI1->SR & SPI_SR_RXNE));rxBuf=SPI1->DR;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = 0xFF;while (!(SPI1->SR & SPI_SR_RXNE));rxBuf=SPI1->DR;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = 0xFF;while (!(SPI1->SR & SPI_SR_RXNE));rxBuf=SPI1->DR;while (!(SPI1->SR & SPI_SR_TXE));while (SPI1->SR & SPI_SR_BSY);GPIOA->BSRR = GPIO_BSRR_BS4;}void test_write(void){GPIOA->BSRR = GPIO_BSRR_BR4;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = NRF24_REG_TX_ADDR | NRF24_CMD_W_REGISTER;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = 0x51;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = 0x52;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = 0x53;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = 0x54;while (!(SPI1->SR & SPI_SR_TXE));SPI1->DR = 0x55;while (!(SPI1->SR & SPI_SR_TXE));while (SPI1->SR & SPI_SR_BSY);GPIOA->BSRR = GPIO_BSRR_BS4;}

author: wek
author_rank: Frequent Contributor
timestamp: February 02, 2023, 01:09:41 pm
content: Basically, after *each* Tx you should wait for RXNE and read out the received byte.JW

author: Ulfberth
author_rank: Newbie
timestamp: February 02, 2023, 01:28:35 pm
content: Is my sequence wrong?My steps when I read from spi:1) I wait for TXE ( transmit buffer empty)2) I put data into tx DR3) I wait for RXE (Receive buffer not empty)4) I take data from rx DR and put it into array

author: langwadt
author_rank: Super Contributor
timestamp: February 02, 2023, 01:48:22 pm
content: Quote from: Ulfberth on February 02, 2023, 01:28:35 pmremember that every transmit is also a receivetry waiting for RXE and doing a dummy read of DR after sending the read command

author: Sherlock Holmes
author_rank: Frequent Contributor
timestamp: February 02, 2023, 01:53:54 pm
content: Quote from: Ulfberth on February 02, 2023, 01:28:35 pmI wrote this last year, the SPI to NRF code works correctly.https://github.com/Steadsoft/embedded/tree/main/NucleoF446RE/NRF24These are just apps, using the API I created, this is for testing RX functionality, all STM32https://github.com/Steadsoft/embedded/blob/main/NucleoF446RE/NRF24/Receiver/nrf_hal_rx_project.cSo focus on the API libraries I created, that's where the real effort went.

author: Ulfberth
author_rank: Newbie
timestamp: February 02, 2023, 02:07:07 pm
content: OMG thanks guys, I was very inattentive and totally forgot that when I send read command of course I have to do dummy read, that's where this weird byte came from.Although it's still unclear for me.Why it is 0x1C I get? because if you look at logic analyzer, it shows that there is 0x0E on MISO like when 0x10(read command) sent, so where this 1C comes from?Also thanks for API's I will take a look at them, after I will manage to get NRF24 working with low level functions

author: peter-h
author_rank: Super Contributor
timestamp: February 02, 2023, 03:29:42 pm
content: Also remember that the receiver should be cleared outbeforeyou load the first byte into the transmitter.This is in case you get e.g. an interrupt or an RTOS switch after loading TX, and then when you clear out RX you end up dumping good data.I have moved most of my SPI stuff to DMA, because software servicing is such a hassle while DMA works really well, and with a negligible CPU overhead. See e.g. this threadhttps://www.eevblog.com/forum/microcontrollers/32f417-spi-running-at-one-third-the-speed-it-should/msg3940687/#msg3940687The 32F4 peripherals are quite slow because they are running at a fraction of the CPU clock speed, and apparently the intention was to use DMA for everything, so ST provided very little buffering on SPI and UARTs.Also carefully check the /CS timing (before and after). IMHO 53.4% of the code found on github doesn't work in the 21st century because it was done on 16MHz chips and nobody checked the timing, so it worked by luck, and when you get to today's 168MHz+ chips, the timing breaks. By 53.4% I mean 53.4% of the portion of github code thateverworked, which is at best 50%

author: Ulfberth
author_rank: Newbie
timestamp: February 03, 2023, 07:09:29 am
content: Yes, I will go for DMA of course, but since I'm not well experience in stm32 and in C, my current goal is to make simple version without DMA or interupts and after it's done, I will go for next step.Eventually I want to create something similar to FPV system for a toy car, based on stm32f407, ov7670 or ov2640 and nrf24l01, I chose those parts simply because I have them laying aroundI know that's not an easy task especially for beginner and such project will need DMA and even some kind of image compression because nrf24 would not be able to send image at 25 fps

author: peter-h
author_rank: Super Contributor
timestamp: February 03, 2023, 11:45:21 am
content: Here is an example source for 32F417 SPI and DMA.Code:/*** DMA-only version of HAL_SPI2_TransmitReceive() but fixed for SPI3 and xx-only options added.** For use where fast transfers are needed, on the limit of SPI3 speed so with zero gaps. This is impossible* to do by polling at 10.5mbps or 21mbps and is probably marginal at 5.25mbps. The 16 bit SPI mode just* manages gap-free with polling but works only with even block sizes, and has the "first byte problem" which* DMA gets around.** ** DMA ONLY SO NO CCM ACCESS SO THE TWO BUFFERS HAVE TO BE "STATIC" **** This function is blocking so the caller can set CS=1 right away (check device data sheet!). A non-blocking* version would make sense only if transmitting only (txonly=true) but the caller would have to tidy up* the DMA and SPI3->CR2.** _spi3_set_mode() can still be used to set up the clock speed, phase, etc; this function does nothing* to SPI3 other than to enable/disable DMA. It does however make sense only if SPI3 is a Master.** Two modes, obviously mutually exclusive, for tx-only and rx-only:* If txonly, dumps rx data so you don't need to allocate a buffer for it* If rxonly, transmits all-0x00 so you don't need to feed SPI with some "known garbage"** The rx-only mode is superfluous in most cases but it does avoid shifting non-0x00 data to the device* while we are reading data out of it. With some devices this can matter. The ADS1118 ADC is one such.** Because this function needs to work with _spi3_set_mode(), care is taken to not modify the SPI config.** The yield parameter just releases control to RTOS, so equal-priority tasks don't have to wait until DMA* finished. This might be useful for long/slow DMA transfers. Usually not useful.* 31/8/22 if true, worsens corruption of incoming USB VCP data, a lot more than it is corrupted* anyway!** Returns (false) if memory is in CCM, size=0, null pointers...** NULL pointers are allowed if using the txonly or rxonly modes; then the unused one can be NULL.***/bool SPI3_DMA_TransmitReceive(uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, bool txonly, bool rxonly, bool yield){// Check for invalid inputsif ( Size==0 ) return (false);if ( (pTxData==NULL) && !rxonly ) return (false);if ( (pRxData==NULL) && !txonly ) return (false);uint32_t txadd = (uint32_t) pTxData;uint32_t rxadd = (uint32_t) pRxData;static uint8_t txonly_target=0;// must not be in CCMstatic uint8_t rxonly_source=0;// must not be in CCM// Test for CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K (linkerscript.ld)if ( ((txadd>=0x10000000) && (txadd<0x10010000)) || ((rxadd>=0x10000000) && (rxadd<0x10010000)) ){return(false);}// DMA ch 1 clock enable already done in b_main.c//RCC->AHB1ENR |= (1u << 21);// DMA1EN=1 - DMA1 clock enable//hang_around_us(1);// give it a chance to wake upSPI3->CR1 &= ~SPI_CR1_SPE;// disable SPI// DMA1 Ch 0 Stream 0 is SPI3 RXDMA1_Stream0->CR = 0;// disable DMA so all regs can be written// Wait until DMA clears the EN bit by itself.// Should be pointless since the previous transfer waited for end of transfer (hardware bug)//while ( (DMA1_Stream0->CR & DMA_SxCR_EN) != 0) {};DMA1->LIFCR = (0x03d << 0);// clear int flags & transfer complete - 111101 stream 0DMA1_Stream0->NDTR = Size;if (txonly){DMA1_Stream0->M0AR = (uint32_t) &txonly_target;// memory address to dump rx data to}else{DMA1_Stream0->M0AR = rxadd;// memory address in normal mode}DMA1_Stream0->PAR = (uint32_t) &(SPI3->DR);// peripheral addressDMA1_Stream0->FCR = 0;// direct modeif (txonly){DMA1_Stream0->CR = 0 << 25// CHSEL: ch 0| 0 << 23// MBURST: memory burst - single transfer| 0 << 21// PBURST: peripheral burst - single transfer| 3 << 16// PL: highest priority| 0 << 15// PINCOS: no peripheral address increment offset| 0 << 13// MSIZE: memory data size: byte| 0 << 11// PSIZE: peripheral data size: byte| 0 << 10// MINC: memory address increment: 0| 0 << 9// PINC: peripheral address increment: 0| 0 << 8// CIRC: no circular mode| 0 << 6// DIR: peripheral to memory| 0 << 5// PFCTRL: DMA is flow controller| 1 << 0;// EN: enable stream}else{DMA1_Stream0->CR = 0 << 25// CHSEL: ch 0| 0 << 23// MBURST: memory burst - single transfer| 0 << 21// PBURST: peripheral burst - single transfer| 3 << 16// PL: highest priority| 0 << 15// PINCOS: no peripheral address increment offset| 0 << 13// MSIZE: memory data size: byte| 0 << 11// PSIZE: peripheral data size: byte| 1 << 10// MINC: memory address increment: 1| 0 << 9// PINC: peripheral address increment: 0| 0 << 8// CIRC: no circular mode| 0 << 6// DIR: peripheral to memory| 0 << 5// PFCTRL: DMA is flow controller| 1 << 0;// EN: enable stream}// DMA1 Ch 0 Stream 7 is SPI3 TXDMA1_Stream7->CR = 0;// disable DMA so all regs can be written// Wait until DMA clears the EN bit by itself.// Should be pointless since the previous transfer waited for end of transfer (hardware bug)//while ( (DMA1_Stream7->CR & DMA_SxCR_EN) != 0) {};DMA1->HIFCR = (0x03d << 22);// clear int flags & transfer complete - 111101 stream 7DMA1_Stream7->NDTR = Size;if (rxonly){DMA1_Stream7->M0AR = (uint32_t) &rxonly_source;// memory address to fetch dummy tx data from}else{DMA1_Stream7->M0AR = txadd;// memory address in normal mode}DMA1_Stream7->PAR = (uint32_t) &(SPI3->DR);// peripheral addressDMA1_Stream7->FCR = 0;// direct modeif (rxonly){DMA1_Stream7->CR = 0 << 25// CHSEL: ch 0| 0 << 23// MBURST: memory burst - single transfer| 0 << 21// PBURST: peripheral burst - single transfer| 0 << 16// PL: priority low| 0 << 15// PINCOS: no peripheral address increment offset| 0 << 13// MSIZE: memory data size: byte| 0 << 11// PSIZE: peripheral data size: byte| 0 << 10// MINC: memory address increment: 0| 0 << 9// PINC: peripheral address increment: 0| 0 << 8// CIRC: no circular mode| 1 << 6// DIR: memory to peripheral| 0 << 5// PFCTRL: DMA is flow controller| 1 << 0;// EN: enable stream}else{DMA1_Stream7->CR = 0 << 25// CHSEL: ch 0| 0 << 23// MBURST: memory burst - single transfer| 0 << 21// PBURST: peripheral burst - single transfer| 0 << 16// PL: priority low| 0 << 15// PINCOS: no peripheral address increment offset| 0 << 13// MSIZE: memory data size: byte| 0 << 11// PSIZE: peripheral data size: byte| 1 << 10// MINC: memory address increment: 1| 0 << 9// PINC: peripheral address increment: 0| 0 << 8// CIRC: no circular mode| 1 << 6// DIR: memory to peripheral| 0 << 5// PFCTRL: DMA is flow controller| 1 << 0;// EN: enable stream}// Config SPI3 to let DMA handle the data. These need to be cleared when transfer complete!// This starts the transferSPI3->CR2 |= 3;// TXDMAEN, RXDMAEN: 11 - both set in one goSPI3->CR1 |= (1<<6);// SPE=1 enable SPI// Wait for DMA to finish. Blocking is necessary to prevent device CS=1 too early.// There could be a timeout here but a failure is impossible short of duff silicon, because// we are a Master and generating the SPI clock.// 20/11/22: A primitive timeout has been implemented anyway.volatile uint32_t limit = 0;while(true){limit--;if (limit==0) break;// Either end-transfer detection method below works but the NDTR may be less reliable// http://efton.sk/STM32/gotcha/g20.html// https://www.eevblog.com/forum/microcontrollers/32f4-3-ways-to-detect-end-of-dma-transfer/#if 0uint16_t temp1;temp1 = DMA1_Stream0->NDTR;if ( temp1 == 0 ) break;// transfer count = 0#elseuint32_t temp2;temp2 = DMA1->LISR;if ( (temp2 & (1<<5)) !=0 ) break;// TCIF0#endifif (yield) osDelay(1);// release to RTOS (see notes in comments)// Third method of detecting end of transfer: wait for DMA to set EN to 0.// All 3 methods produce the same time from last SPI clock to CS=1 (1.8us).//if ( (DMA1_Stream0->CR & DMA_SxCR_EN) == 0) break;}SPI3->CR2 &= ~3;// TXDMAEN, RXDMAEN: 00 - both cleared in one go// Clear int pending flags. They get cleared at the top of this function anyway, but...DMA1->LIFCR = (0x03d << 0);// clear int flags & transfer complete - 111101 stream 0DMA1->HIFCR = (0x03d << 22);// clear int flags & transfer complete - 111101 stream 7// Clear any rx data and the overrun flag in case not all received data was read// A small delay is needed because the very last transfer may not propagate to DR.// Not needed because RX DMA has read out everythinghang_around_us(1);SPI3->DR;SPI3->DR;SPI3->SR;SPI3->CR1 &= ~(1<<6);// SPE=0 disable SPIreturn (true);}

author: Sherlock Holmes
author_rank: Frequent Contributor
timestamp: February 03, 2023, 05:00:08 pm
content: Quote from: Ulfberth on February 02, 2023, 02:07:07 pmI spent a lot of time refining the two libraries that are in that project's repo. I too was monitoring everything with a scope and logic analyzer.The small API presented in that library contains most of the details that I addressed and were put together painstakingly and with input from numerous blogs and so on.The two test projects 'Transmitter' and 'Receiver' are very draft, they were really just heavily exercising the NRF device, hammering it with data, the 'Receiver; us just a slightly tweaked copy of the transmitter code so isn't even receiving yet.The 'Transmitter' project is the one I used as I tested, very heavily running for hours on end, the interrupt handling was the last stuff I added. Of course that code is tied to the board I used and that code is confined to these functions:https://github.com/Steadsoft/embedded/blob/main/libraries/nrf24_hal_support/nrf24_hal_support.c#L58https://github.com/Steadsoft/embedded/blob/main/libraries/nrf24_hal_support/nrf24_hal_support.c#L73https://github.com/Steadsoft/embedded/blob/main/libraries/nrf24_hal_support/nrf24_hal_support.c#L121It was during this work that I began to fatigue over how unhelpful the C language can be, in order to create clean, tidy, readable code I had to spend a great deal of time doing donkey work, so I've postponed that NRF work as I start looking at designing a better programming language.This was one of themore helpful blogs, worth a read.

author: Sherlock Holmes
author_rank: Frequent Contributor
timestamp: February 03, 2023, 05:21:35 pm
content: Quote from: Ulfberth on February 02, 2023, 02:07:07 pmThat1Crings a bell, its been like 10 weeks now since I even looked at the NRF stuff so I've gone rusty, if you'd posted your question back then I'd have been right in there with you!Note that whenever you send a command opcode, the NRFconcurrentlysends back the status register, so it could be that that you're seeing. SPI is basically just shift registers and there are two so as it shifts the bits in that you are sending, it has to shift something out, so it might as well havesomeuse, that's why they shift the status out, just to avoid wasting that opportunity.That characteristic is embedded in the API, all NRF command calls rely on_ReadSingleByteRegisteror_WriteSingleByteRegisterand thesealwaysreturns the status, since the very act ofissuing a commandalwayscauses the status to come back anyway, so as a convenience I always return it.Each of the numerous API calls include that last status arg and I suspect that's what you're seeing - although the bit pattern isn't quite what I'd expect...This is theNRF specI used by the way.I spent many hours on all this and was often puzzled, but I got it to make total sense in the end and the stuff I was seeing on the scope was always understood, always made sense to me but only after weeks n weeks of experimentation.