cancel
Showing results for 
Search instead for 
Did you mean: 

Synchronous Sampling over SPI - Trigger SPI transfers via timer

gcharles
Associate II

The issue being addressed is synchronous sampling of an ADC over SPI with a STM32G474 operating as the SPI master.  The phase noise is material and can not tolerate cycle to cycle jitter.  The ADC bases its timing from the falling edge of the slave select signal.  My approach is to use one timer (TIM16) to trigger a second timer (TIM5) which operates at the sampling frequency such at a programmable interval from 1 second to 16 seconds 1024 samples from the ADC are taken at 1 MSps.  TIM5 is operated in PWM mode generating an active low pulse acting as the slave select with the crystal based STM32G474 providing a reasonably low phase noise timing reference.  I've setup a DMA, channel 3, to trigger on the TIM5 channel 1 compare result with the DMA set to perform 16-bit transfers from memory to peripheral (SPI2 Data Register) with the number of words to transfer set to 2 to generate a 32-bit SPI transaction.  Everything works as expected except the DMA only transfers the first 16-bit word (half word as configured in the  STM32CubeMX GUI).  The DMA channel 3 DMA_CNDTR3 register shows the initial number of data to be transferred as 2 and decrements to 1, but not to 0.  The SPI transfer only includes 16-clocks.  I can create a 32-bit transfer, by preloading the SPI2 data register with two writes of 16-bits and having the DMA transfer a 16-bit field enabling the SPI2 via the SPI_CR1_SPE bit to the SPI2 Configuration Register 1 to start the transfer; however, this occurs after the falling edge of the TIM5 generated slave select.  This in combination with the CPOL being set to 1 and the SPI forcing the serial clock to 0 when not enabled violates the ADCs SPI timing specification.  I believe the use of TIM16 to trigger TIM5 is not relevant to the issue.

I've included the relevant code below.  It would be appreciated if someone could point out something I'm missing in the configuration to enable the desired behavior or enlighten me on why the DMA is not behaving as I expect it should.  If my chosen configuration can not meet the requirements I've listed then suggestions would also be appreciated.

  

 

SPI_HandleTypeDef hspi2;

TIM_HandleTypeDef htim5;
TIM_HandleTypeDef htim16;
DMA_HandleTypeDef hdma_tim5_ch1;

int main(void)
{
  uint16_t i, j, k, done;
	uint32_t timer_16_value[128], timer_3_value[128];
  const volatile uint16_t spi2_cr1_start = SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_MSTR | SPI_CR1_CPHA | SPI_CR1_CPOL | SPI_CR1_SPE;
  const volatile uint16_t spi2_cr1_stop  = SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_MSTR | SPI_CR1_CPHA | SPI_CR1_CPOL;
  uint16_t transmit_data[4] = {0x8102, 0x8304, 0x0506, 0x0781};
  uint16_t receive_data[4]  = {0x0000, 0x0000, 0x0000, 0x0000};

  HAL_Init();

  SystemClock_Config();

  MX_DMA_Init();
  MX_TIM16_Init();
  MX_TIM5_Init();
  MX_SPI2_Init();

  MX_NVIC_Init();

  MX_TIM16_Init();
 
  TIM16->CCER |= TIM_CCER_CC1E;
  TIM16->BDTR |= TIM_BDTR_MOE | TIM_BDTR_OSSR;

  TIM5->CCR1 = 1;   // Set the delay to turn on time for CH1 / PA0
  TIM5->CCR2 = 40;  // Set the delay to turn off time for CH1 / PA0
  TIM5->CCER |= TIM_CCER_CC1E | TIM_CCER_CC2E;  // Enable all compares 1 & 2 
  TIM5->DIER |= TIM_DIER_UDE | TIM_DMA_CC1; // Enable automatic DMA update, specifically for capture / compare channel 1 - falling edge of TIM5 output used as SPI_SSn
  TIM5->CR1 |= TIM_CR1_CEN; // Start the timer counter

  TIM16->CCR1 = 1;

  SPI2->CR2 &= ~(SPI_CR2_FRXTH); // Clear CR2 FRXTH - set FIFO watermark at 16bits
  SPI2->CR1 = spi2_cr1_start;
  HAL_DMA_Start(&hdma_tim5_ch1,  transmit_data, &(SPI2->DR), 2);
 
  SPI2->CR2 &= ~(SPI_CR2_FRXTH); // Clear CR2 FRXTH - set FIFO watermark at 16bits

  TIM16->CR1 |= TIM_CR1_CEN | TIM_CR1_OPM;  // Start TIM16 which will trigger TIM5 which generates SPI_CSn and DMA to trigger transaction

  i = 0;
  while (i < 2) {
    if (SPI2->SR & SPI_FLAG_RXNE) {
      receive_data[i] = (uint16_t) SPI2->DR;
      i++;    
    }
  }

  while (1)
  {
     // main loop

    }

}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Configure the main internal regulator output voltage
  */
  HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1);

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = RCC_PLLM_DIV3;
  RCC_OscInitStruct.PLL.PLLN = 32;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
  RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
  RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV2;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }

  /** Enables the Clock Security System
  */
  HAL_RCC_EnableCSS();
}

