SignalLab — Student Guide

ECE Emerge Lab | Signal Generation and Analysis

SignalLab is a MATLAB tool for generating and analysing electrical signals — the same kinds of signals you measure on an oscilloscope in lab.

Everything is controlled through a single object that stores your signal settings: amplitude, frequency, sampling rate, and waveform type.

1. Creating a Signal Object

Create a SignalLab object by specifying three things:

s = SignalLab(amplitude, frequency, samplingRate)

String syntax reminder: always use double quotes for text in MATLAB: "square", "sine", etc. Single quotes create an older type called a character vector — avoid them.

s = SignalLab(1, 1000, 100000);   % 1 V amplitude, 1 kHz, sampled at 100 kHz

Use info() to see the current settings of your object.

s.info()
--- SignalLab ---
  Amplitude    : 1 V
  Frequency    : 1000 Hz
  Phase        : 0 rad
  Sampling Rate: 100000 Hz
  Wave Type    : sine

2. Generating and Plotting a Signal in the Time Domain

Use generate to produce a signal of a given duration (in seconds). It returns two arrays: a time vector t and the signal samples x.

[t, x] = s.generate(duration)

Let's generate 3 milliseconds of a 1 kHz sine wave and plot it.

[t, x] = s.generate(0.003);

figure;
s.plotSignal(t, x);
title("Sine Wave — Time Domain");

figure_0.png

What you should see: three complete cycles of a smooth sine wave.

plotSignal is your main tool for viewing signals. Use it after every generate call to confirm your signal looks as expected.

Note: SignalLab automatically adjusts the duration so the signal always contains a whole number of complete periods. This keeps the signal clean and avoids errors in frequency analysis.

3. Choosing a Waveform

By default, SignalLab generates a sine wave. Use setWave to switch:

s = s.setWave("sine")
s = s.setWave("square")
s = s.setWave("triangle")
s = s.setWave("sawtooth")

Important: you must re-assign s when calling setWave, otherwise the change has no effect. See the Common Mistakes section for a full explanation.

Let's generate and plot all four waveforms side by side.

s = SignalLab(1, 1000, 100000);

[t, x_sine]     = s.generate(0.003);

s = s.setWave("square");
[t, x_square]   = s.generate(0.003);

s = s.setWave("triangle");
[t, x_triangle] = s.generate(0.003);

s = s.setWave("sawtooth");
[t, x_sawtooth] = s.generate(0.003);

figure;
subplot(4,1,1);  s.plotSignal(t, x_sine);      title("Sine");
subplot(4,1,2);  s.plotSignal(t, x_square);    title("Square");
subplot(4,1,3);  s.plotSignal(t, x_triangle);  title("Triangle");
subplot(4,1,4);  s.plotSignal(t, x_sawtooth);  title("Sawtooth");

figure_1.png

What you should see: four distinct waveform shapes. The sine is smooth; the square switches abruptly between +1 V and −1 V; the triangle ramps linearly up and down; the sawtooth rises steadily then resets sharply.

Square waves also support a duty cycle — the percentage of each period the signal spends at its high value. The default is 50%.

s = s.setWave("square", 75);    % 75% duty cycle
[t, x_dc75] = s.generate(0.003);

figure;
subplot(2,1,1);  s.plotSignal(t, x_square);  title("Square — 50% duty cycle");
subplot(2,1,2);  s.plotSignal(t, x_dc75);    title("Square — 75% duty cycle");

figure_2.png

4. Viewing Signals in the Frequency Domain

The time domain shows what a signal looks like. The frequency domain shows what frequencies it contains.

Use plotSpectrum to compute and display the frequency spectrum in dB:

s.plotSpectrum(x)

No prior setup is needed — it computes the FFT internally.

Sine wave spectrum

A pure sine wave contains only a single frequency. You should see one sharp peak at 1 kHz, and nothing else.

s = SignalLab(1, 1000, 100000);
[t, x_sine] = s.generate(0.003);

figure;
subplot(2,1,1);  s.plotSignal(t, x_sine);     title("Sine — Time Domain");
subplot(2,1,2);  s.plotSpectrum(x_sine);      title("Sine — Frequency Domain");

figure_3.png

Square wave spectrum

A square wave is built from an infinite sum of odd harmonics (1st, 3rd, 5th, ...). In the frequency domain, you should see peaks at 1 kHz, 3 kHz, 5 kHz, 7 kHz, ... with amplitudes that decrease as 1/n.

s = s.setWave("square");
[t, x_square] = s.generate(0.003);

figure;
subplot(2,1,1);  s.plotSignal(t, x_square);   title("Square — Time Domain");
subplot(2,1,2);  s.plotSpectrum(x_square);    title("Square — Frequency Domain");

