This Instructable shows how to generate super fast analog voltage changes from an Arduino and a simple resistor and capacitor pair. One application where this is useful is in generating graphics on an oscilloscope. There are several other projects that have done this. Johngineer shows a simple Christmas tree using pulse width modulation (PWM). Others have improved upon that project by using a resistor ladder or using a dedicated digital-to-analog converter chip.
Using PWM causes a lot of flicker, while using a resistor ladder or a digital-to-analog converter requires more output pins and components that may not be readily available. The circuit I use is the same dead simple resistor and capacitor pair as used in the Christmas tree demo, but operates with significantly less flicker.
First, I will guide you through the process of building the circuit. Then I will teach you how to add your own image. Finally, I will introduce the theory on what makes it faster.
If you liked this Instructable, please consider voting for it! :)
Teachers! Did you use this instructable in your classroom?
Add a Teacher Note to share how you incorporated it into your lesson.
Step 1: Building the Circuit
To build the circuit, you will need the following:
a) An Arduino based on the Atmel 16MHz ATmega328P, such as an Arduino Uno or Arduino Nano.
b) Two resistors of value R that is at least 150Ω.
c) Two capacitors of value C such that C = 0.0015 / R, examples:
- R = 150Ω and C = 10µ
- R = 1.5kΩ and C = 1µ
- R = 15kΩ and C = 100nF
- R = 150kΩ and C = 10nF
The reasons for choosing these values are two fold. Primarily, we want to keep the current on the pins of the Arduino below the maximum rated current of 40mA. Using a value of 150Ω limits the current to 30mA when used with the Arduino supply voltage of 5V. Larger values of R will decrease the current and are therefore acceptable.
The second constraint is that we want to keep the time constant, which is the product of R and C, equal to about 1.5ms. The software has been specifically tuned for this time constant. While it is possible to adjust the values of R and C in the software, there is a narrow range around which it will work, so pick components as close to the suggested ratio as possible.
A more thorough explanation on why the RC constant is important will be given in the theory section, after I have shown you how to assemble the demonstration circuit.
Step 2: Setting Up the Oscilloscope
The demonstration requires an oscilloscope set to X/Y mode. The test leads need to be hooked up as shown in the schematics. Your oscilloscope will differ from mine, but I will walk through the necessary steps to set up X/Y mode on my unit:
a) Set the horizontal sweep to be controlled by Channel B (the X axis).
b) Set the oscilloscope to dual channel mode.
c) Set the volts/div on both channels so that it can display voltages from 0V to 5V. I set mine to 0.5V/div.
d) Set the coupling mode to DC on both channels.
e) Adjust the position of X and Y so that the dot is in the lower left corner of the screen when the Arduino is powered off.
Step 3: Download and Run the Software
Download the software from the Fast Vector Display For Arduino repository. The software is licensed under the GNU Affero Public License v3 and can be freely used and modified under the terms of that license.
Open the "fast-vector-display-arduino.ino" file in the Arduino IDE and upload to your Arduino. Momentarily, you will see an "Happy New Year" animation on your oscilloscope screen.
I developed this project as a personal hackaton in the weeks leading up to Christmas, so there is a Christmas and New Year themed message you can see by modifying the PATTERN variable in the code.
Step 4: Create Your Own Custom Drawing
If you wish to create your own drawing, you can paste point coordinates into the Arduino sketch on the line that defines USER_PATTERN.
I found that Inkscape is a pretty good tool for making a custom drawing:
- Create text using a large, bold font such as Impact.
- Select the text object and select "Object to Path" from the "Path" menu.
- Select individual letters and overlap them to make a connected shape
- Select "Union" from the "Path" menu to combine them into a single curve.
- If there are holes in any letters, cut a small notch by drawing a rectangle with the rectangle tool and subtract it from the contour using the "Difference" tool.
- Double-click the path to show the nodes.
- Rectangle select all nodes and click the "Make selected nodes corner" tool.
- Save the SVG file.
The important thing is that your drawing should have a single closed path and no holes. Make sure your design has fewer than about 130 points.
Step 5: Paste the Coordinates From the SVG File Into the Arduino IDE
- Open the SVG file and copy out the coordinates. These will be embedded in the "path" element. The first pair of coordinates can be ignored; replace them with 0,0.
- Paste the coordinates into the Arduino sketch inside the brackets right after "#define USER_PATTERN".
- Replace all the spaces with commas, otherwise you will get a compile error. The "Replace & Find" tool may be helpful.
- Compile and run!
- If you have problems, watch the serial console for any errors. In particular, you will see messages if your pattern has too many points for the internal buffer. In such cases, the image will exhibit excessive flicker.
Step 6: Understand Why PWM Is So Slow
To begin, let's review the behavior of a capacitor as it is charging.
A capacitor that is connected to a voltage source Vcc will ramp up its voltage according to an exponential curve. This curve is asymptotic, meaning it will slow down as it approaches the target voltage. For all practical purposes, the voltage is "close enough" after 5 RC seconds. The RC is called the "time constant". As we saw earlier, it is the product of the values of the resistor and capacitor in your circuit. The problem is that 5 RC is a rather long time for updating each point in a graphics display. This leads to a lot of flicker!
When we use pulse width modulation (PWM) to charge a capacitor, we are no better off. With PWM the voltage switches rapidly between 0V and 5V. In practice, this means we rapidly alternate between pushing charge into the capacitor and pulling a little bit of it right out again -- this push and pull is rather like trying to run a marathon by taking a big step forwards and then a little step backwards over and over again.
When you average it all out, the behavior of charging a capacitor using PWM is exactly the same as if you had used a steady voltage of Vpwm to charge the capacitor. It still takes about 5 RC seconds for us to get "close enough" to the desired voltage.
Step 7: Get From a to B, a Tad Bit Faster.
Suppose we have a capacitor that is already charged up to Va. Suppose we use analogWrite() to write out the new value of b. What is the minimum amount of time you have to wait for the voltage Vb to be attained?
If you guessed 5 RC seconds, that is great! By waiting 5 RC seconds, the capacitor is going to be charged to very nearly Vb. But if we want, we can actually wait a tad bit less.
Look at the charge curve. You see, the capacitor already was at Va when we began. This means that we do not have to wait the time t_a. We would only have to if we were charging the capacitor from zero.
So by not waiting that time, we see an improvement. The time t_ab is actually a bit shorter than 5 RC.
But hold on, we can do so much better! Look at all that space above v_b. That is the difference between Vcc, the maximum voltage available to us, and the Vb we intend to reach. Can you see how that extra voltage can help us get where we want to go much faster?
Step 8: Get From a to B, With a Turbo Charger!
That's right. Instead of using PWM at the target voltage V_b, we hold it at a steady Vcc for a much, much shorter period of time. I call this the Turbo Charger method and it gets us where we want to go really, really fast! After the time delay (which we must compute), we slam on the brakes by switching over to PWM at V_b. This keeps the voltage from overshooting the target.
With this method, it is possible to change the voltage in the capacitor from V_a to V_b in a fraction of the time than using just PWM. This is how you get places, baby!
Step 9: Understand the Code
A picture is worth a thousand words, so the diagram shows the data and the operations that are performed in the code. From left to right:
The graphics data is stored in PROGMEM (that is, flash memory) as a list of points.
- Any combination of translation, scaling and rotation operations are combined into an affine transformation matrix. This is done once at the start of each animation frame.
Points are read one-by-one from graphics data and are each multiplied by the stored transformation matrix.
The transformed points are fed through a scissoring algorithm that crops any points outside of the visible area.
Using an RC delay lookup table, the points are converted into driving voltages and time delays. The RC delay lookup table is stored in EEPROM and can be re-used for multiple runs of the code. At startup, the RC lookup table is checked for accuracy and any incorrect values are updated. The use of EEPROM saves valuable RAM memory.
The driving voltages and delays are written to the inactive frame in the frame buffer. The frame buffer contains space for an active frame and an inactive frame. Once a complete frame is written, the inactive frame is made active.
An interrupt service routine continually re-draws the picture by reading of voltage values and delays from the active frame buffer. Based on those values, it adjust the duty-cycles of the output pins. Timer 1 is used for measuring the time delay down to a few nanoseconds of precision, while timer 2 is used for controlling the duty cycle of pins.
The pin with the largest change in voltage is always "turbo charged" with a duty-cycle of zero or 100%, providing the fastest charge or discharge time. The pin with a lesser change in voltage is driven with a duty cycle chosen to match the transition time of the first pin—this time matching is important to ensure that lines are drawn straight on the oscilloscope.
Step 10: With Great Speed, Comes Great Responsibility.
Since this method is so much faster than PWM, why doesn't analogWrite() use it? Well, because using just PWM is good enough for most programs and is a lot more forgiving. The "Turbo Charger" method, however, requires careful coding and is only suitable for specific cases:
- It is extremely sensitive to timing. Once we reach the target voltage level, the driving pin must immediately be switched into regular PWM mode in order to avoid overshooting the target voltage.
- It requires knowledge of the RC constant, so these values must be entered beforehand. With incorrect values, the timing will be wrong and the voltages will be incorrect. With regular PWM, there is a guarantee that you will settle onto the correct voltage after some time, even if the RC constant is not known.
- Computing the precise time interval for charging the capacitor requires logarithmic equations which are too slow for real-time computation on the Arduino. These must be pre-computed before each animation frame and cached in memory somewhere.
- Programs dealing with this method must contend with the fact that the delays are very non-linear (they are, in fact, exponential). Target voltages near Vcc or GND will take many orders of magnitude longer to reach than voltages near the midpoint.
To overcome these limitations, my vector graphics code does the following things:
- It uses Timer 1 at 16kHz and an interrupt service routine for precise output manipulation and timing.
- It requires a specific value of RC time constant to be used, limiting the choices of the capacitor and resistor values.
- It stores the time delays for all the points in an animation frame in a memory buffer. This means the routine that computes the time delays runs at a much slower rate than the interrupt service routine that updates the output pins. Any given frame might be painted several dozen times before a new set of delays for the next frame is ready to be used.
- The use of a memory buffer puts a constraint on the number of points that can be drawn per frame. I employ a space efficient encoding to get the most out of the available RAM, but it is still limited to about 150 points. Beyond a hundred or so points or so, the display would start to flicker anyway, so it's a moot point!