Chapter 11: Robot Software Architecture
Lesson 2: Drivers, Middleware, and Applications
This lesson formalizes the three core software layers that connect robot hardware to high-level behavior: drivers (hardware access and timing), middleware (communication and coordination), and applications (robot behaviors and user-level logic). We develop mathematical models of latency, jitter, and real-time schedulability, and illustrate how clean layer boundaries improve correctness, portability, and safety.
1. Layered View and Role of Each Layer
Building on the layered stack idea from Lesson 1, robot software can be idealized as a composition of three functional layers:
- Drivers: lowest-level software that talks directly to hardware interfaces (SPI/I2C/CAN/UART/EtherCAT), handles calibration constants, unit conversions, interrupts, and safety interlocks.
- Middleware: the “glue” that moves data between processes/machines and coordinates timing, discovery, serialization, and fault handling (e.g., publish–subscribe buses).
- Applications: task/behavior-level programs that combine perception, control, planning, and HRI into robot behavior. Applications should be hardware-agnostic to the extent possible.
flowchart TD
H["Hardware (sensors/actuators/buses)"] --> D["Drivers: read/write + timing"]
D --> M["Middleware: message bus + sync + discovery"]
M --> A["Applications: behaviors + supervision"]
A --> M
M --> D
Formally, we can model each layer as an input–output system. Let \( \mathbf{u}_h(t) \) be hardware-level signals (voltages, counts), and \( \mathbf{x}(t) \) be software-level states exposed upward. A driver implements an abstraction map \( \mathcal{D}: \mathbf{u}_h \mapsto \mathbf{x} \). Middleware implements a transport/coordination operator \( \mathcal{M} \), and the application implements a policy \( \mathcal{A} \). The closed robot software loop is
\[ \mathbf{x}(t)=\mathcal{D}\!\left(\mathbf{u}_h(t)\right), \quad \hat{\mathbf{x}}(t)=\mathcal{M}\!\left(\mathbf{x}(t)\right), \quad \mathbf{u}_h(t)=\mathcal{A}\!\left(\hat{\mathbf{x}}(t)\right). \]
The design principle is: drivers must know hardware; applications must not. Middleware is the contract between the two.
2. Drivers: Deterministic Hardware Abstractions
Drivers typically include: (i) bus protocol handling, (ii) interrupt service routines (ISR), (iii) filtering and unit conversion, (iv) device state machines, and (v) safety checks. For a sensor producing raw samples \( s_k \), a driver often exposes an estimate \( y_k \) via a calibrated affine map \( y_k = \alpha s_k + \beta \).
If sampling is periodic with period \( T_s \), a driver can be modeled as a discrete-time system:
\[ y_k = g(s_k),\quad k\in\mathbb{Z}_{\ge 0}, \]
where \( g(\cdot) \) includes calibration and optional filtering. Example: a first-order low-pass digital filter in a driver,
\[ y_k = (1-\lambda) y_{k-1} + \lambda s_k,\quad 0 < \lambda \le 1. \]
This is stable for any \( 0 < \lambda \le 1 \) because the homogeneous equation \( y_k = (1-\lambda) y_{k-1} \) has solution \( y_k = (1-\lambda)^k y_0 \) and \( |1-\lambda| < 1 \). Thus \( y_k \to 0 \) as \( k\to\infty \), guaranteeing BIBO stability.
Drivers must also enforce hard timing. Let a driver task have worst-case execution time \( C_d \) and period \( T_d \). Its CPU utilization is \( U_d = C_d/T_d \). Since drivers are close to hardware, they are usually assigned the highest real-time priority.
3. Middleware: Message Passing, Latency, and Jitter
Middleware provides a structured way to move data between software components. Even without naming a specific framework, most robot middleware offers:
- Publish–subscribe transport for streaming sensor data,
- Request–reply for configuration/queries,
- Time synchronization and buffering,
- Serialization and type checking,
- Fault isolation and restarts.
A key mathematical issue is communication delay. Suppose a driver publishes messages at rate \( \lambda \) (messages/sec) into a middleware queue with service rate \( \mu \). A standard first approximation is an M/M/1 queue, giving mean waiting time
\[ W = \frac{1}{\mu-\lambda},\quad \text{valid when } 0\le \lambda < \mu. \]
Derivation sketch. In steady state, the probability that the system has \( n \) messages is geometric: \( P(N=n)=(1-\rho)\rho^n \), where \( \rho=\lambda/\mu \). Hence \( \mathbb{E}[N]=\rho/(1-\rho)=\lambda/(\mu-\lambda) \). By Little’s law \( \mathbb{E}[N]=\lambda W \), so \( W=\mathbb{E}[N]/\lambda = 1/(\mu-\lambda) \).
Latency is not constant. Let the middleware add delay \( d_m \) with jitter (random deviation) \( \delta_m \) satisfying \( |\delta_m| \le j_m \). If the driver adds delay \( d_d \) and the application adds \( d_a \), total end-to-end delay is
\[ D = d_d + d_m + d_a,\qquad J = j_d + j_m + j_a. \]
In control loops (which you know from Linear Control), large \( D \) and \( J \) reduce stability margins. Therefore a major middleware design goal is to bound delay/jitter, not merely minimize average latency.
4. Applications: Composition and Real-Time Scheduling
Applications fuse sensing and actuation into behaviors. Even “simple” robot applications are typically multiple periodic tasks: a sensor read task, a state-estimate task, a controller task, and a logging/user task. Let there be \( n \) periodic tasks with execution times \( C_i \) and periods \( T_i \). Total utilization is \( U=\sum_{i=1}^n C_i/T_i \).
Under rate-monotonic scheduling (RMS) (shorter period = higher priority), a classical sufficient condition for schedulability is
\[ U \le n\left(2^{1/n}-1\right). \]
Proof sketch (Liu–Layland bound). Consider the worst case where tasks are released simultaneously. RMS is optimal among fixed-priority schedulers for periodic tasks with deadlines equal to periods. By bounding the worst-case processor demand over any interval and applying induction on \( n \), one obtains a sufficient utilization limit equal to \( U_n = n(2^{1/n}-1) \). The bound is conservative but guarantees no missed deadlines.
For large \( n \), the bound approaches \( \ln 2 \approx 0.693 \). Hence, if your combined driver+middleware+application real-time tasks exceed ~69% CPU utilization, missed deadlines become likely unless more advanced scheduling is used.
5. Implementation Examples Across Languages
The goal here is not to implement a full framework, but to show the “shape” of each layer and how they connect.
5.1 Python Example (Driver + Lightweight Middleware + App)
Python is often used for prototyping applications and some higher-level
drivers. Common robotics-related libraries include
pyserial, python-can, numpy, and
scipy.
import time
import numpy as np
import serial # pyserial driver
# ---------------- Driver Layer ----------------
class IMUDriver:
def __init__(self, port="/dev/ttyUSB0", baud=115200, alpha=1.0, beta=0.0):
self.ser = serial.Serial(port, baudrate=baud, timeout=0.1)
self.alpha = alpha
self.beta = beta
self.y_prev = 0.0
self.lam = 0.2 # filter coefficient (0 < lam <= 1)
def read_raw(self):
line = self.ser.readline().decode().strip()
# raw sample s_k as float; robust parsers are recommended in real drivers
return float(line) if line else None
def read(self):
s_k = self.read_raw()
if s_k is None:
return None
# calibration: y = alpha s + beta
y_k = self.alpha * s_k + self.beta
# simple low-pass filtering
y_k = (1 - self.lam) * self.y_prev + self.lam * y_k
self.y_prev = y_k
return y_k
# ---------------- Middleware Layer (Pub/Sub) ----------------
# ultra-lightweight local pub/sub using a shared buffer
class Topic:
def __init__(self):
self.msg = None
self.t = None
def publish(self, msg):
self.msg = msg
self.t = time.time()
def latest(self):
return self.msg, self.t
imu_topic = Topic()
# ---------------- Application Layer ----------------
def control_application():
# pretend we are doing a proportional controller u = -K y
K = 0.8
while True:
y, t_msg = imu_topic.latest()
if y is not None:
u = -K * y
print(f"[APP] y={y:.3f}, u={u:.3f}, age={time.time()-t_msg:.3f}s")
time.sleep(0.01)
# wiring layers together
imu = IMUDriver()
def driver_task():
while True:
y = imu.read()
if y is not None:
imu_topic.publish(y)
time.sleep(0.005) # T_d = 5 ms
# In practice, run driver_task and control_application in separate threads/processes.
5.2 C++ Example (Hardware Driver Skeleton)
C++ is standard for low-level drivers and high-performance middleware.
Frequently used robotics libraries: Eigen for linear
algebra, Linux SocketCAN for CAN buses,
boost::asio for async I/O.
#include <iostream>
#include <chrono>
#include <thread>
#include <cmath>
// ---------------- Driver Layer ----------------
class EncoderDriver {
public:
EncoderDriver(double counts_per_rev, double alpha=1.0, double beta=0.0)
: cpr_(counts_per_rev), alpha_(alpha), beta_(beta), y_prev_(0.0), lam_(0.1) {}
// mock raw read from bus
int read_raw_counts() {
static int c = 0;
return c++ % 4096; // pretend counts arrive from SPI/CAN
}
double read_radians() {
int s_k = read_raw_counts();
double y_k = alpha_ * s_k + beta_; // calibration
y_k = (1.0 - lam_) * y_prev_ + lam_ * y_k; // filtering
y_prev_ = y_k;
// convert counts to radians
return (2.0 * M_PI * y_k) / cpr_;
}
private:
double cpr_, alpha_, beta_;
double y_prev_, lam_;
};
int main() {
EncoderDriver enc(4096.0);
const int Td_ms = 2;
while(true){
double theta = enc.read_radians();
std::cout << "[DRV] theta=" << theta << " rad\n";
std::this_thread::sleep_for(std::chrono::milliseconds(Td_ms));
}
}
5.3 Java Example (Application Using Middleware Abstraction)
Java is less common for drivers, but useful for UI, supervision, and some applications. Robotics-related options include Java DDS clients or ROSJava (details later in Chapter 12).
import java.util.concurrent.atomic.AtomicReference;
// ---------------- Middleware Abstraction ----------------
class Topic<T> {
private final AtomicReference<T> msg = new AtomicReference<>();
private volatile long tNano = 0;
public void publish(T m){
msg.set(m);
tNano = System.nanoTime();
}
public T latest(){
return msg.get();
}
public double ageSeconds(){
return (System.nanoTime() - tNano) * 1e-9;
}
}
// ---------------- Application Layer ----------------
public class RobotApp {
static Topic<Double> imuTopic = new Topic<>();
public static void main(String[] args) throws Exception {
double K = 0.5;
while(true){
Double y = imuTopic.latest();
if(y != null){
double u = -K * y;
System.out.println("[APP] y=" + y + " u=" + u
+ " age=" + imuTopic.ageSeconds() + "s");
}
Thread.sleep(10);
}
}
}
5.4 MATLAB/Simulink Example (Layered Blocks via Script)
MATLAB is widely used for modeling and rapid prototyping. A common pattern is to implement drivers as S-Functions and middleware as Simulink buses or UDP blocks, while applications are control/state blocks.
%% ---------------- Driver Layer (mock) ----------------
% raw sensor sample s_k
s_k = randn();
% calibration
alpha = 1.2; beta = -0.05;
y_k = alpha*s_k + beta;
% low-pass filtering in driver
lambda = 0.2;
persistent y_prev
if isempty(y_prev), y_prev = 0; end
y_k = (1-lambda)*y_prev + lambda*y_k;
y_prev = y_k;
%% ---------------- Middleware Layer (bus-like) ----------------
% Here we emulate middleware by packaging data into a struct.
msg.data = y_k;
msg.timestamp = tic; % local timebase
%% ---------------- Application Layer ----------------
K = 0.8;
u = -K * msg.data; % proportional control policy
fprintf('[APP] y=%.3f, u=%.3f\n', msg.data, u);
In Simulink, the same structure appears as: Driver block → Bus/Transport block → Application block. We avoid detailed Simulink diagrams here, but you should recognize the three-layer separation.
6. Problems and Solutions
Problem 1 (End-to-End Delay): A sensor driver adds a deterministic delay of \( d_d=2 \) ms. Middleware adds \( d_m=6 \) ms with jitter bound \( j_m=1 \) ms. The application adds \( d_a=3 \) ms with jitter bound \( j_a=0.5 \) ms. Compute total delay and jitter bounds.
Solution:
\[ D = d_d + d_m + d_a = 2 + 6 + 3 = 11\text{ ms}, \qquad J = j_d + j_m + j_a \approx 0 + 1 + 0.5 = 1.5\text{ ms}. \]
Thus any message can be delayed between about \( 11 \pm 1.5 \) ms (ignoring rare OS spikes).
Problem 2 (Queue Stability): A middleware queue is approximated as M/M/1 with \( \mu = 250 \) msg/s. If the driver publishes at \( \lambda = 200 \) msg/s, compute the mean waiting time. Is the queue stable?
Solution:
\[ \lambda < \mu \Rightarrow \text{stable}. \quad W = \frac{1}{\mu-\lambda} = \frac{1}{250-200} = \frac{1}{50} = 0.02\text{ s}. \]
Mean waiting time is about \( 20 \) ms; rising \( \lambda \) closer to \( \mu \) would blow up latency.
Problem 3 (RMS Schedulability): Three periodic tasks run on a robot CPU: (i) driver ISR: \( C_1=0.5 \) ms, \( T_1=5 \) ms; (ii) middleware comm: \( C_2=1 \) ms, \( T_2=10 \) ms; (iii) application control loop: \( C_3=2 \) ms, \( T_3=20 \) ms. Check the Liu–Layland bound.
Solution:
\[ U = \frac{0.5}{5} + \frac{1}{10} + \frac{2}{20} = 0.1 + 0.1 + 0.1 = 0.3. \]
\[ U_3 = 3\left(2^{1/3}-1\right) \approx 3(1.2599-1) \approx 0.7797. \]
Since \( U=0.3 \le U_3 \), the tasks are schedulable under RMS.
Problem 4 (Driver Filter Stability): Consider the driver filter \( y_k = (1-\lambda)y_{k-1} + \lambda s_k \). Prove BIBO stability for \( 0 < \lambda \le 1 \).
Solution:
The homogeneous response satisfies \( y_k = (1-\lambda)y_{k-1} \), giving \( y_k = (1-\lambda)^k y_0 \). Since \( 0 < \lambda \le 1 \Rightarrow |1-\lambda| < 1 \), the homogeneous term decays to zero. The forced response is a bounded convolution of a decaying kernel with bounded input \( s_k \), hence bounded. Therefore the filter is BIBO stable.
7. Summary
Drivers provide deterministic, safety-aware access to hardware and enforce local timing; middleware structures system-wide data movement and synchronization; applications assemble robot behaviors on top of these abstractions. We modeled latency and jitter across layers, used queueing theory to reason about transport delay, and applied real-time schedulability bounds to layered robot tasks. These ideas prepare us for explicit data-flow and message-passing patterns in Lesson 3.
8. References
- Liu, C.L., & Layland, J.W. (1973). Scheduling algorithms for multiprogramming in a hard-real-time environment. Journal of the ACM, 20(1), 46–61.
- Sha, L., Rajkumar, R., & Lehoczky, J.P. (1990). Priority inheritance protocols: an approach to real-time synchronization. IEEE Transactions on Computers, 39(9), 1175–1185.
- Kopetz, H. (1997). Event-triggered versus time-triggered real-time systems. Operating Systems of the 90s and Beyond, Lecture Notes in Computer Science, 563, 87–101.
- Leveson, N.G. (2011). Engineering a safer world: Systems thinking applied to safety. Foundations and Trends in Systems and Control, 1(1), 1–83.
- Lee, E.A. (2008). Cyber physical systems: Design challenges. Proceedings of the 11th IEEE Symposium on Object Oriented Real-Time Distributed Computing, 363–369.
- Rajkumar, R., Lee, I., Sha, L., & Stankovic, J. (2010). Cyber-physical systems: The next computing revolution. Proceedings of the 47th Design Automation Conference, 731–736.