Wiblocks --- NB1A DAC

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