1 // SPDX-License-Identifier: GPL-2.0-only 2 /* 3 * Copyright (C) 2024 Linaro Ltd. 4 */ 5 6 #include <linux/clk.h> 7 #include <linux/delay.h> 8 #include <linux/device.h> 9 #include <linux/gpio/consumer.h> 10 #include <linux/jiffies.h> 11 #include <linux/mod_devicetable.h> 12 #include <linux/module.h> 13 #include <linux/of.h> 14 #include <linux/platform_device.h> 15 #include <linux/property.h> 16 #include <linux/regulator/consumer.h> 17 #include <linux/pwrseq/provider.h> 18 #include <linux/string.h> 19 #include <linux/types.h> 20 21 struct pwrseq_qcom_wcn_pdata { 22 const char *const *vregs; 23 size_t num_vregs; 24 unsigned int pwup_delay_ms; 25 unsigned int gpio_enable_delay_ms; 26 const struct pwrseq_target_data **targets; 27 bool has_vddio; /* separate VDD IO regulator */ 28 int (*match)(struct pwrseq_device *pwrseq, struct device *dev); 29 }; 30 31 struct pwrseq_qcom_wcn_ctx { 32 struct pwrseq_device *pwrseq; 33 struct device_node *of_node; 34 const struct pwrseq_qcom_wcn_pdata *pdata; 35 struct regulator_bulk_data *regs; 36 struct regulator *vddio; 37 struct gpio_desc *bt_gpio; 38 struct gpio_desc *wlan_gpio; 39 struct gpio_desc *xo_clk_gpio; 40 struct clk *clk; 41 unsigned long last_gpio_enable_jf; 42 }; 43 44 static void pwrseq_qcom_wcn_ensure_gpio_delay(struct pwrseq_qcom_wcn_ctx *ctx) 45 { 46 unsigned long diff_jiffies; 47 unsigned int diff_msecs; 48 49 if (!ctx->pdata->gpio_enable_delay_ms) 50 return; 51 52 diff_jiffies = jiffies - ctx->last_gpio_enable_jf; 53 diff_msecs = jiffies_to_msecs(diff_jiffies); 54 55 if (diff_msecs < ctx->pdata->gpio_enable_delay_ms) 56 msleep(ctx->pdata->gpio_enable_delay_ms - diff_msecs); 57 } 58 59 static int pwrseq_qcom_wcn_vddio_enable(struct pwrseq_device *pwrseq) 60 { 61 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 62 63 return regulator_enable(ctx->vddio); 64 } 65 66 static int pwrseq_qcom_wcn_vddio_disable(struct pwrseq_device *pwrseq) 67 { 68 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 69 70 return regulator_disable(ctx->vddio); 71 } 72 73 static const struct pwrseq_unit_data pwrseq_qcom_wcn_vddio_unit_data = { 74 .name = "vddio-enable", 75 .enable = pwrseq_qcom_wcn_vddio_enable, 76 .disable = pwrseq_qcom_wcn_vddio_disable, 77 }; 78 79 static int pwrseq_qcom_wcn_vregs_enable(struct pwrseq_device *pwrseq) 80 { 81 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 82 83 return regulator_bulk_enable(ctx->pdata->num_vregs, ctx->regs); 84 } 85 86 static int pwrseq_qcom_wcn_vregs_disable(struct pwrseq_device *pwrseq) 87 { 88 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 89 90 return regulator_bulk_disable(ctx->pdata->num_vregs, ctx->regs); 91 } 92 93 static const struct pwrseq_unit_data pwrseq_qcom_wcn_vregs_unit_data = { 94 .name = "regulators-enable", 95 .enable = pwrseq_qcom_wcn_vregs_enable, 96 .disable = pwrseq_qcom_wcn_vregs_disable, 97 }; 98 99 static int pwrseq_qcom_wcn_clk_enable(struct pwrseq_device *pwrseq) 100 { 101 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 102 103 return clk_prepare_enable(ctx->clk); 104 } 105 106 static int pwrseq_qcom_wcn_clk_disable(struct pwrseq_device *pwrseq) 107 { 108 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 109 110 clk_disable_unprepare(ctx->clk); 111 112 return 0; 113 } 114 115 static const struct pwrseq_unit_data pwrseq_qcom_wcn_clk_unit_data = { 116 .name = "clock-enable", 117 .enable = pwrseq_qcom_wcn_clk_enable, 118 .disable = pwrseq_qcom_wcn_clk_disable, 119 }; 120 121 static const struct pwrseq_unit_data *pwrseq_qcom_wcn3990_unit_deps[] = { 122 &pwrseq_qcom_wcn_vddio_unit_data, 123 &pwrseq_qcom_wcn_vregs_unit_data, 124 NULL, 125 }; 126 127 static const struct pwrseq_unit_data pwrseq_qcom_wcn3990_unit_data = { 128 .name = "clock-enable", 129 .deps = pwrseq_qcom_wcn3990_unit_deps, 130 .enable = pwrseq_qcom_wcn_clk_enable, 131 .disable = pwrseq_qcom_wcn_clk_disable, 132 }; 133 134 static const struct pwrseq_unit_data *pwrseq_qcom_wcn_unit_deps[] = { 135 &pwrseq_qcom_wcn_vregs_unit_data, 136 &pwrseq_qcom_wcn_clk_unit_data, 137 NULL 138 }; 139 140 static int pwrseq_qcom_wcn6855_clk_assert(struct pwrseq_device *pwrseq) 141 { 142 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 143 144 if (!ctx->xo_clk_gpio) 145 return 0; 146 147 msleep(1); 148 149 gpiod_set_value_cansleep(ctx->xo_clk_gpio, 1); 150 usleep_range(100, 200); 151 152 return 0; 153 } 154 155 static const struct pwrseq_unit_data pwrseq_qcom_wcn6855_xo_clk_assert = { 156 .name = "xo-clk-assert", 157 .enable = pwrseq_qcom_wcn6855_clk_assert, 158 }; 159 160 static const struct pwrseq_unit_data *pwrseq_qcom_wcn6855_unit_deps[] = { 161 &pwrseq_qcom_wcn_vregs_unit_data, 162 &pwrseq_qcom_wcn_clk_unit_data, 163 &pwrseq_qcom_wcn6855_xo_clk_assert, 164 NULL 165 }; 166 167 static int pwrseq_qcom_wcn_bt_enable(struct pwrseq_device *pwrseq) 168 { 169 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 170 171 pwrseq_qcom_wcn_ensure_gpio_delay(ctx); 172 gpiod_set_value_cansleep(ctx->bt_gpio, 1); 173 ctx->last_gpio_enable_jf = jiffies; 174 175 return 0; 176 } 177 178 static int pwrseq_qcom_wcn_bt_disable(struct pwrseq_device *pwrseq) 179 { 180 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 181 182 gpiod_set_value_cansleep(ctx->bt_gpio, 0); 183 184 return 0; 185 } 186 187 static const struct pwrseq_unit_data pwrseq_qcom_wcn_bt_unit_data = { 188 .name = "bluetooth-enable", 189 .deps = pwrseq_qcom_wcn_unit_deps, 190 .enable = pwrseq_qcom_wcn_bt_enable, 191 .disable = pwrseq_qcom_wcn_bt_disable, 192 }; 193 194 static const struct pwrseq_unit_data pwrseq_qcom_wcn6855_bt_unit_data = { 195 .name = "bluetooth-enable", 196 .deps = pwrseq_qcom_wcn6855_unit_deps, 197 .enable = pwrseq_qcom_wcn_bt_enable, 198 .disable = pwrseq_qcom_wcn_bt_disable, 199 }; 200 201 static int pwrseq_qcom_wcn_wlan_enable(struct pwrseq_device *pwrseq) 202 { 203 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 204 205 pwrseq_qcom_wcn_ensure_gpio_delay(ctx); 206 gpiod_set_value_cansleep(ctx->wlan_gpio, 1); 207 ctx->last_gpio_enable_jf = jiffies; 208 209 return 0; 210 } 211 212 static int pwrseq_qcom_wcn_wlan_disable(struct pwrseq_device *pwrseq) 213 { 214 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 215 216 gpiod_set_value_cansleep(ctx->wlan_gpio, 0); 217 218 return 0; 219 } 220 221 static const struct pwrseq_unit_data pwrseq_qcom_wcn_wlan_unit_data = { 222 .name = "wlan-enable", 223 .deps = pwrseq_qcom_wcn_unit_deps, 224 .enable = pwrseq_qcom_wcn_wlan_enable, 225 .disable = pwrseq_qcom_wcn_wlan_disable, 226 }; 227 228 static const struct pwrseq_unit_data pwrseq_qcom_wcn6855_wlan_unit_data = { 229 .name = "wlan-enable", 230 .deps = pwrseq_qcom_wcn6855_unit_deps, 231 .enable = pwrseq_qcom_wcn_wlan_enable, 232 .disable = pwrseq_qcom_wcn_wlan_disable, 233 }; 234 235 static int pwrseq_qcom_wcn_pwup_delay(struct pwrseq_device *pwrseq) 236 { 237 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 238 239 if (ctx->pdata->pwup_delay_ms) 240 msleep(ctx->pdata->pwup_delay_ms); 241 242 return 0; 243 } 244 245 static int pwrseq_qcom_wcn6855_xo_clk_deassert(struct pwrseq_device *pwrseq) 246 { 247 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 248 249 if (ctx->xo_clk_gpio) { 250 usleep_range(2000, 5000); 251 gpiod_set_value_cansleep(ctx->xo_clk_gpio, 0); 252 } 253 254 return pwrseq_qcom_wcn_pwup_delay(pwrseq); 255 } 256 257 static const struct pwrseq_target_data pwrseq_qcom_wcn_bt_target_data = { 258 .name = "bluetooth", 259 .unit = &pwrseq_qcom_wcn_bt_unit_data, 260 .post_enable = pwrseq_qcom_wcn_pwup_delay, 261 }; 262 263 static const struct pwrseq_target_data pwrseq_qcom_wcn_wlan_target_data = { 264 .name = "wlan", 265 .unit = &pwrseq_qcom_wcn_wlan_unit_data, 266 .post_enable = pwrseq_qcom_wcn_pwup_delay, 267 }; 268 269 /* There are no separate BT and WLAN enablement pins */ 270 static const struct pwrseq_target_data pwrseq_qcom_wcn3990_bt_target_data = { 271 .name = "bluetooth", 272 .unit = &pwrseq_qcom_wcn3990_unit_data, 273 }; 274 275 static const struct pwrseq_target_data pwrseq_qcom_wcn3990_wlan_target_data = { 276 .name = "wlan", 277 .unit = &pwrseq_qcom_wcn3990_unit_data, 278 }; 279 280 static const struct pwrseq_target_data pwrseq_qcom_wcn6855_bt_target_data = { 281 .name = "bluetooth", 282 .unit = &pwrseq_qcom_wcn6855_bt_unit_data, 283 .post_enable = pwrseq_qcom_wcn6855_xo_clk_deassert, 284 }; 285 286 static const struct pwrseq_target_data pwrseq_qcom_wcn6855_wlan_target_data = { 287 .name = "wlan", 288 .unit = &pwrseq_qcom_wcn6855_wlan_unit_data, 289 .post_enable = pwrseq_qcom_wcn6855_xo_clk_deassert, 290 }; 291 292 static const struct pwrseq_target_data *pwrseq_qcom_wcn_targets[] = { 293 &pwrseq_qcom_wcn_bt_target_data, 294 &pwrseq_qcom_wcn_wlan_target_data, 295 NULL 296 }; 297 298 static const struct pwrseq_target_data *pwrseq_qcom_wcn3990_targets[] = { 299 &pwrseq_qcom_wcn3990_bt_target_data, 300 &pwrseq_qcom_wcn3990_wlan_target_data, 301 NULL 302 }; 303 304 static const struct pwrseq_target_data *pwrseq_qcom_wcn6855_targets[] = { 305 &pwrseq_qcom_wcn6855_bt_target_data, 306 &pwrseq_qcom_wcn6855_wlan_target_data, 307 NULL 308 }; 309 310 static const char *const pwrseq_qca6390_vregs[] = { 311 "vddio", 312 "vddaon", 313 "vddpmu", 314 "vddrfa0p95", 315 "vddrfa1p3", 316 "vddrfa1p9", 317 "vddpcie1p3", 318 "vddpcie1p9", 319 }; 320 321 static const struct pwrseq_qcom_wcn_pdata pwrseq_qca6390_of_data = { 322 .vregs = pwrseq_qca6390_vregs, 323 .num_vregs = ARRAY_SIZE(pwrseq_qca6390_vregs), 324 .pwup_delay_ms = 60, 325 .gpio_enable_delay_ms = 100, 326 .targets = pwrseq_qcom_wcn_targets, 327 }; 328 329 static const char *const pwrseq_wcn3990_vregs[] = { 330 /* vddio is handled separately */ 331 "vddxo", 332 "vddrf", 333 "vddch0", 334 "vddch1", 335 }; 336 337 static int pwrseq_qcom_wcn3990_match(struct pwrseq_device *pwrseq, 338 struct device *dev); 339 340 static const struct pwrseq_qcom_wcn_pdata pwrseq_wcn3990_of_data = { 341 .vregs = pwrseq_wcn3990_vregs, 342 .num_vregs = ARRAY_SIZE(pwrseq_wcn3990_vregs), 343 .pwup_delay_ms = 50, 344 .targets = pwrseq_qcom_wcn3990_targets, 345 .has_vddio = true, 346 .match = pwrseq_qcom_wcn3990_match, 347 }; 348 349 static const char *const pwrseq_wcn6750_vregs[] = { 350 "vddaon", 351 "vddasd", 352 "vddpmu", 353 "vddrfa0p8", 354 "vddrfa1p2", 355 "vddrfa1p7", 356 "vddrfa2p2", 357 }; 358 359 static const struct pwrseq_qcom_wcn_pdata pwrseq_wcn6750_of_data = { 360 .vregs = pwrseq_wcn6750_vregs, 361 .num_vregs = ARRAY_SIZE(pwrseq_wcn6750_vregs), 362 .pwup_delay_ms = 50, 363 .gpio_enable_delay_ms = 5, 364 .targets = pwrseq_qcom_wcn_targets, 365 }; 366 367 static const char *const pwrseq_wcn6855_vregs[] = { 368 "vddio", 369 "vddaon", 370 "vddpmu", 371 "vddpmumx", 372 "vddpmucx", 373 "vddrfa0p95", 374 "vddrfa1p3", 375 "vddrfa1p9", 376 "vddpcie1p3", 377 "vddpcie1p9", 378 }; 379 380 static const struct pwrseq_qcom_wcn_pdata pwrseq_wcn6855_of_data = { 381 .vregs = pwrseq_wcn6855_vregs, 382 .num_vregs = ARRAY_SIZE(pwrseq_wcn6855_vregs), 383 .pwup_delay_ms = 50, 384 .gpio_enable_delay_ms = 5, 385 .targets = pwrseq_qcom_wcn6855_targets, 386 }; 387 388 static const char *const pwrseq_wcn7850_vregs[] = { 389 "vdd", 390 "vddio", 391 "vddio1p2", 392 "vddaon", 393 "vdddig", 394 "vddrfa1p2", 395 "vddrfa1p8", 396 }; 397 398 static const struct pwrseq_qcom_wcn_pdata pwrseq_wcn7850_of_data = { 399 .vregs = pwrseq_wcn7850_vregs, 400 .num_vregs = ARRAY_SIZE(pwrseq_wcn7850_vregs), 401 .pwup_delay_ms = 50, 402 .targets = pwrseq_qcom_wcn_targets, 403 }; 404 405 static int pwrseq_qcom_wcn_match_regulator(struct pwrseq_device *pwrseq, 406 struct device *dev, 407 const char *name) 408 { 409 struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); 410 struct device_node *dev_node = dev->of_node; 411 412 /* 413 * The PMU supplies power to the Bluetooth and WLAN modules. both 414 * consume the PMU AON output so check the presence of the 415 * 'vddaon-supply' property and whether it leads us to the right 416 * device. 417 */ 418 if (!of_property_present(dev_node, name)) 419 return PWRSEQ_NO_MATCH; 420 421 struct device_node *reg_node __free(device_node) = 422 of_parse_phandle(dev_node, name, 0); 423 if (!reg_node) 424 return PWRSEQ_NO_MATCH; 425 426 /* 427 * `reg_node` is the PMU AON regulator, its parent is the `regulators` 428 * node and finally its grandparent is the PMU device node that we're 429 * looking for. 430 */ 431 if (!reg_node->parent || !reg_node->parent->parent || 432 reg_node->parent->parent != ctx->of_node) 433 return PWRSEQ_NO_MATCH; 434 435 return PWRSEQ_MATCH_OK; 436 } 437 438 static int pwrseq_qcom_wcn_match(struct pwrseq_device *pwrseq, 439 struct device *dev) 440 { 441 return pwrseq_qcom_wcn_match_regulator(pwrseq, dev, "vddaon-supply"); 442 } 443 444 static int pwrseq_qcom_wcn3990_match(struct pwrseq_device *pwrseq, 445 struct device *dev) 446 { 447 int ret; 448 449 /* BT device */ 450 ret = pwrseq_qcom_wcn_match_regulator(pwrseq, dev, "vddio-supply"); 451 if (ret == PWRSEQ_MATCH_OK) 452 return ret; 453 454 /* WiFi device match */ 455 return pwrseq_qcom_wcn_match_regulator(pwrseq, dev, "vdd-1.8-xo-supply"); 456 } 457 458 static int pwrseq_qcom_wcn_probe(struct platform_device *pdev) 459 { 460 struct device *dev = &pdev->dev; 461 struct pwrseq_qcom_wcn_ctx *ctx; 462 struct pwrseq_config config; 463 int i, ret; 464 465 ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL); 466 if (!ctx) 467 return -ENOMEM; 468 469 ctx->of_node = dev->of_node; 470 471 ctx->pdata = device_get_match_data(dev); 472 if (!ctx->pdata) 473 return dev_err_probe(dev, -ENODEV, 474 "Failed to obtain platform data\n"); 475 476 ctx->regs = devm_kcalloc(dev, ctx->pdata->num_vregs, 477 sizeof(*ctx->regs), GFP_KERNEL); 478 if (!ctx->regs) 479 return -ENOMEM; 480 481 for (i = 0; i < ctx->pdata->num_vregs; i++) 482 ctx->regs[i].supply = ctx->pdata->vregs[i]; 483 484 ret = devm_regulator_bulk_get(dev, ctx->pdata->num_vregs, ctx->regs); 485 if (ret < 0) 486 return dev_err_probe(dev, ret, 487 "Failed to get all regulators\n"); 488 489 if (ctx->pdata->has_vddio) { 490 ctx->vddio = devm_regulator_get(dev, "vddio"); 491 if (IS_ERR(ctx->vddio)) 492 return dev_err_probe(dev, PTR_ERR(ctx->vddio), "Failed to get VDDIO\n"); 493 } 494 495 ctx->bt_gpio = devm_gpiod_get_optional(dev, "bt-enable", GPIOD_OUT_LOW); 496 if (IS_ERR(ctx->bt_gpio)) 497 return dev_err_probe(dev, PTR_ERR(ctx->bt_gpio), 498 "Failed to get the Bluetooth enable GPIO\n"); 499 500 /* 501 * FIXME: This should actually be GPIOD_OUT_LOW, but doing so would 502 * cause the WLAN power to be toggled, resulting in PCIe link down. 503 * Since the PCIe controller driver is not handling link down currently, 504 * the device becomes unusable. So we need to keep this workaround until 505 * the link down handling is implemented in the controller driver. 506 */ 507 ctx->wlan_gpio = devm_gpiod_get_optional(dev, "wlan-enable", 508 GPIOD_ASIS); 509 if (IS_ERR(ctx->wlan_gpio)) 510 return dev_err_probe(dev, PTR_ERR(ctx->wlan_gpio), 511 "Failed to get the WLAN enable GPIO\n"); 512 513 ctx->xo_clk_gpio = devm_gpiod_get_optional(dev, "xo-clk", 514 GPIOD_OUT_LOW); 515 if (IS_ERR(ctx->xo_clk_gpio)) 516 return dev_err_probe(dev, PTR_ERR(ctx->xo_clk_gpio), 517 "Failed to get the XO_CLK GPIO\n"); 518 519 /* 520 * Set direction to output but keep the current value in order to not 521 * disable the WLAN module accidentally if it's already powered on. 522 */ 523 gpiod_direction_output(ctx->wlan_gpio, 524 gpiod_get_value_cansleep(ctx->wlan_gpio)); 525 526 ctx->clk = devm_clk_get_optional(dev, NULL); 527 if (IS_ERR(ctx->clk)) 528 return dev_err_probe(dev, PTR_ERR(ctx->clk), 529 "Failed to get the reference clock\n"); 530 531 memset(&config, 0, sizeof(config)); 532 533 config.parent = dev; 534 config.owner = THIS_MODULE; 535 config.drvdata = ctx; 536 config.match = ctx->pdata->match ? : pwrseq_qcom_wcn_match; 537 config.targets = ctx->pdata->targets; 538 539 ctx->pwrseq = devm_pwrseq_device_register(dev, &config); 540 if (IS_ERR(ctx->pwrseq)) 541 return dev_err_probe(dev, PTR_ERR(ctx->pwrseq), 542 "Failed to register the power sequencer\n"); 543 544 return 0; 545 } 546 547 static const struct of_device_id pwrseq_qcom_wcn_of_match[] = { 548 { 549 .compatible = "qcom,wcn3950-pmu", 550 .data = &pwrseq_wcn3990_of_data, 551 }, 552 { 553 .compatible = "qcom,wcn3988-pmu", 554 .data = &pwrseq_wcn3990_of_data, 555 }, 556 { 557 .compatible = "qcom,wcn3990-pmu", 558 .data = &pwrseq_wcn3990_of_data, 559 }, 560 { 561 .compatible = "qcom,wcn3991-pmu", 562 .data = &pwrseq_wcn3990_of_data, 563 }, 564 { 565 .compatible = "qcom,wcn3998-pmu", 566 .data = &pwrseq_wcn3990_of_data, 567 }, 568 { 569 .compatible = "qcom,qca6390-pmu", 570 .data = &pwrseq_qca6390_of_data, 571 }, 572 { 573 .compatible = "qcom,wcn6855-pmu", 574 .data = &pwrseq_wcn6855_of_data, 575 }, 576 { 577 .compatible = "qcom,wcn7850-pmu", 578 .data = &pwrseq_wcn7850_of_data, 579 }, 580 { 581 .compatible = "qcom,wcn6750-pmu", 582 .data = &pwrseq_wcn6750_of_data, 583 }, 584 { } 585 }; 586 MODULE_DEVICE_TABLE(of, pwrseq_qcom_wcn_of_match); 587 588 static struct platform_driver pwrseq_qcom_wcn_driver = { 589 .driver = { 590 .name = "pwrseq-qcom_wcn", 591 .of_match_table = pwrseq_qcom_wcn_of_match, 592 }, 593 .probe = pwrseq_qcom_wcn_probe, 594 }; 595 module_platform_driver(pwrseq_qcom_wcn_driver); 596 597 MODULE_AUTHOR("Bartosz Golaszewski <bartosz.golaszewski@linaro.org>"); 598 MODULE_DESCRIPTION("Qualcomm WCN PMU power sequencing driver"); 599 MODULE_LICENSE("GPL"); 600