diff --git a/Makefile b/Makefile index bc4e8e53..8096e439 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,7 @@ INCLUDES += \ # Add your source files here. SRCS += \ + ./watch-library/hardware/watch/watch_adc.c \ ./watch-library/hardware/watch/watch_extint.c \ ./watch-library/hardware/watch/watch_gpio.c \ ./watch-library/hardware/watch/watch_rtc.c \ diff --git a/app.c b/app.c index 7b7fec6b..3da6b45e 100644 --- a/app.c +++ b/app.c @@ -1,3 +1,4 @@ +#include #include "app.h" #include "watch.h" #include "delay.h" @@ -6,12 +7,15 @@ void app_init(void) { } void app_setup(void) { - watch_enable_leds(); + watch_enable_adc(); watch_enable_display(); } bool app_loop(void) { - watch_display_main_line("123456"); + uint16_t vcc = watch_get_vcc_voltage(); + char buf[7]; + snprintf(buf, 7, "%6d", vcc); + watch_display_main_line(buf); - return true; + return false; } \ No newline at end of file diff --git a/watch-library/hardware/watch/watch_adc.c b/watch-library/hardware/watch/watch_adc.c index 476f0cbb..11cc318c 100644 --- a/watch-library/hardware/watch/watch_adc.c +++ b/watch-library/hardware/watch/watch_adc.c @@ -23,152 +23,45 @@ */ #include "watch_adc.h" -#include "driver_init.h" - -static void _watch_sync_adc(void) { - while (ADC->SYNCBUSY.reg); -} - -static 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); // wait for "result ready" flag - - return ADC->RESULT.reg; -} +#include "adc.h" void watch_enable_adc(void) { - MCLK->APBCMASK.reg |= MCLK_APBCMASK_ADC; - GCLK->PCHCTRL[ADC_GCLK_ID].reg = GCLK_PCHCTRL_GEN_GCLK0 | GCLK_PCHCTRL_CHEN; + adc_init(); + adc_enable(); +} - 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_input(const uint16_t port_pin) { + uint8_t port = port_pin >> 8; + uint16_t pin = port_pin & 0xF; - 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; + PORT->Group[port].DIRCLR.reg = (1 << pin); + PORT->Group[port].PINCFG[pin].reg |= PORT_PINCFG_INEN; + PORT->Group[port].PINCFG[pin].reg &= ~PORT_PINCFG_PULLEN; + PORT->Group[port].PINCFG[pin].reg |= PORT_PINCFG_PMUXEN; + if (pin & 1) { + PORT->Group[port].PMUX[pin>>1].bit.PMUXO = HAL_GPIO_PMUX_ADC; } 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(pin, PINMUX_PB04B_ADC_AIN12); - break; - case A1: - gpio_set_pin_function(pin, PINMUX_PB01B_ADC_AIN9); - break; - case A2: - 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; + PORT->Group[port].PMUX[pin>>1].bit.PMUXE = HAL_GPIO_PMUX_ADC; } } -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; - } +uint16_t watch_get_analog_pin_level(const uint16_t pin) { + return adc_get_analog_value(pin); } -void watch_set_analog_num_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(); -} - -static inline uint32_t _watch_adc_get_reference_voltage(const watch_adc_reference_voltage reference) { - switch (reference) { - case ADC_REFERENCE_INTREF: - return ADC_REFCTRL_REFSEL_INTREF_Val; - break; - case ADC_REFERENCE_VCC_DIV1POINT6: - return ADC_REFCTRL_REFSEL_INTVCC0_Val; - break; - case ADC_REFERENCE_VCC_DIV2: - return ADC_REFCTRL_REFSEL_INTVCC1_Val; - break; - case ADC_REFERENCE_VCC: - return ADC_REFCTRL_REFSEL_INTVCC2_Val; - break; - } - - return 0; -} - -void watch_set_analog_reference_voltage(watch_adc_reference_voltage reference) { +/// TODO: put reference voltage stuff into gossamer? +void _watch_set_analog_reference_voltage(uint8_t reference); +void _watch_set_analog_reference_voltage(uint8_t reference) { ADC->CTRLA.bit.ENABLE = 0; - if (reference == ADC_REFERENCE_INTREF) SUPC->VREF.bit.VREFOE = 1; + if (reference == ADC_REFCTRL_REFSEL_INTREF_Val) SUPC->VREF.bit.VREFOE = 1; else SUPC->VREF.bit.VREFOE = 0; - ADC->REFCTRL.bit.REFSEL = _watch_adc_get_reference_voltage(reference); + ADC->REFCTRL.bit.REFSEL = reference; ADC->CTRLA.bit.ENABLE = 1; - _watch_sync_adc(); + while (ADC->SYNCBUSY.reg); // throw away one measurement after reference change (the channel doesn't matter). - _watch_get_analog_value(ADC_INPUTCTRL_MUXPOS_SCALEDCOREVCC); + adc_get_analog_value_for_channel(ADC_INPUTCTRL_MUXPOS_SCALEDCOREVCC); } uint16_t watch_get_vcc_voltage(void) { @@ -176,24 +69,26 @@ uint16_t watch_get_vcc_voltage(void) { uint8_t oldref = ADC->REFCTRL.bit.REFSEL; // if we weren't already using the internal reference voltage, select it now. - if (oldref != ADC_REFERENCE_INTREF) watch_set_analog_reference_voltage(ADC_REFERENCE_INTREF); + if (oldref != ADC_REFCTRL_REFSEL_INTREF_Val) _watch_set_analog_reference_voltage(ADC_REFCTRL_REFSEL_INTREF_Val); // get the data - uint32_t raw_val = _watch_get_analog_value(ADC_INPUTCTRL_MUXPOS_SCALEDIOVCC_Val); + uint32_t raw_val = adc_get_analog_value_for_channel(ADC_INPUTCTRL_MUXPOS_SCALEDIOVCC_Val); // restore the old reference, if needed. - if (oldref != ADC_REFERENCE_INTREF) watch_set_analog_reference_voltage(oldref); + if (oldref != ADC_REFCTRL_REFSEL_INTREF_Val) _watch_set_analog_reference_voltage(oldref); return (uint16_t)((raw_val * 1000) / (1024 * 1 << ADC->AVGCTRL.bit.SAMPLENUM)); } -inline void watch_disable_analog_input(const uint8_t pin) { - gpio_set_pin_function(pin, GPIO_PIN_FUNCTION_OFF); +inline void watch_disable_analog_input(const uint16_t port_pin) { + uint8_t port = port_pin >> 8; + uint16_t pin = port_pin & 0xF; + + PORT->Group[port].DIRSET.reg = (1 << pin); \ + PORT->Group[port].PINCFG[pin].reg &= ~(PORT_PINCFG_PULLEN | PORT_PINCFG_INEN); \ + PORT->Group[port].PINCFG[pin].reg &= ~PORT_PINCFG_PMUXEN; \ } inline void watch_disable_adc(void) { - ADC->CTRLA.bit.ENABLE = 0; - _watch_sync_adc(); - - MCLK->APBCMASK.reg &= ~MCLK_APBCMASK_ADC; + adc_disable(); } diff --git a/watch-library/shared/watch/watch.h b/watch-library/shared/watch/watch.h index 01606b44..92504913 100644 --- a/watch-library/shared/watch/watch.h +++ b/watch-library/shared/watch/watch.h @@ -65,7 +65,7 @@ typedef void (*watch_cb_t)(void); #include "watch_slcd.h" #include "watch_extint.h" #include "watch_tcc.h" -// #include "watch_adc.h" +#include "watch_adc.h" #include "watch_gpio.h" // #include "watch_i2c.h" // #include "watch_spi.h" diff --git a/watch-library/shared/watch/watch_adc.h b/watch-library/shared/watch/watch_adc.h index ea4fa9e3..b2685da9 100644 --- a/watch-library/shared/watch/watch_adc.h +++ b/watch-library/shared/watch/watch_adc.h @@ -21,8 +21,9 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -#ifndef _WATCH_ADC_H_INCLUDED -#define _WATCH_ADC_H_INCLUDED + +#pragma once + ////< @file watch_adc.h #include "watch.h" @@ -39,95 +40,17 @@ void watch_enable_adc(void); /** @brief Configures the selected pin for analog input. - * @param pin One of pins A0-A4. + * @param pin One of the analog pins, access using the HAL_GPIO_Ax_pin() macro. */ -void watch_enable_analog_input(const uint8_t pin); +void watch_enable_analog_input(const uint16_t pin); /** @brief Reads an analog value from one of the pins. - * @param pin One of pins A0-A4. + * @param pin One of the analog pins, access using the HAL_GPIO_Ax_pin() macro. * @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_analog_num_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); - -typedef enum { - ADC_REFERENCE_INTREF = 0, - ADC_REFERENCE_VCC_DIV1POINT6, - ADC_REFERENCE_VCC_DIV2, - ADC_REFERENCE_VCC, -} watch_adc_reference_voltage; - - -/** @brief Selects the reference voltage to use for analog readings. Default is ADC_REFERENCE_VCC. - * @param reference One of ADC_REFERENCE_VCC, ADC_REFERENCE_VCC_DIV1POINT6, ADC_REFERENCE_VCC_DIV2 - * or ADC_REFERENCE_INTREF. - * @details In order to turn an analog voltage into a 16-bit integer, the ADC needs to compare the - * measured voltage to a reference point. For example, if you were powering the watch with - * VCC == 3.0V and you had two 10K resistors connected in series from 3V to GND, you could - * expect to get 3 volts when you measure the top of the voltage divider, 0 volts at the - * bottom, and 1.5 volts in the middle. If you read these values uising a reference voltage - * of ADC_REFERENCE_VCC, the top value would be about 65535, the bottom about 0, and the - * middle about 32768. However! If we used ADC_REFERENCE_VCC_DIV2 as our reference, we would - * expect to get 65535 both at the top and the middle, because the largest value the ADC can - * measure in this configutation is 1.5V (VCC / 2). - * - * By changing the reference voltage from ADC_REFERENCE_VCC to ADC_REFERENCE_VCC_DIV1POINT6 - * or ADC_REFERENCE_VCC_DIV2, you can get more resolution when measuring small voltages (i.e. - * a phototransistor circuit in low light). - * - * There is also a special reference voltage called ADC_REFERENCE_INTREF. The SAM L22's - * Supply Controller provides a selectable voltage reference (by default, 1.024 V) that you - * can select as a reference voltage for ADC conversions. Unlike the three references we - * talked about in the last paragraph, this reference voltage does not depend on VCC, which - * makes it very useful for measuring the battery voltage (since you can't really compare - * VCC to itself). You can change the INTREF voltage to 2.048 or 4.096 V by poking at the - * supply controller's VREF register, but the watch library does not support this use case. - **/ -void watch_set_analog_reference_voltage(watch_adc_reference_voltage reference); +uint16_t watch_get_analog_pin_level(const uint16_t pin); /** @brief Returns the voltage of the VCC supply in millivolts (i.e. 3000 mV == 3.0 V). If running on * a coin cell, this will be the battery voltage. @@ -144,7 +67,7 @@ uint16_t watch_get_vcc_voltage(void); /** @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); +void watch_disable_analog_input(const uint16_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 @@ -154,4 +77,3 @@ void watch_disable_analog_input(const uint8_t pin); void watch_disable_adc(void); /// @} -#endif