mbox series

[v5,0/7] Netronix embedded controller driver for Kobo and Tolino ebook readers

Message ID 20201201011513.1627028-1-j.neuschaefer@gmx.net
Headers show
Series Netronix embedded controller driver for Kobo and Tolino ebook readers | expand

Message

J. Neuschäfer Dec. 1, 2020, 1:15 a.m. UTC
This patchset adds basic support for the embedded controller found on
older ebook reader boards designed by/with the ODM Netronix Inc.[1] and
sold by Kobo or Tolino, for example the Kobo Aura and the Tolino Shine.
These drivers are based on information contained in the vendor kernel
sources, but in order to all information in a single place, I documented
the register interface of the EC on GitHub[2].

[1]: http://www.netronixinc.com/products.aspx?ID=1
[2]: https://github.com/neuschaefer/linux/wiki/Netronix-MSP430-embedded-controller

v5:
- Avoid truncation of PWM period and duty cycle to 32 bits
- A few cleanups and additional comments in the PWM driver
- Use regmap_multi_reg_write

v4:
- Spell out ODM (original design manufacturer)
- Solve corner cases in the RTC driver
- Clean up use of log levels vs. error codes
- Add more comments explaining some peculiarities
- Add missing MODULE_ALIAS lines
- Various other cleanups


v3:
- https://lore.kernel.org/lkml/20200924192455.2484005-1-j.neuschaefer@gmx.net/
- A few code cleanups
- A few devicetree related cleanups
- PWM and RTC functionality were moved from subnodes in the devicetree to
  the main node. This also means that the subdrivers no longer need DT
  compatible strings, but are instead loaded via the mfd_cell mechanism.
- The drivers are now published under GPLv2-or-later rather than GPLv2-only.


v2:
- https://lore.kernel.org/lkml/20200905133230.1014581-1-j.neuschaefer@gmx.net/
- Moved txt DT bindings to patch descriptions and removed patch 1/10
  "DT bindings in plain text format"
- New patch 7/10 "rtc: Introduce RTC_TIMESTAMP_END_2255"
- Rebased on 5.9-rc3
- Various other changes which are documented in each patch

v1:
- https://lore.kernel.org/lkml/20200620223915.1311485-1-j.neuschaefer@gmx.net/

Jonathan Neuschäfer (7):
  dt-bindings: Add vendor prefix for Netronix, Inc.
  dt-bindings: mfd: Add binding for Netronix embedded controller
  mfd: Add base driver for Netronix embedded controller
  pwm: ntxec: Add driver for PWM function in Netronix EC
  rtc: New driver for RTC in Netronix embedded controller
  MAINTAINERS: Add entry for Netronix embedded controller
  ARM: dts: imx50-kobo-aura: Add Netronix embedded controller

 .../bindings/mfd/netronix,ntxec.yaml          |  76 ++++++
 .../devicetree/bindings/vendor-prefixes.yaml  |   2 +
 MAINTAINERS                                   |   9 +
 arch/arm/boot/dts/imx50-kobo-aura.dts         |  16 +-
 drivers/mfd/Kconfig                           |  11 +
 drivers/mfd/Makefile                          |   1 +
 drivers/mfd/ntxec.c                           | 216 ++++++++++++++++++
 drivers/pwm/Kconfig                           |   8 +
 drivers/pwm/Makefile                          |   1 +
 drivers/pwm/pwm-ntxec.c                       | 173 ++++++++++++++
 drivers/rtc/Kconfig                           |   8 +
 drivers/rtc/Makefile                          |   1 +
 drivers/rtc/rtc-ntxec.c                       | 143 ++++++++++++
 include/linux/mfd/ntxec.h                     |  34 +++
 14 files changed, 698 insertions(+), 1 deletion(-)
 create mode 100644 Documentation/devicetree/bindings/mfd/netronix,ntxec.yaml
 create mode 100644 drivers/mfd/ntxec.c
 create mode 100644 drivers/pwm/pwm-ntxec.c
 create mode 100644 drivers/rtc/rtc-ntxec.c
 create mode 100644 include/linux/mfd/ntxec.h

--
2.29.2

Comments

Uwe Kleine-König Dec. 1, 2020, 7:20 a.m. UTC | #1
Hello Jonathan,

very nice driver, just a few minor comments below.

On Tue, Dec 01, 2020 at 02:15:10AM +0100, Jonathan Neuschäfer wrote:
> +static struct ntxec_pwm *pwmchip_to_priv(struct pwm_chip *chip)

a function prefix would be great here, I'd pick ntxec_pwm_from_chip as
name.

> +{
> +	return container_of(chip, struct ntxec_pwm, chip);
> +}
> +
> +[...]
> +static int ntxec_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm_dev,
> +			   const struct pwm_state *state)
> +{
> +	struct ntxec_pwm *priv = pwmchip_to_priv(pwm_dev->chip);
> +	unsigned int period, duty;
> +	struct reg_sequence regs[] = {
> +		{ NTXEC_REG_PERIOD_HIGH },
> +		{ NTXEC_REG_PERIOD_LOW },
> +		{ NTXEC_REG_DUTY_HIGH },
> +		{ NTXEC_REG_DUTY_LOW }
> +	};
> +	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);

