Lab 105 (AI KIT): Dual Digital MEMS Microphone —
from PDM Capture to Sound Direction Sensing
Introduction
Almost every sensor you have interfaced with so far in this lab series answers a question about a single point in space: how fast is the board rotating, what is the air pressure here, and how strong is the magnetic field at this spot. A microphone is different. Sound is a traveling wave, and the moment you have two microphones separated by a known distance, you can measure not only what a sound is, but where it came from.
The PSoC 6 AI Evaluation Kit gives us exactly that opportunity. The board carries two Infineon IM72D128V01XA1 digital MEMS microphones (U4 and U5) that share a single two-wire PDM bus. This is not an accident of the board layout — it is a deliberate design choice that makes the kit suitable for microphone-array and Edge AI audio applications.
In this lab, we will build the complete audio signal chain from the ground up, layer by layer. We begin with a single microphone and the most basic question — "what do the raw numbers look like?" — and finish with a working demonstration that senses whether a sound came from the left, the center, or the right.
What You Will Learn
- How a 1-bit PDM bitstream differs from conventional ADC sampling, and how the PSoC 6 PDM/PCM hardware block converts it into 16-bit PCM audio samples.
- How the choice of decimation rate determines the PDM clock frequency, and how that clock frequency, in turn, selects the microphone's power mode.
- Why raw PCM samples carry a DC offset, and how to remove it with a one-line first-order high-pass filter.
- How to compute RMS energy and a relative dB reading using integer arithmetic only, and display it as a live text bar meter over UART.
- How to detect acoustic transients (a hand clap) reliably using an energy threshold plus a debounce window.
- How two microphones share one data line by using opposite clock edges, and how to separate them into independent Left and Right channels.
- How to estimate the Time Difference of Arrival (TDOA) between the two channels using cross-correlation, and turn it into a coarse direction indication.
Prerequisite
Reading Materials: Datasheets and Other Resources
Background and Theory of Operation
The IM72D128V01XA1 Digital MEMS Microphone — Device and Initialization
The IM72D128V01XA1 Digital MEMS Microphone — Device and Initialization
Overview of the IM72D128V01XA1 MEMS Microphone
The IM72D128V01XA1 is an ultra-high-performance digital PDM (Pulse Density Modulation) MEMS microphone featuring IP57 dust and water resistance, a high signal-to-noise ratio (SNR) of 72dB(A), and an acoustic overload point of 128 dBSPL. In this evaluation kit, there are two such microphones (U4 and U5) configured for stereo audio, which share a single PDM bus connected to the PSoC 6 MCU.
Step 1: Device Initialization
Unlike I2C or SPI sensors, the IM72D128V01XA1 does not rely on digital register writes for initialization. Instead, it is initialized purely via hardware signals:
- Supply Power and Clock: The device is powered by the kit's 3.3V rail (VCC_3V3). Initialization begins as soon as VDD and the PDM clock (fclock) are applied.
- Wait for Idle Tone: Within 3 ms of applying power and the clock signal, the microphone will output an idle tone (a sequence of alternating 1s and 0s representing a zero input signal) over the PDM data line until a valid microphone signal becomes available.
Step 2: Enable Sensors and Configure Power Modes
The operating power mode of the microphone is not set via software commands; rather, it is dynamically determined by the frequency of the PDM clock (fclock) supplied by the MCU:
- Standby Mode: Set the clock frequency to ≤ 350 kHz. The data pad becomes high impedance, putting the device in its lowest power state (typically 25 µA).
- Low Power Mode: Set the clock frequency to 450 ~ 850 kHz.
- Normal Mode: Set the clock frequency between 1.2 MHz and 1.65 MHz.
- High-Performance Mode: Set the clock frequency between 2.0 MHz and 3.3 MHz.
Step 3: Configure Output Data Rates (ODR)
The actual audio sample rate is handled by the MCU's PDM/PCM conversion block.
- To match the standard data streaming applications on the PSoC 6 AI Evaluation Kit, configure the MCU to collect PDM or PCM audio data at 8 kHz or 16 kHz.
Step 4: Reading Audio Data
Audio data is transmitted over a 1-bit PDM interface using two shared MCU pins: PDM_CLK (P10[4]) and PDM_DATA (P10[5]). Because the two microphones share the same data line, they use a stereo multiplexing configuration based on their hardware SELECT pin wiring:
- Reading the Left Microphone (U4): Its SELECT pin is physically tied to GND. You must read its data on the falling edge of the PDM clock.
- Reading the Right Microphone (U5): Its SELECT pin is physically tied to 3.3V. You must read its data on the rising edge of the PDM clock.
From PDM Bits to Sound Direction — Signal Chain and DoA Principles
From PDM Bits to Sound Direction — Signal Chain and DoA Principles
The four initialization steps above describe how the two IM72D128V01XA1 microphones are powered and clocked, and how each one is assigned to a clock edge. What they do not yet explain is how a stream of single-bit pulses becomes a usable audio number, why that number drifts away from zero, how we turn it into a loudness reading, and — most interesting of all — how two microphones can tell us the direction a sound came from. Those four ideas form the theory on which the experiments #1 ~ #6 are based.
1. From 1-Bit PDM to Multi-Bit PCM (Decimation)
1. From 1-Bit PDM to Multi-Bit PCM (Decimation)
A PDM microphone does not output a "sample" in the way an ADC does. It outputs a very fast stream of single bits (1s and 0s) at the PDM clock rate. The amplitude of the sound is encoded in the local density of 1s: a loud positive pressure produces many 1s clustered together, silence produces a balanced alternating pattern, and a loud negative pressure produces many 0s. Internally, this is the output of a 1-bit sigma-delta modulator, which deliberately pushes its quantization noise into very high frequencies (a process called noise shaping).
To recover normal audio samples, we must do two things, in order:
- Low-pass filter the 1-bit stream to throw away the shaped high-frequency quantization noise and keep only the audio band.
- Decimate (down-sample) the filtered stream from the megahertz PDM rate down to the audio rate (8 kHz or 16 kHz). The result is a stream of multi-bit signed PCM samples (16-bit on this kit).
The relationship between the two rates is fixed by a single integer, the decimation rate:
This one equation quietly ties the theory back to Step 2 (power modes). Because the PDM clock frequency determines the microphone's power mode, your choice of decimation rate must place the clock within a valid mode band. The table below shows clean choices for this kit:
| PCM rate fPCM | Decimation rate | Resulting fPDM_CLK | Microphone power mode |
|---|---|---|---|
| 16 kHz | 96 | 1.536 MHz | Normal (1.2–1.65 MHz) |
| 16 kHz | 128 | 2.048 MHz | High-Performance (2.0–3.3 MHz) |
| 8 kHz | 192 | 1.536 MHz | Normal (1.2–1.65 MHz) |
2. Why the Raw Signal Has a DC Offset — the DC Blocker
2. Why the Raw Signal Has a DC Offset — the DC Blocker
When you first print raw PCM samples (Exp#1), you will notice that "silence" is not centered on zero — the samples hover around some non-zero value. This DC offset comes from the microphone's own bias and from the decimation filter chain. It carries no acoustic information, but it will ruin any loudness calculation because it adds a constant energy that has nothing to do with sound.
The cure is a first-order high-pass filter, often called a DC blocker. It subtracts the slowly-varying average while letting the audio through:
where α is a constant just below 1 (for example, 0.995). The closer α is to 1, the lower the cut-off frequency and the gentler the filter. This single arithmetic line runs comfortably on the CM4 in real time and is the recommended first processing step for every experiment after Exp#1.
3. Measuring Loudness: RMS and Relative dB
3. Measuring Loudness: RMS and Relative dB
Once the signal is centered on zero, "How loud is it?" becomes a well-defined question. Loudness is the energy of the signal over a short window, which we summarise with the Root-Mean-Square (RMS) value over N samples:
Because the human ear responds to loudness logarithmically, it is more natural to display the result on a relative decibel scale:
4. Two Microphones, One Data Line: Stereo Multiplexing
4. Two Microphones, One Data Line: Stereo Multiplexing
As described in Step 4, microphones U4 and U5 share the same two pins — PDM_CLK (P10[4]) and PDM_DATA (P10[5]). They avoid a collision by using opposite clock edges, chosen by each microphone's hardwired SELECT pin:
- Left (U4), SELECT tied to GND → drives valid data on the falling edge.
- Right (U5), SELECT tied to 3.3 V → drives valid data on the rising edge.
This is time-division multiplexing on a single wire. The PSoC 6 PDM/PCM block, when configured for stereo mode, samples both edges and separates the two bitstreams into independent Left and Right channels — each decimated into its own 16-bit PCM sample. From your firmware's point of view, you simply receive interleaved samples (L, R, L, R…).
5. Sensing Direction: Time Difference of Arrival (TDOA)
5. Sensing Direction: Time Difference of Arrival (TDOA)
Sound is a pressure wave that travels through air at a finite speed, approximately:
Because the two microphones sit a small distance d apart, a sound coming from one side reaches the nearer microphone slightly before the farther one. That tiny delay is the Time Difference of Arrival (TDOA), written τ. For a distant source (far-field, plane wave) arriving at angle θ measured from the broadside direction (perpendicular to the line joining the two mics):
When the source is directly in front (θ = 0), both microphones hear it at the same instant and τ = 0. When the source is off to one side, τ grows, reaching its maximum when the source is on the microphone axis (θ = 90°):
Measuring τ digitally. We never measure τ in seconds directly; we measure it as a whole number of sample delays between the Left and Right channels using cross-correlation. We slide one channel against the other and find the lag k that makes them line up best:
The lag k* that maximizes RLR[k] is the arrival delay in samples; its sign tells us which microphone heard the sound first, i.e., which side the source is on. Converting to seconds: τ = k* / fs.
The largest lag we could ever see is the maximum delay expressed in samples:
Two techniques push the resolution finer and can be offered as extensions in Exp#6:
- Sub-sample interpolation: fit a parabola to the correlation peak and its two neighbors to estimate a fractional lag, recovering angle information below one sample.
- Higher sample rate / higher PDM clock: raising fs (and choosing High-Performance mode from the table above) increases nmax and therefore the number of distinguishable directions.
Schematic Circuit and Pin Configuration
Lab Experiments
Experiment #1: PDM/PCM Capture and Raw Sample Inspection
Experiment #1:— PDM/PCM Capture and Raw Sample Inspection
Objective
Bring the PDM/PCM hardware block to life, capture a block of audio samples from the left microphone (U4), and print the raw numbers over the UART console. By the end of this experiment, you will have answered three questions with your own measurements:
- What does a raw 16-bit PCM sample from a MEMS microphone actually look like as a number?
- Is "silence" centered on zero? (It is not — and finding out why is the point.)
- How much does the sample value swing when you speak, compared to when the room is quiet?
Everything built here becomes the foundation for Exp#2 through Exp#6. We deliberately keep this first experiment in mono mode — the second microphone is ignored until Exp#5.
Step 1: Create the Application
Using the ModusToolbox™ Project Creator, create a new application with the following settings:
- BSP: CY8CKIT-062S2-AI
- Template: Dual-CPU Empty PSoC™ 6 App
- Application name: Lab105_C1_PDM_Capture
As in the previous sensor labs, the CM0+ project is left untouched — it only releases the CM4 core. All of our work goes into the CM4 project. Copy in your existing UART_Console.c/.h and GPIO_Control.c/.h modules from the earlier labs, then add the two new files created below.
Step 2: Understand the Audio Clock Path
Before writing any code, make sure you can trace the 1.536 MHz PDM clock back to its physical source. Everything in Step 3 is simply an instruction to build the chain shown below.
Check your understanding before continuing. Using the figure, answer these on paper:
- If CLK_HF[1] were 12.288 MHz instead, what divider would be needed to still produce 1.536 MHz?
- Which single number in the figure determines the microphone's power mode — and which determines the audio sample rate?
Step 3: Extend Board_Config.h
Following the convention used throughout this lab series, every physical pin and every magic number lives in one place. Add the following block to your existing Board_Config.h.
File: Board_Config.h
/* =====================================================
* PDM Digital Microphone (Lab 105)
*
* The kit carries TWO IM72D128V01XTMA1 microphones:
* U4 = left (SELECT tied to GND, data on falling edge)
* U5 = right (SELECT tied to 3V3, data on rising edge)
*
* They share ONE clock pin and ONE data pin. Separating
* them is a job for the PDM/PCM block, not for wiring.
* ===================================================== */
/* ---- Physical pins ---------------------------------------
* These come from the kit guide / BSP. Never hard-code pin
* numbers anywhere else in the project. */
#define PDM_MIC_CLK_PIN (P10_4) /* PDM_CLK */
#define PDM_MIC_DATA_PIN (P10_5) /* PDM_DATA */
/* ---- Audio format ----------------------------------------
* Fs is the rate at which finished PCM samples appear.
* It is NOT the rate at which the PDM wire toggles. */
#define PDM_SAMPLE_RATE_HZ (16000u)
/* TODO 1 --------------------------------------------------
* Work out the decimation rate.
*
* Recall from the theory section:
* f_PDM_CLK = Fs x (decimation rate)
*
* Requirement: the resulting PDM clock must land inside the
* microphone's NORMAL power mode band, which the datasheet
* gives as 1.2 MHz to 1.65 MHz.
*
* With Fs = 16000 Hz, choose a decimation rate that puts the
* clock at 1.536 MHz. Write that number below.
* -------------------------------------------------------- */
#define PDM_DECIMATION_RATE ( /* TODO 1 */ )
/* Sample word size. 16 signed bits give a range of
* -32768 .. +32767 per sample. */
#define PDM_WORD_LENGTH (16u)
/* ---- Gain -------------------------------------------------
* Gain is expressed in units of 0.5 dB, so 20 means +10 dB.
* A gain of 0 leaves this particular board far too quiet to
* work with. We will test this claim in Exercise 1. */
#define PDM_LEFT_GAIN (20)
#define PDM_RIGHT_GAIN (20)
/* ---- Audio subsystem clock -------------------------------
* 24.576 MHz was chosen because it divides EXACTLY by
* 1.536 MHz (ratio = 16). An inexact ratio would force the
* hardware to approximate, shifting the real sample rate. */
#define AUDIO_PLL_CLOCK_HZ (24576000u)
Step 4: The PDM_Mic Module
The microphone driver follows the same shape as the IMU and pressure sensor drivers from the earlier labs: a small header exposing a handful of verbs, and a source file that keeps all HAL objects private.
File: PDM_Mic.h — given complete
The header is the contract of the module. It is provided in full because agreeing on the interface is what lets everyone's main.c look the same.
#ifndef PDM_MIC_H
#define PDM_MIC_H
#include <stdint.h>
#include <stdbool.h>
/* Initialize the audio clock tree and the PDM/PCM block.
* Must be called once, before any other function here.
* Returns true on success. */
bool PDM_Mic_Init(void);
/* Begin / end PCM sample production. While started, the
* hardware fills its RX FIFO continuously whether or not we
* are reading from it. */
bool PDM_Mic_Start(void);
bool PDM_Mic_Stop(void);
/* Throw away whatever is currently sitting in the FIFO.
* Useful when you want a FRESH block rather than stale
* samples that accumulated while the CPU was busy. */
void PDM_Mic_Flush(void);
/* Block until exactly sample_count samples have been placed
* into buffer. Returns true on success. */
bool PDM_Mic_ReadBlock(int16_t *buffer, uint32_t sample_count);
#endif /* PDM_MIC_H */
File: PDM_Mic.c — skeleton
#include "cyhal.h"
#include "Board_Config.h"
#include "PDM_Mic.h"
/* ---- Private HAL objects ----------------------------------
* These are 'static' so that no other file can reach into the
* driver and manipulate the hardware behind its back. This
* is the same encapsulation discipline used in the IMU and
* pressure sensor modules from the earlier labs. */
static cyhal_pdm_pcm_t pdm_pcm_obj; /* the PDM/PCM peripheral */
static cyhal_clock_t pll_clock; /* PLL0 */
static cyhal_clock_t audio_clock; /* CLK_HF[1] */
/* ============================================================
* PDM/PCM configuration structure
*
* TODO 2 -----------------------------------------------------
* Fill in the six fields below using the constants you defined
* in Board_Config.h.
*
* The one that requires thought is .mode . The available
* choices are:
* CYHAL_PDM_PCM_MODE_LEFT
* CYHAL_PDM_PCM_MODE_RIGHT
* CYHAL_PDM_PCM_MODE_STEREO
*
* In Exp#1 we want ONLY microphone U4. Look back at the theory
* section: U4 has its SELECT pin tied to GND. Which of the
* three modes corresponds to that microphone?
* ---------------------------------------------------------- */
static const cyhal_pdm_pcm_cfg_t pdm_pcm_cfg =
{
.sample_rate = /* TODO 2a */,
.decimation_rate = /* TODO 2b */,
.mode = /* TODO 2c */,
.word_length = /* TODO 2d */,
.left_gain = /* TODO 2e */,
.right_gain = /* TODO 2f */,
};
/* ============================================================
* Build the clock chain shown in Figure Exp#1-1.
*
* TODO 3 -----------------------------------------------------
* Complete the three missing calls. The pattern for every
* clock in the HAL is the same three-step ritual:
*
* reserve -> configure -> enable
*
* Useful functions:
* cyhal_clock_reserve(&obj, &CYHAL_CLOCK_PLL[0]);
* cyhal_clock_reserve(&obj, &CYHAL_CLOCK_HF[1]);
* cyhal_clock_set_frequency(&obj, hz, NULL);
* cyhal_clock_set_source(&child, &parent);
* cyhal_clock_set_enabled(&obj, true, true);
*
* Every one of them returns cy_rslt_t. Check every result:
* a silently ignored clock failure is one of the hardest
* embedded bugs to track down later.
* ---------------------------------------------------------- */
static bool PDM_Mic_ClockInit(void)
{
/* --- Step A : PLL0 becomes a 24.576 MHz source --------- */
if (CY_RSLT_SUCCESS != cyhal_clock_reserve(&pll_clock,
&CYHAL_CLOCK_PLL[0])) {
return false;
}
/* TODO 3a : set PLL0 frequency to AUDIO_PLL_CLOCK_HZ,
* then enable it. Return false on any failure. */
/* --- Step B : CLK_HF[1] takes PLL0 as its parent ------- */
/* TODO 3b : reserve CLK_HF[1] into audio_clock. */
/* TODO 3c : make pll_clock the SOURCE of audio_clock,
* then enable audio_clock. */
return true;
}
bool PDM_Mic_Init(void)
{
if (!PDM_Mic_ClockInit()) {
return false;
}
/* TODO 4 ------------------------------------------------
* Call cyhal_pdm_pcm_init(). Its parameters, in order:
*
* 1. address of the pdm_pcm_obj
* 2. The DATA pin
* 3. The CLK pin
* 4. Address of the clock source to use
* 5. Address of the configuration structure
*
* Careful: DATA comes BEFORE CLK. Swapping them is a
* popular mistake and produces silence, not an error.
*
* Return false if the call does not return
* CY_RSLT_SUCCESS, otherwise fall through to return true.
* ------------------------------------------------------ */
return true;
}
bool PDM_Mic_Start(void)
{
return (CY_RSLT_SUCCESS == cyhal_pdm_pcm_start(&pdm_pcm_obj));
}
bool PDM_Mic_Stop(void)
{
return (CY_RSLT_SUCCESS == cyhal_pdm_pcm_stop(&pdm_pcm_obj));
}
void PDM_Mic_Flush(void)
{
(void)cyhal_pdm_pcm_clear(&pdm_pcm_obj);
}
/* ============================================================
* TODO 5: implement the block read.
* See Figure Exp#1-2 and the pseudocode below the listing.
* ========================================================== */
bool PDM_Mic_ReadBlock(int16_t *buffer, uint32_t sample_count)
{
/* TODO 5 */
return false; /* replace this */
}
Figure Exp#1-2 shows the required control flow.
The same logic in pseudocode:
FUNCTION ReadBlock(buffer, sample_count) RETURNS boolean
collected <- 0
WHILE collected < sample_count DO
chunk <- sample_count - collected // still missing
result <- hardware_read( destination = buffer[collected],
length = ADDRESS OF chunk )
IF result is not SUCCESS THEN
RETURN false
END IF
// the hardware has an OVERWRITTEN chunk with the number of
// samples it actually delivered, which may be smaller
collected <- collected + chunk
END WHILE
RETURN true
END FUNCTION
Step 5: main.c
The main program captures one block of samples, prints the first 32 of them, and then reports simple statistics over the whole block: minimum, maximum, peak-to-peak swing, and mean. The mean is the interesting one — it is our measurement of the DC offset.
Figure Exp#1-3 shows the overall structure.
File: main.c — skeleton
#include "cybsp.h"
#include "cyhal.h"
#include "Board_Config.h"
#include "UART_Console.h"
#include "PDM_Mic.h"
/* At 16 kHz, 256 samples is 16 ms of audio.
* Small enough to print, long enough to be meaningful. */
#define CAPTURE_SAMPLES (256u)
#define PRINT_SAMPLES (32u) /* how many values to list */
#define SETTLE_BLOCKS (4u) /* discarded after start */
/* Static, not on the stack: 256 x 2 bytes is 512 bytes, which
* is more than we want to place on a small startup stack. */
static int16_t audio_buf[CAPTURE_SAMPLES];
/* ============================================================
* TODO 6 : compute and print block statistics.
*
* Report four numbers over one capture block:
* min - smallest sample value seen
* max - largest sample value seen
* p-p - peak-to-peak, i.e. max minus min
* mean - arithmetic average -> THE DC OFFSET
*
* Arithmetic notes:
* - Use int32_t for the running sum. 256 samples of
* magnitude 32768 needs 24 bits, so int32_t is safe,
* but an int16_t accumulator would overflow immediately.
* - Integer division truncates toward zero. That is
* acceptable here; we are not chasing a fractional count.
* - Initialise min_val to the LARGEST possible sample and
* max_val to the SMALLEST, so that the first comparison
* always replaces them.
* ========================================================== */
static void AnalyseBlock(const int16_t *buf, uint32_t count)
{
int32_t sum = 0;
int16_t min_val = 32767; /* deliberately the maximum */
int16_t max_val = -32768; /* deliberately the minimum */
/* TODO 6a : loop over the block, accumulating sum and
* tracking min_val and max_val. */
/* TODO 6b : compute the mean from sum and count. */
/* TODO 6c : print the four values on one line using
* UART_Print() and UART_PrintInt().
* Suggested format:
* min = -461 max = -352 p-p = 109 mean = -409
*/
}
int main(void)
{
cy_rslt_t result = cybsp_init();
if (CY_RSLT_SUCCESS != result) {
CY_ASSERT(0);
}
__enable_irq();
UART_Console_Init();
UART_PrintLine("");
UART_PrintLine("=========================================");
UART_PrintLine(" Lab 105 Exp#1 : PDM/PCM Raw Capture");
UART_PrintLine(" Microphone : U4 (left channel, mono)");
UART_PrintLine(" Fs = 16000 Hz");
UART_PrintLine("=========================================");
if (!PDM_Mic_Init()) {
UART_PrintLine("ERROR: PDM_Mic_Init() failed.");
for (;;) { }
}
UART_PrintLine("PDM/PCM block initialised.");
if (!PDM_Mic_Start()) {
UART_PrintLine("ERROR: PDM_Mic_Start() failed.");
for (;;) { }
}
/* TODO 7 ------------------------------------------------
* The microphone emits an 'idle tone' for the first few
* milliseconds after the clock starts, before real audio
* is available. Read and DISCARD SETTLE_BLOCKS blocks so
* that this start-up artefact never reaches our analysis.
*
* Hint: call PDM_Mic_ReadBlock() into audio_buf and simply
* ignore the contents. Cast the return value to (void) to
* make it obvious to a reader that this is intentional.
* ------------------------------------------------------ */
UART_PrintLine("Settling complete. Starting capture.");
UART_PrintLine("");
for (;;) {
/* Drop stale samples so each printout is a fresh
* snapshot rather than a queue backed up behind our
* one-second delay. */
PDM_Mic_Flush();
if (!PDM_Mic_ReadBlock(audio_buf, CAPTURE_SAMPLES)) {
UART_PrintLine("ERROR: read failed.");
continue;
}
UART_PrintLine("--- raw samples ---");
/* TODO 8 --------------------------------------------
* Print the first PRINT_SAMPLES values of audio_buf.
* Lay them out 8 per line so the terminal stays
* readable: print a TAB between values, and a newline
* after every 8th value.
*
* Hint: the modulo operator tells you when you have
* reached the end of a row.
* -------------------------------------------------- */
AnalyseBlock(audio_buf, CAPTURE_SAMPLES);
UART_PrintLine("");
cyhal_system_delay_ms(1000u);
}
}
Pseudocode for AnalyseBlock() (TODO 6):
PROCEDURE AnalyseBlock(buf, count)
sum <- 0
min_val <- +32767 // start at the extreme opposite
max_val <- -32768 // so the first sample wins
FOR i FROM 0 TO count-1 DO
s <- buf[i]
sum <- sum + s // 32-bit accumulator
IF s < min_val THEN min_val <- s END IF
IF s > max_val THEN max_val <- s END IF
END FOR
mean <- sum / count // integer division
PRINT "min = ", min_val
PRINT "max = ", max_val
PRINT "p-p = ", max_val - min_val
PRINT "mean = ", mean // <-- the DC offset
END PROCEDURE
Step 6: Build, Program, and Observe
- Connect the kit over USB-C and confirm KitProg3 enumerates.
- Open a serial terminal on the KitProg3 COM port at 115200-8-N-1.
- Build and program the CM4 application.
- Keep the room quiet and watch a few blocks scroll past. Then speak towards the board and watch again.
You should see output resembling the following. The exact numbers will differ on your board — that is expected and is itself part of the lesson.
--- raw samples ---
-412 -389 -401 -430 -418 -395 -407 -421
-399 -416 -408 -388 -425 -410 -402 -419
-393 -414 -430 -401 -387 -422 -409 -398
-417 -404 -391 -428 -413 -400 -406 -423
min = -461 max = -352 p-p = 109 mean = -409
Observations to Record
Fill in the following table from your own board. You will refer back to these numbers in Exp#2 and Exp#3 when choosing detection thresholds.
| Condition | min | max | peak-to-peak | mean |
|---|---|---|---|---|
| Quiet room | ||||
| Speaking normally at ~30 cm | ||||
| Hand clap | ||||
| Finger covering U4 |
Two things are worth noticing as you fill this in:
- The mean stays roughly constant across all four conditions, while the peak-to-peak swing changes dramatically. That separation is exactly what the DC blocker in Exp#2 exploits.
- Covering U4 with a finger should visibly reduce the swing. If it does not, you may be covering U5 instead — a useful early hint that the two microphones are physically distinct, which Exp#5 will explore properly.
Exercises
Exercise 1 — Gain. Change PDM_LEFT_GAIN from 20 to 0, rebuild, and repeat the quiet-room and speaking measurements. Remember that the units are 0.5 dB, so this is a 10 dB reduction. Does the DC offset scale with the gain, or is it independent of it? What does your answer suggest about where in the signal chain the offset originates?
Exercise 2 — Sample rate. Change PDM_SAMPLE_RATE_HZ to 8000. Then work out for yourself what PDM_DECIMATION_RATE must be set to if the PDM clock is to stay at 1.536 MHz, and change it accordingly. Capture 256 samples again — how many milliseconds of audio does one block now represent? Try whistling a high note at both settings and describe the difference.
Exercise 3 — Breaking the power mode (do this last). Set the decimation rate to 64, leaving Fs at 16 kHz. Calculate the resulting PDM clock frequency and locate it in the microphone's power-mode table. You should find it lands in an undefined gap between Low-Power and Normal mode. Program the board and describe what you observe. Then restore the decimation rate to the value from TODO 1.
Checkpoint
Before moving to Exp#2, confirm that you can answer yes to all of the following:
- All 8 TODO items have been implemented, and the project builds with no warnings.
- The application programs run without errors from OpenOCD.
- The banner appears on the terminal, and samples scroll past once per second.
- Sample values change visibly when you speak or clap.
- You have recorded a numerical value for the DC offset (the mean) on your own board.
That last number is the one you will need immediately in Exp#2.
Experiment #2: DC Removal and a Live RMS / dB Bar Meter
Experiment #2 — DC Removal and a Live RMS / dB Bar Meter
Objective
In Exp#1, you discovered that "silence" sits at roughly −400 rather than at zero. In this experiment, you will remove that offset, turn the corrected signal into a single loudness number, and display it as a live bar meter on the terminal — something like this:
RMS = 118 -48.9 dBFS [####----------------]
RMS = 2947 -20.9 dBFS [#############-------]
RMS = 96 -50.7 dBFS [###-----------------]
Everything is computed with integer arithmetic only. No float, no double, no math.h. This is not an arbitrary restriction: the loudness calculation you build here will later run inside a real-time loop alongside a machine-learning model, where every cycle and every byte of code space matters.
Three ideas carry this experiment:
- A one-pole high-pass filter removes the DC offset using nothing but addition and bit shifts.
- RMS collapses a window of samples into a single number representing energy.
- A logarithm converts that number to decibels, matching how human hearing actually works — and the logarithm can be extracted almost for free from the bit pattern of an integer.
Step 1: The Signal Chain
Figure Exp#2-1 shows the complete path from raw sample to bar meter. Each stage is a small function you will write.
Step 2: The DC Blocker
The theory section gave the filter as:
Figure Exp#2-2 shows the same equation drawn as a signal flow graph. Each z⁻¹ block simply means "the value from one sample ago", which in C is a static variable.
The cut-off frequency of this filter is approximately:
New file: Audio_DSP.h — given complete
#ifndef AUDIO_DSP_H
#define AUDIO_DSP_H
#include <stdint.h>
/* ---- DC blocker ------------------------------------------
* Reset clears the filter's memory. Call it whenever the
* audio stream is interrupted, otherwise the stale history
* will produce a transient in the output. */
void DcBlocker_Reset(void);
int32_t DcBlocker_Step(int16_t x);
/* ---- Helpers ---------------------------------------------- */
uint32_t Isqrt_u32(uint32_t v); /* floor(sqrt(v)) */
int32_t Log2_Q8(uint32_t v); /* log2(v) x 256 */
/* ---- Loudness ---------------------------------------------
* Rms_Block : windowed RMS of an already DC-corrected block
* Rms_To_dBFS_x10 : convert RMS to dBFS in units of 0.1 dB,
* so -489 means -48.9 dBFS */
uint32_t Rms_Block(const int32_t *y, uint32_t count);
int32_t Rms_To_dBFS_x10(uint32_t rms);
#endif /* AUDIO_DSP_H */
New file: Audio_DSP.c — skeleton
#include "Audio_DSP.h"
#include "Board_Config.h"
/* ============================================================
* TODO 1 : choose the filter constant.
*
* We want a cut-off around 10 Hz, safely below the 20 Hz
* low-frequency roll-off of the microphone itself, so that we
* remove the DC offset without touching real audio.
*
* f_c = Fs / ( 2 * PI * 2^k )
*
* With Fs = 16000 Hz, evaluate f_c for k = 7, 8 and 9.
* Write the k that lands nearest to 10 Hz.
*
* (Compute this on paper. Show your three values in your
* lab report, not just the answer.)
* ---------------------------------------------------------- */
#define DC_SHIFT ( /* TODO 1 */ )
/* ---- Filter state ------------------------------------------
* 'static' at file scope means these persist between calls but
* are invisible outside this file. They ARE the z-inverse-1
* blocks in Figure Exp#2-2. */
static int32_t dc_prev_x = 0;
static int32_t dc_prev_y = 0;
void DcBlocker_Reset(void)
{
dc_prev_x = 0;
dc_prev_y = 0;
}
/* ============================================================
* TODO 2 : implement one step of the DC blocker.
*
* The equation, with the multiply replaced by a shift:
*
* y = x - prev_x + prev_y - (prev_y >> DC_SHIFT)
* \________/ \___________________________/
* difference alpha * prev_y
*
* Then update BOTH stored values for the next call, and
* return y.
*
* Order matters: compute y FIRST, using the old prev_x and
* prev_y, and only then overwrite them. Updating too early
* is the most common bug in this function.
*
* Note on the shift: prev_y is int32_t, i.e. SIGNED. A right
* shift of a negative signed value is arithmetic on every
* toolchain we use here, which is what we want.
* ---------------------------------------------------------- */
int32_t DcBlocker_Step(int16_t x)
{
/* TODO 2 */
return 0; /* replace this */
}
/* ============================================================
* Integer square root - GIVEN, no changes needed.
*
* This is the classic digit-by-digit binary method. It finds
* floor(sqrt(v)) using only shifts, comparisons and
* subtraction - no division and no floating point.
*
* How it works, briefly: it builds the answer two bits of
* input at a time, exactly the way long division builds a
* quotient one digit at a time. 'd' walks down the powers of
* four; 'c' accumulates the root; 'x' holds the remainder.
*
* You are not required to derive this. You ARE expected to be
* able to explain why it contains no division.
* ========================================================== */
uint32_t Isqrt_u32(uint32_t v)
{
uint32_t x = v;
uint32_t c = 0u;
uint32_t d = 1u << 30; /* highest power of 4 in 32 bits */
while (d > x) {
d >>= 2;
}
while (d != 0u) {
if (x >= c + d) {
x -= c + d;
c = (c >> 1) + d;
} else {
c >>= 1;
}
d >>= 2;
}
return c;
}
/* ============================================================
* TODO 3 : base-2 logarithm in Q8 fixed point.
*
* Returns log2(v) multiplied by 256. Two parts:
*
* (a) INTEGER part. The position of the most significant set
* bit IS the integer part of log2. For v = 1000, the MSB
* sits at bit 9, and indeed log2(1000) = 9.97.
* Find it by shifting right until only 1 remains, counting
* the shifts.
*
* (b) FRACTIONAL part. Normalise v so its MSB sits at bit 8,
* giving a value in the range 256..511:
*
* norm = (e >= 8) ? (v >> (e - 8)) : (v << (8 - e));
*
* Then norm - 256 is the fraction, scaled by 256.
* We are approximating log2(1+f) with f itself, which is
* accurate to about 0.09 - roughly half a decibel later.
* Perfectly adequate for a bar meter, and worth knowing
* that we made the trade deliberately.
*
* Result: e * 256 + (norm - 256)
* ---------------------------------------------------------- */
int32_t Log2_Q8(uint32_t v)
{
if (v == 0u) {
return 0;
}
/* TODO 3a : find e, the position of the most significant bit */
/* TODO 3b : normalise and extract the fraction */
/* TODO 3c : combine and return */
return 0; /* replace this */
}
/* ============================================================
* TODO 4 : windowed RMS of a DC-corrected block.
*
* RMS = sqrt( (1/N) * SUM of y[n]^2 )
*
* Three arithmetic hazards to handle, in order:
*
* 1. CLAMP. After the DC blocker, y can briefly exceed the
* int16 range. Clamp each value to -32768..32767 before
* squaring. Saturation is the standard audio response to
* overload - far better than letting a value wrap around
* and turn a loud sound into a quiet one.
*
* 2. ACCUMULATOR WIDTH. 32767^2 is about 1.07e9. Adding up
* 1024 of them reaches 1.1e12, which does NOT fit in 32
* bits. Use uint64_t for the accumulator.
*
* 3. Divide by count BEFORE taking the square root, and only
* then narrow to uint32_t for Isqrt_u32().
* ---------------------------------------------------------- */
uint32_t Rms_Block(const int32_t *y, uint32_t count)
{
/* TODO 4 */
return 0u; /* replace this */
}
/* ============================================================
* TODO 5 : convert RMS to dBFS, in units of 0.1 dB.
*
* Start from the definition and change the base:
*
* 20*log10(v) = 20 * log2(v) / log2(10)
* = 6.0206 * log2(v)
*
* We hold log2 in Q8, and we want tenths of a dB, so:
*
* dB_x10 = ( Log2_Q8(v) * 60206 ) / 256000
*
* (60206 = 6.0206 x 10000, and 256000 = 256 x 1000.
* Check the magnitude: Log2_Q8 tops out near 4096, so the
* product peaks around 2.5e8 - comfortably inside int32_t.)
*
* That gives dB relative to ONE COUNT. Full scale is 32768
* counts, which is +90.3 dB on that scale. Subtract
* DBFS_FULL_SCALE_X10 so that full scale reads 0.0 dBFS and
* everything quieter reads negative, as audio engineers expect.
*
* Finally, clamp the result at DBFS_FLOOR_X10 so that silence
* does not produce an absurd number.
* ---------------------------------------------------------- */
#define DBFS_FULL_SCALE_X10 (903) /* 20*log10(32768) */
#define DBFS_FLOOR_X10 (-600) /* -60.0 dBFS floor */
int32_t Rms_To_dBFS_x10(uint32_t rms)
{
if (rms == 0u) {
return DBFS_FLOOR_X10;
}
/* TODO 5 */
return DBFS_FLOOR_X10; /* replace this */
}
Pseudocode for TODO 2 and TODO 4:
FUNCTION DcBlocker_Step(x) RETURNS integer
// use the OLD stored values to build the new output
y <- x - prev_x + prev_y - ARITHMETIC_SHIFT_RIGHT(prev_y, DC_SHIFT)
prev_x <- x // only now may we overwrite
prev_y <- y
RETURN y
END FUNCTION
FUNCTION Rms_Block(y, count) RETURNS unsigned
acc <- 0 // 64-bit
FOR i FROM 0 TO count-1 DO
s <- y[i]
IF s > 32767 THEN s <- 32767 END IF // clamp
IF s < -32768 THEN s <- -32768 END IF
acc <- acc + (s * s) // promote to 64-bit
END FOR
mean_square <- acc / count // now fits in 32 bits
RETURN Isqrt_u32(mean_square)
END FUNCTION
Step 3: The Bar Meter and main.c
Figure Exp#2-3 shows the new main loop. Note two deliberate differences from Exp#1:
- There is no flush and no one-second delay. The meter must run continuously, and the DC blocker's memory only makes sense if the sample stream is unbroken.
- Each line is terminated with a carriage return rather than a newline, so the meter updates in place instead of scrolling.
File: main.c — skeleton
#include "cybsp.h"
#include "cyhal.h"
#include "Board_Config.h"
#include "UART_Console.h"
#include "PDM_Mic.h"
#include "Audio_DSP.h"
/* 1024 samples at 16 kHz = 64 ms, giving about 15 meter
* updates per second. Fast enough to feel live, slow enough
* that the terminal keeps up. */
#define RMS_WINDOW (1024u)
#define BAR_WIDTH (20u)
static int16_t raw_buf[RMS_WINDOW]; /* x[n] from hardware */
static int32_t dsp_buf[RMS_WINDOW]; /* y[n] after DC block */
/* ============================================================
* TODO 6 : render the bar.
*
* Map the dB reading onto BAR_WIDTH character cells:
*
* -60.0 dBFS -> 0 filled cells (silence)
* 0.0 dBFS -> 20 filled cells (full scale)
*
* Since db_x10 runs from -600 to 0, the linear mapping is:
*
* filled = (db_x10 + 600) * BAR_WIDTH / 600
*
* Clamp filled to the range 0..BAR_WIDTH, then print:
* '[' then 'filled' hashes then the remaining dashes
* then ']'
*
* Use UART_Print() with a one-character string, or build the
* line in a local char array and send it in one go. The
* second approach is noticeably faster - think about why.
* ---------------------------------------------------------- */
static void PrintBar(int32_t db_x10)
{
/* TODO 6 */
}
int main(void)
{
if (CY_RSLT_SUCCESS != cybsp_init()) {
CY_ASSERT(0);
}
__enable_irq();
UART_Console_Init();
UART_PrintLine("");
UART_PrintLine("=========================================");
UART_PrintLine(" Lab 105 Exp#2 : DC Removal + RMS Meter");
UART_PrintLine("=========================================");
if (!PDM_Mic_Init()) {
UART_PrintLine("ERROR: PDM_Mic_Init() failed.");
for (;;) { }
}
if (!PDM_Mic_Start()) {
UART_PrintLine("ERROR: PDM_Mic_Start() failed.");
for (;;) { }
}
/* Discard the microphone start-up idle tone, exactly as in
* Exp#1, and only THEN clear the filter memory - otherwise the
* filter would still be digesting the idle tone. */
for (uint32_t i = 0u; i < 4u; i++) {
(void)PDM_Mic_ReadBlock(raw_buf, RMS_WINDOW);
}
DcBlocker_Reset();
for (;;) {
if (!PDM_Mic_ReadBlock(raw_buf, RMS_WINDOW)) {
continue;
}
/* TODO 7 --------------------------------------------
* Run every sample of raw_buf through DcBlocker_Step()
* and store the results into dsp_buf.
* -------------------------------------------------- */
/* TODO 8 --------------------------------------------
* a) compute rms using Rms_Block()
* b) compute db using Rms_To_dBFS_x10()
* c) print a line of the form
*
* RMS = 2947 -20.9 dBFS [######--------------]
*
* then call PrintBar(db) and finish with a CARRIAGE
* RETURN (not a newline) so the next update
* overwrites this line in place.
*
* Printing tenths: db is in units of 0.1 dB, so the
* whole part is db/10 and the tenths digit is
* db%10. Watch the sign - for negative values,
* print the minus once and use the magnitude for
* both parts, or the output reads "-20.-9".
* -------------------------------------------------- */
}
}
Step 4: Build and Observe
- Build, program, and open the terminal at 115200-8-N-1.
- In a quiet room, note the resting reading. It should be a small negative dBFS value and stable.
- Speak, clap, and whistle. The bar should respond immediately and fall back smoothly.
- Now cover the microphone with a finger and confirm the reading drops.
Observations to Record
| Condition | RMS | dBFS | bar cells filled |
|---|---|---|---|
| Quiet room (noise floor) | |||
| Normal speech at ~30 cm | |||
| Hand clap (peak reading) | |||
| Finger over U4 |
Keep the quiet room and hand-clap rows safe. In Exp#3, you will need a threshold that sits clearly above the first and clearly below the second.
Exercises
Exercise 1 — Filter cut-off. Change DC_SHIFT to 4 (a much higher cut-off, around 160 Hz) and then to 12 (around 2.5 Hz). For each, describe two things: how quickly the reading settles after you press the reset button, and whether low-pitched sounds such as a hum still register. Explain the trade-off you have found in your own words.
Exercise 2 — Window length. Change RMS_WINDOW to 128 and then to 4096. How does the meter's behavior change when you clap? Which setting would you choose for a VU meter a human watches, and which for detecting a short transient? These are different answers, and Exp#3 will need the second one.
Exercise 3 — Remove the clamp. Temporarily delete the clamping step in Rms_Block() and shout directly into the microphone. Watch for a reading that jumps downward at the loudest moment. Explain what happened at the bit level. Then restore the clamp.
Exercise 4 — Verify the logarithm. Add temporary code that prints Rms_To_dBFS_x10() for the fixed inputs 1, 32, 1024, 32768. Compare each against 20·log10(v/32768) computed on a calculator, and tabulate the error. Where is the approximation worst, and does that matter for a bar meter?
Checkpoint
- All 8 TODO items have been implemented, and the project builds with no warnings.
- The quiet-room reading is stable and does not drift over a minute of running.
- The bar responds to speech and returns to the floor afterward.
- No floating-point types appear anywhere in your code. Check by searching for float and double.
- You have recorded quiet-room and hand-clap RMS values for use in Exp#3.
Experiment #3: Clap Detection with LED Response
Experiment #3 — Clap Detection with LED Response
Objective
C2 gave you a number that tracks loudness. This experiment turns that number into a decision: has a clap just happened? And then into a pattern: two claps switch the LED on, three claps switch it off.
Detection sounds simple — pick a threshold, compare, and done. It is not, and the three problems below are why. Solving them is the real content of Exp#3, and every one of them reappears in serious audio event detection, including the machine-learning systems later in this course.
- A fixed threshold does not survive contact with the real world. A value tuned in a quiet classroom will fire constantly in a noisy lab, and one tuned for the lab will ignore claps in a quiet room. The threshold must adapt.
- One clap is not one event. A clap has a decaying tail lasting tens of milliseconds. A naive detector reports five or six claps from a single one.
- Counting claps requires memory. "Two claps" only means something if the program remembers the first one and knows how long to keep waiting for the second.
Step 1: How Detection Actually Works
Figure Exp#3-1 shows a clap arriving twice, drawn as the frame-by-frame RMS you computed in Exp#2. Study it before writing any code — almost every design decision in this experiment is visible in this one picture.
Three mechanisms are visible, and each maps to a block of code you will write:
| Mechanism | What it solves |
|---|---|
| Adaptive noise floor green dashed line |
A slowly-moving estimate of "how loud is this room right now". The threshold is derived from it, so the detector automatically retunes itself when you move between rooms. |
| Relative threshold red dashed line |
We do not ask "Is this loud?" but "Is this much louder than usual?" A multiple of the floor rather than a fixed number. |
| Refractory period shaded band |
After a detection, ignore everything for a fixed time. The clap's tail passes harmlessly. Borrowed directly from how a biological neuron behaves after firing. |
Step 2: Choosing the Frame Size
C2 used a 1024-sample window because humans need a steady number to read. A transient detector wants the opposite. Consider what happens to a 5 ms clap averaged over different window lengths:
| Window | Duration at 16 kHz | Effect of a short clap |
|---|---|---|
| 1024 samples | 64 ms | Clap energy is diluted across mostly-silent samples; the peak barely rises. |
| 128 samples | 8 ms | Comparable to the clap itself, the peak stands out clearly. |
| 16 samples | 1 ms | So short that individual waveform cycles show through; the reading becomes erratic. |
This is exactly the trade-off you measured in Exp#2, Exercise 2. We take the middle option.
Step 3: The Clap_Detect Module
New file: Clap_Detect.h — given complete
#ifndef CLAP_DETECT_H
#define CLAP_DETECT_H
#include <stdint.h>
/* Result of one frame of processing. CLAP_NONE is returned on
* almost every frame; a real event appears only when a whole
* clap PATTERN has finished and been classified. */
typedef enum clap_event_t {
CLAP_NONE = 0,
CLAP_SINGLE,
CLAP_DOUBLE,
CLAP_TRIPLE,
CLAP_MANY
} clap_event_t;
void ClapDetect_Init(void);
/* Feed exactly one frame's RMS value. Call once per frame,
* every frame, without gaps - the internal timers count in
* frames, so a missed call distorts every timing constant. */
clap_event_t ClapDetect_Update(uint32_t frame_rms);
/* Exposed for calibration printouts. */
uint32_t ClapDetect_NoiseFloor(void);
#endif /* CLAP_DETECT_H */
New file: Clap_Detect.c — skeleton
#include "Clap_Detect.h"
#include "Board_Config.h"
/* ============================================================
* TODO 1 : timing constants, expressed in FRAMES.
*
* One frame is 128 samples at 16 kHz = 8 ms.
* Convert each duration below into a frame count:
*
* REFRACTORY_FRAMES : about 160 ms of deafness after a
* detection. Long enough to swallow a
* clap's tail, short enough that two
* deliberate claps are still separable.
*
* PATTERN_FRAMES : about 700 ms of waiting for the NEXT
* clap before deciding the pattern has
* ended.
*
* Show your arithmetic in the lab report.
* ---------------------------------------------------------- */
#define REFRACTORY_FRAMES ( /* TODO 1a */ )
#define PATTERN_FRAMES ( /* TODO 1b */ )
/* ============================================================
* TODO 2 : detection thresholds, calibrated from YOUR board.
*
* Look up the two numbers you recorded in Exp#2:
* F = quiet-room RMS (the noise floor)
* C = hand-clap peak RMS
*
* (a) TRIGGER_RATIO
* We want the threshold to sit comfortably between F and
* C - not just above F, or ordinary noise fires it; not
* just below C, or a soft clap is missed.
*
* The balanced choice is the GEOMETRIC mean, i.e. halfway
* between them on a logarithmic scale:
*
* TRIGGER_RATIO ~= sqrt( C / F )
*
* Compute C/F for your board, take the square root, and
* round to a whole number. If your result is below 4,
* use 4; if above 16, use 16.
*
* (b) MIN_RMS_ABSOLUTE
* A floor on the floor. In a very quiet room the noise
* floor can fall so low that ANY small sound is 8 times
* larger than it. This constant stops the detector from
* becoming absurdly sensitive.
*
* Use roughly 3 x your quiet-room RMS.
* ---------------------------------------------------------- */
#define TRIGGER_RATIO ( /* TODO 2a */ )
#define MIN_RMS_ABSOLUTE ( /* TODO 2b */ )
/* ---- Noise floor tracking rates ---------------------------
* Deliberately ASYMMETRIC. The floor must rise slowly, so a
* loud sound cannot drag it up and blind the detector; but it
* may fall faster, so the detector recovers sensitivity soon
* after a noisy period ends.
*
* rise: 2^9 = 512 frames ~ 4.1 s
* fall: 2^5 = 32 frames ~ 256 ms
* ---------------------------------------------------------- */
#define FLOOR_RISE_SHIFT (9)
#define FLOOR_FALL_SHIFT (5)
#define FLOOR_INITIAL (200u)
/* ---- Internal state --------------------------------------- */
typedef enum state_t {
ST_IDLE = 0, /* nothing heard recently */
ST_COUNTING /* a pattern is in progress */
} state_t;
static state_t state;
static uint32_t noise_floor;
static uint32_t refractory; /* frames remaining */
static uint32_t pattern_timer; /* frames remaining */
static uint32_t clap_count;
void ClapDetect_Init(void)
{
state = ST_IDLE;
noise_floor = FLOOR_INITIAL;
refractory = 0u;
pattern_timer = 0u;
clap_count = 0u;
}
uint32_t ClapDetect_NoiseFloor(void)
{
return noise_floor;
}
/* ============================================================
* One frame of processing.
*
* The order of the four blocks below is not arbitrary. Read
* the whole function before filling anything in.
* ========================================================== */
clap_event_t ClapDetect_Update(uint32_t frame_rms)
{
clap_event_t evt = CLAP_NONE;
uint32_t detected = 0u;
if (refractory > 0u) {
/* TODO 3 --------------------------------------------
* We are deaf this frame. Do exactly two things:
* - count the refractory timer down by one
* - do NOT touch noise_floor, and do NOT detect
*
* Leaving the floor frozen here is the point. See the
* yellow note under Figure Exp#3-1.
* -------------------------------------------------- */
} else {
/* TODO 4 --------------------------------------------
* (a) Update the noise floor towards frame_rms, using
* the ASYMMETRIC rates:
*
* if frame_rms is ABOVE the floor:
* floor += (frame_rms - floor) >> FLOOR_RISE_SHIFT
* otherwise:
* floor -= (floor - frame_rms) >> FLOOR_FALL_SHIFT
*
* Both forms subtract the smaller value from the
* larger, so the intermediate result is never
* negative - important, because these are unsigned.
*
* (b) Decide whether this frame is a detection. BOTH
* conditions must hold:
*
* frame_rms > noise_floor * TRIGGER_RATIO
* frame_rms > MIN_RMS_ABSOLUTE
*
* (c) If it is a detection, set detected = 1 and load
* refractory with REFRACTORY_FRAMES.
* -------------------------------------------------- */
}
/* --------------------------------------------------------
* The pattern state machine runs on EVERY frame, including
* refractory frames, because its timer must keep counting
* down while we are deaf.
* ------------------------------------------------------ */
switch (state) {
case ST_IDLE:
/* TODO 5 ----------------------------------------
* If detected: this is the first clap of a new
* pattern. Set clap_count to 1, load
* pattern_timer with PATTERN_FRAMES, and move to
* ST_COUNTING.
* ---------------------------------------------- */
break;
case ST_COUNTING:
/* TODO 6 ----------------------------------------
* Two cases:
*
* - detected : another clap belongs to this
* pattern. Increment clap_count and RESTART
* pattern_timer. Stay in ST_COUNTING.
*
* - otherwise : count pattern_timer down. When it
* reaches zero the pattern is complete - map
* clap_count onto the matching clap_event_t
* (1 -> CLAP_SINGLE, 2 -> CLAP_DOUBLE,
* 3 -> CLAP_TRIPLE, anything more -> CLAP_MANY),
* store it in evt, and return to ST_IDLE.
* ---------------------------------------------- */
break;
default:
state = ST_IDLE;
break;
}
return evt;
}
Figure Exp#3-2 is the same state machine drawn out. Note that the refractory counter sits outside it, acting as a gate on the word "detected".
Pseudocode for the whole update function:
FUNCTION ClapDetect_Update(frame_rms) RETURNS event
event <- NONE
detected <- FALSE
IF refractory > 0 THEN
refractory <- refractory - 1 // deaf; floor frozen
ELSE
IF frame_rms > noise_floor THEN
noise_floor <- noise_floor
+ (frame_rms - noise_floor) >> FLOOR_RISE_SHIFT
ELSE
noise_floor <- noise_floor
- (noise_floor - frame_rms) >> FLOOR_FALL_SHIFT
END IF
IF frame_rms > noise_floor * TRIGGER_RATIO
AND frame_rms > MIN_RMS_ABSOLUTE THEN
detected <- TRUE
refractory <- REFRACTORY_FRAMES
END IF
END IF
// ---- state machine, runs every frame ----
IF state = IDLE THEN
IF detected THEN
clap_count <- 1
pattern_timer <- PATTERN_FRAMES
state <- COUNTING
END IF
ELSE IF state = COUNTING THEN
IF detected THEN
clap_count <- clap_count + 1
pattern_timer <- PATTERN_FRAMES // restart
ELSE
pattern_timer <- pattern_timer - 1
IF pattern_timer = 0 THEN
event <- CLASSIFY(clap_count)
state <- IDLE
END IF
END IF
END IF
RETURN event
END FUNCTION
Step 4: main.c
File: main.c
#include "cybsp.h"
#include "cyhal.h"
#include "Board_Config.h"
#include "UART_Console.h"
#include "GPIO_Control.h"
#include "PDM_Mic.h"
#include "Audio_DSP.h"
#include "Clap_Detect.h"
/* 128 samples at 16 kHz = 8 ms per frame. */
#define FRAME_SAMPLES (128u)
/* Print a calibration line about once per second.
* 1000 ms / 8 ms = 125 frames. */
#define STATUS_INTERVAL (125u)
static int16_t raw_buf[FRAME_SAMPLES];
static int32_t dsp_buf[FRAME_SAMPLES];
int main(void)
{
uint32_t frame_counter = 0u;
if (CY_RSLT_SUCCESS != cybsp_init()) {
CY_ASSERT(0);
}
__enable_irq();
UART_Console_Init();
GPIO_Control_Init();
UART_PrintLine("");
UART_PrintLine("=========================================");
UART_PrintLine(" Lab 105 Exp#3 : Clap Detection");
UART_PrintLine(" 2 claps = LED ON, 3 claps = LED OFF");
UART_PrintLine("=========================================");
if (!PDM_Mic_Init() || !PDM_Mic_Start()) {
UART_PrintLine("ERROR: microphone bring-up failed.");
for (;;) { }
}
/* Discard the start-up idle tone, then clear filter state. */
for (uint32_t i = 0u; i < 32u; i++) {
(void)PDM_Mic_ReadBlock(raw_buf, FRAME_SAMPLES);
}
DcBlocker_Reset();
ClapDetect_Init();
for (;;) {
uint32_t rms;
clap_event_t evt;
if (!PDM_Mic_ReadBlock(raw_buf, FRAME_SAMPLES)) {
continue;
}
/* TODO 7 ------------------------------------------------
* Reuse Exp#2 exactly:
* a) run every sample through DcBlocker_Step() into
* dsp_buf
* b) rms = Rms_Block(dsp_buf, FRAME_SAMPLES)
* c) evt = ClapDetect_Update(rms)
* ------------------------------------------------------ */
/* TODO 8 ------------------------------------------------
* React to the event:
*
* CLAP_SINGLE : print a message only, change nothing.
* A single clap is almost always an
* accidental noise, so it must not
* control anything.
*
* CLAP_DOUBLE : LED_On(), print "LED ON"
* CLAP_TRIPLE : LED_Off(), print "LED OFF"
* CLAP_MANY : print a message only.
*
* CLAP_NONE : do nothing at all. This is the case
* on well over 99% of frames, so keep
* it cheap - no printing here.
* ------------------------------------------------------ */
/* Periodic calibration line. Watch these two numbers
* while tuning TODO 2 on your own board. */
frame_counter++;
if ((frame_counter % STATUS_INTERVAL) == 0u) {
UART_Print("rms = ");
UART_PrintInt((int32_t)rms);
UART_Print(" floor = ");
UART_PrintInt((int32_t)ClapDetect_NoiseFloor());
UART_Print(" threshold = ");
UART_PrintInt((int32_t)(ClapDetect_NoiseFloor() * TRIGGER_RATIO));
UART_PrintLine("");
}
}
}
Step 5: Calibrate, Then Test
Do not skip the calibration stage. Build, program, and first simply watch the status line for thirty seconds without making any noise.
- Confirm floor settles to a stable value, and that rms hovers near it.
- Clap once and check that CLAP_SINGLE is reported roughly 700 ms later — the pattern timer has to expire first.
- Clap twice, about a third of a second apart. The LED should switch on.
- Clap three times. The LED should switch off.
- Now carry the board to a noisier place and repeat without changing any constants. The adaptive floor should let it keep working.
Typical status output while calibrating:
rms = 104 floor = 98 threshold = 784
rms = 111 floor = 99 threshold = 792
rms = 96 floor = 99 threshold = 792
CLAP: 2 -> LED ON
rms = 130 floor = 101 threshold = 808
Observations to Record
| Measurement | Value |
|---|---|
| Settled noise floor, quiet room | |
| Resulting threshold | |
| Your TRIGGER_RATIO, and the C/F it came from | |
| Successful double-claps out of 10 attempts | |
| False detections in 60 s of silence | |
| Settled noise floor in a noisier location |
The last two rows together are your detector's quality, and they pull against each other. Lowering the threshold catches more real claps and also more false ones. There is no setting that wins both. Recognizing that trade-off — and choosing a point on it deliberately — is what separates an engineered detector from a guessed one.
Exercises
Exercise 1 — Remove the refractory period. Set REFRACTORY_FRAMES to 0 and clap once. Count the number of claps the detector reports. Relate the number you observe to the decaying tail in Figure Exp#3-1, then restore the constant.
Exercise 2 — Let the floor chase the clap. Move the noise-floor update to run on every frame, including refractory frames. Then clap repeatedly once per second for 10 seconds. Describe what happens to the detector's sensitivity over that period and explain why, referring to the threshold formula.
Exercise 3 — Symmetric tracking. Set FLOOR_RISE_SHIFT equal to FLOOR_FALL_SHIFT (try 5 for both, then 9 for both). For each setting, note how quickly the detector recovers after you play music near it, and whether it still detects claps during the music. Argue which asymmetry a real product should ship with.
Exercise 4 — Widen the pattern window. Set PATTERN_FRAMES to correspond to 2 seconds. The detector becomes more forgiving of slow clapping, but something noticeably worsens from the user's point of view. Identify it, and suggest how a real product might get both.
Exercise 5 — A different gesture (open-ended). Extend the state machine to distinguish between a slow double clap (roughly 600 ms apart) and a fast one (roughly 200 ms apart), and assign different actions to each. You will need to record the gap between claps rather than only counting them. Sketch your modified state diagram before writing code.
Checkpoint
- All 8 TODO items have been implemented, and the project builds with no warnings.
- The status line shows a stable noise floor in a quiet room.
- A single clap produces exactly one CLAP_SINGLE, not several.
- Two claps switch the LED on; three switch it off; both work at least 8 times out of 10.
- The detector still works after being moved to a room with a different background noise level, with no constants changed.
Experiment #4: Single-Tone Detection with the Goertzel Algorithm
Experiment #4 — Single-Tone Detection with the Goertzel Algorithm
Objective
C3 ended with a problem. Your clap detector fires on a slammed door, a dropped book, or a cough — because energy is the only thing it measures, and all of those are bursts of energy. To tell sounds apart you need to know not just how much energy arrived, but at which frequencies.
The obvious tool is the FFT. It is also the wrong tool here, and understanding why is the point of this experiment. The FFT computes every frequency bin. If you only care about one — "is there a 1 kHz tone present?" — you have paid for 128 answers to get 1.
The Goertzel algorithm computes a single DFT bin using a two-line recurrence, three state variables, and no buffer of twiddle factors. In this experiment, you will use it to detect a whistle or a tone from a phone app, and light the LED while that tone is present.
Step 1: Why Not Just Use an FFT?
Compare the two for a 256-point analysis on this board:
| 256-point FFT | Goertzel, 1 bin | |
|---|---|---|
| Multiplications | ~(N/2)·log2N = 1024 complex | N = 256 real |
| State/memory | N-sample buffer + twiddle table | 3 variables |
| Needs the whole block first? | Yes | No — processes each sample as it arrives |
| Bins produced | All 128 useful bins | Exactly the one you asked for |
| Library required | Yes (CMSIS-DSP or similar) | No — about 15 lines of C |
The break-even point is roughly log2N bins. Below that, run Goertzel once per frequency of interest; above it, use an FFT. For a DTMF decoder needing 8 tones out of 256 bins, Goertzel wins comfortably. For a spectrogram feeding a neural network, the FFT wins — which is exactly why the DEEPCRAFT™ preprocessing you will meet later in this course uses one.
Step 2: How Goertzel Works
The algorithm has two halves. During the block, a second-order recurrence runs once per sample:
At the end of the block, one closing calculation extracts the bin's power:
where s1 and s2 are simply the last two values of s. Figure Exp#4-1 draws the recurrence. Compare it with the DC blocker in Figure Exp#2-2 — same idea, but with two delays instead of one, which is what allows it to resonate rather than merely filter.
Notice what is missing from the diagram: there is no output during the block. Nothing is read from s until all N samples have been fed in. The algorithm accumulates evidence and only then provides an answer.
Step 3: Choosing the Bin
The coefficient depends on which DFT bin you want. Bin k corresponds to frequency:
and the spacing between adjacent bins — the resolution of this measurement — is:
Figure Exp#4-2 shows what the detector actually responds to. It is not a knife-edge at one frequency; it is a lobe about one bin wide, with weaker sidelobes on either side.
Step 4: The Goertzel Module
New file: Goertzel.h — given complete
#ifndef GOERTZEL_H
#define GOERTZEL_H
#include <stdint.h>
/* One detector instance. The struct exists so that you can
* create SEVERAL detectors at different frequencies without
* duplicating any code - see Exercise 4. */
typedef struct goertzel_t {
int32_t coeff_q14; /* 2*cos(2*pi*k/N), Q14 fixed point */
uint32_t n; /* block length in samples */
int32_t s1; /* s[n-1] */
int32_t s2; /* s[n-2] */
uint32_t fed; /* samples fed so far this block */
} goertzel_t;
void Goertzel_Init(goertzel_t *g, int32_t coeff_q14, uint32_t n);
void Goertzel_Reset(goertzel_t *g);
void Goertzel_Step(goertzel_t *g, int32_t x);
/* Close out the block: returns the bin magnitude and clears
* the state ready for the next block. */
uint32_t Goertzel_Magnitude(goertzel_t *g);
#endif /* GOERTZEL_H */
New file: Goertzel.c — skeleton
#include "Goertzel.h"
#include "Audio_DSP.h" /* for Isqrt_u32() from Exp#2 */
/* ============================================================
* TODO 1 : work out the bin and the coefficient.
*
* Target frequency : 1000 Hz
* Sample rate : 16000 Hz
* Block length N : 256
*
* (a) k = round( f_target * N / Fs )
* Compute it. With these numbers k comes out as a whole
* number exactly, which is a convenient accident - note
* in your report what k would be for 1100 Hz instead.
*
* (b) Delta_f = Fs / N. Write it down; it is the resolution
* of everything you measure in this experiment.
*
* (c) coeff = 2 * cos( 2*pi*k / N )
* Evaluate this on a calculator, then convert to Q14
* fixed point by multiplying by 16384 and rounding:
*
* COEFF_Q14 = round( coeff * 16384 )
*
* Sanity check before you type it in: coeff must lie
* between -2 and +2, so COEFF_Q14 must lie between
* -32768 and +32768. A value outside that range means
* you used degrees instead of radians.
* ---------------------------------------------------------- */
#define GOERTZEL_N (256u)
#define GOERTZEL_COEFF_Q14 ( /* TODO 1c */ )
void Goertzel_Init(goertzel_t *g, int32_t coeff_q14, uint32_t n)
{
g->coeff_q14 = coeff_q14;
g->n = n;
Goertzel_Reset(g);
}
void Goertzel_Reset(goertzel_t *g)
{
g->s1 = 0;
g->s2 = 0;
g->fed = 0u;
}
/* ============================================================
* TODO 2 : one step of the recurrence.
*
* s0 = x + coeff * s1 - s2
* s2 = s1
* s1 = s0
*
* In Q14 fixed point the multiply becomes:
*
* (coeff_q14 * s1) >> 14
*
* ARITHMETIC WARNING - this is the Exp#2 lesson again, in a
* sharper form. When a tone at the target frequency is
* present, s1 GROWS throughout the block, reaching roughly
* (amplitude x N / 2). With N = 256 and a loud tone that is
* several million. Multiplying several million by a
* coefficient of up to 32768 overflows a 32-bit product long
* before the shift can bring it back down.
*
* Cast to int64_t BEFORE multiplying, shift, then narrow back
* to int32_t. Work out the worst-case magnitudes yourself and
* put them in your report - do not just copy the cast.
*
* Remember to increment g->fed.
* ---------------------------------------------------------- */
void Goertzel_Step(goertzel_t *g, int32_t x)
{
/* TODO 2 */
}
/* ============================================================
* Close out the block and return a magnitude.
* ========================================================== */
uint32_t Goertzel_Magnitude(goertzel_t *g)
{
int64_t power;
uint64_t p;
uint32_t mag;
/* TODO 3 --------------------------------------------------
* Compute the bin power:
*
* |X|^2 = s1^2 + s2^2 - coeff * s1 * s2
*
* Build it in stages so no intermediate overflows. A safe
* order is:
*
* prod = (coeff_q14 * s1) >> 14 // 64-bit
* power = s1*s1 + s2*s2 - prod*s2 // 64-bit
*
* Every one of those terms needs int64_t. s1 alone can be
* a few million, so s1*s1 is of the order of 10^13.
*
* Power is a squared quantity and cannot truly be negative,
* but rounding in the fixed-point arithmetic can push it
* slightly below zero. Clamp it at 0.
* -------------------------------------------------------- */
/* TODO 4 --------------------------------------------------
* Turn power into a magnitude of convenient size.
*
* a) shift p right by 16 so it fits in 32 bits
* b) mag = Isqrt_u32( (uint32_t)p ) - reuse Exp#2's helper
*
* With N = 256 this scaling makes the reported magnitude
* come out at roughly HALF the amplitude of a pure tone at
* the target frequency. That is a useful sanity check:
* play a loud 1 kHz tone and the magnitude should read a
* few thousand, not a few million.
* -------------------------------------------------------- */
Goertzel_Reset(g);
return mag;
}
Pseudocode for TODO 2 and TODO 3:
PROCEDURE Goertzel_Step(g, x)
// 64-bit product, then bring back to 32 bits
s0 <- x + SHIFT_RIGHT( WIDEN64(g.coeff_q14) * WIDEN64(g.s1), 14 )
- g.s2
g.s2 <- g.s1 // order matters, exactly as in Exp#2
g.s1 <- s0
g.fed <- g.fed + 1
END PROCEDURE
FUNCTION Goertzel_Magnitude(g) RETURNS unsigned
prod <- SHIFT_RIGHT( WIDEN64(g.coeff_q14) * WIDEN64(g.s1), 14 )
power <- WIDEN64(g.s1) * g.s1
+ WIDEN64(g.s2) * g.s2
- prod * g.s2
IF power < 0 THEN power <- 0 END IF
p <- SHIFT_RIGHT(power, 16)
mag <- Isqrt_u32( NARROW32(p) )
Goertzel_Reset(g)
RETURN mag
END FUNCTION
Step 5: Deciding That a Tone Is Present
A large magnitude is not by itself proof of a tone. Very loud broadband noise puts energy into every bin, including ours. The reliable question is not "is this bin large?" but "is this bin large compared with the total energy?"
You already have the total energy: the block RMS from Exp#2. For a pure tone at the target frequency, the bin magnitude is a substantial fraction of the RMS. For broadband noise of the same RMS, the energy is spread across all 128 bins, and any single bin holds far less.
The second condition is the same idea as MIN_RMS_ABSOLUTE in Exp#3: in a silent room, the ratio test alone becomes meaninglessly sensitive.
Figure Exp#4-3 shows the whole per-block flow.
Step 6: main.c
File: main.c
#include "cybsp.h"
#include "cyhal.h"
#include "Board_Config.h"
#include "UART_Console.h"
#include "GPIO_Control.h"
#include "PDM_Mic.h"
#include "Audio_DSP.h"
#include "Goertzel.h"
/* 256 samples at 16 kHz = 16 ms per block.
* MUST match the N used to compute the coefficient. */
#define BLOCK_SAMPLES (256u)
/* ============================================================
* TODO 5 : detection constants.
*
* (a) TONE_DOMINANCE_PCT
* The bin magnitude must exceed this percentage of the
* block RMS. Start at 40 and tune it on your board.
* Too low and loud speech triggers it; too high and a
* soft whistle is missed.
*
* Test in integers without division:
*
* (mag * 100) > (rms * TONE_DOMINANCE_PCT)
*
* (b) TONE_MIN_MAG
* Absolute floor. Use roughly 3 x your Exp#2 quiet-room
* RMS, exactly as you did for MIN_RMS_ABSOLUTE in Exp#3.
*
* (c) TONE_CONFIRM_BLOCKS
* How many consecutive blocks must agree. Each block is
* 16 ms, so 4 blocks means the tone must persist about
* 64 ms before the LED responds. Start at 4.
* ---------------------------------------------------------- */
#define TONE_DOMINANCE_PCT ( /* TODO 5a */ )
#define TONE_MIN_MAG ( /* TODO 5b */ )
#define TONE_CONFIRM_BLOCKS ( /* TODO 5c */ )
#define STATUS_INTERVAL (30u) /* about twice a second */
static int16_t raw_buf[BLOCK_SAMPLES];
static int32_t dsp_buf[BLOCK_SAMPLES];
static goertzel_t tone;
int main(void)
{
uint32_t streak = 0u;
uint32_t block_count = 0u;
uint32_t led_on = 0u;
if (CY_RSLT_SUCCESS != cybsp_init()) {
CY_ASSERT(0);
}
__enable_irq();
UART_Console_Init();
GPIO_Control_Init();
UART_PrintLine("");
UART_PrintLine("=========================================");
UART_PrintLine(" Lab 105 Exp#4 : Goertzel Tone Detection");
UART_PrintLine(" Target : 1000 Hz (whistle or tone app)");
UART_PrintLine("=========================================");
if (!PDM_Mic_Init() || !PDM_Mic_Start()) {
UART_PrintLine("ERROR: microphone bring-up failed.");
for (;;) { }
}
for (uint32_t i = 0u; i < 16u; i++) {
(void)PDM_Mic_ReadBlock(raw_buf, BLOCK_SAMPLES);
}
DcBlocker_Reset();
Goertzel_Init(&tone, GOERTZEL_COEFF_Q14, BLOCK_SAMPLES);
for (;;) {
uint32_t mag;
uint32_t rms;
uint32_t hit;
if (!PDM_Mic_ReadBlock(raw_buf, BLOCK_SAMPLES)) {
continue;
}
/* TODO 6 ------------------------------------------------
* For every sample in the block:
* a) y = DcBlocker_Step(raw_buf[i])
* b) store y into dsp_buf[i] (needed for the RMS)
* c) Goertzel_Step(&tone, y)
*
* One pass, three jobs. Do NOT loop three times.
* ------------------------------------------------------ */
/* TODO 7 ------------------------------------------------
* a) mag = Goertzel_Magnitude(&tone)
* Call this exactly ONCE per block - it resets the
* detector's state as a side effect.
*
* b) rms = Rms_Block(dsp_buf, BLOCK_SAMPLES)
*
* c) hit = 1 if BOTH conditions from TODO 5 hold,
* otherwise 0.
*
* d) if hit, increment streak; otherwise, reset it to 0.
* ------------------------------------------------------ */
/* TODO 8 ------------------------------------------------
* Drive the LED from the streak, and print ONLY on a
* change of state - never on every block.
*
* streak >= TONE_CONFIRM_BLOCKS and LED currently off
* -> LED_On(), print "TONE 1000 Hz : ON"
*
* streak == 0 and LED currently on
* -> LED_Off(), print "TONE 1000 Hz : off"
*
* Use the led_on variable to remember the current state
* so that the message appears once per transition.
* ------------------------------------------------------ */
/* Calibration line. Watch mag against rms while you
* tune TODO 5. */
block_count++;
if ((block_count % STATUS_INTERVAL) == 0u) {
UART_Print("mag = ");
UART_PrintInt((int32_t)mag);
UART_Print(" rms = ");
UART_PrintInt((int32_t)rms);
UART_Print(" streak = ");
UART_PrintInt((int32_t)streak);
UART_PrintLine("");
}
}
}
Step 7: Test
- Build, program, and open the terminal at 115200-8-N-1.
- Use a tone-generator app on a phone. Play 1000 Hz near the board. The LED should light and stay lit.
- Now sweep the app slowly from 800 Hz to 1200 Hz and watch mag in the status line. You are measuring Figure Exp#4-2 on real hardware.
- Play 500 Hz at the same volume. The magnitude should collapse even though the RMS is similar — this is the whole point of the experiment.
- Now clap, and speak loudly. Both produce a large RMS. Does the tone detector stay quiet?
Observations to Record
Table 1 — frequency response. Play each tone at a constant volume and record the magnitude. This is Figure Exp#4-2, measured.
| Tone (Hz) | 500 | 875 | 970 | 1000 | 1030 | 1125 | 2000 |
|---|---|---|---|---|---|---|---|
| mag | |||||||
| rms |
Table 2 — selectivity against other sounds.
| Sound | mag | rms | Detected? |
|---|---|---|---|
| 1000 Hz tone | |||
| Whistle | |||
| Hand clap | |||
| Loud speech |
Exercises
Exercise 1 — Retarget the detector. Change the target to 2000 Hz. Compute the new k and the new coefficient, and verify k is again a whole number. Confirm the detector now ignores 1000 Hz and responds to 2000 Hz. Then try 1100 Hz, where k is not a whole number — what did you have to round to, and what frequency is your detector actually watching?
Exercise 2 — Resolution versus latency. Change N to 512 (and recompute the coefficient). Calculate the new Δf and the new block duration. Repeat the frequency sweep from Table 1 and describe how the response curve changed shape. State the trade-off you have found in one sentence.
Exercise 3 — Measure the leakage. Using your Table 1 results, express the 970 Hz and 1030 Hz magnitudes as a percentage of the 1000 Hz magnitude. Those tones sit roughly half a bin away. Compare your measurement with the figure quoted in the note under Figure Exp#4-2, and account for any difference.
Exercise 4 — Two tones at once. The goertzel_t struct exists precisely so that this is easy. Create a second detector at a different frequency and feed both from the same sample loop. Light the LED only when both tones are present simultaneously. This is, in miniature, exactly how a DTMF telephone decoder works.
Exercise 5 — Back to Exp#3 (open-ended). Combine Exp#3 and Exp#4: use the clap detector to find the moment something loud happened, then use one or more Goertzel detectors to classify what it was. What would you need to measure to reliably separate a clap from a door slam? Sketch a design; you are not required to implement it.
Checkpoint
- All 8 TODO items have been implemented, and the project builds with no warnings.
- Your computed COEFF_Q14 lies between −32768 and +32768.
- A 1000 Hz tone lights the LED; a 500 Hz tone at a similar volume does not.
- A hand clap produces a large RMS but does not trigger the tone detector.
- Table 1 is complete and shows a clear peak at 1000 Hz.
- Your report contains the worst-case magnitude analysis requested in TODO 2 — not just the 64-bit cast.
Experiment #5: Stereo Capture: Separating U4 and U5
C5 — Stereo Capture: Separating U4 and U5
Objective
This is the turning point of the lab. Everything up to now — capture, DC removal, loudness, transient detection, tone detection — could have been done on any board with a single microphone. From here on, we use both, and the experiments become beyond the capabilities of a single-microphone board.
C5 has three goals, and the middle one is the one that cannot be skipped:
- Switch the PDM/PCM block to stereo mode and understand how two microphones share one wire.
- Determine experimentally which physical microphone — U4 or U5 — appears on which software channel. This is not something to look up; it is something to measure.
- Display both channels as side-by-side meters with a simple left/right balance indicator.
The balance indicator will work impressively well — and then fail in a very specific and instructive way. Understanding why it fails is what makes Exp#6 necessary.
Step 1: Two Microphones, One Wire
Recall from the theory section that U4 and U5 share both PDM_CLK (P10_4) and PDM_DATA (P10_5). They avoid colliding by using opposite clock edges, selected by each device's hardwired SELECT pin.
Those samples arrive interleaved — one channel, then the other, alternating. A buffer of 2N samples, therefore, contains N samples of each channel, as shown in Figure Exp#5-2.
Step 2: Switching to Stereo
Start from your Exp#2 project (you need the DSP module, not the clap detector). The change to PDM_Mic.c is a single line.
static const cyhal_pdm_pcm_cfg_t pdm_pcm_cfg = {
.sample_rate = PDM_SAMPLE_RATE_HZ,
.decimation_rate = PDM_DECIMATION_RATE,
/* TODO 1 ------------------------------------------------
* In Exp#1 this was CYHAL_PDM_PCM_MODE_LEFT, which captured
* one microphone only. Change it so that BOTH edges are
* sampled and both microphones are delivered.
*
* Then answer this in your report before building:
* if you keep asking PDM_Mic_ReadBlock() for 1024 samples,
* how many MILLISECONDS of audio do you now receive?
* ------------------------------------------------------ */
.mode = /* TODO 1 */,
.word_length = PDM_WORD_LENGTH,
.left_gain = PDM_LEFT_GAIN,
.right_gain = PDM_RIGHT_GAIN,
};
Step 3: One Filter Per Channel
Your DC blocker from Exp#2 keeps its history in two file-scope static variables. That was fine for one channel. With two channels, it is now a bug: feeding alternating samples from two different microphones into one filter's memory produces nonsense from both.
The fix is the pattern you already met in Exp#4, where goertzel_t let you create as many detectors as you liked. Do the same to the DC blocker: move its state into a struct so that each channel owns an independent instance.
Modify Audio_DSP.h — given complete
/* ---- DC blocker, instance based ---------------------------
* One of these per audio channel. Two channels sharing a
* single filter state is the classic stereo DSP bug. */
typedef struct dcblocker_t {
int32_t prev_x;
int32_t prev_y;
} dcblocker_t;
void DcBlocker_Init(dcblocker_t *d);
int32_t DcBlocker_Step(dcblocker_t *d, int16_t x);
/* ---- Stereo helper ----------------------------------------
* Split an interleaved buffer of (frames * 2) samples into two
* separate arrays of 'frames' samples each. */
void Stereo_Split(const int16_t *interleaved, uint32_t frames,
int16_t *ch0, int16_t *ch1);
Modify Audio_DSP.c — skeleton
/* ============================================================
* TODO 2 : convert the DC blocker to an instance.
*
* Delete the two file-scope statics from Exp#2 and rewrite both
* functions to work through the pointer instead. The maths is
* identical - only where the state LIVES has changed.
*
* y = x - d->prev_x + d->prev_y - (d->prev_y >> DC_SHIFT)
*
* The update-ordering trap from Exp#2 is still here. Compute y
* from the old state before overwriting anything.
* ---------------------------------------------------------- */
void DcBlocker_Init(dcblocker_t *d)
{
/* TODO 2a */
}
int32_t DcBlocker_Step(dcblocker_t *d, int16_t x)
{
/* TODO 2b */
return 0; /* replace this */
}
/* ============================================================
* TODO 3 : de-interleave.
*
* For i from 0 to frames-1:
* ch0[i] = interleaved[2*i]
* ch1[i] = interleaved[2*i + 1]
*
* Simple, but get the indexing right - an off-by-one here
* swaps the two microphones and will quietly invert every
* direction result in Exp#6.
* ---------------------------------------------------------- */
void Stereo_Split(const int16_t *interleaved, uint32_t frames,
int16_t *ch0, int16_t *ch1)
{
/* TODO 3 */
}
'Step 4: main.c
File: main.c
#include "cybsp.h"
#include "cyhal.h"
#include "Board_Config.h"
#include "UART_Console.h"
#include "PDM_Mic.h"
#include "Audio_DSP.h"
/* 1024 frames at 16 kHz = 64 ms, as in Exp#2.
* Because we are now in stereo, that is 2048 SAMPLES. */
#define FRAMES (1024u)
#define TOTAL_SAMPLES (FRAMES * 2u)
#define BAR_WIDTH (14u)
/* Balance is called only when one side leads the other by
* more than this percentage. 25 means "at least 25% louder". */
#define BALANCE_MARGIN_PCT (25u)
static int16_t raw_buf[TOTAL_SAMPLES]; /* interleaved */
static int16_t ch0_raw[FRAMES];
static int16_t ch1_raw[FRAMES];
static int32_t ch0_dsp[FRAMES];
static int32_t ch1_dsp[FRAMES];
static dcblocker_t dc_ch0;
static dcblocker_t dc_ch1;
/* Reuse your Exp#2 bar renderer, shortened to BAR_WIDTH cells so
* that two of them fit on one terminal line. */
static void PrintBar(int32_t db_x10)
{
char bar[BAR_WIDTH + 3u];
int32_t filled = (db_x10 + 600) * (int32_t)BAR_WIDTH / 600;
uint32_t i;
if (filled < 0) { filled = 0; }
if (filled > (int32_t)BAR_WIDTH) { filled = (int32_t)BAR_WIDTH; }
bar[0] = '[';
for (i = 0u; i < BAR_WIDTH; i++) {
bar[i + 1u] = (i < (uint32_t)filled) ? '#' : '-';
}
bar[BAR_WIDTH + 1u] = ']';
bar[BAR_WIDTH + 2u] = '\0';
UART_Print(bar);
}
int main(void)
{
if (CY_RSLT_SUCCESS != cybsp_init()) {
CY_ASSERT(0);
}
__enable_irq();
UART_Console_Init();
UART_PrintLine("");
UART_PrintLine("=========================================");
UART_PrintLine(" Lab 105 Exp#5 : Stereo Capture");
UART_PrintLine(" ch0 = ? ch1 = ? (identify in Step 5)");
UART_PrintLine("=========================================");
if (!PDM_Mic_Init() || !PDM_Mic_Start()) {
UART_PrintLine("ERROR: microphone bring-up failed.");
for (;;) { }
}
/* Discard the start-up idle tone. Note the sample count:
* it is TOTAL_SAMPLES, not FRAMES. */
for (uint32_t i = 0u; i < 4u; i++) {
(void)PDM_Mic_ReadBlock(raw_buf, TOTAL_SAMPLES);
}
/* Two channels, two independent filters. */
DcBlocker_Init(&dc_ch0);
DcBlocker_Init(&dc_ch1);
for (;;) {
uint32_t rms0;
uint32_t rms1;
int32_t db0;
int32_t db1;
if (!PDM_Mic_ReadBlock(raw_buf, TOTAL_SAMPLES)) {
continue;
}
/* TODO 4 ------------------------------------------------
* a) Stereo_Split() the interleaved buffer into
* ch0_raw and ch1_raw.
*
* b) Run each channel through ITS OWN DC blocker into
* ch0_dsp and ch1_dsp. Two loops, or one loop doing
* both - either is fine, but dc_ch0 must only ever
* see ch0 samples.
*
* c) rms0 = Rms_Block(ch0_dsp, FRAMES)
* rms1 = Rms_Block(ch1_dsp, FRAMES)
*
* d) db0 / db1 via Rms_To_dBFS_x10()
* ------------------------------------------------------ */
/* TODO 5 ------------------------------------------------
* Print one line containing, in order:
*
* "ch0 " , the bar for db0 , two spaces ,
* "ch1 " , the bar for db1 , two spaces ,
* the balance verdict , then a CARRIAGE RETURN.
*
* The balanced verdict compares the two RMS values with
* a margin, so that near-equal levels report "center"
* rather than flickering:
*
* rms0 * 100 > rms1 * (100 + BALANCE_MARGIN_PCT)
* -> "<<< ch0 "
* rms1 * 100 > rms0 * (100 + BALANCE_MARGIN_PCT)
* -> " ch1 >>>"
* otherwise
* -> "center"
*
* Keep every verdict string the SAME LENGTH. With a
* carriage-return display, a shorter string leaves
* characters from the previous line on screen.
*
* Overflow check before you write it: rms is at most
* 32768 and we multiply by 125, giving about 4.1
* million. Comfortable in 32 bits.
* ------------------------------------------------------ */
}
}
Step 5: Identify the Channels — the Core Experiment
Do not skip this and do not guess. Every result in Exp#6 depends on knowing which array holds which microphone.
First, locate the microphones physically. Find U4 and U5 on the board using the silkscreen or the assembly drawing in the kit's hardware design files. Note which one is nearer to which edge of the board, and pick a consistent way to describe their positions — for example, "the one nearer the USB connector".
Then run the test. With the program running and the terminal showing both bars:
- Press a fingertip firmly over U4 only, leaving U5 clear. Note which bar drops.
- Release, wait for both bars to recover, then cover U5 only. Note which bar drops.
- Repeat each test three times to be sure the result is consistent and not a coincidence of background noise.
- As a cross-check, speak quietly from a position clearly to one side of the board and confirm the expected bar is the taller one.
| Test | ch0 bar | ch1 bar | Conclusion |
|---|---|---|---|
| Cover U4 | |||
| Cover U5 | |||
| Neither covered, quiet |
Record your findings permanently. Once you are confident, write it into Board_Config.h so that the rest of the lab reads from one authoritative place:
/* TODO 6 --------------------------------------------------
* Determined by measurement in Exp#5 Step 5, on this board.
* Fill in the two comments with what YOU observed, and set
* the index is defined to match.
*
* Observation: covering U4 made channel ____ drop.
* Observation: covering U5 made channel ____ drop.
* -------------------------------------------------------- */
#define MIC_U4_CHANNEL ( /* TODO 6a : 0 or 1 */ )
#define MIC_U5_CHANNEL ( /* TODO 6b : 0 or 1 */ )
Step 6: Where Amplitude Stops Working
Your balance indicator should feel almost magical when you speak from a few centimeters to one side. Now move away and try again from across the room. It stops working — and the reason is geometry, not a bug in your code.
Close to the board, the two path lengths differ by a large fraction, so the inverse-square law makes the near microphone measurably louder. From across the room, both paths are almost the same length — a few millimeters of difference on top of several meters — so both microphones hear essentially the same amplitude.
Observations to Record
Speak at a steady volume from directly to one side of the board, at each distance. Record both RMS values and whether the balance indicator called it correctly.
| Distance | rms0 | rms1 | ratio | verdict correct? |
|---|---|---|---|---|
| 5 cm | ||||
| 20 cm | ||||
| 1 m | ||||
| 3 m |
Identify the distance at which the indicator becomes unreliable on your board, and keep that number. It is the practical boundary between "near field" and "far field" for this microphone spacing, and it is the reason Exp#6 exists.
Exercises
Exercise 1 — Share one filter deliberately. Temporarily feed both channels through dc_ch0. Describe what happens to both readings and explain it in terms of the filter's stored history. Then restore the correct code. This is the bug the struct refactor exists to prevent.
Exercise 2 — Break the de-interleaving. Change Stereo_Split() so that both outputs read from even indices. Do the meters still look plausible? This is a useful lesson in how a wrong stereo program can look entirely healthy — and why Step 5's covering test, which produces an unambiguous asymmetric result, is the only trustworthy check.
Exercise 3 — Gain mismatch. Set PDM_RIGHT_GAIN to 26 while leaving the left at 20 — a 3 dB imbalance. Re-run the 5 cm test from both sides. How large a real positional difference does this artificial imbalance imitate? Restore the gains afterward.
Exercise 4 — Remove the redundant buffers. The current code keeps the interleaved buffer and two split copies. Rewrite the processing so that the split and DC filtering occur in a single pass, writing directly to ch0_dsp and ch1_dsp. Calculate the SRAM saved. Then argue whether the clarity you gave up was worth it — there is no single right answer, but you must take a position.
Exercise 5 — Predict Exp#6. Look up the speed of sound and estimate the spacing between U4 and U5 on your board (measure it with a ruler if you can). Calculate the maximum time difference a sound could have between the two microphones, and convert it into a number of samples at 16 kHz. Write down your answer before starting Exp#6 — the result will probably surprise you, and it defines what Exp#6 can and cannot achieve.
Checkpoint
- All six TODO items are implemented, and the project builds with no warnings.
- Both meters respond, and covering one microphone drops one bar, not both.
- The Step 5 table is complete and consistent across three repetitions.
- MIC_U4_CHANNEL and MIC_U5_CHANNEL are recorded in Board_Config.h with your observations in the comments.
- You have measured the distance at which the balance indicator stops working.
- Exercise 5 is answered, in samples, before you begin Exp#6.
Experiment #6: Sound Direction Sensing via Cross-Correlation
Experiment #6 — Sound Direction Sensing via Cross-Correlation
Objective
Clap to one side of the board, and the terminal tells you which side. That is the whole demonstration, and it is the only experiment in this lab that a single-microphone board fundamentally cannot perform.
C5 showed that loudness ceases to carry direction information once the source is more than a short distance away. What survives is timing: the sound reaches the nearer microphone slightly before the farther one, and that difference depends only on the angle and the microphone spacing, not on the source's distance.
The difficulty is the word "slightly". In Exp#5, Exercise 5, you calculated the largest possible arrival delay for your board. If you did it correctly, you found it was barely more than one sample period at 16 kHz — which means a whole-sample measurement can distinguish only three states. This experiment is largely about confronting that number and taking action.
Step 1: The Geometry
For a source far enough away that the arriving wavefronts are effectively flat — the far-field case from Figure Exp#5-3 — the extra distance the sound must travel to reach the second microphone is a simple piece of trigonometry.
Two consequences are worth stating plainly:
- A source directly in front (θ = 0) gives τ = 0. Both microphones hear it simultaneously.
- The delay is largest when the source lies along the microphone axis (θ = 90°), giving τmax = d / c.
Step 2: The Resolution Problem, and How to Fix It
We do not measure τ in seconds. We measure it as the number of samples of delay between the two channels. So the question that decides whether this experiment is possible at all is:
Fill this table in using your own measured spacing from Exp#5. Do it before writing any code.
| Sample rate Fs | Sample period | nmax for your d | Usable direction bins |
|---|---|---|---|
| 16 kHz | 62.5 µs | ||
| 32 kHz | 31.25 µs | ||
| 48 kHz | 20.83 µs |
So we change the sample rate. Look back at Exp#1: the PDM clock is set by Fs × the decimation factor. If we raise Fs and lower the decimation rate by the same factor, the PDM clock remains unchanged, the microphone stays in exactly the same power mode, and nothing about the hardware changes — but we get three times as many samples per second to measure delay with.
/* ---- Board_Config.h : Exp#6 audio format --------------------
*
* TODO 1 ---------------------------------------------------
* We want Fs = 48000 Hz for finer timing resolution, while
* keeping the PDM clock at 1.536 MHz so that the microphone
* remains in its Normal power mode (1.2 - 1.65 MHz).
*
* From Exp#1: f_PDM_CLK = Fs x decimation rate
*
* Work out the decimation rate that satisfies both.
* Confirm your answer by multiplying it back out.
*
* If your HAL rejects that decimation value, fall back to
* Fs = 32000 Hz and recompute - the experiment still works,
* just with coarser resolution. Record which you used.
* --------------------------------------------------------- */
#define PDM_SAMPLE_RATE_HZ (48000u)
#define PDM_DECIMATION_RATE ( /* TODO 1 */ )
Even after this, we still need sub-sample precision to get more than a crude three-way answer. That comes in Step 4.
Step 3: Cross-Correlation
To find how far one channel is delayed relative to the other, we slide one against the other and look for the alignment that makes them match best:
When the two signals line up, positive samples multiply positive and negative samples multiply negative, so the sum is large. When they are misaligned, the products have opposite signs and largely cancel each other out. The lag k that maximizes R[k] is our estimate of the delay.
Figure Exp#6-2 shows both the correlation and the subsequent refinement.
New file: Xcorr.h — given complete
#ifndef XCORR_H
#define XCORR_H
#include <stdint.h>
#define XCORR_MAX_LAG_LIMIT (8) /* array bound */
typedef struct xcorr_result_t {
int32_t peak_k; /* integer lag of the maximum */
int32_t lag_q8; /* refined lag, in samples x 256 */
int64_t peak; /* correlation value at the peak */
int64_t mean; /* mean correlation across all lags */
uint32_t valid; /* 1 if the estimate is trustworthy */
} xcorr_result_t;
/* Estimate the delay of b relative to a over n samples,
* searching lags from -max_lag to +max_lag. */
void Xcorr_Estimate(const int32_t *a, const int32_t *b,
uint32_t n, int32_t max_lag,
xcorr_result_t *out);
#endif /* XCORR_H */
New file: Xcorr.c — skeleton
#include "Xcorr.h"
/* One entry per searched lag, from -max_lag to +max_lag. */
static int64_t r[(2 * XCORR_MAX_LAG_LIMIT) + 1];
void Xcorr_Estimate(const int32_t *a, const int32_t *b,
uint32_t n, int32_t max_lag,
xcorr_result_t *out)
{
int32_t k;
int32_t best_i = 0;
int64_t sum = 0;
if (max_lag > XCORR_MAX_LAG_LIMIT)
{
max_lag = XCORR_MAX_LAG_LIMIT;
}
/* TODO 2 ------------------------------------------------
* Fill r[] with the correlation at each lag.
*
* For each k from -max_lag to +max_lag, store the result
* at index (k + max_lag) so that negative lags land at the
* start of the array:
*
* r[k + max_lag] = SUM over n of a[i] * b[i + k]
*
* TWO THINGS TO GET RIGHT:
*
* 1. RANGE. b[i+k] must stay inside the buffer. Run i
* from max_lag to (n - max_lag - 1), NOT from 0 to
* n-1. Reading past the end of an array here will not
* crash - it will quietly read whatever is next in
* memory and give you a plausible wrong answer.
*
* 2. WIDTH. a and b hold DC-blocked samples up to about
* 32768. Each product reaches 1.07e9, and we add
* hundreds of them. The accumulator must be int64_t,
* and you must widen BEFORE multiplying - the same
* trap as Exp#2 and Exp#4.
* ------------------------------------------------------ */
/* TODO 3 ------------------------------------------------
* Find the index of the largest entry in r[], and also
* accumulate 'sum' so that a mean can be computed.
*
* Store into out->peak_k the LAG (not the array index),
* i.e. best_i - max_lag.
*
* Store out->peak and out->mean.
* ------------------------------------------------------ */
/* TODO 4 ------------------------------------------------
* Sub-sample refinement by parabolic interpolation.
*
* Given the peak and its two neighbours, the vertex of the
* parabola through them sits at a fractional offset:
*
* R[-1] - R[+1]
* delta = -------------------------- x 0.5
* R[-1] - 2*R[0] + R[+1]
*
* where R[0] is the peak and R[-1], R[+1] its neighbours.
*
* Compute delta in Q8 (i.e. multiplied by 256):
*
* num = R[-1] - R[+1]
* den = R[-1] - 2*R[0] + R[+1]
* delta_q8 = (num * 128) / den
*
* (128 rather than 256 because of the factor of one half.)
*
* GUARDS - all three matter:
* - if the peak is at either END of the searched range,
* it has no neighbour on one side. Skip refinement
* and set valid = 0.
* - if den == 0, skip refinement (division by zero).
* - clamp delta_q8 to +/-128, i.e. half a sample. A
* larger value means the parabola did not fit and the
* result is not trustworthy.
*
* Finally: out->lag_q8 = (out->peak_k * 256) + delta_q8
* ------------------------------------------------------ */
/* TODO 5 ------------------------------------------------
* Decide whether this estimate is worth believing.
*
* A correlation of pure background noise still produces a
* peak somewhere - it just is not meaningful. A real
* arrival makes the peak stand out clearly above the
* average across all lags.
*
* Set out->valid = 1 only if the peak is positive AND
* exceeds the mean by a clear margin, for example:
*
* peak > mean * 2 (with mean > 0)
*
* Otherwise set out->valid = 0 and leave lag_q8 at 0.
* ------------------------------------------------------ */
}
Pseudocode for TODO 2 and TODO 4:
// ---- TODO 2 : correlation ----
FOR k FROM -max_lag TO +max_lag DO
acc <- 0 // 64-bit
FOR i FROM max_lag TO n - max_lag - 1 DO
acc <- acc + WIDEN64(a[i]) * WIDEN64(b[i + k])
END FOR
r[k + max_lag] <- acc
END FOR
// ---- TODO 4 : parabolic vertex ----
IF best_i = 0 OR best_i = 2*max_lag THEN
valid <- 0 // no neighbour
RETURN
END IF
Rm <- r[best_i - 1]
R0 <- r[best_i]
Rp <- r[best_i + 1]
num <- Rm - Rp
den <- Rm - 2*R0 + Rp
IF den = 0 THEN
delta_q8 <- 0
ELSE
delta_q8 <- (num * 128) / den
END IF
IF delta_q8 > 128 THEN delta_q8 <- 128 END IF
IF delta_q8 < -128 THEN delta_q8 <- -128 END IF
lag_q8 <- peak_k * 256 + delta_q8
Step 4: Only Measure When There Is Something to Measure
Correlating background noise yields a meaningless lag estimate. The estimate is only worth computing when a real sound has arrived — ideally, a sharp, broadband one like a clap, which correlates crisply.
This is the Exp#3 idea again, in its simplest form: gate the measurement on energy.
Step 5: main.c
File: main.c
#include "cybsp.h"
#include "cyhal.h"
#include "Board_Config.h"
#include "UART_Console.h"
#include "GPIO_Control.h"
#include "PDM_Mic.h"
#include "Audio_DSP.h"
#include "Xcorr.h"
/* 512 frames at 48 kHz = 10.7 ms - long enough to contain a
* clap's onset, short enough to respond quickly. */
#define FRAMES (512u)
#define TOTAL_SAMPLES (FRAMES * 2u)
/* ============================================================
* TODO 6 : search range and decision thresholds.
*
* (a) SEARCH_LAG
* Two competing pressures decide this one.
*
* Searching too WIDE is harmful: lags beyond n_max are
* physically impossible for the direct sound, so any peak
* out there belongs to a reflection - and a strong
* reflection can outweigh the direct arrival.
*
* Searching too NARROW is also harmful, for a subtler
* reason. Look back at TODO 4: parabolic refinement
* needs a neighbour on BOTH sides of the peak, and gives
* up otherwise. So the search must extend at least one
* lag beyond the largest peak that can physically occur.
*
* Rule: round n_max UP to the next whole number - that is
* the largest integer peak possible - then add 1 so that
* peak still has neighbours.
*
* Must not exceed XCORR_MAX_LAG_LIMIT.
*
* (b) ONSET_MIN_RMS
* Only run the correlator when the block is clearly
* louder than background. Use roughly 5 x your Exp#2
* quiet-room RMS as a starting point, then tune.
*
* (c) CENTRE_BAND_Q8
* Lags smaller than this in magnitude are reported as
* "centre". Start at 128, which is half a sample - on
* this board that corresponds to about +/- 8 degrees.
*
* Try 64 as well and see what happens: a quarter-sample
* band is narrower than the real clap-to-clap scatter, so
* "CENTRE" almost never appears and the verdict flickers
* between LEFT and RIGHT for a source straight ahead.
* ---------------------------------------------------------- */
#define SEARCH_LAG ( /* TODO 6a */ )
#define ONSET_MIN_RMS ( /* TODO 6b */ )
#define CENTRE_BAND_Q8 ( /* TODO 6c */ )
#define POINTER_HALF (10) /* cells each side of centre */
static int16_t raw_buf[TOTAL_SAMPLES];
static int16_t ch0_raw[FRAMES];
static int16_t ch1_raw[FRAMES];
static int32_t ch0_dsp[FRAMES];
static int32_t ch1_dsp[FRAMES];
static dcblocker_t dc_ch0;
static dcblocker_t dc_ch1;
/* ============================================================
* TODO 7 : draw a pointer showing where the sound came from.
*
* Build a string of (2*POINTER_HALF + 1) cells with '|' at the
* centre and '#' at the position corresponding to lag_q8:
*
* offset = lag_q8 * POINTER_HALF / (SEARCH_LAG * 256)
* cell = POINTER_HALF + offset
*
* Clamp cell to the valid range, then print
* '[' cells ']'
*
* Think about the sign before you write it: a positive lag
* means the source is nearer ch0's microphone. Which end of
* the bar should that be? Pick a convention, write it in a
* comment, and check it against reality in Step 6.
* ---------------------------------------------------------- */
static void PrintPointer(int32_t lag_q8)
{
/* TODO 7 */
}
int main(void)
{
if (CY_RSLT_SUCCESS != cybsp_init()) {
CY_ASSERT(0);
}
__enable_irq();
UART_Console_Init();
GPIO_Control_Init();
UART_PrintLine("");
UART_PrintLine("=========================================");
UART_PrintLine(" Lab 105 Exp#6 : Sound Direction Sensing");
UART_PrintLine(" Clap to one side of the board.");
UART_PrintLine("=========================================");
if (!PDM_Mic_Init() || !PDM_Mic_Start()) {
UART_PrintLine("ERROR: microphone bring-up failed.");
for (;;) { }
}
for (uint32_t i = 0u; i < 8u; i++) {
(void)PDM_Mic_ReadBlock(raw_buf, TOTAL_SAMPLES);
}
DcBlocker_Init(&dc_ch0);
DcBlocker_Init(&dc_ch1);
for (;;) {
xcorr_result_t res;
uint32_t rms0;
uint32_t rms1;
if (!PDM_Mic_ReadBlock(raw_buf, TOTAL_SAMPLES)) {
continue;
}
/* Same front end as Exp#5. */
Stereo_Split(raw_buf, FRAMES, ch0_raw, ch1_raw);
for (uint32_t i = 0u; i < FRAMES; i++) {
ch0_dsp[i] = DcBlocker_Step(&dc_ch0, ch0_raw[i]);
ch1_dsp[i] = DcBlocker_Step(&dc_ch1, ch1_raw[i]);
}
rms0 = Rms_Block(ch0_dsp, FRAMES);
rms1 = Rms_Block(ch1_dsp, FRAMES);
/* TODO 8 ------------------------------------------------
* a) ONSET GATE. If neither channel exceeds
* ONSET_MIN_RMS, skip this block entirely - do not
* correlate and do not print.
*
* b) Call Xcorr_Estimate(ch0_dsp, ch1_dsp, FRAMES,
* SEARCH_LAG, &res).
*
* c) If res.valid is 0, print nothing and continue.
* An unreliable estimate is worse than no estimate.
*
* d) Otherwise print one line containing:
* - the refined lag in samples, to two decimals
* (lag_q8 / 256 for the whole part, and the
* remainder scaled for the fraction - the same
* sign handling as Exp#2's PrintTenths)
* - the pointer bar from TODO 7
* - a verdict string: LEFT / CENTRE / RIGHT,
* chosen by comparing lag_q8 against
* CENTRE_BAND_Q8
* ------------------------------------------------------ */
}
}
Step 6: Calibrate the Sign, Then Test
As in Exp#5, do not assume. Establish the sign convention by measurement before trusting any result.
- Hold the board flat, with the two microphones in a line running left to right in front of you.
- Identify which physical microphone is on your left, using the MIC_U4_CHANNEL / MIC_U5_CHANNEL mapping you recorded in Exp#5.
- Clap clearly to the far left of the board, at arm's length and roughly in the plane of the two microphones. Record the sign of the reported lag.
- Clap to the far right. The sign should be reversed.
- Clap directly in front, equidistant from both. The lag should be near zero.
- Repeat every five times. If the sign is inconsistent, the problem is usually a source that is too close — step further back so the far-field assumption holds.
| Clap position | lag (samples) | sign | consistent over 5 tries? |
|---|---|---|---|
| Far left | |||
| Directly in front | |||
| Far right |
Once the signs are established, fix the verdict strings in TODO 8 so that "LEFT" really means left. Then run the full test:
lag = -1.42 [#--------|----------] LEFT
lag = +0.08 [---------|----------] CENTRE
lag = +1.67 [---------|---------#] RIGHT
Observations to Record
Table 1 — angular behavior. Clap at each position at roughly 1 m, five times, and record the mean reported lag.
| Position | far left | 45° left | front | 45° right | far right |
|---|---|---|---|---|---|
| mean lag | |||||
| spread |
Compare the far-left and far-right values against the ±nmax you predicted in Step 2. They should be close to the limit and not exceed it — a lag beyond nmax is physically impossible and indicates a reflection or a bad estimate.
Table 2 — reliability. Out of 10 claps at each position, how many produced the correct verdict?
| Condition | correct / 10 | notes |
|---|---|---|
| Left, 1 m | ||
| Right, 1 m | ||
| Left, 3 m | ||
| Left, 1 m, facing a hard wall |
Compare the 3 m row against the same row in Exp#5's distance table. Exp#5's amplitude method had failed by that range; this one should not have.
Exercises
Exercise 1 — Go back to 16 kHz. Restore Fs = 16000 and its original decimation rate, adjust SEARCH_LAG to match, and repeat Table 2. Quantify how much worse it is. This is the experiment you were spared by Step 2, and it makes the value of that one changed constant concrete.
Exercise 2 — Turn off the refinement. Set delta_q8 to 0 so that only whole-sample lags are reported. How many distinct values does the output now take? Relate that count to nmax, and state how many directions the system can actually distinguish without interpolation.
Exercise 3 — Estimate the angle. Convert your refined lag into an angle using θ = arcsin(τ·c/d). You will need arcsin without floating point — a small lookup table of sin values, searched linearly, is entirely adequate for the handful of angles this system can resolve. Add the angle to the display. Then state honestly, from your Table 1 spread, what angular accuracy you can actually claim.
Exercise 4 — Reflections. Point the board at a hard wall about a meter away and clap from one side. Explain what a strong reflection does to the correlation function, and why widening SEARCH_LAG beyond nmax makes the problem worse rather than better.
Exercise 5 — Averaging (open-ended). Single claps give scattered estimates. Modify the program to collect several valid estimates and report their median rather than each one individually. Why is a median a better choice than a mean here? Consider what a single reflection-corrupted estimate does to each.
Exercise 6 — The full system (open-ended). Combine Exp#3, #4, and #6: detect that an event occurred, classify roughly what it was, and report where it came from. Sketch the architecture and identify which module owns which decision. You are not required to implement it.
Checkpoint
- All 8 TODO items have been implemented, and the project builds with no warnings.
- The Step 2 table is completed using your own measured microphone spacing.
- A clap from the left and a clap from the right produce lags of opposite sign, consistently over five repetitions.
- A clap directly in front produces a lag near zero.
- No reported lag exceeds your predicted nmax.
- Direction is still reported correctly at 3 m, where Exp#5's amplitude method had already failed.