Go Down

Topic: Converting ATMega to Zero - PWM Help (Read 260 times) previous topic - next topic

timschultz42

Hello!

I have built the Arduino Uno PedalShield from Electrosmash, which is a DIY electric guitar effects pedal.  Instead of using the standard Uno, I decided to try the Adafruit Metro M0 Express as the speed and memory were both nice additions. I am having difficulty converting their program over to leverage the Zero's registers, however, and I was hoping that someone could confirm my thoughts.


The PCB that I purchased uses OCR1AL and OCR1BL for writing the transformed signal, which translates to D9 & D10.
   
Based on MartinL's comments in another thread:
Quote
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
Looking for confirmation, but I believe that I can simply 'jumper' from D6 & D7 into the PCBs D9 & D10 pin, and then write the input values to REG_TCC_CCB2 and REG_TCC_CCB3.

My biggest point of confusion comes in how to properly set up the PWM, and also if there is a better alternative to using the ISR with the Zero.

Thanks!
Tim

Here is the basic example that they provide.
Code: [Select]

// CC-by-www.Electrosmash.com
// Based on OpenMusicLabs previous works.
// clean_pedalshield_uno.ino reads the ADC and plays it back into the PWM output
 
//defining hardware resources.
#define LED 13
#define FOOTSWITCH 12
#define TOGGLE 2
#define PUSHBUTTON_1 A5
#define PUSHBUTTON_2 A4

//defining the output PWM parameters
#define PWM_FREQ 0x00FF // pwm frequency - 31.3KHz
#define PWM_MODE 0 // Fast (1) or Phase Correct (0)
#define PWM_QTY 2 // 2 PWMs in parallel

//other variables
int input, vol_variable=512;
int counter=0;
byte ADC_low, ADC_high;
 
void setup() {
  //setup IO
  pinMode(FOOTSWITCH, INPUT_PULLUP);
  pinMode(PUSHBUTTON_1, INPUT_PULLUP);
  pinMode(PUSHBUTTON_2, INPUT_PULLUP);
  pinMode(LED, OUTPUT);
 
  // setup ADC
  ADMUX = 0x60; // left adjust, adc0, internal vcc
  ADCSRA = 0xe5; // turn on adc, ck/32, auto trigger
  ADCSRB = 0x07; // t1 capture for trigger
  DIDR0 = 0x01; // turn off digital inputs for adc0
 
  // setup PWM
  TCCR1A = (((PWM_QTY - 1) << 5) | 0x80 | (PWM_MODE << 1)); //
  TCCR1B = ((PWM_MODE << 3) | 0x11); // ck/1
  TIMSK1 = 0x20; // interrupt on capture interrupt
  ICR1H = (PWM_FREQ >> 8);
  ICR1L = (PWM_FREQ & 0xff);
  DDRB |= ((PWM_QTY << 1) | 0x02); // turn on outputs
  sei(); // turn on interrupts - not really necessary with arduino
  }
 
void loop()
{
  //Turn on the LED if the effect is ON.
  if (digitalRead(FOOTSWITCH)) digitalWrite(LED, HIGH);
    else  digitalWrite(LED, LOW);
 
  //nothing else here, all happens in the Timer 1 interruption.
}
 
ISR(TIMER1_CAPT_vect)
{
  // get ADC data
  ADC_low = ADCL; // you need to fetch the low byte first
  ADC_high = ADCH;
  //construct the input sumple summing the ADC low and high byte.
  input = ((ADC_high << 8) | ADC_low) + 0x8000; // make a signed 16b value
 
 
  //write the PWM signal
  OCR1AL = ((input + 0x8000) >> 8); // convert to unsigned, send out high byte
  OCR1BL = input; // send out low byte
 
}

MartinL

#1
Jul 09, 2018, 10:31 am Last Edit: Jul 09, 2018, 10:34 am by MartinL
Hi Tim,

