Why I Started Using This Tool
My proximity alarm could measure distance ten times a second, and the buzzer was wired and tested. But the two didn't know about each other. The whole point of the project is a parking-sensor feel: nothing when far, slow beeps as you get closer, a frantic chirp right at the sensor.
The KSSG1203 is a passive buzzer, so it needs a square wave at a few kHz. My first shell test with
pinctrl sounded awful, because toggling a pin from software is slow and jittery. Doing it from a C
thread would be better, but it would burn a whole CPU core and still stutter every time Linux paused my thread.
The Pi has a better answer built into the chip: a hardware PWM controller, a little circuit that
makes a perfect square wave all by itself while our program sleeps. Since I'd already learned to reach the GPIO
registers through /dev/mem for the distance sensor, I decided to drive the PWM the same way, straight
through its registers. Same tools as last time, just a few more registers.
Missed the distance task? Start with Part 10: My Raspberry Pi Can See: Timing an Ultrasonic Echo in C Through /dev/mem (video).
Here's the video walkthrough of this post, if you'd rather watch it step by step:
What It Does
The buzzer task is one new file, buzzer.c, plus one setter in the shared frame:
- Setup: map three register pages from
/dev/mem: GPIO, PWM and the clock manager. Set the PWM clock to the 19.2 MHz crystal divided by 4 (with the clock manager's0x5Apassword and a stop-wait-divide-start sequence), set RNG1 = 1777 counts for a 2.7 kHz tone, DAT1 = 0 for silence, turn the PWM on in mark/space mode, and only then switch GPIO18 to ALT5. - Mapping: beyond 150 cm, silent. From 150 cm to 10 cm, volume rises from 5 % to 100 % and the beep period shrinks from 1000 ms to 100 ms, in a straight line. 100 % volume is 50 % duty, the loudest a square wave gets.
- Rhythm: every 10 ms, the task copies the frame, computes volume and period, writes them back with
Frame_setBuzzer, and sets DAT1 from a phase counter: 50 ms of tone at the start of each period, silence for the rest. A new distance is heard within 10 ms. - Clean exit: DAT1 = 0, GPIO18 back to an input, PWM and clock off, pages unmapped.
The console now shows dist=44cm buz=76%/318ms, and more importantly, you can close your eyes and
hear how close your hand is.
Step by Step
1. Recap: what we already have
From Part 8, the hardware:
Pi pin 1 (3.3 V) ββ buzzer + ... buzzer β ββ C β
Pi pin 12 (GPIO18) ββ 1 kΞ© ββ B β 2N3904
Pi pin 6 (GND) βββββββββββββββ E βββββββββββββββ
GPIO18 high: the transistor switches on and current flows through the buzzer. GPIO18 low: off. And the KSSG1203 is a passive buzzer: on steady DC it only clicks. It needs a square wave.
From Part 6, two frame fields that have been waiting for today:
UINT8 buzzerVolume; /* 0..100 % */
UINT16 buzzerPeriodMs; /* beep period, 0 = silent */
From Part 10, the input: distanceCm and distanceValid, updated every
100 ms, and the /dev/mem + mmap trick for reaching hardware registers.
2. A passive buzzer needs a wave: three knobs
Part 8 told us what each property of the square wave does. Now we turn each one into a number:
| Knob | What it is | What you hear | Our choice |
|---|---|---|---|
| Frequency | Waves per second | Pitch | A fixed 2700 Hz, near the buzzer's loudest point |
| Duty cycle | How much of each wave is "on" | Loudness | 0 to 50 %, from buzzerVolume |
| Beep period | How often we beep | Urgency | 1000 ms far away, down to 100 ms close |
Why 2700 Hz? Every transducer has a resonant frequency where the little disc wants
to vibrate, and there it's much louder. For buzzers this size it's usually somewhere between 2 and 4 kHz. Check your
datasheet; if you don't have it, try a few values and keep the loudest. It's one #define.
Why stop at 50 % duty? A 50 % square wave pushes the disc and lets it go for equal times: that's the strongest vibration. Past 50 %, the coil is just on for longer. It doesn't get louder, it only warms up. So 100 % volume means 50 % duty.
An honest note: our ears don't hear loudness in a straight line, so 10 % volume isn't "one tenth as loud". For an alarm that doesn't matter. All we need is "quiet" far away and "loud" up close.
3. Why hardware PWM?
In Part 8 we made a rough buzz from the shell with a pinctrl loop. It sounded bad, because each toggle
took far too long and the timing jittered. Could our C program toggle GPIO18 itself, the way we pulsed TRIG in
Part 10?
| Software toggling | Hardware PWM | |
|---|---|---|
| Who makes the wave | Our thread: set, wait 185 Β΅s, clear, wait 185 Β΅s, forever | A circuit inside the chip |
| CPU | Spins one core at 100 % | ~0 %: we only write a register when the volume changes |
| Clean tone? | Every time Linux pauses our thread, the tone stutters | Perfect, even when Linux is busy |
PWM means pulse width modulation: a square wave where you choose the frequency and the width of the "on" pulse. The BCM2837 has a PWM controller with two channels, and GPIO18 is one of the few pins that can be connected to it. That's exactly why we picked GPIO18 back in Part 3.
4. Three register blocks this time
In Part 10 we needed only the GPIO block. To make PWM work, we need three:
| Block | Job | Bus address (datasheet) | ARM physical (Pi 3) |
|---|---|---|---|
| GPIO | Connect GPIO18 to the PWM | 0x7E200000 | 0x3F200000 |
| PWM | Make the square wave | 0x7E20C000 | 0x3F20C000 |
| Clock manager | Give the PWM a clock to count with | 0x7E101000 | 0x3F101000 |
Same rule as last time: the datasheet prints bus addresses starting with 0x7E, and on the Pi 3 the ARM
sees them at 0x3F. (On a Pi 4, 0xFE.) All three start on a 4 KB boundary, so we map
one page per block, three mmap calls on the same /dev/mem file.
You can see two of them claimed by kernel drivers:
sudo cat /proc/iomem | grep -E 'gpio|cprman'
# 3f101000-3f102fff : cprman@7e101000 β the clock manager
# 3f200000-3f2000b3 : gpio@7e200000
The PWM block usually isn't listed at all, because no kernel driver is using it. Good: it's ours.
5. Step 1: connect GPIO18 to the PWM (ALT5)
Remember the 3-bit function select from Part 10? 000 = input, 001 =
output. The other six values are alternate functions: the pin gets connected to one of the chip's
built-in circuits instead of being a plain GPIO.
For GPIO18, ALT5 (code 010) is PWM0, channel 1 of the PWM controller.
GPIO18 β GPFSEL1, bits 24..26 (18 / 10 = 1, (18 % 10) Γ 3 = 24)
010 = ALT5 = PWM0
It's the same gpioMode() read-clear-set-write as last time, with a new mode number. And when we stop, we
set GPIO18 back to input: the Pi's own pull-down holds the transistor's base low, so the buzzer
stays silent. That's exactly how the Pi boots, as we saw in Part 8.
6. Step 2: the clock manager
The PWM is a counter, and a counter needs a clock to count with. The chip's clock manager has a separate clock just for the PWM, and we have to set it up ourselves:
- Source: the 19.2 MHz crystal oscillator on the board (source number
1). It's always there and never changes speed. - Divider: 19.2 MHz / 4 = 4.8 MHz. The PWM counts 4.8 million times per second.
The clock manager has two quirks, and both are safety features:
- The password. Every write must have
0x5Ain its top 8 bits. Write without it and the chip ignores you. That protects the clocks from a stray pointer write, which, with/dev/mem, is a very good idea. - Never change a running clock. The order is: switch the clock off, wait until the BUSY bit clears, set the divider, switch it on again. Changing the divider while the clock runs can glitch it.
clk[CM_PWMCTL] = CM_PASSWD | CM_SRC_OSC; /* ENAB off */
usleep(CM_SETTLE_US); /* give BUSY time to show up */
while (clk[CM_PWMCTL] & CM_BUSY) { ... } /* wait, with a timeout */
clk[CM_PWMDIV] = CM_PASSWD | (PWM_CLK_DIV << 12); /* integer divider in bits 12..23 */
usleep(CM_SETTLE_US);
clk[CM_PWMCTL] = CM_PASSWD | CM_SRC_OSC | CM_ENAB;
usleep(CM_SETTLE_US); /* let it start before the PWM uses it */
And of course, the wait has a timeout, our rule from Part 10: never wait forever for hardware.
Those usleep(110) calls are the bug I hit. My first version had no pauses, and the
buzzer stayed silent, even though every register read back perfectly and vcgencmd measure_clock pwm
reported 4.8 MHz. The reason: right after you write "ENAB off", the clock hasn't raised BUSY yet. So the
while loop sees BUSY = 0, thinks "already stopped!", and we change the divider of a clock that is still
running. The clock generator keeps going, but the PWM channel behind it never starts. A pause of about 110 Β΅s (the
same as the well-known wiringPi library uses) gives the hardware time to react. Lesson: hardware is not as
fast as your C code. A register that reads back right doesn't mean the circuit behind it has caught up.
7. Step 3: the PWM itself β RNG1 and DAT1
Now the easy part. Channel 1 of the PWM has two numbers:
| Register | Meaning | Our value |
|---|---|---|
| RNG1 | Counts per wave: sets the frequency | 4 800 000 / 2700 = 1777 |
| DAT1 | Counts the output stays high: sets the duty cycle | 0 (silent) to 888 (50 %) |
one wave = 1777 counts at 4.8 MHz = 370 Β΅s β 2701 Hz
DAT1 = 888: ββββββββββββββββββββββββββββββββ 50 % duty: loud
DAT1 = 89: ββββββββββββββββββββββββββββββββ 5 % duty: quiet
DAT1 = 0: ββββββββββββββββββββββββββββββββ silent, transistor off
So turning volume into a register value is one line:
pwm[PWM_DAT1] = (PWM_RANGE * volume) / 200u; /* 100 % volume = 50 % duty */
In the CTL register we set two bits: PWEN1 switches channel 1 on, and MSEN1 selects mark/space mode. Without MSEN1, the PWM uses its default mode, which spreads the "on" counts evenly over the wave (great for dimming an LED, useless for a tone). Mark/space gives what we drew above: one clean high block, then one low block, every wave.
Notice what DAT1 = 0 gives us for free: the pin stays low, the transistor stays off. So a beep is simply "DAT1 = volume for 50 ms, then DAT1 = 0". The wave keeps running underneath. We never stop or restart anything.
8. From distance to sound: the mapping
This is the part that makes it an alarm. We pick two distances:
BUZZER_FAR_CM= 150 cm: farther than that (or no valid reading) β silent.BUZZER_NEAR_CM= 10 cm: closer than that β loudest and fastest.
In between, both knobs slide in a straight line:
volume = 5 % + (100 % β 5 %) Γ (150 β cm) / 140
periodMs = 100 + (1000 β 100) ms Γ (cm β 10) / 140
| Distance | Volume | Beep period | What you hear |
|---|---|---|---|
| no reading, or > 150 cm | 0 % | 0 (silent) | nothing |
| 150 cm | 5 % | 1000 ms | a faint tick, once a second |
| 100 cm | 38 % | 678 ms | beep... beep... |
| 50 cm | 72 % | 357 ms | beep, beep, beep |
| β€ 10 cm | 100 % | 100 ms | beepbeepbeepbeep, loud |
That's the goal from our project plan: nearly silent when objects are far away, louder and more urgent as
they get closer. All five numbers are #defines in buzzer.h, so tuning the feel is
a recompile, not a rewrite.
We do it with integer math, multiply first, divide last, so we never lose the fraction too early.
95 Γ 140 is far from overflowing a UINT32.
9. The beep rhythm: a 10 ms tick and a phase counter
Every beep is 50 ms of tone. Then silence until the period is over. How do we time that?
The obvious way is: beep, sleep 50 ms, silence, sleep period β 50 ms. But at 150 cm that second sleep
is 950 ms. If you jump in front of the sensor during that time, the alarm doesn't react for almost
a second. That's bad for an alarm.
So instead the task wakes up every 10 ms, on the same drift-free Tick timing as every
other task, and keeps a phase counter: "how many ms into the current period am I?"
every 10 ms:
read the frame, compute volume and period
if silent, or the phase passed the period: phase = 0 (start a new period)
tone on if phase < 50 ms, otherwise off
phase += 10
period = 357 ms (50 cm):
phase: 0 50 357
βββββββββββββββββββββββββββββββββββββββββ ...
beep silence beep
Now a new distance changes the sound within 10 ms. And when the period suddenly gets shorter than where the phase already is, the next tick simply starts a new period with a beep right away: just what you want when something jumps closer.
Is waking 100 times a second expensive? No. Each tick is a frame copy, a few multiplications and one register write: microseconds of work, then sleep. Compare that with the echo busy-wait from Part 10.
10. The frame gets another writer: Frame_setBuzzer
The buzzer task owns buzzerVolume and buzzerPeriodMs. It computes them
and writes them to the frame, so the console (and later the LCD) can show what the buzzer is doing. Same pattern as
Frame_setDistance. In frame.h:
void Frame_setBuzzer(UINT8 volume, UINT16 periodMs);
In frame.c:
void Frame_setBuzzer(UINT8 volume, UINT16 periodMs)
{
pthread_mutex_lock(&frameLock);
frame.buzzerVolume = volume;
frame.buzzerPeriodMs = periodMs;
pthread_mutex_unlock(&frameLock);
}
Both together, under one lock: nobody should ever see "volume 100 %, period from the old silent state". And
Frame_toString from Part 6 has been printing buz=%u%%/%ums since day one. Today those
numbers finally move.
The data flow is now a real pipeline, and no task calls another:
ultrasonic task ββFrame_setDistanceβββΆ FRAME ββFrame_getβββΆ buzzer task βββΆ PWM registers
β² β
ββββFrame_setBuzzerββββββ
β
sw alive task prints it all
11. buzzer.h: the public menu
#ifndef BUZZER_H
#define BUZZER_H
/************************************************************************/
/* Buzzer task: turns the distance into sound on the KSSG1203. */
/* GPIO18 runs the BCM2837 hardware PWM: the tone frequency sets the */
/* pitch, the duty cycle sets the volume, and short beeps repeated */
/* every buzzerPeriodMs set the urgency. Far: silent. Close: loud. */
/* Registers are accessed through /dev/mem. Needs root. */
/************************************************************************/
#include "ctypes.h"
#define BUZZER_TICK_MS (10u) /* check the distance every 10 ms */
#define BUZZER_GPIO (18u) /* BCM GPIO18, physical pin 12, ALT5 = PWM0 */
#define BUZZER_TONE_HZ (2700u) /* pitch: near the resonance, check your datasheet */
#define BUZZER_BEEP_MS (50u) /* length of one beep */
#define BUZZER_FAR_CM (150u) /* farther than this: silent */
#define BUZZER_NEAR_CM (10u) /* closer than this: loudest and fastest */
#define BUZZER_MIN_VOLUME (5u) /* % at BUZZER_FAR_CM: nearly silent */
#define BUZZER_SLOW_PERIOD_MS (1000u) /* one beep per second at BUZZER_FAR_CM */
#define BUZZER_FAST_PERIOD_MS (100u) /* ten beeps per second at BUZZER_NEAR_CM */
INT32 Buzzer_start(void);
void Buzzer_stop(void);
#endif
Same shape as ultrasonic.h: start, stop, and every number you might want to tune with a comment that
tells you what it does.
12. buzzer.c: the buzzer thread
Here's the whole file. Then we'll walk through it.
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include "frame.h"
#include "tick.h"
#include "buzzer.h"
/* BCM2837 (Pi 3) physical addresses. The datasheet lists the bus addresses
0x7E200000, 0x7E20C000 and 0x7E101000; the ARM sees 0x7E.. at 0x3F.. */
#define PERI_BASE (0x3F000000u)
#define GPIO_PHYS_BASE (PERI_BASE + 0x200000u) /* GPIO pins */
#define PWM_PHYS_BASE (PERI_BASE + 0x20C000u) /* PWM controller */
#define CLK_PHYS_BASE (PERI_BASE + 0x101000u) /* clock manager */
#define REGS_MAP_SIZE (4096u) /* one page per block */
/* GPIO registers, as 32-bit word offsets */
#define GPFSEL0 (0x00u / 4u) /* function select: 3 bits per pin, 10 pins per register */
#define GPIO_INPUT (0u)
#define GPIO_ALT5 (2u) /* GPIO18 ALT5 = PWM0, channel 1 */
/* Clock manager: the PWM clock. Every write needs the password in bits 24..31 */
#define CM_PWMCTL (0xA0u / 4u)
#define CM_PWMDIV (0xA4u / 4u)
#define CM_PASSWD (0x5Au << 24)
#define CM_SRC_OSC (1u) /* source: 19.2 MHz crystal oscillator */
#define CM_ENAB (1u << 4) /* clock on */
#define CM_BUSY (1u << 7) /* clock still running */
#define CM_BUSY_TRIES (1000u) /* x 10 us = give up after 10 ms */
#define CM_SETTLE_US (110u) /* time for the clock to react to a write */
#define PWM_CLK_DIV (4u) /* 19.2 MHz / 4 = 4.8 MHz */
#define PWM_CLK_HZ (19200000u / PWM_CLK_DIV)
/* PWM controller registers, as 32-bit word offsets */
#define PWM_CTL (0x00u / 4u)
#define PWM_RNG1 (0x10u / 4u) /* channel 1: counts per wave = frequency */
#define PWM_DAT1 (0x14u / 4u) /* channel 1: counts high = duty cycle */
#define PWM_PWEN1 (1u << 0) /* channel 1 enable */
#define PWM_MSEN1 (1u << 7) /* mark/space mode: high for DAT, low for RNG - DAT */
#define PWM_RANGE (PWM_CLK_HZ / BUZZER_TONE_HZ)
static pthread_t buzThread;
static volatile BOOLEAN buzRunning = FALSE;
static volatile UINT32 *gpio = NULL; /* volatile: every access really hits the hardware */
static volatile UINT32 *pwm = NULL;
static volatile UINT32 *clk = NULL;
static volatile UINT32 *mapRegs(INT32 fd, UINT32 phys)
{
void *map = mmap(NULL, REGS_MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, phys);
if (map == MAP_FAILED)
{
perror("mmap /dev/mem");
return NULL;
}
return (volatile UINT32 *)map;
}
static void unmapRegs(volatile UINT32 **regs)
{
if (*regs != NULL)
{
munmap((void *)*regs, REGS_MAP_SIZE);
*regs = NULL;
}
}
static void gpioMode(UINT32 pin, UINT32 mode)
{
UINT32 reg = GPFSEL0 + (pin / 10u);
UINT32 shift = (pin % 10u) * 3u;
gpio[reg] = (gpio[reg] & ~(7u << shift)) | (mode << shift);
}
/* Stop the PWM clock, wait until it has really stopped, set the divider,
start it again. Returns -1 if the clock never stops.
The clock needs a moment after each write: right after "ENAB off" BUSY
is not set yet, and without the wait we would change the divider of a
running clock. The generator keeps running, but the PWM never starts. */
static INT32 clockSetup(void)
{
UINT32 tries = 0u;
clk[CM_PWMCTL] = CM_PASSWD | CM_SRC_OSC; /* ENAB off */
usleep(CM_SETTLE_US); /* give BUSY time to show up */
while (clk[CM_PWMCTL] & CM_BUSY)
{
if (++tries > CM_BUSY_TRIES)
{
return -1;
}
usleep(10);
}
clk[CM_PWMDIV] = CM_PASSWD | (PWM_CLK_DIV << 12); /* integer divider in bits 12..23 */
usleep(CM_SETTLE_US);
clk[CM_PWMCTL] = CM_PASSWD | CM_SRC_OSC | CM_ENAB;
usleep(CM_SETTLE_US); /* let it start before the PWM uses it */
return 0;
}
/* 0..100 % volume = 0..50 % duty cycle. A 50 % square wave is the loudest. */
static void pwmVolume(UINT8 volume)
{
pwm[PWM_DAT1] = (PWM_RANGE * volume) / 200u;
}
/* Far: silent. Between FAR and NEAR: louder and faster. Near: loudest, fastest. */
static void distanceToBuzzer(const FRAME *f, UINT8 *volume, UINT16 *periodMs)
{
UINT32 cm = f->distanceCm;
UINT32 span = BUZZER_FAR_CM - BUZZER_NEAR_CM;
if ((!f->distanceValid) || (cm > BUZZER_FAR_CM))
{
*volume = 0u;
*periodMs = 0u;
}
else if (cm <= BUZZER_NEAR_CM)
{
*volume = 100u;
*periodMs = BUZZER_FAST_PERIOD_MS;
}
else
{
*volume = (UINT8)(BUZZER_MIN_VOLUME +
((100u - BUZZER_MIN_VOLUME) * (BUZZER_FAR_CM - cm)) / span);
*periodMs = (UINT16)(BUZZER_FAST_PERIOD_MS +
((BUZZER_SLOW_PERIOD_MS - BUZZER_FAST_PERIOD_MS) * (cm - BUZZER_NEAR_CM)) / span);
}
}
/* Silence everything and leave GPIO18 the way the Pi boots: an input */
static void hwOff(void)
{
pwmVolume(0u);
gpioMode(BUZZER_GPIO, GPIO_INPUT); /* pull-down keeps the 2N3904 off */
pwm[PWM_CTL] = 0u;
clk[CM_PWMCTL] = CM_PASSWD | CM_SRC_OSC; /* clock off */
}
static void *buzzer_task(void *arg)
{
struct timespec next;
FRAME snapshot;
UINT8 volume;
UINT16 periodMs;
UINT32 phaseMs = 0u; /* where we are in the beep period */
(void)arg;
clock_gettime(CLOCK_MONOTONIC, &next);
while (buzRunning)
{
Frame_get(&snapshot); /* lock, copy, unlock */
distanceToBuzzer(&snapshot, &volume, &periodMs);
Frame_setBuzzer(volume, periodMs);
if ((periodMs == 0u) || (phaseMs >= periodMs))
{
phaseMs = 0u; /* start a new period */
}
pwmVolume(((periodMs != 0u) && (phaseMs < BUZZER_BEEP_MS)) ? volume : 0u);
phaseMs += BUZZER_TICK_MS;
Tick_addMs(&next, BUZZER_TICK_MS);
Tick_sleepUntil(&next);
}
return NULL;
}
INT32 Buzzer_start(void)
{
INT32 fd;
fd = open("/dev/mem", O_RDWR | O_SYNC); /* root only: this is all of physical memory */
if (fd < 0)
{
perror("/dev/mem");
return -1;
}
gpio = mapRegs(fd, GPIO_PHYS_BASE);
pwm = mapRegs(fd, PWM_PHYS_BASE);
clk = mapRegs(fd, CLK_PHYS_BASE);
close(fd); /* the mappings stay valid */
if ((gpio == NULL) || (pwm == NULL) || (clk == NULL))
{
unmapRegs(&gpio);
unmapRegs(&pwm);
unmapRegs(&clk);
return -1;
}
pwm[PWM_CTL] = 0u; /* PWM off while we change its clock */
usleep(10);
if (clockSetup() != 0)
{
fprintf(stderr, "PWM clock did not stop\n");
unmapRegs(&gpio);
unmapRegs(&pwm);
unmapRegs(&clk);
return -1;
}
pwm[PWM_RNG1] = PWM_RANGE; /* pitch */
pwmVolume(0u); /* start silent */
pwm[PWM_CTL] = PWM_PWEN1 | PWM_MSEN1;
gpioMode(BUZZER_GPIO, GPIO_ALT5); /* connect the pin to PWM0 */
buzRunning = TRUE;
if (pthread_create(&buzThread, NULL, buzzer_task, NULL) != 0)
{
buzRunning = FALSE;
hwOff();
unmapRegs(&gpio);
unmapRegs(&pwm);
unmapRegs(&clk);
return -1;
}
return 0;
}
void Buzzer_stop(void)
{
if (buzRunning)
{
buzRunning = FALSE;
pthread_join(buzThread, NULL); /* wait for the last tick */
hwOff(); /* leave the pin safe and silent */
unmapRegs(&gpio);
unmapRegs(&pwm);
unmapRegs(&clk);
Frame_setBuzzer(0u, 0u);
}
}
Let's walk through it.
The addresses. PERI_BASE is the Pi 3's peripheral window, and the three blocks are
offsets from it, with the datasheet's bus addresses in the comment. On a Pi 4, you'd change one line:
0xFE000000.
mapRegs and unmapRegs. Three blocks means three mmaps, so the
mapping code from Part 10 became a tiny helper. We open /dev/mem once, map three pages, and close the
file. If any map fails, we unmap whatever did work and give up. unmapRegs takes a pointer to
the pointer, so it can also set it back to NULL; calling it twice is harmless.
gpioMode again? Yes, the same five lines as in ultrasonic.c. Copying them
once is fine. When a third task needs GPIO, that's the moment to move them into their own gpio.c. Not
before.
Buzzer_start: silence first, then connect. The order matters, just like "TRIG low before output" last time:
- PWM off, then set up the clock.
- RNG1 = pitch, DAT1 = 0: the wave runs, but silent.
- PWM on.
- Only now connect GPIO18 to the PWM with ALT5.
So the pin never sees a random wave from whatever was left in the registers. And if anything fails,
Buzzer_start cleans up and returns -1.
clockSetup is section 6 in code: off, a 110 Β΅s pause, wait for BUSY with a 10 ms
timeout, divide, pause, on, pause. The BUSY wait sleeps 10 Β΅s per try instead of spinning flat out. All the pauses
together are well under a millisecond, and they only happen once, at start-up.
distanceToBuzzer is section 8: a pure function. Frame in, two numbers out, no hardware
and no locks. That makes it the easiest function in the project to test: give it 50 cm and check you get 72 % and
357 ms.
The task loop is section 9: copy the frame, compute, write the result back to the frame, then set
DAT1 from the phase. Notice Frame_get and Frame_setBuzzer each take the lock for a moment,
and nothing slow happens while we hold it.
hwOff and Buzzer_stop. Stop the thread, then DAT1 = 0, GPIO18 back to
input, PWM off, clock off, unmap, and mark the buzzer silent in the frame. The alarm leaves the Pi the way it found
it. If the program is killed with kill -9 and can't clean up, GPIO18 might be left on ALT5. More on that
in the troubleshooting table.
13. main.c: also fatal
#include "buzzer.h"
if (Buzzer_start() != 0)
{
/* an alarm that can't make noise isn't an alarm: this one is fatal */
fprintf(stderr, "could not start buzzer task\n");
Ultrasonic_stop();
HWAlive_stop();
SWAlive_stop();
return EXIT_FAILURE;
}
Buzzer_stop(); /* stop in reverse start order */
Ultrasonic_stop();
HWAlive_stop();
SWAlive_stop();
Same decision as the sensor last time: no LED is fine, but an alarm without a voice is not an alarm. So we stop
everything we started and exit with a failure, and journalctl tells us why.
We start the buzzer after the ultrasonic task, and stop it first. Reverse order on the way out: the buzzer is silenced while the sensor is still measuring, so it never beeps based on a reading from a task that's already gone.
14. CMake
add_executable(proxalarm
src/main.c
src/frame.c
src/tick.c
src/swalive.c
src/hwalive.c
src/ultrasonic.c
src/buzzer.c)
One more .c file, no new libraries.
15. Build, deploy, listen
cmake --preset pi-debug
cmake --build --preset deploy
ssh au@au.local
sudo ./proxalarm
Move your hand slowly toward the sensor:
Main running. Press Ctrl+C to stop.
[alive 1] dist=---cm buz=0%/0ms led=0 lcd="Proximity alarm|starting..."
[alive 2] dist=---cm buz=0%/0ms led=1 lcd="Proximity alarm|starting..."
[alive 3] dist=120cm buz=25%/807ms led=1 lcd="Proximity alarm|starting..."
[alive 4] dist=44cm buz=76%/318ms led=0 lcd="Proximity alarm|starting..."
[alive 5] dist=24cm buz=90%/190ms led=0 lcd="Proximity alarm|starting..."
[alive 6] dist=9cm buz=100%/100ms led=1 lcd="Proximity alarm|starting..."
And you hear it: quiet ticks far away, a calm beep... beep..., then faster and louder, and a frantic chirp right at the sensor. Pull your hand away and it goes silent again. That's the proximity alarm.
While it runs, open a second SSH window and look at the pin:
pinctrl get 18
# 18: a5 pd | lo // GPIO18 = PWM0_0
a5: ALT5, connected to PWM0. (The exact text depends on your pinctrl version.) Press
Ctrl+C in the first window, then check again:
pinctrl get 18
# 18: ip pd | lo // GPIO18 = input
Back to an input with its pull-down: silent and safe. Then the real way:
sudo systemctl restart proxalarm
journalctl -u proxalarm -f
16. When things go wrong
| Problem | What it means | Fix |
|---|---|---|
/dev/mem: Permission denied | Not root | sudo ./proxalarm, or run it as the service |
PWM clock did not stop | The clock manager never cleared BUSY | Reboot. If it keeps happening, another program or driver is using the PWM clock |
buz= changes, pinctrl get 18 shows a5, registers look right, still silent | The PWM channel isn't running. Read PWM_STA (offset 0x04): bit 9 (STA1) must be 1 | The clock was changed while still running: keep the CM_SETTLE_US pauses in clockSetup |
buz= numbers change but no sound at all | Wiring, or GPIO18 isn't on ALT5 | pinctrl get 18 while running must show a5. Then re-check E-B-C from Part 8 |
| Only clicks, no tone | The pin switches, but the PWM isn't making a wave | Check PWM_MSEN1 and PWM_PWEN1 in CTL, and that RNG1 isn't 0 |
| Sound is weak at every distance | Tone far from the buzzer's resonance | Try BUZZER_TONE_HZ values from 2000 to 4000 and keep the loudest |
| Tone goes wrong when something plays audio | The headphone jack also uses the PWM controller | On a Pi that only runs the alarm, add dtparam=audio=off to /boot/firmware/config.txt |
Silent even when close, dist= shows --- | No valid reading, so silence is correct | Fix the sensor first: Part 10's troubleshooting table |
| Beeps at 120 cm feel too eager (or too lazy) | That's tuning, not a bug | Change BUZZER_FAR_CM, BUZZER_MIN_VOLUME or the periods in buzzer.h |
| Buzzer keeps sounding after a crash | Killed before Buzzer_stop could clean up | pinctrl set 18 ip pd, or reboot |
17. What's next
The alarm now sees and speaks: three tasks, one frame, a real pipeline from sound wave in to sound wave out. One part is still missing from our hardware list: the LCD1602. Next we wire it over I2C, just two signal wires for a whole display, and test it from the console before we write the LCD task that puts the distance on the screen.
Key Takeaways
- A passive buzzer has three knobs: frequency = pitch (fixed near resonance, 2700 Hz), duty cycle = loudness (0β50 %), beep period = urgency (1000 ms far, 100 ms close).
- Hardware PWM makes a perfect tone with ~0 % CPU. Connect GPIO18 to it with ALT5, then RNG1 sets the frequency and DAT1 sets the duty cycle. MSEN1 gives a clean square wave.
- The PWM needs a clock, and the clock manager is protected: password
0x5Ain every write, and always stop, wait for BUSY (with a timeout), divide, start. - The mapping is a pure function: beyond 150 cm silent, then volume and beep rate rise in a straight line to 10 cm. Five
#defines tune the whole feel. - A 10 ms tick with a phase counter reacts to a new distance within 10 ms, instead of finishing a 1-second sleep first. Silence first, connect the pin last, and leave it as an input on exit.
Final Verdict
Hardware PWM was the right call. The tone is perfectly clean, the CPU cost is basically zero, and it taught me something real about the chip: peripherals need clocks, and clocks need care. It's also a nice showcase of the frame design: the ultrasonic task writes the distance, the buzzer task reads it and writes its own status, and the alive task prints it all. No task calls another.
For a beginner project on a modern toolchain, the kernel's PWM driver (dtoverlay=pwm) is the safer
route, with no clock registers to get wrong. But if you want to really understand what that driver does for you,
writing RNG1 and DAT1 yourself is the best way to learn it.
Gotchas I hit, so you don't have to:
- No password, no clock. Every clock manager write needs
0x5Ain the top byte, or it's silently ignored. - Stop the clock and wait for BUSY before changing the divider, and put a timeout on that wait.
- Pause before you check BUSY. This one cost me an evening: without ~110 Β΅s after "clock off", BUSY isn't set yet, the divider changes on a running clock, and the PWM silently never starts, while every register reads back perfectly.
PWM_STAbit 9 tells the truth. - Set MSEN1. The default PWM mode spreads the pulses out: great for LEDs, terrible for a tone.
- Volume above 50 % duty isn't louder, it just heats the coil. Map 100 % volume to 50 % duty.
- Silence first, connect the pin last. Set DAT1 = 0 and start the PWM before switching GPIO18 to ALT5.
- Don't sleep through a whole beep period. A 10 ms tick with a phase counter keeps the alarm responsive.
- The headphone jack shares the PWM. If anything plays audio, they fight;
dtparam=audio=offends that. - Tune the tone to your buzzer. Its resonant frequency makes a huge difference in loudness.
Next up: wiring the LCD1602 over I2C, so the alarm can show the distance as well as shout it.
Related Videos
- Make Your Raspberry Pi SCREAM at Something Closer! Hardware PWM Buzzer in C (Parking Sensor Effect)
- Raspberry Pi Measure Distance to the MICROSECOND in C (HC-SR04 + GPIO Registers + pthreads)
- Wire the buzzer with a 2N3904 transistor
- Build the heartbeat thread and shared frame
- Build, deploy and debug on the Pi with one command (CMake presets + GDB)