Skip to content

C++ for Hardware

C++ is essential for ATE (Automatic Test Equipment) programming, firmware development, and performance-critical validation applications. This guide covers C++ fundamentals with a focus on hardware validation and embedded systems.


Why C++ for Validation?

Advantage Description
Performance Direct hardware access, minimal overhead
Memory Control Manual memory management for predictable behavior
ATE Support Native language for Teradyne, Advantest platforms
Firmware Embedded systems, bare-metal programming
Legacy Code Large existing codebase in validation

Embedded C++ Essentials

The volatile Keyword

The volatile keyword tells the compiler that a variable's value may change at any time, preventing optimization that could skip reads/writes.

// Hardware register - MUST be volatile
volatile uint32_t* const STATUS_REG = (uint32_t*)0x40001000;
volatile uint32_t* const CONTROL_REG = (uint32_t*)0x40001004;
volatile uint32_t* const DATA_REG = (uint32_t*)0x40001008;

// Without volatile - WRONG! Compiler may optimize away reads
uint32_t* status = (uint32_t*)0x40001000;
while (*status == 0) { }  // May become infinite loop!

// With volatile - CORRECT
volatile uint32_t* status_v = (volatile uint32_t*)0x40001000;
while (*status_v == 0) { }  // Always reads from hardware

// Example: Waiting for hardware ready bit
void wait_for_ready() {
    volatile uint32_t* status = (volatile uint32_t*)0x40001000;

    // Poll until bit 0 (READY) is set
    while ((*status & 0x01) == 0) {
        // Compiler MUST read status each iteration
    }
}

// Example: Interrupt flag cleared by hardware
volatile bool interrupt_occurred = false;

void ISR_Handler() {
    interrupt_occurred = true;  // Set in ISR
}

void main_loop() {
    while (!interrupt_occurred) {
        // Without volatile, compiler might cache the value
    }
    interrupt_occurred = false;
}

// Memory-mapped peripheral structure
struct __attribute__((packed)) UART_Regs {
    volatile uint32_t DATA;     // 0x00 - Data register
    volatile uint32_t STATUS;   // 0x04 - Status register
    volatile uint32_t CONTROL;  // 0x08 - Control register
    volatile uint32_t BAUD;     // 0x0C - Baud rate register
};

volatile UART_Regs* const UART0 = (UART_Regs*)0x40010000;

void uart_send(uint8_t byte) {
    // Wait for TX buffer empty
    while (!(UART0->STATUS & (1 << 7))) { }
    UART0->DATA = byte;
}

When to Use volatile

  • Memory-mapped hardware registers
  • Variables modified by ISRs (Interrupt Service Routines)
  • Variables shared between threads (consider std::atomic instead)
  • DMA buffers

Never use volatile for thread synchronization - use std::atomic or mutexes.


The const Keyword

const provides compile-time guarantees about immutability and enables compiler optimizations.

// 1. Const variables - value cannot change
const uint32_t MAX_BUFFER_SIZE = 1024;
const double PI = 3.14159265359;

// 2. Const pointers vs pointer to const
int value = 10;
int another = 20;

const int* ptr1 = &value;      // Pointer to const int (data is const)
// *ptr1 = 20;                 // ERROR: Cannot modify data
ptr1 = &another;               // OK: Can change pointer

int* const ptr2 = &value;      // Const pointer to int (pointer is const)
*ptr2 = 20;                    // OK: Can modify data
// ptr2 = &another;            // ERROR: Cannot change pointer

const int* const ptr3 = &value; // Both const
// *ptr3 = 20;                 // ERROR
// ptr3 = &another;            // ERROR

// 3. Const member functions - don't modify object
class Device {
private:
    uint32_t base_address_;
    bool initialized_;

public:
    // Const method - promises not to modify member variables
    uint32_t getBaseAddress() const {
        return base_address_;
    }

    bool isInitialized() const {
        return initialized_;
    }

    // Non-const method - may modify object
    void initialize() {
        initialized_ = true;
    }

    // Const-correct read function
    uint32_t readRegister(uint32_t offset) const {
        volatile uint32_t* reg = (volatile uint32_t*)(base_address_ + offset);
        return *reg;
    }
};

// 4. Const references - efficient parameter passing
void processData(const std::vector<uint32_t>& data) {
    // Cannot modify data, but no copy made
    for (const auto& value : data) {
        // Process value
    }
}

// 5. Const return values
class Config {
private:
    std::string name_;

public:
    // Return const reference to prevent modification
    const std::string& getName() const {
        return name_;
    }
};

