Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bricks/_common/sources.mk
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ PBIO_SRC_C = $(addprefix lib/pbio/,\
drv/reset/reset_nxt.c \
drv/reset/reset_stm32.c \
drv/resistor_ladder/resistor_ladder.c \
drv/sound/beep_sampled.c \
drv/sound/sound_ev3.c \
drv/sound/sound_nxt.c \
drv/sound/sound_stm32_hal_dac.c \
drv/stack/stack_embedded.c \
Expand Down
55 changes: 55 additions & 0 deletions lib/pbio/drv/sound/beep_sampled.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 The Pybricks Authors

// Converts beeps into an array of samples

#include <pbdrv/config.h>

#if PBDRV_CONFIG_SOUND_BEEP_SAMPLED

#include <stddef.h>
#include <stdint.h>

#include <pbdrv/sound.h>
#include <pbio/util.h>

static uint16_t waveform_data[128];

static void pbdrv_sound_generate_square_wave(uint16_t sample_attenuator) {
uint16_t lo_amplitude_value = INT16_MAX - sample_attenuator;
uint16_t hi_amplitude_value = sample_attenuator + INT16_MAX;

size_t i = 0;
for (; i < PBIO_ARRAY_SIZE(waveform_data) / 2; i++) {
waveform_data[i] = lo_amplitude_value;
}
for (; i < PBIO_ARRAY_SIZE(waveform_data); i++) {
waveform_data[i] = hi_amplitude_value;
}
}

// For 0 frequencies that are just flat lines.
static void pbdrv_sound_generate_line_wave(void) {
for (size_t i = 0; i < PBIO_ARRAY_SIZE(waveform_data); i++) {
waveform_data[i] = INT16_MAX;
}
}

void pbdrv_beep_start(uint32_t frequency, uint16_t sample_attenuator) {
if (frequency == 0) {
pbdrv_sound_generate_line_wave();
} else {
pbdrv_sound_generate_square_wave(sample_attenuator);
}

if (frequency < 64) {
frequency = 64;
}
if (frequency > 24000) {
frequency = 24000;
}

pbdrv_sound_start(&waveform_data[0], PBIO_ARRAY_SIZE(waveform_data), frequency * PBIO_ARRAY_SIZE(waveform_data));
}

#endif
135 changes: 135 additions & 0 deletions lib/pbio/drv/sound/sound_ev3.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 The Pybricks Authors

#include <pbdrv/config.h>

#if PBDRV_CONFIG_SOUND_EV3

#include <math.h>
#include <stdint.h>

#include <pbdrv/gpio.h>

#include <tiam1808/ehrpwm.h>
#include <tiam1808/hw/soc_AM1808.h>
#include <tiam1808/hw/hw_syscfg0_AM1808.h>
#include <tiam1808/hw/hw_types.h>
#include <tiam1808/psc.h>

#include "../drv/gpio/gpio_ev3.h"

// This module covers the frequency range from 64 Hz to 10 kHz.
// It uses a maximum duty cycle of 1/16, and it controls volume
// by shortening the duty cycle. The hardware has a resolution
// of 16 bits for the period counter, and we want 8 bits for the volume,
// so we use the following timebase division factors to make this work:
//
// 64 Hz - 900 Hz => /40
// 900 Hz - 8 kHz => /4
// 8 kHz - 10 kHz => /1

// Audio amplifier enable
static const pbdrv_gpio_t pin_sound_en = PBDRV_GPIO_EV3_PIN(13, 3, 0, 6, 15);
// Audio output pin
#define SYSCFG_PINMUX3_PINMUX3_7_4_GPIO0_0 0
static const pbdrv_gpio_t pin_audio = PBDRV_GPIO_EV3_PIN(3, 7, 4, 0, 0);

void pbdrv_sound_stop() {
// Force the output low
EHRPWMAQContSWForceOnB(SOC_EHRPWM_0_REGS, EHRPWM_AQCSFRC_CSFB_LOW, EHRPWM_AQSFRC_RLDCSF_IMMEDIATE);
// Clean up counter
HWREGH(SOC_EHRPWM_0_REGS + EHRPWM_TBCTL) |= EHRPWM_TBCTL_CTRMODE_STOPFREEZE;
EHRPWMWriteTBCount(SOC_EHRPWM_0_REGS, 0);
}

void pbdrv_beep_start(uint32_t frequency, uint16_t sample_attenuator) {
// Clamp the frequency into the supported range
if (frequency < 64) {
frequency = 64;
}
if (frequency > 10000) {
frequency = 10000;
}

// Clamp the volume into the supported range
if (sample_attenuator > INT16_MAX) {
sample_attenuator = INT16_MAX;
}
// Extract bits[14:7] for our 8-bit volume resolution
sample_attenuator >>= 7;

// Configure the timebase depending on which bucket the frequency is in.
// Don't use EHRPWMTimebaseClkConfig because its calculation algorithm
// isn't very good and is also tricky to fix.
uint32_t timebase_div;
if (frequency < 900) {
timebase_div = 40;
HWREGH(SOC_EHRPWM_0_REGS + EHRPWM_TBCTL) = (HWREGH(SOC_EHRPWM_0_REGS + EHRPWM_TBCTL) &
~(EHRPWM_TBCTL_CLKDIV | EHRPWM_TBCTL_HSPCLKDIV)) |
(EHRPWM_TBCTL_CLKDIV_DIVBY4 << EHRPWM_TBCTL_CLKDIV_SHIFT) |
(EHRPWM_TBCTL_HSPCLKDIV_DIVBY10 << EHRPWM_TBCTL_HSPCLKDIV_SHIFT);
} else if (frequency < 8000) {
timebase_div = 4;
HWREGH(SOC_EHRPWM_0_REGS + EHRPWM_TBCTL) = (HWREGH(SOC_EHRPWM_0_REGS + EHRPWM_TBCTL) &
~(EHRPWM_TBCTL_CLKDIV | EHRPWM_TBCTL_HSPCLKDIV)) |
(EHRPWM_TBCTL_CLKDIV_DIVBY4 << EHRPWM_TBCTL_CLKDIV_SHIFT) |
(EHRPWM_TBCTL_HSPCLKDIV_DIVBY1 << EHRPWM_TBCTL_HSPCLKDIV_SHIFT);
} else {
timebase_div = 1;
HWREGH(SOC_EHRPWM_0_REGS + EHRPWM_TBCTL) = (HWREGH(SOC_EHRPWM_0_REGS + EHRPWM_TBCTL) &
~(EHRPWM_TBCTL_CLKDIV | EHRPWM_TBCTL_HSPCLKDIV)) |
(EHRPWM_TBCTL_CLKDIV_DIVBY1 << EHRPWM_TBCTL_CLKDIV_SHIFT) |
(EHRPWM_TBCTL_HSPCLKDIV_DIVBY1 << EHRPWM_TBCTL_HSPCLKDIV_SHIFT);
}

// The way that this code controls volume by adjusting the duty cycle
// is not linear across the frequency range. For a basic beep driver,
// this empirical formula compensates well enough.
uint32_t freq_adj = powf(2, log10f(frequency) - 3) * 256;

uint32_t pwm_period = (SOC_EHRPWM_0_MODULE_FREQ / timebase_div + frequency / 2) / frequency;
uint32_t pwm_duty_cycle = (pwm_period * sample_attenuator / 256) / 16 * freq_adj / 256;

// Program PWM to generate a square wave of this frequency + duty cycle
EHRPWMLoadCMPB(SOC_EHRPWM_0_REGS, pwm_duty_cycle, true, 0, true);
EHRPWMPWMOpPeriodSet(SOC_EHRPWM_0_REGS, pwm_period, EHRPWM_COUNT_UP, true);

// Stop forcing output low
EHRPWMAQContSWForceOnB(SOC_EHRPWM_0_REGS, 0, EHRPWM_AQSFRC_RLDCSF_IMMEDIATE);
}

void pbdrv_sound_init() {
// Turn on EPWM
PSCModuleControl(SOC_PSC_1_REGS, HW_PSC_EHRPWM, PSC_POWERDOMAIN_ALWAYS_ON, PSC_MDCTL_NEXT_ENABLE);

// The stop function performs various initializations
pbdrv_sound_stop();

// Program EPWM to generate the desired wave shape
// Pulse goes high @ t=0
// Pulse goes low @ t=CMPB
EHRPWMConfigureAQActionOnB(
SOC_EHRPWM_0_REGS,
EHRPWM_AQCTLB_ZRO_EPWMXBHIGH,
EHRPWM_AQCTLB_PRD_DONOTHING,
EHRPWM_AQCTLB_CAU_DONOTHING,
EHRPWM_AQCTLB_CAD_DONOTHING,
EHRPWM_AQCTLB_CBU_EPWMXBLOW,
EHRPWM_AQCTLB_CBD_DONOTHING,
EHRPWM_AQSFRC_ACTSFB_DONOTHING
);
// Disable unused features
EHRPWMDBOutput(SOC_EHRPWM_0_REGS, EHRPWM_DBCTL_OUT_MODE_BYPASS);
EHRPWMChopperDisable(SOC_EHRPWM_0_REGS);
EHRPWMTZTripEventDisable(SOC_EHRPWM_0_REGS, false);
EHRPWMTZTripEventDisable(SOC_EHRPWM_0_REGS, true);

// Configure IO pin mode
pbdrv_gpio_alt(&pin_audio, SYSCFG_PINMUX3_PINMUX3_7_4_EPWM0B);
// Turn speaker amplifier on
// We turn the amplifier on and leave it turned on, because otherwise
// it will generate a popping sound whenever it is enabled.
pbdrv_gpio_out_high(&pin_sound_en);
}

#endif // PBDRV_CONFIG_SOUND_EV3
13 changes: 13 additions & 0 deletions lib/pbio/include/pbdrv/sound.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@

#if PBDRV_CONFIG_SOUND

/**
* Starts playing a square wave until pbdrv_sound_stop() is called.
*
* @param [in] frequency The frequency of the wave in Hz.
* @param [in] sample_attenuator The normalized attenuation to apply to get the requested volume.
*/
void pbdrv_beep_start(uint32_t frequency, uint16_t sample_attenuator);

#if PBDRV_CONFIG_SOUND_SAMPLED
/**
* Starts playing a sound repeatedly until pbdrv_sound_stop() is called.
*
Expand All @@ -25,6 +34,7 @@
* @param [in] sample_rate The sample rate of @p data in Hz.
*/
void pbdrv_sound_start(const uint16_t *data, uint32_t length, uint32_t sample_rate);
#endif

/**
* Stops any currently playing sound.
Expand All @@ -34,6 +44,9 @@ void pbdrv_sound_stop(void);

#else // PBDRV_CONFIG_SOUND

static inline void pbdrv_beep_start(uint32_t frequency, uint16_t sample_attenuator) {
}

static inline void pbdrv_sound_start(const uint16_t *data, uint32_t length, uint32_t sample_rate) {
}

Expand Down
4 changes: 4 additions & 0 deletions lib/pbio/platform/ev3/pbdrvconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
#define PBDRV_CONFIG_RESET (1)
#define PBDRV_CONFIG_RESET_EV3 (1)

#define PBDRV_CONFIG_SOUND (1)
#define PBDRV_CONFIG_SOUND_EV3 (1)
#define PBDRV_CONFIG_SOUND_DEFAULT_VOLUME 75

#define PBDRV_CONFIG_UART (1)
#define PBDRV_CONFIG_UART_DEBUG_FIRST_PORT (1)
#define PBDRV_CONFIG_UART_EV3 (1)
Expand Down
3 changes: 3 additions & 0 deletions lib/pbio/platform/nxt/pbdrvconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
#define PBDRV_CONFIG_RESET_NXT (1)

#define PBDRV_CONFIG_SOUND (1)
#define PBDRV_CONFIG_SOUND_DEFAULT_VOLUME 100
#define PBDRV_CONFIG_SOUND_SAMPLED (1)
#define PBDRV_CONFIG_SOUND_BEEP_SAMPLED (1)
#define PBDRV_CONFIG_SOUND_NXT (1)

#define PBDRV_CONFIG_STACK (1)
Expand Down
3 changes: 3 additions & 0 deletions lib/pbio/platform/prime_hub/pbdrvconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@
#define PBDRV_CONFIG_RESISTOR_LADDER_NUM_DEV (2)

#define PBDRV_CONFIG_SOUND (1)
#define PBDRV_CONFIG_SOUND_DEFAULT_VOLUME 100
#define PBDRV_CONFIG_SOUND_SAMPLED (1)
#define PBDRV_CONFIG_SOUND_BEEP_SAMPLED (1)
#define PBDRV_CONFIG_SOUND_STM32_HAL_DAC (1)

#define PBDRV_CONFIG_UART (1)
Expand Down
32 changes: 32 additions & 0 deletions lib/tiam1808/drivers/ehrpwm.c
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,38 @@ void EHRPWMPWMOpFreqSet(unsigned int baseAddr,
}


/**
* \brief This API configures the PWM Frequency/Period. The period count
* determines the period of the final output waveform.
* This function loads the precise period value specified.
*
* \param baseAddr Base Address of the PWM Module Registers.
* \param tbPeriod Timebase period.
*
* \param counterDir Direction of the counter(up, down, up-down)
* \param enableShadowWrite Whether write to Period register is to be shadowed
*
* \return None.
*
**/
void EHRPWMPWMOpPeriodSet(unsigned int baseAddr,
unsigned int tbPeriod,
unsigned int counterDir,
bool enableShadowWrite)
{
HWREGH(baseAddr + EHRPWM_TBCTL) = (HWREGH(baseAddr + EHRPWM_TBCTL) &
(~EHRPWM_PRD_LOAD_SHADOW_MASK)) | ((enableShadowWrite <<
EHRPWM_TBCTL_PRDLD_SHIFT) & EHRPWM_PRD_LOAD_SHADOW_MASK);

HWREGH(baseAddr + EHRPWM_TBCTL) = (HWREGH(baseAddr + EHRPWM_TBCTL) &
(~EHRPWM_COUNTER_MODE_MASK)) | ((counterDir <<
EHRPWM_TBCTL_CTRMODE_SHIFT) & EHRPWM_COUNTER_MODE_MASK);

HWREGH(baseAddr + EHRPWM_TBPRD) = (unsigned short)tbPeriod;

}


/**
* \brief This API configures emulation mode. This setting determines
* the behaviour of Timebase during emulation (debugging).
Expand Down
3 changes: 3 additions & 0 deletions lib/tiam1808/tiam1808/ehrpwm.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ void EHRPWMTimebaseClkConfig(unsigned int baseAddr, unsigned int tbClk,
void EHRPWMPWMOpFreqSet(unsigned int baseAddr, unsigned int tbClk,
unsigned int pwmFreq,unsigned int counterDir,
bool enableShadowWrite);
void EHRPWMPWMOpPeriodSet(unsigned int baseAddr, unsigned int tbPeriod,
unsigned int counterDir,
bool enableShadowWrite);
void EHRPWMTBEmulationModeSet(unsigned int baseAddr, unsigned int mode);
void EHRPWMTimebaseSyncEnable(unsigned int baseAddr, unsigned int tbPhsValue,
unsigned int phsCountDir);
Expand Down
42 changes: 2 additions & 40 deletions pybricks/common/pb_type_speaker.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ typedef struct {
uint16_t sample_attenuator;
} pb_type_Speaker_obj_t;

static uint16_t waveform_data[128];

static mp_obj_t pb_type_Speaker_volume(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
PB_PARSE_ARGS_METHOD(n_args, pos_args, kw_args,
pb_type_Speaker_obj_t, self,
Expand All @@ -63,44 +61,8 @@ static mp_obj_t pb_type_Speaker_volume(size_t n_args, const mp_obj_t *pos_args,
}
static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Speaker_volume_obj, 1, pb_type_Speaker_volume);

static void pb_type_Speaker_generate_square_wave(uint16_t sample_attenuator) {
uint16_t lo_amplitude_value = INT16_MAX - sample_attenuator;
uint16_t hi_amplitude_value = sample_attenuator + INT16_MAX;

size_t i = 0;
for (; i < MP_ARRAY_SIZE(waveform_data) / 2; i++) {
waveform_data[i] = lo_amplitude_value;
}
for (; i < MP_ARRAY_SIZE(waveform_data); i++) {
waveform_data[i] = hi_amplitude_value;
}
}

// For 0 frequencies that are just flat lines.
static void pb_type_Speaker_generate_line_wave(void) {
for (size_t i = 0; i < MP_ARRAY_SIZE(waveform_data); i++) {
waveform_data[i] = INT16_MAX;
}
}

static void pb_type_Speaker_start_beep(uint32_t frequency, uint16_t sample_attenuator) {
// TODO: allow other wave shapes - sine, triangle, sawtooth
// TODO: don't recreate waveform if it hasn't changed shape or volume

if (frequency == 0) {
pb_type_Speaker_generate_line_wave();
} else {
pb_type_Speaker_generate_square_wave(sample_attenuator);
}

if (frequency < 64) {
frequency = 64;
}
if (frequency > 24000) {
frequency = 24000;
}

pbdrv_sound_start(&waveform_data[0], MP_ARRAY_SIZE(waveform_data), frequency * MP_ARRAY_SIZE(waveform_data));
pbdrv_beep_start(frequency, sample_attenuator);
}

static void pb_type_Speaker_stop_beep(void) {
Expand All @@ -117,7 +79,7 @@ static mp_obj_t pb_type_Speaker_make_new(const mp_obj_type_t *type, size_t n_arg

// REVISIT: If a user creates two Speaker instances, this will reset the volume settings for both.
// If done only once per singleton, however, altered volume settings would be persisted between program runs.
self->volume = 100;
self->volume = PBDRV_CONFIG_SOUND_DEFAULT_VOLUME;
self->sample_attenuator = INT16_MAX;

return MP_OBJ_FROM_PTR(self);
Expand Down
Loading