From 66d45c521edded56ef867b4fe89759d2bce7b75f Mon Sep 17 00:00:00 2001
From: Joey Castillo <jose.castillo@gmail.com>
Date: Mon, 13 Sep 2021 09:48:31 -0400
Subject: implement ADC functionality

---
 watch-library/hw/driver_init.c        |  12 ----
 watch-library/include/component/adc.h |   2 +
 watch-library/watch/watch_adc.c       | 117 ++++++++++++++++++++++++++++++++--
 watch-library/watch/watch_adc.h       |  80 +++++++++++++++++++++--
 4 files changed, 187 insertions(+), 24 deletions(-)

diff --git a/watch-library/hw/driver_init.c b/watch-library/hw/driver_init.c
index daf3901d..564ec7a7 100644
--- a/watch-library/hw/driver_init.c
+++ b/watch-library/hw/driver_init.c
@@ -15,8 +15,6 @@
 
 struct slcd_sync_descriptor SEGMENT_LCD_0;
 
-struct adc_sync_descriptor ADC_0;
-
 struct calendar_descriptor CALENDAR_0;
 
 struct i2c_m_sync_desc I2C_0;
@@ -25,16 +23,6 @@ struct pwm_descriptor PWM_0;
 
 struct pwm_descriptor PWM_1;
 
-void ADC_0_CLOCK_init(void) {
-	hri_mclk_set_APBCMASK_ADC_bit(MCLK);
-	hri_gclk_write_PCHCTRL_reg(GCLK, ADC_GCLK_ID, CONF_GCLK_ADC_SRC | (1 << GCLK_PCHCTRL_CHEN_Pos));
-}
-
-void ADC_0_init(void) {
-	ADC_0_CLOCK_init();
-	adc_sync_init(&ADC_0, ADC, (void *)NULL);
-}
-
 void CALENDAR_0_CLOCK_init(void) {
 	hri_mclk_set_APBAMASK_RTC_bit(MCLK);
 }
