Skip to main content
Graduate
July 18, 2025
Solved

Driving DAC from circular buffer using DMA and TIM - why is it not working?

  • July 18, 2025
  • 7 replies
  • 881 views

As the title says, I've got a (circular) buffer of uint16_t values that I want to send to the DAC every time the timer updates.  It seems like it's 95% working, in that if I manually trigger the DAC:

for (int i = 0; i < 10000; ++i)
{
 LL_DAC_TrigSWConversion(DAC1, LL_DAC_CHANNEL_1);
 HAL_Delay(1);
}

I can see my waveform on the output pin. But when I try to have the DMA send the values to the DAC, the output never changes.

One complication is that I need to run in two modes - in one mode I'm taking the ADC input, processing it, and sending that to the DAC, in the other mode (I'm calling "inject" mode) the ADC is ignored and the DAC values come from my memory buffer.

The code to switch between modes looks like this:

if (inject && !Inject)
{
	// Turn on inject mode
	// HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t*)Transfer, 4096, DAC_ALIGN_12B_R);
	LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_3);
	while (LL_DMA_IsEnabledChannel(DMA1, LL_DMA_CHANNEL_3)); // Wait till off
	LL_DAC_Disable(DAC1, LL_DAC_CHANNEL_1);
	//
	LL_DAC_ClearFlag_DMAUDR1(DAC1);
	LL_DMA_ClearFlag_TC3(DMA1);
	LL_DMA_ClearFlag_HT3(DMA1);
	LL_DMA_ClearFlag_TE3(DMA1);
	//
	LL_DAC_SetTriggerSource(DAC1, LL_DAC_CHANNEL_1, LL_DAC_TRIG_EXT_TIM4_TRGO);
	LL_DAC_EnableTrigger(DAC1, LL_DAC_CHANNEL_1);
	//
	LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_3, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
	LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_3, LL_DMA_MODE_CIRCULAR);
	LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_3, LL_DMA_PERIPH_NOINCREMENT);
	LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_3, LL_DMA_MEMORY_INCREMENT);
	LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_3, LL_DMA_PDATAALIGN_HALFWORD);
	LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_3, LL_DMA_MDATAALIGN_HALFWORD);

	LL_DMA_SetPeriphAddress(DMA1, LL_DMA_CHANNEL_3, (uint32_t)&DAC1->DHR12R1);
	LL_DMA_SetMemoryAddress(DMA1, LL_DMA_CHANNEL_3, (uint32_t)Transfer);
	LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_3, 4096);

	// Enable DMA request on DAC channel
	LL_DAC_EnableDMAReq(DAC1, LL_DAC_CHANNEL_1);

	// Enable DMA channel
	LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_3);
	LL_DAC_Enable(DAC1, LL_DAC_CHANNEL_1);
	//
	LL_TIM_SetTriggerOutput(TIM4, LL_TIM_TRGO_UPDATE);
	// LL_DAC_TrigSWConversion(DAC1, LL_DAC_CHANNEL_1);
	//
	/*
	for (int i = 0; i < 10000; ++i)
	{
	 LL_DAC_TrigSWConversion(DAC1, LL_DAC_CHANNEL_1);
		HAL_Delay(1);
	}
	*/
	Inject = inject;
}
else if ((!inject) && Inject)
{
	// Turn off inject mode
	LL_DAC_SetTriggerSource(DAC1, LL_DAC_CHANNEL_1, LL_DAC_TRIG_SOFTWARE);
	LL_DAC_DisableTrigger(DAC1, LL_DAC_CHANNEL_1);
	// LL_DAC_Disable(DAC1, LL_DAC_CHANNEL_1);
	//
	LL_DAC_DisableDMAReq(DAC1, LL_DAC_CHANNEL_1);
	LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_3);
	//
	Inject = inject;
}

Here are the init functions created from the IOC file:

