| // 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 |