diff options
Diffstat (limited to 'src/drivers/i2c')
-rw-r--r-- | src/drivers/i2c/Kconfig | 3 | ||||
-rw-r--r-- | src/drivers/i2c/Makefile.inc | 5 | ||||
-rw-r--r-- | src/drivers/i2c/ww_ring/Kconfig | 3 | ||||
-rw-r--r-- | src/drivers/i2c/ww_ring/Makefile.inc | 1 | ||||
-rw-r--r-- | src/drivers/i2c/ww_ring/ww_ring.c | 401 | ||||
-rw-r--r-- | src/drivers/i2c/ww_ring/ww_ring.h | 30 |
6 files changed, 440 insertions, 3 deletions
diff --git a/src/drivers/i2c/Kconfig b/src/drivers/i2c/Kconfig index 5c55c44d07..1949fa6d3b 100644 --- a/src/drivers/i2c/Kconfig +++ b/src/drivers/i2c/Kconfig @@ -6,5 +6,6 @@ source src/drivers/i2c/i2cmux2/Kconfig source src/drivers/i2c/lm63/Kconfig source src/drivers/i2c/rtd2132/Kconfig source src/drivers/i2c/tpm/Kconfig -source src/drivers/i2c/w83795/Kconfig source src/drivers/i2c/w83793/Kconfig +source src/drivers/i2c/w83795/Kconfig +source src/drivers/i2c/ww_ring/Kconfig diff --git a/src/drivers/i2c/Makefile.inc b/src/drivers/i2c/Makefile.inc index 81ca409209..c69dc02158 100644 --- a/src/drivers/i2c/Makefile.inc +++ b/src/drivers/i2c/Makefile.inc @@ -1,11 +1,12 @@ subdirs-y += adm1026 subdirs-y += adm1027 subdirs-y += adt7463 +subdirs-y += at24rf08c subdirs-y += i2cmux subdirs-y += i2cmux2 subdirs-y += lm63 subdirs-y += rtd2132 subdirs-y += tpm -subdirs-y += w83795 subdirs-y += w83793 -subdirs-y += at24rf08c +subdirs-y += w83795 +subdirs-y += ww_ring diff --git a/src/drivers/i2c/ww_ring/Kconfig b/src/drivers/i2c/ww_ring/Kconfig new file mode 100644 index 0000000000..67a12eb50b --- /dev/null +++ b/src/drivers/i2c/ww_ring/Kconfig @@ -0,0 +1,3 @@ +config DRIVERS_I2C_WW_RING + bool + depends on CHROMEOS diff --git a/src/drivers/i2c/ww_ring/Makefile.inc b/src/drivers/i2c/ww_ring/Makefile.inc new file mode 100644 index 0000000000..963411ae1a --- /dev/null +++ b/src/drivers/i2c/ww_ring/Makefile.inc @@ -0,0 +1 @@ +verstage-$(CONFIG_DRIVERS_I2C_WW_RING) += ww_ring.c diff --git a/src/drivers/i2c/ww_ring/ww_ring.c b/src/drivers/i2c/ww_ring/ww_ring.c new file mode 100644 index 0000000000..b074576914 --- /dev/null +++ b/src/drivers/i2c/ww_ring/ww_ring.c @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2015 Google, Inc. + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +/* + * This is a driver for the Whirlwind LED ring, which is equipped with two LED + * microcontrollers TI LP55231 (http://www.ti.com/product/lp55231), each of + * them driving three multicolor LEDs. + * + * The only connection between the ring and the main board is an i2c bus. + * + * This driver imitates a depthcharge display device. On initialization the + * driver sets up the controllers to prepare them to accept programs to run. + * + * When a certain vboot state needs to be indicated, the program for that + * state is loaded into the controllers, resulting in the state appropriate + * LED behavior. + */ + +#include <console/console.h> +#include <device/i2c.h> +#include <string.h> + +#include "drivers/i2c/ww_ring/ww_ring.h" + +/* Number of lp55321 controllers on the ring */ +#define WW_RING_NUM_LED_CONTROLLERS 2 + +/* I2c address of the first of the controllers, the rest are contiguous. */ +#define WW_RING_BASE_ADDR 0x32 + +/* Key lp55231 registers. */ +#define LP55231_ENGCTRL1_REG 0x00 +#define LP55231_ENGCTRL2_REG 0x01 +#define LP55231_D1_CRT_CTRL_REG 0x26 +#define LP55231_MISC_REG 0x36 +#define LP55231_RESET_REG 0x3d +#define LP55231_ENG1_PROG_START 0x4c +#define LP55231_PROG_PAGE_REG 0x4f +#define LP55231_PROG_BASE_REG 0x50 + +/* LP55231_D1_CRT_CTRL_REG, default value, applies to all nine of them */ +#define LP55231_CRT_CTRL_DEFAULT 0xaf + +/* LP55231_ENGCTRL1_REG fields */ +#define LP55231_ENGCTRL1_CHIP_EN 0x40 +#define LP55231_ENGCTRL1_ALL_ENG_GO 0x2a + +/* LP55231_MISC_REG fields. */ +#define LP55231_MISC_AUTOINCR (1 << 6) +#define LP55231_MISC_PUMP_1X (1 << 3) +#define LP55231_MISC_INT_CLK (3 << 0) + +/* Goes into LP55231_RESET_REG to reset the chip. */ +#define LP55231_RESET_VALUE 0xff + +/* + * The controller has 192 bytes of SRAM for code/data, availabe as six 32 byte + * pages. + */ +#define LP55231_PROG_PAGE_SIZE 32 +#define LP55231_PROG_PAGES 6 +#define LP55231_MAX_PROG_SIZE (LP55231_PROG_PAGE_SIZE * LP55231_PROG_PAGES) + +/* There are threee independent engines/cores in the controller. */ +#define LP55231_NUM_OF_ENGINES 3 + +/* + * Structure to cache data relevant to accessing one controller. I2c interface + * to use, device address on the i2c bus and a data buffer for write + * transactions. The most bytes sent at a time is the register address plus + * the program page size. + */ +typedef struct { + unsigned i2c_bus; + uint8_t dev_addr; + uint8_t data_buffer[LP55231_PROG_PAGE_SIZE + 1]; +} TiLp55231; + +/* + * Structure to describe an lp55231 program: pointer to the text of the + * program, its size and load address (load addr + size sould not exceed + * LP55231_MAX_PROG_SIZE), and start addresses for all of the three + * engines. + */ +typedef struct { + const uint8_t *program_text; + uint8_t program_size; + uint8_t load_addr; + uint8_t engine_start_addr[LP55231_NUM_OF_ENGINES]; +} TiLp55231Program; + +/* A structure to bind controller programs to a vboot state. */ +typedef struct { + enum VbScreenType_t vb_screen; + const TiLp55231Program * programs[WW_RING_NUM_LED_CONTROLLERS]; +} WwRingStateProg; + +static void ww_ring_init(unsigned i2c_bus); + +/****************************************************************/ +/* LED ring program definitions for different vboot states. */ + +static const uint8_t blink_program_text[] = { + 0x40, 0x40, 0x9D, 0x04, 0x40, 0x40, 0x7E, + 0x00, 0x9D, 0x07, 0x40, 0x00, 0x9D, 0x04, + 0x40, 0x00, 0x7E, 0x00, 0xA0, 0x00, 0x00, + 0x00 }; + +static const TiLp55231Program led_blink_program = { + blink_program_text, + sizeof(blink_program_text), + 0, + {0, + sizeof(blink_program_text) - 2, + sizeof(blink_program_text) - 2} +}; + +static const WwRingStateProg state_programs[] = { + /* + * for test purposes the blank screen program is set to blinking, will + * be changed soon. + */ + {VB_SCREEN_BLANK, {&led_blink_program, &led_blink_program} }, + /* Other vboot state programs are coming. */ +}; +/* */ +/****************************************************************/ + +/* Controller descriptors. */ +static TiLp55231 lp55231s[WW_RING_NUM_LED_CONTROLLERS]; + +/* + * i2c transfer function for the driver. To keep things simple, the function + * repeats the transfer, if the first attempt fails. This is OK with the + * controller and makes it easier to handle errors. + * + * Note that the reset register accesses are expected to fail on writes, but + * due to a bug in the ipq806x i2c controller, the error is reported on the + * following read attempt. + * + * To work around this the driver writes and then reads the reset register, + * the transfer function ignores errors when accessing the reset register. + */ + +static int ledc_transfer(TiLp55231 *ledc, struct i2c_seg *segs, + int seg_count, int reset) +{ + int rv, max_attempts = 2; + + while (max_attempts--) { + rv = i2c_transfer(ledc->i2c_bus, segs, seg_count); + + /* Accessing reset regsiter is expected to fail. */ + if (!rv || reset) + break; + } + + if (rv) { + if (!reset) + printk(BIOS_WARNING, + "%s: dev %#x, reg %#x, %s transaction error.\n", + __func__, segs->chip, segs->buf[0], + seg_count == 1 ? "write" : "read"); + else + rv = 0; + } + + return rv; +} + +/* + * The controller is programmed to autoincrement on writes, so up to page size + * bytes can be transmitted in one write transaction. + */ +static int ledc_write(TiLp55231 *ledc, uint8_t start_addr, + const uint8_t *data, unsigned count) +{ + struct i2c_seg seg; + + if (count > (sizeof(ledc->data_buffer) - 1)) { + printk(BIOS_WARNING, "%s: transfer size too large (%d bytes)\n", + __func__, count); + return -1; + } + + memcpy(ledc->data_buffer + 1, data, count); + ledc->data_buffer[0] = start_addr; + + seg.read = 0; + seg.chip = ledc->dev_addr; + seg.buf = ledc->data_buffer; + seg.len = count + 1; + + return ledc_transfer(ledc, &seg, 1, start_addr == LP55231_RESET_REG); +} + +/* To keep things simple, read is limited to one byte at a time. */ +static int ledc_read(TiLp55231 *ledc, uint8_t addr, uint8_t *data) +{ + struct i2c_seg seg[2]; + + seg[0].read = 0; + seg[0].chip = ledc->dev_addr; + seg[0].buf = &addr; + seg[0].len = 1; + + seg[1].read = 1; + seg[1].chip = ledc->dev_addr; + seg[1].buf = data; + seg[1].len = 1; + + return ledc_transfer(ledc, seg, ARRAY_SIZE(seg), + addr == LP55231_RESET_REG); +} + +/* + * Reset transaction is expected to result in a failing i2c command, + * no need to return a value. + */ +static void ledc_reset(TiLp55231 *ledc) +{ + uint8_t data; + + data = LP55231_RESET_VALUE; + ledc_write(ledc, LP55231_RESET_REG, &data, 1); + + /* + * This read is not necessary for the chip reset, but is required to + * work around the i2c driver bug where the missing ACK on the last + * byte of the write transaction is ignored, but the next transaction + * fails. + */ + ledc_read(ledc, LP55231_RESET_REG, &data); +} + +/* + * Write a program into the internal lp55231 memory. Split write transactions + * into sections fitting into memory pages. + */ +static void ledc_write_program(TiLp55231 *ledc, uint8_t load_addr, + const uint8_t *program, unsigned count) +{ + uint8_t page_num = load_addr / LP55231_PROG_PAGE_SIZE; + unsigned page_offs = load_addr % LP55231_PROG_PAGE_SIZE; + + if ((load_addr + count) > LP55231_MAX_PROG_SIZE) { + printk(BIOS_WARNING, + "%s: program of size %#x does not fit at addr %#x\n", + __func__, count, load_addr); + return; + } + + while (count) { + unsigned segment_size = LP55231_PROG_PAGE_SIZE - page_offs; + + if (segment_size > count) + segment_size = count; + + ledc_write(ledc, LP55231_PROG_PAGE_REG, &page_num, 1); + ledc_write(ledc, LP55231_PROG_BASE_REG + page_offs, + program, segment_size); + + count -= segment_size; + page_offs = 0; + page_num++; + } +} + +/* Run an lp55231 program on a controller. */ +static void ledc_run_program(TiLp55231 *ledc, + const TiLp55231Program *program_desc) +{ + uint8_t data; + int i; + + data = 0; + ledc_write(ledc, LP55231_ENGCTRL2_REG, &data, 1); + data = 0x15; + ledc_write(ledc, LP55231_ENGCTRL2_REG, &data, 1); + ledc_write_program(ledc, program_desc->load_addr, + program_desc->program_text, + program_desc->program_size); + + for (i = 0; i < sizeof(program_desc->engine_start_addr); i++) + ledc_write(ledc, LP55231_ENG1_PROG_START + i, + program_desc->engine_start_addr + i, 1); + + data = 0; + ledc_write(ledc, LP55231_ENGCTRL2_REG, &data, 1); + data = 0x2a; + ledc_write(ledc, LP55231_ENGCTRL2_REG, &data, 1); +} + +/* + * Initialize a controller to a state were it is ready to accept programs, and + * try to confirm that we are in fact talking to a lp55231 + */ +static int ledc_init_validate(TiLp55231 *ledc) +{ + const uint8_t ctrl1_reset[] = { + 0, + LP55231_ENGCTRL1_CHIP_EN, + LP55231_ENGCTRL1_CHIP_EN | LP55231_ENGCTRL1_ALL_ENG_GO + }; + uint8_t data; + int i; + + ledc_reset(ledc); + + /* Set up all engines to run. */ + for (i = 0; i < ARRAY_SIZE(ctrl1_reset); i++) + ledc_write(ledc, LP55231_ENGCTRL1_REG, ctrl1_reset + i, 1); + + /* + * Internal clock, 3.3V output (pump 1x), autoincrement on multibyte + * writes. + */ + data = LP55231_MISC_AUTOINCR | + LP55231_MISC_PUMP_1X | LP55231_MISC_INT_CLK; + ledc_write(ledc, LP55231_MISC_REG, &data, 1); + + /* + * All nine current control registers are supposed to return the same + * value at reset. + */ + for (i = 0; i < 9; i++) { + data = 0; + ledc_read(ledc, LP55231_D1_CRT_CTRL_REG + i, &data); + if (data != LP55231_CRT_CTRL_DEFAULT) { + printk(BIOS_WARNING, + "%s: read %#2.2x from register %#x\n", __func__, + data, LP55231_D1_CRT_CTRL_REG + i); + return -1; + } + } + + return 0; +} + +/* + * Find a program matching screen type, and run it on both controllers, if + * found. + */ +int ww_ring_display_pattern(unsigned i2c_bus, enum VbScreenType_t screen_type) +{ + int i; + static int initted; + + if (!initted) { + ww_ring_init(i2c_bus); + initted = 1; + } + + for (i = 0; i < ARRAY_SIZE(state_programs); i++) + if (state_programs[i].vb_screen == screen_type) { + int j; + for (j = 0; j < WW_RING_NUM_LED_CONTROLLERS; j++) + ledc_run_program(lp55231s + j, + state_programs[i].programs[j]); + return 0; + } + + printk(BIOS_WARNING, "%s: did not find program for screen %d\n", + __func__, screen_type); + + return -1; +} + + +#define LP55231_I2C_BASE_ADDR 0x32 + +static void ww_ring_init(unsigned i2c_bus) +{ + TiLp55231 *ledc; + int i, count = 0; + + for (i = 0, ledc = lp55231s; + i < WW_RING_NUM_LED_CONTROLLERS; + i++, ledc++) { + + ledc->i2c_bus = i2c_bus; + ledc->dev_addr = LP55231_I2C_BASE_ADDR + i; + + if (!ledc_init_validate(ledc)) + count++; + } + + printk(BIOS_INFO, "WW_RING: initialized %d out of %d\n", count, i); + if (count != i) + printk(BIOS_WARNING, "WW_RING: will keep going anyway\n"); +} diff --git a/src/drivers/i2c/ww_ring/ww_ring.h b/src/drivers/i2c/ww_ring/ww_ring.h new file mode 100644 index 0000000000..628cd6c504 --- /dev/null +++ b/src/drivers/i2c/ww_ring/ww_ring.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015 Google, Inc. + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#ifndef __SRC_DRIVERS_VIDEO_WW_RING__H__ +#define __SRC_DRIVERS_VIDEO_WW_RING__H__ + +#if IS_ENABLED(CONFIG_CHROMEOS) +#include <vboot_api.h> + +/* + * ww_ring_display_pattern + * + * Display pattern on the ring LEDs. + */ +int ww_ring_display_pattern(unsigned i2c_bus, enum VbScreenType_t screen_type); + +#else +static inline int ww_ring_display_pattern(unsigned i2c_bus, int screen_type) { return 0;} +#endif +#endif |