// 6. constexpr - Compile-time constants (C++11+)
constexpr uint32_t CLOCK_FREQ = 100000000;  // 100 MHz
constexpr uint32_t calculateBaudDivisor(uint32_t baud) {
    return CLOCK_FREQ / (16 * baud);
}
constexpr uint32_t BAUD_115200_DIV = calculateBaudDivisor(115200);  // Computed at compile time

Bitwise Operators Deep Dive

#include <cstdint>
#include <bitset>

// ============================================================
// BITWISE OPERATORS
// ============================================================

// AND (&) - Mask/clear bits
uint32_t reg = 0xABCD1234;
uint32_t masked = reg & 0x0000FFFF;    // Keep lower 16 bits: 0x1234
uint32_t bit_check = reg & (1 << 5);   // Check if bit 5 is set

// OR (|) - Set bits
uint32_t with_bit_set = reg | (1 << 7);         // Set bit 7
uint32_t combined = 0x00 | 0x01 | 0x04 | 0x10;  // Set bits 0, 2, 4

// XOR (^) - Toggle bits
uint32_t toggled = reg ^ 0x0000FFFF;   // Toggle lower 16 bits
uint32_t swapped = a ^ b; b = a ^ b; a = swapped;  // XOR swap trick

// NOT (~) - Invert all bits
uint32_t inverted = ~reg;              // All bits flipped
uint32_t clear_mask = ~(1 << 5);       // All 1s except bit 5

// Left Shift (<<) - Multiply by 2^n
uint32_t mul_8 = value << 3;           // Multiply by 8
uint32_t bit_position = 1 << n;        // Set bit at position n

// Right Shift (>>) - Divide by 2^n
uint32_t div_4 = value >> 2;           // Divide by 4
uint32_t upper_byte = reg >> 24;       // Get bits [31:24]

// ============================================================
// COMMON BIT MANIPULATION PATTERNS
// ============================================================

// Bit manipulation macros (widely used in embedded)
#define BIT(n)              (1UL << (n))
#define SET_BIT(reg, n)     ((reg) |= BIT(n))
#define CLR_BIT(reg, n)     ((reg) &= ~BIT(n))
#define TGL_BIT(reg, n)     ((reg) ^= BIT(n))
#define GET_BIT(reg, n)     (((reg) >> (n)) & 1UL)
#define CHK_BIT(reg, n)     ((reg) & BIT(n))

// Multi-bit field macros
#define FIELD_MASK(width)           ((1UL << (width)) - 1)
#define GET_FIELD(reg, pos, width)  (((reg) >> (pos)) & FIELD_MASK(width))
#define SET_FIELD(reg, pos, width, val) \
    ((reg) = ((reg) & ~(FIELD_MASK(width) << (pos))) | (((val) & FIELD_MASK(width)) << (pos)))

// Example usage
uint32_t status = 0x12345678;
uint32_t field = GET_FIELD(status, 8, 4);  // Extract 4 bits starting at bit 8
SET_FIELD(status, 16, 8, 0xAB);            // Set 8-bit field at bit 16

// ============================================================
// BIT FIELD EXTRACTION FUNCTIONS
// ============================================================

// Extract bit field [msb:lsb]
inline uint32_t extract_bits(uint32_t value, int msb, int lsb) {
    uint32_t width = msb - lsb + 1;
    uint32_t mask = (1UL << width) - 1;
    return (value >> lsb) & mask;
}

// Insert value into bit field [msb:lsb]
inline uint32_t insert_bits(uint32_t reg, uint32_t value, int msb, int lsb) {
    uint32_t width = msb - lsb + 1;
    uint32_t mask = (1UL << width) - 1;
    reg &= ~(mask << lsb);           // Clear field
    reg |= (value & mask) << lsb;    // Insert value
    return reg;
}

// Example: PCIe TLP Header parsing
uint32_t tlp_header = 0x40000001;
uint32_t fmt = extract_bits(tlp_header, 30, 29);    // Format field
uint32_t type = extract_bits(tlp_header, 28, 24);   // Type field
uint32_t length = extract_bits(tlp_header, 9, 0);   // Length field

// ============================================================
// USEFUL BIT TRICKS
// ============================================================

// Check if power of 2
bool is_power_of_2(uint32_t n) {
    return n && !(n & (n - 1));
}

// Count set bits (population count)
int popcount(uint32_t n) {
    int count = 0;
    while (n) {
        count += n & 1;
        n >>= 1;
    }
    return count;
}
// Or use built-in: __builtin_popcount(n)

