diff options
author | Kevin O'Connor <kevin@koconnor.net> | 2017-08-06 11:20:12 -0400 |
---|---|---|
committer | Kevin O'Connor <kevin@koconnor.net> | 2017-09-20 12:55:28 -0400 |
commit | 73a1c9d249170a2edc842af7f852e60c427f0f6d (patch) | |
tree | d896406bc29e0f3e80b55dd3136c44170bcc15ed /src/linux/pca9685.c | |
parent | d851882278644b959d9175d6b303631ddc8cd2c6 (diff) | |
download | kutter-73a1c9d249170a2edc842af7f852e60c427f0f6d.tar.gz kutter-73a1c9d249170a2edc842af7f852e60c427f0f6d.tar.xz kutter-73a1c9d249170a2edc842af7f852e60c427f0f6d.zip |
linux: Add support for pca9685 i2c pwm devices
Add support for controlling pca9685 PWM drivers using the standard
Linux I2C interface. The pca9685 device is found on Replicape boards.
Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
Diffstat (limited to 'src/linux/pca9685.c')
-rw-r--r-- | src/linux/pca9685.c | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/src/linux/pca9685.c b/src/linux/pca9685.c new file mode 100644 index 00000000..4c1611aa --- /dev/null +++ b/src/linux/pca9685.c @@ -0,0 +1,205 @@ +// Communicating with a PCA9685 pwm device via linux i2c +// +// Copyright (C) 2017 Kevin O'Connor <kevin@koconnor.net> +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include <fcntl.h> // open +#include <linux/i2c-dev.h> // I2C_SLAVE +#include <stdio.h> // snprintf +#include <sys/ioctl.h> // ioctl +#include <unistd.h> // write +#include "basecmd.h" // oid_alloc +#include "board/misc.h" // timer_from_us +#include "command.h" // DECL_COMMAND +#include "internal.h" // report_errno +#include "sched.h" // DECL_SHUTDOWN + + +/**************************************************************** + * Low-level i2c + ****************************************************************/ + +#define P9_MODE1 0x00 +#define P9_PRESCALE 0xfe +#define CHANNEL_MAX 15 +#define CHANNEL_ALL 61 +#define VALUE_MAX 4096 + +#define OSC_MHZ 25 +#define CLOCK_MHZ timer_from_us(1) + +struct i2c_s { + uint8_t bus; + uint8_t addr; + uint32_t cycle_ticks; + int fd; +}; +static struct i2c_s devices[16]; +static int devices_count; + +static void +pca9685_write(int fd, uint8_t channel, uint16_t value) +{ + uint8_t full_on = 0x00; + if (value >= VALUE_MAX) { + full_on = 0x10; + value = 0x0000; + } else if (!value) { + value = 0x1000; + } + uint8_t msg[5] = { channel*4 + 0x06, 0x00, full_on, value, value >> 8 }; + int ret = write(fd, msg, sizeof(msg)); + if (ret < 0) { + report_errno("write value i2c", ret); + try_shutdown("Unable to update PCA9685 value"); + } +} + +static int +open_i2c(uint8_t bus, uint8_t addr, uint32_t cycle_ticks) +{ + // Find existing device (if already opened) + int i; + for (i=0; i<devices_count; i++) + if (devices[i].bus == bus && devices[i].addr == addr) { + if (cycle_ticks != devices[i].cycle_ticks) + shutdown("All PCA9685 channels must have the same cycle_ticks"); + return devices[i].fd; + } + + // Setup new I2C device + if (devices_count >= ARRAY_SIZE(devices)) + shutdown("Too many i2c devices"); + char fname[256]; + snprintf(fname, sizeof(fname), "/dev/i2c-%d", bus); + int fd = open(fname, O_RDWR|O_CLOEXEC); + if (fd < 0) { + report_errno("open i2c", fd); + shutdown("Unable to open i2c device"); + } + int ret = ioctl(fd, I2C_SLAVE, addr); + if (ret < 0) { + report_errno("ioctl i2c", fd); + shutdown("Unable to set address on i2c device"); + } + ret = set_non_blocking(fd); + if (ret < 0) + shutdown("Unable to set non-blocking on i2c device"); + + // Init PCA9685 + const uint8_t sleep_msg[2] = { P9_MODE1, 0x31 }; + ret = write(fd, sleep_msg, sizeof(sleep_msg)); + if (ret < 0) { + report_errno("write sleep i2c", ret); + shutdown("Unable to sleep PCA9685"); + } + uint32_t freq = DIV_ROUND_CLOSEST(OSC_MHZ*cycle_ticks, 4096*CLOCK_MHZ) - 1; + freq = freq > 0xff ? 0xff : (freq < 0x03 ? 0x03 : freq); + uint8_t freq_msg[2] = { P9_PRESCALE, freq }; + ret = write(fd, freq_msg, sizeof(freq_msg)); + if (ret < 0) { + report_errno("write freq i2c", ret); + shutdown("Unable to set freq on PCA9685"); + } + const uint8_t wake_msg[2] = { P9_MODE1, 0x21 }; + ret = write(fd, wake_msg, sizeof(wake_msg)); + if (ret < 0) { + report_errno("write unsleep i2c", ret); + shutdown("Unable to wake PCA9685"); + } + usleep(500); + pca9685_write(fd, CHANNEL_ALL, 0); + + devices[devices_count].bus = bus; + devices[devices_count].addr = addr; + devices[devices_count].cycle_ticks = cycle_ticks; + devices[devices_count].fd = fd; + return fd; +} + +void +pca9685_shutdown(void) +{ + int i; + for (i=0; i<devices_count; i++) + pca9685_write(devices[i].fd, CHANNEL_ALL, 0); +} +DECL_SHUTDOWN(pca9685_shutdown); + + +/**************************************************************** + * Command interface + ****************************************************************/ + +struct i2cpwm_s { + struct timer timer; + int fd; + uint8_t channel; + uint16_t value; + uint32_t max_duration; +}; + +DECL_CONSTANT(PCA9685_MAX, VALUE_MAX); + +static uint_fast8_t +pca9685_end_event(struct timer *timer) +{ + shutdown("Missed scheduling of next pca9685 event"); +} + +static uint_fast8_t +pca9685_event(struct timer *timer) +{ + struct i2cpwm_s *p = container_of(timer, struct i2cpwm_s, timer); + pca9685_write(p->fd, p->channel, p->value); + if (!p->value || !p->max_duration) + return SF_DONE; + p->timer.waketime += p->max_duration; + p->timer.func = pca9685_end_event; + return SF_RESCHEDULE; +} + +void +command_config_pca9685(uint32_t *args) +{ + struct i2cpwm_s *p = oid_alloc(args[0], command_config_pca9685 + , sizeof(*p)); + uint8_t bus = args[1]; + uint8_t addr = args[2]; + p->channel = args[3]; + if (p->channel > CHANNEL_MAX) + shutdown("Invalid pca9685 channel"); + p->max_duration = args[5]; + p->fd = open_i2c(bus, addr, args[4]); +} +DECL_COMMAND(command_config_pca9685, "config_pca9685 oid=%c bus=%c addr=%c" + " channel=%c cycle_ticks=%u max_duration=%u"); + +void +command_schedule_pca9685_out(uint32_t *args) +{ + struct i2cpwm_s *p = oid_lookup(args[0], command_config_pca9685); + sched_del_timer(&p->timer); + p->timer.func = pca9685_event; + p->timer.waketime = args[1]; + p->value = args[2]; + if (p->value > VALUE_MAX) + shutdown("Invalid pca9685 value"); + sched_add_timer(&p->timer); +} +DECL_COMMAND(command_schedule_pca9685_out, + "schedule_pca9685_out oid=%c clock=%u value=%hu"); + +void +command_set_pca9685_out(uint32_t *args) +{ + uint8_t bus = args[0], addr = args[1], channel = args[2]; + uint16_t value = args[4]; + if (channel > CHANNEL_MAX || value > VALUE_MAX) + shutdown("Invalid pca9685 channel or value"); + int fd = open_i2c(bus, addr, args[3]); + pca9685_write(fd, channel, value); +} +DECL_COMMAND(command_set_pca9685_out, "set_pca9685_out bus=%c addr=%c" + " channel=%c cycle_ticks=%u value=%hu"); |