Real-Time Vehicle Control Systems

Our vehicles are becoming more and more computer systems, computer systems with wheels, wings, propellors, and many variations such as caterpillar tracks.

Real-time vehicle control and monitoring is an important and complex scenario for advanced C++ development. In this scenario software has to be developed that can be entrusted with the well-being of the vehicle’s crew and passengers. In this scenario, we examine what libraries might prove to be valuable building blocks, what kind of architecture and control flow might be involved, and best-practices for safe systems.

With this high-tech control system in mind, let’s examine the role individual Boost libraries might play.

Libraries

Lbrary Possible role in the tractor

Boost.Asio

Real-time-ish I/O, CAN adapters, Ethernet, serial devices, timers

Boost.Atomic

Lock-free/shared state between control threads

Boost.Lockfree

Sensor/command queues between acquisition and control threads

Boost.Thread

Worker threads and synchronization

Boost.Chrono

Precise timing and control-loop periods

Boost.Units

Prevent mixing meters, feet, radians, PSI, kPa, etc.

Boost.Numeric/odeint

Simulating vehicle dynamics and hydraulic/engine systems

Boost.Math

Filters, interpolation, statistics, numerical functions

Boost.Geometry

Vehicle position, heading, paths and spatial calculations

Boost.CircularBuffer

Recent sensor history / moving windows

Boost.MultiIndex

Efficiently indexing large collections of sensor/device data

Boost.Log

Diagnostics and event logging

Boost.Stacktrace

Diagnostic information after failures

Boost.System

Portable error handling

Boost.ProgramOptions

Configuration/calibration parameters

Boost.Json

Configuration and diagnostic interfaces

Boost.Serialization

Saving/restoring configuration or state

Boost.Statechart

Vehicle operating modes and state machines

Boost.Msm

High-performance state-machine implementation, if appropriate

Boost.SafeNumerics

Detecting arithmetic errors in safety-sensitive calculations

Boost.Test

Automated testing of control algorithms

Architecture

As an example, we will look at a work-horse vehicle, the caterpillar tractor. The following image shows how much a modern interface might influence the design of the interior of the cab:

Caterpillar Tractor Controls

The basic architecture of a vehicle control app could be as simple as:

              every 10 ms

                  ┌───────────────┐
                  │ Read sensors  │
                  └───────┬───────┘
                          │
                          ▼
                  ┌───────────────┐
                  │ Validate data │
                  └───────┬───────┘
                          │
                          ▼
                  ┌───────────────┐
                  │ Update state  │
                  └───────┬───────┘
                          │
                          ▼
                  ┌───────────────┐
                  │ Control law   │
                  │ PID / model   │
                  └───────┬───────┘
                          │
                          ▼
                  ┌───────────────┐
                  │ Send commands │
                  └───────┬───────┘
                          │
                          ▼
            wait until next 10 ms starts

PID stands for Proportional-Integral-Derivative. It is one of the most common feedback-control algorithms used in engineering. (1)

Simulate a PID Controller

Let’s build a small, runnable vehicle speed-control simulation, showing the PID approach in action.

We’ll model a vehicle that must accelerate to a target speed and maintain it, even when a hill introduces extra resistance. A PID controller adjusts the throttle, while we simulate the vehicle’s motion over time.

The example demonstrate a useful separation of responsibilities:

  • Boost.Numeric/odeint : integrates the vehicle’s equations of motion.

  • Boost.Atomic : safely publishes the target speed between a simulated supervisory controller and the control loop.

#include <boost/atomic.hpp>
#include <boost/numeric/odeint.hpp>
#include <iostream>

// State variables: position (m), speed (m/s).
using State = std::array<double, 2>;
using namespace boost::numeric::odeint;

// Simplified vehicle parameters.
constexpr double mass = 1500.0;          // Kg
constexpr double max_force = 5000.0;     // Newtons
constexpr double rolling_force = 180.0;  // Newtons
constexpr double drag_factor = 0.42;     // Newtons / (meters/second)^2
constexpr double gravity = 9.81;         // meters/second^2

// PID controller: output is throttle in the range [0 = none, 1 = max].
struct PID
{
    double kp = 0.025;
    double ki = 0.003;
    double kd = 0.002;

    double integral = 0.0;
    double previous_error = 0.0;

