10
$\begingroup$

I'm trying to find the zero crossings of a sine wave to transform the sine wave into a square wave. The only problem is that the sine wave is noisy, so I'm getting lots of jitter and false zero crossings.

Can anyone recommend any simple psuedocode or relevant materials? So far I have something like this:

if (sample[i]>0 && sample[i+1]<0) || (sample[i]<0 && sample[i+1]>0)

Can anyone recommend a more robust method?

$\endgroup$
2
  • $\begingroup$ What is the purpose of you trying to make it a square wave? Are you trying to find out where the signal starts and ends? If you are I can recommend a method. $\endgroup$
    – Spacey
    Commented Nov 2, 2012 at 18:05
  • $\begingroup$ if((sample[i]*sample[i+1])<0) zero_crossing++; $\endgroup$ Commented Apr 2, 2017 at 17:53

4 Answers 4

8
$\begingroup$

You could try low-pass filtering the input signal to get smoother zero-crossings (or even band-pass filtering if you have a good idea of the frequency location of the sine wave). The risk is that if sample-accurate phase information is essential to your application, the additional lag from the filter might be a problem.

Another approach: instead of trying to transform the sine wave into a square wave, what about getting an independent square wave oscillator to align itself in phase/frequency with the sine wave? This can be done with a phase-locked loop.

$\endgroup$
0
6
$\begingroup$

What you've shown certainly is a zero-crossing detector. A couple things come to mind that might improve your situation:

  • If you have noise that is outside the band of your signal (which is almost certainly the case, since your input is a pure tone), then you can improve the signal-to-noise ratio by applying a bandpass filter around the signal of interest. The passband width of the filter should be chosen based on how precisely you know the sinusoid frequency a priori. By reducing the amount of noise present on the sinusoid, the number of false zero crossings and their jitter about the correct crossing times will be reduced.

    • As a side note, if you don't have good information ahead of time, you could use a more sophisticated technique known as an adaptive line enhancer, which, as its name implies, is an adaptive filter that will enhance a periodic input signal. However, this is a somewhat advanced topic, and you typically have a good enough idea of your signal's frequency that this sort of approach isn't needed.
  • With respect to the zero-crossing detector itself, you might add some hysteresis to the process. This would prevent the generation of extra spurious measured crossings around the correct crossing instant. Adding hysteresis to the detector might look something like this:

    if ((state == POSITIVE) && (sample[i - 1] > -T) && (sample[i] < -T))
    {
        // handle negative zero-crossing
        state = NEGATIVE;
    }
    else if ((state == NEGATIVE) && (sample[i - 1] < T) && (sample[i] > T))
    {
        // handle positive zero-crossing
        state = POSITIVE;
    }
    

    Effectively, you add some state to your zero-crossing detector. If you believe the input signal to have a positive value, you require that the signal dip down below a chosen threshold value -T in order to declare a real zero crossing. Likewise, you require that the signal rise back up above the threshold T in order to declare that the signal has oscillated back to positive again.

    You could choose the thresholds to be whatever you want, but for a balanced signal like a sinusoid, it makes sense to have them be symmetric about zero. This approach can help give you a cleaner-looking output, but it will add some time delay due to the fact that you're actually measuring non-zero threshold crossings instead of zero crossings.

As pichenettes suggested in his answer, a phase-locked loop would be most likely the best way to go, as a PLL does pretty much exactly what you're trying to do. In short, you run a square wave generator that runs in parallel with the input sinusoid. The PLL makes periodic phase measurements on the sinusoid, then filters that stream of measurements in order to steer the instantaneous frequency of the square wave generator. At some point, the loop will (hopefully) lock, at which point the square wave should be locked in frequency and phase with the sinusoid of the input (with some amount of error, of course; nothing in engineering is perfect).

