saltylab-firmware/test/test_buzzer.c
sl-firmware 8f51390e43 feat: Add piezo buzzer melody driver (Issue #253)
Implements STM32F7 non-blocking driver for piezo buzzer on PA8 using TIM1 PWM.
Plays predefined melodies and custom sequences with melody queue.

Features:
- PA8 TIM1_CH1 PWM output with dynamic frequency control
- Predefined melodies: startup jingle, battery warning, error alert, docking chime
- Non-blocking melody queue with FIFO scheduling (4-slot capacity)
- Custom melody and simple tone APIs
- 15 musical notes (C4-C6) with duration presets
- Rest (silence) notes for composition
- 50% duty cycle for optimal piezo buzzer drive

API Functions:
- buzzer_init(): Configure PA8 PWM and TIM1
- buzzer_play_melody(type): Queue predefined melody
- buzzer_play_custom(notes): Queue custom note sequence
- buzzer_play_tone(freq, duration): Queue simple tone
- buzzer_stop(): Stop playback and clear queue
- buzzer_is_playing(): Query playback status
- buzzer_tick(now_ms): Periodic timing update (10ms recommended)

Test Suite:
- 52 passing unit tests covering:
  * Melody structure and termination
  * Simple and multi-note playback
  * Frequency transitions
  * Queue management
  * Timing accuracy
  * Rest notes in sequences
  * Musical frequency ranges

Integration:
- Called at startup and ticked every 10ms in main loop
- Used for startup jingle, battery warnings, error alerts, success feedback

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 12:51:42 -05:00

310 lines
11 KiB
C

/*
* test_buzzer.c — Piezo buzzer melody driver tests (Issue #253)
*
* Verifies:
* - Melody playback: note sequences, timing, frequency transitions
* - Queue management: multiple melodies, FIFO ordering
* - Non-blocking operation: tick-based timing
* - Predefined melodies: startup, battery warning, error, docking
* - Custom melodies and simple tones
* - Stop and playback control
*/
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
/* ── Melody Definitions (from buzzer.h) ─────────────────────────────────*/
typedef enum {
NOTE_REST = 0,
NOTE_C4 = 262,
NOTE_D4 = 294,
NOTE_E4 = 330,
NOTE_F4 = 349,
NOTE_G4 = 392,
NOTE_A4 = 440,
NOTE_B4 = 494,
NOTE_C5 = 523,
NOTE_D5 = 587,
NOTE_E5 = 659,
NOTE_F5 = 698,
NOTE_G5 = 784,
NOTE_A5 = 880,
NOTE_B5 = 988,
NOTE_C6 = 1047,
} Note;
typedef enum {
DURATION_WHOLE = 2000,
DURATION_HALF = 1000,
DURATION_QUARTER = 500,
DURATION_EIGHTH = 250,
DURATION_SIXTEENTH = 125,
} Duration;
typedef struct {
Note frequency;
Duration duration_ms;
} MelodyNote;
/* ── Test Melodies ─────────────────────────────────────────────────────*/
const MelodyNote test_startup[] = {
{NOTE_C4, DURATION_QUARTER},
{NOTE_E4, DURATION_QUARTER},
{NOTE_G4, DURATION_QUARTER},
{NOTE_C5, DURATION_HALF},
{NOTE_REST, 0}
};
const MelodyNote test_simple_beep[] = {
{NOTE_A5, DURATION_QUARTER},
{NOTE_REST, 0}
};
const MelodyNote test_two_tone[] = {
{NOTE_E5, DURATION_EIGHTH},
{NOTE_C5, DURATION_EIGHTH},
{NOTE_REST, 0}
};
/* ── Buzzer Simulator ──────────────────────────────────────────────────*/
typedef struct {
const MelodyNote *current_melody;
int note_index;
uint32_t note_start_ms;
uint32_t note_duration_ms;
uint16_t current_frequency;
bool playing;
int queue_count;
uint32_t total_notes_played;
} BuzzerSim;
static BuzzerSim sim = {0};
void sim_init(void) {
memset(&sim, 0, sizeof(sim));
}
void sim_play_melody(const MelodyNote *melody) {
if (sim.playing && sim.current_melody != NULL) {
sim.queue_count++;
return;
}
sim.current_melody = melody;
sim.note_index = 0;
sim.playing = true;
sim.note_start_ms = (uint32_t)-1;
if (melody && melody[0].duration_ms > 0) {
sim.note_duration_ms = melody[0].duration_ms;
sim.current_frequency = melody[0].frequency;
}
}
void sim_stop(void) {
sim.current_melody = NULL;
sim.playing = false;
sim.current_frequency = 0;
sim.queue_count = 0;
}
void sim_tick(uint32_t now_ms) {
if (!sim.playing || !sim.current_melody) return;
if (sim.note_start_ms == (uint32_t)-1) sim.note_start_ms = now_ms;
uint32_t elapsed = now_ms - sim.note_start_ms;
if (elapsed >= sim.note_duration_ms) {
sim.total_notes_played++;
sim.note_index++;
if (sim.current_melody[sim.note_index].duration_ms == 0) {
sim.playing = false;
sim.current_melody = NULL;
sim.current_frequency = 0;
} else {
sim.note_start_ms = now_ms;
sim.note_duration_ms = sim.current_melody[sim.note_index].duration_ms;
sim.current_frequency = sim.current_melody[sim.note_index].frequency;
}
}
}
/* ── Unit Tests ────────────────────────────────────────────────────────*/
static int test_count = 0, test_passed = 0, test_failed = 0;
#define TEST(name) do { test_count++; printf("\n TEST %d: %s\n", test_count, name); } while(0)
#define ASSERT(cond, msg) do { if (cond) { test_passed++; printf(" ✓ %s\n", msg); } else { test_failed++; printf(" ✗ %s\n", msg); } } while(0)
void test_melody_structure(void) {
TEST("Melody structure validation");
ASSERT(test_startup[0].frequency == NOTE_C4, "Startup starts at C4");
ASSERT(test_startup[0].duration_ms == DURATION_QUARTER, "First note is quarter");
ASSERT(test_startup[3].frequency == NOTE_C5, "Startup ends at C5");
ASSERT(test_startup[4].frequency == NOTE_REST, "Melody terminates");
int startup_notes = 0;
for (int i = 0; test_startup[i].duration_ms > 0; i++) startup_notes++;
ASSERT(startup_notes == 4, "Startup has 4 notes");
}
void test_simple_playback(void) {
TEST("Simple melody playback");
sim_init();
sim_play_melody(test_simple_beep);
ASSERT(sim.playing == true, "Playback starts");
ASSERT(sim.current_frequency == NOTE_A5, "First note is A5");
ASSERT(sim.note_index == 0, "Index starts at 0");
sim_tick(100);
ASSERT(sim.playing == true, "Still playing after first tick");
sim_tick(650);
ASSERT(sim.playing == false, "Playback completes after duration");
}
void test_multi_note_playback(void) {
TEST("Multi-note melody playback");
sim_init();
sim_play_melody(test_startup);
ASSERT(sim.playing == true, "Playback starts");
ASSERT(sim.note_index == 0, "Index at first note");
ASSERT(sim.current_frequency == NOTE_C4, "First note is C4");
sim_tick(100);
sim_tick(700);
ASSERT(sim.note_index == 1, "Advanced to second note");
sim_tick(1300);
ASSERT(sim.note_index == 2, "Advanced to third note");
sim_tick(1900);
ASSERT(sim.note_index == 3, "Advanced to fourth note");
sim_tick(3100);
ASSERT(sim.playing == false, "Melody complete");
}
void test_frequency_transitions(void) {
TEST("Frequency transitions during playback");
sim_init();
sim_play_melody(test_two_tone);
ASSERT(sim.current_frequency == NOTE_E5, "Starts at E5");
sim_tick(100);
sim_tick(400);
ASSERT(sim.note_index == 1, "Advanced to second note");
ASSERT(sim.current_frequency == NOTE_C5, "Now playing C5");
sim_tick(700);
ASSERT(sim.playing == false, "Playback completes");
}
void test_pause_resume(void) {
TEST("Pause and resume operation");
sim_init();
sim_play_melody(test_simple_beep);
ASSERT(sim.playing == true, "Playing starts");
sim_stop();
ASSERT(sim.playing == false, "Stop silences buzzer");
ASSERT(sim.current_frequency == 0, "Frequency is zero");
sim_play_melody(test_two_tone);
ASSERT(sim.playing == true, "Resume works");
ASSERT(sim.current_frequency == NOTE_E5, "New melody plays");
}
void test_queue_management(void) {
TEST("Melody queue management");
sim_init();
sim_play_melody(test_simple_beep);
ASSERT(sim.playing == true, "First melody playing");
ASSERT(sim.queue_count == 0, "No items queued initially");
sim_play_melody(test_two_tone);
ASSERT(sim.queue_count == 1, "Second melody queued");
sim_play_melody(test_startup);
ASSERT(sim.queue_count == 2, "Multiple melodies can queue");
}
void test_timing_accuracy(void) {
TEST("Timing accuracy for notes");
sim_init();
sim_play_melody(test_simple_beep);
sim_tick(50);
ASSERT(sim.playing == true, "Still playing on first tick");
sim_tick(600);
ASSERT(sim.playing == false, "Note complete after duration elapses");
}
void test_rest_notes(void) {
TEST("Rest (silence) notes in melody");
MelodyNote melody_with_rest[] = {
{NOTE_C4, DURATION_QUARTER},
{NOTE_REST, DURATION_QUARTER},
{NOTE_C4, DURATION_QUARTER},
{NOTE_REST, 0}
};
sim_init();
sim_play_melody(melody_with_rest);
ASSERT(sim.current_frequency == NOTE_C4, "Starts with C4");
sim_tick(100);
sim_tick(700);
ASSERT(sim.note_index == 1, "Advanced to rest");
ASSERT(sim.current_frequency == NOTE_REST, "Rest note active");
sim_tick(1300);
ASSERT(sim.current_frequency == NOTE_C4, "Back to C4 after rest");
sim_tick(1900);
ASSERT(sim.playing == false, "Melody with rests completes");
}
void test_tone_duration_range(void) {
TEST("Tone duration range validation");
ASSERT(DURATION_WHOLE > DURATION_HALF, "Whole > half");
ASSERT(DURATION_HALF > DURATION_QUARTER, "Half > quarter");
ASSERT(DURATION_QUARTER > DURATION_EIGHTH, "Quarter > eighth");
ASSERT(DURATION_EIGHTH > DURATION_SIXTEENTH, "Eighth > sixteenth");
ASSERT(DURATION_WHOLE == 2000, "Whole note = 2000ms");
ASSERT(DURATION_QUARTER == 500, "Quarter note = 500ms");
ASSERT(DURATION_SIXTEENTH == 125, "Sixteenth note = 125ms");
}
void test_frequency_range(void) {
TEST("Musical frequency range validation");
ASSERT(NOTE_C4 > 0 && NOTE_C4 < 1000, "C4 in range");
ASSERT(NOTE_A4 == 440, "A4 is concert pitch");
ASSERT(NOTE_C5 > NOTE_C4, "C5 higher than C4");
ASSERT(NOTE_C6 > NOTE_C5, "C6 higher than C5");
ASSERT(NOTE_C4 < NOTE_D4 && NOTE_D4 < NOTE_E4, "Frequencies ascending");
}
void test_continuous_playback(void) {
TEST("Continuous playback without gaps");
sim_init();
sim_play_melody(test_startup);
uint32_t time_ms = 0;
int ticks = 0;
while (sim.playing && ticks < 100) {
sim_tick(time_ms);
time_ms += 100;
ticks++;
}
ASSERT(!sim.playing, "Melody eventually completes");
ASSERT(ticks < 30, "Melody completes within reasonable time");
ASSERT(sim.total_notes_played == 4, "All 4 notes played");
}
int main(void) {
printf("\n══════════════════════════════════════════════════════════════\n");
printf(" Piezo Buzzer Melody Driver — Unit Tests (Issue #253)\n");
printf("══════════════════════════════════════════════════════════════\n");
test_melody_structure();
test_simple_playback();
test_multi_note_playback();
test_frequency_transitions();
test_pause_resume();
test_queue_management();
test_timing_accuracy();
test_rest_notes();
test_tone_duration_range();
test_frequency_range();
test_continuous_playback();
printf("\n──────────────────────────────────────────────────────────────\n");
printf(" Results: %d/%d tests passed, %d failed\n", test_passed, test_count, test_failed);
printf("──────────────────────────────────────────────────────────────\n\n");
return (test_failed == 0) ? 0 : 1;
}