Low Voltage AC Source (Part 2)

DAC AC electronics

Let's move on and generate a sine wave signal for our AC source. Our first approach: using a DAC.

Generating a Sine Wave using a DAC

In the first part of this project we discussed the basics of generating an AC signal. While it's not strictly necessary for an AC signal to have a sine waveform, it would be nice to get as close as possible to what we know from mains voltage. This is why we are now going to look at ways to generate such a signal. Our first approach: using a DAC.

Digital-to-analog converters (DACs) are a straight forward choice, if the task is to generate analog signals with a microcontroller. Unfortunately, the Arduino does not have a built-in DAC and this is why we have to use an external DAC like the MCP4725. I already made a two-part tutorial on this DAC, so I won't go into any details here. If you want to know what you can do with this module and what it's limits are, take a look at the MCP4725 tutorial. Today, we will focus on the sine wave signal generation using this DAC.

How do we do this? Well, the DAC allows us to output arbitrary analog voltages between 0V and round about 5V. This means that we can calculate the output voltage using the sin function and then just need to output the calculated values with the correct timing to match the desired frequency. While this sounds quite trivial at first sight, the correct timing part is a particularly troublesome one. Thus, let's look at this step-by-step starting with a very basic test circuit.

The Circuit

The circuit for our little AC source is not very complex. If you remember the abstract circuit from the last part of this project, we can put in the MCP4725 DAC module in the place of the signal generation circuit. Abstract Circuit Diagram with MCP4725 DAC

The image below shows how everything gets wired up. The MCP4725 is connected via I2C. For the voltage divider, that generates our new ground reference, I used two 220 Ω resistors. The additional 10 kΩ resistor is our load over which we can measure the AC signal with an oscilloscope or a DMM. Circuit on a Breadboard

The voltage divider limits our maximum current to \(I_{peak} = {V_{peak} \over R} = {2.5 V \over 220 Ω} = 11.4 mA\). If we need a higher current we would need to decrease the resistor values. This is, however, not a real solution as the current that flows directly across the two resistors also increases. More and more power gets unnecessarily lost as heat.

Voltage dividers are not intended to power anything. In this circuit we are kind of abusing a voltage divider. However, the maximum current the DAC can supply, while still outputting a stable signal is below 10 mA anyway. Our AC source is not a very powerful one. This is an issue, we will take care of in a later phase of this project. For now, we stick to what we have.

Hints for measuring the signal with an Oscilloscope
For measuring our AC signal, we need to connect the probe to one side of the resistor and the ground lead to the other side. Nothing special, however, there is one thing to keep in mind: the ground of all channels are connected to each other and to mains earth. This is normally not an issue, however, our case is somewhat special, as we use the 2.5V output of our voltage divider as ground for the AC part of our circuit. For this reason, there are two things you should avoid:

  • Do not short the voltage divider output to ground via a second probe
    When connecting a second probe for another measurement, remember that the grounds are all connected together. If you connect the ground lead of one channel to the 2.5 V generated by the voltage divider and the ground lead of another channel to the Arduino's ground, you produce a short.
  • Do not short the voltage divider output to ground via the mains earth connection Remember that the oscilloscopes ground leads are connected to mains earth. If the Arduino's ground is also connected to mains earth (e.g. via your computer), you short the voltage dividers output to the Arduino's ground when connecting the probe. A current can flow all the way through the oscilloscope, the power line, your computer and the USB cable to the Arduino's ground. Use a multimeter to check whether the Arduino's ground is connected to mains earth before you connect the probe. You can e.g. measure continuity between the outer ring of the oscilloscopes BNC jacks and the metal shield of the Arduino's USB jack. If they are connected, power the Arduino via a battery and disconnect the USB cable before the measurement to avoid a short. Alternatively you can use a USB isolator.

Luckily, the resistors of the voltage divider limit the current in case of a short. This prevents any serious damage.

The Theory: Outputting Analog Signals

Before we walk through the code for generating the sine wave, let's look at some fundamentals. You can't just output a sine wave with a DAC, just like you can't just measure an analog signal with an ADC. If we want to work with analog signals with a microcontroller, we need to think about how to represent the analog signal digitally.

Resolution and sampling rate

Resolution

If you have read the tutorial on the MCP4725 DAC, you know already that we use a number between 0 and 4096 to represent its output voltage. For the Arduino's ADC the measured voltage is represented in a number between 0 and 1023. In the analog world there is an endless amount of possible voltages between 0 V and 5 V. ADCs and DACs, however, have a limited resolution. They can only differentiate between a limited number of different analog values. The Arduino's ADC has a 10-bit resolution and can differentiate between 1024 different voltage. The MCP4725 DAC can differentiate between 4096 output values. At 5 V supply voltage, this allows us to set the output voltage in 1.22 mV steps. If we use larger steps or in other words a low output resolution, the steps become visible in the outputted signal. The output signal looks like a staircase. The extreme example is a digital output which has a 1-bit resolution and only knows two different output values: on and off.

Sampling Rate