    double update(double target, double speed, double dt)
    {
        const double error = target - speed;

        // Limit the integral term to reduce integral windup.
        integral = std::clamp(integral + error * dt,
            -20.0, 20.0);

        const double derivative = (error - previous_error) / dt;
        previous_error = error;

        const double output =
            kp * error + ki * integral + kd * derivative;

        return std::clamp(output, 0.0, 1.0);
    }
};

// Vehicle equations of motion.
// The throttle and road slope remain constant during each step.
struct VehicleDynamics
{
    double throttle;
    double slope;

    void operator()(const State& x, State& dxdt, double /* time */) const
    {
        const double speed = x[1];

        const double traction = throttle * max_force;
        const double drag = drag_factor * speed * std::abs(speed);
        const double rolling = rolling_force * std::tanh(speed / 0.2);

        // Convert road slope (rise / horizontal distance) to
        // the component of gravity acting along the road.
        const double hill_force =
            mass * gravity * slope /
            std::sqrt(1.0 + slope * slope);

        const double acceleration =
            (traction - drag - rolling - hill_force) / mass;

        dxdt[0] = speed;         // Position derivative.
        dxdt[1] = acceleration;  // Speed derivative.
    }
};

int main()
{
    constexpr double dt = 0.01;       // 100 Hz control rate.
    constexpr int total_steps = 3000; // 30 simulated seconds.

    // Shared target speed, in metres per second.
    // Another thread could safely update this atomic value.
    boost::atomic<double> target_speed_mps{ 25.0 };

    PID pid;
    State vehicle{ 0.0, 0.0 }; // Initial position and speed.
    runge_kutta4<State> integrator;

    // Print an aligned telemetry table.
    std::cout << std::left
        << std::setw(10) << "Time (s)"
        << std::right
        << std::setw(14) << "Target (m/s)"
        << std::setw(14) << "Speed (m/s)"
        << std::setw(16) << "Position (m)"
        << std::setw(12) << "Throttle"
        << '\n';

    std::cout << std::string(66, '-') << '\n';
    std::cout << std::fixed << std::setprecision(2);

    bool target_changed = false;

    for (int step = 0; step < total_steps; ++step)
    {
        const double t = step * dt;

        // At 20 seconds, reduce the target speed to 18 m/s.
        if (t >= 20.0 && !target_changed)
        {
            target_speed_mps.store(
                18.0, boost::memory_order_relaxed);
            target_changed = true;
        }

        const double target =
            target_speed_mps.load(boost::memory_order_relaxed);

        // Compute throttle from the current speed error.
        const double throttle = pid.update(target, vehicle[1], dt);

        // Simulate a gradient section from 8 to 16 seconds.
        const double slope = (t >= 8.0 && t < 16.0) ? 0.3 : 0.0;

        // Integrate vehicle position and speed by one time step.
        VehicleDynamics dynamics{ throttle, slope };
        integrator.do_step(dynamics, vehicle, t, dt);

        // Display telemetry once per simulated second.
        if (step % 100 == 0)
        {
            std::cout << std::left
                << std::setw(10) << t
                << std::right
                << std::setw(14) << target
                << std::setw(14) << vehicle[1]
                << std::setw(16) << vehicle[0]
                << std::setw(12) << throttle
                << '\n';
        }
    }

    return 0;
}

Build with a C++17-capable compiler.

What to look for in the results:

  • From 0 to 8 seconds, throttle increases the vehicle’s speed toward 25 m/s.

  • From 8 to 16 seconds, uphill resistance increases, so the controller should maintain a higher throttle to compensate, though momentum is a factor.

  • At 20 seconds, the target drops to 18 m/s and there is no uphill. The controller reduces throttle, and the vehicle slows as drag and rolling resistance dissipate its momentum.

This is a simplified throttle-only controller, with no active braking, actuator delay, gear shifting, or traction limits.

