| // SPDX-License-Identifier: (GPL-2.0+ OR MIT) |
| /* |
| * Copyright (c) 2019 Amlogic, Inc. All rights reserved. |
| */ |
| |
| #undef pr_fmt |
| #define pr_fmt(fmt) "pwm: " fmt |
| |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/device.h> |
| #include <linux/platform_device.h> |
| #include <linux/mutex.h> |
| #include <linux/err.h> |
| #include <linux/slab.h> |
| #include <linux/pwm.h> |
| #include <linux/amlogic/pwm-meson.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/pm_domain.h> |
| |
| /** |
| * pwm_constant_enable() |
| * - start a constant PWM output toggling |
| * @chip: aml_pwm_chip struct |
| * @index: pwm channel to choose,like PWM_A or PWM_B |
| */ |
| int pwm_constant_enable(struct meson_pwm *meson, int index) |
| { |
| struct meson_pwm_channel *channel; |
| u32 enable; |
| |
| if (index > 1) { |
| dev_err(meson->chip.dev, |
| "index or value is not within the scope!\n"); |
| return -EINVAL; |
| } |
| |
| /* update each channel status */ |
| channel = pwm_get_chip_data(meson->chip.pwms + index); |
| channel->variant.constant = true; |
| |
| /* If the current device is not in the RPM_ACTIVE state, |
| * domain may be in poweroff state, do not update reg, |
| * wait for update in pwm enable. |
| */ |
| if (meson->data->runtime_enabled && |
| !atomic_read(&meson->chip.dev->power.usage_count)) |
| return 0; |
| |
| switch (index) { |
| case 0: |
| enable = MISC_A_CONSTANT; |
| break; |
| case 1: |
| enable = MISC_B_CONSTANT; |
| break; |
| default: |
| return -EINVAL; |
| } |
| regmap_update_bits(meson->regmap_base, REG_MISC_AB, enable, enable); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(pwm_constant_enable); |
| |
| /** |
| * pwm_constant_disable() - stop a constant PWM output toggling |
| * @chip: aml_pwm_chip struct |
| * @index: pwm channel to choose,like PWM_A or PWM_B |
| */ |
| int pwm_constant_disable(struct meson_pwm *meson, int index) |
| { |
| struct meson_pwm_channel *channel; |
| u32 enable; |
| |
| if (index > 1) { |
| dev_err(meson->chip.dev, |
| "index or value is not within the scope!\n"); |
| return -EINVAL; |
| } |
| |
| /* update each channel status */ |
| channel = pwm_get_chip_data(meson->chip.pwms + index); |
| channel->variant.constant = false; |
| |
| /* If the current device is not in the RPM_ACTIVE state, |
| * domain may be in poweroff state, do not update reg, |
| * wait for update in pwm enable. |
| */ |
| if (meson->data->runtime_enabled && |
| !atomic_read(&meson->chip.dev->power.usage_count)) |
| return 0; |
| |
| switch (index) { |
| case 0: |
| enable = BIT(28); |
| break; |
| |
| case 1: |
| enable = BIT(29); |
| break; |
| |
| default: |
| return -EINVAL; |
| } |
| regmap_update_bits(meson->regmap_base, |
| REG_MISC_AB, enable, PWM_DISABLE); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(pwm_constant_disable); |
| |
| static ssize_t constant_show(struct device *child, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| struct meson_pwm_channel *channel; |
| struct meson_pwm *meson = |
| (struct meson_pwm *)dev_get_drvdata(child); |
| |
| channel = pwm_get_chip_data(meson->chip.pwms); |
| return sprintf(buf, "%d\n", channel->variant.constant); |
| } |
| |
| static ssize_t constant_store(struct device *child, |
| struct device_attribute *attr, |
| const char *buf, size_t size) |
| { |
| struct meson_pwm *meson = |
| (struct meson_pwm *)dev_get_drvdata(child); |
| int val, ret, id, res; |
| |
| res = sscanf(buf, "%d %d", &val, &id); |
| if (res != 2) { |
| dev_err(child, "Can't parse pwm id,usage:[value index]\n"); |
| return -EINVAL; |
| } |
| |
| switch (val) { |
| case 0: |
| ret = pwm_constant_disable(meson, id); |
| break; |
| case 1: |
| ret = pwm_constant_enable(meson, id); |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| |
| return ret ? : size; |
| } |
| |
| /** |
| * pwm_set_times() - set PWM times output toggling |
| * set pwm a1 and pwm a2 timer together |
| * and pwm a1 should be set first |
| * @chip: aml_pwm_chip struct |
| * @index: pwm channel to choose,like PWM_A or PWM_B,range from 1 to 15 |
| * @value: blink times to set,range from 1 to 255 |
| */ |
| int pwm_set_times(struct meson_pwm *meson, |
| int index, int value) |
| { |
| struct meson_pwm_channel *channel; |
| unsigned int clear_val, val; |
| |
| if (value < 0 || value > 255 || index > 3) { |
| dev_err(meson->chip.dev, |
| "index or value is not within the scope!\n"); |
| return -EINVAL; |
| } |
| |
| /* update each channel status */ |
| channel = pwm_get_chip_data(meson->chip.pwms + index); |
| channel->variant.times = value; |
| |
| /* If the current device is not in the RPM_ACTIVE state, |
| * domain may be in poweroff state, do not update reg, |
| * wait for update in pwm enable. |
| */ |
| if (meson->data->runtime_enabled && |
| !atomic_read(&meson->chip.dev->power.usage_count)) |
| return 0; |
| |
| switch (index) { |
| case 0: |
| clear_val = 0xff << 24; |
| val = value << 24; |
| break; |
| case 1: |
| clear_val = 0xff << 8; |
| val = value << 8; |
| break; |
| case 2: |
| clear_val = 0xff << 16; |
| val = value << 16; |
| break; |
| case 3: |
| clear_val = 0xff; |
| val = value; |
| break; |
| default: |
| return -EINVAL; |
| } |
| regmap_update_bits(meson->regmap_base, REG_TIME_AB, clear_val, val); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(pwm_set_times); |
| |
| static ssize_t times_show(struct device *child, |
| struct device_attribute *attr, char *buf) |
| { |
| struct meson_pwm_channel *channel; |
| struct meson_pwm *meson = |
| (struct meson_pwm *)dev_get_drvdata(child); |
| |
| channel = pwm_get_chip_data(meson->chip.pwms); |
| return sprintf(buf, "%d\n", channel->variant.times); |
| } |
| |
| static ssize_t times_store(struct device *child, |
| struct device_attribute *attr, |
| const char *buf, size_t size) |
| { |
| struct meson_pwm *meson = |
| (struct meson_pwm *)dev_get_drvdata(child); |
| int val, ret, id, res; |
| |
| res = sscanf(buf, "%d %d", &val, &id); |
| if (res != 2) { |
| dev_err(child, |
| "Can't parse pwm id and value,usage:[value index]\n"); |
| return -EINVAL; |
| } |
| ret = pwm_set_times(meson, id, val); |
| |
| return ret ? : size; |
| } |
| |
| /** |
| * pwm_blink_enable() |
| * - start a blink PWM output toggling |
| * txl only support 8 channel blink output |
| * @chip: aml_pwm_chip struct |
| * @index: pwm channel to choose,like PWM_A or PWM_B |
| */ |
| int pwm_blink_enable(struct meson_pwm *meson, int index) |
| { |
| struct meson_pwm_channel *channel; |
| u32 enable; |
| |
| if (index > 1) { |
| dev_err(meson->chip.dev, |
| "value or index is not within the scope!\n"); |
| return -EINVAL; |
| } |
| |
| /* update each channel status */ |
| channel = pwm_get_chip_data(meson->chip.pwms + index); |
| channel->variant.blink_enable = true; |
| |
| /* If the current device is not in the RPM_ACTIVE state, |
| * domain may be in poweroff state, do not update reg, |
| * wait for update in pwm enable. |
| */ |
| if (meson->data->runtime_enabled && |
| !atomic_read(&meson->chip.dev->power.usage_count)) |
| return 0; |
| |
| switch (index) { |
| case 0: |
| enable = BLINK_A; |
| break; |
| case 1: |
| enable = BLINK_B; |
| break; |
| default: |
| return -EINVAL; |
| } |
| regmap_update_bits(meson->regmap_base, REG_BLINK_AB, enable, enable); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(pwm_blink_enable); |
| |
| /** |
| * pwm_blink_disable() - stop a constant PWM output toggling |
| * @chip: aml_pwm_chip struct |
| * @index: pwm channel to choose,like PWM_A or PWM_B |
| */ |
| int pwm_blink_disable(struct meson_pwm *meson, int index) |
| { |
| struct meson_pwm_channel *channel; |
| u32 enable; |
| |
| if (index > 1) { |
| dev_err(meson->chip.dev, |
| "value or index is not within the scope!\n"); |
| return -EINVAL; |
| } |
| |
| /* update each channel status */ |
| channel = pwm_get_chip_data(meson->chip.pwms + index); |
| channel->variant.blink_enable = false; |
| |
| /* If the current device is not in the RPM_ACTIVE state, |
| * domain may be in poweroff state, do not update reg, |
| * wait for update in pwm enable. |
| */ |
| if (meson->data->runtime_enabled && |
| !atomic_read(&meson->chip.dev->power.usage_count)) |
| return 0; |
| |
| switch (index) { |
| case 0: |
| enable = BLINK_A; |
| break; |
| |
| case 1: |
| enable = BLINK_B; |
| break; |
| |
| default: |
| return -EINVAL; |
| } |
| regmap_update_bits(meson->regmap_base, |
| REG_BLINK_AB, enable, PWM_DISABLE); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(pwm_blink_disable); |
| |
| static ssize_t blink_enable_show(struct device *child, |
| struct device_attribute *attr, char *buf) |
| { |
| struct meson_pwm_channel *channel; |
| struct meson_pwm *meson = |
| (struct meson_pwm *)dev_get_drvdata(child); |
| |
| channel = pwm_get_chip_data(meson->chip.pwms); |
| return sprintf(buf, "%d\n", channel->variant.blink_enable); |
| } |
| |
| static ssize_t blink_enable_store(struct device *child, |
| struct device_attribute *attr, |
| const char *buf, size_t size) |
| { |
| struct meson_pwm *meson = |
| (struct meson_pwm *)dev_get_drvdata(child); |
| int val, ret, id, res; |
| |
| res = sscanf(buf, "%d %d", &val, &id); |
| if (res != 2) { |
| dev_err(child, "blink enable,Can't parse pwm id,usage:[value index]\n"); |
| return -EINVAL; |
| } |
| |
| switch (val) { |
| case 0: |
| ret = pwm_blink_disable(meson, id); |
| break; |
| case 1: |
| ret = pwm_blink_enable(meson, id); |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| |
| return ret ? : size; |
| } |
| |
| /** |
| * pwm_set_blink_times() - set PWM blink times output toggling |
| * @chip: aml_pwm_chip struct |
| * @index: pwm channel to choose,like PWM_A or PWM_B |
| * @value: blink times to set,range from 1 to 15 |
| */ |
| int pwm_set_blink_times(struct meson_pwm *meson, |
| int index, |
| int value) |
| { |
| struct meson_pwm_channel *channel; |
| unsigned int clear_val, val; |
| |
| if (value < 0 || value > 15 || index > 1) { |
| dev_err(meson->chip.dev, |
| "value or index is not within the scope!\n"); |
| return -EINVAL; |
| } |
| |
| /* update each channel status */ |
| channel = pwm_get_chip_data(meson->chip.pwms + index); |
| channel->variant.blink_times = value; |
| |
| /* If the current device is not in the RPM_ACTIVE state, |
| * domain may be in poweroff state, do not update reg, |
| * wait for update in pwm enable. |
| */ |
| if (meson->data->runtime_enabled && |
| !atomic_read(&meson->chip.dev->power.usage_count)) |
| return 0; |
| |
| switch (index) { |
| case 0: |
| clear_val = 0xf; |
| val = value; |
| break; |
| |
| case 1: |
| clear_val = 0xf << 4; |
| val = value << 4; |
| break; |
| |
| default: |
| return -EINVAL; |
| } |
| regmap_update_bits(meson->regmap_base, REG_BLINK_AB, clear_val, val); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(pwm_set_blink_times); |
| |
| static ssize_t blink_times_show(struct device *child, |
| struct device_attribute *attr, char *buf) |
| { |
| struct meson_pwm_channel *channel; |
| struct meson_pwm *meson = |
| (struct meson_pwm *)dev_get_drvdata(child); |
| |
| channel = pwm_get_chip_data(meson->chip.pwms); |
| return sprintf(buf, "%d\n", channel->variant.blink_times); |
| } |
| |
| static ssize_t blink_times_store(struct device *child, |
| struct device_attribute *attr, |
| const char *buf, size_t size) |
| { |
| struct meson_pwm *meson = |
| (struct meson_pwm *)dev_get_drvdata(child); |
| int val, ret, id, res; |
| |
| res = sscanf(buf, "%d %d", &val, &id); |
| if (res != 2) { |
| dev_err(child, |
| "Can't parse pwm id and value,usage:[value index]\n"); |
| return -EINVAL; |
| } |
| ret = pwm_set_blink_times(meson, id, val); |
| |
| return ret ? : size; |
| } |
| |
| int pwm_register_debug(struct meson_pwm *meson) |
| { |
| unsigned int i, value; |
| |
| /* If the current device is not in the RPM_ACTIVE state, |
| * domain may be in poweroff state, do not read registers. |
| */ |
| if (meson->data->runtime_enabled && |
| !atomic_read(&meson->chip.dev->power.usage_count)) { |
| pr_info("Make sure the device is in RPM_ACTIVE state\n"); |
| return -EINVAL; |
| } |
| |
| for (i = 0; i < 8; i++) { |
| regmap_read(meson->regmap_base, 4 * i, &value); |
| pr_info("[base+%x] = %x\n", 4 * i, value); |
| } |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(pwm_register_debug); |
| |
| static DEVICE_ATTR_RW(constant); |
| static DEVICE_ATTR_RW(times); |
| static DEVICE_ATTR_RW(blink_enable); |
| static DEVICE_ATTR_RW(blink_times); |
| |
| static struct attribute *pwm_attrs[] = { |
| &dev_attr_constant.attr, |
| &dev_attr_times.attr, |
| &dev_attr_blink_enable.attr, |
| &dev_attr_blink_times.attr, |
| NULL, |
| }; |
| |
| static struct attribute_group pwm_attr_group = { |
| .attrs = pwm_attrs, |
| }; |
| |
| int meson_pwm_sysfs_init(struct device *dev) |
| { |
| int retval; |
| |
| retval = sysfs_create_group(&dev->kobj, &pwm_attr_group); |
| if (retval) { |
| dev_err(dev, |
| "pwm sysfs group creation failed: %d\n", retval); |
| return retval; |
| } |
| |
| return 0; |
| } |
| |
| void meson_pwm_sysfs_exit(struct device *dev) |
| { |
| sysfs_remove_group(&dev->kobj, &pwm_attr_group); |
| } |