/**
  * @brief SPI2 Initialization Function
  * @PAram None
  * @retval None
  */
static void MX_SPI2_Init(void)
{

  /* USER CODE BEGIN SPI2_Init 0 */

  /* USER CODE END SPI2_Init 0 */

  /* USER CODE BEGIN SPI2_Init 1 */

  /* USER CODE END SPI2_Init 1 */
  /* SPI2 parameter configuration*/
  hspi2.Instance = SPI2;
  hspi2.Init.Mode = SPI_MODE_MASTER;
  hspi2.Init.Direction = SPI_DIRECTION_2LINES;
  hspi2.Init.DataSize = SPI_DATASIZE_16BIT;
  hspi2.Init.CLKPolarity = SPI_POLARITY_HIGH;
  hspi2.Init.CLKPhase = SPI_PHASE_2EDGE;
  hspi2.Init.NSS = SPI_NSS_SOFT;
  hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
  hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  hspi2.Init.CRCPolynomial = 7;
  hspi2.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
  hspi2.Init.NSSPMode = SPI_NSS_PULSE_DISABLE;
  if (HAL_SPI_Init(&hspi2) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN SPI2_Init 2 */

  /* USER CODE END SPI2_Init 2 */

}

/**
  * @brief TIM5 Initialization Function
  * @PAram None
  * @retval None
  */
static void MX_TIM5_Init(void)
{

  /* USER CODE BEGIN TIM5_Init 0 */

  /* USER CODE END TIM5_Init 0 */

  TIM_SlaveConfigTypeDef sSlaveConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  TIM_OC_InitTypeDef sConfigOC = {0};

  /* USER CODE BEGIN TIM5_Init 1 */

  /* USER CODE END TIM5_Init 1 */
  htim5.Instance = TIM5;
  htim5.Init.Prescaler = 0;
  htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim5.Init.Period = 65535;
  htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim5) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_PWM_Init(&htim5) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_OnePulse_Init(&htim5, TIM_OPMODE_SINGLE) != HAL_OK)
  {
    Error_Handler();
  }
  sSlaveConfig.SlaveMode = TIM_SLAVEMODE_COMBINED_RESETTRIGGER;
  sSlaveConfig.InputTrigger = TIM_TS_ITR7;
  if (HAL_TIM_SlaveConfigSynchro(&htim5, &sSlaveConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim5, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigOC.OCMode = TIM_OCMODE_COMBINED_PWM2;
  sConfigOC.Pulse = 0;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_LOW;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  if (HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  if (HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM5_Init 2 */

  /* USER CODE END TIM5_Init 2 */
  HAL_TIM_MspPostInit(&htim5);

}

/**
  * @brief TIM16 Initialization Function
  * @PAram None
  * @retval None
  */
static void MX_TIM16_Init(void)
{

  /* USER CODE BEGIN TIM16_Init 0 */

  /* USER CODE END TIM16_Init 0 */

  TIM_OC_InitTypeDef sConfigOC = {0};
  TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};

  /* USER CODE BEGIN TIM16_Init 1 */

  /* USER CODE END TIM16_Init 1 */
  htim16.Instance = TIM16;
  htim16.Init.Prescaler = 63;
  htim16.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim16.Init.Period = 1000;
  htim16.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim16.Init.RepetitionCounter = 0;
  htim16.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim16) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_OC_Init(&htim16) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_OnePulse_Init(&htim16, TIM_OPMODE_SINGLE) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigOC.OCMode = TIM_OCMODE_ACTIVE;
  sConfigOC.Pulse = 1;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
  sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
  if (HAL_TIM_OC_ConfigChannel(&htim16, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }
  sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
  sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
  sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
  sBreakDeadTimeConfig.DeadTime = 0;
  sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
  sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
  sBreakDeadTimeConfig.BreakFilter = 0;
  sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
  if (HAL_TIMEx_ConfigBreakDeadTime(&htim16, &sBreakDeadTimeConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM16_Init 2 */

  /* USER CODE END TIM16_Init 2 */
  HAL_TIM_MspPostInit(&htim16);

}

/**
  * Enable DMA controller clock
  */
static void MX_DMA_Init(void)
{

  /* DMA controller clock enable */
  __HAL_RCC_DMAMUX1_CLK_ENABLE();
  __HAL_RCC_DMA1_CLK_ENABLE();

  /* DMA interrupt init */
  /* DMA1_Channel3_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);
  /* DMAMUX_OVR_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMAMUX_OVR_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMAMUX_OVR_IRQn);

}

/**
  * @brief TIM16 Initialization Function
  * @PAram None
  * @retval None
  */
static void MOD_MX_TIM16_Init(void)
{

  /* USER CODE BEGIN TIM16_Init 0 */

  /* USER CODE END TIM16_Init 0 */

  TIM_OC_InitTypeDef sConfigOC = {0};
  TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};

  /* USER CODE BEGIN TIM16_Init 1 */

  /* USER CODE END TIM16_Init 1 */
  htim16.Instance = TIM16;
  htim16.Init.Prescaler = 63;
  htim16.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim16.Init.Period = 10;
  htim16.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim16.Init.RepetitionCounter = 0;
  htim16.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim16) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_OC_Init(&htim16) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_OnePulse_Init(&htim16, TIM_OPMODE_SINGLE) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigOC.OCMode = TIM_OCMODE_PWM2;
  sConfigOC.Pulse = 1;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
  sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
  if (HAL_TIM_OC_ConfigChannel(&htim16, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }
  sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
  sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
  sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
  sBreakDeadTimeConfig.DeadTime = 0;
  sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
  sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
  sBreakDeadTimeConfig.BreakFilter = 0;
  sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
  if (HAL_TIMEx_ConfigBreakDeadTime(&htim16, &sBreakDeadTimeConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM16_Init 2 */

  /* USER CODE END TIM16_Init 2 */
  HAL_TIM_MspPostInit(&htim16);

}

 

 

 

1 ACCEPTED SOLUTION

Accepted Solutions
MasterT
Lead

Better way is to set SPI as a slave, and two timers as CS and SCLK drivers for both - ADC & SPI pins. DMA latency in this case not relevant and doesn't cause any jitter, same as SPI internal clock diivider- simply is not in use if SPI acts as a slave.

Here is an example to chain timers: 

https://community.st.com/t5/stm32-mcus-products/spi-with-dma-add-delay-between-bytes/m-p/227885#M50764 

View solution in original post

3 REPLIES 3
MasterT
Lead

Better way is to set SPI as a slave, and two timers as CS and SCLK drivers for both - ADC & SPI pins. DMA latency in this case not relevant and doesn't cause any jitter, same as SPI internal clock diivider- simply is not in use if SPI acts as a slave.

Here is an example to chain timers: 

https://community.st.com/t5/stm32-mcus-products/spi-with-dma-add-delay-between-bytes/m-p/227885#M50764 

gcharles
Associate II

MasterT,

Excellent suggestion as it will perform better in real time than the structure I envisioned.  It seems like ST should rip off the band-aid with regard to the SPI controller, given its limitations necessitating a workaround for both slave select and clock to function in a pretty common use case. 

In case there is a use case scenario for which your suggestion will not work, I'm going to include the description of the changes needed to enable my original structure to work:

  1. Note the STM devices with a DMAMUX instance can no longer have multiple outputs act as a DMA request as apparently (according to other posted solutions) have done in the past.
  2. To work around 1. two DMA channels are used to supply the data, one using the channel compare assigned to create the falling edge of the PWM used as slave select - this will load 16-bits into the SPI FIFO.  The second is simply a channel compare not assigned to an output.  As the timing for this loop is important to the response of the loop the STM32 is controlling the goal is to minimize the time from falling slave select to the start of data being shifted.  So the other channel compare is set to trigger before the falling edge of slave select, such that the synchronous delay results in the first edge of the serial clock arriving 1/2 period after the falling edge of slave select.   

Note this implementation is limited to two transfers of 16-bits for a total of 32-bits of transfer.  An additional channel compare could be added to increase the transfer size to 48-bits.  The implementation also requires separate pointers for each 16-bits of the transfer, which lends to an awkward implementation needed additional comments to indicate the reasoning / limitations behind the transmit data formatting.

FredS
Senior

    Hallo gcharles,

Although I am a real newbee in programming MCU's, I guess I have an explanation for observing one SPI transfer, instead of the two you expect.

I am struggling with the implementation of a similar design, where the ADC readout frequency is dictated by the process (readout of a linear CCD) that produces a signal to be digitized. I was encouraged to inform myself by reading documentation,taking me a lot of time but slowly brings some light in my darkness.

One thing I learned is the meaning of 'TransferCount' is not obvious: is it indicating the number of transactions or the number of bytes to be transfered? After looking to a number of code-samples I concluded it is the former, which implies your condition for the while loop should be 'i < 1', as the ADC will deliver one word per acquisition.

  while (i < 2) {
    if (SPI2->SR & SPI_FLAG_RXNE) {
      receive_data[i] = (uint16_t) SPI2->DR;
      i++;    
    }
  }

A big part of the remaining discussion is not relevant for me, and a bit hard to follow for a novice. Please correct me if I'm wrong, that will be helpful for me as well.

Success with your project,

Fred Schimmel