EasyADC - call for feedback
lonesock
Posts: 917
Hi, All.
Here is a(nother) simple sigma-delta ADC 2 channel object. The main difference here is that the output for all sample rates is automatically scaled to a 16-bit number, and I tried to give some simple-to-follow theory about the circuit fundamentals.
I would really appreciate any feedback on the writeup or code. Note that I intentionally avoided using the schematic symbols to keep the file simple ASCII.
thanks,
Jonathan
Edited:
* added fast median-of-3 filtering (thanks, Dave Hein!!)
* added Rbias, did easy then complex circuits, changed Tau recommendation again.
* to change the wording around Tau
* to incorporate some clarification, and changed to either frequency or period specification.
Here is a(nother) simple sigma-delta ADC 2 channel object. The main difference here is that the output for all sample rates is automatically scaled to a 16-bit number, and I tried to give some simple-to-follow theory about the circuit fundamentals.
I would really appreciate any feedback on the writeup or code. Note that I intentionally avoided using the schematic symbols to keep the file simple ASCII.
thanks,
Jonathan
Edited:
* added fast median-of-3 filtering (thanks, Dave Hein!!)
* added Rbias, did easy then complex circuits, changed Tau recommendation again.
* to change the wording around Tau
* to incorporate some clarification, and changed to either frequency or period specification.
{{ 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
spin
13K
Comments
I don't understand this comment:
* you want large resistor values if you are trying to measure low impedence sources
Can you clarify that?
Nice to see an explanation of tau. I still don't have a good feeling for the time constants at work...
BTW: Regarding min and max readings, I have a calculator here:
http://www.pulsedpower.net/Applets/Electronics/SigmaDeltaADC/SigmaDelta.html
I was playing with moch code trying to make up for the variable A/D timeings allowed.
@ Rayman: Oops, sorry about that. It was A) confusing, and wrong! I meant that if you are measuring the voltage coming off an op-amp or something, then using small resistor values is OK. But if you need to measure something where leaking current will affect the voltage, you should use larger resistors. The voltage at the common node is ~ 1.6V, so you will actually be sinking or sourcing some current with this ADC circuit: (V_measure_me - 1.6) / Rmeasure. In these cases, you may want to make Rmeasure large, like 1 Mohm or so, then adjust your other resistors and caps to match. I really like that calculator! I had found it before, then lost it, so thanks for the (now bookmarked) link!
@ Perry: Thanks! Your scope is actually why I finally wrote this up ;-) EasyADC is pretty simple to embed, but I actually want to add the option of triggering to EasyADC, as monitoring the values in PASM is much more feasible than in Spin. Let me know when you get back home, we can collaborate [8^)
@ David: Thanks, I'm glad it helped! Hopefully the recent updates helped more, rather than hindered.
Thanks for your feedback, more is still welcome! [8^)
Jonathan
tau = RC = 2^bits / clkfreq ----> note that the resolution is a factor.
That differs from your prescription of
tau = ~100 / clkfreq.
We agree for a level of around 7 bits. Suppose you are shooting for 11 bits. In a time interval of 100/clkfreq the summing node can slew the equivalent of 16 bits. Is that important?
I really don't have a handle of this and I'd like to hear reasoning or empirical experience that leads to one conclusion or another. A longer tau will also slow down the step response.
I'm sure that's all stuff you knew, it's just my reasoning for the recommendation. If I really wanted to do 1Msps, I'd probably pick a Tau that was only about 10 / clock_rate.
Jonathan
I suspect that the RC time constant, tau, is not low-pass filtering your input signal in the manner that you describe. The capacitors are sitting at a summing junction, and a suitable analogy would be an inverting op-amp with a capacitor from the inverting input to ground. To first order, the capacitor at virtual ground sees no voltage changes and does not enter into the equations for gain or frequency response. It does enter into the noise gain of the op-amp, but in the sigma-delta that is a different issue (jitter).
A separate point that I wanted to raise is that the input to the summing junction is a current. Voltage in series with a resistor is just one way to supply that current. The current might instead come from a photodiode, or a transistor collector, or an AD592 temperature sensor, or a charge displacement device like a piezo film tab, etc..
This is related to the tau issue. Suppose there happens to be a step change in current at the input. That might be a step change in the voltage at your resistor input, or a step change in light level falling on a photodiode. The response of the sigma delta is immediate. That is, if the ADC synchronously starts counting at the onset of the step, the count is correct for the new step value at all bit levels. (I'm currently working with a system where the prop controls the flash of a laser and also reads the photodiode response, so it can be synchronous in that manner.) On the other hand, if the ADC sampling is asynchronous, it will take one sampling period for the step to resolve, but that is a digital issue that has nothing to do with the input RC. My point again is that the choice of C does not affect the step or frequency response in the analog domain, pre-delta. The sigma in the prop is done by the summation.
I know, this does not resolve the issue of how to choose the capacitors!
Thanks for the feedback. I definitely need to think about this some more, but here's my current take on it:
* regarding the filtering, I was saying that the RC lowpass filter is essentially for the feedback Out pin. As such, it does limit the response speed of counter-driven "keep the common node at 1.65V", so I would think that if the input changes faster than circuit can keep up, I'd have an issue. I will definitely do some experimentation and simulation this coming weekend.
* regarding the input to the common node actually being a current, that is an excellent point! I was writing this assuming someone wished to measure voltage, but I should definitely include this. (I've used some piezos' current as a cheap electronic drum sensor before in a similar circuit...fun stuff!) Thanks!
Thanks for all the help & insight!
Jonathan
The blue line is an exaggerated graph of voltage fluctuations around the threshold associated with three different voltage inputs, 1/2, 3/4 and 7/8 of the power supply voltage Vdd. This assumes that your Rin = Rout, so that the full scale range is nominally 0 to 3.3 V. Feedback holds the Prop input pin close to its threshold, nominally 1.65V. The fluctuations are exaggerated and would normally be much smaller in amplitude, but they are in scale with one another.
On the left is the case where the summing node input is at Vdd/2, right at threshold, so the voltage across Rin is zero and it contributes no net current. The feedback pin flips with every clock cycle, and the voltage moves up and down accordingly, at clkfreq/2. I won't get quantitative about the amplitude right away, but suffice it to say that the amplitude easy to calculate from the current through Rout, the capacitance, and the clock period.
The fluctuations have a bit of exponential character, but in this situation a linear approximation will be very good. Also, noise will add a chaotic element to the real, observed pattern, but the average of 50% high and 50% low holds as if it were the ideal pattern. The model system has zero noise and a razor sharp threshold.
When the input voltage takes a step up to 3/4 * Vdd (red line in the middle segment of the graph), the feedback pin immediately goes into a cycle of 4, with 1 high clock period and 3 low clock periods, a base frequency now of clkfreq/4 instead of clkfreq/2. That comes from charge balance, the requirement that current coming into the summing node has to balance with current leaving. In this case both Rin and Rout enter into the calculation, and it turns out (not surprisingly) that the current when the feedback pin is high is 3 times the current when the feedback pin is low. The voltage cycle around threshold has a steep rise and a slow fall. The amplitude is higher than the previous case, but not by much (* 1.5 to be exact).
The third step goes up to 7/8 of Vdd. The analysis again comes out with a steep rise and a slow fall, due to resistor currents adding in one direction and subtracting in the other direction. It is a cycle of 8 with a base frequency of clkfreq/8, consisting of 1 high period and 7 low periods, and the amplitude is 7/4 of the amplitude when Vin = Vdd/2.
As Vin goes closer and closer to the limit at Vdd, the compensating fluctuations continue so long as feedback is capable of maintaining the charge balance. Frequency tends to zero (impossible to filter!) and amplitude goes to twice the value it had when Vin = Vdd/2. (The math is basic Ohm's law and I = C dV/dt).
The effect of increasing the value of the input capacitor is only to decrease the amplitude of the fluctuations around the threshold, as seen here...
The threshold is ideally razor sharp and detects the transitions no matter how closely they split on either side. The graph below shows a "small" capacitor with small RC on top versus a "big" capacitor with long RC on the bottom. Same timing, squashed amplitude. Conclusion: The input time constant has nothing to do with low-pass filtering the input signal.
It must be said also that in reality the pattern will be subject to dithering due to noise and the ADC result is an average ratio of high to low that comes out in the count over many cycles. The dithering may be a good thing up to a point.
Note that there is not a period where the feedback pattern has to "catch up" with the input step due to the time constant of either Rin or Rout with the input capacitor. Depending on where the step happens within one clock period, there may be a brief period of adjustment, but that will resolve within a few clock cycles. Other than that, the pattern associated with the new input level starts immediately, so you might say that the step response is on the same order as clkfreq. Of course, practically that is irrelevant, because nothing can get around the time required to accumulate enough counts for the desired resolution.
There are situations that can put the system in a state where it takes time to recover. Examples are: 1) taking the input voltage out of range, or 2) replacing the input resistor with a capacitor and trying to feed in an AC square wave (which takes it out of range because I = C dV/dt) or 3) making a sudden change in the value of C (which also takes it out of range, because I = V dC/dt).
I think the choice of capacitor comes down to second order effects. It has to be large enough to keep the fluctuations small around the threshold. There may be a wide optimum having to do with noise around the threshold. The totem pole input is biased in its linear region at threshold,so it will be generating power supply spikes. Non-ideal characteristics of the capacitor might be important, things like ESR and soakage.
I will immediately remove the portion about the speed of the "response to an input" regarding the capacitor value. However, I would still like to make some recommendation as to the minimum value of C (as that seems to be missing from most sigma-delta adc math I've seen, perhaps for good reason!). Do you have a recommendation for a value that will be large enough to keep the fluctuations small around the threshold? This will be a function of at least the clock speed of the counter (so people running at PLL1x would need to adjust their minimum values, for example), and the Rout value.
Just another quick thought...the larger the value of C, to more the threshold will jump around if there is noise on VDD or GND, right? So I'm guessing there's a "large large enough to keep the fluctuations small around the threshold" factor, but also a "small enough to minimize any effects of noise on the supply lines" factor.
Again, I really appreciate the feedback, Tracy! Thanks!!
Jonathan
Ray experimented with capacitor values from zero (+ parasitics) up to (his words, responding to Peter Verkaik, middle of page 3): "Peter, I tried using a really big C, and it didn't work very well... Individual samples all had values of 0, 50, or 100%. (This is with DC input). So, I think there must be some upper limit on RC as well, regardless of the input frequency."
That might have something to do with the less than ideal characteristics of really big C's, and the fact that the Prop threshold is less than razor sharp. Maybe there is small hysteresis due to internal wiring. Question up in the air.
For the lower limit of capacitor value, another factor may be how closely the signal has to approach to the full scale rails. The period between compensating pulses become longer as the signal approaches full scale, so a larger capacitor value (RC product) is required to keep the voltage within some delta of the threshold. For example, if the signal stays within a factor of 7/8 of the full scale, then the minimum base frequency that has to be filtered is 10 MHz, and the time constant RC should be >> 0.1µs. Suppose Rin=10k and Rout=10k, parallel equivalent = 5k. That makes RC >> 20pF. So a choice of 470 pF or 1 nF should be good. Probably NPO/COG. A corollary is that signals should be centered around the threshold if possible and avoid coming too close to full scale.
That is a less stringent requirement than the criterion that involves resolution. There the RC product should be chosen so that the summing node voltage changes at most by one bit during one clock tick, and comes down to RC = 2^n / clkfreq. For R=5k and n=11 bits and clkfreq = 80MHz, that would be C = 5 nF.
I'm not sure if either of those criteria stand on firm ground or are just rules of thumb.
I learned a lot from deSilva's comments in other threads. But, in that one, I still don't know for sure what he was trying to say (except that he was right all the time
So, in a politician-style issue dodge, maybe I'll change my writeup to say "Pick the value C so that if you want, you could use the circuit as a DAC with the RC low-pass filter", that way people can use the circuit for multiple purposes. [8^)
Jonathan
Thank you for that link to the old Post. I learned a lot from that.
Also to note: using what I have learned here from Lonesock, Tracy, and deSilva I have managed to get the poor mans Oscilloscope working on bread board with a 1.2MHz sample rate. This was accomplished using 0.001uf caps, with R1 = 5.6K and R2 = 11.2K, with a signal diode on the input, and a ground line on one side, a Vdd line on the other side of the line between R2 and the connector for the 'Probe'. works great this way, and is stable. I am going to modify my PerfBoard version of the Pore Mans Oscilloscope to reflect these changes and see how well it does. Of course it is limited to measuring positive signals.
Jonathan
P.S. At the risk of hijacking this thread, David, I'd love to help on the scope code, if you'd like...just let me know!
The only thing I am concerned about is the loop waiting for more to do. That should be wraped around a waitcnt as when doing nothing the cog will be rdlongs on memory as fast as it can affecting the memory bandwidth available for other tasks.
You can find the latest version at:
http://forums.parallax.com/showthread.php?130582-Poor-Man-s-Digital-Oscilloscope-
Perry
This code is poor practice.
It will use up the bandwidth of the memory buss doing nothing.
There should be a waitcnt in the loop at least about 1/2 the size of the expected interval
Phil's software radio does this in two places.
1 the quadrature code checks for frequency change
2 the output to the sound DAC
It will not go to shortwave frequencys as promised unless you wrap the offending code with waitcnts.
To note: either way if I succeed this will require 6 cogs (because of the limited number of counters available).
1 - as kuroneko points out, that memory access is available whether I choose to use it or not, it doesn't affect any other cog's hub access at all.
2 - I have found with both a DAC (single pin plus a RC low-pass) and sigma-delta ADC, that I get small glitches in the output / input when other cogs enter or exit any low-power sleep state. Try this: on a demo board, set up a simple sine-wave output on the headphones. Next, start up a dummy cog that does some dummy work (like a simple djnz loop), then does a waitcnt, where the waitcnt period is specified in a Hub variable. Listen to the output in the headphones as you dial that period around.
Assuming the scope isn't going to run on batteries, you _might_ want to consider minimizing the use of any low-power wait* commands.
Jonathan
PUB fill_buffer( buffer_ptr, num_samples, filter )
Note that the filtering method takes more time (144 clocks, to be precise, so ~ 0.55 MHz with the prop clock @ 80 MHz).
Jonathan
There seems to be some interaction between channels, the B channel is hooked to nothing.
Any explanations for the false signal on B?
Are your pins for B right next to the pins for A? You may be getting cross-talk...can you try it with another set of pins for B (or A), preferably a distance away from the other channel's pins?
Jonathan
these are my channels
I'll upload the latest version when I can get the output od the time difference correct.