summaryrefslogtreecommitdiffstats
path: root/drivers/platform/chrome/cros_hps_i2c.c
blob: 62ccb1acb5de0fd0d6573d88c74c172169f863a8 (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
// SPDX-License-Identifier: GPL-2.0
/*
 * Driver for the ChromeOS human presence sensor (HPS), attached via I2C.
 *
 * The driver exposes HPS as a character device, although currently no read or
 * write operations are supported. Instead, the driver only controls the power
 * state of the sensor, keeping it on only while userspace holds an open file
 * descriptor to the HPS device.
 *
 * Copyright 2022 Google LLC.
 */

#include <linux/acpi.h>
#include <linux/fs.h>
#include <linux/gpio/consumer.h>
#include <linux/i2c.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/pm_runtime.h>

#define HPS_ACPI_ID		"GOOG0020"

struct hps_drvdata {
	struct i2c_client *client;
	struct miscdevice misc_device;
	struct gpio_desc *enable_gpio;
};

static void hps_set_power(struct hps_drvdata *hps, bool state)
{
	gpiod_set_value_cansleep(hps->enable_gpio, state);
}

static int hps_open(struct inode *inode, struct file *file)
{
	struct hps_drvdata *hps = container_of(file->private_data,
					       struct hps_drvdata, misc_device);
	struct device *dev = &hps->client->dev;

	return pm_runtime_resume_and_get(dev);
}

static int hps_release(struct inode *inode, struct file *file)
{
	struct hps_drvdata *hps = container_of(file->private_data,
					       struct hps_drvdata, misc_device);
	struct device *dev = &hps->client->dev;

	return pm_runtime_put(dev);
}

static const struct file_operations hps_fops = {
	.owner = THIS_MODULE,
	.open = hps_open,
	.release = hps_release,
};

static int hps_i2c_probe(struct i2c_client *client)
{
	struct hps_drvdata *hps;
	int ret;

	hps = devm_kzalloc(&client->dev, sizeof(*hps), GFP_KERNEL);
	if (!hps)
		return -ENOMEM;

	hps->misc_device.parent = &client->dev;
	hps->misc_device.minor = MISC_DYNAMIC_MINOR;
	hps->misc_device.name = "cros-hps";
	hps->misc_device.fops = &hps_fops;

	i2c_set_clientdata(client, hps);
	hps->client = client;

	/*
	 * HPS is powered on from firmware before entering the kernel, so we
	 * acquire the line with GPIOD_OUT_HIGH here to preserve the existing
	 * state. The peripheral is powered off after successful probe below.
	 */
	hps->enable_gpio = devm_gpiod_get(&client->dev, "enable", GPIOD_OUT_HIGH);
	if (IS_ERR(hps->enable_gpio)) {
		ret = PTR_ERR(hps->enable_gpio);
		dev_err(&client->dev, "failed to get enable gpio: %d\n", ret);
		return ret;
	}

	ret = misc_register(&hps->misc_device);
	if (ret) {
		dev_err(&client->dev, "failed to initialize misc device: %d\n", ret);
		return ret;
	}

	hps_set_power(hps, false);
	pm_runtime_enable(&client->dev);
	return 0;
}

static void hps_i2c_remove(struct i2c_client *client)
{
	struct hps_drvdata *hps = i2c_get_clientdata(client);

	pm_runtime_disable(&client->dev);
	misc_deregister(&hps->misc_device);

	/*
	 * Re-enable HPS, in order to return it to its default state
	 * (i.e. powered on).
	 */
	hps_set_power(hps, true);
}

static int hps_suspend(struct device *dev)
{
	struct i2c_client *client = to_i2c_client(dev);
	struct hps_drvdata *hps = i2c_get_clientdata(client);

	hps_set_power(hps, false);
	return 0;
}

static int hps_resume(struct device *dev)
{
	struct i2c_client *client = to_i2c_client(dev);
	struct hps_drvdata *hps = i2c_get_clientdata(client);

	hps_set_power(hps, true);
	return 0;
}
static UNIVERSAL_DEV_PM_OPS(hps_pm_ops, hps_suspend, hps_resume, NULL);

static const struct i2c_device_id hps_i2c_id[] = {
	{ "cros-hps", 0 },
	{ }
};
MODULE_DEVICE_TABLE(i2c, hps_i2c_id);

#ifdef CONFIG_ACPI
static const struct acpi_device_id hps_acpi_id[] = {
	{ HPS_ACPI_ID, 0 },
	{ }
};
MODULE_DEVICE_TABLE(acpi, hps_acpi_id);
#endif /* CONFIG_ACPI */

static struct i2c_driver hps_i2c_driver = {
	.probe_new = hps_i2c_probe,
	.remove = hps_i2c_remove,
	.id_table = hps_i2c_id,
	.driver = {
		.name = "cros-hps",
		.pm = &hps_pm_ops,
		.acpi_match_table = ACPI_PTR(hps_acpi_id),
	},
};
module_i2c_driver(hps_i2c_driver);

MODULE_ALIAS("acpi:" HPS_ACPI_ID);
MODULE_AUTHOR("Sami Kyöstilä <skyostil@chromium.org>");
MODULE_DESCRIPTION("Driver for ChromeOS HPS");
MODULE_LICENSE("GPL");