$\endgroup$
3
  • $\begingroup$ Is that a Schmitt-trigger? $\endgroup$
    – Davorin
    Commented Sep 23, 2013 at 15:15
  • $\begingroup$ Indeed, you could say that it is a software version of a Schmitt trigger. The defining characteristic of a Schmitt trigger is that it is a comparator with hysteresis $\endgroup$
    – Jason R
    Commented Sep 23, 2013 at 15:17
  • $\begingroup$ To avoid not detecting the transition include in any of both conditions also the threshold T. Meaning instead of && (sample[i - 1] > -T) && (sample[i] < -T)), use && (sample[i - 1] >= -T) && (sample[i] < -T)). This needs to be applied to both ifand else if statements. $\endgroup$
    – marc
    Commented Dec 10, 2019 at 18:27
2
$\begingroup$

I have good experience with a very simple method to find the sign changes in signal at times:

  1. a=diff(sign(signal)) !=0 # this detects the sign changes
  2. candidates=times[a] # these are all the candidate points, including the false crossings
  3. find clusters of points in candidates
  4. average/median each cluster, this is your sign change

  5. do correlation with step function at point predicted by 4

  6. fit curve to correlation results and find peak

In my case 5 and 6 don't increase the precision of the method. You can dither your signal with noise and see if it helps.

$\endgroup$
2
$\begingroup$

I know this question is rather old, but I had to implement zero crossing recently. I implemented the way Dan suggested and am rather pleased with the result. Heres my python code, if anyone is interested. Im not really an elegant programmer, pls bear with me.

import numpy as np
import matplotlib.pyplot as plt
from itertools import cycle

fig = plt.figure()
ax = fig.add_subplot(111)

sample_time = 0.01
sample_freq = 1/sample_time

# a-priori knowledge of frequency, in this case 1Hz, make target_voltage variable to use as trigger?
target_freq = 1
target_voltage = 0

time = np.arange(0.0, 5.0, 0.01)
data = np.cos(2*np.pi*time)
noise = np.random.normal(0,0.2, len(data))
data = data + noise


line, = ax.plot(time, data, lw=2)

candidates = [] #indizes of candidates (values better?)
for i in range(0, len(data)-1):
    if data[i] < target_voltage and data[i+1] > target_voltage:
        #positive crossing
        candidates.append(time[i])
    elif data[i] > target_voltage and data[i+1] < target_voltage:
        #negative crossing
        candidates.append(time[i])

ax.plot(candidates, np.ones(len(candidates)) * target_voltage, 'rx')
print('candidates: ' + str(candidates))

#group candidates by threshhold
groups = [[]]
time_thresh = target_freq / 8;
group_idx = 0;

for i in range(0, len(candidates)-1):
    if(candidates[i+1] - candidates[i] < time_thresh):
        groups[group_idx].append(candidates[i])
        if i == (len(candidates) - 2):
            # special case for last candidate
            # in this case last candidate belongs to the present group
            groups[group_idx].append(candidates[i+1])
    else:
        groups[group_idx].append(candidates[i])
        groups.append([])
        group_idx = group_idx + 1
        if i == (len(candidates) - 2):
            # special case for last candidate
            # in this case last candidate belongs to the next group
            groups[group_idx].append(candidates[i+1])



cycol = cycle('bgcmk')
for i in range(0, len(groups)):
    for j in range(0, len(groups[i])):
        print('group' + str(i) + ' candidate nr ' + str(j) + ' value: ' + str(groups[i][j]))
    ax.plot(groups[i], np.ones(len(groups[i])) * target_voltage, color=next(cycol), marker='o',  markersize=4)


#determine zero_crosses from groups
zero_crosses = []

for i in range(0, len(groups)):
    group_median = groups[i][0] + ((groups[i][-1] - groups [i][0])/2)
    print('group median: ' + str(group_median))
    #find index that best matches time-vector
    idx = np.argmin(np.abs(time - group_median))
    print('index of timestamp: ' + str(idx))
    zero_crosses.append(time[idx])


#plot zero crosses
ax.plot(zero_crosses, np.ones(len(zero_crosses)) * target_voltage, 'bx', markersize=10) 
plt.show()

Pls Note: my code doesn't detect signs and uses a little a-priori knowledge of a target frequency to determine the time-threshold. This threshold is used to group the multiple crossing (different color dots in picture) from which the one closest to the groups median is selected (blue crosses in the picture).

Noisy sine wave with marked zero crosses

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.