Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DDS Support for Arduino Zero (and its built-in DAC) #15

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 185 additions & 71 deletions DDS.cpp
Original file line number Diff line number Diff line change
@@ -1,57 +1,120 @@
#include <Arduino.h>
#include "DDS.h"

#ifdef __SAMD21G18A__

// The SimpleAudioPlayerZero sample project found at:
// https://www.arduino.cc/en/Tutorial/SimpleAudioPlayerZero
// is an execellent reference for setting up the Timer/Counter
#define TC_ISBUSY() (TC5->COUNT16.STATUS.reg & TC_STATUS_SYNCBUSY)
#define TC_WAIT() while (TC_ISBUSY());
#define TC_ENABLE() TC5->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE; TC_WAIT();
#define TC_RESET() TC5->COUNT16.CTRLA.reg = TC_CTRLA_SWRST; TC_WAIT(); \
while (TC5->COUNT16.CTRLA.bit.SWRST);
#define TC_DISABLE() TC5->COUNT16.CTRLA.reg &= ~TC_CTRLA_ENABLE; TC_WAIT();

#endif

// To start the DDS, we use Timer1, set to the reference clock
// We use Timer2 for the PWM output, running as fast as feasible
void DDS::start() {

#ifdef DDS_DEBUG_SERIAL
// Print these debug statements (commont to both the AVR and the SAMD21)
Serial.print(F("DDS SysClk: "));
Serial.println(F_CPU/8);
Serial.print(F("DDS RefClk: "));
Serial.println(refclk, DEC);
#endif

#ifdef __SAMD21G18A__
// SAMD21, like Ardino Zero

// Enable the Generic Clock Generator 0 and configure for TC4 and TC5.
// We only need TC5, but they are configured together
GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID(GCM_TC4_TC5)) ;
while (GCLK->STATUS.bit.SYNCBUSY);

TC_RESET();

// Set TC5 16 bit
TC5->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16;

// Set TC5 mode as match frequency
TC5->COUNT16.CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ;
TC5->COUNT16.CTRLA.reg |= TC_CTRLA_PRESCALER_DIV1 | TC_CTRLA_ENABLE;
TC5->COUNT16.CC[0].reg = (uint16_t) (SystemCoreClock / DDS_REFCLK_DEFAULT - 1);
TC_WAIT()

// Configure interrupt
NVIC_DisableIRQ(TC5_IRQn);
NVIC_ClearPendingIRQ(TC5_IRQn);
NVIC_SetPriority(TC5_IRQn, 0);
NVIC_EnableIRQ(TC5_IRQn);

// Enable TC5 Interrupt
TC5->COUNT16.INTENSET.bit.MC0 = 1;
TC_WAIT();

//Configure the DAC
#if COMPARATOR_BITS==6
// Not sure why you'd use 6-bit, other than debugging.
// Reduced amplitude: clockTick() and getDutyCycle() do not scale the 6 up to 8-bits
analogWriteResolution(8);
#elif COMPARATOR_BITS == 8 || COMPARATOR_BITS == 10
analogWriteResolution(COMPARATOR_BITS);
#else
#error Unsupported resoltuion for DAC (expected 8 or 10)
#endif
analogWrite(A0, 0);

#else
// For AVRs

// Use the clkIO clock rate
ASSR &= ~(_BV(EXCLK) | _BV(AS2));

