From: Thomas Cort Date: Mon, 29 Jul 2013 16:21:33 +0000 (-0400) Subject: tda19988: driver for the TDA19988 HDMI Transmitter X-Git-Tag: v3.3.0~880 X-Git-Url: http://zhaoyanbai.com/repos/%22http:/www.isc.org/icons/already%20found%20%2A/doxygen-warnings.log?a=commitdiff_plain;h=1b78e86fb85a0e2b90d5b3556195302212895655;p=minix.git tda19988: driver for the TDA19988 HDMI Transmitter Change-Id: Ia7750df3dd4ec4bd68624c800a0241c70eea7ca4 --- diff --git a/distrib/sets/lists/minix/md.evbarm b/distrib/sets/lists/minix/md.evbarm index e671d2f73..70d70eaa9 100644 --- a/distrib/sets/lists/minix/md.evbarm +++ b/distrib/sets/lists/minix/md.evbarm @@ -109,5 +109,6 @@ ./usr/sbin/gpio minix-sys ./usr/sbin/i2c minix-sys ./usr/sbin/random minix-sys +./usr/sbin/tda19988 minix-sys ./usr/tests/minix-posix/mod minix-sys ./usr/tests/minix-posix/test63 minix-sys diff --git a/drivers/Makefile b/drivers/Makefile index 99fe499c2..3a5e7c353 100644 --- a/drivers/Makefile +++ b/drivers/Makefile @@ -23,7 +23,7 @@ SUBDIR= ahci amddev atl2 at_wini audio dec21140A dp8390 dpeth \ .endif .if ${MACHINE_ARCH} == "earm" -SUBDIR= cat24c256 fb gpio i2c mmc log tty random +SUBDIR= cat24c256 fb gpio i2c mmc log tda19988 tty random .endif .endif # ${MKIMAGEONLY} != "yes" diff --git a/drivers/tda19988/Makefile b/drivers/tda19988/Makefile new file mode 100644 index 000000000..f9605d9e0 --- /dev/null +++ b/drivers/tda19988/Makefile @@ -0,0 +1,14 @@ +# Makefile for the tda19988 HDMI framer found on the BeagleBone Black. +PROG= tda19988 +SRCS= tda19988.c + +DPADD+= ${LIBI2CDRIVER} ${LIBBLOCKDRIVER} ${LIBSYS} ${LIBTIMERS} +LDADD+= -li2cdriver -lblockdriver -lsys -ltimers + +MAN= + +BINDIR?= /usr/sbin + +CPPFLAGS+= -I${NETBSDSRCDIR} + +.include diff --git a/drivers/tda19988/README.txt b/drivers/tda19988/README.txt new file mode 100644 index 000000000..7b47e66a4 --- /dev/null +++ b/drivers/tda19988/README.txt @@ -0,0 +1,79 @@ +TDA19988 Driver (HDMI Framer) +============================= + +Overview +-------- + +This is the driver for the HDMI Framer chip commonly found on the BeagleBone +Black. + +Interface +--------- + +To make things easy for the frame buffer driver, a block device driver +interface is provided. Read requests cause the TDA19988 driver to read +the EDID and return the data. + +Documentation +------------- + +The documentation available is rather thin. NXP doesn't provide a +datasheet on their website and they did not respond to my request +for a datasheet. There are a few other sources of information: + + * TDA9983B.pdf - this chip is similar but not the same. Full + register descriptions are provided. This was suggested to me by + the author of the Linux driver and someone at BeagleBoard.org as + neither of them were able to get a full datasheet for the TDA19988. + + * TDA19988.pdf - you can probably find this on the net. It's a + pre-production draft of the datasheet for the TDA19988. It has + some information about how things work, but it doesn't give details + on the registers. + + * LPC4350_FPU_TFT_HDMI-v2.0/Driver/tda19988.c - there's some + example code from NXP for a Hitex LPC4350 REV A5 which includes + a BSD licensed driver from NXP. It isn't complete as it only does + test mode, put-through mode, and EDID reading. Some of the comments + in the code make it seem like it isn't totally working/tested. + + * linux/drivers/gpu/drm/i2c/tda998x_drv.c - there is a Linux driver + which implements a lot of functionality. As is always the case, + great care has to be taken to only study the code to learn the + functionality of the chip and not reproduce/copy the code. + +Limitations +----------- + +Currently, only the EDID reading functionality is implemented. + +Testing the Code +---------------- + +Starting up an instance: + +/bin/service up /usr/sbin/tda19988 -label tda19988.1.3470 \ + -args 'cec_bus=1 cec_address=0x34 hdmi_bus=1 hdmi_address=0x70' + +Killing an instance: + +/bin/service down tda19988.1.3470 + +The driver is meant to be accessed from other drivers using the block +device protocol, so it doesn't have a reserved major number and device file. +However, if you want a simple test from user space, you can create a temporary +device file to read the EDID like this: + +cd /dev +mknod tda19988 b 32 0 +chmod 600 tda19988 +/bin/service up /usr/sbin/tda19988 -label tda19988.1.3470 \ + -dev /dev/tda19988 \ + -args 'cec_bus=1 cec_address=0x34 hdmi_bus=1 hdmi_address=0x70' +dd if=/dev/tda19988 of=/root/edid.dat count=1 bs=128 +/bin/service down tda19988.1.3470 +hexdump -C /root/edid.dat +rm tda19988 + +The hexdump should begin with the EDID magic number: 00 ff ff ff ff ff ff 00 + diff --git a/drivers/tda19988/tda19988.c b/drivers/tda19988/tda19988.c new file mode 100644 index 000000000..0d19422de --- /dev/null +++ b/drivers/tda19988/tda19988.c @@ -0,0 +1,1086 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/* constants */ +#define NR_DEVS 1 /* number of devices this driver handles */ +#define TDA19988_DEV 0 /* index of TDA19988 device */ +#define EDID_LEN 128 /* length of standard EDID block */ + +/* When passing data over a grant one needs to pass + * a buffer to sys_safecopy copybuff is used for that*/ +#define COPYBUF_SIZE 0x1000 /* 4k buf */ +static unsigned char copybuf[COPYBUF_SIZE]; + +/* The device has two I2C interfaces CEC (0x34) and HDMI (0x70). This driver + * needs access to both. + */ + +/* + * CEC - Register and Bit Definitions + */ + +#define CEC_STATUS_REG 0xfe +#define CEC_STATUS_CONNECTED_MASK 0x02 + +#define CEC_ENABLE_REG 0xff +#define CEC_ENABLE_ALL_MASK 0x87 + +/* + * HDMI - Pages + * + * The HDMI part is much bigger than the CEC part. Memory is accessed according + * to page and address. Once the page is set, only the address needs to be + * sent if accessing memory locations within the same page (you don't need to + * send the page number every time). + */ + +#define HDMI_CTRL_PAGE 0x00 +#define HDMI_PPL_PAGE 0x02 +#define HDMI_EDID_PAGE 0x09 +#define HDMI_INFO_PAGE 0x10 +#define HDMI_AUDIO_PAGE 0x11 +#define HDMI_HDCP_OTP_PAGE 0x12 +#define HDMI_GAMUT_PAGE 0x13 + +/* + * The page select register isn't part of a page. A dummy value of 0xff is + * used to signfiy this in the code. + */ +#define HDMI_PAGELESS 0xff + +/* + * Control Page Registers and Bit Definitions + */ + +#define HDMI_CTRL_REV_LO_REG 0x00 +#define HDMI_CTRL_REV_HI_REG 0x02 + +#define HDMI_CTRL_RESET_REG 0x0a +#define HDMI_CTRL_RESET_DDC_MASK 0x02 + +#define HDMI_CTRL_DDC_CTRL_REG 0x0b +#define HDMI_CTRL_DDC_EN_MASK 0x00 + +#define HDMI_CTRL_DDC_CLK_REG 0x0c +#define HDMI_CTRL_DDC_CLK_EN_MASK 0x01 + +#define HDMI_CTRL_INTR_CTRL_REG 0x0f +#define HDMI_CTRL_INTR_EN_GLO_MASK 0x04 + +#define HDMI_CTRL_INT_REG 0x11 +#define HDMI_CTRL_INT_EDID_MASK 0x02 + +/* + * EDID Page Registers and Bit Definitions + */ + +#define HDMI_EDID_DATA_REG 0x00 + +#define HDMI_EDID_DEV_ADDR_REG 0xfb +#define HDMI_EDID_DEV_ADDR 0xa0 + +#define HDMI_EDID_OFFSET_REG 0xfc +#define HDMI_EDID_OFFSET 0x00 + +#define HDMI_EDID_SEG_PTR_ADDR_REG 0xfc +#define HDMI_EDID_SEG_PTR_ADDR 0x00 + +#define HDMI_EDID_SEG_ADDR_REG 0xfe +#define HDMI_EDID_SEG_ADDR 0x00 + +#define HDMI_EDID_REQ_REG 0xfa +#define HDMI_EDID_REQ_READ_MASK 0x01 + +/* + * HDCP and OTP + */ +#define HDMI_HDCP_OTP_DDC_CLK_REG 0x9a +#define HDMI_HDCP_OTP_DDC_CLK_MASK 0x27 + +/* this register/mask isn't documented but it has to be cleared/set */ +#define HDMI_HDCP_OTP_SOME_REG 0x9b +#define HDMI_HDCP_OTP_SOME_MASK 0x02 + +/* + * Pageless Registers + */ + +#define HDMI_PAGE_SELECT_REG 0xff + +/* + * Constants + */ + +/* Revision of the TDA19988. */ +#define HDMI_REV_TDA19988 0x0331 + +/* the bus that this device is on (counting starting at 1) */ +static uint32_t cec_bus; +static uint32_t hdmi_bus; + +/* slave address of the device */ +static i2c_addr_t cec_address; +static i2c_addr_t hdmi_address; + +/* endpoint for the driver for the bus itself. */ +static endpoint_t cec_bus_endpoint; +static endpoint_t hdmi_bus_endpoint; + +/* logging - use with log_warn(), log_info(), log_debug(), log_trace(), etc */ +static struct log log = { + .name = "tda19988", + .log_level = LEVEL_INFO, + .log_func = default_log +}; + +static void sef_local_startup(void); +static int sef_cb_lu_state_save(int); +static int lu_state_restore(void); +static int sef_cb_init(int type, sef_init_info_t * info); + +/* CEC Module */ +static int is_display_connected(void); +static int enable_hdmi_module(void); + +/* HDMI Module */ +static int set_page(uint8_t page); +static int hdmi_read(uint8_t page, uint8_t reg, uint8_t * val); +static int hdmi_write(uint8_t page, uint8_t reg, uint8_t val); +static int hdmi_set(uint8_t page, uint8_t reg, uint8_t mask); +static int hdmi_clear(uint8_t page, uint8_t reg, uint8_t mask); + +static int hdmi_ddc_enable(void); +static int hdmi_init(void); +static int check_revision(void); +static int read_edid(uint8_t * data, size_t count); + +/* libblockdriver callbacks */ +static int tda19988_blk_open(dev_t minor, int access); +static int tda19988_blk_close(dev_t minor); +static ssize_t tda19988_blk_transfer(dev_t minor, int do_write, u64_t pos, + endpoint_t endpt, iovec_t * iov, unsigned count, int flags); +static int tda19988_blk_ioctl(dev_t minor, unsigned int request, + endpoint_t endpt, cp_grant_id_t grant); +static struct device *tda19988_blk_part(dev_t minor); +static int tda19988_blk_other(message * m); + +/* Entry points into the device dependent code of block drivers. */ +struct blockdriver tda19988_tab = { + .bdr_type = BLOCKDRIVER_TYPE_OTHER, + .bdr_open = tda19988_blk_open, + .bdr_close = tda19988_blk_close, + .bdr_transfer = tda19988_blk_transfer, + .bdr_ioctl = tda19988_blk_ioctl, /* nop -- always returns EINVAL */ + .bdr_cleanup = NULL, /* nothing allocated -- nothing to clean up */ + .bdr_part = tda19988_blk_part, + .bdr_geometry = NULL, /* no geometry (cylinders, heads, sectors, etc) */ + .bdr_intr = NULL, /* i2c devices don't generate interrupts */ + .bdr_alarm = NULL, /* alarm not needed */ + .bdr_other = tda19988_blk_other, /* to recv notify events from DS */ + .bdr_device = NULL /* 1 insance per device, threads not needed */ +}; + +/* counts the number of times a device file is open */ +static int openct[NR_DEVS]; + +/* base and size of each device */ +static struct device geom[NR_DEVS]; + +static int +tda19988_blk_open(dev_t minor, int access) +{ + log_trace(&log, "tda19988_blk_open(%d,%d)\n", minor, access); + if (tda19988_blk_part(minor) == NULL) { + return ENXIO; + } + + openct[minor]++; + + return OK; +} + +static int +tda19988_blk_close(dev_t minor) +{ + log_trace(&log, "tda19988_blk_close(%d)\n", minor); + if (tda19988_blk_part(minor) == NULL) { + return ENXIO; + } + + if (openct[minor] < 1) { + log_warn(&log, "closing unopened device %d\n", minor); + return EINVAL; + } + openct[minor]--; + + return OK; +} + +static ssize_t +tda19988_blk_transfer(dev_t minor, int do_write, u64_t pos64, + endpoint_t endpt, iovec_t * iov, unsigned nr_req, int flags) +{ + unsigned count; + struct device *dv; + u64_t dv_size; + int r; + u64_t position; + cp_grant_id_t grant; + + log_trace(&log, "tda19988_blk_transfer()\n"); + + /* Get minor device information. */ + dv = tda19988_blk_part(minor); + if (dv == NULL) { + return ENXIO; + } + + if (nr_req > NR_IOREQS) { + return EINVAL; + } + + dv_size = dv->dv_size; + if (pos64 >= dv_size) { + return OK; /* Beyond EOF */ + } + + if (nr_req > 0) { + + /* How much to transfer and where to / from. */ + count = iov->iov_size; + grant = (cp_grant_id_t) iov->iov_addr; + + /* check for EOF */ + if (pos64 >= dv_size) { + return 0; + } + + /* don't go past the end of the device */ + if (pos64 + count > dv_size) { + count = dv_size - position; + } + + /* don't overflow copybuf */ + if (count > COPYBUF_SIZE) { + count = COPYBUF_SIZE; + } + + log_debug(&log, "transfering 0x%x bytes\n", count); + + if (do_write) { + + log_warn(&log, "Error: writing to read-only device\n"); + return EACCES; + + } else { + + if (is_display_connected() == 1) { + + r = hdmi_init(); + if (r != OK) { + log_warn(&log, + "Failed to enable HDMI module\n"); + return EIO; + } + + memset(copybuf, '\0', COPYBUF_SIZE); + r = read_edid(copybuf, count); + if (r != OK) { + log_warn(&log, + "read_edid() failed (r=%d)\n", r); + return r; + } + + r = sys_safecopyto(endpt, grant, (vir_bytes) + 0, (vir_bytes) copybuf, count); + if (r != OK) { + log_warn(&log, "safecopyto failed\n"); + return EINVAL; + } + + return iov->iov_size; + } else { + log_warn(&log, "Display not connected.\n"); + return ENODEV; + } + } + } else { + + /* empty request */ + return 0; + } +} + +static int +tda19988_blk_ioctl(dev_t minor, unsigned int request, endpoint_t endpt, + cp_grant_id_t grant) +{ + log_trace(&log, "tda19988_blk_ioctl(%d)\n", minor); + /* no supported ioctls for this device */ + return EINVAL; +} + +static struct device * +tda19988_blk_part(dev_t minor) +{ + log_trace(&log, "tda19988_blk_part(%d)\n", minor); + + if (minor >= NR_DEVS) { + return NULL; + } + + return &geom[minor]; +} + +static int +tda19988_blk_other(message * m) +{ + int r; + + log_trace(&log, "tda19988_blk_other(0x%x)\n", m->m_type); + + switch (m->m_type) { + case NOTIFY_MESSAGE: + if (m->m_source == DS_PROC_NR) { + log_debug(&log, + "bus driver changed state, update endpoint\n"); + i2cdriver_handle_bus_update(&cec_bus_endpoint, cec_bus, + cec_address); + i2cdriver_handle_bus_update(&hdmi_bus_endpoint, + hdmi_bus, hdmi_address); + } + r = OK; + break; + default: + log_warn(&log, "Invalid message type (0x%x)\n", m->m_type); + r = EINVAL; + break; + } + + return r; +} + +/* + * Check to see if a display is connected. + * Returns 1 for yes, 0 for no, -1 for error. + */ +static int +is_display_connected(void) +{ + int r; + minix_i2c_ioctl_exec_t ioctl_exec; + + memset(&ioctl_exec, '\0', sizeof(minix_i2c_ioctl_exec_t)); + + /* Read from CEC */ + ioctl_exec.iie_op = I2C_OP_READ_WITH_STOP; + ioctl_exec.iie_addr = cec_address; + + /* write the register address */ + ioctl_exec.iie_cmd[0] = CEC_STATUS_REG; + ioctl_exec.iie_cmdlen = 1; + + /* read 1 byte */ + ioctl_exec.iie_buflen = 1; + + r = i2cdriver_exec(cec_bus_endpoint, &ioctl_exec); + if (r != OK) { + log_warn(&log, "Reading connection status failed (r=%d)\n", r); + return -1; + } + + if ((CEC_STATUS_CONNECTED_MASK & ioctl_exec.iie_buf[0]) == 0) { + log_debug(&log, "No Display Detected\n"); + return 0; + } else { + log_debug(&log, "Display Detected\n"); + return 1; + } +} + +/* + * Enable all the modules and clocks. + */ +static int +enable_hdmi_module(void) +{ + int r; + minix_i2c_ioctl_exec_t ioctl_exec; + + memset(&ioctl_exec, '\0', sizeof(minix_i2c_ioctl_exec_t)); + + /* Write to CEC */ + ioctl_exec.iie_op = I2C_OP_WRITE_WITH_STOP; + ioctl_exec.iie_addr = cec_address; + + /* write the register address and value */ + ioctl_exec.iie_buf[0] = CEC_ENABLE_REG; + ioctl_exec.iie_buf[1] = CEC_ENABLE_ALL_MASK; + ioctl_exec.iie_buflen = 2; + + r = i2cdriver_exec(cec_bus_endpoint, &ioctl_exec); + if (r != OK) { + log_warn(&log, "Writing enable bits failed (r=%d)\n", r); + return -1; + } + + log_debug(&log, "HDMI module enabled\n"); + + return OK; +} + +static int +set_page(uint8_t page) +{ + int r; + static int current_page = HDMI_PAGELESS; + + if (page != current_page) { + + r = hdmi_write(HDMI_PAGELESS, HDMI_PAGE_SELECT_REG, page); + if (r != OK) { + return r; + } + + current_page = page; + } + + return OK; +} + +static int +hdmi_read_block(uint8_t page, uint8_t reg, uint8_t * buf, size_t buflen) +{ + + int r; + minix_i2c_ioctl_exec_t ioctl_exec; + + if (buf == NULL || buflen > I2C_EXEC_MAX_BUFLEN) { + log_warn(&log, + "Read block called with NULL pointer or invalid buflen.\n"); + return EINVAL; + } + + if (page != HDMI_PAGELESS) { + r = set_page(page); + if (r != OK) { + log_warn(&log, "Unable to set page to 0x%x\n", page); + return r; + } + } + + memset(&ioctl_exec, '\0', sizeof(minix_i2c_ioctl_exec_t)); + + /* Read from HDMI */ + ioctl_exec.iie_op = I2C_OP_READ_WITH_STOP; + ioctl_exec.iie_addr = hdmi_address; + + /* write the register address */ + ioctl_exec.iie_cmd[0] = reg; + ioctl_exec.iie_cmdlen = 1; + + /* read bytes */ + ioctl_exec.iie_buflen = buflen; + + r = i2cdriver_exec(hdmi_bus_endpoint, &ioctl_exec); + if (r != OK) { + log_warn(&log, "hdmi_read() failed (r=%d)\n", r); + return -1; + } + + memcpy(buf, ioctl_exec.iie_buf, buflen); + + log_trace(&log, "Read %d bytes from reg 0x%x in page 0x%x\n", buflen, + reg, page); + + return OK; +} + +static int +hdmi_read(uint8_t page, uint8_t reg, uint8_t * val) +{ + + int r; + minix_i2c_ioctl_exec_t ioctl_exec; + + if (val == NULL) { + log_warn(&log, "Read called with NULL pointer\n"); + return EINVAL; + } + + if (page != HDMI_PAGELESS) { + r = set_page(page); + if (r != OK) { + log_warn(&log, "Unable to set page to 0x%x\n", page); + return r; + } + } + + memset(&ioctl_exec, '\0', sizeof(minix_i2c_ioctl_exec_t)); + + /* Read from HDMI */ + ioctl_exec.iie_op = I2C_OP_READ_WITH_STOP; + ioctl_exec.iie_addr = hdmi_address; + + /* write the register address */ + ioctl_exec.iie_cmd[0] = reg; + ioctl_exec.iie_cmdlen = 1; + + /* read 1 byte */ + ioctl_exec.iie_buflen = 1; + + r = i2cdriver_exec(hdmi_bus_endpoint, &ioctl_exec); + if (r != OK) { + log_warn(&log, "hdmi_read() failed (r=%d)\n", r); + return -1; + } + + *val = ioctl_exec.iie_buf[0]; + + log_trace(&log, "Read 0x%x from reg 0x%x in page 0x%x\n", *val, reg, + page); + + return OK; +} + +static int +hdmi_write(uint8_t page, uint8_t reg, uint8_t val) +{ + + int r; + minix_i2c_ioctl_exec_t ioctl_exec; + + if (page != HDMI_PAGELESS) { + r = set_page(page); + if (r != OK) { + log_warn(&log, "Unable to set page to 0x%x\n", page); + return r; + } + } + + memset(&ioctl_exec, '\0', sizeof(minix_i2c_ioctl_exec_t)); + + /* Write to HDMI */ + ioctl_exec.iie_op = I2C_OP_WRITE_WITH_STOP; + ioctl_exec.iie_addr = hdmi_address; + + /* write the register address and value */ + ioctl_exec.iie_buf[0] = reg; + ioctl_exec.iie_buf[1] = val; + ioctl_exec.iie_buflen = 2; + + r = i2cdriver_exec(hdmi_bus_endpoint, &ioctl_exec); + if (r != OK) { + log_warn(&log, "hdmi_write() failed (r=%d)\n", r); + return -1; + } + + log_trace(&log, "Successfully wrote 0x%x to reg 0x%x in page 0x%x\n", + val, reg, page); + + return OK; +} + +static int +hdmi_set(uint8_t page, uint8_t reg, uint8_t mask) +{ + + int r; + uint8_t val; + + val = 0x00; + + r = hdmi_read(page, reg, &val); + if (r != OK) { + return r; + } + + val |= mask; + + r = hdmi_write(page, reg, val); + if (r != OK) { + return r; + } + + return OK; +} + +static int +hdmi_clear(uint8_t page, uint8_t reg, uint8_t mask) +{ + + int r; + uint8_t val; + + val = 0x00; + + r = hdmi_read(page, reg, &val); + if (r != OK) { + return r; + } + + val &= ~mask; + + r = hdmi_write(page, reg, val); + if (r != OK) { + return r; + } + + return OK; +} + +static int +check_revision(void) +{ + int r; + uint8_t rev_lo; + uint8_t rev_hi; + uint16_t revision; + + r = hdmi_read(HDMI_CTRL_PAGE, HDMI_CTRL_REV_LO_REG, &rev_lo); + if (r != OK) { + log_warn(&log, "Failed to read rev_lo (r=%d)\n", r); + return -1; + } + + r = hdmi_read(HDMI_CTRL_PAGE, HDMI_CTRL_REV_HI_REG, &rev_hi); + if (r != OK) { + log_warn(&log, "Failed to read rev_hi (r=%d)\n", r); + return -1; + } + + revision = ((rev_hi << 8) | rev_lo); + if (revision != HDMI_REV_TDA19988) { + + log_warn(&log, "Unrecognized value in revision registers.\n"); + log_warn(&log, "Read: 0x%x | Expected: 0x%x\n", revision, + HDMI_REV_TDA19988); + return -1; + } + + log_debug(&log, "Device Revision: 0x%x\n", revision); + + return OK; +} + +static int +hdmi_ddc_enable(void) +{ + int r; + + /* Soft Reset DDC Bus */ + r = hdmi_set(HDMI_CTRL_PAGE, HDMI_CTRL_RESET_REG, + HDMI_CTRL_RESET_DDC_MASK); + if (r != OK) { + return r; + } + micro_delay(100000); + r = hdmi_clear(HDMI_CTRL_PAGE, HDMI_CTRL_RESET_REG, + HDMI_CTRL_RESET_DDC_MASK); + if (r != OK) { + return r; + } + micro_delay(100000); + + /* Enable DDC */ + r = hdmi_write(HDMI_CTRL_PAGE, HDMI_CTRL_DDC_CTRL_REG, + HDMI_CTRL_DDC_EN_MASK); + if (r != OK) { + return r; + } + + /* Setup the clock (I think) */ + r = hdmi_write(HDMI_CTRL_PAGE, HDMI_CTRL_DDC_CLK_REG, + HDMI_CTRL_DDC_CLK_EN_MASK); + if (r != OK) { + return r; + } + + r = hdmi_write(HDMI_HDCP_OTP_PAGE, HDMI_HDCP_OTP_DDC_CLK_REG, + HDMI_HDCP_OTP_DDC_CLK_MASK); + if (r != OK) { + return r; + } + log_debug(&log, "DDC Enabled\n"); + + return OK; +} + +static int +hdmi_init(void) +{ + int r; + + /* Turn on HDMI module (slave 0x70) */ + r = enable_hdmi_module(); + if (r != OK) { + log_warn(&log, "HDMI Module Init Failed\n"); + return -1; + } + + /* Read chip version to ensure compatibility */ + r = check_revision(); + if (r != OK) { + log_warn(&log, "Couldn't find expected TDA19988 revision\n"); + return -1; + } + + /* Turn on DDC interface between TDA19988 and display */ + r = hdmi_ddc_enable(); + if (r != OK) { + log_warn(&log, "Failed to enable DDC\n"); + return -1; + } + + return OK; +} + +static int +read_edid(uint8_t * buf, size_t count) +{ + int r; + int i, j; + int tries; + int edid_ready; + uint8_t val; + + log_debug(&log, "Reading edid...\n"); + + if (buf == NULL || count < EDID_LEN) { + log_warn(&log, "Expected 128 byte data buffer\n"); + return -1; + } + + r = hdmi_clear(HDMI_HDCP_OTP_PAGE, HDMI_HDCP_OTP_SOME_REG, + HDMI_HDCP_OTP_SOME_MASK); + if (r != OK) { + log_warn(&log, "Failed to clear bit in HDCP OTP reg\n"); + return -1; + } + + /* Enable EDID Block Read Interrupt */ + r = hdmi_set(HDMI_CTRL_PAGE, HDMI_CTRL_INT_REG, + HDMI_CTRL_INT_EDID_MASK); + if (r != OK) { + log_warn(&log, "Failed to enable EDID Block Read interrupt\n"); + return -1; + } + + /* enable global interrupts */ + r = hdmi_write(HDMI_CTRL_PAGE, HDMI_CTRL_INTR_CTRL_REG, + HDMI_CTRL_INTR_EN_GLO_MASK); + if (r != OK) { + log_warn(&log, "Failed to enable interrupts\n"); + return -1; + } + + /* Set Device Address */ + r = hdmi_write(HDMI_EDID_PAGE, HDMI_EDID_DEV_ADDR_REG, + HDMI_EDID_DEV_ADDR); + if (r != OK) { + log_warn(&log, "Couldn't set device address\n"); + return -1; + } + + /* Set Offset */ + r = hdmi_write(HDMI_EDID_PAGE, HDMI_EDID_OFFSET_REG, HDMI_EDID_OFFSET); + if (r != OK) { + log_warn(&log, "Couldn't set offset\n"); + return -1; + } + + /* Set Segment Pointer Address */ + r = hdmi_write(HDMI_EDID_PAGE, HDMI_EDID_SEG_PTR_ADDR_REG, + HDMI_EDID_SEG_PTR_ADDR); + if (r != OK) { + log_warn(&log, "Couldn't set segment pointer address\n"); + return -1; + } + + /* Set Segment Address */ + r = hdmi_write(HDMI_EDID_PAGE, HDMI_EDID_SEG_ADDR_REG, + HDMI_EDID_SEG_ADDR); + if (r != OK) { + log_warn(&log, "Couldn't set segment address\n"); + return -1; + } + + /* + * Toggle EDID Read Request Bit to request a read. + */ + + r = hdmi_write(HDMI_EDID_PAGE, HDMI_EDID_REQ_REG, + HDMI_EDID_REQ_READ_MASK); + if (r != OK) { + log_warn(&log, "Couldn't set Read Request bit\n"); + return -1; + } + + r = hdmi_write(HDMI_EDID_PAGE, HDMI_EDID_REQ_REG, 0x00); + if (r != OK) { + log_warn(&log, "Couldn't clear Read Request bit\n"); + return -1; + } + + log_debug(&log, "Starting polling\n"); + + /* poll interrupt status flag */ + edid_ready = 0; + for (tries = 0; tries < 100; tries++) { + + r = hdmi_read(HDMI_CTRL_PAGE, HDMI_CTRL_INT_REG, &val); + if (r != OK) { + log_warn(&log, "Read failed while polling int flag\n"); + return -1; + } + + if (val & HDMI_CTRL_INT_EDID_MASK) { + log_debug(&log, "Mask Set\n"); + edid_ready = 1; + break; + } + + micro_delay(1000); + } + + if (!edid_ready) { + log_warn(&log, "Data Ready interrupt never fired.\n"); + return EBUSY; + } + + log_debug(&log, "Ready to read\n"); + + /* Finally, perform the read. */ + memset(buf, '\0', count); + r = hdmi_read_block(HDMI_EDID_PAGE, HDMI_EDID_DATA_REG, buf, EDID_LEN); + if (r != OK) { + log_warn(&log, "Failed to read EDID data\n"); + return -1; + } + + /* Disable EDID Block Read Interrupt */ + r = hdmi_clear(HDMI_CTRL_PAGE, HDMI_CTRL_INT_REG, + HDMI_CTRL_INT_EDID_MASK); + if (r != OK) { + log_warn(&log, + "Failed to disable EDID Block Read interrupt\n"); + return -1; + } + + r = hdmi_set(HDMI_HDCP_OTP_PAGE, HDMI_HDCP_OTP_SOME_REG, + HDMI_HDCP_OTP_SOME_MASK); + if (r != OK) { + log_warn(&log, "Failed to set bit in HDCP/OTP reg\n"); + return -1; + } + + log_debug(&log, "Done EDID Reading\n"); + + return OK; +} + +static int +sef_cb_lu_state_save(int UNUSED(state)) +{ + ds_publish_u32("cec_bus", cec_bus, DSF_OVERWRITE); + ds_publish_u32("hdmi_bus", hdmi_bus, DSF_OVERWRITE); + ds_publish_u32("cec_address", cec_address, DSF_OVERWRITE); + ds_publish_u32("hdmi_address", hdmi_address, DSF_OVERWRITE); + return OK; +} + +static int +lu_state_restore(void) +{ + /* Restore the state. */ + u32_t value; + + ds_retrieve_u32("cec_bus", &value); + ds_delete_u32("cec_bus"); + cec_bus = (int) value; + + ds_retrieve_u32("hdmi_bus", &value); + ds_delete_u32("hdmi_bus"); + hdmi_bus = (int) value; + + ds_retrieve_u32("cec_address", &value); + ds_delete_u32("cec_address"); + cec_address = (int) value; + + ds_retrieve_u32("hdmi_address", &value); + ds_delete_u32("hdmi_address"); + hdmi_address = (int) value; + + return OK; +} + +static int +sef_cb_init(int type, sef_init_info_t * UNUSED(info)) +{ + int r; + + if (type == SEF_INIT_LU) { + /* Restore the state. */ + lu_state_restore(); + } + + geom[TDA19988_DEV].dv_base = ((u64_t) (0)); + geom[TDA19988_DEV].dv_size = ((u64_t) (128)); + + /* + * CEC Module + */ + + /* look-up the endpoint for the bus driver */ + cec_bus_endpoint = i2cdriver_bus_endpoint(cec_bus); + if (cec_bus_endpoint == 0) { + log_warn(&log, "Couldn't find bus driver.\n"); + return EXIT_FAILURE; + } + + /* claim the device */ + r = i2cdriver_reserve_device(cec_bus_endpoint, cec_address); + if (r != OK) { + log_warn(&log, "Couldn't reserve device 0x%x (r=%d)\n", + cec_address, r); + return EXIT_FAILURE; + } + + /* + * HDMI Module + */ + + /* look-up the endpoint for the bus driver */ + hdmi_bus_endpoint = i2cdriver_bus_endpoint(hdmi_bus); + if (hdmi_bus_endpoint == 0) { + log_warn(&log, "Couldn't find bus driver.\n"); + return EXIT_FAILURE; + } + + /* claim the device */ + r = i2cdriver_reserve_device(hdmi_bus_endpoint, hdmi_address); + if (r != OK) { + log_warn(&log, "Couldn't reserve device 0x%x (r=%d)\n", + hdmi_address, r); + return EXIT_FAILURE; + } + + if (type != SEF_INIT_LU) { + + /* sign up for updates about the i2c bus going down/up */ + r = i2cdriver_subscribe_bus_updates(cec_bus); + if (r != OK) { + log_warn(&log, "Couldn't subscribe to bus updates\n"); + return EXIT_FAILURE; + } + + /* sign up for updates about the i2c bus going down/up */ + r = i2cdriver_subscribe_bus_updates(hdmi_bus); + if (r != OK) { + log_warn(&log, "Couldn't subscribe to bus updates\n"); + return EXIT_FAILURE; + } + + i2cdriver_announce(cec_bus); + if (cec_bus != hdmi_bus) { + i2cdriver_announce(hdmi_bus); + } + + log_trace(&log, "announced\n"); + } + + return OK; +} + +static void +sef_local_startup(void) +{ + /* + * Register init callbacks. Use the same function for all event types + */ + sef_setcb_init_fresh(sef_cb_init); + sef_setcb_init_lu(sef_cb_init); + sef_setcb_init_restart(sef_cb_init); + + /* + * Register live update callbacks. + */ + /* Agree to update immediately when LU is requested in a valid state. */ + sef_setcb_lu_prepare(sef_cb_lu_prepare_always_ready); + /* Support live update starting from any standard state. */ + sef_setcb_lu_state_isvalid(sef_cb_lu_state_isvalid_standard); + /* Register a custom routine to save the state. */ + sef_setcb_lu_state_save(sef_cb_lu_state_save); + + /* Let SEF perform startup. */ + sef_startup(); +} + +static int +tda19988_env_parse() +{ + int r; + long int cec_busl; + long int cec_addressl; + long int hdmi_busl; + long int hdmi_addressl; + + r = env_parse("cec_bus", "d", 0, &cec_busl, 1, 3); + if (r != EP_SET) { + return -1; + } + cec_bus = (uint32_t) cec_busl; + + r = env_parse("cec_address", "x", 0, &cec_addressl, 0x34, 0x37); + if (r != EP_SET) { + return -1; + } + cec_address = (i2c_addr_t) cec_addressl; + + r = env_parse("hdmi_bus", "d", 0, &hdmi_busl, 1, 3); + if (r != EP_SET) { + return -1; + } + hdmi_bus = (uint32_t) hdmi_busl; + + r = env_parse("hdmi_address", "x", 0, &hdmi_addressl, 0x70, 0x73); + if (r != EP_SET) { + return -1; + } + hdmi_address = (i2c_addr_t) hdmi_addressl; + + return OK; +} + +int +main(int argc, char *argv[]) +{ + int r; + + env_setargs(argc, argv); + + r = tda19988_env_parse(); + if (r < 0) { + log_warn(&log, + "Expecting -args 'cec_bus=X cec_address=0xAA hdmi_bus=Y hdmi_address=0xBB'\n"); + log_warn(&log, + "Example -args 'cec_bus=1 cec_address=0x34 hdmi_bus=1 hdmi_address=0x70'\n"); + return EXIT_FAILURE; + } + + sef_local_startup(); + + log_debug(&log, "Startup Complete\n"); + blockdriver_task(&tda19988_tab); + log_debug(&log, "Shutting down\n"); + + return OK; +} diff --git a/etc/system.conf b/etc/system.conf index 209a45435..9f1857c1c 100644 --- a/etc/system.conf +++ b/etc/system.conf @@ -568,7 +568,7 @@ service fb PRIVCTL # 4 ; ipc - SYSTEM pm rs ds vm vfs + SYSTEM pm rs ds vm vfs tda19988 ; }; @@ -613,6 +613,11 @@ service cat24c256 ipc SYSTEM RS DS i2c; }; +service tda19988 +{ + ipc SYSTEM RS DS i2c; +}; + service vbox { system diff --git a/etc/usr/rc b/etc/usr/rc index ab8595c8f..999d4aac2 100644 --- a/etc/usr/rc +++ b/etc/usr/rc @@ -210,6 +210,10 @@ start) test -e /dev/eepromb1s50 || (cd /dev && MAKEDEV eepromb1s50) up cat24c256 -dev /dev/eepromb1s50 \ -label cat24c256.1.50 -args 'bus=1 address=0x50' + + # Start TDA19988 driver for EDID reading. + up tda19988 -label tda19988.1.3470 -args \ + 'cec_bus=1 cec_address=0x34 hdmi_bus=1 hdmi_address=0x70' ;; UNKNOWN) echo "Unable to detect board -- assuming BeagleBoard-xM"