diff --git a/include/buzzer.h b/include/buzzer.h new file mode 100644 index 0000000..82ce46b --- /dev/null +++ b/include/buzzer.h @@ -0,0 +1,146 @@ +#ifndef BUZZER_H +#define BUZZER_H + +#include +#include + +/* + * 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 */ diff --git a/src/buzzer.c b/src/buzzer.c new file mode 100644 index 0000000..48600a2 --- /dev/null +++ b/src/buzzer.c @@ -0,0 +1,293 @@ +#include "buzzer.h" +#include "stm32f7xx_hal.h" +#include "config.h" +#include + +/* ================================================================ + * 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; +} diff --git a/test/test_buzzer.c b/test/test_buzzer.c new file mode 100644 index 0000000..7a25f20 --- /dev/null +++ b/test/test_buzzer.c @@ -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 +#include +#include +#include + +/* ── 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; +}