cancel
Showing results for 
Search instead for 
Did you mean: 

Interface with external ADC via SPI at 1MS/s

CPurc.1
Associate II

Hi all,

I am using the NUCLEO-F446RE with the STM32F446RE. I am trying to interface with an external dual sampling differential ADC: ADS9224R.

ADC Timing Diagram

0693W00000KZlwCQAT.pngThe ADC can provide samples at up to 3M/s. I would be happy to achieve ~0.8M/s sampling.

I have been struggling to meet the timing that I require and can only reach ~180kHz with the STM32. My problem is that initiating separate SPI receive calls takes too long between commands.

Running the following code to initiate back to back SPI reads:

HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); //CS LOW
  HAL_SPI_Receive(&hspi1, (uint8_t *)spi_buffer, 1,40);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); //CS high
 
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); //CS LOW
  HAL_SPI_Receive(&hspi1, (uint8_t *)spi_buffer, 1,40);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); //CS high

Produces the following timing:

0693W00000KZlxPQAT.png 

The STM32 is operating at 180MHz, and the SPI CLK is at ~30MHz. You can see that once the SPI initiates it is very fast, but the time to initiate the next SPI read is taking too long (~6us) which is limiting the rate at which I can read from the ADC.

I understand that this approach will only read ADC-0A, and not ADC-0B simultaneously. I have an implementation that uses QSPI but suffers from the same problem. I have simplified to single line SPI in hope that more people can provide help and I can transfer this to a dual line implementation.

Thank you all for your help!

1 ACCEPTED SOLUTION

Accepted Solutions

Ok, next plan, no code yet. If the 2nd SPI is slave, why not put both in slave mode (with circular RX DMA)? Then, we need another component, generating the clock pulses. This could be Timer TIM1 in one pulse mode and using a repetition counter of 7 (for 8 pulses). The pulse train (clock) is output in PWM GenerationCH1 mode.

How to trigger TIM1? Manually, for testing, worked.