// First, the timer for the PWM output
// Setup the timer to use OC2B (pin 3) in fast PWM mode with a configurable top
// Run it without the prescaler
#ifdef DDS_PWM_PIN_3
TCCR2A = (TCCR2A | _BV(COM2B1)) & ~(_BV(COM2B0) | _BV(COM2A1) | _BV(COM2A0)) |
#ifdef DDS_PWM_PIN_3
TCCR2A = (TCCR2A | _BV(COM2B1)) & ~(_BV(COM2B0) | _BV(COM2A1) | _BV(COM2A0)) |
_BV(WGM21) | _BV(WGM20);
TCCR2B = (TCCR2B & ~(_BV(CS22) | _BV(CS21))) | _BV(CS20) | _BV(WGM22);
#else
TCCR2B = (TCCR2B & ~(_BV(CS22) | _BV(CS21))) | _BV(CS20) | _BV(WGM22);
#else
// Alternatively, use pin 11
// Enable output compare on OC2A, toggle mode
TCCR2A = _BV(COM2A1) | _BV(WGM21) | _BV(WGM20);
//TCCR2A = (TCCR2A | _BV(COM2A1)) & ~(_BV(COM2A0) | _BV(COM2B1) | _BV(COM2B0)) |
// _BV(WGM21) | _BV(WGM20);
TCCR2B = _BV(CS20);
#endif
TCCR2A = _BV(COM2A1) | _BV(WGM21) | _BV(WGM20);
//TCCR2A = (TCCR2A | _BV(COM2A1)) & ~(_BV(COM2A0) | _BV(COM2B1) | _BV(COM2B0)) |
// _BV(WGM21) | _BV(WGM20);
TCCR2B = _BV(CS20);
#endif

// Set the top limit, which will be our duty cycle accuracy.
// Setting Comparator Bits smaller will allow for higher frequency PWM,
// with the loss of resolution.
#ifdef DDS_PWM_PIN_3
OCR2A = pow(2,COMPARATOR_BITS)-1;
OCR2B = 0;
#else
OCR2A = 0;
#endif
#ifdef DDS_PWM_PIN_3
OCR2A = DDS_MAX_COMPARATOR-1;
OCR2B = 0;
#else
OCR2A = 0;
#endif

#ifdef DDS_USE_ONLY_TIMER2
TIMSK2 |= _BV(TOIE2);
#endif
#ifdef DDS_USE_ONLY_TIMER2
TIMSK2 |= _BV(TOIE2);
#endif

// Second, setup Timer1 to trigger the ADC interrupt
// This lets us use decoding functions that run at the same reference
// clock as the DDS.
// We use ICR1 as TOP and prescale by 8
TCCR1B = _BV(CS10) | _BV(WGM13) | _BV(WGM12);
TCCR1A = 0;
ICR1 = ((F_CPU / 1) / refclk) - 1;
#ifdef DDS_DEBUG_SERIAL
Serial.print(F("DDS SysClk: "));
Serial.println(F_CPU/8);
Serial.print(F("DDS RefClk: "));
Serial.println(refclk, DEC);
Serial.print(F("DDS ICR1: "));
Serial.println(ICR1, DEC);
#endif
ICR1 = ((F_CPU / 1) / refclk) - 1;
#ifdef DDS_DEBUG_SERIAL
Serial.print(F("DDS ICR1: "));
Serial.println(ICR1, DEC);
#endif

// Configure the ADC here to automatically run and be triggered off Timer1
ADMUX = _BV(REFS0) | _BV(ADLAR) | 0; // Channel 0, shift result left (ADCH used)
Expand All @@ -60,46 +123,54 @@ void DDS::start() {
DIDR0 |= _BV(0);
ADCSRB = _BV(ADTS2) | _BV(ADTS1) | _BV(ADTS0);
ADCSRA = _BV(ADEN) | _BV(ADSC) | _BV(ADATE) | _BV(ADIE) | _BV(ADPS2); // | _BV(ADPS0);

#endif
}

void DDS::stop() {
#ifdef __SAMD21G18A__
TC_DISABLE();
TC_RESET();
analogWrite(A0, 0);
#else
// TODO: Stop the timers.
#ifndef DDS_USE_ONLY_TIMER2
TCCR1B = 0;
#ifndef DDS_USE_ONLY_TIMER2
TCCR1B = 0;
#endif
TCCR2B = 0;
#endif
TCCR2B = 0;
}

