12bb70056SOleksij Rempel // SPDX-License-Identifier: GPL-2.0 22bb70056SOleksij Rempel /* 32bb70056SOleksij Rempel * Copyright (c) 2018 Pengutronix, Oleksij Rempel <o.rempel@pengutronix.de> 42bb70056SOleksij Rempel */ 52bb70056SOleksij Rempel 62bb70056SOleksij Rempel #include <linux/clk.h> 72bb70056SOleksij Rempel #include <linux/interrupt.h> 82bb70056SOleksij Rempel #include <linux/io.h> 92bb70056SOleksij Rempel #include <linux/kernel.h> 102bb70056SOleksij Rempel #include <linux/mailbox_controller.h> 112bb70056SOleksij Rempel #include <linux/module.h> 122bb70056SOleksij Rempel #include <linux/of_device.h> 132bb70056SOleksij Rempel #include <linux/slab.h> 142bb70056SOleksij Rempel 152bb70056SOleksij Rempel /* Transmit Register */ 162bb70056SOleksij Rempel #define IMX_MU_xTRn(x) (0x00 + 4 * (x)) 172bb70056SOleksij Rempel /* Receive Register */ 182bb70056SOleksij Rempel #define IMX_MU_xRRn(x) (0x10 + 4 * (x)) 192bb70056SOleksij Rempel /* Status Register */ 202bb70056SOleksij Rempel #define IMX_MU_xSR 0x20 212bb70056SOleksij Rempel #define IMX_MU_xSR_GIPn(x) BIT(28 + (3 - (x))) 222bb70056SOleksij Rempel #define IMX_MU_xSR_RFn(x) BIT(24 + (3 - (x))) 232bb70056SOleksij Rempel #define IMX_MU_xSR_TEn(x) BIT(20 + (3 - (x))) 242bb70056SOleksij Rempel #define IMX_MU_xSR_BRDIP BIT(9) 252bb70056SOleksij Rempel 262bb70056SOleksij Rempel /* Control Register */ 272bb70056SOleksij Rempel #define IMX_MU_xCR 0x24 282bb70056SOleksij Rempel /* General Purpose Interrupt Enable */ 292bb70056SOleksij Rempel #define IMX_MU_xCR_GIEn(x) BIT(28 + (3 - (x))) 302bb70056SOleksij Rempel /* Receive Interrupt Enable */ 312bb70056SOleksij Rempel #define IMX_MU_xCR_RIEn(x) BIT(24 + (3 - (x))) 322bb70056SOleksij Rempel /* Transmit Interrupt Enable */ 332bb70056SOleksij Rempel #define IMX_MU_xCR_TIEn(x) BIT(20 + (3 - (x))) 342bb70056SOleksij Rempel /* General Purpose Interrupt Request */ 352bb70056SOleksij Rempel #define IMX_MU_xCR_GIRn(x) BIT(16 + (3 - (x))) 362bb70056SOleksij Rempel 372bb70056SOleksij Rempel #define IMX_MU_CHANS 16 382bb70056SOleksij Rempel #define IMX_MU_CHAN_NAME_SIZE 20 392bb70056SOleksij Rempel 402bb70056SOleksij Rempel enum imx_mu_chan_type { 412bb70056SOleksij Rempel IMX_MU_TYPE_TX, /* Tx */ 422bb70056SOleksij Rempel IMX_MU_TYPE_RX, /* Rx */ 432bb70056SOleksij Rempel IMX_MU_TYPE_TXDB, /* Tx doorbell */ 442bb70056SOleksij Rempel IMX_MU_TYPE_RXDB, /* Rx doorbell */ 452bb70056SOleksij Rempel }; 462bb70056SOleksij Rempel 472bb70056SOleksij Rempel struct imx_mu_con_priv { 482bb70056SOleksij Rempel unsigned int idx; 492bb70056SOleksij Rempel char irq_desc[IMX_MU_CHAN_NAME_SIZE]; 502bb70056SOleksij Rempel enum imx_mu_chan_type type; 512bb70056SOleksij Rempel struct mbox_chan *chan; 522bb70056SOleksij Rempel struct tasklet_struct txdb_tasklet; 532bb70056SOleksij Rempel }; 542bb70056SOleksij Rempel 552bb70056SOleksij Rempel struct imx_mu_priv { 562bb70056SOleksij Rempel struct device *dev; 572bb70056SOleksij Rempel void __iomem *base; 582bb70056SOleksij Rempel spinlock_t xcr_lock; /* control register lock */ 592bb70056SOleksij Rempel 602bb70056SOleksij Rempel struct mbox_controller mbox; 612bb70056SOleksij Rempel struct mbox_chan mbox_chans[IMX_MU_CHANS]; 622bb70056SOleksij Rempel 632bb70056SOleksij Rempel struct imx_mu_con_priv con_priv[IMX_MU_CHANS]; 642bb70056SOleksij Rempel struct clk *clk; 652bb70056SOleksij Rempel int irq; 662bb70056SOleksij Rempel 672bb70056SOleksij Rempel bool side_b; 682bb70056SOleksij Rempel }; 692bb70056SOleksij Rempel 702bb70056SOleksij Rempel static struct imx_mu_priv *to_imx_mu_priv(struct mbox_controller *mbox) 712bb70056SOleksij Rempel { 722bb70056SOleksij Rempel return container_of(mbox, struct imx_mu_priv, mbox); 732bb70056SOleksij Rempel } 742bb70056SOleksij Rempel 752bb70056SOleksij Rempel static void imx_mu_write(struct imx_mu_priv *priv, u32 val, u32 offs) 762bb70056SOleksij Rempel { 772bb70056SOleksij Rempel iowrite32(val, priv->base + offs); 782bb70056SOleksij Rempel } 792bb70056SOleksij Rempel 802bb70056SOleksij Rempel static u32 imx_mu_read(struct imx_mu_priv *priv, u32 offs) 812bb70056SOleksij Rempel { 822bb70056SOleksij Rempel return ioread32(priv->base + offs); 832bb70056SOleksij Rempel } 842bb70056SOleksij Rempel 852bb70056SOleksij Rempel static u32 imx_mu_xcr_rmw(struct imx_mu_priv *priv, u32 set, u32 clr) 862bb70056SOleksij Rempel { 872bb70056SOleksij Rempel unsigned long flags; 882bb70056SOleksij Rempel u32 val; 892bb70056SOleksij Rempel 902bb70056SOleksij Rempel spin_lock_irqsave(&priv->xcr_lock, flags); 912bb70056SOleksij Rempel val = imx_mu_read(priv, IMX_MU_xCR); 922bb70056SOleksij Rempel val &= ~clr; 932bb70056SOleksij Rempel val |= set; 942bb70056SOleksij Rempel imx_mu_write(priv, val, IMX_MU_xCR); 952bb70056SOleksij Rempel spin_unlock_irqrestore(&priv->xcr_lock, flags); 962bb70056SOleksij Rempel 972bb70056SOleksij Rempel return val; 982bb70056SOleksij Rempel } 992bb70056SOleksij Rempel 1002bb70056SOleksij Rempel static void imx_mu_txdb_tasklet(unsigned long data) 1012bb70056SOleksij Rempel { 1022bb70056SOleksij Rempel struct imx_mu_con_priv *cp = (struct imx_mu_con_priv *)data; 1032bb70056SOleksij Rempel 1042bb70056SOleksij Rempel mbox_chan_txdone(cp->chan, 0); 1052bb70056SOleksij Rempel } 1062bb70056SOleksij Rempel 1072bb70056SOleksij Rempel static irqreturn_t imx_mu_isr(int irq, void *p) 1082bb70056SOleksij Rempel { 1092bb70056SOleksij Rempel struct mbox_chan *chan = p; 1102bb70056SOleksij Rempel struct imx_mu_priv *priv = to_imx_mu_priv(chan->mbox); 1112bb70056SOleksij Rempel struct imx_mu_con_priv *cp = chan->con_priv; 1122bb70056SOleksij Rempel u32 val, ctrl, dat; 1132bb70056SOleksij Rempel 1142bb70056SOleksij Rempel ctrl = imx_mu_read(priv, IMX_MU_xCR); 1152bb70056SOleksij Rempel val = imx_mu_read(priv, IMX_MU_xSR); 1162bb70056SOleksij Rempel 1172bb70056SOleksij Rempel switch (cp->type) { 1182bb70056SOleksij Rempel case IMX_MU_TYPE_TX: 1192bb70056SOleksij Rempel val &= IMX_MU_xSR_TEn(cp->idx) & 1202bb70056SOleksij Rempel (ctrl & IMX_MU_xCR_TIEn(cp->idx)); 1212bb70056SOleksij Rempel break; 1222bb70056SOleksij Rempel case IMX_MU_TYPE_RX: 1232bb70056SOleksij Rempel val &= IMX_MU_xSR_RFn(cp->idx) & 1242bb70056SOleksij Rempel (ctrl & IMX_MU_xCR_RIEn(cp->idx)); 1252bb70056SOleksij Rempel break; 1262bb70056SOleksij Rempel case IMX_MU_TYPE_RXDB: 1272bb70056SOleksij Rempel val &= IMX_MU_xSR_GIPn(cp->idx) & 1282bb70056SOleksij Rempel (ctrl & IMX_MU_xCR_GIEn(cp->idx)); 1292bb70056SOleksij Rempel break; 1302bb70056SOleksij Rempel default: 1312bb70056SOleksij Rempel break; 1322bb70056SOleksij Rempel } 1332bb70056SOleksij Rempel 1342bb70056SOleksij Rempel if (!val) 1352bb70056SOleksij Rempel return IRQ_NONE; 1362bb70056SOleksij Rempel 1372bb70056SOleksij Rempel if (val == IMX_MU_xSR_TEn(cp->idx)) { 1382bb70056SOleksij Rempel imx_mu_xcr_rmw(priv, 0, IMX_MU_xCR_TIEn(cp->idx)); 1392bb70056SOleksij Rempel mbox_chan_txdone(chan, 0); 1402bb70056SOleksij Rempel } else if (val == IMX_MU_xSR_RFn(cp->idx)) { 1412bb70056SOleksij Rempel dat = imx_mu_read(priv, IMX_MU_xRRn(cp->idx)); 1422bb70056SOleksij Rempel mbox_chan_received_data(chan, (void *)&dat); 1432bb70056SOleksij Rempel } else if (val == IMX_MU_xSR_GIPn(cp->idx)) { 1442bb70056SOleksij Rempel imx_mu_write(priv, IMX_MU_xSR_GIPn(cp->idx), IMX_MU_xSR); 1452bb70056SOleksij Rempel mbox_chan_received_data(chan, NULL); 1462bb70056SOleksij Rempel } else { 1472bb70056SOleksij Rempel dev_warn_ratelimited(priv->dev, "Not handled interrupt\n"); 1482bb70056SOleksij Rempel return IRQ_NONE; 1492bb70056SOleksij Rempel } 1502bb70056SOleksij Rempel 1512bb70056SOleksij Rempel return IRQ_HANDLED; 1522bb70056SOleksij Rempel } 1532bb70056SOleksij Rempel 1542bb70056SOleksij Rempel static int imx_mu_send_data(struct mbox_chan *chan, void *data) 1552bb70056SOleksij Rempel { 1562bb70056SOleksij Rempel struct imx_mu_priv *priv = to_imx_mu_priv(chan->mbox); 1572bb70056SOleksij Rempel struct imx_mu_con_priv *cp = chan->con_priv; 1582bb70056SOleksij Rempel u32 *arg = data; 1592bb70056SOleksij Rempel 1602bb70056SOleksij Rempel switch (cp->type) { 1612bb70056SOleksij Rempel case IMX_MU_TYPE_TX: 1622bb70056SOleksij Rempel imx_mu_write(priv, *arg, IMX_MU_xTRn(cp->idx)); 1632bb70056SOleksij Rempel imx_mu_xcr_rmw(priv, IMX_MU_xCR_TIEn(cp->idx), 0); 1642bb70056SOleksij Rempel break; 1652bb70056SOleksij Rempel case IMX_MU_TYPE_TXDB: 1662bb70056SOleksij Rempel imx_mu_xcr_rmw(priv, IMX_MU_xCR_GIRn(cp->idx), 0); 1672bb70056SOleksij Rempel tasklet_schedule(&cp->txdb_tasklet); 1682bb70056SOleksij Rempel break; 1692bb70056SOleksij Rempel default: 1702bb70056SOleksij Rempel dev_warn_ratelimited(priv->dev, "Send data on wrong channel type: %d\n", cp->type); 1712bb70056SOleksij Rempel return -EINVAL; 1722bb70056SOleksij Rempel } 1732bb70056SOleksij Rempel 1742bb70056SOleksij Rempel return 0; 1752bb70056SOleksij Rempel } 1762bb70056SOleksij Rempel 1772bb70056SOleksij Rempel static int imx_mu_startup(struct mbox_chan *chan) 1782bb70056SOleksij Rempel { 1792bb70056SOleksij Rempel struct imx_mu_priv *priv = to_imx_mu_priv(chan->mbox); 1802bb70056SOleksij Rempel struct imx_mu_con_priv *cp = chan->con_priv; 1812bb70056SOleksij Rempel int ret; 1822bb70056SOleksij Rempel 1832bb70056SOleksij Rempel if (cp->type == IMX_MU_TYPE_TXDB) { 1842bb70056SOleksij Rempel /* Tx doorbell don't have ACK support */ 1852bb70056SOleksij Rempel tasklet_init(&cp->txdb_tasklet, imx_mu_txdb_tasklet, 1862bb70056SOleksij Rempel (unsigned long)cp); 1872bb70056SOleksij Rempel return 0; 1882bb70056SOleksij Rempel } 1892bb70056SOleksij Rempel 1902bb70056SOleksij Rempel ret = request_irq(priv->irq, imx_mu_isr, IRQF_SHARED, cp->irq_desc, 1912bb70056SOleksij Rempel chan); 1922bb70056SOleksij Rempel if (ret) { 1932bb70056SOleksij Rempel dev_err(priv->dev, 1942bb70056SOleksij Rempel "Unable to acquire IRQ %d\n", priv->irq); 1952bb70056SOleksij Rempel return ret; 1962bb70056SOleksij Rempel } 1972bb70056SOleksij Rempel 1982bb70056SOleksij Rempel switch (cp->type) { 1992bb70056SOleksij Rempel case IMX_MU_TYPE_RX: 2002bb70056SOleksij Rempel imx_mu_xcr_rmw(priv, IMX_MU_xCR_RIEn(cp->idx), 0); 2012bb70056SOleksij Rempel break; 2022bb70056SOleksij Rempel case IMX_MU_TYPE_RXDB: 2032bb70056SOleksij Rempel imx_mu_xcr_rmw(priv, IMX_MU_xCR_GIEn(cp->idx), 0); 2042bb70056SOleksij Rempel break; 2052bb70056SOleksij Rempel default: 2062bb70056SOleksij Rempel break; 2072bb70056SOleksij Rempel } 2082bb70056SOleksij Rempel 2092bb70056SOleksij Rempel return 0; 2102bb70056SOleksij Rempel } 2112bb70056SOleksij Rempel 2122bb70056SOleksij Rempel static void imx_mu_shutdown(struct mbox_chan *chan) 2132bb70056SOleksij Rempel { 2142bb70056SOleksij Rempel struct imx_mu_priv *priv = to_imx_mu_priv(chan->mbox); 2152bb70056SOleksij Rempel struct imx_mu_con_priv *cp = chan->con_priv; 2162bb70056SOleksij Rempel 2172bb70056SOleksij Rempel if (cp->type == IMX_MU_TYPE_TXDB) 2182bb70056SOleksij Rempel tasklet_kill(&cp->txdb_tasklet); 2192bb70056SOleksij Rempel 2202bb70056SOleksij Rempel imx_mu_xcr_rmw(priv, 0, 2212bb70056SOleksij Rempel IMX_MU_xCR_TIEn(cp->idx) | IMX_MU_xCR_RIEn(cp->idx)); 2222bb70056SOleksij Rempel 2232bb70056SOleksij Rempel free_irq(priv->irq, chan); 2242bb70056SOleksij Rempel } 2252bb70056SOleksij Rempel 2262bb70056SOleksij Rempel static const struct mbox_chan_ops imx_mu_ops = { 2272bb70056SOleksij Rempel .send_data = imx_mu_send_data, 2282bb70056SOleksij Rempel .startup = imx_mu_startup, 2292bb70056SOleksij Rempel .shutdown = imx_mu_shutdown, 2302bb70056SOleksij Rempel }; 2312bb70056SOleksij Rempel 2322bb70056SOleksij Rempel static struct mbox_chan * imx_mu_xlate(struct mbox_controller *mbox, 2332bb70056SOleksij Rempel const struct of_phandle_args *sp) 2342bb70056SOleksij Rempel { 2352bb70056SOleksij Rempel u32 type, idx, chan; 2362bb70056SOleksij Rempel 2372bb70056SOleksij Rempel if (sp->args_count != 2) { 2382bb70056SOleksij Rempel dev_err(mbox->dev, "Invalid argument count %d\n", sp->args_count); 2392bb70056SOleksij Rempel return ERR_PTR(-EINVAL); 2402bb70056SOleksij Rempel } 2412bb70056SOleksij Rempel 2422bb70056SOleksij Rempel type = sp->args[0]; /* channel type */ 2432bb70056SOleksij Rempel idx = sp->args[1]; /* index */ 2442bb70056SOleksij Rempel chan = type * 4 + idx; 2452bb70056SOleksij Rempel 2462bb70056SOleksij Rempel if (chan >= mbox->num_chans) { 2472bb70056SOleksij Rempel dev_err(mbox->dev, "Not supported channel number: %d. (type: %d, idx: %d)\n", chan, type, idx); 2482bb70056SOleksij Rempel return ERR_PTR(-EINVAL); 2492bb70056SOleksij Rempel } 2502bb70056SOleksij Rempel 2512bb70056SOleksij Rempel return &mbox->chans[chan]; 2522bb70056SOleksij Rempel } 2532bb70056SOleksij Rempel 2542bb70056SOleksij Rempel static void imx_mu_init_generic(struct imx_mu_priv *priv) 2552bb70056SOleksij Rempel { 2562bb70056SOleksij Rempel if (priv->side_b) 2572bb70056SOleksij Rempel return; 2582bb70056SOleksij Rempel 2592bb70056SOleksij Rempel /* Set default MU configuration */ 2602bb70056SOleksij Rempel imx_mu_write(priv, 0, IMX_MU_xCR); 2612bb70056SOleksij Rempel } 2622bb70056SOleksij Rempel 2632bb70056SOleksij Rempel static int imx_mu_probe(struct platform_device *pdev) 2642bb70056SOleksij Rempel { 2652bb70056SOleksij Rempel struct device *dev = &pdev->dev; 2662bb70056SOleksij Rempel struct device_node *np = dev->of_node; 2672bb70056SOleksij Rempel struct resource *iomem; 2682bb70056SOleksij Rempel struct imx_mu_priv *priv; 2692bb70056SOleksij Rempel unsigned int i; 2702bb70056SOleksij Rempel int ret; 2712bb70056SOleksij Rempel 2722bb70056SOleksij Rempel priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); 2732bb70056SOleksij Rempel if (!priv) 2742bb70056SOleksij Rempel return -ENOMEM; 2752bb70056SOleksij Rempel 2762bb70056SOleksij Rempel priv->dev = dev; 2772bb70056SOleksij Rempel 2782bb70056SOleksij Rempel iomem = platform_get_resource(pdev, IORESOURCE_MEM, 0); 2792bb70056SOleksij Rempel priv->base = devm_ioremap_resource(&pdev->dev, iomem); 2802bb70056SOleksij Rempel if (IS_ERR(priv->base)) 2812bb70056SOleksij Rempel return PTR_ERR(priv->base); 2822bb70056SOleksij Rempel 2832bb70056SOleksij Rempel priv->irq = platform_get_irq(pdev, 0); 2842bb70056SOleksij Rempel if (priv->irq < 0) 2852bb70056SOleksij Rempel return priv->irq; 2862bb70056SOleksij Rempel 2872bb70056SOleksij Rempel priv->clk = devm_clk_get(dev, NULL); 2882bb70056SOleksij Rempel if (IS_ERR(priv->clk)) { 2892bb70056SOleksij Rempel if (PTR_ERR(priv->clk) != -ENOENT) 2902bb70056SOleksij Rempel return PTR_ERR(priv->clk); 2912bb70056SOleksij Rempel 2922bb70056SOleksij Rempel priv->clk = NULL; 2932bb70056SOleksij Rempel } 2942bb70056SOleksij Rempel 2952bb70056SOleksij Rempel ret = clk_prepare_enable(priv->clk); 2962bb70056SOleksij Rempel if (ret) { 2972bb70056SOleksij Rempel dev_err(dev, "Failed to enable clock\n"); 2982bb70056SOleksij Rempel return ret; 2992bb70056SOleksij Rempel } 3002bb70056SOleksij Rempel 3012bb70056SOleksij Rempel for (i = 0; i < IMX_MU_CHANS; i++) { 3022bb70056SOleksij Rempel struct imx_mu_con_priv *cp = &priv->con_priv[i]; 3032bb70056SOleksij Rempel 3042bb70056SOleksij Rempel cp->idx = i % 4; 3052bb70056SOleksij Rempel cp->type = i >> 2; 3062bb70056SOleksij Rempel cp->chan = &priv->mbox_chans[i]; 3072bb70056SOleksij Rempel priv->mbox_chans[i].con_priv = cp; 3082bb70056SOleksij Rempel snprintf(cp->irq_desc, sizeof(cp->irq_desc), 3092bb70056SOleksij Rempel "imx_mu_chan[%i-%i]", cp->type, cp->idx); 3102bb70056SOleksij Rempel } 3112bb70056SOleksij Rempel 3122bb70056SOleksij Rempel priv->side_b = of_property_read_bool(np, "fsl,mu-side-b"); 3132bb70056SOleksij Rempel 3142bb70056SOleksij Rempel spin_lock_init(&priv->xcr_lock); 3152bb70056SOleksij Rempel 3162bb70056SOleksij Rempel priv->mbox.dev = dev; 3172bb70056SOleksij Rempel priv->mbox.ops = &imx_mu_ops; 3182bb70056SOleksij Rempel priv->mbox.chans = priv->mbox_chans; 3192bb70056SOleksij Rempel priv->mbox.num_chans = IMX_MU_CHANS; 3202bb70056SOleksij Rempel priv->mbox.of_xlate = imx_mu_xlate; 3212bb70056SOleksij Rempel priv->mbox.txdone_irq = true; 3222bb70056SOleksij Rempel 3232bb70056SOleksij Rempel platform_set_drvdata(pdev, priv); 3242bb70056SOleksij Rempel 3252bb70056SOleksij Rempel imx_mu_init_generic(priv); 3262bb70056SOleksij Rempel 3274013286cSThierry Reding return devm_mbox_controller_register(dev, &priv->mbox); 3282bb70056SOleksij Rempel } 3292bb70056SOleksij Rempel 3302bb70056SOleksij Rempel static int imx_mu_remove(struct platform_device *pdev) 3312bb70056SOleksij Rempel { 3322bb70056SOleksij Rempel struct imx_mu_priv *priv = platform_get_drvdata(pdev); 3332bb70056SOleksij Rempel 3342bb70056SOleksij Rempel clk_disable_unprepare(priv->clk); 3352bb70056SOleksij Rempel 3362bb70056SOleksij Rempel return 0; 3372bb70056SOleksij Rempel } 3382bb70056SOleksij Rempel 3392bb70056SOleksij Rempel static const struct of_device_id imx_mu_dt_ids[] = { 3402bb70056SOleksij Rempel { .compatible = "fsl,imx6sx-mu" }, 3412bb70056SOleksij Rempel { }, 3422bb70056SOleksij Rempel }; 3432bb70056SOleksij Rempel MODULE_DEVICE_TABLE(of, imx_mu_dt_ids); 3442bb70056SOleksij Rempel 3452bb70056SOleksij Rempel static struct platform_driver imx_mu_driver = { 3462bb70056SOleksij Rempel .probe = imx_mu_probe, 3472bb70056SOleksij Rempel .remove = imx_mu_remove, 3482bb70056SOleksij Rempel .driver = { 3492bb70056SOleksij Rempel .name = "imx_mu", 3502bb70056SOleksij Rempel .of_match_table = imx_mu_dt_ids, 3512bb70056SOleksij Rempel }, 3522bb70056SOleksij Rempel }; 3532bb70056SOleksij Rempel module_platform_driver(imx_mu_driver); 3542bb70056SOleksij Rempel 3552bb70056SOleksij Rempel MODULE_AUTHOR("Oleksij Rempel <o.rempel@pengutronix.de>"); 3562bb70056SOleksij Rempel MODULE_DESCRIPTION("Message Unit driver for i.MX"); 3572bb70056SOleksij Rempel MODULE_LICENSE("GPL v2"); 358