Shop OBEX P1 Docs P2 Docs Learn Events
Simultaneous tasks on a SX28 — Parallax Forums

Simultaneous tasks on a SX28

Matthias09Matthias09 Posts: 47
edited 2009-11-15 04:21 in General Discussion
Hey guys,

I have a SX 28 that controls a stepper motor and ultrasound sensors. The sensors are measuring the position of a ball on a beam and the stepper motor turns the beam accordingly (Ball and Beam system).
So far I have one big loop in which:
1) Sensors read the ball position
2) angle to turn the beam is calculated
3) and the stepper motor is instructed to turn (cw or ccw) with one step
4) then next loop

The problem I have here is that the calculation and sensor read outs take some time which result in a big time one loop needs to run and hence in a non smooth stepper motor movement. I only run one step per loop as the direction of turning the beam can change with every loop.

So my idea is: instead of letting the motor wait in every loop for the instruction for the next step, I could just run it "in background" in one direction and only give him signals when it's time to change directions. This signal would come from the main loop that runs simultaneously and still keeps on calculating the position of the ball and the direction to turn.

Now I know that the SX only has one processor inside, so would it work, when I make this whole thing with an periodical interrupt (rather than a loop). It would look like that in pseudo code:


INTERRUPT
Every 20ms
1) Sensors read the ball position
2) angle to turn the beam is calculated
if directions <> current direction
give signal to stepper to change direction
endif


MAIN:
DO
DRIVE_STEPPER direction
LOOP