// Set our current sine wave frequency in Hz
ddsAccumulator_t DDS::calcFrequency(unsigned short freq) {
// Fo = (M*Fc)/2^N
// M = (Fo/Fc)*2^N
ddsAccumulator_t newStep;

if(refclk == DDS_REFCLK_DEFAULT) {
// Try to use precalculated values if possible
if(freq == 2200) {
newStep = (2200.0 / (DDS_REFCLK_DEFAULT+DDS_REFCLK_OFFSET)) * pow(2,ACCUMULATOR_BITS);
return (2200.0 / (DDS_REFCLK_DEFAULT+DDS_REFCLK_OFFSET)) * DDS_MAX_ACCUMULATOR;
} else if (freq == 1200) {
newStep = (1200.0 / (DDS_REFCLK_DEFAULT+DDS_REFCLK_OFFSET)) * pow(2,ACCUMULATOR_BITS);
return (1200.0 / (DDS_REFCLK_DEFAULT+DDS_REFCLK_OFFSET)) * DDS_MAX_ACCUMULATOR;
} else if(freq == 2400) {
newStep = (2400.0 / (DDS_REFCLK_DEFAULT+DDS_REFCLK_OFFSET)) * pow(2,ACCUMULATOR_BITS);
return (2400.0 / (DDS_REFCLK_DEFAULT+DDS_REFCLK_OFFSET)) * DDS_MAX_ACCUMULATOR;
} else if (freq == 1500) {
newStep = (1500.0 / (DDS_REFCLK_DEFAULT+DDS_REFCLK_OFFSET)) * pow(2,ACCUMULATOR_BITS);
return (1500.0 / (DDS_REFCLK_DEFAULT+DDS_REFCLK_OFFSET)) * DDS_MAX_ACCUMULATOR;
} else if (freq == 600) {
newStep = (600.0 / (DDS_REFCLK_DEFAULT+DDS_REFCLK_OFFSET)) * pow(2,ACCUMULATOR_BITS);
return (600.0 / (DDS_REFCLK_DEFAULT+DDS_REFCLK_OFFSET)) * DDS_MAX_ACCUMULATOR;
}
} else {
newStep = pow(2,ACCUMULATOR_BITS)*freq / (refclk+refclkOffset);
}
return newStep;
}

// Not a pre-calc frequency OR not using default REFCLK
return DDS_MAX_ACCUMULATOR * freq / (refclk+refclkOffset);
}

// Degrees should be between -360 and +360 (others don't make much sense)
void DDS::setPhaseDeg(int16_t degrees) {
accumulator = degrees * (pow(2,ACCUMULATOR_BITS)/360.0);
accumulator = degrees * (DDS_MAX_ACCUMULATOR/360.0);
}
void DDS::changePhaseDeg(int16_t degrees) {
accumulator += degrees * (pow(2,ACCUMULATOR_BITS)/360.0);
accumulator += degrees * (DDS_MAX_ACCUMULATOR/360.0);
}

