SAMD51 (adafruit feather m4 express) D12(PA22) and D13(PA23) VDCPWM

This is a lot of fun trying to figure out. I’m glad that MartinL put up several examples. And thanks to Robin2 for the serial input.

I don’t have a lot of experience, but I’m planning on using this to control some fans.

I’ve been following along the datasheet and commenting where I find things as best I can. Is there like a best practice cheat sheet or something? Like, for example is it a bad idea to NOT divide the DPLL0 clock source?

Does CC[0] == WO[0] && … CC[3] == WO[3] for a given TCC? Or do the channels correspond to the IOSET(s)?

// Adafruit feather m4 express: Set-up digital pin D12 and D13 to 25KHz variable duty cycle
// PWM frequency = (selected clock source / DIV) / (PRESCALER_DIV * (PERIOD + 1))
//                  ( GCLK_GENCTRL_SRC_DPLL0 / GCLK_GENCTRL_DIV(0) ) / (TC_CTRLA_PRESCALER_DIV2

#define PERIOD 2399

const byte numChars = 32;
char receivedChars[numChars]; // an array to store the received data
String inString = "";
float inputfloat = 0;
boolean newData = false;

void setup()
{
  // Set up the generic clock (GCLK7) to clock timer TCC1
  GCLK->GENCTRL[7].reg = GCLK_GENCTRL_DIV(0) |       // Divide the clock source
                         GCLK_GENCTRL_IDC |          // Set the duty cycle to 50/50 HIGH/LOW "square wave"
                         GCLK_GENCTRL_GENEN |        // Enable GCLK7
                         //GCLK_GENCTRL_SRC_DFLL;      // Select 48MHz DFLL clock source
                         //GCLK_GENCTRL_SRC_DPLL1;     // Select 100MHz DPLL clock source
                         GCLK_GENCTRL_SRC_DPLL0;     // Select 120MHz DPLL clock source
  while (GCLK->SYNCBUSY.bit.GENCTRL7);               // Wait for synchronization

  // http://ww1.microchip.com/downloads/en/DeviceDoc/60001507C.pdf
  // page 169
  GCLK->PCHCTRL[25].reg = GCLK_PCHCTRL_CHEN |        // Enable the TCC0/TCC1 perhipheral channel
                          GCLK_PCHCTRL_GEN_GCLK7;    // Connect generic clock 7 to TCC0 and TCC1

  // Enable the peripheral multiplexer on pin D12
  PORT->Group[g_APinDescription[12].ulPort].PINCFG[g_APinDescription[12].ulPin].bit.PMUXEN = 1;

  // Enable the peripheral multiplexer on pin D13
  PORT->Group[g_APinDescription[13].ulPort].PINCFG[g_APinDescription[13].ulPin].bit.PMUXEN = 1;

  // http://ww1.microchip.com/downloads/en/DeviceDoc/60001507C.pdf
  // page 35
  // Set the D12 (PORT_PA22 TCC1/WO[6]) peripheral multiplexer to peripheral (EVEN port number) F(0x5): TCC1, Channel 0
  PORT->Group[g_APinDescription[12].ulPort].PMUX[g_APinDescription[12].ulPin >> 1].reg |= PORT_PMUX_PMUXE(5);

  // Set the D13 (PORT_PA23 TCC1/WO[7]) peripheral multiplexer to peripheral (ODD port number) F(0x5): TCC1, Channel 1
  PORT->Group[g_APinDescription[13].ulPort].PMUX[g_APinDescription[13].ulPin >> 1].reg |= PORT_PMUX_PMUXO(5);

  TCC1->CTRLA.reg = TC_CTRLA_PRESCALER_DIV2 |        // Set prescaler to 2, 120MHz/2 = 60MHz
                    TC_CTRLA_PRESCSYNC_PRESC;        // Set the reset/reload to trigger on prescaler clock

  TCC1->WAVE.reg = TC_WAVE_WAVEGEN_NPWM;             // Set-up TCC1 timer for Normal (single slope) PWM mode (NPWM)
  while (TCC1->SYNCBUSY.bit.WAVE)                    // Wait for synchronization

    //pwm resolution
    //log(PER.reg 2399) / log(2) = 2^11 steps (2048)
    TCC1->PER.reg = PERIOD;                          // Set-up the PER (period) register for 25KHz
  while (TCC1->SYNCBUSY.bit.PER);                    // Wait for synchronization

  TCC1->CC[0].reg = PERIOD;                          // D12??? all the way on
  while (TCC1->SYNCBUSY.bit.CC0);                    // Wait for synchronization

  TCC1->CC[1].reg = PERIOD;                          // D13??? all the way on
  while (TCC1->SYNCBUSY.bit.CC1);                    // Wait for synchronization

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


  Serial.begin(9600);
  while (!Serial);
  Serial.println("<Arduino is ready>");
}

