Frequency modulation via DMA

kdharbert

Well-known member
I’ve been doing frequency modulation with a Teensy 4.1 using interrupt code. I modulate a 1khz sine wave on a 40khz carrier by looping through a series of timer frequencies in an interrupt that reconfigures the timer after each cycle. I’m having to dig deep on STM32 DMA, and ran into a set of features that would let me do the modulation without the interrupt code. I'm newish to DMA usage and I suspect that the Teensy 4.1 can also use DMA to loop through a ring buffer containing the timer values and auto load them into the timer’s configuration register. Can I get a code fragment that would do this?
 
I did something similar for the SmartMatrix library. Not sure if this is exactly what you need, but it uses DMA to update the timer registers with each cycle. I would recommend reading through the FlexPWM chapter of the reference manual - it has lots of extra features that might be useful. Here is a sample sketch.

Code:
// Sample code that demonstrates arbitrary pulse chain generation using FlexPWM.
// Each pulse triggers DMA to write new data into the FlexPWM registers which takes effect on the next pulse.
// This allows independent control of the frequency and duty cycle for each pulse without using interrupts.

#include <Arduino.h>
#include <DMAChannel.h>

/* The following pins with FlexPWM capability can be used: 
   Teensy 4.0: Pins 0-9, 22-25, 28-29, 33-39
   Teensy 4.1: Pins 0-9, 22-25, 28-29, 33, 36-37, 42-47, 51, 54
   Teensy Micromod: Pins 0-9, 22-25, 28-29, 33-39 */
const int pin = 0;  // Teensy pin number

// Pulse data
const int numPulses = 4;
float frequencies[numPulses] = { 1500000, 750000, 375000, 187500 };  // Hz
float dutyCycles[numPulses] = { 0.5, 0.5, 0.5, 0.5 };
const int prescale = 0;  // increase timer prescale for very low frequencies (minimum frequency is F_BUS/65536)
const bool invertPolarity = false;

// FlexPWM pin/channel structure (from cores/pwm.c)
struct pwm_pin_info_struct {
  uint8_t type;
  uint8_t module;
  uint8_t channel;
  uint8_t muxval;
};
extern "C" const struct pwm_pin_info_struct pwm_pin_info[];

DMAChannel timerUpdateDMA;
volatile DMAMEM int16_t timerVals[numPulses][2];  // allocate buffer in RAM2 to hold data for two timer registers per pulse

