summaryrefslogtreecommitdiffstats
path: root/platforms/avr/drivers/audio_pwm_hardware.c
blob: df03a4558cad8d56ba9ac62b8b90cd612203348c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
/* Copyright 2016 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/>.
 */

#if defined(__AVR__)
#    include <avr/pgmspace.h>
#    include <avr/interrupt.h>
#    include <avr/io.h>
#endif

#include "audio.h"

extern bool    playing_note;
extern bool    playing_melody;
extern uint8_t note_timbre;

#define CPU_PRESCALER 8

/*
  Audio Driver: PWM

  drive up to two speakers through the AVR PWM hardware-peripheral, using timer1 and/or timer3 on Atmega32U4.

  the primary channel_1 can be connected to either pin PC4 PC5 or PC6 (the later being used by most AVR based keyboards) with a PMW signal generated by timer3
  and an optional secondary channel_2 on either pin PB5, PB6 or PB7, with a PWM signal from timer1

  alternatively, the PWM pins on PORTB can be used as only/primary speaker
*/

#if defined(AUDIO_PIN) && (AUDIO_PIN != C4) && (AUDIO_PIN != C5) && (AUDIO_PIN != C6) && (AUDIO_PIN != B5) && (AUDIO_PIN != B6) && (AUDIO_PIN != B7) && (AUDIO_PIN != D5)
#    error "Audio feature enabled, but no suitable pin selected as AUDIO_PIN - see docs/feature_audio under the AVR settings for available options."
#endif

#if (AUDIO_PIN == C4) || (AUDIO_PIN == C5) || (AUDIO_PIN == C6)
#    define AUDIO1_PIN_SET
#    define AUDIO1_TIMSKx TIMSK3
#    define AUDIO1_TCCRxA TCCR3A
#    define AUDIO1_TCCRxB TCCR3B
#    define AUDIO1_ICRx ICR3
#    define AUDIO1_WGMx0 WGM30
#    define AUDIO1_WGMx1 WGM31
#    define AUDIO1_WGMx2 WGM32
#    define AUDIO1_WGMx3 WGM33
#    define AUDIO1_CSx0 CS30
#    define AUDIO1_CSx1 CS31
#    define AUDIO1_CSx2 CS32

#    if (AUDIO_PIN == C6)
#        define AUDIO1_COMxy0 COM3A0
#        define AUDIO1_COMxy1 COM3A1
#        define AUDIO1_OCIExy OCIE3A
#        define AUDIO1_OCRxy OCR3A
#        define AUDIO1_PIN C6
#        define AUDIO1_TIMERx_COMPy_vect TIMER3_COMPA_vect
#    elif (AUDIO_PIN == C5)
#        define AUDIO1_COMxy0 COM3B0
#        define AUDIO1_COMxy1 COM3B1
#        define AUDIO1_OCIExy OCIE3B
#        define AUDIO1_OCRxy OCR3B
#        define AUDIO1_PIN C5
#        define AUDIO1_TIMERx_COMPy_vect TIMER3_COMPB_vect
#    elif (AUDIO_PIN == C4)
#        define AUDIO1_COMxy0 COM3C0
#        define AUDIO1_COMxy1 COM3C1
#        define AUDIO1_OCIExy OCIE3C
#        define AUDIO1_OCRxy OCR3C
#        define AUDIO1_PIN C4
#        define AUDIO1_TIMERx_COMPy_vect TIMER3_COMPC_vect
#    endif
#endif

#if defined(AUDIO_PIN) && defined(AUDIO_PIN_ALT) && (AUDIO_PIN == AUDIO_PIN_ALT)
#    error "Audio feature: AUDIO_PIN and AUDIO_PIN_ALT on the same pin makes no sense."
#endif

#if ((AUDIO_PIN == B5) && ((AUDIO_PIN_ALT == B6) || (AUDIO_PIN_ALT == B7))) || ((AUDIO_PIN == B6) && ((AUDIO_PIN_ALT == B5) || (AUDIO_PIN_ALT == B7))) || ((AUDIO_PIN == B7) && ((AUDIO_PIN_ALT == B5) || (AUDIO_PIN_ALT == B6)))
#    error "Audio feature: PORTB as AUDIO_PIN and AUDIO_PIN_ALT at the same time is not supported."
#endif

#if defined(AUDIO_PIN_ALT) && (AUDIO_PIN_ALT != B5) && (AUDIO_PIN_ALT != B6) && (AUDIO_PIN_ALT != B7)
#    error "Audio feature: the pin selected as AUDIO_PIN_ALT is not supported."
#endif

#if (AUDIO_PIN == B5) || (AUDIO_PIN == B6) || (AUDIO_PIN == B7) || (AUDIO_PIN_ALT == B5) || (AUDIO_PIN_ALT == B6) || (AUDIO_PIN_ALT == B7) || (AUDIO_PIN == D5)
#    define AUDIO2_PIN_SET
#    define AUDIO2_TIMSKx TIMSK1
#    define AUDIO2_TCCRxA TCCR1A
#    define AUDIO2_TCCRxB TCCR1B
#    define AUDIO2_ICRx ICR1
#    define AUDIO2_WGMx0 WGM10
#    define AUDIO2_WGMx1 WGM11
#    define AUDIO2_WGMx2 WGM12
#    define AUDIO2_WGMx3 WGM13
#    define AUDIO2_CSx0 CS10
#    define AUDIO2_CSx1 CS11
#    define AUDIO2_CSx2 CS12

