diff options
author | Russell King <rmk+kernel@armlinux.org.uk> | 2016-10-05 07:47:50 -0400 |
---|---|---|
committer | Russell King <rmk+kernel@armlinux.org.uk> | 2018-04-24 05:44:32 -0400 |
commit | 7e8675f000bc7e20f4efb72cf624f4109301002b (patch) | |
tree | 37aa85f14297ed296907bcf9a00ed41ad582bc2c /drivers/gpu/drm/i2c | |
parent | f0316f93897c4c4e67278b175bfbfd3a95ba650a (diff) |
drm/i2c: tda998x: add CEC support
The TDA998x is a HDMI transmitter with a TDA9950 CEC engine integrated
onto the same die. Add support for the TDA9950 CEC engine to the
TDA998x driver.
Reviewed-by: Hans Verkuil <hans.verkuil@cisco.com>
Signed-off-by: Russell King <rmk+kernel@armlinux.org.uk>
Diffstat (limited to 'drivers/gpu/drm/i2c')
-rw-r--r-- | drivers/gpu/drm/i2c/Kconfig | 1 | ||||
-rw-r--r-- | drivers/gpu/drm/i2c/tda998x_drv.c | 195 |
2 files changed, 187 insertions, 9 deletions
diff --git a/drivers/gpu/drm/i2c/Kconfig b/drivers/gpu/drm/i2c/Kconfig index 3a232f5ff0a1..65d3acb61c03 100644 --- a/drivers/gpu/drm/i2c/Kconfig +++ b/drivers/gpu/drm/i2c/Kconfig | |||
@@ -22,6 +22,7 @@ config DRM_I2C_SIL164 | |||
22 | config DRM_I2C_NXP_TDA998X | 22 | config DRM_I2C_NXP_TDA998X |
23 | tristate "NXP Semiconductors TDA998X HDMI encoder" | 23 | tristate "NXP Semiconductors TDA998X HDMI encoder" |
24 | default m if DRM_TILCDC | 24 | default m if DRM_TILCDC |
25 | select CEC_CORE if CEC_NOTIFIER | ||
25 | select SND_SOC_HDMI_CODEC if SND_SOC | 26 | select SND_SOC_HDMI_CODEC if SND_SOC |
26 | help | 27 | help |
27 | Support for NXP Semiconductors TDA998X HDMI encoders. | 28 | Support for NXP Semiconductors TDA998X HDMI encoders. |
diff --git a/drivers/gpu/drm/i2c/tda998x_drv.c b/drivers/gpu/drm/i2c/tda998x_drv.c index 16e0439cad44..eb9916bd84a4 100644 --- a/drivers/gpu/drm/i2c/tda998x_drv.c +++ b/drivers/gpu/drm/i2c/tda998x_drv.c | |||
@@ -16,8 +16,10 @@ | |||
16 | */ | 16 | */ |
17 | 17 | ||
18 | #include <linux/component.h> | 18 | #include <linux/component.h> |
19 | #include <linux/gpio/consumer.h> | ||
19 | #include <linux/hdmi.h> | 20 | #include <linux/hdmi.h> |
20 | #include <linux/module.h> | 21 | #include <linux/module.h> |
22 | #include <linux/platform_data/tda9950.h> | ||
21 | #include <linux/irq.h> | 23 | #include <linux/irq.h> |
22 | #include <sound/asoundef.h> | 24 | #include <sound/asoundef.h> |
23 | #include <sound/hdmi-codec.h> | 25 | #include <sound/hdmi-codec.h> |
@@ -29,6 +31,8 @@ | |||
29 | #include <drm/drm_of.h> | 31 | #include <drm/drm_of.h> |
30 | #include <drm/i2c/tda998x.h> | 32 | #include <drm/i2c/tda998x.h> |
31 | 33 | ||
34 | #include <media/cec-notifier.h> | ||
35 | |||
32 | #define DBG(fmt, ...) DRM_DEBUG(fmt"\n", ##__VA_ARGS__) | 36 | #define DBG(fmt, ...) DRM_DEBUG(fmt"\n", ##__VA_ARGS__) |
33 | 37 | ||
34 | struct tda998x_audio_port { | 38 | struct tda998x_audio_port { |
@@ -55,6 +59,7 @@ struct tda998x_priv { | |||
55 | struct platform_device *audio_pdev; | 59 | struct platform_device *audio_pdev; |
56 | struct mutex audio_mutex; | 60 | struct mutex audio_mutex; |
57 | 61 | ||
62 | struct mutex edid_mutex; | ||
58 | wait_queue_head_t wq_edid; | 63 | wait_queue_head_t wq_edid; |
59 | volatile int wq_edid_wait; | 64 | volatile int wq_edid_wait; |
60 | 65 | ||
@@ -67,6 +72,9 @@ struct tda998x_priv { | |||
67 | struct drm_connector connector; | 72 | struct drm_connector connector; |
68 | 73 | ||
69 | struct tda998x_audio_port audio_port[2]; | 74 | struct tda998x_audio_port audio_port[2]; |
75 | struct tda9950_glue cec_glue; | ||
76 | struct gpio_desc *calib; | ||
77 | struct cec_notifier *cec_notify; | ||
70 | }; | 78 | }; |
71 | 79 | ||
72 | #define conn_to_tda998x_priv(x) \ | 80 | #define conn_to_tda998x_priv(x) \ |
@@ -345,6 +353,12 @@ struct tda998x_priv { | |||
345 | #define REG_CEC_INTSTATUS 0xee /* read */ | 353 | #define REG_CEC_INTSTATUS 0xee /* read */ |
346 | # define CEC_INTSTATUS_CEC (1 << 0) | 354 | # define CEC_INTSTATUS_CEC (1 << 0) |
347 | # define CEC_INTSTATUS_HDMI (1 << 1) | 355 | # define CEC_INTSTATUS_HDMI (1 << 1) |
356 | #define REG_CEC_CAL_XOSC_CTRL1 0xf2 | ||
357 | # define CEC_CAL_XOSC_CTRL1_ENA_CAL BIT(0) | ||
358 | #define REG_CEC_DES_FREQ2 0xf5 | ||
359 | # define CEC_DES_FREQ2_DIS_AUTOCAL BIT(7) | ||
360 | #define REG_CEC_CLK 0xf6 | ||
361 | # define CEC_CLK_FRO 0x11 | ||
348 | #define REG_CEC_FRO_IM_CLK_CTRL 0xfb /* read/write */ | 362 | #define REG_CEC_FRO_IM_CLK_CTRL 0xfb /* read/write */ |
349 | # define CEC_FRO_IM_CLK_CTRL_GHOST_DIS (1 << 7) | 363 | # define CEC_FRO_IM_CLK_CTRL_GHOST_DIS (1 << 7) |
350 | # define CEC_FRO_IM_CLK_CTRL_ENA_OTP (1 << 6) | 364 | # define CEC_FRO_IM_CLK_CTRL_ENA_OTP (1 << 6) |
@@ -359,6 +373,7 @@ struct tda998x_priv { | |||
359 | # define CEC_RXSHPDLEV_HPD (1 << 1) | 373 | # define CEC_RXSHPDLEV_HPD (1 << 1) |
360 | 374 | ||
361 | #define REG_CEC_ENAMODS 0xff /* read/write */ | 375 | #define REG_CEC_ENAMODS 0xff /* read/write */ |
376 | # define CEC_ENAMODS_EN_CEC_CLK (1 << 7) | ||
362 | # define CEC_ENAMODS_DIS_FRO (1 << 6) | 377 | # define CEC_ENAMODS_DIS_FRO (1 << 6) |
363 | # define CEC_ENAMODS_DIS_CCLK (1 << 5) | 378 | # define CEC_ENAMODS_DIS_CCLK (1 << 5) |
364 | # define CEC_ENAMODS_EN_RXSENS (1 << 2) | 379 | # define CEC_ENAMODS_EN_RXSENS (1 << 2) |
@@ -417,6 +432,114 @@ cec_read(struct tda998x_priv *priv, u8 addr) | |||
417 | return val; | 432 | return val; |
418 | } | 433 | } |
419 | 434 | ||
435 | static void cec_enamods(struct tda998x_priv *priv, u8 mods, bool enable) | ||
436 | { | ||
437 | int val = cec_read(priv, REG_CEC_ENAMODS); | ||
438 | |||
439 | if (val < 0) | ||
440 | return; | ||
441 | |||
442 | if (enable) | ||
443 | val |= mods; | ||
444 | else | ||
445 | val &= ~mods; | ||
446 | |||
447 | cec_write(priv, REG_CEC_ENAMODS, val); | ||
448 | } | ||
449 | |||
450 | static void tda998x_cec_set_calibration(struct tda998x_priv *priv, bool enable) | ||
451 | { | ||
452 | if (enable) { | ||
453 | u8 val; | ||
454 | |||
455 | cec_write(priv, 0xf3, 0xc0); | ||
456 | cec_write(priv, 0xf4, 0xd4); | ||
457 | |||
458 | /* Enable automatic calibration mode */ | ||
459 | val = cec_read(priv, REG_CEC_DES_FREQ2); | ||
460 | val &= ~CEC_DES_FREQ2_DIS_AUTOCAL; | ||
461 | cec_write(priv, REG_CEC_DES_FREQ2, val); | ||
462 | |||
463 | /* Enable free running oscillator */ | ||
464 | cec_write(priv, REG_CEC_CLK, CEC_CLK_FRO); | ||
465 | cec_enamods(priv, CEC_ENAMODS_DIS_FRO, false); | ||
466 | |||
467 | cec_write(priv, REG_CEC_CAL_XOSC_CTRL1, | ||
468 | CEC_CAL_XOSC_CTRL1_ENA_CAL); | ||
469 | } else { | ||
470 | cec_write(priv, REG_CEC_CAL_XOSC_CTRL1, 0); | ||
471 | } | ||
472 | } | ||
473 | |||
474 | /* | ||
475 | * Calibration for the internal oscillator: we need to set calibration mode, | ||
476 | * and then pulse the IRQ line low for a 10ms ± 1% period. | ||
477 | */ | ||
478 | static void tda998x_cec_calibration(struct tda998x_priv *priv) | ||
479 | { | ||
480 | struct gpio_desc *calib = priv->calib; | ||
481 | |||
482 | mutex_lock(&priv->edid_mutex); | ||
483 | if (priv->hdmi->irq > 0) | ||
484 | disable_irq(priv->hdmi->irq); | ||
485 | gpiod_direction_output(calib, 1); | ||
486 | tda998x_cec_set_calibration(priv, true); | ||
487 | |||
488 | local_irq_disable(); | ||
489 | gpiod_set_value(calib, 0); | ||
490 | mdelay(10); | ||
491 | gpiod_set_value(calib, 1); | ||
492 | local_irq_enable(); | ||
493 | |||
494 | tda998x_cec_set_calibration(priv, false); | ||
495 | gpiod_direction_input(calib); | ||
496 | if (priv->hdmi->irq > 0) | ||
497 | enable_irq(priv->hdmi->irq); | ||
498 | mutex_unlock(&priv->edid_mutex); | ||
499 | } | ||
500 | |||
501 | static int tda998x_cec_hook_init(void *data) | ||
502 | { | ||
503 | struct tda998x_priv *priv = data; | ||
504 | struct gpio_desc *calib; | ||
505 | |||
506 | calib = gpiod_get(&priv->hdmi->dev, "nxp,calib", GPIOD_ASIS); | ||
507 | if (IS_ERR(calib)) { | ||
508 | dev_warn(&priv->hdmi->dev, "failed to get calibration gpio: %ld\n", | ||
509 | PTR_ERR(calib)); | ||
510 | return PTR_ERR(calib); | ||
511 | } | ||
512 | |||
513 | priv->calib = calib; | ||
514 | |||
515 | return 0; | ||
516 | } | ||
517 | |||
518 | static void tda998x_cec_hook_exit(void *data) | ||
519 | { | ||
520 | struct tda998x_priv *priv = data; | ||
521 | |||
522 | gpiod_put(priv->calib); | ||
523 | priv->calib = NULL; | ||
524 | } | ||
525 | |||
526 | static int tda998x_cec_hook_open(void *data) | ||
527 | { | ||
528 | struct tda998x_priv *priv = data; | ||
529 | |||
530 | cec_enamods(priv, CEC_ENAMODS_EN_CEC_CLK | CEC_ENAMODS_EN_CEC, true); | ||
531 | tda998x_cec_calibration(priv); | ||
532 | |||
533 | return 0; | ||
534 | } | ||
535 | |||
536 | static void tda998x_cec_hook_release(void *data) | ||
537 | { | ||
538 | struct tda998x_priv *priv = data; | ||
539 | |||
540 | cec_enamods(priv, CEC_ENAMODS_EN_CEC_CLK | CEC_ENAMODS_EN_CEC, false); | ||
541 | } | ||
542 | |||
420 | static int | 543 | static int |
421 | set_page(struct tda998x_priv *priv, u16 reg) | 544 | set_page(struct tda998x_priv *priv, u16 reg) |
422 | { | 545 | { |
@@ -657,10 +780,13 @@ static irqreturn_t tda998x_irq_thread(int irq, void *data) | |||
657 | sta, cec, lvl, flag0, flag1, flag2); | 780 | sta, cec, lvl, flag0, flag1, flag2); |
658 | 781 | ||
659 | if (cec & CEC_RXSHPDINT_HPD) { | 782 | if (cec & CEC_RXSHPDINT_HPD) { |
660 | if (lvl & CEC_RXSHPDLEV_HPD) | 783 | if (lvl & CEC_RXSHPDLEV_HPD) { |
661 | tda998x_edid_delay_start(priv); | 784 | tda998x_edid_delay_start(priv); |
662 | else | 785 | } else { |
663 | schedule_work(&priv->detect_work); | 786 | schedule_work(&priv->detect_work); |
787 | cec_notifier_set_phys_addr(priv->cec_notify, | ||
788 | CEC_PHYS_ADDR_INVALID); | ||
789 | } | ||
664 | 790 | ||
665 | handled = true; | 791 | handled = true; |
666 | } | 792 | } |
@@ -981,6 +1107,8 @@ static int tda998x_connector_fill_modes(struct drm_connector *connector, | |||
981 | if (connector->edid_blob_ptr) { | 1107 | if (connector->edid_blob_ptr) { |
982 | struct edid *edid = (void *)connector->edid_blob_ptr->data; | 1108 | struct edid *edid = (void *)connector->edid_blob_ptr->data; |
983 | 1109 | ||
1110 | cec_notifier_set_phys_addr_from_edid(priv->cec_notify, edid); | ||
1111 | |||
984 | priv->sink_has_audio = drm_detect_monitor_audio(edid); | 1112 | priv->sink_has_audio = drm_detect_monitor_audio(edid); |
985 | } else { | 1113 | } else { |
986 | priv->sink_has_audio = false; | 1114 | priv->sink_has_audio = false; |
@@ -1024,6 +1152,8 @@ static int read_edid_block(void *data, u8 *buf, unsigned int blk, size_t length) | |||
1024 | offset = (blk & 1) ? 128 : 0; | 1152 | offset = (blk & 1) ? 128 : 0; |
1025 | segptr = blk / 2; | 1153 | segptr = blk / 2; |
1026 | 1154 | ||
1155 | mutex_lock(&priv->edid_mutex); | ||
1156 | |||
1027 | reg_write(priv, REG_DDC_ADDR, 0xa0); | 1157 | reg_write(priv, REG_DDC_ADDR, 0xa0); |
1028 | reg_write(priv, REG_DDC_OFFS, offset); | 1158 | reg_write(priv, REG_DDC_OFFS, offset); |
1029 | reg_write(priv, REG_DDC_SEGM_ADDR, 0x60); | 1159 | reg_write(priv, REG_DDC_SEGM_ADDR, 0x60); |
@@ -1043,14 +1173,15 @@ static int read_edid_block(void *data, u8 *buf, unsigned int blk, size_t length) | |||
1043 | msecs_to_jiffies(100)); | 1173 | msecs_to_jiffies(100)); |
1044 | if (i < 0) { | 1174 | if (i < 0) { |
1045 | dev_err(&priv->hdmi->dev, "read edid wait err %d\n", i); | 1175 | dev_err(&priv->hdmi->dev, "read edid wait err %d\n", i); |
1046 | return i; | 1176 | ret = i; |
1177 | goto failed; | ||
1047 | } | 1178 | } |
1048 | } else { | 1179 | } else { |
1049 | for (i = 100; i > 0; i--) { | 1180 | for (i = 100; i > 0; i--) { |
1050 | msleep(1); | 1181 | msleep(1); |
1051 | ret = reg_read(priv, REG_INT_FLAGS_2); | 1182 | ret = reg_read(priv, REG_INT_FLAGS_2); |
1052 | if (ret < 0) | 1183 | if (ret < 0) |
1053 | return ret; | 1184 | goto failed; |
1054 | if (ret & INT_FLAGS_2_EDID_BLK_RD) | 1185 | if (ret & INT_FLAGS_2_EDID_BLK_RD) |
1055 | break; | 1186 | break; |
1056 | } | 1187 | } |
@@ -1058,17 +1189,22 @@ static int read_edid_block(void *data, u8 *buf, unsigned int blk, size_t length) | |||
1058 | 1189 | ||
1059 | if (i == 0) { | 1190 | if (i == 0) { |
1060 | dev_err(&priv->hdmi->dev, "read edid timeout\n"); | 1191 | dev_err(&priv->hdmi->dev, "read edid timeout\n"); |
1061 | return -ETIMEDOUT; | 1192 | ret = -ETIMEDOUT; |
1193 | goto failed; | ||
1062 | } | 1194 | } |
1063 | 1195 | ||
1064 | ret = reg_read_range(priv, REG_EDID_DATA_0, buf, length); | 1196 | ret = reg_read_range(priv, REG_EDID_DATA_0, buf, length); |
1065 | if (ret != length) { | 1197 | if (ret != length) { |
1066 | dev_err(&priv->hdmi->dev, "failed to read edid block %d: %d\n", | 1198 | dev_err(&priv->hdmi->dev, "failed to read edid block %d: %d\n", |
1067 | blk, ret); | 1199 | blk, ret); |
1068 | return ret; | 1200 | goto failed; |
1069 | } | 1201 | } |
1070 | 1202 | ||
1071 | return 0; | 1203 | ret = 0; |
1204 | |||
1205 | failed: | ||
1206 | mutex_unlock(&priv->edid_mutex); | ||
1207 | return ret; | ||
1072 | } | 1208 | } |
1073 | 1209 | ||
1074 | static int tda998x_connector_get_modes(struct drm_connector *connector) | 1210 | static int tda998x_connector_get_modes(struct drm_connector *connector) |
@@ -1423,6 +1559,9 @@ static void tda998x_destroy(struct tda998x_priv *priv) | |||
1423 | cancel_work_sync(&priv->detect_work); | 1559 | cancel_work_sync(&priv->detect_work); |
1424 | 1560 | ||
1425 | i2c_unregister_device(priv->cec); | 1561 | i2c_unregister_device(priv->cec); |
1562 | |||
1563 | if (priv->cec_notify) | ||
1564 | cec_notifier_put(priv->cec_notify); | ||
1426 | } | 1565 | } |
1427 | 1566 | ||
1428 | /* I2C driver functions */ | 1567 | /* I2C driver functions */ |
@@ -1472,11 +1611,13 @@ static int tda998x_get_audio_ports(struct tda998x_priv *priv, | |||
1472 | static int tda998x_create(struct i2c_client *client, struct tda998x_priv *priv) | 1611 | static int tda998x_create(struct i2c_client *client, struct tda998x_priv *priv) |
1473 | { | 1612 | { |
1474 | struct device_node *np = client->dev.of_node; | 1613 | struct device_node *np = client->dev.of_node; |
1614 | struct i2c_board_info cec_info; | ||
1475 | u32 video; | 1615 | u32 video; |
1476 | int rev_lo, rev_hi, ret; | 1616 | int rev_lo, rev_hi, ret; |
1477 | 1617 | ||
1478 | mutex_init(&priv->mutex); /* protect the page access */ | 1618 | mutex_init(&priv->mutex); /* protect the page access */ |
1479 | mutex_init(&priv->audio_mutex); /* protect access from audio thread */ | 1619 | mutex_init(&priv->audio_mutex); /* protect access from audio thread */ |
1620 | mutex_init(&priv->edid_mutex); | ||
1480 | init_waitqueue_head(&priv->edid_delay_waitq); | 1621 | init_waitqueue_head(&priv->edid_delay_waitq); |
1481 | timer_setup(&priv->edid_delay_timer, tda998x_edid_delay_done, 0); | 1622 | timer_setup(&priv->edid_delay_timer, tda998x_edid_delay_done, 0); |
1482 | INIT_WORK(&priv->detect_work, tda998x_detect_work); | 1623 | INIT_WORK(&priv->detect_work, tda998x_detect_work); |
@@ -1564,6 +1705,9 @@ static int tda998x_create(struct i2c_client *client, struct tda998x_priv *priv) | |||
1564 | 1705 | ||
1565 | irq_flags = | 1706 | irq_flags = |
1566 | irqd_get_trigger_type(irq_get_irq_data(client->irq)); | 1707 | irqd_get_trigger_type(irq_get_irq_data(client->irq)); |
1708 | |||
1709 | priv->cec_glue.irq_flags = irq_flags; | ||
1710 | |||
1567 | irq_flags |= IRQF_SHARED | IRQF_ONESHOT; | 1711 | irq_flags |= IRQF_SHARED | IRQF_ONESHOT; |
1568 | ret = request_threaded_irq(client->irq, NULL, | 1712 | ret = request_threaded_irq(client->irq, NULL, |
1569 | tda998x_irq_thread, irq_flags, | 1713 | tda998x_irq_thread, irq_flags, |
@@ -1579,7 +1723,34 @@ static int tda998x_create(struct i2c_client *client, struct tda998x_priv *priv) | |||
1579 | cec_write(priv, REG_CEC_RXSHPDINTENA, CEC_RXSHPDLEV_HPD); | 1723 | cec_write(priv, REG_CEC_RXSHPDINTENA, CEC_RXSHPDLEV_HPD); |
1580 | } | 1724 | } |
1581 | 1725 | ||
1582 | priv->cec = i2c_new_dummy(client->adapter, priv->cec_addr); | 1726 | priv->cec_notify = cec_notifier_get(&client->dev); |
1727 | if (!priv->cec_notify) { | ||
1728 | ret = -ENOMEM; | ||
1729 | goto fail; | ||
1730 | } | ||
1731 | |||
1732 | priv->cec_glue.parent = &client->dev; | ||
1733 | priv->cec_glue.data = priv; | ||
1734 | priv->cec_glue.init = tda998x_cec_hook_init; | ||
1735 | priv->cec_glue.exit = tda998x_cec_hook_exit; | ||
1736 | priv->cec_glue.open = tda998x_cec_hook_open; | ||
1737 | priv->cec_glue.release = tda998x_cec_hook_release; | ||
1738 | |||
1739 | /* | ||
1740 | * Some TDA998x are actually two I2C devices merged onto one piece | ||
1741 | * of silicon: TDA9989 and TDA19989 combine the HDMI transmitter | ||
1742 | * with a slightly modified TDA9950 CEC device. The CEC device | ||
1743 | * is at the TDA9950 address, with the address pins strapped across | ||
1744 | * to the TDA998x address pins. Hence, it always has the same | ||
1745 | * offset. | ||
1746 | */ | ||
1747 | memset(&cec_info, 0, sizeof(cec_info)); | ||
1748 | strlcpy(cec_info.type, "tda9950", sizeof(cec_info.type)); | ||
1749 | cec_info.addr = priv->cec_addr; | ||
1750 | cec_info.platform_data = &priv->cec_glue; | ||
1751 | cec_info.irq = client->irq; | ||
1752 | |||
1753 | priv->cec = i2c_new_device(client->adapter, &cec_info); | ||
1583 | if (!priv->cec) { | 1754 | if (!priv->cec) { |
1584 | ret = -ENODEV; | 1755 | ret = -ENODEV; |
1585 | goto fail; | 1756 | goto fail; |
@@ -1609,10 +1780,16 @@ static int tda998x_create(struct i2c_client *client, struct tda998x_priv *priv) | |||
1609 | return 0; | 1780 | return 0; |
1610 | 1781 | ||
1611 | fail: | 1782 | fail: |
1783 | /* if encoder_init fails, the encoder slave is never registered, | ||
1784 | * so cleanup here: | ||
1785 | */ | ||
1786 | if (priv->cec) | ||
1787 | i2c_unregister_device(priv->cec); | ||
1788 | if (priv->cec_notify) | ||
1789 | cec_notifier_put(priv->cec_notify); | ||
1612 | if (client->irq) | 1790 | if (client->irq) |
1613 | free_irq(client->irq, priv); | 1791 | free_irq(client->irq, priv); |
1614 | err_irq: | 1792 | err_irq: |
1615 | i2c_unregister_device(priv->cec); | ||
1616 | return ret; | 1793 | return ret; |
1617 | } | 1794 | } |
1618 | 1795 | ||