ARM Cortex M4, e.g. Adafruit Feather M4: DAC vs. PWM?

acc. to ARM Cortex M4, e.g. Adafruit Feather M4

The Adafruit company writes that A0 and A1 may be either DAC or pwm, IIUC:

Analog Pins:

A0 - This pin is analog input A0 but is also an analog output due to having a DAC (digital-to-analog converter). You can set the raw voltage to anything from 0 to 3.3V, unlike PWM outputs this is a true analog output
A1 - This pin is analog input A1 but is also an analog output due to having a DAC (digital-to-analog converter). This is the second DAC, and is 'independent' of A0. You can set the raw voltage to anything from 0 to 3.3V, unlike PWM outputs this is a true analog output. Also can do PWM.

pwm is applied via analogWrite on digital pins, but which are the correct commands for "real" DAC voltage vs. pwm on the analog ports?

The code says you just use "analogWrite()", and if it's one of the DAC pins, you get the real DAC.

I'm not sure how you'd force PWM on A1 that can apparently do either...

an actual pwmWrite vs. analogWrite IMO would be better, clearer and more unequivocal, but nonetheless I'm still curious about pwm on Analog Pin A1 now though.

Hi dsyleixa,

On the Adafruit Feather M4 it's possible to get PWM on A1, (aka port pin PA05), using timer TC0, channel 1.

A0 on the other hand doesn't have PWM functionality, as it's not connected to any timers.

Hi dsyleixa,

Here's some code that outputs 50Hz PWM, on pin A1, using timer TC0 in Match PWM (MPWM) mode. I ran it on my Adafruit Itsy Bitsy M4, but it'll work on both the Metro M4 and Feather M4 as well.

Note that it's necessary to first activate timer TC0, as by default the Adafruit core code doesn't power up timers: TC0, TC1 or TC2. This means that Adafruit are only providing a DAC output on this pin with analogWrite() and not PWM.

// Output 50Hz PWM, on pin A1, using Adafruit Metro M4, Feather M4 or Itsy Bitsy M4

void setup() 
{
  MCLK->APBAMASK.reg |= MCLK_APBAMASK_TC0;           // Activate timer TC0
  
  // Set up the generic clock (GCLK7) used to clock timers 
  GCLK->GENCTRL[7].reg = GCLK_GENCTRL_DIV(3) |       // Divide the 48MHz clock source by divisor 3: 48MHz/3 = 16MHz
                         GCLK_GENCTRL_IDC |          // Set the duty cycle to 50/50 HIGH/LOW
                         GCLK_GENCTRL_GENEN |        // Enable GCLK7
                         GCLK_GENCTRL_SRC_DFLL;      // Generate from 48MHz DFLL clock source
  while (GCLK->SYNCBUSY.bit.GENCTRL7);               // Wait for synchronization  

  GCLK->PCHCTRL[9].reg = GCLK_PCHCTRL_CHEN |         // Enable perhipheral channel
                         GCLK_PCHCTRL_GEN_GCLK7;     // Connect generic clock 7 to TC0

  // Enable the peripheral multiplexer on pin A1
  PORT->Group[g_APinDescription[A1].ulPort].PINCFG[g_APinDescription[A1].ulPin].bit.PMUXEN = 1;
  
  // Set A1 the peripheral multiplexer to peripheral E(4): TC0, Channel 1
  PORT->Group[g_APinDescription[A1].ulPort].PMUX[g_APinDescription[A1].ulPin >> 1].reg |= PORT_PMUX_PMUXO(4);
  
  TC0->COUNT16.CTRLA.reg = TC_CTRLA_PRESCALER_DIV16 |        // Set prescaler to 16, 16MHz/16 = 1MHz
                           TC_CTRLA_PRESCSYNC_PRESC |        // Set the reset/reload to trigger on prescaler clock
                           TC_CTRLA_MODE_COUNT16;            // Set the counter to 16-bit mode

  TC0->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_MPWM;      // Set-up TC0 timer for Match PWM mode (MPWM)

  TC0->COUNT16.CC[0].reg = 19999;                    // Use CC0 register as TOP value, set for 50Hz PWM
  while (TC0->COUNT16.SYNCBUSY.bit.CC0);             // Wait for synchronization

  TC0->COUNT16.CC[1].reg = 9999;                     // Set the duty cycle to 50% (CC1 half of CC0)
  while (TC0->COUNT16.SYNCBUSY.bit.CC1);             // Wait for synchronization

  TC0->COUNT16.CTRLA.bit.ENABLE = 1;                 // Enable timer TC0
  while (TC0->COUNT16.SYNCBUSY.bit.ENABLE);          // Wait for synchronization
}

void loop() {}

hi,
thank you very much for your code!
I now see that just A1 provides pwm, not A0 though.
But tbh, 50Hz is a bit slow, is it possble to have 600-800 Hz instead, similar to common Arduino pwm frequency, and won't that interfere then with different pwm pins, i2c clock, SPI clock a/o timers ?

Hi dysleixa,

To change the PWM frequency to say 700Hz, just set the CC0 register to 1427, instead of 19999.

The PWM frequency formula is:

PWM Frequency = GCLK frequency / (N * (TOP + 1))

where:
N = timer prescaler (1, 2, 4, 8, 16, 64, 256 or 1024)
TOP = the value of the CC0 register (in MPWM mode)

The PWM resolution formula is:

PWM Resolution = log(CC0 + 1)/log(2)