#    if (AUDIO_PIN == B5) || (AUDIO_PIN_ALT == B5)
#        define AUDIO2_COMxy0 COM1A0
#        define AUDIO2_COMxy1 COM1A1
#        define AUDIO2_OCIExy OCIE1A
#        define AUDIO2_OCRxy OCR1A
#        define AUDIO2_PIN B5
#        define AUDIO2_TIMERx_COMPy_vect TIMER1_COMPA_vect
#    elif (AUDIO_PIN == B6) || (AUDIO_PIN_ALT == B6)
#        define AUDIO2_COMxy0 COM1B0
#        define AUDIO2_COMxy1 COM1B1
#        define AUDIO2_OCIExy OCIE1B
#        define AUDIO2_OCRxy OCR1B
#        define AUDIO2_PIN B6
#        define AUDIO2_TIMERx_COMPy_vect TIMER1_COMPB_vect
#    elif (AUDIO_PIN == B7) || (AUDIO_PIN_ALT == B7)
#        define AUDIO2_COMxy0 COM1C0
#        define AUDIO2_COMxy1 COM1C1
#        define AUDIO2_OCIExy OCIE1C
#        define AUDIO2_OCRxy OCR1C
#        define AUDIO2_PIN B7
#        define AUDIO2_TIMERx_COMPy_vect TIMER1_COMPC_vect
#    elif (AUDIO_PIN == D5) && defined(__AVR_ATmega32A__)
#        pragma message "Audio support for ATmega32A is experimental and can cause crashes."
#        undef AUDIO2_TIMSKx
#        define AUDIO2_TIMSKx TIMSK
#        define AUDIO2_COMxy0 COM1A0
#        define AUDIO2_COMxy1 COM1A1
#        define AUDIO2_OCIExy OCIE1A
#        define AUDIO2_OCRxy OCR1A
#        define AUDIO2_PIN D5
#        define AUDIO2_TIMERx_COMPy_vect TIMER1_COMPA_vect
#    endif
#endif

// C6 seems to be the assumed default by many existing keyboard - but sill warn the user
#if !defined(AUDIO1_PIN_SET) && !defined(AUDIO2_PIN_SET)
#    pragma message "Audio feature enabled, but no suitable pin selected - see docs/feature_audio under the AVR settings for available options. Don't expect to hear anything... :-)"
// TODO: make this an error - go through the breaking-change-process and change all keyboards to the new define
#endif
// -----------------------------------------------------------------------------

#ifdef AUDIO1_PIN_SET
static float channel_1_frequency = 0.0f;
void         channel_1_set_frequency(float freq) {
    if (freq == 0.0f)  // a pause/rest is a valid "note" with freq=0
    {
        // disable the output, but keep the pwm-ISR going (with the previous
        // frequency) so the audio-state keeps getting updated
        // Note: setting the duty-cycle 0 is not possible on non-inverting PWM mode - see the AVR data-sheet
        AUDIO1_TCCRxA &= ~(_BV(AUDIO1_COMxy1) | _BV(AUDIO1_COMxy0));
        return;
    } else {
        AUDIO1_TCCRxA |= _BV(AUDIO1_COMxy1);  // enable output, PWM mode
    }

    channel_1_frequency = freq;

    // set pwm period
    AUDIO1_ICRx = (uint16_t)(((float)F_CPU) / (freq * CPU_PRESCALER));
    // and duty cycle
    AUDIO1_OCRxy = (uint16_t)((((float)F_CPU) / (freq * CPU_PRESCALER)) * note_timbre / 100);
}

void channel_1_start(void) {
    // enable timer-counter ISR
    AUDIO1_TIMSKx |= _BV(AUDIO1_OCIExy);
    // enable timer-counter output
    AUDIO1_TCCRxA |= _BV(AUDIO1_COMxy1);
}

void channel_1_stop(void) {
    // disable timer-counter ISR
    AUDIO1_TIMSKx &= ~_BV(AUDIO1_OCIExy);
    // disable timer-counter output
    AUDIO1_TCCRxA &= ~(_BV(AUDIO1_COMxy1) | _BV(AUDIO1_COMxy0));
}
#endif

#ifdef AUDIO2_PIN_SET
static float channel_2_frequency = 0.0f;
void         channel_2_set_frequency(float freq) {
    if (freq == 0.0f) {
        AUDIO2_TCCRxA &= ~(_BV(AUDIO2_COMxy1) | _BV(AUDIO2_COMxy0));
        return;
    } else {
        AUDIO2_TCCRxA |= _BV(AUDIO2_COMxy1);
    }

    channel_2_frequency = freq;

    AUDIO2_ICRx  = (uint16_t)(((float)F_CPU) / (freq * CPU_PRESCALER));
    AUDIO2_OCRxy = (uint16_t)((((float)F_CPU) / (freq * CPU_PRESCALER)) * note_timbre / 100);
}

