A simple C++17 class library for low level programming. Interfacing to hardware devices via MMIO registers. Interface code generated from CMSIS-SVD via Jinja templates.

I recently read Real Time C++, by Christopher Kormanyos. It gives a good overview of C++ for small embedded devices and I’ve been meaning to try out some of the ideas for a while.

It presents a template based method for low level register. To summarise it looks a bit like this:

    // Set port b to 1
    reg_access<std::uint8_t,
               std::uint8_t,
               mcal::reg::portb,
               UINT8_C(0x01)>::reg_set();

For me, that is verbose and possibly error prone. Using the SVD Jinja Generator a lot of verbosity can be avoided.

I would prefer something like this:

    // Set a LED0 port to 1
    FPGAIO_dev<base_addr::CMSDK_CM3::FPGAIO> FPGAIO_i;
    FPGAIO_i.LED.LED0.write(1);

This solution has two parts:


Register Access Base Classes

The methods below show the implementation of just the write.

  1. The register base class has a simple template methods for register access.
template<uintptr_t BASE_ADDR, class R> class reg {
    public :
    using datatype_t = typename R::datatype;
    void write(datatype_t value) {
        *reinterpret_cast<volatile datatype_t*>(BASE_ADDR + R::offset) = value;
    }
...
};
  1. The register bit field base class also has matching methods. This is more complex - as there are optimizations to avoid unnecessary shifts and read-write modify operations.
template<uintptr_t BASE_ADDR, class R, class F> class reg_field {
    public :
    using r_datatype_t = typename R::datatype;
    using f_datatype_t = typename F::datatype;
    void write(f_datatype_t value) {
        if constexpr ((R::bit_width == F::bit_width) && (F::bit_offset == 0)) {
           // Write to single bit.
           *reinterpret_cast<volatile r_datatype_t*>(BASE_ADDR + R::offset) = 
                                     (r_datatype_t) value ;
        } else if constexpr (R::field_count == 1) {
           // Write to single field.
           *reinterpret_cast<volatile r_datatype_t*>(BASE_ADDR + R::offset) = 
                                     ((r_datatype_t)value << F::bit_offset) & F::bit_mask;
        } else {
            // Read write modify
            r_datatype_t reg_value = *reinterpret_cast<volatile r_datatype_t*>(BASE_ADDR + R::offset);
            reg_value = (((r_datatype_t)value << F::bit_offset) & F::bit_mask) | (reg_value & ~F::bit_mask);
            *reinterpret_cast<volatile r_datatype_t*>(BASE_ADDR + R::offset) = reg_value;
        }
    }
...
};

Generated Register Definitions

The example output is generated from this SVD.

  1. A generated ‘param’ file defines the registers and fields. It comes from this template.
namespace mmio_param {
    /* FPGA System Control I/O */
    namespace FPGAIO {
       /* LED Connections */
       struct LED_r {
           using datatype = std::uint32_t;
           static constexpr unsigned int offset = 0x0;
           static constexpr unsigned int bit_width = 32;
           static constexpr unsigned int field_count = 2;
       }; /* LED_r */
       namespace LED {
          /* None */
          struct LED0_f {
              using datatype = bool;
              static constexpr unsigned int bit_offset = 0;
              static constexpr unsigned int bit_width = 1;
              static constexpr unsigned int bit_mask = 0x1;
          };
....
       }
   }
}
  1. A generated ‘regs’ file defines each register and field as a class, which inherits from the base class. (template)
namespace mmio_regs {
    /* FPGA System Control I/O */
    namespace FPGAIO {
        /* LED Connections */
        template<const std::uintptr_t BASE_ADDR> class LED 
            : public mmio_device::reg<BASE_ADDR, 
                                mmio_param::FPGAIO::LED_r> {
            public:
            /* None */
            mmio_device::reg_field<BASE_ADDR, 
                             mmio_param::FPGAIO::LED_r,
                             mmio_param::FPGAIO::LED::LED0_f> LED0;
....
        };
    }
}
  1. A generated ‘dev’ file defines the MMIO device. (template)
/*   FPGA System Control I/O */
template<std::uintptr_t BASE_ADDR> class FPGAIO_dev  {
public:
    /* LED Connections */
   mmio_regs::FPGAIO::LED<BASE_ADDR> LED;

...
};
  1. Finally the generated ‘device’ file includes the base addresses. (template)
namespace base_addr {

    namespace CMSDK_CM3 {
...
           static constexpr uintptr_t                    FPGAIO = 0x40028000;
...
    }
}

All that code compiles down to no more than is needed.

    8002:	2000      	movs	r0, #0
    8004:	6813      	ldr	r3, [r2, #0]
            reg_value = (((r_datatype_t)value << F::bit_offset) & F::bit_mask) | (reg_value & ~F::bit_mask);
    8006:	f043 0301 	orr.w	r3, r3, #1
            *reinterpret_cast<volatile r_datatype_t*>(BASE_ADDR + R::offset) = reg_value;
    800a:	6013      	str	r3, [r2, #0]

    8010:	40028000 	.word	0x40028000

With this setup register and field access can be reduced to C++ class member access.

However, even with plain C it’s possible to use just a struct, such as ARM do with CMSIS headers. However, they still use macros to access fields. (Although bitfield structs could also be used).

But using a full C++ class abstraction it’s possible to do more optimizations, protect some register accesses, and even use custom instructions to access registers.

In this case I’ve placed the base address in the template parameters, just to differentiate from a traditional C struct, but it could have been a run time member.

  • Advantage: With optimization code can be reduced to simply load stores, no base pointer needs to be stored or passed as a parameter.

  • Disadvantage: Every higher level class that uses these peripheral classes will need to be a template class.

This method still has some complexity - but it’s encapsulated in classes not directly used as much as possible. The client code is as simple as possible.