1427 lines
24 KiB
Markdown
1427 lines
24 KiB
Markdown
# Rust on Robots
|
|
## Hands-on Embedded Rust on STM32
|
|
|
|
**Duration:** 120 minutes
|
|
**Participants:** ~20
|
|
|
|
---
|
|
# Today is about robot firmware architecture
|
|
|
|
We use Rust to understand how robot software is structured:
|
|
|
|
```text
|
|
hardware signal
|
|
|
|
|
software type
|
|
|
|
|
robot event
|
|
|
|
|
robot state
|
|
|
|
|
decision
|
|
|
|
|
actuator command
|
|
```
|
|
|
|
---
|
|
# We will not build a full robot in two hours
|
|
|
|
We do not have:
|
|
|
|
- full chassis
|
|
- motors
|
|
- motor drivers
|
|
- camera module
|
|
- ultrasonic hardware
|
|
- playing field
|
|
|
|
---
|
|
# We will build a robot framework
|
|
We do have:
|
|
|
|
- STM32 Blue Pill
|
|
- ST-Link programmer
|
|
- RGB LED
|
|
- 5-way button
|
|
- Rust
|
|
- a robot control model
|
|
|
|
=> The STM32 board is our small, reduced robot.
|
|
|
|
---
|
|
# The workshop target
|
|
|
|
By the end, we want this model:
|
|
|
|
```text
|
|
button / sensor input
|
|
|
|
|
RobotEvent
|
|
|
|
|
RobotMode
|
|
|
|
|
MotionCommand
|
|
|
|
|
LED status or wheel-speed model
|
|
```
|
|
|
|
This is the same shape as a real robot controller.
|
|
|
|
Different hardware. Same architecture.
|
|
|
|
---
|
|
# Room calibration
|
|
|
|
1. Who has written Rust before?
|
|
2. Who has programmed a microcontroller before?
|
|
3. Who has built, programmed or broken a robot before?
|
|
|
|
---
|
|
# Get the Repo
|
|
|
|
Code along and try out yourself
|
|
|
|
`git.wieerwill.dev/wieerwill/rust-stm32`
|
|
|
|
1. Clone it
|
|
2. install RustUp or Nix shell
|
|
3. try it out (more in Chapter 0)
|
|
|
|
---
|
|
# My RoboCup Junior Soccer journey
|
|
A soccer robot starts simple:
|
|
|
|
```text
|
|
turn on
|
|
|
|
|
show status
|
|
|
|
|
wait for start
|
|
|
|
|
find the ball
|
|
|
|
|
drive toward the ball
|
|
|
|
|
avoid problems
|
|
|
|
|
recover and continue
|
|
```
|
|
|
|
---
|
|
# A (soccer) robot is a loop with consequences
|
|
```text
|
|
observe
|
|
|
|
|
interpret
|
|
|
|
|
decide
|
|
|
|
|
move
|
|
|
|
|
observe again
|
|
```
|
|
|
|
The hard part is keeping this loop understandable when reality becomes messy.
|
|
|
|
---
|
|
# Robot software touches the real world
|
|
|
|
A robot has messy inputs:
|
|
|
|
- camera frame
|
|
- ball position
|
|
- line / field detection
|
|
- ultrasonic distance
|
|
- start button
|
|
- battery state
|
|
- motor feedback
|
|
|
|
And physical outputs:
|
|
|
|
- wheel speeds
|
|
- LED status
|
|
- logs
|
|
- stop behaviour
|
|
- recovery behaviour
|
|
|
|
---
|
|
# The board is our reduced robot
|
|
|
|
```text
|
|
RGB LED -> actuator / status output
|
|
5-way button -> input device / fake sensor rig
|
|
STM32 loop -> robot control loop
|
|
Rust types -> domain model
|
|
```
|
|
|
|
Small hardware. Real architecture.
|
|
|
|
---
|
|
# Chapter map
|
|
|
|
```text
|
|
00_check toolchain check
|
|
01_rust_basics values, functions, mutation
|
|
02_ownership ownership and borrowing
|
|
03_types domain types, enums, Result
|
|
04_first_firmware minimal STM32 firmware
|
|
05_rgb_output LED as actuator
|
|
06_button_events button as sensor
|
|
07_events raw input to robot event
|
|
08_state_machine robot behaviour
|
|
09_sensor_pipeline camera and ultrasonic model
|
|
10_motor_commands motion intent to wheel speeds
|
|
11_kinematics geometry and inverse kinematics
|
|
12_embassy async Embassy loop and timers
|
|
```
|
|
|
|
---
|
|
# Chapter 0: Toolchain
|
|
---
|
|
# Chapter 0: Toolchain check
|
|
|
|
Open: `src/bin/00_check.rs`
|
|
|
|
Run: `run-00`
|
|
|
|
Expected result:
|
|
```text
|
|
The host program prints the workshop toolchain check.
|
|
```
|
|
|
|
---
|
|
# Why Rust for this?
|
|
|
|
- Who owns this peripheral?
|
|
- Who may mutate robot state?
|
|
- What happens when a sensor fails?
|
|
- Which commands are valid?
|
|
- Which states should be impossible?
|
|
- Where does hardware end and behaviour begin?
|
|
|
|
Rust makes many of those boundaries visible.
|
|
|
|
---
|
|
# Rust is strict in useful places
|
|
- memory safety without a garbage collector
|
|
- explicit ownership
|
|
- controlled mutation
|
|
- strong enums and pattern matching
|
|
- `Option` and `Result`
|
|
- small `no_std` programs for microcontrollers
|
|
|
|
---
|
|
# But Rust does not fix everything
|
|
- wrong wiring
|
|
- noisy sensors
|
|
- weak batteries
|
|
- bad control logic
|
|
- missing calibration
|
|
|
|
---
|
|
# Chapter 01: Values
|
|
---
|
|
# Chapter 01: Values are explicit
|
|
|
|
```rust
|
|
let speed = 42;
|
|
let mut target_speed = 0;
|
|
|
|
target_speed = speed;
|
|
```
|
|
|
|
Default: values are immutable.
|
|
Mutation must be requested with `mut`.
|
|
|
|
For robot code, that is a good default.
|
|
|
|
---
|
|
# Robot translation: Mutation should have a home
|
|
Bad:
|
|
```text
|
|
everything can change everything
|
|
```
|
|
|
|
Better:
|
|
```text
|
|
sensor reading
|
|
|
|
|
event
|
|
|
|
|
state transition
|
|
|
|
|
command
|
|
```
|
|
|
|
The robot state changes in one obvious place.
|
|
|
|
---
|
|
# Chapter 01 exercise: Change behaviour without changing structure
|
|
Open:
|
|
```text
|
|
src/bin/01_rust_basics.rs
|
|
```
|
|
|
|
Tasks:
|
|
1. Change a constant.
|
|
2. Change a function return value.
|
|
3. Add a new status value.
|
|
4. Run the program.
|
|
|
|
Checkpoint:
|
|
```text
|
|
run-01
|
|
```
|
|
|
|
---
|
|
# Chapter 02: Ownership
|
|
---
|
|
# Chapter 02: Ownership means responsibility
|
|
```rust
|
|
let command = String::from("forward");
|
|
let next_command = command;
|
|
// command is no longer usable here
|
|
```
|
|
|
|
One value has one owner.
|
|
When ownership moves, responsibility moves.
|
|
|
|
---
|
|
# Robot translation: Peripherals are resources
|
|
A GPIO pin is not just an integer.
|
|
|
|
It represents access to real hardware.
|
|
|
|
```text
|
|
LED pin owner
|
|
|
|
|
configure output
|
|
|
|
|
set high / low
|
|
```
|
|
|
|
Ownership makes accidental sharing harder.
|
|
|
|
---
|
|
# Borrowing means temporary access
|
|
```rust
|
|
fn print_command(command: &str) {
|
|
println!("command = {command}");
|
|
}
|
|
|
|
let command = String::from("forward");
|
|
print_command(&command);
|
|
|
|
// command is still usable here
|
|
```
|
|
|
|
A borrow can inspect without taking ownership.
|
|
|
|
---
|
|
# Mutable borrowing: One writer at a time
|
|
```rust
|
|
fn stop(command: &mut MotionCommand) {
|
|
command.forward = 0.0;
|
|
command.strafe = 0.0;
|
|
command.rotate = 0.0;
|
|
}
|
|
```
|
|
|
|
Rust does not like hidden shared mutation.
|
|
|
|
For robot code, that is useful.
|
|
|
|
---
|
|
# Robot translation: Who may change the robot state?
|
|
A robot state should not be mutated from everywhere.
|
|
|
|
Better:
|
|
```text
|
|
sensor module -> creates observations
|
|
event module -> creates RobotEvent
|
|
state machine -> changes RobotMode
|
|
command module -> creates MotionCommand
|
|
hardware module -> applies output
|
|
```
|
|
|
|
Each layer has a job.
|
|
|
|
---
|
|
# Chapter 02 exercise: Fix the ownership error
|
|
Open:
|
|
```text
|
|
src/bin/02_ownership.rs
|
|
```
|
|
|
|
Tasks:
|
|
1. Run it.
|
|
2. Read the compiler error.
|
|
3. Fix it with a borrow.
|
|
4. Add a mutable command update.
|
|
|
|
Checkpoint:
|
|
```text
|
|
run-02
|
|
```
|
|
|
|
---
|
|
# Chapter 03: Types
|
|
---
|
|
# Chapter 03: Numbers are not enough
|
|
```rust
|
|
let value = 42;
|
|
```
|
|
|
|
What is it?
|
|
- centimetres?
|
|
- percent?
|
|
- PWM duty cycle?
|
|
- motor speed?
|
|
- battery voltage?
|
|
- camera pixel coordinate?
|
|
|
|
Robot code needs meaning, not just numbers.
|
|
|
|
---
|
|
# Types are safety rails
|
|
```rust
|
|
struct DistanceCm(u16);
|
|
struct MotorPower(f32);
|
|
struct BallXPixel(u16);
|
|
struct BatteryMillivolts(u16);
|
|
```
|
|
|
|
A number alone does not tell us what it means.
|
|
|
|
Types carry meaning.
|
|
|
|
---
|
|
# Enums model robot reality better than booleans
|
|
```rust
|
|
enum BallObservation {
|
|
NotSeen,
|
|
Left,
|
|
Center,
|
|
Right,
|
|
TooClose,
|
|
}
|
|
```
|
|
|
|
This is clearer than:
|
|
|
|
```rust
|
|
ball_seen: bool
|
|
ball_left: bool
|
|
ball_right: bool
|
|
```
|
|
|
|
Because impossible combinations disappear.
|
|
|
|
---
|
|
# Failure is part of the API
|
|
|
|
```rust
|
|
enum SensorError {
|
|
Timeout,
|
|
OutOfRange,
|
|
NotCalibrated,
|
|
}
|
|
|
|
fn read_distance() -> Result<DistanceCm, SensorError> {
|
|
todo!()
|
|
}
|
|
```
|
|
|
|
A sensor may fail.
|
|
|
|
The type should admit that.
|
|
|
|
---
|
|
# Robot translation: Sensor data is evidence
|
|
A sensor reading is not behaviour.
|
|
|
|
```text
|
|
raw echo time
|
|
|
|
|
distance
|
|
|
|
|
classification
|
|
|
|
|
RobotEvent
|
|
|
|
|
state transition
|
|
```
|
|
|
|
The robot should not directly react to raw numbers.
|
|
|
|
---
|
|
# Chapter 03 exercise: Replace loose values with domain types
|
|
Open:
|
|
```text
|
|
src/bin/03_types.rs
|
|
```
|
|
|
|
Tasks:
|
|
1. Replace a raw distance number with `DistanceCm`.
|
|
2. Replace booleans with `BallObservation`.
|
|
3. Return `Result` from a fake sensor function.
|
|
4. Match on the result.
|
|
|
|
Checkpoint:
|
|
```text
|
|
run-03
|
|
```
|
|
|
|
---
|
|
# Chapter 04: Embedded Rust
|
|
---
|
|
# Chapter 04: First embedded Rust program
|
|
On the STM32 we usually do not have:
|
|
- an operating system
|
|
- heap by default
|
|
- files
|
|
- a normal terminal
|
|
- `std`
|
|
|
|
We write `no_std` firmware.
|
|
|
|
---
|
|
# Bare metal means we own the loop
|
|
```text
|
|
configure hardware
|
|
|
|
|
enter loop
|
|
|
|
|
read input
|
|
|
|
|
update state
|
|
|
|
|
write output
|
|
|
|
|
repeat
|
|
```
|
|
|
|
That loop is the beginning of a robot controller.
|
|
|
|
---
|
|
# Firmware is not an app with a main window
|
|
On a microcontroller:
|
|
- startup code prepares the chip
|
|
- firmware configures peripherals
|
|
- the loop runs forever
|
|
- timing is explicit
|
|
- debugging is limited
|
|
- output may be one LED
|
|
|
|
---
|
|
# Tooling checkpoint
|
|
Open:
|
|
```text
|
|
src/bin/04_first_firmware.rs
|
|
```
|
|
|
|
Build:
|
|
```text
|
|
build-04
|
|
```
|
|
|
|
Flash:
|
|
```text
|
|
run-04
|
|
```
|
|
|
|
Expected result: The board boots and reaches the firmware loop.
|
|
|
|
---
|
|
# Hardware silence is a valid embedded state
|
|
If nothing happens, debug in layers:
|
|
1. Is the board powered?
|
|
2. Is the ST-Link visible?
|
|
3. Is the chip target correct?
|
|
4. Did the firmware flash?
|
|
5. Is the pin correct?
|
|
6. Is the LED orientation correct?
|
|
|
|
Do not randomly change five things at once!
|
|
|
|
---
|
|
# Embedded debugging is layered
|
|
1. host build works?
|
|
2. target build works?
|
|
3. probe visible?
|
|
4. flash succeeds?
|
|
5. firmware boots?
|
|
6. pin toggles?
|
|
7. external wiring works?
|
|
|
|
Debug from the outside in.
|
|
|
|
---
|
|
# Chapter 05: Output
|
|
---
|
|
# Chapter 05: Output - LED as actuator
|
|
An LED is a tiny actuator.
|
|
|
|
It gives us visible robot state:
|
|
|
|
```text
|
|
Off -> idle
|
|
Blue blink -> searching
|
|
Green -> driving
|
|
Red blink -> obstacle / error
|
|
```
|
|
|
|
The LED becomes the robot dashboard.
|
|
|
|
---
|
|
# Robot translation: Status output matters
|
|
A robot needs visible state because:
|
|
- it may not have a screen
|
|
- logs may be unavailable
|
|
- behaviour may look ambiguous
|
|
- debugging happens on the floor
|
|
- humans need to know if it is safe
|
|
|
|
A status LED is not decoration.
|
|
|
|
---
|
|
# Chapter 05 exercise: Status output
|
|
Open:
|
|
```text
|
|
src/bin/05_rgb_output.rs
|
|
```
|
|
|
|
Tasks:
|
|
1. Make the LED blink.
|
|
2. Change the colour.
|
|
3. Add a status pattern.
|
|
4. Use one pattern for `Idle`.
|
|
5. Use one pattern for `Searching`.
|
|
|
|
Checkpoint:
|
|
```text
|
|
run-05
|
|
```
|
|
|
|
---
|
|
# Chapter 06: Input
|
|
---
|
|
# Chapter 06: Input - button as sensor
|
|
The 5-way button becomes our fake sensor rig.
|
|
|
|
```text
|
|
Center -> start / stop
|
|
Left -> ball left
|
|
Right -> ball right
|
|
Up -> ball centered
|
|
Down -> obstacle detected
|
|
```
|
|
|
|
This lets us simulate camera or ultrasonic input.
|
|
|
|
---
|
|
# Raw input is not robot meaning
|
|
Button press:
|
|
```text
|
|
raw electrical state
|
|
```
|
|
|
|
Robot event:
|
|
```text
|
|
BallLeft
|
|
ObstacleDetected
|
|
StartStop
|
|
```
|
|
|
|
We separate hardware reading from robot meaning.
|
|
|
|
---
|
|
# Robot translation: Perception is a boundary
|
|
The controller should not care whether `BallLeft` came from:
|
|
- a camera
|
|
- a test file
|
|
- a simulator
|
|
- a button
|
|
- a future ML model
|
|
|
|
It only needs the event.
|
|
|
|
---
|
|
# Chapter 06 exercise: Button to event
|
|
Open:
|
|
```text
|
|
src/bin/06_button_events.rs
|
|
```
|
|
|
|
Tasks:
|
|
1. Read the 5-way button.
|
|
2. Convert direction to `RobotEvent`.
|
|
3. Show the event with LED status or logging.
|
|
4. Add a default `NoEvent` case.
|
|
|
|
Checkpoint:
|
|
```text
|
|
run-06
|
|
```
|
|
|
|
---
|
|
# Chapter 07: Events
|
|
---
|
|
# Chapter 07: Events
|
|
```rust
|
|
enum RobotEvent {
|
|
NoEvent,
|
|
StartStop,
|
|
BallLeft,
|
|
BallRight,
|
|
BallCentered,
|
|
BallLost,
|
|
ObstacleDetected,
|
|
Timeout,
|
|
}
|
|
```
|
|
|
|
Events are interpreted facts.
|
|
|
|
They are not raw hardware.
|
|
|
|
---
|
|
# Event priority matters
|
|
What should win?
|
|
```text
|
|
BallCentered + ObstacleDetected
|
|
```
|
|
|
|
Probably not:
|
|
```text
|
|
drive forward
|
|
```
|
|
|
|
Some events are safety-critical.
|
|
|
|
---
|
|
# Chapter 07 exercise: Add event priority
|
|
Open:
|
|
```text
|
|
src/bin/07_events.rs
|
|
```
|
|
|
|
Tasks:
|
|
1. Convert button direction to `RobotEvent`.
|
|
2. Add a function `prioritize_event`.
|
|
3. Ensure `ObstacleDetected` wins.
|
|
4. Keep `NoEvent` harmless.
|
|
|
|
Checkpoint:
|
|
```text
|
|
test-07
|
|
```
|
|
|
|
---
|
|
# Chapter 08: State Machine
|
|
---
|
|
# Chapter 08: Robot state machine
|
|
The robot needs memory.
|
|
|
|
```rust
|
|
enum RobotMode {
|
|
Idle,
|
|
SearchBall,
|
|
AlignToBall,
|
|
DriveToBall,
|
|
AvoidObstacle,
|
|
Error,
|
|
}
|
|
```
|
|
|
|
It must know what it is currently trying to do.
|
|
|
|
---
|
|
# State is not the same as event
|
|
Event:
|
|
```text
|
|
BallLeft
|
|
```
|
|
|
|
State:
|
|
```text
|
|
AlignToBall
|
|
```
|
|
|
|
Command:
|
|
```text
|
|
RotateLeft
|
|
```
|
|
|
|
Keep them separate.
|
|
|
|
---
|
|
# State produces command
|
|
```rust
|
|
enum MotionCommand {
|
|
Stop,
|
|
Forward,
|
|
RotateLeft,
|
|
RotateRight,
|
|
StrafeLeft,
|
|
StrafeRight,
|
|
Avoid,
|
|
}
|
|
```
|
|
|
|
The robot should think in intent first.
|
|
|
|
---
|
|
# Decision function
|
|
```rust
|
|
fn decide(
|
|
mode: RobotMode,
|
|
event: RobotEvent,
|
|
) -> (RobotMode, MotionCommand) {
|
|
match (mode, event) {
|
|
(_, RobotEvent::ObstacleDetected) =>
|
|
(RobotMode::AvoidObstacle, MotionCommand::Stop),
|
|
|
|
(RobotMode::Idle, RobotEvent::StartStop) =>
|
|
(RobotMode::SearchBall, MotionCommand::RotateLeft),
|
|
|
|
(RobotMode::SearchBall, RobotEvent::BallLeft) =>
|
|
(RobotMode::AlignToBall, MotionCommand::RotateLeft),
|
|
|
|
(RobotMode::SearchBall, RobotEvent::BallRight) =>
|
|
(RobotMode::AlignToBall, MotionCommand::RotateRight),
|
|
|
|
(RobotMode::SearchBall, RobotEvent::BallCentered) =>
|
|
(RobotMode::DriveToBall, MotionCommand::Forward),
|
|
|
|
(mode, _) =>
|
|
(mode, MotionCommand::Stop),
|
|
}
|
|
}
|
|
```
|
|
|
|
One place. One transition table. Less mystery.
|
|
|
|
---
|
|
# Chapter 08 exercise: Make the robot behave
|
|
Open:
|
|
```text
|
|
src/bin/08_state_machine.rs
|
|
```
|
|
|
|
Tasks:
|
|
1. Add `BallLost -> SearchBall`.
|
|
2. Add `StartStop -> Idle`.
|
|
3. Ensure `ObstacleDetected` always wins.
|
|
4. Map `RobotMode` to LED status.
|
|
5. Add one new mode or command.
|
|
|
|
Checkpoint:
|
|
```text
|
|
run-08
|
|
```
|
|
|
|
---
|
|
# We can test robot behaviour without a robot
|
|
```rust
|
|
#[test]
|
|
fn obstacle_wins_over_ball() {
|
|
let (mode, command) = decide(
|
|
RobotMode::DriveToBall,
|
|
RobotEvent::ObstacleDetected,
|
|
);
|
|
|
|
assert_eq!(mode, RobotMode::AvoidObstacle);
|
|
assert_eq!(command, MotionCommand::Stop);
|
|
}
|
|
```
|
|
|
|
Robot logic can be tested on the laptop.
|
|
|
|
---
|
|
# Chapter 09: Sensors
|
|
---
|
|
# Chapter 09: Camera model
|
|
Real camera pipeline:
|
|
```text
|
|
camera frame
|
|
|
|
|
color / blob / ML detection
|
|
|
|
|
BallObservation
|
|
|
|
|
RobotEvent
|
|
```
|
|
|
|
---
|
|
# Camera model -> Workshop model
|
|
```rust
|
|
enum BallObservation {
|
|
NotSeen,
|
|
Left,
|
|
Center,
|
|
Right,
|
|
TooClose,
|
|
}
|
|
```
|
|
|
|
The controller should not care *how* the ball was detected.
|
|
|
|
---
|
|
# Camera data is not clean
|
|
A camera may give:
|
|
- no ball
|
|
- multiple candidates
|
|
- wrong colour
|
|
- motion blur
|
|
- partial visibility
|
|
- bad lighting
|
|
- stale frame
|
|
|
|
So convert pixels into a small domain model (later).
|
|
|
|
---
|
|
# Chapter 09: Ultrasonic model
|
|
```rust
|
|
struct RawEchoMicros(u32);
|
|
struct DistanceCm(u16);
|
|
enum ObstacleObservation {
|
|
Clear,
|
|
Warning,
|
|
TooClose,
|
|
}
|
|
```
|
|
|
|
Pipeline:
|
|
```text
|
|
raw echo -> distance -> classification -> event
|
|
```
|
|
|
|
A distance value is not behaviour.
|
|
|
|
---
|
|
# Sensor fusion starts small
|
|
For a soccer robot with vision and Ultrasonic:
|
|
```text
|
|
BallCentered + Clear -> DriveToBall
|
|
|
|
BallCentered + TooClose -> Stop or AvoidObstacle
|
|
|
|
BallLost + Clear -> SearchBall
|
|
|
|
BallLeft + Warning -> Align carefully
|
|
```
|
|
|
|
Multiple inputs must become one decision.
|
|
|
|
---
|
|
# Chapter 09 exercise: Sensor pipeline
|
|
Open:
|
|
```text
|
|
src/bin/09_sensor_pipeline.rs
|
|
```
|
|
|
|
Tasks:
|
|
1. Convert fake camera input to `BallObservation`.
|
|
2. Convert fake ultrasonic distance to `ObstacleObservation`.
|
|
3. Convert observations to `RobotEvent`.
|
|
4. Feed the event into `decide()`.
|
|
5. Add one test for obstacle priority.
|
|
|
|
Checkpoint:
|
|
```text
|
|
test-09
|
|
```
|
|
|
|
---
|
|
# Chapter 10: Motion
|
|
---
|
|
# Chapter 10: Motion commands
|
|
The robot does not think in PWM first.
|
|
|
|
It thinks in motion intent:
|
|
```rust
|
|
struct MotionVector {
|
|
forward: f32,
|
|
strafe: f32,
|
|
rotate: f32,
|
|
}
|
|
```
|
|
|
|
Then a lower layer translates intent into wheel speeds.
|
|
|
|
---
|
|
# From symbolic command to vector
|
|
```rust
|
|
fn command_to_vector(command: MotionCommand) -> MotionVector {
|
|
match command {
|
|
MotionCommand::Stop => MotionVector {
|
|
forward: 0.0,
|
|
strafe: 0.0,
|
|
rotate: 0.0,
|
|
},
|
|
MotionCommand::Forward => MotionVector {
|
|
forward: 0.6,
|
|
strafe: 0.0,
|
|
rotate: 0.0,
|
|
},
|
|
MotionCommand::RotateLeft => MotionVector {
|
|
forward: 0.0,
|
|
strafe: 0.0,
|
|
rotate: -0.4,
|
|
},
|
|
MotionCommand::RotateRight => MotionVector {
|
|
forward: 0.0,
|
|
strafe: 0.0,
|
|
rotate: 0.4,
|
|
},
|
|
_ => MotionVector {
|
|
forward: 0.0,
|
|
strafe: 0.0,
|
|
rotate: 0.0,
|
|
},
|
|
}
|
|
}
|
|
```
|
|
|
|
Intent becomes a numeric movement request.
|
|
|
|
---
|
|
# Four-wheel omni drive: Command to wheels
|
|
```rust
|
|
struct WheelSpeeds {
|
|
front_left: f32,
|
|
front_right: f32,
|
|
rear_left: f32,
|
|
rear_right: f32,
|
|
}
|
|
|
|
fn mix(cmd: MotionVector) -> WheelSpeeds {
|
|
WheelSpeeds {
|
|
front_left: cmd.forward + cmd.strafe + cmd.rotate,
|
|
front_right: cmd.forward - cmd.strafe - cmd.rotate,
|
|
rear_left: cmd.forward - cmd.strafe + cmd.rotate,
|
|
rear_right: cmd.forward + cmd.strafe - cmd.rotate,
|
|
}
|
|
}
|
|
```
|
|
|
|
The formula is simple. The real robot is not.
|
|
|
|
---
|
|
# Real motors add boring but important details
|
|
A real motor layer needs:
|
|
- speed normalization
|
|
- motor orientation calibration
|
|
- PWM generation
|
|
- motor driver limits
|
|
- battery compensation
|
|
- emergency stop behaviour
|
|
- testing on the floor
|
|
|
|
Rust helps keep the layers honest.
|
|
|
|
---
|
|
# Chapter 10 exercise: From robot command to wheel speeds
|
|
Open:
|
|
```text
|
|
src/bin/10_motor_commands.rs
|
|
```
|
|
|
|
Tasks:
|
|
1. Convert `MotionCommand` to `MotionVector`.
|
|
2. Convert `MotionVector` to `WheelSpeeds`.
|
|
3. Normalize speeds to `-1.0..=1.0`.
|
|
4. Add a test for `Stop`.
|
|
5. Add a test for `RotateLeft`.
|
|
|
|
Checkpoint:
|
|
```text
|
|
test-10
|
|
```
|
|
|
|
---
|
|
# Chapter 11: Kinematics
|
|
---
|
|
# Chapter 11: Kinematics enters when geometry matters
|
|
For a mobile base:
|
|
```text
|
|
robot motion intent -> wheel speeds
|
|
```
|
|
|
|
For a robot arm:
|
|
```text
|
|
target position -> joint angles -> motor commands
|
|
```
|
|
|
|
That second step is inverse kinematics.
|
|
|
|
---
|
|
# Forward kinematics asks: Where did we end up?
|
|
```text
|
|
joint angles
|
|
|
|
|
robot geometry
|
|
|
|
|
tool position
|
|
```
|
|
|
|
Example:
|
|
|
|
```text
|
|
base angle + shoulder angle + elbow angle
|
|
=> x / y / z position
|
|
```
|
|
|
|
Good for checking and simulation.
|
|
|
|
---
|
|
# Inverse kinematics asks: How do we get there?
|
|
```text
|
|
target position
|
|
|
|
|
robot geometry
|
|
|
|
|
joint angles
|
|
```
|
|
|
|
Example:
|
|
|
|
```rust
|
|
struct ArmTarget {
|
|
x_mm: f32,
|
|
y_mm: f32,
|
|
z_mm: f32,
|
|
}
|
|
|
|
struct JointAngles {
|
|
base_deg: f32,
|
|
shoulder_deg: f32,
|
|
elbow_deg: f32,
|
|
}
|
|
```
|
|
|
|
---
|
|
# Invalid movement should fail before hardware moves
|
|
```rust
|
|
enum KinematicsError {
|
|
TargetOutOfReach,
|
|
JointLimitExceeded,
|
|
CollisionRisk,
|
|
}
|
|
|
|
fn inverse_kinematics(
|
|
target: ArmTarget,
|
|
) -> Result<JointAngles, KinematicsError> {
|
|
todo!()
|
|
}
|
|
```
|
|
|
|
The safest motor command is the one you never send.
|
|
|
|
---
|
|
# Chapter 11 exercise: Reject invalid motion
|
|
Open:
|
|
```text
|
|
src/bin/11_kinematics.rs
|
|
```
|
|
|
|
Tasks:
|
|
1. Run the tests.
|
|
2. Change one reachable target.
|
|
3. Add one rejected target.
|
|
4. Keep unsafe motion out of the command layer.
|
|
|
|
Checkpoint:
|
|
```text
|
|
test-11
|
|
```
|
|
|
|
---
|
|
# Chapter 12: Embassy
|
|
---
|
|
# Chapter 12: Why Embassy?
|
|
So far, we wrote the loop ourselves:
|
|
```text
|
|
configure hardware
|
|
↓
|
|
loop forever
|
|
↓
|
|
check time
|
|
↓
|
|
read input
|
|
↓
|
|
update state
|
|
↓
|
|
write output
|
|
```
|
|
|
|
But it becomes noisy when several things must happen at different rates.
|
|
|
|
---
|
|
# Chapter 12: Robots rarely do only one thing
|
|
A robot may need to:
|
|
* blink status LED every 500 ms
|
|
* read buttons or sensors often
|
|
* print diagnostics occasionally
|
|
* update behaviour state
|
|
* send motor commands regularly
|
|
* wait without blocking everything else
|
|
|
|
A single hand-written loop can do this.
|
|
|
|
It just gets ugly.
|
|
|
|
---
|
|
# Chapter 12: Reuse established embedded crates
|
|
|
|
We do not need to build every firmware primitive ourselves.
|
|
|
|
Embassy gives us reusable embedded building blocks:
|
|
|
|
* async executor
|
|
* task spawning
|
|
* timers
|
|
* tickers
|
|
* async waiting
|
|
* embedded-focused scheduling model
|
|
|
|
The setup stays explicit.
|
|
|
|
The waiting and task structure get cleaner.
|
|
|
|
---
|
|
# Chapter 12: What Embassy is
|
|
Embassy is an embedded Rust framework built around async Rust.
|
|
|
|
It provides an async executor designed for embedded systems.
|
|
|
|
Important embedded properties:
|
|
|
|
* no heap required
|
|
* tasks are statically allocated
|
|
* async/await works on microcontrollers
|
|
* timers can express waiting without busy-loop plumbing
|
|
|
|
This is not a desktop runtime.
|
|
|
|
---
|
|
# Chapter 12: From one loop to tasks
|
|
Manual loop model:
|
|
```text
|
|
main loop
|
|
↓
|
|
do everything
|
|
↓
|
|
manually track time
|
|
↓
|
|
repeat
|
|
```
|
|
|
|
Embassy model:
|
|
```text
|
|
init hardware
|
|
↓
|
|
spawn tasks
|
|
↓
|
|
await timers
|
|
↓
|
|
executor resumes work
|
|
```
|
|
|
|
The firmware still owns the hardware.
|
|
|
|
The structure is cleaner.
|
|
|
|
---
|
|
# Chapter 12: Timer-driven waiting
|
|
Instead of manual delay plumbing:
|
|
```rust
|
|
loop {
|
|
do_work();
|
|
busy_wait();
|
|
}
|
|
```
|
|
Embassy lets us express waiting directly:
|
|
```rs
|
|
loop {
|
|
do_work();
|
|
Timer::after(Duration::from_millis(500)).await;
|
|
}
|
|
```
|
|
The code says what it means:
|
|
`do work, then wait`
|
|
|
|
---
|
|
# Chapter 12: Periodic work with Ticker
|
|
For periodic work:
|
|
```rs
|
|
let mut ticker = Ticker::every(Duration::from_millis(500));
|
|
loop {
|
|
ticker.next().await;
|
|
hprintln!("heartbeat");
|
|
}
|
|
```
|
|
|
|
A ticker is useful for:
|
|
|
|
* heartbeat output
|
|
* periodic sensor sampling
|
|
* regular status updates
|
|
* control-loop prototypes
|
|
|
|
---
|
|
# Chapter 12: Async task shape
|
|
```rs
|
|
#[embassy_executor::task]
|
|
async fn heartbeat_task() {
|
|
let mut ticker = Ticker::every(Duration::from_millis(500));
|
|
loop {
|
|
ticker.next().await;
|
|
hprintln!("heartbeat");
|
|
}
|
|
}
|
|
```
|
|
The task looks sequential.
|
|
|
|
The executor handles waiting and resuming.
|
|
|
|
---
|
|
# Chapter 12: Main task stays explicit
|
|
```rs
|
|
#[embassy_executor::main]
|
|
async fn main(spawner: Spawner) {
|
|
spawner.spawn(heartbeat_task()).unwrap();
|
|
loop {
|
|
hprintln!("main task alive");
|
|
Timer::after(Duration::from_secs(2)).await;
|
|
}
|
|
}
|
|
```
|
|
The structure is still visible:
|
|
```text
|
|
start executor
|
|
↓
|
|
spawn heartbeat
|
|
↓
|
|
main task keeps running
|
|
```
|
|
|
|
---
|
|
# Chapter 12: Embassy example
|
|
Open: `src/bin/12_embassy.rs`
|
|
|
|
Run: `run-12`
|
|
|
|
Tasks:
|
|
1. Run the baseline.
|
|
2. Change the heartbeat text.
|
|
3. Change the timer period.
|
|
4. Add a second async task.
|
|
5. Compare this with chapter 04.
|
|
6. Decide which version is easier to extend.
|
|
|
|
---
|
|
# Chapter 12: What Embassy does not solve
|
|
Embassy does not remove:
|
|
* hardware setup
|
|
* ownership rules
|
|
* bad wiring
|
|
* wrong pin mapping
|
|
* sensor noise
|
|
* control logic bugs
|
|
* the need for testing on real hardware
|
|
|
|
It helps structure waiting and concurrent tasks.
|
|
|
|
It does not make the robot correct by itself.
|
|
|
|
---
|
|
# The full architecture we built toward
|
|
```text
|
|
camera / ultrasonic / button (Sensors)
|
|
|
|
|
raw reading
|
|
|
|
|
domain observation
|
|
|
|
|
RobotEvent
|
|
|
|
|
RobotMode
|
|
|
|
|
MotionCommand
|
|
|
|
|
MotionVector
|
|
|
|
|
WheelSpeeds
|
|
|
|
|
motor driver (Output)
|
|
```
|
|
|
|
---
|
|
# Hardware and software work together in layers
|
|
```text
|
|
Hardware layer
|
|
GPIO, timers, PWM, ADC, buses
|
|
|
|
Driver layer
|
|
LED, button, motor driver, sensor
|
|
|
|
Domain layer
|
|
BallObservation, ObstacleObservation
|
|
|
|
Behaviour layer
|
|
RobotMode, state machine
|
|
|
|
Control layer
|
|
MotionCommand, wheel speeds
|
|
```
|
|
|
|
Do not mix this into one loop!
|
|
|
|
---
|
|
# What Rust helped with
|
|
- explicit ownership of resources
|
|
- visible mutation
|
|
- clear domain types
|
|
- enums for valid states
|
|
- `Result` for sensor failure
|
|
- testable robot logic
|
|
- separation between hardware and behaviour
|
|
- small embedded programs without `std`
|
|
|
|
Rust did not remove the need for calibration, physics and debugging.
|
|
|
|
---
|
|
# What robotics taught us
|
|
- Raw input is not meaning
|
|
- Meaning is not behaviour
|
|
- Behaviour is not motor output
|
|
- Motor output is not guaranteed motion
|
|
- Real systems need feedback
|
|
- Safety should win over ambition
|
|
|
|
The robot is a loop, not a script
|
|
|
|
---
|
|
# Where to continue
|
|
Next useful steps:
|
|
|
|
0. Get yourself a microcontroller
|
|
1. Add real ultrasonic hardware
|
|
2. Add motor driver output
|
|
3. Add a camera module or external vision process
|
|
4. Add logs and host-side tests
|
|
5. Move repeated tasks to Embassy async
|
|
6. Put the board onto a small chassis
|
|
7. Test on the floor
|
|
|
|
Build one layer at a time
|
|
|
|
---
|
|
# Useful references
|
|
- The Rust Book: ownership and borrowing
|
|
- The Embedded Rust Book: `no_std` and bare-metal concepts
|
|
- probe-rs: flashing and embedded debugging tooling
|
|
- RoboCupJunior Soccer rules and description
|