Time (s)    Target (m/s)   Speed (m/s)    Position (m)    Throttle
------------------------------------------------------------------
0.00               25.00          0.03            0.00        1.00
1.00               25.00          2.03            1.03        0.63
2.00               25.00          3.93            4.02        0.58
3.00               25.00          5.67            8.83        0.54
4.00               25.00          7.27           15.31        0.50
5.00               25.00          8.74           23.33        0.46
6.00               25.00         10.08           32.75        0.43
7.00               25.00         11.32           43.46        0.40
8.00               25.00         12.41           55.34        0.37
9.00               25.00         10.77           66.92        0.42
10.00              25.00          9.26           76.92        0.46
11.00              25.00          7.88           85.48        0.49
12.00              25.00          6.61           92.72        0.52
13.00              25.00          5.45           98.74        0.55
14.00              25.00          4.39          103.65        0.58
15.00              25.00          3.41          107.54        0.60
16.00              25.00          2.54          110.50        0.62
17.00              25.00          4.40          113.98        0.57
18.00              25.00          6.10          119.24        0.53
19.00              25.00          7.67          126.14        0.49
20.00              18.00          9.09          134.53        0.00
21.00              18.00          9.85          144.01        0.26
22.00              18.00         10.55          154.21        0.25
23.00              18.00         11.18          165.08        0.23
24.00              18.00         11.77          176.56        0.21
25.00              18.00         12.30          188.59        0.20
26.00              18.00         12.79          201.14        0.19
27.00              18.00         13.23          214.15        0.18
28.00              18.00         13.64          227.59        0.17
29.00              18.00         14.01          241.42        0.16

Now, find the line:

const double slope = (t >= 8.0 && t < 16.0) ? 0.3 : 0.0;

and change it to:

const double slope = (t >= 8.0 && t < 16.0) ? -0.1 : 0.0;

A downhill slope. Now we are going to see why brakes are necessary:

Time (s)    Target (m/s)   Speed (m/s)    Position (m)    Throttle
------------------------------------------------------------------
0.00               25.00          0.03            0.00        1.00
1.00               25.00          2.03            1.03        0.63
2.00               25.00          3.93            4.02        0.58
3.00               25.00          5.67            8.83        0.54
4.00               25.00          7.27           15.31        0.50
5.00               25.00          8.74           23.33        0.46
6.00               25.00         10.08           32.75        0.43
7.00               25.00         11.32           43.46        0.40
8.00               25.00         12.45           55.34        0.37
9.00               25.00         14.41           68.79        0.32
10.00              25.00         16.19           84.10        0.28
11.00              25.00         17.82          101.12        0.24
12.00              25.00         19.31          119.70        0.20
13.00              25.00         20.66          139.70        0.17
14.00              25.00         21.89          160.99        0.14
15.00              25.00         23.01          183.45        0.11
16.00              25.00         24.02          206.98        0.08
17.00              25.00         24.02          231.00        0.08
18.00              25.00         24.02          255.01        0.08
19.00              25.00         24.02          279.03        0.08
20.00              18.00         24.02          303.05        0.00
21.00              18.00         23.74          326.93        0.00
22.00              18.00         23.46          350.53        0.00
23.00              18.00         23.19          373.86        0.00
24.00              18.00         22.92          396.91        0.00
25.00              18.00         22.66          419.70        0.00
26.00              18.00         22.39          442.22        0.00
27.00              18.00         22.13          464.49        0.00
28.00              18.00         21.88          486.49        0.00
29.00              18.00         21.63          508.24        0.00
Note

Check out the foot-off-the-throttle situation for the last 9 seconds!

Next Steps

A natural next step would be to add Boost.Units for dimensional checking and Boost.Statechart or Boost.Msm to model vehicle modes such as Cruising, HillClimb, Braking, and EmergencyStop.

Footnotes

(1) In the tractor example, “Control law — PID/model” means that the software controlling things such as hydraulic valves, blade position, engine speed, or vehicle speed could use either a PID controller, a model-based controller, or a combination of the two. Suppose the tractor’s blade is supposed to be at a particular height. The controller continuously compares desired blade height against actual blade height, the delta being treated as an error. A PID controller uses that error in three ways:

Part Meaning Intuition

P

Proportional

“How far away am I right now?”

I

Integral

“How long have I been wrong?”

D

Derivative

“How quickly am I approaching the target?”

The P component might say: "We’re 10 cm low, so raise the blade substantially" and as the blade gets closer: "We’re only 2 cm low, so raise it much more gently". The I component notices if, for example, the blade consistently stays 1 cm below the target because of hydraulic leakage or load. The D component notices that the blade is moving rapidly toward the target and reduces the command - to help prevent overshooting.