summaryrefslogtreecommitdiff
path: root/drivers/pwm/pwm-ntxec.c
blob: 28d1c2e5a98fe36e8750a2b511622f928da41204 (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
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * The Netronix embedded controller is a microcontroller found in some
 * e-book readers designed by the original design manufacturer Netronix, Inc.
 * It contains RTC, battery monitoring, system power management, and PWM
 * functionality.
 *
 * This driver implements PWM output.
 *
 * Copyright 2020 Jonathan Neuschäfer <j.neuschaefer@gmx.net>
 *
 * Limitations:
 * - The get_state callback is not implemented, because the current state of
 *   the PWM output can't be read back from the hardware.
 * - The hardware can only generate normal polarity output.
 * - The period and duty cycle can't be changed together in one atomic action.
 */

#include <linux/mfd/ntxec.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
#include <linux/regmap.h>
#include <linux/types.h>

struct ntxec_pwm {
	struct ntxec *ec;
};

static struct ntxec_pwm *ntxec_pwm_from_chip(struct pwm_chip *chip)
{
	return pwmchip_get_drvdata(chip);
}

#define NTXEC_REG_AUTO_OFF_HI	0xa1
#define NTXEC_REG_AUTO_OFF_LO	0xa2
#define NTXEC_REG_ENABLE	0xa3
#define NTXEC_REG_PERIOD_LOW	0xa4
#define NTXEC_REG_PERIOD_HIGH	0xa5
#define NTXEC_REG_DUTY_LOW	0xa6
#define NTXEC_REG_DUTY_HIGH	0xa7

/*
 * The time base used in the EC is 8MHz, or 125ns. Period and duty cycle are
 * measured in this unit.
 */
#define TIME_BASE_NS 125

/*
 * The maximum input value (in nanoseconds) is determined by the time base and
 * the range of the hardware registers that hold the converted value.
 * It fits into 32 bits, so we can do our calculations in 32 bits as well.
 */
#define MAX_PERIOD_NS (TIME_BASE_NS * 0xffff)

static int ntxec_pwm_set_raw_period_and_duty_cycle(struct pwm_chip *chip,
						   int period, int duty)
{
	struct ntxec_pwm *priv = ntxec_pwm_from_chip(chip);

	/*
	 * Changes to the period and duty cycle take effect as soon as the
	 * corresponding low byte is written, so the hardware may be configured
	 * to an inconsistent state after the period is written and before the
	 * duty cycle is fully written. If, in such a case, the old duty cycle
	 * is longer than the new period, the EC may output 100% for a moment.
	 *
	 * To minimize the time between the changes to period and duty cycle
	 * taking effect, the writes are interleaved.
	 */

	struct reg_sequence regs[] = {
		{ NTXEC_REG_PERIOD_HIGH, ntxec_reg8(period >> 8) },
		{ NTXEC_REG_DUTY_HIGH, ntxec_reg8(duty >> 8) },
		{ NTXEC_REG_PERIOD_LOW, ntxec_reg8(period) },
		{ NTXEC_REG_DUTY_LOW, ntxec_reg8(duty) },
	};

	return regmap_multi_reg_write(priv->ec->regmap, regs, ARRAY_SIZE(regs));
}

static int ntxec_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm_dev,
			   const struct pwm_state *state)
{
	struct ntxec_pwm *priv = ntxec_pwm_from_chip(chip);
	unsigned int period, duty;
	int res;

	if (state->polarity != PWM_POLARITY_NORMAL)
		return -EINVAL;

	period = min_t(u64, state->period, MAX_PERIOD_NS);
	duty   = min_t(u64, state->duty_cycle, period);

	period /= TIME_BASE_NS;
	duty   /= TIME_BASE_NS;

	/*
	 * Writing a duty cycle of zero puts the device into a state where
	 * writing a higher duty cycle doesn't result in the brightness that it
	 * usually results in. This can be fixed by cycling the ENABLE register.
	 *
	 * As a workaround, write ENABLE=0 when the duty cycle is zero.
	 * The case that something has previously set the duty cycle to zero
	 * but ENABLE=1, is not handled.
	 */
	if (state->enabled && duty != 0) {
		res = ntxec_pwm_set_raw_period_and_duty_cycle(chip, period, duty);
		if (res)
			return res;

		res = regmap_write(priv->ec->regmap, NTXEC_REG_ENABLE, ntxec_reg8(1));
		if (res)
			return res;

		/* Disable the auto-off timer */
		res = regmap_write(priv->ec->regmap, NTXEC_REG_AUTO_OFF_HI, ntxec_reg8(0xff));
		if (res)
			return res;

		return regmap_write(priv->ec->regmap, NTXEC_REG_AUTO_OFF_LO, ntxec_reg8(0xff));
	} else {
		return regmap_write(priv->ec->regmap, NTXEC_REG_ENABLE, ntxec_reg8(0));
	}
}

static const struct pwm_ops ntxec_pwm_ops = {
	.apply = ntxec_pwm_apply,
	/*
	 * No .get_state callback, because the current state cannot be read
	 * back from the hardware.
	 */
};

static int ntxec_pwm_probe(struct platform_device *pdev)
{
	struct ntxec *ec = dev_get_drvdata(pdev->dev.parent);
	struct ntxec_pwm *priv;
	struct pwm_chip *chip;

	device_set_of_node_from_dev(&pdev->dev, pdev->dev.parent);

	chip = devm_pwmchip_alloc(&pdev->dev, 1, sizeof(*priv));
	if (IS_ERR(chip))
		return PTR_ERR(chip);
	priv = ntxec_pwm_from_chip(chip);

	priv->ec = ec;
	chip->ops = &ntxec_pwm_ops;

	return devm_pwmchip_add(&pdev->dev, chip);
}

static struct platform_driver ntxec_pwm_driver = {
	.driver = {
		.name = "ntxec-pwm",
	},
	.probe = ntxec_pwm_probe,
};
module_platform_driver(ntxec_pwm_driver);

MODULE_AUTHOR("Jonathan Neuschäfer <j.neuschaefer@gmx.net>");
MODULE_DESCRIPTION("PWM driver for Netronix EC");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:ntxec-pwm");