sl-firmware 8f51390e43 feat: Add piezo buzzer melody driver (Issue #253)
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>
2026-03-02 12:51:42 -05:00

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;
}