Close

Time measurement with CPLD

A project log for Magnetostrictive Position Sensor

Hard- and Software for a position measurement sensor with a magnet and a magnetostrictive wire.

florian-bFlorian B. 02/07/2025 at 13:300 Comments

Time measurement with CPLD

My initial approach for time measurement was to utilize the Input capture feature of the Arduino UNO's timer1.

Another option to measure the time is by utilizing a CPLD. With this I can measure up to signals of up to 4 signals. We can apply digital delays and signal debouncing. I am using an older but hobbyist friendly 5V tolerant and thus "Arduino compatible" EPM7128SLC84-15 for this. This device limits the acquisition to 4 Pulses with a maximum time count of 13 bits. With a more powerful device more bits and more signals can be measured but for my experiments this is just enough.

The following VHDL code implements such a time measurement counter.

A measurement is initiated with a High-Low-Transition of the TRIG Signal. This event will start a Time Counter. that is clocked with externally provided 16MHz clock pulses at COUNT_CLK. After a time delay it accepts Low signals at input SIG and records the time. Up to 4 signals can be measured with a resolution of 13 bits.

The Arduino reads the counter value in chunks of 4 bit nibbles adressed by A0 and A1. The data is available at the Data port D0..D3. Before the first read out READ_RES is set to Low and a negative Clock pulse is applied at READ_CLK. After all nibbles are read the read counter can be advanced by setting READ_INC to High and applying another READ_CLK pulse. Values are combined in the Arduino sketch.

Signals are:

-  COUNT_CLK   - Input for external clock Into CPLD

-  TRIG                 - Input for Trigger pulse

-  SIG                   - Input for digitized Signal pulse

-  READ_CLK      - Clock Input for read interface

-  READ_RES      - Reset Input for read interface (to reset to first measured signal)

-  READ_INC      - Increment Input for read interface (to advance to next measured signal)

- A0, A1               - Nibble select input to select the counter nibbles

