qmk_firmware

QMK firmware for my keyboards (Corne, Sweep Ferris) and trackball (Ploopy Adept)
Log | Files | Refs | Submodules | LICENSE

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 }