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)
- amplitude — peak amplitude in Volts
- frequency — signal frequency in Hz
- samplingRate — number of samples per second (Hz)
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");
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");
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");
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");
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");
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");
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)
- fDom — dominant frequency (Hz): the frequency of the tallest spectral peak
- spectrum — single-sided magnitude spectrum (V); each peak height equals the true signal amplitude at that frequency
- freqAxis — frequency axis (Hz), one entry per spectral bin
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:
- Use a long signal (many cycles).
duration = 100/f0gives 100 cycles and enough frequency resolution to isolate each harmonic bin cleanly. - The pattern
[~, idx] = min(abs(freqAxis - targetFreq))finds the bin closest to any target frequency — use it for every harmonic you want to read. - For square and triangle waves, even harmonic bins will contain small non-zero values (~1e-4 V) due to numerical rounding, not real signal content. These should be treated as exactly zero.
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")
s.plotSignal(t, x3); title("Up to harmonic 3")
s.plotSignal(t, x5); title("Up to harmonic 5")
s.plotSignal(t, x9); title("Up to harmonic 9")
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;
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")
s.plotSpectrum(x3); title("Spectrum — up to harmonic 3")
s.plotSpectrum(x5); title("Spectrum — up to harmonic 5")
s.plotSpectrum(x9); title("Spectrum — up to harmonic 9")
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)");
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");
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);
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; s.plotSignal(t, xf); title("After Filtering — only fundamental remains");
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");
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:
- ±2.5 V (full scale = 5 V) — default, used for lab signals
- ±25 V (full scale = 50 V) — used for higher voltage measurements
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
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");
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
Compare the same signal on the correct ±25 V range (no clipping):
s_large.plotQuantization(x_large, 50); % ±25 V range — no clipping
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)");
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");
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