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>
310 lines
11 KiB
C
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;
|
|
}
|