void changeDutyCycle() {
  if (newData == true) {
    inString = receivedChars;
    float rawinputfloat = inString.toFloat();
    inString = "";
    inputfloat = constrain(rawinputfloat, 0.00, 100.00);
    float fperiod = inputfloat * (PERIOD / 100.00);
    int period = constrain(fperiod, 0, PERIOD);
    // http://ww1.microchip.com/downloads/en/DeviceDoc/60001507C.pdf
    //page 43-44 table 6-29
    // is ccbuf[0] the IOSET 1 pins?, ie if I want different duty cycles per pin do I have to pick another pin in a different IOSET?
    TCC1->CCBUF[0].reg = period; //per can equal 0-2399 (+1) microseconds?
    TCC1->CCBUF[1].reg = period; //does this need any input validation?
  }
}

void loop()
{
  recvWithEndMarker();
  changeDutyCycle();
  showNewData();
}


void recvWithEndMarker() {
  static byte ndx = 0;
  const char endMarker = '\n';
  char rc;

  // if (Serial.available() > 0) {
  while (Serial.available() > 0 && newData == false) {
    rc = Serial.read();

    if (rc != '\n') {
      receivedChars[ndx] = rc;
      ndx++;
      if (ndx >= numChars) {
        ndx = numChars - 1;
      }
    }
    else {
      receivedChars[ndx] = '\0'; // terminate the string
      ndx = 0;
      newData = true;
    }
  }
}

void showNewData() {
  if (newData == true) {
    Serial.print("This just in ... ");
    Serial.println(inputfloat);
    newData = false;
  }
}

I’m using the arduino 1.8.9 IDE and adafruit’s latest board definition and this compiles.

Thank you for your time.

Edit: forgot some lines whoops

http://ww1.microchip.com/downloads/en/DeviceDoc/60001507C.pdf

I found on page 156 of the datasheet:

14.6.2.7 Changing the Clock Frequency
The selected source for a Generator can be divided by writing a division value in the Division Factor bit
field of the Generator Control register (GENCTRLn.DIV). How the actual division factor is calculated is
depending on the Divide Selection bit (GENCTRLn.DIVSEL).
If GENCTRLn.DIVSEL=0 and GENCTRLn.DIV is either 0 or 1, the output clock will be undivided.
Note: The number of available DIV bits may vary from Generator to Generator.

One question down, several more to go!

Hi dquigg,

Does CC[0] == WO[0] && ... CC[3] == WO[3] for a given TCC?

By default they do, (but this can be changed in the "Output Matrix Channel Pin Routing Configuration" described on page 1853 of the SAMD51 datasheet in the Waveform Extension section).

If there are more output channels than timer compare channels then the compare channels are repeated. For example timer TCC0 has outputs WO[0]...WO[7], but only 6 compare channels CC[0] to CC[5], therefore if WO[5] == CC[5] then WO[6] == CC[0] and WO[7] == CC[1].

Enabling dead-time insertion makes the corresponding compare and repeated compare channels, for example on timer TCC0 CC[1] on WO[1] and CC[1] on W[7], become the complementary low side and high side outputs.

MartinL:
Hi dquigg,

By default they do, (but this can be changed in the “Output Matrix Channel Pin Routing Configuration” described on page 1853 of the SAMD51 datasheet in the Waveform Extension section).