diff --git a/watch-library/include/component/adc.h b/watch-library/include/component/adc.h
index 468160c0..f51dc639 100644
--- a/watch-library/include/component/adc.h
+++ b/watch-library/include/component/adc.h
@@ -344,6 +344,7 @@ typedef union {
 #define   ADC_INPUTCTRL_MUXNEG_AIN5_Val   _U_(0x5)   /**< \brief (ADC_INPUTCTRL) ADC AIN5 Pin */
 #define   ADC_INPUTCTRL_MUXNEG_AIN6_Val   _U_(0x6)   /**< \brief (ADC_INPUTCTRL) ADC AIN6 Pin */
 #define   ADC_INPUTCTRL_MUXNEG_AIN7_Val   _U_(0x7)   /**< \brief (ADC_INPUTCTRL) ADC AIN7 Pin */
+#define   ADC_INPUTCTRL_MUXNEG_GND_Val    _U_(0x18)  /**< \brief (ADC_INPUTCTRL) Internal GND */
 #define ADC_INPUTCTRL_MUXNEG_AIN0   (ADC_INPUTCTRL_MUXNEG_AIN0_Val << ADC_INPUTCTRL_MUXNEG_Pos)
 #define ADC_INPUTCTRL_MUXNEG_AIN1   (ADC_INPUTCTRL_MUXNEG_AIN1_Val << ADC_INPUTCTRL_MUXNEG_Pos)
 #define ADC_INPUTCTRL_MUXNEG_AIN2   (ADC_INPUTCTRL_MUXNEG_AIN2_Val << ADC_INPUTCTRL_MUXNEG_Pos)
@@ -352,6 +353,7 @@ typedef union {
 #define ADC_INPUTCTRL_MUXNEG_AIN5   (ADC_INPUTCTRL_MUXNEG_AIN5_Val << ADC_INPUTCTRL_MUXNEG_Pos)
 #define ADC_INPUTCTRL_MUXNEG_AIN6   (ADC_INPUTCTRL_MUXNEG_AIN6_Val << ADC_INPUTCTRL_MUXNEG_Pos)
 #define ADC_INPUTCTRL_MUXNEG_AIN7   (ADC_INPUTCTRL_MUXNEG_AIN7_Val << ADC_INPUTCTRL_MUXNEG_Pos)
+#define ADC_INPUTCTRL_MUXNEG_GND    (ADC_INPUTCTRL_MUXNEG_GND_Val << ADC_INPUTCTRL_MUXNEG_Pos)
 #define ADC_INPUTCTRL_MASK          _U_(0x1F1F)  /**< \brief (ADC_INPUTCTRL) MASK Register */
 
 /* -------- ADC_CTRLC : (ADC Offset: 0x0A) (R/W 16) Control C -------- */
diff --git a/watch-library/watch/watch_adc.c b/watch-library/watch/watch_adc.c
index ebb8bb60..4a7a44ff 100644
--- a/watch-library/watch/watch_adc.c
+++ b/watch-library/watch/watch_adc.c
@@ -22,24 +22,127 @@
  * SOFTWARE.
  */
 
- static bool ADC_0_ENABLED = false;
+void _watch_sync_adc() {
+    while (ADC->SYNCBUSY.reg);
+}
+
+uint16_t _watch_get_analog_value(uint16_t channel) {
+    if (ADC->INPUTCTRL.bit.MUXPOS != channel) {
+        ADC->INPUTCTRL.bit.MUXPOS = channel;
+        _watch_sync_adc();
+    }
+
+    ADC->SWTRIG.bit.START = 1;
+    while (!ADC->INTFLAG.bit.RESRDY);
+
+    return ADC->RESULT.reg;
+}
+
+void watch_enable_adc() {
+    MCLK->APBCMASK.reg |= MCLK_APBCMASK_ADC;
+    GCLK->PCHCTRL[ADC_GCLK_ID].reg = GCLK_PCHCTRL_GEN_GCLK0 | GCLK_PCHCTRL_CHEN;
+
+    uint16_t calib_reg = 0;
+    calib_reg = ADC_CALIB_BIASREFBUF((*(uint32_t *)ADC_FUSES_BIASREFBUF_ADDR >> ADC_FUSES_BIASREFBUF_Pos)) |
+                ADC_CALIB_BIASCOMP((*(uint32_t *)ADC_FUSES_BIASCOMP_ADDR >> ADC_FUSES_BIASCOMP_Pos));
 
-void watch_enable_analog(const uint8_t pin) {
-    if (!ADC_0_ENABLED) ADC_0_init();
-    ADC_0_ENABLED = true;
+    if (!ADC->SYNCBUSY.bit.SWRST) {
+        if (ADC->CTRLA.bit.ENABLE) {
+            ADC->CTRLA.bit.ENABLE = 0;
+            _watch_sync_adc();
+        }
+        ADC->CTRLA.bit.SWRST = 1;
+    }
+    _watch_sync_adc();
+
+    if (USB->DEVICE.CTRLA.bit.ENABLE) {
+        // if USB is enabled, we are running an 8 MHz clock.
+        // divide by 16 for a 500kHz ADC clock.
+        ADC->CTRLB.bit.PRESCALER = ADC_CTRLB_PRESCALER_DIV16_Val;
+    } else {
+        // otherwise it's 4 Mhz. divide by 8 for a 500kHz ADC clock.
+        ADC->CTRLB.bit.PRESCALER = ADC_CTRLB_PRESCALER_DIV8_Val;
+    }
+    ADC->CALIB.reg = calib_reg;
+    ADC->REFCTRL.bit.REFSEL = ADC_REFCTRL_REFSEL_INTVCC2_Val;
+    ADC->INPUTCTRL.bit.MUXNEG = ADC_INPUTCTRL_MUXNEG_GND_Val;
+    ADC->CTRLC.bit.RESSEL = ADC_CTRLC_RESSEL_16BIT_Val;
+    ADC->AVGCTRL.bit.SAMPLENUM = ADC_AVGCTRL_SAMPLENUM_16_Val;
+    ADC->SAMPCTRL.bit.SAMPLEN = 0;
+    ADC->INTENSET.reg = ADC_INTENSET_RESRDY;
+    ADC->CTRLA.bit.ENABLE = 1;
+    _watch_sync_adc();
+    // throw away one measurement after reference change (the channel doesn't matter).
+    _watch_get_analog_value(ADC_INPUTCTRL_MUXPOS_SCALEDCOREVCC);
+}
 
+void watch_enable_analog_input(const uint8_t pin) {
     gpio_set_pin_direction(pin, GPIO_DIRECTION_OFF);
     switch (pin) {
         case A0:
-            gpio_set_pin_function(A0, PINMUX_PB04B_ADC_AIN12);
+            gpio_set_pin_function(pin, PINMUX_PB04B_ADC_AIN12);
             break;
         case A1:
-            gpio_set_pin_function(A1, PINMUX_PB01B_ADC_AIN9);
+            gpio_set_pin_function(pin, PINMUX_PB01B_ADC_AIN9);
             break;
         case A2:
-            gpio_set_pin_function(A2, PINMUX_PB02B_ADC_AIN10);
+            gpio_set_pin_function(pin, PINMUX_PB02B_ADC_AIN10);
+            break;
+        case A3:
+            gpio_set_pin_function(pin, PINMUX_PB03B_ADC_AIN11);
+            break;
+        case A4:
+            gpio_set_pin_function(pin, PINMUX_PB00B_ADC_AIN8);
             break;
         default:
             return;
     }
 }
+
+uint16_t watch_get_analog_pin_level(const uint8_t pin) {
+    switch (pin) {
+        case A0:
+            return _watch_get_analog_value(ADC_INPUTCTRL_MUXPOS_AIN12_Val);
+        case A1:
+            return _watch_get_analog_value(ADC_INPUTCTRL_MUXPOS_AIN9_Val);
+        case A2:
+            return _watch_get_analog_value(ADC_INPUTCTRL_MUXPOS_AIN10_Val);
+        case A3:
+            return _watch_get_analog_value(ADC_INPUTCTRL_MUXPOS_AIN11_Val);
+        case A4:
+            return _watch_get_analog_value(ADC_INPUTCTRL_MUXPOS_AIN8_Val);
+        default:
+            return 0;
+    }
+}
+
+void watch_set_num_analog_samples(uint16_t samples) {
+    // ignore any input that's not a power of 2 (i.e. only one bit set)
+    if (__builtin_popcount(samples) != 1) return;
+    // if only one bit is set, counting the trailing zeroes is equivalent to log2(samples)
+    uint8_t sample_val = __builtin_ctz(samples);
+    // make sure the desired value is within range and set it, if so.
+    if (sample_val <= ADC_AVGCTRL_SAMPLENUM_1024_Val) {
+        ADC->AVGCTRL.bit.SAMPLENUM = sample_val;
+        _watch_sync_adc();
+    }
+}
+
+void watch_set_analog_sampling_length(uint8_t cycles) {
+    // for clarity the API asks the user how many cycles they want the measurement to take.
+    // but the ADC always needs at least one cycle; it just wants to know how many *extra* cycles we want.
+    // so we subtract one from the user-provided value, and clamp to the maximum.
+    ADC->SAMPCTRL.bit.SAMPLEN = (cycles - 1) & 0x3F;
+    _watch_sync_adc();
+}
+
+inline void watch_disable_analog_input(const uint8_t pin) {
+    gpio_set_pin_function(pin, GPIO_PIN_FUNCTION_OFF);
+}
+
+inline void watch_disable_adc() {
+    ADC->CTRLA.bit.ENABLE = 0;
+    _watch_sync_adc();
+
+    MCLK->APBCMASK.reg &= ~MCLK_APBCMASK_ADC;
+}
diff --git a/watch-library/watch/watch_adc.h b/watch-library/watch/watch_adc.h
index d4620365..77a8bfb7 100644
--- a/watch-library/watch/watch_adc.h
+++ b/watch-library/watch/watch_adc.h
@@ -24,12 +24,82 @@
 ////< @file watch_adc.h
 
 /** @addtogroup adc Analog Input
-  * @brief This section covers functions related to the SAM L22's analog-to-digital converter, as well as
-  *        configuring and reading values from the three analog-capable pins on the 9-pin connector.
+  * @brief This section covers functions related to the SAM L22's analog-to-digital converter,
+  *        as well as configuring and reading values from the five analog-capable pins on the
+  *        9-pin connector.
   */
 /// @{
-/** @brief Enables the ADC peripheral, and configures the selected pin for analog input.
-  * @param pin One of pins A0, A1 or A2.
+/** @brief Enables the ADC peripheral. You must call this before attempting to read a value
+  *        from an analog pin.
   */
-void watch_enable_analog(const uint8_t pin);
+void watch_enable_adc();
+
+/** @brief Configures the selected pin for analog input.
+  * @param pin One of pins A0-A4.
+  */
+void watch_enable_analog_input(const uint8_t pin);
+
+/** @brief Reads an analog value from one of the pins.
+  * @param pin One of pins A0-A4.
+  * @return a 16-bit unsigned integer from 0-65535 representing the sampled value, unless you
+  *         have changed the number of samples. @see watch_set_num_analog_samples for details
+  *         on how that function changes the values returned from this one.
+  **/
+uint16_t watch_get_analog_pin_level(const uint8_t pin);
+
+/** @brief Sets the number of samples to accumulate when measuring a pin level. Default is 16.
+  * @param samples A power of 2 <= 1024. Specifically: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512
+                   or 1024. Any other value will be ignored.
+  * @details The SAM L22's ADC has a resolution of 12 bits. By default, the watch configures
+  *          the ADC to take 16 samples of the analog input and accumulate them in the result
+  *          register; this effectively gives us a 16-bit resolution, at the cost of taking 16
+  *          ADC cycles to complete a measurement. If you are measuring a slowly changing signal
+  *          like a thermistor output or an ambient light sensor this is probably fine, even
+  *          desirable. If you are measuring something a bit more fast-paced, like an analog
+  *          accelerometer, you may wish to exchange precision for speed. In this case you may
+  *          call this function to configure the ADC to accumulate fewer samples. HOWEVER! Note
+  *          that this may change the range of values returned from watch_get_analog_pin_level:
+  *            - For watch_set_num_analog_samples(1), the returned value will be 12 bits (0-4095).
+  *            - For watch_set_num_analog_samples(2), the returned value will be 13 bits (0-8191).
+  *            - For watch_set_num_analog_samples(4), the returned value will be 14 bits (0-16383).
+  *            - For watch_set_num_analog_samples(8), the returned value will be 15 bits (0-32767).
+  *         For sampling values over 16, the returned value will still be 16 bits (0-65535); the
+  *         ADC will automatically divide the measured value by whatever factor is necessary to fit
+  *         the result in 16 bits.
+  * @see watch_get_analog_pin_level
+  **/
+void watch_set_num_analog_samples(uint16_t samples);
+
+/** @brief Sets the length of time spent sampling, which allows measurement of higher impedance inputs.
+  *        Default is 1.
+  * @param cycles The number of ADC cycles to sample, between 1 and 64.
+  * @see this article by Thea Flowers: https://blog.thea.codes/getting-the-most-out-of-the-samd21-adc/
+  *      which is where I learned all of this.
+  * @details To measure an analog value, the SAM L22 must charge a capacitor to the analog voltage
+  *          presented at the input. This takes time. Importantly, the higher the input impedance,
+  *          the more time this takes. As a basic example: if you are using a thermistor tied to
+  *          VCC to measure temperature, the capacitor has to charge through the thermistor. The
+  *          higher the resistor value, the higher the input impedance, and the more time we need
+  *          to allow for the measurement. By default, the ADC is configured to run on a 500 kHz
+  *          clock with a sample time of one cycle. This is appropriate for an input impedance up
+  *          to about 28kΩ. Setting the sampling time to 4 cycles allows for an input impedance up
+  *          to 123kΩ. Setting the sampling time to the maximum of 64 cycles theoretically allows
+  *          for input impedance up to 2 MΩ. (I based these numbers on the calculator in the linked
+  *          blog post; it also has a ton of great info on the SAM D21 ADC, which is similar to the
+  *          SAM L22's).
+  **/
+void watch_set_analog_sampling_length(uint8_t cycles);
+
+/** @brief Disables the analog circuitry on the selected pin.
+  * @param pin One of pins A0-A4.
+  */
+void watch_disable_analog_input(const uint8_t pin);
+
+/** @brief Disables the ADC peripheral.
+  * @note You will need to call watch_enable_adc to re-enable the ADC peripheral. When you do, it will
+  *       have the default settings of 16 samples and 1 measurement cycle; if you customized these
+  *       parameters, you will need to set them up again.
+  **/
+void watch_disable_adc();
+
 /// @}
-- 
cgit v1.2.3