Project Liquid

AVR timers

Low-level access

There are 2 types of timer/counters typical to most 8-bit AVR microcontrollers: 8-bit (typically Timer0) and 16-bit (Timer1). Additional instances are almost identical, with the same register layouts, so it makes sense to represent them with the same structure. The structure needs to be parametrized with the addresses of the registers and any individual timer/counter differences.

Register locations

The first decision is how to reflect the different register locations. The most straightforward way is to hardcode the addresses in individual classes:

struct Timer1 {
    uint16_t TCCRA_addr = 0x80;
    // ...
};

This is great for optimization, but very bad for reuse. In theory, different chips can have the registers at different locations, and other instances of the timer (like Timer3, Timer4, Timer5), would need to be represented as unrelated structures. This means you could not easily write code that works with any 16-bit timer.

I also considered using templates, with register locations as template arguments:

template<uint16_t tccra_addr>
struct Timer16 { ... };

// Micro 1
Timer16<0x80> timer1;
Timer16<0x90> timer3;

// Some other micro
Timer16<0xB0> timer1;

This helps with code reuse and can accommodate different register locations on different devices, while still allowing for easy optimization by the compiler. But the same problem exists, different timers are unrelated structures, which limits the way they can be used.

To address the issue, I can put the register addresses as struct data:

struct Timer16bit
{
    const uint16_t TCCRA_addr;
};

Timer16bit timer1 { 0x80 };

Now all instances of the timer are of the same type, so I can write a function that takes any Timer16bit object. The main downside is that register addresses will likely need to be calculated at run-time, resulting in less optimal code. It also increases the size of each object, with multiple register addresses. That can be partially mitigated by storing a pointer to a struct with a collection of register addresses.

I feel like in this case, the run-time register address calculation is a price worth paying for the increased flexibility. Especially that, with the right use of const and constexpr, the compiler can still optimize most address calculations. But this will depend greatly on how the client code is written.

I ended up with the following structures. Most timer registers are accessed using the base address + offset. Interrupt addresses are specified separately.

class AvrTimer16
{
public:
    struct Config {
        uint16_t base;
        uint16_t timskAddr;
        uint16_t tifrAddr;
        int      irqCompA;
    };

private:
    const Config &config;

// ...

public:
    constexpr AvrTimer16(const Config &config_) : config(config_) {}

    auto TCCRA() const { /* access TCCRA using config.base address */ }

If all the values are known at compile-time, the address calculation is still optimized out:

void foo()
{
    AvrTimer16 t1(cfg1);
    t1.TCCRA().WGM10 = 3;
}
    r30, 0x0200
    r31, 0x0201
    ld      r24, Z
    ori     r24, 0x03       
    st      Z, r24
    ret

In other cases, it is done at run-time:

static AvrTimer16 t1(cfg1);

void foo(AvrTimer16 &tx)
{
    tx.TCCRA().WGM10 = 3;
}
    movw    r26, r24
    ld      r30, X+
    ld      r31, X
    ld      r0, Z+
    ld      r31, Z
    mov     r30, r0
    ld      r24, Z
    ori     r24, 0x03       
    st      Z, r24
    ret
Timer modes

To represent different modes of operation, I created inner structs with functions that configure the corresponding mode. The inner structs are mainly for organizational purposes, providing namespaces for related enums and functions.

Once again, the main challenge was balancing flexibility with the potential for the compiler to optimize the code. This was especially true for clock selection and frequency calculation, because calculating the necessary register values can be computationally expensive, and the arguments are often known at compile time. In the end, I ended up with timer mode constexpr methods that return an ComponentConfig object. The object contains a configuration function, with as many pre-calculated values as possible, potentially eliminating run-time operations.

struct CTCMode {
    //...

    static constexpr auto configureSquareWave(unsigned long ioFreq, 
                                              float freq)
    {
        const auto clock = findClock(ioFreq, freq);
        const auto valid = clock.prescaler != 0;
        const auto ocrValue = valid ? 
            getOcr(ioFreq, clock.prescaler, freq) : 0;

        auto c = [=](const AvrTimer16 &obj) {
            obj.writeWgm(AvrTimer16::WaveformGenerationMode::CtcToOcr);
            obj.TCCRB().CS = clock.value;
            obj.OCRA() = ocrValue;
            obj.TCCRA().COMA = CompareOuputMode::Toggle;
            
            return true;
        };

        return ComponentConfig<decltype(c)> {valid, c};
    }

In the example above, the most computationally expensive operations are findClock() and getOcr(), which calculate the clock prescaler and OCR values for the requested frequency. However, since both those methods, as well as configure() are constexpr, the calculation can be done at compile-time.

The actual register write operation obviously cannot be executed at compile-time, and so it is returned as a function. It is then wrapped in a ComponentConfig object that provides error handling. Ideally, a std::optional or exceptions would be used, but they’re not available for the AVR target (as far as I know).

template <class T> struct ComponentConfig {
    bool isValid;
    T    configFunc;

    operator bool() const { return isValid; }
};

The parent Timer class provides a convenience method apply() for calling the resulting config function. An example usage of this API in the client code could look like this.

auto outputSquareWaveOptimized() -> void
{
    using CTC = AvrTimer16::CTCMode;

    constexpr auto cfg1 = CTC::configureSquareWave(F_CPU, 99300400);
    static_assert(cfg1.isValid == false);

    constexpr auto cfg2 = CTC::configureSquareWave(F_CPU, 0.0001f);
    static_assert(cfg2.isValid == false);

    constexpr auto cfg3 = CTC::configureSquareWave(F_CPU, 300);
    static_assert(cfg3.isValid);

    constexpr auto t3 = Board::makeTimer16(Timer16::Timer3);
    t3.apply(cfg3);
}

In the resulting assembly code, there are no run-time frequency calculations, since all values were pre-calculated by constexpr functions.

;; Dump of assembler code for function outputSquareWaveOptimized():
ldi     r26,    0x91    ; 145
ldi     r27,    0x00    ; 0
ld      r24,    X
andi    r24,    0xE7    ; 231
ori     r24,    0x08    ; 8
st      X,      r24
ldi     r30,    0x90    ; 144
ldi     r31,    0x00    ; 0
ld      r24,    Z
andi    r24,    0xFC    ; 252
st      Z,      r24
ld      r24,    X
andi    r24,    0xF8    ; 248
ori     r24,    0x01    ; 1
st      X,      r24
ldi     r24,    0x29    ; 41
ldi     r25,    0x68    ; 104
sts     0x0099, r25
sts     0x0098, r24
ld      r24,    Z
andi    r24,    0x3F    ; 63
ori     r24,    0x40    ; 64
st      Z,      r24
ret

High-level interface

As well as a low-level interface, Liquid provides a higher-level abstraction of certain functionalities that can be implemented using a Timer/Counter. Currently, these are PWM, Square wave, and periodic interrupt. These interfaces can be implemented by 16-bit timers, as well as 8-bit timers, which will be represented by different classes. It should be possible to mix the two implementations in the same program, so I decided to use virtual base classes for the API. In other words, I would like the ability to write a function that takes a Pwm object and call it with an argument that’s backed by either a 16-bit or 8-bit timer.

These APIs are one layer above the previously described classes, and I’m willing to trade a performance hit for the extra flexibility.

class Pwm
{
public:
    virtual ~Pwm() = default;
    
    virtual auto configure(unsigned long fCpu, 
                           unsigned long min, 
                           unsigned long max) -> bool = 0;
    virtual auto setDutyCycle(float dutyCycle) -> void = 0;
};

class SquareWave
{
public:
    virtual ~SquareWave() = default;

    virtual auto configure(unsigned long fCpu, 
                           float freq) -> bool = 0;
    virtual auto setFrequency(unsigned long fCpu, 
                              float freq) -> bool = 0;
};


class Timer
{
public:
    virtual ~Timer() = default;

    virtual auto enablePeriodicInterrupt(unsigned long fCpu, 
        float freq, const IrqHandler &handler) -> bool = 0;
    virtual auto disablePeriodicInterrupt() -> void = 0;
    virtual auto stop() -> void = 0;
};

The concrete implementations use an AvrTimer16 object and functions from the corresponding timer mode struct. Unfortunately, since I’m using C++17 at this point, I cannot use constexpr with the virtual functions. I will explore the option, if/when I move the project to C++20.

class PwmImpl : public Pwm
{
public:
    PwmImpl(AvrTimer16 timer_, CompareOutputChannel channel_) 
      : timer(timer_), channel(channel_) {}

    virtual ~PwmImpl() = default;

    auto setDutyCycle(float dutyCycle) -> void override
    {
        auto config = AvrTimer16::FastPwmMode::setDutyCycle(
            dutyCycle, channel);
        timer.apply(config);
    }

    auto configure(unsigned long fCpu, 
         unsigned long min, unsigned long max) -> bool override
    {
        auto config = AvrTimer16::FastPwmMode::configure(
            channel, fCpu, min, max);
        if (!config) return false;
        timer.apply(config);
        return true;
    }

private:
    AvrTimer16                 timer;
    const CompareOutputChannel channel;
};

Here is an example of the high-level API usage. The code below configures PWM output on pin D12, with a frequency between 10kHz and 40kHz and a duty cycle of 75%

auto pwm1 = Board::makePwm(Board::Gpio::D12);
pwm1.configure(F_CPU, 10000, 40000);
pwm1.setDutyCycle(0.75f);
About PWM Frequencies

The example above configures PWM frequency by specifying a desired range, rather than a precise value. This is because when using the hardware Timer module for generating a PWM signal, the frequency is determined by the clock prescaler and timer resolution, so it only provides a limited selection of values. Precise frequency PWM can be achieved by using interrupts to toggle the output pin in software. However, in most cases, it is not necessary to precisely control the frequency when using PWM, and I’d rather save the CPU cycles and interrupt handling for other tasks.

Remaining work

AVR timers provide many more modes of operation, but I would only add them as needed for other projects. Some of the things I would like to add next are:

  • PWM, Square Wave, and Periodic Interrupt implementations using 8-bit Timer/Counters
  • Different resolutions in 16-bit timer modes

Leave a Reply

Your email address will not be published. Required fields are marked *