1
\$\begingroup\$

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)
    {
    }
}
\$\endgroup\$
  • 3
    \$\begingroup\$ This question can be fully answered by just looking at the programming manual document. \$\endgroup\$ – Eugene Sh. May 16 '16 at 16:23
  • \$\begingroup\$ I think you're looking at this in the wrong way. Triggering the DMA from a timer should certainly be possible, but what you want to do is configure the DMA module to only transfer a single sample's worth of data for each trigger, while still incrementing through your whole data block before wrapping. This sort of behavior has certainly been possible with the DMA modules in MCUs I've used in the past - the manual for yours should tell if it can too. \$\endgroup\$ – brhans May 16 '16 at 17:20
  • \$\begingroup\$ @brhans - absolutely, that's what I'd like to do, but I can't find a way to make it do that from the reference manual. The transfer addresses are shadowed and reloaded from the 'set' registers each time a transfer is started, so if I set a DMA size of 1 byte and trigger it repeatedly with the timer it keeps sending the same byte, I need to somehow get the address increments to carry over to the next transfer. \$\endgroup\$ – Charlie Skilbeck May 16 '16 at 17:39
  • \$\begingroup\$ You should really add the code you are trying. You should have no code in the timer, the interrupt will just trigger the transfer cells to happen. You only set up the source address, destination address, etc in the main loop somewhere, then let it automatically go through the entire memory chunk. \$\endgroup\$ – justing May 17 '16 at 0:00
  • \$\begingroup\$ justing, code added. I don't see how to get the DMA to transfer the data at a certain rate (ie slower than afap) \$\endgroup\$ – Charlie Skilbeck May 17 '16 at 5:59
2
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ So how frequently will each packet be transferred by the DMA? \$\endgroup\$ – Charlie Skilbeck May 17 '16 at 18:15
  • \$\begingroup\$ I'm interested in step 1 - does it change the rate at which the DMA packets are delivered? I didn't think so from looking at the datasheet, but I might have missed something \$\endgroup\$ – Charlie Skilbeck May 17 '16 at 18:33
  • \$\begingroup\$ DMA triggered by timer and update DAC data register each trigger event, DAC just set new voltage level according to it data register. So timer frequency - it is frequency of DMA byte transfer and it is output frequency of DAC. \$\endgroup\$ – imbearr May 18 '16 at 10:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.