Object-Oriented Programming with C++

Chapter 12
Embedded and Mechatronic C++ Applications

Bachelor Program in Mechatronics Engineering
Object-Oriented Programming with C++   •   Bachelor

Embedded and Mechatronic C++ Applications

Introduction and Context

Mechatronic systems combine mechanical structures, sensors, actuators, embedded processors, communication networks, and control algorithms. C++ is particularly suitable for these systems because it supports abstraction through classes while permitting direct access to memory, registers, timing mechanisms, and hardware interfaces.

Embedded C++ differs from desktop C++ in its constraints. A microcontroller may have only a few kilobytes of RAM, limited flash memory, no operating system, strict timing requirements, and a power budget measured in milliwatts. Consequently, good design must balance readability, reuse, safety, execution time, memory consumption, and hardware independence.

This chapter develops object-oriented techniques for sensor and actuator drivers, motors, communication buses, finite-state machines, robotics, and cyber-physical systems. The emphasis is not merely on syntax, but on designing dependable software that interacts with physical processes.

Learning Objectives

By the end of this chapter, students should be able to:

  1. Explain the constraints that distinguish embedded C++ from general-purpose C++.
  2. Design object-oriented interfaces for sensors, actuators, motors, and communication buses.
  3. Apply encapsulation, abstraction, composition, inheritance, and dependency inversion appropriately.
  4. Implement finite-state machines for reactive mechatronic systems.
  5. Analyze timing, memory, power, concurrency, and fault-handling requirements.
  6. Develop a structured architecture for a small robotic or cyber-physical system.
  7. Evaluate safety, reliability, cybersecurity, and maintainability implications.

1. Embedded C++ as a Systems Programming Tool

An embedded program normally interacts with hardware through memory-mapped registers, peripheral controllers, interrupts, timers, and direct input/output pins. Unlike a desktop process, it may execute continuously and may have no facility for dynamic recovery after a failure.

Important constraints include:

  • Limited memory: stack, heap, static data, and program code compete for finite memory.
  • Deterministic timing: a control loop may need to execute every 1 ms or 10 ms.
  • Power limitations: unnecessary computation and peripheral activity reduce battery life.
  • Hardware dependence: pin assignments, registers, electrical levels, and bus protocols matter.
  • Fault exposure: a software error can cause physical motion, overheating, or unsafe operation.
  • Limited debugging: printf-style debugging may disturb timing or be unavailable.

C++ features remain useful, but they should be selected deliberately. Classes, namespaces, templates, constexpr, strong enumerations, references, and RAII can improve reliability. Unrestricted dynamic allocation, deep inheritance hierarchies, exceptions, and large standard-library components may be unsuitable on a small target.

A useful design principle is: abstract what varies, but expose timing and resource costs when they matter. A motor interface can hide whether the motor uses PWM or a smart serial controller, but it should not hide a requirement that commands be issued at a fixed rate.

2. Hardware Abstraction and Object-Oriented Drivers

A hardware driver translates a physical or peripheral interface into a software interface. A good driver has four responsibilities:

  1. Configure the peripheral safely.
  2. Read or write hardware data.
  3. Convert raw values into engineering units where appropriate.
  4. Report errors and invalid conditions.

It should not normally contain high-level robot behaviour. For example, a temperature driver should measure temperature; a separate supervisory controller should decide whether to shut down a motor.

2.1 Encapsulation

Encapsulation prevents unrelated code from manipulating hardware incorrectly. A sensor object can keep calibration constants and bus details private while exposing operations such as begin(), read(), and status().

#include <cstdint>

class TemperatureSensor {
public:
    bool begin();
    bool read_celsius(float& value);
    bool is_valid() const;

private:
    std::int16_t raw_value_{0};
    bool valid_{false};
    float offset_{0.0F};
};

The reference parameter avoids returning a dynamically allocated object. The Boolean result distinguishes a successful measurement from a failed transaction.

2.2 Interfaces and Dependency Inversion