If we want to generate a signal that changes over time, the sampling rate is another important dimension. It tells us how often we measure or output a new voltage sample per second. To represent a signal digitally we need to transform the continuous signal into a series of discrete samples. However, if the sampling rate is too low we won't be able to reconstruct the original waveform from our discrete samples. To reconstruct a sine wave we need at least two samples per period to determine its frequency and amplitude. We need to sample with a frequency that is at least twice as high as the sine waves actual frequency. This is also known under the Nyquist-Shannon sampling theorem.

How Resolution and Sampling Rate relate

The sampling theorem tells us, that we need at least a 100 samples per second to be able to reconstruct a 50 Hz sine wave. There is an important catch, however. With only 2 samples per sine wave period we can only measure two different voltages per sine wave period which corresponds to a 1-bit resolution. While this is enough to mathematically reconstruct the signal, if we know that it is a sine wave signal with a frequency lower than our sampling frequency, this doesn't help us much, if we want to output a sine wave signal. The outputted signal would correspond to a 50 Hz square wave signal, which could also be generated by just toggling a pin or using PWM. Such a signal can be transformed into a sine wave signal as well, but that's going to be the topic for the next time. The 12-bit resolution of our DAC is of no use here. The sampling rate is the limiting factor here, not the DAC's resolution. If we want to generate a proper sine wave, we need a higher sampling rate.

To make use of the DAC's resolution, we need to choose the sampling rate to be as high as possible. There is an upper limit, however. Our sampling rate is primarily limited by the I2C transmission speed. If we want to output a sine signal with a high frequency, we need make trade-offs for the resolution. We can only output a few samples per period, as the period time is very short. If the frequency is low, on the other hand, we are able to achieve a higher resolution. At very low frequencies, the DAC's resolution becomes the limiting factor. This is however, not anything we should worry about in this project.

In Practice: Correct Timing matters

Enough theory, let's get started. To generate the sine wave with a specific frequency, we need to make sure to output the samples with the correct timing. Before we do that, however, we should first determine the highest possible sample rate. This is what we are going to start with.

Determining the Minimum Sample Time

The easiest way to determine the maximum sample rate is to just try to output as many samples as possible and measure the time required per sample. The code below outputs 10 samples in a loop and determines the average time this takes. For accurate measurements, we would need to know exactly what commands we are going to execute in which order in our final code. This is nothing that we could do beforehand, however, we can approximate the required time and later add some extra time to the measurement to be safe. Using a loop to output samples from a buffer should be close enough to our final program to provide us with such an approximation.

#include <Adafruit_MCP4725.h>

Adafruit_MCP4725 dac;
const int address = 0x60;
unsigned short values[10] = {};

void setup() {
  // Setup DAC
  dac.begin(address);

  // Measure time needed to write a value
  long start_time = micros();
  for(int i = 0; i < 10; i++) {
    dac.setVoltage(values[i], false);  
  }
  long end_time = micros();

  // Send value to computer
  Serial.begin(9600);
  Serial.print("Microseconds per Iteration: ");
  Serial.println((end_time-start_time)/10);
}

void loop() {
}

I measured 157 us per iteration and you should get a similar value if you're using an Arduino Uno. To be sure, let's work with 200 us per iteration. This allows us to achieve a sampling rate of 5000 samples per second. If you're using a different Arduino board then the Arduino Uno or a different version of the libraries you might get different result. In this case just adjust the value in my code to your own measurement.

The Speed of Sine Calculations

To output a sine wave, we need to generate the respective samples for it. For this we are going to use the sin function. There is however a small issue: the Arduino is not very fast when it comes to floating-point arithmetic. Try replacing values[i] with sin(i) in the code we just used to determine the maximum sampling rate. If you do so, you can see that the required time per sample increases by around 100 us. The calculation for the real values is a bit more complex and would increase the time to around 350 us.

The good thing is, that a sine wave is not a random sequence of samples. Each period is the same. This means, that if we set the sampling frequency to be a multiple of the sine wave frequency, we can output the same data in each period. We can precalculate this data once we know the desired frequency and store it in an array. This way our output loop can just read the values from the array and sending it to the DAC with the correct timing.

Precalculating the Samples

If we use our maximum sample rate of 5000 samples per second, we can output up to 100 samples in each period of a 50 Hz sine wave. This is the number of samples we need to precalculate and put in our buffer. To perform the actual calculation, we can map the 100 steps to numbers between \(0\) and \(2 \cdot \pi\) and put the result into the sin function. sin always returns a number between -1 and 1. For outputting this result with the DAC we need to transform it into the range between 0 and 4095. Our new midpoint is 2048 with corresponds to 2.5 V. As we cannot output the full 5V with the DAC we need to make a compromise and go with 4.989V instead. This corresponds to 4095 which is our maximum value. For the sake of symmetry, we should then use 1 as our lowest value. This corresponds to 1.22 mV. If we take care of all this, we end up with the following piece of code to precalculate our samples:

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

We are not done yet. For our loop we always need to know the number of samples per period. As already said, for a 50 Hz sine wave and a sampling rate of 5000 samples per second we end up with a maximum of 100 samples per period. To calculate this number for other frequencies as well, we can simply divide the sampling frequency by the sine wave frequency:
\(n_{steps} = {f_{samp} \over f_{sine}}\)

