NB1A Numerically Controlled Oscillator (NCO)
Using a counter timer interrupt the
NB1A can be programmed to
function as a four-channel numerically controlled oscillator. This
example describes how to generate four independent sine waves using
the
NB1A. The DAC codes for
a single cycle of a sine wave are stored in an array. An interrupt
routine is used to trigger the periodic update of the DAC channels.
Wavetable
To discretely generate a waveform samples of the waveform are
periodically output using a DAC. Each sample represents the value of
the waveform at a specific phase of a single cycle. To determine which
sample is to be output a phase accumulator is used. The phase
accumulator is incremented periodically and a wavetable address is
calculated. Increasing the value of the phase increment increases the
frequency of the output waveform. Decreasing the value decreases the
output waveform frequency.
The frequency is determined by the formula --
frequency = sample rate / number of samples per cycle
The size of the wavetable and the number of bits per sample is
determined by the acceptable signal-to-error noise ratio (SNR). In
this example the wavetable contains 64 bytes (one byte for each
sample). The DAC on the NB1A is an 8 bit DAC. The SNR is 30dB. If a
table size of 2048 bytes is used then the SNR would be 54.7 (Moore
1988).
For this example a sample rate of 3750Hz was chosen. If all 64 samples
are output at the sample rate the frequency will be ≈ 60Hz
(3750/64). This corresponds to a phase increment of one. Every
266μS the phase accumulator is incremented by one and a sample is
output. For higher output frequencies the phase increment would be
greater than one and samples would be skipped. For lower frequencies
the phase increment would be less than one and samples would be
repeated.
To determine the phase increment required to generate a specific
frequency --
phase increment = frequency * sine table length / sample rate
Wavetable Optimization for Sinusoidal Waveforms
For this example the waveform is sinusoidal and the wavetable contains
one full cycle of the waveform. Using the half-wave symmetry of the
sinewave the size of the wavetable could be reduced by a factor of
two. When the phase-accumulator is on the second half of the waveform
the values output would be negative (with respect to the mid-scale of
the DAC).
Using the quarter-wave symmetry of the sinewave the size of the
wavetable could be reduced by a factor of four. This would require
decrementing the phase-accumulator during the second and fourth
quarters of the cycle (and changing signs for the third and fourth
quarter).
Exploiting the sine-wave symmetries would make the code more difficult
to modify for other waveforms. The trade-off is memory size for code
extensibility.
NCO Data Structure
The heart of this NCO implementation is the structure that holds the
phase accumulator, the phase increment and the control byte for the
DAC (below).
struct nco_struct {
union {
struct {
unsigned UNUSED1 : 16;
unsigned UNUSED2 : 9;
unsigned round : 1;
unsigned address : 6;
} acc_bits;
unsigned long acc_long;
} acc;
unsigned long inc;
unsigned char control; // control byte for the TLV5620
};
The phase accumulator determines the current memory location in the
wavetable. In this example the wavetable is 64 bytes so only the top
six bits are required for the address
(
acc.acc_bits.address
). The seventh bit
(
acc.acc_bits.round
) is used to round up the address
value (use of the rounding bit in the phase accumulator improves the
SNR by ≈ 6dB (Moore 1988) . The lower 23 bits are used in the
phase calculation but are ignored in the address calculation. If the
wavetable size is changed then the size of
acc.acc_bits.address
,
acc.acc_bits.UNUSED2
and
acc.acc_bits.UNUSED1
can be changed.
Phase Accumulator and DAC Updates
Outputting waveforms consists of periodically updating the
phase-accumulators and outputting the DAC codes (below). The
phase-accumulator value is updated by adding the phase-increment. The
address of the DAC code to be output is contained in the address
bits. If the rounding bit is set then the address is incremented by
one. If the address is incremented then it needs to be masked to
prevent generating an address to a non-existent wavetable location.
.
.
#define nco_addr(i) (nco[i].acc.acc_bits.address)
#define nco_round(i) (nco[i].acc.acc_bits.round)
.
.
unsigned char addr;
.
.
for (i = 0; i < NCO_NUM_CHS; i++) {
nco_inc_phase_acc(i);
addr = nco_addr(i);
if (nco_round(i)) {
addr += 1;
addr &= NCO_ADDR_MASK;
}
//
// update DAC channel i
//
.
.
}
NCO Output
The oscilloscope picture (right) shows the output of two channels of
the NCO. Channel one is set to a frequency of 100Hz, channel two is
set to 60Hz. Although only two channels could be shown on the
oscilloscope all four channels are running. No filtering was added to
the DAC outputs.
With the sample rate set to 3750Hz and a wavetable size of 64
samples a 100Hz output contains 37 samples per cycle.
The 60Hz output contains 58 samples per cycle.
NB1A NCO Example
#include <avr/interrupt.h>
#include <avr/io.h>
// Numerically Controlled Oscillator (NCO)
// Uses the quad DAC on the NB1A to create a four channel numerically
// controlled oscillator.
extern "C"
{
#include <NCO.h>
}
#define UC_MOSI PB3
#define UC_MISO PB4
#define UC_SCK PB5
volatile unsigned char update_dac = 0;
void setup() {
// Compensating for the 12MHz XTAL
// 12800 = (16/12) * 9600
// 25600 = (16/12) * 19200
Serial.begin(12800);
PORTB |= (1<<NCO_DAC_LOAD) | (1<<NCO_DAC_LATCH);
DDRB |= (1<<UC_MOSI) | (0<<UC_MISO) | (1<<UC_SCK) | (1<<NCO_DAC_LOAD) | (1<<NCO_DAC_LATCH);
DDRD |= (1<<PD7);
// Setup the SPI control register (SPCR)
// and the status register (SPSR)
// Page 174 ATmega328P Datasheet Rev 8025I-AVR-02/09
// SPIE = 1 SPI interrupts enabled
// SPE = 1 SPI enabled
// DORD = 0 Data Order MSB First (=1 LSB First)
// MSTR = 1 Master mode (=0 Slave)
// CPOL = 0 SCK low when idle (=1 SCK high when idle)
// CPHA = 1 Sample on leading edge (=1 falling edge)
// SPR1, SPR2 = 0 fosc/4 (= 1 fosc/16)
// (= 2 fosc/64)
// (= 3 fosc/128)
// SPSR is read only except for the double speed bit
// SPI2X = 1 double speed
SPCR = (0<<SPIE) | (1<<SPE) | (1<<MSTR) | (1<<CPHA);
SPSR = (1<<SPI2X);
// page 158
// Clear timer on Compare Match (CTC) Mode
// Counter (TCNT2) counts up to OCR2A and is is cleared
// when TCNT2 == OCR2A use the OCF2A flag to generate an interrupt.
// Each interrupt occurs at a frequency of = fclk_io / N * (1+OCR2A)
// N is 1,8,32,64,256,1024
// fclk_io = 12MHz for the NB1A
// With N = 256 the frequency range is 23437.5Hz (OCR2A = 0)
// to 91.55Hz (OCR2A = 255)
// (OCR2A, Freq) = (2, 7812Hz), (3, 5859Hz)
// TCCR2A Timer/Counter Control Register A
// COM2A1 = 0 Normal port operation for OC2A
// COM2A0 = 0
// COM2B1 = x
// COM2B0 = x
// BIT3
// BIT2
// WGM21 = 1 WGM2x are the waveform generation bits for
// WGM20 = 0 channel the counter-timer 2. There are three
// bits WGM21 and WMG21 are in TCCRA and
// WGM22 is in TCCR2B
TCCR2A |= (1<<WGM21);
TCCR2A &= ~((1<<COM2A1) | (1<<COM2A0) | (1<<WGM20));
// TCCR2B
// FOC2A = x Force Output Compare (not used)
// FOC2B = x Force Output Compare (not used)
// BIT5
// BIT4
// WGM22 = 0 CTC (WGM21 and WGM20 are in TCCR2A)
// CS22 = 1 Prescaler = 32 (NCO_CLOCK_DIV32)
// CS21 = 0
// CS20 = 0
TCCR2B = NCO_CLOCK_DIV32;
// For this NCO the prescaler is set to divide by 32.
// Setting OCR2A to NCO_OCR2A (99) gives a sampling
// frequency of 3750
OCR2A = NCO_OCR2A;
TIMSK2 |= (1<<OCIE2A);
nco_init();
nco_set_freq(0, 60);
nco_set_freq(1, 100);
nco_set_freq(2, 300);
nco_set_freq(3, 200);
sei();
}
void loop() {
unsigned char pt;
if (update_dac) {
nco_update();
update_dac = 0;
}
}
ISR(TIMER2_COMPA_vect) {
#if DEBUG
if (PIND & (1<<PD7)) PORTD &= ~(1<<PD7);
else PORTD |= (1<<PD7);
#endif
update_dac++;
}
References
Snell, John 1988 "Design of a Digital Oscillator That Will Generate up
to 256 Low-Distortion Sine Waves in Real Time"
Foundations of
Computer Music. Cambridge, Mass.: MIT Press
Moore, F. Richard 1988 "Table Lookup Noise for Sinusoidal Digital
Oscillators"
Foundations of Computer Music. Cambridge, Mass.:
MIT Press