dev: Add a new RegisterBank which helps handle device registers.
authorGabe Black <gabe.black@gmail.com>
Wed, 21 Oct 2020 03:03:13 +0000 (20:03 -0700)
committerGabe Black <gabe.black@gmail.com>
Sun, 1 Nov 2020 13:08:26 +0000 (13:08 +0000)
This change includes both the RegisterBank class and register classes,
and a unit test which exercises them.

Change-Id: I28ef0c0b9192ad786625ac83f096f69d8e5af00f
Reviewed-on: https://gem5-review.googlesource.com/c/public/gem5/+/35856
Reviewed-by: Matthew Poremba <matthew.poremba@amd.com>
Maintainer: Gabe Black <gabe.black@gmail.com>
Tested-by: kokoro <noreply+kokoro@google.com>
src/dev/SConscript
src/dev/reg_bank.hh [new file with mode: 0644]
src/dev/reg_bank.test.cc [new file with mode: 0644]

index 6af5ee2419c09598a9eb4ab20dbb1c9053bbb22a..02f973315c1e875c54796b9f1c62c518606393c8 100644 (file)
@@ -54,3 +54,5 @@ Source('pixelpump.cc')
 
 DebugFlag('Intel8254Timer')
 DebugFlag('MC146818')
+
+GTest('reg_bank.test', 'reg_bank.test.cc')
diff --git a/src/dev/reg_bank.hh b/src/dev/reg_bank.hh
new file mode 100644 (file)
index 0000000..7abe02c
--- /dev/null
@@ -0,0 +1,945 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met: redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer;
+ * redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution;
+ * neither the name of the copyright holders nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef __DEV_REG_BANK_HH__
+#define __DEV_REG_BANK_HH__
+
+#include <algorithm>
+#include <bitset>
+#include <cstring>
+#include <functional>
+#include <initializer_list>
+#include <iostream>
+#include <map>
+#include <sstream>
+#include <utility>
+
+#include "base/bitfield.hh"
+#include "base/logging.hh"
+#include "base/types.hh"
+#include "sim/byteswap.hh"
+#include "sim/serialize_handlers.hh"
+
+/*
+ * Device models often have contiguous banks of registers which can each
+ * have unique and arbitrary behavior when they are completely or partially
+ * read or written. Historically it's been up to each model to map an access
+ * which covers an arbitrary portion of that register bank down to individual
+ * registers. It must handle cases where registers are only partially accessed,
+ * or where multiple registers are accessed at the same time, or a combination
+ * of both.
+ *
+ *
+ * == RegisterBank ==
+ *
+ * The RegisterBank class(es), defined below, handle that mapping, and let the
+ * device model focus on defining what each of the registers actually do when
+ * read or written. Once it's set up, it has two primary interfaces which
+ * access the registers it contains:
+ *
+ * void read(Addr addr, void *buf, Addr bytes);
+ * void write(Addr addr, const void *buf, Addr bytes);
+ *
+ * These two methods will handle a read or write contained within the register
+ * bank starting at address "addr". The data that will be written or has been
+ * read is pointed to by "buf", and is "bytes" bytes long.
+ *
+ * These methods are virtual, so if you need to implement extra rules, like
+ * for instance that registers can only be accessed one at a time, that
+ * accesses have to be aligned, have to access complete registers, etc, that
+ * can be added in a subclass.
+ *
+ * Additionally, each RegisterBank has a name and a base address which is
+ * passed into the constructor. The meaning of the "base" value can be whatever
+ * makes sense for your device, and is considered the lowest address contained
+ * in the bank. The value could be the offset of this bank of registers within
+ * the device itself, with the device's own offset subtracted out before read
+ * or write are called. It could alternatively be the base address of the
+ * entire device, with the address from accesses passed into read or write
+ * unmodified.
+ *
+ * To add actual registers to the RegisterBank (discussed below), you can use
+ * either the addRegister method which adds a single register, or addRegisters
+ * which adds an initializer list of them all at once. The register will be
+ * appended to the end of the bank as they're added, contiguous to the
+ * existing registers. The size of the bank is automatically accumulated as
+ * registers are added.
+ *
+ * The base(), size() and name() methods can be used to access each of those
+ * read only properties of the RegisterBank instance.
+ *
+ * While the RegisterBank itself doesn't have any data in it directly and so
+ * has no endianness, it's very likely all the registers within it will have
+ * the same endinanness. The bank itself therefore has a default endianness
+ * which, unless specified otherwise, will be passed on to the register types
+ * within it. The RegisterBank class is templated on its endianness. There are
+ * RegisterBankLE and RegisterBankBE aliases to make it a little easier to
+ * refer to one or the other version.
+ *
+ *
+ * == Register interface ==
+ *
+ * Every register in a RegisterBank needs to inherit, directly or indirectly,
+ * from the RegisterBase class. Each register must have a name (for debugging),
+ * and a well defined size. The following methods define the interface the
+ * register bank uses to access the register, and where the register can
+ * implement its special behaviors:
+ *
+ * void read(void *buf);
+ * void read(void *buf, off_t offset, size_t bytes);
+ *
+ * void write(const void *buf);
+ * void write(const void *buf, off_t offset, size_t bytes);
+ *
+ * The single argument versions of these methods completely overwrite the
+ * register's contents with whatever is pointed to by buf.
+ *
+ * The version which also takes "offset" and "bytes" arguments reads or writes
+ * only a portion of the register, starting "offset" bytes from the start of
+ * the register, and writing or reading the next "bytes" bytes.
+ *
+ * Each register also needs to implement serialize or unserialize methods
+ * which make it accessible to the checkpointing mechanism. If a register
+ * doesn't need to be serialized (for instance if it has a fixed value) then
+ * it still has to implement these methods, but they don't have to actually do
+ * anything.
+ *
+ *
+ * == Basic Register types ==
+ *
+ * Some simple register types have been defined which handle basic, common
+ * behaviors found in many devices:
+ *
+ * = RegisterRaz and RegisterRao =
+ *
+ * RegisterRaz (read as zero) and RegisterRao (read as one) will ignore writes,
+ * and will return all zeroes or ones, respectively, when read. These can have
+ * arbitrary alignment and size, and can be used for, for instance,
+ * unimplemented registers that still need to take up a certain amount of
+ * space, or for gaps between registers which still need to handle accesses
+ * even though they don't do anything or hold any data.
+ *
+ * For instance, a device might have several regions of registers which are
+ * aligned on different boundaries, but which might not take up all of the
+ * space in each region. The extra space can be filled with a RegisterRaz or
+ * RegisterRao, making it possible to implement all the registers as a single
+ * bank.
+ *
+ * If you need a register with a different fill pattern, you can subclass the
+ * RegisterRoFill type and implement its "fill" method. This should behave
+ * like the three argument form of the read() method, described above.
+ *
+ * = RegisterBuf and RegisterLBuf =
+ *
+ * These two types act like inert blobs of storage. They don't have any
+ * special behavior and can have any arbitrary size like the RegisterRao and
+ * RegisterRaz types above, but these registers actually store what's written
+ * to them.
+ *
+ * The RegisterBuf type acts as an interface to a buffer stored elsewhere. That
+ * makes it possible to, for instance, alias the same buffer to different parts
+ * of the register space, or to expose some other object which needs to exist
+ * outside of the register bank for some reason.
+ *
+ * The RegisterLBuf does the same thing, except it uses a local buffer it
+ * manages. That makes it a little easier to work with if you don't need the
+ * flexibility of the RegisterBuf type.
+ *
+ *
+ * == Typed Registers ==
+ *
+ * The Register template class is for more complex registers with side effects,
+ * and/or which hold structured data. The template arguments define what type
+ * the register should hold, and also its endianness.
+ *
+ * = Access handlers =
+ *
+ * Instead of subclassing the Register<Data> type and redefining its read/write
+ * methods, reads and writes are implemented using replaceable handlers with
+ * these signatures:
+ *
+ * Data read(Register<Data> &reg);
+ * Data partialRead(Register<Data> &reg, int first, int last);
+ * void write(Register<Data> &reg, const Data &value);
+ * void partialWrite(Register<Data> &reg, const Data &value,
+ *                   int first, int last);
+ *
+ * The "partial" version of these handlers take "first" and "last" arguments
+ * which specify what bits of the register to modify. They should be
+ * interpreted like the same arguments in base/bitfield.hh. The endianness
+ * of the register will have already been dealt with by the time the handler
+ * is called.
+ *
+ * The read and partialRead handlers should generate whatever value reading the
+ * register should return, based on (or not based on) the state of "reg". The
+ * partial handler should keep the bits it returns in place. For example, if
+ * bits 15-8 are read from a 16 bit register with the value 0x1234, it should
+ * return 0x1200, not 0x0012.
+ *
+ * The write and partialWrite handlers work the same way, except in they write
+ * instead of read. They are responsible for updating the value in reg in
+ * whatever way and to whatever value is appropriate, based on
+ * (or not based on) the value of "value" and the state of "reg".
+ *
+ * The default implementations of the read and write handlers simply return or
+ * update the value stored in reg. The default partial read calls the read
+ * handler (which may not be the default), and trims down the data as required.
+ * The default partial write handler calls the read handler (which may not be
+ * the default), updates the value as requested, and then calls the write
+ * handler (which may not be the default).
+ *
+ * Overriding the partial read or write methods might be necessary if reads or
+ * writes have side effects which should affect only the part of the register
+ * read or written. For instance, there might be some status bits which will
+ * be cleared when accessed. Only the bits which were actually accessed should
+ * be affected, even if they're grouped together logically with the other bits
+ * in a single register.
+ *
+ * To set your own handlers, you can use the "reader", "writer",
+ * "partialReader", and "partialWriter" methods. Each of these takes a single
+ * callable argument (lambda, functor, function pointer, etc.) which will
+ * replace the current corresponding handler.
+ *
+ * These methods all return a reference to the current Register so that they
+ * can be strung together without having to respecify what object you're
+ * modifying over and over again.
+ *
+ * There are also versions of these which will set up methods on some object as
+ * the handlers. These take a pointer to whatever object will handle the call,
+ * and a member function pointer to the method that will actually implement
+ * the handler. This can be used if, for instance, the registers are all
+ * members of a RegisterBank subclass, and need to call methods on their
+ * parent class to actually implement the behavior. These methods must have
+ * the same signature as above, with the exception that they are methods and
+ * not bare functions.
+ *
+ * When updating the register's value in custom write or partialWrite handlers,
+ * be sure to use the "update" method which will honor read only bits. There
+ * is an alternative form of update which also takes a custom bitmask, if you
+ * need to update bits other than the normally writeable ones.
+ *
+ * = Read only bits =
+ *
+ * Often registers have bits which are fixed and not affected by writes. To
+ * specify which bits are writeable, use the "writeable" method which takes a
+ * single argument the same type as the type of the register. It should hold a
+ * bitmask where a 1 bit can be written, and a 0 cannot. Calling writeable with
+ * no arguments will return the current bitmask.
+ *
+ * A shorthand "readonly" method marks all bits as read only.
+ *
+ * Both methods return a reference to the current Register so they can be
+ * strung together into a sequence when configuring it.
+ *
+ * = Underlying data and serialization =
+ *
+ * The "get" method returns a reference to the underlying storage inside the
+ * register. That can be used to manually update the entire register, even bits
+ * which are normally read only, or for structured data, to access members of
+ * the underlying data type.
+ *
+ * For instance, if the register holds a BitUnion, you could use the get()
+ * method to access the bitfields within it:
+ *
+ * reg.get().bitfieldA = reg.get().bitfieldB;
+ *
+ * The serialize and unserialize methods for these types will pass through the
+ * underlying data within the register. For instance, when serializing a
+ * Register<Foo>, the value in the checkpoint will be the same as if you had
+ * serialized a Foo directly, with the value stored in the register.
+ *
+ * = Aliases =
+ *
+ * Some convenient aliases have been defined for frequently used versions of
+ * the Register class. These are
+ *
+ * Register(8|16|32|64)(LE|BE|)
+ *
+ * Where the underlying type of the register is a uint8_t, uint16_t, etc, and
+ * the endianness is little endian, big endian, or whatever the default is for
+ * the RegisterBank.
+ */
+
+// Common bases to make it easier to identify both endiannesses at once.
+class RegisterBankBase
+{
+  public:
+    class RegisterBaseBase {};
+};
+
+template <ByteOrder BankByteOrder>
+class RegisterBank : public RegisterBankBase
+{
+  public:
+    // Static helper methods for implementing register types.
+    template <typename Data>
+    static constexpr Data
+    readWithMask(const Data &value, const Data &bitmask)
+    {
+        return value & bitmask;
+    }
+
+    template <typename Data>
+    static constexpr Data
+    writeWithMask(const Data &old, const Data &value, const Data &bitmask)
+    {
+        return readWithMask(
+                old, (Data)~bitmask) | readWithMask(value, bitmask);
+    }
+
+    class RegisterBase : public RegisterBankBase::RegisterBaseBase
+    {
+      protected:
+        const std::string _name;
+        size_t _size = 0;
+
+      public:
+        constexpr RegisterBase(const std::string &new_name, size_t new_size) :
+            _name(new_name), _size(new_size)
+        {}
+        virtual ~RegisterBase() {}
+
+        // Read the register's name.
+        virtual const std::string &name() const { return _name; }
+
+        // Read the register's size in bytes.
+        size_t size() const { return _size; }
+
+        // Perform a read on the register.
+        virtual void read(void *buf) = 0;
+        virtual void read(void *buf, off_t offset, size_t bytes) = 0;
+
+        // Perform a write on the register.
+        virtual void write(const void *buf) = 0;
+        virtual void write(const void *buf, off_t offset, size_t bytes) = 0;
+
+        // Methods for implementing serialization for checkpoints.
+        virtual void serialize(std::ostream &os) const = 0;
+        virtual bool unserialize(const std::string &s) = 0;
+    };
+
+    // Filler registers which return a fixed pattern.
+    class RegisterRoFill : public RegisterBase
+    {
+      protected:
+        constexpr RegisterRoFill(
+                const std::string &new_name, size_t new_size) :
+            RegisterBase(new_name, new_size)
+        {}
+
+        virtual void fill(void *buf, off_t offset, size_t bytes) = 0;
+
+      public:
+        // Ignore writes.
+        void write(const void *buf) override {}
+        void write(const void *buf, off_t offset, size_t bytes) override {}
+
+        // Use fill() to handle reads.
+        void read(void *buf) override { fill(buf, 0, this->size()); }
+        void
+        read(void *buf, off_t offset, size_t bytes) override
+        {
+            fill(buf, offset, bytes);
+        }
+
+        void serialize(std::ostream &os) const override {}
+        bool unserialize(const std::string &s) override { return true; }
+    };
+
+    // Register which reads as all zeroes.
+    class RegisterRaz : public RegisterRoFill
+    {
+      protected:
+        void
+        fill(void *buf, off_t offset, size_t bytes) override
+        {
+            bzero(buf, bytes);
+        }
+
+      public:
+        RegisterRaz(const std::string &new_name, size_t new_size) :
+            RegisterRoFill(new_name, new_size)
+        {}
+    };
+
+    // Register which reads as all ones.
+    class RegisterRao : public RegisterRoFill
+    {
+      protected:
+        void
+        fill(void *buf, off_t offset, size_t bytes) override
+        {
+            memset(buf, 0xff, bytes);
+        }
+
+      public:
+        RegisterRao(const std::string &new_name, size_t new_size) :
+            RegisterRoFill(new_name, new_size)
+        {}
+    };
+
+    // Register which acts as a simple buffer.
+    class RegisterBuf : public RegisterBase
+    {
+      private:
+        void *_ptr = nullptr;
+
+      public:
+        RegisterBuf(const std::string &new_name, void *ptr, size_t bytes) :
+            RegisterBase(new_name, bytes), _ptr(ptr)
+        {}
+
+        void write(const void *buf) override { write(buf, 0, this->size()); }
+        void
+        write(const void *buf, off_t offset, size_t bytes) override
+        {
+            assert(offset + bytes <= this->size());
+            memcpy((uint8_t *)_ptr + offset, buf, bytes);
+        }
+
+        void read(void *buf) override { read(buf, 0, this->size()); }
+        void
+        read(void *buf, off_t offset, size_t bytes) override
+        {
+            assert(offset + bytes <= this->size());
+            memcpy(buf, (uint8_t *)_ptr + offset, bytes);
+        }
+
+        // The buffer's owner is responsible for serializing it.
+        void serialize(std::ostream &os) const override {}
+        bool unserialize(const std::string &s) override { return true; }
+    };
+
+    // Same as above, but which keeps its storage locally.
+    template <int BufBytes>
+    class RegisterLBuf : public RegisterBuf
+    {
+      public:
+        std::array<uint8_t, BufBytes> buffer;
+
+        RegisterLBuf(const std::string &new_name) :
+            RegisterBuf(new_name, buffer.data(), BufBytes)
+        {}
+
+        void
+        serialize(std::ostream &os) const override
+        {
+            if (BufBytes)
+                ShowParam<uint8_t>::show(os, buffer[0]);
+            for (int i = 1; i < BufBytes; i++) {
+                os << " ";
+                ShowParam<uint8_t>::show(os, buffer[i]);
+            }
+        }
+
+        bool
+        unserialize(const std::string &s) override
+        {
+            std::vector<std::string> tokens;
+            std::istringstream is(s);
+
+            std::string token;
+            while (is >> token)
+                tokens.push_back(token);
+
+            if (tokens.size() != BufBytes) {
+                warn("Size mismatch unserialing %s, expected %d, got %d",
+                        this->name(), BufBytes, tokens.size());
+                return false;
+            }
+
+            for (int i = 0; i < BufBytes; i++) {
+                if (!ParseParam<uint8_t>::parse(tokens[i], buffer[i]))
+                    return false;
+            }
+
+            return true;
+        }
+    };
+
+    template <typename Data, ByteOrder RegByteOrder=BankByteOrder>
+    class Register : public RegisterBase
+    {
+      protected:
+        using This = Register<Data, RegByteOrder>;
+
+      public:
+        using ReadFunc = std::function<Data (This &reg)>;
+        using PartialReadFunc = std::function<
+            Data (This &reg, int first, int last)>;
+        using WriteFunc = std::function<void (This &reg, const Data &value)>;
+        using PartialWriteFunc = std::function<
+            void (This &reg, const Data &value, int first, int last)>;
+
+      private:
+        Data _data = {};
+        Data _writeMask = mask(sizeof(Data) * 8);
+
+        ReadFunc _reader = defaultReader;
+        WriteFunc _writer = defaultWriter;
+        PartialWriteFunc _partialWriter = defaultPartialWriter;
+        PartialReadFunc _partialReader = defaultPartialReader;
+
+      protected:
+        static Data defaultReader(This &reg) { return reg.get(); }
+
+        static Data
+        defaultPartialReader(This &reg, int first, int last)
+        {
+            return mbits(reg._reader(reg), first, last);
+        }
+
+        static void
+        defaultWriter(This &reg, const Data &value)
+        {
+            reg.update(value);
+        }
+
+        static void
+        defaultPartialWriter(This &reg, const Data &value, int first, int last)
+        {
+            reg._writer(reg, writeWithMask<Data>(reg._reader(reg), value,
+                                                 mask(first, last)));
+        }
+
+        constexpr Data
+        htoreg(Data data)
+        {
+            switch (RegByteOrder) {
+              case ByteOrder::big:
+                return htobe(data);
+              case ByteOrder::little:
+                return htole(data);
+              default:
+                panic("Unrecognized byte order %d.", (unsigned)RegByteOrder);
+            }
+        }
+
+        constexpr Data
+        regtoh(Data data)
+        {
+            switch (RegByteOrder) {
+              case ByteOrder::big:
+                return betoh(data);
+              case ByteOrder::little:
+                return letoh(data);
+              default:
+                panic("Unrecognized byte order %d.", (unsigned)RegByteOrder);
+            }
+        }
+
+      public:
+
+        /*
+         * Interface for setting up the register.
+         */
+
+        // Constructor which lets data default initialize itself.
+        constexpr Register(const std::string &new_name) :
+            RegisterBase(new_name, sizeof(Data))
+        {}
+
+        // Constructor and move constructor with an initial data value.
+        constexpr Register(const std::string &new_name, const Data &new_data) :
+            RegisterBase(new_name, sizeof(Data)), _data(new_data)
+        {}
+        constexpr Register(const std::string &new_name,
+                           const Data &&new_data) :
+            RegisterBase(new_name, sizeof(Data)), _data(new_data)
+        {}
+
+        // Set which bits of the register are writeable.
+        constexpr This &
+        writeable(const Data &new_mask)
+        {
+            _writeMask = new_mask;
+            return *this;
+        }
+
+        // Set the register as read only.
+        constexpr This &readonly() { return writeable(0); }
+
+        // Set the callables which handles reads or writes.
+        // The default reader just returns the register value.
+        // The default writer uses the write mask to update the register value.
+        constexpr This &
+        reader(const ReadFunc &new_reader)
+        {
+            _reader = new_reader;
+            return *this;
+        }
+        template <class Parent, class... Args>
+        constexpr This &
+        reader(Parent *parent, Data (Parent::*nr)(Args... args))
+        {
+            auto wrapper = [parent, nr](Args&&... args) -> Data {
+                return (parent->*nr)(std::forward<Args>(args)...);
+            };
+            return reader(wrapper);
+        }
+        constexpr This &
+        writer(const WriteFunc &new_writer)
+        {
+            _writer = new_writer;
+            return *this;
+        }
+        template <class Parent, class... Args>
+        constexpr This &
+        writer(Parent *parent, void (Parent::*nw)(Args... args))
+        {
+            auto wrapper = [parent, nw](Args&&... args) {
+                (parent->*nw)(std::forward<Args>(args)...);
+            };
+            return writer(wrapper);
+        }
+
+        // Set the callables which handle reads or writes. These may need to
+        // be handled specially if, for instance, accessing bits outside of
+        // the enables would have side effects that shouldn't happen.
+        //
+        // The default partial reader just uses the byte enables to mask off
+        // bits that are not being read.
+        //
+        // The default partial writer reads the current value of the register,
+        // uses the byte enables to update only the bytes that are changing,
+        // and then writes the result back to the register.
+        constexpr This &
+        partialReader(const PartialReadFunc &new_reader)
+        {
+            _partialReader = new_reader;
+            return *this;
+        }
+        template <class Parent, class... Args>
+        constexpr This &
+        partialReader(Parent *parent, Data (Parent::*nr)(Args... args))
+        {
+            auto wrapper = [parent, nr](Args&&... args) -> Data {
+                return (parent->*nr)(std::forward<Args>(args)...);
+            };
+            return partialReader(wrapper);
+        }
+        constexpr This &
+        partialWriter(const PartialWriteFunc &new_writer)
+        {
+            _partialWriter = new_writer;
+            return *this;
+        }
+        template <class Parent, class... Args>
+        constexpr This &
+        partialWriter(Parent *parent, void (Parent::*nw)(Args... args))
+        {
+            auto wrapper = [parent, nw](Args&&... args) {
+                return (parent->*nw)(std::forward<Args>(args)...);
+            };
+            return partialWriter(wrapper);
+        }
+
+
+        /*
+         * Interface for accessing the register's state, for use by the
+         * register's helper functions and the register bank.
+         */
+
+        const Data &writeable() const { return _writeMask; }
+
+        // Directly access the underlying data value.
+        const Data &get() const { return _data; }
+        Data &get() { return _data; }
+
+        // Update data while applying a mask.
+        void
+        update(const Data &new_data, const Data &bitmask)
+        {
+            _data = writeWithMask(_data, new_data, bitmask);
+        }
+        // This version uses the default write mask.
+        void
+        update(const Data &new_data)
+        {
+            _data = writeWithMask(_data, new_data, _writeMask);
+        }
+
+
+        /*
+         * Interface for reading/writing the register, for use by the
+         * register bank.
+         */
+
+        // Perform a read on the register.
+        void
+        read(void *buf) override
+        {
+            Data data = htoreg(_reader(*this));
+            memcpy(buf, (uint8_t *)&data, sizeof(data));
+        }
+
+        void
+        read(void *buf, off_t offset, size_t bytes) override
+        {
+            // Move the region we're reading to be little endian, since that's
+            // what gem5 uses internally in BitUnions, masks, etc.
+            const off_t host_off = (RegByteOrder != ByteOrder::little) ?
+                sizeof(Data) - (offset + bytes) : offset;
+
+            const int first = (host_off + bytes) * 8 - 1;
+            const int last = host_off * 8;
+            Data data = htoreg(_partialReader(*this, first, last));
+
+            memcpy(buf, (uint8_t *)&data + offset, bytes);
+        }
+
+        // Perform a write on the register.
+        void
+        write(const void *buf) override
+        {
+            Data data;
+            memcpy((uint8_t *)&data, buf, sizeof(data));
+            data = regtoh(data);
+            _writer(*this, data);
+        }
+
+        void
+        write(const void *buf, off_t offset, size_t bytes) override
+        {
+            Data data = {};
+            memcpy((uint8_t *)&data + offset, buf, bytes);
+
+            data = regtoh(data);
+
+            // Move the region we're reading to be little endian, since that's
+            // what gem5 uses internally in BitUnions, masks, etc.
+            const off_t host_off = (RegByteOrder != ByteOrder::little) ?
+                sizeof(Data) - (offset + bytes) : offset;
+
+            const int first = (host_off + bytes) * 8 - 1;
+            const int last = host_off * 8;
+            _partialWriter(*this, data, first, last);
+        }
+
+        // Serialize our data using existing mechanisms.
+        void
+        serialize(std::ostream &os) const override
+        {
+            ShowParam<Data>::show(os, get());
+        }
+
+        bool
+        unserialize(const std::string &s) override
+        {
+            return ParseParam<Data>::parse(s, get());
+        }
+    };
+
+  private:
+    std::map<Addr, std::reference_wrapper<RegisterBase>> _offsetMap;
+
+    Addr _base = 0;
+    Addr _size = 0;
+    const std::string _name;
+
+  public:
+
+    using Register8 = Register<uint8_t>;
+    using Register8LE = Register<uint8_t, ByteOrder::little>;
+    using Register8BE = Register<uint8_t, ByteOrder::big>;
+    using Register16 = Register<uint16_t>;
+    using Register16LE = Register<uint16_t, ByteOrder::little>;
+    using Register16BE = Register<uint16_t, ByteOrder::big>;
+    using Register32 = Register<uint32_t>;
+    using Register32LE = Register<uint32_t, ByteOrder::little>;
+    using Register32BE = Register<uint32_t, ByteOrder::big>;
+    using Register64 = Register<uint64_t>;
+    using Register64LE = Register<uint64_t, ByteOrder::little>;
+    using Register64BE = Register<uint64_t, ByteOrder::big>;
+
+
+    constexpr RegisterBank(const std::string &new_name, Addr new_base) :
+        _base(new_base), _name(new_name)
+    {}
+
+    virtual ~RegisterBank() {}
+
+    void
+    addRegisters(
+            std::initializer_list<std::reference_wrapper<RegisterBase>> regs)
+    {
+        panic_if(regs.size() == 0, "Adding an empty list of registers to %s?",
+                 name());
+        for (auto &reg: regs) {
+            _offsetMap.emplace(_base + _size, reg);
+            _size += reg.get().size();
+        }
+    }
+
+    void addRegister(RegisterBase &reg) { addRegisters({reg}); }
+
+    Addr base() const { return _base; }
+    Addr size() const { return _size; }
+    const std::string &name() const { return _name; }
+
+    virtual void
+    read(Addr addr, void *buf, Addr bytes)
+    {
+        uint8_t *ptr = (uint8_t *)buf;
+        // Number of bytes we've transferred.
+        Addr done = 0;
+
+        panic_if(addr - base() + bytes > size(),
+            "Out of bounds read in register bank %s, address %#x, size %d.",
+            name(), addr, bytes);
+
+        auto it = _offsetMap.lower_bound(addr);
+        if (it == _offsetMap.end() || it->first > addr)
+            it--;
+
+        if (it->first < addr) {
+            RegisterBase &reg = it->second.get();
+            // Skip at least the beginning of the first register.
+
+            // Figure out what parts of it we're accessing.
+            const off_t reg_off = addr - it->first;
+            const size_t reg_bytes = std::min(reg.size() - reg_off,
+                                              bytes - done);
+
+            // Actually do the access.
+            reg.read(ptr, reg_off, reg_bytes);
+            done += reg_bytes;
+            it++;
+
+            // Was that everything?
+            if (done == bytes)
+                return;
+        }
+
+        while (true) {
+            RegisterBase &reg = it->second.get();
+
+            const size_t reg_size = reg.size();
+            const size_t remaining = bytes - done;
+
+            if (remaining == reg_size) {
+                // A complete register read, and then we're done.
+                reg.read(ptr + done);
+                return;
+            } else if (remaining > reg_size) {
+                // A complete register read, with more to go.
+                reg.read(ptr + done);
+                done += reg_size;
+                it++;
+            } else {
+                // Skip the end of the register, and then we're done.
+                reg.read(ptr + done, 0, remaining);
+                return;
+            }
+        }
+    }
+
+    virtual void
+    write(Addr addr, const void *buf, Addr bytes)
+    {
+        const uint8_t *ptr = (const uint8_t *)buf;
+        // Number of bytes we've transferred.
+        Addr done = 0;
+
+        panic_if(addr - base() + bytes > size(),
+            "Out of bounds write in register bank %s, address %#x, size %d.",
+            name(), addr, bytes);
+
+        auto it = _offsetMap.lower_bound(addr);
+        if (it == _offsetMap.end() || it->first > addr)
+            it--;
+
+        if (it->first < addr) {
+            RegisterBase &reg = it->second.get();
+            // Skip at least the beginning of the first register.
+
+            // Figure out what parts of it we're accessing.
+            const off_t reg_off = addr - it->first;
+            const size_t reg_bytes = std::min(reg.size() - reg_off,
+                                              bytes - done);
+
+            // Actually do the access.
+            reg.write(ptr, reg_off, reg_bytes);
+            done += reg_bytes;
+            it++;
+
+            // Was that everything?
+            if (done == bytes)
+                return;
+        }
+
+        while (true) {
+            RegisterBase &reg = it->second.get();
+
+            const size_t reg_size = reg.size();
+            const size_t remaining = bytes - done;
+
+            if (remaining == reg_size) {
+                // A complete register write, and then we're done.
+                reg.write(ptr + done);
+                return;
+            } else if (remaining > reg_size) {
+                // A complete register write, with more to go.
+                reg.write(ptr + done);
+                done += reg_size;
+                it++;
+            } else {
+                // Skip the end of the register, and then we're done.
+                reg.write(ptr + done, 0, remaining);
+                return;
+            }
+        }
+    }
+};
+
+using RegisterBankLE = RegisterBank<ByteOrder::little>;
+using RegisterBankBE = RegisterBank<ByteOrder::big>;
+
+// Delegate serialization to the individual RegisterBase subclasses.
+template <class T>
+struct ParseParam<T, std::enable_if_t<std::is_base_of<
+    typename RegisterBankBase::RegisterBaseBase, T>::value>>
+{
+    static bool
+    parse(const std::string &s, T &value)
+    {
+        return value.unserialize(s);
+    }
+};
+
+template <class T>
+struct ShowParam<T, std::enable_if_t<std::is_base_of<
+    typename RegisterBankBase::RegisterBaseBase, T>::value>>
+{
+    static void
+    show(std::ostream &os, const T &value)
+    {
+        value.serialize(os);
+    }
+};
+
+#endif // __DEV_REG_BANK_HH__
diff --git a/src/dev/reg_bank.test.cc b/src/dev/reg_bank.test.cc
new file mode 100644 (file)
index 0000000..348900b
--- /dev/null
@@ -0,0 +1,1276 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met: redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer;
+ * redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution;
+ * neither the name of the copyright holders nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-copy"
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#pragma GCC diagnostic pop
+
+#include <vector>
+
+#include "dev/reg_bank.hh"
+
+// Compare the elements of an array against expected values.
+using testing::ElementsAre;
+// This version is needed with enough elements, empirically more than 10.
+using testing::ElementsAreArray;
+
+
+/*
+ * The RegisterRaz (read as zero) type.
+ */
+
+class RegisterRazTest : public testing::Test
+{
+  protected:
+    static constexpr size_t BufSize = 12;
+    static constexpr size_t BufOffset = 4;
+    static constexpr size_t RazSize = 4;
+
+    std::array<uint8_t, BufSize> buf;
+    RegisterBankLE::RegisterRaz raz;
+
+    RegisterRazTest() : raz("raz", RazSize)
+    {
+        buf.fill(0xff);
+    }
+};
+
+TEST_F(RegisterRazTest, Name)
+{
+    EXPECT_EQ(raz.name(), "raz");
+}
+
+TEST_F(RegisterRazTest, Size)
+{
+    EXPECT_EQ(raz.size(), RazSize);
+}
+
+// Accessing the entire register at once.
+TEST_F(RegisterRazTest, FullAccess)
+{
+    raz.write(buf.data() + BufOffset);
+    raz.read(buf.data() + BufOffset);
+    EXPECT_THAT(buf, ElementsAreArray({0xff, 0xff, 0xff, 0xff,
+                                       0x00, 0x00, 0x00, 0x00,
+                                       0xff, 0xff, 0xff, 0xff}));
+}
+
+// Partial access, excluding the start of the register.
+TEST_F(RegisterRazTest, PartialAccessHigh)
+{
+    raz.write(buf.data() + BufOffset, 1, 3);
+    raz.read(buf.data() + BufOffset, 1, 3);
+    EXPECT_THAT(buf, ElementsAreArray({0xff, 0xff, 0xff, 0xff,
+                                       0x00, 0x00, 0x00, 0xff,
+                                       0xff, 0xff, 0xff, 0xff}));
+}
+
+// Partial access, excluding the end of the register.
+TEST_F(RegisterRazTest, PartialAccessLow)
+{
+    raz.write(buf.data() + BufOffset, 0, 3);
+    raz.read(buf.data() + BufOffset, 0, 3);
+    EXPECT_THAT(buf, ElementsAreArray({0xff, 0xff, 0xff, 0xff,
+                                       0x00, 0x00, 0x00, 0xff,
+                                       0xff, 0xff, 0xff, 0xff}));
+}
+
+// Partial access, excluding both ends of the register.
+TEST_F(RegisterRazTest, PartialAccessMid)
+{
+    raz.write(buf.data() + BufOffset, 1, 2);
+    raz.read(buf.data() + BufOffset, 1, 2);
+    EXPECT_THAT(buf, ElementsAreArray({0xff, 0xff, 0xff, 0xff,
+                                       0x00, 0x00, 0xff, 0xff,
+                                       0xff, 0xff, 0xff, 0xff}));
+}
+
+TEST_F(RegisterRazTest, Serialize)
+{
+    std::ostringstream os;
+    raz.serialize(os);
+    EXPECT_EQ(os.str(), "");
+}
+
+TEST_F(RegisterRazTest, Unserialize)
+{
+    std::string s;
+    EXPECT_TRUE(raz.unserialize(s));
+}
+
+
+/*
+ * The RegisterRao (read as one) type.
+ */
+
+class RegisterRaoTest : public testing::Test
+{
+  protected:
+    static constexpr size_t BufSize = 12;
+    static constexpr size_t BufOffset = 4;
+    static constexpr size_t RaoSize = 4;
+
+    std::array<uint8_t, BufSize> buf;
+    RegisterBankLE::RegisterRao rao;
+
+    RegisterRaoTest() : rao("rao", RaoSize)
+    {
+        buf.fill(0x00);
+    }
+};
+
+TEST_F(RegisterRaoTest, Name)
+{
+    EXPECT_EQ(rao.name(), "rao");
+}
+
+TEST_F(RegisterRaoTest, Size)
+{
+    EXPECT_EQ(rao.size(), RaoSize);
+}
+
+// Accessing the entire register at once.
+TEST_F(RegisterRaoTest, FullAccess)
+{
+    rao.write(buf.data() + BufOffset);
+    rao.read(buf.data() + BufOffset);
+    EXPECT_THAT(buf, ElementsAreArray({0x00, 0x00, 0x00, 0x00,
+                                       0xff, 0xff, 0xff, 0xff,
+                                       0x00, 0x00, 0x00, 0x00}));
+}
+
+// Partial access, excluding the start of the register.
+TEST_F(RegisterRaoTest, PartialAccessHigh)
+{
+    rao.write(buf.data() + BufOffset, 1, 3);
+    rao.read(buf.data() + BufOffset, 1, 3);
+    EXPECT_THAT(buf, ElementsAreArray({0x00, 0x00, 0x00, 0x00,
+                                       0xff, 0xff, 0xff, 0x00,
+                                       0x00, 0x00, 0x00, 0x00}));
+}
+
+// Partial access, excluding the end of the register.
+TEST_F(RegisterRaoTest, PartialAccessLow)
+{
+    rao.write(buf.data() + BufOffset, 0, 3);
+    rao.read(buf.data() + BufOffset, 0, 3);
+    EXPECT_THAT(buf, ElementsAreArray({0x00, 0x00, 0x00, 0x00,
+                                       0xff, 0xff, 0xff, 0x00,
+                                       0x00, 0x00, 0x00, 0x00}));
+}
+
+// Partial access, excluding both ends of the register.
+TEST_F(RegisterRaoTest, PartialAccessMid)
+{
+    rao.write(buf.data() + BufOffset, 1, 2);
+    rao.read(buf.data() + BufOffset, 1, 2);
+    EXPECT_THAT(buf, ElementsAreArray({0x00, 0x00, 0x00, 0x00,
+                                       0xff, 0xff, 0x00, 0x00,
+                                       0x00, 0x00, 0x00, 0x00}));
+}
+
+TEST_F(RegisterRaoTest, Serialize)
+{
+    std::ostringstream os;
+    rao.serialize(os);
+    EXPECT_EQ(os.str(), "");
+}
+
+TEST_F(RegisterRaoTest, Unserialize)
+{
+    std::string s;
+    EXPECT_TRUE(rao.unserialize(s));
+}
+
+
+/*
+ * The RegisterBuf type.
+ */
+
+class RegisterBufTest : public testing::Test
+{
+  protected:
+    static constexpr size_t RegSize = 4;
+
+    RegisterBankLE::RegisterBuf reg;
+
+    std::array<uint8_t, RegSize * 3> buf;
+    std::array<uint8_t, RegSize * 3> backing;
+
+  public:
+    RegisterBufTest() : reg("buf_reg", backing.data() + RegSize, RegSize),
+        buf{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc},
+        backing{0x10, 0x20, 0x30, 0x40, 0x50, 0x60,
+                0x70, 0x80, 0x90, 0xa0, 0xb0, 0xc0}
+    {}
+};
+
+TEST_F(RegisterBufTest, Name)
+{
+    EXPECT_EQ(reg.name(), "buf_reg");
+}
+
+TEST_F(RegisterBufTest, Size)
+{
+    EXPECT_EQ(reg.size(), RegSize);
+}
+
+// Read the entire register.
+TEST_F(RegisterBufTest, FullRead)
+{
+    reg.read(buf.data() + RegSize);
+    EXPECT_THAT(buf, ElementsAreArray({0x1, 0x2, 0x3, 0x4,
+                                       0x50, 0x60, 0x70, 0x80,
+                                       0x9, 0xa, 0xb, 0xc}));
+    EXPECT_THAT(backing, ElementsAreArray({0x10, 0x20, 0x30, 0x40,
+                                           0x50, 0x60, 0x70, 0x80,
+                                           0x90, 0xa0, 0xb0, 0xc0}));
+}
+
+// Write the entire register.
+TEST_F(RegisterBufTest, FullWrite)
+{
+    reg.write(buf.data() + RegSize);
+    EXPECT_THAT(buf, ElementsAreArray({0x1, 0x2, 0x3, 0x4,
+                                       0x5, 0x6, 0x7, 0x8,
+                                       0x9, 0xa, 0xb, 0xc}));
+    EXPECT_THAT(backing, ElementsAreArray({0x10, 0x20, 0x30, 0x40,
+                                           0x5, 0x6, 0x7, 0x8,
+                                           0x90, 0xa0, 0xb0, 0xc0}));
+}
+
+// Partial read, excluding the start of the register.
+TEST_F(RegisterBufTest, PartialReadHigh)
+{
+    reg.read(buf.data() + RegSize, 1, 3);
+    EXPECT_THAT(buf, ElementsAreArray({0x1, 0x2, 0x3, 0x4,
+                                       0x60, 0x70, 0x80, 0x8,
+                                       0x9, 0xa, 0xb, 0xc}));
+    EXPECT_THAT(backing, ElementsAreArray({0x10, 0x20, 0x30, 0x40,
+                                           0x50, 0x60, 0x70, 0x80,
+                                           0x90, 0xa0, 0xb0, 0xc0}));
+}
+
+// Partial write, excluding the start of the register.
+TEST_F(RegisterBufTest, PartialWriteHigh)
+{
+    reg.write(buf.data() + RegSize, 1, 3);
+    EXPECT_THAT(buf, ElementsAreArray({0x1, 0x2, 0x3, 0x4,
+                                       0x5, 0x6, 0x7, 0x8,
+                                       0x9, 0xa, 0xb, 0xc}));
+    EXPECT_THAT(backing, ElementsAreArray({0x10, 0x20, 0x30, 0x40,
+                                           0x50, 0x5, 0x6, 0x7,
+                                           0x90, 0xa0, 0xb0, 0xc0}));
+}
+
+// Partial read, excluding the end of the register.
+TEST_F(RegisterBufTest, PartialReadLow)
+{
+    reg.read(buf.data() + RegSize, 0, 3);
+    EXPECT_THAT(buf, ElementsAreArray({0x1, 0x2, 0x3, 0x4,
+                                       0x50, 0x60, 0x70, 0x8,
+                                       0x9, 0xa, 0xb, 0xc}));
+    EXPECT_THAT(backing, ElementsAreArray({0x10, 0x20, 0x30, 0x40,
+                                           0x50, 0x60, 0x70, 0x80,
+                                           0x90, 0xa0, 0xb0, 0xc0}));
+}
+
+// Partial write, excluding the end of the register.
+TEST_F(RegisterBufTest, PartialWriteLow)
+{
+    reg.write(buf.data() + RegSize, 0, 3);
+    EXPECT_THAT(buf, ElementsAreArray({0x1, 0x2, 0x3, 0x4,
+                                       0x5, 0x6, 0x7, 0x8,
+                                       0x9, 0xa, 0xb, 0xc}));
+    EXPECT_THAT(backing, ElementsAreArray({0x10, 0x20, 0x30, 0x40,
+                                           0x5, 0x6, 0x7, 0x80,
+                                           0x90, 0xa0, 0xb0, 0xc0}));
+}
+
+// Partial read, excluding both ends of the register.
+TEST_F(RegisterBufTest, PartialReadMid)
+{
+    reg.read(buf.data() + RegSize, 1, 2);
+    EXPECT_THAT(buf, ElementsAreArray({0x1, 0x2, 0x3, 0x4,
+                                       0x60, 0x70, 0x7, 0x8,
+                                       0x9, 0xa, 0xb, 0xc}));
+    EXPECT_THAT(backing, ElementsAreArray({0x10, 0x20, 0x30, 0x40,
+                                           0x50, 0x60, 0x70, 0x80,
+                                           0x90, 0xa0, 0xb0, 0xc0}));
+}
+
+// Partial write, excluding both ends of the register.
+TEST_F(RegisterBufTest, PartialWriteMid)
+{
+    reg.write(buf.data() + RegSize, 1, 2);
+    EXPECT_THAT(buf, ElementsAreArray({0x1, 0x2, 0x3, 0x4,
+                                       0x5, 0x6, 0x7, 0x8,
+                                       0x9, 0xa, 0xb, 0xc}));
+    EXPECT_THAT(backing, ElementsAreArray({0x10, 0x20, 0x30, 0x40,
+                                           0x50, 0x5, 0x6, 0x80,
+                                           0x90, 0xa0, 0xb0, 0xc0}));
+}
+
+TEST_F(RegisterBufTest, Serialize)
+{
+    std::ostringstream os;
+    reg.serialize(os);
+    EXPECT_EQ(os.str(), "");
+}
+
+TEST_F(RegisterBufTest, Unserialize)
+{
+    std::string s;
+    EXPECT_TRUE(reg.unserialize(s));
+}
+
+
+/*
+ * The RegisterLBuf type. Since it's so similar to RegisterBuf, just do a
+ * basic check that it's applying it's locally managed buffer to it's parent
+ * type.
+ */
+
+class RegisterLBufTest : public testing::Test
+{
+  protected:
+    static constexpr size_t RegSize = 12;
+
+    RegisterBankLE::RegisterLBuf<12> reg;
+    std::array<uint8_t, 4> to_write;
+
+  public:
+    RegisterLBufTest() : reg("lbuf_reg"), to_write{0x1, 0x2, 0x3, 0x4}
+    {
+        reg.buffer.fill(0xff);
+    }
+};
+
+TEST_F(RegisterLBufTest, Name)
+{
+    EXPECT_EQ(reg.name(), "lbuf_reg");
+}
+
+TEST_F(RegisterLBufTest, PartialWrite)
+{
+    reg.write(to_write.data(), 4, 4);
+    EXPECT_THAT(reg.buffer, ElementsAreArray({0xff, 0xff, 0xff, 0xff,
+                                              0x1, 0x2, 0x3, 0x4,
+                                              0xff, 0xff, 0xff, 0xff}));
+}
+
+TEST_F(RegisterLBufTest, Serialize)
+{
+    std::ostringstream os;
+    for (int i = 0; i < reg.buffer.size(); i++)
+        reg.buffer[i] = i;
+    reg.serialize(os);
+    EXPECT_EQ(os.str(), "0 1 2 3 4 5 6 7 8 9 10 11");
+}
+
+TEST_F(RegisterLBufTest, UnserializeSucess)
+{
+    std::string s = "0 1 2 3 4 5 6 7 8 9 10 11";
+    EXPECT_TRUE(reg.unserialize(s));
+    EXPECT_THAT(reg.buffer, ElementsAreArray({0, 1, 2, 3, 4, 5,
+                                              6, 7, 8, 9, 10, 11}));
+}
+
+TEST_F(RegisterLBufTest, UnserializeFailure)
+{
+    std::string s = "0 1 2 3 4 5 6 7 8 9 10";
+    EXPECT_FALSE(reg.unserialize(s));
+    EXPECT_THAT(reg.buffer, ElementsAreArray({0xff, 0xff, 0xff, 0xff,
+                                              0xff, 0xff, 0xff, 0xff,
+                                              0xff, 0xff, 0xff, 0xff}));
+}
+
+
+/*
+ * The templated Register<> type which takes a backing type and endianness
+ * as template parameters.
+ */
+
+class TypedRegisterTest : public testing::Test
+{
+  protected:
+    using BackingType = uint16_t;
+    static constexpr size_t RegSize = sizeof(BackingType);
+
+    // We'll typically test with the little endian version, since it only
+    // matters for a few methods.
+    RegisterBankLE::Register<BackingType> reg;
+    RegisterBankBE::Register<BackingType> regBE;
+
+    std::array<uint8_t, RegSize * 3> buf;
+
+    TypedRegisterTest() : reg("le_reg", 0x1122), regBE("be_reg", 0x1122),
+        buf{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}
+    {}
+};
+
+TEST_F(TypedRegisterTest, DefaultConstructor)
+{
+    RegisterBankLE::Register<uint32_t> def("def");
+    EXPECT_EQ(def.get(), 0);
+}
+
+TEST_F(TypedRegisterTest, Name)
+{
+    EXPECT_EQ(reg.name(), "le_reg");
+}
+
+TEST_F(TypedRegisterTest, Size)
+{
+    EXPECT_EQ(reg.size(), RegSize);
+}
+
+TEST_F(TypedRegisterTest, Writable)
+{
+    // By default, all bits of the registers are writeable.
+    EXPECT_EQ(reg.writeable(), 0xffff);
+}
+
+// Verify that get returns the initial value of the reg.
+TEST_F(TypedRegisterTest, GetInitial)
+{
+    EXPECT_EQ(reg.get(), 0x1122);
+}
+
+TEST_F(TypedRegisterTest, Get)
+{
+    reg.get() = 0x1020;
+    EXPECT_EQ(reg.get(), 0x1020);
+    reg.get() = 0x3040;
+    EXPECT_EQ(reg.get(), 0x3040);
+}
+
+// Do a full big endian read using the default read handler.
+TEST_F(TypedRegisterTest, BigEndianDefaultFullRead)
+{
+    regBE.read(buf.data() + RegSize);
+    EXPECT_EQ(regBE.get(), 0x1122);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x11, 0x22, 0x5, 0x6));
+}
+
+// Do a full big endian write using the default write handler.
+TEST_F(TypedRegisterTest, BigEndianDefaultFullWrite)
+{
+    regBE.write(buf.data() + RegSize);
+    EXPECT_EQ(regBE.get(), 0x0304);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x3, 0x4, 0x5, 0x6));
+}
+
+// Do a partial big endian read of the low half of the register.
+TEST_F(TypedRegisterTest, BigEndianDefaultPartialReadLow)
+{
+    regBE.read(buf.data() + RegSize, 0, 1);
+    EXPECT_EQ(regBE.get(), 0x1122);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x11, 0x4, 0x5, 0x6));
+}
+
+// Do a partial big endian read of the high half of the register.
+TEST_F(TypedRegisterTest, BigEndianDefaultPartialReadHigh)
+{
+    regBE.read(buf.data() + RegSize, 1, 1);
+    EXPECT_EQ(regBE.get(), 0x1122);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x22, 0x4, 0x5, 0x6));
+}
+
+// Do a partial big endian write of the low half of the register.
+TEST_F(TypedRegisterTest, BigEndianDefaultPartialWriteLow)
+{
+    regBE.write(buf.data() + RegSize, 0, 1);
+    EXPECT_EQ(regBE.get(), 0x0322);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x3, 0x4, 0x5, 0x6));
+}
+
+// Do a partial big endian write of the High half of the register.
+TEST_F(TypedRegisterTest, BigEndianDefaultPartialWriteHigh)
+{
+    regBE.write(buf.data() + RegSize, 1, 1);
+    EXPECT_EQ(regBE.get(), 0x1103);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x3, 0x4, 0x5, 0x6));
+}
+
+// Do a full little endian read using the default read handler.
+TEST_F(TypedRegisterTest, LittleEndianDefaultFullRead)
+{
+    reg.read(buf.data() + RegSize);
+    EXPECT_EQ(reg.get(), 0x1122);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x22, 0x11, 0x5, 0x6));
+}
+
+// Do a full little endian write using the default write handler.
+TEST_F(TypedRegisterTest, LittleEndianDefaultFullWrite)
+{
+    reg.write(buf.data() + RegSize);
+    EXPECT_EQ(reg.get(), 0x0403);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x3, 0x4, 0x5, 0x6));
+}
+
+// Do a partial little endian read of the low half of the register.
+TEST_F(TypedRegisterTest, LittleEndianDefaultPartialReadLow)
+{
+    reg.read(buf.data() + RegSize, 0, 1);
+    EXPECT_EQ(reg.get(), 0x1122);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x22, 0x4, 0x5, 0x6));
+}
+
+// Do a partial little endian read of the high half of the register.
+TEST_F(TypedRegisterTest, LittleEndianDefaultPartialReadHigh)
+{
+    reg.read(buf.data() + RegSize, 1, 1);
+    EXPECT_EQ(reg.get(), 0x1122);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x11, 0x4, 0x5, 0x6));
+}
+
+// Do a partial little endian write of the low half of the register.
+TEST_F(TypedRegisterTest, LittleEndianDefaultPartialWriteLow)
+{
+    reg.write(buf.data() + RegSize, 0, 1);
+    EXPECT_EQ(reg.get(), 0x1103);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x3, 0x4, 0x5, 0x6));
+}
+
+// Do a partial little endian write of the High half of the register.
+TEST_F(TypedRegisterTest, LittleEndianDefaultPartialWriteHigh)
+{
+    reg.write(buf.data() + RegSize, 1, 1);
+    EXPECT_EQ(reg.get(), 0x0322);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x3, 0x4, 0x5, 0x6));
+}
+
+// Set a mask for use on writes.
+TEST_F(TypedRegisterTest, SetWriteable)
+{
+    reg.writeable(0xff00);
+    reg.write(buf.data() + RegSize);
+    EXPECT_EQ(reg.get(), 0x0422);
+
+    regBE.writeable(0xff00);
+    regBE.write(buf.data() + RegSize);
+    EXPECT_EQ(regBE.get(), 0x0322);
+}
+
+// Make a register read only.
+TEST_F(TypedRegisterTest, ReadOnly)
+{
+    reg.readonly();
+    reg.write(buf.data() + RegSize);
+    EXPECT_EQ(reg.get(), 0x1122);
+}
+
+// Update a register with an explicit mask.
+TEST_F(TypedRegisterTest, UpdateWithMask)
+{
+    reg.update(0xeeee, 0x0ff0);
+    EXPECT_EQ(reg.get(), 0x1ee2);
+}
+
+// Update a register using the register's built in mask.
+TEST_F(TypedRegisterTest, UpdateDefaultMask)
+{
+    reg.writeable(0xf00f);
+    reg.update(0xeeee);
+    EXPECT_EQ(reg.get(), 0xe12e);
+}
+
+// Set a custom read handler for a register.
+TEST_F(TypedRegisterTest, Reader)
+{
+    RegisterBankLE::Register<BackingType> *reg_ptr = nullptr;
+    BackingType ret = 0x3344;
+
+    reg.reader([&reg_ptr, &ret](auto &r){
+        reg_ptr = &r;
+        return ret;
+    });
+
+    reg.read(buf.data() + RegSize);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x44, 0x33, 0x5, 0x6));
+    EXPECT_EQ(reg_ptr, &reg);
+}
+
+// Set a custom read handler for a register which is a class method.
+TEST_F(TypedRegisterTest, ReaderMF)
+{
+    using Reg = RegisterBankLE::Register<BackingType>;
+
+    struct ReadStruct
+    {
+        Reg *reg_ptr = nullptr;
+        BackingType ret = 0x3344;
+
+        BackingType
+        reader(Reg &r)
+        {
+            reg_ptr = &r;
+            return ret;
+        }
+    } read_struct;
+
+    reg.reader(&read_struct, &ReadStruct::reader);
+
+    reg.read(buf.data() + RegSize);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x44, 0x33, 0x5, 0x6));
+    EXPECT_EQ(read_struct.reg_ptr, &reg);
+}
+
+// Set a custom write handler for a register.
+TEST_F(TypedRegisterTest, Writer)
+{
+    RegisterBankLE::Register<BackingType> *reg_ptr = nullptr;
+    BackingType value = 0;
+
+    reg.writer([&reg_ptr, &value](auto &r, const BackingType &v) {
+        reg_ptr = &r;
+        value = v;
+    });
+
+    reg.write(buf.data() + RegSize);
+    EXPECT_EQ(reg_ptr, &reg);
+    EXPECT_EQ(value, 0x0403);
+}
+
+// Set a custom write handler for a register which is a class method.
+TEST_F(TypedRegisterTest, WriterMF)
+{
+    using Reg = RegisterBankLE::Register<BackingType>;
+
+    struct WriteStruct
+    {
+        Reg *reg_ptr = nullptr;
+        BackingType value = 0;
+
+        void
+        writer(Reg &r, const BackingType &v)
+        {
+            reg_ptr = &r;
+            value = v;
+        }
+    } write_struct;
+
+    reg.writer(&write_struct, &WriteStruct::writer);
+
+    reg.write(buf.data() + RegSize);
+    EXPECT_EQ(write_struct.reg_ptr, &reg);
+    EXPECT_THAT(write_struct.value, 0x0403);
+}
+
+// Set a custom partial read handler for a register.
+TEST_F(TypedRegisterTest, PartialReader)
+{
+    RegisterBankLE::Register<BackingType> *reg_ptr = nullptr;
+    int first = 0;
+    int last = 0;
+    BackingType ret = 0x3344;
+
+    reg.partialReader([&reg_ptr, &first, &last, ret](auto &r, int f, int l) {
+        reg_ptr = &r;
+        first = f;
+        last = l;
+        return ret;
+    });
+
+    reg.read(buf.data() + RegSize, 1, 1);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x33, 0x4, 0x5, 0x6));
+    EXPECT_EQ(reg_ptr, &reg);
+    EXPECT_EQ(first, 15);
+    EXPECT_EQ(last, 8);
+}
+
+// Set a custom partial read handler for a register which is a class method.
+TEST_F(TypedRegisterTest, PartialReaderMF)
+{
+    using Reg = RegisterBankLE::Register<BackingType>;
+
+    struct ReadStruct
+    {
+        Reg *reg_ptr = nullptr;
+        int first = 0;
+        int last = 0;
+        BackingType ret = 0x3344;
+
+        BackingType
+        reader(Reg &r, int f, int l)
+        {
+            reg_ptr = &r;
+            first = f;
+            last = l;
+            return ret;
+        }
+    } read_struct;
+
+    reg.partialReader(&read_struct, &ReadStruct::reader);
+
+    reg.read(buf.data() + RegSize, 1, 1);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x33, 0x4, 0x5, 0x6));
+    EXPECT_EQ(read_struct.reg_ptr, &reg);
+    EXPECT_EQ(read_struct.first, 15);
+    EXPECT_EQ(read_struct.last, 8);
+}
+
+// Set a custom partial write handler for a register.
+TEST_F(TypedRegisterTest, PartialWriter)
+{
+    RegisterBankLE::Register<BackingType> *reg_ptr = nullptr;
+    BackingType value = 0;
+    int first = 0;
+    int last = 0;
+
+    reg.partialWriter([&reg_ptr, &value, &first, &last](
+                auto &r, const BackingType &v, int f, int l) {
+        reg_ptr = &r;
+        value = v;
+        first = f;
+        last = l;
+    });
+
+    reg.write(buf.data() + RegSize, 1, 1);
+    EXPECT_EQ(reg_ptr, &reg);
+    EXPECT_EQ(value, 0x300);
+    EXPECT_EQ(first, 15);
+    EXPECT_EQ(last, 8);
+}
+
+// Set a custom partial write handler for a register which is a class method.
+TEST_F(TypedRegisterTest, PartialWriterMF)
+{
+    using Reg = RegisterBankLE::Register<BackingType>;
+
+    struct WriteStruct
+    {
+        Reg *reg_ptr = nullptr;
+        BackingType value = 0;
+        int first = 0;
+        int last = 0;
+
+        void
+        writer(Reg &r, const BackingType &v, int f, int l)
+        {
+            reg_ptr = &r;
+            value = v;
+            first = f;
+            last = l;
+        }
+    } write_struct;
+
+    reg.partialWriter(&write_struct, &WriteStruct::writer);
+
+    reg.write(buf.data() + RegSize, 1, 1);
+    EXPECT_EQ(write_struct.reg_ptr, &reg);
+    EXPECT_EQ(write_struct.value, 0x300);
+    EXPECT_EQ(write_struct.first, 15);
+    EXPECT_EQ(write_struct.last, 8);
+}
+
+// Default partial reader with a custom read handler.
+TEST_F(TypedRegisterTest, PartialReaderReader)
+{
+    RegisterBankLE::Register<BackingType> *reg_ptr = nullptr;
+    BackingType ret = 0x3344;
+
+    reg.reader([&reg_ptr, &ret](auto &r){
+        reg_ptr = &r;
+        return ret;
+    });
+
+    reg.read(buf.data() + RegSize, 1, 1);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x33, 0x4, 0x5, 0x6));
+    EXPECT_EQ(reg_ptr, &reg);
+}
+
+// Default partial writer with custome read and write handlers.
+TEST_F(TypedRegisterTest, PartialWriterReaderWriter)
+{
+    RegisterBankLE::Register<BackingType> *read_reg_ptr = nullptr;
+    BackingType read_ret = 0x3344;
+
+    RegisterBankLE::Register<BackingType> *write_reg_ptr = nullptr;
+    BackingType write_value = 0;
+
+    reg.reader([&read_reg_ptr, read_ret](auto &r){
+        read_reg_ptr = &r;
+        return read_ret;
+    }).writer([&write_reg_ptr, &write_value](auto &r, const BackingType &v) {
+        write_reg_ptr = &r;
+        write_value = v;
+    });
+
+    reg.write(buf.data() + RegSize, 1, 1);
+    EXPECT_THAT(buf, ElementsAre(0x1, 0x2, 0x3, 0x4, 0x5, 0x6));
+    EXPECT_EQ(read_reg_ptr, &reg);
+    EXPECT_EQ(write_reg_ptr, &reg);
+    EXPECT_EQ(write_value, 0x0344);
+}
+
+TEST_F(TypedRegisterTest, Serialize)
+{
+    std::ostringstream os;
+    reg.serialize(os);
+    EXPECT_EQ(os.str(), "4386");
+}
+
+TEST_F(TypedRegisterTest, UnserializeSucess)
+{
+    std::string s = "1234";
+    EXPECT_TRUE(reg.unserialize(s));
+    EXPECT_EQ(reg.get(), 1234);
+}
+
+TEST_F(TypedRegisterTest, UnserializeFailure)
+{
+    std::string s = "not_a_number";
+    EXPECT_FALSE(reg.unserialize(s));
+}
+
+/*
+ * The RegisterBank itself.
+ */
+
+class RegisterBankTest : public testing::Test
+{
+  protected:
+    class TestRegBank : public RegisterBankLE
+    {
+      public:
+        TestRegBank(const std::string &new_name, Addr new_base) :
+            RegisterBankLE(new_name, new_base)
+        {}
+    };
+
+    enum AccessType {
+        Read,
+        Write,
+        PartialRead,
+        PartialWrite
+    };
+
+    struct Access
+    {
+        AccessType type;
+        uint32_t value = 0;
+        int first = 0;
+        int last = 0;
+        uint32_t ret = 0;
+
+        Access(AccessType _type) : type(_type) {}
+        Access(AccessType _type, uint32_t _value,
+                int _first, int _last, uint32_t _ret) :
+            type(_type), value(_value),
+            first(_first), last(_last), ret(_ret)
+        {}
+
+        bool
+        operator == (const Access &other) const
+        {
+            return type == other.type && value == other.value &&
+                first == other.first && last == other.last &&
+                ret == other.ret;
+        }
+    };
+
+    // A 32 bit register which keeps track of what happens to it.
+    class TestReg : public TestRegBank::Register32
+    {
+      public:
+        std::vector<Access> accesses;
+
+        TestReg(const std::string &new_name, uint32_t initial) :
+            TestRegBank::Register32(new_name, initial)
+        {
+            reader([this](auto &r) {
+                Access access(Read);
+                access.ret = defaultReader(r);
+                accesses.push_back(access);
+                return access.ret;
+            });
+            writer([this](auto &r, const uint32_t &v) {
+                Access access(Write);
+                access.value = v;
+                defaultWriter(r, v);
+                accesses.push_back(access);
+            });
+            partialReader([this](auto &r, int f, int l) {
+                Access access(PartialRead);
+                access.first = f;
+                access.last = l;
+                access.ret = defaultPartialReader(r, f, l);
+                accesses.push_back(access);
+                return access.ret;
+            });
+            partialWriter([this](auto &r, const uint32_t &v, int f, int l) {
+                Access access(PartialWrite);
+                access.value = v;
+                access.first = f;
+                access.last = l;
+                defaultPartialWriter(r, v, f, l);
+                accesses.push_back(access);
+            });
+        }
+    };
+
+    TestReg reg0, reg1, reg2;
+    TestRegBank emptyBank, fullBank;
+
+    std::array<uint8_t, 12> buf;
+
+    RegisterBankTest() :
+        reg0("reg0", 0xd3d2d1d0), reg1("reg1", 0xe3e2e1e0),
+        reg2("reg2", 0xf3f2f1f0),
+        emptyBank("empty", 0x12345), fullBank("full", 0x1000),
+        buf{0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
+            0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc}
+    {
+        fullBank.addRegisters({reg0, reg1, reg2});
+    }
+};
+
+// Some basic accessors.
+
+TEST_F(RegisterBankTest, Name)
+{
+    EXPECT_EQ(emptyBank.name(), "empty");
+    EXPECT_EQ(fullBank.name(), "full");
+}
+
+TEST_F(RegisterBankTest, Base)
+{
+    EXPECT_EQ(emptyBank.base(), 0x12345);
+    EXPECT_EQ(fullBank.base(), 0x1000);
+}
+
+// Adding registers, and the size accessor. With registers, size is boring.
+TEST_F(RegisterBankTest, AddRegistersSize)
+{
+    EXPECT_EQ(emptyBank.size(), 0);
+    emptyBank.addRegister(reg0);
+    EXPECT_EQ(emptyBank.size(), 4);
+    emptyBank.addRegisters({reg1, reg2});
+    EXPECT_EQ(emptyBank.size(), 12);
+}
+
+// Reads.
+
+TEST_F(RegisterBankTest, ReadOneAlignedFirst)
+{
+    fullBank.read(0x1000, buf.data() + 4, 4);
+    EXPECT_THAT(buf, ElementsAreArray({0x11, 0x22, 0x33, 0x44,
+                                       0xd0, 0xd1, 0xd2, 0xd3,
+                                       0x99, 0xaa, 0xbb, 0xcc}));
+    EXPECT_THAT(reg0.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xd3d2d1d0)
+                ));
+    EXPECT_TRUE(reg1.accesses.empty());
+    EXPECT_TRUE(reg2.accesses.empty());
+}
+
+TEST_F(RegisterBankTest, ReadOneAlignedMid)
+{
+    fullBank.read(0x1004, buf.data() + 4, 4);
+    EXPECT_THAT(buf, ElementsAreArray({0x11, 0x22, 0x33, 0x44,
+                                       0xe0, 0xe1, 0xe2, 0xe3,
+                                       0x99, 0xaa, 0xbb, 0xcc}));
+    EXPECT_TRUE(reg0.accesses.empty());
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xe3e2e1e0)
+                ));
+    EXPECT_TRUE(reg2.accesses.empty());
+}
+
+TEST_F(RegisterBankTest, ReadOneAlignedLast)
+{
+    fullBank.read(0x1008, buf.data() + 4, 4);
+    EXPECT_THAT(buf, ElementsAreArray({0x11, 0x22, 0x33, 0x44,
+                                       0xf0, 0xf1, 0xf2, 0xf3,
+                                       0x99, 0xaa, 0xbb, 0xcc}));
+    EXPECT_TRUE(reg0.accesses.empty());
+    EXPECT_TRUE(reg1.accesses.empty());
+    EXPECT_THAT(reg2.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xf3f2f1f0)
+                ));
+}
+
+TEST_F(RegisterBankTest, ReadTwoAligned)
+{
+    fullBank.read(0x1004, buf.data() + 2, 8);
+    EXPECT_THAT(buf, ElementsAreArray({0x11, 0x22, 0xe0, 0xe1,
+                                       0xe2, 0xe3, 0xf0, 0xf1,
+                                       0xf2, 0xf3, 0xbb, 0xcc}));
+    EXPECT_TRUE(reg0.accesses.empty());
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xe3e2e1e0)
+                ));
+    EXPECT_THAT(reg2.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xf3f2f1f0)
+                ));
+}
+
+TEST_F(RegisterBankTest, ReadContained)
+{
+    fullBank.read(0x1001, buf.data() + 4, 2);
+    EXPECT_THAT(buf, ElementsAreArray({0x11, 0x22, 0x33, 0x44,
+                                       0xd1, 0xd2, 0x77, 0x88,
+                                       0x99, 0xaa, 0xbb, 0xcc}));
+    EXPECT_THAT(reg0.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xd3d2d1d0),
+                Access(PartialRead, 0, 23, 8, 0x00d2d100)
+                ));
+    EXPECT_TRUE(reg1.accesses.empty());
+    EXPECT_TRUE(reg2.accesses.empty());
+}
+
+TEST_F(RegisterBankTest, ReadOneSpanning)
+{
+    fullBank.read(0x1002, buf.data() + 4, 4);
+    EXPECT_THAT(buf, ElementsAreArray({0x11, 0x22, 0x33, 0x44,
+                                       0xd2, 0xd3, 0xe0, 0xe1,
+                                       0x99, 0xaa, 0xbb, 0xcc}));
+    EXPECT_THAT(reg0.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xd3d2d1d0),
+                Access(PartialRead, 0, 31, 16, 0xd3d20000)
+                ));
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xe3e2e1e0),
+                Access(PartialRead, 0, 15, 0, 0x0000e1e0)
+                ));
+    EXPECT_TRUE(reg2.accesses.empty());
+}
+
+TEST_F(RegisterBankTest, ReadTwoSpanning)
+{
+    fullBank.read(0x1002, buf.data() + 2, 8);
+    EXPECT_THAT(buf, ElementsAreArray({0x11, 0x22, 0xd2, 0xd3,
+                                       0xe0, 0xe1, 0xe2, 0xe3,
+                                       0xf0, 0xf1, 0xbb, 0xcc}));
+    EXPECT_THAT(reg0.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xd3d2d1d0),
+                Access(PartialRead, 0, 31, 16, 0xd3d20000)
+                ));
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xe3e2e1e0)
+                ));
+    EXPECT_THAT(reg2.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xf3f2f1f0),
+                Access(PartialRead, 0, 15, 0, 0x0000f1f0)
+                ));
+}
+
+TEST_F(RegisterBankTest, ReadPartialFull)
+{
+    fullBank.read(0x1002, buf.data() + 4, 6);
+    EXPECT_THAT(buf, ElementsAreArray({0x11, 0x22, 0x33, 0x44,
+                                       0xd2, 0xd3, 0xe0, 0xe1,
+                                       0xe2, 0xe3, 0xbb, 0xcc}));
+    EXPECT_THAT(reg0.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xd3d2d1d0),
+                Access(PartialRead, 0, 31, 16, 0xd3d20000)
+                ));
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xe3e2e1e0)
+                ));
+    EXPECT_TRUE(reg2.accesses.empty());
+}
+
+TEST_F(RegisterBankTest, ReadFullPartial)
+{
+    fullBank.read(0x1004, buf.data() + 4, 6);
+    EXPECT_THAT(buf, ElementsAreArray({0x11, 0x22, 0x33, 0x44,
+                                       0xe0, 0xe1, 0xe2, 0xe3,
+                                       0xf0, 0xf1, 0xbb, 0xcc}));
+    EXPECT_TRUE(reg0.accesses.empty());
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xe3e2e1e0)
+                ));
+    EXPECT_THAT(reg2.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xf3f2f1f0),
+                Access(PartialRead, 0, 15, 0, 0x0000f1f0)
+                ));
+}
+
+TEST_F(RegisterBankTest, ReadLastPartial)
+{
+    fullBank.read(0x100a, buf.data() + 4, 2);
+    EXPECT_THAT(buf, ElementsAreArray({0x11, 0x22, 0x33, 0x44,
+                                       0xf2, 0xf3, 0x77, 0x88,
+                                       0x99, 0xaa, 0xbb, 0xcc}));
+    EXPECT_TRUE(reg0.accesses.empty());
+    EXPECT_TRUE(reg1.accesses.empty());
+    EXPECT_THAT(reg2.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xf3f2f1f0),
+                Access(PartialRead, 0, 31, 16, 0xf3f20000)
+                ));
+}
+
+// Write.
+
+TEST_F(RegisterBankTest, WriteOneAlignedFirst)
+{
+    fullBank.write(0x1000, buf.data() + 4, 4);
+    EXPECT_EQ(reg0.get(), 0x88776655);
+    EXPECT_EQ(reg1.get(), 0xe3e2e1e0);
+    EXPECT_EQ(reg2.get(), 0xf3f2f1f0);
+    EXPECT_THAT(reg0.accesses, ElementsAre(
+                Access(Write, 0x88776655, 0, 0, 0)
+                ));
+    EXPECT_TRUE(reg1.accesses.empty());
+    EXPECT_TRUE(reg2.accesses.empty());
+}
+
+TEST_F(RegisterBankTest, WriteOneAlignedMid)
+{
+    fullBank.write(0x1004, buf.data() + 4, 4);
+    EXPECT_EQ(reg0.get(), 0xd3d2d1d0);
+    EXPECT_EQ(reg1.get(), 0x88776655);
+    EXPECT_EQ(reg2.get(), 0xf3f2f1f0);
+    EXPECT_TRUE(reg0.accesses.empty());
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Write, 0x88776655, 0, 0, 0)
+                ));
+    EXPECT_TRUE(reg2.accesses.empty());
+}
+
+TEST_F(RegisterBankTest, WriteOneAlignedLast)
+{
+    fullBank.write(0x1008, buf.data() + 4, 4);
+    EXPECT_EQ(reg0.get(), 0xd3d2d1d0);
+    EXPECT_EQ(reg1.get(), 0xe3e2e1e0);
+    EXPECT_EQ(reg2.get(), 0x88776655);
+    EXPECT_TRUE(reg0.accesses.empty());
+    EXPECT_TRUE(reg1.accesses.empty());
+    EXPECT_THAT(reg2.accesses, ElementsAre(
+                Access(Write, 0x88776655, 0, 0, 0)
+                ));
+}
+
+TEST_F(RegisterBankTest, WriteTwoAligned)
+{
+    fullBank.write(0x1004, buf.data() + 2, 8);
+    EXPECT_EQ(reg0.get(), 0xd3d2d1d0);
+    EXPECT_EQ(reg1.get(), 0x66554433);
+    EXPECT_EQ(reg2.get(), 0xaa998877);
+    EXPECT_TRUE(reg0.accesses.empty());
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Write, 0x66554433, 0, 0, 0)
+                ));
+    EXPECT_THAT(reg2.accesses, ElementsAre(
+                Access(Write, 0xaa998877, 0, 0, 0)
+                ));
+}
+
+TEST_F(RegisterBankTest, WriteContained)
+{
+    fullBank.write(0x1001, buf.data() + 4, 2);
+    EXPECT_EQ(reg0.get(), 0xd36655d0);
+    EXPECT_EQ(reg1.get(), 0xe3e2e1e0);
+    EXPECT_EQ(reg2.get(), 0xf3f2f1f0);
+    EXPECT_THAT(reg0.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xd3d2d1d0),
+                Access(Write, 0xd36655d0, 0, 0, 0),
+                Access(PartialWrite, 0x00665500, 23, 8, 0)
+                ));
+    EXPECT_TRUE(reg1.accesses.empty());
+    EXPECT_TRUE(reg2.accesses.empty());
+}
+
+TEST_F(RegisterBankTest, WriteOneSpanning)
+{
+    fullBank.write(0x1002, buf.data() + 4, 4);
+    EXPECT_EQ(reg0.get(), 0x6655d1d0);
+    EXPECT_EQ(reg1.get(), 0xe3e28877);
+    EXPECT_EQ(reg2.get(), 0xf3f2f1f0);
+    EXPECT_THAT(reg0.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xd3d2d1d0),
+                Access(Write, 0x6655d1d0, 0, 0, 0),
+                Access(PartialWrite, 0x66550000, 31, 16, 0)
+                ));
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xe3e2e1e0),
+                Access(Write, 0xe3e28877, 0, 0, 0),
+                Access(PartialWrite, 0x00008877, 15, 0, 0)
+                ));
+    EXPECT_TRUE(reg2.accesses.empty());
+}
+
+TEST_F(RegisterBankTest, WriteTwoSpanning)
+{
+    fullBank.write(0x1002, buf.data() + 2, 8);
+    EXPECT_EQ(reg0.get(), 0x4433d1d0);
+    EXPECT_EQ(reg1.get(), 0x88776655);
+    EXPECT_EQ(reg2.get(), 0xf3f2aa99);
+    EXPECT_THAT(reg0.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xd3d2d1d0),
+                Access(Write, 0x4433d1d0, 0, 0, 0),
+                Access(PartialWrite, 0x44330000, 31, 16, 0)
+                ));
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Write, 0x88776655, 0, 0, 0)
+                ));
+    EXPECT_THAT(reg2.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xf3f2f1f0),
+                Access(Write, 0xf3f2aa99, 0, 0, 0),
+                Access(PartialWrite, 0x0000aa99, 15, 0, 0)
+                ));
+}
+
+TEST_F(RegisterBankTest, WritePartialFull)
+{
+    fullBank.write(0x1002, buf.data() + 4, 6);
+    EXPECT_EQ(reg0.get(), 0x6655d1d0);
+    EXPECT_EQ(reg1.get(), 0xaa998877);
+    EXPECT_EQ(reg2.get(), 0xf3f2f1f0);
+    EXPECT_THAT(reg0.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xd3d2d1d0),
+                Access(Write, 0x6655d1d0, 0, 0, 0),
+                Access(PartialWrite, 0x66550000, 31, 16, 0)
+                ));
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Write, 0xaa998877, 0, 0, 0)
+                ));
+    EXPECT_TRUE(reg2.accesses.empty());
+}
+
+TEST_F(RegisterBankTest, WriteFullPartial)
+{
+    fullBank.write(0x1004, buf.data() + 4, 6);
+    EXPECT_EQ(reg0.get(), 0xd3d2d1d0);
+    EXPECT_EQ(reg1.get(), 0x88776655);
+    EXPECT_EQ(reg2.get(), 0xf3f2aa99);
+    EXPECT_TRUE(reg0.accesses.empty());
+    EXPECT_THAT(reg1.accesses, ElementsAre(
+                Access(Write, 0x88776655, 0, 0, 0)
+                ));
+    EXPECT_THAT(reg2.accesses, ElementsAre(
+                Access(Read, 0, 0, 0, 0xf3f2f1f0),
+                Access(Write, 0xf3f2aa99, 0, 0, 0),
+                Access(PartialWrite, 0x0000aa99, 15, 0, 0)
+                ));
+}