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 RegisterGroup
s 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>;