You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
funreg/GUIDE.md

4.8 KiB

Guided Overview

Let's say you have a couple LEDs that are controlled by memory-mapped registers, a common case for embedded microcontrollers. To begin, you'll need to define the register used to control the LEDs.

For an 8-bit register at memory address 0x0021, you would write:

using PORTA_OUT = fr::MemRegister<uint8_t, 0x0021>;

MemRegister is an Register that uses MemoryIO access. Registers have static functions for interacting with their contents; for example, we could now do PORTA_OUT::write(0x10) or auto state = PORTA::read().

A lot more can be done with registers once we define some register masks. A RegisterMask lets us name one or more bits within a register.

To name our two LEDs, which are only controlled by single bits, we write:

using LED_1 = fr::RegisterMask<PORTA_OUT, (1 << 0)>; // bit zero
using LED_2 = fr::RegisterMask<PORTA_OUT, (1 << 2)>; // bit two

We can also give a name to both LEDs:

using LED_ALL = fr::RegisterMask<PORTA_OUT, (1 << 0) | (1 << 2)>;

Now, we can control the LEDs either directly or through the register. Direct calls would be like LED_1::set(); there is also clear, toggle, read, write, and test.

These calls can also be made through the register using template parameters:

PORTA_OUT::set<LED_2>();

Registers can take multiple masks at once too. The masks will be merged so that the register is only read and written once:

PORTA_OUT::toggle<LED_1, LED_2>();

A modify function is also supported, which takes a list of mask operations as shown below. This allows the different operations to be carried out together, still keeping to a single register read and write:

PORTA_OUT::modify<LED_1::set, LED_2::clear>(); // Only have LED_1 turned on

What if we need to add a third LED? And what if that LED is on a different register, PORTB?

This is where RegisterGroup comes in handy:

using PORTB_OUT = fr::MemRegister<uint8_t, 0x0041>;

using LED_3 = fr::RegisterMask<PORTB_OUT, (1 << 5)>;

// Group the output ports together:
using LEDS = fr::RegisterGroup<PORTA_OUT, PORTB_OUT>;

By grouping the two registers, we can carry out our modification calls without worrying about which mask is for what register. The RegisterGroup will take of that, while still merging operations when possible to maintain minimal I/O:

LEDS::clear<LED_1, LED_2, LED_3>();

LEDS::modify<LED_1::set,
             LED_2::clear,
             LED_3::toggle>(); // You get the idea...

Other features

Multi-bit masks

Say bits two through five in a register select a clock's prescaler:

using CLOCK_DIV = fr::RegisterMask<CLOCK_CONTROL, 0x3C>;

The mask's write function will let you write values to the field:

CLOCK_DIV::write<0x03>();

This will read the register's current value, clear all bits selected by the mask, set the new value 0x03 in the mask's location, then update the register.

write can also be included in modify chains:

CLOCK_CONTROL::modify<CLOCK_DIV::write<0x03>, CLOCK_ENABLE::set>();

A RegisterMaskValue can also be defined to identify specific values:

using CLOCK_DIV4 = fr::RegisterMaskValue<CLOCK_DIV, 0x03>;

RegisterMaskValue supports three functions: set, which would call CLOCK_DIV::write<0x03>(); clear, which clears the masked bits; and test, which would confirm that the register contains the value 0x03 in the masked bits' location.

External registers

"External" registers are registers that are not memory-mapped. These are also supported in funreg, and can even be placed in RegisterGroups with other register types.

An "access type" must be defined to specify how the register is accessed. Here is the definition of MemoryIO, which is used for memory-mapped registers:

template<typename T, uintptr_t Addr>
struct MemoryIO {
    using type = T;
    constexpr static auto addr = Addr;

    /**
     * Reads the register's value.
     */
    constexpr static T read() {
        return *reinterpret_cast<volatile T*>(Addr);
    }

    /**
     * Overwrites the register's value.
     */
    constexpr static void write(const T& value) {
        *reinterpret_cast<volatile T*>(Addr) = value;
    }
};

Custom access types should use MemoryIO as a template. For example, here is an access type for ports on x86 processors:

template<typename T, uintptr_t Addr>
struct PortIO {
    using type = T;
    constexpr static auto addr = Addr;

    static T read() {
        T ret;
        asm volatile("in %0, %1" : "=r" (ret) : "r" (Addr));
        return ret; 
    }

    static void write(const T& value) {
        asm volatile("out %0, %1" :: "r" (value), "r" (Addr));
    }
};

Now, just define your register(s) using ExtRegister:

using KEYBOARD = fr::ExtRegister<PortIO, uint8_t, 0x60>;