2021-01-25 00:41:24 +03:00
// 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 device * dev ;
struct ntxec * ec ;
struct pwm_chip chip ;
} ;
static struct ntxec_pwm * ntxec_pwm_from_chip ( struct pwm_chip * chip )
{
return container_of ( chip , struct ntxec_pwm , 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 8 MHz , or 125 ns . 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 = {
. owner = THIS_MODULE ,
. 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 ;
pdev - > dev . of_node = pdev - > dev . parent - > of_node ;
priv = devm_kzalloc ( & pdev - > dev , sizeof ( * priv ) , GFP_KERNEL ) ;
if ( ! priv )
return - ENOMEM ;
priv - > ec = ec ;
priv - > dev = & pdev - > dev ;
chip = & priv - > chip ;
chip - > dev = & pdev - > dev ;
chip - > ops = & ntxec_pwm_ops ;
chip - > npwm = 1 ;
2021-07-07 19:28:13 +03:00
return devm_pwmchip_add ( & pdev - > dev , chip ) ;
2021-01-25 00:41:24 +03:00
}
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 " ) ;