Low Voltage AC Source (Part 4)

electronics AC DAC PWM

Today, we are going to look at a third method for generating a sine wave with an Arduino: using the Arduino's PWM output as DAC.

PWM DACs

In the last part of this project we looked at RC filters and how they can be used to remove specific frequency components from a signal. We made use of a low-pass filter to remove the higher order harmonics of a square wave to transform it into a sine wave. Let's say we had limited success with that.

This time, we try a slightly different approach. If we generate a square wave with an Arduino, it does not only have higher frequency components, but also a DC component. In the last part of this project, I didn't tell you about this DC component. The DC component is always the signals average voltage. In case of a square wave between 0 V and 5 V the average value is exactly 2.5 V. All these voltages are given in reference to the Arduino's ground. In reference to the ground for our AC circuit this corresponds to 0 V and this is why we could ignore the DC part the last time. Today, this DC component is what we are interested in.

If we generate a PWM signal, the signal does not only have a specific frequency, but we can also define its duty cycle. In case of our square wave the duty cycle is exactly 50 %. The output is HIGH for 50 % of the time and LOW for the rest of the time. The catch is, that if we change the duty cycle, we also change the average voltage of the signal. This is visible in the image below, which shows three PWM signals with different duty cycles. The average voltage is marked as a blue line.

PWM signals with different duty cycles and their average voltage

We can calculate the average voltage using a simple formula with \(D\) as duty cycle:
\(V_{avg} = (V_{max}-V_{min}) \cdot D\)

In theory, we can generate any average voltage between 0 V and 5 V. The Arduino Uno allows us to specify a value between 0 and 255 as duty cycle, which means that in theory the PWM output can be used as an 8-bit DAC. But wait, there is one thing we should not forget: the outputted signal is not a constant voltage, but changes constantly. To really use the PWM output as a DAC, we need to smooth out the output signal. In other words, we need to remove all high frequency components from it. Luckily, we already know how this can be done: we use a low pass filter.

Does this mean we run into the same problem as the last time, where we had issues to separate the signal components we wanted to keep from the ones we wanted to remove? Well, not necessarily. If the PWM frequency is chosen high enough, there is a clear distinction between the desired output signal and the PWM signal with its high frequency components. This makes using a filter much easier. Let's start and see how this works in practice.

A First Try

As we are need a low-pass filter, just like in the last part of this project, we can simply use the same circuit. The low pass filter consists of a 220 Ω resistor and a 10 uF capacitor and thus has a cutoff frequency of roughly 72 Hz. Pin 9 of the Arduino is used as PWM pin and just like the last times we use a voltage divider of two 220 Ω resistors to create our new ground reference at 2.5 V.

1st order RC low pass filter circuit

Nothing new for the circuit, but what about the code? Well, a PWM DAC is not that much different from a normal DAC, so we should be able to reuse most of the code from the second part of this project. Before I show you the code, let's talk about the parts that need to be adjusted.

First, we don't need the MCP4725 library anymore as we are not using this external DAC module. In consequence, the DAC initialization can also be removed. Additionally, we need a new method to output the data samples in the loop procedure. For our PWM DAC this is really simple. We just set the duty cycle via analogWrite:

analogWrite(9, voltages[current_step]);

If we use analogWrite the duty cycle needs to be given as a value between 0 and 255. This means, we need to adjust our code for precalculating the output values. The new code looks like this:

// Precalculate Sine Values (0 - 255)
for(int i = 0; i < steps; i++) {
  voltages[i] = (sin(i*3.14*2/steps) + 1) * 127.5;
}

Last not but least we need to adjust the timing. Like for the DAC module, we could go through a lot of effort here to achieve a perfect timing. But, let's think about that later. For the moment, let's adjust the usPerCommand to a value that make sense for PWM output.

What value makes sense? Well, the Arduino Uno uses a PWM frequency of 490 Hz on pin 9 by default. This means that a full period takes slightly over 2 ms. The maximum speed at which we can change the duty cycle is once per period. Let's use a slightly higher value to be sure and set usPerCommand to 2500 (2.5 ms). The complete code now looks like this:

#include <EEPROM.h>

// PWM frequency is 490 Hz so we roughly upate every 2 ms
const int usPerCommand = 2500;

// Precalculated Voltage Buffer
const int BUFFER_SIZE = 256;
unsigned int voltages[BUFFER_SIZE];
unsigned int steps;
unsigned int usPerStep;

unsigned int current_step = 0;
unsigned long start_time;

