Chapter 11: Robot Software Architecture
Lesson 1: Layered Robotics Software Stacks
This lesson introduces the layered (or “stacked”) organization of robot software as a principled way to manage complexity, timing constraints, and hardware diversity. We develop a formal model of layers, quantify latency and stability effects across the stack, and show minimal multi-language implementations that reflect real robot systems.
1. Motivation and Big Picture
Robots tightly couple software with physical dynamics. Unlike pure software systems, a robot stack must simultaneously handle: (i) real-time sensing and actuation, (ii) noisy data streams, (iii) safety-critical failure modes, and (iv) heterogeneous hardware.
A layered software stack partitions responsibilities into levels of abstraction. Each layer exposes a stable interface upward and hides implementation details downward. This reduces coupling and allows teams to develop and verify modules independently.
flowchart TD
PHY["Physical robot (links, motors, sensors)"] --> DRV["Device drivers / firmware"]
DRV --> MID["Middleware (messages, time, parameters)"]
MID --> ALG["Algorithms (estimation, control, planning)"]
ALG --> APP["Applications (tasks, UI, autonomy)"]
APP --> ALG
ALG --> MID
MID --> DRV
Notice the bidirectional arrows: higher layers send commands downward; lower layers send measurements upward. Our objective is to understand what guarantees each layer should provide (e.g., timing, correctness, safety) so the whole system behaves predictably.
2. A Formal View: Layers as a Directed Graph
Let the software stack be a directed acyclic graph (DAG) \( \mathcal{G} = (\mathcal{V}, \mathcal{E}) \), where nodes are modules/layers and edges indicate allowed dependencies (calls, messages, or shared state).
We index layers from bottom to top: \( L_0 \) (hardware interface) up to \( L_N \) (application layer). A strict layered stack satisfies:
\[ (L_i, L_j) \in \mathcal{E} \;\Rightarrow\; j \le i+1 . \]
In words: a layer only depends on itself or the layer directly below it. Many real systems loosen this rule (creating “shortcuts”), but strict layering is the baseline for reasoning and verification.
Each layer \( L_i \) exports an interface set \( \mathcal{I}_i \) and implements a function \( f_i \) mapping inputs from below to outputs above:
\[ y_i(t) = f_i\!\big(x_i(t), \theta_i\big), \quad x_i(t) = y_{i-1}(t), \]
where \( \theta_i \) are internal parameters hidden from higher layers. The full stack composes these maps:
\[ y_N(t) = (f_N \circ f_{N-1} \circ \cdots \circ f_0)\, x_0(t). \]
Key principle: layering is valuable only if each interface \( \mathcal{I}_i \) remains stable while \( f_i \) evolves. This is what enables hardware swaps, algorithm upgrades, or application changes without rewriting the entire system.
3. Timing, Latency, and the Stack
A robot’s closed loop runs through multiple layers. Each layer introduces computation and communication delay. Let the delay of layer \( L_i \) be \( d_i \ge 0 \). The end-to-end sensing-to-actuation delay is:
\[ D \;=\; \sum_{i=0}^{N} d_i . \]
Proposition 1 (Additivity of Layer Delays): If data flows sequentially through layers, the total delay is the sum of per-layer delays.
Proof: Consider a sample that arrives at \( L_0 \) at time \( t \). It exits layer \( L_0 \) at \( t+d_0 \), exits layer \( L_1 \) at \( t+d_0+d_1 \), and so on. By induction, after layer \( L_k \) the time is \( t+\sum_{i=0}^{k} d_i \). Setting \( k=N \) yields the claim. ∎
Students familiar with linear control know delays reduce stability margins. Suppose a continuous-time plant \( \dot{\mathbf{x}} = \mathbf{A}\mathbf{x} + \mathbf{B}\mathbf{u} \) is controlled by a discrete-time controller with sampling period \( T \). Let the stack delay be \( D = mT \) for integer \( m \ge 0 \).
Zero-order hold discretization gives:
\[ \mathbf{x}[k+1] = \mathbf{A}_d \mathbf{x}[k] + \mathbf{B}_d \mathbf{u}[k-m], \]
with \( \mathbf{A}_d = e^{\mathbf{A}T} \) and \( \mathbf{B}_d = \int_0^T e^{\mathbf{A}\sigma}\mathbf{B}\, d\sigma \).
To analyze stability, define the augmented state \( \mathbf{z}[k] = \big[\mathbf{x}[k]^\top, \mathbf{u}[k-1]^\top, \dots, \mathbf{u}[k-m]^\top\big]^\top \). Then:
\[ \mathbf{z}[k+1] = \underbrace{\begin{bmatrix} \mathbf{A}_d & \mathbf{0} & \cdots & \mathbf{B}_d \\ \mathbf{0} & \mathbf{0} & \cdots & \mathbf{0} \\ \vdots & \vdots & \ddots & \vdots \\ \mathbf{0} & \mathbf{I} & \cdots & \mathbf{0} \end{bmatrix}}_{\mathbf{A}_{aug}} \mathbf{z}[k] \;+\; \underbrace{\begin{bmatrix} \mathbf{0} \\ \mathbf{I} \\ \mathbf{0} \\ \vdots \end{bmatrix}}_{\mathbf{B}_{aug}} \mathbf{u}[k]. \]
Proposition 2 (Delay-Stability Condition): Under state feedback \( \mathbf{u}[k]=\mathbf{K}\mathbf{x}[k] \), the delayed closed loop is stable iff all eigenvalues of \( \mathbf{A}_{aug} + \mathbf{B}_{aug}\tilde{\mathbf{K}} \) lie strictly inside the unit circle, where \( \tilde{\mathbf{K}}=[\mathbf{K}, \mathbf{0}, \dots, \mathbf{0}] \).
Proof sketch: The augmented system is an LTI discrete-time system. Standard discrete-time stability theory states stability is equivalent to spectral radius \( \rho(\cdot) < 1 \). Substituting the feedback law yields the closed-loop augmented matrix above. ∎
The engineering takeaway: layers that add delay (particularly middleware, serialization, network hops, or heavy perception) directly affect closed-loop stability. Therefore, layered design must accompany timing budgets.
flowchart TD
SENS["Sensors"] -->|d0| DRV["Driver"]
DRV -->|d1| MID["Middleware"]
MID -->|d2| CTRL["Control/Algo"]
CTRL -->|d3| ACT["Actuators"]
ACT --> PLANT["Plant dynamics"]
PLANT --> SENS
NOTE["Total delay D = d0+d1+d2+d3"]:::note
classDef note fill:#f7f7f7,stroke:#999,color:#111;
4. Typical Layers and Interface Contracts
A practical stack usually includes:
- Layer \(L_0\): Drivers / HAL — exposes hardware as typed signals (encoder counts, IMU frames, motor commands). Contract: deterministic timing, bounded noise, unit conventions.
- Layer \(L_1\): Middleware — routes data among components. Contract: message semantics, QoS, time synchronization.
- Layer \(L_2\): Algorithms — estimation and control logic. Contract: inputs/outputs in physical units, robustness to missing data.
- Layer \(L_3\): Applications — task logic, UI, autonomy scripts. Contract: safety constraints and operational goals.
We can express interface correctness formally. Let \( \mathcal{X}_i \) be admissible inputs to layer \( L_i \) and \( \mathcal{Y}_i \) be admissible outputs. An interface is correct if:
\[ \forall x \in \mathcal{X}_i,\;\; f_i(x,\theta_i) \in \mathcal{Y}_i . \]
In robotics, \( \mathcal{Y}_i \) often includes timing bounds: if a layer promises output period \( T_i \) with jitter \( \delta_i \), then:
\[ |t_{k+1} - t_k - T_i| \le \delta_i \quad \forall k . \]
These constraints are the mathematical core of “real-time” guarantees and show why the bottom layers must be engineered for determinism.
5. Minimal Layered Stack Example (Multi-Language)
We build a minimal four-layer stack around a 1-D motor with encoder: drivers read counts, middleware republishes state, algorithms compute a control law, and the app sets a reference. The focus is architecture, not motor physics.
5.1 Python (ROS2-style layering)
# L0: driver abstraction
class EncoderDriver:
def __init__(self, counts_per_rev=4096):
self.cpr = counts_per_rev
self.counts = 0
def read_counts(self):
return self.counts
def write_motor_pwm(self, pwm):
# placeholder for real hardware output
self.counts += int(0.1 * pwm)
# L1: middleware-like bus
class MessageBus:
def __init__(self):
self.subs = {}
def publish(self, topic, msg):
for cb in self.subs.get(topic, []):
cb(msg)
def subscribe(self, topic, cb):
self.subs.setdefault(topic, []).append(cb)
# L2: algorithm (P controller)
class PositionController:
def __init__(self, bus, kp=0.2):
self.bus = bus
self.kp = kp
self.ref = 0.0
bus.subscribe("ref", self.set_ref)
bus.subscribe("pos", self.on_pos)
def set_ref(self, r):
self.ref = r
def on_pos(self, pos):
u = self.kp * (self.ref - pos)
self.bus.publish("cmd_pwm", u)
# L3: application (sets reference)
class App:
def __init__(self, bus):
self.bus = bus
def step(self, t):
r = 10.0 if t > 1.0 else 0.0
self.bus.publish("ref", r)
# wire stack
drv = EncoderDriver()
bus = MessageBus()
ctrl = PositionController(bus)
app = App(bus)
# connect L0 <-> L1
bus.subscribe("cmd_pwm", drv.write_motor_pwm)
def driver_tick():
counts = drv.read_counts()
pos = counts / drv.cpr * 2.0 * 3.14159
bus.publish("pos", pos)
# simulate
dt = 0.01
t = 0.0
for k in range(400):
app.step(t)
driver_tick()
t += dt
This code mirrors a ROS2 stack: layered classes, message bus, and clear dependency direction.
5.2 C++ (interfaces and dependency direction)
#include <iostream>
#include <functional>
#include <unordered_map>
#include <vector>
// L1: minimal bus
class Bus {
public:
using Callback = std::function<void(double)>;
void publish(const std::string& topic, double msg) {
for (auto& cb : subs_[topic]) cb(msg);
}
void subscribe(const std::string& topic, Callback cb) {
subs_[topic].push_back(cb);
}
private:
std::unordered_map<std::string, std::vector<Callback>> subs_;
};
// L0: driver interface
class EncoderDriver {
public:
EncoderDriver(int cpr=4096): cpr_(cpr), counts_(0) {}
int readCounts() const { return counts_; }
void writePWM(double pwm){ counts_ += static_cast<int>(0.1*pwm); }
int cpr() const { return cpr_; }
private:
int cpr_, counts_;
};
// L2: controller
class PositionController {
public:
PositionController(Bus& bus, double kp=0.2): bus_(bus), kp_(kp), ref_(0.0) {
bus_.subscribe("ref", [&](double r){ ref_=r; });
bus_.subscribe("pos", [&](double p){ onPos(p); });
}
void onPos(double pos){
double u = kp_*(ref_ - pos);
bus_.publish("cmd_pwm", u);
}
private:
Bus& bus_; double kp_, ref_;
};
int main(){
EncoderDriver drv;
Bus bus;
PositionController ctrl(bus);
bus.subscribe("cmd_pwm", [&](double u){ drv.writePWM(u); });
double t=0.0, dt=0.01;
for(int k=0;k<400;k++){
double ref = (t>1.0)?10.0:0.0;
bus.publish("ref", ref);
int counts = drv.readCounts();
double pos = counts / double(drv.cpr()) * 2.0 * 3.14159;
bus.publish("pos", pos);
t += dt;
}
}
5.3 Java (layer packaging)
import java.util.*;
import java.util.function.Consumer;
// L1 bus
class Bus {
private Map<String, List<Consumer<Double>>> subs = new HashMap<>();
void publish(String topic, double msg){
subs.getOrDefault(topic, List.of()).forEach(cb -> cb.accept(msg));
}
void subscribe(String topic, Consumer<Double> cb){
subs.computeIfAbsent(topic, k -> new ArrayList<>()).add(cb);
}
}
// L0 driver
class EncoderDriver {
int cpr = 4096;
int counts = 0;
int readCounts(){ return counts; }
void writePWM(double pwm){ counts += (int)(0.1*pwm); }
}
// L2 controller
class PositionController {
Bus bus; double kp=0.2; double ref=0.0;
PositionController(Bus b){
bus=b;
bus.subscribe("ref", r -> ref=r);
bus.subscribe("pos", this::onPos);
}
void onPos(double pos){
double u = kp*(ref-pos);
bus.publish("cmd_pwm", u);
}
}
public class Main {
public static void main(String[] args){
EncoderDriver drv = new EncoderDriver();
Bus bus = new Bus();
new PositionController(bus);
bus.subscribe("cmd_pwm", drv::writePWM);
double t=0, dt=0.01;
for(int k=0;k<400;k++){
double ref = (t>1.0)?10.0:0.0;
bus.publish("ref", ref);
double pos = drv.readCounts()/(double)drv.cpr * 2*Math.PI;
bus.publish("pos", pos);
t += dt;
}
}
}
5.4 MATLAB/Simulink Conceptual Mapping
In MATLAB, layered design often appears as separate scripts/functions plus Simulink blocks. The middleware is typically ROS Toolbox or a custom message loop.
% L0: driver stub
function [counts, cpr] = read_encoder()
persistent c; if isempty(c), c=0; end
cpr = 4096;
counts = c;
end
function write_pwm(u)
persistent c; if isempty(c), c=0; end
c = c + int32(0.1*u);
end
% L2: controller
function u = p_controller(ref, pos, kp)
u = kp*(ref - pos);
end
% main (L3 app + L1 loop)
kp = 0.2; dt = 0.01; t = 0;
for k=1:400
if t > 1.0, ref = 10; else, ref = 0; end
[counts, cpr] = read_encoder();
pos = double(counts)/double(cpr)*2*pi;
u = p_controller(ref, pos, kp);
write_pwm(u);
t = t + dt;
end
In Simulink, this would correspond to blocks: Encoder Read (L0) → ROS Publish/Subscribe (L1) → Controller (L2) → PWM Output (L0), with a Step block as L3. We avoid images here, but you should recognize the same layered dependencies.
6. Problems and Solutions
Problem 1 (Layer Dependency Check): Consider layers \(L_0,L_1,L_2,L_3\). Suppose there is an edge \( (L_3,L_1) \). (a) Does this violate strict layering? (b) Give one practical reason it might still be allowed.
Solution:
(a) Yes. Strict layering requires dependencies only to the same or directly lower layer, so \( (L_3,L_1) \) skips \(L_2\). (b) A practical reason is performance: an application may directly request low-level telemetry for debugging or safety interlocks, reducing delay compared to routing through algorithms.
Problem 2 (Total Delay): A stack has delays \(d_0=1\) ms, \(d_1=3\) ms, \(d_2=5\) ms. Compute total delay and interpret its effect on feedback.
Solution:
\[ D = d_0+d_1+d_2 = 1+3+5 = 9\ \text{ms}. \]
This delay shifts the effective feedback measurement 9 ms into the past. If the plant has fast dynamics near this time scale, phase margin decreases and the controller may need retuning or a faster stack.
Problem 3 (Augmented Stability): A discretized plant is \( x[k+1]=a x[k] + b u[k-1] \) (one-step delay). Let \(u[k]=K x[k]\). Derive the augmented closed-loop matrix and the stability condition.
Solution:
Augmented state \( z[k]=[x[k],u[k-1]]^\top \). Then
\[ z[k+1]= \begin{bmatrix} a & b\\ K & 0 \end{bmatrix} z[k]. \]
Stability requires eigenvalues of \( \begin{bmatrix} a & b\\ K & 0\end{bmatrix} \) satisfy \( |\lambda|<1 \). The characteristic polynomial is
\[ \lambda^2 - a\lambda - bK = 0. \]
Choose \(K\) so both roots are inside the unit circle (students may apply Jury or direct root bounds).
Problem 4 (Interface Set): A driver exports position in radians and velocity in rad/s. An algorithm layer assumes degrees and deg/s. Formally state which interface correctness condition is violated.
Solution:
The admissible output set \( \mathcal{Y}_0 \) of the driver is “values expressed in SI units.” The algorithm’s input set \( \mathcal{X}_1 \) expects degrees. Thus there exists \( x\in\mathcal{X}_0 \) such that \( f_0(x)\notin\mathcal{X}_1 \), violating:
\[ \forall x \in \mathcal{X}_0,\; f_0(x)\in \mathcal{Y}_0 \subseteq \mathcal{X}_1 . \]
The fix is a unit-conversion adapter layer or a corrected interface contract.
Problem 5 (Timing Budget): A controller needs total delay \(D \le 4\) ms. Middleware and algorithm layers already cost \(d_1+d_2=3.2\) ms. What is the maximum allowed driver delay \(d_0\)?
Solution:
\[ d_0 \le 4 - 3.2 = 0.8\ \text{ms}. \]
This shows why low-level layers often run on MCUs or real-time OSes.
7. Summary
We modeled layered robot software stacks as directed dependency graphs and as compositional input/output maps. We proved delay additivity and connected stack latency to closed-loop stability through discrete-time augmented models. Finally, we implemented a minimal layered stack in Python, C++, Java, and MATLAB, emphasizing clean interfaces and dependency direction.
8. References
- Brooks, R.A. (1986). A robust layered control system for a mobile robot. IEEE Journal of Robotics and Automation, 2(1), 14–23.
- Albus, J.S. (1991). Outline for a theory of intelligence. IEEE Transactions on Systems, Man, and Cybernetics, 21(3), 473–509.
- Sangiovanni-Vincentelli, A., & Martin, G. (2001). Platform-based design and software layers. IEEE Design & Test of Computers, 18(6), 23–33.
- Sha, L., Rajkumar, R., & Lehoczky, J. (1990). Priority inheritance protocols: An approach to real-time synchronization. IEEE Transactions on Computers, 39(9), 1175–1185.
- Henzinger, T.A., Kopke, P.W., Puri, A., & Varaiya, P. (1995). What’s decidable about hybrid automata? Journal of Computer and System Sciences, 57(1), 94–124.
- Lee, E.A. (2008). Cyber physical systems: Design challenges. IEEE International Symposium on Object/Component/Service-Oriented Real-Time Distributed Computing, 363–369.