Skip to content

Commit

Permalink
Add document describing ADC free running mode
Browse files Browse the repository at this point in the history
Also add init_ADC1() to wiring.c for parts that have that.
  • Loading branch information
SpenceKonde committed Jul 8, 2020
1 parent 183e81c commit 728f234
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 16 deletions.
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
2.0.5
* Internal change to ADC initialization (saves a bit of flash) and init_ADC1() function for parts that have ADC1.
* Quick fix to naming of .hex and .lst files
2.0.4
* Switch to new and improved compiler toolchain - now get informative errors if sketch is too big on parts that use RJMP, bugfix for eeprom.h (not to be confused with EEPROM.h), various header improvements (board manager install only).
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ This core uses a simple scheme for assigning the Arduino pin numbers: Pins are n

#### Arduino Pin Numbers
When a single number is used to refer to a pin - in the documentation, or in your code - it is always the "Arduino pin number". These are the pin numbers shown in orange (for pins capable of analogRead()) and blue (for pins that are not) on the pinout charts. All of the other ways of referring to pins are #defined to the corresponding Arduino pin number.

#### An and PIN_An constants
The core also provides An and PIN_An constants (where n is a number from 0 to 11). These refer to the ADC0 *channel* numbers. This naming system is similar to what was used on many classic AVR cores - on some of those, it is used to simplify the code behind analogRead() - but here, they are just #defined as the corresponding Arduino pin number. These are not shown on the pinout charts, as this is a deprecated way of referring to pins. The mapping of analog channels to pins is shown in the the datasheet under the I/O Multiplexing Considerations chapter. There are additionally PIN_An defines for compatibility with the official cores - these likewise point to the digital pin number associated with the analog channel. Note that channel A0 is on the UPDI/Reset pin - however, even when configured as UPDI, it can be used as an input as long as the signals it can be exposed to do not look like the UPDI enable sequence.
The core also provides An and PIN_An constants (where n is a number from 0 to 11). These refer to the ADC0 *channel* numbers. This naming system is similar to what was used on many classic AVR cores **but here, they are just #defined as the corresponding Arduino pin number**. If you need to get the analog channel number on a digital pin, use the `digitalPinToAnalogInput(pin)` macro. The An numbers are not shown on the pinout charts - just use the digital pin numbers. The mapping of analog channels to pins is shown in the the datasheet under the I/O Multiplexing Considerations chapter, and reproduced in the [advanced ADC documentation page](megaavr/extras/ADCFreerunAndMore.md). Note that channel A0 is on the UPDI/Reset pin - however, even when configured as UPDI, it can be used as an input as long as the signals it can be exposed to do not look like the UPDI enable sequence.

### Serial (UART) Support
All of these parts have a single hardware serial port (UART). It works exactly like the one on official Arduino boards (except that there is no auto-reset, unless you are using Optiboot and have configured that pin to act as reset, or have wired up an "ersatz reset pin" as described above). See the pinout charts for the location of the serial pins.
Expand Down Expand Up @@ -222,7 +223,7 @@ ADC0.SAMPCTRL=0; //minimum sampling length = 0+2 = 2 ADC clock cycles

With the minimum sampling length, analogRead() speed would be approximately doubled from it's already-faster value.

**Note:** The 3217,1617,3216,1616, and 1614 have a second ADC, ADC1. On the 20 and 24-pin parts, these could be used to provide analogRead() on additional pins (it can also read from DAC2). Currently, there are no plans to implement this in the core due to the large number of currently available pins. Instead, it is recommended that users who wish to "take over" and ADC to use it's more advanced functionality choose ADC1 for this purpose. In the future, examples showing use of ADC1 in this way may be published.
**Note:** The 3217,1617,3216,1616, and 1614 have a second ADC, ADC1. On the 20 and 24-pin parts, these could be used to provide analogRead() on additional pins (it can also read from DAC2). Currently, there are no plans to implement this in the core due to the large number of currently available pins. Instead, it is recommended that users who wish to "take over" an ADC to use it's more advanced functionality choose ADC1 for this purpose. In the future, examples showing use of ADC1 in this way may be published. As of 2.0.5, megaTinyCore provides a function `init_ADC1()` which initializes ADC1 in the same way that ADC0 is (with correct prescaler for clock speed and VDD reference).