A controller should depend on a stable interface rather than a particular hardware chip. An abstract interface is useful when different sensors or test doubles must be substituted.

class DistanceSensor {
public:
    virtual ~DistanceSensor() = default;
    virtual bool start_measurement() = 0;
    virtual bool data_ready() const = 0;
    virtual bool distance_mm(std::uint16_t& distance) = 0;
};

The principle of dependency inversion is especially valuable for testing. A robot controller can receive a simulated DistanceSensor instead of accessing real hardware. However, virtual dispatch consumes some memory and execution time. On very small systems, a template-based policy or function object can provide static polymorphism:

template <typename Sensor>
class ObstacleMonitor {
public:
    explicit ObstacleMonitor(Sensor& sensor) : sensor_(sensor) {}
    bool obstacle_detected() {
        std::uint16_t d{};
        return sensor_.distance_mm(d) && d < 200;
    }
private:
    Sensor& sensor_;
};

The threshold is expressed in millimetres, making its unit explicit. A production interface should also specify sensor range, update rate, latency, and failure behaviour.

2.3 Register Access and volatile

Hardware registers can change independently of program execution. A register or variable shared with an interrupt service routine may require volatile, which tells the compiler not to remove or combine accesses. volatile does not make an operation atomic, does not provide mutual exclusion, and does not solve data races between threads.

A register definition might conceptually look like this:

struct PortRegisters {
    volatile std::uint32_t direction;
    volatile std::uint32_t output;
    volatile std::uint32_t input;
};

Actual addresses and bit meanings are platform-specific and must come from the manufacturer’s reference manual. Incorrect register assumptions can damage hardware or create unsafe behaviour.

3. Sensors, Actuators, and Engineering Units

Sensors produce raw electrical or digital values; control algorithms require physical quantities. A driver should clearly identify where conversion occurs.

For a linear sensor with raw code r, reference code r₀, and scale k, a calibrated value can be represented as:

x = k(r − r₀) + b

Here, x is the measured quantity, k is the scale factor, b is an offset, and r and r₀ are raw-code values. The units of k must be the units of x per code. This model applies only when the sensor response is sufficiently linear over the operating range.

For an N-bit analogue-to-digital converter with reference voltage Vref, the ideal voltage represented by code n is approximately:

V = nVref / (2N − 1)

where 0 ≤ n ≤ 2^N − 1. The equation assumes an ideal converter and neglects noise, reference error, quantisation effects, and input conditioning.

Worked Example: Analogue Sensor Conversion

Suppose a 12-bit converter uses Vref = 3.3 V and returns n = 2048. Then:

V ≈ 2048 × 3.3 / 4095 ≈ 1.6504 V.

The result is approximately 1.65 V. It should not be reported as more accurate than the reference, sensor, and calibration permit.

Actuator interfaces should constrain unsafe commands. For example, a motor command can use a bounded type or explicit saturation:

class Motor {
public:
    void set_duty_percent(float duty) {
        if (duty > 100.0F) duty = 100.0F;
        if (duty < -100.0F) duty = -100.0F;
        duty_ = duty;
        apply_hardware(duty_);
    }

    float duty_percent() const { return duty_; }

private:
    float duty_{0.0F};
    void apply_hardware(float duty);
};

The negative value may represent reverse direction. A real motor driver must also consider dead time, current limits, braking mode, thermal protection, and an emergency-disable path.

4. Motors and Closed-Loop Control

A motor subsystem commonly contains a command interface, a position or speed sensor, a controller, and a safety supervisor. Separating these roles avoids placing control logic inside low-level register code.

For a proportional controller, the command is:

u(t) = Kp e(t)

where e(t) = r(t) − y(t) is the difference between reference r(t) and measured output y(t), Kp is the proportional gain, and u(t) is the actuator command. In a digital implementation, values are sampled at discrete times k:

u[k] = Kp e[k]