figure_4.png

Triangle and sawtooth spectra

The triangle wave also has only odd harmonics, but they fall off faster (as 1/n²), so higher harmonics are much weaker. The sawtooth has both odd and even harmonics (1/n for all n).

s = s.setWave("triangle");
[t, x_triangle] = s.generate(0.003);

s = s.setWave("sawtooth");
[t, x_sawtooth] = s.generate(0.003);

figure;
subplot(2,2,1);  s.plotSignal(t, x_triangle);   title("Triangle — Time");
subplot(2,2,2);  s.plotSpectrum(x_triangle);    title("Triangle — Frequency");
subplot(2,2,3);  s.plotSignal(t, x_sawtooth);   title("Sawtooth — Time");
subplot(2,2,4);  s.plotSpectrum(x_sawtooth);    title("Sawtooth — Frequency");

figure_5.png

5. Extracting Spectrum Data

plotSpectrum shows the spectrum visually. When you need the actual numbers — for example, to read off the amplitude at each harmonic frequency — use computeSpectrum.

[fDom, spectrum, freqAxis] = s.computeSpectrum(x)

DC offset is removed automatically before the FFT, so a constant voltage offset will not appear in the spectrum.

Example: read harmonic amplitudes from a square wave

s = SignalLab(1, 1000, 100000);
s = s.setWave("square");
[~, x] = s.generate(0.1);              % 100 ms = 100 cycles, fine frequency resolution

[fDom, spectrum, freqAxis] = s.computeSpectrum(x);

fprintf("Dominant frequency: %.0f Hz\n", fDom);

for k = [1 3 5 7 9]
    targetFreq = k * 1000;
    [~, idx]   = min(abs(freqAxis - targetFreq));
    fprintf("Harmonic %d (%5d Hz):  %.4f V\n", k, targetFreq, spectrum(idx));
end
Dominant frequency: 1000 Hz
Harmonic 1 ( 1000 Hz):  1.2732 V
Harmonic 3 ( 3000 Hz):  0.4244 V
Harmonic 5 ( 5000 Hz):  0.2546 V
Harmonic 7 ( 7000 Hz):  0.1818 V
Harmonic 9 ( 9000 Hz):  0.1415 V

These match the theoretical values: the k-th odd harmonic of a square wave has amplitude 4A/(kπ).

Tips for clean harmonic extraction:

6. Harmonic Synthesis (Fourier Series)

Any periodic waveform can be built by adding sine waves together — this is the Fourier series.

Use generateHarmonics(N, duration) where N is the highest harmonic number to include. For square and triangle waves, even harmonics are automatically skipped.

Building a square wave step by step

Watch how the signal gets closer to a square wave as we add more harmonics.

s = SignalLab(1, 1000, 100000);
s = s.setWave("square");

[t, x1] = s.generateHarmonics(1,  0.003);   % 1st harmonic only
[t, x3] = s.generateHarmonics(3,  0.003);   % up to 3rd harmonic
[t, x5] = s.generateHarmonics(5,  0.003);   % up to 5th harmonic
[t, x9] = s.generateHarmonics(9,  0.003);   % up to 9th harmonic

figure;
 s.plotSignal(t, x1);  title("Up to harmonic 1")

figure_6.png

 s.plotSignal(t, x3);  title("Up to harmonic 3")

figure_7.png

 s.plotSignal(t, x5);  title("Up to harmonic 5")

figure_8.png

 s.plotSignal(t, x9);  title("Up to harmonic 9")

figure_9.png

Comparing synthesis to the ideal square wave

[t, x_ideal] = s.generate(0.003);

figure;
plot(t, x_ideal, "k--", "LineWidth", 1.2);
hold on;
plot(t, x9, "b-",  "LineWidth", 1.5);
hold off;
xlabel("Time (s)");  ylabel("Amplitude (V)");
legend("Ideal square", "9 harmonics");
title("Fourier Synthesis vs Ideal Square Wave");
grid on;

figure_10.png

Harmonic synthesis in the frequency domain

Each harmonic shows up as a separate peak in the spectrum. Adding more harmonics fills in more peaks and makes the waveform more square.

s.plotSpectrum(x1);  title("Spectrum — harmonic 1 only")

figure_11.png

s.plotSpectrum(x3);  title("Spectrum — up to harmonic 3")

figure_12.png

s.plotSpectrum(x5);  title("Spectrum — up to harmonic 5")

figure_13.png

s.plotSpectrum(x9);  title("Spectrum — up to harmonic 9")

figure_14.png

7. Adding Noise

Use generateNoisy to add random Gaussian noise to the current waveform. The second argument sets the noise level (standard deviation in volts). If omitted, it defaults to 0.1 V.

