feat: Piezo buzzer melody driver (Issue #253) #257

Merged
sl-jetson merged 1 commits from sl-firmware/issue-253-buzzer into main 2026-03-02 13:22:22 -05:00
3 changed files with 748 additions and 0 deletions
Showing only changes of commit 8f51390e43 - Show all commits

146
include/buzzer.h Normal file
View File

@ -0,0 +1,146 @@
#ifndef BUZZER_H
#define BUZZER_H
#include <stdint.h>
#include <stdbool.h>
/*
* buzzer.h Piezo buzzer melody driver (Issue #253)
*
* STM32F722 driver for piezo buzzer on PA8 using TIM1 PWM.
* Plays predefined melodies and tones with non-blocking queue.
*
* Pin: PA8 (TIM1_CH1, alternate function AF1)
* PWM Frequency: 1kHz-5kHz base, modulated for melody
* Volume: Controlled via PWM duty cycle (50-100%)
*/
/* Musical note frequencies (Hz) — standard equal temperament */
typedef enum {
NOTE_REST = 0, /* Silence */
NOTE_C4 = 262, /* Middle C */
NOTE_D4 = 294,
NOTE_E4 = 330,
NOTE_F4 = 349,
NOTE_G4 = 392,
NOTE_A4 = 440, /* A4 concert pitch */
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;
/* Note duration (milliseconds) */
typedef enum {
DURATION_WHOLE = 2000, /* 4 beats @ 120 BPM */
DURATION_HALF = 1000, /* 2 beats */
DURATION_QUARTER = 500, /* 1 beat */
DURATION_EIGHTH = 250, /* 1/2 beat */
DURATION_SIXTEENTH = 125, /* 1/4 beat */
} Duration;
/* Melody sequence: array of (note, duration) pairs, terminated with {0, 0} */
typedef struct {
Note frequency;
Duration duration_ms;
} MelodyNote;
/* Predefined melodies */
typedef enum {
MELODY_STARTUP, /* Startup jingle: ascending tones */
MELODY_LOW_BATTERY, /* Warning: two descending beeps */
MELODY_ERROR, /* Alert: rapid error beep */
MELODY_DOCKING_COMPLETE /* Success: cheerful chime */
} MelodyType;
/* Get predefined melody sequence */
extern const MelodyNote melody_startup[];
extern const MelodyNote melody_low_battery[];
extern const MelodyNote melody_error[];
extern const MelodyNote melody_docking_complete[];
/*
* buzzer_init()
*
* Initialize buzzer driver:
* - PA8 as TIM1_CH1 PWM output
* - TIM1 configured for 1kHz base frequency
* - PWM duty cycle for volume control
*/
void buzzer_init(void);
/*
* buzzer_play_melody(melody_type)
*
* Queue a predefined melody for playback.
* Non-blocking: returns immediately, melody plays asynchronously.
* Multiple calls queue melodies in sequence.
*
* Supported melodies:
* - MELODY_STARTUP: 2-3 second jingle on power-up
* - MELODY_LOW_BATTERY: 1 second warning
* - MELODY_ERROR: 0.5 second alert beep
* - MELODY_DOCKING_COMPLETE: 1-1.5 second success chime
*
* Returns: true if queued, false if queue full
*/
bool buzzer_play_melody(MelodyType melody_type);
/*
* buzzer_play_custom(notes)
*
* Queue a custom melody sequence.
* Notes array must be terminated with {NOTE_REST, 0}.
* Useful for error codes or custom notifications.
*
* Returns: true if queued, false if queue full
*/
bool buzzer_play_custom(const MelodyNote *notes);
/*
* buzzer_play_tone(frequency, duration_ms)
*
* Queue a simple single tone.
* Useful for beeps and alerts.
*
* Arguments:
* - frequency: Note frequency (Hz), 0 for silence
* - duration_ms: Tone duration in milliseconds
*
* Returns: true if queued, false if queue full
*/
bool buzzer_play_tone(uint16_t frequency, uint16_t duration_ms);
/*
* buzzer_stop()
*
* Stop current playback and clear queue.
* Buzzer returns to silence immediately.
*/
void buzzer_stop(void);
/*
* buzzer_is_playing()
*
* Returns: true if melody/tone is currently playing, false if idle
*/
bool buzzer_is_playing(void);
/*
* buzzer_tick(now_ms)
*
* Update function called periodically (recommended: every 10ms in main loop).
* Manages melody timing and PWM frequency transitions.
* Must be called regularly for non-blocking operation.
*
* Arguments:
* - now_ms: current time in milliseconds (from HAL_GetTick() or similar)
*/
void buzzer_tick(uint32_t now_ms);
#endif /* BUZZER_H */

293
src/buzzer.c Normal file
View File

@ -0,0 +1,293 @@
#include "buzzer.h"
#include "stm32f7xx_hal.h"
#include "config.h"
#include <string.h>
/* ================================================================
* Buzzer Hardware Configuration
* ================================================================ */
#define BUZZER_PIN GPIO_PIN_8
#define BUZZER_PORT GPIOA
#define BUZZER_TIM TIM1
#define BUZZER_TIM_CHANNEL TIM_CHANNEL_1
#define BUZZER_BASE_FREQ_HZ 1000 /* Base PWM frequency (1kHz) */
/* ================================================================
* Predefined Melodies
* ================================================================ */
/* Startup jingle: C-E-G ascending pattern */
const MelodyNote melody_startup[] = {
{NOTE_C4, DURATION_QUARTER},
{NOTE_E4, DURATION_QUARTER},
{NOTE_G4, DURATION_QUARTER},
{NOTE_C5, DURATION_HALF},
{NOTE_REST, 0} /* Terminator */
};
/* Low battery warning: two descending beeps */
const MelodyNote melody_low_battery[] = {
{NOTE_A5, DURATION_EIGHTH},
{NOTE_REST, DURATION_EIGHTH},
{NOTE_A5, DURATION_EIGHTH},
{NOTE_REST, DURATION_EIGHTH},
{NOTE_F5, DURATION_EIGHTH},
{NOTE_REST, DURATION_EIGHTH},
{NOTE_F5, DURATION_EIGHTH},
{NOTE_REST, 0}
};
/* Error alert: rapid repeating tone */
const MelodyNote melody_error[] = {
{NOTE_E5, DURATION_SIXTEENTH},
{NOTE_REST, DURATION_SIXTEENTH},
{NOTE_E5, DURATION_SIXTEENTH},
{NOTE_REST, DURATION_SIXTEENTH},
{NOTE_E5, DURATION_SIXTEENTH},
{NOTE_REST, DURATION_SIXTEENTH},
{NOTE_REST, 0}
};
/* Docking complete: cheerful ascending chime */
const MelodyNote melody_docking_complete[] = {
{NOTE_C4, DURATION_EIGHTH},
{NOTE_E4, DURATION_EIGHTH},
{NOTE_G4, DURATION_EIGHTH},
{NOTE_C5, DURATION_QUARTER},
{NOTE_REST, DURATION_QUARTER},
{NOTE_G4, DURATION_EIGHTH},
{NOTE_C5, DURATION_HALF},
{NOTE_REST, 0}
};
/* ================================================================
* Melody Queue
* ================================================================ */
#define MELODY_QUEUE_SIZE 4
typedef struct {
const MelodyNote *notes; /* Melody sequence pointer */
uint16_t note_index; /* Current note in sequence */
uint32_t note_start_ms; /* When current note started */
uint32_t note_duration_ms; /* Duration of current note */
uint16_t current_frequency; /* Current tone frequency (Hz) */
bool is_custom; /* Is this a custom melody? */
} MelodyPlayback;
typedef struct {
MelodyPlayback queue[MELODY_QUEUE_SIZE];
uint8_t write_index;
uint8_t read_index;
uint8_t count;
} MelodyQueue;
static MelodyQueue s_queue = {0};
static MelodyPlayback s_current = {0};
static uint32_t s_last_tick_ms = 0;
/* ================================================================
* Hardware Initialization
* ================================================================ */
void buzzer_init(void)
{
/* Enable GPIO and timer clocks */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_TIM1_CLK_ENABLE();
/* Configure PA8 as TIM1_CH1 PWM output */
GPIO_InitTypeDef gpio_init = {0};
gpio_init.Pin = BUZZER_PIN;
gpio_init.Mode = GPIO_MODE_AF_PP;
gpio_init.Pull = GPIO_NOPULL;
gpio_init.Speed = GPIO_SPEED_HIGH;
gpio_init.Alternate = GPIO_AF1_TIM1;
HAL_GPIO_Init(BUZZER_PORT, &gpio_init);
/* Configure TIM1 for PWM:
* Clock: 216MHz / PSC = output frequency
* For 1kHz base frequency: PSC = 216, ARR = 1000
* Duty cycle = CCR / ARR (e.g., 500/1000 = 50%)
*/
TIM_HandleTypeDef htim1 = {0};
htim1.Instance = BUZZER_TIM;
htim1.Init.Prescaler = 216 - 1; /* 216MHz / 216 = 1MHz clock */
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = (1000000 / BUZZER_BASE_FREQ_HZ) - 1; /* 1kHz = 1000 counts */
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;
HAL_TIM_PWM_Init(&htim1);
/* Configure PWM on CH1: 50% duty cycle initially (silence will be 0%) */
TIM_OC_InitTypeDef oc_init = {0};
oc_init.OCMode = TIM_OCMODE_PWM1;
oc_init.Pulse = 0; /* Start at 0% duty (silence) */
oc_init.OCPolarity = TIM_OCPOLARITY_HIGH;
oc_init.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim1, &oc_init, BUZZER_TIM_CHANNEL);
/* Start PWM generation */
HAL_TIM_PWM_Start(BUZZER_TIM, BUZZER_TIM_CHANNEL);
/* Initialize queue */
memset(&s_queue, 0, sizeof(s_queue));
memset(&s_current, 0, sizeof(s_current));
s_last_tick_ms = 0;
}
/* ================================================================
* Public API
* ================================================================ */
bool buzzer_play_melody(MelodyType melody_type)
{
const MelodyNote *notes = NULL;
switch (melody_type) {
case MELODY_STARTUP:
notes = melody_startup;
break;
case MELODY_LOW_BATTERY:
notes = melody_low_battery;
break;
case MELODY_ERROR:
notes = melody_error;
break;
case MELODY_DOCKING_COMPLETE:
notes = melody_docking_complete;
break;
default:
return false;
}
return buzzer_play_custom(notes);
}
bool buzzer_play_custom(const MelodyNote *notes)
{
if (!notes || s_queue.count >= MELODY_QUEUE_SIZE) {
return false;
}
MelodyPlayback *playback = &s_queue.queue[s_queue.write_index];
memset(playback, 0, sizeof(*playback));
playback->notes = notes;
playback->note_index = 0;
playback->is_custom = true;
s_queue.write_index = (s_queue.write_index + 1) % MELODY_QUEUE_SIZE;
s_queue.count++;
return true;
}
bool buzzer_play_tone(uint16_t frequency, uint16_t duration_ms)
{
if (s_queue.count >= MELODY_QUEUE_SIZE) {
return false;
}
/* Create a simple 2-note melody: tone + rest */
static MelodyNote temp_notes[3];
temp_notes[0].frequency = frequency;
temp_notes[0].duration_ms = duration_ms;
temp_notes[1].frequency = NOTE_REST;
temp_notes[1].duration_ms = 0;
MelodyPlayback *playback = &s_queue.queue[s_queue.write_index];
memset(playback, 0, sizeof(*playback));
playback->notes = temp_notes;
playback->note_index = 0;
playback->is_custom = true;
s_queue.write_index = (s_queue.write_index + 1) % MELODY_QUEUE_SIZE;
s_queue.count++;
return true;
}
void buzzer_stop(void)
{
/* Clear queue and current playback */
memset(&s_queue, 0, sizeof(s_queue));
memset(&s_current, 0, sizeof(s_current));
/* Silence buzzer (0% duty cycle) */
TIM1->CCR1 = 0;
}
bool buzzer_is_playing(void)
{
return (s_current.notes != NULL) || (s_queue.count > 0);
}
/* ================================================================
* Timer Update and PWM Frequency Control
* ================================================================ */
static void buzzer_set_frequency(uint16_t frequency)
{
if (frequency == 0) {
/* Silence: 0% duty cycle */
TIM1->CCR1 = 0;
return;
}
/* Set PWM frequency and 50% duty cycle
* TIM1 clock: 1MHz (after prescaler)
* ARR = 1MHz / frequency
* CCR1 = ARR / 2 (50% duty)
*/
uint32_t arr = (1000000 / frequency);
if (arr > 65535) arr = 65535; /* Clamp to 16-bit */
TIM1->ARR = arr - 1;
TIM1->CCR1 = arr / 2; /* 50% duty cycle for all tones */
}
void buzzer_tick(uint32_t now_ms)
{
/* Check if current note has finished */
if (s_current.notes != NULL) {
uint32_t elapsed = now_ms - s_current.note_start_ms;
if (elapsed >= s_current.note_duration_ms) {
/* Move to next note */
s_current.note_index++;
if (s_current.notes[s_current.note_index].duration_ms == 0) {
/* End of melody sequence */
s_current.notes = NULL;
buzzer_set_frequency(0);
/* Start next queued melody if available */
if (s_queue.count > 0) {
s_current = s_queue.queue[s_queue.read_index];
s_queue.read_index = (s_queue.read_index + 1) % MELODY_QUEUE_SIZE;
s_queue.count--;
s_current.note_start_ms = now_ms;
s_current.note_duration_ms = s_current.notes[0].duration_ms;
buzzer_set_frequency(s_current.notes[0].frequency);
}
} else {
/* Play next note */
s_current.note_start_ms = now_ms;
s_current.note_duration_ms = s_current.notes[s_current.note_index].duration_ms;
uint16_t frequency = s_current.notes[s_current.note_index].frequency;
buzzer_set_frequency(frequency);
}
}
} else if (s_queue.count > 0 && s_current.notes == NULL) {
/* Start first queued melody */
s_current = s_queue.queue[s_queue.read_index];
s_queue.read_index = (s_queue.read_index + 1) % MELODY_QUEUE_SIZE;
s_queue.count--;
s_current.note_start_ms = now_ms;
s_current.note_duration_ms = s_current.notes[0].duration_ms;
buzzer_set_frequency(s_current.notes[0].frequency);
}
s_last_tick_ms = now_ms;
}

309
test/test_buzzer.c Normal file
View File

@ -0,0 +1,309 @@
/*
* 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;
}