Skip to content

DSP filters: EMA, Median, Kalman and the radar range profile

Module 4 — Signal analysis · Slides: slides.md · Module overview · Course page

Open pillar 3 (Analysis) with temporal filters. Recognise that noise wears many faces, choose EMA, Median or Kalman1D to match, use the shared update/value/reset API, and see the radar range profile the C side already computes.

By the end of this lesson, you will:

  1. Tell apart kinds of noise (white noise, spikes, drift), and choose the filter that suits each, with a reason.
  2. Compute one EMA step with y = αx + (1−α)y_prev and the median of a window by hand, and explain the smoothness-versus-response trade-off as α changes.
  3. Explain the Kalman gain K = P/(P+R) as the weight given to the measurement, and state the effect of raising r or q.
  4. Create a dsp filter with keyword arguments once outside the loop, feed it one sample at a time with update(), and explain why creating it inside the loop never smooths anything.

You’ve been through module 3, and saw a gauge jitter even while the board lay still, in lesson 3.2. This lesson is the answer to that symptom. Open s08_filters_full.py (in lesson 4.2) and try it before taking it apart.

Run s08_filters_full.py before reading any code. The top graph is the raw signal (three-axis acceleration magnitude); the bottom graph is the same value after filtering. Shake the board gently and switch filters in the dropdown, watching how the bottom line’s behaviour changes, and how the noise-down % bar reports how much jitter was reduced.

No sensor gives a smooth value. Noise wears several faces: white noise, small spikes scattered by heat or the ADC (suits EMA/SMA); spikes/outliers, values that jump occasionally from multipath or signal collisions (suits Median); drift, a value slowly sliding (suits an HPF); and mixed noise (suits Kalman). There’s no single perfect filter — you have to look at the enemy before picking up a tool. Analysis always comes before Training, because if you feed in a dirty signal, the model learns the “spikes” along with everything else.

A temporal filter is memory — it weighs a new value against the past. The filters in dsp run as a stream on the CM33, and every one shares the same API: create it once outside the loop → y = f.update(x) every round → f.value() reads the latest value → f.reset() clears the state. Every parameter is a keyword argument (alpha=, window=, q=, r=) — passing one positionally, such as dsp.EMA(0.15), throws a TypeError. If you accidentally create the filter inside the loop, its memory gets wiped every round, and the line will never smooth out.

EMA: $y[n] = \alpha x[n] + (1-\alpha) y[n-1]$. A small α is smooth but slow to follow; a large α is quick but spikes still show through. EMA_ALPHA = 0.15 means trusting a new value 15%. Median sorts the latest N values and takes the middle one, so an outlier never has a chance to win (for example, 98, 101, 240, 99, 100 gives 100, while the average gives 127.6). The firmware requires the window to be an odd number no greater than 15. Kalman1D keeps both an estimate and its uncertainty P. Every step it computes $K = P/(P+R)$, then $x \leftarrow x + K(z - x)$. A high r (distrusting the sensor) shrinks K, giving smoothness; a high q lets the true value move quickly. You can think of Kalman as an EMA that adjusts its own α.

A real-world example is the radar range profile: sensors.radar_range() returns distance_m, peak_db, resolution_m, target, seq. The C side does HPF → FFT → dB → peak-finding, all of it. Resolution is roughly 0.33 metres per bin, and the range loves to jump due to multipath — genuine spikes. The suitable filter is therefore Median, and in real work we often chain filters — Median to guard against spikes first, then EMA to smooth what’s left.

05_radar_distance.py is a radar tape measure that uses Median to guard against jumping readings. Predict before running it: if you walk in and out in front of the board, will the number stay steady or jump around? Then run it on the board and compare.

File What this file teaches
examples/05_radar_distance.py Radar Range: a tape measure on screen (Bar + Seg7 + graph)

This lesson’s slides also reference files in another lesson:

The same questions are in quiz.yaml for automated checking.

  1. Radar distance occasionally jumps due to multipath reflections. Which filter should be chosen first? (single choice · objective 1)

    • a) EMA
    • b) Median
    • c) HPF
    • d) No filtering needed
    Solution

    b — a jump is a genuine spike. Median votes for the middle value, so it can drop the spike entirely, while EMA would just smooth it into a bump that’s still visible.

  2. y_prev = 10, x = 20, and α = 0.25. What is this round’s EMA value? (single choice · objective 2)

    • a) 12.5
    • b) 15
    • c) 17.5
    • d) 20
    Solution

    a — y = 0.25 × 20 + 0.75 × 10 = 5 + 7.5 = 12.5. The new value only gets a quarter of the weight, so the line follows slowly but stays smooth.

  3. A five-value Median window is 98, 101, 240, 99, 100. What’s the result? (single choice · objective 2)

    • a) 127.6
    • b) 100
    • c) 240
    • d) 98
    Solution

    b — sorted: 98, 99, 100, 101, 240. The middle value is 100 — the spike 240 gets pushed to the edge, while the average, 127.6, gets dragged along by it.

  4. In Kalman1D, if r increases (trusting the sensor less) while q stays the same, what happens? (single choice · objective 3)

    • a) K grows, and the value jumps to follow the measurement
    • b) K shrinks, the value moves in smaller steps — smoother but slower to follow
    • c) No effect, since K is constant
    • d) The filter stops working
    Solution

    b — K = P/(P+R). As R grows, K shrinks, so x += K(z − x) moves less. You gain smoothness at the cost of response, the same trade-off as a small α in EMA.

  5. This code creates f = dsp.EMA(alpha=0.15) inside the loop every round, then calls f.update(x). What shows on the graph? (single choice · objective 4)

    • a) A very smooth line, because α is small
    • b) The filtered line looks just like the raw line, because a freshly created filter returns the first value fed to it directly, and its memory is wiped every round
    • c) The program throws TypeError
    • d) The line stays at zero forever
    Solution

    b — state lives inside the object. Recreating it every round means starting fresh every time, so the filter never has a past to weigh against. It must be created once, outside the loop.

  • Compute EMA by hand for three steps from x = 10, 10, 20, starting at y = 10, at α = 0.5 and α = 0.1, and compare which one keeps up better.
  • Find the median of the window [5, 7, 90, 6, 5], compare it against the average, and note in your learning log why they differ.
  • In the REPL, try calling dsp.EMA(0.15), look at the error message, then fix it to dsp.EMA(alpha=0.15).

