Code:
{{
Easy ADC
Jonathan "lonesock" Dummer
Sigma-Delta ADC is very easy with the propeller. This object shows a simple
implementation of a 2-channel ADC input. Here's how to wire it up:
Simplest ADC circuit:
In ----------+ <= common node, at threshold voltage
|
Out ---Rout--+---Rmeasure---* Voltage_to_measure
|
Gnd ----C----+
Using counter mode POS (or NEG) w/ feedback, the propeller makes Out-pin the opposite of In-pin,
while counting the number of clock cycles that A was high (or low for NEG mode). The threshold
for the input pin is halfway between VDD and Gnd, or ~1.6V. If the common node voltage is above the
threshold, the In pin will read high, and the propeller's counter will drive the Out pin Low, and
vice-versa, effectively pulling the common node to the threshold voltage. Over a given time
period, the percentage of time spent pulling the common node voltage low is proportional to the
input Voltage.
The capacitor acts in conjunction with the resistors to act as a RC low-pass filter for the Out-
pin's output. The capacitor is needed, otherwise as soon as the Out pin is set, the voltage at
the common node changes instantaneously, with no chance for the counter to actually count anything.
So, the cap and resistor are a simple RC filter, smoothing out high-frequency noise, with the
corner-frequency of the filter at 1/Tau, where Tau is the time constant of the filter, which for
a RC filter is simply:
Tau = Rout*C
Various values of C seem to work fine, so I am recommending setting the C value as if you were using
the RC filter for a DAC, so Rout*C ~= 1 / typical_output_sample_frequency. NOTE: this recommendation
is subject to change!!
With a little bit of Kirchhoff's Current Law (KCL, a.k.a. "summing the current into a node should equal 0"),
and assuming steady state (so the capicitors act as an open circuit):
* The maximum input is hit when the Out pin is driven low 100% of the time: Voltage = 1.6 * (1 + Rmeasure / Rout)
* The minimum input is hit when the Out pin is driven high 100% of the time: Voltage = 1.6 * (1 - Rmeasure / Rout)
The maximum value coming out of the counter is simply the period over which you sample. For example,
with a 80 MHz system, running a sampling rate of 44.1 kHz means the sampling period is 1814 clocks.
The minimum value coming out of the counter is obviously 0, and the maximum possible value of the counter
is 1814 (if the In pin was high the whole time, the counter would add 1 to PHSx for each clock). This
is equivalent to about 10.8 bits. If you wanted the module to report a 16-bit number exactly, you
would need to make sure the sampling period was 65535 (or 1.22 kHz on a 80 MHz system). To make the
output of the system easier to use, I adjust the FRQx value so that period * FRQx = 2^31-1. That way,
after a full period is complete, I can shift right by 15 bits, yielding a 16-bit number. Note that
this does _NOT_ mean you are getting 16 significant bits, only that the reading are always scaled to
that range.
Things to note:
* you want large resistor values if you are trying to measure an unbuffered voltage source
- for example, if you want to measure the voltage on a capacitor...the circuit
will bleed current (V_measure_me - 1.6) / Rmeasure. If you are measuring the
output of an op-amp, the resistor values you choose are no big deal (other than
setting gain). Note that the larger the Resistors, the noiser the system can get.
* keep your traces as short as possible
* use non-adjascent pins for In and Out if possible (to reduce cross-talk)
* if possible use a low cog for pins P0..P15 (cog 0 is best, then 1, etc)
* if possible use a high cog for pins P16..P31 (cog 7 is best, then 6, etc.)
* there is no point summing then averaging N values...it's the same as using sample-rate / N
* averaging is still useful if you are smoothing a waveform
* restart the cog to change the sampling rate
* the PASM loop takes about 80 clocks, so don't try to go over clkfreq/80 Hz for the sample-rate
Here is a more complex version of the circuit, to add some features:
+-Rbias-+
| |
VDD -+---C---+ <= common node
|
In -----Rin--+---Rmeasure--- Voltage to measure
|
Out ----Rout-+
|
Gnd -----C---+
Rin does nothing as the input pin is high impedence, but can be used to switch the function
of the In and Out pins, changing the ADC's range. For example, if Rin = 0.5*Rout, switching pin
functions (Out is now the sense pin, and In is now the feedback pin) would yield a new range that
is 2x larger.
Note that if you have 2 capacitors to constant voltages (VDD and GND), the equivalent capacitance
of the system looks like both capacitors are in parallel, or 2*C (2 caps are very useful since
noise on either VDD or Gnd can affect the threshold voltage).
Rbias can be left out of the circuit as well, but since the sum of the current coming into the
node is 0, you can use Rbias to inject a constant current into the node (since VDD is constant,
and the threshold voltage is constant (thanks to the prop's counter) we are basically offsetting
the center point). As a useful example, if Rbias == Rmeasure, you effectively shifted the input
range to be +- 1.6 * Rmeasure/Rout, centered around 0 V! You can verify this using KCL. The more
intuitive way to see this, at least for me, is to note that Rmeasure & Rbias form a resistive divider
between Vin and VDD such that the common node is exactly 1/2 way between the the two, i.e. when the
input voltage is 0 then the common node voltage would be at VDD/2, which is already the threshold
voltage, and the counter doesn't have to sink or source any current. If you want to push the center
point around you can change Rbias, and/or connect it to Gnd instead of VDD. For extreme flexibility,
you could even use a potentiometer with one leg each to Gnd and VDD, or even another output pin via
a RC filter DAC connected via a resistor to the common node, to affect fine tuning electronically.
}}
VAR
long control
byte cog
PUB start_freq( Ain, Aout, Bin, Bout, SampleHz )
'' helper function, to convert Sample Hertz to (rounded) period in system clocks
return start_period( Ain, Aout, Bin, Bout, (clkfreq + (SampleHz >> 1)) / SampleHz )
PUB start_period( Ain, Aout, Bin, Bout, PeriodClocks )
'' initialize the sigma-delta ADC cog
stop
drive_mask := (|<Aout) + (|<Bout)
period := PeriodClocks #> 10 ' 80 for non-filtered, 144 for filtered
ctra_val := constant(%01001 << 26) | (Aout << 9) | (Ain)
ctrb_val := constant(%01001 << 26) | (Bout << 9) | (Bin)
frq_val := POSX / (period - 4) ' -4 because I am resetting PHSx immediately after reading it, so that instruction costs me 4 clocks
control~~
return cog := cognew( @EasyADC_pasm, @control ) + 1
PUB fill_buffer( buffer_ptr, num_samples, filter )
'' fill a buffer with alternating channel A & B 16-bit unsigned samples
'' period must be 80 clocks or more
repeat
while control
if filter AND (period > 143)
' THE FILTERED VERSION IS SLOWER! period must be 144 clocks or more
control := -((num_samples << 16) + buffer_ptr)
else
' unfiltered...the minimum period was enforced at start
control := (num_samples << 16) + buffer_ptr
PUB wait_till_full
'' you can call fill_beffer earlier, then just spin here until the buffer is full
repeat
while control
PUB stop
'' kill the cog
if cog
cogstop( cog~ - 1 )
DAT
ORG 0
EasyADC_pasm ' initialize
mov ctra, ctra_val
mov frqa, frq_val
mov ctrb, ctrb_val
mov frqb, frq_val
mov dira, drive_mask
mov buf_ptr, #0
wrlong buf_ptr, par
' spin until I get a non-zero pointer
get_buffer rdlong buf_ptr, par wz
mov timeout, cnt
if_z jmp #get_buffer
' prime the pump:
' - set initial old* values to be 0 and max, so the 1st value will be the median
' - start my timer and clear the PHSx values to 0 (note the offset relative to when cnt was sampled)
mov phsa, #0
mov oldA1, #0
mov phsb, #0
' now I have some time to waste, while the 1st sample is gathered
neg oldA2, #1
mov oldB1, #0
neg oldB2, #1
add timeout, period
' if negative, we want filtering
abs buf_ptr, buf_ptr wc ' C flag holds if I want to filter
' the high 16 bits of the pointer are actually the sample count
' (note: this won't affect the pointer...the upper bits are ignored (anything above 32kB is wrapped)
mov buf_cnt, buf_ptr
shr buf_cnt, #16
sample_loop waitcnt timeout, period
' grab the new values and reset the counters (saves 2 instructions and 2 variables, costs 4 clocks per counter)
mov A, phsa
mov phsa, #0
mov B, phsb
mov phsb, #0
' should I skip the brilliant median filter? (courtesy of Dave Hein...thanks!)
if_nc jmp #:skip_median_filter
' Store the original value of A
mov filter0, A
' Get the minimum of each of the 3 possible pairs ( A & oldA1, A & oldA2, oldA1 & oldA2 )
mov filter1, oldA1
max filter1, oldA2
max oldA2, A ' this modifies oldA2, but it will be overwritten soon anyway
max A, oldA1 ' A is modified (will be the median filtered value)
' find the maximum of those 3 minima
min A, oldA2
min A, filter1
' update my tracking variables
mov oldA2, oldA1
mov oldA1, filter0
' repeat for channel B
mov filter0, B
mov filter1, oldB1
max filter1, oldB2
max oldB2, B
max B, oldB1
min B, oldB2
min B, filter1
mov oldB2, oldB1
mov oldB1, filter0
:skip_median_filter
' scale the values
shr A, #15
shr B, #15
{ limit the values (I shouldn't need this!)
max A, limitFFFF
max B, limitFFFF
'}
' write the value (putting A in the high 2 bytes of the long...Little-Endian)
shl A, #16
or A, B
wrlong A, buf_ptr
add buf_ptr, #4
' keep going?
djnz buf_cnt, #sample_loop
' done...signal done-ness
wrlong buf_cnt, par
jmp #get_buffer
{===== PASM Parameters and 'constants' =====}
drive_mask long 0
ctra_val long 0
ctrb_val long 0
period long 0
frq_val long 0
limitFFFF long $FFFF
{===== PASM scratch variables =====}
timeout res 1
buf_ptr res 1
buf_cnt res 1
A res 1
B res 1
filter0 res 1
filter1 res 1
oldA1 res 1
oldA2 res 1
oldB1 res 1
oldB2 res 1
Bookmarks