- D0,D1,D2,D3    - Data output port to transmit data to the Arduino

    library IEEE;
    use IEEE.STD_LOGIC_1164.ALL;
    use IEEE.NUMERIC_STD.ALL;

    library IEEE;
    use IEEE.STD_LOGIC_1164.ALL;
    use IEEE.NUMERIC_STD.ALL;

    entity TimeMeasurementCounter is
         generic (
            data_width : integer := 13
         );
         Port (
              COUNT_CLK : in STD_LOGIC;                -- 16 MHz clock for time measurement
              READ_CLK : in STD_LOGIC;               -- Clock signal for micro controller read out
              READ_RES : in STD_LOGIC;               -- READ_RES='0' + one READ_CLK resets read counter
              READ_INC : in STD_LOGIC;               -- READ_INC='1' + one READ_CLK incements read counter
              TRIG : in STD_LOGIC;                   -- start the measurement
              SIG : in STD_LOGIC;                         -- signals 1-0-Transition to be measured
              A : in STD_LOGIC_VECTOR (1 downto 0);  -- Nibble address 
              D : out STD_LOGIC_VECTOR (3 downto 0)  -- DAta port for nibble
         );
    end TimeMeasurementCounter;

    architecture Behavioral of TimeMeasurementCounter is
         signal TC : STD_LOGIC_VECTOR (data_width downto 0) := (others => '0');     -- Timer Counter
         type reg_array is array (0 to 3) of STD_LOGIC_VECTOR (data_width downto 0);  
         signal  R : reg_array := (others => (others => '0'));              -- Register for measured times
         signal  RC : STD_LOGIC_VECTOR (2 downto 0) := (others => '0');     -- Index for registration process
         signal  RC2 : STD_LOGIC_VECTOR (1 downto 0) := (others => '0');    -- Index for readout process
         signal  sig_state: STD_LOGIC := '0';                               -- state is 1 to indicate that a signal was registered
                                                                                                  -- will go back to 0 when SIG gets back to 1
         constant DEAD_TIME : INTEGER := 80;                                -- Pulses registered after 40us * 16MHz
         constant DEGLITCH_TIME : INTEGER := 250;                                -- Pulses registered after 40us * 16MHz
         signal TCOLD : STD_LOGIC_VECTOR (data_width downto 0) := (others => '0'); 
    begin

         -- Counter and Signal registration process
         process (COUNT_CLK,SIG,sig_state)

         begin
              if rising_edge(COUNT_CLK) then
                    if  TRIG = '0'  then             -- Initialization and start of measurement
                         TC <= (others => '0');
                    else                                        -- Counting
                          TC <= std_logic_vector(unsigned(TC)+1);
                    end if;
                end if;
         end process;

         process (TRIG,SIG)
          variable rccounter : unsigned (2 downto 0);
         begin
                if  TRIG='0' then
                    RC <= (others => '0');
                    for i in 0 to 3 loop
                      R(i) <= (others => '0');
                    end loop;
                    TCOLD <= TC;
                elsif falling_edge(SIG) and (unsigned(TC)-unsigned(TCOLD)) > DEGLITCH_TIME and unsigned(TC) > DEAD_TIME  then  -- Signal registration
                    rccounter := unsigned(RC);
                    if rccounter < 4 then   -- register up to 4 signals
                      R(to_integer(unsigned(RC))) <= std_logic_vector(unsigned(TC));
                      rccounter := rccounter+1;
                      RC <= std_logic_vector(rccounter);
                      TCOLD <= TC;
                    end if;
                end if;
                    
         end process;
                 
                        
         -- Readout process             
         process (READ_CLK)                    --- is clocked with a READ_CLK
        --        variable X : STD_LOGIC_VECTOR (15 downto 0) := (others => '0');
         begin
          if falling_edge(READ_CLK) then
             if READ_RES='0' then                    --- Reset the Read Counter
                  RC2 <= (others => '0');
             elsif READ_INC ='1' then                --- Increment Read Counter
                  RC2 <= std_logic_vector(unsigned(RC2) + 1);
              end if;
            end if;
          --- Copy adressed Nibble into D
          -- X(data_width downto 0) := R(to_integer(unsigned(RC2)));
          -- D <= X(4 * to_integer(unsigned(A)) + 3 downto 4 * to_integer(unsigned(A)));
            case A is
                when "00" =>
                     D(3 downto 0) <=     R(to_integer(unsigned(RC2)))(3 downto 0);
                when "01" =>
                     D(3 downto 0) <=     R(to_integer(unsigned(RC2)))(7 downto 4);
                when "10" =>
                     D(3 downto 0) <=     R(to_integer(unsigned(RC2)))(11 downto 8);
                when "11" =>
                     D(0 downto 0) <=     R(to_integer(unsigned(RC2)))(12 downto 12);
                     D(3 downto 1) <= "000";
                when others =>
                     D <= "0000";
            end case;
         end process;    
         
    end Behavioral;

The following Code snippet shows how read-out is done in Arduino:

long readCounterNibble(int n)
{
  long x = 0;
  switch(n)
  {
    case 0: digitalWrite(AD0,0); digitalWrite(AD1,0); break;
    case 1: digitalWrite(AD0,1); digitalWrite(AD1,0); break;
    case 2: digitalWrite(AD0,0); digitalWrite(AD1,1); break;
    case 3: digitalWrite(AD0,1); digitalWrite(AD1,1); break;
  }  delayMicroseconds(10);
  digitalWrite(READ_INC,LOW);
  digitalWrite(READ_RES,HIGH);
  delayMicroseconds(10);
  clockLow(READ_CLK);
   digitalWrite(READ_INC,LOW);

  if(digitalRead(A0)) x |= 1;
  if(digitalRead(A1)) x |= 2;
  if(digitalRead(A2)) x |= 4;
  if(digitalRead(A3)) x |= 8;
 
  return x;
}

long readCounter(int ch)
{
  long c = 0;
  //  |
  c = readCounterNibble(0)  | (readCounterNibble(1) << 4 ) |  (readCounterNibble(2) << 8) | (readCounterNibble(3) <<12);
  return c ;
}

The measurement values are registered in 4 channels and analyzed in a "Quartet tracker".

The following snippet shows the  "Quartet tracker" which tracks a complete or incomplete quartet of pulses.

#define NMEAS 4

#define AUTOTUNE_DT
#define AUTOTUNE_TM

typedef struct {
    long val;
    long lastval;
    int upd;
} Channel;

typedef struct {
  long dt,tm;
  Channel * channels;
  int astate=0;
  long value;
} Measurement;