The SAMD21 can be used to replace the Atmega328P in this instance, provided your external system can be modified to use only 0V to +3.3V analog inputs and can cope with +3.3V PWM outputs. Also, in part due to its flexibility, the SAMD21 code will be a good deal more complex.

To implement your project on a SAMD21 will require the input interrupts to be forwarded to the ADC and TCCx timer over the SAMD21's event system. The event system is a 12-channel highway used to allow peripheral communication that functions independently from the processor core.

It might be possible to replace the interrupt service routine with the SAMD21's Direct Memory Access Controller (DMAC) and after setup() have the sketch run without any processor intervention. The DMAC can also use the event system to trigger an ADC conversion and load the timer registers.

Regarding the PWM timer outputs, the SAMD21 does have timer outputs on D9 and D10, but unlike the Uno has them on different timer periherals, (TCC0 and TCC1). If you require the two PWM timer outputs to be synchronous with one another then it will be necessary, like you mention, to employ two pins that use the same timer.

Here's a link to some SAMD21 DMAC examples that includes the ADC and PWM: https://github.com/manitou48/ZERO.

MartinL

#2
Jul 10, 2018, 10:28 am Last Edit: Jul 10, 2018, 10:28 am by MartinL
Hi Tim,

Here's the SAMD21 code that generates a 31.25kHz, dual slope, PWM signal on outputs D6 and D7:

Code: [Select]
// Output 31.25kHz PWM on timer TCC0 (8-bit resolution)
void setup()
{
  GCLK->GENDIV.reg = 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

  GCLK->GENCTRL.reg = 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 2 PWM channels: timer TCC0 outputs CC2 and CC3 on D6 and D7
  PORT->Group[g_APinDescription[6].ulPort].PINCFG[g_APinDescription[6].ulPin].bit.PMUXEN = 1;
  PORT->Group[g_APinDescription[7].ulPort].PINCFG[g_APinDescription[7].ulPin].bit.PMUXEN = 1;
 
  // 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[6].ulPort].PMUX[g_APinDescription[6].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;
 
  // Feed GCLK4 to TCC0 and TCC1
  GCLK->CLKCTRL.reg = 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
  TCC0->WAVE.reg |= TCC_WAVE_POL(0x3) |           // 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 PERB register,
  // this determines the frequency of the PWM operation:
  TCC0->PER.reg = 0xFF;                           // Set the frequency of the PWM on TCC0 to 31.25kHz
  while (TCC0->SYNCBUSY.bit.PER);                 // Wait for synchronization
 
  // The CCBx register value corresponds to the pulsewidth in microseconds (us)
  TCC0->CC[2].reg = 0x7F;                         // TCC0 CCB2 - 50% duty-cycle on D6
  while (TCC0->SYNCBUSY.bit.CC2);                 // Wait for synchronization
  TCC0->CC[3].reg = 0x7F;                         // TCC0 CCB3 - 50% duty-cycle on D7
  while (TCC0->SYNCBUSY.bit.CC3);                 // Wait for synchronization
 
  // Divide the 31.25kHz signal by 1 giving 31.250kHz (32us) TCC0 timer tick and enable the timer
  TCC0->CTRLA.reg |= 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() { }

 Generic clock 4 (GCLK4) is set-up to generate a 16MHz and is used to clock the TCC0 timer, so that it mimics the 16MHz used by AVR timers.

To change the period or duty-cycle during operation (in the loop()), it's better to use the buffered registers:

Code: [Select]
TCC0->CCB[2].reg = 0x7F;                       // TCC0 CCB2 - 50% duty-cycle on D6
while (TCC0->SYNCBUSY.bit.CCB2);                 // Wait for synchronization

Code: [Select]
TCC0->PERB.reg = 0xFF;                         // Set the frequency of the PWM on TCC0 to 31.25kHz
while (TCC0->SYNCBUSY.bit.PERB);                 // Wait for synchronization

Go Up