diff --git a/bricks/_common/sources.mk b/bricks/_common/sources.mk index 7d0c5dedd..4448b7f08 100644 --- a/bricks/_common/sources.mk +++ b/bricks/_common/sources.mk @@ -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 \ diff --git a/lib/pbio/drv/sound/beep_sampled.c b/lib/pbio/drv/sound/beep_sampled.c new file mode 100644 index 000000000..9b0e8d68d --- /dev/null +++ b/lib/pbio/drv/sound/beep_sampled.c @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors + +// Converts beeps into an array of samples + +#include + +#if PBDRV_CONFIG_SOUND_BEEP_SAMPLED + +#include +#include + +#include +#include + +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 diff --git a/lib/pbio/drv/sound/sound_ev3.c b/lib/pbio/drv/sound/sound_ev3.c new file mode 100644 index 000000000..f4b94d0e7 --- /dev/null +++ b/lib/pbio/drv/sound/sound_ev3.c @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors + +#include + +#if PBDRV_CONFIG_SOUND_EV3 + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#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 diff --git a/lib/pbio/include/pbdrv/sound.h b/lib/pbio/include/pbdrv/sound.h index c4a6239c3..8b88af044 100644 --- a/lib/pbio/include/pbdrv/sound.h +++ b/lib/pbio/include/pbdrv/sound.h @@ -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. * @@ -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. @@ -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) { } diff --git a/lib/pbio/platform/ev3/pbdrvconfig.h b/lib/pbio/platform/ev3/pbdrvconfig.h index 89cc99731..233001610 100644 --- a/lib/pbio/platform/ev3/pbdrvconfig.h +++ b/lib/pbio/platform/ev3/pbdrvconfig.h @@ -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) diff --git a/lib/pbio/platform/nxt/pbdrvconfig.h b/lib/pbio/platform/nxt/pbdrvconfig.h index d901b70fe..72c635b53 100644 --- a/lib/pbio/platform/nxt/pbdrvconfig.h +++ b/lib/pbio/platform/nxt/pbdrvconfig.h @@ -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) diff --git a/lib/pbio/platform/prime_hub/pbdrvconfig.h b/lib/pbio/platform/prime_hub/pbdrvconfig.h index c6717d4d4..61c6d5eb6 100644 --- a/lib/pbio/platform/prime_hub/pbdrvconfig.h +++ b/lib/pbio/platform/prime_hub/pbdrvconfig.h @@ -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) diff --git a/lib/tiam1808/drivers/ehrpwm.c b/lib/tiam1808/drivers/ehrpwm.c index 59646b963..e159125d4 100644 --- a/lib/tiam1808/drivers/ehrpwm.c +++ b/lib/tiam1808/drivers/ehrpwm.c @@ -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). diff --git a/lib/tiam1808/tiam1808/ehrpwm.h b/lib/tiam1808/tiam1808/ehrpwm.h index 08fa40946..a14ad7fc4 100644 --- a/lib/tiam1808/tiam1808/ehrpwm.h +++ b/lib/tiam1808/tiam1808/ehrpwm.h @@ -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); diff --git a/pybricks/common/pb_type_speaker.c b/pybricks/common/pb_type_speaker.c index 676677995..fcefea41f 100644 --- a/pybricks/common/pb_type_speaker.c +++ b/pybricks/common/pb_type_speaker.c @@ -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, @@ -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) { @@ -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); diff --git a/pybricks/hubs/pb_type_ev3brick.c b/pybricks/hubs/pb_type_ev3brick.c index f4444326c..b8d5f29dd 100644 --- a/pybricks/hubs/pb_type_ev3brick.c +++ b/pybricks/hubs/pb_type_ev3brick.c @@ -20,6 +20,7 @@ typedef struct _hubs_EV3Brick_obj_t { mp_obj_t buttons; mp_obj_t light; mp_obj_t screen; + mp_obj_t speaker; mp_obj_t system; } hubs_EV3Brick_obj_t; @@ -53,6 +54,7 @@ static mp_obj_t hubs_EV3Brick_make_new(const mp_obj_type_t *type, size_t n_args, self->buttons = pb_type_Keypad_obj_new(pb_type_ev3brick_button_pressed); self->light = common_ColorLight_internal_obj_new(pbsys_status_light_main); self->screen = pb_type_Image_display_obj_new(); + self->speaker = mp_call_function_0(MP_OBJ_FROM_PTR(&pb_type_Speaker)); self->system = MP_OBJ_FROM_PTR(&pb_type_System); return MP_OBJ_FROM_PTR(self); @@ -63,6 +65,7 @@ static const pb_attr_dict_entry_t hubs_EV3Brick_attr_dict[] = { PB_DEFINE_CONST_ATTR_RO(MP_QSTR_buttons, hubs_EV3Brick_obj_t, buttons), PB_DEFINE_CONST_ATTR_RO(MP_QSTR_light, hubs_EV3Brick_obj_t, light), PB_DEFINE_CONST_ATTR_RO(MP_QSTR_screen, hubs_EV3Brick_obj_t, screen), + PB_DEFINE_CONST_ATTR_RO(MP_QSTR_speaker, hubs_EV3Brick_obj_t, speaker), PB_DEFINE_CONST_ATTR_RO(MP_QSTR_system, hubs_EV3Brick_obj_t, system), PB_ATTR_DICT_SENTINEL };