Lab 7b: GPIO DAC Interface
In Lab 7b, we will write Python programs to interface to the
digital-to-analog converter (DAC) we built in Lab
7a. After hooking up the DAC to the Raspberry Pi, we'll test the
programming interface in stages, ultimately creating our own wacky
waveforms.
The reason we used FETs in the DAC circuit is that the output
voltages of the Pi's GPIO pins are unreliable/unknown (nominally around
3.3 V), and could conceivably vary from pin-to-pin. The FETs let the
GPIO voltages simply switch in a well-defined 5 V. The
GPIO pins also drive the LEDs directly, so don't be surprised if some light
up before the power supply to the breadboard is turned on.
The Actual Lab
Some of the instructions below are less than fully descriptive. You are
left to figure some things out on your own. As usual, consult the Tips to potentially save some time.
Python Code for GPIO
This lab asks that you create a variety of programs—usually
building from a previous step—to progressively add functionality to
your DAC interface. Don't just keep modifying the same code, or you lose
the earlier versions. At each step, copy the latest code to a new file and
edit the new one for the new capability.
You are expected to know how to create Python programs, make them
executable, and accept command line arguments (see Lab 3 for a refresher). The key new piece is
the manipulation of the GPIO pins. The core GPIO capability is illustrated
in the program below.
#!/usr/bin/env python
import RPi.GPIO as GPIO
GPIO.setwarnings(False) # eliminates warning message
GPIO.setmode(GPIO.BCM) # use the BCM pin labeling scheme
GPIO.setup(21,GPIO.OUT) # set GPIO21 (pin 40) as output
GPIO.output(21,GPI.HIGH) # turn GPIO21 ON
GPIO.output(21,GPI.LOW) # turn GPIO21 OFF
Other variants are possible, and will be introduced below.
Lab Procedure
- Connect the Raspberry Pi GPIO to your DAC breadboard using
male-to-female ribbon cable. You need 8 GPIOxx pins plus one ground.
Think first about making life easy later. By this, I mean that you
will be writing 8-bit values to the GPIO, and this goes much more smoothly
if choosing sequential GPIOxx numbers. They may not map to sequential
pins, but will make coding much easier. It does not much matter
which 8 sequential pins you select, or the byte order (whether the most
significant bit—MSB— is at the high or low end). But a range
is nice.
- Try a test program to send a fixed value between 0 and 255 (expressed
in the program as decimal or hex—0x5a, for example) to the GPIO. See that the
LEDs on the inputs light up in the correct pattern (hex is more immediately
interpretable in this sense). See the tip on looping
across 8 bits. Once you accomplish
this feat, you are home-free. It may take you some time to get this right.
- Now create a new program that will accept an integer command-line
argument (between 0 and 255) and present this single value to the GPIO
pins. Once this is running, send a variety of values and verify that
the patterns are correct. One of the tips suggests
a way to make your program flexible enough to read either decimal or
hex from the command line.
- Use your new program to verify the DAC operation using a DVM.
Sending 255 (or 0xff) should yield the full-scale voltage.
Take this opportunity to adjust the pot to give you the desired full-scale
voltage (and see related tip). See that you get
appropriate output voltages depending on the integer value you request
(try a good variety).
- Now make a third program that takes a floating point argument, checks
that it is within the valid range (exiting if not), converts to the appropriate
integer value, and sends this to the DAC. The goal is to get the DAC to
put out the voltage you ask for. It may not be exact, as the
output is confined to a discrete number of possibilities. But you can
check that you are within a single step-size.
- Make a fourth program to cycle through all integers (0 to 255),
and repeat—in such a way as to make a saw-tooth ramp. Watch the
output on the oscilloscope, triggering on a negative slope to catch the
sharp drop. Triggering of the scope is likely to be jumpy, due to spikes
in your waveform, which we'll fix later. If the slow-rising side is not
perfectly uniform/straight, then you may have a DAC problem (resistor
values, bad/mis-placed FET, etc.). Fix it up if this is the case.
It could also be a mis-routing of the 8 parallel bits to your FET inputs.
In fact, it may be instructive to switch two inputs to see what shape
results in the waveform. Could get funky. Once the ramp is
regular (aside from spikes), note the period, and calculate the time per
step/update (also being aware that each step requires the writing of 8
bits). Check the amplitude and make sure it's full-scale. Zoom in to the
bottom of the ramp to see individual steps, and capture an image of this.
- We will now explore and optimize the bit writing sequence in forming
the ramp. In order to facilitate understanding, probe three things on the
scope: the DAC output (ramp), the least significant bit (LSB) from the Pi,
and the the bit four steps above the LSB (bit 5; skip 3).
- Arrange the writing of each new value to start with the LSB and
proceed to the MSB. Note the direction and approximate magnitude of spikes
on the ramp. Figure out why this would happen, if updating, say
00111111 to 01000000 by sequentially updating digits
from the right (note: the slew rate limitations of the op-amp may limit the
depth of the spikes). Now zoom in the time base to resolve the LSB digital
input pattern (and why is it basically a regular square wave? think even
and odd). Note where the 5th bit transitions occur relative to the LSB
transitions: are they aligned/coincident or offset? It may help your
understanding if you also probe other bits to see when they change relative to
the LSB pattern.
- Now flip the order of writes to go backwards: MSB first, proceeding
to the LSB. Note the tip on reversing the range so the
program can remain largely the same. Repeat the tasks in the previous step
for this arrangement.
- Now the real deal: we will write the GPIO in a single command, by
sending a list of BCM numbers (pins) and a list of values they are to take
on. Example syntax looks like:
writelist =
[0]*8 #
makes [0,0,0,0,0,0,0,0]
for bcm_num in range(bcm_low,bcm_low+8):
val = (intval >> (bcm_num - bcm_low)) & 0x01
writelist[bcm_num - bcm_low] =
val #
populate list with values to write
GPIO.output(range(bcm_low,bcm_low+8),writelist) # write all values in one
command
Once this works, repeat the tasks from the previous two steps. Of
critical importance is the relative timing of the digital outputs from the
Pi. And do the spikes look better? Is the overall period of the ramp
different (e.g., 8 times faster)?
- When the ramp is running, trigger on the negative-going (fast) side,
and expand the time scale until you can measure the slew rate of the
op-amp as it goes from the highest voltage to the lowest. Express in volts per microsecond, and compare to the specification
for the op-amp. It is important to do this before hooking up a filter
(next step)
- To eliminate spikes (which are due to a momentarily
indeterminate state as the GPIO/DAC switches from one value to
another), first look at the step size. Figuring you want to preserve some
semblance of this feature,
but eliminate spikes, pick a resistor/capacitor combination whose RC time
constant is a few times shorter than the step size. Use a resistor at
least
100 Ω (preferably higher) to keep the op-amp from having to
drive too much current, and select an appropriate associated capacitor in a
low-pass arrangement (R between output and scope and C from scope and
ground). This should eliminate spikes and keep the steps, allowing for
more robust triggering. You can think of the low pass filter as a voltage
divider, with the lower leg (capacitor) acting as a resistor that is small
for high frequencies, becoming arbitrarily large for low frequencies.
- Now that you have a smooth ramp waveform running, and stable
triggering, load down the Pi to see what happens. If we were running a
graphical interface, moving the mouse, dragging a window around, or
starting a large application would be good choices. But in our simple
interface, we will make a little program to keep the Pi busy for a few
seconds. Here is an example that spends two seconds writing to the screen
(in a way that does not "kill" the screen/history with a hundred thousand
lines; the \r is the key, requiring writing to stdout
in order to function; the flush() forces an update for each
output line, keeping the display busy):
#!/usr/bin/env python
import time
import sys
beg = time.time()
now = beg
line = 0
while (now - beg < 2.0): # run for 2 seconds
now = time.time()
line += 1
strn = "Printing line number %d\r" % line
sys.stdout.write(strn)
sys.stdout.flush()
sys.stdout.write('\n') # make a new line for the prompt
What do you notice happening when the Pi is busy? Why does this happen?
It can also be interesting to run the program several times
without the ramp running to see how many lines get printed when
the ramp generation is not consuming resources.
- Now be creative. Make a new program that
puts out an "interesting" waveform of your
choosing/design—;something a function generator cannot do.
But think it out well enough in advance that you can successfully program
the wave function in Python. Math is good. Sure, you could draw a row
of houses, but this is hard to describe in a program (though the more
ambitious may try—;can even try a chimney!). Anyway, come up with
something you're happy to look upon.
- Keep your programs separate rather than continually modifying the same
one. This way you can include listings of each in your write-up, and also
re-use them if necessary.
- Select your 8 GPIO pins to have sequential BCM numbers. Then you can
put the setup() into a loop, like:
for bcm_num in range(bcm_low,bcm_low+8): # hits 8 values,
starting at bcm_low (user-defined)
GPIO.setup(bcm_num,GPIO.OUT)
# one line gets them all
Similarly, writing the bits can be done in a loop.
- Applying a bandwidth limit (e.g., 20 MHz) to the scope will clean
up your trace without compromising your ability to see individual steps.
Look for the 20 MHz button on the channel (vertical) menu, where you
would select AC or DC coupling, for instance.
- Looping backwards through BCM numbers is as simple as
range(bcm_low+7,bcm_low-1,-1), which will produce 8 values
stopping at bcm_low (the "end" value in the range()
function is not included in the list; try different range()
commands in an interactive Python session to cement your understanding).
- Values can also be specified for output within a loop:
for bcm_num in range(bcm_low,bcm_low+8):
out_val = (intval >> (bcm_num - bcm_low)) & 0x01
GPIO.output(bcm_num,out_val)
where intval is the integer value (0–255) to be exported
to the GPIO pins. This form assumes the bcm_low pin is
associated with the least significant bit (LSB). The construction would
change for a different byte order. Don't use this blindly:
understand exactly how it works (the >> and &
operations, particularly).
- Both hex and decimal input can be useful, but the int()
function needs to know what it's converting. So the following code will
allow a command-line argument to be either decimal or hex (defaulting to
zero):
input_str = '0'
if (len(sys.argv) > 1):
input_str = sys.argv[1]
if input_str.find('x') > -1:
intval = int(input_str,16)
else:
intval = int(input_str)
Now the presence of x in the input signals a hex number. So
0xaa will interpret to the same thing as 170, and
result in an alternating LED pattern.
- It is a good idea to make sure you are not sending an integer outside
of the range from 0 to 255, to avoid unexpected results.
- A multiple of 2.55 V for the full scale makes the
stepsize a convenient multiple of 10 mV.
Lab 7b Write-up
The write-up for Lab 7b is to be combined with a writeup of
Lab 7a. For this segment, include
program listings, sketches or captures of waveforms, and all
numbers/calculations and descriptions requested in the sections above.
Include example inputs/outputs of your programs. For example, if you
put in the floating point request for 1.357 volts, what voltage do you
read on the DVM. Give enough examples to be convincing.
Back to Physics 122 page