/** * struct lpg - LPG device context * @dev: pointer to LPG device * @map: regmap for register access * @lock: used to synchronize LED and pwm callback requests * @pwm: PWM-chip object, if operating in PWM mode * @data: reference to version specific data * @lut_base: base address of the LUT block (optional) * @lut_size: number of entries in the LUT block * @lut_bitmap: allocation bitmap for LUT entries * @pbs_dev: PBS device * @lpg_chan_sdam: LPG SDAM peripheral device * @lut_sdam: LUT SDAM peripheral device * @pbs_en_bitmap: bitmap for tracking PBS triggers * @triled_base: base address of the TRILED block (optional) * @triled_src: power-source for the TRILED * @triled_has_atc_ctl: true if there is TRI_LED_ATC_CTL register * @triled_has_src_sel: true if there is TRI_LED_SRC_SEL register * @channels: list of PWM channels * @num_channels: number of @channels
*/ struct lpg { struct device *dev; struct regmap *map;
/** * struct lpg_channel - per channel data * @lpg: reference to parent lpg * @base: base address of the PWM channel * @triled_mask: mask in TRILED to enable this channel * @lut_mask: mask in LUT to start pattern generator for this channel * @subtype: PMIC hardware block subtype * @sdam_offset: channel offset in LPG SDAM * @in_use: channel is exposed to LED framework * @color: color of the LED attached to this channel * @dtest_line: DTEST line for output, or 0 if disabled * @dtest_value: DTEST line configuration * @pwm_value: duty (in microseconds) of the generated pulses, overridden by LUT * @enabled: output enabled? * @period: period (in nanoseconds) of the generated pulses * @clk_sel: reference clock frequency selector * @pre_div_sel: divider selector of the reference clock * @pre_div_exp: exponential divider of the reference clock * @pwm_resolution_sel: pwm resolution selector * @ramp_enabled: duty cycle is driven by iterating over lookup table * @ramp_ping_pong: reverse through pattern, rather than wrapping to start * @ramp_oneshot: perform only a single pass over the pattern * @ramp_reverse: iterate over pattern backwards * @ramp_tick_ms: length (in milliseconds) of one step in the pattern * @ramp_lo_pause_ms: pause (in milliseconds) before iterating over pattern * @ramp_hi_pause_ms: pause (in milliseconds) after iterating over pattern * @pattern_lo_idx: start index of associated pattern * @pattern_hi_idx: last index of associated pattern
*/ struct lpg_channel { struct lpg *lpg;
/** * struct lpg_led - logical LED object * @lpg: lpg context reference * @cdev: LED class device * @mcdev: Multicolor LED class device * @num_channels: number of @channels * @channels: list of channels associated with the LED
*/ struct lpg_led { struct lpg *lpg;
/** * struct lpg_channel_data - per channel initialization data * @sdam_offset: Channel offset in LPG SDAM * @base: base address for PWM channel registers * @triled_mask: bitmask for controlling this channel in TRILED
*/ struct lpg_channel_data { unsignedint sdam_offset; unsignedint base;
u8 triled_mask;
};
/** * struct lpg_data - initialization data * @lut_base: base address of LUT block * @lut_size: number of entries in LUT * @triled_base: base address of TRILED * @triled_has_atc_ctl: true if there is TRI_LED_ATC_CTL register * @triled_has_src_sel: true if there is TRI_LED_SRC_SEL register * @num_channels: number of channels in LPG * @channels: list of channel initialization data
*/ struct lpg_data { unsignedint lut_base; unsignedint lut_size; unsignedint triled_base; bool triled_has_atc_ctl; bool triled_has_src_sel; int num_channels; conststruct lpg_channel_data *channels;
};
#define PBS_SW_TRIG_BIT BIT(0)
staticint lpg_clear_pbs_trigger(struct lpg *lpg, unsignedint lut_mask)
{
u8 val = 0; int rc;
if (!lpg->lpg_chan_sdam) return 0;
lpg->pbs_en_bitmap &= (~lut_mask); if (!lpg->pbs_en_bitmap) {
rc = nvmem_device_write(lpg->lpg_chan_sdam, SDAM_REG_PBS_SEQ_EN, 1, &val); if (rc < 0) return rc;
if (lpg->lut_sdam) {
val = PBS_SW_TRIG_BIT;
rc = nvmem_device_write(lpg->lpg_chan_sdam, SDAM_PBS_TRIG_CLR, 1, &val); if (rc < 0) return rc;
}
}
return 0;
}
staticint lpg_set_pbs_trigger(struct lpg *lpg, unsignedint lut_mask)
{
u8 val = PBS_SW_TRIG_BIT; int rc;
if (!lpg->lpg_chan_sdam) return 0;
if (!lpg->pbs_en_bitmap) {
rc = nvmem_device_write(lpg->lpg_chan_sdam, SDAM_REG_PBS_SEQ_EN, 1, &val); if (rc < 0) return rc;
staticint triled_set(struct lpg *lpg, unsignedint mask, unsignedint enable)
{ /* Skip if we don't have a triled block */ if (!lpg->triled_base) return 0;
/* * The PWM period is determined by: * * resolution * pre_div * 2^M * period = -------------------------- * refclk * * Resolution = 2^{6 or 9} bits for PWM or * 2^{8, 9, 10, 11, 12, 13, 14, 15} bits for high resolution PWM * pre_div = {1, 3, 5, 6} and * M = [0..7]. * * This allows for periods between 3uS and 384s for PWM channels and periods between * 3uS and 24576s for high resolution PWMs. * The PWM framework wants a period of equal or lower length than requested, * reject anything below minimum period.
*/
/* Limit period to largest possible value, to avoid overflows */
max_period = div64_u64((u64)NSEC_PER_SEC * max_res * LPG_MAX_PREDIV * (1 << LPG_MAX_M),
1024); if (period > max_period)
period = max_period;
/* * Search for the pre_div, refclk, resolution and M by solving the rewritten formula * for each refclk, resolution and pre_div value: * * period * refclk * M = log2 ------------------------------------- * NSEC_PER_SEC * pre_div * resolution
*/
for (i = 0; i < pwm_resolution_count; i++) {
resolution = (1 << pwm_resolution_arr[i]) - 1; for (clk_sel = 1; clk_sel < clk_len; clk_sel++) {
u64 numerator = period * clk_rate_arr[clk_sel];
/* Specify resolution, based on the subtype of the channel */ switch (chan->subtype) { case LPG_SUBTYPE_LPG:
val |= GENMASK(5, 4); break; case LPG_SUBTYPE_PWM:
val |= FIELD_PREP(PWM_SIZE_SELECT_MASK, chan->pwm_resolution_sel); break; case LPG_SUBTYPE_HI_RES_PWM:
val |= FIELD_PREP(PWM_SIZE_HI_RES_MASK, chan->pwm_resolution_sel); break; case LPG_SUBTYPE_LPG_LITE: default:
val |= BIT(4); break;
}
if (!chan->ramp_reverse)
conf |= LPG_PATTERN_CONFIG_LO_TO_HI; if (!chan->ramp_oneshot)
conf |= LPG_PATTERN_CONFIG_REPEAT; if (chan->ramp_ping_pong)
conf |= LPG_PATTERN_CONFIG_TOGGLE; if (chan->ramp_hi_pause_ms)
conf |= LPG_PATTERN_CONFIG_PAUSE_HI; if (chan->ramp_lo_pause_ms)
conf |= LPG_PATTERN_CONFIG_PAUSE_LO;
/* * Due to LPG hardware bug, in the PWM mode, having enabled PWM, * We have to write PWM values one more time.
*/ if (chan->enabled)
lpg_apply_pwm_value(chan);
}
/* Hardware only support oneshot or indefinite loops */ if (repeat != -1 && repeat != 1) return -EINVAL;
/* * The standardized leds-trigger-pattern format defines that the * brightness of the LED follows a linear transition from one entry * in the pattern to the next, over the given delta_t time. It * describes that the way to perform instant transitions a zero-length * entry should be added following a pattern entry. * * The LPG hardware is only able to perform the latter (no linear * transitions), so require each entry in the pattern to be followed by * a zero-length transition.
*/ if (len % 2) return -EINVAL;
for (i = 0; i < len; i += 2) { if (led_pattern[i].brightness != led_pattern[i + 1].brightness) goto out_free_pattern; if (led_pattern[i + 1].delta_t != 0) goto out_free_pattern;
/* * Specifying a pattern of length 1 causes the hardware to iterate * through the entire LUT, so prohibit this.
*/ if (len < 2) goto out_free_pattern;
/* * The LPG plays patterns with at a fixed pace, a "low pause" can be * used to stretch the first delay of the pattern and a "high pause" * the last one. * * In order to save space the pattern can be played in "ping pong" * mode, in which the pattern is first played forward, then "high * pause" is applied, then the pattern is played backwards and finally * the "low pause" is applied. * * The middle elements of the pattern are used to determine delta_t and * the "low pause" and "high pause" multipliers are derrived from this. * * The first element in the pattern is used to determine "low pause". * * If the specified pattern is a palindrome the ping pong mode is * enabled. In this scenario the delta_t of the middle entry (i.e. the * last in the programmed pattern) determines the "high pause". * * SDAM-based devices do not support "ping pong", and only supports * "low pause" and "high pause" with a dedicated SDAM LUT.
*/
/* Detect palindromes and use "ping pong" to reduce LUT usage */ if (lpg->lut_base) { for (i = 0; i < len / 2; i++) {
brightness_a = pattern[i].brightness;
brightness_b = pattern[len - i - 1].brightness;
/* The pattern length to be written to the LUT */ if (ping_pong)
actual_len = (len + 1) / 2; else
actual_len = len;
/* * Validate that all delta_t in the pattern are the same, with the * exception of the middle element in case of ping_pong.
*/
delta_t = pattern[1].delta_t; for (i = 2; i < len; i++) { if (pattern[i].delta_t != delta_t) { /* * Allow last entry in the full or shortened pattern to * specify hi pause. Reject other variations.
*/ if (i != actual_len - 1) goto out_free_pattern;
}
}
/* LPG_RAMP_DURATION_REG is a 9bit */ if (delta_t >= BIT(9)) goto out_free_pattern;
/* * Find "low pause" and "high pause" in the pattern in the LUT case. * SDAM-based devices without dedicated LUT SDAM require equal * duration of all steps.
*/ if (lpg->lut_base || lpg->lut_sdam) {
lo_pause = pattern[0].delta_t;
hi_pause = pattern[actual_len - 1].delta_t;
} else { if (delta_t != pattern[0].delta_t || delta_t != pattern[actual_len - 1].delta_t) goto out_free_pattern;
}
mutex_lock(&lpg->lock);
if (lpg->lut_base)
ret = lpg_lut_store(lpg, pattern, actual_len, &lo_idx, &hi_idx); else
ret = lpg_lut_store_sdam(lpg, pattern, actual_len, &lo_idx, &hi_idx);
if (ret < 0) goto out_unlock;
for (i = 0; i < led->num_channels; i++) {
chan = led->channels[i];
/* * Limitations: * - Updating both duty and period is not done atomically, so the output signal * will momentarily be a mix of the settings. * - Changed parameters takes effect immediately. * - A disabled channel outputs a logical 0.
*/ staticint lpg_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm, conststruct pwm_state *state)
{ struct lpg *lpg = lpg_pwm_from_chip(chip); struct lpg_channel *chan = &lpg->channels[pwm->hwpwm]; int ret = 0;
if (state->polarity != PWM_POLARITY_NORMAL) return -EINVAL;
mutex_lock(&lpg->lock);
if (state->enabled) {
ret = lpg_calc_freq(chan, state->period); if (ret < 0) goto out_unlock;
ret = of_property_read_u32(np, "color", &color); if (ret < 0 && ret != -EINVAL) return dev_err_probe(lpg->dev, ret, "failed to parse \"color\" of %pOF\n", np);
chan->color = color;
*channel = chan;
return 0;
}
staticint lpg_add_led(struct lpg *lpg, struct device_node *np)
{ struct led_init_data init_data = {}; struct led_classdev *cdev; struct mc_subled *info; struct lpg_led *led; constchar *state; int num_channels;
u32 color = 0; int ret; int i;
ret = of_property_read_u32(np, "color", &color); if (ret < 0 && ret != -EINVAL) return dev_err_probe(lpg->dev, ret, "failed to parse \"color\" of %pOF\n", np);
/* Register pattern accessors if we have a LUT block or when using PPG */ if (lpg->lut_base || lpg->lpg_chan_sdam) {
cdev->pattern_set = lpg_pattern_mc_set;
cdev->pattern_clear = lpg_pattern_mc_clear;
}
} else {
ret = lpg_parse_channel(lpg, np, &led->channels[0]); if (ret < 0) return ret;
/* Register pattern accessors if we have a LUT block or when using PPG */ if (lpg->lut_base || lpg->lpg_chan_sdam) {
cdev->pattern_set = lpg_pattern_single_set;
cdev->pattern_clear = lpg_pattern_single_clear;
}
}
if (color == LED_COLOR_ID_RGB)
ret = devm_led_classdev_multicolor_register_ext(lpg->dev, &led->mcdev, &init_data); else
ret = devm_led_classdev_register_ext(lpg->dev, &led->cdev, &init_data); if (ret)
dev_err_probe(lpg->dev, ret, "unable to register %s\n", cdev->name);
lpg->lut_size = data->lut_size; if (data->lut_base)
lpg->lut_base = data->lut_base;
lpg->lut_bitmap = devm_bitmap_zalloc(lpg->dev, lpg->lut_size, GFP_KERNEL); if (!lpg->lut_bitmap) return -ENOMEM;
return 0;
}
staticint lpg_init_sdam(struct lpg *lpg)
{ int i, sdam_count, rc;
u8 val = 0;
sdam_count = of_property_count_strings(lpg->dev->of_node, "nvmem-names"); if (sdam_count <= 0) return 0; if (sdam_count > SDAM_MAX_DEVICES) return -EINVAL;
/* Get the 1st SDAM device for LPG/LUT config */
lpg->lpg_chan_sdam = devm_nvmem_device_get(lpg->dev, "lpg_chan_sdam"); if (IS_ERR(lpg->lpg_chan_sdam)) return dev_err_probe(lpg->dev, PTR_ERR(lpg->lpg_chan_sdam), "Failed to get LPG chan SDAM device\n");
if (sdam_count == 1) { /* Get PBS device node if single SDAM device */
lpg->pbs_dev = get_pbs_client_device(lpg->dev); if (IS_ERR(lpg->pbs_dev)) return dev_err_probe(lpg->dev, PTR_ERR(lpg->pbs_dev), "Failed to get PBS client device\n");
} elseif (sdam_count == 2) { /* Get the 2nd SDAM device for LUT pattern */
lpg->lut_sdam = devm_nvmem_device_get(lpg->dev, "lut_sdam"); if (IS_ERR(lpg->lut_sdam)) return dev_err_probe(lpg->dev, PTR_ERR(lpg->lut_sdam), "Failed to get LPG LUT SDAM device\n");
}
for (i = 0; i < lpg->num_channels; i++) { struct lpg_channel *chan = &lpg->channels[i];
if (chan->sdam_offset) {
rc = nvmem_device_write(lpg->lpg_chan_sdam,
SDAM_PBS_SCRATCH_LUT_COUNTER_OFFSET + chan->sdam_offset, 1, &val); if (rc < 0) return rc;
rc = lpg_sdam_configure_triggers(chan, 0); if (rc < 0) return rc;
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.