Introduction
Generic Register Operation Optimizer (groov) is a library that abstracts read
and write operations on registers in baremetal embedded systems. Some important
features of groov are:
-
Abstraction through separation of name and layout: where possible, registers and fields are referenced by name and manipulated with strong types rather than by poking bits into the layout.
-
Awareness of asynchronous operation: read and write operations are not just assumed to be synchronous to memory, but are modelled with senders to allow for asynchronous event-driven scenarios. Synchronous behaviour is still supported since it is a subset of asynchronous behaviour.
-
Error handling: the same mechanism that allows read and write operations to be asynchronous also provides a path for errors.
Dependencies
This repository uses CPM and a common CI/CD infrastructure.
The library dependencies are:
Registers and Fields
Registers and fields (reg and field types) define data layout. They are
empty types. A register represents an addressable unit of storage, which
contains fields located in certain bit ranges. Fields may in turn contain
(sub)fields.
Registers and fields are quite similar with similar interfaces. They are characterized by:
-
the name
-
the type
-
the location (in fields, a bit range; in registers, an address)
-
the write function
-
any contained fields or subfields
// two subfields located at [1:0] and [3:2]
using my_sub_field_0 = groov::field<"sub_field_0", std::uint32_t, 1, 0>;
using my_sub_field_1 = groov::field<"sub_field_1", std::uint32_t, 3, 2>;
// a field located at [3:0] containing the above subfields
using my_field_0 = groov::field<"field_0", std::uint32_t, 3, 0, groov::w::replace,
my_sub_field_0, my_sub_field_1>;
// a field located at [7:4]
using my_field_1 = groov::field<"field_1", std::uint32_t, 7, 4>;
// a register (address 0xa'0000) containing the above fields
using my_reg = groov::reg<"reg", std::uint32_t, 0xa'0000, groov::w:replace,
my_field_0, my_field_1>;
Types
Both registers and fields expose a type_t alias indicating the type of the
data. For registers, this is an unsigned integral type suitable for bitwise
operations. The type of a field is not so constrained and represents the "API"
type.
|
Note
|
At the moment it is assumed that fields occupy contiguous storage entirely within a register. |
Masks
Both registers and fields expose a mask that indicates their bit extent. For
registers, this is equivalent to the maximum value of their type_t.
Extract and insert
Both registers and fields implement extract and insert operations.
template <std::unsigned_integral T>
[[nodiscard]] constexpr static auto extract(T value) -> type_t;
template <std::unsigned_integral T>
constexpr static void insert(T &dest, type_t value) -> void;
Paths
A path is a way to access a field or register, just like a filesystem path
accesses an object within a filesystem. It is a compile-time list of strings.
Construction and manipulation
Paths can be constructed using UDLs which are in the groov::literals namespace.
using namespace groov::literals;
auto p1 = "reg"_r;
auto p2 = "field"_f;
|
Note
|
The _r and _f literals are equivalent; they are intended
merely to signal "register-like" and "field-like" respectively.
|
Multi-part paths can be constructed using . as a separator. Paths can also be
manipulated with operator/.
auto p1 = "reg"_r;
auto p2 = "field"_f;
// these are all equivalent
auto p3 = p1 / p2;
auto p4 = "reg.field"_f;
auto p5 = groov::path<"reg", "field">{};
Attaching values
Paths may exist as empty objects, carrying information in the type, or they may have values attached.
using namespace groov::literals;
// type information only
auto p1 = "reg.field"_r;
// with a value attached
auto p2 = "reg.field"_r = 42;
Generally, type information is used for reading, and attached values are used for writing.
|
Note
|
Paths carry no layout information; a path with a value represents only a generic binding. It is not converted into a type with layout until it is combined with a group for writing. |
Resolving
Paths on their own are just empty types. Their real power is as an abstraction
that affords resolution. Any object may resolve a path by providing a
resolve member function:
struct reg {
// ...
template <pathlike P> constexpr static auto resolve(P p);
};
Registers and fields can both resolve paths.
using my_field = groov::field<"field", std::uint32_t, 3, 0>;
using my_reg = groov::reg<"reg", std::uint32_t, 0xa'0000, my_field>;
// f has type my_field
auto f = groov::resolve(my_reg{}, "reg.field"_f);
In fact, registers and fields can resolve paths that don’t start "at the root", as long as they are unambiguous.
using my_sub_field_0 = groov::field<"sub_field", std::uint32_t, 1, 0>;
using my_field_0 = groov::field<"field_0", std::uint32_t, 3, 0, my_sub_field_0>;
using my_sub_field_1 = groov::field<"sub_field", std::uint32_t, 5, 4>;
using my_field_1 = groov::field<"field_1", std::uint32_t, 7, 4>;
using my_reg = groov::reg<"reg", std::uint32_t, 0xa'0000, my_field_0, my_field_1>;
// "field_1" is unambiguous within "reg": f has type my_field_1
auto f = groov::resolve(my_reg{}, "field_1"_f);
// "sub_field" is ambiguous within "reg": subf has type groov::ambiguous_t
auto subf = groov::resolve(my_reg{}, "sub_field"_f);
And of course, paths themselves can resolve paths.
auto p1 = "a.b.c.d"_r;
// p2 is "c.d"
auto p2 = groov::resolve(p1, "a.b");
Path resolution can fail in a few ways:
-
ambiguity, as seen above (result:
groov::ambiguous_t) -
the path was too long to resolve (result:
groov::too_long_t) -
plain not found (result:
groov::mismatch_t)
All of these failure types derive from groov::invalid_t. There are utilities
to deal with resolution in generic code:
// free function resolve (calls t.resolve)
template <typename T, pathlike Path, typename... Args>
constexpr auto resolve(T const &t, Path const &p, Args const &...args) -> decltype(auto);
// static_assert if resolution fails, indicating how it failed
template <typename T, pathlike Path, typename... Args>
constexpr auto checked_resolve(T const &t, Path const &p, Args const &...args) -> decltype(auto);
// what type is returned from resolve?
template <typename... Ts>
using resolve_t = decltype(resolve(std::declval<Ts>()...));
// can_resolve is modelled if resolve_t<Args...> is not an invalid_t
template <typename... Args>
concept can_resolve = /* ... */;
// type trait equivalents to the concept can_resolve
template <typename... Args>
constexpr static bool is_resolvable_v = /* ... */;
template <typename... Args>
using is_resolvable_t = /* ... */;
Groups
A group collects registers together with a bus. A bus provides read and write functionality. Like registers and fields, groups can resolve paths.
struct bus { /* ... */ };
using F = groov::field<"field", std::uint32_t, 0, 0>;
using R = groov::reg<"reg", std::uint32_t, 0, F>;
using G = groov::group<"group", bus, R>;
// r has type F
auto r = groov::resolve(G{}, "reg.field"_f);
Combining groups with paths
The combination of a group and one or more paths forms a specification for reading or writing.
constexpr auto grp = G{};
// Use operator/ to combine a single path
auto r_spec = grp / "reg"_r;
auto w_spec = grp / ("reg"_r = 42);
// alternatively, or for multiple paths:
auto r_spec = grp("reg1"_r, "reg2"_r);
auto w_spec = grp("reg1"_r = 42, "reg2"_r = 17);
Reading and writing
Read and write specifications
If a group is combined with paths without values, the result is a read_spec.
If a group is combined with paths with values, the result is a write_spec.
A read_spec is an empty object which carries in its type all the information
necessary for a read. A write_spec is an object carrying similar type
information, but also with register data in the correct layout, ready for writing.
// this does no runtime work, just composing types
auto r_spec = grp / "reg"_r;
// this packs values into register layouts
auto w_spec = grp / ("reg"_r = 42);
|
Note
|
If any bound paths are not resolvable or are ambiguous in the group, a compile-time error is produced. |
A write_spec supports indexing, for retrieving or updating values.
auto w_spec = grp / ("reg"_r = 42);
w_spec["reg"_r] = 17; // update value
assert(w_spec["reg"_r] == 17); // retrieve value
Updates are not limited to reassignment; operator[] returns a proxy object
with all the compound assignment operators defined on it, as well as increment
and decrement operators.
A write_spec may be indexed with any path that is resolvable by a bound path.
auto w_spec = grp / ("reg"_r = 42); // "reg" is bound
w_spec["reg.field"_f] = 1; // OK, assuming "reg" contains "field"
As a short cut, if only one value is specified, a write_spec can implicitly
convert to that value. Note that this is read-only; the converted value is not a
reference that can be used for update.
auto w_spec = grp / ("reg"_r = 42);
assert(w_spec == 42); // implicit conversion
Read
The read function takes a read_spec and produces a
sender.
auto r = groov::read(grp / "reg"_r);
It does this by:
-
computing a mask for each register that is read, according to the fields
-
for each register, calling
readon the bus with the address and mask -
executing
when_allon the resulting senders -
turning the results into a
write_spec
Read can also be piped in a chain of senders.
auto r = async::just(grp / "reg"_r) | groov::read() | async::sync_wait();
To do a simple read from a bus that is synchronous (e.g. an MMIO bus),
sync_read may be called:
auto r = async::just(grp / "reg"_r) | groov::sync_read();
|
Note
|
sync_read automatically assumes that the operation will succeed and
returns the resulting write_spec.
|
Write
The write function takes a write_spec and produces a
sender.
auto r = groov::write(grp("reg"_r = 42));
It does this by:
-
computing masks and identity values for each register that is written, according to the fields
-
for each register, calling
writeon the bus with the address, masks and value -
executing
when_allon the resulting senders
Write can also be piped in a chain of senders.
auto r = async::just(grp("reg"_r = 42)) | groov::write() | async::sync_wait();
To do a simple write to a bus that is synchronous (e.g. an MMIO bus),
sync_write may be called:
auto r = async::just(grp("reg"_r = 42)) | groov::sync_write();
The return value here is as for
async::sync_wait:
a std::optional<stdx::tuple<T>> where T is whatever is sent by the bus
write function. A disengaged optional means that the write completed with an
error (although this should not happen for an MMIO write).
Because read sends a write_spec, a read-modify-write can be achieved with
piping:
async::just(grp / "reg"_r)
| groov::read()
| async::then(
[](auto spec) { spec["reg"_r] ^= 0xffff'ffff;
return spec; })
| groov::write()
| async::sync_wait();
Buses
groov is a library for orchestrating and managing read and write operations
efficiently, but it can’t actually perform the reads and writes to hardware.
That’s what a bus is for. A bus is groov's interface with lower levels and it
is used by read and write. So it has its own read and write API.
struct bus {
template <stdx::ct_string RegisterName, auto Mask>
static auto read(auto addr) -> async::sender auto;
template <stdx::ct_string RegisterName, auto Mask, auto IdMask, auto IdValue>
static auto write(auto addr, auto value) -> async::sender auto;
};
To provide for asynchronous behaviour, a bus’s read and write functions
return senders. These can of course be async::just_result_of senders in the
case of a synchronous operation like memory-mapped IO.
Both read and write operations receive a compile-time mask of what is being
read or written, so that they can optimize best according to hardware
capabilities. For write, bits set in Mask are the bits that must be written,
and bits set in IdMask may be safely written using the corresponding bits in
IdValue.
|
Note
|
Mask and IdMask are disjoint sets of bits. Therefore Idvalue and
value are also disjoint sets of bits. Bits that are zero in IdMask are also
zero in IdValue.
|
Write Functions
Each field and register is parameterized with a write function. By default,
the write function is groov::w::replace.
using my_field = groov::field<"field", std::uint32_t, 3, 0, groov::w::replace>;
Write functions
Built-in write functions are in the groov::w namespace. Examples are:
-
replace("normal write") -
ignore("read-only" in loose parlance - but see note later)
There are 16 possible write functions, each characterized by a truth table relating the current value C, the written value W and the result R. For example:
| C | W | R |
|---|---|---|
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
0 |
1 |
1 |
1 |
| C | W | R |
|---|---|---|
0 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
1 |
1 |
1 |
1 |
The purpose of the write function is to control behaviour when writing, for example:
-
a bus can avoid a read-modify-write by writing identity bits to a field that is not being written to by the user
-
a user can generically set or clear a field
Identity, Set and Clear specs
Each write function may have types (mask specs) exposed:
-
id_spec: an identity mask spec -
set_spec: a set mask spec -
clear_spec: a clear mask spec
A valid write function must have at least one of these types exposed, and may have all of them.
There are three possible mask specs, in the namespace groov::m:
-
zero(mask of zeroes) -
one(mask of ones) -
any(mask of X)
Each mask spec has a function that computes the mask for a field according to its bit range:
template <std::unsigned_integral T, std::size_t Msb, std::size_t Lsb>
constexpr static auto mask() -> T;
Building the write masks
In the general case, a given write to a register will only want to write some of the field values. The other field values are left unspecified. The write machinery uses the field write functions and their associated identity specs to compute:
-
the write mask for the fields to be written
-
the identity mask for the other fields
-
the identity value (if any) for the other fields
For example:
// the default write function is groov::w::replace
using my_field_0 = groov::field<"field_0", std::uint32_t, 9, 0>;
using my_field_1 = groov::field<"field_1", std::uint32_t, 31, 10>;
using my_reg = groov::reg<"reg", std::uint32_t, 0xa'0000, groov::w::replace,
my_field_0, my_field_1>;
// when writing only field_1:
write(grp("my_reg.field_1"_f = 42));
// results in a call to bus::write as if:
constexpr auto write_mask = 0xffff'fc00u; // mask for field_1
constexpr auto identity_mask = 0u; // field_0 has id_spec none
constexpr auto identity_value = 0u; // field_0 has id_spec none
// the write mask and the identity mask don't cover the range of the register,
// so the bus must read-modify-write to preserve unwritten bits
bus::template write<"my_reg", write_mask, identity_mask, identity_value>(/* ... */);
But if the unwritten field(s) are "read-only", i.e. have the ignore write
function (or otherwise have an identity):
using my_field_0 = groov::field<"field_0", std::uint32_t, 9, 0, groov::w::ignore>;
using my_field_1 = groov::field<"field_1", std::uint32_t, 31, 10>;
using my_reg = groov::reg<"reg", std::uint32_t, 0xa'0000, groov::w::replace,
my_field_0, my_field_1>;
// when writing only field_1:
write(grp("my_reg.field_1"_f = 42));
// results in a call to bus::write as if:
constexpr auto write_mask = 0xffff'fc00u; // mask for field_1
constexpr auto identity_mask = 0x0000'03ffu; // field_0 has id_spec any
constexpr auto identity_value = 0u; // field_0 has id_spec any (zeroes are arbitrary)
// the write mask and the identity mask cover the range of the register,
// so the bus may OR the identity value with the written value to preserve
// unwritten bits and avoid read-modify-write
bus::template write<"my_reg", write_mask, identity_mask, identity_value>(/* ... */);
Custom write functions
Custom write functions can be used to deal with registers that have special requirements.
|
Note
|
Try not to think of registers/fields as simply "read-only" or similar. While "read-only" is a good interface-level abstraction, it’s not the same concept when dealing with hardware. Individual bits are not addressable, so a register whose bits are not all "read-only" must be written with some value for those bits (which the hardware may or may not ignore). This is the job of the write function in concert with the bus. |
Imagine that we have a register containing one "writable" bit, and other reserved bits whose write semantics are specified:
| Field | Bits | Description |
|---|---|---|
Reserved |
7 |
Reserved. Write as 0. |
Reserved |
6 |
Reserved. Write as 1. |
Reserved |
5 |
Reserved. Write as 1. |
Reserved |
4 |
Reserved. Write as 0. |
Reserved |
3 |
Reserved. Write as 0. |
Reserved |
2 |
Reserved. Write as 0. |
ENABLE |
1 |
Enable bit. |
Reserved |
0 |
Reserved. Write as 0. |
Note the write semantics here. Bits 5 and 6 must be written as 1. The other
reserved bits must be written as 0.
In order to model this, we could create 3 fields inside a single register — with an appropriate group and bus:
using F0 = groov::field<"reserved0", std::uint8_t, 0, 0, groov::w::ignore>;
using FE = groov::field<"enable", std::uint8_t, 1, 1>;
using F1 = groov::field<"reserved1", std::uint8_t, 7, 2, custom_write_func>;
using R =
groov::reg<"reg", std::uint32_t, REG_ADDRESS, groov::w::replace, F0, FE, F1>;
using G = groov::group<"group1", bus_t, R>;
Notice that R's write function is the default w::replace which is
overridden at the level of individual fields. We are going to write to this
register, after all.
In user code, we’ll only write to the enable field (FE):
groov::write(G{}("reg.enable"_f = 1)) | async::sync_wait();
The table tells us to write bit 0 as 0, and this is already the default given by
w::ignore, so that’s OK for F0.
The custom_write_func needs to provide the correct "identity value" to
write to F1. This it can do with an appropriate id_spec:
struct custom_write_func {
struct id_spec {
template <std::unsigned_integral T, std::size_t Msb, std::size_t Lsb>
constexpr static auto mask() -> T {
// sanity checks: this is just for F1
static_assert(Msb == 7);
static_assert(Lsb == 2);
return 0b0110'0000u; // bits 5 and 6 are 1
}
};
};
And the bus’s write function must take account of the identity mask and value
as well as the user-supplied bit(s) to write, for example:
template <auto Mask, auto IdMask, auto IdValue>
static auto write(auto addr, auto value) -> async::sender auto {
return async::just_result_of([=] {
auto prev = *addr & ~(Mask | IdMask);
*addr = prev | value | IdValue;
});
}
The result of this is that when we write to the enable field, the other bits
of the register get written correctly.
|
Note
|
If a register has bits that are not addressed by any fields, the identity values for those fields will be taken from the write function on the register itself. |
Read-only fields
A field may be denoted read-only by marking its write function as such:
using FR = groov::field<"reserved", std::uint8_t, 1, 1, groov::read_only<groov::w::ignore>>;
If a field is marked read-only, it’s a compile-time error to attempt to assign it a value:
groov::write(G{}("reg.reserved"_f = 1)) | async::sync_wait();
// compile error: "Attempting to write to a read-only field: reserved"
A write function wrapped with read_only in this way must have an id_spec that
provides its identity bits. When the register containing a read-only field is
written, the bits provided by the id_spec for the read-only field(s) will be
used.
|
Note
|
This illustrates the difference between ignore (which is how the
hardware treats writes) and read_only (which is an API-level decision).
ignore is not read_only because there is no hardware prohibition on writing.
The prohibition on writing is enforced at the API level.
|
Generic writing: set and clear
Fields with an appropriate set_spec and/or clear_spec in their
write_function can be generically written to by asking for set or clear:
// the (default) groov::w::replace write function has:
// using set_spec = groov::m::one;
// using clear_spec = groov::m::zero;
using my_field = groov::field<"field", std::uint32_t, 1, 0>;
using my_reg = groov::reg<"reg", std::uint32_t, 0xa'0000, groov::w::replace,
my_field>;
write(grp("my_reg.field"_f = groov::set)); // write all ones
write(grp("my_reg.field"_f = groov::clear)); // write all zeroes
Another example: a field’s write function may be one_to_clear.
// the groov::w::one_to_clear write function has:
// (no set_spec defined)
// using clear_spec = groov::m::one;
using my_field = groov::field<"field", std::uint32_t, 1, 0, groov::w::one_to_clear>;
using my_reg = groov::reg<"reg", std::uint32_t, 0xa'0000, groov::w::replace,
my_field>;
write(grp("my_reg.field"_f = groov::set)); // error! no set_spec is defined
write(grp("my_reg.field"_f = groov::clear)); // writes all ones
Generic writing: enable and disable
Sometimes it is useful for a field’s type to be an enumeration, and that field
may have the concept of enable and disable. In this case, the field can be
written by asking for enable or disable.
enum struct E { ENABLE = 0, DISABLE = 1, OTHER = 2 };
using my_field = groov::field<"field", E, 1, 0>;
using my_reg = groov::reg<"reg", std::uint32_t, 0xa'0000, groov::w::replace,
my_field>;
write(grp("my_reg.field"_f = groov::enable)); // writes ENABLE (0)
write(grp("my_reg.field"_f = groov::disable)); // writes DISABLE (1)
The enumeration may be more than one bit. It must have ENABLE and DISABLE
values for enable and disable to work respectively.
Testing
Register definitions are often difficult to handle in testing. They can map to fixed memory addresses or represent special hardware capabilities that are not available on the test target.
groov provides facilities to aid with testing.
Injecting a Test Bus
groov allows a test bus to be injected for a groov::group. The
groov::group associates a bus with a set of registers. For example:
using g0 =
groov::group<
"some_group", groov::mmio_bus<>,
reg0, reg1, reg2
>;
g0 describes a group consisting of reg0, reg1, and reg2 that will
all use the groov::mmio_bus.
We can tell groov that "some_group" should use our testing_bus
instead. To do so, we:
-
Include the
groov/test.hppfile before any othergroovheader. -
Specify the
test_bus_listbefore including any othergroovheader. -
Provide the test bus implementation
For example, our test_thing.cpp file might start like this:
#incldue <groov/test.hpp>
namespace groov::test {
using test_bus_list =
make_test_bus_list<
test_bus<"some_group", testing_bus>
>;
}
// ... continue with includes
The code snippet will inject testing_bus for "some_group"
instead of of the bus that was specified in the group’s definition.
We can implement testing_bus however we wish to provide the read
and write functions that will faciliate our testing.
Default Test Bus
groov provides a test bus that offers common capabilities. You can
specify use of the default test bus as follows:
#incldue <groov/test.hpp>
namespace groov::test {
using test_bus_list =
make_test_bus_list<
default_test_bus<"some_group">,
test_bus<"my_group", my_bus>
>;
}
// ... continue with includes
In the above example, groov will inject the default test bus for
"some_group" and my_bus for "my_group".
The default test bus is backed by a unique store for each group. The store provides functions to read and write register values, reset the store, and set functions to handle read or write register access.
The default test bus will perform normal read-modify-write actions on the store. Reads of a non-initialized register in the store will return a disengaged optional. As such, the sender read will return an optional<read_spec>.
This can be undesirable if the groov::group that has an injected test bus is being used by other components in your test. The signature will change from a read_spec to an optional<read_spec>. How the default test bus handles optional read values is controlled by a policy. The default policy is:
namespace groov::test {
struct optional_policy {
template <auto> auto operator()(auto, auto value) { return value; }
};
}
You can provide your own policy where the interface is:
struct my_policy {
template <stdx::ct_string RegName, auto Mask> auto operator()(auto addr, auto value) {
// RegName is the name of the register being read
// Mask is the read mask
// addr is the address being read
// value is an std::optional<T> and T is the register value type.
return /* return whatever for your signature */
}
};
If you want to throw on a read of an unitialized register you could:
#incldue <groov/test.hpp>
struct my_throw_policy {
template <stdx::ct_string, auto> auto operator()(auto addr, auto value) {
if (not value) { throw addr; }
return *value;
}
};
namespace groov::test {
using test_bus_list =
make_test_bus_list<
test_bus<"some_group", groov::test::bus<"some_group", my_thow_policy>
>;
}
The Store
The simplest way to interact with the store is via functions that take
the groov::group as an argument. This enables an interface that uses
the same path-like mechanisms to specify a register as normal groov
interactions.
Given,
using reg0 = groov::reg<"reg0", std::uint32_t, ADD0, groov::w::replace, field0, field1>;
using reg1 = groov::reg<"reg1", std::uint32_t, ADD1, groov::w::replace, field0, field1>;
using reg2 = groov::reg<"reg2", std::uint32_t, ADD2, groov::w::replace, field0, field1>;
using G0 =
groov::group<
"some_group", groov::mmio_bus<>,
reg0, reg1, reg2
>;
constexpr auto grp0 = G0{};
reset_store
Resets the store to an empty state.
template <typename Group>
groov::test::reset_store();
template <typename Group>
groov::test::reset_store(Group);
groov::test::reset_store<G0>();
// or
groov::test::reset_store(grp0);
set_value
Set the value in the store for the specified register. The register is specified via a path. The value will be cast to the register’s value type when it is stored.
template <typename Group, pathlike P, typename V>
void groov::test::set_value(P path, V value);
template <typename Group, pathlike P, typename V>
void groov::test::set_value(Group, P path, V value);
groov::test::set_value<G0>("reg1"_r, 0xdeadbeef);
// or
groov::test::set_value(grp0, "reg1"_r, 0xdeadbeef);
get_value
Get the value in the store for the specified register. The register is specified via a path. The value will be returned as an std::optional<T> where T is the register’s value type.
If no value has been set in the store for the specified register path, the returned optional will be disengaged.
template <typename Group, pathlike P>
auto groov::test::get_value(P path) -> std::optional<register-type>;
template <typename Group, pathlike P>
auto groov::test::get_value(Group, P path) -> std::optional<register-type>;
auto v = groov::test::get_value<G0>("reg1"_r);
// or
auto v = groov::test::get_value(grp0, "reg1"_r);
set_write_function
If a write function is set in the store for a specific register, it will be called for each write access to that register. The write function’s signature is:
void(unspecified-type-erased address ,unspecified-type-erased value)
where unspecified-type-erased is a type-erased type that provides the following interface:
struct _unspecified-type-erased_ {
template <typename T> auto get() const -> std::optional<T>;
};
If the object contains a value of type T an engaged optional with the value is returned, otherwise a disengaged optional is returned.
A helper function is provided to reduce syntactic noise. The helper is found via ADL.
template <typename T> auto get(_unspecified-type-erased_ v) -> std::optional<T>;
Both the address and the value are passed to the write function as type-erased values. The address type should be the type of the register’s address and the value type should be the type of the register’s value; each described in the groov::reg definition.
template <typename Group, pathlike P, typename F>
void set_write_function(P p, F &&f);
template <typename Group, pathlike P, typename F>
void set_write_function(Group, P p, F &&f);
The following example stores the last address and value written in variables captured by the lambda and increments a counter.
int write_call_count = 0;
void *write_addr = 0;
std::uint32_t write_value = 0;
groov::test::set_write_function<G0>("reg0"_r,
[&](auto addr, auto value) {
++write_call_count;
write_addr = get<std::uint32_t *>(addr).value_or(nullptr);
write_value = get<std::uint32_t>(value).value_or(0);
});
|
Note
|
If a write function is set, no value will be stored within the store on write. A subsequent read with no read function set will result in a disengaged optional or a value that was set prior to providing the write function.
|
set_read_function
If a read function is set in the store for a specific register, it will be called for each read access to that register. The read function’s signature is:
unspecified-type-erased(unspecified-type-erased address)
where unspecified-type-erased is a type-erased type that provides the following interface:
struct _unspecified-type-erased_ {
template <typename T> auto get() const -> std::optional<T>;
};
If the object contains a value of type T an engaged optional with the value is returned, otherwise a disengaged optional is returned.
A helper function is provided to reduce syntactic noise. The helper is found via ADL.
template <typename T> auto get(_unspecified-type-erased_ v) -> std::optional<T>;
The address is passed to the read function in a type-erased value. The wrapped address type is the register’s address type. The read function’s returned value type should be the register’s value type. The register’s address and value types are each described in the groov::reg defintion.
template <typename Group, pathlike P, typename F>
void set_read_function(P p, F &&f);
template <typename Group, pathlike P, typename F>
void set_read_function(Group, P p, F &&f);
The following example stores the last read address and increments a counter. It always returns 0xbabeface.
int read_call_count = 0;
void *read_addr = 0;
groov::test::set_read_function<G0>("reg0"_r,
[&](auto addr) {
++read_call_count;
read_addr = get<std::uint32_t *>(addr).value_or(nullptr);
return 0xbabeface;
});
Accessing the store
|
Note
|
The helper functions above are more ergonomic. If you know the group type or have access to a group instance, consider using those methods. |
Sometimes it is not possible to get the groov::group type or instance directly. The group might be sandwiched between layers that you are testing against. In these situations the bus is easily injected based on having the "name" of the group and the store must be accessed the same way.
The store is a little more difficult to work with directly because all values are unspecified_type_erased as described above for the read/write functions.
The direct store interface is:
namespace groov::test {
template <stdx::ct_string Group>
struct store {
public:
// Reset (clear) the store for the group.
inline static void reset();
// Set the value in the group's store at the specified address.
// Addresses access entire registers so value represents the entire register.
// If there is a write function set, it will be called.
static void set_value(unspecified-type-erased addr, unspecified-type-erased value);
// Get the value in the group's store at the specified address.
// Addresses access entire registers so the returned value represents
// the entire register.
// If there is a read function set, it will be called.
static auto get_value(unspecified-type-erased addr) -> unspecified-type-erased;
// Set a function to be called when the specified address is written
// to in the group's store. The function has a signature of
// void(address_t, value_t)
template <typename F>
static void set_write_function(unspecified-type-erased addr, F && f);
// Set a function to be called when the specified address is read
// in the group's store. The function has a signature of
// value_t(address_t)
template <typename F>
static void set_read_function(unspecified-type-erased addr, F && f);
};
}