2023-09-29 12:06 AM
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);
}
Solved! Go to Solution.
2023-09-29 10:55 AM
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:
2023-09-29 10:55 AM
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:
2023-10-02 03:18 PM
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:
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.
2023-12-23 03:48 AM
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