Commit | Line | Data |
---|---|---|
95f25efe WS |
1 | /* |
2 | * Freescale eSDHC i.MX controller driver for the platform bus. | |
3 | * | |
4 | * derived from the OF-version. | |
5 | * | |
6 | * Copyright (c) 2010 Pengutronix e.K. | |
7 | * Author: Wolfram Sang <w.sang@pengutronix.de> | |
8 | * | |
9 | * This program is free software; you can redistribute it and/or modify | |
10 | * it under the terms of the GNU General Public License as published by | |
11 | * the Free Software Foundation; either version 2 of the License. | |
12 | */ | |
13 | ||
14 | #include <linux/io.h> | |
15 | #include <linux/delay.h> | |
16 | #include <linux/err.h> | |
17 | #include <linux/clk.h> | |
0c6d49ce | 18 | #include <linux/gpio.h> |
e149860d | 19 | #include <linux/slab.h> |
95f25efe | 20 | #include <linux/mmc/host.h> |
58ac8177 RZ |
21 | #include <linux/mmc/mmc.h> |
22 | #include <linux/mmc/sdio.h> | |
0c6d49ce | 23 | #include <mach/esdhc.h> |
95f25efe WS |
24 | #include "sdhci-pltfm.h" |
25 | #include "sdhci-esdhc.h" | |
26 | ||
58ac8177 RZ |
27 | /* VENDOR SPEC register */ |
28 | #define SDHCI_VENDOR_SPEC 0xC0 | |
29 | #define SDHCI_VENDOR_SPEC_SDIO_QUIRK 0x00000002 | |
30 | ||
58ac8177 RZ |
31 | /* |
32 | * The CMDTYPE of the CMD register (offset 0xE) should be set to | |
33 | * "11" when the STOP CMD12 is issued on imx53 to abort one | |
34 | * open ended multi-blk IO. Otherwise the TC INT wouldn't | |
35 | * be generated. | |
36 | * In exact block transfer, the controller doesn't complete the | |
37 | * operations automatically as required at the end of the | |
38 | * transfer and remains on hold if the abort command is not sent. | |
39 | * As a result, the TC flag is not asserted and SW received timeout | |
40 | * exeception. Bit1 of Vendor Spec registor is used to fix it. | |
41 | */ | |
42 | #define ESDHC_FLAG_MULTIBLK_NO_INT (1 << 1) | |
e149860d | 43 | |
57ed3314 SG |
44 | enum imx_esdhc_type { |
45 | IMX25_ESDHC, | |
46 | IMX35_ESDHC, | |
47 | IMX51_ESDHC, | |
48 | IMX53_ESDHC, | |
49 | }; | |
50 | ||
e149860d RZ |
51 | struct pltfm_imx_data { |
52 | int flags; | |
53 | u32 scratchpad; | |
57ed3314 | 54 | enum imx_esdhc_type devtype; |
842afc02 | 55 | struct esdhc_platform_data boarddata; |
e149860d RZ |
56 | }; |
57 | ||
57ed3314 SG |
58 | static struct platform_device_id imx_esdhc_devtype[] = { |
59 | { | |
60 | .name = "sdhci-esdhc-imx25", | |
61 | .driver_data = IMX25_ESDHC, | |
62 | }, { | |
63 | .name = "sdhci-esdhc-imx35", | |
64 | .driver_data = IMX35_ESDHC, | |
65 | }, { | |
66 | .name = "sdhci-esdhc-imx51", | |
67 | .driver_data = IMX51_ESDHC, | |
68 | }, { | |
69 | .name = "sdhci-esdhc-imx53", | |
70 | .driver_data = IMX53_ESDHC, | |
71 | }, { | |
72 | /* sentinel */ | |
73 | } | |
74 | }; | |
75 | MODULE_DEVICE_TABLE(platform, imx_esdhc_devtype); | |
76 | ||
77 | static inline int is_imx25_esdhc(struct pltfm_imx_data *data) | |
78 | { | |
79 | return data->devtype == IMX25_ESDHC; | |
80 | } | |
81 | ||
82 | static inline int is_imx35_esdhc(struct pltfm_imx_data *data) | |
83 | { | |
84 | return data->devtype == IMX35_ESDHC; | |
85 | } | |
86 | ||
87 | static inline int is_imx51_esdhc(struct pltfm_imx_data *data) | |
88 | { | |
89 | return data->devtype == IMX51_ESDHC; | |
90 | } | |
91 | ||
92 | static inline int is_imx53_esdhc(struct pltfm_imx_data *data) | |
93 | { | |
94 | return data->devtype == IMX53_ESDHC; | |
95 | } | |
96 | ||
95f25efe WS |
97 | static inline void esdhc_clrset_le(struct sdhci_host *host, u32 mask, u32 val, int reg) |
98 | { | |
99 | void __iomem *base = host->ioaddr + (reg & ~0x3); | |
100 | u32 shift = (reg & 0x3) * 8; | |
101 | ||
102 | writel(((readl(base) & ~(mask << shift)) | (val << shift)), base); | |
103 | } | |
104 | ||
7e29c306 WS |
105 | static u32 esdhc_readl_le(struct sdhci_host *host, int reg) |
106 | { | |
842afc02 SG |
107 | struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host); |
108 | struct pltfm_imx_data *imx_data = pltfm_host->priv; | |
109 | struct esdhc_platform_data *boarddata = &imx_data->boarddata; | |
e149860d | 110 | |
913413c3 | 111 | /* fake CARD_PRESENT flag */ |
7e29c306 WS |
112 | u32 val = readl(host->ioaddr + reg); |
113 | ||
e149860d | 114 | if (unlikely((reg == SDHCI_PRESENT_STATE) |
913413c3 SG |
115 | && gpio_is_valid(boarddata->cd_gpio))) { |
116 | if (gpio_get_value(boarddata->cd_gpio)) | |
7e29c306 | 117 | /* no card, if a valid gpio says so... */ |
803862a6 | 118 | val &= ~SDHCI_CARD_PRESENT; |
7e29c306 WS |
119 | else |
120 | /* ... in all other cases assume card is present */ | |
121 | val |= SDHCI_CARD_PRESENT; | |
122 | } | |
123 | ||
124 | return val; | |
125 | } | |
126 | ||
127 | static void esdhc_writel_le(struct sdhci_host *host, u32 val, int reg) | |
128 | { | |
e149860d RZ |
129 | struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host); |
130 | struct pltfm_imx_data *imx_data = pltfm_host->priv; | |
842afc02 | 131 | struct esdhc_platform_data *boarddata = &imx_data->boarddata; |
e149860d RZ |
132 | |
133 | if (unlikely((reg == SDHCI_INT_ENABLE || reg == SDHCI_SIGNAL_ENABLE) | |
913413c3 | 134 | && (boarddata->cd_type == ESDHC_CD_GPIO))) |
7e29c306 WS |
135 | /* |
136 | * these interrupts won't work with a custom card_detect gpio | |
7e29c306 WS |
137 | */ |
138 | val &= ~(SDHCI_INT_CARD_REMOVE | SDHCI_INT_CARD_INSERT); | |
139 | ||
58ac8177 RZ |
140 | if (unlikely((imx_data->flags & ESDHC_FLAG_MULTIBLK_NO_INT) |
141 | && (reg == SDHCI_INT_STATUS) | |
142 | && (val & SDHCI_INT_DATA_END))) { | |
143 | u32 v; | |
144 | v = readl(host->ioaddr + SDHCI_VENDOR_SPEC); | |
145 | v &= ~SDHCI_VENDOR_SPEC_SDIO_QUIRK; | |
146 | writel(v, host->ioaddr + SDHCI_VENDOR_SPEC); | |
147 | } | |
148 | ||
7e29c306 WS |
149 | writel(val, host->ioaddr + reg); |
150 | } | |
151 | ||
95f25efe WS |
152 | static u16 esdhc_readw_le(struct sdhci_host *host, int reg) |
153 | { | |
154 | if (unlikely(reg == SDHCI_HOST_VERSION)) | |
155 | reg ^= 2; | |
156 | ||
157 | return readw(host->ioaddr + reg); | |
158 | } | |
159 | ||
160 | static void esdhc_writew_le(struct sdhci_host *host, u16 val, int reg) | |
161 | { | |
162 | struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host); | |
e149860d | 163 | struct pltfm_imx_data *imx_data = pltfm_host->priv; |
95f25efe WS |
164 | |
165 | switch (reg) { | |
166 | case SDHCI_TRANSFER_MODE: | |
167 | /* | |
168 | * Postpone this write, we must do it together with a | |
169 | * command write that is down below. | |
170 | */ | |
58ac8177 RZ |
171 | if ((imx_data->flags & ESDHC_FLAG_MULTIBLK_NO_INT) |
172 | && (host->cmd->opcode == SD_IO_RW_EXTENDED) | |
173 | && (host->cmd->data->blocks > 1) | |
174 | && (host->cmd->data->flags & MMC_DATA_READ)) { | |
175 | u32 v; | |
176 | v = readl(host->ioaddr + SDHCI_VENDOR_SPEC); | |
177 | v |= SDHCI_VENDOR_SPEC_SDIO_QUIRK; | |
178 | writel(v, host->ioaddr + SDHCI_VENDOR_SPEC); | |
179 | } | |
e149860d | 180 | imx_data->scratchpad = val; |
95f25efe WS |
181 | return; |
182 | case SDHCI_COMMAND: | |
58ac8177 RZ |
183 | if ((host->cmd->opcode == MMC_STOP_TRANSMISSION) |
184 | && (imx_data->flags & ESDHC_FLAG_MULTIBLK_NO_INT)) | |
185 | val |= SDHCI_CMD_ABORTCMD; | |
e149860d | 186 | writel(val << 16 | imx_data->scratchpad, |
95f25efe WS |
187 | host->ioaddr + SDHCI_TRANSFER_MODE); |
188 | return; | |
189 | case SDHCI_BLOCK_SIZE: | |
190 | val &= ~SDHCI_MAKE_BLKSZ(0x7, 0); | |
191 | break; | |
192 | } | |
193 | esdhc_clrset_le(host, 0xffff, val, reg); | |
194 | } | |
195 | ||
196 | static void esdhc_writeb_le(struct sdhci_host *host, u8 val, int reg) | |
197 | { | |
198 | u32 new_val; | |
199 | ||
200 | switch (reg) { | |
201 | case SDHCI_POWER_CONTROL: | |
202 | /* | |
203 | * FSL put some DMA bits here | |
204 | * If your board has a regulator, code should be here | |
205 | */ | |
206 | return; | |
207 | case SDHCI_HOST_CONTROL: | |
208 | /* FSL messed up here, so we can just keep those two */ | |
209 | new_val = val & (SDHCI_CTRL_LED | SDHCI_CTRL_4BITBUS); | |
210 | /* ensure the endianess */ | |
211 | new_val |= ESDHC_HOST_CONTROL_LE; | |
212 | /* DMA mode bits are shifted */ | |
213 | new_val |= (val & SDHCI_CTRL_DMA_MASK) << 5; | |
214 | ||
215 | esdhc_clrset_le(host, 0xffff, new_val, reg); | |
216 | return; | |
217 | } | |
218 | esdhc_clrset_le(host, 0xff, val, reg); | |
913413c3 SG |
219 | |
220 | /* | |
221 | * The esdhc has a design violation to SDHC spec which tells | |
222 | * that software reset should not affect card detection circuit. | |
223 | * But esdhc clears its SYSCTL register bits [0..2] during the | |
224 | * software reset. This will stop those clocks that card detection | |
225 | * circuit relies on. To work around it, we turn the clocks on back | |
226 | * to keep card detection circuit functional. | |
227 | */ | |
228 | if ((reg == SDHCI_SOFTWARE_RESET) && (val & 1)) | |
229 | esdhc_clrset_le(host, 0x7, 0x7, ESDHC_SYSTEM_CONTROL); | |
95f25efe WS |
230 | } |
231 | ||
232 | static unsigned int esdhc_pltfm_get_max_clock(struct sdhci_host *host) | |
233 | { | |
234 | struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host); | |
235 | ||
236 | return clk_get_rate(pltfm_host->clk); | |
237 | } | |
238 | ||
239 | static unsigned int esdhc_pltfm_get_min_clock(struct sdhci_host *host) | |
240 | { | |
241 | struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host); | |
242 | ||
243 | return clk_get_rate(pltfm_host->clk) / 256 / 16; | |
244 | } | |
245 | ||
913413c3 SG |
246 | static unsigned int esdhc_pltfm_get_ro(struct sdhci_host *host) |
247 | { | |
842afc02 SG |
248 | struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host); |
249 | struct pltfm_imx_data *imx_data = pltfm_host->priv; | |
250 | struct esdhc_platform_data *boarddata = &imx_data->boarddata; | |
913413c3 SG |
251 | |
252 | switch (boarddata->wp_type) { | |
253 | case ESDHC_WP_GPIO: | |
254 | if (gpio_is_valid(boarddata->wp_gpio)) | |
255 | return gpio_get_value(boarddata->wp_gpio); | |
256 | case ESDHC_WP_CONTROLLER: | |
257 | return !(readl(host->ioaddr + SDHCI_PRESENT_STATE) & | |
258 | SDHCI_WRITE_PROTECT); | |
259 | case ESDHC_WP_NONE: | |
260 | break; | |
261 | } | |
262 | ||
263 | return -ENOSYS; | |
264 | } | |
265 | ||
0c6d49ce | 266 | static struct sdhci_ops sdhci_esdhc_ops = { |
e149860d | 267 | .read_l = esdhc_readl_le, |
0c6d49ce | 268 | .read_w = esdhc_readw_le, |
e149860d | 269 | .write_l = esdhc_writel_le, |
0c6d49ce WS |
270 | .write_w = esdhc_writew_le, |
271 | .write_b = esdhc_writeb_le, | |
272 | .set_clock = esdhc_set_clock, | |
273 | .get_max_clock = esdhc_pltfm_get_max_clock, | |
274 | .get_min_clock = esdhc_pltfm_get_min_clock, | |
913413c3 | 275 | .get_ro = esdhc_pltfm_get_ro, |
0c6d49ce WS |
276 | }; |
277 | ||
85d6509d SG |
278 | static struct sdhci_pltfm_data sdhci_esdhc_imx_pdata = { |
279 | .quirks = ESDHC_DEFAULT_QUIRKS | SDHCI_QUIRK_BROKEN_ADMA | |
280 | | SDHCI_QUIRK_BROKEN_CARD_DETECTION, | |
281 | /* ADMA has issues. Might be fixable */ | |
282 | .ops = &sdhci_esdhc_ops, | |
283 | }; | |
284 | ||
7e29c306 WS |
285 | static irqreturn_t cd_irq(int irq, void *data) |
286 | { | |
287 | struct sdhci_host *sdhost = (struct sdhci_host *)data; | |
288 | ||
289 | tasklet_schedule(&sdhost->card_tasklet); | |
290 | return IRQ_HANDLED; | |
291 | }; | |
292 | ||
85d6509d | 293 | static int __devinit sdhci_esdhc_imx_probe(struct platform_device *pdev) |
95f25efe | 294 | { |
85d6509d SG |
295 | struct sdhci_pltfm_host *pltfm_host; |
296 | struct sdhci_host *host; | |
297 | struct esdhc_platform_data *boarddata; | |
95f25efe | 298 | struct clk *clk; |
0c6d49ce | 299 | int err; |
e149860d | 300 | struct pltfm_imx_data *imx_data; |
95f25efe | 301 | |
85d6509d SG |
302 | host = sdhci_pltfm_init(pdev, &sdhci_esdhc_imx_pdata); |
303 | if (IS_ERR(host)) | |
304 | return PTR_ERR(host); | |
305 | ||
306 | pltfm_host = sdhci_priv(host); | |
307 | ||
308 | imx_data = kzalloc(sizeof(struct pltfm_imx_data), GFP_KERNEL); | |
309 | if (!imx_data) | |
310 | return -ENOMEM; | |
57ed3314 SG |
311 | |
312 | imx_data->devtype = pdev->id_entry->driver_data; | |
85d6509d SG |
313 | pltfm_host->priv = imx_data; |
314 | ||
95f25efe WS |
315 | clk = clk_get(mmc_dev(host->mmc), NULL); |
316 | if (IS_ERR(clk)) { | |
317 | dev_err(mmc_dev(host->mmc), "clk err\n"); | |
85d6509d SG |
318 | err = PTR_ERR(clk); |
319 | goto err_clk_get; | |
95f25efe WS |
320 | } |
321 | clk_enable(clk); | |
322 | pltfm_host->clk = clk; | |
323 | ||
57ed3314 | 324 | if (!is_imx25_esdhc(imx_data)) |
37865fe9 EB |
325 | host->quirks |= SDHCI_QUIRK_BROKEN_TIMEOUT_VAL; |
326 | ||
57ed3314 | 327 | if (is_imx25_esdhc(imx_data) || is_imx35_esdhc(imx_data)) |
0c6d49ce | 328 | /* Fix errata ENGcm07207 present on i.MX25 and i.MX35 */ |
16a790bc | 329 | host->quirks |= SDHCI_QUIRK_NO_MULTIBLOCK; |
0c6d49ce | 330 | |
57ed3314 | 331 | if (is_imx53_esdhc(imx_data)) |
58ac8177 RZ |
332 | imx_data->flags |= ESDHC_FLAG_MULTIBLK_NO_INT; |
333 | ||
842afc02 | 334 | if (!host->mmc->parent->platform_data) { |
913413c3 SG |
335 | dev_err(mmc_dev(host->mmc), "no board data!\n"); |
336 | err = -EINVAL; | |
337 | goto no_board_data; | |
338 | } | |
842afc02 SG |
339 | imx_data->boarddata = *((struct esdhc_platform_data *) |
340 | host->mmc->parent->platform_data); | |
341 | boarddata = &imx_data->boarddata; | |
913413c3 SG |
342 | |
343 | /* write_protect */ | |
344 | if (boarddata->wp_type == ESDHC_WP_GPIO) { | |
0c6d49ce WS |
345 | err = gpio_request_one(boarddata->wp_gpio, GPIOF_IN, "ESDHC_WP"); |
346 | if (err) { | |
347 | dev_warn(mmc_dev(host->mmc), | |
913413c3 SG |
348 | "no write-protect pin available!\n"); |
349 | boarddata->wp_gpio = -EINVAL; | |
0c6d49ce | 350 | } |
913413c3 SG |
351 | } else { |
352 | boarddata->wp_gpio = -EINVAL; | |
353 | } | |
354 | ||
355 | /* card_detect */ | |
356 | if (boarddata->cd_type != ESDHC_CD_GPIO) | |
357 | boarddata->cd_gpio = -EINVAL; | |
7e29c306 | 358 | |
913413c3 SG |
359 | switch (boarddata->cd_type) { |
360 | case ESDHC_CD_GPIO: | |
7e29c306 WS |
361 | err = gpio_request_one(boarddata->cd_gpio, GPIOF_IN, "ESDHC_CD"); |
362 | if (err) { | |
913413c3 | 363 | dev_err(mmc_dev(host->mmc), |
7e29c306 WS |
364 | "no card-detect pin available!\n"); |
365 | goto no_card_detect_pin; | |
366 | } | |
367 | ||
7e29c306 WS |
368 | err = request_irq(gpio_to_irq(boarddata->cd_gpio), cd_irq, |
369 | IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, | |
370 | mmc_hostname(host->mmc), host); | |
371 | if (err) { | |
913413c3 | 372 | dev_err(mmc_dev(host->mmc), "request irq error\n"); |
7e29c306 WS |
373 | goto no_card_detect_irq; |
374 | } | |
913413c3 | 375 | /* fall through */ |
7e29c306 | 376 | |
913413c3 SG |
377 | case ESDHC_CD_CONTROLLER: |
378 | /* we have a working card_detect back */ | |
7e29c306 | 379 | host->quirks &= ~SDHCI_QUIRK_BROKEN_CARD_DETECTION; |
913413c3 SG |
380 | break; |
381 | ||
382 | case ESDHC_CD_PERMANENT: | |
383 | host->mmc->caps = MMC_CAP_NONREMOVABLE; | |
384 | break; | |
385 | ||
386 | case ESDHC_CD_NONE: | |
387 | break; | |
0c6d49ce | 388 | } |
16a790bc | 389 | |
85d6509d SG |
390 | err = sdhci_add_host(host); |
391 | if (err) | |
392 | goto err_add_host; | |
393 | ||
95f25efe | 394 | return 0; |
7e29c306 | 395 | |
913413c3 SG |
396 | err_add_host: |
397 | if (gpio_is_valid(boarddata->cd_gpio)) | |
398 | free_irq(gpio_to_irq(boarddata->cd_gpio), host); | |
399 | no_card_detect_irq: | |
400 | if (gpio_is_valid(boarddata->cd_gpio)) | |
401 | gpio_free(boarddata->cd_gpio); | |
402 | if (gpio_is_valid(boarddata->wp_gpio)) | |
403 | gpio_free(boarddata->wp_gpio); | |
404 | no_card_detect_pin: | |
405 | no_board_data: | |
85d6509d SG |
406 | clk_disable(pltfm_host->clk); |
407 | clk_put(pltfm_host->clk); | |
913413c3 SG |
408 | err_clk_get: |
409 | kfree(imx_data); | |
85d6509d SG |
410 | sdhci_pltfm_free(pdev); |
411 | return err; | |
95f25efe WS |
412 | } |
413 | ||
85d6509d | 414 | static int __devexit sdhci_esdhc_imx_remove(struct platform_device *pdev) |
95f25efe | 415 | { |
85d6509d | 416 | struct sdhci_host *host = platform_get_drvdata(pdev); |
95f25efe | 417 | struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host); |
e149860d | 418 | struct pltfm_imx_data *imx_data = pltfm_host->priv; |
842afc02 | 419 | struct esdhc_platform_data *boarddata = &imx_data->boarddata; |
85d6509d SG |
420 | int dead = (readl(host->ioaddr + SDHCI_INT_STATUS) == 0xffffffff); |
421 | ||
422 | sdhci_remove_host(host, dead); | |
0c6d49ce | 423 | |
913413c3 | 424 | if (gpio_is_valid(boarddata->wp_gpio)) |
0c6d49ce | 425 | gpio_free(boarddata->wp_gpio); |
95f25efe | 426 | |
913413c3 SG |
427 | if (gpio_is_valid(boarddata->cd_gpio)) { |
428 | free_irq(gpio_to_irq(boarddata->cd_gpio), host); | |
7e29c306 | 429 | gpio_free(boarddata->cd_gpio); |
7e29c306 WS |
430 | } |
431 | ||
95f25efe WS |
432 | clk_disable(pltfm_host->clk); |
433 | clk_put(pltfm_host->clk); | |
e149860d | 434 | kfree(imx_data); |
85d6509d SG |
435 | |
436 | sdhci_pltfm_free(pdev); | |
437 | ||
438 | return 0; | |
95f25efe WS |
439 | } |
440 | ||
85d6509d SG |
441 | static struct platform_driver sdhci_esdhc_imx_driver = { |
442 | .driver = { | |
443 | .name = "sdhci-esdhc-imx", | |
444 | .owner = THIS_MODULE, | |
445 | }, | |
57ed3314 | 446 | .id_table = imx_esdhc_devtype, |
85d6509d SG |
447 | .probe = sdhci_esdhc_imx_probe, |
448 | .remove = __devexit_p(sdhci_esdhc_imx_remove), | |
449 | #ifdef CONFIG_PM | |
450 | .suspend = sdhci_pltfm_suspend, | |
451 | .resume = sdhci_pltfm_resume, | |
452 | #endif | |
95f25efe | 453 | }; |
85d6509d SG |
454 | |
455 | static int __init sdhci_esdhc_imx_init(void) | |
456 | { | |
457 | return platform_driver_register(&sdhci_esdhc_imx_driver); | |
458 | } | |
459 | module_init(sdhci_esdhc_imx_init); | |
460 | ||
461 | static void __exit sdhci_esdhc_imx_exit(void) | |
462 | { | |
463 | platform_driver_unregister(&sdhci_esdhc_imx_driver); | |
464 | } | |
465 | module_exit(sdhci_esdhc_imx_exit); | |
466 | ||
467 | MODULE_DESCRIPTION("SDHCI driver for Freescale i.MX eSDHC"); | |
468 | MODULE_AUTHOR("Wolfram Sang <w.sang@pengutronix.de>"); | |
469 | MODULE_LICENSE("GPL v2"); |