The equation assumes a suitable actuator scale and a controller operating within its valid sampling and saturation limits. Increasing Kp may reduce error but can cause oscillation or excessive actuator demand.

A practical object-oriented design might use:

class SpeedController {
public:
    SpeedController(float kp, float limit)
        : kp_(kp), limit_(limit) {}

    float update(float reference, float measured) {
        float u = kp_ * (reference - measured);
        if (u > limit_) u = limit_;
        if (u < -limit_) u = -limit_;
        return u;
    }
private:
    float kp_;
    float limit_;
};

The controller does not directly access a motor register. This separation allows simulation, unit testing, and replacement of the motor driver.

Sampling period is important. If a controller is designed for a 10 ms period but the task executes irregularly, its effective behaviour changes. Time-dependent controllers should receive measured elapsed time or be scheduled by a deterministic timer.

5. Communication Buses

Mechatronic devices commonly communicate through UART, I²C, SPI, CAN, USB, or industrial Ethernet. Each bus has different topology, bandwidth, addressing, error handling, and timing characteristics.

BusTypical strengthsImportant design concerns
UARTSimple point-to-point communicationFraming, baud-rate agreement, buffering
I²CMultiple addressed devices using few wiresPull-up resistors, bus capacitance, arbitration
SPIHigh speed and simple transactionsChip-select management, no inherent addressing
CANRobust multi-node automotive/industrial networkArbitration, message identifiers, termination

A bus abstraction should represent transactions without pretending that all buses behave identically.

class BusDevice {
public:
    virtual ~BusDevice() = default;
    virtual bool write(std::uint8_t address,
                       const std::uint8_t* data,
                       std::size_t length) = 0;
    virtual bool read(std::uint8_t address,
                      std::uint8_t* data,
                      std::size_t length) = 0;
};

Robust communication code checks message length, validates identifiers, handles timeouts, and verifies integrity using a checksum or CRC when specified by the protocol. A timeout must be finite; waiting indefinitely can stop a safety-critical control loop.

6. Finite-State Machines for Reactive Systems

A finite-state machine (FSM) represents behaviour as a finite set of states and transitions triggered by events or conditions. It is more predictable than scattering Boolean flags throughout a program.

A mobile robot might use the states Idle, Running, Obstacle, Fault, and EmergencyStop. Transitions should specify both the event and the action taken. Entry and exit actions are often preferable to repeating commands on every loop iteration.

enum class RobotState { Idle, Running, Obstacle, Fault, EmergencyStop };

class RobotController {
public:
    void update(bool start, bool obstacle, bool fault,
                bool emergency_stop) {
        switch (state_) {
        case RobotState::Idle:
            stop_motors();
            if (emergency_stop) state_ = RobotState::EmergencyStop;
            else if (fault) state_ = RobotState::Fault;
            else if (start) state_ = RobotState::Running;
            break;
        case RobotState::Running:
            if (emergency_stop) state_ = RobotState::EmergencyStop;
            else if (fault) state_ = RobotState::Fault;
            else if (obstacle) state_ = RobotState::Obstacle;
            else drive_forward();
            break;
        case RobotState::Obstacle:
            stop_motors();
            if (!obstacle) state_ = RobotState::Running;
            break;
        case RobotState::Fault:
        case RobotState::EmergencyStop:
            stop_motors();
            break;
        }
    }
private:
    RobotState state_{RobotState::Idle};
    void stop_motors();
    void drive_forward();
};

The example illustrates a safety priority: emergency stop dominates normal operation. A complete design would define how faults are cleared, whether reset requires an operator, and how sensor failure is detected.

7. Scheduling, Interrupts, and Real-Time Behaviour

A control system may contain periodic tasks such as sensor sampling, control computation, communication, and diagnostics. If the worst-case execution time of task i is Ci and its period is Ti, a basic processor-utilisation estimate is:

U = ∑(Ci / Ti)

Ci and Ti must use the same time unit, and U is dimensionless. This estimate is useful but not sufficient to prove schedulability: priorities, deadlines, blocking, interrupt time, and scheduling policy also matter.

