Still struggling with PWM and looking for a sanity check

I've been putting in too many hours into this simple task and I need someone to steer me true again.

I'm using the SAMD21G based Seeeduino XIAO (Schematic) and having a problem similar to this person. I've been studying the datasheet specifically pages 33-36 about this whole multiplexing business.

I'm trying to use 2 pots fed by 3.3v as analog inputs to control the period and duty cycle of a PWM signal on one of the output pins.

I'm trying to use D2 as the output pin. I'm trying to use A1 and A3 as the analog inputs.

Pin "D2" is really Port A pin 10 (PA10) which is really pin 15 on the SAMD21G which has function E (TCC1/WO[0]) and function F (TCC0/WO[2]) and GCLK4. Riiiight?

I'm using a level shifter for the output to get the PWM to a 5V signal. The level shifter pins work to get a 16x4 screen working.

I'm sending "D2" (3rd pin from top on left hand side) to the shifter, then to an LED. The LED stays lit. I can adjust the two pots all I want and there is no difference to the light intensity. I can connect a VMM to the pot center pin and see the voltage sweep from a few mV all the way to 3.28V.

What is wrong? I see other people had this problem then they seem to get it. I want to get it. Help me get it. Failing for weeks on this has been painful.

Everything I want to do hinges on understanding this basic thing. I've whored around the 328, the 2560, this SAMD21G, and the SAM3X8 and have been trying to get some variant of this application working and just can't make it happen. I finally picked one, it's the SAMD21G.

Tons of respect for people that can master these buggars. There is work to do. For me it starts with 2 knobs and a tunable pulse. Then ill want to add 2 buttons and a way to cycle through the clock dividers.

Please help.

//sets the period of the PWM signal, PWM period = wPer / gen clock rate 
volatile unsigned char wPer = 255;
//This variable is to generate the duty cycle of the PWM signal 0.5 --> 50%
volatile float pWMDC = .5;
//selects the gen clock for setting the waveform generator clock or sample rate
const unsigned char gClock = 4;
//sets the divide factor for the gen clk, 48MHz / 3 = 16MHz
const unsigned char dFactor = 3;

// XIAO Links

// multiplexer pin table - https://files.seeedstudio.com/wiki/Seeeduino-XIAO/res/ATSAMD21G18A-MU-Datasheet.pdf pages 33-36
// Pinout - https://files.seeedstudio.com/wiki/Seeeduino-XIAO/img/Seeeduino-XIAO-pinout.jpg


//Pin "D2" is really Port A pin 10 (PA10) which is really pin 15 on the SAMD21G which has function E (TCC1/WO[0]) and function F (TCC0/WO[2]) and GCLK4

