@ -1,29 +1,15 @@
/* Copyright 2020 Jack Humbert
* Copyright 2020 JohSchneider
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 2 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program . If not , see < http : //www.gnu.org/licenses/>.
*/
/*
Audio Driver : PWM
the duty - cycle is always kept at 50 % , and the pwm - period is adjusted to match the frequency of a note to be played back .
this driver uses the chibios - PWM system to produce a square - wave on specific output pins that are connected to the PWM hardware .
The hardware directly toggles the pin via its alternate function . see your MCUs data - sheet for which pin can be driven by what timer - looking for TIMx_CHy and the corresponding alternate function .
*/
// Copyright 2022 Stefan Kerkmann
// Copyright 2020 Jack Humbert
// Copyright 2020 JohSchneider
// SPDX-License-Identifier: GPL-2.0-or-later
// Audio Driver: PWM the duty-cycle is always kept at 50%, and the pwm-period is
// adjusted to match the frequency of a note to be played back. This driver uses
// the chibios-PWM system to produce a square-wave on specific output pins that
// are connected to the PWM hardware. The hardware directly toggles the pin via
// its alternate function. see your MCUs data-sheet for which pin can be driven
// by what timer - looking for TIMx_CHy and the corresponding alternate
// function.
# include "audio.h"
# include "ch.h"
@ -33,53 +19,36 @@ The hardware directly toggles the pin via its alternate function. see your MCUs
# error "Audio feature enabled, but no pin selected - see docs / feature_audio under the ARM PWM settings"
# endif
# if !defined(AUDIO_PWM_COUNTER_FREQUENCY)
# define AUDIO_PWM_COUNTER_FREQUENCY 100000
# endif
extern bool playing_note ;
extern bool playing_melody ;
extern uint8_t note_timbre ;
static PWMConfig pwmCFG = {
. frequency = 100000 , /* PWM clock frequency */
// CHIBIOS-BUG? can't set the initial period to <2, or the pwm (hard or software) takes ~130ms with .frequency=500000 for a pwmChangePeriod to take effect; with no output=silence in the meantime
. period = 2 , /* initial PWM period (in ticks) 1S (1/10kHz=0.1mS 0.1ms*10000 ticks=1S) */
. callback = NULL , /* no callback, the hardware directly toggles the pin */
. channels =
{
# if AUDIO_PWM_CHANNEL == 4
{ PWM_OUTPUT_DISABLED , NULL } , /* channel 0 -> TIMx_CH1 */
{ PWM_OUTPUT_DISABLED , NULL } , /* channel 1 -> TIMx_CH2 */
{ PWM_OUTPUT_DISABLED , NULL } , /* channel 2 -> TIMx_CH3 */
{ PWM_OUTPUT_ACTIVE_HIGH , NULL } /* channel 3 -> TIMx_CH4 */
# elif AUDIO_PWM_CHANNEL == 3
{ PWM_OUTPUT_DISABLED , NULL } ,
{ PWM_OUTPUT_DISABLED , NULL } ,
{ PWM_OUTPUT_ACTIVE_HIGH , NULL } , /* TIMx_CH3 */
{ PWM_OUTPUT_DISABLED , NULL }
# elif AUDIO_PWM_CHANNEL == 2
{ PWM_OUTPUT_DISABLED , NULL } ,
{ PWM_OUTPUT_ACTIVE_HIGH , NULL } , /* TIMx_CH2 */
{ PWM_OUTPUT_DISABLED , NULL } ,
{ PWM_OUTPUT_DISABLED , NULL }
# else /*fallback to CH1 */
{ PWM_OUTPUT_ACTIVE_HIGH , NULL } , /* TIMx_CH1 */
{ PWM_OUTPUT_DISABLED , NULL } ,
{ PWM_OUTPUT_DISABLED , NULL } ,
{ PWM_OUTPUT_DISABLED , NULL }
# endif
} ,
} ;
static PWMConfig pwmCFG = { . frequency = AUDIO_PWM_COUNTER_FREQUENCY , /* PWM clock frequency */
. period = 2 ,
. callback = NULL ,
. channels = { [ ( AUDIO_PWM_CHANNEL - 1 ) ] = { . mode = PWM_OUTPUT_ACTIVE_HIGH , . callback = NULL } } } ;
static float channel_1_frequency = 0.0f ;
void channel_1_set_frequency ( float freq ) {
void channel_1_set_frequency ( float freq ) {
channel_1_frequency = freq ;
if ( freq < = 0.0 ) // a pause/rest has freq=0
if ( freq < = 0.0 ) {
// a pause/rest has freq=0
return ;
}
pwmcnt_t period = ( pwmCFG . frequency / freq ) ;
pwmChangePeriod ( & AUDIO_PWM_DRIVER , period ) ;
pwmEnableChannel ( & AUDIO_PWM_DRIVER , AUDIO_PWM_CHANNEL - 1 ,
// adjust the duty-cycle so that the output is for 'note_timbre' duration HIGH
PWM_PERCENTAGE_TO_WIDTH ( & AUDIO_PWM_DRIVER , ( 100 - note_timbre ) * 100 ) ) ;
chSysLockFromISR ( ) ;
pwmChangePeriodI ( & AUDIO_PWM_DRIVER , period ) ;
pwmEnableChannelI ( & AUDIO_PWM_DRIVER , AUDIO_PWM_CHANNEL - 1 ,
// adjust the duty-cycle so that the output is for 'note_timbre' duration HIGH
PWM_PERCENTAGE_TO_WIDTH ( & AUDIO_PWM_DRIVER , ( 100 - note_timbre ) * 100 ) ) ;
chSysUnlockFromISR ( ) ;
}
float channel_1_get_frequency ( void ) {
@ -95,54 +64,53 @@ void channel_1_stop(void) {
pwmStop ( & AUDIO_PWM_DRIVER ) ;
}
static void gpt_callback ( GPTDriver * gptp ) ;
GPTConfig gptCFG = {
/* a whole note is one beat, which is - per definition in musical_notes.h - set to 64
the longest note is BREAVE_DOT = 128 + 64 = 192 , the shortest SIXTEENTH = 4
the tempo ( which might vary ! ) is in bpm ( beats per minute )
therefore : if the timer ticks away at . frequency = ( 60 * 64 ) Hz ,
and the . interval counts from 64 downwards - audio_update_state is
called just often enough to not miss any notes
*/
. frequency = 60 * 64 ,
. callback = gpt_callback ,
} ;
static virtual_timer_t audio_vt ;
static void audio_callback ( virtual_timer_t * vtp , void * p ) ;
// a regular timer task, that checks the note to be currently played and updates
// the pwm to output that frequency.
static void audio_callback ( virtual_timer_t * vtp , void * p ) {
float freq ; // TODO: freq_alt
if ( audio_update_state ( ) ) {
freq = audio_get_processed_frequency ( 0 ) ; // freq_alt would be index=1
channel_1_set_frequency ( freq ) ;
}
chSysLockFromISR ( ) ;
chVTSetI ( & audio_vt , TIME_MS2I ( 16 ) , audio_callback , NULL ) ;
chSysUnlockFromISR ( ) ;
}
void audio_driver_initialize ( void ) {
pwmStart ( & AUDIO_PWM_DRIVER , & pwmCFG ) ;
// connect the AUDIO_PIN to the PWM hardware
# if defined(USE_GPIOV1) // STM32F103C8
palSetLineMode ( AUDIO_PIN , PAL_MODE_ALTERNATE_PUSHPULL ) ;
# if defined(USE_GPIOV1) // STM32F103C8, RP2040
palSetLineMode ( AUDIO_PIN , AUDIO_PWM_ PAL_MODE) ;
# else // GPIOv2 (or GPIOv3 for f4xx, which is the same/compatible at this command)
palSetLineMode ( AUDIO_PIN , PAL_MODE_ALTERNATE ( AUDIO_PWM_PAL_MODE ) ) ;
# endif
gptStart ( & AUDIO_STATE_TIMER , & gptCFG ) ;
chVTObjectInit ( & audio_vt ) ;
}
void audio_driver_start ( void ) {
channel_1_stop ( ) ;
channel_1_start ( ) ;
if ( playing_note | | playing_melody ) {
gptStartContinuous ( & AUDIO_STATE_TIMER , 64 ) ;
if ( ( playing_note | | playing_melody ) & & ! chVTIsArmed ( & audio_vt ) ) {
// a whole note is one beat, which is - per definition in
// musical_notes.h - set to 64 the longest note is
// BREAVE_DOT=128+64=192, the shortest SIXTEENTH=4 the tempo (which
// might vary!) is in bpm (beats per minute) therefore: if the timer
// ticks away at 64Hz (~16.6ms) audio_update_state is called just often
// enough to not miss any notes
chVTSet ( & audio_vt , TIME_MS2I ( 16 ) , audio_callback , NULL ) ;
}
}
void audio_driver_stop ( void ) {
channel_1_stop ( ) ;
gptStopTimer ( & AUDIO_STATE_TIMER ) ;
}
/* a regular timer task, that checks the note to be currently played
* and updates the pwm to output that frequency
*/
static void gpt_callback ( GPTDriver * gptp ) {
float freq ; // TODO: freq_alt
if ( audio_update_state ( ) ) {
freq = audio_get_processed_frequency ( 0 ) ; // freq_alt would be index=1
channel_1_set_frequency ( freq ) ;
}
chVTReset ( & audio_vt ) ;
}