Announcement

Collapse
No announcement yet.

Comparator trick - hysteresis (Arduino Uno/Nano)

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

  • Comparator trick - hysteresis (Arduino Uno/Nano)

    The analog comparators in popular AVR controllers Atmega328P, Attyin85 etc. don't have any hysteresis which makes them quite useless (bouncing output on slow/noisy signals).

    The solution I present here uses a few external resistors and a simple interrupt routine. No RAM is used and the overhead is negligible if the assembler ISR is used.
    The exmple code implements a relaxation oscillator experiment that can be set up very quickly.

    Arduino IDE instructions:
    - create a new sketch.
    - delete all the text in (sketch name).ino
    - Click the tree dots (top right) and then on "New File"
    - Name the file as "main.c" and paste the code listed below.

    Brief explanation: The comparator interrupt is enabled on both edges (toggle). The interrupt handler sends the comparator state to pin PB0 as either a low level (comp. low) or a high impedance level (comp. high). This output is connected to the comparator "-" input (AIN0 - pin 6) via a resistive feedback network. The signal (a charging/discharging capacitor in this case) is fed to the comparator "+" input (AIN1 - pin 7).

    The attached LTSpice file contains the equivalent electronic circuit using an open collector/drain comparator such as the LM311. The schematic of an Arduino setup is in the same file.

    I'm using this technique to design a minimalistic PI that I will be disclosing in further posts.

    Atmega328P_Comparator_Hysteresis.zip

    Click image for larger version  Name:	hysteresis_setup.png Views:	0 Size:	82.8 KB ID:	418106

    Click image for larger version  Name:	scope.png Views:	0 Size:	386.7 KB ID:	418105

    PHP Code:
    /* ============================================================ */
    /*                                                                            
      Atmega328P_Comparator_Hysteresis.c                                    
     (c) 2023 Oscar Gonzalez                                                

     Simulation of an LM311 type comparator (open collector) with positive  
     feedback for hysteresis. (See LTSpice circuit of the same name).  
                                                                              
     Comment out statement "#define _USE_ASSEMBLY_INTERRUPTS_" to use the C
      version of the interrupt  
     */
    /* ============================================================ */
     #define F_CPU 16000000L

    //gcc 7.4.0

    #include <avr/io.h>
    #include <avr/interrupt.h>
    #include <stdint.h>

    #define _USE_ASSEMBLY_INTERRUPTS_

    void init_comp(){  

      
    // PB1 (arduino Pin 10) drives the capacitor
        
    DDRB |= (<< PB1);
        
    // PB0 (arduino pin 9) outputs the comparator state.
        // Open collector output is simulated as follows:
        //  - write port bit to 0 permanently
        //  - output low: conf. pin as output DDRB |= (1 << PB0)
        //  - output high impedance: conf.pin as input DDRB | ~(1 << PB0)
        
    PORTB &= ~(<< PB0) ;

        
    // disable digital on comparator inputs
        
    DIDR1 | (<< AIN1D) | (<< AIN0D);

        
    //Enable comparator interrupt (on toggle by default)
        
    ACSR |= (<< ACI); // clear interrupt flag
        
    ACSR |= (<< ACIE);
    }

    void main(){
      
    cli();
      
    init_comp();
      
    sei();
      
    // start charging the capacitor through PB1 (pin 10)
      
    PORTB |= (<< PB1);
      while(
    1);
    }

    #ifdef _USE_ASSEMBLY_INTERRUPTS_

    ISR(ANALOG_COMP_vectISR_NAKED) {

      
    // As "naked" this interrupt does not push any registers for low latency.
      // This is possible because none of the instructions alter the status register
      // or any registers used by the C compiler.

      
    asm volatile(
        
    "IN __tmp_reg__, %2\n"
        "SBRC __tmp_reg__, 5\n"
        "RJMP L1\n"
        "SBI %0, 0\n"  
    // comp. output low
        
    "CBI %1, 1\n"  // capacitor drive low
        
    "RETI\n"
        "L1: CBI %0, 0\n"   
    // comp. output to high impedance
        
    "SBI %1, 1\n"       // capacitor drive high
        
    "RETI\n"
      
    :: "I" (_SFR_IO_ADDR(DDRB)), "I" (_SFR_IO_ADDR(PORTB)), "I" (_SFR_IO_ADDR(ACSR))
      );
    }

    #else

    ISR(ANALOG_COMP_vect) {

      
    // Send comp output to PB0 as open collector signal
      
    uint8_t comp_state ACSR & (<< ACO);

      if (
    comp_state == 0){
        
    DDRB |= (<< PB0);    // comp. output low
        
    PORTB &= ~(<< PB1);  // capacitor drive low
      
    }
      else  {
        
    DDRB &= ~(<< PB0);   // comp. output to high impedance
        
    PORTB |= (<< PB1);   // capacitor drive high
      
    }
    }

    #endif 

  • #2
    In this second experiment the charge in the capacitor will be held for a period of time at the lower threshold of the comparator.

    Click image for larger version