### DAC Support
The 1-series parts have an 8-bit DAC which can generate a real analog voltage (note that this provides very low current and can only be used as a voltage reference, it cannot be used to power other devices). This generates voltages between 0 and the selected VREF (which cannot be VCC, unfortunately). In 2.0.0 and later, set the DAC reference voltage via the DACReference() function - pass it one of the INTERNAL reference options listed under the ADC section above. In versions prior to 2.0.0, select the DAC VREF voltage from the Tools -> DAC Voltage Reference submenu. This voltage must be lower than Vcc to get the correct voltages. Call analogWrite() on the DAC pin to set the voltage to be output by the DAC. To turn off the DAC output, call digitalWrite() on that pin.
Expand Down
4 changes: 4 additions & 0 deletions megaavr/cores/arduino/api/Common.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ int8_t digitalRead(pin_size_t pinNumber);
void DACReference(uint8_t mode);
#endif

#ifdef ADC0

This comment has been minimized.

Copy link
@MCUdude

MCUdude Jul 9, 2020

Contributor

This must be a typo...

This comment has been minimized.

Copy link
@SpenceKonde

SpenceKonde via email Jul 9, 2020

Author Owner

This comment has been minimized.

Copy link
@MCUdude

MCUdude Jul 11, 2020

Contributor

if defined ADC0, initialize ADC1?

This comment has been minimized.

Copy link
@SpenceKonde

SpenceKonde Jul 12, 2020

Author Owner

aaaaha - oops. Thanks.

I am... surprised that it lets one compile for parts with ADC0 but not ADC1, as the actual function definition isn't present there....

void init_ADC1(void);
#endif

#ifndef DISABLEMILLIS
unsigned long millis(void);
void init_millis();
Expand Down
64 changes: 53 additions & 11 deletions megaavr/cores/arduino/wiring.c
Original file line number Diff line number Diff line change
Expand Up @@ -595,35 +595,34 @@ void init()
compensate for this! */

#if F_CPU >= 12000000 // 16 MHz / 16 = 1 MHz, 20 MHz / 16 = 1.25 MHz
ADC0.CTRLC |= ADC_PRESC_DIV16_gc;
ADC0.CTRLC = ADC_PRESC_DIV16_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 6000000 // 8 MHz / 8 = 1 MHz, 10 MHz / 64 = 1.25 MHz
ADC0.CTRLC |= ADC_PRESC_DIV8_gc;
ADC0.CTRLC = ADC_PRESC_DIV8_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 3000000 // 4 MHz / 32 = 1 MHz, 5 MHz / 32 = 1.25 MHz
ADC0.CTRLC |= ADC_PRESC_DIV4_gc;
ADC0.CTRLC = ADC_PRESC_DIV4_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#else // 1 MHz / 2 = 500 kHz - the lowest setting
ADC0.CTRLC |= ADC_PRESC_DIV2_gc;
ADC0.CTRLC = ADC_PRESC_DIV2_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#endif
ADC0.SAMPCTRL=14; //16 ADC clock sampling time - should be about the same amount of *time* as originally?
#else //if SLOWADC is defined - as of 2.0.0 this option isn't exposed.
/* ADC clock around 125 kHz - datasheet spec's 50 kHz to 1.5 MHz */
#if F_CPU >= 16000000 // 16 MHz / 128 = 125 kHz, 20 MHz / 128 = 156.250 kHz
ADC0.CTRLC |= ADC_PRESC_DIV128_gc;
ADC0.CTRLC = ADC_PRESC_DIV128_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 8000000 // 8 MHz / 64 = 125 kHz, 10 MHz / 64 = 156.25 KHz
ADC0.CTRLC |= ADC_PRESC_DIV64_gc;
ADC0.CTRLC = ADC_PRESC_DIV64_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 4000000 // 4 MHz / 32 = 125 kHz, 5 MHz / 32 = 156.25 KHz
ADC0.CTRLC |= ADC_PRESC_DIV32_gc;
ADC0.CTRLC = ADC_PRESC_DIV32_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 2000000 // 2 MHz / 16 = 125 kHz - note that megaTinyCore does not provide support for 2 MHz
ADC0.CTRLC |= ADC_PRESC_DIV16_gc;
ADC0.CTRLC = ADC_PRESC_DIV16_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 1000000 // 1 MHz / 8 = 125 kHz
ADC0.CTRLC |= ADC_PRESC_DIV8_gc;
ADC0.CTRLC = ADC_PRESC_DIV8_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#else // 128 kHz / 2 = 64 kHz -> This is the closest you can get, the prescaler is 2
ADC0.CTRLC |= ADC_PRESC_DIV2_gc;
ADC0.CTRLC = ADC_PRESC_DIV2_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#endif
#endif

