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>
294 lines
9.1 KiB
C
294 lines
9.1 KiB
C
#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;
|
|
}
|