There is still an issue, however. To be able to use our buffer with precalculated values, the sampling frequency needs to be a true multiple of the sine wave frequency. Additionally, it would be good to ensure that certain characteristic point of the sine wave are always outputted. We should at least ensure this for the maximum and minimum. I additionally included the null passes into this set of characteristic point. This means, we need at least 4 samples per period. As show in the image below, we can then go on and further divide the sine wave and add more samples. The number of steps, however, always needs to be a multiple of four. Sine wave with marked characteristic points

For 50 Hz everything works perfectly fine. For a value like 21.7 Hz, however, this is different. Our sampling frequency now needs to be a multiple of 21.7 Hz. The maximum possible sampling frequency to achieve this with our DAC is 4991 Hz. This would require 230 steps per period, however. 230 is not a multiple of 4. We thus need to lower the sampling frequency a bit more to make the number of samples per period a multiple of 4. A possible value would be 4947.6 Hz which result in 228 steps per period.

Programmatically, this can solved as following:

// 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;

The code makes use of the fact, that the remainder of a division is discarded when working with integers. By first dividing by 4 and then multiplying by 4 we get the highest number of possible samples per period that is a multiple of 4. The additional checks ensure that the number of steps is never below 4 and never bigger than the size of our buffer. This limits the maximum frequency of our AC source to 1.25 kHz. For this high frequency, however, we only get the minimum 4 samples per period and can only differentiate between 3 different voltage values. For lower frequencies we get a much better resolution. This is exactly what we wanted to achieve.

To enable us to ensure the correct timing, the code also uses the determined number of samples per period to calculate the real sampling frequency. To be more specific it calculates the time that needs to pass before the next sample should be outputted. In our output loop, we are going to use this value to wait correspondingly long. This allows us to enforce correct timing and thus match the desired frequency.

Putting Everything Together

Let's put everything together. The first step is to initialize the DAC. After that we need to request the desired frequency from the user. This can be done using Serial.parseFloat(). To allow our AC source to later be used without a computer attached to it, I also added a bit of code that stores the frequency in the Arduino's EEPROM. The EEPROM is a small storage area whose content is retained even if the power is removed. It is often used to store configuration values like the desired frequency in our case. If the user does not enter a number within 10 s the program uses the stored frequency. To change the frequency, just press the reset button on the Arduino Uno and it asks you to enter the frequency again.

Once we know the frequency, a suitable sampling rate is determined and the samples are precalculated. In the loop procedure we then output these samples. If we reach the end of our sample buffer, we just start over at the beginning. To ensure the correct timing, an empty while loop is used that uses the micros function to check whether the time until outputting the next sample has already passed. After we have waited long enough, we increment the start time for next iteration. It is crucial that we don't simply write start_time = micros() as time goes on between the two calls to micros. This time would be lost for us, and we would thus output the samples too slow. Additionally, the micros function has only an accuracy of 4 us. In some cases we won't be able to exactly match the required waiting time with our loop. As we always increment the start_time by the amount of microseconds we should have waited and not time we actually waited, the wait time in the next iteration is a bit shorter, in case we wait a bit too long. This way the remaining timing errors don't add up and are thus not noticeable.

With that being said, here is the complete code:

#include <Adafruit_MCP4725.h>
#include <EEPROM.h>

// DAC
const int I2C_ADDR = 0x60;
Adafruit_MCP4725 dac;

// Time needed for each iteration
const int usPerCommand = 200;

// 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 and DAC
void setup() {
  // Init DAC
  dac.begin(I2C_ADDR);
  
  // 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 (1 - 4095)
  for(int i = 0; i < steps; i++) {
    voltages[i] = sin(i*3.14*2/steps) * 2047 + 2048;
  }

  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() {
  dac.setVoltage(voltages[current_step], false);
  current_step++;
  if(current_step >= steps) current_step = 0;

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

The Result

To be honest, the code is a bit more complex this time, but don't worry if you don't understand it fully. Once you have uploaded the program to the Arduino, you can see what it does. It welcomes us with a prompt to enter the frequency in the serial monitor. Just enter your desired frequency. The maximum frequency is 1250 Hz. If you don't have an oscilloscope, you can also choose a very slow frequency like 0.01 Hz and check the output with a multimeter.

Frequency prompt in serial monitor

After entering the frequency, the program outputs the calculated number of samples per period and the time to wait before the next sample and starts outputting the sine wave signal. The image below shows sine wave signal measured with my oscilloscope.

Measured sine wave signal

The signal has a peak-to-peak voltage of 5 V, an amplitude of 2.5 V and an RMS voltage of 1.77 V. The maximum current is limited to a few milliamperes, not only by the voltage divider, but also by the DAC whose output becomes unstable as soon as a higher current is drawn.

Although these limits are pretty tight, this AC source would already allow us to experiment with some simple rectifier circuits. We will look at how to increase this limits in one of the future parts of this project. First, however, I will show you some more techniques to generate a sine wave with the Arduino Uno.

Previous Post Next Post