/* Enable ADC */
ADC0.CTRLA |= ADC_ENABLE_bm;
analogReference(VDD);
#endif

#ifdef __AVR_ATtinyxy2__
Expand All @@ -640,6 +639,49 @@ void init()
sei();
}

#ifdef ADC1
void init_ADC1(){
#ifndef SLOWADC
/* ADC clock 1 MHz to 1.25 MHz at frequencies supported by megaTinyCore
Unlike the classic AVRs, which demand 50~200 kHz, for these, the datasheet
spec's 50 kHz to 1.5 MHz. We hypothesize that lower clocks provide better
response to high impedance signals, since the sample and hold circuit will
be connected to the pin for longer, though the datasheet does not explicitly
state that this is the case. However, we can use the SAMPLEN register to
compensate for this! */

#if F_CPU >= 12000000 // 16 MHz / 16 = 1 MHz, 20 MHz / 16 = 1.25 MHz
ADC1.CTRLC = ADC_PRESC_DIV16_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 6000000 // 8 MHz / 8 = 1 MHz, 10 MHz / 64 = 1.25 MHz
ADC1.CTRLC = ADC_PRESC_DIV8_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 3000000 // 4 MHz / 32 = 1 MHz, 5 MHz / 32 = 1.25 MHz
ADC1.CTRLC = ADC_PRESC_DIV4_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#else // 1 MHz / 2 = 500 kHz - the lowest setting
ADC1.CTRLC = ADC_PRESC_DIV2_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#endif
ADC1.SAMPCTRL=14; //16 ADC clock sampling time - should be about the same amount of *time* as originally?
#else //if SLOWADC is defined - as of 2.0.0 this option isn't exposed.
/* ADC clock around 125 kHz - datasheet spec's 50 kHz to 1.5 MHz */
#if F_CPU >= 16000000 // 16 MHz / 128 = 125 kHz, 20 MHz / 128 = 156.250 kHz
ADC1.CTRLC = ADC_PRESC_DIV128_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 8000000 // 8 MHz / 64 = 125 kHz, 10 MHz / 64 = 156.25 KHz
ADC1.CTRLC = ADC_PRESC_DIV64_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 4000000 // 4 MHz / 32 = 125 kHz, 5 MHz / 32 = 156.25 KHz
ADC1.CTRLC = ADC_PRESC_DIV32_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 2000000 // 2 MHz / 16 = 125 kHz - note that megaTinyCore does not provide support for 2 MHz
ADC1.CTRLC = ADC_PRESC_DIV16_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#elif F_CPU >= 1000000 // 1 MHz / 8 = 125 kHz
ADC1.CTRLC = ADC_PRESC_DIV8_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#else // 128 kHz / 2 = 64 kHz -> This is the closest you can get, the prescaler is 2
ADC1.CTRLC = ADC_PRESC_DIV2_gc|ADC_REFSEL_VDDREF_gc|ADC_SAMPCAP_bm;;
#endif
#endif

/* Enable ADC */
ADC1.CTRLA |= ADC_ENABLE_bm;
}
#endif

