3
\$\begingroup\$

First of all, my knowledge in C++ are very limited but I have a good background in C.

I'm currently writing firmware for an STM32 MCU using C++. I have to code a driver to communicate with an IC using the SPI interface. The IC has many registers. The registers are accessed like a memory, each of them are addressable. Unlike a memory where all cells have the same size, some registers are larger than others. Most ICs work in the same way. For this reason, I'm trying the make the driver abstract so it can be reused, with little changes, with other ICs or even communication interfaces. I'm also seeking for simplicity of use. I would like writing to a register to be as simple as writing to a variable and reading from a register to be as simple as reading from a variable. The code I came up with work as desired but I'm not too sure about the implementation. Some of my concerns are written as comments in the code (e.g. Info member not being private). My main concern is that each register creates a new template implementation. The code size will grow with the number of register an IC has. Since the MCU is very limited in memory, code size may end up being a problem. I'm sure there is a way around creating a template implementation for each register since all registers contain an Info field. The reading and writing functions do not care about the structure of a register, each register can be seen as a byte array. As such, the reading and writing functions only require the Info field to know the address and size.

#include <stdint.h>
#include <stdio.h>

struct Reg_t
{
    uint8_t Address;
    uint8_t Size;
};

struct RegA_t
{
    uint8_t Field1 : 1;
    uint8_t Field2 : 3;
    uint8_t Field3 : 4;
    // Would like Info to be private but cannot
    // Only Driver class need access
    //private:
    // Cannot use inheritance because of static const
    // Different registers must not share the same Address and Size in memory
    static const Reg_t Info;
};

struct RegB_t
{
    uint32_t Field1 : 6;
    uint32_t Field2 : 12;
    uint32_t Field3 : 14;
    // Would like Info to be private but cannot
    // Only Driver class need access
    //private:
    // Cannot use inheritance because of static const
    // Different registers must not share the same Address and Size in memory
    static const Reg_t Info;
};

const Reg_t RegA_t::Info = {0x00, sizeof(RegA_t)};
const Reg_t RegB_t::Info = {0x01, sizeof(RegB_t)};

class Device
{
    template <typename T>
    class Driver
    {
        public:
        T& operator=(T& buf)
        {
            // Implement code to write to the device
            printf("Writing to the device...\n");
            printf("Register address: %u, size: %u\n", buf.Info.Address, buf.Info.Size);
            
            return buf;
        }
        operator T() const
        {
            T buf = {0};
            
            // Implement code to read from the device
            printf("Reading from the device...\n");
            printf("Register address: %u, size: %u\n", buf.Info.Address, buf.Info.Size);
            
            return buf;
        }
    };
    public:
    Driver<RegA_t> RegA;
    Driver<RegB_t> RegB;
};

int main()
{
    Device myDevice;
    RegA_t RegA;
    RegB_t RegB;
    
    // Reading the register
    RegA = myDevice.RegA;
    // Editing the content
    RegA.Field1 = 1;
    RegA.Field2 = 2;
    RegA.Field3 = 3;
    // Updating the content
    myDevice.RegA = RegA;
    
    printf("\n");
    
    // Reading the register
    RegB = myDevice.RegB;
    // Editing the content
    RegB.Field1 = 1;
    RegB.Field2 = 2;
    RegB.Field3 = 3;
    // Updating the content
    myDevice.RegB = RegB;
    
    return 0;
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

The idea is good, but it can be improved. First, decouple the register types from their address. Consider for example that an MCU might have multiple instances of an UART, and you want to declare a struct UART only once, but then use it at different addresses. It would be nice to be able to write:

class Device {
    template<typename T, uint8_t Address>
    struct Driver {
        T& operator=(const T& buf) {
            std::cout << "Writing to the device...\n"
                      << "Register address: " << Address
                      << ", size: " << sizeof(T) << '\n';
            return *this;
        }
        ...
    };

public:
    Driver<UART, 0x00> UART1;
    Driver<UART, 0x10> UART2;
};

Note however that by making the address part of the type (whether that is via an Info variable or as a template parameter for Driver) means that each UART in the above example will get its own, distinct template instantiation of Driver. This might lead to a larger amount of machine code than necessary. Consider making the address just a member variable of Driver:

class Device {
    template<typename T>
    struct Driver {
        Driver(uint8_t address): address(address) {}

        T& operator=(const T& buf) {
            std::cout << "Writing to the device...\n"
                      << "Register address: " << address
                      << ", size: " << sizeof(T) << '\n';
            return *this;
        }
        ...
    private:
        uint8_t address;
    };

public:
    Driver<UART> UART1(0x00);
    Driver<UART> UART2(0x10);
};

Even better, the latter also allows you to declare multiple registers of the same type using array notation:

Driver<UART> UARTs[2] = {0x00, 0x10};

Some other improvements:

  • Driver can use sizeof(T) to get the size of the register, so no need to explicitly put that into an Info variable or to pass that as a (template) parameter.
  • operator=() should take a const reference as input, and return *this.
  • Use the C++ form of #includes: <cstdint> and <cstdio>
  • Use C++'s std::cout instead of C's printf() if possible, it is much safer. If you can, use C++17's std::format to get the best of both worlds.
\$\endgroup\$
6
  • \$\begingroup\$ I must have been not very clear in my question. The registers I'm referring to are located in a separate IC, lets say a sensor. The MCU read from and write to the sensor registers using a communication interface such as SPI, I2C, ... Those interfaces have already been implemented as objects. I'm trying to abstract the sensor registers access not the communication interfaces. I want reading a register to be as simple as reading a variable and writing to a register to be as simple as writing to a variable. \$\endgroup\$
    – A.Lacasse
    Feb 17, 2022 at 22:16
  • \$\begingroup\$ It doesn't matter if those registers are directly on the MCU or on some device connected via SPI, I2C, and so on. Consider an SPI to UART bridge that has multiple ports. \$\endgroup\$
    – G. Sliepen
    Feb 17, 2022 at 22:34
  • \$\begingroup\$ I'm not sure to see the improuvement when compared to the initial code. RegA and RegB aren't the same, one is 1 byte and the other is 4 bytes. I cannot represent both by the same struct as you did (struct UART). Lets say the IC is an AS3935, all the registers have differents fields. \$\endgroup\$
    – A.Lacasse
    Feb 17, 2022 at 23:00
  • \$\begingroup\$ Then you write Driver<RegA_t> RegA(0x00); Driver<RegB_t> RegB(0x01); \$\endgroup\$
    – G. Sliepen
    Feb 18, 2022 at 6:57
  • \$\begingroup\$ As I understand, your solution is to extract the address and size of a register from its struct but doesn't reduce code size. A template instance will still be created for each registers. By storing the address inside the Driver class, it uses more memory if multiple Device objets are created. If there are 10 Device objets, the address variable of each registers will be duplicated 10 times. This is why I declared the address as static inside the registers struct. \$\endgroup\$
    – A.Lacasse
    Feb 18, 2022 at 15:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.