Speed problem with "MCP3208 fast ADC 12 bit 8 channel" object
MJHanagan
Posts: 189
I am encountering a speed problem with a Microchip MCP3208 ADC and the MCP3208 fast ADC 12 bit 8 channel object (see http://obex.parallax.com/objects/224/) . According the documentation the assembly written routine is capable of making ADC measurements at a rate of 50k Hz per channel. I need to measure two channels so this should half the rate to 25k Hz per channel. I am using a REPEAT loop to measure the first channel then the second storing the results in two arrays. I am using a zero crossing detector signal on an input pin to provide synchronized timing during the first half of a 60 Hz line cycle (i.e. a synchronized 8.33 msec data collection period). In the code below I am simulating the zero cross signal using a generated pulse on an output pin which is tied to the zero cross input pin via a 5k resistor (I measure a perfect 60.00 Hz on the input pin).
When I run the code I am only getting 45 data measurements during the zero cross high input to time. If the 50k Hz value is correct then I should be getting about 208 measurements per channel during this period. There is likely to be some processing time to assess the status of the zero cross input pin and increment the data counter but I think these operations should be relatively fast even in SPIN.
Can anyone help explain why I am getting so few measurements?
Here is the code:
And here is the ADC object code:
When I run the code I am only getting 45 data measurements during the zero cross high input to time. If the 50k Hz value is correct then I should be getting about 208 measurements per channel during this period. There is likely to be some processing time to assess the status of the zero cross input pin and increment the data counter but I think these operations should be relatively fast even in SPIN.
Can anyone help explain why I am getting so few measurements?
Here is the code:
CON
_clkmode = xtal1 + pll16x
_xinfreq = 5_000_000
' serial terminal baud rate (default I/O pins are 30 and 31):
PSTBaud = 115200
OBJ
pst : "Parallax Serial Terminal"
' adc : "MCP3208 ADC"
adc : "MCP3208 Fast"
VAR
long DIOpin, CSpin, ClkPin, ZCinPin, ZCoutPin, BMode, nChannel, KbCmd, ADCcog, ZCPcog
long Vdata [500], Idata[500], VChan, IChan
long Stack1[50]
PUB Main | i, j, k, l, m, n, msec
pst.Start( PSTBaud )
pst.Clear
pst.Beep
pst.str(String("Initializing..."))
DIOpin := 1 'MCP3208 data in/data out pin
ClkPin := 0 'MCP3208 clock pin
CSPin := 3 'MCP3208 chip select pin
BMode := %1 'MCP3208 mode (1=single ended, 0=differential)
nChannel := 1 'Default ADC channel number
VChan := 1 'Voltage channel
IChan := 2 'Current channel
ZCoutPin := 7 'Zero crossing simulator output pin
ZCinPin := 6 'Zero crossing simulator input pin
DIRA[ ZCinPin ]~
ADCcog := adc.start( DIOpin, ClkPin, CSPin, BMode ) 'Start the MCP3208 ADC routine
pst.str( String( pst#NL, "ADC runnning in cog: "))
pst.dec( ADCcog )
ZCPcog := cognew( HzPulse( ZCoutPin, 120 ), @Stack1 ) + 1 'Start the 60 Hz zero crossing pulse signal
pst.str( String( pst#NL, "ZC pulse running in cog: "))
pst.dec( ZCPcog )
REPEAT
pst.str(String( pst#NL, "C=Channel select, M=measure, <ESC>=quit"))
KbCmd := pst.CharIn 'Get command from keyboard
if KbCmd == "c" OR KbCmd == "C"
pst.str( String( pst#NL, "Select channel: "))
nChannel := pst.CharIn - "0"
if KbCmd == "m" OR KbCmd == "M"
pst.str( String( pst#NL, "Measuring channel "))
pst.dec( nChannel )
pst.str( String( "..."))
j := adc.in( nChannel )
pst.str( String( " Result="))
pst.dec( j )
if KbCmd == "s" OR KbCmd == "S"
pst.str( String( pst#NL, "Scanning channels..."))
n := 0
REPEAT UNTIL INA[ ZCinPin ] == 0 'Wait for ZC input to go low then high again
REPEAT UNTIL INA[ ZCinPin ] == 1
'********* Measure V-I channels loop
REPEAT UNTIL INA[ ZCinPin ] == 0 'Make as many measurements on channels 1 and 2 during the pulse high time (~8.33 msec)
VData[n] := adc.in( VChan )
IData[n] := adc.in( IChan )
n++
'*********
pst.str( String( " Done. Results:"))
m := n 'Save the number of measurements made.
n := 0
REPEAT m 'Display all the measuremnts made
pst.str( String( pst#NL, "VData["))
pst.dec( n )
pst.str( String( "] = "))
pst.dec( VData[n] )
pst.str( String( " = "))
VData[n] := ( VData[n] * 3300 ) / 4096
pst.dec( VData[n] )
pst.str( String( "mV IData["))
pst.dec( n )
pst.str( String( "] = "))
pst.dec( IData[n] )
pst.str( String( " = "))
IData[n] := ( IData[n] * 3300 ) / 4096
pst.dec( IData[n] )
pst.str( String( "mV"))
n++
pst.str( String( pst#NL, "Measurment in period: "))
pst.dec( m )
if KbCmd == 27
cogstop( ADCcog )
cogstop( ZCPcog )
pst.str(String( pst#NL, pst#NL, "All done, good-bye."))
ABORT
PUB HzPulse( _OutPin, _Freq ) | i, OutPin, HzCNT
OutPin := _OutPin
HzCNT := CLKFREQ / _Freq
DIRA[ OutPin ]~~
OUTA[ OutPin ]~
i := CNT
REPEAT 'Generate a pulsed output at the specified frequency
!OUTA[ OutPin ]
i := i + HzCNT
WAITCNT( i )
And here is the ADC object code:
''MCP3208_fast.spin
''
''Copyright (c) 2007 Jim Kuhlman
''
'' See end of file for terms of use.
''
''*****************************************************
''* MCP3208 12-bit/8-channel ADC Driver *
''* Works w/indiv channel on chip rather than all 8 *
''* Provides single sample or average of n samples *
''* Provides single mode only, no differential mode *
''* Does not provide DAC output like original code *
''*****************************************************
'' rev 1.0 09-16-07 Fixed bug in start routine for delay value.
CON
#1, _setup, _single, _average, _delay, _div, _adcready
' cmds for assy routine: 1=setup, 2=single adc sample, 3=average of n adc samples, etc.
VAR
long cog, delay
long cmd, chnl, nsamples, data[4] '7 longs (3 longs, 8 words)
PUB start(dpin, cpin, spin, mode) : okay
'' Start driver - starts a cog
'' returns false if no cog available
'' may be called again to change settings
''
'' dpin = pin connected to both DIN and DOUT on MCP3208
'' cpin = pin connected to CLK on MCP3208
'' spin = pin connected to CS on MCP3208
'' mode not used, included for compatability with older program
stop ' stop cog if running from before
cmd := 0 ' tell adc_read to wait for next command
cog := cognew(@adc_read, @cmd) + 1 ' store adc_read and shared mem address
waitcnt(clkfreq/4 + cnt) 'wait 1/4 sec for cog to start
chnl := dpin ' initialize for call to _setup
nsamples := cpin
data[0] := spin
data[1] := 0
do_cmd(_setup, @chnl) ' initialize assy routine for dpin, cpin and spin pins on MCP3208
if clkfreq => 80_000_000 ' make it work for prop2 also
delay := clkfreq/2_000_000 ' time delay for half cycle of 50KHz sample rate
else ' set for 1MHz clock rate to chip
delay := 40 ' if clkfreq < 80,000,000 then set to 40
do_cmd(_delay, @delay) ' pass time delay parameter to assy routine
return cog
PUB stop
'' Stop driver - frees a cog
if cog
cogstop(cog~ - 1)
PUB in(channel)
'' Read the current sample from an ADC channel (0..7)
chnl := channel
do_cmd(_single, @chnl) ' get single sample from channel
return data.word[channel]
PUB average(channel, n)
'' Average n samples from an ADC channel (0..7), n <= 16
chnl := channel ' set channel number in xfr array
nsamples := n <# 16 ' limit # of samples to 16 max
nsamples #>= 1 ' must be greater than zero
do_cmd(_average, @chnl) ' get average of n samples from specified channel
return data.word[channel]
PUB div(divisor, dividend) : quotient
'' Divide 16 bit dividend (in long)
'' by 16 bit divisor (in long)
'' Return 16 bit quotient (in long)
chnl := divisor
nsamples := dividend
do_cmd(_div, @chnl)
quotient := nsamples
PRI do_cmd(adc_command, argptr)
cmd := adc_command << 16 + argptr 'write command and pointer to shared memory loc
repeat while cmd 'wait for command to be cleared, signifying completion
DAT
''
''************************************
''* Assembly language MCP3208 driver *
''************************************
org
'************************************
adc_read rdlong t1,par wz ' wait for command
if_z jmp #adc_read
movd :arg,#arg0
mov t2,t1
mov t3,#4 ' get 4 arguments
:arg rdlong arg0,t2 ' store in arg0, arg1, arg2, arg3
add :arg, hex200 ' update destination addr for next loop
add t2,#4 ' point to next input arg
djnz t3,#:arg
ror t1,#16+2 ' lookup command address
add t1,#jmptable
movs :tabloc,t1
rol t1,#2
shl t1,#3
:tabloc mov t2,0
shr t2,t1
and t2,#$FF
jmp t2 ' jump to command
'************************************
jmptable byte 0 '0 not used
byte setup_ '1 setup adc routine
byte single_ '2 single adc value
byte average_ '3 average adc values
byte delay_ '4 delay for adc clock
byte div_ '5 divide 16 x 16 bits
byte adcready '6 loop for next instruction
'************************************
setup_ mov t1, arg0
call #param 'setup DIN/DOUT pin
mov dmask,t2
mov t1, arg1
call #param 'setup CLK pin
mov cmask,t2
mov t1, arg2
call #param 'setup CS pin
mov smask,t2
jmp #adcready
'************************************
param mov t2,#1 'make pin mask in t2
shl t2,t1
param_ret ret
'************************************
delay_ mov t4, arg0 'set up delay time for chip clock
shr t4, #3 ' divide it by 8 clock cycles/ 2 instr
sub t4, #4 ' adj for 1/2 cycle
mov delayval, t4 ' store in delayval for delay routines
jmp #adcready
'************************************
div_ mov t1, arg0 ' move divisor to t1
mov t2, arg1 ' move dividend to t2
call #divide
mov t3, par ' shared mem ref to t3
add t3, #8 ' point to output
wrlong t2, t3 ' write quotient to nsamples
jmp #adcready
'************************************
single_ mov arg1, #1 'initialize arg1, not supplied with single_ call
'************************************
average_ mov t3, arg1 'set for n sample average (or 1 if single sample)
or dira,cmask 'set CLK line to output
or dira,smask 'set CS line to output
mov command,#$18 'init command to 011000, start/single
add command, arg0 'add in channel number to command bits
mov t2, #0 'initialize data[channel] accumulator
:bloop mov stream,command 'set up for new sample
or outa,smask ' CS high
or dira,dmask ' make DIN/DOUT output
andn outa,cmask ' CLK MUST be low before CS enabled!!!
andn outa,smask ' CS low
nop ' wait 8 clock cycles for TSUCS
nop
mov bits,#5 'ready 20 bits (cs+1+diff+ch[3]+0+0+data[12])
:cloop test stream,#$10 wc 'update DIN/DOUT
muxc outa,dmask
andn outa,cmask 'CLK low
call #delay1 ' adjust clock cycle time
rcl stream,#1 ' shift out next bit
or outa,cmask 'CLK high
call #delay2 ' adjust clock cycle time
djnz bits,#:cloop
andn dira,dmask ' make DIN/DOUT input to get data
mov bits,#14 'get the data 2-null plus 12 data
:dloop andn outa,cmask 'CLK low
call #delay1 ' adjust clock cycle time
or outa,cmask 'CLK high
test dmask,ina wc 'sample DIN/DOUT
rcl stream,#1 ' store bit for output
call #delay2 ' adjust clock cycle time
djnz bits, #:dloop 'next data bit
and stream,mask12 'trim sample
add t2, stream 'accumulate sum in t2
djnz t3, #:bloop 'more samples for average?
cmp arg1, #1 wz ' see if n > 1
if_nz mov t1, arg1 ' set up for divide if needed, arg1 = number of samples, n
if_nz call #divide ' divide t2 by t1 or sum/n -> t2
mov t3, arg0 'load channel number, 0..7
shl t3, #1 'mult by 2 for word offset in data[ ]
add t3, #12 'add 12 for 3 long offset (cmd + chnl + nsamples)
mov t1,par 'reset sample pointer
add t1, t3 'add offset, t1 now points to data[channel]
wrword t2,t1 'write sample to data[channel]
'fall through to adcready and exit
'************************************
adcready wrlong zero,par 'zero command to tell caller we're done
jmp #adc_read 'back to wait for another command
'************************************
delay1 mov t4, delayval ' load delayval, clock cycles
nop ' adj to half cycle
nop
:tloop1 nop
djnz t4, #:tloop1 ' delay 1/2 uSec = 1/2 MCP3208 clk cycle
delay1_ret ret
'************************************
delay2 mov t4, delayval ' load delayval, clock cycles
:tloop2 nop
djnz t4, #:tloop2 ' delay 1/2 uSec = 1/2 MCP3208 clk cycle
delay2_ret ret
'************************************
' Divide
' in: t1 = 16-bit divisor in long
' t2 = 16-bit dividend in long
' out: t2 = 16-bit quotient (in long), truncated
' temp: t3 = loop counter
divide shl t1,#15 'get divisor into t1[30..15]
mov t3,#16 'loop counter for 16 bits
:loop cmpsub t2,t1 wc 'if t1 =< t2 subtract it, quotient bit -> c bit
rcl t2,#1 'rotate c into quotient and shift dividend
djnz t3,#:loop 'loop until done
and t2, hexFFFF 'mask quotient, drop the remainder in [31..16]
divide_ret ret
'************************************
' Initialized data
'
hex200 long $200 'constants
hexFFFF long $FFFF
mask12 long $FFF
zero long 0
'************************************
' Uninitialized data
'
arg0 res 1 ' arguments passed from high-level
arg1 res 1
arg2 res 1
arg3 res 1
dmask res 1 ' MCP3208 pin masks
cmask res 1
smask res 1
t1 res 1 ' temps
t2 res 1
t3 res 1
t4 res 1
dx res 1 ' for atn routine
dy res 1
command res 1 ' for 3208 routine
stream res 1
bits res 1
delayval res 1
{ Terms of use:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
}

Comments
Anyway, I get around that by writing a PASM loop around the assembly part. It speeds things right up.
Attached is a version that automatically samples at full speed. It's based off the 3 ADC object, so you may need to trim it to one ADC. It should work, but it's been a while since I looked at that code.
You should also move all of the PST statements out if you want maximum speed. It's sometimes surprising how slow those routines are. If you want something really fast, don't output anything except the number and characters such as space and CR.
OBJ pst : "Parallax Serial Terminal" ' adc : "MCP3208 ADC" adc : "MCP3208 FastMaybe I'm missing something but it looks like you have three objects but two nicknames.
The first "adc" object is commented out so I am only using the "Parallax Serial Terminal" and the "MCP3208 Fast" objects. For some unknown reason I could not get the basic "MCP3208 ADC" object to work on anything but channel 0.
I don't think I have any PST statements inside the ADC measurement loop - only the adc.in(ch1), adc.in(ch2) and the n++ statements. I assume the PST's before and after have no impact on the speed.
So you think the REPEAT UNTIL INA[ ZCinPin ] == 1 and n++ statements slow this down sufficiently to where I only get ~25% of the possible measurements? Wow - that is very surprizing.
Unfortunately, I don't know PSAM and haven't touched assembly based code since 1977 (just after it was invented!!!). I may have to settle for 45 data points for now.
Another way to speed things up is to not do what the fast object is doing, reading only 1 channel. You can read all 8 channels at once, saving you the overhead of the inter-COG communication and the second SPIN call.
adc.in->do_cmd-> "ASM Call"
You could speed it up by directly going to the ASM call, without the Spin overhead (aka, do all the memory manipulation yourself in your end object).
Additionally, you can unroll your loop for a bit more speed. It will probably be pretty minor, though.
You should test the various pieces of code to see how long they take. Do something like:
Just after it was invented????? I programmed in assembler quite a few years before that. Assembler was the first step up from programming in machine language. It was essentially machine language using mnemonics instead of binary/octal/hex numbers.
FF
Thanks to all of you for your input!
Thanks for the intro link Peter. Now I will go %@%@ crazy looking through my book archive because I just know I kept my copy of Starting Forth. Have not until this thread really considered playing with Forth again.........
FF
I suggest that you create a new method that combines the "repeat until" loop with the in and do_cmd methods. This will eliminate the calling overhead within the loop. You should also pass @VData and @IData as pointers to the new method. This will give you higher speed.
You can read 8 channels at once? Can you share some example code?
The MCP320x chips do not measure the inputs simultaneously - only one channel at a time. If you can get it to run at its top rated speed of 100 kHz then you can measure all 8 channels sequentially at an effective per channel rate of 100/8=12.5 kHz.
If you truly need simultaneous measurements you would need to get a different chip, OR you could use 8 MCP3202's and tie their CS and CLK lines together so they measure in perfect timing sequence. You would need 8 dedicated DIO lines on the Propeller (or a fast bidiectional serial/parallel shift register). Wow, 8 ADC signal at 100 kHz speed - not bad!
I wrote some code that got me to the full 100 kHz speed. Since I was only using a 2-channel MCP3202 I got 50 kHz per channel measurement rate. This code could be modified to scan all 8 channels on a MCP3208. My code is a bit of a WIP so not sure if it would be of value to you. Let me know if you still want to see it.
That's what I thought, hence the question - I thought I must be missing something.
When your code's ready to be shared I wouldn't mind taking a look - thanks.