blob: 95bb4ae00d26ee77cc819972e5b59f2edf963841 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <ctype.h>
#include <errno.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <threads.h>
#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/platform-defs.h>
#include <ddk/protocol/i2c.h>
#include <zircon/types.h>
// https://www.nxp.com/docs/en/data-sheet/PCA9956A.pdf
// See 7.3 Register definitions
#define PCA9956_MODE_REGISTER_1 0x00
#define PCA9956_MODE_REGISTER_2 0x01
#define PCA9956_DUTY_CYCLE 0x08
#define PCA9956_PWM_BASE 0x0A
#define PCA9956_IREF_BASE 0x22
#define PCA9956_IREFALL 0x40
// If bit is set in an address, reads and writes will auto increment.
// See 7.2 Control register
#define AUTO_INCREMENT_ADDRESS_MASK 0x80
#define NUM_PWM_CHANNELS 24
// 3 decimal digits + 1 whitespace
#define UINT8_PRINT_SIZE 4
typedef struct gauss_led {
// All i2c operations should take the lock to make sure reset is atomic.
mtx_t lock;
zx_device_t* device;
i2c_protocol_t i2c;
i2c_channel_t channel;
} gauss_led_t;
// Driver subpath types
typedef enum path_type {
PATH_NONE,
PATH_RESET,
PATH_PWM,
PATH_DUTY_CYCLE,
} path_type_t;
static const struct {
const char* str;
path_type_t path;
} kPathStrToPath[] = {
{"reset", PATH_RESET},
{"pwm", PATH_PWM},
{"duty_cycle", PATH_DUTY_CYCLE},
};
// Device instance
typedef struct gauss_led_dev {
path_type_t path;
gauss_led_t* led;
iotxn_t* txn;
} gauss_led_dev_t;
// Attempts to parse a decmial uint8 from |*buf|. Will update |buf| if parsing
// is successful. Will skip whitespace before and after the number.
static int parse_uint8(char** buf) {
unsigned value;
int num_chars;
if (sscanf(*buf, "%u%n", &value, &num_chars) != 1) {
return -1;
}
if (value > UINT8_MAX) {
return -1;
}
*buf += num_chars;
while (isspace(**buf)) {
++(*buf);
}
return value;
}
static void gauss_led_handle_read_complete(zx_status_t status,
const uint8_t* data,
size_t actual,
gauss_led_dev_t* dev,
iotxn_t* txn) {
char buf[256] = {};
switch (dev->path) {
case PATH_PWM: {
if (actual < NUM_PWM_CHANNELS) {
zxlogf(ERROR, "Failed to read pcm channels\n");
iotxn_complete(txn, ZX_ERR_INTERNAL, 0);
return;
}
size_t remain = sizeof(buf);
char* cur = buf;
for (size_t i = 0; i < actual; ++i) {
int ret = snprintf(cur, remain, "%hhu ", data[i]);
if (ret < 0) {
zxlogf(ERROR, "snprintf failed\n");
iotxn_complete(txn, ZX_ERR_INTERNAL, 0);
return;
}
cur += ret;
remain -= ret;
}
*(cur - 1) = '\n';
break;
}
case PATH_DUTY_CYCLE: {
if (actual < 1) {
zxlogf(ERROR, "Failed to read duty cycle value\n");
iotxn_complete(txn, ZX_ERR_INTERNAL, 0);
return;
}
int len = snprintf(buf, sizeof(buf), "%hhu\n", data[0]);
if (len < 0) {
zxlogf(ERROR, "snprintf failed\n");
iotxn_complete(txn, ZX_ERR_INTERNAL, 0);
return;
}
break;
}
default:
assert(false);
}
size_t len = strlen(buf);
assert(len <= txn->length);
iotxn_copyto(txn, buf, len, 0);
iotxn_complete(txn, ZX_OK, len);
}
static void i2c_complete(zx_status_t status, const uint8_t* data, size_t actual,
void* cookie) {
gauss_led_dev_t* dev = cookie;
if (!dev) {
if (status != ZX_OK) {
zxlogf(ERROR, "i2c transaction failed\n");
}
return;
}
assert(dev->txn);
iotxn_t* txn = dev->txn;
dev->txn = NULL;
if (txn->opcode == IOTXN_OP_READ) {
gauss_led_handle_read_complete(status, data, actual, dev, txn);
} else if (txn->opcode == IOTXN_OP_WRITE) {
iotxn_complete(txn, status, txn->length);
} else {
zxlogf(ERROR, "Unexpected transaction type\n");
assert(false);
}
}
static zx_status_t gauss_led_i2c_transact(gauss_led_t* gauss_led,
uint8_t* write_buf,
size_t write_len,
size_t read_len,
gauss_led_dev_t* dev) {
mtx_lock(&gauss_led->lock);
zx_status_t status = i2c_write_read(&gauss_led->channel, write_buf, write_len,
read_len, i2c_complete, dev);
mtx_unlock(&gauss_led->lock);
if (status != ZX_OK) {
zxlogf(ERROR, "Failed to write to I2C: %d\n", (int)status);
return status;
}
return status;
}
static zx_status_t gauss_led_reset_helper(gauss_led_t* gauss_led) {
zx_status_t status = ZX_OK;
// Set max gain control for all IREF registers.
{
uint8_t buf[] = {PCA9956_IREFALL, 0xff};
if ((status = i2c_write_read(&gauss_led->channel, buf, sizeof(buf), 0,
i2c_complete, NULL)) != ZX_OK) {
return status;
}
}
// Enable auto-increment for registers 00h to 39h.
{
uint8_t buf[] = {PCA9956_MODE_REGISTER_1, 0x40};
if ((status = i2c_write_read(&gauss_led->channel, buf, sizeof(buf), 0,
i2c_complete, NULL)) != ZX_OK) {
return status;
}
}
// Reset MODE2
// Set LEDOUT0 - LEDOUT5 to max
// Initialize GRPPWM (pwm duty cycle)
{
uint8_t buf[] = {PCA9956_MODE_REGISTER_2 | AUTO_INCREMENT_ADDRESS_MASK,
0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x08};
if ((status = i2c_write_read(&gauss_led->channel, buf, sizeof(buf), 0,
i2c_complete, NULL)) != ZX_OK) {
return status;
}
}
// Turn off all LEDs
uint8_t buf[NUM_PWM_CHANNELS + 1] =
{PCA9956_PWM_BASE | AUTO_INCREMENT_ADDRESS_MASK};
if ((status = i2c_write_read(&gauss_led->channel, buf, sizeof(buf), 0,
i2c_complete, NULL)) != ZX_OK) {
return status;
}
return status;
}
static zx_status_t gauss_led_reset(gauss_led_t* gauss_led) {
mtx_lock(&gauss_led->lock);
zx_status_t status = gauss_led_reset_helper(gauss_led);
mtx_unlock(&gauss_led->lock);
if (status != ZX_OK) {
zxlogf(ERROR, "Failed to initialize LED controller: %d\n", (int)status);
return status;
}
return status;
}
static zx_status_t gauss_led_dev_get_pwm(gauss_led_dev_t* dev, iotxn_t* txn) {
size_t min_len = NUM_PWM_CHANNELS * UINT8_PRINT_SIZE;
if (txn->length < min_len) {
zxlogf(ERROR, "Read is too short, must be atleast %zd\n", min_len);
return ZX_ERR_BUFFER_TOO_SMALL;
}
uint8_t write_buf[] = {PCA9956_PWM_BASE | AUTO_INCREMENT_ADDRESS_MASK};
zx_status_t status = gauss_led_i2c_transact(dev->led, write_buf,
sizeof(write_buf),
NUM_PWM_CHANNELS, dev);
if (status != ZX_OK) {
zxlogf(ERROR, "Failed to get pwm values: %d\n", (int)status);
return status;
}
return status;
}
static zx_status_t gauss_led_dev_set_pwm(gauss_led_dev_t* dev, iotxn_t* txn) {
char buf[512];
if (txn->length > sizeof(buf) - 1) {
zxlogf(ERROR, "Write is too long\n");
return ZX_ERR_INVALID_ARGS;
}
ssize_t ret = iotxn_copyfrom(txn, buf, sizeof(buf), 0);
if (ret < 0) {
zxlogf(ERROR, "Failed to copy data\n");
return ZX_ERR_INTERNAL;
}
buf[txn->length] = '\0';
uint8_t write_buf[NUM_PWM_CHANNELS + 1] =
{PCA9956_PWM_BASE | AUTO_INCREMENT_ADDRESS_MASK};
size_t idx = 0;
char* cur = buf;
while (*cur) {
if (idx >= NUM_PWM_CHANNELS) {
zxlogf(ERROR, "Too many values, expected %d\n", NUM_PWM_CHANNELS);
return ZX_ERR_INVALID_ARGS;
}
int val = parse_uint8(&cur);
if (val < 0) {
zxlogf(ERROR, "Invalid RGB value\n");
return ZX_ERR_INVALID_ARGS;
}
write_buf[1 + idx] = val;
++idx;
}
if (idx != NUM_PWM_CHANNELS) {
zxlogf(ERROR, "Not enough values, expected %d\n", NUM_PWM_CHANNELS);
return ZX_ERR_INVALID_ARGS;
}
zx_status_t status = gauss_led_i2c_transact(dev->led, write_buf,
sizeof(write_buf), 0, dev);
if (status != ZX_OK) {
zxlogf(ERROR, "Failed to set LEDs: %d\n", (int)status);
return status;
}
return status;
}
static zx_status_t gauss_led_dev_get_duty_cycle(gauss_led_dev_t* dev,
iotxn_t* txn) {
size_t min_len = UINT8_PRINT_SIZE;
if (txn->length < min_len) {
zxlogf(ERROR, "Read is too short, must be atleast %d\n",
UINT8_PRINT_SIZE);
return ZX_ERR_BUFFER_TOO_SMALL;
}
uint8_t write_buf[] = {PCA9956_DUTY_CYCLE};
zx_status_t status = gauss_led_i2c_transact(dev->led, write_buf,
sizeof(write_buf), 1, dev);
if (status != ZX_OK) {
zxlogf(ERROR, "Failed to get pwm values: %d\n", (int)status);
return status;
}
return status;
}
static zx_status_t gauss_led_dev_set_duty_cycle(gauss_led_dev_t* dev,
iotxn_t* txn) {
char buf[64];
if (txn->length > sizeof(buf) - 1) {
zxlogf(ERROR, "Write is too long\n");
return ZX_ERR_INVALID_ARGS;
}
ssize_t ret = iotxn_copyfrom(txn, buf, sizeof(buf), 0);
if (ret < 0) {
zxlogf(ERROR, "Failed to copy data\n");
return ZX_ERR_INTERNAL;
}
buf[txn->length] = '\0';
char* cur = buf;
int value = parse_uint8(&cur);
if (value < -1) {
zxlogf(ERROR, "Invalid value\n");
return ZX_ERR_INVALID_ARGS;
}
if (*cur != '\0') {
zxlogf(ERROR, "Trailing data\n");
return ZX_ERR_INVALID_ARGS;
}
uint8_t write_buf[] = {PCA9956_DUTY_CYCLE, value};
zx_status_t status = gauss_led_i2c_transact(dev->led, write_buf,
sizeof(write_buf), 0, dev);
if (status != ZX_OK) {
zxlogf(ERROR, "Failed to set LEDs: %d\n", (int)status);
return status;
}
return status;
}
static void gauss_led_dev_handle_read(gauss_led_dev_t* dev, iotxn_t* txn) {
zx_status_t status = ZX_OK;
switch (dev->path) {
case PATH_RESET:
status = ZX_ERR_NOT_SUPPORTED;
break;
case PATH_PWM:
status = gauss_led_dev_get_pwm(dev, txn);
break;
case PATH_DUTY_CYCLE:
status = gauss_led_dev_get_duty_cycle(dev, txn);
break;
default:
status = ZX_ERR_NOT_SUPPORTED;
zxlogf(ERROR, "Unsupported path type: %d\n", (int)dev->path);
assert(false);
break;
}
if (status != ZX_OK) {
iotxn_complete(txn, status, 0);
}
}
static void gauss_led_dev_handle_write(gauss_led_dev_t* dev, iotxn_t* txn) {
zx_status_t status = ZX_OK;
switch (dev->path) {
case PATH_RESET:
// For resets, we don't want to block iotxn_complete on the i2c writes.
dev->txn = NULL;
iotxn_complete(txn, gauss_led_reset(dev->led), txn->length);
return;
case PATH_PWM:
status = gauss_led_dev_set_pwm(dev, txn);
break;
case PATH_DUTY_CYCLE:
status = gauss_led_dev_set_duty_cycle(dev, txn);
break;
default:
status = ZX_ERR_NOT_SUPPORTED;
zxlogf(ERROR, "Unsupported path type: %d\n", (int)dev->path);
assert(false);
break;
}
if (status != ZX_OK) {
iotxn_complete(txn, status, 0);
}
}
static void gauss_led_dev_iotxn_queue(void* ctx, iotxn_t* txn) {
if (txn->offset > 0) {
iotxn_complete(txn, ZX_OK, 0);
return;
}
gauss_led_dev_t* dev = ctx;
if (dev->txn != NULL) {
zxlogf(ERROR, "Transaction already pending\n");
iotxn_complete(txn, ZX_ERR_BAD_STATE, 0);
return;
}
dev->txn = txn;
if (txn->opcode == IOTXN_OP_READ) {
gauss_led_dev_handle_read(dev, txn);
} else if (txn->opcode == IOTXN_OP_WRITE) {
gauss_led_dev_handle_write(dev, txn);
} else {
iotxn_complete(txn, ZX_ERR_INVALID_ARGS, 0);
}
}
static void gauss_led_dev_release(void* ctx) {
gauss_led_dev_t* dev = ctx;
free(dev);
}
static zx_protocol_device_t gauss_led_dev_ops = {
.version = DEVICE_OPS_VERSION,
.iotxn_queue = gauss_led_dev_iotxn_queue,
.release = gauss_led_dev_release,
};
static zx_status_t gauss_led_open_at(void* ctx, zx_device_t** dev_out,
const char* path, uint32_t flags) {
gauss_led_t* gauss_led = ctx;
path_type_t path_type = PATH_NONE;
for (size_t i = 0; i < countof(kPathStrToPath); ++i) {
if (strcmp(kPathStrToPath[i].str, path) == 0) {
path_type = kPathStrToPath[i].path;
break;
}
}
if (path_type == PATH_NONE) {
return ZX_ERR_NOT_SUPPORTED;
}
gauss_led_dev_t* dev = calloc(1, sizeof(*dev));
if (!dev) {
return ZX_ERR_NO_MEMORY;
}
dev->path = path_type;
dev->led = gauss_led;
device_add_args_t args = {
.version = DEVICE_ADD_ARGS_VERSION,
.name = "led",
.ctx = dev,
.ops = &gauss_led_dev_ops,
.flags = DEVICE_ADD_INSTANCE,
};
zx_device_t* tmp_out;
zx_status_t status = device_add(gauss_led->device, &args, &tmp_out);
if (status != ZX_OK) {
free(dev);
return status;
}
*dev_out = tmp_out;
return ZX_OK;
}
static void gauss_led_release(void* ctx) {
gauss_led_t* gauss_led = ctx;
i2c_channel_release(&gauss_led->channel);
free(gauss_led);
}
static zx_protocol_device_t gauss_led_device_protocol = {
.version = DEVICE_OPS_VERSION,
.open_at = gauss_led_open_at,
.release = gauss_led_release,
};
static zx_status_t gauss_led_bind(void* ctx, zx_device_t* parent) {
gauss_led_t* gauss_led = calloc(1, sizeof(*gauss_led));
if (!gauss_led) {
return ZX_ERR_NO_MEMORY;
}
if (device_get_protocol(parent, ZX_PROTOCOL_I2C,
&gauss_led->i2c) != ZX_OK) {
free(gauss_led);
return ZX_ERR_NOT_SUPPORTED;
}
zx_status_t status = i2c_get_channel(&gauss_led->i2c, 0,
&gauss_led->channel);
if (status != ZX_OK) {
free(gauss_led);
zxlogf(ERROR, "Failed to get channel: %d\n", (int)status);
return status;
}
device_add_args_t args = {
.version = DEVICE_ADD_ARGS_VERSION,
.name = "gauss-led",
.ctx = gauss_led,
.ops = &gauss_led_device_protocol,
};
if ((status = device_add(parent, &args, &gauss_led->device)) != ZX_OK) {
free(gauss_led);
return status;
}
if ((status = gauss_led_reset(gauss_led)) != ZX_OK) {
free(gauss_led);
return status;
}
return status;
}
static zx_driver_ops_t i2c_led_driver_ops = {
.version = DRIVER_OPS_VERSION,
.bind = gauss_led_bind,
};
// clang-format off
ZIRCON_DRIVER_BEGIN(gauss_i2c_led, i2c_led_driver_ops, "zircon", "0.1", 4)
BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_PDEV),
BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_GOOGLE),
BI_ABORT_IF(NE, BIND_PLATFORM_DEV_PID, PDEV_PID_GAUSS),
BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_DID, PDEV_DID_GAUSS_LED),
ZIRCON_DRIVER_END(gauss_i2c_led)
// clang-format on