Interrupt service routines should be short. They may capture a timestamp, move data into a buffer, or set an event flag. Lengthy calculations, blocking calls, and dynamic allocation should normally be performed in a task or main loop.

Shared data requires careful design. A multi-byte value can be inconsistent if an interrupt updates it while foreground code reads it. Solutions include atomic operations, critical sections, lock-free single-producer/single-consumer buffers, or message queues, depending on the platform.

8. Memory, Power, and Reliability

Memory usage includes:

  • static and global objects;
  • stack frames;
  • dynamic allocations;
  • buffers and communication queues;
  • program code and read-only data.

A bounded, statically allocated buffer is often preferable to repeated heap allocation. If dynamic allocation is used, allocation failure and fragmentation must be addressed. std::array is usually more predictable than a dynamically growing container on a constrained target.

Power-aware software can reduce clock frequency, disable unused peripherals, batch communication, and enter sleep states between scheduled tasks. The design must preserve wake-up latency and sampling requirements.

Reliability practices include assertions during development, range checking, watchdog timers, brownout detection, sensor plausibility checks, and a defined fail-safe state. A watchdog is not a substitute for diagnosing faults; it is a recovery mechanism for failures such as deadlock or runaway execution.

9. Case Study: Illustrative Autonomous Conveyor Cell

Background

Consider an illustrative automated conveyor cell that detects packages, measures their position, and diverts selected packages using a servo actuator. The controller contains a microcontroller, an optical sensor, an encoder, a motor driver, a servo, and a CAN connection to a supervisory controller.

Problem

The system must operate continuously. A missed sensor event can cause a package to be diverted incorrectly, while a stuck sensor or communication failure must not cause uncontrolled motion.

Analysis

The system is decomposed into:

  • OpticalSensorDriver: reports beam interruption and diagnostics;
  • Encoder: provides position increments;
  • ConveyorMotor: accepts bounded speed commands;
  • ServoActuator: moves to validated positions;
  • CanInterface: exchanges commands and status;
  • CellController: implements the operating FSM;
  • SafetySupervisor: monitors emergency stop, overcurrent, and timeout conditions.

The controller uses an FSM with Stopped, Starting, Running, Diverting, and Fault. The low-level drivers do not decide whether a package should be diverted; they only provide trustworthy measurements and bounded commands.

Approach and Solution

A periodic task samples the optical sensor and encoder. An event is timestamped when a package is detected. The controller estimates when the package reaches the diversion position using encoder counts rather than assuming constant conveyor speed. This makes the system more robust to speed variation.

Every command has a timeout. If the supervisory controller stops sending a valid heartbeat, the cell transitions to Fault and commands zero motor speed. If the optical sensor remains blocked longer than a configured maximum, the controller records a diagnostic fault rather than repeatedly triggering diversions.

Testing uses a simulated sensor, motor, and CAN interface. Unit tests verify state transitions, boundary conditions, and emergency-stop priority. Hardware-in-the-loop tests verify electrical timing and actuator response.

Outcome

The architecture separates hardware concerns from operational policy, permits automated testing, and provides explicit fault behaviour. It also makes it possible to replace the optical sensor or CAN implementation without rewriting the state machine.

Learning Points

  1. Physical systems require explicit units, limits, and timing assumptions.
  2. Safety behaviour belongs in a supervisory layer as well as in actuator drivers.
  3. Simulation interfaces reduce dependence on laboratory hardware during development.
  4. A state machine makes abnormal conditions visible and testable.
  5. Reliability depends on interactions among software, electronics, mechanics, and operators.

10. Safety, Cybersecurity, and Professional Practice

Embedded mechatronic software can cause physical injury or environmental damage. Engineers should identify hazards, define safe states, and provide independent emergency-stop mechanisms where required. Software controls should not be the sole protection against a dangerous mechanical movement.

Cybersecurity becomes important when devices are networked. Secure design includes authenticated commands, protected firmware updates, least-privilege access, replay protection where appropriate, input validation, and secure handling of diagnostic interfaces. A CAN message identifier alone is not necessarily authentication.

Relevant standards depend on the application and jurisdiction. Industrial machinery, automotive systems, medical devices, and collaborative robots have different safety and cybersecurity frameworks. Engineers must consult applicable standards rather than assuming that a general coding convention is sufficient.

Ethically, developers must communicate limitations honestly. A sensor with a nominal accuracy of ±2% should not be represented as exact, and a controller should not silently ignore a failed measurement. Logs, version control, code review, traceable requirements, and documented calibration support professional accountability.

11. Contemporary Developments

Modern embedded systems increasingly use multicore microcontrollers, real-time operating systems, model-based design, over-the-air updates, and edge machine learning. C++ is also used with safer subsets, static analysis, coding standards, and hardware abstraction frameworks.

Machine learning can classify sensor patterns or detect anomalies, but it introduces memory, timing, validation, and explainability concerns. A learned model should not replace deterministic safety interlocks unless its limitations have been rigorously assessed. Hybrid systems commonly use machine learning for perception and conventional verified logic for actuation limits and emergency response.

The continuing trend is toward connected cyber-physical systems in which sensing, computation, communication, and physical action form a feedback loop. This increases capability but also enlarges the attack surface and the consequences of software defects.

Best-Practice Checklist

  • Define units, ranges, sampling periods, and failure behaviour in interfaces.
  • Keep hardware drivers separate from control policy.
  • Prefer bounded memory use and deterministic execution for critical tasks.
  • Keep interrupt routines short and non-blocking.
  • Validate all external data and communication messages.
  • Make actuator limits and emergency-stop paths explicit.
  • Use interfaces or templates to support simulation and testing.
  • Document assumptions about hardware, timing, calibration, and power.
  • Test nominal, boundary, fault, reset, and communication-loss conditions.
  • Apply relevant safety, cybersecurity, and coding standards.

Chapter Summary

Embedded and mechatronic C++ requires both object-oriented design and systems-level discipline. Encapsulation and interfaces create reusable drivers, while composition separates sensors, actuators, buses, controllers, and supervisory logic. Explicit units, calibration, limits, timing, and error handling are essential when software controls physical equipment.

Finite-state machines provide a clear model for normal and abnormal behaviour. Periodic scheduling, interrupt discipline, bounded memory, power management, and watchdog strategies support dependable operation. In networked systems, communication validation and cybersecurity are part of engineering quality. The conveyor-cell case demonstrates how these principles combine in a practical cyber-physical application.

Key Terms

Encapsulation: Restricting direct access to an object’s internal data and operations.

Hardware abstraction: A software layer that hides hardware-specific details behind an interface.

Driver: Software that configures and operates a hardware device.

Memory-mapped register: A hardware register accessed through a processor address.

volatile: A qualifier indicating that a value may change outside ordinary program flow.

Static polymorphism: Compile-time variation commonly implemented with templates.

Dynamic polymorphism: Run-time method selection commonly implemented with virtual functions.

Calibration: Establishing the relationship between raw measurements and physical quantities.

Actuator saturation: Limiting a command to the physically allowable range.

PWM: Pulse-width modulation used to control average power or voltage.

Finite-state machine: A model consisting of states, events, guards, and transitions.

Interrupt service routine: Code executed in response to a hardware or software interrupt.

Worst-case execution time: The maximum execution duration considered for a task.

Watchdog timer: A timer that resets or signals a system if software fails to respond.

Fail-safe state: A condition designed to reduce hazard after a fault.

Cyber-physical system: A system in which computation and communication interact with physical processes.

CRC: Cyclic redundancy check used to detect errors in transmitted data.

Dependency inversion: Designing high-level policy to depend on abstractions rather than concrete hardware.

Review Questions