void setup_timers() {

/* TYPE A TIMER */
Expand Down
114 changes: 111 additions & 3 deletions megaavr/extras/ADCFreerunAndMore.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,117 @@ Free-running mode is also particularly useful if you need to do something if the

## Enabling free-running mode

```
ADC0.MUXPOS=0x06; //reads from PA6/arduino pin 2, ADC0 channel 6
ADC0.CTRLA=ADC_ENABLE_bm|ADC_FREERUN_bm; //start in freerun
ADC0.COMMAND=ADC_STCONV_bm; //start first conversion!
```

The MUXPOS is the number of the analog channel that you want to read in free running mode. See the table below - you can also use digitalPinToAnalogInput(p) - obviously this only works for ADC0.

Pin | 8-pin | 14-pin | 20-pin | 24-pin | ADC0 | ADC1
--------|-------|--------|--------|--------|-------|----
PIN_PA0 | 5 | 11 | 17 | 21 | AIN0 | -
PIN_PA1 | 2 | 8 | 14 | 18 | AIN1 | -
PIN_PA2 | 3 | 9 | 15 | 19 | AIN2 | -
PIN_PA3 | 4 | 10 | 16 | 20 | AIN3 | -
PIN_PA4 | - | 0 | 0 | 0 | AIN4 | AIN0
PIN_PA5 | - | 1 | 1 | 1 | AIN5 | AIN1
PIN_PA6 | 0 | 2 | 2 | 2 | AIN6 | AIN2
PIN_PA7 | 1 | 3 | 3 | 3 | AIN7 | AIN3
PIN_PB7 | - | - | - | 4 | - | AIN4
PIN_PB6 | - | - | - | 5 | - | AIN5
PIN_PB5 | - | - | 4 | 6 | AIN8 | -
PIN_PB4 | - | - | 5 | 7 | AIN9 | -
PIN_PB5 | - | 4 | 6 | 8 | - | -
PIN_PB4 | - | 5 | 7 | 9 | - | -
PIN_PB1 | - | 6 | 8 | 10 | AIN10 | -
PIN_PB0 | - | 7 | 9 | 11 | AIN11 | -
PIN_PC0 | - | - | 10 | 12 | - | AIN6
PIN_PC1 | - | - | 11 | 13 | - | AIN7
PIN_PC2 | - | - | 12 | 14 | - | AIN8
PIN_PC3 | - | - | 13 | 15 | - | AIN9
PIN_PC4 | - | - | - | 16 | - | AIN10
PIN_PC5 | - | - | - | 17 | - | AIN11

## Reading Results
When using the ADC in free running mode, you must write your own code to get the value. Typically, you want to grab the most recent value; one decision to make is whether you want to wait for a reading if you have already read the value since the last reading was taken, or if that doesn't matter.

Reading the value when you want to make sure you have a fresh value is straightforward:
```
uint16_t getAnalogValue() {
while (!(ADC0.INTFLAGS & ADC_RESRDY_bm)) { // Check for the RESRDY flag (Result Ready)
;//spin if there's no new result
}
return ADC0.RES; // return the current result on channel A - this clears the RESRDY flag
}
```

This comment has been minimized.

Copy link
@MCUdude

MCUdude Jul 9, 2020

Contributor

Pro tip: add a c or cpp right after the three apostrophes to tell the markdown parser that the code you want to display is C or C++ code. You'll get color highlighting! This works for a ton of other languages as well.

 uint16_t getAnalogValue() {
   while (!(ADC0.INTFLAGS & ADC_RESRDY_bm)) { // Check for the RESRDY flag (Result Ready)
     ;//spin if there's no new result
   }
   return ADC0.RES; // return the current result on channel A - this clears the RESRDY flag
 }
If you don't, it's even simpler:
```
uint16_t getAnalogValue() {
return ADC0.RES; // return the most recent result on channel A
}
```

Alternately, you may wish to use an ISR when the ADC conversion is complete - though be aware that with the ADC so much faster on these parts, you may find yourself dealing with more interrupts than you were expecting (unless you are using the accumulation funciton, see below). This makes sense when there's a clear thing that you want to do with each result - just be sure that whatever it is, it's fast (for example, if you're controlling PWM duty cycle, write the register directly, don't use analogWrite()).

```
// during setup proceedures, before starting freerunning mode conversions.
ADC0.INTCTRL=ADC_RESRDY_bm; //enable the interrupt
// The ISR
ISR(ADC0_RESRDY_vect) {
useADCValue(ADC0.RES); //reading ADC0.RES clears the RESRDY flag.
// If for some reason you're not reading the value, though, you need to manually clear the flag by writing a 1 to it.
}
```

## Turning off free-running mode
To stop taking samples in free running mode, simply clear the `ADC_FREERUN_bm` bit in `ADCn.CTRLA`. Be aware that an "extra" reading is always taken when disabling free running mode. See [silicon errata](Errata.md) for more details.

## Changing channel, other settings in free running mode
It is recommended to stop free running mode prior to changing the channel. On some tinyAVR parts, most ADC settings cannot be changed without first turning off free running mode. See [the table of silicon errata](Errata.md)

# Automatic Accumulation
One of the most common applications of multiple consecutive ADC reads is to sum multiple readings to take an average, or for the purpose of inccreasing accuracy through the technique of oversampling and decimation. The modern AVR devices have an automatic accumulator feature to do this for you to make this process easier; it is controlled by `ADCn.CTRLB`. This is the only thing controlled by this register, so you can set it directly. Valid options are:
* `ADC_SAMPNUM_ACC1_gc`
* `ADC_SAMPNUM_ACC2_gc`
* `ADC_SAMPNUM_ACC4_gc`
* `ADC_SAMPNUM_ACC8_gc`
* `ADC_SAMPNUM_ACC16_gc`
* `ADC_SAMPNUM_ACC32_gc`
* `ADC_SAMPNUM_ACC64_gc`

When the conversion is complete, you can read it from the `ADCn.RES` register. This can be used either with the builtin analogRead() functions (set this register before using analogRead()), in free running mode, or manually. Because taking large numbers of samples in this way can take a while, manually starting a conversion after setting an interrupt on RESRDY may be of particular use here.

# Oversampling and decimation
If you need more accuracy - assuming the signal is changing (it won't work if you get the same result every time - you'll just get that value out) - you can use the technique of "oversampling and decimation" - for each extra bit of accuracy you want, take 4 times as many samples, and then right-shift the result one bit. For example, if we want 1e3 bits of accuracy from a 10-bit ADC:

```
// global variables:
volatile uint16_t latest_reading=0; // remember to disabe interrupts while reading this so it doesn't get changed by the ISR while you're halfway through reading it.
// Setup:
ADC0.CTRLB=ADC_SAMPNUM_ACC64_gc; //take 64 samples for 3 extra bits
ADC0.MUXPOS=0x06; //reads from PA6/arduino pin 2, ADC0 channel 6
ADC0.CTRLA=ADC_ENABLE_bm|ADC_FREERUN_bm; //start in freerun
ADC0.COMMAND=ADC_STCONV_bm; //start first conversion!
// These readings will take forever (1.5-1.8ms), so we will keep that global updated via an ISR:
ISR(ADC_RESRDY_vect) {
uint16_t raw_reading=ADC0.RES;
latest_reading=raw_reading>>3; // 0~8191
}
```

Note that future updates of megaTinyCore may provide an automated way of configuring analogRead() to do this via an extension to analogReadResolution() - though the need for non-blocking code may render this inappropriate.

# Faster ADC conversions? Extra-high-impedance sources?
On classic AVRs, the ADC wanted to be clocked at 100-200kHz, and sampled for 1.5 ADC clocks; hence, the sampling time was 15-30 uS. On the tinyAVR 0/1-series, the ADC clock can be up to 1.5 MHz. megaTinyCore configures it for 1 MHz on 16 MHz derived clocks, and 1.25 MHz on 20 MHz derived clocks. These parts also provide the ADCn.SAMPCTRL register to allow a longer sampling time - the sampling time is 2+ADCn.SAMPCTRL. By default, megaTinyCore configures this for 14. 14+2 = 16; this is a conservative attempt to ensure that the accuracy on modern AVR parts does not suffer with higher-impedance sources, particularly ones that worked with classic AVRs. Since the faster clock permits much faster conversions anyway, this was considered an acceptable limitation for the defaut. Because the sampling cap is smaller on these parts, though, this is probably significantly higher than it needs to be. For low impedance sources, this can be cranked all the way back to 0 if conversion speed is of particular importance - this will reduce the conversion time from 29 ADC clocks (29us or 23us for 16 and 20 MHz derived clocks, respectively) to 13 (13us or 10.5us). By comparison, on classic AVRs, the ADC conversion time was between 65u and 130us (104us on most common clock speeds).

##
On the other hand, if you are reading a particularly high impedance source, you may wish to *increase* the sampling time instead. SAMPCTRL can be set as high as 31, which would give a sampling time of 33 ADC clocks (conversion time 46 ADC clocks - 46 or 37 us).

# Faster ADC conversions?
If the signal you are measuring is low impedance, you
# ADC1 on ATtiny1614, 1616, 1617, 3216, 3217
These parts have a second ADC, just like the first - everything about it is identical except the pin assignments. As of 2.0.5, megaTinyCore provides a function `init_ADC1()` that can be called to initialize ADC1 the same way as ADC0 is initialized. This is particularly convenient if writing library code which may run on multiple clock speeds, as you dont have to duplicate the calculation of an appropriate prescaler based on F_CPU. Because it is fully independent, it is well suited to use in the background, and/or for long-running accumulated readings.

0 comments on commit 728f234

Please sign in to comment.