153. Mandatory Build-Time Timebase Selection

You are implementing firmware for a system that depends on a hardware timebase.

Exactly one timebase must be selected at build time.
Runtime selection is architecturally forbidden.

Your task is to complete the program so that:

  • The timebase is selected only at compile time
  • The program cannot build unless a valid timebase type is chosen
  • No runtime configuration, branching, or fallback is possible
  • This must be enforced using the C++ type system, not comments or conventions

 

Input / Program Flow:

The firmware performs the following steps:

  1. Reads a tick counter from a hardware timebase
  2. Converts ticks to milliseconds
  3. Prints the elapsed time

Two valid timebases exist:

  • SysTickTimer
    • Frequency: 1 kHz
    • Tick count: 100
  • RtcTimer
    • Frequency: 32 Hz
    • Tick count: 4

Only one timebase is ever used in a firmware build.

 

Output:

The program must print exactly:

elapsed_ms=100 

Note:
This output corresponds to selecting SysTickTimer.
Validation of RtcTimer is performed by recompiling the program with a different compile-time selection.

 

Constraints:

  • No virtual functions
  • No inheritance used for polymorphism
  • No runtime conditionals (if, switch)
  • No heap allocation
  • No STL containers
  • No macros for selection
  • Timebase selection must be enforced at compile time
  • Program must fail to build if selection is missing

 

 

 

Need Help? Refer to the Quick Guide below

Polymorphism ("Many Forms") allows you to treat different objects (like TempSensor, PressureSensor) as a single generic type (Sensor). It decouples the Interface (what a function does) from the Implementation (how it does it).

In Embedded C++, it comes in two flavors:

  1. Runtime (Dynamic): Uses virtual functions. Flexible, but has slight overhead.
  2. Compile-Time (Static): Uses Overloading and Templates. Zero overhead, but less flexible.

Syntax & Usage (Runtime Polymorphism)

1. Virtual Functions

The virtual keyword tells the compiler: "Don't bind this function call yet. Wait until the program runs to see what type of object this actually is."

class Protocol {
public:
    // Virtual: Can be overridden by children
    virtual void send(uint8_t data) { 
        printf("Generic Send\n"); 
    }
};

class UART : public Protocol {
public:
    // Override: Replace generic logic with UART logic
    void send(uint8_t data) override { 
        UART_HW->DR = data; 
    }
};

class SPI : public Protocol {
public:
    void send(uint8_t data) override { 
        SPI_HW->DR = data; 
    }
};

// The Magic: This function works for UART, SPI, I2C, etc.
void transmit_data(Protocol* p, uint8_t byte) {
    p->send(byte); // Calls the specific version automatically
}

2. Abstract Classes (Interfaces)

In HAL design, the Base class often has no logic—it just defines the API. We use Pure Virtual Functions (= 0) to enforce this.

A class with pure virtual functions cannot be instantiated.

// Interface (Contract)
class IDigitalOut {
public:
    virtual void write(bool state) = 0; // Pure Virtual
    virtual ~IDigitalOut() {}           // Virtual Destructor (Required)
};

// Implementation
class GpioPin : public IDigitalOut {
public:
    void write(bool state) override {
        // Hardware specific code
    }
};

How It Works: The V-Table

Dynamic polymorphism isn't magic; it uses a lookup table.

  1. V-Table (Flash): The compiler creates a static table of function pointers for every class.
  2. vptr (RAM): Every object gets a hidden pointer (vptr) pointing to its class's V-Table.
StructureCost (Overhead)
RAM4 bytes per object (for the vptr).
FlashOne table per Class (not per object).
CPU2-3 extra cycles (Fetch pointer -> Jump).

Syntax & Usage (Static Polymorphism)

1. Function Overloading

Same function name, different arguments. Resolved at compile time.

void log(int i);
void log(char* s);
// Compiler picks the right one. Zero overhead.

2. Templates (CRTP)

Used when you want polymorphic behavior (generic drivers) but cannot afford the RAM cost of V-Tables.

template <typename T>
void send_byte(T& driver, uint8_t b) {
    driver.send(b); // Resolved at compile time!
}

Relevance in Embedded/Firmware

1. Hardware Abstraction Layer (HAL)

This is the standard use case. You define an ISerial interface. Your application uses ISerial.

  • Board A uses STM32UART.
  • Board B uses NordicUART.
  • Unit Tests use MockUART. You can swap hardware without changing a single line of application logic.

2. Heterogeneous Collections

You can iterate over a list of different devices.

// Array of pointers to the Base class
Sensor* sensors[] = { &temp, &imu, &battery };

for (auto* s : sensors) {
    s->read(); // Calls Temp::read(), then IMU::read()...
}

3. Testability (Mocking)

Polymorphism is essential for Unit Testing on a PC.

You pass a MockGPIO (which just prints to screen) into your driver instead of a real RealGPIO (which touches registers).

Common Pitfalls (Practical Tips)

PitfallDetails
❌ Missing Virtual DestructorCRITICAL: If a Base class has virtual functions, the Destructor must be virtual. If not, delete base_ptr will not call the Derived destructor, causing resource leaks.
❌ Object Slicing

Polymorphism only works with Pointers (Base*) or References (Base&).

void func(Base b) -> Slices the object (removes derived data).

void func(Base& b) -> Keeps polymorphism.

override KeywordAlways use override in derived classes. If you accidentally change the signature (send(int) vs send(uint8_t)), override triggers a compile error. Without it, you create a generic bug.
❌ Calling Virtual in ConstructorNever call a virtual function inside a Constructor or Destructor. The object is not fully formed yet, so it will call the Base version, not the Derived version.