Hands-on: a live spectrum from the IMU
Module 4 — Signal analysis · Slides: slides.md · Module overview · Course page
Fill a four-step pipeline in s09_fft_spectrum.py — remove DC, apply a Hann window, compute the magnitude, find the peak — then shake the board and watch the spectrum bars and the dominant frequency follow your rhythm, and switch off each step in turn to see what it fixes.
Objectives
Section titled “Objectives”By the end of this lesson, you will:
- Fill the four steps in practice/s09_fft_spectrum.py until the spectrum bars follow your shaking, and the displayed peak is close to your shakes per second (for example, three shakes per second gives about 3 Hz).
- Run once without removing DC and once without the window, and explain how the spectrum changes and why.
- Explain how collecting N points with an even 1000/FS ms delay, and normalising the bars by the highest peak, affect reading the spectrum.
Before you start
Section titled “Before you start”You’ve been through lesson 4.3, and know how a bin converts to Hz and why DC must be removed and the window applied. Prepare three shaking rhythms (slow, medium, fast), and count the shakes per second for each to compare against.
- Hardware: a TESAIoT Dev Kit board already flashed with BENTO’s MicroPython firmware, or the BENTO Emulator inside BENTO IDE
- Prior lesson: lesson 4.3 — The FFT and the frequency domain: bins, Nyquist, DC, leakage and the Hann window
Concepts
Section titled “Concepts”The whole file reads as one sentence: collect N points → remove DC → apply the window → FFT → find the magnitude → find the peak → draw the bars. What’s already given is collecting az from sensors.bmi270.motion(), 32 points, with an even 1000/FS ms delay (an FFT only reads frequency correctly when the spacing is equal), a fft() function using radix-2 Cooley-Tukey, written in Python so you can read it as just organized addition and multiplication, and the bar drawing, created once before the loop.
The four steps we fill in are: (1) mean = sum(buf) / N (2) re = [(buf[i] - mean) * (0.5 - 0.5 * math.cos(2 * math.pi * i / (N - 1))) for i in range(N)], which removes DC and applies the window’s shape in one line (3) mag = [math.sqrt(re[k] * re[k] + im[k] * im[k]) for k in range(HALF)], and (4) kmax = max(range(1, HALF), key=lambda k: mag[k]), starting at 1 to skip bin 0, where leftover DC might linger. The dominant frequency is shown as kmax * FS / N. The bars are normalized by the highest peak, so they always fill the screen whether you shake hard or gently.
If you forget any step, the symptom tells you: skip removing DC and bin 0 towers over everything; skip the window and the peak’s neighbouring bars leak higher than they should; skip the magnitude and the bars stay flat; skip finding the peak and the dominant frequency stays stuck at bin 1. Success isn’t just watching the bars move — you should be able to say what a peak of 3 Hz actually means, and why shaking faster shifts the peak toward higher frequency.
Worked example
Section titled “Worked example”s09_fft_spectrum.py in the examples folder is the reference version, using N = 64. s09_fft_spectrum_full.py adds EMA-averaging the spectrum to keep the bars steady, shows the dominant frequency large on a Seg7, holds the highest peak (peak-hold), and measures total energy (RMS).
| File | What this file teaches |
|---|---|
| examples/s09_fft_spectrum.py | From the time domain to the frequency domain (FFT) |
| examples/s09_fft_spectrum_full.py | A live FFT spectrum from the IMU (full version) |
Practice
Section titled “Practice”The 4 # TODO: comments are at lines 95 (remove DC), 100 (Hann window), 110 (magnitude), and 115 (peak). Replace the starting values with the calls the hints describe, then shake the board’s Z axis, alternating fast and slow. If the bars never move, check your indentation and variable names first.
| Practice file | Topic |
|---|---|
| practice/s09_fft_spectrum.py | From the time domain to the frequency domain (the fill-in-the-code version) |
Solution
Section titled “Solution”Open the solution after trying on your own at least once, and read how to use the solutions first.
| Solution | Pairs with |
|---|---|
| solution/s09_fft_spectrum.py | practice/s09_fft_spectrum.py |
Check your understanding
Section titled “Check your understanding”The same questions are in quiz.yaml for automated checking.
-
Put the steps of s09_fft_spectrum.py’s pipeline in order (ordering · objective 1)
- a) fft(re, im)
- b) mean = sum(buf) / N (remove DC)
- c) kmax = max(range(1, HALF), key=…) (find the peak)
- d) apply the Hann window
- e) mag = sqrt(re² + im²) over the first half
Solution
b → d → a → e → c — remove DC → window → FFT → magnitude → peak. The FFT step is already given; the other four are the blanks we fill in.
-
You’ve filled everything in. The bars move with the shaking, but the peak label stays stuck at 1.6 Hz (bin 1) forever. Which step is still unfilled? (single choice · objective 1)
- a) Remove DC
- b) Hann window
- c) Magnitude
- d) Find the peak (kmax is still its starting value, 1)
Solution
d — the placeholder kmax = 1 always points the dominant frequency at bin 1. It must be replaced with max(range(1, HALF), key=lambda k: mag[k]).
-
If you don’t apply the Hann window, how does the spectrum change? (single choice · objective 2)
- a) Bin 0 towers over everything
- b) The peak stays where it was, but the neighbouring bars leak higher than they should — the spectrum gets messy
- c) All bars go flat
- d) The dominant frequency disappears
Solution
b — a window edge that doesn’t line up with the signal’s period leaks energy (spectral leakage). Hann pushes the edges to zero, reducing the leak. Bin 0 towering is instead the symptom of not removing DC.
-
If you remove
time.sleep_ms(int(1000 / FS))from the loop that collects N points, what happens? (single choice · objective 3)- a) No effect, since the FFT doesn’t care about timing
- b) The real sampling rate is no longer FS, so converting kmax · FS / N to Hz becomes wrong
- c) The spectrum always gets finer
- d) The board restarts
Solution
b — the formula f = k·FS/N assumes points are equally spaced 1/FS seconds apart. If the real rate differs, the Hz value shown no longer matches reality.
The MVP for lessons 4.3–4.4: run s09_fft_spectrum.py and read a live spectrum — the frequency bars and the peak (Hz) genuinely change with faster or slower shaking.
- All four steps in the practice file are filled in, and it runs on the emulator or the board.
- Shake at three rhythms (slow, medium, fast), note the peak in Hz for each in your learning log, and compare against the shakes-per-second you counted.
- Run once without removing DC (leave
mean = 0.0) and once without the window, and explain how the spectrum changes. - Be able to explain where the DC-removal, window, magnitude and peak steps sit in the pipeline, and what each does.
Going further
Section titled “Going further”In the next pair of lessons (4.5–4.6), we’ll move on from the spectrum to a sliding window and a feature vector — what a model actually sees.
Next lesson: lesson 4.5 — Features and windows: what a model actually sees
Reflect
Section titled “Reflect”- How closely did the peak you measured match the rhythm you counted yourself? If it was off, do you think that came from the bin resolution, or from your own hand?
- If you needed to inspect a motor spinning at 1,500 RPM, what’s the minimum FS you’d need to set?
Review questions
Answer on your own first, then open the answer.
-
Order the steps of the pipeline in s09_fft_spectrum.py. (Objective 1)
- fft(re, im)
- mean = sum(buf) / N (ตัด DC)
- kmax = max(range(1, HALF), key=...) (หา peak)
- คูณ Hann window
- mag = sqrt(re² + im²) ครึ่งแรก
Show answer
Correct order: B. mean = sum(buf) / N (ตัด DC) → D. คูณ Hann window → A. fft(re, im) → E. mag = sqrt(re² + im²) ครึ่งแรก → C. kmax = max(range(1, HALF), key=...) (หา peak)
ตัด DC → window → FFT → magnitude → peak ขั้น FFT ให้ไว้แล้ว ส่วนอีกสี่ขั้นคือช่องที่เราเติม
-
Everything moves with the shaking, but the peak label is stuck at 1.6 Hz (bin 1). Which step is still unfilled? (Objective 1)
- ตัด DC
- Hann window
- magnitude
- หา peak (kmax ยังเป็นค่าเริ่มต้น 1)
Show answer
Answer: D. หา peak (kmax ยังเป็นค่าเริ่มต้น 1)
placeholder kmax = 1 ทำให้ความถี่เด่นชี้ bin 1 เสมอ ต้องแทนด้วย max(range(1, HALF), key=lambda k: mag[k])
-
Without the Hann window, how does the spectrum change? (Objective 2)
- bin 0 พุ่งกลบทุกอย่าง
- ยอดยังอยู่ที่เดิม แต่แท่งข้างเคียงรั่วสูงกว่าที่ควร สเปกตรัมเลอะ
- แท่งแบนราบทั้งหมด
- ความถี่เด่นหายไป
Show answer
Answer: B. ยอดยังอยู่ที่เดิม แต่แท่งข้างเคียงรั่วสูงกว่าที่ควร สเปกตรัมเลอะ
ขอบของหน้าต่างที่ไม่พอดีคาบเวลาของสัญญาณทำให้พลังงานรั่ว (spectral leakage) Hann กดขอบให้เป็นศูนย์จึงลดการรั่ว ส่วน bin 0 พุ่งเป็นอาการของการไม่ตัด DC
-
If you remove time.sleep_ms(int(1000 / FS)) from the loop that collects N points, what happens? (Objective 3)
- ไม่มีผลเพราะ FFT ไม่สนเวลา
- อัตราสุ่มจริงไม่ใช่ FS อีกต่อไป การแปลง kmax · FS / N เป็น Hz จึงผิด
- สเปกตรัมละเอียดขึ้นเสมอ
- บอร์ดรีสตาร์ต
Show answer
Answer: B. อัตราสุ่มจริงไม่ใช่ FS อีกต่อไป การแปลง kmax · FS / N เป็น Hz จึงผิด
สูตร f = k·FS/N ถือว่าจุดห่างกัน 1/FS วินาทีเท่ากันทุกจุด ถ้าอัตราจริงต่างไป ค่า Hz ที่แสดงจะไม่ตรงกับความจริง
Cite this lesson
If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.
"Hands-on: a live spectrum from the IMU" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
Thai attribution: "ลงมือทำ: สเปกตรัมสดจาก IMU" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
Lesson link: https://tesaiot.github.io/tesa-qualification-program/en/courses/edge-ai-developer/m04-analysis/l04-fft-spectrum-lab/
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA