At what V level does an analog (A2) input fire an interrupt? (pinMode(A2, INPUT_PULLDOWN);

I’m building a energy reading device to attached to my houses AC service meter. It’s been done before, but i’m ignoring those designs and trying my own approaches. (reinvent the wheel)

I decided to use hardware interrupts even though I could poll fast enough in code, but then couldn’t see how to accomplish anything else in code if I did.

At first I used the D2 as interrupt input. I connected a 4v square wave at 10% duty cycle and got it to work. Using it, I was able to test my KWattHours code, and know that at least the very basic parts worked.

Because my IR trigger is only about 1v, I tried using an anlalog pin, hoping I could pick off a lower level (than 4v) trigger level. I tried using A2 to trigger the iput. It worked a 2v peek test signal as did D1, but not a 1v level.

Now the question. Can a analog (A2) input be set for a lower firing level than 2v? I was actually surprised that it did work at 2v, so I thought there might be a way to adjust this threshold.
Any ideas?

FYI: I love my DSO Nano v3 o’scope! It’s tiny and has a square wave generator on it. Very handy. It generates square waves from 10Hz to 1MHz and 10% to 90% Duty cycle. $85, wow.


You are actually not using an analog input when you set the pin to INPUT_PULLDOWN; it will be a digital input under those conditions. The minimum voltage need to cause a HIGH reading is given in the docs in the I/O Characteristics section of the device data sheet. For the Photon, the formula is,

0.41 * (V3v3 - 2) +1.3 If the V3v3 voltage is 3.3 volts, then the minimum is 1.83 volts (max would be 3.6 volts)

As far as I know, there is no way to adjust this threshold; it’s based on the electrical characteristics of the circuit, not some software construct. You probably don’t want to be right at the minimum either; I would probably stick with 3 volts, though 2 might be safe.

Thanks very much. I’ll check out the data sheets.

You could add an opamp comparator circuit to give you a nice 3.3 edge when your input signal exceedes a adjustable value.

I know other µC have the ability to trigger interrupts via internal comparators comparing two input signals, and I would be surprised if the STM32 used here would not have that feature, but that’s rather bare metal programming and not supported in the Wiring framework.

I wrote some code. You should not use it. Well, maybe you can, but I sort of hacked this together quickly to see if you can do an interrupt-based level trigger on an analog pin. The answer is, yes, I think you can, using the STM32F2xx Analog Watchdog feature.

But you can really only have one because there is only one extra ADC. And it only works on pins A1, A2, and A7 because those are the only ones connected to ADC3. But it seems to work. You can have it trigger on RISING, FALLING, or CHANGE, on any value along with a hysteresis amount.

It’s kind of cool; I was surprised I could get it to work. I tested it on unmodified system firmware 0.5.1 and it seems to work for me, but there’s no guarantee it will work on future system versions and certainly not future hardware!

#include "Particle.h"

// Additional includes for hardware access
#include "adc_hal.h"
#include "gpio_hal.h"
#include "pinmap_hal.h"
#include "pinmap_impl.h"

// STM32F2xx Analog Watchdog Example
// Basically, this works sort of like a regular attachInterrupt digital pin interrupt, except it
// works with analog signals!
// You can only use one of these because there is only one extra ADC to run this on, ADC3.
// Also, it can only use pins A1, A2, or A7 on the Photon because those are the only pins that can be used with ADC3.
// It works for RISING, FALLING, or CHANGE, depending on the mode value that you set.
// Note that there are also two values: value and hysteresis. The latter is necessary because the analog signal rarely
// goes cleanly from one state to another, so the hysteresis value prevents the signal from triggering multiple times
// when transitioning states. The default value is 75.

class AnalogInterrupt {
	AnalogInterrupt(int pin, InterruptMode mode, raw_interrupt_handler_t handler, int value, int hysteresis = 75);
	virtual ~AnalogInterrupt();

	void start();
	void setValue(int value);

	void updateLimits();
	void interruptHandler();
	static void interruptHandlerStatic();

	int pin;
	InterruptMode mode;
	raw_interrupt_handler_t handler;
	int value;
	uint8_t channel = 0;
	bool wasBelow;
	int hysteresis;

	// We store this so the system interrupt handler can find the object. This means there can only be
	// one of these objects, but that's a reasonable assumption because there is only one ADC3 in hardware,
	// so you can't have more than one of these, anyway.
	static AnalogInterrupt *instance;

// Allocation of the static class member, essentially a global variable
AnalogInterrupt *AnalogInterrupt::instance;

AnalogInterrupt::AnalogInterrupt(int pin, InterruptMode mode, raw_interrupt_handler_t handler, int value, int hysteresis) :
		pin(pin), mode(mode), handler(handler), value(value), hysteresis(hysteresis) {

	// These are the pin to channel assigments for the Photon:
	// A0 = ADC_Channel_15 = PC5 = ADC12_IN15
	// A1 = ADC_Channel_13 = PC3 = ADC123_IN13
	// A2 = ADC_Channel_12 = PC2 = ADC123_IN12
	// A3 = ADC_Channel_5  = PA5 = ADC12_IN5
	// A4 = ADC_Channel_6  = PA6 = ADC12_IN6
	// A5 = ADC_Channel_7  = PA7 = ADC12_IN7
	// A6 = ADC_Channel_4  = PA4 = ADC12_IN4, DAC_OUT1
	// A7 = ADC_Channel_0  = PA0 = ADC123_IN0, WKUP

	// Since we use ADC3 so as to not conflict with normal ADC operations on ADC1 and ADC2, this
	// restricts the pins we can monitor to A1, A2 and A7 (WKP).
	switch(pin) {
	case A1:
		channel = ADC_Channel_13;

	case A2:
		channel = ADC_Channel_12;

	case A7:
		channel = ADC_Channel_0;
	instance = this;
AnalogInterrupt::~AnalogInterrupt() {


void AnalogInterrupt::start() {
	ADC_InitTypeDef ADC_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;

	// Add the system interrupt handler
	attachSystemInterrupt(SysInterrupt_ADC_IRQ, interruptHandlerStatic);

	// Set the pin to analog input
	pinMode(pin, AN_INPUT);
	wasBelow = (analogRead(pin) < value);

	// Useful example code

	// Enable the HSI

	// Wait until HSI oscillator is ready
	while(RCC_GetFlagStatus(RCC_FLAG_HSIRDY) == RESET) {

	//This needs to change if using an ADC other than ADC3
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC3, ENABLE);

	// ADC Configuration
	ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
	ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_NbrOfConversion = 1;
	ADC_Init(ADC3, &ADC_InitStructure);

	// Typically ADC3 = ADC3 and ADC3_CHANNEL = ADC_Channel_13 (pin A1)
	ADC_RegularChannelConfig(ADC3, channel, 1, ADC_SampleTime_15Cycles);

	// Configure high and low analog watchdog thresholds

	// Configure channel31 as the single analog watchdog guarded channel
	ADC_AnalogWatchdogSingleChannelConfig(ADC3, channel);

	// Enable analog watchdog on one regular channel
	ADC_AnalogWatchdogCmd(ADC3, ADC_AnalogWatchdog_SingleRegEnable);

	// There's one interrupt for ADC1-3, so it's not necessary to separate that out based on ADC number
	NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

	// Enable AWD interrupt

	// Enable ADC

	// Sample code for the STM32F1xx checks the ADC_FLAG_ADONS here for ready, but I don't
	// this that's necessary on the STM32F2xx

	// Start ADC Software Conversion

void AnalogInterrupt::setValue(int value) {
	this->value = value;
	wasBelow = (analogRead(pin) < value);


void AnalogInterrupt::updateLimits() {
	if (wasBelow) {
		ADC_AnalogWatchdogThresholdsConfig(ADC3, value + hysteresis, 0);
	else {
		ADC_AnalogWatchdogThresholdsConfig(ADC3, 4095, value - hysteresis);

// [static]
void AnalogInterrupt::interruptHandlerStatic() {

void AnalogInterrupt::interruptHandler() {

	if (ADC_GetITStatus(ADC3, ADC_IT_AWD) != RESET) {
		// AWD (Analog WatchDog) Interrupt
		if (wasBelow) {
			// Not below anymore
			if (mode == RISING || mode == CHANGE) {
			wasBelow = false;
		else {
			// Not above anymore
			if (mode == FALLING || mode == CHANGE) {
			wasBelow = true;


		// Clear ADC AWD pending interrupt bit
		ADC_ClearITPendingBit(ADC3, ADC_IT_AWD);


void testInterruptHandler(); // forward declaration

// Note: You can only use A1, A2, or A7!
AnalogInterrupt analogInterrupt(A1, RISING, testInterruptHandler, 1000);

volatile unsigned long interruptTime = 0;

void setup() {


void loop() {
	if (interruptTime != 0) {
		Serial.printlnf("interrupt at %lu", interruptTime);
		interruptTime = 0;

	// This doesn't do anything useful; it just here for testing purposes
	// to make sure the analog watchdog doesn't break normal analogRead
	// and vice-versa.

void testInterruptHandler() {
	// This function is called at interrupt time. Make sure you don't put any long-running
	// operations, allocate memory including using malloc or new, use String, any of the
	// Particle cloud features like publish, or the Serial port here!
	interruptTime = millis();


@rickkas7 This is fantastic!

As a beginner working with ADCs, what considerations would I need to take into account to adapt this code for one of the newer platforms, such as the BSOM or TSOM?

This is fairly difficult to adapt to Gen 3, but should be possible.

The nRF52840 ADC API is completely different than the STM32 API. I think you need to run the ADC continuously in DMA mode, then enable LIMIT mode. The easiest way would be to start with ADCDMAGen3_RK and turn on limit detection. The limit events should come into the adcCallback function.

However, I’ve never tried it and I’m not positive it will work.