Direct Digital Synthesizer (DDS) (Part 7)

Part 12345678

A recent project lead me to excavate a quite old DDS design which may be of some interest for you. Instead of using a ladder of resistors, this design features a simple, cheap and yet powerful digital to analog converter. I will later explain why I choose this particular Microchip circuit but for now I will concentrate on its key features.

  • 12-Bit Resolution
  • ±0.2 LSB DNL (typ)
  • ±2 LSB INL (typ)
  • Single (MCP4921) or Dual Channel (MCP4922)
  • Rail-to-Rail Output
  • SPI™ Interface with 20 MHz Clock Support: that’s very convenient for the DDS application
  • Fast Settling Time of 4.5 µs
  • Selectable Unity or 2x Gain Output
  • External VREF Input. In this way you can choose th full range of the output signal
  • 2.7V to 5.5V Single-Supply Operation (n other words it is Uno and Due compatible)

The chip is encapsulated in a convenient PDIP 8 pins package which is very handy for prototyping; its also comes in smaller packages such as  SOIC, MSOP and TSSOP. For more information about the MCP4921, check its datasheet >here<. You may also use the MCP4821 which is almost identical but features a built in reference voltage (2.048 V) check its datasheet >here<.

Note: Five years from now, very few publications were dealing the MCP4921 DAC; after a quick overview on the subject, I realized that many posts cover the subject. However I decided to stick to my original idea as later post will refer to this one.

Driving the MCP4921 DAC is quite easy from a hardware and a software perspective. The picture below illustrates a quick and dirty implementation of the chip using an Arduino Nano and a bread board. All you have to do is to wire the SPI ports properly.

Next is an example of wiring. Real basic: the output swing from GND to the voltage applied to Vref (actually 5 V.

The you may use a couple of simple functions as a starting point: initialize the chip

/* Initialize the chip */
void PlainMCP4921::InitializeMCP4921(uint8_t csPin)
	/* Initialize SPI port */
	/* Record the chip select pin mask */
	_csPinMask = (1 << csPin);
	/* Set chip select pin as an output pin */
	DDRB |= _csPinMask;
	/* Set default state high */
	PORTB |= _csPinMask;

and set require a digital value conversion

/* Write parameters and data; gain is as per HT_GAI_X constants */
void PlainMCP4921::Convert(uint16_t digitalValue, uint8_t gain)
	uint8_t LSB, MSB;
	/* Set parameter bytes */
	/* Set data bytes */
	MSB |= ((digitalValue >> 8) & 0x0F);
	LSB = (digitalValue & 0xFF);
	/* Send data */
	PORTB &= ~_csPinMask; /* Assert converter */
	PORTB |= _csPinMask; /* Deassert converter */

These very simple functions shall be reused in the in the original PlainDDS library and renamed PlainMCP4921. In this way we get an improved version of PlainDDS, now capable of a dynamic range of 12 bits instead of the original 6 bits. However the timer limitations stay the same and the deign fails to achieve high frequencies. However, it is still very convenient for many applications. Next is an example of code which generates a fixed frequency signal:

#include <PlainDDS_MCP4921.h>
#include <PlainSPI.h>

PlainDDS_MCP4921 DDS; /* Create DDS object */

/* Blink control led */
void BlinkLed(uint16_t cycles, uint16_t duration) 
	const uint8_t ledPin = PIND7;
	uint8_t ledPinMask = (1 << PIND7);
	DDRD |= ledPinMask; 
	PORTD &= ~ledPinMask; 
	for (uint8_t i = 0; i < (cycles << 1); i++)	{
		delay(duration >> 1);
		PORTD ^= ledPinMask;

void loop(void)

void setup(void)
	BlinkLed(5, 200);

… where the most difficult part is probably the LED blinker. As usual, the libraries are available as per the arduinoos policy

Next post on same subject



Leave a Reply

You must be logged in to post a comment.