11a11d88fSJerome Brunet // SPDX-License-Identifier: (GPL-2.0 OR MIT) 21a11d88fSJerome Brunet // 31a11d88fSJerome Brunet // Copyright (c) 2018 BayLibre, SAS. 41a11d88fSJerome Brunet // Author: Jerome Brunet <jbrunet@baylibre.com> 51a11d88fSJerome Brunet 61a11d88fSJerome Brunet #include <linux/clk.h> 71a11d88fSJerome Brunet #include <linux/module.h> 81a11d88fSJerome Brunet #include <linux/of_platform.h> 91a11d88fSJerome Brunet #include <linux/regmap.h> 101a11d88fSJerome Brunet #include <sound/soc.h> 111a11d88fSJerome Brunet 121a11d88fSJerome Brunet #include "axg-tdm-formatter.h" 131a11d88fSJerome Brunet 141a11d88fSJerome Brunet struct axg_tdm_formatter { 151a11d88fSJerome Brunet struct list_head list; 161a11d88fSJerome Brunet struct axg_tdm_stream *stream; 171a11d88fSJerome Brunet const struct axg_tdm_formatter_driver *drv; 181a11d88fSJerome Brunet struct clk *pclk; 191a11d88fSJerome Brunet struct clk *sclk; 201a11d88fSJerome Brunet struct clk *lrclk; 211a11d88fSJerome Brunet struct clk *sclk_sel; 221a11d88fSJerome Brunet struct clk *lrclk_sel; 231a11d88fSJerome Brunet bool enabled; 241a11d88fSJerome Brunet struct regmap *map; 251a11d88fSJerome Brunet }; 261a11d88fSJerome Brunet 271a11d88fSJerome Brunet int axg_tdm_formatter_set_channel_masks(struct regmap *map, 281a11d88fSJerome Brunet struct axg_tdm_stream *ts, 291a11d88fSJerome Brunet unsigned int offset) 301a11d88fSJerome Brunet { 311a11d88fSJerome Brunet unsigned int val, ch = ts->channels; 321a11d88fSJerome Brunet unsigned long mask; 331a11d88fSJerome Brunet int i, j; 341a11d88fSJerome Brunet 351a11d88fSJerome Brunet /* 361a11d88fSJerome Brunet * Distribute the channels of the stream over the available slots 371a11d88fSJerome Brunet * of each TDM lane 381a11d88fSJerome Brunet */ 391a11d88fSJerome Brunet for (i = 0; i < AXG_TDM_NUM_LANES; i++) { 401a11d88fSJerome Brunet val = 0; 411a11d88fSJerome Brunet mask = ts->mask[i]; 421a11d88fSJerome Brunet 431a11d88fSJerome Brunet for (j = find_first_bit(&mask, 32); 441a11d88fSJerome Brunet (j < 32) && ch; 451a11d88fSJerome Brunet j = find_next_bit(&mask, 32, j + 1)) { 461a11d88fSJerome Brunet val |= 1 << j; 471a11d88fSJerome Brunet ch -= 1; 481a11d88fSJerome Brunet } 491a11d88fSJerome Brunet 501a11d88fSJerome Brunet regmap_write(map, offset, val); 511a11d88fSJerome Brunet offset += regmap_get_reg_stride(map); 521a11d88fSJerome Brunet } 531a11d88fSJerome Brunet 541a11d88fSJerome Brunet /* 551a11d88fSJerome Brunet * If we still have channel left at the end of the process, it means 561a11d88fSJerome Brunet * the stream has more channels than we can accommodate and we should 571a11d88fSJerome Brunet * have caught this earlier. 581a11d88fSJerome Brunet */ 591a11d88fSJerome Brunet if (WARN_ON(ch != 0)) { 601a11d88fSJerome Brunet pr_err("channel mask error\n"); 611a11d88fSJerome Brunet return -EINVAL; 621a11d88fSJerome Brunet } 631a11d88fSJerome Brunet 641a11d88fSJerome Brunet return 0; 651a11d88fSJerome Brunet } 661a11d88fSJerome Brunet EXPORT_SYMBOL_GPL(axg_tdm_formatter_set_channel_masks); 671a11d88fSJerome Brunet 681a11d88fSJerome Brunet static int axg_tdm_formatter_enable(struct axg_tdm_formatter *formatter) 691a11d88fSJerome Brunet { 701a11d88fSJerome Brunet struct axg_tdm_stream *ts = formatter->stream; 711a11d88fSJerome Brunet bool invert = formatter->drv->invert_sclk; 721a11d88fSJerome Brunet int ret; 731a11d88fSJerome Brunet 741a11d88fSJerome Brunet /* Do nothing if the formatter is already enabled */ 751a11d88fSJerome Brunet if (formatter->enabled) 761a11d88fSJerome Brunet return 0; 771a11d88fSJerome Brunet 781a11d88fSJerome Brunet /* 791a11d88fSJerome Brunet * If sclk is inverted, invert it back and provide the inversion 801a11d88fSJerome Brunet * required by the formatter 811a11d88fSJerome Brunet */ 821a11d88fSJerome Brunet invert ^= axg_tdm_sclk_invert(ts->iface->fmt); 831a11d88fSJerome Brunet ret = clk_set_phase(formatter->sclk, invert ? 180 : 0); 841a11d88fSJerome Brunet if (ret) 851a11d88fSJerome Brunet return ret; 861a11d88fSJerome Brunet 871a11d88fSJerome Brunet /* Setup the stream parameter in the formatter */ 881a11d88fSJerome Brunet ret = formatter->drv->ops->prepare(formatter->map, formatter->stream); 891a11d88fSJerome Brunet if (ret) 901a11d88fSJerome Brunet return ret; 911a11d88fSJerome Brunet 921a11d88fSJerome Brunet /* Enable the signal clocks feeding the formatter */ 931a11d88fSJerome Brunet ret = clk_prepare_enable(formatter->sclk); 941a11d88fSJerome Brunet if (ret) 951a11d88fSJerome Brunet return ret; 961a11d88fSJerome Brunet 971a11d88fSJerome Brunet ret = clk_prepare_enable(formatter->lrclk); 981a11d88fSJerome Brunet if (ret) { 991a11d88fSJerome Brunet clk_disable_unprepare(formatter->sclk); 1001a11d88fSJerome Brunet return ret; 1011a11d88fSJerome Brunet } 1021a11d88fSJerome Brunet 1031a11d88fSJerome Brunet /* Finally, actually enable the formatter */ 1041a11d88fSJerome Brunet formatter->drv->ops->enable(formatter->map); 1051a11d88fSJerome Brunet formatter->enabled = true; 1061a11d88fSJerome Brunet 1071a11d88fSJerome Brunet return 0; 1081a11d88fSJerome Brunet } 1091a11d88fSJerome Brunet 1101a11d88fSJerome Brunet static void axg_tdm_formatter_disable(struct axg_tdm_formatter *formatter) 1111a11d88fSJerome Brunet { 1121a11d88fSJerome Brunet /* Do nothing if the formatter is already disabled */ 1131a11d88fSJerome Brunet if (!formatter->enabled) 1141a11d88fSJerome Brunet return; 1151a11d88fSJerome Brunet 1161a11d88fSJerome Brunet formatter->drv->ops->disable(formatter->map); 1171a11d88fSJerome Brunet clk_disable_unprepare(formatter->lrclk); 1181a11d88fSJerome Brunet clk_disable_unprepare(formatter->sclk); 1191a11d88fSJerome Brunet formatter->enabled = false; 1201a11d88fSJerome Brunet } 1211a11d88fSJerome Brunet 1221a11d88fSJerome Brunet static int axg_tdm_formatter_attach(struct axg_tdm_formatter *formatter) 1231a11d88fSJerome Brunet { 1241a11d88fSJerome Brunet struct axg_tdm_stream *ts = formatter->stream; 1251a11d88fSJerome Brunet int ret = 0; 1261a11d88fSJerome Brunet 1271a11d88fSJerome Brunet mutex_lock(&ts->lock); 1281a11d88fSJerome Brunet 1291a11d88fSJerome Brunet /* Catch up if the stream is already running when we attach */ 1301a11d88fSJerome Brunet if (ts->ready) { 1311a11d88fSJerome Brunet ret = axg_tdm_formatter_enable(formatter); 1321a11d88fSJerome Brunet if (ret) { 1331a11d88fSJerome Brunet pr_err("failed to enable formatter\n"); 1341a11d88fSJerome Brunet goto out; 1351a11d88fSJerome Brunet } 1361a11d88fSJerome Brunet } 1371a11d88fSJerome Brunet 1381a11d88fSJerome Brunet list_add_tail(&formatter->list, &ts->formatter_list); 1391a11d88fSJerome Brunet out: 1401a11d88fSJerome Brunet mutex_unlock(&ts->lock); 1411a11d88fSJerome Brunet return ret; 1421a11d88fSJerome Brunet } 1431a11d88fSJerome Brunet 1441a11d88fSJerome Brunet static void axg_tdm_formatter_dettach(struct axg_tdm_formatter *formatter) 1451a11d88fSJerome Brunet { 1461a11d88fSJerome Brunet struct axg_tdm_stream *ts = formatter->stream; 1471a11d88fSJerome Brunet 1481a11d88fSJerome Brunet mutex_lock(&ts->lock); 1491a11d88fSJerome Brunet list_del(&formatter->list); 1501a11d88fSJerome Brunet mutex_unlock(&ts->lock); 1511a11d88fSJerome Brunet 1521a11d88fSJerome Brunet axg_tdm_formatter_disable(formatter); 1531a11d88fSJerome Brunet } 1541a11d88fSJerome Brunet 1551a11d88fSJerome Brunet static int axg_tdm_formatter_power_up(struct axg_tdm_formatter *formatter, 1561a11d88fSJerome Brunet struct snd_soc_dapm_widget *w) 1571a11d88fSJerome Brunet { 1581a11d88fSJerome Brunet struct axg_tdm_stream *ts = formatter->drv->ops->get_stream(w); 1591a11d88fSJerome Brunet int ret; 1601a11d88fSJerome Brunet 1611a11d88fSJerome Brunet /* 1621a11d88fSJerome Brunet * If we don't get a stream at this stage, it would mean that the 1631a11d88fSJerome Brunet * widget is powering up but is not attached to any backend DAI. 1641a11d88fSJerome Brunet * It should not happen, ever ! 1651a11d88fSJerome Brunet */ 1661a11d88fSJerome Brunet if (WARN_ON(!ts)) 1671a11d88fSJerome Brunet return -ENODEV; 1681a11d88fSJerome Brunet 1691a11d88fSJerome Brunet /* Clock our device */ 1701a11d88fSJerome Brunet ret = clk_prepare_enable(formatter->pclk); 1711a11d88fSJerome Brunet if (ret) 1721a11d88fSJerome Brunet return ret; 1731a11d88fSJerome Brunet 1741a11d88fSJerome Brunet /* Reparent the bit clock to the TDM interface */ 1751a11d88fSJerome Brunet ret = clk_set_parent(formatter->sclk_sel, ts->iface->sclk); 1761a11d88fSJerome Brunet if (ret) 1771a11d88fSJerome Brunet goto disable_pclk; 1781a11d88fSJerome Brunet 1791a11d88fSJerome Brunet /* Reparent the sample clock to the TDM interface */ 1801a11d88fSJerome Brunet ret = clk_set_parent(formatter->lrclk_sel, ts->iface->lrclk); 1811a11d88fSJerome Brunet if (ret) 1821a11d88fSJerome Brunet goto disable_pclk; 1831a11d88fSJerome Brunet 1841a11d88fSJerome Brunet formatter->stream = ts; 1851a11d88fSJerome Brunet ret = axg_tdm_formatter_attach(formatter); 1861a11d88fSJerome Brunet if (ret) 1871a11d88fSJerome Brunet goto disable_pclk; 1881a11d88fSJerome Brunet 1891a11d88fSJerome Brunet return 0; 1901a11d88fSJerome Brunet 1911a11d88fSJerome Brunet disable_pclk: 1921a11d88fSJerome Brunet clk_disable_unprepare(formatter->pclk); 1931a11d88fSJerome Brunet return ret; 1941a11d88fSJerome Brunet } 1951a11d88fSJerome Brunet 1961a11d88fSJerome Brunet static void axg_tdm_formatter_power_down(struct axg_tdm_formatter *formatter) 1971a11d88fSJerome Brunet { 1981a11d88fSJerome Brunet axg_tdm_formatter_dettach(formatter); 1991a11d88fSJerome Brunet clk_disable_unprepare(formatter->pclk); 2001a11d88fSJerome Brunet formatter->stream = NULL; 2011a11d88fSJerome Brunet } 2021a11d88fSJerome Brunet 2031a11d88fSJerome Brunet int axg_tdm_formatter_event(struct snd_soc_dapm_widget *w, 2041a11d88fSJerome Brunet struct snd_kcontrol *control, 2051a11d88fSJerome Brunet int event) 2061a11d88fSJerome Brunet { 2071a11d88fSJerome Brunet struct snd_soc_component *c = snd_soc_dapm_to_component(w->dapm); 2081a11d88fSJerome Brunet struct axg_tdm_formatter *formatter = snd_soc_component_get_drvdata(c); 2091a11d88fSJerome Brunet int ret = 0; 2101a11d88fSJerome Brunet 2111a11d88fSJerome Brunet switch (event) { 2121a11d88fSJerome Brunet case SND_SOC_DAPM_PRE_PMU: 2131a11d88fSJerome Brunet ret = axg_tdm_formatter_power_up(formatter, w); 2141a11d88fSJerome Brunet break; 2151a11d88fSJerome Brunet 2161a11d88fSJerome Brunet case SND_SOC_DAPM_PRE_PMD: 2171a11d88fSJerome Brunet axg_tdm_formatter_power_down(formatter); 2181a11d88fSJerome Brunet break; 2191a11d88fSJerome Brunet 2201a11d88fSJerome Brunet default: 2211a11d88fSJerome Brunet dev_err(c->dev, "Unexpected event %d\n", event); 2221a11d88fSJerome Brunet return -EINVAL; 2231a11d88fSJerome Brunet } 2241a11d88fSJerome Brunet 2251a11d88fSJerome Brunet return ret; 2261a11d88fSJerome Brunet } 2271a11d88fSJerome Brunet EXPORT_SYMBOL_GPL(axg_tdm_formatter_event); 2281a11d88fSJerome Brunet 2291a11d88fSJerome Brunet int axg_tdm_formatter_probe(struct platform_device *pdev) 2301a11d88fSJerome Brunet { 2311a11d88fSJerome Brunet struct device *dev = &pdev->dev; 2321a11d88fSJerome Brunet const struct axg_tdm_formatter_driver *drv; 2331a11d88fSJerome Brunet struct axg_tdm_formatter *formatter; 2341a11d88fSJerome Brunet struct resource *res; 2351a11d88fSJerome Brunet void __iomem *regs; 2361a11d88fSJerome Brunet int ret; 2371a11d88fSJerome Brunet 2381a11d88fSJerome Brunet drv = of_device_get_match_data(dev); 2391a11d88fSJerome Brunet if (!drv) { 2401a11d88fSJerome Brunet dev_err(dev, "failed to match device\n"); 2411a11d88fSJerome Brunet return -ENODEV; 2421a11d88fSJerome Brunet } 2431a11d88fSJerome Brunet 2441a11d88fSJerome Brunet formatter = devm_kzalloc(dev, sizeof(*formatter), GFP_KERNEL); 2451a11d88fSJerome Brunet if (!formatter) 2461a11d88fSJerome Brunet return -ENOMEM; 2471a11d88fSJerome Brunet platform_set_drvdata(pdev, formatter); 2481a11d88fSJerome Brunet formatter->drv = drv; 2491a11d88fSJerome Brunet 2501a11d88fSJerome Brunet res = platform_get_resource(pdev, IORESOURCE_MEM, 0); 2511a11d88fSJerome Brunet regs = devm_ioremap_resource(dev, res); 2521a11d88fSJerome Brunet if (IS_ERR(regs)) 2531a11d88fSJerome Brunet return PTR_ERR(regs); 2541a11d88fSJerome Brunet 2551a11d88fSJerome Brunet formatter->map = devm_regmap_init_mmio(dev, regs, drv->regmap_cfg); 2561a11d88fSJerome Brunet if (IS_ERR(formatter->map)) { 2571a11d88fSJerome Brunet dev_err(dev, "failed to init regmap: %ld\n", 2581a11d88fSJerome Brunet PTR_ERR(formatter->map)); 2591a11d88fSJerome Brunet return PTR_ERR(formatter->map); 2601a11d88fSJerome Brunet } 2611a11d88fSJerome Brunet 2621a11d88fSJerome Brunet /* Peripharal clock */ 2631a11d88fSJerome Brunet formatter->pclk = devm_clk_get(dev, "pclk"); 2641a11d88fSJerome Brunet if (IS_ERR(formatter->pclk)) { 2651a11d88fSJerome Brunet ret = PTR_ERR(formatter->pclk); 2661a11d88fSJerome Brunet if (ret != -EPROBE_DEFER) 2671a11d88fSJerome Brunet dev_err(dev, "failed to get pclk: %d\n", ret); 2681a11d88fSJerome Brunet return ret; 2691a11d88fSJerome Brunet } 2701a11d88fSJerome Brunet 2711a11d88fSJerome Brunet /* Formatter bit clock */ 2721a11d88fSJerome Brunet formatter->sclk = devm_clk_get(dev, "sclk"); 2731a11d88fSJerome Brunet if (IS_ERR(formatter->sclk)) { 2741a11d88fSJerome Brunet ret = PTR_ERR(formatter->sclk); 2751a11d88fSJerome Brunet if (ret != -EPROBE_DEFER) 2761a11d88fSJerome Brunet dev_err(dev, "failed to get sclk: %d\n", ret); 2771a11d88fSJerome Brunet return ret; 2781a11d88fSJerome Brunet } 2791a11d88fSJerome Brunet 2801a11d88fSJerome Brunet /* Formatter sample clock */ 2811a11d88fSJerome Brunet formatter->lrclk = devm_clk_get(dev, "lrclk"); 2821a11d88fSJerome Brunet if (IS_ERR(formatter->lrclk)) { 2831a11d88fSJerome Brunet ret = PTR_ERR(formatter->lrclk); 2841a11d88fSJerome Brunet if (ret != -EPROBE_DEFER) 2851a11d88fSJerome Brunet dev_err(dev, "failed to get lrclk: %d\n", ret); 2861a11d88fSJerome Brunet return ret; 2871a11d88fSJerome Brunet } 2881a11d88fSJerome Brunet 2891a11d88fSJerome Brunet /* Formatter bit clock input multiplexer */ 2901a11d88fSJerome Brunet formatter->sclk_sel = devm_clk_get(dev, "sclk_sel"); 2911a11d88fSJerome Brunet if (IS_ERR(formatter->sclk_sel)) { 2921a11d88fSJerome Brunet ret = PTR_ERR(formatter->sclk_sel); 2931a11d88fSJerome Brunet if (ret != -EPROBE_DEFER) 2941a11d88fSJerome Brunet dev_err(dev, "failed to get sclk_sel: %d\n", ret); 2951a11d88fSJerome Brunet return ret; 2961a11d88fSJerome Brunet } 2971a11d88fSJerome Brunet 2981a11d88fSJerome Brunet /* Formatter sample clock input multiplexer */ 2991a11d88fSJerome Brunet formatter->lrclk_sel = devm_clk_get(dev, "lrclk_sel"); 3001a11d88fSJerome Brunet if (IS_ERR(formatter->lrclk_sel)) { 3011a11d88fSJerome Brunet ret = PTR_ERR(formatter->lrclk_sel); 3021a11d88fSJerome Brunet if (ret != -EPROBE_DEFER) 3031a11d88fSJerome Brunet dev_err(dev, "failed to get lrclk_sel: %d\n", ret); 3041a11d88fSJerome Brunet return ret; 3051a11d88fSJerome Brunet } 3061a11d88fSJerome Brunet 3071a11d88fSJerome Brunet return devm_snd_soc_register_component(dev, drv->component_drv, 3081a11d88fSJerome Brunet NULL, 0); 3091a11d88fSJerome Brunet } 3101a11d88fSJerome Brunet EXPORT_SYMBOL_GPL(axg_tdm_formatter_probe); 3111a11d88fSJerome Brunet 3121a11d88fSJerome Brunet int axg_tdm_stream_start(struct axg_tdm_stream *ts) 3131a11d88fSJerome Brunet { 3141a11d88fSJerome Brunet struct axg_tdm_formatter *formatter; 3151a11d88fSJerome Brunet int ret = 0; 3161a11d88fSJerome Brunet 3171a11d88fSJerome Brunet mutex_lock(&ts->lock); 3181a11d88fSJerome Brunet ts->ready = true; 3191a11d88fSJerome Brunet 3201a11d88fSJerome Brunet /* Start all the formatters attached to the stream */ 3211a11d88fSJerome Brunet list_for_each_entry(formatter, &ts->formatter_list, list) { 3221a11d88fSJerome Brunet ret = axg_tdm_formatter_enable(formatter); 3231a11d88fSJerome Brunet if (ret) { 3241a11d88fSJerome Brunet pr_err("failed to start tdm stream\n"); 3251a11d88fSJerome Brunet goto out; 3261a11d88fSJerome Brunet } 3271a11d88fSJerome Brunet } 3281a11d88fSJerome Brunet 3291a11d88fSJerome Brunet out: 3301a11d88fSJerome Brunet mutex_unlock(&ts->lock); 3311a11d88fSJerome Brunet return ret; 3321a11d88fSJerome Brunet } 3331a11d88fSJerome Brunet EXPORT_SYMBOL_GPL(axg_tdm_stream_start); 3341a11d88fSJerome Brunet 3351a11d88fSJerome Brunet void axg_tdm_stream_stop(struct axg_tdm_stream *ts) 3361a11d88fSJerome Brunet { 3371a11d88fSJerome Brunet struct axg_tdm_formatter *formatter; 3381a11d88fSJerome Brunet 3391a11d88fSJerome Brunet mutex_lock(&ts->lock); 3401a11d88fSJerome Brunet ts->ready = false; 3411a11d88fSJerome Brunet 3421a11d88fSJerome Brunet /* Stop all the formatters attached to the stream */ 3431a11d88fSJerome Brunet list_for_each_entry(formatter, &ts->formatter_list, list) { 3441a11d88fSJerome Brunet axg_tdm_formatter_disable(formatter); 3451a11d88fSJerome Brunet } 3461a11d88fSJerome Brunet 3471a11d88fSJerome Brunet mutex_unlock(&ts->lock); 3481a11d88fSJerome Brunet } 3491a11d88fSJerome Brunet EXPORT_SYMBOL_GPL(axg_tdm_stream_stop); 3501a11d88fSJerome Brunet 3511a11d88fSJerome Brunet struct axg_tdm_stream *axg_tdm_stream_alloc(struct axg_tdm_iface *iface) 3521a11d88fSJerome Brunet { 3531a11d88fSJerome Brunet struct axg_tdm_stream *ts; 3541a11d88fSJerome Brunet 3551a11d88fSJerome Brunet ts = kzalloc(sizeof(*ts), GFP_KERNEL); 3561a11d88fSJerome Brunet if (ts) { 3571a11d88fSJerome Brunet INIT_LIST_HEAD(&ts->formatter_list); 3581a11d88fSJerome Brunet mutex_init(&ts->lock); 3591a11d88fSJerome Brunet ts->iface = iface; 3601a11d88fSJerome Brunet } 3611a11d88fSJerome Brunet 3621a11d88fSJerome Brunet return ts; 3631a11d88fSJerome Brunet } 3641a11d88fSJerome Brunet EXPORT_SYMBOL_GPL(axg_tdm_stream_alloc); 3651a11d88fSJerome Brunet 3661a11d88fSJerome Brunet void axg_tdm_stream_free(struct axg_tdm_stream *ts) 3671a11d88fSJerome Brunet { 3681a11d88fSJerome Brunet /* 3691a11d88fSJerome Brunet * If the list is not empty, it would mean that one of the formatter 3701a11d88fSJerome Brunet * widget is still powered and attached to the interface while we 3711a11d88fSJerome Brunet * we are removing the TDM DAI. It should not be possible 3721a11d88fSJerome Brunet */ 3731a11d88fSJerome Brunet WARN_ON(!list_empty(&ts->formatter_list)); 3741a11d88fSJerome Brunet mutex_destroy(&ts->lock); 3751a11d88fSJerome Brunet kfree(ts); 3761a11d88fSJerome Brunet } 3771a11d88fSJerome Brunet EXPORT_SYMBOL_GPL(axg_tdm_stream_free); 3781a11d88fSJerome Brunet 3791a11d88fSJerome Brunet MODULE_DESCRIPTION("Amlogic AXG TDM formatter driver"); 3801a11d88fSJerome Brunet MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>"); 3811a11d88fSJerome Brunet MODULE_LICENSE("GPL v2"); 382