In lesson 4.2, we’ll fill in the s08_filters.py file so it creates a filter from its name, filters a real signal, and measures the result as a noise-down %.

Next lesson: lesson 4.2 — Hands-on: filters cleaning a signal live

  • Which kind of noise does the work you care about run into most, and how much lag are you willing to accept in exchange for smoothness?
  • Why does measuring smoothness by jitter between frames work on a real sensor, even with no “answer key” for what the true value actually is?

Review questions

Answer on your own first, then open the answer.

  1. Radar distance jumps now and then because of multipath. Which filter should you try first? (Objective 1)

    1. EMA
    2. Median
    3. HPF
    4. ไม่ต้องกรอง
    Show answer

    Answer: B. Median

    การกระโดดเป็น spike แท้ ๆ Median โหวตค่ากลางจึงตัด spike ทิ้งได้ ส่วน EMA จะเกลี่ยให้เป็นโหนกที่ยังเห็นอยู่

  2. y_prev = 10, x = 20 and α = 0.25. What is this step's EMA? (Objective 2)

    1. 12.5
    2. 15
    3. 17.5
    4. 20
    Show answer

    Answer: A. 12.5

    y = 0.25 × 20 + 0.75 × 10 = 5 + 7.5 = 12.5 ค่าใหม่ได้น้ำหนักแค่หนึ่งในสี่ เส้นจึงตามช้าแต่เรียบ

  3. A five-value Median window holds 98 101 240 99 100. What is the output? (Objective 2)

    1. 127.6
    2. 100
    3. 240
    4. 98
    Show answer

    Answer: B. 100

    เรียงได้ 98 99 100 101 240 ค่ากลางคือ 100 spike 240 ถูกดันไปขอบ ขณะที่ค่าเฉลี่ย 127.6 ถูก spike ลาก

  4. In Kalman1D, raising r (trusting the sensor less) with q unchanged does what? (Objective 3)

    1. K โตขึ้น ค่ากระโดดตามการวัด
    2. K เล็กลง ค่าขยับทีละน้อย เรียบขึ้นแต่ตามช้าลง
    3. ไม่มีผลเพราะ K คงที่
    4. ฟิลเตอร์หยุดทำงาน
    Show answer

    Answer: B. K เล็กลง ค่าขยับทีละน้อย เรียบขึ้นแต่ตามช้าลง

    K = P/(P+R) เมื่อ R โต K เล็กลง x += K(z − x) จึงขยับน้อย ได้ความเรียบแลกกับการตอบสนองแบบเดียวกับ α เล็กของ EMA

  5. This code creates f = dsp.EMA(alpha=0.15) inside the loop on every pass and then calls f.update(x). What does the chart show? (Objective 4)

    1. เส้นเรียบมากเพราะ α เล็ก
    2. เส้นกรองเหมือนเส้นดิบ เพราะฟิลเตอร์ที่เพิ่งสร้างคืนค่าแรกที่ป้อนตรง ๆ และความจำถูกลบทุกรอบ
    3. โปรแกรมโยน TypeError
    4. เส้นเป็นศูนย์ตลอด
    Show answer

    Answer: B. เส้นกรองเหมือนเส้นดิบ เพราะฟิลเตอร์ที่เพิ่งสร้างคืนค่าแรกที่ป้อนตรง ๆ และความจำถูกลบทุกรอบ

    state อยู่ในอ็อบเจกต์ การสร้างใหม่ทุกรอบคือการเริ่มต้นใหม่ทุกครั้ง ฟิลเตอร์จึงไม่มีอดีตให้ถ่วง ต้องสร้างครั้งเดียวนอกลูป

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.

"DSP filters: EMA, Median, Kalman and the radar range profile" 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: "ฟิลเตอร์ DSP: EMA, Median, Kalman และ radar range profile" จาก 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/l01-dsp-filters/

Full guide: how to cite TESA

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