Detecting Zero-Crossings

Posted on
  • Hi,

    I'm developing a system to read LTC timecode (cf wiki, Good Article), which is an audio signal (I'd guess between .32V and .77V RMS). TL;DR the links, it's a binary pattern in Bi-phase mark coding. I want to sense the period between zero crossings to decode the signal. Will reading a Pin in "both" mode:

    setWatch( BMCman.tick, A0, {repeat:true, edge:'both'} ) ;
    

    have the desired result, or should I use a Zero-Crossing detection circuit to condition the signal to a rising edge before sensing it?

    Thanks.

  • If the voltage really does drop below zero, your best bet is to 'bias' it up so the average voltage is at the point at which Espruino's input flips between a 0 and a 1:

                  3.3v
                   |
                  _|_
                 |   |
                 |   |  10k Ohm
                 |   |
                 |___|
                   |
    IN    | |      |
    ------| |------|------------ Espruino Pin
          | |      |
                  _|_
        10uF     |   |
                 |   |  10k Ohm
                 |   |
                 |___|
                   |
                   |
                  GND
    

    Those values are just total guesses, but they should work. If you have some kind of oscillocope it'd be a massive help so you can see what's going on. If that doesn't work, you may have to resort to using a separate comparator.

    At that point you should be able to use setWatch like you suggest though. Just 2 potential issues:

    • In setWatch( BMCman.tick, A0, {repeat:true, edge:'both'} ) ;, the this value won't be correct when you call BMCman.tick - you actually need BMCman.tick.bind(BMCman) to force it.

    • Looking at that document, the time code can be about 2.4kHz - which is right on the limit of what Espruino can handle in normal JS, so you'll have to be super careful to get your code to run quickly... Possibly resorting to using espruino.com/Compilation for the watch handler.

    Something like the following might be a good start though:

    function start() {
      var d,c = "";
      setWatch( function(e) {d=e.time-e.lastTime>0.00075;if(d||e.sta­te)c+=1^d;}, A0, {repeat:true, edge:'both'} ) ;
      setInterval(function() {
        var sync = c.indexOf("0011111111111101");
        // ideally you'd use 'sync' for re-syncing here
        console.log(c);
        c="";
      }, 500);
    }
    

    The watch handler is pretty cryptic because it's trying to be fast to execute, but:

    • Using local variables to try and keep the speed up
    • d is true if it's a long pulse
    • if it's a short pulse and it's low, we ignore it (so we don't get 2 bits for the double-transition)
    • otherwise we negate d (short pulses are 1, long are 0) and stick it on the end of the string c
    • every so often we check for the sync bits - we don't do it that often because we want the bits to be handled first.
  • Thanks Gordon,

    I've been developing the idea fully in JS, but from the outset I suspected I'd have to compile it, or perhaps bash it out in ASM.

    function BiPhaseDecoder() {
      // gaps
      this.last_B_period = 0.0 ;
      this.this_period   = 0.0 ;
      this.big_periods   = 0.0 ;
      this.small_periods = 0.0 ;
      // FSM
      this.in_long_gap   = false ;
      this.in_first_half = false ;
      // booting
      this.till_booted   = 32 ; // arbitrary
      // The bit I'm decoding
      this.bit           = -1 ;
    
      // register Event
      this.bitEvent = function(){};
    }
    
    // process
    BiPhaseDecoder.prototype.tick = function( event_data ) {
      // get the time since last crossing
      this.this_period = (event_data.time - event_data.lastTime) ;
      // determine if it's a long or short period
      // cf: http://www.avrfreaks.net/forum/tut-pc-av­r-softunderstanding-bi-phase-mark-coding­
      // "If it's more than 3/4 of the bit period, you've just received a zero;
      // otherwise, you got half of a one."
      // Note: by only ever comparing current period to long-ifyied last period, we
      // can tolerate increases in speed of transmission, if gradual; and need no
      // a priori data on expected rate / samps per sec.
      if( (this.this_period * 4.0) > (this.last_B_period * 3.0) ) {
        // this is a long period
        this.last_B_period = this.this_period ;
        this.in_long_gap   = true ;
        // get stats
        this.big_periods += this.this_period ;
        this.big_periods *= 0.5 ;
      } else {
        // otherwise a short period
        this.last_B_period = this.this_period * 2.0 ; // last_B_period is always big
        this.in_long_gap   = false ;
        // get stats
        this.small_periods += this.this_period ;
        this.small_periods *= 0.5 ;
      } // if this_period > (3/4 * last_B_period)
    
      // a long gap == bit 0; or two short gaps == bit 1
      if( this.in_long_gap ) {
        this.in_first_half = false ;
        this.bit = 0 ;
      } else {
        if (this.in_first_half) {
          this.in_first_half = false ;
          this.bit = 1 ;
        } else {
          this.in_first_half = true ;
          this.bit = -1 ; // not ready to TX yet
        }
      } // if in_long_gap
      
      if( this.till_booted < 0 ) {
        // emit the bit if ready
        if( (this.bit >= 0) ) {
          this.bitEvent( this.bit ) ;
        }
      } else {
        // it will take some time to get a good sampling of big and small periods
        this.till_booted-- ;
        // keep flushing the stats till it stabilizes
        this.big_periods   = 0.0 ;
        this.small_periods = 0.0 ;
        // possibly, 'if( Math.abs( (small * 2.0) - big ) < 1e-5 ) till_booted = -1 ;'
      } // if booted
    } ;
    

    Which is pretty heavy, but easier to debug. This method makes no assumptions about the short bit period, and would tolerate a 'ripple' in the transmission rate. First step would be minifying it I guess, and maybe ditching the avg. gap length I'm keeping.

    I'm then pushing the 'bit' into a Uint16Array - like a LIFO - lots of shifts and carrys (probably better in ASM too) then it's just 'bitfield[4] & 0x3FFD' to find the sync word.

    I didn't know about the x.y.bind(x) gotcha - thanks for that.

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Detecting Zero-Crossings

Posted by Avatar for BitMeddler @BitMeddler

Actions