static void MX_DMA_Init(void)
{

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

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

}
static void MX_TIM4_Init(void)
{

 /* USER CODE BEGIN TIM4_Init 0 */

 /* USER CODE END TIM4_Init 0 */

 TIM_ClockConfigTypeDef sClockSourceConfig = {0};
 TIM_MasterConfigTypeDef sMasterConfig = {0};

 /* USER CODE BEGIN TIM4_Init 1 */

 /* USER CODE END TIM4_Init 1 */
 htim4.Instance = TIM4;
 htim4.Init.Prescaler = 239;
 htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
 htim4.Init.Period = 10;
 htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
 htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
 if (HAL_TIM_Base_Init(&htim4) != HAL_OK)
 {
 Error_Handler();
 }
 sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
 if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK)
 {
 Error_Handler();
 }
 sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
 sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
 if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK)
 {
 Error_Handler();
 }
 /* USER CODE BEGIN TIM4_Init 2 */

 /* USER CODE END TIM4_Init 2 */

}
static void MX_DAC1_Init(void)
{

 /* USER CODE BEGIN DAC1_Init 0 */

 /* USER CODE END DAC1_Init 0 */

 DAC_ChannelConfTypeDef sConfig = {0};

 /* USER CODE BEGIN DAC1_Init 1 */

 /* USER CODE END DAC1_Init 1 */

 /** DAC Initialization
 */
 hdac1.Instance = DAC1;
 if (HAL_DAC_Init(&hdac1) != HAL_OK)
 {
 Error_Handler();
 }

 /** DAC channel OUT1 config
 */
 sConfig.DAC_HighFrequency = DAC_HIGH_FREQUENCY_INTERFACE_MODE_AUTOMATIC;
 sConfig.DAC_DMADoubleDataMode = DISABLE;
 sConfig.DAC_SignedFormat = DISABLE;
 sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE;
 sConfig.DAC_Trigger = DAC_TRIGGER_T4_TRGO;
 sConfig.DAC_Trigger2 = DAC_TRIGGER_NONE;
 sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
 sConfig.DAC_ConnectOnChipPeripheral = DAC_CHIPCONNECT_EXTERNAL;
 sConfig.DAC_UserTrimming = DAC_TRIMMING_FACTORY;
 if (HAL_DAC_ConfigChannel(&hdac1, &sConfig, DAC_CHANNEL_1) != HAL_OK)
 {
 Error_Handler();
 }
 /* USER CODE BEGIN DAC1_Init 2 */

 /* USER CODE END DAC1_Init 2 */

}
static void MX_ADC1_Init(void)
{

 /* USER CODE BEGIN ADC1_Init 0 */

 /* USER CODE END ADC1_Init 0 */

 ADC_MultiModeTypeDef multimode = {0};
 ADC_ChannelConfTypeDef sConfig = {0};

 /* USER CODE BEGIN ADC1_Init 1 */

 /* USER CODE END ADC1_Init 1 */

 /** Common config
 */
 hadc1.Instance = ADC1;
 hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV2;
 hadc1.Init.Resolution = ADC_RESOLUTION_12B;
 hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
 hadc1.Init.GainCompensation = 0;
 hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
 hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
 hadc1.Init.LowPowerAutoWait = DISABLE;
 hadc1.Init.ContinuousConvMode = DISABLE;
 hadc1.Init.NbrOfConversion = 1;
 hadc1.Init.DiscontinuousConvMode = DISABLE;
 hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
 hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
 hadc1.Init.DMAContinuousRequests = DISABLE;
 hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
 hadc1.Init.OversamplingMode = DISABLE;
 if (HAL_ADC_Init(&hadc1) != HAL_OK)
 {
 Error_Handler();
 }

 /** Configure the ADC multi-mode
 */
 multimode.Mode = ADC_MODE_INDEPENDENT;
 if (HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode) != HAL_OK)
 {
 Error_Handler();
 }

 /** Configure Regular Channel
 */
 sConfig.Channel = ADC_CHANNEL_1;
 sConfig.Rank = ADC_REGULAR_RANK_1;
 sConfig.SamplingTime = ADC_SAMPLETIME_2CYCLES_5;
 sConfig.SingleDiff = ADC_SINGLE_ENDED;
 sConfig.OffsetNumber = ADC_OFFSET_NONE;
 sConfig.Offset = 0;
 if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
 {
 Error_Handler();
 }
 /* USER CODE BEGIN ADC1_Init 2 */

 /* USER CODE END ADC1_Init 2 */

}