// Find first set bit (from LSB)
int find_first_set(uint32_t n) {
    if (n == 0) return -1;
    return __builtin_ffs(n) - 1;  // GCC built-in
}

// Round up to next power of 2
uint32_t next_power_of_2(uint32_t n) {
    n--;
    n |= n >> 1;
    n |= n >> 2;
    n |= n >> 4;
    n |= n >> 8;
    n |= n >> 16;
    return n + 1;
}

// Byte swap (endian conversion)
uint32_t byte_swap_32(uint32_t val) {
    return ((val >> 24) & 0x000000FF) |
           ((val >> 8)  & 0x0000FF00) |
           ((val << 8)  & 0x00FF0000) |
           ((val << 24) & 0xFF000000);
}
// Or use: __builtin_bswap32(val)

// Align to boundary
inline uint32_t align_up(uint32_t value, uint32_t alignment) {
    return (value + alignment - 1) & ~(alignment - 1);
}

inline uint32_t align_down(uint32_t value, uint32_t alignment) {
    return value & ~(alignment - 1);
}

Bit Fields in Structures

#include <cstdint>

// Bit fields allow compact storage and direct bit access
// WARNING: Bit field layout is implementation-defined!

// PCIe Configuration Space Header (Type 0)
struct __attribute__((packed)) PCIeConfigHeader {
    uint16_t vendor_id;
    uint16_t device_id;
    uint16_t command;
    uint16_t status;
    uint8_t  revision_id;
    uint8_t  prog_if;
    uint8_t  subclass;
    uint8_t  class_code;
    uint8_t  cache_line_size;
    uint8_t  latency_timer;
    uint8_t  header_type;
    uint8_t  bist;
    uint32_t bar[6];
    uint32_t cardbus_cis;
    uint16_t subsystem_vendor_id;
    uint16_t subsystem_id;
    uint32_t expansion_rom;
    uint8_t  capabilities_ptr;
    uint8_t  reserved[7];
    uint8_t  interrupt_line;
    uint8_t  interrupt_pin;
    uint8_t  min_grant;
    uint8_t  max_latency;
};

// Using bit fields for hardware registers
struct StatusRegister {
    uint32_t ready      : 1;   // Bit 0
    uint32_t busy       : 1;   // Bit 1
    uint32_t error      : 1;   // Bit 2
    uint32_t reserved1  : 5;   // Bits 3-7
    uint32_t error_code : 8;   // Bits 8-15
    uint32_t count      : 12;  // Bits 16-27
    uint32_t reserved2  : 4;   // Bits 28-31
};

// Union for dual access (raw and structured)
union HardwareRegister {
    uint32_t raw;
    struct {
        uint32_t enable     : 1;
        uint32_t mode       : 3;
        uint32_t speed      : 4;
        uint32_t reserved   : 8;
        uint32_t data       : 16;
    } fields;
};

// Usage
HardwareRegister reg;
reg.raw = 0x12340000;                    // Write raw value
reg.fields.enable = 1;                   // Set enable bit
reg.fields.mode = 5;                     // Set mode field
uint32_t value = reg.raw;                // Read as 32-bit

// Type-safe register access class
template<typename T>
class Register {
private:
    volatile T* addr_;

public:
    explicit Register(uintptr_t address)
        : addr_(reinterpret_cast<volatile T*>(address)) {}

    T read() const { return *addr_; }
    void write(T value) { *addr_ = value; }

    void set_bits(T mask) { *addr_ |= mask; }
    void clear_bits(T mask) { *addr_ &= ~mask; }
    void toggle_bits(T mask) { *addr_ ^= mask; }

    bool test_bit(int bit) const { return (*addr_ >> bit) & 1; }
};

Memory Alignment and Packing

#include <cstdint>
#include <cstddef>

// Natural alignment (compiler default)
struct NaturalAlignment {
    char a;       // 1 byte, offset 0
    // 3 bytes padding
    int b;        // 4 bytes, offset 4
    char c;       // 1 byte, offset 8
    // 3 bytes padding
};  // Total: 12 bytes (with padding)

// Packed structure (no padding) - for hardware registers/protocols
struct __attribute__((packed)) PackedStruct {
    char a;       // 1 byte, offset 0
    int b;        // 4 bytes, offset 1
    char c;       // 1 byte, offset 5
};  // Total: 6 bytes (no padding)