For the above example:

PWM frequency = 16MHz / (16 * (19999 + 1)) = 50Hz

PWM resolution = log(19999 + 1)/log(2) = 14bits

The CC1 register determines the PWM duty cycle from 0 for (0%) up to the CC0 register value (100%).

If you intend to change the duty cycle during operation then it's best to use the buffered register CCBUF1:

TC0->COUNT16.CCBUF[1].reg = 4999;                  // Set the duty cycle by loading buffered CC1 register CCBUF1

The buffered BUFCCx registers allow the CCx registers to be updated without generating glitches on your PWM output. On the SAMD51 the BUFCCx registers have no corresponding synchronization bit to check in the SYNCBUSY register. The CCBUFx registers are loaded into the CCx registers at the start of each timer cycle.

thank you for your efforts - but OMG, that is far beyond my skills, I don't understand all that maths, too many variables, registers, and formulas, I just can speak simple Arduinish, e.g. analogWrite(A1, 127) :slight_smile:

and BTW, which is the pwm range? 0...255 or 0...1024? Can it be changed arbitrarily by setPwmRange(A1, range) or sth.?

thank you for your efforts - but OMG, that is far beyond my skills, I don't understand all that maths, too many variables, registers, and formulas, I just can speak simple Arduinish, e.g. analogWrite(A1, 127) :slight_smile:

Well you never know, if you ever happen to need PWM output on A1?

you are right, it's a) out of curiosity and b) because missing all pins 2,3,7,8 on the Feather M4, and 5,6,9,10 are already used for SPI (TFT, touch, SD), so eventually I have left just 4,11,12,13 for digital, 1-Wire, echo, trigger, interrupt, or pwm pins. So having an extra pwm would be fine, but admittedly not absolutely necessary for A1.
OTOH, as Adafruit is advertizing this feature for common Arduino IDE users, I actually supposed pwm on A1 to be more convenient to use by "common Arduino API programming style"

and BTW, which is the pwm range? 0...255 or 0...1024? Can it be changed arbitrarily by setPwmRange(A1, range) or sth.?

The PWM range is between 0 and the value in the CC0 register.

The analogWrite() function, on the Adafruit M4 boards only give you 8-bit resolution (0-255), running at 1.83kHz. The SAMD51 timers are capable of much higher PWM resolution though.

If you prefer, it's possible to set up A1 to output exactly the same PWM as analogWrite(), 0-255 input running at 1.83kHz.

The following code mimics analogWrite() giving 8-bit resolution (0-255), at 1.83kHz on A1:

// Output 1.83kHz, 8-bit resolution on Metro M4/Feather M4 or Itsy Bitsy M4

void setup() 
{
  MCLK->APBAMASK.reg |= MCLK_APBAMASK_TC0;           // Activate timer TC0
  
  // Set up the generic clock (GCLK7) used to clock timers 
  GCLK->GENCTRL[7].reg = GCLK_GENCTRL_DIV(1) |       // Divide the 120MHz clock source by divisor 1: 120MHz/1 = 120MHz
                         GCLK_GENCTRL_IDC |          // Set the duty cycle to 50/50 HIGH/LOW
                         GCLK_GENCTRL_GENEN |        // Enable GCLK7
                         GCLK_GENCTRL_SRC_DPLL0;     // Generate from 120MHz DPLL clock source
  while (GCLK->SYNCBUSY.bit.GENCTRL7);               // Wait for synchronization  

  GCLK->PCHCTRL[9].reg = GCLK_PCHCTRL_CHEN |         // Enable perhipheral channel
                         GCLK_PCHCTRL_GEN_GCLK7;     // Connect generic clock 7 to TC0

  // Enable the peripheral multiplexer on pin A1
  PORT->Group[g_APinDescription[A1].ulPort].PINCFG[g_APinDescription[A1].ulPin].bit.PMUXEN = 1;
  
  // Set A1 the peripheral multiplexer to peripheral E(4): TC0, Channel 1
  PORT->Group[g_APinDescription[A1].ulPort].PMUX[g_APinDescription[A1].ulPin >> 1].reg |= PORT_PMUX_PMUXO(4);
  
  TC0->COUNT8.CTRLA.reg = TC_CTRLA_PRESCALER_DIV256 |     // Set prescaler to 256, 120MHz/256 = 4687.50kHz
                           TC_CTRLA_PRESCSYNC_PRESC |     // Set the reset/reload to trigger on prescaler clock
                           TC_CTRLA_MODE_COUNT8;          // Set the counter to 8-bit mode

  TC0->COUNT8.WAVE.reg = TC_WAVE_WAVEGEN_NPWM;      // Set-up TC0 timer for Normal PWM mode (NPWM)

  TC0->COUNT8.PER.reg = 0xFF;                       // Use PER register as TOP value, set for 1.83kHz PWM
  while (TC0->COUNT8.SYNCBUSY.bit.PER);             // Wait for synchronization

  TC0->COUNT8.CC[1].reg = 127;                      // Set the duty cycle to 50% (CC1 half of PER)
  while (TC0->COUNT8.SYNCBUSY.bit.CC1);             // Wait for synchronization

  TC0->COUNT8.CTRLA.bit.ENABLE = 1;                 // Enable timer TC0
  while (TC0->COUNT8.SYNCBUSY.bit.ENABLE);          // Wait for synchronization
}

void loop() {}

that is great, thanks a lot! I'll immediately try this out when I'm home tonight!