Electronic – Can the STM32 DMA speed be controlled by the timer

dmastm32stm32f10xtimer

Is it possible to have the DMA controller on an STM32 transfer each packet only when a timer update event occurs or can you only control the start of a whole chunk of DMA?

The use case is (on an STM32 with no HW DAC) to set the PWM duty cycle of TIMER1 from a block of sample data in memory at a specific timing interval. Currently I generate a CPU interrupt (using TIMER2) and manually stuff the PWM pulse value for TIMER1 in the ISR, which works, but I'd like to get the CPU out of there if I can.

I've looked at the reference manual and can't find a standard way to do this (which I may well have missed), but perhaps there's a sneaky method which would use another DMA channel to tweak the primary output DMA channel… or something…

Update: code added (which doesn't use DMA, just stuffs it in a timer ISR)

#define WAV

extern "C" {
    #include "misc.h"
    #include "GPIO_stm32f10x.h"
    #include "stm32f10x_tim.h"
    #include "TIM_ex.h"
    #include "wav.h"
    #include "stm32f10x_dma.h"
}

#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

extern "C" void TIM2_IRQHandler()
{
    static uint32_t i = 1;
    static uint32_t j = 0;

    // clear timer2 irq status
    TIM2->SR = (uint16_t)~TIM_IT_Update;

    // heartbeat toggle PORTA:0 every tick
    GPIOA->BSRR = i;
    i ^= 0x10001;

    #ifdef WAV
    TIM_SetChannel1Pulse(TIM1, wav[j]);
    if(++j >= ARRAY_SIZE(wav))
    {
        j = 0;
    }
    #else
    static uint32_t k = 0;
    TIM1->CCR1 = sine[j & 0xff];
    j += (sine[(k >> 6) & 0xff] >> 1) + 32;
    k += 1;
    #endif
}

int main()
{
    // switch on some peripheral clocks
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);      // DMA1
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);    // TIMER2
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA +
                            RCC_APB2Periph_AFIO +
                            RCC_APB2Periph_TIM1, ENABLE);   // PORTA, AFIO, TIMER1

    // set PORTA:0 to output
    GPIO_InitTypeDef a0Init;
    a0Init.GPIO_Pin = GPIO_Pin_0;
    a0Init.GPIO_Mode = GPIO_Mode_Out_PP;
    a0Init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &a0Init);

    // setup timer2 @ 8KHz
    TIM_TimeBaseInitTypeDef t2Init;
    t2Init.TIM_CounterMode = TIM_CounterMode_Up;
    t2Init.TIM_Prescaler = 0;
    t2Init.TIM_Period = 72000000 / 8000 - 1;
    t2Init.TIM_ClockDivision = TIM_CKD_DIV1;
    t2Init.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM2, &t2Init);

    // enable TIMER2 IRQs
    NVIC_InitTypeDef nvicInit;
    nvicInit.NVIC_IRQChannel = TIM2_IRQn;
    nvicInit.NVIC_IRQChannelPreemptionPriority = 0;
    nvicInit.NVIC_IRQChannelSubPriority = 1;
    nvicInit.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&nvicInit);

    // switch on TIMER2 update IRQs
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

    // setup timer1 for 7 bit PWM
    TIM_TimeBaseInitTypeDef t1Init;
    t1Init.TIM_Prescaler = 0;
    t1Init.TIM_CounterMode = TIM_CounterMode_Up;
    t1Init.TIM_Period = 256;
    t1Init.TIM_ClockDivision = TIM_CKD_DIV1;
    t1Init.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM1, &t1Init);

    // setup timer1 output channel for PWM
    TIM_OCInitTypeDef  t1_OCInit;
    t1_OCInit.TIM_OCMode = TIM_OCMode_PWM2;   
    t1_OCInit.TIM_OutputState = TIM_OutputState_Enable;   
    t1_OCInit.TIM_OutputNState = TIM_OutputNState_Enable;   
    t1_OCInit.TIM_Pulse = 0;
    t1_OCInit.TIM_OCPolarity = TIM_OCPolarity_Low;   
    t1_OCInit.TIM_OCNPolarity = TIM_OCNPolarity_Low;   
    t1_OCInit.TIM_OCIdleState = TIM_OCIdleState_Set;   
    t1_OCInit.TIM_OCNIdleState = TIM_OCIdleState_Reset;   
    TIM_OC1Init(TIM1, &t1_OCInit);

    // switch timer1 to PWM mode
    TIM_EnablePWMOutputs(TIM1);

    // set PORTA:8 to alt. function output (ie timer1 PWM)
    GPIO_InitTypeDef a8Init;
    a8Init.GPIO_Pin = GPIO_Pin_8;
    a8Init.GPIO_Mode = GPIO_Mode_AF_PP;
    a8Init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &a8Init);

    TIM_Cmd(TIM1, ENABLE);
    TIM_Cmd(TIM2, ENABLE);                              // start timers

    while(1)
    {
    }
}

Best Answer

Used variables:

#define CHUNK_SIZE 255
// uint8_t buffer[CHUNK_SIZE]; optional!!!
uint16_t offset = 0;

Algorithm

Initialize your DAC.

Initialize DMA:

  1. configure required stream/channel for selected DMA according to update event of selected timer
  2. set DMA data address to &waf + offset
  3. set DMA periferal address to DAC_DATA_REGISTER
  4. set DMA length to ((offset + CHUNK_SIZE) < ARRAY_SIZE(wav)) ? CHUNK_SIZE : (ARRAY_SIZE(wav) - offset )
  5. add calculated DMA length to offset for next use
  6. enable only total complete IRQ

Initialize your timer:

  1. setup required frequency
  2. do not enable any timer's IRQ
  3. enable DMA triggering by timer update event

Start timer

When DMA TC IRQ will occur

  1. if offset == ARRAY_SIZE(wav) then stop timer - playing finished
  2. otherwise reinitialize DMA with new offset

That's all! You must be sure that reinitialize of DMA faster then timer period end! Otherwise pause timer by gate timer's clock while reinit process. If DMA can't read from flash memory use buffer in ram and copy wav by parts with memcpy before each chunck playing (before DMA initialize process).

Sorry for grammatical errors.