void setup() 
{ 
  SerialUSB.begin(115200);
  pinMode(2, OUTPUT);

  analogReadResolution(8); //set the ADC resolution to match the PWM max resolution (0 to 255)
  
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(dFactor) |          // Divide the main clock down by some factor to get generic clock
                    GCLK_GENDIV_ID(gClock);            // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |           
                     GCLK_GENCTRL_GENEN |         // Enable GCLK4
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                     GCLK_GENCTRL_ID(gClock);          // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Enable the port multiplexer for the digital pin 2.
  PORT->Group[g_APinDescription[2].ulPort].PINCFG[g_APinDescription[2].ulPin].bit.PMUXEN = 1;
  
   //Connect the TCC1 timer to digital output - port pins are paired odd PMUO and even PMUXE (note D7 is commented out and D3 is not) 
  PORT->Group[g_APinDescription[2].ulPort].PMUX[g_APinDescription[2].ulPin >> 1].reg = PORT_PMUX_PMUXO_E | PORT_PMUX_PMUXE_E;

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

  //Set for Single slope PWM operation: timers or counters count up to TOP value and then repeat
  REG_TCC1_WAVE |= TCC_WAVE_WAVEGEN_NPWM;       // Reverse the output polarity on all TCC1 outputs
                   //TCC_WAVE_POL(0xF)      //this line inverts the output waveform
                   //TCC_WAVE_WAVEGEN_DSBOTH;    // Setup dual slope PWM on TCC1
  while (TCC1->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_TCC1_PER = wPer;         // This sets the rate or frequency of PWM signal. 
  while (TCC1->SYNCBUSY.bit.PER);                // Wait for synchronization
  
  // Set the PWM signal to output 50% duty cycle initially (0.5 x 255)
  REG_TCC1_CC0 = pWMDC*wPer;        
  while (TCC1->SYNCBUSY.bit.CC0);                // Wait for synchronization

  //enable interrupts
  REG_TCC1_INTENSET = TCC_INTENSET_OVF; //Set up interrupt at TOP of each PWM cycle
  enable_interrupts(); //enable in NVIC
  
  // Set prescaler and enable the outputs
  REG_TCC1_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 |    // Divide GCLK4 by 1
                    TCC_CTRLA_ENABLE;             // Enable the TCC1 output
  while (TCC1->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
}

void loop() { 

  SerialUSB.println(REG_TCC1_PER);
  SerialUSB.println(REG_TCC1_CC0);
 }

//This function sets the interrupts priority to highest and then enables the PWM interrupt
void enable_interrupts() {
  NVIC_SetPriority(TCC1_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority
  NVIC_EnableIRQ(TCC1_IRQn);
}

//This ISR is called at the end or TOP of each PWM cycle
void TCC1_Handler() {
    REG_TCC1_PER = analogRead(1); //Get period from A1
    while (TCC1->SYNCBUSY.bit.PER);
    REG_TCC1_CC0 = (analogRead(3)/255.0)*analogRead(1); //calculate PWM using A0 reading and A1 current state
    while (TCC1->SYNCBUSY.bit.CC0);
    REG_TCC1_INTFLAG = TC_INTFLAG_OVF; //Need to reset interrupt
}

xiao pin ports.jpg

xiao pin ports.jpg

criticalpoint:
I'm using a level shifter for the output to get the PWM to a 5V signal. The level shifter pins work to get a 16x4 screen working.

Which IC are you using as the level shifter?

(Trick question - unless it is an IC, it is probably not working properly. :grinning: )

I don't know this hardware, but I'd say this:

Break the problem down into smaller stages.
Stage 1 is to get just some PWM signal on Pin2 ( any frequency / any duty cycle )
Stage 2 is to start altering parameters to get different frequencies / duty cycles on that pin.
State 3 is to take the values the pots deliver, and convert these to the ranges necessary to drive the timer.

You may find that there is a conflict between the Arduino environment and your direct use of timer registers.
Have you an oscilloscope so you can see what is happening on the pin ?

On an Arduino Uno (ATmega328P) , what you are doing would be a simple modification of this sketch from Gammon Forum : Electronics : Microprocessors : Timers and counters :

// Example of modulating a 38 kHz frequency duty cycle by reading a potentiometer
// Author: Nick Gammon
// Date: 24 September 2012

const byte POTENTIOMETER = A0;
const byte LED = 10;  // Timer 1 "B" output: OC1B

// Clock frequency divided by 38 kHz frequency desired
const long timer1_OCR1A_Setting = F_CPU / 38000L;

void setup() 
 {
  pinMode (LED, OUTPUT);

  // set up Timer 1 - gives us 38.005 kHz 
  // Fast PWM top at OCR1A
  TCCR1A = bit (WGM10) | bit (WGM11) | bit (COM1B1); // fast PWM, clear OC1B on compare
  TCCR1B = bit (WGM12) | bit (WGM13) | bit (CS10);   // fast PWM, no prescaler
  OCR1A =  timer1_OCR1A_Setting - 1;                 // zero relative  
  }  // end of setup

void loop()
  {
  // alter Timer 1 duty cycle in accordance with pot reading
  OCR1B = (((long) (analogRead (POTENTIOMETER) + 1) * timer1_OCR1A_Setting) / 1024L) - 1;
  
  // do other stuff here
  }

Thank you for your replies. I could just be getting my lines crossed here because like I said I’ve been whoring around the AT328, ATM2560, this SAMD21G, and the SAM3X8 so I’ve tried to do this on nano, nano, mega mega, knockoff Zero, Seeeduino XIAO, and Due. I listed some twice because I toasted them. Honestly I’m pretty reckless and rough on tech. Thankfully these are cheap and the voltages aren’t dangerous.

Maybe when I explain my real requirement you’ll see why I went this way.

-I need to precisely fine tune a PWM pulse width with resolution of about 50nS or less, but be able to create pulse trains from around 50nS period with a duty cycle below 1% (.5% actually) up to 10kHz (perhaps eventually 100kHz) up to DC with everything in between.

I’m trying to use 2 pots for now, It needs to have 1 rotary knob that toggles between pulse width adjust and duty cycle adjust so the user can see what their doing on a small lcd screen and have faith that that is the signal coming out. My circuitous path took me to D21 for price and PWM port programabillity.

My other attempts got all out of wack on the 16mhz setups… maybe there is a way… but I really liked the way this guy explained it, and demonstrated it.

I’m not programming registers the same way like on the AT328 so this does seem a bit different. The way the pin multiplexing is done is different on different boards even with the same MCU, not to mention entirely different architectures and manufacturers (Atmel vs Arm).

There are limited pins on the XIAO and there are implied limitations that I think I’m not doing a good job explaining, or maybe this belongs in a D21 thread or on the seeeduino site. I don’t know what I don’t know and I’m still new about where to ask what.

Thanks for reading.

Which IC are you using as the level shifter?

(Trick question - unless it is an IC, it is not going to work at all! :grinning: )

It's a bidirectional IC and it has been tested but that point isn't a factor for anything but the final PWM out im sending to the 3.3v side.