7#include "CppTimerStdFuncCallback.h"
11#include <condition_variable>
19constexpr unsigned long kI2cSlaveRequest = 0x0703;
21constexpr std::uint8_t kMode1Register = 0x00;
22constexpr std::uint8_t kMode2Register = 0x01;
23constexpr std::uint8_t kLed0OnLowRegister = 0x06;
24constexpr std::uint8_t kAllLedOnLowRegister = 0xFA;
25constexpr std::uint8_t kPrescaleRegister = 0xFE;
27constexpr std::uint8_t kMode1SleepBit = 0x10;
28constexpr std::uint8_t kMode1AutoIncrementBit = 0x20;
29constexpr std::uint8_t kMode2TotemPoleBit = 0x04;
30constexpr std::uint8_t kFullOffBit = 0x10;
32constexpr float kPca9685OscillatorHz = 25000000.0f;
33constexpr std::uint8_t kMinimumPrescale = 3;
34constexpr std::uint8_t kMaximumPrescale = 255;
35constexpr useconds_t kWakeDelayUs = 5000;
37bool waitForWakeDelay(useconds_t delay_us)
noexcept {
43 std::condition_variable condition;
46 CppTimerCallback timer;
47 timer.registerEventCallback([&]() {
48 std::lock_guard<std::mutex> lock(mutex);
50 condition.notify_one();
53 const std::uint64_t delay_ns =
54 static_cast<std::uint64_t
>(delay_us) * 1000ULL;
56 timer.startns(
static_cast<long>(delay_ns), ONESHOT);
61 std::unique_lock<std::mutex> lock(mutex);
62 condition.wait(lock, [&]() {
return fired; });
81 std::uint8_t device_address,
82 float pwm_frequency_hz,
86 if (i2c_device_path ==
nullptr || !hasValidChannelMap(channel_map) ||
87 pwm_frequency_hz <= 0.0f) {
94 i2c_fd_ = ::open(i2c_device_path, O_RDWR | O_CLOEXEC);
104 static_cast<unsigned long>(device_address)) < 0) {
111 channel_map_ = channel_map;
113 if (!configurePca9685(pwm_frequency_hz)) {
124 if (!setAllOutputsEnabled(
false)) {
131 cached_targets_ = {};
148 if (targets.base_ticks > kMaxPulseTicks ||
149 targets.lower_ticks > kMaxPulseTicks ||
150 targets.upper_ticks > kMaxPulseTicks ||
151 targets.gripper_ticks > kMaxPulseTicks) {
156 cached_targets_ = targets;
163 if (!writeTargetsToHardware(cached_targets_)) {
179 if (!setAllOutputsEnabled(
false)) {
200 if (!setAllOutputsEnabled(
true) || !writeTargetsToHardware(cached_targets_)) {
213 (void)setAllOutputsEnabled(
false);
218 cached_targets_ = {};
238 return output_state_;
246 switch (last_error_) {
250 return "invalid argument";
252 return "controller not initialised";
254 return "failed to open I2C device";
256 return "failed to select I2C slave address";
258 return "PCA9685 I/O failed";
260 return "unknown error";
264bool MotionController::configurePca9685(
float pwm_frequency_hz)
noexcept {
265 const float prescale_value =
266 (kPca9685OscillatorHz / (4096.0f * pwm_frequency_hz)) - 1.0f;
267 const long prescale_long = std::lround(prescale_value);
269 if (prescale_long < kMinimumPrescale || prescale_long > kMaximumPrescale) {
274 if (!writeRegister(kMode1Register, kMode1SleepBit)) {
278 if (!writeRegister(kPrescaleRegister,
279 static_cast<std::uint8_t
>(prescale_long))) {
283 if (!writeRegister(kMode2Register, kMode2TotemPoleBit)) {
287 if (!writeRegister(kMode1Register, kMode1AutoIncrementBit)) {
291 if (!waitForWakeDelay(kWakeDelayUs)) {
298bool MotionController::writeTargetsToHardware(
300 return writeChannelPulse(channel_map_.base, targets.base_ticks) &&
301 writeChannelPulse(channel_map_.lower, targets.lower_ticks) &&
302 writeChannelPulse(channel_map_.upper, targets.upper_ticks) &&
303 writeChannelPulse(channel_map_.gripper, targets.gripper_ticks);
306bool MotionController::writeChannelPulse(std::uint8_t channel,
307 std::uint16_t pulse_ticks)
noexcept {
308 if (channel >= kPca9685ChannelCount || pulse_ticks > kMaxPulseTicks) {
313 const std::uint8_t register_address =
static_cast<std::uint8_t
>(
314 kLed0OnLowRegister + (4U * channel));
315 const std::uint8_t payload[4] = {
318 static_cast<std::uint8_t
>(pulse_ticks & 0xFFU),
319 static_cast<std::uint8_t
>((pulse_ticks >> 8U) & 0x0FU)};
321 return writeRegisters(register_address, payload, 4);
324bool MotionController::writeRegister(std::uint8_t reg,
325 std::uint8_t value)
noexcept {
326 return writeRegisters(reg, &value, 1);
329bool MotionController::writeRegisters(std::uint8_t start_reg,
330 const std::uint8_t* values,
331 std::uint8_t value_count)
noexcept {
332 if (i2c_fd_ < 0 || values == nullptr || value_count == 0 || value_count > 4) {
337 std::uint8_t buffer[5] = {};
338 buffer[0] = start_reg;
340 for (std::uint8_t index = 0; index < value_count; ++index) {
341 buffer[index + 1] = values[index];
344 const std::size_t bytes_to_write =
345 static_cast<std::size_t
>(value_count) + 1U;
346 const ssize_t bytes_written =
347 ::write(i2c_fd_, buffer, bytes_to_write);
349 return bytes_written ==
static_cast<ssize_t
>(bytes_to_write);
352bool MotionController::setAllOutputsEnabled(
bool enabled)
noexcept {
354 const std::uint8_t payload[4] = {
358 static_cast<std::uint8_t
>(enabled ? 0x00 : kFullOffBit)};
360 return writeRegisters(kAllLedOnLowRegister, payload, 4);
363bool MotionController::hasValidChannelMap(
365 const std::array<std::uint8_t, kServoCount> channels = {
369 channel_map.gripper};
371 for (std::size_t i = 0; i < channels.size(); ++i) {
372 if (channels[i] >= kPca9685ChannelCount) {
376 for (std::size_t j = i + 1; j < channels.size(); ++j) {
377 if (channels[i] == channels[j]) {
392 (void)setAllOutputsEnabled(
false);
@ NONE
No hardware action required.
PCA9685-based servo output controller for ARGUS.
MotionControllerError
Error codes reported by MotionController operations.
@ NOT_INITIALISED
Controller is not initialised.
@ DEVICE_SELECT_FAILED
Failed to select I2C slave address.
@ DEVICE_IO_FAILED
I/O transaction failed.
@ DEVICE_OPEN_FAILED
Failed to open I2C device.
@ INVALID_ARGUMENT
Invalid input argument.
MotionOutputState
High-level state of motion output availability.
@ UNINITIALISED
Hardware not initialised.
@ DISABLED
Initialised but outputs disabled.
@ FAULT
Fault state; output path blocked until reset/re-init.
@ ENABLED
Outputs enabled and can accept target writes.
MotionControllerError lastError() const noexcept
Get last controller error code.
MotionController() noexcept
Construct motion controller in uninitialised state.
void freeze() noexcept
Freeze/disable motion output immediately.
bool isEnabled() const noexcept
Check whether motion output is currently enabled.
const char * lastErrorString() const noexcept
Get human-readable last error string.
~MotionController() noexcept
Destroy motion controller and release resources.
bool initialise(const char *i2c_device_path="/dev/i2c-1", std::uint8_t device_address=0x40, float pwm_frequency_hz=50.0f, MotionChannelMap channel_map={}) noexcept
Initialise PCA9685 output path.
bool isFrozen() const noexcept
Check whether output is currently frozen.
bool isInitialised() const noexcept
Check whether controller was successfully initialised.
bool setTargets(const MeArmJointTargets &targets) noexcept
Write joint targets to hardware when motion is enabled.
void shutdown() noexcept
Disable outputs and close hardware resources.
MotionOutputState outputState() const noexcept
Get current output state.
bool enable() noexcept
Enable motion output after successful initialisation.
Raw PWM tick targets for each MeArm joint.
Mapping of logical MeArm joints to PCA9685 channels.