// TODO: Clean this up a bit..
Expand All @@ -112,64 +183,107 @@ void DDS::clockTick() {
if(running) {
accumulator += stepRate;
if(timeLimited && tickDuration == 0) {
#ifndef DDS_PWM_PIN_3
OCR2A = 0;
#ifdef __SAMD21G18A__
#ifdef DDS_IDLE_HIGH
analogWrite(A0, DDS_MAX_COMPARATOR/2);
#else
analogWrite(A0, 0);
#endif
#else
#ifdef DDS_IDLE_HIGH
#ifndef DDS_PWM_PIN_3
OCR2A = 0;
#else
#ifdef DDS_IDLE_HIGH
// Set the duty cycle to 50%
OCR2B = pow(2,COMPARATOR_BITS)/2;
#else
OCR2B = DDS_MAX_COMPARATOR/2;
#else
// Set duty cycle to 0, effectively off
OCR2B = 0;
#endif
OCR2B = 0;
#endif
#endif
#endif
running = false;
accumulator = 0;
} else {
#ifdef DDS_PWM_PIN_3
OCR2B = getDutyCycle();
#ifdef __SAMD21G18A__
analogWrite(A0, getDutyCycle());
#else
#ifdef DDS_PWM_PIN_3
OCR2B = getDutyCycle();
#else
OCR2A = getDutyCycle();
#endif
#endif
}
// Reduce our playback duration by one tick
tickDuration--;
} else {
#ifdef __SAMD21G18A__
#ifdef DDS_IDLE_HIGH
analogWrite(A0, DDS_MAX_COMPARATOR/2);
#else
analogWrite(A0, 0);
#endif
#else
// Hold it low
#ifndef DDS_PWM_PIN_3
#ifndef DDS_PWM_PIN_3
OCR2A = 0;
#else
#ifdef DDS_IDLE_HIGH
#else
#ifdef DDS_IDLE_HIGH
// Set the duty cycle to 50%
OCR2B = pow(2,COMPARATOR_BITS)/2;
#else
// Set duty cycle to 0, effectively off
OCR2B = 0;
#endif
OCR2B = DDS_MAX_COMPARATOR/2;
#else
// Set duty cycle to 0, effectively off
OCR2B = 0;
#endif
#endif
#endif
}
}

uint8_t DDS::getDutyCycle() {
#if ACCUMULATOR_BIT_SHIFT >= 24
//TODO: rename this function as it is more than just dutyCycle?
//now it powers both a PWM dutyCycle as well as a DAC value on the Zero
ddsComparitor_t DDS::getDutyCycle() {
#if ACCUMULATOR_BITS - ACCUMULATOR_BIT_SHIFT > 8
// For larger sineTables which need more thn 8 bits.
uint16_t phAng;
#else
// For standard sineTables which need 8 bits.
uint8_t phAng;
#endif

if(amplitude == 0) // Shortcut out on no amplitude
return 128>>(8-COMPARATOR_BITS);
return DDS_MAX_COMPARATOR/2;

// Lookup the value from the sin table.
phAng = (accumulator >> ACCUMULATOR_BIT_SHIFT);
int8_t position = pgm_read_byte_near(ddsSineTable + phAng); //>>(8-COMPARATOR_BITS);
// Apply scaling and return
#if DDS_LOOKUPVALUE_BITS > 8
// 16-bit lookup
int16_t position = pgm_read_word_near(ddsSineTable + phAng); //>>(8-COMPARATOR_BITS);
int32_t scaled = position;
#else
// 8-bit lookup
int8_t position = pgm_read_byte_near(ddsSineTable + phAng); //>>(8-COMPARATOR_BITS);
int16_t scaled = position;
// output = ((duty * amplitude) / 256) + 128
// This keeps amplitudes centered around 50% duty
#endif

// If there's an amplitude, scale it
if(amplitude != 255) { // Amplitude is reduced, so do the full math
scaled *= amplitude;
scaled >>= 8+(8-COMPARATOR_BITS);
} else { // Otherwise, only shift for the comparator bits
scaled >>= (8-COMPARATOR_BITS);
scaled *= amplitude; // multiply by the amplitude, max 255 or an 8-bit shift
scaled >>= 8; // bring it back 8 bits devide
}
scaled += 128>>(8-COMPARATOR_BITS);

// Scale to the number of bits available
#if COMPARATOR_BITS > DDS_LOOKUPVALUE_BITS
scaled <<= (COMPARATOR_BITS - DDS_LOOKUPVALUE_BITS);
#elif COMPARATOR_BITS < DDS_LOOKUPVALUE_BITS
scaled >>= (DDS_LOOKUPVALUE_BITS-COMPARATOR_BITS);
#else
// COMARATOR_BITS == DDS_LOOKUPVALUE_BITS -- no math needed.
#endif

//Move the sinewave up 1/2 scale into the positive range.
scaled += DDS_MAX_COMPARATOR/2;
return scaled;
}

Loading