Question I have here:
- during the execution of the periodical interrupt, is the loop in MAIN still working, or is it stopped temporarily until the interrupt finished working? (which meant I would still have the same time problem)
- I already have a non periodical interrupt, that fires whenever the encoder (that measures the angle of the beam) detects a change on one of it's two channels (Quad). Can I have both kinds of interrupts at the same time. Do the interfere somehow? Should I consider anything?
- Any other idea how to delink the stepper motor driver from the calculation (in case the periodical interrupt idea doesn't work)?

Independent of that I have another question. In Debug Mode the motor runs as slow as in normal mode. I use 50MhZ in normal mode, and Debug should only work in 4Mhz (as far as I know). Shouldn't there be a difference in performance? Means the motor should run significantly smoother when not in debug mode? Of course I use the oscillator for the 50mhz.

This are my first lines of code, where I think I define the speed of the processor.

DEVICE SX28, OSCHS3, TURBO, STACKX, OPTIONX    
FREQ 50_000_000




Thanks guys! Thanks! You are great and without your support (on other earlier questions) I wouldn't be where I'm today!

Matthias

Post Edited (Matthias09) : 10/18/2009 2:32:13 AM GMT

Comments

  • ZootZoot Posts: 2,227
    edited 2009-10-18 02:50
    There are a *lot* of ways to approach this. This is just one way (pseudo code):

    
    
    
    Interrupt 100000 ' 10us ticks
    
    SensorMeasure:
        sensorTmr = sensorTmr + 1 ' used for timekeeping in main loop (non-interrupt code)
        
        IF sensorState = 2 THEN ' taking measurement
           IF SensorPin = 1 ' say if you are measuring a ping pulse
                sensorVal = sensorVal + 1
           ENDIF
        ENDIF
    
    StepperOut:
        motorFrame = motorFrame + 1  ' this and 100 below lets you control speed; I leave that to you
    
        IF motorFrame > = 100 THEN ' step once per 10 milliseconds (100 * 100us) = 10000us = 10ms
            motorFrame = 0
    
        IF motor < 128 THEN ' say if 128 = neutral, 255 = full forward; i'm not counting pulses here nor speed here, but you could
           motorIdx = motorIdx - 1
        ELSEIF motor > 128 THEN
           motorIdx = motorIdx + 1
        ENDIF
        
        motorIdx = motorIdx & %11 ' 0-3
    
        IF motorIdx = 0 THEN   ' move stepper pulse pattern to output pins use your patterns
           MotorPins = %0001
        ELSEIF motorIdx = 1 THEN
           MotorPins = %1000
       ELSEIF motorIdx = 2 THEN
           MotorPins = %0010
       ELSE
           MotorPins = %0100
       ENDIF
    
       ENDIF ' if motor frame
    
       RETURNINT ' interrupt over
    
    
    Main:
    
     ' now in the main line, you can use the timer and set the state for triggering/reading sensors, and setting motor stuff
    
    
    Sensor_State_Stuff:
    
        IF sensorState = 0 THEN ' waiting to read...
            IF sensorTmr >= 200 ' 200 * 10 us = 20ms between reads
                sensorTmr = 0 ' clear the timer
                HIGH SensorPin  ' trigger Ping ultrasonic
                sensorState = 1 ' next state
           ENDIF
        ENDIF
    
        IF sensorState = 1 THEN ' waiting for high pulse on ping to finish triggering the ultrasonics   
            IF sensorTmr >= 1 THEN ' 10us trigger
                LOW SensorPin ' end pulse
                IF sensorTmr >=2 THEN ' wait for at least 10us low
     
                   sensorTmr = 0 ' clear the timer 
                   sensorVal = 0 ' clear the last measured value
                   INPUT SensorPin ' make pin input
                   sensorState = 2    ' next state
                ENDIF
            ENDIF
        ENDIF
       
       IF sensorState = 2 THEN ' measuring
             IF SensorPin = 0 THEN ' wait for pulse to end
                 LOW SensorPin ' turn pin off to keep trigger low
                 sensorReading = sensorVal ' move count to stable variable
                 sensorTmr = 0
                 sensorState = 0 ' back to waiting for next measure cycle
             ENDIF
        ENDIF
    
    
    Motor_Stuff:
        IF sensorReading < 20 THEN
           motor = 10 ' turn one direction
        ELSEIF sensorReading > 200 THEN
          motor = 245
        ELSE
          motor = 128 ' stop
        ENDIF
    
    Done:
    
       GOTO Main
    
    




    The idea here is your main loop keeps moving through and checking the timers or setting parameters for the interrupt. Even if your main loop "lags", it doesn't matter, the motor will continue to pulse, and the last sensor reading is available, etc.

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    When the going gets weird, the weird turn pro. -- HST

    1uffakind.com/robots/povBitMapBuilder.php
    1uffakind.com/robots/resistorLadder.php


    Post Edited (Zoot) : 10/18/2009 2:55:43 AM GMT
  • pjvpjv Posts: 1,903
    edited 2009-10-18 16:24
    Hello Matthias;

    The SX can very easily handle multiple threads, one per sensor/action. If you look up the first iteration of SX design contests, I had submitted an entry to handle mutiple threads. Each is independent of the others, hence each times itself. Lookup the RTOS entry, and it will do what you want. It is in assembler, and can not be interfaced to SX/B in those cases where SX/B uses any blocking delays like DELAYUS or serial comms. These you would need to modify into assembler.

    Cheers,

    Peter (pjv)
  • PropabilityPropability Posts: 142
    edited 2009-10-18 18:42
    @ Peter

    The link for your RTOS seems to be broken in the Contest pages. Checked some of the others and they work.
  • Matthias09Matthias09 Posts: 47
    edited 2009-10-18 23:00
    @ Peter

    I think you mean this thread:
    http://www.parallax.com/tabid/677/Default.aspx

    However, as Propability mentioned, I cannot access any code. Unfortunately I'm not able to read/write assembler, hence I'll probably not be able to understand your awarded program.

    @ Zoot

    Wow! thanks a lot! I'll work on that approach!

    Matthias

    Post Edited (Matthias09) : 10/19/2009 3:01:49 AM GMT
  • Matthias09Matthias09 Posts: 47
    edited 2009-11-14 00:55
    Zoot,

    I tried your little program [noparse]:)[/noparse] and it works great!! Stepper runs faster and much smoother, everything works...except: the Ping.

    I have an interrupt running with 100,000 frequency. Is this freq dependent from the freq the SX is running with? Means, does it change in debug mode (4mhz) and running mode (50mhz). Does the debug mode in any way prevent the Ping to work properly?

    I always receive 0 as a result of the distance measured by the ping. I also figured out that the interrupt doesn't event count the distance up, after the pulse was sent. (it does not jump in the proper interrupt section, as the change of the variable sensorstate happens too quickly, I figured this out by putting a Pause (counting up tix) after the measurement has ended to delay the change of sensorstate to the next step, start pulse).

    I attached my program. I hope you can give me a hint.

    Best and have a good weekend!

    Matthias

    Post Edited (Matthias09) : 11/14/2009 1:02:03 AM GMT
  • ZootZoot Posts: 2,227
    edited 2009-11-14 01:30
    RTCC interrupt rates are independent of system clock frequency; that presumes that the given clock is fast enough to let the interrupts run completely before the next one is called. No way 100_000 will run at 4mhz with this kind of code. I would run at 20MHZ or better yet 50MHZ (which is what it looks like you're doing). If debugging, make sure the Key is clocking at 50mhz.

    Second, and this may be the cause of the problem -- you don't just want to wait for the ping response, you want to actually check that the ping line goes high THEN goes low, not just check for low. I am sure the ping will not respond in 10us after the trigger line goes low. The ping is programmed to *always* return some kind of high pulse, so you need to check for the edge before closing out the reading.

    I know -- my bad -- I didn't think about this in my original example. Probably something like this would suffice:

     ' ----- End Pulse USD -----
    
        IF General_Var(1) = 3 THEN             ' waiting for high pulse on ping to finish triggering the USD       
           IF Tix >= 1 THEN             ' 10us trigger
                LOW USD_Pin             ' end pulse
                IF Tix >= 2 THEN             ' wait for at least 10us low
                   Tix = 0                 ' clear the timer 
                   rawdata = 0             ' clear the last measured value
                   INPUT USD_Pin             ' make pin input
                   General_Var(1) = 4            ' next state
                ENDIF
             ENDIF
        ENDIF
    
    
    ' ----- wait for pin to go high before waiting for sample -----
       
        IF General_Var(1) = 4 THEN     
        IF USD_Pin = 0 THEN          'still low
            Tix = 0     ' keep the counts cleared
                    rawdata = 0
            ELSE
            General_Var(1) = 5  'went high, wait and measure...
             ENDIF
    
         ENDIF
    
     ' ----- Measuring USD -----
       
        IF General_Var(1) = 5 THEN             ' measuring
        IF USD_Pin = 0 THEN             ' Signal is high as long as no feedback. Once USD receives something, then signal goes low. 
                            ' wait for pulse to end. only if pulse has ended, procedures goes ahead, otherwise abortion here
                 led2 = ison
             LOW USD_Pin             ' pulse ended: turn pin off to keep trigger low
                 Distance = rawdata         ' move count to stable variable
                 Tix = 0                ' clear the timer 
                            ' from now on I have 5ms until the next reading of the USD (set at 1*)
                  IF Tix >= 4000 THEN
            General_Var(1) = 2                 ' end this step, skip the index finding step and system check
             ENDIF
             
        ENDIF
       
         ENDIF
    
    
    

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    When the going gets weird, the weird turn pro. -- HST

    1uffakind.com/robots/povBitMapBuilder.php
    1uffakind.com/robots/resistorLadder.php


    Post Edited (Zoot) : 11/14/2009 2:16:02 AM GMT
  • ZootZoot Posts: 2,227
    edited 2009-11-14 02:08
    Oh, yeah, and you'd need to do this in the ISR (which is a bit long but looks basically OK).

     IF General_Var(1) = [b]5[/b] THEN             ' status of the USD:  2=wait+start pulse, 3=end pulse + prepare reading, 4=reading
        led1 = ison    
        IF USD_Pin = 1 THEN                ' measuring the ping pulse. count up time variable (rawdata) as long as there is high at the USD
         INC rawdata                ' store results in rawdata
        ENDIF
     ENDIF
    
    

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    When the going gets weird, the weird turn pro. -- HST

    1uffakind.com/robots/povBitMapBuilder.php
    1uffakind.com/robots/resistorLadder.php
  • ZootZoot Posts: 2,227
    edited 2009-11-14 02:11
    Oh shoot, I just caught this. What is this about? Seems like it would never exit this state, as you clear TIX then wait for it to go past 4ms. If you want a delay after the sample is finished, you'd need to another state or something to allow for clearing TIX then waiting for it to count up to 4ms again:

        IF General_Var(1) = 5 THEN             ' measuring
        IF USD_Pin = 0 THEN             ' Signal is high as long as no feedback. Once USD receives something, then signal goes low. 
                            ' wait for pulse to end. only if pulse has ended, procedures goes ahead, otherwise abortion here
                 led2 = ison
             LOW USD_Pin             ' pulse ended: turn pin off to keep trigger low
                 Distance = rawdata         ' move count to stable variable
    [b]             Tix = 0                ' clear the timer 
                            ' from now on I have 5ms until the next reading of the USD (set at 1*)
                  IF Tix >= 4000 THEN
            General_Var(1) = 2                 ' end this step, skip the index finding step and system check
             ENDIF
    [/b] ' the above couldn't possibly exit this state????         
        ENDIF
       
         ENDIF
    
    

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    When the going gets weird, the weird turn pro. -- HST

    1uffakind.com/robots/povBitMapBuilder.php
    1uffakind.com/robots/resistorLadder.php
  • Matthias09Matthias09 Posts: 47
    edited 2009-11-15 01:00
    Hey Zoot,

    thank you very much! I appreciate your helpful support and the best thing: The Ping runs now!! [noparse]:)[/noparse]
    So thanks a lot! I would have never found the error.

    However, I got a problem: the values the Ping returns are jumping up and down vastly (e.g. 10 cm distance in rawdata: 19, 59, 9, 36,...). Something must affect the counting of cycles in the interrupt (INC rawdata).

    So my question: in DEBUG mode, can I run also with 50mhz, when i defined it with the FREQ command? I know I then use the SX Key clock, and I thought that one can only run with max. 4Mhz (which would be too slow).

    I fear, that my interrupt is so long, that it needs sometimes all the clock cycles available between two interrupts just for itself and leaving no time to run the main loop.

    I calculated this way: SX runs at 50Mhz (hopefully also in DEBUG mode). One clock cycle then needs 1/50MHz = 20ns = 0.20us to run. My interrupt runs 100,000 per second or every 10us. Between two interrupts I hence can make 10us/20ns = 10000/20 = 500 clock cycles.
    Means after every 500 clock cycles, the interrupt occurs, runs, takes away some cycles for itself and leaves some cycles left for the main loop.

    Is this correct? Could it be that the interrupt *sometimes* uses all 500 cycles for itself so the main loop cannot go on and the end of the high signal of the PING is not detected immediately (i know that already GOTO needs 3 cycles to process)?

    Warmest Wishes,

    Matthias
  • ZootZoot Posts: 2,227
    edited 2009-11-15 01:16
    Your thinking is spot on. And 500 cycles between interrupts is plenty, presuming no long division or some such in the ISR. So I don't think that's it, given what you have there. Post the most recent (running) version of your program.

    Debug should work OK at 50mhz -- make sure to pull your resonator or crystal.

    If you do a simple PULSOUT then PULSIN app as a test for the Ping (like the official Ping demos) do you get the same data bouncing around?

    Also, given that most recent version of your app does not process the returned Ping pulse length (by dividing by 2 to remove the return trip and doing a fractional multiply to convert to centimeters) if you are looking at the raw word data returned, a jump of +/- 10 is not highly significant. The Ping can return a maximum pulse of ~18ms = 18000us = 1800 ticks --- so a bounce there of 10 would equal ~0.55 % error or ~1.65 cm when converted and after removing round trip. Not too bad really. What are you using as test surface? Try a nice stiff flat piece of something about 30-40cm away and perpendicular to the Ping and see if you get stable readings.

    Of course, if the jumps you are seeing are after correct conversion to cm., that's odd.

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    When the going gets weird, the weird turn pro. -- HST

    1uffakind.com/robots/povBitMapBuilder.php
    1uffakind.com/robots/resistorLadder.php
  • Matthias09Matthias09 Posts: 47
    edited 2009-11-15 01:58
    Zoot,

    find the program attached. I've commended some thing out, that are about to return once I finished testing the program.

    Thanks for the info about the cycles!

    I have made tests like you said, fixed the object in one position and took measurements. it's bascially always like. two types of results: one is right distance, the other significanlty lower, they change randomly with the right results coming more often:
    Position one: 83, 82, 79, 33, 82, 34, 83, 82 (right: 80, wrong 30)
    Position two: 50, 50, 20, 47, 50, 20, 50, 49, 20 (right: 50, wrong 20)

    Matthias
  • ZootZoot Posts: 2,227
    edited 2009-11-15 03:07
    Ummm.. where did the conversion number come from:

    RawToCm CON 2257 ' 1cm / 29.034us. see docu
    RawToMm CON 22572 ' 1mm / 2.9034us. see docu


    Remember your ticks are 10us -- not 2us or .8us like on some of the Stamps.

    So.......hitting the docs:

    Host Device Input Trigger Pulse tOUT 2 µs (min), 5 µs typical 
    Echo Holdoff tHOLDOFF 750 µs ' quite a bit until pulse will start
    Burst Frequency tBURST 200 µs @ 40 kHz 
    Echo Return Pulse Minimum tIN-MIN 115 µs ' min pulse
    Echo Return Pulse Maximum tIN-MAX 18.5 ms ' max pulse
     PING))) 
    Sensor 
    Delay before next measurement  200 µs ' could be good to know if they are being triggered too fast
    
    
    RawToIn         CON     889                     ' 1 / 73.746 (with **)
    RawToCm         CON     2257                    ' 1 / 29.034 (with **)
    
    ' At sea level sound travels through air at 1130 feet per second.  This
    ' equates to 1 inch in 73.746 uS, or 1 cm in 29.034 uS).
    
    
    



    So you want to divide in the form 1/29.034 where 1 = 1us. But you are counting 10 us increments. So you need to multiply the fractional constant by 10.

    That means:

    distance = raw ** ((1/2.9034)*$FFFF)
    distance = raw ** ( 22571.812357925192 )
    distance = raw ** 22572

    But you need to divide by two, ideally before scaling, to minimize error:

    distance = (raw+1)/2 ' cleaner rounding
    distance = distance ** 8887

    Picking a random sample of say 9000us (approx halfway through the Ping's useful distance), that should give something like 150 cm:

    9000us = 900 ticks
    ( 900 ticks + 1 ) / 2 = 450 return trip
    450 ** 22572 = 154.99198901350423
    or 154 cm given that we are not accommodating rounding when using ** above.

    Sounds about right. ** is better if the fractional part is less than 1, i.e. multiplying by .7564 or something. */ is required if the fraction part is greater than 1, i.e. multiplying by 1.345. ** is more precise.

    The rest looks OK. The anamalous data on the longer measurements is sure odd. Perhaps grasping at straws -- is the ping close to the floor/surface? What did you use for a test object? Are you just watching these values in the debugger? Without a "break" in a known location how do you know you are seeing the most recent full measurement, rather than a value in progress?

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    When the going gets weird, the weird turn pro. -- HST

    1uffakind.com/robots/povBitMapBuilder.php
    1uffakind.com/robots/resistorLadder.php
  • Matthias09Matthias09 Posts: 47
    edited 2009-11-15 03:10
    Ok, I think it's 'old' signals comming in from the last reading loop.
    I increased the times between to reading to 0.5sec and there were no false (lower readings anymore).

    So this smaller values must have been signals from the previous pulse that were bouncing back from long distant other objects and arriving shorlty after I've sent the 2nd pulse and put the pin to input mode and start counting until I recieve something.

    However I just tried an older code with classical pulseout, pulsein and with 10ms pause between two reads and that worked just fine. When I use the same 10ms pause for the new algorithm, it gives me most of the time the lower of the two values (in Position two from post above then most of the time 20s).

    So is there anything PULSEIN does, what I would have to do manually in the new code as well?

    Matthias
  • Matthias09Matthias09 Posts: 47
    edited 2009-11-15 03:11
    Just saw you replied [noparse]:)[/noparse]

    I don't convert so far anything. Just use raw data.

    I'm gonna read right now your answer! I appreciate this very much!

    Matthias
  • ZootZoot Posts: 2,227
    edited 2009-11-15 03:14
    I've used the Pings quite a bit. I've had to put delays between multiple firings in sometimes up 50-75ms but not more. Sound just can't go that far indoors and live smile.gif. Generally I fire 'em w/less than 20ms between triggers.

    In any case, 500ms between triggers seems awfully long.

    Can you post the "classical" program? I am really wondering about the location of a BREAK statement so that the WATCHEed variables are updated at the right time.

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    When the going gets weird, the weird turn pro. -- HST

    1uffakind.com/robots/povBitMapBuilder.php
    1uffakind.com/robots/resistorLadder.php
  • Matthias09Matthias09 Posts: 47
    edited 2009-11-15 03:17
    Hi Zoot,

    here is the classical program. It shows the same results like the new one, but without the jumps to smaller values. I use here a 50us pulse rather than a 10us in the new program.

    Best,

    Matthias


    
    DEVICE SX28, OSCHS3, TURBO, STACKX, OPTIONX
    FREQ 50_000_000
    
    
    
    
    LED1         PIN RC.2 OUTPUT
    LED2         PIN RC.3 OUTPUT
    LED3         PIN RC.4 OUTPUT
    LED4         PIN RC.5 OUTPUT
    IsOn         CON 1
    IsOff         CON 0
    Yes         CON 1
    No         CON 0
    
    
    US_Green    PIN RC.7             'green labeled Ultrasonic sensor
    RawToMm     CON 22572            ' 1mm / 2.9034us. see docu 
    Green     CON 1                'for green labeled Ultrasound sensor
    Black         CON 0                'for black labeled Ultrasound sensor
    FirstRun    VAR BIT
        
    whereisit     var word
    
    
    
    '===========# STARTUP #======================================================================================================
    
    PROGRAM Start
    
    GET_BALL_POSITION    FUNC 2, 0         
     
    Start:    
    
    
    DO
    
        whereisit = GET_BALL_POSITION 
        watch whereisit, 16, sdec
           \ BREAK    
    
    LOOP
    
    END
    
    
    
    
    '-------------# GET_BALL_POSITION #------------------------------------------------------------------------------------------
    
    FUNC GET_BALL_POSITION
    
        
        US_Green = 0             
        PULSOUT US_Green, 5         
        PAUSEUS 10             
        PULSIN US_Green, 1, rawdata      
    
        PAUSE 10                
    
        RETURN rawdata     
    
    
    ENDFUNC
    
    
    
  • ZootZoot Posts: 2,227
    edited 2009-11-15 03:38
    Yeah, but that version has a break statement.

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    When the going gets weird, the weird turn pro. -- HST

    1uffakind.com/robots/povBitMapBuilder.php
    1uffakind.com/robots/resistorLadder.php
  • Matthias09Matthias09 Posts: 47
    edited 2009-11-15 03:43
    To your questions:

    After process is finished, I store the counting variable in a second one (more stable one, like you suggested). I only watch this stable variable, so can be sure I don't see any values in progress.

    I think the ping might grasp some other things too, but as I said: with the classical program in same environment, it works just fine.

    As you talked about it earlier: when I have a long code that at several positions changes the variable, how can I define I position where I want to see it?
    Normally I think with break, means where break stands, there I see all the variables at this time.However in this code with the cyclical interrupt, break doesn't work, as it prevents somehow the ping and encoder to work properly. What would I do here?

    I just made another check with the new code and 0.5s distance between two readings. I get still the smaller values. Less often, but still there. Less often probably because I just measure less often.

    Your time and efforts are most appreciated.

    Matthias

    Post Edited (Matthias09) : 11/15/2009 4:24:22 AM GMT
  • ZootZoot Posts: 2,227
    edited 2009-11-15 04:21
    Technically, since distance is a word it could be possible for the debugger to display between byte moves.

    You just need to put the break in state 5 (in the last iteration) after distance is updated.

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    When the going gets weird, the weird turn pro. -- HST

    1uffakind.com/robots/povBitMapBuilder.php
    1uffakind.com/robots/resistorLadder.php
Sign In or Register to comment.