mirror of
https://mirrors.bfsu.edu.cn/git/linux.git
synced 2024-12-18 00:24:58 +08:00
15fe53be46
If DSI host attachment fails, the LT9611UXC driver will remove the
bridge without ensuring that there is no outstanding HPD work being
done. In rare cases this can result in the warnings regarding the mutex
being incorrect. Fix this by forcebly freing IRQ and flushing the work.
DEBUG_LOCKS_WARN_ON(lock->magic != lock)
WARNING: CPU: 0 PID: 10 at kernel/locking/mutex.c:582 __mutex_lock+0x468/0x77c
Modules linked in:
CPU: 0 PID: 10 Comm: kworker/0:1 Tainted: G U 6.6.0-rc5-next-20231011-gd81f81c2b682-dirty #1206
Hardware name: Qualcomm Technologies, Inc. Robotics RB5 (DT)
Workqueue: events lt9611uxc_hpd_work
pstate: 60400005 (nZCv daif +PAN -UAO -TCO -DIT -SSBS BTYPE=--)
pc : __mutex_lock+0x468/0x77c
lr : __mutex_lock+0x468/0x77c
sp : ffff8000800a3c70
x29: ffff8000800a3c70 x28: 0000000000000000 x27: ffffd595fe333000
x26: ffff7c2f0002c005 x25: ffffd595ff1b3000 x24: ffffd595fccda5a0
x23: 0000000000000000 x22: 0000000000000002 x21: ffff7c2f056d91c8
x20: 0000000000000000 x19: ffff7c2f056d91c8 x18: fffffffffffe8db0
x17: 000000040044ffff x16: 005000f2b5503510 x15: 0000000000000000
x14: 000000000006efb8 x13: 0000000000000000 x12: 0000000000000037
x11: 0000000000000001 x10: 0000000000001470 x9 : ffff8000800a3ae0
x8 : ffff7c2f0027f8d0 x7 : ffff7c2f0027e400 x6 : ffffd595fc702b54
x5 : 0000000000000000 x4 : ffff8000800a0000 x3 : 0000000000000000
x2 : 0000000000000000 x1 : 0000000000000000 x0 : ffff7c2f0027e400
Call trace:
__mutex_lock+0x468/0x77c
mutex_lock_nested+0x24/0x30
drm_bridge_hpd_notify+0x2c/0x5c
lt9611uxc_hpd_work+0x6c/0x80
process_one_work+0x1ec/0x51c
worker_thread+0x1ec/0x3e4
kthread+0x120/0x124
ret_from_fork+0x10/0x20
irq event stamp: 15799
hardirqs last enabled at (15799): [<ffffd595fc702ba4>] finish_task_switch.isra.0+0xa8/0x278
hardirqs last disabled at (15798): [<ffffd595fd5a1580>] __schedule+0x7b8/0xbd8
softirqs last enabled at (15794): [<ffffd595fc690698>] __do_softirq+0x498/0x4e0
softirqs last disabled at (15771): [<ffffd595fc69615c>] ____do_softirq+0x10/0x1c
Fixes: bc6fa8676e
("drm/bridge/lontium-lt9611uxc: move HPD notification out of IRQ handler")
Signed-off-by: Dmitry Baryshkov <dmitry.baryshkov@linaro.org>
Reviewed-by: Robert Foss <rfoss@kernel.org>
Signed-off-by: Robert Foss <rfoss@kernel.org>
Link: https://patchwork.freedesktop.org/patch/msgid/20231011220002.382422-1-dmitry.baryshkov@linaro.org
1028 lines
26 KiB
C
1028 lines
26 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Copyright (c) 2018, The Linux Foundation. All rights reserved.
|
|
* Copyright (c) 2019-2020. Linaro Limited.
|
|
*/
|
|
|
|
#include <linux/firmware.h>
|
|
#include <linux/gpio/consumer.h>
|
|
#include <linux/i2c.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/of_graph.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/regmap.h>
|
|
#include <linux/regulator/consumer.h>
|
|
#include <linux/wait.h>
|
|
#include <linux/workqueue.h>
|
|
|
|
#include <sound/hdmi-codec.h>
|
|
|
|
#include <drm/drm_atomic_helper.h>
|
|
#include <drm/drm_bridge.h>
|
|
#include <drm/drm_mipi_dsi.h>
|
|
#include <drm/drm_print.h>
|
|
#include <drm/drm_probe_helper.h>
|
|
|
|
#define EDID_BLOCK_SIZE 128
|
|
#define EDID_NUM_BLOCKS 2
|
|
|
|
#define FW_FILE "lt9611uxc_fw.bin"
|
|
|
|
struct lt9611uxc {
|
|
struct device *dev;
|
|
struct drm_bridge bridge;
|
|
struct drm_connector connector;
|
|
|
|
struct regmap *regmap;
|
|
/* Protects all accesses to registers by stopping the on-chip MCU */
|
|
struct mutex ocm_lock;
|
|
|
|
struct wait_queue_head wq;
|
|
struct work_struct work;
|
|
|
|
struct device_node *dsi0_node;
|
|
struct device_node *dsi1_node;
|
|
struct mipi_dsi_device *dsi0;
|
|
struct mipi_dsi_device *dsi1;
|
|
struct platform_device *audio_pdev;
|
|
|
|
struct gpio_desc *reset_gpio;
|
|
struct gpio_desc *enable_gpio;
|
|
|
|
struct regulator_bulk_data supplies[2];
|
|
|
|
struct i2c_client *client;
|
|
|
|
bool hpd_supported;
|
|
bool edid_read;
|
|
/* can be accessed from different threads, so protect this with ocm_lock */
|
|
bool hdmi_connected;
|
|
uint8_t fw_version;
|
|
};
|
|
|
|
#define LT9611_PAGE_CONTROL 0xff
|
|
|
|
static const struct regmap_range_cfg lt9611uxc_ranges[] = {
|
|
{
|
|
.name = "register_range",
|
|
.range_min = 0,
|
|
.range_max = 0xd0ff,
|
|
.selector_reg = LT9611_PAGE_CONTROL,
|
|
.selector_mask = 0xff,
|
|
.selector_shift = 0,
|
|
.window_start = 0,
|
|
.window_len = 0x100,
|
|
},
|
|
};
|
|
|
|
static const struct regmap_config lt9611uxc_regmap_config = {
|
|
.reg_bits = 8,
|
|
.val_bits = 8,
|
|
.max_register = 0xffff,
|
|
.ranges = lt9611uxc_ranges,
|
|
.num_ranges = ARRAY_SIZE(lt9611uxc_ranges),
|
|
};
|
|
|
|
struct lt9611uxc_mode {
|
|
u16 hdisplay;
|
|
u16 vdisplay;
|
|
u8 vrefresh;
|
|
};
|
|
|
|
/*
|
|
* This chip supports only a fixed set of modes.
|
|
* Enumerate them here to check whether the mode is supported.
|
|
*/
|
|
static struct lt9611uxc_mode lt9611uxc_modes[] = {
|
|
{ 1920, 1080, 60 },
|
|
{ 1920, 1080, 30 },
|
|
{ 1920, 1080, 25 },
|
|
{ 1366, 768, 60 },
|
|
{ 1360, 768, 60 },
|
|
{ 1280, 1024, 60 },
|
|
{ 1280, 800, 60 },
|
|
{ 1280, 720, 60 },
|
|
{ 1280, 720, 50 },
|
|
{ 1280, 720, 30 },
|
|
{ 1152, 864, 60 },
|
|
{ 1024, 768, 60 },
|
|
{ 800, 600, 60 },
|
|
{ 720, 576, 50 },
|
|
{ 720, 480, 60 },
|
|
{ 640, 480, 60 },
|
|
};
|
|
|
|
static struct lt9611uxc *bridge_to_lt9611uxc(struct drm_bridge *bridge)
|
|
{
|
|
return container_of(bridge, struct lt9611uxc, bridge);
|
|
}
|
|
|
|
static struct lt9611uxc *connector_to_lt9611uxc(struct drm_connector *connector)
|
|
{
|
|
return container_of(connector, struct lt9611uxc, connector);
|
|
}
|
|
|
|
static void lt9611uxc_lock(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
mutex_lock(<9611uxc->ocm_lock);
|
|
regmap_write(lt9611uxc->regmap, 0x80ee, 0x01);
|
|
}
|
|
|
|
static void lt9611uxc_unlock(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
regmap_write(lt9611uxc->regmap, 0x80ee, 0x00);
|
|
msleep(50);
|
|
mutex_unlock(<9611uxc->ocm_lock);
|
|
}
|
|
|
|
static irqreturn_t lt9611uxc_irq_thread_handler(int irq, void *dev_id)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = dev_id;
|
|
unsigned int irq_status = 0;
|
|
unsigned int hpd_status = 0;
|
|
|
|
lt9611uxc_lock(lt9611uxc);
|
|
|
|
regmap_read(lt9611uxc->regmap, 0xb022, &irq_status);
|
|
regmap_read(lt9611uxc->regmap, 0xb023, &hpd_status);
|
|
if (irq_status)
|
|
regmap_write(lt9611uxc->regmap, 0xb022, 0);
|
|
|
|
if (irq_status & BIT(0)) {
|
|
lt9611uxc->edid_read = !!(hpd_status & BIT(0));
|
|
wake_up_all(<9611uxc->wq);
|
|
}
|
|
|
|
if (irq_status & BIT(1)) {
|
|
lt9611uxc->hdmi_connected = hpd_status & BIT(1);
|
|
schedule_work(<9611uxc->work);
|
|
}
|
|
|
|
lt9611uxc_unlock(lt9611uxc);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static void lt9611uxc_hpd_work(struct work_struct *work)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = container_of(work, struct lt9611uxc, work);
|
|
bool connected;
|
|
|
|
if (lt9611uxc->connector.dev) {
|
|
if (lt9611uxc->connector.dev->mode_config.funcs)
|
|
drm_kms_helper_hotplug_event(lt9611uxc->connector.dev);
|
|
} else {
|
|
|
|
mutex_lock(<9611uxc->ocm_lock);
|
|
connected = lt9611uxc->hdmi_connected;
|
|
mutex_unlock(<9611uxc->ocm_lock);
|
|
|
|
drm_bridge_hpd_notify(<9611uxc->bridge,
|
|
connected ?
|
|
connector_status_connected :
|
|
connector_status_disconnected);
|
|
}
|
|
}
|
|
|
|
static void lt9611uxc_reset(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
gpiod_set_value_cansleep(lt9611uxc->reset_gpio, 1);
|
|
msleep(20);
|
|
|
|
gpiod_set_value_cansleep(lt9611uxc->reset_gpio, 0);
|
|
msleep(20);
|
|
|
|
gpiod_set_value_cansleep(lt9611uxc->reset_gpio, 1);
|
|
msleep(300);
|
|
}
|
|
|
|
static void lt9611uxc_assert_5v(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
if (!lt9611uxc->enable_gpio)
|
|
return;
|
|
|
|
gpiod_set_value_cansleep(lt9611uxc->enable_gpio, 1);
|
|
msleep(20);
|
|
}
|
|
|
|
static int lt9611uxc_regulator_init(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
int ret;
|
|
|
|
lt9611uxc->supplies[0].supply = "vdd";
|
|
lt9611uxc->supplies[1].supply = "vcc";
|
|
|
|
ret = devm_regulator_bulk_get(lt9611uxc->dev, 2, lt9611uxc->supplies);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return regulator_set_load(lt9611uxc->supplies[0].consumer, 200000);
|
|
}
|
|
|
|
static int lt9611uxc_regulator_enable(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
int ret;
|
|
|
|
ret = regulator_enable(lt9611uxc->supplies[0].consumer);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
usleep_range(1000, 10000); /* 50000 according to dtsi */
|
|
|
|
ret = regulator_enable(lt9611uxc->supplies[1].consumer);
|
|
if (ret < 0) {
|
|
regulator_disable(lt9611uxc->supplies[0].consumer);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct lt9611uxc_mode *lt9611uxc_find_mode(const struct drm_display_mode *mode)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(lt9611uxc_modes); i++) {
|
|
if (lt9611uxc_modes[i].hdisplay == mode->hdisplay &&
|
|
lt9611uxc_modes[i].vdisplay == mode->vdisplay &&
|
|
lt9611uxc_modes[i].vrefresh == drm_mode_vrefresh(mode)) {
|
|
return <9611uxc_modes[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static struct mipi_dsi_device *lt9611uxc_attach_dsi(struct lt9611uxc *lt9611uxc,
|
|
struct device_node *dsi_node)
|
|
{
|
|
const struct mipi_dsi_device_info info = { "lt9611uxc", 0, NULL };
|
|
struct mipi_dsi_device *dsi;
|
|
struct mipi_dsi_host *host;
|
|
struct device *dev = lt9611uxc->dev;
|
|
int ret;
|
|
|
|
host = of_find_mipi_dsi_host_by_node(dsi_node);
|
|
if (!host) {
|
|
dev_err(dev, "failed to find dsi host\n");
|
|
return ERR_PTR(-EPROBE_DEFER);
|
|
}
|
|
|
|
dsi = devm_mipi_dsi_device_register_full(dev, host, &info);
|
|
if (IS_ERR(dsi)) {
|
|
dev_err(dev, "failed to create dsi device\n");
|
|
return dsi;
|
|
}
|
|
|
|
dsi->lanes = 4;
|
|
dsi->format = MIPI_DSI_FMT_RGB888;
|
|
dsi->mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_SYNC_PULSE |
|
|
MIPI_DSI_MODE_VIDEO_HSE;
|
|
|
|
ret = devm_mipi_dsi_attach(dev, dsi);
|
|
if (ret < 0) {
|
|
dev_err(dev, "failed to attach dsi to host\n");
|
|
return ERR_PTR(ret);
|
|
}
|
|
|
|
return dsi;
|
|
}
|
|
|
|
static int lt9611uxc_connector_get_modes(struct drm_connector *connector)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = connector_to_lt9611uxc(connector);
|
|
unsigned int count;
|
|
struct edid *edid;
|
|
|
|
edid = drm_bridge_get_edid(<9611uxc->bridge, connector);
|
|
drm_connector_update_edid_property(connector, edid);
|
|
count = drm_add_edid_modes(connector, edid);
|
|
kfree(edid);
|
|
|
|
return count;
|
|
}
|
|
|
|
static enum drm_connector_status lt9611uxc_connector_detect(struct drm_connector *connector,
|
|
bool force)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = connector_to_lt9611uxc(connector);
|
|
|
|
return lt9611uxc->bridge.funcs->detect(<9611uxc->bridge);
|
|
}
|
|
|
|
static enum drm_mode_status lt9611uxc_connector_mode_valid(struct drm_connector *connector,
|
|
struct drm_display_mode *mode)
|
|
{
|
|
struct lt9611uxc_mode *lt9611uxc_mode = lt9611uxc_find_mode(mode);
|
|
|
|
return lt9611uxc_mode ? MODE_OK : MODE_BAD;
|
|
}
|
|
|
|
static const struct drm_connector_helper_funcs lt9611uxc_bridge_connector_helper_funcs = {
|
|
.get_modes = lt9611uxc_connector_get_modes,
|
|
.mode_valid = lt9611uxc_connector_mode_valid,
|
|
};
|
|
|
|
static const struct drm_connector_funcs lt9611uxc_bridge_connector_funcs = {
|
|
.fill_modes = drm_helper_probe_single_connector_modes,
|
|
.detect = lt9611uxc_connector_detect,
|
|
.destroy = drm_connector_cleanup,
|
|
.reset = drm_atomic_helper_connector_reset,
|
|
.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
|
|
.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
|
|
};
|
|
|
|
static int lt9611uxc_connector_init(struct drm_bridge *bridge, struct lt9611uxc *lt9611uxc)
|
|
{
|
|
int ret;
|
|
|
|
if (!bridge->encoder) {
|
|
DRM_ERROR("Parent encoder object not found");
|
|
return -ENODEV;
|
|
}
|
|
|
|
lt9611uxc->connector.polled = DRM_CONNECTOR_POLL_HPD;
|
|
|
|
drm_connector_helper_add(<9611uxc->connector,
|
|
<9611uxc_bridge_connector_helper_funcs);
|
|
ret = drm_connector_init(bridge->dev, <9611uxc->connector,
|
|
<9611uxc_bridge_connector_funcs,
|
|
DRM_MODE_CONNECTOR_HDMIA);
|
|
if (ret) {
|
|
DRM_ERROR("Failed to initialize connector with drm\n");
|
|
return ret;
|
|
}
|
|
|
|
return drm_connector_attach_encoder(<9611uxc->connector, bridge->encoder);
|
|
}
|
|
|
|
static int lt9611uxc_bridge_attach(struct drm_bridge *bridge,
|
|
enum drm_bridge_attach_flags flags)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = bridge_to_lt9611uxc(bridge);
|
|
int ret;
|
|
|
|
if (!(flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR)) {
|
|
ret = lt9611uxc_connector_init(bridge, lt9611uxc);
|
|
if (ret < 0)
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static enum drm_mode_status
|
|
lt9611uxc_bridge_mode_valid(struct drm_bridge *bridge,
|
|
const struct drm_display_info *info,
|
|
const struct drm_display_mode *mode)
|
|
{
|
|
struct lt9611uxc_mode *lt9611uxc_mode;
|
|
|
|
lt9611uxc_mode = lt9611uxc_find_mode(mode);
|
|
|
|
return lt9611uxc_mode ? MODE_OK : MODE_BAD;
|
|
}
|
|
|
|
static void lt9611uxc_video_setup(struct lt9611uxc *lt9611uxc,
|
|
const struct drm_display_mode *mode)
|
|
{
|
|
u32 h_total, hactive, hsync_len, hfront_porch;
|
|
u32 v_total, vactive, vsync_len, vfront_porch;
|
|
|
|
h_total = mode->htotal;
|
|
v_total = mode->vtotal;
|
|
|
|
hactive = mode->hdisplay;
|
|
hsync_len = mode->hsync_end - mode->hsync_start;
|
|
hfront_porch = mode->hsync_start - mode->hdisplay;
|
|
|
|
vactive = mode->vdisplay;
|
|
vsync_len = mode->vsync_end - mode->vsync_start;
|
|
vfront_porch = mode->vsync_start - mode->vdisplay;
|
|
|
|
regmap_write(lt9611uxc->regmap, 0xd00d, (u8)(v_total / 256));
|
|
regmap_write(lt9611uxc->regmap, 0xd00e, (u8)(v_total % 256));
|
|
|
|
regmap_write(lt9611uxc->regmap, 0xd00f, (u8)(vactive / 256));
|
|
regmap_write(lt9611uxc->regmap, 0xd010, (u8)(vactive % 256));
|
|
|
|
regmap_write(lt9611uxc->regmap, 0xd011, (u8)(h_total / 256));
|
|
regmap_write(lt9611uxc->regmap, 0xd012, (u8)(h_total % 256));
|
|
|
|
regmap_write(lt9611uxc->regmap, 0xd013, (u8)(hactive / 256));
|
|
regmap_write(lt9611uxc->regmap, 0xd014, (u8)(hactive % 256));
|
|
|
|
regmap_write(lt9611uxc->regmap, 0xd015, (u8)(vsync_len % 256));
|
|
|
|
regmap_update_bits(lt9611uxc->regmap, 0xd016, 0xf, (u8)(hsync_len / 256));
|
|
regmap_write(lt9611uxc->regmap, 0xd017, (u8)(hsync_len % 256));
|
|
|
|
regmap_update_bits(lt9611uxc->regmap, 0xd018, 0xf, (u8)(vfront_porch / 256));
|
|
regmap_write(lt9611uxc->regmap, 0xd019, (u8)(vfront_porch % 256));
|
|
|
|
regmap_update_bits(lt9611uxc->regmap, 0xd01a, 0xf, (u8)(hfront_porch / 256));
|
|
regmap_write(lt9611uxc->regmap, 0xd01b, (u8)(hfront_porch % 256));
|
|
}
|
|
|
|
static void lt9611uxc_bridge_mode_set(struct drm_bridge *bridge,
|
|
const struct drm_display_mode *mode,
|
|
const struct drm_display_mode *adj_mode)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = bridge_to_lt9611uxc(bridge);
|
|
|
|
lt9611uxc_lock(lt9611uxc);
|
|
lt9611uxc_video_setup(lt9611uxc, mode);
|
|
lt9611uxc_unlock(lt9611uxc);
|
|
}
|
|
|
|
static enum drm_connector_status lt9611uxc_bridge_detect(struct drm_bridge *bridge)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = bridge_to_lt9611uxc(bridge);
|
|
unsigned int reg_val = 0;
|
|
int ret;
|
|
bool connected = true;
|
|
|
|
lt9611uxc_lock(lt9611uxc);
|
|
|
|
if (lt9611uxc->hpd_supported) {
|
|
ret = regmap_read(lt9611uxc->regmap, 0xb023, ®_val);
|
|
|
|
if (ret)
|
|
dev_err(lt9611uxc->dev, "failed to read hpd status: %d\n", ret);
|
|
else
|
|
connected = reg_val & BIT(1);
|
|
}
|
|
lt9611uxc->hdmi_connected = connected;
|
|
|
|
lt9611uxc_unlock(lt9611uxc);
|
|
|
|
return connected ? connector_status_connected :
|
|
connector_status_disconnected;
|
|
}
|
|
|
|
static int lt9611uxc_wait_for_edid(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
return wait_event_interruptible_timeout(lt9611uxc->wq, lt9611uxc->edid_read,
|
|
msecs_to_jiffies(500));
|
|
}
|
|
|
|
static int lt9611uxc_get_edid_block(void *data, u8 *buf, unsigned int block, size_t len)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = data;
|
|
int ret;
|
|
|
|
if (len > EDID_BLOCK_SIZE)
|
|
return -EINVAL;
|
|
|
|
if (block >= EDID_NUM_BLOCKS)
|
|
return -EINVAL;
|
|
|
|
lt9611uxc_lock(lt9611uxc);
|
|
|
|
regmap_write(lt9611uxc->regmap, 0xb00b, 0x10);
|
|
|
|
regmap_write(lt9611uxc->regmap, 0xb00a, block * EDID_BLOCK_SIZE);
|
|
|
|
ret = regmap_noinc_read(lt9611uxc->regmap, 0xb0b0, buf, len);
|
|
if (ret)
|
|
dev_err(lt9611uxc->dev, "edid read failed: %d\n", ret);
|
|
|
|
lt9611uxc_unlock(lt9611uxc);
|
|
|
|
return 0;
|
|
};
|
|
|
|
static struct edid *lt9611uxc_bridge_get_edid(struct drm_bridge *bridge,
|
|
struct drm_connector *connector)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = bridge_to_lt9611uxc(bridge);
|
|
int ret;
|
|
|
|
ret = lt9611uxc_wait_for_edid(lt9611uxc);
|
|
if (ret < 0) {
|
|
dev_err(lt9611uxc->dev, "wait for EDID failed: %d\n", ret);
|
|
return NULL;
|
|
} else if (ret == 0) {
|
|
dev_err(lt9611uxc->dev, "wait for EDID timeout\n");
|
|
return NULL;
|
|
}
|
|
|
|
return drm_do_get_edid(connector, lt9611uxc_get_edid_block, lt9611uxc);
|
|
}
|
|
|
|
static const struct drm_bridge_funcs lt9611uxc_bridge_funcs = {
|
|
.attach = lt9611uxc_bridge_attach,
|
|
.mode_valid = lt9611uxc_bridge_mode_valid,
|
|
.mode_set = lt9611uxc_bridge_mode_set,
|
|
.detect = lt9611uxc_bridge_detect,
|
|
.get_edid = lt9611uxc_bridge_get_edid,
|
|
};
|
|
|
|
static int lt9611uxc_parse_dt(struct device *dev,
|
|
struct lt9611uxc *lt9611uxc)
|
|
{
|
|
lt9611uxc->dsi0_node = of_graph_get_remote_node(dev->of_node, 0, -1);
|
|
if (!lt9611uxc->dsi0_node) {
|
|
dev_err(lt9611uxc->dev, "failed to get remote node for primary dsi\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
lt9611uxc->dsi1_node = of_graph_get_remote_node(dev->of_node, 1, -1);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int lt9611uxc_gpio_init(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
struct device *dev = lt9611uxc->dev;
|
|
|
|
lt9611uxc->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
|
|
if (IS_ERR(lt9611uxc->reset_gpio)) {
|
|
dev_err(dev, "failed to acquire reset gpio\n");
|
|
return PTR_ERR(lt9611uxc->reset_gpio);
|
|
}
|
|
|
|
lt9611uxc->enable_gpio = devm_gpiod_get_optional(dev, "enable", GPIOD_OUT_LOW);
|
|
if (IS_ERR(lt9611uxc->enable_gpio)) {
|
|
dev_err(dev, "failed to acquire enable gpio\n");
|
|
return PTR_ERR(lt9611uxc->enable_gpio);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int lt9611uxc_read_device_rev(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
unsigned int rev0, rev1, rev2;
|
|
int ret;
|
|
|
|
lt9611uxc_lock(lt9611uxc);
|
|
|
|
ret = regmap_read(lt9611uxc->regmap, 0x8100, &rev0);
|
|
ret |= regmap_read(lt9611uxc->regmap, 0x8101, &rev1);
|
|
ret |= regmap_read(lt9611uxc->regmap, 0x8102, &rev2);
|
|
if (ret)
|
|
dev_err(lt9611uxc->dev, "failed to read revision: %d\n", ret);
|
|
else
|
|
dev_info(lt9611uxc->dev, "LT9611 revision: 0x%02x.%02x.%02x\n", rev0, rev1, rev2);
|
|
|
|
lt9611uxc_unlock(lt9611uxc);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int lt9611uxc_read_version(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
unsigned int rev;
|
|
int ret;
|
|
|
|
lt9611uxc_lock(lt9611uxc);
|
|
|
|
ret = regmap_read(lt9611uxc->regmap, 0xb021, &rev);
|
|
if (ret)
|
|
dev_err(lt9611uxc->dev, "failed to read revision: %d\n", ret);
|
|
else
|
|
dev_info(lt9611uxc->dev, "LT9611 version: 0x%02x\n", rev);
|
|
|
|
lt9611uxc_unlock(lt9611uxc);
|
|
|
|
return ret < 0 ? ret : rev;
|
|
}
|
|
|
|
static int lt9611uxc_hdmi_hw_params(struct device *dev, void *data,
|
|
struct hdmi_codec_daifmt *fmt,
|
|
struct hdmi_codec_params *hparms)
|
|
{
|
|
/*
|
|
* LT9611UXC will automatically detect rate and sample size, so no need
|
|
* to setup anything here.
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
static void lt9611uxc_audio_shutdown(struct device *dev, void *data)
|
|
{
|
|
}
|
|
|
|
static int lt9611uxc_hdmi_i2s_get_dai_id(struct snd_soc_component *component,
|
|
struct device_node *endpoint)
|
|
{
|
|
struct of_endpoint of_ep;
|
|
int ret;
|
|
|
|
ret = of_graph_parse_endpoint(endpoint, &of_ep);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/*
|
|
* HDMI sound should be located as reg = <2>
|
|
* Then, it is sound port 0
|
|
*/
|
|
if (of_ep.port == 2)
|
|
return 0;
|
|
|
|
return -EINVAL;
|
|
}
|
|
|
|
static const struct hdmi_codec_ops lt9611uxc_codec_ops = {
|
|
.hw_params = lt9611uxc_hdmi_hw_params,
|
|
.audio_shutdown = lt9611uxc_audio_shutdown,
|
|
.get_dai_id = lt9611uxc_hdmi_i2s_get_dai_id,
|
|
};
|
|
|
|
static int lt9611uxc_audio_init(struct device *dev, struct lt9611uxc *lt9611uxc)
|
|
{
|
|
struct hdmi_codec_pdata codec_data = {
|
|
.ops = <9611uxc_codec_ops,
|
|
.max_i2s_channels = 2,
|
|
.i2s = 1,
|
|
.data = lt9611uxc,
|
|
};
|
|
|
|
lt9611uxc->audio_pdev =
|
|
platform_device_register_data(dev, HDMI_CODEC_DRV_NAME,
|
|
PLATFORM_DEVID_AUTO,
|
|
&codec_data, sizeof(codec_data));
|
|
|
|
return PTR_ERR_OR_ZERO(lt9611uxc->audio_pdev);
|
|
}
|
|
|
|
static void lt9611uxc_audio_exit(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
if (lt9611uxc->audio_pdev) {
|
|
platform_device_unregister(lt9611uxc->audio_pdev);
|
|
lt9611uxc->audio_pdev = NULL;
|
|
}
|
|
}
|
|
|
|
#define LT9611UXC_FW_PAGE_SIZE 32
|
|
static void lt9611uxc_firmware_write_page(struct lt9611uxc *lt9611uxc, u16 addr, const u8 *buf)
|
|
{
|
|
struct reg_sequence seq_write_prepare[] = {
|
|
REG_SEQ0(0x805a, 0x04),
|
|
REG_SEQ0(0x805a, 0x00),
|
|
|
|
REG_SEQ0(0x805e, 0xdf),
|
|
REG_SEQ0(0x805a, 0x20),
|
|
REG_SEQ0(0x805a, 0x00),
|
|
REG_SEQ0(0x8058, 0x21),
|
|
};
|
|
|
|
struct reg_sequence seq_write_addr[] = {
|
|
REG_SEQ0(0x805b, (addr >> 16) & 0xff),
|
|
REG_SEQ0(0x805c, (addr >> 8) & 0xff),
|
|
REG_SEQ0(0x805d, addr & 0xff),
|
|
REG_SEQ0(0x805a, 0x10),
|
|
REG_SEQ0(0x805a, 0x00),
|
|
};
|
|
|
|
regmap_write(lt9611uxc->regmap, 0x8108, 0xbf);
|
|
msleep(20);
|
|
regmap_write(lt9611uxc->regmap, 0x8108, 0xff);
|
|
msleep(20);
|
|
regmap_multi_reg_write(lt9611uxc->regmap, seq_write_prepare, ARRAY_SIZE(seq_write_prepare));
|
|
regmap_noinc_write(lt9611uxc->regmap, 0x8059, buf, LT9611UXC_FW_PAGE_SIZE);
|
|
regmap_multi_reg_write(lt9611uxc->regmap, seq_write_addr, ARRAY_SIZE(seq_write_addr));
|
|
msleep(20);
|
|
}
|
|
|
|
static void lt9611uxc_firmware_read_page(struct lt9611uxc *lt9611uxc, u16 addr, char *buf)
|
|
{
|
|
struct reg_sequence seq_read_page[] = {
|
|
REG_SEQ0(0x805a, 0xa0),
|
|
REG_SEQ0(0x805a, 0x80),
|
|
REG_SEQ0(0x805b, (addr >> 16) & 0xff),
|
|
REG_SEQ0(0x805c, (addr >> 8) & 0xff),
|
|
REG_SEQ0(0x805d, addr & 0xff),
|
|
REG_SEQ0(0x805a, 0x90),
|
|
REG_SEQ0(0x805a, 0x80),
|
|
REG_SEQ0(0x8058, 0x21),
|
|
};
|
|
|
|
regmap_multi_reg_write(lt9611uxc->regmap, seq_read_page, ARRAY_SIZE(seq_read_page));
|
|
regmap_noinc_read(lt9611uxc->regmap, 0x805f, buf, LT9611UXC_FW_PAGE_SIZE);
|
|
}
|
|
|
|
static char *lt9611uxc_firmware_read(struct lt9611uxc *lt9611uxc, size_t size)
|
|
{
|
|
struct reg_sequence seq_read_setup[] = {
|
|
REG_SEQ0(0x805a, 0x84),
|
|
REG_SEQ0(0x805a, 0x80),
|
|
};
|
|
|
|
char *readbuf;
|
|
u16 offset;
|
|
|
|
readbuf = kzalloc(ALIGN(size, 32), GFP_KERNEL);
|
|
if (!readbuf)
|
|
return NULL;
|
|
|
|
regmap_multi_reg_write(lt9611uxc->regmap, seq_read_setup, ARRAY_SIZE(seq_read_setup));
|
|
|
|
for (offset = 0;
|
|
offset < size;
|
|
offset += LT9611UXC_FW_PAGE_SIZE)
|
|
lt9611uxc_firmware_read_page(lt9611uxc, offset, &readbuf[offset]);
|
|
|
|
return readbuf;
|
|
}
|
|
|
|
static int lt9611uxc_firmware_update(struct lt9611uxc *lt9611uxc)
|
|
{
|
|
int ret;
|
|
u16 offset;
|
|
size_t remain;
|
|
char *readbuf;
|
|
const struct firmware *fw;
|
|
|
|
struct reg_sequence seq_setup[] = {
|
|
REG_SEQ0(0x805e, 0xdf),
|
|
REG_SEQ0(0x8058, 0x00),
|
|
REG_SEQ0(0x8059, 0x50),
|
|
REG_SEQ0(0x805a, 0x10),
|
|
REG_SEQ0(0x805a, 0x00),
|
|
};
|
|
|
|
|
|
struct reg_sequence seq_block_erase[] = {
|
|
REG_SEQ0(0x805a, 0x04),
|
|
REG_SEQ0(0x805a, 0x00),
|
|
REG_SEQ0(0x805b, 0x00),
|
|
REG_SEQ0(0x805c, 0x00),
|
|
REG_SEQ0(0x805d, 0x00),
|
|
REG_SEQ0(0x805a, 0x01),
|
|
REG_SEQ0(0x805a, 0x00),
|
|
};
|
|
|
|
ret = request_firmware(&fw, FW_FILE, lt9611uxc->dev);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
dev_info(lt9611uxc->dev, "Updating firmware\n");
|
|
lt9611uxc_lock(lt9611uxc);
|
|
|
|
regmap_multi_reg_write(lt9611uxc->regmap, seq_setup, ARRAY_SIZE(seq_setup));
|
|
|
|
/*
|
|
* Need erase block 2 timess here. Sometimes, block erase can fail.
|
|
* This is a workaroud.
|
|
*/
|
|
regmap_multi_reg_write(lt9611uxc->regmap, seq_block_erase, ARRAY_SIZE(seq_block_erase));
|
|
msleep(3000);
|
|
regmap_multi_reg_write(lt9611uxc->regmap, seq_block_erase, ARRAY_SIZE(seq_block_erase));
|
|
msleep(3000);
|
|
|
|
for (offset = 0, remain = fw->size;
|
|
remain >= LT9611UXC_FW_PAGE_SIZE;
|
|
offset += LT9611UXC_FW_PAGE_SIZE, remain -= LT9611UXC_FW_PAGE_SIZE)
|
|
lt9611uxc_firmware_write_page(lt9611uxc, offset, fw->data + offset);
|
|
|
|
if (remain > 0) {
|
|
char buf[LT9611UXC_FW_PAGE_SIZE];
|
|
|
|
memset(buf, 0xff, LT9611UXC_FW_PAGE_SIZE);
|
|
memcpy(buf, fw->data + offset, remain);
|
|
lt9611uxc_firmware_write_page(lt9611uxc, offset, buf);
|
|
}
|
|
msleep(20);
|
|
|
|
readbuf = lt9611uxc_firmware_read(lt9611uxc, fw->size);
|
|
if (!readbuf) {
|
|
ret = -ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
if (!memcmp(readbuf, fw->data, fw->size)) {
|
|
dev_err(lt9611uxc->dev, "Firmware update failed\n");
|
|
print_hex_dump(KERN_ERR, "fw: ", DUMP_PREFIX_OFFSET, 16, 1, readbuf, fw->size, false);
|
|
ret = -EINVAL;
|
|
} else {
|
|
dev_info(lt9611uxc->dev, "Firmware updates successfully\n");
|
|
ret = 0;
|
|
}
|
|
kfree(readbuf);
|
|
|
|
out:
|
|
lt9611uxc_unlock(lt9611uxc);
|
|
lt9611uxc_reset(lt9611uxc);
|
|
release_firmware(fw);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static ssize_t lt9611uxc_firmware_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t len)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = dev_get_drvdata(dev);
|
|
int ret;
|
|
|
|
ret = lt9611uxc_firmware_update(lt9611uxc);
|
|
if (ret < 0)
|
|
return ret;
|
|
return len;
|
|
}
|
|
|
|
static ssize_t lt9611uxc_firmware_show(struct device *dev, struct device_attribute *attr, char *buf)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = dev_get_drvdata(dev);
|
|
|
|
return sysfs_emit(buf, "%02x\n", lt9611uxc->fw_version);
|
|
}
|
|
|
|
static DEVICE_ATTR_RW(lt9611uxc_firmware);
|
|
|
|
static struct attribute *lt9611uxc_attrs[] = {
|
|
&dev_attr_lt9611uxc_firmware.attr,
|
|
NULL,
|
|
};
|
|
|
|
static const struct attribute_group lt9611uxc_attr_group = {
|
|
.attrs = lt9611uxc_attrs,
|
|
};
|
|
|
|
static const struct attribute_group *lt9611uxc_attr_groups[] = {
|
|
<9611uxc_attr_group,
|
|
NULL,
|
|
};
|
|
|
|
static int lt9611uxc_probe(struct i2c_client *client)
|
|
{
|
|
struct lt9611uxc *lt9611uxc;
|
|
struct device *dev = &client->dev;
|
|
int ret;
|
|
bool fw_updated = false;
|
|
|
|
if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
|
|
dev_err(dev, "device doesn't support I2C\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
lt9611uxc = devm_kzalloc(dev, sizeof(*lt9611uxc), GFP_KERNEL);
|
|
if (!lt9611uxc)
|
|
return -ENOMEM;
|
|
|
|
lt9611uxc->dev = dev;
|
|
lt9611uxc->client = client;
|
|
mutex_init(<9611uxc->ocm_lock);
|
|
|
|
lt9611uxc->regmap = devm_regmap_init_i2c(client, <9611uxc_regmap_config);
|
|
if (IS_ERR(lt9611uxc->regmap)) {
|
|
dev_err(lt9611uxc->dev, "regmap i2c init failed\n");
|
|
return PTR_ERR(lt9611uxc->regmap);
|
|
}
|
|
|
|
ret = lt9611uxc_parse_dt(dev, lt9611uxc);
|
|
if (ret) {
|
|
dev_err(dev, "failed to parse device tree\n");
|
|
return ret;
|
|
}
|
|
|
|
ret = lt9611uxc_gpio_init(lt9611uxc);
|
|
if (ret < 0)
|
|
goto err_of_put;
|
|
|
|
ret = lt9611uxc_regulator_init(lt9611uxc);
|
|
if (ret < 0)
|
|
goto err_of_put;
|
|
|
|
lt9611uxc_assert_5v(lt9611uxc);
|
|
|
|
ret = lt9611uxc_regulator_enable(lt9611uxc);
|
|
if (ret)
|
|
goto err_of_put;
|
|
|
|
lt9611uxc_reset(lt9611uxc);
|
|
|
|
ret = lt9611uxc_read_device_rev(lt9611uxc);
|
|
if (ret) {
|
|
dev_err(dev, "failed to read chip rev\n");
|
|
goto err_disable_regulators;
|
|
}
|
|
|
|
retry:
|
|
ret = lt9611uxc_read_version(lt9611uxc);
|
|
if (ret < 0) {
|
|
dev_err(dev, "failed to read FW version\n");
|
|
goto err_disable_regulators;
|
|
} else if (ret == 0) {
|
|
if (!fw_updated) {
|
|
fw_updated = true;
|
|
dev_err(dev, "FW version 0, enforcing firmware update\n");
|
|
ret = lt9611uxc_firmware_update(lt9611uxc);
|
|
if (ret < 0)
|
|
goto err_disable_regulators;
|
|
else
|
|
goto retry;
|
|
} else {
|
|
dev_err(dev, "FW version 0, update failed\n");
|
|
ret = -EOPNOTSUPP;
|
|
goto err_disable_regulators;
|
|
}
|
|
} else if (ret < 0x40) {
|
|
dev_info(dev, "FW version 0x%x, HPD not supported\n", ret);
|
|
} else {
|
|
lt9611uxc->hpd_supported = true;
|
|
}
|
|
lt9611uxc->fw_version = ret;
|
|
|
|
init_waitqueue_head(<9611uxc->wq);
|
|
INIT_WORK(<9611uxc->work, lt9611uxc_hpd_work);
|
|
|
|
ret = request_threaded_irq(client->irq, NULL,
|
|
lt9611uxc_irq_thread_handler,
|
|
IRQF_ONESHOT, "lt9611uxc", lt9611uxc);
|
|
if (ret) {
|
|
dev_err(dev, "failed to request irq\n");
|
|
goto err_disable_regulators;
|
|
}
|
|
|
|
i2c_set_clientdata(client, lt9611uxc);
|
|
|
|
lt9611uxc->bridge.funcs = <9611uxc_bridge_funcs;
|
|
lt9611uxc->bridge.of_node = client->dev.of_node;
|
|
lt9611uxc->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID;
|
|
if (lt9611uxc->hpd_supported)
|
|
lt9611uxc->bridge.ops |= DRM_BRIDGE_OP_HPD;
|
|
lt9611uxc->bridge.type = DRM_MODE_CONNECTOR_HDMIA;
|
|
|
|
drm_bridge_add(<9611uxc->bridge);
|
|
|
|
/* Attach primary DSI */
|
|
lt9611uxc->dsi0 = lt9611uxc_attach_dsi(lt9611uxc, lt9611uxc->dsi0_node);
|
|
if (IS_ERR(lt9611uxc->dsi0)) {
|
|
ret = PTR_ERR(lt9611uxc->dsi0);
|
|
goto err_remove_bridge;
|
|
}
|
|
|
|
/* Attach secondary DSI, if specified */
|
|
if (lt9611uxc->dsi1_node) {
|
|
lt9611uxc->dsi1 = lt9611uxc_attach_dsi(lt9611uxc, lt9611uxc->dsi1_node);
|
|
if (IS_ERR(lt9611uxc->dsi1)) {
|
|
ret = PTR_ERR(lt9611uxc->dsi1);
|
|
goto err_remove_bridge;
|
|
}
|
|
}
|
|
|
|
return lt9611uxc_audio_init(dev, lt9611uxc);
|
|
|
|
err_remove_bridge:
|
|
free_irq(client->irq, lt9611uxc);
|
|
cancel_work_sync(<9611uxc->work);
|
|
drm_bridge_remove(<9611uxc->bridge);
|
|
|
|
err_disable_regulators:
|
|
regulator_bulk_disable(ARRAY_SIZE(lt9611uxc->supplies), lt9611uxc->supplies);
|
|
|
|
err_of_put:
|
|
of_node_put(lt9611uxc->dsi1_node);
|
|
of_node_put(lt9611uxc->dsi0_node);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void lt9611uxc_remove(struct i2c_client *client)
|
|
{
|
|
struct lt9611uxc *lt9611uxc = i2c_get_clientdata(client);
|
|
|
|
free_irq(client->irq, lt9611uxc);
|
|
cancel_work_sync(<9611uxc->work);
|
|
lt9611uxc_audio_exit(lt9611uxc);
|
|
drm_bridge_remove(<9611uxc->bridge);
|
|
|
|
mutex_destroy(<9611uxc->ocm_lock);
|
|
|
|
regulator_bulk_disable(ARRAY_SIZE(lt9611uxc->supplies), lt9611uxc->supplies);
|
|
|
|
of_node_put(lt9611uxc->dsi1_node);
|
|
of_node_put(lt9611uxc->dsi0_node);
|
|
}
|
|
|
|
static struct i2c_device_id lt9611uxc_id[] = {
|
|
{ "lontium,lt9611uxc", 0 },
|
|
{ /* sentinel */ }
|
|
};
|
|
|
|
static const struct of_device_id lt9611uxc_match_table[] = {
|
|
{ .compatible = "lontium,lt9611uxc" },
|
|
{ /* sentinel */ }
|
|
};
|
|
MODULE_DEVICE_TABLE(of, lt9611uxc_match_table);
|
|
|
|
static struct i2c_driver lt9611uxc_driver = {
|
|
.driver = {
|
|
.name = "lt9611uxc",
|
|
.of_match_table = lt9611uxc_match_table,
|
|
.dev_groups = lt9611uxc_attr_groups,
|
|
},
|
|
.probe = lt9611uxc_probe,
|
|
.remove = lt9611uxc_remove,
|
|
.id_table = lt9611uxc_id,
|
|
};
|
|
module_i2c_driver(lt9611uxc_driver);
|
|
|
|
MODULE_AUTHOR("Dmitry Baryshkov <dmitry.baryshkov@linaro.org>");
|
|
MODULE_LICENSE("GPL v2");
|
|
|
|
MODULE_FIRMWARE(FW_FILE);
|