Current section

Files

Jump to
bb_servo_pca9685 usage-rules.md
Raw

usage-rules.md

<!--
SPDX-FileCopyrightText: 2026 James Harton
SPDX-License-Identifier: Apache-2.0
-->
# BB.Servo.PCA9685 Usage Rules
`bb_servo_pca9685` drives RC servos through a PCA9685 16-channel PWM board over
I²C for [Beam Bots](https://hexdocs.pm/bb). It supplies two DSL components: a
robot-level `BB.Servo.PCA9685.Controller` (a `BB.Controller` wrapping one
physical board) and a per-joint `BB.Servo.PCA9685.Actuator` (a `BB.Actuator`
driving one channel). For BB framework basics, see `bb`'s rules
(`mix usage_rules.sync <file> bb:all`); this file covers only what's specific to
this driver.
## Core principles
1. **One controller per board, many actuators per controller.** The controller
owns the I²C connection; each actuator names it via `controller:` and claims
one channel (`0..15`). A board has 16 channels — add a second controller (at
a different `address:`) for more servos.
2. **Servo geometry comes from the joint, not the actuator.** The actuator reads
the joint's `limit` (via the injected motor profile) and maps that motor
range linearly onto the PWM pulse range. You never declare rotation range or
speed on the actuator — only the pulse endpoints if the servo's defaults
don't fit.
3. **Position feedback is open-loop.** RC servos report nothing back. Pair each
actuator with core's `BB.Sensor.OpenLoopPositionEstimator`, which
interpolates position from the `BeginMotion` message the actuator publishes.
## Installing
```sh
mix igniter.install bb_servo_pca9685
```
This adds a controller entry plus a `:config.:pca9685` param group to your robot
and sets the bus on the application child spec. Actuator and sensor entries are
per-joint and are **not** added automatically — the installer prints a snippet
to copy into each joint.
## Wiring it in
The controller goes at robot level; each servo is an actuator inside its joint,
with an open-loop estimator alongside it:
```elixir
defmodule MyRobot.Robot do
use BB
controllers do
controller :pca9685, {BB.Servo.PCA9685.Controller, bus: "i2c-1", address: 0x40}
end
topology do
link :base do
joint :shoulder do
type :revolute
limit lower: ~u(-45 degree),
upper: ~u(45 degree),
effort: ~u(1 newton_meter),
velocity: ~u(60 degree_per_second)
actuator :shoulder_servo, {BB.Servo.PCA9685.Actuator, channel: 0, controller: :pca9685}
sensor :shoulder_feedback,
{BB.Sensor.OpenLoopPositionEstimator, actuator: :shoulder_servo}
link :upper_arm do
end
end
end
end
end
```
Command it with `BB.Actuator` once the robot is armed (values are joint-space
radians; BB applies the joint transmission before the driver sees motor-space):
```elixir
BB.Actuator.set_position(MyRobot.Robot, :shoulder_servo, 0.5)
BB.Actuator.set_position!(MyRobot.Robot, :shoulder_servo, 0.5)
{:ok, :accepted} = BB.Actuator.set_position_sync(MyRobot.Robot, :shoulder_servo, 0.5)
```
## Options
**Controller** (`controllers` slot):
| Option | Default | Meaning |
|---|---|---|
| `:bus` | required | I²C bus name, e.g. `"i2c-1"` |
| `:address` | required | I²C address, e.g. `0x40` |
| `:pwm_freq` | `50` | PWM frequency in Hz (50 suits most analogue servos) |
| `:oe_pin` | none | GPIO pin for hardware output-enable |
**Actuator** (per-joint `actuator` slot):
| Option | Default | Meaning |
|---|---|---|
| `:channel` | required | Board channel, `0..15` |
| `:controller` | required | Name of the controller entry to route through |
| `:min_pulse` | `500` | Pulse width (µs) mapped to the joint's lower limit |
| `:max_pulse` | `2500` | Pulse width (µs) mapped to the joint's upper limit |
## Anti-patterns
- **Don't try to set the servo's travel range or speed on the actuator.** There
is no such option — it derives everything from the joint `limit`. Widen the
joint limit or adjust `:min_pulse`/`:max_pulse` to match the physical servo.
- **Don't lean on `disarm/1` as a power cut if the controller might be dead.**
Both the actuator's disarm (pulse → 0) and the controller's disarm (OE pin
high) route through the live controller process; if it has crashed the disarm
fails and the robot enters `:error`. For a kill that survives a dead
controller, wire `:oe_pin` — a clean controller shutdown pulls it high in the
device's `terminate/2`.
- **Don't expect the controller to run under simulation.** Controllers default
to `simulation: :omit`, so the real PCA9685 controller does not start; set
`simulation: :mock` or `:start` on the entry if you need it. Actuators are
swapped for `BB.Sim.Actuator` and the open-loop estimator works unchanged.
- **Address the actuator by name or by full path, never a partial one.** All
three transports accept either, and a name is resolved for you. A partial
path like `[:shoulder, :servo]` matches no subscriber, so the command is
published and dropped.
- **Don't reuse a component name across joints.** Names are unique robot-wide,
not scoped to their joint, so `:servo` on two joints fails to compile. Name
them after the joint (`:shoulder_servo`, `:elbow_servo`).
## Further reading
- [bb_servo_pca9685 docs](https://hexdocs.pm/bb_servo_pca9685)
- `bb`'s actuator and safety rules (`bb:actuators`, `bb:safety-and-commands`)