float channel_2_get_frequency(void) { return channel_2_frequency; }

void channel_2_start(void) {
    AUDIO2_TIMSKx |= _BV(AUDIO2_OCIExy);
    AUDIO2_TCCRxA |= _BV(AUDIO2_COMxy1);
}

void channel_2_stop(void) {
    AUDIO2_TIMSKx &= ~_BV(AUDIO2_OCIExy);
    AUDIO2_TCCRxA &= ~(_BV(AUDIO2_COMxy1) | _BV(AUDIO2_COMxy0));
}
#endif

void audio_driver_initialize() {
#ifdef AUDIO1_PIN_SET
    channel_1_stop();
    setPinOutput(AUDIO1_PIN);
#endif

#ifdef AUDIO2_PIN_SET
    channel_2_stop();
    setPinOutput(AUDIO2_PIN);
#endif

    // TCCR3A / TCCR3B: Timer/Counter #3 Control Registers TCCR3A/TCCR3B, TCCR1A/TCCR1B
    // Compare Output Mode (COM3An and COM1An) = 0b00 = Normal port operation
    //   OC3A -- PC6
    //   OC3B -- PC5
    //   OC3C -- PC4
    //   OC1A -- PB5
    //   OC1B -- PB6
    //   OC1C -- PB7

    // Waveform Generation Mode (WGM3n) = 0b1110 = Fast PWM Mode 14. Period = ICR3, Duty Cycle OCR3A)
    //   OCR3A - PC6
    //   OCR3B - PC5
    //   OCR3C - PC4
    //   OCR1A - PB5
    //   OCR1B - PB6
    //   OCR1C - PB7

    // Clock Select (CS3n) = 0b010 = Clock / 8
#ifdef AUDIO1_PIN_SET
    // initialize timer-counter
    AUDIO1_TCCRxA = (0 << AUDIO1_COMxy1) | (0 << AUDIO1_COMxy0) | (1 << AUDIO1_WGMx1) | (0 << AUDIO1_WGMx0);
    AUDIO1_TCCRxB = (1 << AUDIO1_WGMx3) | (1 << AUDIO1_WGMx2) | (0 << AUDIO1_CSx2) | (1 << AUDIO1_CSx1) | (0 << AUDIO1_CSx0);
#endif

#ifdef AUDIO2_PIN_SET
    AUDIO2_TCCRxA = (0 << AUDIO2_COMxy1) | (0 << AUDIO2_COMxy0) | (1 << AUDIO2_WGMx1) | (0 << AUDIO2_WGMx0);
    AUDIO2_TCCRxB = (1 << AUDIO2_WGMx3) | (1 << AUDIO2_WGMx2) | (0 << AUDIO2_CSx2) | (1 << AUDIO2_CSx1) | (0 << AUDIO2_CSx0);
#endif
}

void audio_driver_stop() {
#ifdef AUDIO1_PIN_SET
    channel_1_stop();
#endif

#ifdef AUDIO2_PIN_SET
    channel_2_stop();
#endif
}

void audio_driver_start(void) {
#ifdef AUDIO1_PIN_SET
    channel_1_start();
    if (playing_note) {
        channel_1_set_frequency(audio_get_processed_frequency(0));
    }
#endif

#if !defined(AUDIO1_PIN_SET) && defined(AUDIO2_PIN_SET)
    channel_2_start();
    if (playing_note) {
        channel_2_set_frequency(audio_get_processed_frequency(0));
    }
#endif
}

static volatile uint32_t isr_counter = 0;
#ifdef AUDIO1_PIN_SET
ISR(AUDIO1_TIMERx_COMPy_vect) {
    isr_counter++;
    if (isr_counter < channel_1_frequency / (CPU_PRESCALER * 8)) return;

    isr_counter        = 0;
    bool state_changed = audio_update_state();

    if (!playing_note && !playing_melody) {
        channel_1_stop();
#    ifdef AUDIO2_PIN_SET
        channel_2_stop();
#    endif
        return;
    }

    if (state_changed) {
        channel_1_set_frequency(audio_get_processed_frequency(0));
#    ifdef AUDIO2_PIN_SET
        if (audio_get_number_of_active_tones() > 1) {
            channel_2_set_frequency(audio_get_processed_frequency(1));
        } else {
            channel_2_stop();
        }
#    endif
    }
}
#endif

#if !defined(AUDIO1_PIN_SET) && defined(AUDIO2_PIN_SET)
ISR(AUDIO2_TIMERx_COMPy_vect) {
    isr_counter++;
    if (isr_counter < channel_2_frequency / (CPU_PRESCALER * 8)) return;

    isr_counter        = 0;
    bool state_changed = audio_update_state();

    if (!playing_note && !playing_melody) {
        channel_2_stop();
        return;
    }

    if (state_changed) {
        channel_2_set_frequency(audio_get_processed_frequency(0));
    }
}
#endif