Beam Bots Logo

Beam Bots PCA9685 servo control

CILicense: Apache 2.0Hex version badgeHexdocs badgeREUSE statusAsk DeepWiki

BB.Servo.PCA9685

BB integration for driving RC servos via PCA9685 16-channel PWM controller over I2C.

This library provides a controller and actuator module for controlling RC servos connected to a PCA9685 board.

Installation

Add bb_servo_pca9685 to your list of dependencies in mix.exs:

def deps do
[
{:bb_servo_pca9685, "~> 0.7.0"}
]
end

Requirements

Usage

Define a controller and joints with servo actuators in your robot DSL:

defmodule MyRobot do
use BB
controllers do
# Define the PCA9685 controller at robot level
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
joint :elbow do
type :revolute
limit lower: ~u(-90 degree),
upper: ~u(90 degree),
effort: ~u(1 newton_meter),
velocity: ~u(60 degree_per_second)
actuator :elbow_servo, {BB.Servo.PCA9685.Actuator, channel: 1, controller: :pca9685}
sensor :elbow_feedback,
{BB.Sensor.OpenLoopPositionEstimator, actuator: :elbow_servo}
link :forearm
end
end
end
end
end
end

Component names are unique across the whole robot, so each servo and estimator needs its own name rather than a per-joint :servo.

The actuator automatically derives its configuration from the joint limits - no need to specify servo rotation range or speed separately.

Sending Commands

Use the BB.Actuator module to send commands to servos. The robot must be armed first — a disarmed robot will not move, and as of bb 0.23 the framework refuses the command before it reaches the driver.

Three transports are available. They differ only in delivery: all three arrive at the driver's handle_command/2, and each takes either the actuator's unique name or its full path through the topology.

Direct Delivery (for time-critical control)

Fire-and-forget, addressed by the actuator's unique name:

BB.Actuator.set_position!(MyRobot, :shoulder_servo, 0.5)

Synchronous Delivery (with acknowledgement)

Wait for the actuator to acknowledge the command:

case BB.Actuator.set_position_sync(MyRobot, :shoulder_servo, 0.5) do
{:ok, :accepted} -> :ok
{:error, reason} -> handle_error(reason)
end

Pubsub Delivery (for orchestration)

BB.Actuator.set_position/4 publishes to [:actuator | path] rather than addressing the process directly, which is what makes logging, replay and multi-subscriber patterns possible:

BB.Actuator.set_position(MyRobot, :shoulder_servo, 0.5, command_id: make_ref())
# Or by full path
BB.Actuator.set_position(MyRobot, [:base, :shoulder, :shoulder_servo], 0.5)

Components

Controller

BB.Servo.PCA9685.Controller manages communication with the PCA9685 board. Define one controller per physical PCA9685 device.

Options:

OptionTypeDefaultDescription
busstringrequiredI2C bus name (e.g. "i2c-1")
addressintegerrequiredI2C address of the PCA9685 (e.g. 0x40)
pwm_freqinteger50PWM frequency in Hz
oe_pinintegernilOptional output-enable GPIO pin

Actuator

BB.Servo.PCA9685.Actuator controls a single servo on one of the 16 channels.

Options:

OptionTypeDefaultDescription
channel0-15requiredPCA9685 channel number
controlleratomrequiredName of the controller in robot registry
min_pulseinteger500Minimum PWM pulse width (microseconds)
max_pulseinteger2500Maximum PWM pulse width (microseconds)

To reverse a servo relative to its joint, configure the actuator's joint transmission rather than passing an actuator option:

actuator :shoulder_servo, {BB.Servo.PCA9685.Actuator, channel: 0, controller: :pca9685} do
transmission do
reversed? true
end
end

Behaviour:

Sensor

Use BB.Sensor.OpenLoopPositionEstimator from the BB core library for position feedback. It subscribes to actuator BeginMotion messages and interpolates position during movement.

sensor :shoulder_feedback, {BB.Sensor.OpenLoopPositionEstimator, actuator: :shoulder_servo}

How It Works

Architecture

Controller (GenServer)
|
v wraps
PCA9685.Device (I2C communication)
^
| used by
Actuator (GenServer) --publishes--> BeginMotion --> Sensor (GenServer)
|
v publishes
JointState

Multiple actuators share a single controller. Each actuator controls one of the 16 available channels.

Position Mapping

The actuator maps the joint's position limits to the servo's PWM range:

Joint lower limit -> min_pulse (500 microseconds)
Joint upper limit -> max_pulse (2500 microseconds)
Joint centre -> mid_pulse (1500 microseconds)

For a joint with limits -45 degrees to +45 degrees:

Position Feedback

Since RC servos don't provide position feedback, the open-loop position estimator estimates position based on commanded targets and expected arrival times:

  1. Actuator sends command and publishes BeginMotion with expected arrival time
  2. Sensor receives BeginMotion and interpolates position during movement
  3. After arrival time, sensor reports the target position

This provides realistic position feedback for trajectory planning and monitoring.

Motion Lifecycle

When a position command is processed:

  1. Actuator clamps position to joint limits
  2. Converts angle to PWM pulse width
  3. Sends command to controller via BB.Process.call
  4. Controller writes PWM to the PCA9685 over I2C
  5. Publishes BB.Message.Actuator.BeginMotion with:
    • initial_position - where the servo was
    • target_position - where it's going
    • expected_arrival - when it should arrive (monotonic milliseconds)
    • command_id - correlation ID (if provided)
    • command_type - :position

Documentation

Full documentation is available at HexDocs.