Short Answer Questions

  1. Why are bounded memory use and deterministic timing important in embedded C++?
  2. What responsibilities should a sensor driver have?
  3. Explain the difference between volatile and atomic access.
  4. What is the purpose of a finite-state machine in a robot controller?
  5. Why should an interrupt service routine normally be short?

Long Answer Questions

  1. Explain how encapsulation and dependency inversion can be used to design portable sensor drivers.
  2. Compare UART, I²C, SPI, and CAN for mechatronic applications.
  3. Discuss memory, power, and timing trade-offs in an embedded C++ system.
  4. Design a finite-state machine for a motorized access gate, including fault and emergency-stop behaviour.
  5. Explain how software architecture contributes to functional safety and cybersecurity.

Analytical / Application Questions

  1. Design the classes and interfaces for a two-wheel mobile robot that uses two encoders, a distance sensor, two motors, and a CAN diagnostic link. Justify the separation between drivers and control policy.
  2. A periodic task has execution time 0.8 ms and period 5 ms, while a second task has execution time 1.2 ms and period 10 ms. Calculate the utilisation using U = ∑(Ci/Ti), and explain why the result alone does not prove schedulability.
  3. A conveyor controller continues operating after losing its supervisory heartbeat. Propose an FSM transition strategy, timeout policy, actuator response, and testing plan that would make the design safer.

Multiple Choice Questions with Answers

Question 1

Which responsibility most appropriately belongs to a low-level sensor driver?

A. Selecting the robot’s route

B. Converting device data and reporting measurement validity

C. Deciding the factory production schedule

D. Training a machine-learning model

Correct Answer: B. Converting device data and reporting measurement validity

Question 2

What does volatile primarily tell the compiler?

A. The variable is always atomic

B. The variable cannot be modified

C. The value may change outside ordinary program flow

D. The variable is stored in flash memory

Correct Answer: C. The value may change outside ordinary program flow

Question 3

For a 12-bit ideal ADC with 3.3 V reference, what voltage approximately corresponds to code 2048?

A. 0.805 V

B. 1.650 V

C. 2.475 V

D. 3.300 V

Correct Answer: B. 1.650 V

Question 4

Which design most directly supports replacing a real sensor with a simulated sensor during testing?

A. Hard-coding register addresses in the controller

B. Using a stable sensor interface

C. Using global variables for every measurement

D. Placing all logic in an interrupt routine

Correct Answer: B. Using a stable sensor interface

Question 5

What is a principal advantage of a finite-state machine?

A. It eliminates all hardware faults

B. It makes states and transitions explicit

C. It guarantees unlimited memory

D. It removes the need for timing analysis

Correct Answer: B. It makes states and transitions explicit

Question 6

If a task has execution time 0.8 ms and period 5 ms, what is its utilisation contribution?

A. 0.016

B. 0.08

C. 0.16

D. 6.25

Correct Answer: C. 0.16

Question 7

Which action is generally appropriate for an interrupt service routine?

A. Performing lengthy blocking communication

B. Allocating large dynamic objects

C. Capturing an event and setting a flag

D. Executing a complete path-planning algorithm

Correct Answer: C. Capturing an event and setting a flag

Question 8

Why should actuator commands be bounded?

A. To prevent commands exceeding physical or safety limits

B. To increase compiler warning messages

C. To guarantee sensor accuracy

D. To remove the need for calibration

Correct Answer: A. To prevent commands exceeding physical or safety limits

Question 9

Which bus is especially associated with robust multi-node industrial or automotive communication?

A. CAN

B. I²C only for single devices

C. Simple GPIO

D. Analogue voltage input

Correct Answer: A. CAN

Question 10

In the conveyor-cell case, why is encoder position preferable to assuming constant conveyor speed?

A. It eliminates the need for any sensor

B. It accounts better for speed variation

C. It guarantees zero measurement noise

D. It makes emergency stops unnecessary

Correct Answer: B. It accounts better for speed variation

Object-Oriented Programming with C++
Chapter 12 of 14