/*
 * Quartett-Tracker
 */
int analyze4( long * signal, Measurement *meas)
{
      int signal_length;
      long value=0;
      int i;
         
        
        signal_length=i;

        if (meas->astate == 0 && signal_length >= 4) { // Capture mode
            for (int i = 0; i < signal_length; i++) {
                for (int j = i + 1; j < signal_length; j++) {
                    if (signal[i] > 0 && signal[j] > 0) {
                        if (abs(signal[j] - signal[i] - meas->dt) < EPS) {
                            if (signal[i] < meas->tm && signal[j] < meas->tm) {
                                meas->channels[0].val = signal[i];
                                meas->channels[1].val = signal[j];
                                meas->astate = 1;
                            }
                            if (signal[i] >= meas->tm && signal[j] >= meas->tm && abs(2 * meas->tm - meas->channels[0].val - signal[j]) < EPS) {
                                 meas->channels[2].val = signal[i];
                                 meas->channels[3].val = signal[j];
                                if (meas->astate == 1) {
                                    meas->astate = 2;
                                  //printf("Locked at %d\n",t);
                                }
                            }
                        }
                    }
                }
            }
            if (meas->astate != 2) {
                for (int i = 0; i < 4; i++) {
                    meas->channels[i].val = 0;
                }
                meas->astate = 0;
            }

        } else if (meas->astate == 2) { // Tracking mode
            backup(meas->channels);
            int k = -1;
            for (int j = 0; j < 4; j++) {
                long delta=10000;
                // Assign best matching signals to channels
                for (int i = 0; i < signal_length && signal[i] !=0; i++) {
                    if (abs(meas->channels[j].lastval - signal[i]) < EPS2) {
                        int found=0;
                        
                        // avoid double assignment
                        for(int p=i-1; p>0; p--)
                        {
                          if( meas->channels[p].val==signal[i] )
                          {
                            found=1;
                          }
                        }
                        if( found==0 ) 
                        {
                          if((i<2 && signal[i]<meas->tm) ||(i>1 && signal[i]>=meas->tm)) { 
                            if( meas->channels[j].upd==0) 
                            {
                              meas->channels[j].val = signal[i]; // channel not updated => assign Signal value
                              meas->channels[j].upd = 1;         // mark updated
                              k = j;
                            }
                            else // channel already updated => assign Signal if it is nearer to last value:
                            {
                              long ndelta = abs(meas->channels[j].lastval - signal[i]);
                              if( ndelta < delta) // signal is closer to last value
                              {
                                meas->channels[j].val = signal[i]; // assign signal value
                                meas->channels[j].upd = 1;
                                k = j;
                                delta=ndelta;  // this is now the best assignment
                              }
                            }
                            
                          }
                        }
                       
                    }
                }
            }
            // Autotune dt and tm
            #ifdef AUTOTUNE_DT
            if(meas->channels[0].upd==1 && meas->channels[1].upd==1)
            {
              long dtn = meas->channels[1].val - meas->channels[0].val;
              if( abs(meas->dt - dtn) < EPS)
                meas->dt = meas->dt *0.6 + dtn*0.4;
            }
             if(meas->channels[3].upd==1 && meas->channels[2].upd==1)
            {
              long dtn = meas->channels[3].val - meas->channels[2].val;
              if( abs(meas->dt - dtn) < EPS)
                meas->dt = meas->dt *0.6 + dtn*0.4;
            }
            #endif
            #ifdef AUTOTUNE_TM
            if(meas->channels[0].upd==1 && meas->channels[3].upd==1)
            {
              long tmn = 0.5*(meas->channels[0].val + meas->channels[3].val);
              if( abs(meas->tm - tmn) < EPS)
                meas->tm = meas->tm *0.6 + tmn*0.4;
            }
             if(meas->channels[0].upd==1 && meas->channels[1].upd==1)
            {
              long tmn = 0.5*(meas->channels[0].val + meas->channels[1].val);
              if( abs(meas->tm - tmn) < EPS)
                meas->tm = meas->tm *0.6 + tmn*0.4;
            }
            #endif
            if(debugmode==1)
            {
              //Serial.print(" DT:"); Serial.print(meas->dt); Serial.print(" ");  
           //   Serial.print(" TM:"); Serial.print(meas->tm); Serial.print(" ");  
            }
            if (k >= 0) {
              // Reconstruct the missign signals:
                for (int j = 0; j < 4; j++) {
                    if (j != k) {
                        if (k == 0) {
                            if (j == 1 && meas->channels[j].upd == 0) {
                                meas->channels[j].val = meas->channels[k].val + meas->dt;
                            }
                            if (j == 2 && meas->channels[j].upd == 0) {
                                meas->channels[j].val = 2 * meas->tm - meas->channels[k].val - meas->dt;
                            }
                            if (j == 3 &&  meas->channels[j].upd == 0) {
                                meas->channels[j].val = 2 * meas->tm - meas->channels[k].val;
                            }
                        } else if (k == 1) {
                            if (j == 0 &&  meas->channels[j].upd == 0) {
                                meas->channels[j].val = meas->channels[k].val - meas->dt;
                            }
                            if (j == 2 &&  meas->channels[j].upd == 0) {
                                meas->channels[j].val = 2 * meas->tm - meas->channels[k].val;
                            }
                            if (j == 3 &&  meas->channels[j].upd == 0) {
                                meas->channels[j].val = 2 * meas->tm - meas->channels[k].val + meas->dt;
                            }
                        } else if (k == 2) {
                            if (j == 0 && meas->channels[j].upd == 0) {
                                meas->channels[j].val = 2 * meas->tm - meas->channels[k].val - meas->dt;
                            }
                            if (j == 1 && meas->channels[j].upd == 0) {
                                meas->channels[j].val = 2 * meas->tm - meas->channels[k].val;
                            }
                            if (j == 3 && meas->channels[j].upd == 0) {
                                meas->channels[j].val = meas->channels[k].val + meas->dt;
                            }
                        } else if (k == 3) {
                            if (j == 0 && meas->channels[j].upd == 0) {
                                meas->channels[j].val = 2 * meas->tm - meas->channels[k].val;
                            }
                            if (j == 1 &&  meas->channels[j].upd == 0) {
                                meas->channels[j].val = 2 * meas->tm - meas->channels[k].val + meas->dt;
                            }
                            if (j == 2 &&  meas->channels[j].upd == 0) {
                                meas->channels[j].val = meas->channels[k].val - meas->dt;
                            }
                        }
                    }
                }
               
            }
            else
            {
             // astate=0;
            }
        }
       // if(debugmode==1) printc(meas->channels);
        
    value= meas->channels[1].val;
    /**
     * Fallback für unlocked
     */
    if(meas->astate==2 || meas->channels[1].upd)
    {
      value= meas->channels[1].val;
    }
    else if(meas->channels[2].upd)
    {
      value = 2*meas->tm-meas->channels[2].val;
    }
    else if(meas->channels[3].upd)
    {
     value= 2*meas->tm-meas->channels[3].val -meas->dt;
    }
    else if(meas->channels[0].upd)
    {
     value=meas->channels[0].upd+meas->dt;
    }     
    //if(debugmode==1)
    //  Serial.print(" STATE:"); Serial.print(meas->astate);
    meas->value=value; 
    return value;
}


How does it work?

The signal must be a quartet of 2 doublets, one original and one reflected.

Pulses are assigned to the matching quartet pulses. When moving the
magnet away from the receiver coil the times of the first dublet will
rise and the times of the second one will decrease.

Note:

1. The quartet tracker requires initially one quartet for locking in. Once locked, individual signals may be missing. The quartet tracker assigns the measurements to the respective peak channel and reconstructs missing signals. No values are output before locking. To compensate the Arduino Sketch has a analyze1 method which analyzes only the first or second pulse.


2. The DT and TM values must be determined once and stored in the program. For determination, Printmode (with p) and Debug-Mode (with d) must be activated. 

From the outputs: VAL:<val> X1:<T1> X2:<T2> X3:<T3> X4:<T4>

DT: Distance of the peaks in a doublet: DT = <T2>-<T1> = <T4>-<T3>

TM: "Length" of the wire: <T3>=2TM-<T2> or <T4>=2TM-<T1>

Issues:

Here you can see how the Quartet Analyzer is reconstructing incomplete Quartets. In the middle time range all four signals are present. However the analyzer can reconstruct the VAL-signal (magenta) reliably even if raw signals are missing.

Discussions