[t, x] = s.generateNoisy(duration, noiseAmp)
s = SignalLab(1, 1000, 100000);
[t, x_clean] = s.generate(0.003);
[t, x_noisy] = s.generateNoisy(0.003, 0.3);   % 0.3 V noise

figure;
subplot(2,1,1);  s.plotSignal(t, x_clean);  title("Clean Sine");
subplot(2,1,2);  s.plotSignal(t, x_noisy);  title("Noisy Sine (0.3 V noise)");

figure_15.png

Notice how the noise affects the spectrum — it raises the noise floor across all frequencies.

figure;
subplot(2,1,1);  s.plotSpectrum(x_clean);  title("Clean Sine — Spectrum");
subplot(2,1,2);  s.plotSpectrum(x_noisy);  title("Noisy Sine — Spectrum");

figure_16.png

8. Measuring Signal Properties

SignalLab can measure key properties of any signal vector.

s = SignalLab(1, 1000, 100000);
[t, x] = s.generate(0.003);

A = s.measureAmplitude(x)        % should return 1.0 V
A = 1
f = s.measureFrequency(x)        % should return 1000 Hz
f = 1000

Signal-to-Noise Ratio (SNR)

SNR measures how much stronger the signal is compared to the noise, expressed in decibels (dB). A higher value means a cleaner signal.

[t, x_clean] = s.generate(0.003);
[t, x_noisy] = s.generateNoisy(0.003, 0.3);

snr_clean = s.calculateSNR(x_clean)   % expect high SNR (> 40 dB)
snr_clean = 299.8643
snr_noisy = s.calculateSNR(x_noisy)   % expect lower SNR
snr_noisy = 7.5825

Use visualizeSNR to see both the signal and the signal band highlighted in the spectrum.

s.visualizeSNR(x_noisy);

figure_17.png

9. Filtering a Signal

filterSignal keeps only the frequency components between fLow and fHigh Hz, removing everything outside that range.

xf = s.filterSignal(x, fLow, fHigh)

Example: isolate the fundamental frequency from a square wave by passing only frequencies near 1 kHz. The result should be a sine wave.

s = SignalLab(1, 1000, 100000);
s = s.setWave("square");
[t, x_sq] = s.generate(0.003);

xf = s.filterSignal(x_sq, 500, 1500);   % pass only 1 kHz ± 500 Hz
figure;
s.plotSignal(t, x_sq);   title("Original Square Wave");

figure_18.png

figure; s.plotSignal(t, xf);     title("After Filtering — only fundamental remains");

figure_19.png


figure; s.plotSpectrum(x_sq)
hold on;
xline(500,  "g--", "fLow",  "LabelVerticalAlignment", "bottom");
xline(1500, "g--", "fHigh", "LabelVerticalAlignment", "bottom");
hold off;
title("Square Spectrum — filter band shown");

figure_20.png

10. Simulating ADC Quantization

Every measurement you make with the M2K passes through a 12-bit Analog-to-Digital Converter (ADC). The ADC divides its input voltage range into 2^12 = 4096 discrete levels and rounds every sample to the nearest one. The rounding error is called quantization noise.

The M2K has two input ranges:

The size of one level (the LSB) determines how much noise is added:

LSB = full-scale range / 2^bits

For the M2K at ±2.5 V: LSB = 5 / 4096 ≈ 1.22 mV

The theoretical SNR for an ideal N-bit ADC is:

SNR ≈ 6.02 × N + 1.76  dB

For 12 bits: SNR ≈ 74 dB — this is the noise floor you observe in lab.

Apply quantization to a signal

xq = s.quantizeSignal(x)              % 12-bit, ±2.5 V (M2K default)
xq = s.quantizeSignal(x, range)       % specify full-scale range
xq = s.quantizeSignal(x, range, bits) % specify range and bit depth
s = SignalLab(1, 1000, 100000);
[t, x] = s.generate(0.003);

xq_12bit = s.quantizeSignal(x);          % 12-bit ±2.5 V  (M2K low range)
xq_8bit  = s.quantizeSignal(x, 5, 8);   % 8-bit  ±2.5 V  (coarser)
xq_4bit  = s.quantizeSignal(x, 5, 4);   % 4-bit  ±2.5 V  (very coarse)

Use plotQuantization to see the full picture in one figure: original vs quantized signal, quantization error, and spectrum comparison.

s.plotQuantization(x);                   % default: 12-bit, ±2.5 V

figure_21.png

Effect of bit depth

Reducing the number of bits makes the quantization steps visible in the time domain and raises the noise floor in the spectrum.

figure;
subplot(3,1,1);  s.plotSignal(t, xq_12bit);  title("12-bit quantization");
subplot(3,1,2);  s.plotSignal(t, xq_8bit);   title("8-bit quantization");
subplot(3,1,3);  s.plotSignal(t, xq_4bit);   title("4-bit quantization");