Name:	Charge_and_hold.jpg
Views:	164
Size:	139.2 KB
ID:	418135

    The code uses Timer1 to release the capacitor from the hold state every millisecond.

    PHP Code:
    /* ============================================================ */
    /*                                                                            
      Atmega328P_Comparator_Hysteresis.c                                    
     (c) 2023 Oscar Gonzalez  
                                                
     Simulation of an LM311 type comparator (open collector) with positive  
     feedback for hysteresis. (See LTSpice circuit of the same name).

     In this experiment we will place the capacitor on hold after the partial
     discharge to the lower threshold voltage and keep it there for 1 millisecond.
     For this we use Timer1.

     This experiment uses the same setup of LTSpice file
      "Atmega328P_Comparator_Hysteresis.asc".
                                                                            
     Comment out statement "#define _USE_ASSEMBLY_INTERRUPTS_" to use the C
      version of the interrupt  
    */
    /* ============================================================ */
    #define F_CPU 16000000L

    #define _USE_ASSEMBLY_INTERRUPTS_

    //gcc 7.4.0

    #include <avr/io.h>
    #include <avr/interrupt.h>
    #include <stdint.h>

    #define TIMER1_PERIOD 16000UL
                                  
    void init(){  

        
    // PB1 (arduino Pin 10) drives the capacitor
        
    DDRB |= (<< PB1);
        
    // PB0 (arduino pin 9) outputs the comparator state.
        // Open collector output is implemented as follows:
        //  - write port bit to 0 permanently
        //  - output low: conf. pin as output DDRB |= (1 << PB0)
        //  - output high impedance: conf.pin as input DDRB | ~(1 << PB0)
        
    PORTB &= ~(<< PB0) ;

        
    // disable digital on comparator inputs
        
    DIDR1 | (<< AIN1D) | (<< AIN0D);

        
    //Enable comparator interrupt (on toggle by default)
        
    ACSR |= (<< ACIE);
        
    ACSR |= (<< ACI); // clear interrupt flag

        // Timer1 releases the capacitor from a hold state after a given
        // period of time.

        // Timer 1, CTC mode, ICR1 controls resolution (TOP value)
        
    TCCR1B |= (<< WGM13) | (<< WGM12);
        
    // set the TOP value
        
    ICR1 =  TIMER1_PERIOD;
        
    //enable interrupt at IC compare match
        
    TIMSK1 |= (<< ICIE1);
        
    // start timer 1 no prescaling
        
    TCCR1B |= (<< CS10);
    }
    void main(){
      
    cli();
      
    init();
      
    sei();
      
    PORTB |= (<< PB1) ;
      while(
    1);
    }
    #ifdef _USE_ASSEMBLY_INTERRUPTS_  
      
      
    ISR(ANALOG_COMP_vectISR_NAKED) {
        
    // None of these instructions alters the status register
        
    asm volatile(
          
    "IN __tmp_reg__, %2\n"
          "SBIC __tmp_reg__, 5\n"
          "RJMP COMP_HIGH\n"
          "COMP_LOW:\n"
          "  SBI %0, 0\n"
          "  CBI %1, 1\n"
          "  RETI\n"
          "COMP_HIGH:\n"
          "  CBI %0, 0\n"
          "  CBI %0, 1\n"
          "  RETI\n"
        
    :: "I" (_SFR_IO_ADDR(DDRB)), "I" (_SFR_IO_ADDR(PORTB)), "I" (_SFR_IO_ADDR(ACSR))
        );
      }

      
    ISR(TIMER1_CAPT_vectISR_NAKED) {
        
    // release outputs (from tri-state)
        
    asm volatile(
          
    "SBI %0, 1\n"
          "SBI %1, 1\n"
          "RETI\n"
        
    :: "I" (_SFR_IO_ADDR(DDRB)), "I" (_SFR_IO_ADDR(PORTB))
        );
      }

    #else

      
    ISR(ANALOG_COMP_vect) {

        
    // Send comp output to PB0 as open collector signal
        
    uint8_t comp_state ACSR & (<< ACO);

        if (
    comp_state == 0){
          
    DDRB |= (<< PB0);    // comp. output low
          
    PORTB &= ~(<< PB1);  // capacitor drive to high impedance
        
    }
        else  {
          
    DDRB &= ~(<< PB0);   // comp. output to high impedance
          
    DDRB &= ~(<< PB1);   // capacitor drive to high impedance (Hold)
        
    }
      }

      
    ISR(TIMER1_CAPT_vect) {

        
    // release PB1 output from tri-state
        
    DDRB |= (<< PB1);
        
    PORTB |= (<< PB1);
      }
    #endif


    ​ 

    Comment


    • #3
      Teleno - the link to your LTSpice simulation (zip file) is broken.
      It produces a message: "Invalid Page URL. If this is an error and the page should exist, please contact the system administrator and tell them how you got this message."

      Comment


      • #4
        Originally posted by Qiaozhi View Post
        Teleno - the link to your LTSpice simulation (zip file) is broken.
        It produces a message: "Invalid Page URL. If this is an error and the page should exist, please contact the system administrator and tell them how you got this message."
        Let me try to upload it again...

        Atmega328P_Comparator_Hysteresis.zip

        Comment


        • #5
          Here's the schematic of the PI driver and preamp that motivated this work.

          C1 is charged to a given voltage through a buck converter for efficiency, then discharged to a second lower voltage through the TX coil, then put on hold.
          Ths hold voltage provides the analog reference for the preamp.
          The amplified signal is then referred to ground before being passed to the ADC fo an Arduino board.
          Gain is low, around 30, for direct sampling.

          The driving logic is to be replaced by the internal comparator of an Atmega328P (Arduino Uno/Micro) and associated code.

          By looking at V(com) in the plot you will immedialtely understand the relationship with the two experiments above.


          Click image for larger version  Name:	PI_buck_schematic.png Views:	0 Size:	47.8 KB ID:	418146

          Click image for larger version  Name:	PI_buck_plot.png Views:	0 Size:	30.2 KB ID:	418148

          Comment


          • #6
            Originally posted by Teleno View Post

            Let me try to upload it again...

            [ATTACH]n418139[/ATTACH]
            When I try to open the simulation in LTSpice it fails with a DDE error, and a whole bunch of missing files. I'm not sure what these are, but they might be Visual Basic or Javascript.
            Is this because I'm trying to open this in LInux, or are there some files not included in the zip?

            Comment


            • #7
              Must be the Linux, it opens up fine for me (Win10).
              I just installed the latest Ltspice version and it opens up fine.
              (boring update in progress though)


              Click image for larger version

