SPI Asynchronous DMA Topic is solved

ChibiOS public support forum for all topics not covered by a specific support forum.

Moderators: RoccoMarco, lbednarz, utzig, tfAteba, barthess

rj-ae
Posts: 4
Joined: Wed Apr 06, 2022 3:44 pm
Has thanked: 1 time

SPI Asynchronous DMA

Postby rj-ae » Wed Jul 06, 2022 10:44 am

Hi all,

I'm writing a piece of code for an STM32F4x to sample four channels of an external ADC (ADS8167) chip via SPI. The desired sampling frequency is 1kHz and ideally the four channels should be sampled as closely as possible in time. The external ADC samples on the slave select edges thus requiring this to be toggled. This would lead to four SPI bursts at the start of the period after which the system would remain idle until the cycle repeats. My preferred way of doing this would be to completely offload this to hardware timers and DMA, however, there is a hardware bug on our board such that I have to assert the slave select pin via software. After doing so I start a DMA transfer to read out the channel. On first glance this worked perfectly fine, however to be sure I checked the timing by toggling several GPIO pins. The result is shown below. Channel 2 shows the 'main' frequency of 1kHz. Each low-level pulse indicates a new period. Channel 0, indicates the DMA receive ISR at work for each pulse. As you see, these pulses are spaced at 100us, which is exactly equal to my system tick period. This observation has led to two questions:

  1. How can I decrease the burst period with ChibiOS? Is it possible to have the SPI low-level-driver callback run asynchronous to the ChibiOS system tick? This so I can decrease the burst period to 25us instead of 100us.
  2. Does this also mean the SPI interrupt uses 400us of system time? Or are other tasks handled in between the interrupts?

I checked the SPI IRQ priority and it is currently at 10. Tinkering with this did not change the behavior in any way. Is it at all possible to do what I want with ChibiOS or should I write my own code for it?

timing.png


Thanks for your help in advance!
Remi

steved
Posts: 823
Joined: Fri Nov 09, 2012 2:22 pm
Has thanked: 12 times
Been thanked: 135 times

Re: SPI Asynchronous DMA

Postby steved » Wed Jul 06, 2022 1:05 pm

Two possibilities that I can think of:

1. You can generate an interrupt each time SPI completes. Use that to start off the next transfer.
2. Use a spare hardware timer to generate interrupts at a faster rate.

rj-ae
Posts: 4
Joined: Wed Apr 06, 2022 3:44 pm
Has thanked: 1 time

Re: SPI Asynchronous DMA

Postby rj-ae » Wed Jul 06, 2022 1:34 pm

Thanks for your quick response @steved. At the moment I am doing what you have proposed. I have a callback configured for when the DMA finishes. When it does the interrupt configures a timer to wait for 25us (or less). This in turn calls another interrupt that starts the next transfer. However, the SPI DMA interrupt keeps lining up with the system tick clock instead of whatever lower value I set for the timer.

In a previous version I had a spare timer running at a faster rate that would launch a SPI transfer independent of the DMA transfer status, however this didn't work as the first SPI transfer would not finish before the timer set up a next transfer, leading to SPI DMA errors and the system entering the halt state.

Below the pieces of code that are related to this question. Hopefully that gives more insight into my setup.

The piece of code below sets up CC0 (the burst timer) and CC1 (main frequency 1kHz) of TIM5.

Code: Select all

void ads8167_configure(void) {
   utils_sys_lock_cnt();

   state = INIT;

   // ------------- Timer5 for external ADC sampling ------------- //
   TIM_DeInit(TIM5);

   TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
   TIM_OCInitTypeDef TIM_OCInitStructure;

   TIM5->CNT = 0;

   RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE);

   TIM_TimeBaseStructure.TIM_Prescaler = 0;
   TIM_TimeBaseStructure.TIM_Period = TIM5_CLOCK / ADC_SAMPLE_RATE; //  auto reload register value
   TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // count up till value of auto reload register
   TIM_TimeBaseStructure.TIM_ClockDivision = 0;
   TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
   TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); // sets counter, auto reload and prescaler registers

   TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
   TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
   TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
   TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High;
   TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set;
   TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Set;

   TIM_OCInitStructure.TIM_Pulse = TIM5_CLOCK / ADC_BURST_FREQUENCY;
   TIM_OC1Init(TIM5, &TIM_OCInitStructure);
   TIM_OC1PreloadConfig(TIM5, TIM_OCPreload_Disable);

   TIM_OCInitStructure.TIM_Pulse = TIM5_CLOCK / ADC_SAMPLE_RATE;
   TIM_OC2Init(TIM5, &TIM_OCInitStructure);
   TIM_OC2PreloadConfig(TIM5, TIM_OCPreload_Enable);

   TIM_ARRPreloadConfig(TIM5, ENABLE);
   TIM_CCPreloadControl(TIM5, ENABLE);

   // PWM outputs have to be enabled in order to trigger ADC on CCx
   TIM_CtrlPWMOutputs(TIM5, ENABLE);
   TIM_SelectInputTrigger(TIM5, TIM_TS_ITR1);
   TIM_SelectSlaveMode(TIM5, TIM_SlaveMode_Reset);

   // Enable TIM5
   TIM_Cmd(TIM5, ENABLE);

   // Enable CC1 and CC2 interrupt
   TIM_ITConfig(TIM5, TIM_IT_CC1, ENABLE);
   TIM_ITConfig(TIM5, TIM_IT_CC2, ENABLE);
   utils_sys_unlock_cnt();

   state = BURST;
   nvicEnableVector(TIM5_IRQn, 6);
}