figure_22.png

Effect of ADC range — and clipping

If the signal amplitude exceeds the ADC range, the ADC clips the signal. This creates large distortion and many spurious harmonics in the spectrum. The plotQuantization method highlights clipped samples in red.

Example: a 3 V amplitude signal fed into the ±2.5 V range will clip.

s_large = SignalLab(3, 1000, 100000);    % 3 V amplitude — exceeds ±2.5 V range
[t, x_large] = s_large.generate(0.003);

s_large.plotQuantization(x_large, 5);    % clips — clipped samples shown in red

figure_23.png

Compare the same signal on the correct ±25 V range (no clipping):

s_large.plotQuantization(x_large, 50);   % ±25 V range — no clipping

figure_24.png

Connecting to your lab measurements

The spectrum you measured on the M2K has a noise floor at approximately -74 dBFS. This matches the theoretical 12-bit noise floor above. You can verify this by quantizing your theoretical signal and comparing its spectrum to your measured spectrum — they should have similar noise floors.

s = SignalLab(1, 1000, 100000);
[t, x]   = s.generate(0.003);
xq       = s.quantizeSignal(x);          % simulate M2K digitisation

figure;
subplot(2,1,1);  s.plotSpectrum(x);   title("Theoretical — no quantization");
subplot(2,1,2);  s.plotSpectrum(xq);  title("Theoretical — after 12-bit quantization (M2K)");

figure_25.png

11. Common Mistakes

Mistake 1: Forgetting to re-assign when changing wave type

s.setWave("square")       % WRONG  — s is unchanged
s = s.setWave("square")   % CORRECT

Why this happens: SignalLab is a value class in MATLAB. Think of s as a box containing your signal settings. When you call s.setWave(...), MATLAB makes a temporary modified copy of the box — then throws it away because you never said where to put it. The original s is untouched.

Writing s = s.setWave(...) tells MATLAB: put the modified copy back into s. Now the original is replaced with the updated version.

Mistake 2: Creating a new object after configuring it

This is easy to do when running lines out of order:

s = s.setWave("square");          % configure square wave
s = SignalLab(1, 1000, 100000);   % OOPS — brand new object, resets to sine!
[t, x] = s.generate(0.003);       % generates a sine, not a square

The constructor always starts fresh. Anything set before it is lost.

Rule of thumb: create first, configure second, use third.

s = SignalLab(1, 1000, 100000);   % 1. create
s = s.setWave("square");          % 2. configure
[t, x] = s.generate(0.003);       % 3. use
s.info()
--- SignalLab ---
  Amplitude    : 1 V
  Frequency    : 1000 Hz
  Phase        : 0 rad
  Sampling Rate: 100000 Hz
  Wave Type    : square
  Duty Cycle   : 50%

Mistake 3: Plotting without opening a new figure

Calling plotSignal twice without a figure command overwrites the first plot. Use figure to open a new window and subplot to arrange multiple plots together.

figure;
subplot(2,1,1);  s.plotSignal(t, x1);   title("First signal");
subplot(2,1,2);  s.plotSignal(t, x3);   title("Second signal");

figure_26.png

12. Quick Reference

CREATE      s = SignalLab(A, f, Fs)
SETTINGS    s.info()
WAVE TYPE   s = s.setWave("square")
DUTY CYCLE  s = s.setWave("square", 75)
GENERATE
  [t,x] = s.generate(duration)
  [t,x] = s.generateHarmonics(N, duration)   N = highest harmonic
  [t,x] = s.generateNoisy(duration, noiseAmp)
PLOT
  s.plotSignal(t, x)      time domain
  s.plotSpectrum(x)       frequency domain (dB)
  s.visualizeSNR(x)       time + spectrum with SNR
SPECTRAL ANALYSIS
  [fDom, spectrum, freqAxis] = s.computeSpectrum(x)
    fDom     — dominant frequency (Hz)
    spectrum — magnitude at each bin (V); peaks = true amplitudes
    freqAxis — frequency axis (Hz)
  [~, idx] = min(abs(freqAxis - targetFreq))   find bin nearest targetFreq
  amp = spectrum(idx)                           read amplitude at that bin
MEASURE / PROCESS
  A   = s.measureAmplitude(x)
  f   = s.measureFrequency(x)
  snr = s.calculateSNR(x)
  xf  = s.filterSignal(x, fLow, fHigh)
  xq  = s.quantizeSignal(x, range, bits)   range default=5V, bits default=12
QUANTIZATION PLOT
  s.plotQuantization(x)                    12-bit, ±2.5 V
  s.plotQuantization(x, range, bits)       custom range / bit depth