63. Compute ADC Voltage Scale

A microcontroller reads an analog input using a 12-bit ADC with a digital output range of 0–4095.

In embedded firmware, ADC readings are commonly converted into physical units using fixed-point arithmetic to avoid floating-point overhead and to ensure deterministic behavior. A common pattern is to compute a scale factor at compile time and apply it at runtime using integer math.

Your task is to compute an ADC-to-microvolts scale factor at compile time using a constexpr function, and then use it at runtime to convert an ADC reading into microvolts (µV).

Task Requirements

  1. Create a constexpr function named computeScale.
    • It must compute microvolts per ADC count using the formula:

      (maxVoltage_mV * 1000) / maxAdc 
    • The computation must use integer division, meaning the result is truncated.
  2. Define a constexpr int32_t constant named SCALE_UV by calling:

    computeScale(4095, 3300)
    
  3. Read a 32-bit integer input x, representing the ADC digital value.
  4. Compute the voltage in microvolts using:

    voltage_uv = x * SCALE_UV
    
  5. Print the computed voltage in microvolts.

 

Input Format

  • A single 32-bit integer x representing the ADC digital value

Output Format

  • A single 32-bit integer representing the voltage in microvolts (µV)

 

Example 1

Input:

1000 

Output:

805000

 

Example 2

Input:

4095 

Output:

3296475

 

Constraints

  • 0 ≤ x ≤ 4095
  • maxAdc = 4095
  • maxVoltage_mV = 3300 (3.3 V reference)
  • All calculations must use 32-bit fixed-width integers (int32_t)
  • No floating-point math (float, double) is allowed
  • SCALE_UV must be evaluated at compile time

 

 

 

 

 

Need Help? Refer to the Quick Guide below

In Embedded C, we often use macros (#define) to perform math before the code runs (e.g., #define PRESCALER (CLK / BAUD)).

In C++, constexpr (Constant Expression) allows you to write standard C++ functions and variables that the compiler evaluates during compilation.

This means complex calculations (like generating CRC tables or converting units) happen on the developer's PC, and only the final result is burned into the microcontroller's Flash memory.

Syntax & Usage

1. Variables (True Constants)

Unlike const (which just means "read-only" and might typically sit in RAM or Flash), constexpr guarantees the value is known at compile time.

// C-Style
#define MAX_VOLTAGE 3.3

// C++ Style
constexpr float MAX_VOLTAGE = 3.3f;
constexpr float THRESHOLD = MAX_VOLTAGE / 2.0f; 
// Compiler calculates 1.65f and stores it directly. No runtime division.

2. Functions (The Powerhouse)

You can write functions that execute during compilation.

// Calculates factorial at compile-time
constexpr int factorial(int n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));
}

// Usage
// The compiler effectively writes: int buf_size = 120;
int buf_size = factorial(5); 

3. If/Loops (C++14 onwards)

Modern constexpr supports standard logic.


constexpr uint32_t calculate_prescaler(uint32_t sysclk, uint32_t target_freq) {
    uint32_t div = sysclk / target_freq;
    if (div > 0xFFFF) return 0xFFFF; // Clamp to 16-bit
    return div;
}

Comparison: The Evolution of Constants

Feature#defineconstconstexpr
MechanismText Replacement.Read-Only Variable.Compile-Time Calculation.
Type SafetyNone.Strong.Strong.
DebugHard (Symbol usually lost).Easy.Easy (if runtime fallback).
MemoryNone (Code literal).RAM or Flash.None (Code literal).

Relevance in Embedded/Firmware

1. Zero-Overhead Lookup Tables

Instead of hardcoding a 256-byte CRC table or Sine Wave array (which is error-prone to type manually), you can write a constexpr function to generate it. The compiler fills the array in Flash for you.

struct Table { uint8_t data[256]; };

constexpr Table generate_crc() {
    Table t = {};
    // ... Fill t.data logic ...
    return t;
}

// Table is generated during compilation and stored in Flash (.rodata)
constexpr Table crc_table = generate_crc(); 

2. Safe Register Math

Calculating bitmasks or clock dividers often involves complex shifts and divisions. Using macros (#define) is messy and error-prone. constexpr functions allow you to write readable logic with validation checks (asserts) that run at compile time.

3. Static Assertions

You can use constexpr values in static_assert to prevent invalid configurations from even compiling.

constexpr int BUFFER_SIZE = 128;
static_assert(BUFFER_SIZE % 8 == 0, "Buffer must be aligned to 8 bytes");

Common Pitfalls (Practical Tips)

PitfallDetails
❌ Runtime Usage

A constexpr function can be called at runtime if the inputs aren't constants.

int x = 5; factorial(x); runs on the CPU, not the compiler.

❌ DebuggingYou cannot "step through" a constexpr calculation in the debugger because it happened before the program started.
❌ Complexity LimitCompilers place limits on how many steps a constexpr function can take (recursion depth or loop count) to prevent the compilation from hanging forever.
✅ Use constevalIn C++20, the keyword consteval was added. It forces immediate evaluation and throws an error if it attempts to run at runtime.