Next: Automatically from TIM2. (Put TIM into slave trigger mode and route the TIM2 event to TIM1 ITR.

Don't have that ADC for testing.

Soon more...

KnarfB

View solution in original post

27 REPLIES 27
KnarfB
Principal III

HAL functions seem to be a bit heavy for tight timing. You may have go down to register level. This is almost trivial for the GPIO but not so much for the SPI. You might also generate low level (LL) code which is close to register level prog.

ADC sampling usually requires a low jitter. So triggering by software might not be feasible at all.

The most deterministic and "hardware only" way is using a timer event triggering SPI transfers using DMA. Here is an example: https://electronics.stackexchange.com/questions/353152/stm32f-how-to-config-dma-transfer-to-spi-triggered-by-timer

hth

KnarfB

Thanks for your help. I understand that this is not deterministic, but I was trying to produce the lowest possible timing by executing the HAL commands back to back, without having to handle the interrupt of a timer to potentially slow down the execution.

Would DMA speed up the operation, because the CPU does not need to handle writing the buffer to memory between each SPI receive call, allowing the next SPI call can be initiated sooner?

Is the timing I require actually realistic for my processor? I have not worked at these data rates before, I thought having to read an external ADC with SPI at ~1MS/s would be a reasonably common application.

Thanks

The timing you require is possible, but not realistic with HAL. The synchronization of the CS line provides an additional hurdle, unless you can keep it low.

As KnarfB says, triggering the SPI from a timer is your best bet here. You'll also need a DMA channel to read the incoming data.

If you feel a post has answered your question, please click "Accept as Solution".
MasterT
Senior III

Options:

a. Cascaded 2 Timers, first to generate CS (NSS), second - gated slave - to generate spi clock. Than you configure SPI as a slave and clocking data out of adc. SPI configured with DMA., both sides SPI and ADC pins interconnected and jumped to timers PWM outputs (except data pin, of course, this one goes directly to MISO)

The issue with stm32F4 is that timers don't have "Combine PWM" capability, so this trick I have successfully implemented on F7 & H7 uCPU may not work if ADC too sensitive for NSS-SCK timings phases.

b. Turn SPI into I2S mode. It solves problem that F4 spi don't drive CS(NSS) pin in hardware. If I2S mode, than Left-Right channel selector is your NSS driven by hardware. All it takes is to set I2S with DMA. I have interfaced with AD7988-5 / DAC80501 / and a lot others 16-24 bits ADC and DAC's. Especially this trick very jitterhelpful for 24-bits peripherals, since common SPI is 16-bits max, and driving something in software slow and creates a lot of jitter.

HAL driver limits I2S to 192 ksps, but there is a way:

  
  HAL_I2S_DMAPause( &hi2s2);
  Serial.print(F("\n\tchange i2s-2 clock to 250 ksps..."));
  delay(100);
  (*(uint32_t*)0x40003820) = 0x0000000C; 
// fS = I2SxCLK / [(32*2)*((2*I2SDIV)+ODD)*4)] 
// when the channel frame is 32-bit wide
  HAL_I2S_DMAResume( &hi2s2);

Hi,

here is an example, but its for another MCU (STM32L432KC @ 80 MHz SYSCLK) and writing SPI bytes @ 1 MHz by DMA under control of timer TIM2. Timer config:

0693W00000KZqHiQAL.pngYellow - conflict warnings for that Nucleo board, can be ignored.0693W00000KZqHnQAL.pngTimer runs at full speed with 80 ticks/µs.

Channel CH1 and CH2 generate the CS signal. CH1 the first, falling edge, CH2 the 2nd, rising edge. Edge positions can be freely configured as needed by their pulse values.

CH3 trailing (falling) edge triggers next byte to be sent. This is not fully done in the config but in the code.

CH4 is only a trigger for the attached LA. It toggles at each µs (timer overflow).

Cyclic DMA config in TIM2 tab:

0693W00000KZqJ0QAL.pngCode in main() before (empty) main loop:

HAL_DMA_Start(&hdma_tim2_ch3, (uint32_t)buffer, (uint32_t)&(SPI1->DR), 1);
  HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
  HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
  HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
  HAL_TIM_OC_Start(&htim2, TIM_CHANNEL_4);
  __HAL_SPI_ENABLE(&hspi1);
  __HAL_TIM_ENABLE_DMA(&htim2, TIM_DMA_CC3);

HAL_DMA_Start sets src and dst addresses. Destination is the SPI1 data register. Each write to it generates a SPI transfer. Then the timer channels are started, SPI is enabled and finally the DMA trigger.

Output:

0693W00000KZqK8QAL.pnghth

KnarfB

Thanks you KnarfB for the clear example, I will try this tomorrow. How would you go about reading the second SPI output (SDO-0B) in time with the first?

You could activate a second SPI as slave and feed the clock from 1st SPI to 2nd.

Also note that you have true double buffer (2 pointer) DMA avail on F446, see stm32f4xx_hal_dma_ex.h. This allows copy free buffer reception in contrast to my above code using cyclic (1 pointer) DMA.

hth

KnarfB

Thanks for the suggestion regarding the second slave SPI.

I have been trying to set up the TIM2 peripheral to match your screenshots within the CUBEIDE, however I am missing the "combined PWM1" mode for Channel 1. It only lets me select "PWM mode 1" or "PWM mode 2", as shown below:

0693W00000KZy9IQAT.pngSelecting PWM Mode 1 instead, produces an output on PA0 (TIM2_CH1) that pulses high for 0.1us every 1us. Changing the pulse parameter for channels 1 and 2, do not change the waveform. Is this because the "combined PWM1" mode was not used for Channel 1?

Thanks again for your help.

Ooops, combined PWM is not avail on F4. But, we don't really need it here. Normal PWM Mode 2 of a single channel, say CH1, yields a CS pulse (low active) starting at counter value 0 and ending at the programmed pulse value. You may then shift the value of the DMA triggering channel (CH3) a bit more to the left and should get the same timings.

hth

KnarfB