2017/03/28

C vs C++, Part II, Beautiful & efficient

In the first part of this series, we used C++ features (operator overloading and templates) to eliminate all defines and macros necessary for using a Pin. This way, we achieved the same performance of C code, but slightly increased readability, and hugely increased code safety. The result is a library that looks like this:

template<uint16_t address_>
struct Register {
 void operator=   (uint8_t _r)
 {
  *reinterpret_cast<volatile uint8_t*>(address_) = _r;
 }
 operator uint8_t   () const
 {
  return *reinterpret_cast<volatile uint8_t*>(address_);
 }
 operator volatile uint8_t& ()
 {
  return *reinterpret_cast<volatile uint8_t*>(address_);
 }

 template<uint8_t bit_>
 void setBit() { *reinterpret_cast<volatile uint8_t*>(address_) |= (1 << bit_); }
 template<uint8_t bit_>
 void clearBit() { *reinterpret_cast<volatile uint8_t*>(address_) &= ~(1 << bit_); }
};

Register<0x24> DDRB;
Register<0x25> PORTB;

constexpr uint8_t DDB5 = 5;
constexpr uint8_t PORTB5 = 5;


And sample code that looks identical to Atmel's, and compiles to just two instructions.

int main(void)
{
 DDRB.setBit<DDB5>();
 PORTB.setBit<PORTB5>();
 
 while (1);
}

However, our goal is to achieve code as easy to use as Arduino's, and we're still far from that. First thing we need to do is to remove a small source of errors. Both DDRB and PORTB give very little useful information to the high level user. Sure, they tell you exactly what registers are involved, but all we care about is lighting an LED, and that isn't clear at all. The final user shouldn't need to know what registers or bit management are involved in the process. To fix this, we start by grouping related registers in a common struct. In fact some vendors follow this pattern in their own C code, but in the case of AVR, it's entirely up to us.

template<typename PORT_>
struct GPIOPort
{
public:
 typedef PORT_      PORT;
 typedef Register<PORT_::address - 1> DDR;
 typedef Register<PORT_::address - 2> PIN;

public:
 DDR  ddr;
 PIN  pin;
 PORT port;
};

GPIOPort<decltype(PORTB)> GPIOPortB;

This structure provides access to every register related to one specific port, as long as we specify the right Port register. On this base, we can now construct a templated Pin struct that will work on any port, with just one parameter. This Pin struct will provide a very explicit naming for better readability.

template<typename Port_, uint8_t bit_>
class Pin {
public:
 void setOutput() { mDdr |= bitMask; }
 void setHigh() { mPort |= bitMask; }

private:
 typedef typename GPIOPort<Port_>::PORT PORT;
 typedef typename GPIOPort<Port_>::DDR DDR;

 static constexpr uint8_t bit = bit_;
 static constexpr uint8_t bitMask = 1 << bit;

 PORT mPort;
 DDR mDdr;
};
 
Pin<PORTB, 5> BuiltInLed;


That completes our pin access library and, of a sudden, our user code looks like this:

int main(void)
{
 BuiltInLed.setOutput();
 BuiltInLed.setHigh();
 
 while (1);
}

That is even nicer than Arduino's code. And more importantly, it still compiles to just two assembler instructions. So in review, the new code is just as fast as the fastest C version that atmel itself provides, even nicer than Arduino code, and several times safer than both. What we did in this simple example, wich about 30 lines of code, illustrates the huge potential of C++ in restricted environments, where a lot of information is available at compile time, and thus can be used with templates and constexprs to reduce the final code size to a minimun. At the same time, the use of C++'s strong typing prevents many errors that were possible with the #define based versions, and warns our users quickly during compile time, saving lots of time debugging in a simulator or mcu.

No comments:

Post a Comment