Changing Arduino Zero PWM Frequency

I have an application where I need to output a PWM frequency >250kHz. Using Arduino's analogWrite function only gets me to 187kHz max. I found the Due has this function, PWMC_ConfigureClocks(), to change the frequency. Is there something similar for the Zero?

I don't think ZERO has an API to set PWM frequency. (The API for Teensy ARM products has an analogWriteFrequency(pin,herz), cold comfort :frowning: )

If you want to roll your own, there is sample code in the thread
ZERO IRrremote
that does pwm at user-selected frequency.

Also in the core packages/arduino/hardware/samd/1.6.1/cores/arduino/Tone.cpp
there is code to set the frequency of TC5, selecting best prescaler and compare value.

Hi joelcrouch,

Welcome to the Arduino Zero forum.

Both the AVR and the ARM processors are capable of controlling both the phase and frequency using dual slope PWM. If you're interested in reading further, it's detailed in the SAMD21 datasheet, page 660, under the heading “Dual Slope PWM Generation”. In dual slope PWM the timer counts up to a given value then counts back down to zero, and so on...

Dual slope PWM on the SAMD21 is provided by the timers TCC0, TCC1 and TCC2. These timers are clocked by one of the processor's generic clocks (GCLK). There are 8 GCLKs in total, 0 to 3 are used by Arduino, but you're free to use the others. It's possible to set up a given GCLK to feed a 48MHz (CPU clock frequency) signal to one of the timers. However, due to the high speed of your 250kHz PWM signal in relation to the 48MHz clock frequency, you'll end up with poor resolution. Here's the calculations:

The frequency for dual slope PWM is determined by:

Frequency = GCLK frequency / (2 * N * PER) where N = prescaler value (CTRLA register)

The value in the PER register determines the maximum value the timer counts up to.

