Announcement

Collapse
No announcement yet.

Modified sketch for improved timing precision.

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

  • #31
    Originally posted by SaltyDog View Post
    Here is the code. You will need to swap the pins back of course.
    I've adapted the code to include the pot filtering by Gounghouk.

    Additionally, I've limited pot reading to 10 times / sec instead of 1000 times as per Gounghouk, which I believe isn't necessary.
    Reading is now done at the end of the Tx/sample sequence (right after the EFE sample) to avoid old values mixed with new values, which can happen when the calculation is interrupted.

    I invite you to try it out.

    PI_SaltyDog.zip

    PHP Code:
    
    /** Arduino Pulse Induction Metal Detector mods to drive the Tx and mainSample signals by PWM
      *
      *   Version 2.0.1
      *   Date: Feb 8 2024
      *   Author: Teleno (based on George's original sketch)
      *
      * This version drives the Tx and mainSample pins using two PWM outputs of Timer1
      * rather than driving them "manually" with digitalWrite(). This way the duration of
      * the corresponding pulses is jitter free and can be changed in exact increments
      * of the system clock period (62.5 ns).
      *
      * Since TImer1 only has two separate PWM outputs, the driving of the efeSample pin
      * is still done manually with digitalWrite(). Jitter in the efeSample is less critical
      * because at this delay the input signal is almost constant.
      *
      * In the original version the mainSample pin is already assigned to a PWM output of Timer1,
      * so this pin is left unchanged. On the other hand, the Tx pin is not PWM, so in order
      * to use this sketch you must swap the Tx and efeSample pins (Tx -> pin 10; efeSample -> pin 8)
      * so that TImer1 PWM output OC1B can drive the Tx signal
      *
      * The original contains variables that can be replaced by #define statements of the C
      * preproessor, saving memory for further modifications (adding an I2C display, etc.). Other
      * variables are no longer necessary as this version requires less calculations.
      *
      * Timer1 is configured as CTC (clear timer on compare), with the maximum count being stored in
      * the ICR1 register. This would be the txPeriodCount. The Tx pulse starts at count 1 and ends
      * at txOn + 1, according to the values stored in OCR1B and the set/clear flags in TCCR1A. The
      * interrupts take care of dynamically changing the configuration between each edge of the pulses to
      * prepare the next edge. The timing of the Tx and mainSamnple signals is done by hardware and
      * do not depend on the latency of the interrupts (no offset values required).
    */
    
    // One microsecond is 16 system clock cycles
    #define US 16
    
    // Interrupt state machine
    #define MODE_SAMPLE_ON  0
    #define MODE_SAMPLE_OFF 1
    #define MODE_EF_ON      2
    #define MODE_EF_OFF     3
    
    // Pin assignments
    #define txPin  10         // Assign pin 10 to TX output
    #define mainSamplePin  9  // Assign pin 9 to main sample pulse
    #define efeSamplePin   8  // Assign pin 8 to EFE sample pulse
    #define audioPin  11      // Assign pin 11 to audio chopper
    #define boostPin  12      // Assign pin 12 to boost switch
    #define delayPin  A0      // Assign delay pot to A0
    
    // Program constants
    #define normalPower  50       // Normal TX-on time (50us)
    #define boostPower  100       // Boost TX-on time (100us)
    #define readDelayLimit 100    // Wait 100 TX periods (100ms) before reading delay pot
    #define syncDemodOn    LOW    // Sample gate turns on when input high
    #define syncDemodOff  HIGH    // Sample gate turns off when input low
    
    // Detector timings
    word txOn = normalPower;     // TX-on time using normal power mode
    volatile word mainDelay =     10;     // Main sample pulse delay
    #define defMainDelay 10       // Default main sample delay (10us)
    #define mainSample 50        // Main sample pulse width (50us) >= 3us
    #define efeDelay  240        // EFE sample pulse delay (240us)
    #define efeSample  50        // EFE sample pulse width (same as main sample) >= 3us
    #define txPeriod 1000        // TX period (1ms)
    
    // Timing offsets
    #define efeDelayOffset 12    // EFE delay pulse offset (12us)
    
    // Program variables
    volatile word txOnCount;                     // TX pulse
    volatile word mainDelayCount;                // Main sample delay
    volatile word mainSampleCount;               // Main sample pulse
    volatile word efeDelayCount;                 // EFE sample delay
    volatile word efeSampleCount;                // EFE sample pulse
    volatile word txPeriodCount;                 // TX period
    volatile word delayVal = 0;                  // Delay pot value
    volatile boolean readDelayPot = false;       // Delay pot read (true or false)
    volatile byte readDelayCounter = 0;          // Read delay pot counter
    volatile byte mode = MODE_SAMPLE_ON;         // initial state of Interrupt state machine
    byte potRange = 1;                            // fine 0=63 uS,or coarse 1=126 uS additional main sample delay
    
    void calcTimerValues() {
      if (digitalRead(boostPin) == HIGH) {           // Get boost switch position
        txOn = normalPower;                          // Set TX-on to 50us if HIGH
      } else {
        txOn = boostPower;                           // Set TX-on to 100us if LOW
      }
      txOnCount = 1 + txOn * US;                            // TX-on count for Timer1
      mainDelayCount = txOnCount + mainDelay * US;          // Main sample delay count for Timer1
      mainSampleCount = mainDelayCount + mainSample * US;   // Main sample pulse count for Timer1
      unsigned int temp = (efeDelay - efeDelayOffset) * US;
      efeDelayCount = txOnCount + temp;                     // EFE sample delay count for Timer1
      efeSampleCount = efeDelayCount + efeSample * US;      // EFE sample pulse count for Timer 1
    
      txPeriodCount = txPeriod * US;         // TX period count for Timer1
    }
    
    
    void setup() {
    
      pinMode(txPin, OUTPUT);           // Set TX pin to output mode
      pinMode(mainSamplePin, OUTPUT);   // Set main sample pin to output mode
      pinMode(efeSamplePin, OUTPUT);    // Set EFE sample pin to output mode
      pinMode(boostPin, INPUT_PULLUP);  // Set Boost switch pin to input mode with pullup resistor
      calcTimerValues();                // Calculate all timer values
    
    /**  TIMER1 ***
      * URL: https://dbuezas.github.io/arduino-web-timers/#mcu=ATMEGA328P&timer=1&topValue=ICR1&ICR1=54661&CompareOutputModeA=set&timerMode=CTC&CompareOutputModeB=set&OCR1A=54613&OCR1B=27307&InterruptOnTimerOverflow=off&interruptA=on&interruptB=on&InterruptOnInputCapture=on
      * Mode     : CTC
      * Period   : 1 ms
      * Frequency: 1KHz
      * Outputs  :
      *  - OC1A: pin 9
      *  - OC1B: pin 10
      */
      noInterrupts();
      TCNT1 = 0;
      OCR1A = mainDelayCount;
      OCR1B = 1;
      ICR1  = txPeriodCount;
      TCCR1A = 1 << COM1A1 | 1 << COM1B1;
      TIMSK1 = 1 << OCIE1A |1 << OCIE1B;
      interrupts();
      TCCR1B = 1 << WGM13 | 1 << WGM12 | 1 << CS10;
    
      analogWrite(audioPin, 127);     // Set audioPin with 50% duty cycle PWM
    }
    ISR(TIMER1_COMPB_vect) {
    /* on OCR0B match - the TX signal */
      // This was the rising edge of the Tx signal, prepare the falling edge.
      OCR1B = txOnCount;
      TCCR1A &= ~(1 << COM1B0);    // clear Tx pin on compare match
      // Disable OCR1B interrupt
      TIMSK1 &= ~(1 << OCIE1B);
      mode = MODE_SAMPLE_ON;
    }
    
    ISR(TIMER1_COMPA_vect) {
    /* on OCR0A match -  sample signals */
      switch (mode) {
    
        case MODE_SAMPLE_ON:
        // This was the falling edge of the mainSample signal.
          //prepare the rising edge.
          TCCR1A |= (1 << COM1A1) | (1 << COM1A0);   // set mainSample pin on compare match
          OCR1A = mainSampleCount;
          mode = MODE_SAMPLE_OFF;
        break;
    
        case MODE_SAMPLE_OFF:
        // This was the rising edge of the Tx signal, disconnect this PWM pin and set it HIGH
          // prepare EF sample rising edge
          OCR1A = efeDelayCount;
          digitalWrite(mainSamplePin, syncDemodOff);    // pull OC2B high
          TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0));   // disconnect OC2B from output
          mode = MODE_EF_ON;
        break;
    
        case MODE_EF_ON:
          //digitalWrite(efeSamplePin, syncDemodOn);    // Turn on EFE sample gate
          PORTB &= 0xFE;
          OCR1A = efeSampleCount;
          mode = MODE_EF_OFF;
        break;
    
        case MODE_EF_OFF:
          //digitalWrite(efeSamplePin, syncDemodOff);    // Turn off EFE sample gate
          PORTB |= 0x01;
          // Prepare falling edge of mainSample
          OCR1A = mainDelayCount;
          TCCR1A |= (1 << COM1A1);                       // clear mainSample pin on compare match
          //Prepare the rising edge of the Tx signal
          OCR1B = 1;
          TCCR1A |= (1 << COM1B0);                       // set Tx pin on compare match
          // Enable OCR1B interrupt
          TIFR1 |= (1 << OCF1B);                         // clear flag;
          TIMSK1 |= 1 << OCIE1B;                         // Enable interrupt
          mode = MODE_SAMPLE_ON;
      }
    }
    
    void loop() {
        // Read potentiometer, mask out the bottom 3 jittery bits and double the range if selected
        mainDelay = defMainDelay + (((analogRead(delayPin)) & 0x03F8)<<potRange)/US;  // new delay value
        // wait till the end of the cycle to reacalculate the values
        while(mode != MODE_SAMPLE_ON);
        calcTimerValues();                                      // Calculate new timer values
        delay(100);                                             // read pot 10 times per second                    
    }​​ 
    
    Attached Files

    Comment


    • #32
      Originally posted by Teleno View Post

      My scope images correspond to pins 8 and 10, not to TP5 and TP8 (which are inverted by Q3 and Q4).
      So the scope refers to the polarities at the base of the transistors, not at their collectors.
      So don't take offence because there was a misunderstanding, we were looking at different nodes.

      I haven't changed the polarity in the code, so it won't change in the PCB either.

      For future reference: I will always talk about the signals at the pins of tne Arduino Nano.
      I would suggest you keep your scope references to the Test Points as that is what everyone is used to by going by the book. Your scope photos above did not refer to were you were testing, so of course I was confused.

      Comment


      • #33
        Originally posted by Teleno View Post
        I've adapted the code to include the pot filtering by Gounghouk.

        Additionally, I've limited pot reading to 10 times / sec instead of 1000 times as per Gounghouk, which I believe isn't necesary.
        Reading is now done at the end of the Tx/sample sequence (right after the EFE sample) to avoid old values mixed with new values, which can happen when the calculation is interrupted.
        You miss the point. You have put a decision point(while loop) back into loop() therefore creating jitter again. Gounghouk is reading the pot every loop, but who cares, everything is done by interrupt, at least there is no timing jitter in his loop() code.

        Comment


        • #34
          Originally posted by SaltyDog View Post
          You miss the point. You have put a decision point(while loop) back into loop() therefore creating jitter again. Gounghouk is reading the pot every loop, but who cares, everything is done by interrupt, at least there is no timing jitter in his loop() code.
          PWM is not affected by loops, branches etc. in the code, as it is done in hardware.

          That's the whole point of this code, delegating to a hardware timer the generation the pulses (instead of digitalWrites() fast or slow), and get of software jitter.

          Then you can add anything to the loop() code (displays etc.) without fear of jittering the pulses. See where I'm going? How about a nice OLED display to see the actual delay, for example.

          It's absurd to have an MCU based detector if your loop() can't do any magic.

          Comment


          • #35
            Let's move EVERYTHING to the interrupts. The loop() is now totally empty )

            PI_SaltyDog2.zip

            PHP Code:
            /* Arduino Pulse Induction Metal Detector mods to drive the Tx and mainSample signals by PWM  */
            
            
            // One microsecond is 16 system clock cycles
            #define US 16
            
            // Interrupt state machine
            #define MODE_SAMPLE_ON  0
            #define MODE_SAMPLE_OFF 1
            #define MODE_EF_ON      2
            #define MODE_EF_OFF     3
            
            // Pin assignments
            #define txPin  10         // Assign pin 10 to TX output
            #define mainSamplePin  9  // Assign pin 9 to main sample pulse
            #define efeSamplePin   8  // Assign pin 8 to EFE sample pulse
            #define audioPin  11      // Assign pin 11 to audio chopper
            #define boostPin  12      // Assign pin 12 to boost switch
            #define delayPin  A0      // Assign delay pot to A0
            
            // Program constants
            #define normalPower  50       // Normal TX-on time (50us)
            #define boostPower  100       // Boost TX-on time (100us)
            #define readDelayLimit 100    // Wait 100 TX periods (100ms) before reading delay pot
            #define syncDemodOn    LOW    // Sample gate turns on when input high
            #define syncDemodOff  HIGH    // Sample gate turns off when input low
            
            // Detector timings
            word txOn = normalPower;     // TX-on time using normal power mode
            volatile word mainDelay =     10;     // Main sample pulse delay
            #define defMainDelay 10       // Default main sample delay (10us)
            #define mainSample 50        // Main sample pulse width (50us) >= 3us
            #define efeDelay  240        // EFE sample pulse delay (240us)
            #define efeSample  50        // EFE sample pulse width (same as main sample) >= 3us
            #define txPeriod 1000        // TX period (1ms)
            
            // Timing offsets
            #define efeDelayOffset 12    // EFE delay pulse offset (12us)
            
            // Program variables
            volatile word txOnCount;                     // TX pulse
            volatile word mainDelayCount;                // Main sample delay
            volatile word mainSampleCount;               // Main sample pulse
            volatile word efeDelayCount;                 // EFE sample delay
            volatile word efeSampleCount;                // EFE sample pulse
            volatile word txPeriodCount;                 // TX period
            volatile word delayVal = 0;                  // Delay pot value
            volatile boolean readDelayPot = false;       // Delay pot read (true or false)
            volatile byte mode = MODE_SAMPLE_ON;         // initial state of Interrupt state machine
            volatile byte readPotCounter = 0;             // increased every millisecond
            byte potRange = 0;                           // fine 0=63 uS,or coarse 1=126 uS additional main sample delay
            
            void calcTimerValues() {
              if (digitalRead(boostPin) == HIGH) {           // Get boost switch position
                txOn = normalPower;                          // Set TX-on to 50us if HIGH
              } else {
                txOn = boostPower;                           // Set TX-on to 100us if LOW
              }
              txOnCount = 1 + txOn * US;                            // TX-on count for Timer1
              mainDelayCount = txOnCount + mainDelay * US;          // Main sample delay count for Timer1
              mainSampleCount = mainDelayCount + mainSample * US;   // Main sample pulse count for Timer1
              unsigned int temp = (efeDelay - efeDelayOffset) * US;
              efeDelayCount = txOnCount + temp;                     // EFE sample delay count for Timer1
              efeSampleCount = efeDelayCount + efeSample * US;      // EFE sample pulse count for Timer 1
            
              txPeriodCount = txPeriod * US;         // TX period count for Timer1
            }
            
            
            void setup() {
            
              pinMode(txPin, OUTPUT);           // Set TX pin to output mode
              pinMode(mainSamplePin, OUTPUT);   // Set main sample pin to output mode
              pinMode(efeSamplePin, OUTPUT);    // Set EFE sample pin to output mode
              pinMode(boostPin, INPUT_PULLUP);  // Set Boost switch pin to input mode with pullup resistor
              calcTimerValues();                // Calculate all timer values
            
            /**  TIMER1 ***
              * URL: https://dbuezas.github.io/arduino-web-timers/#mcu=ATMEGA328P&timer=1&topValue=ICR1&ICR1=54661&CompareOutputModeA=set&timerMode=CTC&CompareOutputModeB=set&OCR1A=54613&OCR1B=27307&InterruptOnTimerOverflow=off&interruptA=on&interruptB=on&InterruptOnInputCapture=on
              * Mode     : CTC
              * Period   : 1 ms
              * Frequency: 1KHz
              * Outputs  :
              *  - OC1A: pin 9
              *  - OC1B: pin 10
              */
              noInterrupts();
              TCNT1 = 0;
              OCR1A = mainDelayCount;
              OCR1B = 1;
              ICR1  = txPeriodCount;
              TCCR1A = 1 << COM1A1 | 1 << COM1B1;
              TIMSK1 = 1 << OCIE1A |1 << OCIE1B;
              interrupts();
              TCCR1B = 1 << WGM13 | 1 << WGM12 | 1 << CS10;
            
              analogWrite(audioPin, 127);     // Set audioPin with 50% duty cycle PWM
            }
            ISR(TIMER1_COMPB_vect) {
            /* on OCR0B match - the TX signal */
              // This was the rising edge of the Tx signal, prepare the falling edge.
              OCR1B = txOnCount;
              TCCR1A &= ~(1 << COM1B0);    // clear Tx pin on compare match
              // Disable OCR1B interrupt
              TIMSK1 &= ~(1 << OCIE1B);  
              mode = MODE_SAMPLE_ON;
            }
            
            ISR(TIMER1_COMPA_vect) {
            /* on OCR0A match -  sample signals */
              switch (mode) {
            
                case MODE_SAMPLE_ON:
                // This was the falling edge of the mainSample signal.
                  //prepare the rising edge.
                  TCCR1A |= (1 << COM1A1) | (1 << COM1A0);   // set mainSample pin on compare match
                  OCR1A = mainSampleCount;
                  mode = MODE_SAMPLE_OFF;
                break;
            
                case MODE_SAMPLE_OFF:
                // This was the rising edge of the Tx signal, disconnect this PWM pin and set it HIGH
                  // prepare EF sample rising edge
                  OCR1A = efeDelayCount;
                  digitalWrite(mainSamplePin, syncDemodOff);    // pull OC2B high
                  TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0));   // disconnect OC2B from output
                  mode = MODE_EF_ON;
                break;
            
                case MODE_EF_ON:
                  //digitalWrite(efeSamplePin, syncDemodOn);    // Turn on EFE sample gate
                  PORTB &= 0xFE;
                  OCR1A = efeSampleCount;
                  mode = MODE_EF_OFF;
                break;
            
                case MODE_EF_OFF:
                  //digitalWrite(efeSamplePin, syncDemodOff);    // Turn off EFE sample gate
                  PORTB |= 0x01;
                  // Prepare falling edge of mainSample
                  OCR1A = mainDelayCount;
                  TCCR1A |= (1 << COM1A1);                       // clear mainSample pin on compare match
                  //Prepare the rising edge of the Tx signal
                  OCR1B = 1;
                  TCCR1A |= (1 << COM1B0);                       // set Tx pin on compare match
                  // Enable OCR1B interrupt
                  TIFR1 |= (1 << OCF1B);                         // clear flag;
                  TIMSK1 |= 1 << OCIE1B;                         // Enable interrupt
                  readPotCounter++;                              // Read delay pot counter
                  if (readPotCounter >= 100){                                // read pot counter 10 times per second
                    // Read potentiometer, mask out the bottom 3 jittery bits and double the range if selected
                    mainDelay = defMainDelay + (((analogRead(delayPin)) & 0x03F8)<<potRange)/US;  // new delay value
                    calcTimerValues();                                       // Calculate new timer values    
                    readPotCounter = 0;
                  }
                  mode = MODE_SAMPLE_ON;
              }
            }
            
            void loop() {
                      
            }​ 
            
            Attached Files

            Comment


            • #36
              Why are the efeSamplePin code commented?

              Comment


              • #37
                Originally posted by SaltyDog View Post
                Why are the efeSamplePin code commented?
                The efeSamplePin pin cannot be driven by PWM because Timer1 only has 2 PWM outputs. So it has to be driven manually.
                Instead of using digitalWrite() I use "PORTB &= 0xFE" to clear it and "PORTB |= 0x01" to set it. Directly write to the port with no intermediate libraries.
                Sort of a digitalWriteÜberUltraFast()

                Comment


                • #38
                  Ok, here is some feedback on your latest code. Firstly it all works as you intended with this build and no funny whistles on audio.

                  Secondly testing in the field with various metal types is quite different that the other versions of code. The MD will only respond to gold in a small section of the delay range. Previously it did not care where the delay was for gold, or other metals, within broad limits.

                  Which is probably good news, as now we have a way to discriminate for gold. For example, if I tune the delay for gold and the target goes away on higher settings of delay, then it IS gold. If it responds at both settings of delay, then it is likely NOT gold.

                  I would be interested to hear how you get on in REAL outside testing. I will do more testing when I can.

                  Comment


                  • #39
                    Originally posted by Teleno View Post
                    The efeSamplePin pin cannot be driven by PWM because Timer1 only has 2 PWM outputs. So it has to be driven manually.
                    Instead of using digitalWrite() I use "PORTB &= 0xFE" to clear it and "PORTB |= 0x01" to set it. Directly write to the port with no intermediate libraries.
                    Sort of a digitalWriteÜberUltraFast()
                    12-14 times faster, actually.
                    Port/Pin manipulation is always the better choice.
                    ...
                    What to say?
                    Somehow I neglected this topic and now when reading all the new posts; I am impressed, nice job people!
                    Well done!

                    Comment


                    • #40
                      Nice work. An obvious mod is to now tie the potRange to a switch on a spare digital input.

                      Comment


                      • #41
                        Originally posted by SaltyDog View Post
                        Ok, here is some feedback on your latest code. Firstly it all works as you intended with this build and no funny whistles on audio.

                        Secondly testing in the field with various metal types is quite different that the other versions of code. The MD will only respond to gold in a small section of the delay range. Previously it did not care where the delay was for gold, or other metals, within broad limits.

                        Which is probably good news, as now we have a way to discriminate for gold. For example, if I tune the delay for gold and the target goes away on higher settings of delay, then it IS gold. If it responds at both settings of delay, then it is likely NOT gold.

                        I would be interested to hear how you get on in REAL outside testing. I will do more testing when I can.
                        Nice teamwork! Huge shoutout to you for fearlessly rewiring your box and patiently testing multiple versions of my mod. Without your feedback I couldn't have ironed out the devilish details.

                        Curious about how the sensitivity with this mod compares to previous.

                        What you say about the delays and gold, it's probably time to populate the now deserted loop() with code to drive some cheap I2C OLED display from the Chinese supplier and visualize what the actual delays are. We then can tune the range in code.

                        Comment


                        • #42
                          Originally posted by Teleno View Post
                          Nice teamwork! Huge shoutout to you for fearlessly rewiring your box and patiently testing multiple versions of my mod. Without your feedback I couldn't have ironed out the devilish details.

                          Curious about how the sensitivity with this mod compares to previous.

                          What you say about the delays and gold, it's probably time to populate the now deserted loop() with code to drive some cheap I2C OLED display from the Chinese supplier and visualize what the actual delays are. We then can tune the range in code.
                          Couldn't you initially serial print to a laptop to see the values in a bench setting? If the pot value is enough then put a DVM on the wiper and measure using a 10 turn pot.

                          Comment


                          • #43
                            Originally posted by Gunghouk View Post

                            Couldn't you initially serial print to a laptop to see the values in a bench setting?
                            Certainly, now just send me some gold, haha!
                            Actually I've attached a 128x32 OLED and can see the delay in real time. My eyes are not so good anymore so I'm looking for big fonts. Will post the code when I'm satisfied with it.

                            Comment


                            • #44
                              Originally posted by Teleno View Post

                              Will post the code when I'm satisfied with it.
                              Will post some gold when I'm satisfied with your code

                              Comment


                              • #45
                                Another thought couldn't the audio frequency be tied to the delay setting?

                                You'll have to excuse any dumb or obvious statements I make as this is my first excursion into metal detecting and Arduinos as part of my search for relief from retirement boredom.

                                Comment

                                Working...
                                X