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