feat: Piezo buzzer melody driver (Issue #253) #257
146
include/buzzer.h
Normal file
146
include/buzzer.h
Normal 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
293
src/buzzer.c
Normal 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
309
test/test_buzzer.c
Normal 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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user