// Setup frequency
void setup() {
  // Read desired frequency
  Serial.begin(9600);
  Serial.print("Enter Frequency (Hz): ");

  // Wait 10s for input otherwise take stored value
  Serial.setTimeout(10000);
  float frequency = Serial.parseFloat();
  if(frequency == 0) EEPROM.get(0, frequency);
  else EEPROM.put(0, frequency);

  Serial.println(frequency);

  // Calculate number of possible steps
  int possible_steps = 1000000/usPerCommand/frequency;

  // Steps need to be a multiple of 4 to keep the sine form
  steps = (possible_steps  / 4) * 4;
  if(steps > BUFFER_SIZE) steps = BUFFER_SIZE;
  if(steps < 4) steps = 4;

  // Time per Step
  usPerStep = 1000000 / (frequency * steps);
  if(usPerStep < usPerCommand) usPerStep = usPerCommand;

  // Precalculate Sine Values (0 - 255)
  for(int i = 0; i < steps; i++) {
    voltages[i] = (sin(i*3.14*2/steps) + 1) * 127.5;
  }

  Serial.print("Number of output steps: ");
  Serial.println(steps);

  Serial.print("Microseconds per step: ");
  Serial.println(usPerStep);

  Serial.print("Archieved Frequency (Hz): ");
  Serial.println(1000000.0/float(steps)/float(usPerStep));

  // Initially set start time
  start_time = micros();
}

// Output values
void loop() {
  analogWrite(9, voltages[current_step]);
  current_step++;
  if(current_step >= steps) current_step = 0;

  while(micros()-start_time < usPerStep);
  start_time += usPerStep;
}

In the image below you can see the result. It's not exactly what we have hoped for, but the generated signal is at least similar to a sine wave. What is the problem and how can we improve this?

Signal generated by our program

There are two main issues: the voltage spikes and the fact that the curve looks a bit different in each period. The voltage spikes are easy to explain: each time the PWM output switches to HIGH the capacitor charges up and the voltage rises and once the PWM output switches to LOW the voltage decreases again. The reason why this is visible that much is, that the PWM frequency of 490 Hz is still quite low for generating a 50 Hz sine wave. If we look at the FFT of the filtered output signal, we can see that the frequency components of the PWM signal are not filtered out completely. Even after filtering the PWM signal, the output signal still oscillates around the set output voltage by about 500 mV.

FFT of the filtered 490 Hz PWM signal with 50 % duty cycle

The second issue can be explained by the fact that the PWM frequency of 490 Hz is not a true multiple of the sampling rate, which we used to precalculate and output the sine wave values. This can be easily seen in the image below.

Sampling of the output signal

The red line shows the mean voltage we set using analogWrite. However, if we change the duty cycle the PWM signal does not instantaneously adjust to the new value. The new value is loaded once the current period of the PWM signal finishes. Thus, the real output corresponds to the blue line, where we can see the same effect as on the oscilloscope: each period of the generated sine wave looks a bit different. Well, that's not exactly true. If we observe the signal a bit longer, we can see that the pattern repeats after 100 ms which corresponds to exactly 5 sine wave periods or 49 PWM periods. This means that the timing error won't rise indefinitely, however, it's still bad. We need a way to fix both issues: the voltage spikes and the timing errors.

Improving the PWM DAC

How can we improve our PWM DAC? Well, to get rid of the voltage spikes, we could use a higher order filter as we did in the last part of this project. This, however, won't solve our timing issues. A better solution, that improves both issues is to increase the PWM frequency.

A higher PWM frequency means, that it's AC frequency components get even more attenuated by the low pass filter. It also means, that we can output a more precise sine wave with more samples and less timing errors. Ideally, we would additionally synchronize PWM, sampling and sine wave frequency. While this would be possible in theory, it is unnecessarily complex. If the PWM frequency is high enough, the remaining timing errors are so small, that we can just ignore them.

To sum up, increasing the PWM frequency seems like the perfect solution for our issues. The question is now: how can we increase the PWM frequency? There is no way to specify a PWM frequency in the analogWrite call. Fact is, there is no predefined function, that we could call to do this. In case of the Arduino Uno, the PWM output is generated by the ATmega328P's hardware timers. The PWM output for pin 9 and 10 is generated by timer 1. There are special Arduino libraries, like TimerOne, that give us more control over this timer. With them we could also change the PWM frequency. However, we can also do this by directly accessing the timer registers and reconfiguring it. This is what the following line of code does:

TCCR1B = (1<<WGM12) | (1<<CS10);