void setup() {
  // Calculate timer period and ontime (in ticks) for each pulse
  for (int i = 0; i < numPulses; i++) {
    int16_t periodTicks = (int16_t)((F_BUS_ACTUAL >> prescale) / frequencies[i]);
    int16_t onTimeTicks = (int16_t)((F_BUS_ACTUAL >> prescale) * dutyCycles[i] / frequencies[i]);
    timerVals[i][0] = periodTicks - 1;
    timerVals[i][1] = onTimeTicks - 1;
  }
  arm_dcache_flush((void *)&timerVals[0][0], sizeof(timerVals));  // need to flush cache so that DMA can read data from timerVals if it is declared DMAMEM

  // configure pin with high speed and drive strength configuration
  pinMode(pin, OUTPUT);
  *(portControlRegister(pin)) = 0xFF;

  // Get hardware struct associated with the selected pin
  const pwm_pin_info_struct pinInfo = pwm_pin_info[pin];

  // Get the register structure associated with this PWM module
  IMXRT_FLEXPWM_t *flexpwm;
  uint8_t submodule = pinInfo.module & 0x03;
  uint16_t bitmask = 1 << submodule;
  uint8_t pwmDmaTrigger;
  switch ((pinInfo.module >> 4) & 3) {
    case 0:
      flexpwm = &IMXRT_FLEXPWM1;
      pwmDmaTrigger = DMAMUX_SOURCE_FLEXPWM1_WRITE0 + submodule;
      break;
    case 1:
      flexpwm = &IMXRT_FLEXPWM2;
      pwmDmaTrigger = DMAMUX_SOURCE_FLEXPWM2_WRITE0 + submodule;
      break;
    case 2:
      flexpwm = &IMXRT_FLEXPWM3;
      pwmDmaTrigger = DMAMUX_SOURCE_FLEXPWM3_WRITE0 + submodule;
      break;
    default:
      flexpwm = &IMXRT_FLEXPWM4;
      pwmDmaTrigger = DMAMUX_SOURCE_FLEXPWM4_WRITE0 + submodule;
  }

  // stop timer until after setup is complete
  flexpwm->MCTRL &= ~FLEXPWM_MCTRL_RUN(bitmask);

  // set the pin mux
  *(portConfigRegister(pin)) = pinInfo.muxval;

  // get timer registers that control this pin
  volatile uint16_t *timerRegisterPeriod, *timerRegisterOnTime;
  uint16_t polarityBit, enableBit;
  switch (pinInfo.channel) {
    case 0:  // Channel X
      timerRegisterPeriod = &(flexpwm->SM[submodule].VAL1);
      timerRegisterOnTime = &(flexpwm->SM[submodule].VAL0);
      polarityBit = invertPolarity ? 0 : FLEXPWM_SMOCTRL_POLX;
      enableBit = FLEXPWM_OUTEN_PWMX_EN(bitmask);
      break;
    case 1:  // Channel A
      timerRegisterPeriod = &(flexpwm->SM[submodule].VAL1);
      timerRegisterOnTime = &(flexpwm->SM[submodule].VAL3);
      polarityBit = invertPolarity ? FLEXPWM_SMOCTRL_POLA : 0;
      enableBit = FLEXPWM_OUTEN_PWMA_EN(bitmask);
      break;
    default:  // Channel B
      timerRegisterPeriod = &(flexpwm->SM[submodule].VAL1);
      timerRegisterOnTime = &(flexpwm->SM[submodule].VAL5);
      polarityBit = invertPolarity ? FLEXPWM_SMOCTRL_POLB : 0;
      enableBit = FLEXPWM_OUTEN_PWMB_EN(bitmask);
  }

  // set up PWM with initial settings
  flexpwm->MCTRL |= FLEXPWM_MCTRL_CLDOK(bitmask);
  flexpwm->SM[submodule].INIT = 0;
  flexpwm->SM[submodule].VAL2 = 0;
  flexpwm->SM[submodule].VAL4 = 0;
  flexpwm->SM[submodule].CTRL = FLEXPWM_SMCTRL_FULL | FLEXPWM_SMCTRL_PRSC(prescale);
  flexpwm->SM[submodule].OCTRL = polarityBit;
  flexpwm->OUTEN |= enableBit;
  flexpwm->MCTRL |= FLEXPWM_MCTRL_LDOK(bitmask);

  // Generate a DMA trigger at the beginning of each cycle (when the signal goes high)
  // This is used to trigger DMA transfer to reload a new period and duty cycle into the registers, which take effect on the next cycle.
  flexpwm->SM[submodule].DMAEN |= FLEXPWM_SMDMAEN_VALDE;  // enable DMA trigger at cycle start

  // set up DMA transfer
  timerUpdateDMA.begin(false);
  unsigned int minorLoopBytes, minorLoopIterations, majorLoopBytes, majorLoopIterations;
  int destinationAddressOffset, destinationAddressLastOffset, sourceAddressOffset, sourceAddressLastOffset, minorLoopOffset;
  volatile void *destinationAddress1, *destinationAddress2, *sourceAddress;

  // configure DMA transfer
  // timerUpdateDMA updates two FlexPWM registers from the timerVals array.
  // The source address reads from timerVals[x][0], then timerVals[x][1], then moves to the next index.
  // The destination address writes to timerRegisterPeriod, then timerRegisterOnTime.
  // A minor loop offset is used to reset the destination address after each transaction.
  // When all iterations are complete (all pulses have been output), the source address is reset to the beginning and the process repeats.
  minorLoopIterations = 2;  // number of registers to update
  minorLoopBytes = minorLoopIterations * sizeof(uint16_t);
  majorLoopIterations = numPulses;
  majorLoopBytes = majorLoopIterations * minorLoopBytes;
  sourceAddress = &timerVals[0][0];
  sourceAddressOffset = sizeof(uint16_t);  // address offset from timerVals[x][0] to timerVals[x][1]
  sourceAddressLastOffset = -majorLoopBytes;
  destinationAddress1 = timerRegisterPeriod;
  destinationAddress2 = timerRegisterOnTime;
  destinationAddressOffset = (int)destinationAddress2 - (int)destinationAddress1;
  minorLoopOffset = -minorLoopIterations * destinationAddressOffset;
  destinationAddressLastOffset = minorLoopOffset;

  DMA_CR |= DMA_CR_EMLM;  // Enable minor loop mapping so that we can have a minor loop offset on the destination address
  timerUpdateDMA.disable();
  timerUpdateDMA.TCD->SADDR = sourceAddress;
  timerUpdateDMA.TCD->SOFF = sourceAddressOffset;
  timerUpdateDMA.TCD->SLAST = sourceAddressLastOffset;
  timerUpdateDMA.TCD->ATTR_SRC = DMA_TCD_ATTR_SIZE_16BIT;
  timerUpdateDMA.TCD->DADDR = destinationAddress1;
  timerUpdateDMA.TCD->DOFF = destinationAddressOffset;
  timerUpdateDMA.TCD->DLASTSGA = destinationAddressLastOffset;
  timerUpdateDMA.TCD->ATTR_DST = DMA_TCD_ATTR_SIZE_16BIT;
  timerUpdateDMA.TCD->NBYTES_MLOFFYES = DMA_TCD_NBYTES_DMLOE | DMA_TCD_NBYTES_MLOFFYES_MLOFF(minorLoopOffset) | DMA_TCD_NBYTES_MLOFFYES_NBYTES(minorLoopBytes);
  timerUpdateDMA.TCD->BITER = majorLoopIterations;
  timerUpdateDMA.TCD->CITER = majorLoopIterations;
  timerUpdateDMA.triggerAtHardwareEvent(pwmDmaTrigger);
  timerUpdateDMA.enable();

  // After everything is set up: enable FlexPWM timer to start process
  flexpwm->MCTRL |= FLEXPWM_MCTRL_RUN(1 << submodule);
}

void loop() {
}

Here is the output (configured to make a repeating chain of 4 pulses):

screenshot.png
 
Back
Top