Name:	image.png
Views:	135
Size:	275.3 KB
ID:	418151

              Comment


              • #8
                Originally posted by Qiaozhi View Post

                When I try to open the simulation in LTSpice it fails with a DDE error, and a whole bunch of missing files. I'm not sure what these are, but they might be Visual Basic or Javascript.
                Is this because I'm trying to open this in LInux, or are there some files not included in the zip?
                What about the simulation file in post #5, does it open?

                Comment


                • #9
                  Opens up here too:

                  Click image for larger version

Name:	image.png
Views:	122
Size:	113.0 KB
ID:	418162

                  Comment


                  • #10
                    OK - I've discovered the problem.
                    If you right-click on the asc file and send it to LTSpice, you get a DDE failure; but if you select the asc file from within LTSpice then everything works as expected.

                    Comment


                    • #11
                      Next step: measure the timing of the analog waveform, average the measurements and use them to synthesisze the waveform without flicker.

                      - Set up the analog comparator and pins to create the analog waveform (as previously)
                      - Measure the timestamp of every rising/falling edge of the comparator. Low-pass filter the measurements.
                      - Disconnect the comparator. Store the filtered timestamps in compare registers OCR1A, OCR1B of Timer1 to generate the pin signals that replicate the analog waveform.

                      PHP Code:
                      /* ============================================================ */
                      /*                                                                            
                        PI_charge_hold_capture.c                                    
                       (c) 2023 Oscar Gonzalez  
                                                                
                       Simulation of an LM311 type comparator (open collector) with positive  
                       feedback for hysteresis. (See LTSpice circuit of the same name).

                       Like in the previous experiment, we will place the capacitor on hold after the partial
                       discharge to the lower threshold voltage and keep it there for 1 millisecond.
                       For this we use Timer1.

                       Additionally, we will measure the timestamps of the comparator's rising and falling edges
                       and use them to synthesize the analog waveform digitally for more stability and less flicker.

                       This experiment uses the same setup of LTSpice file
                        "Atmega328P_Comparator_Hysteresis.asc".
                      */
                      /* ============================================================ */

                      #define F_CPU 16000000L

                      //gcc 7.4.0

                      #include <avr/io.h>
                      #include <avr/interrupt.h>
                      #include <stdint.h>
                      #include <stdbool.h>
                      #include <string.h>
                      #include <stdio.h>
                      #include <util/delay.h>

                      #define TIMER1_RESOLUTION 16000UL
                      #define MICROSECOND 16
                      #define BAUD_RATE_9600 ((F_CPU / (16 * 9600UL) -1))

                      // output states
                      #define STATE_HIGH_IMPEDANCE DDRB &= ~((1 << PB1) | (1 << PB0))
                      #define STATE_OUTPUT_LOW  DDRB |= (1 << PB0); PORTB &= ~(1 << PB1)
                      #define STATE_RELEASE DDRB |= (1 << PB1); PORTB |= (1 << PB1)
                      #define COMP_OUTPUT_LOW  (ACSR & (1 << ACO)) == 0

                      // working modes
                      #define MODE_MEASURE_WAVE (ACSR & (1 << ACIE)) > 0

                      volatile uint16_t comp_fallingEdgecomp_risingEdge;
                      volatile edgesReady false;

                      void transmit(unsigned char data[]) {
                        
                      uint8_t i 0;
                        while (
                      data[i] != 0)
                        {
                          while (!( 
                      UCSR0A & (<< UDRE0))); /* Wait for empty transmit buffer*/
                          
                      UDR0 data[i];            /* Put data into buffer, sends the data */
                          
                      i++;                             /* increment counter           */
                        
                      }
                      }

                      void init_uart(){
                        
                      /* UART. Set Baudrate to 9600 bps */
                        // write to lower byte
                        
                      UBRR0L = (uint8_t)(BAUD_RATE_9600 0xFF);
                        
                      // write to higher byte
                        
                      UBRR0H = (uint8_t)(BAUD_RATE_9600 >> 8);
                        
                      UCSR0C 0x06;       /* Set frame format: 8data, 1stop bit  */
                        
                      UCSR0B = (<< TXEN0); /* Enable  transmitter */
                      }

                      void measure_timing(){  
                        
                      // PB1 = OC1A = Arduino Pin 9 as outputs the trigger signal
                          
                      DDRB |= (<< PB1);
                          
                      // PB0 is used as the comparator output.
                          // Open collector output is simulated as follows:
                          //  - write port bit to 0 permanently
                          //  - output low: conf. pin as output DDRB |= (1 << PB0)
                          //  - output high impedance: conf.pin as input DDRB | ~(1 << PB0)
                          
                      PORTB &= ~(<< PB0) ;

                          
                      //Connect comparator output to capture input of Timer1
                          
                      ACSR |= (<< ACIC);
                          
                      DIDR1 | (<< AIN1D) | (<< AIN0D); // disable digital on comp. inputs
                          
                      ACSR |= (<< ACI); // clear interrupt flag
                          
                      ACSR |= (<< ACIE); //interrupt enable (on toggle by default)

                          // Timer 1, CTC mode, OCR1A controls resolution
                          
                      TCCR1B |= (<< WGM12);
                          
                      // set the TOP value
                          
                      OCR1A =  TIMER1_RESOLUTION;
                          
                      //enable interrupt at OC1A compare match
                          
                      TIMSK1 |= (<< OCIE1A);                                
                          
                      TIFR1 |= (<< OCF1A); // clear compare flag
                          // start timer 1 no prescaling
                          
                      TCCR1B |= (<< CS10);
                      }

                      void apply_timing(){
                        
                      //Disonnect comparator output from capture input of Timer1
                        
                      ACSR &= ~(<< ACIC);
                        
                      ACSR &= ~(<< ACIE); //interrupt disable (on toggle by default)
                        
                      ACSR |= (<< ACI); // clear interrupt flag

                        // Timer 1, CTC mode, ICR1 controls resolution
                        
                      TCCR1B |= (<< WGM13) | (<< WGM12);
                        
                      //enable interrupt at compare match B (A already enabled) and IC
                        
                      TIMSK1 |= (<< OCIE1B) | (<< ICIE1);                                
                        
                      // set the TOP value
                        
                      ICR1 =  TIMER1_RESOLUTION;
                        
                      // timestamps of the edges
                        
                      OCR1A comp_fallingEdge;
                        
                      OCR1B comp_risingEdge;

                        
                      TCNT1 0;
                        
                      TIFR1 |= (<< OCF1B) | (<< OCF1A) | (<< ICF1); // clear both compare flags
                      }

                      void main(){
                        
                      cli();
                        
                      init_uart();
                        
                      measure_timing();
                        
                      sei();

                        
                      // start charging the capacitor
                        
                      PORTB |= (<< PB1);

                        
                      // allow 1 second for the oscillation to settle
                        
                      _delay_ms(1000);

                        
                      //wait till new timing measurements are available from interrupt handlers
                        
                      edgesReady false;
                        while(!
                      edgesReady);

                        
                      // initialize low-pass filter values, fixed point format 12.4
                        
                      uint16_t filteredRisingEdge comp_risingEdge << 4;
                        
                      uint16_t filteredFallingEdge comp_fallingEdge << 4;

                        
                      // filter 1000 measurements
                        
                      for (uint16_t i 01000i++){

                          
                      // wait for new measurements
                          
                      edgesReady false;
                          while(!
                      edgesReady);

                          
                      // filter new values
                          
                      filteredRisingEdge filteredRisingEdge - (filteredRisingEdge >> 4) + comp_risingEdge;
                          
                      filteredFallingEdge filteredFallingEdge - (filteredFallingEdge >> 4) + comp_fallingEdge;
                        }
                        
                      cli();

                        
                      // Return values to format 16.0
                        
                      comp_risingEdge filteredRisingEdge >> 4;
                        
                      comp_fallingEdge filteredFallingEdge >> 4;

                        
                      // send filtered timing values to serial
                        
                      unsigned char message[40];
                        
                      sprintf (message"fall/rise: %u usec / %u usec\n"comp_fallingEdge/16comp_risingEdge/16),
                        
                      transmit(message);

                        
                      // disconnect the analog comparator and generate the analog waveform by
                        // directly applying the filtered measured timing values.
                        // This synthesized waveform has less flicker than the analog waveform
                        // and is suitable for precisely driving a PI detector.
                        
                      apply_timing();
                        
                      sei();

                        
                      // a small visual effect
                        
                      while(1){
                          
                      ICR1 = (ICR1 350 MICROSECOND)? ICR1-10 1000 MICROSECOND;
                          
                      _delay_ms(10);
                        };
                      }
                        
                        
                      ISR(ANALOG_COMP_vect) {
                          
                      // Send comp output to PB0 as open collector signal
                          
                      if (COMP_OUTPUT_LOW) {
                            
                      STATE_OUTPUT_LOW;
                            
                      comp_fallingEdge ICR1;
                            
                      TCCR1B |= (<< ICES1);     // prepare Tim1 capture on positive edge
                          
                      }
                          else {
                            
                      STATE_HIGH_IMPEDANCE;
                            
                      comp_risingEdge ICR1;
                            
                      TCCR1B &= ~(<< ICES1);     // prepare Tim1 capture on negative edge
                            
                      edgesReady true;
                          }
                        }

                        
                      ISR(TIMER1_COMPA_vect) {
                          if( 
                      MODE_MEASURE_WAVE ){
                            
                      STATE_RELEASE;
                          }
                          else {
                            
                      STATE_OUTPUT_LOW;
                         }
                        }

                        
                      ISR(TIMER1_COMPB_vect) {
                          
                      STATE_HIGH_IMPEDANCE;
                        }

                        
                      ISR(TIMER1_CAPT_vect) {
                          
                      STATE_RELEASE;
                        } 

                      Comment


                      • #12
                        I see you are an expert in programming, can you add phase shift to this program ?
                        HTML Code:
                        void setup()
                        {
                            Serial.begin(9600);
                            TCCR2A = 0x00;  //always reset
                            TCCR2B = 0x00; //always reset
                            TCCR2A = (1<<COM2A1)|(1<<COM2B1) | (0<<WGM21)|(1<<WGM20); //Mode-1
                            TCCR2B = (1<<CS22)|(1<<CS21)|(0<<CS20)|(0<<WGM22); //Mode-1 and prescale 256
                            pinMode(11, OUTPUT);  //Ch-A
                            pinMode(3, OUTPUT);     //Ch-B
                         
                        }
                        
                        void loop()
                        {
                        
                        }​

                        Comment


                        • #13
                          Originally posted by pito View Post
                          I see you are an expert in programming, can you add phase shift to this program ?
                          HTML Code:
                          void setup()
                          {
                          Serial.begin(9600);
                          TCCR2A = 0x00; //always reset
                          TCCR2B = 0x00; //always reset
                          TCCR2A = (1<<COM2A1)|(1<<COM2B1) | (0<<WGM21)|(1<<WGM20); //Mode-1
                          TCCR2B = (1<<CS22)|(1<<CS21)|(0<<CS20)|(0<<WGM22); //Mode-1 and prescale 256
                          pinMode(11, OUTPUT); //Ch-A
                          pinMode(3, OUTPUT); //Ch-B
                          
                          }
                          
                          void loop()
                          {
                          
                          }​
                          If you open a new thread and explain what you're trying to do I might be able to help..Preferably with a timing diagram of the signals you want to generate.

                          Comment


                          • #14
                            Originally posted by ivconic View Post
                            Must be the Linux, it opens up fine for me (Win10).
                            I just installed the latest Ltspice version and it opens up fine.
                            (boring update in progress though)


                            Click image for larger version  Name:	image.png Views:	37 Size:	275.3 KB ID:	418151
                            Errata:

                            These connections in the schematic are wrong

                            PB0 should be pin 8 (not pin 9)
                            PB1 should be pin 9 (not pin 10)

                            Comment

                            Working...
                            X