forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
304 lines
7.1 KiB
304 lines
7.1 KiB
// SPDX-License-Identifier: GPL-2.0 |
|
/* |
|
* Expose a PWM controlled by the ChromeOS EC to the host processor. |
|
* |
|
* Copyright (C) 2016 Google, Inc. |
|
*/ |
|
|
|
#include <linux/module.h> |
|
#include <linux/platform_data/cros_ec_commands.h> |
|
#include <linux/platform_data/cros_ec_proto.h> |
|
#include <linux/platform_device.h> |
|
#include <linux/pwm.h> |
|
#include <linux/slab.h> |
|
|
|
/** |
|
* struct cros_ec_pwm_device - Driver data for EC PWM |
|
* |
|
* @dev: Device node |
|
* @ec: Pointer to EC device |
|
* @chip: PWM controller chip |
|
*/ |
|
struct cros_ec_pwm_device { |
|
struct device *dev; |
|
struct cros_ec_device *ec; |
|
struct pwm_chip chip; |
|
}; |
|
|
|
/** |
|
* struct cros_ec_pwm - per-PWM driver data |
|
* @duty_cycle: cached duty cycle |
|
*/ |
|
struct cros_ec_pwm { |
|
u16 duty_cycle; |
|
}; |
|
|
|
static inline struct cros_ec_pwm_device *pwm_to_cros_ec_pwm(struct pwm_chip *c) |
|
{ |
|
return container_of(c, struct cros_ec_pwm_device, chip); |
|
} |
|
|
|
static int cros_ec_pwm_request(struct pwm_chip *chip, struct pwm_device *pwm) |
|
{ |
|
struct cros_ec_pwm *channel; |
|
|
|
channel = kzalloc(sizeof(*channel), GFP_KERNEL); |
|
if (!channel) |
|
return -ENOMEM; |
|
|
|
pwm_set_chip_data(pwm, channel); |
|
|
|
return 0; |
|
} |
|
|
|
static void cros_ec_pwm_free(struct pwm_chip *chip, struct pwm_device *pwm) |
|
{ |
|
struct cros_ec_pwm *channel = pwm_get_chip_data(pwm); |
|
|
|
kfree(channel); |
|
} |
|
|
|
static int cros_ec_pwm_set_duty(struct cros_ec_device *ec, u8 index, u16 duty) |
|
{ |
|
struct { |
|
struct cros_ec_command msg; |
|
struct ec_params_pwm_set_duty params; |
|
} __packed buf; |
|
struct ec_params_pwm_set_duty *params = &buf.params; |
|
struct cros_ec_command *msg = &buf.msg; |
|
|
|
memset(&buf, 0, sizeof(buf)); |
|
|
|
msg->version = 0; |
|
msg->command = EC_CMD_PWM_SET_DUTY; |
|
msg->insize = 0; |
|
msg->outsize = sizeof(*params); |
|
|
|
params->duty = duty; |
|
params->pwm_type = EC_PWM_TYPE_GENERIC; |
|
params->index = index; |
|
|
|
return cros_ec_cmd_xfer_status(ec, msg); |
|
} |
|
|
|
static int cros_ec_pwm_get_duty(struct cros_ec_device *ec, u8 index) |
|
{ |
|
struct { |
|
struct cros_ec_command msg; |
|
union { |
|
struct ec_params_pwm_get_duty params; |
|
struct ec_response_pwm_get_duty resp; |
|
}; |
|
} __packed buf; |
|
struct ec_params_pwm_get_duty *params = &buf.params; |
|
struct ec_response_pwm_get_duty *resp = &buf.resp; |
|
struct cros_ec_command *msg = &buf.msg; |
|
int ret; |
|
|
|
memset(&buf, 0, sizeof(buf)); |
|
|
|
msg->version = 0; |
|
msg->command = EC_CMD_PWM_GET_DUTY; |
|
msg->insize = sizeof(*resp); |
|
msg->outsize = sizeof(*params); |
|
|
|
params->pwm_type = EC_PWM_TYPE_GENERIC; |
|
params->index = index; |
|
|
|
ret = cros_ec_cmd_xfer_status(ec, msg); |
|
if (ret < 0) |
|
return ret; |
|
|
|
return resp->duty; |
|
} |
|
|
|
static int cros_ec_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm, |
|
const struct pwm_state *state) |
|
{ |
|
struct cros_ec_pwm_device *ec_pwm = pwm_to_cros_ec_pwm(chip); |
|
struct cros_ec_pwm *channel = pwm_get_chip_data(pwm); |
|
u16 duty_cycle; |
|
int ret; |
|
|
|
/* The EC won't let us change the period */ |
|
if (state->period != EC_PWM_MAX_DUTY) |
|
return -EINVAL; |
|
|
|
/* |
|
* EC doesn't separate the concept of duty cycle and enabled, but |
|
* kernel does. Translate. |
|
*/ |
|
duty_cycle = state->enabled ? state->duty_cycle : 0; |
|
|
|
ret = cros_ec_pwm_set_duty(ec_pwm->ec, pwm->hwpwm, duty_cycle); |
|
if (ret < 0) |
|
return ret; |
|
|
|
channel->duty_cycle = state->duty_cycle; |
|
|
|
return 0; |
|
} |
|
|
|
static void cros_ec_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm, |
|
struct pwm_state *state) |
|
{ |
|
struct cros_ec_pwm_device *ec_pwm = pwm_to_cros_ec_pwm(chip); |
|
struct cros_ec_pwm *channel = pwm_get_chip_data(pwm); |
|
int ret; |
|
|
|
ret = cros_ec_pwm_get_duty(ec_pwm->ec, pwm->hwpwm); |
|
if (ret < 0) { |
|
dev_err(chip->dev, "error getting initial duty: %d\n", ret); |
|
return; |
|
} |
|
|
|
state->enabled = (ret > 0); |
|
state->period = EC_PWM_MAX_DUTY; |
|
|
|
/* |
|
* Note that "disabled" and "duty cycle == 0" are treated the same. If |
|
* the cached duty cycle is not zero, used the cached duty cycle. This |
|
* ensures that the configured duty cycle is kept across a disable and |
|
* enable operation and avoids potentially confusing consumers. |
|
* |
|
* For the case of the initial hardware readout, channel->duty_cycle |
|
* will be 0 and the actual duty cycle read from the EC is used. |
|
*/ |
|
if (ret == 0 && channel->duty_cycle > 0) |
|
state->duty_cycle = channel->duty_cycle; |
|
else |
|
state->duty_cycle = ret; |
|
} |
|
|
|
static struct pwm_device * |
|
cros_ec_pwm_xlate(struct pwm_chip *pc, const struct of_phandle_args *args) |
|
{ |
|
struct pwm_device *pwm; |
|
|
|
if (args->args[0] >= pc->npwm) |
|
return ERR_PTR(-EINVAL); |
|
|
|
pwm = pwm_request_from_chip(pc, args->args[0], NULL); |
|
if (IS_ERR(pwm)) |
|
return pwm; |
|
|
|
/* The EC won't let us change the period */ |
|
pwm->args.period = EC_PWM_MAX_DUTY; |
|
|
|
return pwm; |
|
} |
|
|
|
static const struct pwm_ops cros_ec_pwm_ops = { |
|
.request = cros_ec_pwm_request, |
|
.free = cros_ec_pwm_free, |
|
.get_state = cros_ec_pwm_get_state, |
|
.apply = cros_ec_pwm_apply, |
|
.owner = THIS_MODULE, |
|
}; |
|
|
|
/* |
|
* Determine the number of supported PWMs. The EC does not return the number |
|
* of PWMs it supports directly, so we have to read the pwm duty cycle for |
|
* subsequent channels until we get an error. |
|
*/ |
|
static int cros_ec_num_pwms(struct cros_ec_device *ec) |
|
{ |
|
int i, ret; |
|
|
|
/* The index field is only 8 bits */ |
|
for (i = 0; i <= U8_MAX; i++) { |
|
ret = cros_ec_pwm_get_duty(ec, i); |
|
/* |
|
* We look for SUCCESS, INVALID_COMMAND, or INVALID_PARAM |
|
* responses; everything else is treated as an error. |
|
* The EC error codes map to -EOPNOTSUPP and -EINVAL, |
|
* so check for those. |
|
*/ |
|
switch (ret) { |
|
case -EOPNOTSUPP: /* invalid command */ |
|
return -ENODEV; |
|
case -EINVAL: /* invalid parameter */ |
|
return i; |
|
default: |
|
if (ret < 0) |
|
return ret; |
|
break; |
|
} |
|
} |
|
|
|
return U8_MAX; |
|
} |
|
|
|
static int cros_ec_pwm_probe(struct platform_device *pdev) |
|
{ |
|
struct cros_ec_device *ec = dev_get_drvdata(pdev->dev.parent); |
|
struct device *dev = &pdev->dev; |
|
struct cros_ec_pwm_device *ec_pwm; |
|
struct pwm_chip *chip; |
|
int ret; |
|
|
|
if (!ec) { |
|
dev_err(dev, "no parent EC device\n"); |
|
return -EINVAL; |
|
} |
|
|
|
ec_pwm = devm_kzalloc(dev, sizeof(*ec_pwm), GFP_KERNEL); |
|
if (!ec_pwm) |
|
return -ENOMEM; |
|
chip = &ec_pwm->chip; |
|
ec_pwm->ec = ec; |
|
|
|
/* PWM chip */ |
|
chip->dev = dev; |
|
chip->ops = &cros_ec_pwm_ops; |
|
chip->of_xlate = cros_ec_pwm_xlate; |
|
chip->of_pwm_n_cells = 1; |
|
chip->base = -1; |
|
ret = cros_ec_num_pwms(ec); |
|
if (ret < 0) { |
|
dev_err(dev, "Couldn't find PWMs: %d\n", ret); |
|
return ret; |
|
} |
|
chip->npwm = ret; |
|
dev_dbg(dev, "Probed %u PWMs\n", chip->npwm); |
|
|
|
ret = pwmchip_add(chip); |
|
if (ret < 0) { |
|
dev_err(dev, "cannot register PWM: %d\n", ret); |
|
return ret; |
|
} |
|
|
|
platform_set_drvdata(pdev, ec_pwm); |
|
|
|
return ret; |
|
} |
|
|
|
static int cros_ec_pwm_remove(struct platform_device *dev) |
|
{ |
|
struct cros_ec_pwm_device *ec_pwm = platform_get_drvdata(dev); |
|
struct pwm_chip *chip = &ec_pwm->chip; |
|
|
|
return pwmchip_remove(chip); |
|
} |
|
|
|
#ifdef CONFIG_OF |
|
static const struct of_device_id cros_ec_pwm_of_match[] = { |
|
{ .compatible = "google,cros-ec-pwm" }, |
|
{}, |
|
}; |
|
MODULE_DEVICE_TABLE(of, cros_ec_pwm_of_match); |
|
#endif |
|
|
|
static struct platform_driver cros_ec_pwm_driver = { |
|
.probe = cros_ec_pwm_probe, |
|
.remove = cros_ec_pwm_remove, |
|
.driver = { |
|
.name = "cros-ec-pwm", |
|
.of_match_table = of_match_ptr(cros_ec_pwm_of_match), |
|
}, |
|
}; |
|
module_platform_driver(cros_ec_pwm_driver); |
|
|
|
MODULE_ALIAS("platform:cros-ec-pwm"); |
|
MODULE_DESCRIPTION("ChromeOS EC PWM driver"); |
|
MODULE_LICENSE("GPL v2");
|
|
|