summaryrefslogtreecommitdiff
path: root/drivers/platform/x86/dell/dell-lis3lv02d.c
blob: d2b34e10c5eba4088a05c76c649c50e42e5ab199 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * lis3lv02d i2c-client instantiation for ACPI SMO88xx devices without I2C resources.
 *
 *  Copyright (C) 2024 Hans de Goede <hansg@kernel.org>
 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/device/bus.h>
#include <linux/dmi.h>
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/platform_device.h>
#include <linux/workqueue.h>
#include "dell-smo8800-ids.h"

#define DELL_LIS3LV02D_DMI_ENTRY(product_name, i2c_addr)                 \
	{                                                                \
		.matches = {                                             \
			DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Dell Inc."),    \
			DMI_EXACT_MATCH(DMI_PRODUCT_NAME, product_name), \
		},                                                       \
		.driver_data = (void *)(uintptr_t)(i2c_addr),            \
	}

/*
 * Accelerometer's I2C address is not specified in DMI nor ACPI,
 * so it is needed to define mapping table based on DMI product names.
 */
static const struct dmi_system_id lis3lv02d_devices[] __initconst = {
	/*
	 * Dell platform team told us that these Latitude devices have
	 * ST microelectronics accelerometer at I2C address 0x29.
	 */
	DELL_LIS3LV02D_DMI_ENTRY("Latitude E5250",     0x29),
	DELL_LIS3LV02D_DMI_ENTRY("Latitude E5450",     0x29),
	DELL_LIS3LV02D_DMI_ENTRY("Latitude E5550",     0x29),
	DELL_LIS3LV02D_DMI_ENTRY("Latitude E6440",     0x29),
	DELL_LIS3LV02D_DMI_ENTRY("Latitude E6440 ATG", 0x29),
	DELL_LIS3LV02D_DMI_ENTRY("Latitude E6540",     0x29),
	/*
	 * Additional individual entries were added after verification.
	 */
	DELL_LIS3LV02D_DMI_ENTRY("Latitude 5480",      0x29),
	DELL_LIS3LV02D_DMI_ENTRY("Latitude E6330",     0x29),
	DELL_LIS3LV02D_DMI_ENTRY("Latitude E6430",     0x29),
	DELL_LIS3LV02D_DMI_ENTRY("Precision 3540",     0x29),
	DELL_LIS3LV02D_DMI_ENTRY("Vostro V131",        0x1d),
	DELL_LIS3LV02D_DMI_ENTRY("Vostro 5568",        0x29),
	DELL_LIS3LV02D_DMI_ENTRY("XPS 15 7590",        0x29),
	DELL_LIS3LV02D_DMI_ENTRY("XPS 15 9550",        0x29),
	{ }
};

static u8 i2c_addr;
static struct i2c_client *i2c_dev;
static bool notifier_registered;

static bool i2c_adapter_is_main_i801(struct i2c_adapter *adap)
{
	/*
	 * Only match the main I801 adapter and reject secondary adapters
	 * which names start with "SMBus I801 IDF adapter".
	 */
	return strstarts(adap->name, "SMBus I801 adapter");
}

static int find_i801(struct device *dev, void *data)
{
	struct i2c_adapter *adap, **adap_ret = data;

	adap = i2c_verify_adapter(dev);
	if (!adap)
		return 0;

	if (!i2c_adapter_is_main_i801(adap))
		return 0;

	*adap_ret = i2c_get_adapter(adap->nr);
	return 1;
}

static void instantiate_i2c_client(struct work_struct *work)
{
	struct i2c_board_info info = { };
	struct i2c_adapter *adap = NULL;

	if (i2c_dev)
		return;

	/*
	 * bus_for_each_dev() and not i2c_for_each_dev() to avoid
	 * a deadlock when find_i801() calls i2c_get_adapter().
	 */
	bus_for_each_dev(&i2c_bus_type, NULL, &adap, find_i801);
	if (!adap)
		return;

	info.addr = i2c_addr;
	strscpy(info.type, "lis3lv02d", I2C_NAME_SIZE);

	i2c_dev = i2c_new_client_device(adap, &info);
	if (IS_ERR(i2c_dev)) {
		dev_err(&adap->dev, "error %ld registering i2c_client\n", PTR_ERR(i2c_dev));
		i2c_dev = NULL;
	} else {
		dev_dbg(&adap->dev, "registered lis3lv02d on address 0x%02x\n", info.addr);
	}

	i2c_put_adapter(adap);
}
static DECLARE_WORK(i2c_work, instantiate_i2c_client);

static int i2c_bus_notify(struct notifier_block *nb, unsigned long action, void *data)
{
	struct device *dev = data;
	struct i2c_client *client;
	struct i2c_adapter *adap;

	switch (action) {
	case BUS_NOTIFY_ADD_DEVICE:
		adap = i2c_verify_adapter(dev);
		if (!adap)
			break;

		if (i2c_adapter_is_main_i801(adap))
			queue_work(system_long_wq, &i2c_work);
		break;
	case BUS_NOTIFY_REMOVED_DEVICE:
		client = i2c_verify_client(dev);
		if (!client)
			break;

		if (i2c_dev == client) {
			dev_dbg(&client->adapter->dev, "lis3lv02d i2c_client removed\n");
			i2c_dev = NULL;
		}
		break;
	default:
		break;
	}

	return 0;
}
static struct notifier_block i2c_nb = { .notifier_call = i2c_bus_notify };

static int __init match_acpi_device_ids(struct device *dev, const void *data)
{
	return acpi_match_device(data, dev) ? 1 : 0;
}

static int __init dell_lis3lv02d_init(void)
{
	const struct dmi_system_id *lis3lv02d_dmi_id;
	struct device *dev;
	int err;

	/*
	 * First check for a matching platform_device. This protects against
	 * SMO88xx ACPI fwnodes which actually do have an I2C resource, which
	 * will already have an i2c_client instantiated (not a platform_device).
	 */
	dev = bus_find_device(&platform_bus_type, NULL, smo8800_ids, match_acpi_device_ids);
	if (!dev) {
		pr_debug("No SMO88xx platform-device found\n");
		return 0;
	}
	put_device(dev);

	lis3lv02d_dmi_id = dmi_first_match(lis3lv02d_devices);
	if (!lis3lv02d_dmi_id) {
		pr_warn("accelerometer is present on SMBus but its address is unknown, skipping registration\n");
		return 0;
	}

	i2c_addr = (long)lis3lv02d_dmi_id->driver_data;

	/*
	 * Register i2c-bus notifier + queue initial scan for lis3lv02d
	 * i2c_client instantiation.
	 */
	err = bus_register_notifier(&i2c_bus_type, &i2c_nb);
	if (err)
		return err;

	notifier_registered = true;

	queue_work(system_long_wq, &i2c_work);
	return 0;
}
module_init(dell_lis3lv02d_init);

static void __exit dell_lis3lv02d_module_exit(void)
{
	if (!notifier_registered)
		return;

	bus_unregister_notifier(&i2c_bus_type, &i2c_nb);
	cancel_work_sync(&i2c_work);
	i2c_unregister_device(i2c_dev);
}
module_exit(dell_lis3lv02d_module_exit);

MODULE_DESCRIPTION("lis3lv02d i2c-client instantiation for ACPI SMO88xx devices");
MODULE_AUTHOR("Hans de Goede <hansg@kernel.org>");
MODULE_LICENSE("GPL");