cancel
Showing results for 
Search instead for 
Did you mean: 

Trigger SPI DMA transfer from Timer directly

JKram.1
Associate II

Hey there, 

at the moment I am stuck with the following problem: I need to trigger a SPI DMA Transfer precisely at each Timer overflow. NVIC Timer interrupts are not an option, because the jitter is way too high when I have to jump into code each time (I managed to trigger SPI-DMA transfer using the Timers NVIC global interrupt, but as I said, jitter is way too high). So now I am looking for a solution that involves the Cube MX, I want to configure the Timer in a way so that it directly triggers the DMA of the SPI? Or is there anything I can once declare in code that will do the job for me? In the end the solution should work without the CPU at all. I am using a NUCLEO-F446RE Board here, running at 180MHz. If someone has a quick solution (In Cube MX or Code, doesn’t matter as long as it works) I would be very thankful!

Greetings Julius

11 REPLIES 11

>I need to trigger a SPI DMA Transfer precisely at each Timer overflow

What do you exactly mean by "SPI DMA Transfer"? Transmission of one SPI frame (byte, halfword)? Or something else?

JW

berendi
Principal

It's definitely possible, if you understand how DMA on the STM32 works, and how to use of the register interface of the peripherals in general.

A DMA request coming from a peripheral is instructing one of the DMA streams to copy one unit of data from the source to the destination address. In the case of SPI transmitting with DMA, the request source is the SPI controller, when it's transmit buffer is empty, and the destination is the SPI data register.

DMA requests coming from a peripheral are hardwired to certain DMA streams (see the DMA1/DMA2 request mapping tables in the reference manual), but the source and destination addresses are flexible. An often overlooked feature of the STM32 DMA controllers is that the peripheral address of a DMA transfer can belong to a different peripheral, not just to the one which is sending the request.

So a timer that can issue a DMA request on an update (overflow) event can request DMA to copy a byte or word to an SPI register, initiating an SPI transfer. There is an important restriction though, only DMA2 can access all peripheral registers, DMA1 can only access peripherals on the APB1 bus (i.e. SPI2/SPI3). It means that TIM1 or TIM8 (on DMA2) can start any of the four SPI peripherals, but TIM2 through TIM7 (on DMA1) can only start SPI2 or SPI3. You shpuld select the SPI and timer peripheral with this in mind.

Now it's quite straightforward to transmit one byte or halfword on a timer DMA request. Set up the DMA stream that belongs to the TIM?_UP request to transfer 8 or 16 bits (as required) from the memory buffer to the SPI data register (SPIx->DR). Set up the timer to the required period length, enable TIM_DIER_UDE, then TIM_CR1_CEN.

