/* Copyright (c) 2015-2016, The Linux Foundation. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 and * only version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ #include #include #include #include #include #include #include #include #include #include "leds-ipq.h" #define LEDC_BASE_REG_OFFSET 0x20040 #define LEDC_REG_SIZE 4 #define BUF_NAME_LEN 25 #define MAX_BLINK_IDX 13 #define LEDC_DRV_NAME "ipq-leds" #define LEDC_ADDR(x) (ledc_base_addr + LEDC_BASE_REG_OFFSET + \ (LEDC_REG_SIZE * (x))) enum ledc_offsets { LEDC_CG0_OFFSET, LEDC_CG1_OFFSET, LEDC_CG2_OFFSET, LEDC_CG3_OFFSET, LEDC_CG4_OFFSET, LEDC_CG5_OFFSET, LEDC_CG6_OFFSET, LEDC_CG7_OFFSET, LEDC_CG8_OFFSET, LEDC_CG9_OFFSET, LEDC_CG10_OFFSET, LEDC_CG11_OFFSET, LEDC_MAX_OFFSET }; enum blink_freq_values { BLINK_2HZ, BLINK_4HZ, BLINK_8HZ, BLINK_16HZ, BLINK_32HZ, BLINK_64HZ }; enum blink_duty_values { BLINK_DUTY_50, BLINK_DUTY_25, BLINK_DUTY_33, BLINK_DUTY_67, BLINK_DUTY_75 }; struct ipq_led_data { struct led_classdev cdev; int led_blink_idx; }; static struct ipq_led_data *leds; static void *ledc_base_addr; static int blink_idx_cnt; static int led_blink_array[MAX_BLINK_IDX]; static int led_blink_src[MAX_BLINK_IDX]; static const u32 *blink_src; static int ipq_set_led_blink_set(struct led_classdev *led_cdev, unsigned long *delay_on, unsigned long *delay_off) { struct ipq_led_data *led_dat = container_of(led_cdev, struct ipq_led_data, cdev); int blink_enable, blink_freq, blink_duty, blink_value, duty_cycle, total_on_off; int reg, mask; if (*delay_on <= 0) blink_enable = blink_freq = blink_duty = 0; else blink_enable = 1; if (blink_enable) { total_on_off = *delay_on + *delay_off; /* Rounding off to the nearest available frequency */ if (total_on_off >= 500) blink_freq = BLINK_2HZ; else if (total_on_off >= 250) blink_freq = BLINK_4HZ; else if (total_on_off >= 125) blink_freq = BLINK_8HZ; else if (total_on_off >= 62) blink_freq = BLINK_16HZ; else if (total_on_off >= 31) blink_freq = BLINK_32HZ; else blink_freq = BLINK_64HZ; blink_freq = blink_freq << 1; duty_cycle = (100 * (*delay_on)) / total_on_off; /* Rounding off to the nearest available duty cycle */ if (duty_cycle >= 75) blink_duty = BLINK_DUTY_75; else if (duty_cycle >= 67) blink_duty = BLINK_DUTY_67; else if (duty_cycle >= 50) blink_duty = BLINK_DUTY_50; else if (duty_cycle >= 33) blink_duty = BLINK_DUTY_33; else blink_duty = BLINK_DUTY_25; blink_duty = blink_duty << 5; } blink_value = blink_enable | blink_freq | blink_duty; reg = readl(LEDC_ADDR(LEDC_CG9_OFFSET)); /* Only for SoC with limited blink sources */ if (blink_src && led_blink_src[led_dat->led_blink_idx] >= LEDC_ST_BLINK_0) { blink_value = blink_value << (8 * (led_blink_src[led_dat->led_blink_idx] % LEDC_ST_BLINK_0)); mask = ~(0xFF << (8 * (led_blink_src[led_dat->led_blink_idx] % LEDC_ST_BLINK_0))); } else { blink_value = blink_value << (led_dat->led_blink_idx * 8); mask = ~(0xFF << (led_dat->led_blink_idx * 8)); } reg = (reg & mask) | blink_value; writel(reg, LEDC_ADDR(LEDC_CG9_OFFSET)); return 0; } int ipq_led_set_blink(int led_idx, unsigned long delay_on, unsigned long delay_off) { struct ipq_led_data led_data; if (!leds) return -ENODEV; led_data.led_blink_idx = led_idx; ipq_set_led_blink_set(&led_data.cdev, &delay_on, &delay_off); return 0; } EXPORT_SYMBOL(ipq_led_set_blink); static void ipq_set_led_brightness_set(struct led_classdev *led_cdev, enum led_brightness brightness) { struct ipq_led_data *led_dat = container_of(led_cdev, struct ipq_led_data, cdev); int reg, mask; unsigned long delay_on, delay_off; delay_on = 0; delay_off = 0; reg = readl(LEDC_ADDR(LEDC_CG6_OFFSET)); mask = 0x1 << led_blink_array[led_dat->led_blink_idx]; if (brightness == LED_OFF) reg = reg & (~mask); else reg = reg | mask; /* Clear blink settings. Setting brightness overrides blink settings. */ ipq_set_led_blink_set(led_cdev, &delay_on, &delay_off); writel(reg, LEDC_ADDR(LEDC_CG6_OFFSET)); } int ipq_led_set_brightness(int led_idx, unsigned int brightness) { struct ipq_led_data led_data; if (!leds) return -ENODEV; led_data.led_blink_idx = led_idx; ipq_set_led_brightness_set(&led_data.cdev, brightness); return 0; } EXPORT_SYMBOL(ipq_led_set_brightness); int ipq_led_source_select(int led_num, enum led_source src_type) { int val, cg_reg; cg_reg = (led_num / NUM_LED_IN_REG) + 1; if (cg_reg > LEDC_CG4_OFFSET || !ledc_base_addr) return -EINVAL; val = readl(LEDC_ADDR(cg_reg)); val &= LED_MASK(led_num); val |= SET_LED(led_num, src_type); writel(val, LEDC_ADDR(cg_reg)); return 0; } EXPORT_SYMBOL(ipq_led_source_select); static int __init ipq_led_probe(struct platform_device *pdev) { int ret, i, j = 0; int lenp; int led_num; uint32_t val_arr[LEDC_MAX_OFFSET]; char buf[BUF_NAME_LEN]; struct resource *res; struct device_node *of_node = pdev->dev.of_node; struct device_node *child; const char *led_default_trigger[MAX_BLINK_IDX] = {NULL}; if (!of_node) return -ENODEV; res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "ledc_base_addr"); if (!res) { dev_err(&pdev->dev, "Could not get ledc base addr.\n"); return -EINVAL; } ledc_base_addr = ioremap(res->start, res->end - res->start + 1); if (!ledc_base_addr) { dev_err(&pdev->dev, "Failed to IO map ledc base addr.\n"); return -ENOMEM; } ret = of_property_read_u32_array(of_node, "qcom,tcsr_ledc_values", val_arr, LEDC_MAX_OFFSET); if (ret) { dev_err(&pdev->dev, "invalid or missing property: qcom,tcsr_ledc_values\n"); return -ENODEV; }; for (i = 0; i < LEDC_MAX_OFFSET; i++) writel(val_arr[i], LEDC_ADDR(i)); ret = of_property_read_u32(of_node, "qcom,ledc_blink_indices_cnt", &blink_idx_cnt); if (ret) { dev_err(&pdev->dev, "missing blink idx count.\n"); return -ENODEV; } if (blink_idx_cnt > MAX_BLINK_IDX) blink_idx_cnt = MAX_BLINK_IDX; ret = of_property_read_u32_array(of_node, "qcom,ledc_blink_indices", led_blink_array, blink_idx_cnt); if (ret) { dev_err(&pdev->dev, "invalid or missing property: blink indices\n"); return -ENODEV; }; memset(led_blink_src, -1, MAX_BLINK_IDX * sizeof(int)); /* Optional - Required when there is limited blink sources */ blink_src = of_get_property(of_node, "qcom,ledc_blink_idx_src_pair", &lenp); if (blink_src) { if (!lenp || lenp / sizeof(int) > MAX_BLINK_IDX * 2) { dev_err(&pdev->dev, "empty/exceeded range for qcom,ledc_blink_idx_src_pair"); return -ENODEV; } for (i = 0; i < lenp / sizeof(int); i += 2) { led_num = led_blink_array[be32_to_cpu(blink_src[i])]; if (be32_to_cpu(blink_src[i]) < MAX_BLINK_IDX && be32_to_cpu(blink_src[i + 1]) >= WIFI_2G_0 && be32_to_cpu(blink_src[i + 1]) <= LEDC_ST_BLINK_3) { led_blink_src[be32_to_cpu(blink_src[i])] = be32_to_cpu(blink_src[i + 1]); ipq_led_source_select(led_num, be32_to_cpu(blink_src[i + 1])); } else { dev_err(&pdev->dev, "invalid property qcom,ledc_blink_idx_src_pair"); return -ENODEV; } } } #ifdef CONFIG_LEDS_TRIGGERS for_each_child_of_node(of_node, child) { led_default_trigger[j] = of_get_property(child, "linux,default-trigger", NULL); ++j; } #endif leds = kzalloc((sizeof(*leds) * blink_idx_cnt), GFP_KERNEL); if (!leds) { ret = -EINVAL; goto err_iounmap; } for (i = 0; i < blink_idx_cnt; i++) { if (!blink_src) leds[i].cdev.blink_set = ipq_set_led_blink_set; else { /* assigning blink_set to LEDs with h/w blink src */ for (j = 0; j < lenp / sizeof(int); j += 2) { if (i == be32_to_cpu(blink_src[j])) leds[i].cdev.blink_set = ipq_set_led_blink_set; } } leds[i].led_blink_idx = i; leds[i].cdev.brightness = LED_OFF; leds[i].cdev.max_brightness = 1; leds[i].cdev.brightness_set = ipq_set_led_brightness_set; #ifdef CONFIG_LEDS_TRIGGERS leds[i].cdev.default_trigger = led_default_trigger[i]; #endif memset(buf, 0, BUF_NAME_LEN); snprintf(buf, BUF_NAME_LEN, "ipq::led%d", i); leds[i].cdev.name = buf; ret = led_classdev_register(&pdev->dev, &leds[i].cdev); if (ret) goto err_iounmap; } return 0; err_iounmap: if (leds) { for (; i > 0; i--) led_classdev_unregister(&leds[i - 1].cdev); kfree(leds); leds = NULL; } pr_err("%s - Error registering class.\n", __func__); iounmap(ledc_base_addr); ledc_base_addr = NULL; return ret; } static int __exit ipq_led_remove(struct platform_device *pdev) { int i; if (leds) { for (i = 0; i < blink_idx_cnt; i++) led_classdev_unregister(&leds[i].cdev); kfree(leds); leds = NULL; } if (ledc_base_addr) { iounmap(ledc_base_addr); ledc_base_addr = NULL; } return 0; } static struct of_device_id ipq_match_table[] = { {.compatible = "qca,ledc"}, {}, }; static struct platform_driver ipq_led_driver = { .probe = ipq_led_probe, .remove = ipq_led_remove, .driver = { .name = LEDC_DRV_NAME, .owner = THIS_MODULE, .of_match_table = ipq_match_table, }, }; module_platform_driver(ipq_led_driver); MODULE_LICENSE("GPL v2"); MODULE_VERSION("0.1"); MODULE_ALIAS("platform:"LEDC_DRV_NAME);