The STM32 L0-series and newer µCs have an interesting feature: the ADC “watchdog”. Some variants have more than one, in fact.

This mechanism can keep track of analog voltages in the digital world, unlike the analog way of using one or more comparators with reference voltage levels. The effect is similar, though: when an ADC reading falls outside a given range, an interrupt can be generated.

For some upcoming explorations, I need to dive a lot more into the analog side of µCs, and so I definitely want to try this out. I’m using an L053 µC on a Nucleo board for this.

Basic ADC use

But let’s walk before trying to run: there’s no support for the L0 series ADC peripheral in JeeH yet. So I wrote a driver - here’s the extract, from arch/stm32l0.h in the repository:

struct ADC {
    constexpr static uint32_t base   = 0x40012400;
    constexpr static uint32_t cr     = base + 0x008;
    constexpr static uint32_t chselr = base + 0x028;
    constexpr static uint32_t dr     = base + 0x040;

    static void init () {
        MMIO32(Periph::rcc+0x34) |= (1<<9);  // enable ADC
        MMIO32(cr) = (1<<31);  // ADCAL
        while (MMIO32(cr) & (1<<31)) {}  // wait until calibration completed
        MMIO32(cr) |= (1<<0);  // ADEN
    }

    // read analog, given a pin (which is also set to analog input mode)
    template< typename pin >
    static uint16_t read (pin& p) {
        pin::mode(Pinmode::in_analog);
        constexpr int off = pin::id < 16 ? 0 :   // A0..A7 => 0..7
                            pin::id < 32 ? -8 :  // B0..B1 => 8..9
                                           -22;  // C0..C5 => 10..15
        return read(pin::id + off);
    }

    // read direct channel number (also: 17 = vref, 18 = temp)
    static uint16_t read (uint8_t chan) {
        MMIO32(chselr) = (1<<chan);
        MMIO32(cr) |= (1<<2);  // start conversion
        while (MMIO32(cr) & (1<<2)) {}  // EOC wait until done
        return MMIO32(dr);
    }
};

And here’s an example project, periodically reading the voltage on PA0:

#include <jee.h>

UartBufDev< PinA<2>, PinA<3> > console;

int printf(const char* fmt, ...) {
    va_list ap; va_start(ap, fmt); veprintf(console.putc, fmt, ap); va_end(ap);
    return 0;
}

PinA<0> ain;
ADC adc;

int main() {
    console.init();
    console.baud(115200, fullSpeedClock());

    adc.init();

    while (true) {
        printf("%d %d\n", ticks, adc.read(ain));
        wait_ms(500);
    }
}

As you can see, there’s always the same sort of boilerplate in these examples. I’m still looking for elegant ways to simplify this, but hey - what’s half a dozen lines of code, eh?

Note that the above ADC struct/class definition is the entire interface to the hardware. This directly addresses the hardware registers and sets things up for polled ADC readout from any analog-capable pin on L0-series µCs. There is no fluff in between, it’s all in this code, the datasheet, and the silicon. That’s because I want to expose this stuff, not hide it!

The watchdog

Onwards, to what this article is about: the analog watchdog. It’s a simple idea: whenever an ADC reading has been made by the hardware, it compares the value with upper and lower bounds. If the value exceeds them on either side, an interrupt can be generated.

Here’s a demo which implements a comparator + LED in code: when the ADC reading is within 1000..3000 (inclusive), the Nucleo’s on-board LED is turned on, else it is turned off:

[... the usual preamble, see above ...]

PinA<5> led;
PinA<0> ain;
ADC adc;
int aVal;

int main() {
    console.init();
    console.baud(115200, fullSpeedClock());
    led.mode(Pinmode::out);

    adc.init();
    adc.window(1000, 3000); // define initial analog watchdog window

    // enable the analog watchdog, by passing in an interrupt handler
    // once outside range, change the range to avoid infinite interrupts
    adc.watch([]() {
        led = 1000 <= aVal && aVal <= 3000;

        if (aVal < 1000)
            adc.window(0, 999);     // low, trigger when it's in range again
        else if (aVal > 3000)
            adc.window(3001, 4095); // high, trigger when it's in range again
        else
            adc.window(1000, 3000); // trigger when it goes out of range

        MMIO32(adc.isr) = (1<<7);   // clear AWD interrupt flag
    });

    while (true) {
        aVal = adc.read(ain);
        printf("%d %d\n", ticks, aVal);
        wait_ms(500);
    }
}

The details of the implementation can be found here, it’s under 20 lines of C++.

As you can see, this line controls the LED:

        led = 1000 <= aVal && aVal <= 3000;

It’s inside an interrupt function (defined using C++11 notation) which is called whenever the analog watchdog fires. The rest of the code is needed to solve an interesting puzzle: once the ADC value falls outside the range, what do we do?

The problem is that doing nothing is not a good option: the watchdog would then continue to fire for every reading and waste a lot of CPU cycles in the interrupt handler.

What we need to do is to adjust the watchdog range to the new situation: if it dropped too low, we set the new range to the low band and wait for the ADC value to increase again, and if it’s rose too high, we set the range to the high band.

Which is exactly what the above code does. The last line is needed to clear the watchdog interrupt and “arm” it again for future ADC readings.

As a result, our interrupt code will be called once whenever the range we’re interested in is exceeded, but also whenever the value falls in range again. This logic allows keeping track of exceptional conditions with virtually no CPU overhead.

There is one important detail, and potential drawback, though: these range checks are only done when an ADC reading has been performed. Without the final loop in the above demo, nothing would happen. And since the loop has a 500 ms delay in it, the LED state changes will be very sluggish (which is intentional in this demo).

For fast range checking, more tricks will be needed. One option is to implement fast continuous ADC cycling with DMA. But that’s for another time …

One last point to make is that the comparisons can be done for all ADC readings, or for readings from one specific channel (but I haven’t implemented this in JeeH yet).