You can transfer up to 3 bytes or halfwords in each timer cycle with a little twist to the above. Note that in the DMA request mapping table, there is one slot which is shared by TIM1_CH1/TIM1_CH2/TIM1_CH3, another by TIM8_CH1/TIM8_CH2/TIM8_CH3. Configure these timer channels as output compare (it's not necessary to assign an actual pin) to issue DMA requests one after another (with necessary spacing), and enable TIM_DIER_CC*DE bits instead of TIM_DIER_UDE.

Sending more than 6 bytes would involve two DMA channels, the one activated by the timer copying a single value to SPIx->CR1, setting SPI_CR1_SPE, the other transferring data to the SPI data register. A timer compare interrupt, timed to happen after SPI has completed shifting out the last data bit, would be needed to prepare the next transfer.

I don't think there is a magic checkbox in CubeMX or a sufficiently documented function in the HAL library to do any of the above. Use the register interface, the procedures are documented in the functional description chapters of the reference manual for each peripheral.

Hey,

I need to send around 100 bytes at each SPI transfer. This transfer should happen every time my timer triggers / overflows. Hope that gives you an idea. It is a new design so it doesn't matter which peripherals I use, I haven't assigned any timers or SPI busses yet, so any solution provided could be implemented. Maybe you have an idea, thanks!

> I need to send around 100 bytes at each SPI transfer.

berendi gave you already the answer together with a bunch of other vital information above.

Another option would be to use a timer which overflows as fast as one SPI frame gets transmitted, and make "its" DMA to transfer data into SPI_DR; then use the timers' master-slave link from the "governing" timer to start/stop this "transferring" timer to generate the 100 transfers when you want, e.g. using the gated slave mode.

I too would not recommend to stick to CubeMX, its purpose (together with Cube/HAL bound to it) is to provide a clicky environment for the "usual" usage cases, which this one is not. Start with reading the SPI, TIM and DMA chapters in RM.

JW

On the peripheral selection, TIM1 and TIM8 are advanced timers that have functionality not needed for this SPI stuff, but depending on your application you might need it elsewhere (e.g. if there are servos to control). TIM6 and TIM7 don't have compare channels, that leaves TIM2 through TIM5. Pinout considerations or limited DMA channel availability might override this later, but I'd start with TIM3 or TIM4 if 16 bit timer precision is enough, TIM2 or TIM5 if 32 bits are needed, and use SPI2 or SPI3 with DMA1 as I have described below. You can take a look at the System architecture diagram at the beginning of chapter 2 in the reference manual to better understand why DMA1 is restricted to APB1 peripherals and DMA2 isn't.

You already have the SPI with DMA and the timer up and running, that's good, as something to start with. I'd suggest starting with rewriting these using the register interface, for two reasons.

  1. HAL functions are too slow. Rule #1: You can have either strict timings or use HAL in a project, never both.
  2. In the process of reimplementing SPI DMA transfer, you'll learn enough of the workings of DMA to be able to implement the missing piece, setting SPI_CR1_SPE with DMA.

This was very helpful indeed. It works now to transfer up to 16 bit at each timer overflow, but I do not understand you solution using the second timer? Does anyone know how the HAL functions would do it? Because they can trigger a multi byte long transfer, maybe I can copy this behavior in some way.

For anyone that needs the code, here is what worked for me (thanks berendi):

void DMA2_Stream5_IRQHandler(void)
{
	/* USER CODE BEGIN DMA2_Stream5_IRQn 0 */
	//HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
 
 
	if(DMA2->HISR & DMA_HISR_HTIF5) { //Half Transfer Interrupt (ONLY happens when more than 16 bits are transferred!)
		//HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
	}
 
	if(DMA2->HISR & DMA_HISR_TCIF5) { //Transfer Complete Interrupt
		//HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
		//HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
		data2--; //Increment tha data variable to see if something changes during transmission
	}
 
	HAL_DMA_IRQHandler(&hdma_tim1_up); //used to reset all flags
}
 
 
void init_Timer_Dma_Spi() {
 
	  __HAL_RCC_SPI1_CLK_ENABLE();
	  __HAL_RCC_DMA2_CLK_ENABLE();
	  __HAL_RCC_TIM1_CLK_ENABLE();
 
	  HAL_TIM_Base_Start(&htim1);	//Start Timer, if I dont do this it doesn't work
 
	  //Reset SPI1 Configuration Register (-> this will disable the SPI1)
	  SPI1->CR1 = 0b00000000;
	  SPI1->CR2 = SPI_CR2_TXEIE | SPI_CR2_TXDMAEN | SPI_CR2_SSOE;	//TXEIE:Txbuffer empty interrupt enable, TXDMAEN: Enable TX dma, SSOE:SSoutputenable
 
	  SPI1->CR1 = SPI_CR1_MSTR | SPI_BAUDRATEPRESCALER_256 | SPI_CR1_SPE | SPI_DATASIZE_16BIT; //Enable SPI, Baudratecontrol = 256, configuration as master, Data frame format 16bit
 
	  //Reset Timer Configuration Registers (-> this will disable the timer)
	  TIM1->CR1 = 0b00000000;
	  TIM1->CR2 = 0b00000000;
	  
	  TIM1->DIER = TIM_DIER_UDE; //UDE Update DMA request enable
	  
	  TIM1->PSC = 500; 			//Set the prescaler
	  TIM1->ARR = 100; 			//Set the autoreload value (timer interval)
	  
	  DMA2_Stream5->CR = 0x0;	//Disable DMA stream
 
	  
	  DMA2_Stream5->M0AR = (uint32_t)&data2;	//Set source memory region
	  DMA2_Stream5->PAR = (uint32_t)&SPI1->DR;	//Set peripheral data register as destination
	  DMA2_Stream5->NDTR = sizeof(data2) ; 		//Number of items to transfer
	  DMA2_Stream5->FCR = 0;					//Turn off FIFO
 
	  NVIC_EnableIRQ(DMA2_Stream5_IRQn);		//Enable interrupt for DMA Channel 5, to deal with finished transfers
 
	  DMA2_Stream5->CR =
	 			  DMA_SxCR_EN | 			//Enable
	 			  DMA_SxCR_HTIE | 			//half transfer interrupt enable
	 			  DMA_SxCR_TCIE | 			//transfer complete interrupt enable
	 			  DMA_MEMORY_TO_PERIPH | 	//DMA Memory to Memory mode
	 			  DMA_SxCR_CIRC | 			//DMA Circular mode
	 			  DMA_PINC_DISABLE | 		//Do not increment peripheral
	 			  DMA_MINC_ENABLE | 		//Do not increment memory  //DMA_MINC_DISABLE
	 			  DMA_SxCR_PSIZE_0 | 		//Peripheral data size 0 = 16bit , 1 = 32bit
	 			  DMA_SxCR_MSIZE_0 | 		//Memory data size 0 = 16bit , 1 = 32bit
	 			  DMA_PRIORITY_VERY_HIGH |	//DMA Priority
	 			  DMA_PBURST_INC16 | 		//peripheral burst: 16 bit
	 			  DMA_MBURST_SINGLE | 		//memory burst: single transfer //DMA_MBURST_SINGLE
	 			  DMA_CHANNEL_6;			//DMA channel in the current stream
 
	  TIM1->CR1 = TIM_CR1_CEN | TIM_CR1_ARPE;	//the CEN is set (Counter enable) and ARPE:Auto-reloadpreloadenable is buffered (TIMx_ARR register is buffered)
	  //Timer is now started and triggers SPI at each overflow
}

All of the above registers are nicely documented in the functional datasheet, as I learned now.

As you might have noticed I am using HAL_TIM_Base_Start, I do not want to use it but it only works when I do. Also, I haven't managed to clear all Interrupt flags correctly, so this is why I use the HAL IRQ Handler in my interrupt routine, maybe someone has an idea for that?

>I  do not understand you solution using the second timer?

Do you mean the one recommended by @Community member​ ? This thread should give you an idea

https://community.st.com/s/question/0D50X0000C4PPGhSQO/stm32f030-combiningmodulating-two-pwm-signals-logical-and-into-one-output

In your case, slave timer frequency = spi baudrate / 8, master timer duty cycle = slave timer period * 100.

>how the HAL functions would do it? Because they can trigger a multi byte long transfer, maybe I can copy this behavior in some way.

Using another DMA channel, DMA2 stream 3 channel 3, and NDTR = 100, the SPI transfer itself would trigger loading the next byte into SPI1->DR. Then use stream 5 channel 6 to start the transfer sequence by the timer.

Don't enable SPI_CR1_SPE in the SPI configuration code above, but put the CR1 value needed to start it in a const value

SPI1->CR1 = SPI_CR1_MSTR | SPI_BAUDRATEPRESCALER_256 | SPI_DATASIZE_16BIT;
const volatile uint32_t spi1_cr1_start = SPI_CR1_MSTR | SPI_BAUDRATEPRESCALER_256 | SPI_CR1_SPE | SPI_DATASIZE_16BIT;

Then let DMA2 stream 5 channel 6 copy this value into SPI1->CR1

DMA2_Stream5->M0AR = (uint32_t)&spi1_cr1_start;	//Set source memory region
DMA2_Stream5->PAR = (uint32_t)&SPI1->CR1;	//Set peripheral control register as destination
DMA2_Stream5->NDTR = 1 ;

and set PSIZE and MSIZE to 32 bits in DMA2_Stream5->CR.

Some housekeeping will be needed after the transfer is complete, disabling SPI and preparing the next transfer, but the DMA transfer complete interrupt comes too early for that, because the last byte will still being transmitted. You can set up a channel of TIM1 to give an interrupt timed so that it happens after the last bit has definitely left the line, clear SPI_CR1_SPE, and enable the DMA channel again, so the next cycle will be prepared.

>I am using HAL_TIM_Base_Start, I do not want to use it but it only works when I do.

The timer should start when you set TIM_CR1_CEN.

>I haven't managed to clear all Interrupt flags correctly

I don't know what you have tried, so no idea here.

>transfer, but the DMA transfer complete interrupt comes too early for that, because the last byte will still being transmitted.

>You can set up a channel of TIM1 to give an interrupt timed so that it happens after the last bit has definitely left the line,

That is IMO better and naturally served by the Rx SPI ISR/DMA TC.

The directly timer-driven DMA/SPI does not need "manual" cleanup/re-priming.

Another option, at the cost of one pin is to use SPI as as slave, driving its SCK from a timer combo. Not Cube-friendly either.

JW

"An often overlooked feature of the STM32 DMA controllers is that the peripheral address of a DMA transfer can belong to a different peripheral, not just to the one which is sending the request."

Can you show where in the reference manual can find this information and how to set the registers?