The ads_isr is called from the TIM5_IRQHandler. Both CC0 and CC1 interrupts trigger this function.

Code: Select all

void ads_isr(void) {
   // Default TIM channel configuration mode values
   static TIM_OCInitTypeDef TIM_OCInitStructure = {
      .TIM_OCMode = TIM_OCMode_PWM1,
      .TIM_OutputState = TIM_OutputState_Enable,
      .TIM_OCPolarity = TIM_OCPolarity_High,
      .TIM_OCNPolarity = TIM_OCNPolarity_High,
      .TIM_OCIdleState = TIM_OCIdleState_Set,
      .TIM_OCNIdleState = TIM_OCNIdleState_Set
   };

   static uint8_t ain_counter = 0;
   switch(state) {
   case INIT:
      state = BURST;
      break;
   case BURST:

      // Determine next state
      if (ain_counter < HALL_SENSORS) {
         palClearPad(GPIOC, 6);
         state = BURST;
         ain_counter++;

         // Collect data
         chSysLockFromISR();
         spiSelectI(driver);
         spiStartReceiveI(driver, 4, (uint8_t *) &ADS_Value);

         // Ensure burst only continues after DMA receive has finished
         palTogglePad(GPIOC, 7);
         TIM_ITConfig(TIM5, TIM_IT_CC1, DISABLE);
         chSysUnlockFromISR();
      } else {
         state = ADS_RESET;
         palTogglePad(GPIOC, 6);

         // Wait for end of ADC Period
         TIM_ITConfig(TIM5, TIM_IT_CC1, DISABLE);
         TIM_ITConfig(TIM5, TIM_IT_CC2, ENABLE);
      }
      break;
   case ADS_RESET:
      ain_counter = 0;

      // TODO find a better way of integrating the position estimator
      // Run this before enabling the compare registers
      palClearPad(GPIOC, 8);
      chSysLockFromISR();
      calculate_position();
      chSysUnlockFromISR();
      palSetPad(GPIOC, 8);

      TIM_OCInitStructure.TIM_Pulse = TIM5_CLOCK / ADC_BURST_FREQUENCY;
      TIM_OC1Init(TIM5, &TIM_OCInitStructure);

      // Launch burst sequence
      TIM_ITConfig(TIM5, TIM_IT_CC1, ENABLE);
      TIM_ITConfig(TIM5, TIM_IT_CC2, DISABLE);

      state = BURST;
      break;
   case STOP:
      break;
   }
}


The SPI DMA callback. This configures CC0 of TIM5 for the next burst acquisition.

Code: Select all

void ads_spi_isr(void) {
   chSysLockFromISR();
   palSetPad(GPIOC, 6);
   spiUnselectI(driver);

   // Reset counter and continue
   TIM5->CNT = 0;
   ADC_Value[HW_ADC_CHANNELS + (ADS_Value >> 20)] = (uint16_t) ((ADS_Value & 0xff) << 8) | ((ADS_Value & 0xff00) >> 8);

   // Continue burst sequence
   TIM_ITConfig(TIM5, TIM_IT_CC1, ENABLE);
   palTogglePad(GPIOC, 7);
   chSysUnlockFromISR();
}


Thanks again! Cheers.

User avatar
Giovanni
Site Admin
Posts: 14444
Joined: Wed May 27, 2009 8:48 am
Location: Salerno, Italy
Has thanked: 1074 times
Been thanked: 921 times
Contact:

Re: SPI Asynchronous DMA

Postby Giovanni » Wed Jul 06, 2022 1:37 pm

Hi,

The SPI driver asynchronous functions are not synchronous with system tick, I don't see how that could happen.

Giovanni

rj-ae
Posts: 4
Joined: Wed Apr 06, 2022 3:44 pm
Has thanked: 1 time

Re: SPI Asynchronous DMA  Topic is solved

Postby rj-ae » Wed Jul 06, 2022 8:19 pm

I fixed it. It was more obvious than I expected, I just had to increase the clock speed on the SPI bus.. :shock:

Thanks anyways. At least it helped me to look somewhere else to find the problem!


Return to “General Support”

Who is online

Users browsing this forum: No registered users and 13 guests