If there are more output channels than timer compare channels then the compare channels are repeated. For example timer TCC0 has outputs WO[0]…WO[7], but only 6 compare channels CC[0] to CC[5], therefore if WO[5] == CC[5] then WO[6] == CC[0] and WO[7] == CC[1].

Enabling dead-time insertion makes the corresponding compare and repeated compare channels, for example on timer TCC0 CC[1] on WO[1] and CC[1] on W[6] become the complementary low side and high side outputs.

Thank you for your reply,

I believe that for TCC1: CC[2] == WO[6] && CC[3] == WO[7] because TCC1 has 4 CC.

// Adafruit feather m4 express: Set-up digital pin D12 and D13 to 25KHz variable duty cycle
// PWM frequency = (selected clock source / DIV) / (PRESCALER_DIV * (PERIOD + 1))
//                  ( GCLK_GENCTRL_SRC_DPLL0 / GCLK_GENCTRL_DIV(0) ) / (TC_CTRLA_PRESCALER_DIV2

#define PERIOD 2399

const byte numChars = 32;
char receivedChars[numChars]; // an array to store the received data
String inString = "";
float inputfloat = 0;
boolean newData = false;

void setup()
{
  // Set up the generic clock (GCLK7) to clock timer TCC1
  GCLK->GENCTRL[7].reg = GCLK_GENCTRL_DIV(0) |       // Divide the clock source
                         GCLK_GENCTRL_IDC |          // Set the duty cycle to 50/50 HIGH/LOW "square wave"
                         GCLK_GENCTRL_GENEN |        // Enable GCLK7
                         //GCLK_GENCTRL_SRC_DFLL;      // Select 48MHz DFLL clock source
                         //GCLK_GENCTRL_SRC_DPLL1;     // Select 100MHz DPLL clock source
                         GCLK_GENCTRL_SRC_DPLL0;     // Select 120MHz DPLL clock source
  while (GCLK->SYNCBUSY.bit.GENCTRL7);               // Wait for synchronization

  // http://ww1.microchip.com/downloads/en/DeviceDoc/60001507C.pdf
  // page 169
  GCLK->PCHCTRL[25].reg = GCLK_PCHCTRL_CHEN |        // Enable the TCC0/TCC1 perhipheral channel
                          GCLK_PCHCTRL_GEN_GCLK7;    // Connect generic clock 7 to TCC0 and TCC1

  // Enable the peripheral multiplexer on pin D12
  PORT->Group[g_APinDescription[12].ulPort].PINCFG[g_APinDescription[12].ulPin].bit.PMUXEN = 1;

  // Enable the peripheral multiplexer on pin D13
  PORT->Group[g_APinDescription[13].ulPort].PINCFG[g_APinDescription[13].ulPin].bit.PMUXEN = 1;

  // http://ww1.microchip.com/downloads/en/DeviceDoc/60001507C.pdf
  // page 35
  // Set the D12 (PORT_PA22 TCC1/WO[6]) peripheral multiplexer to peripheral (EVEN port number) F(0x5): TCC1, Channel 0
  PORT->Group[g_APinDescription[12].ulPort].PMUX[g_APinDescription[12].ulPin >> 1].reg |= PORT_PMUX_PMUXE(5);

  // Set the D13 (PORT_PA23 TCC1/WO[7]) peripheral multiplexer to peripheral (ODD port number) F(0x5): TCC1, Channel 1
  PORT->Group[g_APinDescription[13].ulPort].PMUX[g_APinDescription[13].ulPin >> 1].reg |= PORT_PMUX_PMUXO(5);

  TCC1->CTRLA.reg = TCC_CTRLA_PRESCALER_DIV2 |        // Set prescaler to 2, 120MHz/2 = 60MHz
                    TCC_CTRLA_PRESCSYNC_PRESC;        // Set the reset/reload to trigger on prescaler clock

  TCC1->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;             // Set-up TCC1 timer for Normal (single slope) PWM mode (NPWM)
  while (TCC1->SYNCBUSY.bit.WAVE)                    // Wait for synchronization

    //pwm resolution
    //log(PER.reg 2399) / log(2) = 2^11 steps (2048)
    TCC1->PER.reg = PERIOD;                          // Set-up the PER (period) register for 25KHz
  while (TCC1->SYNCBUSY.bit.PER);                    // Wait for synchronization

  TCC1->CC[2].reg = PERIOD;                          // D12??? all the way on
  while (TCC1->SYNCBUSY.bit.CC2);                    // Wait for synchronization

  TCC1->CC[3].reg = PERIOD;                          // D13??? all the way on
  while (TCC1->SYNCBUSY.bit.CC3);                    // Wait for synchronization

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


  Serial.begin(9600);
  while (!Serial);
  Serial.println("<Arduino is ready>");
}