I'm not a big fan of aligning =. (As if you have to add a longer
variable you have to realign all otherwise unrelated lines.) But that's
subjective and it's up to you if you want to change this.

> +	period /= TIME_BASE_NS;
> +	duty   /= TIME_BASE_NS;
> +
> +	/*
> +	 * 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.
> +	 */
> +
> +	regs[0].def = ntxec_reg8(period >> 8);
> +	regs[1].def = ntxec_reg8(period);
> +	regs[2].def = ntxec_reg8(duty >> 8);
> +	regs[3].def = ntxec_reg8(duty);

You could even minimize the window by changing the order here to

	NTXEC_REG_PERIOD_HIGH
	NTXEC_REG_DUTY_HIGH
	NTXEC_REG_PERIOD_LOW
	NTXEC_REG_DUTY_LOW

but it gets less readable. Maybe move that to a function to have the
reg_sequence and the actual write nearer together? Or somehow name the
indexes to make it more obvious?

> +	res = regmap_multi_reg_write(priv->ec->regmap, regs, ARRAY_SIZE(regs));
> +	if (res)
> +		return res;
> +
> +	/*
> +	 * 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.

If the device already has duty_cycle = 0 but ENABLE = 1, you might get
a failure. But I guess this doesn't need addressing in the code. But
maybe point it out in a comment?

> +	 */
> +	if (state->enabled && duty != 0) {
> +		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));

Given that you cannot read back period and duty anyhow: Does it make
sense to write these only if (state->enabled && duty != 0)?

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

Thanks
Uwe
J. Neuschäfer Dec. 1, 2020, 1:22 p.m. UTC | #2
On Tue, Dec 01, 2020 at 08:20:26AM +0100, Uwe Kleine-König wrote:
> Hello Jonathan,
> 
> very nice driver, just a few minor comments below.
> 
> On Tue, Dec 01, 2020 at 02:15:10AM +0100, Jonathan Neuschäfer wrote:
> > +static struct ntxec_pwm *pwmchip_to_priv(struct pwm_chip *chip)
> 
> a function prefix would be great here, I'd pick ntxec_pwm_from_chip as
> name.

Good point, will do.

> 
> > +{
> > +	return container_of(chip, struct ntxec_pwm, chip);
> > +}
> > +
> > +[...]
> > +static int ntxec_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm_dev,
> > +			   const struct pwm_state *state)
> > +{
> > +	struct ntxec_pwm *priv = pwmchip_to_priv(pwm_dev->chip);
> > +	unsigned int period, duty;
> > +	struct reg_sequence regs[] = {
> > +		{ NTXEC_REG_PERIOD_HIGH },
> > +		{ NTXEC_REG_PERIOD_LOW },
> > +		{ NTXEC_REG_DUTY_HIGH },
> > +		{ NTXEC_REG_DUTY_LOW }
> > +	};
> > +	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);
> 
> I'm not a big fan of aligning =. (As if you have to add a longer
> variable you have to realign all otherwise unrelated lines.) But that's
> subjective and it's up to you if you want to change this.

In this case, I thought it helps the readability, because the lines are
quite similar.

> > +	period /= TIME_BASE_NS;
> > +	duty   /= TIME_BASE_NS;

Here, I did it because I had already aligned the previous two lines.

> > +
> > +	/*
> > +	 * 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.
> > +	 */
> > +
> > +	regs[0].def = ntxec_reg8(period >> 8);
> > +	regs[1].def = ntxec_reg8(period);
> > +	regs[2].def = ntxec_reg8(duty >> 8);
> > +	regs[3].def = ntxec_reg8(duty);
> 
> You could even minimize the window by changing the order here to
> 
> 	NTXEC_REG_PERIOD_HIGH
> 	NTXEC_REG_DUTY_HIGH
> 	NTXEC_REG_PERIOD_LOW
> 	NTXEC_REG_DUTY_LOW

Good idea, but I'm not sure if the EC handles this kind of interleaving
correctly.

> but it gets less readable. Maybe move that to a function to have the
> reg_sequence and the actual write nearer together?

Indeed, a separate function would keep register names and values
together (without resorting to declarations-after-statements).

> Or somehow name the indexes to make it more obvious?

Too much unnecessary complexity, IMHO.

> > +	res = regmap_multi_reg_write(priv->ec->regmap, regs, ARRAY_SIZE(regs));
> > +	if (res)
> > +		return res;
> > +
> > +	/*
> > +	 * 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.
> 
> If the device already has duty_cycle = 0 but ENABLE = 1, you might get
> a failure. But I guess this doesn't need addressing in the code. But
> maybe point it out in a comment?

Good point. I'll add something to the comment.

> > +	 */
> > +	if (state->enabled && duty != 0) {
> > +		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));
> 
> Given that you cannot read back period and duty anyhow: Does it make
> sense to write these only if (state->enabled && duty != 0)?

I think it does.


Thanks,
Jonathan