It sets the PWM frequency to 62.5 kHz. This is the highest PWM frequency, we can achieve that works with the analogWrite procedure and an 8-bit value for the duty cycle. Of course, we also need to adjust the sampling rate we use to precalculate and output the samples. For that we need to adjust the variable usPerCommand. The PWM frequency would allow us to set a new value every 16 us. However, our loop procedure takes more time to execute. We could measure exactly how long it takes to execute our loop procedure. However, we are just going to use a safe value like 100 us per iteration. This allows for a maximum sampling frequency of 10 kHz. As we kept, the code we used for the DAC, this sampling frequency gets adjusted to the configured sine wave frequency. However, as said before, we won't go through the effort to synchronize this with the PWM frequency.

After applying the required changes, the adjusted code looks like this:

#include <EEPROM.h>

// PWM frequency is 62.5 kHz so we can update every 16 us
const int usPerCommand = 100;

// Precalculated Voltage Buffer
const int BUFFER_SIZE = 256;
unsigned int voltages[BUFFER_SIZE];
unsigned int steps;
unsigned int usPerStep;

unsigned int current_step = 0;
unsigned long start_time;

// Setup frequency
void setup() {
  // Read desired frequency
  Serial.begin(9600);
  Serial.print("Enter Frequency (Hz): ");

  // Wait 10s for input otherwise take stored value
  Serial.setTimeout(10000);
  float frequency = Serial.parseFloat();
  if(frequency == 0) EEPROM.get(0, frequency);
  else EEPROM.put(0, frequency);

  Serial.println(frequency);

  // Calculate number of possible steps
  int possible_steps = 1000000/usPerCommand/frequency;

  // Steps need to be a multiple of 4 to keep the sine form
  steps = (possible_steps  / 4) * 4;
  if(steps > BUFFER_SIZE) steps = BUFFER_SIZE;
  if(steps < 4) steps = 4;

  // Time per Step
  usPerStep = 1000000 / (frequency * steps);
  if(usPerStep < usPerCommand) usPerStep = usPerCommand;

  // Precalculate Sine Values (0 - 255)
  for(int i = 0; i < steps; i++) {
    voltages[i] = (sin(i*3.14*2/steps) + 1) * 127.5;
  }

  Serial.print("Number of output steps: ");
  Serial.println(steps);

  Serial.print("Microseconds per step: ");
  Serial.println(usPerStep);

  Serial.print("Archieved Frequency (Hz): ");
  Serial.println(1000000.0/float(steps)/float(usPerStep));

  // Enable the fastest possible frequency for timer 1
  TCCR1B = (1<<WGM12) | (1<<CS10);

  // Initially set start time
  start_time = micros();
}

// Output values
void loop() {
  analogWrite(9, voltages[current_step]);
  current_step++;
  if(current_step >= steps) current_step = 0;

  while(micros()-start_time < usPerStep);
  start_time += usPerStep;
}

As we can achieve quite a high sampling frequency with this solution, we should think about whether we want to adjust the low pass filter to allow higher frequencies for our sine wave. With a sampling frequency of 10 kHz, we could generate a sine wave with a frequency of 1 kHz and more. However, the filters cutoff frequency limits us to 72 Hz at -3 dB gain. For higher frequencies not only the PWM signal, but also our sine wave signal gets more and more attenuated until its no longer recognizable. If we want to allow frequencies above 50 Hz, we need to adjust the filter. For this we can e.g. exchange the 10 uF capacitor with a 1 uF capacitor. This changes the filters cutoff frequency to roughly 720 Hz.

As shown in the image below, we should also add an additional 100 nF capacitor in parallel to the bigger electrolytic capacitor. I won't go into the details of why we need this extra capacitor. The short version: it helps reduce the high frequency noise in the output signal. It is needed because a real capacitor does not behave like an ideal capacitor and always has some inductance too. If you are interested in more details you can search for the so-called equivalent series inductance (ESL) of capacitors. Improved circuit for a higher PWM frequency

The Result

Let's look at the new result. With adjusted program and improved circuit, we now get a clean 50 Hz sine wave.

Generated sine wave after the improvements

Just like with the DAC module, the output signal has an amplitude of 2.5 V and just like with the DAC module this solution cannot provide a lot of power for an AC circuit. Both solutions are pretty much equal. However, as mentioned in the previous part, where we also used a digital output signal, working with a digital output signal is more familiar to us and allows the use of amplification methods that can't be easily applied to analog signals. In the next parts of this project we will focus on the topic of amplification and look at different methods.

Previous Post Next Post