Bryan Huntsman | 3f2bc4d | 2011-08-16 17:27:22 -0700 | [diff] [blame] | 1 | /* Copyright (c) 2011, Code Aurora Forum. All rights reserved. |
| 2 | * |
| 3 | * This program is free software; you can redistribute it and/or modify |
| 4 | * it under the terms of the GNU General Public License version 2 and |
| 5 | * only version 2 as published by the Free Software Foundation. |
| 6 | * |
| 7 | * This program is distributed in the hope that it will be useful, |
| 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 10 | * GNU General Public License for more details. |
| 11 | * |
| 12 | */ |
| 13 | /* |
| 14 | * Qualcomm PMIC8058 Misc Device driver |
| 15 | * |
| 16 | */ |
| 17 | |
| 18 | #include <linux/debugfs.h> |
| 19 | #include <linux/err.h> |
| 20 | #include <linux/interrupt.h> |
| 21 | #include <linux/module.h> |
| 22 | #include <linux/platform_device.h> |
| 23 | #include <linux/slab.h> |
| 24 | #include <linux/mfd/pmic8058.h> |
| 25 | #include <linux/pmic8058-misc.h> |
| 26 | |
| 27 | /* VIB_DRV register */ |
| 28 | #define SSBI_REG_ADDR_DRV_VIB 0x4A |
| 29 | |
| 30 | #define PM8058_VIB_DRIVE_SHIFT 3 |
| 31 | #define PM8058_VIB_LOGIC_SHIFT 2 |
| 32 | #define PM8058_VIB_MIN_LEVEL_mV 1200 |
| 33 | #define PM8058_VIB_MAX_LEVEL_mV 3100 |
| 34 | |
| 35 | /* COINCELL_CHG register */ |
| 36 | #define SSBI_REG_ADDR_COINCELL_CHG (0x2F) |
| 37 | #define PM8058_COINCELL_RESISTOR_SHIFT (2) |
| 38 | |
| 39 | /* Resource offsets. */ |
| 40 | enum PM8058_MISC_IRQ { |
| 41 | PM8058_MISC_IRQ_OSC_HALT = 0 |
| 42 | }; |
| 43 | |
| 44 | struct pm8058_misc_device { |
| 45 | struct pm8058_chip *pm_chip; |
| 46 | struct dentry *dgb_dir; |
| 47 | unsigned int osc_halt_irq; |
| 48 | u64 osc_halt_count; |
| 49 | }; |
| 50 | |
| 51 | static struct pm8058_misc_device *misc_dev; |
| 52 | |
| 53 | int pm8058_vibrator_config(struct pm8058_vib_config *vib_config) |
| 54 | { |
| 55 | u8 reg = 0; |
| 56 | int rc; |
| 57 | |
| 58 | if (misc_dev == NULL) { |
| 59 | pr_info("misc_device is NULL\n"); |
| 60 | return -EINVAL; |
| 61 | } |
| 62 | |
| 63 | if (vib_config->drive_mV) { |
| 64 | if (vib_config->drive_mV < PM8058_VIB_MIN_LEVEL_mV || |
| 65 | vib_config->drive_mV > PM8058_VIB_MAX_LEVEL_mV) { |
| 66 | pr_err("Invalid vibrator drive strength\n"); |
| 67 | return -EINVAL; |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | reg = (vib_config->drive_mV / 100) << PM8058_VIB_DRIVE_SHIFT; |
| 72 | |
| 73 | reg |= (!!vib_config->active_low) << PM8058_VIB_LOGIC_SHIFT; |
| 74 | |
| 75 | reg |= vib_config->enable_mode; |
| 76 | |
| 77 | rc = pm8058_write(misc_dev->pm_chip, SSBI_REG_ADDR_DRV_VIB, ®, 1); |
| 78 | if (rc) |
| 79 | pr_err("%s: pm8058 write failed: rc=%d\n", __func__, rc); |
| 80 | |
| 81 | return rc; |
| 82 | } |
| 83 | EXPORT_SYMBOL(pm8058_vibrator_config); |
| 84 | |
| 85 | /** |
| 86 | * pm8058_coincell_chg_config - Disables or enables the coincell charger, and |
| 87 | * configures its voltage and resistor settings. |
| 88 | * @chg_config: Holds both voltage and resistor values, and a |
| 89 | * switch to change the state of charger. |
| 90 | * If state is to disable the charger then |
| 91 | * both voltage and resistor are disregarded. |
| 92 | * |
| 93 | * RETURNS: an appropriate -ERRNO error value on error, or zero for success. |
| 94 | */ |
| 95 | int pm8058_coincell_chg_config(struct pm8058_coincell_chg_config *chg_config) |
| 96 | { |
| 97 | u8 reg, voltage, resistor; |
| 98 | int rc; |
| 99 | |
| 100 | reg = 0; |
| 101 | voltage = 0; |
| 102 | resistor = 0; |
| 103 | rc = 0; |
| 104 | |
| 105 | if (misc_dev == NULL) { |
| 106 | pr_err("misc_device is NULL\n"); |
| 107 | return -EINVAL; |
| 108 | } |
| 109 | |
| 110 | if (chg_config == NULL) { |
| 111 | pr_err("chg_config is NULL\n"); |
| 112 | return -EINVAL; |
| 113 | } |
| 114 | |
| 115 | if (chg_config->state == PM8058_COINCELL_CHG_DISABLE) { |
| 116 | rc = pm8058_write(misc_dev->pm_chip, |
| 117 | SSBI_REG_ADDR_COINCELL_CHG, ®, 1); |
| 118 | if (rc) |
| 119 | pr_err("%s: pm8058 write failed: rc=%d\n", |
| 120 | __func__, rc); |
| 121 | return rc; |
| 122 | } |
| 123 | |
| 124 | voltage = chg_config->voltage; |
| 125 | resistor = chg_config->resistor; |
| 126 | |
| 127 | if (voltage < PM8058_COINCELL_VOLTAGE_3p2V || |
| 128 | (voltage > PM8058_COINCELL_VOLTAGE_3p0V && |
| 129 | voltage != PM8058_COINCELL_VOLTAGE_2p5V)) { |
| 130 | pr_err("Invalid voltage value provided\n"); |
| 131 | return -EINVAL; |
| 132 | } |
| 133 | |
| 134 | if (resistor < PM8058_COINCELL_RESISTOR_2100_OHMS || |
| 135 | resistor > PM8058_COINCELL_RESISTOR_800_OHMS) { |
| 136 | pr_err("Invalid resistor value provided\n"); |
| 137 | return -EINVAL; |
| 138 | } |
| 139 | |
| 140 | reg |= voltage; |
| 141 | |
| 142 | reg |= (resistor << PM8058_COINCELL_RESISTOR_SHIFT); |
| 143 | |
| 144 | rc = pm8058_write(misc_dev->pm_chip, |
| 145 | SSBI_REG_ADDR_COINCELL_CHG, ®, 1); |
| 146 | |
| 147 | if (rc) |
| 148 | pr_err("%s: pm8058 write failed: rc=%d\n", __func__, rc); |
| 149 | |
| 150 | return rc; |
| 151 | } |
| 152 | EXPORT_SYMBOL(pm8058_coincell_chg_config); |
| 153 | |
| 154 | /* Handle the OSC_HALT interrupt: 32 kHz XTAL oscillator has stopped. */ |
| 155 | static irqreturn_t pm8058_osc_halt_isr(int irq, void *data) |
| 156 | { |
| 157 | struct pm8058_misc_device *miscdev = data; |
| 158 | u64 count = 0; |
| 159 | |
| 160 | if (miscdev) { |
| 161 | miscdev->osc_halt_count++; |
| 162 | count = miscdev->osc_halt_count; |
| 163 | } |
| 164 | |
| 165 | pr_crit("%s: OSC_HALT interrupt has triggered, 32 kHz XTAL oscillator" |
| 166 | " has halted (%llu)!\n", __func__, count); |
| 167 | |
| 168 | return IRQ_HANDLED; |
| 169 | } |
| 170 | |
| 171 | #if defined(CONFIG_DEBUG_FS) |
| 172 | |
| 173 | static int osc_halt_count_get(void *data, u64 *val) |
| 174 | { |
| 175 | struct pm8058_misc_device *miscdev = data; |
| 176 | |
| 177 | if (miscdev == NULL) { |
| 178 | pr_err("%s: null pointer input.\n", __func__); |
| 179 | return -EINVAL; |
| 180 | } |
| 181 | |
| 182 | *val = miscdev->osc_halt_count; |
| 183 | |
| 184 | return 0; |
| 185 | } |
| 186 | |
| 187 | DEFINE_SIMPLE_ATTRIBUTE(dbg_osc_halt_fops, osc_halt_count_get, NULL, "%llu\n"); |
| 188 | |
| 189 | static int __devinit pmic8058_misc_dbg_probe(struct pm8058_misc_device *miscdev) |
| 190 | { |
| 191 | struct dentry *dent; |
| 192 | struct dentry *temp; |
| 193 | |
| 194 | if (miscdev == NULL) { |
| 195 | pr_err("%s: no parent data passed in.\n", __func__); |
| 196 | return -EINVAL; |
| 197 | } |
| 198 | |
| 199 | dent = debugfs_create_dir("pm8058-misc", NULL); |
| 200 | if (dent == NULL || IS_ERR(dent)) { |
| 201 | pr_err("%s: ERR debugfs_create_dir: dent=0x%X\n", |
| 202 | __func__, (unsigned)dent); |
| 203 | return -ENOMEM; |
| 204 | } |
| 205 | |
| 206 | temp = debugfs_create_file("osc_halt_count", S_IRUSR, dent, |
| 207 | miscdev, &dbg_osc_halt_fops); |
| 208 | if (temp == NULL || IS_ERR(temp)) { |
| 209 | pr_err("%s: ERR debugfs_create_file: dent=0x%X\n", |
| 210 | __func__, (unsigned)temp); |
| 211 | goto debug_error; |
| 212 | } |
| 213 | |
| 214 | miscdev->dgb_dir = dent; |
| 215 | return 0; |
| 216 | |
| 217 | debug_error: |
| 218 | debugfs_remove_recursive(dent); |
| 219 | return -ENOMEM; |
| 220 | } |
| 221 | |
| 222 | static int __devexit pmic8058_misc_dbg_remove( |
| 223 | struct pm8058_misc_device *miscdev) |
| 224 | { |
| 225 | if (miscdev->dgb_dir) |
| 226 | debugfs_remove_recursive(miscdev->dgb_dir); |
| 227 | |
| 228 | return 0; |
| 229 | } |
| 230 | |
| 231 | #else |
| 232 | |
| 233 | static int __devinit pmic8058_misc_dbg_probe(struct pm8058_misc_device *miscdev) |
| 234 | { |
| 235 | return 0; |
| 236 | } |
| 237 | |
| 238 | static int __devexit pmic8058_misc_dbg_remove( |
| 239 | struct pm8058_misc_device *miscdev) |
| 240 | { |
| 241 | return 0; |
| 242 | } |
| 243 | |
| 244 | #endif |
| 245 | |
| 246 | |
| 247 | static int __devinit pmic8058_misc_probe(struct platform_device *pdev) |
| 248 | { |
| 249 | struct pm8058_misc_device *miscdev; |
| 250 | struct pm8058_chip *pm_chip; |
| 251 | unsigned int irq; |
| 252 | int rc; |
| 253 | |
| 254 | pm_chip = dev_get_drvdata(pdev->dev.parent); |
| 255 | if (pm_chip == NULL) { |
| 256 | pr_err("%s: no driver data passed in.\n", __func__); |
| 257 | return -EFAULT; |
| 258 | } |
| 259 | |
| 260 | irq = platform_get_irq(pdev, PM8058_MISC_IRQ_OSC_HALT); |
| 261 | if (!irq) { |
| 262 | pr_err("%s: no IRQ passed in.\n", __func__); |
| 263 | return -EFAULT; |
| 264 | } |
| 265 | |
| 266 | miscdev = kzalloc(sizeof *miscdev, GFP_KERNEL); |
| 267 | if (miscdev == NULL) { |
| 268 | pr_err("%s: kzalloc() failed.\n", __func__); |
| 269 | return -ENOMEM; |
| 270 | } |
| 271 | |
| 272 | miscdev->pm_chip = pm_chip; |
| 273 | platform_set_drvdata(pdev, miscdev); |
| 274 | |
| 275 | rc = request_threaded_irq(irq, NULL, pm8058_osc_halt_isr, |
| 276 | IRQF_TRIGGER_RISING | IRQF_DISABLED, |
| 277 | "pm8058-osc_halt-irq", miscdev); |
| 278 | if (rc < 0) { |
| 279 | pr_err("%s: request_irq(%d) FAIL: %d\n", __func__, irq, rc); |
| 280 | platform_set_drvdata(pdev, miscdev->pm_chip); |
| 281 | kfree(miscdev); |
| 282 | return rc; |
| 283 | } |
| 284 | miscdev->osc_halt_irq = irq; |
| 285 | miscdev->osc_halt_count = 0; |
| 286 | |
| 287 | rc = pmic8058_misc_dbg_probe(miscdev); |
| 288 | if (rc) |
| 289 | return rc; |
| 290 | |
| 291 | misc_dev = miscdev; |
| 292 | |
| 293 | pr_notice("%s: OK\n", __func__); |
| 294 | return 0; |
| 295 | } |
| 296 | |
| 297 | static int __devexit pmic8058_misc_remove(struct platform_device *pdev) |
| 298 | { |
| 299 | struct pm8058_misc_device *miscdev = platform_get_drvdata(pdev); |
| 300 | |
| 301 | pmic8058_misc_dbg_remove(miscdev); |
| 302 | |
| 303 | platform_set_drvdata(pdev, miscdev->pm_chip); |
| 304 | free_irq(miscdev->osc_halt_irq, miscdev); |
| 305 | kfree(miscdev); |
| 306 | |
| 307 | return 0; |
| 308 | } |
| 309 | |
| 310 | static struct platform_driver pmic8058_misc_driver = { |
| 311 | .probe = pmic8058_misc_probe, |
| 312 | .remove = __devexit_p(pmic8058_misc_remove), |
| 313 | .driver = { |
| 314 | .name = "pm8058-misc", |
| 315 | .owner = THIS_MODULE, |
| 316 | }, |
| 317 | }; |
| 318 | |
| 319 | static int __init pm8058_misc_init(void) |
| 320 | { |
| 321 | return platform_driver_register(&pmic8058_misc_driver); |
| 322 | } |
| 323 | |
| 324 | static void __exit pm8058_misc_exit(void) |
| 325 | { |
| 326 | platform_driver_unregister(&pmic8058_misc_driver); |
| 327 | } |
| 328 | |
| 329 | module_init(pm8058_misc_init); |
| 330 | module_exit(pm8058_misc_exit); |
| 331 | |
| 332 | MODULE_LICENSE("GPL v2"); |
| 333 | MODULE_DESCRIPTION("PMIC8058 Misc Device driver"); |
| 334 | MODULE_VERSION("1.0"); |
| 335 | MODULE_ALIAS("platform:pmic8058-misc"); |