audio_pwm_software.c 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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. Audio Driver: PWM
  19. 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.
  20. this driver uses the chibios-PWM system to produce a square-wave on any given output pin in software
  21. - a pwm callback is used to set/clear the configured pin.
  22. */
  23. #include "audio.h"
  24. #include "ch.h"
  25. #include "hal.h"
  26. #if !defined(AUDIO_PIN)
  27. # error "Audio feature enabled, but no pin selected - see docs/feature_audio under the ARM PWM settings"
  28. #endif
  29. extern bool playing_note;
  30. extern bool playing_melody;
  31. extern uint8_t note_timbre;
  32. static void pwm_audio_period_callback(PWMDriver *pwmp);
  33. static void pwm_audio_channel_interrupt_callback(PWMDriver *pwmp);
  34. static PWMConfig pwmCFG = {
  35. .frequency = 100000, /* PWM clock frequency */
  36. // 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
  37. .period = 2, /* initial PWM period (in ticks) 1S (1/10kHz=0.1mS 0.1ms*10000 ticks=1S) */
  38. .callback = pwm_audio_period_callback,
  39. .channels =
  40. {
  41. // software-PWM just needs another callback on any channel
  42. {PWM_OUTPUT_ACTIVE_HIGH, pwm_audio_channel_interrupt_callback}, /* channel 0 -> TIMx_CH1 */
  43. {PWM_OUTPUT_DISABLED, NULL}, /* channel 1 -> TIMx_CH2 */
  44. {PWM_OUTPUT_DISABLED, NULL}, /* channel 2 -> TIMx_CH3 */
  45. {PWM_OUTPUT_DISABLED, NULL} /* channel 3 -> TIMx_CH4 */
  46. },
  47. };
  48. static float channel_1_frequency = 0.0f;
  49. void channel_1_set_frequency(float freq) {
  50. channel_1_frequency = freq;
  51. if (freq <= 0.0) // a pause/rest has freq=0
  52. return;
  53. pwmcnt_t period = (pwmCFG.frequency / freq);
  54. pwmChangePeriod(&AUDIO_PWM_DRIVER, period);
  55. pwmEnableChannel(&AUDIO_PWM_DRIVER, AUDIO_PWM_CHANNEL - 1,
  56. // adjust the duty-cycle so that the output is for 'note_timbre' duration HIGH
  57. PWM_PERCENTAGE_TO_WIDTH(&AUDIO_PWM_DRIVER, (100 - note_timbre) * 100));
  58. }
  59. float channel_1_get_frequency(void) {
  60. return channel_1_frequency;
  61. }
  62. void channel_1_start(void) {
  63. pwmStop(&AUDIO_PWM_DRIVER);
  64. pwmStart(&AUDIO_PWM_DRIVER, &pwmCFG);
  65. pwmEnablePeriodicNotification(&AUDIO_PWM_DRIVER);
  66. pwmEnableChannelNotification(&AUDIO_PWM_DRIVER, AUDIO_PWM_CHANNEL - 1);
  67. }
  68. void channel_1_stop(void) {
  69. pwmStop(&AUDIO_PWM_DRIVER);
  70. palClearLine(AUDIO_PIN); // leave the line low, after last note was played
  71. #if defined(AUDIO_PIN_ALT) && defined(AUDIO_PIN_ALT_AS_NEGATIVE)
  72. palClearLine(AUDIO_PIN_ALT); // leave the line low, after last note was played
  73. #endif
  74. }
  75. // generate a PWM signal on any pin, not necessarily the one connected to the timer
  76. static void pwm_audio_period_callback(PWMDriver *pwmp) {
  77. (void)pwmp;
  78. palClearLine(AUDIO_PIN);
  79. #if defined(AUDIO_PIN_ALT) && defined(AUDIO_PIN_ALT_AS_NEGATIVE)
  80. palSetLine(AUDIO_PIN_ALT);
  81. #endif
  82. }
  83. static void pwm_audio_channel_interrupt_callback(PWMDriver *pwmp) {
  84. (void)pwmp;
  85. if (channel_1_frequency > 0) {
  86. palSetLine(AUDIO_PIN); // generate a PWM signal on any pin, not necessarily the one connected to the timer
  87. #if defined(AUDIO_PIN_ALT) && defined(AUDIO_PIN_ALT_AS_NEGATIVE)
  88. palClearLine(AUDIO_PIN_ALT);
  89. #endif
  90. }
  91. }
  92. static void gpt_callback(GPTDriver *gptp);
  93. GPTConfig gptCFG = {
  94. /* a whole note is one beat, which is - per definition in musical_notes.h - set to 64
  95. the longest note is BREAVE_DOT=128+64=192, the shortest SIXTEENTH=4
  96. the tempo (which might vary!) is in bpm (beats per minute)
  97. therefore: if the timer ticks away at .frequency = (60*64)Hz,
  98. and the .interval counts from 64 downwards - audio_update_state is
  99. called just often enough to not miss anything
  100. */
  101. .frequency = 60 * 64,
  102. .callback = gpt_callback,
  103. };
  104. void audio_driver_initialize(void) {
  105. pwmStart(&AUDIO_PWM_DRIVER, &pwmCFG);
  106. palSetLineMode(AUDIO_PIN, PAL_MODE_OUTPUT_PUSHPULL);
  107. palClearLine(AUDIO_PIN);
  108. #if defined(AUDIO_PIN_ALT) && defined(AUDIO_PIN_ALT_AS_NEGATIVE)
  109. palSetLineMode(AUDIO_PIN_ALT, PAL_MODE_OUTPUT_PUSHPULL);
  110. palClearLine(AUDIO_PIN_ALT);
  111. #endif
  112. pwmEnablePeriodicNotification(&AUDIO_PWM_DRIVER); // enable pwm callbacks
  113. pwmEnableChannelNotification(&AUDIO_PWM_DRIVER, AUDIO_PWM_CHANNEL - 1);
  114. gptStart(&AUDIO_STATE_TIMER, &gptCFG);
  115. }
  116. void audio_driver_start(void) {
  117. channel_1_stop();
  118. channel_1_start();
  119. if (playing_note || playing_melody) {
  120. gptStartContinuous(&AUDIO_STATE_TIMER, 64);
  121. }
  122. }
  123. void audio_driver_stop(void) {
  124. channel_1_stop();
  125. gptStopTimer(&AUDIO_STATE_TIMER);
  126. }
  127. /* a regular timer task, that checks the note to be currently played
  128. * and updates the pwm to output that frequency
  129. */
  130. static void gpt_callback(GPTDriver *gptp) {
  131. float freq; // TODO: freq_alt
  132. if (audio_update_state()) {
  133. freq = audio_get_processed_frequency(0); // freq_alt would be index=1
  134. channel_1_set_frequency(freq);
  135. }
  136. }