| /* vim: set noet ts=8 sts=8 sw=8 : */ |
| /* |
| * Copyright © 2010 Saleem Abdulrasool <compnerd@compnerd.org>. |
| * Copyright © 2010 Genesi USA, Inc. <matt@genesi-usa.com>. |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright notice, |
| * this list of conditions and the following disclaimer. |
| * |
| * 2. Redistributions in binary form must reproduce the above copyright notice, |
| * this list of conditions and the following disclaimer in the documentation |
| * and/or other materials provided with the distribution. |
| * |
| * 3. The name of the author may not be used to endorse or promote products |
| * derived from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED |
| * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO |
| * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; |
| * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
| * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
| * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/fb.h> |
| #include <linux/i2c.h> |
| #include <linux/delay.h> |
| #include <linux/sysfs.h> |
| #include <linux/kernel.h> |
| #include <linux/math64.h> |
| #include <linux/console.h> |
| #include <linux/workqueue.h> |
| #include <linux/interrupt.h> |
| #include <linux/edid.h> |
| #include <linux/cea861.h> |
| #include <video/avifb.h> |
| #include <linux/switch.h> |
| #include <linux/hdmi.h> |
| #include "../cea861_modedb.h" |
| #include "siihdmi.h" |
| |
| |
| /* logging helpers */ |
| #define CONTINUE(fmt, ...) printk(KERN_CONT fmt, ## __VA_ARGS__) |
| #define DEBUG(fmt, ...) printk(KERN_DEBUG "SIIHDMI: " fmt, ## __VA_ARGS__) |
| #define ERROR(fmt, ...) printk(KERN_ERR "SIIHDMI: " fmt, ## __VA_ARGS__) |
| #define WARNING(fmt, ...) printk(KERN_WARNING "SIIHDMI: " fmt, ## __VA_ARGS__) |
| #define INFO(fmt, ...) printk(KERN_INFO "SIIHDMI: " fmt, ## __VA_ARGS__) |
| |
| /* DDC segment for 4 block read */ |
| #define EDID_I2C_DDC_DATA_SEGMENT 0x30 |
| |
| /* Default Monspecs when EDID is not available or no format is available in EDID */ |
| static struct fb_monspecs default_monspecs = { |
| .modedb = (struct fb_videomode *) &cea_modes[1], |
| .modedb_len = 1, |
| }; |
| |
| /* Aspect ratio extracted from CEA-861-D: needed for AVI Infoframe */ |
| static enum hdmi_picture_aspect cea_ratios[65] = { |
| HDMI_PICTURE_ASPECT_NONE, /* No VIC: default mode */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 1: 640x480p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 2: 720x480p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 3: 720x480p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 4: 1280x720p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 5: 1920x1080i @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 6: 720(1440)x480i @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 7: 720(1440)x480i @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 8: 720(1440)x240p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 9: 720(1440)x240p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 10: 2880x480i @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 11: 2880x480i @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 12: 2880x240p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 13: 2880x240p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 14: 1440x480p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 15: 1440x480p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 16: 1920x1080p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 17: 720x576p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 18: 720x576p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 19: 1280x720p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 20: 1920x1080i @ 50Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 21: 720(1440)x576i @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 22: 720(1440)x576i @ 50Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 23: 720(1440)x288p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 24: 720(1440)x288p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 25: 2880x576i @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 26: 2880x576i @ 50Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 27: 2880x288p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 28: 2880x288p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 29: 1440x576p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 30: 1440x576p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 31: 1920x1080p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 32: 1920x1080p @ 23.97Hz/24Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 33: 1920x1080p @ 25Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 34: 1920x1080p @ 29.97Hz/30Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 35: 2880x480p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 36: 2880x480p @ 59.94Hz/60Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 37: 2880x576p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 38: 2880x576p @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 39: 1920x1080i (1250 total) @ 50Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 40: 1920x1080i @ 100Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 41: 1280x720p @ 100Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 42: 720x576p @ 100Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 43: 720x576p @ 100Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 44: 720(1440)x576i @ 100Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 45: 720(1440)x576i @ 100Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 46: 1920x1080i @ 119.88/120Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 47: 1280x720p @ 119.88/120Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 48: 720x480p @ 119.88/120Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 49: 720x480p @ 119.88/120Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 50: 720(1440)x480i @ 119.88/120Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 51: 720(1440)x480i @ 119.88/120Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 52: 720x576p @ 200Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 53: 720x576p @ 200Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 54: 720(1440)x576i @ 200Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 55: 720(1440)x576i @ 200Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 56: 720x480p @ 239.76/240Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 57: 720x480p @ 239.76/240Hz */ |
| HDMI_PICTURE_ASPECT_4_3, /* VIC 58: 720(1440)x480i @ 239.76/240Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 59: 720(1440)x480i @ 239.76/240Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 60: 1280x720p @ 24Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 61: 1280x720p @ 25Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 62: 1280x720p @ 30Hz */ |
| HDMI_PICTURE_ASPECT_16_9, /* VIC 63: 1920x1080p @ 120Hz */ |
| HDMI_PICTURE_ASPECT_16_9 /* VIC 64: 1920x1080p @ 100Hz */ |
| }; |
| |
| /* module parameters */ |
| static unsigned int bus_timeout = 50; |
| module_param(bus_timeout, uint, 0644); |
| MODULE_PARM_DESC(bus_timeout, "bus timeout in milliseconds"); |
| |
| static unsigned int seventwenty = 1; |
| module_param(seventwenty, uint, 0644); |
| MODULE_PARM_DESC(seventwenty, "attempt to use 720p mode"); |
| |
| static unsigned int teneighty = 0; |
| module_param(teneighty, uint, 0644); |
| MODULE_PARM_DESC(teneighty, "attempt to use 1080p mode"); |
| |
| static unsigned int useitmodes = 1; |
| module_param(useitmodes, uint, 0644); |
| MODULE_PARM_DESC(useitmodes, "prefer IT modes over CEA modes when sanitizing the modelist"); |
| |
| static unsigned int modevic = 0; |
| module_param_named(vic, modevic, uint, 0644); |
| MODULE_PARM_DESC(modevic, "CEA VIC to try and match before autodetection"); |
| |
| static int siihdmi_detect_revision(struct siihdmi_tx *tx) |
| { |
| u8 data; |
| unsigned long start; |
| |
| start = jiffies; |
| do { |
| data = i2c_smbus_read_byte_data(tx->client, |
| SIIHDMI_TPI_REG_DEVICE_ID); |
| } while (data != SIIHDMI_DEVICE_ID_902x && |
| !time_after(jiffies, start + bus_timeout)); |
| |
| if (data != SIIHDMI_DEVICE_ID_902x) |
| return -ENODEV; |
| |
| INFO("Device ID: %#02x", data); |
| |
| data = i2c_smbus_read_byte_data(tx->client, |
| SIIHDMI_TPI_REG_DEVICE_REVISION); |
| if (data) |
| CONTINUE(" (rev %01u.%01u)", |
| (data >> 4) & 0xf, (data >> 0) & 0xf); |
| |
| data = i2c_smbus_read_byte_data(tx->client, |
| SIIHDMI_TPI_REG_TPI_REVISION); |
| CONTINUE(" (%s", |
| (data & SIIHDMI_VERSION_FLAG_VIRTUAL) ? "Virtual " : ""); |
| data &= ~SIIHDMI_VERSION_FLAG_VIRTUAL; |
| data = data ? data : SIIHDMI_BASE_TPI_REVISION; |
| CONTINUE("TPI revision %01u.%01u)", |
| (data >> 4) & 0xf, (data >> 0) & 0xf); |
| |
| data = i2c_smbus_read_byte_data(tx->client, |
| SIIHDMI_TPI_REG_HDCP_REVISION); |
| if (data) |
| CONTINUE(" (HDCP version %01u.%01u)", |
| (data >> 4) & 0xf, (data >> 0) & 0xf); |
| |
| CONTINUE("\n"); |
| |
| return 0; |
| } |
| |
| static inline int siihdmi_power_up(struct siihdmi_tx *tx) |
| { |
| int ret; |
| |
| DEBUG("Powering up transmitter\n"); |
| |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_PWR_STATE, |
| SIIHDMI_POWER_STATE_D0); |
| if (ret < 0) |
| ERROR("unable to power up transmitter\n"); |
| |
| return ret; |
| } |
| |
| static inline int siihdmi_power_down(struct siihdmi_tx *tx) |
| { |
| int ret; |
| u8 ctrl; |
| |
| DEBUG("Powering down transmitter\n"); |
| |
| #ifdef SIIHDMI_USE_FB |
| memset((void *) &tx->sink.current_mode, 0, sizeof(struct fb_videomode)); |
| #endif |
| |
| ctrl = SIIHDMI_SYS_CTRL_TMDS_OUTPUT_POWER_DOWN; |
| if (tx->sink.type == SINK_TYPE_HDMI) |
| ctrl |= SIIHDMI_SYS_CTRL_OUTPUT_MODE_SELECT_HDMI; |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_SYS_CTRL, ctrl); |
| if (ret < 0) { |
| ERROR("unable to power down transmitter\n"); |
| return ret; |
| } |
| |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_PWR_STATE, |
| SIIHDMI_POWER_STATE_D2); |
| if (ret < 0) { |
| ERROR("unable to set transmitter into D2\n"); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int siihdmi_check_sync_mux_mode(struct siihdmi_tx *tx) |
| { |
| int ret; |
| u8 value; |
| |
| /* Configure only when mux */ |
| if (tx->platform->input_format != YUV_422_8_MUX) |
| return 0; |
| |
| /* Check sync mux mode activated */ |
| value = i2c_smbus_read_byte_data(tx->client, |
| 0x60); |
| if ((value & 1 << 5) && (value & 1 << 7)) |
| return 0; |
| |
| /* Restore explicitly, if necessary, TPI 0x60[5] to enable YC Mux mode */ |
| value |= 1 << 5; /* YC mux mode one-to-two data channel demux enable */ |
| value |= 1 << 7; /* External sync method */ |
| ret = i2c_smbus_write_byte_data(tx->client, |
| 0x60, |
| value); |
| if (ret < 0) { |
| WARNING("failed to configure syn mux mode: %d\n", ret); |
| return ret; |
| } |
| return 0; |
| } |
| |
| static int siihdmi_check_embsync_extract(struct siihdmi_tx *tx) |
| { |
| int ret; |
| u8 value; |
| |
| /* Configure only when mux */ |
| if (tx->platform->input_format != YUV_422_8_MUX) |
| return 0; |
| |
| siihdmi_check_sync_mux_mode(tx); |
| |
| /* Check embedded sync extraction */ |
| value = i2c_smbus_read_byte_data(tx->client, |
| 0x63); |
| if (value & 1 << 6) |
| return 0; |
| |
| value |= 1 << 6; |
| ret = i2c_smbus_write_byte_data(tx->client, |
| 0x63, |
| value); |
| if (ret < 0) { |
| WARNING("failed to activate de generator: %d\n", ret); |
| return ret; |
| } |
| |
| /* Check again .. */ |
| siihdmi_check_sync_mux_mode(tx); |
| return 0; |
| } |
| |
| static int siihdmi_initialise(struct siihdmi_tx *tx) |
| { |
| int ret; |
| u8 value; |
| |
| ret = i2c_smbus_write_byte_data(tx->client, SIIHDMI_TPI_REG_RQB, 0x00); |
| if (ret < 0) { |
| WARNING("unable to initialise device to TPI mode\n"); |
| return ret; |
| } |
| |
| /* step 2: detect revision */ |
| if ((ret = siihdmi_detect_revision(tx)) < 0) { |
| DEBUG("unable to detect device revision\n"); |
| return ret; |
| } |
| |
| /* step 3: power up transmitter */ |
| if ((ret = siihdmi_power_up(tx)) < 0) |
| return ret; |
| |
| /* step 3.5: enable source termination |
| * (recommanded for pixclock > 100 MHz) |
| * 1. Set to internal page 0 (0xBC = 0x01) |
| * 2. Set to indexed register 130 (0xBD = 0x82) |
| * 3. Read value of register (0xBE) |
| * 4. Set bit 0 to 1 in order to enable source termination |
| * 5. Write value to register (0xBE) |
| */ |
| ret = i2c_smbus_write_byte_data(tx->client, 0xBC, 0x01); |
| if (ret < 0) |
| return ret; |
| ret = i2c_smbus_write_byte_data(tx->client, 0xBD, 0x82); |
| if (ret < 0) |
| return ret; |
| value = i2c_smbus_read_byte_data(tx->client, 0xBE); |
| value |= 0x01; |
| ret = i2c_smbus_write_byte_data(tx->client, 0xBE, value); |
| if (ret < 0) |
| return ret; |
| |
| /* step 4: configure input bus and pixel repetition */ |
| if (tx->platform->input_format == YUV_422_8_MUX) { |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_INPUT_BUS_PIXEL_REPETITION, |
| 0x30); |
| if (ret < 0) |
| return ret; |
| } |
| |
| /* step 5: select YC input mode */ |
| |
| /* step 6: configure sync methods */ |
| |
| /* step 7: configure explicit sync DE generation */ |
| |
| /* step 8: configure embedded sync extraction */ |
| |
| /* step 8.5: power down trasnmitter since interrupt power up transmitter |
| * if a display is attached |
| */ |
| value = i2c_smbus_read_byte_data(tx->client, SIIHDMI_TPI_REG_ISR); |
| if (!(value & SIIHDMI_ISR_DISPLAY_ATTACHED)) |
| siihdmi_power_down(tx); |
| |
| /* step 9: setup interrupt service */ |
| if (tx->hotplug.enabled) { |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_IER, |
| SIIHDMI_IER_HOT_PLUG_EVENT); |
| if (ret < 0) |
| WARNING("unable to setup interrupt request\n"); |
| } |
| |
| return ret; |
| } |
| |
| static int siihdmi_read_edid(struct siihdmi_tx *tx, u8 *edid, unsigned int block, size_t size) |
| { |
| u8 offset = block * EDID_BLOCK_SIZE; |
| u8 segment = block >> 1; |
| u8 xfers = segment ? 3 : 2; |
| u8 ctrl; |
| int ret; |
| unsigned long start; |
| |
| struct i2c_msg request[] = { |
| { .addr = EDID_I2C_DDC_DATA_SEGMENT, |
| .flags = 0, |
| .len = 1, |
| .buf = &segment, }, |
| { .addr = EDID_I2C_DDC_DATA_ADDRESS, |
| .flags = 0, |
| .len = 1, |
| .buf = &offset, }, |
| { .addr = EDID_I2C_DDC_DATA_ADDRESS, |
| .flags = I2C_M_RD, |
| .len = size, |
| .buf = edid, }, |
| }; |
| |
| /* step 1: (potentially) disable HDCP */ |
| |
| /* step 2: request the DDC bus */ |
| ctrl = i2c_smbus_read_byte_data(tx->client, SIIHDMI_TPI_REG_SYS_CTRL); |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_SYS_CTRL, |
| ctrl | SIIHDMI_SYS_CTRL_DDC_BUS_REQUEST); |
| if (ret < 0) { |
| DEBUG("unable to request DDC bus\n"); |
| return ret; |
| } |
| |
| /* step 3: poll for bus grant */ |
| start = jiffies; |
| do { |
| ctrl = i2c_smbus_read_byte_data(tx->client, |
| SIIHDMI_TPI_REG_SYS_CTRL); |
| } while ((~ctrl & SIIHDMI_SYS_CTRL_DDC_BUS_GRANTED) && |
| !time_after(jiffies, start + bus_timeout)); |
| |
| if (~ctrl & SIIHDMI_SYS_CTRL_DDC_BUS_GRANTED) |
| goto relinquish; |
| |
| /* step 4: take ownership of the DDC bus */ |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_SYS_CTRL, |
| SIIHDMI_SYS_CTRL_DDC_BUS_REQUEST | |
| SIIHDMI_SYS_CTRL_DDC_BUS_OWNER_HOST); |
| if (ret < 0) { |
| DEBUG("unable to take ownership of the DDC bus\n"); |
| goto relinquish; |
| } |
| |
| /* step 5: read edid */ |
| ret = i2c_transfer(tx->client->adapter, &request[3 - xfers], xfers); |
| if (ret != xfers) |
| DEBUG("unable to read EDID block\n"); |
| |
| relinquish: |
| /* step 6: relinquish ownership of the DDC bus */ |
| start = jiffies; |
| do { |
| i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_SYS_CTRL, |
| 0x00); |
| ctrl = i2c_smbus_read_byte_data(tx->client, |
| SIIHDMI_TPI_REG_SYS_CTRL); |
| } while ((ctrl & SIIHDMI_SYS_CTRL_DDC_BUS_GRANTED) && |
| !time_after(jiffies, start + bus_timeout)); |
| |
| /* step 7: (potentially) enable HDCP */ |
| |
| return ret; |
| } |
| |
| static inline void _process_cea861_vsdb(struct siihdmi_tx *tx, |
| const struct hdmi_vsdb * const vsdb) |
| { |
| unsigned int max_tmds; |
| |
| if (memcmp(vsdb->ieee_registration, CEA861_OUI_REGISTRATION_ID_HDMI_LSB, |
| sizeof(vsdb->ieee_registration))) |
| return; |
| |
| max_tmds = KHZ2PICOS(vsdb->max_tmds_clock * 200); |
| |
| /* Sink type is HDMI when VSDB is available in one of extensions */ |
| tx->sink.type = SINK_TYPE_HDMI; |
| |
| DEBUG("HDMI VSDB detected (basic audio %ssupported)\n", |
| tx->audio.available ? "" : "not "); |
| INFO("HDMI port configuration: %u.%u.%u.%u\n", |
| vsdb->port_configuration_a, vsdb->port_configuration_b, |
| vsdb->port_configuration_c, vsdb->port_configuration_d); |
| |
| if (max_tmds && max_tmds < tx->platform->pixclock) { |
| INFO("maximum TMDS clock limited to %u by device\n", max_tmds); |
| tx->platform->pixclock = max_tmds; |
| } |
| } |
| |
| static inline void _process_cea861_video(struct siihdmi_tx *tx, |
| const struct cea861_video_data_block * const video) |
| { |
| const struct cea861_data_block_header * const header = |
| (struct cea861_data_block_header *) video; |
| u8 i, count; |
| |
| for (i = 0, count = 0; i < header->length; i++) { |
| const int vic = video->svd[i] & ~CEA861_SVD_NATIVE_FLAG; |
| |
| if (vic && vic <= ARRAY_SIZE(cea_modes)) { |
| #ifdef SIIHDMI_USE_FB |
| fb_add_videomode(&cea_modes[vic], &tx->info->modelist); |
| #endif |
| count++; |
| } |
| } |
| |
| DEBUG("%u modes parsed from CEA video data block\n", count); |
| } |
| |
| static inline void _process_cea861_extended(struct siihdmi_tx *tx, |
| const struct cea861_data_block_extended *ext) |
| { |
| static const char * const scannings[] = { |
| [SCAN_INFORMATION_UNKNOWN] = "unknown", |
| [SCAN_INFORMATION_OVERSCANNED] = "overscanned", |
| [SCAN_INFORMATION_UNDERSCANNED] = "underscanned", |
| [SCAN_INFORMATION_RESERVED] = "reserved", |
| }; |
| |
| switch (ext->extension_tag) { |
| case CEA861_DATA_BLOCK_EXTENSION_VIDEO_CAPABILITY: { |
| const struct cea861_video_capability_block * const vcb = |
| (struct cea861_video_capability_block *) ext; |
| INFO("CEA video capability (scanning behaviour):\n" |
| " Preferred Mode: %s\n" |
| " VESA/PC Mode: %s\n" |
| " CEA/TV Mode: %s\n", |
| scannings[vcb->pt_overunder_behavior], |
| scannings[vcb->it_overunder_behavior], |
| scannings[vcb->ce_overunder_behavior]); |
| } |
| break; |
| default: |
| break; |
| } |
| } |
| |
| static void siihdmi_parse_cea861_timing_block(struct siihdmi_tx *tx, |
| const struct edid_extension *ext) |
| { |
| const struct cea861_timing_block * const cea = (struct cea861_timing_block *) ext; |
| const u8 size = cea->dtd_offset - offsetof(struct cea861_timing_block, data); |
| u8 index; |
| |
| BUILD_BUG_ON(sizeof(*cea) != sizeof(*ext)); |
| |
| tx->audio.available = cea->basic_audio_supported; |
| if (cea->underscan_supported) |
| tx->sink.scanning = SCANNING_UNDERSCANNED; |
| |
| if (cea->dtd_offset == CEA861_NO_DTDS_PRESENT) |
| return; |
| |
| index = 0; |
| while (index < size) { |
| const struct cea861_data_block_header * const header = |
| (struct cea861_data_block_header *) &cea->data[index]; |
| |
| switch (header->tag) { |
| case CEA861_DATA_BLOCK_TYPE_VENDOR_SPECIFIC: |
| _process_cea861_vsdb(tx, (struct hdmi_vsdb *) header); |
| break; |
| #ifdef SIIHDMI_USE_FB |
| case CEA861_DATA_BLOCK_TYPE_VIDEO: |
| _process_cea861_video(tx, (struct cea861_video_data_block *) header); |
| break; |
| #endif |
| case CEA861_DATA_BLOCK_TYPE_EXTENDED: |
| _process_cea861_extended(tx, (struct cea861_data_block_extended *) header); |
| break; |
| } |
| |
| index = index + header->length + sizeof(*header); |
| } |
| } |
| |
| static void siihdmi_set_vmode_registers(struct siihdmi_tx *tx, |
| const struct fb_videomode *mode) |
| { |
| enum basic_video_mode_fields { |
| PIXEL_CLOCK, |
| REFRESH_RATE, |
| X_RESOLUTION, |
| Y_RESOLUTION, |
| FIELDS, |
| }; |
| |
| u16 vmode[FIELDS]; |
| u32 pixclk, htotal, vtotal, refresh; |
| u8 format; |
| int ret; |
| u16 hsync_len, vsync_len; |
| |
| BUILD_BUG_ON(sizeof(vmode) != 8); |
| |
| BUG_ON(mode->pixclock == 0); |
| pixclk = PICOS2KHZ(mode->pixclock); |
| |
| htotal = mode->xres + mode->left_margin + mode->hsync_len + mode->right_margin; |
| vtotal = mode->yres + mode->upper_margin + mode->vsync_len + mode->lower_margin; |
| |
| /* explicitly use 64-bit division to avoid overflow truncation */ |
| refresh = (u32) div_u64(pixclk * 100000ull, htotal * vtotal); |
| |
| /* basic video mode data */ |
| vmode[PIXEL_CLOCK] = (u16) (pixclk / 10); |
| if (tx->platform->input_format == YUV_422_8_MUX) |
| vmode[PIXEL_CLOCK] *= 2; |
| |
| /* |
| Silicon Image example code implies refresh to be 6000 for 60Hz? |
| This may work simply because we only test it on little-endian :( |
| */ |
| vmode[REFRESH_RATE] = (u16) refresh; |
| vmode[X_RESOLUTION] = (u16) htotal; |
| vmode[Y_RESOLUTION] = (u16) vtotal; |
| |
| ret = i2c_smbus_write_i2c_block_data(tx->client, |
| SIIHDMI_TPI_REG_VIDEO_MODE_DATA_BASE, |
| sizeof(vmode), |
| (u8 *) vmode); |
| if (ret < 0) |
| DEBUG("unable to write video mode data\n"); |
| |
| /* input format */ |
| format = SIIHDMI_INPUT_VIDEO_RANGE_EXPANSION_AUTO |
| | SIIHDMI_INPUT_COLOR_DEPTH_8BIT; |
| |
| switch (tx->platform->input_format) { |
| case YUV_422_8_MUX: |
| format |= SIIHDMI_INPUT_COLOR_SPACE_YUV_422; |
| break; |
| case RGB_24: |
| default : |
| format |= SIIHDMI_INPUT_COLOR_SPACE_RGB; |
| break; |
| } |
| |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_AVI_INPUT_FORMAT, |
| format); |
| if (ret < 0) |
| DEBUG("unable to set input format\n"); |
| |
| /* output format */ |
| format = SIIHDMI_OUTPUT_VIDEO_RANGE_COMPRESSION_AUTO |
| | SIIHDMI_OUTPUT_COLOR_STANDARD_BT601 |
| | SIIHDMI_OUTPUT_COLOR_DEPTH_8BIT; |
| |
| if (tx->sink.type == SINK_TYPE_HDMI) |
| format |= SIIHDMI_OUTPUT_FORMAT_HDMI_RGB; |
| else |
| format |= SIIHDMI_OUTPUT_FORMAT_DVI_RGB; |
| |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_AVI_OUTPUT_FORMAT, |
| format); |
| if (ret < 0) |
| DEBUG("unable to set output format\n"); |
| |
| /* If format is not muxed, terminated */ |
| if (tx->platform->input_format != YUV_422_8_MUX) |
| return; |
| |
| /* Default values from hdmi_1024x768p60_video_mode */ |
| hsync_len = mode->hsync_len; |
| vsync_len = mode->vsync_len; |
| |
| /* Embedded sync register set */ |
| siihdmi_check_sync_mux_mode(tx); |
| i2c_smbus_write_byte_data(tx->client, 0x62, 0x01); |
| i2c_smbus_write_byte_data(tx->client, 0x64, 0x00); |
| i2c_smbus_write_byte_data(tx->client, 0x65, 0x02); |
| i2c_smbus_write_byte_data(tx->client, 0x66, hsync_len & 0xFF); |
| i2c_smbus_write_byte_data(tx->client, 0x67, (hsync_len >> 8) & 0x03); |
| i2c_smbus_write_byte_data(tx->client, 0x68, 0x01); |
| i2c_smbus_write_byte_data(tx->client, 0x69, vsync_len & 0x3F); |
| siihdmi_check_sync_mux_mode(tx); |
| } |
| |
| static int siihdmi_clear_avi_info_frame(struct siihdmi_tx *tx) |
| { |
| const u8 buffer[SIIHDMI_TPI_REG_AVI_INFO_FRAME_LENGTH] = {0}; |
| int ret; |
| |
| BUG_ON(tx->sink.type != SINK_TYPE_DVI); |
| |
| ret = i2c_smbus_write_i2c_block_data(tx->client, |
| SIIHDMI_TPI_REG_AVI_INFO_FRAME_BASE, |
| sizeof(buffer), buffer); |
| if (ret < 0) |
| DEBUG("unable to clear avi info frame\n"); |
| |
| return ret; |
| } |
| |
| static int siihdmi_set_avi_info_frame(struct siihdmi_tx *tx) |
| { |
| int ret = 0, i; |
| u8 buffer[HDMI_INFOFRAME_HEADER_SIZE + HDMI_AVI_INFOFRAME_SIZE]; |
| int min_refresh, max_refresh; |
| struct hdmi_avi_infoframe infoframe; |
| struct edid_block0 *block0; |
| struct fb_videomode *m; |
| unsigned ratio; |
| enum { |
| ASPECT_RATIO_4_3 = 1, |
| ASPECT_RATIO_16_9 = 2 |
| } modes = 0; |
| |
| hdmi_avi_infoframe_init(&infoframe); |
| |
| infoframe.colorspace = HDMI_COLORSPACE_RGB; |
| infoframe.active_aspect = HDMI_ACTIVE_ASPECT_PICTURE; |
| |
| /* Fill Video Code and Aspect ratio */ |
| if (tx->selected_mode != NULL) { |
| /* Vertical refresh is +/- 0.5% in CEA spec */ |
| min_refresh = tx->selected_mode->refresh * 995 / 1000; |
| max_refresh = tx->selected_mode->refresh * 1005 / 1000; |
| |
| /* Find correct VIC in CEA |
| * Interlaced video are not supported |
| * Vertical refesh is +/-0.5% |
| * The name is NULL or equal to name in cea_modes (some format |
| * have same timings but not same VIC and aspect ratio) |
| */ |
| for (i = 1; i <= ARRAY_SIZE(cea_modes); i++) { |
| m = (struct fb_videomode *) &cea_modes[i]; |
| if (!(m->vmode & FB_VMODE_INTERLACED) && |
| tx->selected_mode->xres == m->xres && |
| tx->selected_mode->yres == m->yres && |
| min_refresh <= m->refresh && |
| max_refresh >= m->refresh && |
| tx->selected_mode->hsync_len == m->hsync_len && |
| (tx->selected_mode->name == NULL || |
| tx->selected_mode->name == m->name)) { |
| infoframe.video_code = i; |
| break; |
| } |
| } |
| |
| /* Get aspect ratio from CEA ratio table */ |
| if (infoframe.video_code > 1 && infoframe.video_code < 64 && |
| (fb_mode_is_equal(&cea_modes[infoframe.video_code], |
| &cea_modes[infoframe.video_code+1]) || |
| fb_mode_is_equal(&cea_modes[infoframe.video_code], |
| &cea_modes[infoframe.video_code-1]))) { |
| /* Video format has two aspect ratio in CEA table |
| * Get correct VIC from E-EDID (supported CEA modes) |
| * If none or both aspect ratio are available, ratio of |
| * screen is calculated and correct VIC is selected. |
| */ |
| if (cea_ratios[infoframe.video_code] == |
| HDMI_PICTURE_ASPECT_16_9) |
| infoframe.video_code--; |
| |
| /* Find in E-EDID which VIC are available */ |
| for (i = 0; i < tx->monspecs.modedb_len; i++) { |
| if (tx->monspecs.modedb[i].name == |
| cea_modes[infoframe.video_code].name) |
| modes |= ASPECT_RATIO_4_3; |
| else if (tx->monspecs.modedb[i].name == |
| cea_modes[infoframe.video_code+1].name) |
| modes |= ASPECT_RATIO_16_9; |
| } |
| |
| /* Calculate ratio from EDID */ |
| if ((modes == 0 || |
| modes == (ASPECT_RATIO_4_3 | ASPECT_RATIO_16_9)) && |
| tx->edid.data != NULL) { |
| block0 = (struct edid_block0 *) tx->edid.data; |
| ratio = block0->maximum_horizontal_image_size * |
| 100 / block0->maximum_vertical_image_size; |
| if (ratio >= 177) |
| infoframe.video_code++; |
| } |
| else if (modes == ASPECT_RATIO_16_9) |
| infoframe.video_code++; |
| |
| /* Select an active aspect ratio: |
| * The CEA modes with two aspect ratio (4:3 and 16:9) |
| * for the same resolution as 480p or 576p, imply that |
| * video must be streched to 4:3 when aspect ratio is |
| * 16:9 since the screen will rescale the image to fit |
| * 16:9. To bypass the streching process, an active |
| * aspect ratio can be specified for both formats in |
| * order to pass HDMI certification: if the active |
| * aspect ratio field is not "As the coded frame" |
| * (= 8), the aspect ratio of displayed picture is not |
| * verified and the 16:9 case doesn't make issue. |
| */ |
| if (cea_ratios[infoframe.video_code] == |
| HDMI_PICTURE_ASPECT_4_3) |
| infoframe.active_aspect = |
| HDMI_ACTIVE_ASPECT_16_9; |
| else |
| infoframe.active_aspect = |
| HDMI_ACTIVE_ASPECT_16_9_CENTER; |
| } |
| infoframe.picture_aspect = cea_ratios[infoframe.video_code]; |
| |
| /* Set pixel repetition */ |
| if (tx->selected_mode->vmode & FB_VMODE_DOUBLE) |
| infoframe.pixel_repeat = 1; |
| |
| /* Log VIC format used */ |
| if (infoframe.video_code > 0) |
| INFO("Use VIC %d\n", infoframe.video_code); |
| } |
| |
| switch (tx->sink.scanning) { |
| case SCANNING_UNDERSCANNED: |
| infoframe.scan_mode = HDMI_SCAN_MODE_UNDERSCAN; |
| break; |
| case SCANNING_OVERSCANNED: |
| infoframe.scan_mode = HDMI_SCAN_MODE_OVERSCAN; |
| break; |
| default: |
| infoframe.scan_mode = HDMI_SCAN_MODE_NONE; |
| } |
| |
| hdmi_avi_infoframe_pack(&infoframe,buffer,sizeof(buffer)); |
| |
| ret = i2c_smbus_write_i2c_block_data(tx->client, |
| SIIHDMI_TPI_REG_AVI_INFO_FRAME_BASE, |
| sizeof(buffer) - SIIHDMI_AVI_INFO_FRAME_OFFSET, |
| (buffer) + SIIHDMI_AVI_INFO_FRAME_OFFSET); |
| |
| if (ret < 0) |
| DEBUG("unable to write avi info frame\n"); |
| |
| return ret; |
| } |
| |
| static int siihdmi_set_audio_info_frame(struct siihdmi_tx *tx) |
| { |
| int ret; |
| struct siihdmi_audio_info_frame packet = { |
| .header = { |
| .info_frame = SIIHDMI_INFO_FRAME_AUDIO, |
| .repeat = false, |
| .enable = tx->audio.available, |
| }, |
| |
| .info_frame = { |
| .header = { |
| .type = INFO_FRAME_TYPE_AUDIO, |
| .version = CEA861_AUDIO_INFO_FRAME_VERSION, |
| .length = sizeof(packet.info_frame) - sizeof(packet.info_frame.header), |
| }, |
| |
| .channel_count = CHANNEL_COUNT_REFER_STREAM_HEADER, |
| .channel_allocation = CHANNEL_ALLOCATION_STEREO, |
| |
| .format_code_extension = CODING_TYPE_REFER_STREAM_HEADER, |
| |
| /* required to Refer to Stream Header by CEA861-D */ |
| .coding_type = CODING_TYPE_REFER_STREAM_HEADER, |
| |
| .sample_size = SAMPLE_SIZE_REFER_STREAM_HEADER, |
| .sample_frequency = FREQUENCY_REFER_STREAM_HEADER, |
| }, |
| }; |
| |
| BUG_ON(tx->sink.type != SINK_TYPE_HDMI); |
| |
| cea861_checksum_hdmi_info_frame((u8 *) &packet.info_frame); |
| |
| BUILD_BUG_ON(sizeof(packet) != SIIHDMI_TPI_REG_AUDIO_INFO_FRAME_LENGTH); |
| ret = i2c_smbus_write_i2c_block_data(tx->client, |
| SIIHDMI_TPI_REG_MISC_INFO_FRAME_BASE, |
| sizeof(packet), |
| (u8 *) &packet); |
| if (ret < 0) |
| DEBUG("unable to write audio info frame\n"); |
| |
| return ret; |
| } |
| |
| static int siihdmi_set_spd_info_frame(struct siihdmi_tx *tx) |
| { |
| int ret; |
| struct siihdmi_spd_info_frame packet = { |
| .header = { |
| .info_frame = SIIHDMI_INFO_FRAME_SPD_ACP, |
| .repeat = false, |
| .enable = true, |
| }, |
| |
| .info_frame = { |
| .header = { |
| .type = INFO_FRAME_TYPE_SOURCE_PRODUCT_DESCRIPTION, |
| .version = CEA861_SPD_INFO_FRAME_VERSION, |
| .length = sizeof(packet.info_frame) - sizeof(packet.info_frame.header), |
| }, |
| |
| .source_device_info = SPD_SOURCE_PC_GENERAL, |
| }, |
| }; |
| |
| BUG_ON(tx->sink.type != SINK_TYPE_HDMI); |
| |
| strncpy(packet.info_frame.vendor, tx->platform->vendor, |
| sizeof(packet.info_frame.vendor)); |
| |
| strncpy(packet.info_frame.description, tx->platform->description, |
| sizeof(packet.info_frame.description)); |
| |
| cea861_checksum_hdmi_info_frame((u8 *) &packet.info_frame); |
| |
| BUILD_BUG_ON(sizeof(packet) != SIIHDMI_TPI_REG_MISC_INFO_FRAME_LENGTH); |
| ret = i2c_smbus_write_i2c_block_data(tx->client, |
| SIIHDMI_TPI_REG_MISC_INFO_FRAME_BASE, |
| sizeof(packet), |
| (u8 *) &packet); |
| if (ret < 0) |
| DEBUG("unable to write SPD info frame\n"); |
| |
| return ret; |
| } |
| |
| static inline void siihdmi_audio_mute(struct siihdmi_tx *tx) |
| { |
| u8 data; |
| |
| data = i2c_smbus_read_byte_data(tx->client, |
| SIIHDMI_TPI_REG_I2S_AUDIO_PACKET_LAYOUT_CTRL); |
| |
| i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_I2S_AUDIO_PACKET_LAYOUT_CTRL, |
| data | SIIHDMI_AUDIO_MUTE); |
| } |
| |
| static inline void siihdmi_audio_unmute(struct siihdmi_tx *tx) |
| { |
| u8 data; |
| |
| data = i2c_smbus_read_byte_data(tx->client, |
| SIIHDMI_TPI_REG_I2S_AUDIO_PACKET_LAYOUT_CTRL); |
| |
| i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_I2S_AUDIO_PACKET_LAYOUT_CTRL, |
| data & ~SIIHDMI_AUDIO_MUTE); |
| } |
| |
| static inline void siihdmi_configure_audio(struct siihdmi_tx *tx) |
| { |
| siihdmi_audio_mute(tx); |
| |
| i2c_smbus_write_byte_data(tx->client, SIIHDMI_TPI_REG_I2S_AUDIO_PACKET_LAYOUT_CTRL, SIIHDMI_AUDIO_SPDIF_ENABLE | SIIHDMI_AUDIO_MUTE); |
| i2c_smbus_write_byte_data(tx->client, SIIHDMI_TPI_REG_I2S_AUDIO_SAMPLING_HBR, 0); |
| i2c_smbus_write_byte_data(tx->client, SIIHDMI_TPI_REG_I2S_ORIGINAL_FREQ_SAMPLE_LENGTH, SIIHDMI_AUDIO_HANDLING_DOWN_SAMPLE); |
| |
| siihdmi_audio_unmute(tx); |
| siihdmi_set_audio_info_frame(tx); |
| } |
| |
| static void siihdmi_print_modeline(const struct siihdmi_tx *tx, |
| const struct fb_videomode *mode, |
| const char * const message) |
| { |
| const bool interlaced = (mode->vmode & FB_VMODE_INTERLACED); |
| const bool double_scan = (mode->vmode & FB_VMODE_DOUBLE); |
| |
| u32 pixclk = mode->pixclock ? PICOS2KHZ(mode->pixclock) : 0; |
| char flag = ' '; |
| |
| pixclk >>= (double_scan ? 1 : 0); |
| |
| #ifdef SIIHDMI_USE_FB |
| if (fb_mode_is_equal(&tx->sink.preferred_mode, mode)) |
| flag = '*'; |
| #endif |
| |
| #ifdef SIIHDMI_USE_FB |
| if (mode->flag & FB_MODE_IS_CEA) |
| flag = 'C'; |
| #endif |
| INFO(" %c \"%dx%d@%d%s\" %lu.%.2lu %u %u %u %u %u %u %u %u %chsync %cvsync", |
| /* CEA or preferred status of modeline */ |
| flag, |
| |
| /* mode name */ |
| mode->xres, mode->yres, |
| mode->refresh << (interlaced ? 1 : 0), |
| interlaced ? "i" : (double_scan ? "d" : ""), |
| |
| /* dot clock frequency (MHz) */ |
| pixclk / 1000ul, |
| pixclk % 1000ul, |
| |
| /* horizontal timings */ |
| mode->xres, |
| mode->xres + mode->right_margin, |
| mode->xres + mode->right_margin + mode->hsync_len, |
| mode->xres + mode->right_margin + mode->hsync_len + mode->left_margin, |
| |
| /* vertical timings */ |
| mode->yres, |
| mode->yres + mode->lower_margin, |
| mode->yres + mode->lower_margin + mode->vsync_len, |
| mode->yres + mode->lower_margin + mode->vsync_len + mode->upper_margin, |
| |
| /* sync direction */ |
| (mode->sync & FB_SYNC_HOR_HIGH_ACT) ? '+' : '-', |
| (mode->sync & FB_SYNC_VERT_HIGH_ACT) ? '+' : '-'); |
| |
| if (message) |
| CONTINUE(" (%s)", message); |
| |
| CONTINUE("\n"); |
| } |
| |
| static int siihdmi_set_resolution(struct siihdmi_tx *tx, |
| const struct fb_videomode *mode) |
| { |
| u8 ctrl; |
| int ret; |
| |
| #ifdef SIIHDMI_USE_FB |
| /* don't care if config differs from FB */ |
| if (0 == memcmp((void *) &tx->sink.current_mode, (void *) mode, sizeof(struct fb_videomode))) |
| { |
| return 0; |
| } |
| #endif |
| |
| INFO("selected configuration: \n"); |
| siihdmi_print_modeline(tx, mode, NULL); |
| |
| ctrl = i2c_smbus_read_byte_data(tx->client, SIIHDMI_TPI_REG_SYS_CTRL); |
| |
| /* setup the sink type */ |
| if (tx->sink.type == SINK_TYPE_DVI) |
| ctrl &= ~SIIHDMI_SYS_CTRL_OUTPUT_MODE_SELECT_HDMI; |
| else |
| ctrl |= SIIHDMI_SYS_CTRL_OUTPUT_MODE_SELECT_HDMI; |
| |
| /* step 1: (potentially) disable HDCP */ |
| |
| /* step 2: (optionally) blank the display */ |
| /* |
| * Note that if we set the AV Mute, switching to DVI could result in a |
| * permanently muted display until a hardware reset. Thus only do this |
| * if the sink is a HDMI connection |
| */ |
| if (tx->sink.type == SINK_TYPE_HDMI) |
| ctrl |= SIIHDMI_SYS_CTRL_AV_MUTE_HDMI; |
| /* optimisation: merge the write into the next one */ |
| |
| /* step 3: prepare for resolution change */ |
| ctrl |= SIIHDMI_SYS_CTRL_TMDS_OUTPUT_POWER_DOWN; |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_SYS_CTRL, |
| ctrl); |
| if (ret < 0) |
| DEBUG("unable to prepare for resolution change\n"); |
| |
| msleep(SIIHDMI_CTRL_INFO_FRAME_DRAIN_TIME); |
| |
| /* step 4: change video resolution */ |
| |
| /* step 5: set the vmode registers */ |
| siihdmi_set_vmode_registers(tx, mode); |
| siihdmi_check_embsync_extract(tx); |
| |
| /* |
| * step 6: |
| * [DVI] clear AVI InfoFrame |
| * [HDMI] set AVI InfoFrame |
| */ |
| if (tx->sink.type == SINK_TYPE_HDMI) |
| siihdmi_set_avi_info_frame(tx); |
| else |
| siihdmi_clear_avi_info_frame(tx); |
| siihdmi_check_embsync_extract(tx); |
| |
| /* step 7: [HDMI] set new audio information */ |
| if (tx->sink.type == SINK_TYPE_HDMI) { |
| if (tx->audio.available) |
| siihdmi_configure_audio(tx); |
| siihdmi_set_spd_info_frame(tx); |
| } |
| |
| /* step 8: enable display */ |
| ctrl &= ~SIIHDMI_SYS_CTRL_TMDS_OUTPUT_POWER_DOWN; |
| /* optimisation: merge the write into the next one */ |
| |
| /* step 9: (optionally) un-blank the display */ |
| ctrl &= ~SIIHDMI_SYS_CTRL_AV_MUTE_HDMI; |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_SYS_CTRL, |
| ctrl); |
| if (ret < 0) |
| DEBUG("unable to enable the display\n"); |
| |
| /* step 10: (potentially) enable HDCP */ |
| |
| #ifdef SIIHDMI_USE_FB |
| memcpy((void *) &tx->sink.current_mode, mode, sizeof(struct fb_videomode)); |
| #endif |
| |
| return ret; |
| } |
| |
| #ifdef SIIHDMI_USE_FB |
| static void siihdmi_dump_modelines(struct siihdmi_tx *tx) |
| { |
| const struct fb_modelist *entry; |
| const struct list_head * const modelines = &tx->info->modelist; |
| |
| INFO("supported modelines:\n"); |
| list_for_each_entry(entry, modelines, list) |
| siihdmi_print_modeline(tx, &entry->mode, NULL); |
| } |
| #endif |
| |
| static inline void siihdmi_process_extensions(struct siihdmi_tx *tx) |
| { |
| const struct edid_block0 * const block0 = |
| (struct edid_block0 *) tx->edid.data; |
| const struct edid_extension * const extensions = |
| (struct edid_extension *) (tx->edid.data + sizeof(*block0)); |
| u8 i; |
| |
| for (i = 0; i < block0->extensions; i++) { |
| const struct edid_extension * const extension = &extensions[i]; |
| |
| if (!edid_verify_checksum((u8 *) extension)) |
| WARNING("EDID block %u CRC mismatch\n", i); |
| |
| switch (extension->tag) { |
| case EDID_EXTENSION_CEA: |
| siihdmi_parse_cea861_timing_block(tx, extension); |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| |
| #ifdef SIIHDMI_USE_FB |
| |
| static const struct fb_videomode * |
| _find_similar_mode(const struct fb_videomode * const mode, struct list_head *head) |
| { |
| const struct fb_modelist *entry, *next; |
| |
| list_for_each_entry_safe(entry, next, head, list) { |
| if (fb_res_is_equal(mode, &entry->mode) && (mode != &entry->mode)) |
| return &entry->mode; |
| } |
| |
| return NULL; |
| } |
| |
| static void siihdmi_sanitize_modelist(struct siihdmi_tx * const tx) |
| { |
| struct list_head *modelist = &tx->info->modelist; |
| const struct fb_modelist *entry, *next; |
| const struct fb_videomode *mode; |
| int num_removed = 0; |
| |
| if ((mode = fb_find_best_display(&tx->info->monspecs, modelist))) |
| tx->sink.preferred_mode = *mode; |
| |
| list_for_each_entry_safe(entry, next, modelist, list) { |
| const char *reason = NULL; |
| mode = &entry->mode; |
| |
| if (mode->vmode & FB_VMODE_INTERLACED) { |
| reason = "interlaced"; |
| } else if (mode->vmode & FB_VMODE_DOUBLE) { |
| reason = "doublescan"; |
| } else if (mode->pixclock < tx->platform->pixclock) { |
| reason = "pixel clock exceeded"; |
| } else if ((tx->sink.type == SINK_TYPE_HDMI) && mode->lower_margin < 2) { |
| /* |
| * HDMI spec (§ 5.1.2) stipulates ≥2 lines of vsync |
| * |
| * We do not care so much on DVI, although it may be that the SII9022 cannot |
| * actually display this mode. Requires testing!! |
| */ |
| reason = "insufficient margin"; |
| } else { |
| const struct fb_videomode *match = _find_similar_mode(mode, modelist); |
| |
| if (match) { |
| /* |
| * Prefer detailed timings found in EDID. Certain sinks support slight |
| * variations of VESA/CEA timings, and using those allows us to support |
| * a wider variety of monitors. |
| */ |
| if ((~(mode->flag) & FB_MODE_IS_DETAILED) && |
| (match->flag & FB_MODE_IS_DETAILED)) { |
| reason = "detailed match present"; |
| } else if ((~(mode->flag) & FB_MODE_IS_CEA) && |
| (match->flag & FB_MODE_IS_CEA)) { |
| if ((tx->sink.type == SINK_TYPE_HDMI) && !useitmodes) { |
| /* |
| * for HDMI connections we want to remove any detailed timings |
| * and leave in CEA mode timings. This is on the basis that you |
| * would expect HDMI monitors to do better with CEA (TV) modes |
| * than you would PC modes. No data is truly lost: these modes |
| * are duplicated in terms of size and refresh but may have |
| * subtle differences insofaras more compatible timings. |
| * |
| * That is, unless we want to prefer IT modes, since most TVs |
| * will overscan CEA modes (720p, 1080p) by default, but display |
| * IT (PC) modes to the edge of the screen. |
| */ |
| reason = "CEA match present"; |
| } else { |
| /* |
| * DVI connections are the opposite to the above; remove CEA |
| * modes which duplicate normal modes, on the basis that a |
| * DVI sink will better display a standard EDID mode but may |
| * not be fully compatible with CEA timings. This is the |
| * behavior on HDMI sinks if we want to prefer IT modes. |
| * |
| * All we do is copy the matched mode into the mode value |
| * such that we remove the correct mode below. |
| */ |
| mode = match; |
| reason = "IT match present"; |
| } |
| } |
| } |
| } |
| |
| if (reason) { |
| struct fb_modelist *modelist = |
| container_of(mode, struct fb_modelist, mode); |
| |
| if (num_removed == 0) { // first time only |
| INFO("Unsupported modelines:\n"); |
| } |
| |
| siihdmi_print_modeline(tx, mode, reason); |
| |
| list_del(&modelist->list); |
| kfree(&modelist->list); |
| num_removed++; |
| } |
| } |
| |
| if (num_removed > 0) { |
| INFO("discarded %u incompatible modes\n", num_removed); |
| } |
| } |
| |
| static inline const struct fb_videomode *_match(const struct fb_videomode * const mode, |
| struct list_head *modelist) |
| { |
| const struct fb_videomode *match; |
| |
| if ((match = fb_find_best_mode_at_most(mode, modelist))) |
| return match; |
| |
| return fb_find_nearest_mode(mode, modelist); |
| } |
| #endif |
| |
| /* This function iterates through the videomode and removes those we don't |
| * support */ |
| static void siihdmi_filter_monspecs(struct fb_monspecs *monspecs) { |
| int i, j; |
| |
| for (i = 0, j = 0; i < monspecs->modedb_len; i++) { |
| struct fb_videomode *m = &monspecs->modedb[i]; |
| |
| if (m->vmode & FB_VMODE_INTERLACED) { |
| // SIIHDMI doesn't handle interlaced video |
| continue; |
| } |
| |
| // We can use this mode |
| memmove(&monspecs->modedb[j++], m, sizeof(*m)); |
| } |
| |
| monspecs->modedb_len = j; |
| } |
| |
| static int siihdmi_setup_display(struct siihdmi_tx *tx) |
| { |
| #ifdef SIIHDMI_USE_FB |
| struct fb_var_screeninfo var = {0}; |
| #endif |
| struct edid_block0 *block0; |
| unsigned width; |
| unsigned height; |
| int ret; |
| u8 isr; |
| u8 *new; |
| int i; |
| |
| BUILD_BUG_ON(sizeof(struct edid_block0) != EDID_BLOCK_SIZE); |
| BUILD_BUG_ON(sizeof(struct edid_extension) != EDID_BLOCK_SIZE); |
| |
| /* defaults */ |
| tx->sink.scanning = SCANNING_EXACT; |
| tx->sink.type = SINK_TYPE_DVI; |
| tx->audio.available = false; |
| |
| isr = i2c_smbus_read_byte_data(tx->client, SIIHDMI_TPI_REG_ISR); |
| DEBUG("hotplug: display %s, powered %s\n", |
| (isr & SIIHDMI_ISR_DISPLAY_ATTACHED) ? "attached" : "detached", |
| (isr & SIIHDMI_ISR_RECEIVER_SENSE) ? "on" : "off"); |
| |
| if (~isr & (SIIHDMI_ISR_DISPLAY_ATTACHED)) |
| return siihdmi_power_down(tx); |
| |
| /* Free previous video mode database and selected mode */ |
| if (tx->monspecs.modedb != NULL) |
| { |
| fb_destroy_modedb(tx->monspecs.modedb); |
| tx->selected_mode = NULL; |
| tx->monspecs.modedb = NULL; |
| tx->monspecs.modedb_len = 0; |
| } |
| |
| /* Allocate EDID buffer for one block */ |
| if (tx->edid.data != NULL) |
| kfree(tx->edid.data); |
| tx->edid.data = kzalloc(EDID_BLOCK_SIZE, GFP_KERNEL); |
| if (!tx->edid.data) |
| return -ENOMEM; |
| |
| /* use EDID to detect sink characteristics */ |
| ret = siihdmi_read_edid(tx, tx->edid.data, 0, EDID_BLOCK_SIZE); |
| block0 = (struct edid_block0 *) tx->edid.data; |
| |
| if (ret < 0 || !edid_verify_checksum((u8 *) block0)) { |
| WARNING("couldn't read EDID, selecting default vmode"); |
| /* Set display to 640x480p @ 60Hz */ |
| tx->selected_mode = avifb_select_mode( |
| tx->platform->lcd_id, |
| &default_monspecs); |
| return siihdmi_set_resolution(tx, tx->selected_mode); |
| } |
| |
| /* Reallocate space for block 0 as well as the extensions */ |
| tx->edid.length = (block0->extensions + 1) * EDID_BLOCK_SIZE; |
| new = krealloc(tx->edid.data, tx->edid.length, GFP_KERNEL); |
| if (!new) |
| return -ENOMEM; |
| tx->edid.data = new; |
| block0 = (struct edid_block0 *) new; |
| |
| /* create monspecs from EDID for the basic stuff */ |
| fb_edid_to_monspecs(tx->edid.data, &tx->monspecs); |
| |
| /* Read all E-EDID */ |
| for (i = 1; i <= block0->extensions; i++) |
| { |
| /* Read an E-EDID block */ |
| new = tx->edid.data + (i * EDID_BLOCK_SIZE); |
| ret = siihdmi_read_edid(tx, new, i, EDID_BLOCK_SIZE); |
| if (ret >= 0) |
| { |
| /* Add monspecs from E-EDID */ |
| fb_edid_add_monspecs(new, &tx->monspecs); |
| } |
| } |
| |
| if (block0->extensions) |
| siihdmi_process_extensions(tx); |
| |
| /* Filter unsupported monspecs */ |
| siihdmi_filter_monspecs(&tx->monspecs); |
| |
| tx->selected_mode = avifb_select_mode(tx->platform->lcd_id, |
| tx->monspecs.modedb_len > 0 ? |
| &tx->monspecs : |
| &default_monspecs); |
| |
| if (IS_ERR_OR_NULL(tx->selected_mode)) |
| return tx->selected_mode ? PTR_ERR(tx->selected_mode) : -ENODEV; |
| |
| /* Width and height in the EDID block are in cm */ |
| width = block0->maximum_horizontal_image_size * 10; |
| height = block0->maximum_vertical_image_size * 10; |
| |
| if (avifb_set_screen_size(tx->platform->lcd_id, |
| width, |
| height) < 0) |
| return -ENODEV; |
| |
| /* Set monitor specs in avifb */ |
| avifb_set_monspecs(tx->platform->lcd_id, &tx->monspecs); |
| |
| ret = siihdmi_set_resolution(tx, tx->selected_mode); |
| if (ret < 0) |
| return ret; |
| |
| #ifdef SIIHDMI_USE_FB |
| /* activate the framebuffer */ |
| fb_videomode_to_var(&var, mode); |
| var.activate = FB_ACTIVATE_ALL; |
| |
| console_lock(); |
| tx->info->flags |= FBINFO_MISC_USEREVENT; |
| fb_set_var(tx->info, &var); |
| tx->info->flags &= ~FBINFO_MISC_USEREVENT; |
| console_unlock(); |
| #endif |
| |
| return 0; |
| } |
| |
| #ifdef SIIHDMI_USE_FB |
| |
| static int siihdmi_blank(struct siihdmi_tx *tx) |
| { |
| u8 data; |
| |
| data = i2c_smbus_read_byte_data(tx->client, SIIHDMI_TPI_REG_RQB); |
| |
| return i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_RQB, |
| data | SIIHDMI_RQB_FORCE_VIDEO_BLANK); |
| } |
| |
| static int siihdmi_unblank(struct siihdmi_tx *tx) |
| { |
| u8 data; |
| |
| data = i2c_smbus_read_byte_data(tx->client, SIIHDMI_TPI_REG_RQB); |
| |
| return i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_RQB, |
| data & ~SIIHDMI_RQB_FORCE_VIDEO_BLANK); |
| } |
| |
| static int siihdmi_fb_event_handler(struct notifier_block *nb, |
| unsigned long val, |
| void *v) |
| { |
| const struct fb_event * const event = v; |
| struct siihdmi_tx * const tx = container_of(nb, struct siihdmi_tx, nb); |
| |
| if (strcmp(event->info->fix.id, tx->platform->framebuffer)) |
| return 0; |
| |
| switch (val) { |
| case FB_EVENT_FB_REGISTERED: |
| case FB_EVENT_FB_UNREGISTERED: |
| return siihdmi_setup_display(tx); |
| |
| case FB_EVENT_MODE_CHANGE: |
| if (event->info->mode) |
| return siihdmi_set_resolution(tx, event->info->mode); |
| case FB_EVENT_MODE_CHANGE_ALL: |
| /* is handled above, removes a "unhandled event" warning in dmesg */ |
| break; |
| case FB_EVENT_BLANK: |
| switch (*((int *) event->data)) { |
| case FB_BLANK_POWERDOWN: |
| /* do NOT siihdmi_power_down() here */ |
| case FB_BLANK_VSYNC_SUSPEND: |
| case FB_BLANK_HSYNC_SUSPEND: |
| case FB_BLANK_NORMAL: |
| return siihdmi_blank(tx); |
| case FB_BLANK_UNBLANK: |
| return siihdmi_unblank(tx); |
| } |
| break; |
| default: |
| DEBUG("unhandled fb event 0x%lx", val); |
| break; |
| } |
| |
| return 0; |
| } |
| #endif |
| |
| static irqreturn_t siihdmi_hotplug_event(int irq, void *dev_id) |
| { |
| struct siihdmi_tx *tx = dev_id; |
| u8 isr; |
| |
| isr = i2c_smbus_read_byte_data(tx->client, SIIHDMI_TPI_REG_ISR); |
| if (~isr & SIIHDMI_ISR_HOT_PLUG_EVENT) |
| goto complete; |
| |
| DEBUG("hotplug: display %s, powered %s\n", |
| (isr & SIIHDMI_ISR_DISPLAY_ATTACHED) ? "attached" : "detached", |
| (isr & SIIHDMI_ISR_RECEIVER_SENSE) ? "on" : "off"); |
| |
| if (isr & SIIHDMI_ISR_DISPLAY_ATTACHED) { |
| siihdmi_power_up(tx); |
| siihdmi_setup_display(tx); |
| } else { |
| siihdmi_power_down(tx); |
| } |
| |
| if (isr & SIIHDMI_ISR_HOT_PLUG_EVENT) { |
| switch_set_state(&tx->sdev,!!(isr & SIIHDMI_ISR_DISPLAY_ATTACHED)); |
| if (tx->platform->on_hotplug) |
| tx->platform->on_hotplug(!!(isr & SIIHDMI_ISR_DISPLAY_ATTACHED)); |
| } |
| |
| complete: |
| /* clear the interrupt */ |
| i2c_smbus_write_byte_data(tx->client, SIIHDMI_TPI_REG_ISR, isr); |
| |
| return IRQ_HANDLED; |
| } |
| |
| #if defined(CONFIG_SYSFS) |
| static ssize_t siihdmi_sysfs_read_edid(struct file *file, |
| struct kobject *kobj, |
| struct bin_attribute *bin_attr, |
| char *buf, loff_t offset, size_t count) |
| { |
| const struct siihdmi_tx * const tx = |
| container_of(bin_attr, struct siihdmi_tx, edid.attributes); |
| |
| return memory_read_from_buffer(buf, count, &offset, |
| tx->edid.data, tx->edid.length); |
| } |
| |
| static ssize_t siihdmi_sysfs_read_audio(struct file *file, |
| struct kobject *kobj, |
| struct bin_attribute *bin_attr, |
| char *buf, loff_t off, size_t count) |
| { |
| static const char * const sources[] = { "none\n", "hdmi\n" }; |
| const struct siihdmi_tx * const tx = |
| container_of(bin_attr, struct siihdmi_tx, audio.attributes); |
| |
| return memory_read_from_buffer(buf, count, &off, |
| sources[tx->audio.available], |
| strlen(sources[tx->audio.available])); |
| } |
| #endif |
| |
| static int __devinit siihdmi_probe(struct i2c_client *client, |
| const struct i2c_device_id *id) |
| { |
| struct siihdmi_tx *tx; |
| int ret; |
| |
| tx = kzalloc(sizeof(struct siihdmi_tx), GFP_KERNEL); |
| if (!tx) |
| return -ENOMEM; |
| |
| tx->client = client; |
| tx->platform = client->dev.platform_data; |
| |
| i2c_set_clientdata(client, tx); |
| |
| /* Check if device is present (chip must be reset before checking I2C) */ |
| if (tx->platform->reset) |
| tx->platform->reset(); |
| ret = i2c_smbus_write_byte_data(tx->client, SIIHDMI_TPI_REG_RQB, 0x00); |
| if (ret < 0) { |
| return -ENODEV; |
| } |
| |
| tx->sdev.name = "hdmi"; |
| ret = switch_dev_register(&tx->sdev); |
| if (ret) |
| goto switch_dev_register_failed; |
| |
| /* i2c_client irq is set in parrot_init_i2c_slave_shared_gpio */ |
| tx->platform->hotplug.start = client->irq; |
| ret = request_threaded_irq(tx->platform->hotplug.start, NULL, siihdmi_hotplug_event, |
| tx->platform->hotplug.flags, |
| tx->platform->hotplug.name, tx); |
| if (ret < 0) |
| WARNING("failed to setup hotplug interrupt: %d\n", ret); |
| else |
| tx->hotplug.enabled = true; |
| |
| /* reinitialise the device to configure irq */ |
| /* step 1: reset and initialise */ |
| if (tx->platform->reset) |
| tx->platform->reset(); |
| /* Do not use spinlock here, the siihdmi_initialise |
| * will call scheduler (need to wait the chip) */ |
| ret = siihdmi_initialise(tx); |
| if (ret < 0) |
| goto irq_initialize_failed; |
| /* Do nothing on the chip configuration because an |
| * interrupt can be raised at anytime. |
| * This will totally break the chip */ |
| |
| #if defined(CONFIG_SYSFS) |
| /* /sys/<>/edid */ |
| tx->edid.attributes.attr.name = "edid"; |
| tx->edid.attributes.attr.mode = 0444; |
| |
| /* maximum size of EDID, not necessarily the size of our data */ |
| tx->edid.attributes.size = SZ_32K; |
| tx->edid.attributes.read = siihdmi_sysfs_read_edid; |
| |
| sysfs_attr_init(&tx->edid.attributes.attr); |
| if (sysfs_create_bin_file(&tx->client->dev.kobj, &tx->edid.attributes) < 0) |
| WARNING("unable to construct attribute for sysfs exported EDID\n"); |
| |
| /* /sys/<>/audio */ |
| tx->audio.attributes.attr.name = "audio"; |
| tx->audio.attributes.attr.mode = 0444; |
| |
| /* we only want to return the value "hdmi" or "none" */ |
| tx->audio.attributes.size = 5; |
| tx->audio.attributes.read = siihdmi_sysfs_read_audio; |
| |
| sysfs_attr_init(&tx->audio.attributes.attr); |
| if (sysfs_create_bin_file(&tx->client->dev.kobj, &tx->audio.attributes) < 0) |
| WARNING("unable to construct attribute for sysfs exported audio\n"); |
| #endif |
| |
| return 0; |
| |
| irq_initialize_failed: |
| if (tx->platform->hotplug.start) |
| free_irq(tx->platform->hotplug.start, tx); |
| switch_dev_unregister(&tx->sdev); |
| switch_dev_register_failed: |
| i2c_set_clientdata(client, NULL); |
| kfree(tx); |
| return ret; |
| } |
| |
| static int __devexit siihdmi_remove(struct i2c_client *client) |
| { |
| struct siihdmi_tx *tx; |
| |
| tx = i2c_get_clientdata(client); |
| |
| if (!tx) |
| return -ENODEV; |
| |
| siihdmi_power_down(tx); |
| |
| if (tx->platform->hotplug.start) |
| free_irq(tx->platform->hotplug.start, tx); |
| |
| #if defined(CONFIG_SYSFS) |
| sysfs_remove_bin_file(&tx->client->dev.kobj, &tx->edid.attributes); |
| sysfs_remove_bin_file(&tx->client->dev.kobj, &tx->audio.attributes); |
| #endif |
| |
| if (tx->edid.data) |
| kfree(tx->edid.data); |
| |
| switch_dev_unregister(&tx->sdev); |
| i2c_set_clientdata(client, NULL); |
| kfree(tx); |
| |
| return 0; |
| } |
| |
| #ifdef CONFIG_PM_SLEEP |
| static int siihdmi_suspend(struct i2c_client *client, pm_message_t state) |
| { |
| DEBUG("suspend\n"); |
| return 0; |
| } |
| |
| static int siihdmi_resume(struct i2c_client *client) |
| { |
| int ret; |
| struct siihdmi_tx *tx; |
| DEBUG("resume\n"); |
| tx = i2c_get_clientdata(client); |
| if (tx == NULL) { |
| ERROR("Unconfigured siihdmi\n"); |
| return 1; |
| } |
| |
| #if 0 |
| if ((ret = siihdmi_initialise(tx)) < 0) |
| return ret; |
| ret = siihdmi_setup_display(tx); |
| #else |
| /* Try to initialize faster */ |
| ret = i2c_smbus_write_byte_data(tx->client, SIIHDMI_TPI_REG_RQB, 0x00); |
| if (ret < 0) { |
| WARNING("unable to initialise device to TPI mode\n"); |
| return ret; |
| } |
| |
| /* power up transmitter */ |
| if ((ret = siihdmi_power_up(tx)) < 0) |
| return ret; |
| |
| if (tx->hotplug.enabled) { |
| /* Re-enable interrupt service */ |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_IER, |
| SIIHDMI_IER_HOT_PLUG_EVENT); |
| if (ret < 0) |
| WARNING("unable to setup interrupt request\n"); |
| } |
| |
| ret = i2c_smbus_write_byte_data(tx->client, |
| SIIHDMI_TPI_REG_SYS_CTRL, |
| SIIHDMI_SYS_CTRL_OUTPUT_MODE_SELECT_HDMI); |
| #endif |
| return 0; |
| } |
| #else |
| #define siihdmi_suspend NULL |
| #define siihdmi_resume NULL |
| #endif |
| |
| static const struct i2c_device_id siihdmi_device_table[] = { |
| { "siihdmi", 0 }, |
| { }, |
| }; |
| |
| static struct i2c_driver siihdmi_driver = { |
| .driver = { |
| .owner = THIS_MODULE, |
| .name = "siihdmi", |
| }, |
| .suspend = siihdmi_suspend, |
| .resume = siihdmi_resume, |
| .probe = siihdmi_probe, |
| .remove = __devexit_p(siihdmi_remove), |
| .id_table = siihdmi_device_table, |
| }; |
| |
| #ifdef MODULE |
| module_i2c_driver(siihdmi_driver); |
| #else |
| static int __init siihdmi_init(void) |
| { |
| return i2c_add_driver(&siihdmi_driver); |
| } |
| |
| late_initcall(siihdmi_init); |
| #endif |
| |
| /* Module Information */ |
| MODULE_AUTHOR("Saleem Abdulrasool <compnerd@compnerd.org>"); |
| MODULE_LICENSE("GPL"); |
| MODULE_DESCRIPTION("Silicon Image SiI9xxx TMDS Driver"); |
| MODULE_DEVICE_TABLE(i2c, siihdmi_device_table); |