In your case N = 1 (as we'd like to clock the timer as fast as possible), therefore...

Frequency = 48MHz / (2 * 1 * 96) = 250kHz

So the PER should be set at 96 (decimal).

The resolution for dual slope PWM is given by:

Resolution = log(PER + 1)/log(2), therefore:

Resolution at 250kHz = log(96 + 1) / log(2) = 6.6 = 6 bits (rounding down)

To change the PWM pulse width (phase), just load the timer's CCBx register with a value between 0 and 96. 0 outputs 0V (0% duty cycle), 96 outputs 3.3V (100% duty cycle). Loading the CCBx register with 48 gives a 50% duty cycle.

The following code sets up GCLK 4 to feed timer TCC0 with 48MHz. The TCC0 is set up for dual slope PWM operation and connected to ouput D7. To control the PWM output just load the REG_TCC0_CCB3 register with a value between 0 and 96. Using my old multimeter I've sucessfully tested the code for 125kHz, however it's not capable of measuring 250kHz and I haven't got a scope, so I'm unable to test it at this higher frequency.

// Output 250kHz PWM on timer TCC0 (6-bit resolution)
void setup() 
{ 
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz/1=48MHz
                    GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                     GCLK_GENCTRL_GENEN |         // Enable GCLK4
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                     GCLK_GENCTRL_ID(4);          // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Enable the port multiplexer for the digital pin D7 
  PORT->Group[g_APinDescription[7].ulPort].PINCFG[g_APinDescription[7].ulPin].bit.PMUXEN = 1;
  
  // Connect the TCC0 timer to digital output D7 - port pins are paired odd PMUO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC2
  PORT->Group[g_APinDescription[6].ulPort].PMUX[g_APinDescription[6].ulPin >> 1].reg = PORT_PMUX_PMUXO_F;

  // Feed GCLK4 to TCC0 and TCC1
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                     GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                     GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Dual slope PWM operation: timers countinuously count up to PER register value then down 0
  REG_TCC0_WAVE |= TCC_WAVE_POL(0xF) |         // Reverse the output polarity on all TCC0 outputs
                    TCC_WAVE_WAVEGEN_DSBOTH;    // Setup dual slope PWM on TCC0
  while (TCC0->SYNCBUSY.bit.WAVE);               // Wait for synchronization

  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation: 
  REG_TCC0_PER = 96;         // Set the frequency of the PWM on TCC0 to 250kHz
  while (TCC0->SYNCBUSY.bit.PER);                // Wait for synchronization
  
  // Set the PWM signal to output 50% duty cycle
  REG_TCC0_CC3 = 48;         // TCC0 CC3 - on D7
  while (TCC0->SYNCBUSY.bit.CC3);                // Wait for synchronization
  
  // Divide the 48MHz signal by 1 giving 48MHz (20.83ns) TCC0 timer tick and enable the outputs
  REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 |    // Divide GCLK4 by 1
                    TCC_CTRLA_ENABLE;             // Enable the TCC0 output
  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
}

void loop() { }

Hi MartinL,
Your code snippet works! I measure 250kHz on oscilloscope.

Thank you,
Joel

MartinL,
Is there an easy way to assign PWM function to another pin other than 7? I tried changing number in your two PORT->Group.... statements above, but it didn't work.

Thanks,
Joel

MartinL,
Running your code breaks analogWrite() function.

Yes, it'll break analogWrite(), because it's also trying to use timer TCC0.

The pin mapping for the TCC0, TCC1 and TCC2 are as follows:

REG_TCC0_CCB0 – digital output D2 (Zero Pro/M0 Pro/M0 – digital pin D4)
REG_TCC0_CCB1 – digital output D5
REG_TCC0_CCB2 – digital output D6
REG_TCC0_CCB3 – digital output D7
REG_TCC1_CCB0 – digital output D4 (Zero Pro/M0 Pro/M0 – digital pin D2)
REG_TCC1_CCB1 – digital output D3
REG_TCC2_CCB0 – digital output D11
REG_TCC2_CCB1 – digital output D13

Note: On Arduino.org's Zero Pro/M0 Pro/M0, D2 and D4 are reversed.

Note that first you'll also have to enable TCC1 and TCC2 in a similar manner to TCC0. You can connect TCC2 to GCLK4 using the lines:

// Feed GCLK4 to TCC2 (and TC3)
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC2 (and TC3)
                     GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                     GCLK_CLKCTRL_ID_TCC2_TC3;    // Feed GCLK4 to TCC2 (and TC3)
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

To connect the timers to their output pins, first enable the port multiplexer on each timer pin:

// Enable the port multiplexer for the 8 PWM channels: timer TCC0, TCC1 and TCC2 outputs
  const uint8_t CHANNELS = 8;
  const uint8_t pwmPins[] = { 2, 3, 4, 5, 6, 7, 11, 13 };
  for (uint8_t i = 0; i < CHANNELS; i++)
  {
     PORT->Group[g_APinDescription[pwmPins[i]].ulPort].PINCFG[g_APinDescription[pwmPins[i]].ulPin].bit.PMUXEN = 1;
  }

Then connect all the timer outputs (TCC0, TCC1 & TCC2) to their respective output pins:

// Connect the TCC timers to the port outputs - port pins are paired odd PMUO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC2
  PORT->Group[g_APinDescription[2].ulPort].PMUX[g_APinDescription[2].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F; 
  PORT->Group[g_APinDescription[4].ulPort].PMUX[g_APinDescription[4].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;
  PORT->Group[g_APinDescription[6].ulPort].PMUX[g_APinDescription[6].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;
  PORT->Group[g_APinDescription[11].ulPort].PMUX[g_APinDescription[11].ulPin >> 1].reg = PORT_PMUX_PMUXO_E | PORT_PMUX_PMUXE_E;

The PMUX registers are arranged in pin pairs odd and even. So for example Arduino's digital pin 13 (D13) is actually the SAMD21 port A, pin 17, or PA17. An odd pin. (See Arduino Zero's schematic diagram). This is paired with its neighboring even pin PA16, (actually digital pin 11). To connect the TCC2 timer to the D13 we have to specify the even SAMD21 pin, in this case PA16 (D11), then connect the timer to D13 using the odd PORT_PMUX_PMUXO_E mask. PMUXO stands for: port multiplexer odd. The E and F suffixes specify that the timers are to be connected.

By the way, the >>1 shift left is actually divide by 2, because there is one PMUX register for each odd and even pair of pins, there's only 16 PMUX registers for 32 pins on a given port.

The g_APinDesciription() function simply converts the Arduino pin numbers into the SAMD21's port and pin representation.

@MartinL, is it really necessary to configure GCLK4? Coudln't one just use GCLK0 which is configured as 48MHz via startup.c?

inquiring minds

Hi mantoui

Thanks for pointing this out.

Yes, in this instance you could make the code more efficient by using GCLK0, but if you wanted divide down the GCLK frequency further to obtain a lower frequency using the REG_GCLK_GENDIV register, then I believe you'd have to use one of the free GCLKs. For example, in my project I divide GCLK4's 48MHz by 3 to obtain 16MHz, so that the PWM timing works out the same as the AVR processors on my Arduino Micro and Mega.

Hi!

I'm doing some research into the feasibility of programming a quadcopter flight controller based on an Arduino Zero, among other things, all the PWM business to control the ESCs.

If I understood the code above correctly, I can program TCC0 to a specific frequency, tie it to D4-D7 to control four ESCs, and still control the duty cycle of each pin via CCB0-CCB3?

On top of that, given clock division combined with this PER and CCB stuff, I should be able to get very fine duty cycle control at lower PWM frequencies? I'm interested in getting four PWMs going at rates of either 490hz or arbitrary rates below that.

Yes, the PER/PERB and CCBx registers allow fine control over the PWM duty cycle and frequency. With TCC0, TCC1 and TCC2 it's possible to control up to 8 motors.

If you're using Electronic Speed Controllers (ESCs), setting the generic clock divisor to 3 to get 16MHz and then using the control A register prescaler of 8 is nice, because the duty cycle can be controlled by loading the CCBx registers with a number between 0-2000. This corresponds to a 0-2000us duty cycle required by most ESCs.

So for 490Hz, the PER register value is:

PER = 16MHz / (2 * 8 * 490) = 2040

where 48MHz / 3 (GCLK divisor) = 16MHz and 8 is the control A prescaler

For some reason initialising the PWM using the buffered PERB/WAVEB registers causes a delay of about 5 seconds before the output becomes active. To overcome this I initialise the PWM using the unbuffered PER/WAVE registers, but use the buffered CCBx registers to control the duty cycle. This way the PWM becomes active immediately. Using the buffered CCBx registers are necessary to prevent changes in the duty cycle from causing glitches on your motors.

Cool, thanks for your help. Even tho it's less intuitive to set up than the analogWrite stuff from the Arduino library, the higher precision should be worth it. And should incur less latency from what I read.

Do I need all that code just to change PWM frequency on the arduino zero?
on the Atmega it was one line:

TCCR1B = TCCR1B & 0b11111000 | 0x01;

did anyone find a simpler way? it is really hard to adjust that complex code to my needs....

Hi shiram,

On the Atmega devices the TCCRxx registers are used to set up PWM, but for complete control of the PWM phase (duty cycle) and frequency you still need to modify their ICRx and OCRxx registers.

The SAMD microcontroller is internally more flexible than the older AVR ICs, it requires that you first connect a generic clock (GCLK) to the timer peripherals. After that the activation of the port pins and the switching of the port multiplexer is analogous to setting up the TCCRxx registers on Atmegas.

it is really hard to adjust that complex code to my needs....

I might be able to help. What are the PWM requirements for your project?

Martini ... you appear to be an expert on the Zero and PWM so maybe there is a simpler method to accomplish what I am trying to do. I had posted the comments below on another section of the forum and was redirected here, and had tried exactly the same thing (to change the PWM frrequency on the Zero (pin 9) that shiram has listed above based on something I found on another post:

Pins 9 and 10: controlled by timer 1 in phase-correct PWM mode (cycle length = 510)

Setting Divisor Frequency
0x01 1 31372.55
0x02 8 3921.16
0x03 64 490.20 <--DEFAULT
0x04 256 122.55
0x05 1024 30.64

TCCR1B = (TCCR1B & 0b11111000) | ;

So I understood (wrongly, apparently) that changing TCCR1B as above would do the trick. But this line of code doesn't compile as the compiler can't find a declaration of TCCR1B. All I am trying to do is change the PWM frequency from the default of near 190 KHz (187 KHz is a number I've seen posted elsewhere) down to 490 Hz or 123 Hz (or similar ... somewhere in the low 100s).

Is there a simple way to do this, or do I need to follow a much more extensive process (which I think you have outlined above but I am not too clear on all of those details). I did find a library (PWM.h) what was supposed to do this for the Zero, but that also doesn't compile or recognize any of the library functions. I would have thought changing a PWM frequency would be relatively easy for an Arduino, but maybe the Zero is a different animal. 187 KHz is just too high for my application. Thanks for any help.

Hi rmay_pci,

Pin 9 and 10 control the output of both phase and phase and frequency correct PWM on the Arduino Uno. At the risk of going off topic the following code for the Uno sets up two servos at 50Hz. The ICR1 register controls the frequency, for example: 20000 = 50Hz, 10000 = 100Hz, 2500 = 400Hz. The OCR1A and OCR1B registers control the duty cycle. If you're using servos just load them with a value between 1000 and 2000, with 1500 being the center point of the servo. Or, if you need a 50% duty cycle with a value that is half ICR1. However, using this method prevents you from using libraries that also use timer1, for instance the servo library.

void setup() {
  // Initialise timer 1 for phase and frequency correct PWM
  pinMode(9, OUTPUT);                         // Set digital pin 9 (D9) to an output
  pinMode(10, OUTPUT);                        // Set digital pin 10 (D10) to an output
  TCCR1A = _BV(COM1A1) | _BV(COM1B1);         // Enable the PWM outputs OC1A, and OC1B on digital pins 9, 10
  TCCR1B = _BV(WGM13) | _BV(CS11);            // Set phase and frequency correct PWM and prescaler of 8 on timer 1
  ICR1 = 20000;                               // Set the PWM frequency to 50Hz
  OCR1A = 1500;                               // Centre the servo on D9
  OCR1B = 1500;                               // Centre the servo on D10
}

void loop() {
  OCR1A = 1000;                               // Move the servo to min position on D9 
  OCR1B = 1000;                               // Move the servo to min position on D10
  delay(1000);                                // Wait for 1 second
  OCR1A = 2000;                               // Move the servo to max position on D9
  OCR1B = 2000;                               // Move the servo to max position on D10
  delay(1000);                                // Wait for 1 second
}

Back to the Zero. It's internally more flexible than the older AVR processors, so it does require more extensive set-up, but much of it's just routing the processor's generic clock to the appropriate timer and hooking up the correct pins for output. Once that's out of the way, it's pretty much the same as the AVR.

Here's similar code for the Zero, but sets up 4 servos at 50Hz on digital pins D2, D5, D6 and D7 using the TCC0 timer. The REG_TCC0_PER register is equivalent of ICR1 and the REG_TCC0_CCBx the OCR1x registers:

// Output 50Hz PWM on timer TCC0 (14-bit resolution)
void setup() 
{ 
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(3) |          // Divide the 48MHz clock source by divisor 3: 48MHz/3=16MHz
                    GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                     GCLK_GENCTRL_GENEN |         // Enable GCLK4
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                     GCLK_GENCTRL_ID(4);          // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Enable the port multiplexer for the 4 PWM channels: timer TCC0 outputs
  const uint8_t CHANNELS = 4;
  const uint8_t pwmPins[] = { 2, 5, 6, 7 };
  for (uint8_t i = 0; i < CHANNELS; i++)
  {
     PORT->Group[g_APinDescription[pwmPins[i]].ulPort].PINCFG[g_APinDescription[pwmPins[i]].ulPin].bit.PMUXEN = 1;
  }
  // Connect the TCC0 timer to the port outputs - port pins are paired odd PMUO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC2
  PORT->Group[g_APinDescription[2].ulPort].PMUX[g_APinDescription[2].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;
  PORT->Group[g_APinDescription[6].ulPort].PMUX[g_APinDescription[6].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;

  // Feed GCLK4 to TCC0 and TCC1
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                     GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                     GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Dual slope PWM operation: timers countinuously count up to PER register value then down 0
  REG_TCC0_WAVE |= TCC_WAVE_POL(0xF) |         // Reverse the output polarity on all TCC0 outputs
                    TCC_WAVE_WAVEGEN_DSBOTTOM;    // Setup dual slope PWM on TCC0
  while (TCC0->SYNCBUSY.bit.WAVE);               // Wait for synchronization

  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation:
  // 20000 = 50Hz, 10000 = 100Hz, 2500  = 400Hz
  REG_TCC0_PER = 20000;      // Set the frequency of the PWM on TCC0 to 50Hz
  while(TCC0->SYNCBUSY.bit.PER);

  // The CCBx register value corresponds to the pulsewidth in microseconds (us)
  REG_TCC0_CCB0 = 1500;       // TCC0 CCB0 - center the servo on D2
  while(TCC0->SYNCBUSY.bit.CCB0);
  REG_TCC0_CCB1 = 1500;       // TCC0 CCB1 - center the servo on D5
  while(TCC0->SYNCBUSY.bit.CCB1);
  REG_TCC0_CCB2 = 1500;       // TCC0 CCB2 - center the servo on D6
  while(TCC0->SYNCBUSY.bit.CCB2);
  REG_TCC0_CCB3 = 1500;       // TCC0 CCB3 - center the servo on D7
  while(TCC0->SYNCBUSY.bit.CCB3);

  // Divide the 16MHz signal by 8 giving 2MHz (0.5us) TCC0 timer tick and enable the outputs
  REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV8 |    // Divide GCLK4 by 8
                    TCC_CTRLA_ENABLE;             // Enable the TCC0 output
  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
}

void loop() { }

Martini,
Thanks very much for the detailed response. I will try this code with the Zero over the weekend to see if I can get it to work.

What we are doing is running a PID heater control algorithm using the duty cycle of the PWM output (which drives a MOSFET, which drives the heater current). The heater power is directly proportional to the PWM duty cycle, but the system has a horribly slow response (it is more like an oven than a contact heater).

Tuning using a normal PID approach has not worked, so we are trying to get fancier with the algorithm and stumbled with the PWM frequency on the Zero being too fast. I tried a Due which seems to have a default PWM frequency of 1 KHz and everything works very well, but we prefer the Zero for size reasons (and because we have already made a custom "shield" with the same footprint). So if I can get the Zero's PWM frequency down I think we will be in business.

Thanks again for the detailed explanation ... people like you who understand all these details are a very valuable resource for those of us who don't but need to get projects working.
Randy

OK, any advice on how to change the frequency on only pin 13? I've got an application where I need its output to be in the audio band, so somewhere between 100Hz and 10kHz would work nicely. I think I can follow the steps from the example above, but how do I change the PWM ratio? Does analogWrite() still work?

Hi pharaohamps,

The following code outputs a 100Hz PWM signal from timer TCC0, on digital pin 13:

// Output 100Hz PWM on timer TCC0 digital pin D13
void setup() 
{ 
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(3) |          // Divide the 48MHz clock source by divisor 3: 48MHz/3=16MHz
                    GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                     GCLK_GENCTRL_GENEN |         // Enable GCLK4
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                     GCLK_GENCTRL_ID(4);          // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Enable the port multiplexer for digital pin 13 (D13): timer TCC0 output
  PORT->Group[g_APinDescription[13].ulPort].PINCFG[g_APinDescription[13].ulPin].bit.PMUXEN = 1;
  
  // Connect the TCC0 timer to the port output - port pins are paired odd PMUO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC2
  PORT->Group[g_APinDescription[11].ulPort].PMUX[g_APinDescription[11].ulPin >> 1].reg = PORT_PMUX_PMUXO_F;
  
  // Feed GCLK4 to TCC0 and TCC1
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                     GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                     GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Dual slope PWM operation: timers countinuously count up to PER register value then down 0
  REG_TCC0_WAVE |= TCC_WAVE_POL(0xF) |         // Reverse the output polarity on all TCC0 outputs
                    TCC_WAVE_WAVEGEN_DSBOTTOM;    // Setup dual slope PWM on TCC0
  while (TCC0->SYNCBUSY.bit.WAVE);               // Wait for synchronization

  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation:
  // 20000 = 50Hz, 10000 = 100Hz, 2500  = 400Hz
  REG_TCC0_PER = 10000;      // Set the frequency of the PWM on TCC0 to 100Hz
  while(TCC0->SYNCBUSY.bit.PER);

  // The CCBx register value corresponds to the pulsewidth in microseconds (us)
  REG_TCC0_CCB3 = 1500;       // TCC0 CCB3 - center the servo on D13
  while(TCC0->SYNCBUSY.bit.CCB3);

  // Divide the 16MHz signal by 8 giving 2MHz (0.5us) TCC0 timer tick and enable the outputs
  REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV8 |    // Divide GCLK4 by 8
                    TCC_CTRLA_ENABLE;             // Enable the TCC0 output
  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
}

void loop() { }

You can change the PWM ratio or duty-cycle by loading the REG_TCC0_CCB3 register with a value between 0 and whatever value is in the REG_TCC0_PER register. For example, at 100Hz the REG_TCC0_PER register contains 10000, therefore loading the REG_TCC0_CCB3 with 5000 will give you a 50% duty-cycle. The REG_TCC0_CCB3 register can be changed with the following code:

// The CCBx register value corresponds to the pulsewidth in microseconds (us)
  REG_TCC0_CCB3 = 5000;       // TCC0 CCB3 - 50% duty cycle on D13
  while(TCC0->SYNCBUSY.bit.CCB3);

Manipulating TCCx timers at the register level will most likely conflict with analogWrite(). It's really a trade-off, analogWrite() provides both useability and portability, while register manipulation ultimately gives you more flexibility.

The example above gives a 13-bit resolution at 100Hz, (that was good enough for my project), however as the SAMD21's TCC0 and TCC1 are 24-bit timers, it's possible to obtain an even higher resolution, by altering the generic clock divisor and the timer's prescaler.

Thanks for the help! Am I correct in thinking that analogWrite() will still work on pins that I haven't changed manually? I'm fine with the standard 8-bit resolution from analogWrite() even on pin 13, so writing a function to change my mapping from 0-255 to 0-1000 is trivial.