void changeDutyCycle() {
  if (newData == true) {
    inString = receivedChars;
    float rawinputfloat = inString.toFloat();
    inString = "";
    inputfloat = constrain(rawinputfloat, 0.00, 100.00);
    float fperiod = inputfloat * (PERIOD / 100.00);
    int period = constrain(fperiod, 0, PERIOD);
    // http://ww1.microchip.com/downloads/en/DeviceDoc/60001507C.pdf
    //page 43-44 table 6-29
    TCC1->CCBUF[2].reg = period; //per can equal 0-2399 (+1) microseconds?
    TCC1->CCBUF[3].reg = period; //does this need any input validation?
  }
}

void loop()
{
  recvWithEndMarker();
  changeDutyCycle();
  showNewData();
}

void recvWithEndMarker() {
  static byte ndx = 0;
  const char endMarker = '\n';
  char rc;
  // if (Serial.available() > 0) {
  while (Serial.available() > 0 && newData == false) {
    rc = Serial.read();

    if (rc != '\n') {
      receivedChars[ndx] = rc;
      ndx++;
      if (ndx >= numChars) {
        ndx = numChars - 1;
      }
    }
    else {
      receivedChars[ndx] = '\0'; // terminate the string
      ndx = 0;
      newData = true;
    }
  }
}

void showNewData() {
  if (newData == true) {
    Serial.print("This just in ... ");
    Serial.println(inputfloat);
    newData = false;
  }
}

I changed TC_ to TCC_ for the prescaler and wavegen.

Time to break out the breadboard to see if it does anything!

It works, 0-3.3v at 24.95KHz. I ran it through a bidirectional level shifter and that goes 0-5v as well.

Thank you for your help

// Adafruit feather m4 express: Set-up digital pin D12 and D13 to 25KHz variable duty cycle
// PWM frequency = (selected clock source / DIV) / (PRESCALER_DIV * (PERIOD + 1))
//                  ( GCLK_GENCTRL_SRC_DPLL0 / GCLK_GENCTRL_DIV(0) ) / (TC_CTRLA_PRESCALER_DIV2

#define PERIOD 2399

const byte numChars = 32;
char receivedChars[numChars]; // an array to store the received data
String inString = "";
float inputfloat = 0;
boolean newData = false;
byte channel = 0; //pin 12 == 0 13 == 1