// Explicit alignment
struct alignas(16) AlignedStruct {
    uint32_t data[4];
};  // Aligned to 16-byte boundary

// Check alignment
static_assert(alignof(AlignedStruct) == 16, "Alignment check");
static_assert(sizeof(PackedStruct) == 6, "Size check");

// DMA buffer with alignment requirement
struct alignas(64) DMABuffer {
    uint8_t data[4096];  // Cache-line aligned for DMA
};

// Memory-mapped register block (must match hardware layout)
struct __attribute__((packed)) GPIORegisters {
    volatile uint32_t DATA;        // 0x00
    volatile uint32_t DIR;         // 0x04
    volatile uint32_t INT_ENABLE;  // 0x08
    volatile uint32_t INT_STATUS;  // 0x0C
    volatile uint32_t INT_CLEAR;   // 0x10
    uint32_t reserved[3];          // 0x14 - 0x1F
    volatile uint32_t ALT_FUNC;    // 0x20
};

static_assert(offsetof(GPIORegisters, ALT_FUNC) == 0x20, "Offset check");

// Pointer alignment check
bool is_aligned(void* ptr, size_t alignment) {
    return (reinterpret_cast<uintptr_t>(ptr) % alignment) == 0;
}

Function Pointers and Callbacks

#include <functional>
#include <cstdint>

// ============================================================
// FUNCTION POINTERS (C-style)
// ============================================================

// Function pointer typedef
typedef void (*ISR_Handler)(void);
typedef uint32_t (*ReadFunc)(uint32_t address);
typedef void (*WriteFunc)(uint32_t address, uint32_t value);

// Function pointer declaration
void (*callback)(int status);

// Interrupt vector table
ISR_Handler vector_table[256] = {nullptr};

void register_isr(int irq_num, ISR_Handler handler) {
    vector_table[irq_num] = handler;
}

void timer_isr() {
    // Handle timer interrupt
}

// Register the ISR
register_isr(15, timer_isr);

// Function pointer for hardware abstraction
struct HardwareInterface {
    ReadFunc read;
    WriteFunc write;
    void (*init)(void);
    void (*deinit)(void);
};

uint32_t pcie_read(uint32_t addr) { /* ... */ return 0; }
void pcie_write(uint32_t addr, uint32_t val) { /* ... */ }
void pcie_init() { /* ... */ }

HardwareInterface pcie_interface = {
    .read = pcie_read,
    .write = pcie_write,
    .init = pcie_init,
    .deinit = nullptr
};

// ============================================================
// MODERN C++ CALLBACKS
// ============================================================

// std::function - more flexible than function pointers
#include <functional>

class Device {
public:
    using Callback = std::function<void(int status, uint32_t data)>;

private:
    Callback completion_callback_;

public:
    void setCallback(Callback cb) {
        completion_callback_ = std::move(cb);
    }

    void onOperationComplete(int status, uint32_t data) {
        if (completion_callback_) {
            completion_callback_(status, data);
        }
    }
};

// Usage with lambda
Device dev;
dev.setCallback([](int status, uint32_t data) {
    if (status == 0) {
        printf("Success: data = 0x%08X\n", data);
    }
});

// Usage with member function
class Controller {
public:
    void handleCompletion(int status, uint32_t data) {
        // Handle completion
    }
};

Controller ctrl;
dev.setCallback([&ctrl](int s, uint32_t d) {
    ctrl.handleCompletion(s, d);
});

Static Keyword Usage

#include <cstdint>

// ============================================================
// 1. STATIC LOCAL VARIABLE - Persists across function calls
// ============================================================
uint32_t getNextSequenceNumber() {
    static uint32_t sequence = 0;  // Initialized once, persists
    return sequence++;
}

// Singleton pattern using static local
class Logger {
public:
    static Logger& getInstance() {
        static Logger instance;  // Thread-safe in C++11+
        return instance;
    }

    void log(const char* msg) { /* ... */ }

private:
    Logger() = default;  // Private constructor
};

// ============================================================
// 2. STATIC MEMBER VARIABLE - Shared across all instances
// ============================================================
class Device {
private:
    static int device_count_;    // Shared by all instances
    int device_id_;

public:
    Device() : device_id_(device_count_++) {}

    static int getDeviceCount() { return device_count_; }
    int getDeviceId() const { return device_id_; }
};

// Must define static member in .cpp file
int Device::device_count_ = 0;

