I started with a simple dream: turn the BBC micro:bit v2 into a voice recorder using Zephyr RTOS. I ended up with something completely different. Here is the story of hardware limitations, driver hallucinations, and the math required to make a 5x5 LED matrix dance to the human voice.
The Dream: The Micro-RecorderThe BBC micro:bit v2 is a powerhouse. It has an nRF52833 SoC, a MEMS microphone, and a speaker. Naturally, my first instinct was: "Let's record audio and play it back."
I set up the Zephyr environment. I configured the ADC. I built a circular buffer system.
And it worked!I could see the RMS values changing in the console when I spoke.
But then I tried to play it back.
Attempt 1: The PWM "Speaker"The micro:bit doesn't have a DAC. It has a piezo-style magnetic buzzer connected to a GPIO pin. To play audio, I had to modulate the PWM duty cycle to mimic an analog wave.
We tried bit-banging (too slow). I tried the standard Zephyr PWM API (too heavy). Finally, I wrote a direct hardware driver using the Nordic HAL and EasyDMA to pump samples to the PWM peripheral without blocking the CPU.
/* The "Technically Working" but "Audio Disaster" Code */
static void audio_hw_play(const int16_t *buf, size_t n) {
NRF_PWM_Type *pwm = (NRF_PWM_Type *)DT_REG_ADDR(DT_NODELABEL(pwm0));
// Map 12-bit audio to PWM duty cycle
for (size_t i = 0; i < n; i++) {
int32_t duty = 500 + (buf[i] * 500) / 2048;
pwm_seq_buf[i] = (uint16_t)duty;
}
// Fire DMA sequence
pwm->SEQ[0].PTR = (uint32_t)pwm_seq_buf;
pwm->TASKS_SEQSTART[0] = 1;
}The Result? It sounded like a chipmunk trapped in a blender.I slowed down the clock. It sounded like a demon trapped in a blender.
The Realization: The onboard component (MLT-8530) is a magnetic buzzer. It is physically tuned to resonate at 2-4kHz (beeps). It cannot reproduce the complex frequencies of human speech. I were trying to play a symphony on a door buzzer.
Therefore, I pivoted. If I can't hear the audio clearly, let's make sure I can see it.
The Pivot: The Sound Level MeterThe goal changed: Build a "Zoom-style" microphone test bar on the micro:bit's 5x5 LED matrix.
Trap #1: The Phantom PDMZephyr has excellent support for PDM (Pulse Density Modulation) microphones, which are common on nRF52 boards. We enabled CONFIG_AUDIO_DMIC, set up the pins, and flashed the board.
But the console spit out errors:
[err] dmic_nrfx_pdm: Requested configuration is not supportedI spent hours tweaking clock frequencies. 16kHz? 15.625kHz? 1.28MHz? Nothing worked.
Therefore, I opened the schematic.
The microphone on the micro:bit v2 (Knowles SPU0410LR5H) is NOT PDM. It is an Analog microphone connected to P0.05 (AIN3).
I was trying to force a digital driver to talk to an analog pin.
The Fix: I threw out the DMIC driver and wrote a custom Device Tree Overlay to configure the SAADC (Successive Approximation ADC). Note the specific gain: the mic has a 1.65V DC bias, so standard gain settings would saturate immediately.
/* app.overlay */
&adc {
channel@3 {
reg = <3>; // AIN3
zephyr,gain = "ADC_GAIN_1_4"; // Range up to 2.4V covers the 1.65V bias
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_AIN3>;
zephyr,resolution = <12>;
};
};The Visualization: Making LEDs DanceNow I had clean audio data. I wanted to map the volume to the LEDs.
Trap #2: The Invisible DisplayI calculated the volume and sent it to the display using display_write.And... the screen stayed black.
The standard Zephyr display driver is generic. It expects a pixel buffer. I assumed 1 byte per pixel (25 bytes).
But the micro:bit driver is bit-packed. It expects a bitmap where 1 bit = 1 pixel. By sending 25 bytes, I was sending "header" data that looked like empty space to the driver.
Therefore, I corrected the buffer mapping. 5 Rows = 5 Bytes.
/* The Correct Bitmap Mapping */
static void update_display(int level) {
uint8_t buf[5];
memset(buf, 0, sizeof(buf));
for (int i = 0; i < 5; i++) {
int row_index = 4 - i; // Invert to fill from bottom up
if (i < level) {
buf[row_index] = 0x1F; // 0x1F = Binary 11111 (All LEDs in row ON)
}
}
// Write 5 bytes to the display controller
display_write(display_dev, 0, 0, &desc, buf);
}The Final Boss: Physics vs. PerceptionI had lights. I had sound. But when I spoke, the lights barely moved. When I blew into the mic, it went crazy.
The Problem:
- RMS vs Peak: I was calculating RMS (Average Energy). Speech has short bursts of energy (consonants) separated by silence. Averaging 100ms of speech results in a low number.
- Physics: Blowing creates physical wind pressure (Massive Voltage). Speech creates tiny air vibrations (Tiny Voltage).
Therefore, I implemented "The Speech Booster" algorithm.
- Peak Detection: Instead of RMS, we calculate Max - Min in the buffer. This catches the "spikes" of your voice instantly.
- Noise Floor Subtraction: I empirically found the silence noise floor was ~94. We subtract this to zero out the room.
- Digital Gain: I multiplied the remaining signal by 3. This stretches the tiny variations of human speech to fill the full 0-5 range of the LED matrix
/* The Speech Booster Logic */
int32_t raw_amp = calculate_peak_amplitude(sample_buffer, BUFFER_SIZE);
// 1. Subtract Noise Floor
int32_t signal = raw_amp - 94;
if (signal < 0) signal = 0;
// 2. Digital Amplification
int32_t boosted = signal * 3;
// 3. Map to LEDs with Peak Hold (Gravity)
if (boosted > 40) target_level = 5;
else if (boosted > 30) target_level = 4;
// ... etcConclusionI failed to build a Voice Recorder, but I succeeded in building a robust Sound Level Meter.
This project is a perfect example of embedded engineering:
- Don't trust the assumptions (Check the schematic!).
- Don't fight the hardware (If it's a buzzer, don't make it sing).
- Math is your friend (Logarithmic scaling makes sensors feel "natural").
The full source code is available below. Flash it to your micro:bit v2, clap your hands or speak, and watch the LEDs dance.
The relevant PR to the official Zephyr RTOS repository and a new sample for the micro:bit v2 are coming soon.
VideoYou can see that the audio is being detected, and the corresponding sound bar on the micro:bit indicates the sound level





_SPmZ63wPgQ.png?auto=compress%2Cformat&w=40&h=40&fit=fillmax&bg=fff&dpr=2)
Comments