void setup()
{
  // Set up the generic clock (GCLK7) to clock timer TCC1
  GCLK->GENCTRL[7].reg = GCLK_GENCTRL_DIV(0) |       // Divide the clock source
                         GCLK_GENCTRL_IDC |          // Set the duty cycle to 50/50 HIGH/LOW "square wave"
                         GCLK_GENCTRL_GENEN |        // Enable GCLK7
                         //GCLK_GENCTRL_SRC_DFLL;      // Select 48MHz DFLL clock source
                         //GCLK_GENCTRL_SRC_DPLL1;     // Select 100MHz DPLL clock source
                         GCLK_GENCTRL_SRC_DPLL0;     // Select 120MHz DPLL clock source
  while (GCLK->SYNCBUSY.bit.GENCTRL7);               // Wait for synchronization

  // http://ww1.microchip.com/downloads/en/DeviceDoc/60001507C.pdf
  // page 169
  GCLK->PCHCTRL[25].reg = GCLK_PCHCTRL_CHEN |        // Enable the TCC0/TCC1 perhipheral channel
                          GCLK_PCHCTRL_GEN_GCLK7;    // Connect generic clock 7 to TCC0 and TCC1

  // Enable the peripheral multiplexer on pin D12
  PORT->Group[g_APinDescription[12].ulPort].PINCFG[g_APinDescription[12].ulPin].bit.PMUXEN = 1;

  // Enable the peripheral multiplexer on pin D13
  PORT->Group[g_APinDescription[13].ulPort].PINCFG[g_APinDescription[13].ulPin].bit.PMUXEN = 1;

  // http://ww1.microchip.com/downloads/en/DeviceDoc/60001507C.pdf
  // page 35
  // Set the D12 (PORT_PA22 TCC1/WO[6]) peripheral multiplexer to peripheral (EVEN port number) F(0x5): TCC1, Channel 0
  PORT->Group[g_APinDescription[12].ulPort].PMUX[g_APinDescription[12].ulPin >> 1].reg |= PORT_PMUX_PMUXE(5);

  // Set the D13 (PORT_PA23 TCC1/WO[7]) peripheral multiplexer to peripheral (ODD port number) F(0x5): TCC1, Channel 1
  PORT->Group[g_APinDescription[13].ulPort].PMUX[g_APinDescription[13].ulPin >> 1].reg |= PORT_PMUX_PMUXO(5);

  TCC1->CTRLA.reg = TCC_CTRLA_PRESCALER_DIV2 |        // Set prescaler to 2, 120MHz/2 = 60MHz
                    TCC_CTRLA_PRESCSYNC_PRESC;        // Set the reset/reload to trigger on prescaler clock

  TCC1->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;             // Set-up TCC1 timer for Normal (single slope) PWM mode (NPWM)
  while (TCC1->SYNCBUSY.bit.WAVE)                    // Wait for synchronization

    //pwm resolution
    //log(PER.reg 2399) / log(2) = 2^11 steps (2048)
    TCC1->PER.reg = PERIOD;                          // Set-up the PER (period) register for 25KHz
  while (TCC1->SYNCBUSY.bit.PER);                    // Wait for synchronization

  TCC1->CC[2].reg = PERIOD;                          // D12??? all the way on
  while (TCC1->SYNCBUSY.bit.CC2);                    // Wait for synchronization

  TCC1->CC[3].reg = PERIOD;                          // D13??? all the way on
  while (TCC1->SYNCBUSY.bit.CC3);                    // Wait for synchronization

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


  Serial.begin(9600);
  while (!Serial);
  Serial.println("<Arduino is ready>");
}

void changeDutyCycle() {
  if (newData == true) {
    inString = receivedChars;
    float rawinputfloat = inString.toFloat();
    inString = "";
    inputfloat = constrain(rawinputfloat, 0.00, 100.00);
    float fperiod = inputfloat * (PERIOD / 100.00);
    int period = constrain(fperiod, 0, PERIOD);
    // http://ww1.microchip.com/downloads/en/DeviceDoc/60001507C.pdf
    //page 43-44 table 6-29
    TCC1->CCBUF[2].reg = period; //per can equal 0-2399 (+1) microseconds
    TCC1->CCBUF[3].reg = period; //does this need any input validation?
  }
}

void loop()
{
  recvWithEndMarker();
  changeDutyCycle();
  showNewData();
}

void recvWithEndMarker() {
  static byte ndx = 0;
  const char endMarker = '\n';
  char rc;
  // if (Serial.available() > 0) {
  while (Serial.available() > 0 && newData == false) {
    rc = Serial.read();
    if (rc != '\n') {
      receivedChars[ndx] = rc;
      ndx++;
      if (ndx >= numChars) {
        ndx = numChars - 1;
      }
    }
    else {
      receivedChars[ndx] = '\0'; // terminate the string
      ndx = 0;
      newData = true;
    }
  }
}

void showNewData() {
  if (newData == true) {
    Serial.print("This just in ... ");
    Serial.println(inputfloat);
    newData = false;
  }
}

Now I’ll work on getting the tach and go from there. I appreciate it.