// ============================================================
// 3. STATIC MEMBER FUNCTION - No 'this' pointer, can't access non-static members
// ============================================================
class MathUtils {
public:
    static uint32_t crc32(const uint8_t* data, size_t len) {
        // Can be called without object: MathUtils::crc32(...)
        return 0;
    }

    static constexpr double PI = 3.14159265359;
};

// ============================================================
// 4. STATIC GLOBAL/FILE SCOPE - Internal linkage (not visible outside file)
// ============================================================
// In .cpp file:
static uint32_t internal_buffer[256];  // Only visible in this file
static void helper_function() { }       // Only visible in this file

// Preferred C++ way: anonymous namespace
namespace {
    uint32_t private_data[256];
    void private_helper() { }
}

// ============================================================
// 5. STATIC IN CLASS TEMPLATE
// ============================================================
template<typename T>
class Container {
    static size_t instance_count;  // One per type instantiation
};

template<typename T>
size_t Container<T>::instance_count = 0;
// Container<int>::instance_count is separate from Container<float>::instance_count

Inline Functions

#include <cstdint>

// ============================================================
// INLINE FUNCTIONS - Request compiler to insert code at call site
// ============================================================

// Inline function (hint to compiler)
inline uint32_t read_reg(volatile uint32_t* addr) {
    return *addr;
}

inline void write_reg(volatile uint32_t* addr, uint32_t value) {
    *addr = value;
}

// Inline in header file (must be inline if defined in header)
// utils.h
inline int max(int a, int b) {
    return (a > b) ? a : b;
}

// Force inline (compiler-specific)
#ifdef __GNUC__
#define FORCE_INLINE __attribute__((always_inline)) inline
#elif defined(_MSC_VER)
#define FORCE_INLINE __forceinline
#else
#define FORCE_INLINE inline
#endif

FORCE_INLINE uint32_t fast_read(volatile uint32_t* addr) {
    return *addr;
}

// Inline class methods (defined inside class are implicitly inline)
class FastMath {
public:
    // Implicitly inline
    int square(int x) { return x * x; }

    // Explicitly inline for out-of-class definition
    inline int cube(int x);
};

inline int FastMath::cube(int x) {
    return x * x * x;
}

// constexpr functions are implicitly inline
constexpr uint32_t compile_time_calc(uint32_t x) {
    return x * x + 1;
}

// ============================================================
// WHEN TO USE INLINE
// ============================================================
// Good candidates:
// - Small functions (1-3 lines)
// - Frequently called in tight loops
// - Accessor methods (getters/setters)
// - Performance-critical register access

// Bad candidates:
// - Large functions (code bloat)
// - Functions with loops
// - Recursive functions
// - Virtual functions (can't be inlined through pointer)

Data Types

Fixed-Width Integer Types

#include <cstdint>
#include <string>

// Fixed-width integer types (preferred for hardware)
int8_t   signed_byte;      // -128 to 127
uint8_t  unsigned_byte;    // 0 to 255
int16_t  signed_word;      // -32768 to 32767
uint16_t unsigned_word;    // 0 to 65535
int32_t  signed_dword;     // -2^31 to 2^31-1
uint32_t unsigned_dword;   // 0 to 2^32-1
int64_t  signed_qword;     // -2^63 to 2^63-1
uint64_t unsigned_qword;   // 0 to 2^64-1

// Pointer-sized integers
uintptr_t address;         // Can hold any pointer value
ptrdiff_t difference;      // Pointer difference

// Floating point
float  single_precision;   // 32-bit
double double_precision;   // 64-bit

// Boolean
bool test_passed = true;

// Character and strings
char single_char = 'A';
std::string device_name = "DDR4_DIMM";

// Arrays
uint32_t registers[16];
double voltages[4] = {1.2, 1.2, 1.2, 1.2};

// Pointers
uint32_t* reg_ptr = &registers[0];
uint32_t value = *reg_ptr;  // Dereference

Memory Management

Stack vs Heap

#include <memory>
#include <vector>

void memory_examples() {
    // Stack allocation (automatic, fast)
    int stack_array[100];           // Fixed size on stack
    double voltage = 1.2;           // Simple types on stack

    // Heap allocation (dynamic, manual)
    int* heap_array = new int[1000];
    // ... use array ...
    delete[] heap_array;            // Must delete!

    // Smart pointers (modern C++, automatic cleanup)
    auto unique_ptr = std::make_unique<int[]>(1000);
    auto shared_ptr = std::make_shared<DeviceInfo>();

    // Containers (automatic memory management)
    std::vector<double> measurements;
    measurements.push_back(1.2);
    measurements.push_back(1.3);
}  // All automatic cleanup at end of scope

