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.7 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>;

fr::MemRegister is an fr::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 fr::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:

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

They also support a modify function, which takes a list of mask operations as shown below. Through modify, the register is only read and written once, minimizing I/O.

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 what you could do:

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 defining a RegisterGroup, we can make the same calls to modify the LEDs as we would with the single register. RegisterGroup will direct masks to their appropriate registers, while merging operations on the same register to maintain that 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, bitwise-OR the value 0x03 to the mask's location, then write the new value to the register.

write can also be included in modify chains:

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

You may also define a RegisterMaskValue to name a specific value:

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

Three functions are supported for RegisterMaskValue: 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 memory-mapped or 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>;