What am I doing wrong?  Thanks for any help!

    This topic has been closed for replies.
    Best answer by cbcooper

    I don't know which change did it, but it's working now!  For reference, here's my code:

    void MyInit_DAC()
    {
    	__HAL_RCC_DAC1_CLK_ENABLE();
    	__HAL_RCC_GPIOA_CLK_ENABLE();
    
    	// Initialize GPIO A4 is an analog output
    	LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_4, LL_GPIO_MODE_ANALOG);
    	LL_GPIO_SetPinPull(GPIOA, LL_GPIO_PIN_4, LL_GPIO_PULL_NO);
    
    	// MCR is reset to zero, and all HAL did was clear some bits.
    	DAC1->MCR = 0;
    
    	// The top half of CR is for channel 2, the bottom half has these bits:
    	// [14] CEN1: DAC channel1 calibration enable (0 = normal operating mode)
    	// [13] DMAUDRIE1: DAC channel 1 DMA underrun interrupt enable (0 = interrupt disabled)
    	// [12] DMAEN1: DAC channel1 DMA enable (we will set this in CopyMemToDAC())
    	// [11:8] MAMP1: DAC channel1 mask/amplitude selector (not used, leave 0)
    	// [7:6] WAVE1: DAC channel1 noise/triangle wave generation enable (not used, leave 0)
    	// [5:2] TSEL1: DAC channnel1 trigger selection (0 = SWTRIG1)
    	// [1] TEN1: DAC channel1 trigger enable (0 = disabled)
    	// [0] EN1: DAC channel1 enable
    	DAC1->CR = DAC_CR_EN1;
    }

     

    void MyMX_DMA_Init_For_MemtoDAC(void)
    {
    	/* DMA controller clock enable */
     SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_DMAMUX1EN);
     SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_DMA1EN);
    
    	uint32_t tmp;
    
    	/* Get the CR register value */
    	tmp = DMA1_Channel1->CCR;
    
    	/* Clear PL, MSIZE, PSIZE, MINC, PINC, CIRC, DIR and MEM2MEM bits */
    	tmp &= ((uint32_t)~(DMA_CCR_PL | DMA_CCR_MSIZE | DMA_CCR_PSIZE |
    	 DMA_CCR_MINC | DMA_CCR_PINC | DMA_CCR_CIRC |
    	 DMA_CCR_DIR | DMA_CCR_MEM2MEM));
    
    	/* Prepare the DMA Channel configuration */
    	tmp |= DMA_CCR_MINC |		// Memory increment yes (peripheral increment no)
    	 DMA_PDATAALIGN_WORD |		// Peripheral takes a 32-bit word
    	 DMA_MDATAALIGN_HALFWORD |		// Memory supplies a 16-bit halfword
    	 DMA_CIRCULAR | // Circular (once memory buffer is exhausted, go back to top and send again)
    	 DMA_PRIORITY_LOW |
    	 DMA_MEMORY_TO_PERIPH;
    
    	/* Write to DMA Channel CR register */
    	DMA1_Channel1->CCR = tmp;
    
    	/*
    	 * "The mapping of resources to DMAMUX is hardwired.
     DMAMUX is used with DMA1 and DMA2:
     For category 3 and category 4 devices:
     • DMAMUX channels 0 to 7 are connected to DMA1 channels 1 to 8
     • DMAMUX channels 8 to 15 are connected to DMA2 channels 1 to 8
     For category 2 devices:
    		• DMAMUX channels 0 to 5 are connected to DMA1 channels 1 to 6
    		• DMAMUX channels 6 to 11 are connected to DMA2 channels 1 to 6"
    	 */
    
    	/* Set peripheral request to DMAMUX channel */
    	DMAMUX1_Channel0->CCR = DMA_REQUEST_DAC1_CHANNEL1;
    
    	/* Clear the DMAMUX synchro overrun flag */
    	DMAMUX1_ChannelStatus->CFR = 1;
    }

     

    void CopyMemToDAC()
    {
    	// Disable channel 1 of the DAC while we set up the transfer
    	DAC1->CR &= ~DAC_CR_EN1;
    
    	// Put some known value as the DAC output
    	DAC1->DHR12R1 = 123;
    
    	// Disable DMA1 channel 1 while we update its settings
    	DMA1_Channel1->CCR &= ~DMA_CCR_EN;
    
    	/* Clear the DMAMUX synchro overrun flag */
    	DMAMUX1_ChannelStatus->CFR = 1;
    
    	/* Clear all flags */
    	DMA1->IFCR = 0x0F;
    
    	/* Configure DMA1 Channel 1 data length */
    	DMA1_Channel1->CNDTR = 4096;		// "Number of data to transfer"
    
    	/* Configure DMA1 Channel 1 destination address */
    	DMA1_Channel1->CPAR = (uint32_t) &DAC1->DHR12R1;
    
    	/* Configure DMA1 Channel 1 source address */
    	DMA1_Channel1->CMAR = (uint32_t) Transfer;
    
    	// Clear any DAC under-run errors
    	WRITE_REG(DAC1->SR, DAC_SR_DMAUDR1);
    
    	// Clear other errors
    	LL_DMA_ClearFlag_TC1(DMA1);
    	LL_DMA_ClearFlag_HT1(DMA1);
    	LL_DMA_ClearFlag_TE1(DMA1);
    
    	// Enable DMA1 channel 1
    	DMA1_Channel1->CCR |= DMA_CCR_EN;
    
    	// Enable DMA on DAC1 channel 1
    	SET_BIT(DAC1->CR, DAC_CR_DMAEN1);
    
    	// Tell DAC1 channel 1 to trigger on dac_chx_trg5; according to table 186 on page 718
    	// this sets the source to TIM4_TRGO
    	DAC1->CR = (DAC1->CR & ~DAC_CR_TSEL1_Msk) | (5 << DAC_CR_TSEL1_Pos) | DAC_CR_TEN1;
    
    	// Re-enable channel 1 of the DAC to start the transfer
    	DAC1->CR |= DAC_CR_EN1;
    }

     

     

    7 replies

    cbcooperAuthor
    Graduate
    July 18, 2025

    I picked the wrong label, this is on a NUCLEO-G491RE board which has the STM32G491 chip.

     

    I added an interface so that the STM32 could report back to the PC on the values of some registers.

    TIM4->CNT increments as expected.

    DMA1->ISR is always 0x0

    DMA1_Channel3->CMAR is always 0x20000330 which is the address of my circular buffer

    DMA1_Channel3->CNDTR is always 0x1000 which is the size, in bytes, of the circular buffer

    DMA1_Channel3->CCR is always 0x5B1

      109876543210

    = 010110110001

       | || ||   |

       | || ||   += Bit 0 = channel enable

       | || |+= Bit 4 = direction (read from memory)

       | || += Bit 5 = circular mode enabled

       | |+= Bit 7 = memory increment mode enabled

       | += Bit 9/8 = 01 = peripheral size 16 bits

       += Bit 11/10 = 01 = memory size 16 bits

    DMA1_Channel3->CPAR is always 0x50000808 which I think is right for DAC1

    DAC1->CR is always 0x1017

    5432109876543210

    0001000000010111

       |         |||

       |         ||+= bit 0 = channel 1 enabled

       |         |+= bit 1 = channel 1 trigger enabled

       |         += bit 5..2 = 0101 = dac_ch1_trg5 = TIM4_TRGO

       += bit 12 = channel 1 DMA enabled

    DAC1->SR is always 0x2800

    5432109876543210

    0010100000000000

      | |

      | += bit 11 = DAC channel 1 ready

      += bit 13 = DAC channel DMA underrun

    The doc says DMA underrun means "the currently selected trigger is driving DAC channel1 conversion at a frequency higher than the DMA service capability rate"

    which I don't believe, I've got TIM4 prescaler set to 23990, I think I'm running at 72 Mhz so TIM4 is incrementing at 3,000 hz with a period of 100 so it's trying to update the DAC at 30 Hz.

    But if I enable the DAC's DMA underrun interrupt and in the ISR clear the underrun and increment a counter, there are definitely underruns occuring, and I think it happens every time TIM4 rolls over.

    Super User
    July 19, 2025

    Maybe incorrect DMAMUX setting?

    JW

    cbcooperAuthor
    Graduate
    July 21, 2025

    Good suggestion! 

    I added

    LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_3, LL_DMAMUX_REQ_DAC1_CH1);

    and it didn't seem to make any difference

     

    Super User
    July 21, 2025

    Read out and check/post content of TIM, DAC, DMA and DMAMUX registers.

    DAC underrun may occur because of sequencing of events: the DMA must be up and running before the trigger source. I don't use Cube and don't understand its gobbledygook to be able to judge whether this is or is not the case.

    You can try single-stepping your code to see where does the underrun happen. You appear to have experimented with manual trigger, was that with or without DMA? You can manual trigger DAC also with stopped code execution, directly from the debugger.

    The DS is not entirely clear whether DAC stops triggering upon underrun, but the requirement to restart the DAC appears to hint so.

    JW

     

    cbcooperAuthor
    Graduate
    July 21, 2025

    I slowed down the timer so I could see things changing:

    htim4.Init.Prescaler = 23990;
    htim4.Init.Period = 100;

    and I have an interface that lets me query a few registers with a Python program (so it's faster than me typing in debugging commands) and it shows me this:

    tim4_cnt=46, dma1_isr=0x900, dma1_ccr=0x5B0, dma1_cmar=0x20000330, dma1_cndtr=0x1000, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=42064
    tim4_cnt=61, dma1_isr=0x900, dma1_ccr=0x5B0, dma1_cmar=0x20000330, dma1_cndtr=0x1000, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=42064
    tim4_cnt=75, dma1_isr=0x900, dma1_ccr=0x5B0, dma1_cmar=0x20000330, dma1_cndtr=0x1000, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=42064
    tim4_cnt=90, dma1_isr=0x900, dma1_ccr=0x5B0, dma1_cmar=0x20000330, dma1_cndtr=0x1000, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=42064
    tim4_cnt=4, dma1_isr=0x900, dma1_ccr=0x5B0, dma1_cmar=0x20000330, dma1_cndtr=0x1000, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=42065

    'num_under' is incremented in MyTIM6_DAC_IRQHandler() when it sees the underrun flag is set, and 'xfer' is the address of my uint16_t buffer that I'm trying to send to the DAC.

    I'm mostly not using Cube's auto-generated code, only in the 'Init()' functions.

    When I add this code:

    for (int i = 0; i < 10000; ++i)
    {
     LL_DAC_TrigSWConversion(DAC1, LL_DAC_CHANNEL_1);
     HAL_Delay(1);
    }

    I'm adding it to the version of the code that is trying to use DMA.  It used to work (would change the value of the DAC) but I just now tried it and it didn't do anything.

     

    cbcooperAuthor
    Graduate
    July 21, 2025

    I mentioned this program has two modes -

    * Mode 1: program reads in ADC value, processes it, writes to DAC

    * Mode 2: program ignores ADC, drives DAC directly from memory buffer (hopefully using DMA)

    The program wakes up in mode 1 and I see this in the registers:

    tim4_cnt=85, dma1_isr=0x0, dma1_ccr=0x0, dma1_cmar=0x0, dma1_cndtr=0x0, xfer=0x20000330, dma_cpar=0x0, dac_cr=0x17, dac_sr=0x800, num_under=0, dac1.DHR12R1=0xF75, DMAMUX1_Channel2->CCR=0x0
    tim4_cnt=0, dma1_isr=0x0, dma1_ccr=0x0, dma1_cmar=0x0, dma1_cndtr=0x0, xfer=0x20000330, dma_cpar=0x0, dac_cr=0x17, dac_sr=0x800, num_under=0, dac1.DHR12R1=0xB24, DMAMUX1_Channel2->CCR=0x0
    tim4_cnt=17, dma1_isr=0x0, dma1_ccr=0x0, dma1_cmar=0x0, dma1_cndtr=0x0, xfer=0x20000330, dma_cpar=0x0, dac_cr=0x17, dac_sr=0x800, num_under=0, dac1.DHR12R1=0x3E4, DMAMUX1_Channel2->CCR=0x0
    tim4_cnt=33, dma1_isr=0x0, dma1_ccr=0x0, dma1_cmar=0x0, dma1_cndtr=0x0, xfer=0x20000330, dma_cpar=0x0, dac_cr=0x17, dac_sr=0x800, num_under=0, dac1.DHR12R1=0x0, DMAMUX1_Channel2->CCR=0x0
    tim4_cnt=50, dma1_isr=0x0, dma1_ccr=0x0, dma1_cmar=0x0, dma1_cndtr=0x0, xfer=0x20000330, dma_cpar=0x0, dac_cr=0x17, dac_sr=0x800, num_under=0, dac1.DHR12R1=0xF78, DMAMUX1_Channel2->CCR=0x0

    When I switch to mode 2 I see this:

    tim4_cnt=10, dma1_isr=0x900, dma1_ccr=0x5B0, dma1_cmar=0x20000330, dma1_cndtr=0x1000, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=730, dac1.DHR12R1=0xAC4, DMAMUX1_Channel2->CCR=0x6
    tim4_cnt=26, dma1_isr=0x900, dma1_ccr=0x5B0, dma1_cmar=0x20000330, dma1_cndtr=0x1000, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=730, dac1.DHR12R1=0xAC4, DMAMUX1_Channel2->CCR=0x6
    tim4_cnt=43, dma1_isr=0x900, dma1_ccr=0x5B0, dma1_cmar=0x20000330, dma1_cndtr=0x1000, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=730, dac1.DHR12R1=0xAC4, DMAMUX1_Channel2->CCR=0x6
    tim4_cnt=59, dma1_isr=0x900, dma1_ccr=0x5B0, dma1_cmar=0x20000330, dma1_cndtr=0x1000, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=730, dac1.DHR12R1=0xAC4, DMAMUX1_Channel2->CCR=0x6
    tim4_cnt=76, dma1_isr=0x900, dma1_ccr=0x5B0, dma1_cmar=0x20000330, dma1_cndtr=0x1000, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=730, dac1.DHR12R1=0xAC4, DMAMUX1_Channel2->CCR=0x6

    DMA1.ISR being set to 0x900 is new, I'm guessing that's because I've now got the DMAMUX set correctly

     

    Super User
    July 21, 2025

    > When I switch to mode 2 I see this:

    > num_under=730,

    > dma1_ccr=0x5B0

    So, the DAC underruns 730x before you even started the DMA?

    You may want to write a simple example only with the DAC and DMA (and then perhaps TIM-triggered, once software ttrigger with DAC works) and nothing else.

    JW

    cbcooperAuthor
    Graduate
    July 21, 2025

    The DAC underruns 1x every time the timer rolls over so the '730' value just indicates how long it took the human (me) to tell the program to switch modes, and then move back to my PC and run the Python program that queried the register values.

    Super User
    July 21, 2025

    Ah, I've just realized what's the root cause of the Transfer Error at DMA is: that DAC in 'G4 is at AHB, and does not support other than word-size transfers.

    JW

    cbcooperAuthor
    Graduate
    July 21, 2025

    Great catch!  I changed the code to say

    LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_3, LL_DMA_PDATAALIGN_WORD);
    LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_3, LL_DMA_MDATAALIGN_WORD);

    and then I removed the dependency on the timer.  I now have it so that similar to "mode 1", the program gets an interrupt when the ADC has a value, but in "mode 2" it discards the ADC value and simply tells the DAC to grab the next value:

    else if (Inject)
    {
     LL_DAC_TrigSWConversion(DAC1, LL_DAC_CHANNEL_1);
     numInjects++;
    }

    is that backwards?  Should I be telling the DMA to send the next value to the DAC instead of telling the DAC to grab the next value from the DMA?

    tim4_cnt=13, dma1_isr=0x0, dma1_ccr=0xAB1, dma1_cmar=0x20000330, dma1_cndtr=0xEDC, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=0, dac1.DHR12R1=0x48C, DMAMUX1_Channel2->CCR=0x6, num_transfer_errors=0, num_injects=4020147
    tim4_cnt=31, dma1_isr=0x0, dma1_ccr=0xAB1, dma1_cmar=0x20000330, dma1_cndtr=0xEDC, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=0, dac1.DHR12R1=0x48C, DMAMUX1_Channel2->CCR=0x6, num_transfer_errors=0, num_injects=4022656
    tim4_cnt=50, dma1_isr=0x0, dma1_ccr=0xAB1, dma1_cmar=0x20000330, dma1_cndtr=0xEDC, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=0, dac1.DHR12R1=0x48C, DMAMUX1_Channel2->CCR=0x6, num_transfer_errors=0, num_injects=4025123
    tim4_cnt=68, dma1_isr=0x0, dma1_ccr=0xAB1, dma1_cmar=0x20000330, dma1_cndtr=0xEDC, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=0, dac1.DHR12R1=0x48C, DMAMUX1_Channel2->CCR=0x6, num_transfer_errors=0, num_injects=4027590
    tim4_cnt=87, dma1_isr=0x0, dma1_ccr=0xAB1, dma1_cmar=0x20000330, dma1_cndtr=0xEDC, xfer=0x20000330, dma_cpar=0x50000808, dac_cr=0x3017, dac_sr=0x800, num_under=0, dac1.DHR12R1=0x48C, DMAMUX1_Channel2->CCR=0x6, num_transfer_errors=0, num_injects=4030106

     

     

    Super User
    July 21, 2025

    > Should I be telling the DMA to send the next value to the DAC instead of telling the DAC to grab the next value from the DMA?

    No.

    You tell DAC to transfer the datum from holding to output register (that's called trigger in the DAC); DAC does that within a couple of its cycles and as it empties its holding register, DAC in turn raises the request to DMA to fill up its holding register.

    JW

    cbcooperAuthor
    Graduate
    July 21, 2025

    I'm wondering if there is a bug in STM32CubeIDE ... if I create a DMA channel (in IOC) for a memory-to-memory transfer, it writes this code for me:

    static void MX_DMA_Init(void)
    {
    
     /* DMA controller clock enable */
     __HAL_RCC_DMAMUX1_CLK_ENABLE();
     __HAL_RCC_DMA1_CLK_ENABLE();
    
     /* Configure DMA request hdma_memtomem_dma1_channel1 on DMA1_Channel1 */
     hdma_memtomem_dma1_channel1.Instance = DMA1_Channel1;
     hdma_memtomem_dma1_channel1.Init.Request = DMA_REQUEST_MEM2MEM;
     hdma_memtomem_dma1_channel1.Init.Direction = DMA_MEMORY_TO_MEMORY;
     hdma_memtomem_dma1_channel1.Init.PeriphInc = DMA_PINC_ENABLE;
     hdma_memtomem_dma1_channel1.Init.MemInc = DMA_MINC_ENABLE;
     hdma_memtomem_dma1_channel1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
     hdma_memtomem_dma1_channel1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
     hdma_memtomem_dma1_channel1.Init.Mode = DMA_NORMAL;
     hdma_memtomem_dma1_channel1.Init.Priority = DMA_PRIORITY_LOW;
     if (HAL_DMA_Init(&hdma_memtomem_dma1_channel1) != HAL_OK)
     {
     Error_Handler( );
     }
    
    }

    but if I instead create a DMA channel that targets DAC1, it does this:

    static void MX_DMA_Init(void)
    {
    
     /* DMA controller clock enable */
     __HAL_RCC_DMAMUX1_CLK_ENABLE();
     __HAL_RCC_DMA1_CLK_ENABLE();
    
     /* DMA interrupt init */
     /* DMA1_Channel1_IRQn interrupt configuration */
     HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
     HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
    
    }

    It "forgets" to set up the DMA registers and the 

    DMA_HandleTypeDef hdma_dac1_ch1;

     

    Super User
    July 21, 2025

    Maybe it inits DMA elsewhere, maybe somewhere in relationship with DAC.

    I don't use Cube.

    JW

    cbcooperAuthor
    Graduate
    July 21, 2025

    I'm giving up on getting DMA to work for this chip, just setting up TIM4 to generate an interrupt and setting the DAC value there. Thanks for helping me try to figure this out!

    I can get it to work with interrupts but not at the speed I need to drive the DAC at.

     

    Super User
    July 21, 2025

    That's what DMA is for.

    If you can't get it working with Cube/HAL, just ditch Cube/HAL and go for register-level programming.

    JW