RAII Pattern

#include <fstream>

// Resource Acquisition Is Initialization
class FileHandler {
private:
    std::ofstream file;

public:
    FileHandler(const std::string& filename)
        : file(filename) {
        if (!file.is_open()) {
            throw std::runtime_error("Failed to open file");
        }
    }

    ~FileHandler() {
        if (file.is_open()) {
            file.close();
        }
    }

    void write(const std::string& data) {
        file << data;
    }

    // Prevent copying
    FileHandler(const FileHandler&) = delete;
    FileHandler& operator=(const FileHandler&) = delete;
};

// Usage - file automatically closed when handler goes out of scope
void log_results() {
    FileHandler log("results.txt");
    log.write("Test passed\n");
}  // File automatically closed here

Classes and Inheritance

Class Definition

#include <string>
#include <cstdint>
#include <stdexcept>

class MemoryDevice {
private:
    std::string name_;
    uint32_t base_address_;
    bool initialized_;

protected:
    uint64_t density_;

public:
    // Constructor
    MemoryDevice(const std::string& name, uint32_t base_addr)
        : name_(name)
        , base_address_(base_addr)
        , initialized_(false)
        , density_(0) {}

    // Virtual destructor for inheritance
    virtual ~MemoryDevice() = default;

    // Getter methods
    const std::string& name() const { return name_; }
    uint32_t base_address() const { return base_address_; }
    bool is_initialized() const { return initialized_; }

    // Virtual methods (can be overridden)
    virtual bool initialize() {
        initialized_ = true;
        return true;
    }

    // Pure virtual method (must be implemented by derived classes)
    virtual void reset() = 0;
};

// Derived class
class DDR4Device : public MemoryDevice {
private:
    int cas_latency_;

public:
    DDR4Device(const std::string& name, uint32_t base_addr, int cl)
        : MemoryDevice(name, base_addr), cas_latency_(cl) {
        density_ = 8ULL * 1024 * 1024 * 1024;  // 8Gb
    }

    void reset() override {
        // DDR4-specific reset
    }
};

Error Handling

Exception Handling

#include <stdexcept>
#include <string>

// Custom exception classes
class DeviceError : public std::runtime_error {
public:
    explicit DeviceError(const std::string& msg)
        : std::runtime_error(msg) {}
};

class TimeoutError : public DeviceError {
private:
    int timeout_ms_;

public:
    TimeoutError(const std::string& msg, int timeout)
        : DeviceError(msg), timeout_ms_(timeout) {}

    int timeout() const { return timeout_ms_; }
};

// Exception handling
void run_test() {
    try {
        if (!device.initialize()) {
            throw DeviceError("Initialization failed");
        }
    }
    catch (const TimeoutError& e) {
        std::cerr << "Timeout: " << e.what() << std::endl;
    }
    catch (const DeviceError& e) {
        std::cerr << "Device error: " << e.what() << std::endl;
    }
    catch (...) {
        std::cerr << "Unknown error" << std::endl;
    }
}

Error Codes Pattern (Embedded)

// Error codes (common in embedded/ATE)
enum class ErrorCode : int32_t {
    SUCCESS = 0,
    ERR_INVALID_PARAM = -1,
    ERR_TIMEOUT = -2,
    ERR_DEVICE_NOT_FOUND = -3,
    ERR_COMMUNICATION = -4
};

ErrorCode read_device(uint32_t address, uint32_t* value) {
    if (value == nullptr) {
        return ErrorCode::ERR_INVALID_PARAM;
    }
    *value = 0x12345678;
    return ErrorCode::SUCCESS;
}

Multithreading

Basic Threading

#include <thread>
#include <mutex>
#include <atomic>

// Shared data protection
std::mutex data_mutex;
std::vector<double> shared_measurements;

void measure_channel(int channel_id) {
    double measurement = read_voltage(channel_id);

    std::lock_guard<std::mutex> lock(data_mutex);
    shared_measurements.push_back(measurement);
}

// Atomic variables for simple synchronization
std::atomic<bool> stop_flag{false};
std::atomic<int> test_count{0};

void run_parallel_tests() {
    std::vector<std::thread> threads;

    for (int i = 0; i < 4; i++) {
        threads.emplace_back(measure_channel, i);
    }

    for (auto& t : threads) {
        t.join();
    }
}


References

  • ISO C++ Standard
  • cppreference.com
  • Embedded C++ Best Practices
  • ATE vendor documentation