Merge branch 'master'
[fw/altos] / src / samd21 / ao_dac_samd21.c
diff --git a/src/samd21/ao_dac_samd21.c b/src/samd21/ao_dac_samd21.c
new file mode 100644 (file)
index 0000000..7dc00c1
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * Copyright © 2020 Keith Packard <keithp@keithp.com>
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include <snek.h>
+#include <ao.h>
+#include <ao_dac-samd21.h>
+#include <ao_tcc-samd21.h>
+
+/* Max DAC output value. We're using left-adjusted values */
+#define SNEK_DAC_MAX           65535
+
+#ifdef SNEK_SAMD21_DAC_TIMER
+/*
+ * If there's a timer available, we can use that
+ * to implement the 'tone' function
+ */
+
+#include "sine.h"
+
+#define NSINE  (sizeof(sine) / sizeof(sine[0]))
+
+static uint16_t current_power;
+static uint16_t power;
+static uint32_t        phase;
+static uint32_t phase_step;
+static volatile bool dac_running;
+
+#define _paste2(x,y)   x ## y
+#define _paste3(x,y,z) x ## y ## z
+#define paste2(x,y)    _paste2(x,y)
+#define paste3(x,y,z)  _paste3(x,y,z)
+#define SAMD21_TCC     paste2(samd21_tcc, SNEK_SAMD21_DAC_TIMER)
+#define SAMD21_TCC_ISR paste3(samd21_tcc, SNEK_SAMD21_DAC_TIMER, _isr)
+
+#define AO_DAC_RATE    24000
+
+#define UINT_TO_FIXED(u)       ((uint32_t) (u) << 16)
+#define FIXED_TO_UINT(u)       ((u) >> 16)
+
+void
+SAMD21_TCC_ISR(void)
+{
+       uint32_t intflag = SAMD21_TCC.intflag;
+       SAMD21_TCC.intflag = intflag;
+       if (intflag & (1 << SAMD21_TCC_INTFLAG_OVF)) {
+               if (phase_step) {
+                       samd21_dac.data = ((uint32_t) sine[FIXED_TO_UINT(phase)] * current_power) >> 16;
+                       if ((phase += phase_step) >= UINT_TO_FIXED(NSINE)) {
+                               phase -= UINT_TO_FIXED(NSINE);
+
+                               current_power = power;
+
+                               /* Stop output at zero crossing when no longer outputing tone */
+                               if (!dac_running) {
+                                       phase_step = 0;
+                                       phase = 0;
+                                       SAMD21_TCC.intenclr = (1 << SAMD21_TCC_INTFLAG_OVF);
+                               }
+                       }
+               }
+       }
+}
+
+void
+ao_dac_set_hz(float hz)
+{
+       /* samples/second = AC_DAC_RATE
+        *
+        * cycles/second = hz
+        *
+        * samples/cycle = AC_DAC_RATE / hz
+        *
+        * step/cycle = 256
+        *
+        * step/sample = step/cycle * cycle/samples
+        *             = TWO_PI * hz / AC_DAC_RATE;
+        */
+       uint32_t new_phase_step = (float) UINT_TO_FIXED(NSINE) * hz / (float) AO_DAC_RATE;
+       ao_arch_critical(
+               if (new_phase_step) {
+                       dac_running = true;
+                       phase_step = new_phase_step;
+                       SAMD21_TCC.intenset = (1 << SAMD21_TCC_INTFLAG_OVF);
+               } else {
+                       dac_running = false;
+               });
+}
+
+static void
+ao_dac_timer_init(void)
+{
+       /* Adjust timer to interrupt once per sample period */
+       SAMD21_TCC.per = AO_HCLK / AO_DAC_RATE;
+
+       /* Enable timer interrupts */
+       samd21_nvic_set_enable(paste3(SAMD21_NVIC_ISR_TCC, SNEK_SAMD21_DAC_TIMER, _POS));
+       samd21_nvic_set_priority(paste3(SAMD21_NVIC_ISR_TCC, SNEK_SAMD21_DAC_TIMER, _POS), 3);
+}
+#else
+#define ao_dac_timer_init()
+#endif
+
+static void
+ao_dac_sync(void)
+{
+       while (samd21_dac.status & (1 << SAMD21_DAC_STATUS_SYNCBUSY))
+               ;
+}
+
+void
+ao_dac_set(uint16_t new_power)
+{
+#if SNEK_DAC_MAX != SNEK_PWM_MAX
+       new_power = (uint16_t) ((uint32_t) new_power * SNEK_DAC_MAX) / SNEK_PWM_MAX;
+#endif
+
+       ao_arch_critical(
+#ifdef SNEK_SAMD21_DAC_TIMER
+               power = new_power;
+               /*
+                * When not generating a tone, just set the DAC
+                * output to the requested level
+                */
+               if (!phase_step) {
+                       current_power = new_power;
+                       samd21_dac.data = new_power;
+               }
+#else
+               samd21_dac.data = new_power;
+#endif
+               );
+}
+
+void
+ao_dac_init(void)
+{
+       /* supply a clock */
+       samd21_gclk_clkctrl(0, SAMD21_GCLK_CLKCTRL_ID_DAC);
+
+       /* enable the device */
+       samd21_pm.apbcmask |= (1 << SAMD21_PM_APBCMASK_DAC);
+
+       /* reset */
+       samd21_dac.ctrla = (1 << SAMD21_DAC_CTRLA_SWRST);
+
+       while ((samd21_dac.ctrla & (1 << SAMD21_DAC_CTRLA_SWRST)) != 0 ||
+              (samd21_dac.status & (1 << SAMD21_DAC_STATUS_SYNCBUSY)) != 0)
+               ao_arch_nop();
+
+       /* Configure using VDD as reference */
+       samd21_dac.ctrlb = ((1 << SAMD21_DAC_CTRLB_EOEN) |
+                           (0 << SAMD21_DAC_CTRLB_IOEN) |
+                           (1 << SAMD21_DAC_CTRLB_LEFTADJ) |
+                           (0 << SAMD21_DAC_CTRLB_VPD) |
+                           (1 << SAMD21_DAC_CTRLB_BDWP) |
+                           (SAMD21_DAC_CTRLB_REFSEL_VDDANA << SAMD21_DAC_CTRLB_REFSEL));
+
+       ao_dac_sync();
+
+       samd21_dac.ctrla = (1 << SAMD21_DAC_CTRLA_ENABLE);
+
+       ao_dac_sync();
+
+       ao_dac_timer_init();
+}