audio_pwm_software.c (5922B)
1 /* Copyright 2020 Jack Humbert 2 * Copyright 2020 JohSchneider 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 /* 19 Audio Driver: PWM 20 21 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. 22 23 this driver uses the chibios-PWM system to produce a square-wave on any given output pin in software 24 - a pwm callback is used to set/clear the configured pin. 25 26 */ 27 #include "audio.h" 28 #include "gpio.h" 29 30 #if !defined(AUDIO_PIN) 31 # error "Audio feature enabled, but no pin selected - see docs/feature_audio under the ARM PWM settings" 32 #endif 33 extern bool playing_note; 34 extern bool playing_melody; 35 extern uint8_t note_timbre; 36 37 static void pwm_audio_period_callback(PWMDriver *pwmp); 38 static void pwm_audio_channel_interrupt_callback(PWMDriver *pwmp); 39 40 static PWMConfig pwmCFG = { 41 .frequency = 100000, /* PWM clock frequency */ 42 // 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 43 .period = 2, /* initial PWM period (in ticks) 1S (1/10kHz=0.1mS 0.1ms*10000 ticks=1S) */ 44 .callback = pwm_audio_period_callback, 45 .channels = 46 { 47 // software-PWM just needs another callback on any channel 48 {PWM_OUTPUT_ACTIVE_HIGH, pwm_audio_channel_interrupt_callback}, /* channel 0 -> TIMx_CH1 */ 49 {PWM_OUTPUT_DISABLED, NULL}, /* channel 1 -> TIMx_CH2 */ 50 {PWM_OUTPUT_DISABLED, NULL}, /* channel 2 -> TIMx_CH3 */ 51 {PWM_OUTPUT_DISABLED, NULL} /* channel 3 -> TIMx_CH4 */ 52 }, 53 }; 54 55 static float channel_1_frequency = 0.0f; 56 void channel_1_set_frequency(float freq) { 57 channel_1_frequency = freq; 58 59 if (freq <= 0.0) // a pause/rest has freq=0 60 return; 61 62 pwmcnt_t period = (pwmCFG.frequency / freq); 63 pwmChangePeriod(&AUDIO_PWM_DRIVER, period); 64 65 pwmEnableChannel(&AUDIO_PWM_DRIVER, AUDIO_PWM_CHANNEL - 1, 66 // adjust the duty-cycle so that the output is for 'note_timbre' duration HIGH 67 PWM_PERCENTAGE_TO_WIDTH(&AUDIO_PWM_DRIVER, (100 - note_timbre) * 100)); 68 } 69 70 float channel_1_get_frequency(void) { 71 return channel_1_frequency; 72 } 73 74 void channel_1_start(void) { 75 pwmStop(&AUDIO_PWM_DRIVER); 76 pwmStart(&AUDIO_PWM_DRIVER, &pwmCFG); 77 78 pwmEnablePeriodicNotification(&AUDIO_PWM_DRIVER); 79 pwmEnableChannelNotification(&AUDIO_PWM_DRIVER, AUDIO_PWM_CHANNEL - 1); 80 } 81 82 void channel_1_stop(void) { 83 pwmStop(&AUDIO_PWM_DRIVER); 84 85 palClearLine(AUDIO_PIN); // leave the line low, after last note was played 86 87 #if defined(AUDIO_PIN_ALT) && defined(AUDIO_PIN_ALT_AS_NEGATIVE) 88 palClearLine(AUDIO_PIN_ALT); // leave the line low, after last note was played 89 #endif 90 } 91 92 // generate a PWM signal on any pin, not necessarily the one connected to the timer 93 static void pwm_audio_period_callback(PWMDriver *pwmp) { 94 (void)pwmp; 95 palClearLine(AUDIO_PIN); 96 97 #if defined(AUDIO_PIN_ALT) && defined(AUDIO_PIN_ALT_AS_NEGATIVE) 98 palSetLine(AUDIO_PIN_ALT); 99 #endif 100 } 101 static void pwm_audio_channel_interrupt_callback(PWMDriver *pwmp) { 102 (void)pwmp; 103 if (channel_1_frequency > 0) { 104 palSetLine(AUDIO_PIN); // generate a PWM signal on any pin, not necessarily the one connected to the timer 105 #if defined(AUDIO_PIN_ALT) && defined(AUDIO_PIN_ALT_AS_NEGATIVE) 106 palClearLine(AUDIO_PIN_ALT); 107 #endif 108 } 109 } 110 111 static void gpt_callback(GPTDriver *gptp); 112 GPTConfig gptCFG = { 113 /* a whole note is one beat, which is - per definition in musical_notes.h - set to 64 114 the longest note is BREAVE_DOT=128+64=192, the shortest SIXTEENTH=4 115 the tempo (which might vary!) is in bpm (beats per minute) 116 therefore: if the timer ticks away at .frequency = (60*64)Hz, 117 and the .interval counts from 64 downwards - audio_update_state is 118 called just often enough to not miss anything 119 */ 120 .frequency = 60 * 64, 121 .callback = gpt_callback, 122 }; 123 124 void audio_driver_initialize_impl(void) { 125 pwmStart(&AUDIO_PWM_DRIVER, &pwmCFG); 126 127 palSetLineMode(AUDIO_PIN, PAL_MODE_OUTPUT_PUSHPULL); 128 palClearLine(AUDIO_PIN); 129 130 #if defined(AUDIO_PIN_ALT) && defined(AUDIO_PIN_ALT_AS_NEGATIVE) 131 palSetLineMode(AUDIO_PIN_ALT, PAL_MODE_OUTPUT_PUSHPULL); 132 palClearLine(AUDIO_PIN_ALT); 133 #endif 134 135 pwmEnablePeriodicNotification(&AUDIO_PWM_DRIVER); // enable pwm callbacks 136 pwmEnableChannelNotification(&AUDIO_PWM_DRIVER, AUDIO_PWM_CHANNEL - 1); 137 138 gptStart(&AUDIO_STATE_TIMER, &gptCFG); 139 } 140 141 void audio_driver_start_impl(void) { 142 channel_1_stop(); 143 channel_1_start(); 144 145 if (playing_note || playing_melody) { 146 gptStartContinuous(&AUDIO_STATE_TIMER, 64); 147 } 148 } 149 150 void audio_driver_stop_impl(void) { 151 channel_1_stop(); 152 gptStopTimer(&AUDIO_STATE_TIMER); 153 } 154 155 /* a regular timer task, that checks the note to be currently played 156 * and updates the pwm to output that frequency 157 */ 158 static void gpt_callback(GPTDriver *gptp) { 159 float freq; // TODO: freq_alt 160 161 if (audio_update_state()) { 162 freq = audio_get_processed_frequency(0); // freq_alt would be index=1 163 channel_1_set_frequency(freq); 164 } 165 }