您尚未登录。

楼主 #1 2020-11-09 14:39:26

X3
会员
注册时间: 2020-04-08
已发帖子: 85
积分: 73.5

全志主线Linux UART串口使用8250驱动程序,但是没有实现RS485控制,有没有哪位打上了patch?

这是RS485应用层编程:
https://www.kernel.org/doc/Documentation/serial/serial-rs485.txt


http://lkml.iu.edu/hypermail/linux/kernel/1407.1/01890.html

[PATCH 5/6] tty: serial: 8250-core: add rs485 support
From: Sebastian Andrzej Siewior
Date: Wed Jul 09 2014 - 13:51:23 EST
Next message: Sebastian Andrzej Siewior: "[PATCH 4/6] tty: serial: 8250-core: reorder serial8250_stop_rx() & serial8250_start_tx()"
Previous message: Sebastian Andrzej Siewior: "[PATCH 2/6] tty: serial: 8250 core: allow to overwrite & export serial8250_startup()"
In reply to: Sebastian Andrzej Siewior: "Re: [PATCH 2/6] tty: serial: 8250 core: allow to overwrite & export serial8250_startup()"
Next in thread: Lennart Sorensen: "Re: [PATCH 5/6] tty: serial: 8250-core: add rs485 support"
Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
So after I stuffed the rs485 support from the omap-serial into
8250-omap, I've been looking at it and the only omap specific part was
the OMAP_UART_SCR_TX_EMPTY part. The driver has always TX_EMPTY set
because the 8250 core expects an interrupt after the TX fifo + shift
register is empty. The rs485 parts seems to wait for half fifo and then
again for the empty fifo. With this change gone it fits fine as generic
code and here it is.

It is expected that the potential rs485 user invokes
serial_omap_probe_rs485() to configure atleast RTS gpio which is a must
have property. The code has been taken from omap-serial driver and
received limited tested due to -ENODEV (the compiler said it works).

Signed-off-by: Sebastian Andrzej Siewior <bigeasy@xxxxxxxxxxxxx>
---
drivers/tty/serial/8250/8250_core.c | 171 ++++++++++++++++++++++++++++++++++++
include/linux/serial_8250.h | 6 ++
2 files changed, 177 insertions(+)

diff --git a/drivers/tty/serial/8250/8250_core.c b/drivers/tty/serial/8250/8250_core.c
index c7c3bf7..bf06a4c 100644
--- a/drivers/tty/serial/8250/8250_core.c
+++ b/drivers/tty/serial/8250/8250_core.c
@@ -39,6 +39,8 @@
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/pm_runtime.h>
+#include <linux/gpio.h>
+#include <linux/of_gpio.h>
#ifdef CONFIG_SPARC
#include <linux/sunserialcore.h>
#endif
@@ -1281,10 +1283,34 @@ static void autoconfig_irq(struct uart_8250_port *up)

static inline void __stop_tx(struct uart_8250_port *p)
{
+ if (p->rs485.flags & SER_RS485_ENABLED) {
+ int ret;
+
+ ret = (p->rs485.flags & SER_RS485_RTS_AFTER_SEND) ? 1 : 0;
+ if (gpio_get_value(p->rts_gpio) != ret) {
+ if (p->rs485.delay_rts_after_send > 0)
+ mdelay(p->rs485.delay_rts_after_send);
+ gpio_set_value(p->rts_gpio, ret);
+ }
+ }
+
if (p->ier & UART_IER_THRI) {
p->ier &= ~UART_IER_THRI;
serial_out(p, UART_IER, p->ier);
}
+
+ if ((p->rs485.flags & SER_RS485_ENABLED) &&
+ !(p->rs485.flags & SER_RS485_RX_DURING_TX)) {
+ /*
+ * Empty the RX FIFO, we are not interested in anything
+ * received during the half-duplex transmission.
+ */
+ serial_out(p, UART_FCR, p->fcr | UART_FCR_CLEAR_RCVR);
+ /* Re-enable RX interrupts */
+ p->ier |= UART_IER_RLSI | UART_IER_RDI;
+ p->port.read_status_mask |= UART_LSR_DR;
+ serial_out(p, UART_IER, p->ier);
+ }
}

static void serial8250_stop_tx(struct uart_port *port)
@@ -1330,6 +1356,20 @@ static void serial8250_start_tx(struct uart_port *port)
if (up->dma && !serial8250_tx_dma(up)) {
goto out;
} else if (!(up->ier & UART_IER_THRI)) {
+
+ if (up->rs485.flags & SER_RS485_ENABLED) {
+ int ret;
+
+ ret = (up->rs485.flags & SER_RS485_RTS_ON_SEND) ? 1 : 0;
+ if (gpio_get_value(up->rts_gpio) != ret) {
+ gpio_set_value(up->rts_gpio, ret);
+ if (up->rs485.delay_rts_before_send > 0)
+ mdelay(up->rs485.delay_rts_before_send);
+ }
+ if (!(up->rs485.flags & SER_RS485_RX_DURING_TX))
+ serial8250_stop_rx(port);
+ }
+
up->ier |= UART_IER_THRI;
serial_port_out(port, UART_IER, up->ier);

@@ -2556,6 +2596,7 @@ serial8250_do_set_termios(struct uart_port *port, struct ktermios *termios,
serial_port_out(port, UART_FCR, UART_FCR_ENABLE_FIFO);
serial_port_out(port, UART_FCR, fcr); /* set fcr */
}
+ up->fcr = fcr;
serial8250_set_mctrl(port, port->mctrl);
spin_unlock_irqrestore(&port->lock, flags);
pm_runtime_mark_last_busy(port->dev);
@@ -2811,6 +2852,124 @@ serial8250_verify_port(struct uart_port *port, struct serial_struct *ser)
return 0;
}

+int serial8250_probe_rs485(struct uart_8250_port *up,
+ struct device *dev)
+{
+ struct serial_rs485 *rs485conf = &up->rs485;
+ struct device_node *np = dev->of_node;
+ u32 rs485_delay[2];
+ enum of_gpio_flags flags;
+ int ret;
+
+ rs485conf->flags = 0;
+ if (!np)
+ return 0;
+
+ /* check for tx enable gpio */
+ up->rts_gpio = of_get_named_gpio_flags(np, "rts-gpio", 0, &flags);
+ if (up->rts_gpio == -EPROBE_DEFER)
+ return -EPROBE_DEFER;
+ if (!gpio_is_valid(up->rts_gpio))
+ return 0;
+
+ ret = devm_gpio_request(dev, up->rts_gpio, "serial_rts");
+ if (ret < 0)
+ return ret;
+ ret = gpio_direction_output(up->rts_gpio,
+ flags & SER_RS485_RTS_AFTER_SEND);
+ if (ret < 0)
+ return ret;
+
+ up->rts_gpio_valid = true;
+
+ if (of_property_read_bool(np, "rs485-rts-active-high"))
+ rs485conf->flags |= SER_RS485_RTS_ON_SEND;
+ else
+ rs485conf->flags |= SER_RS485_RTS_AFTER_SEND;
+
+ if (of_property_read_u32_array(np, "rs485-rts-delay",
+ rs485_delay, 2) == 0) {
+ rs485conf->delay_rts_before_send = rs485_delay[0];
+ rs485conf->delay_rts_after_send = rs485_delay[1];
+ }
+
+ if (of_property_read_bool(np, "rs485-rx-during-tx"))
+ rs485conf->flags |= SER_RS485_RX_DURING_TX;
+
+ if (of_property_read_bool(np, "linux,rs485-enabled-at-boot-time"))
+ rs485conf->flags |= SER_RS485_ENABLED;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(serial8250_probe_rs485);
+
+static void serial8250_config_rs485(struct uart_port *port,
+ struct serial_rs485 *rs485conf)
+{
+ struct uart_8250_port *up =
+ container_of(port, struct uart_8250_port, port);
+ unsigned long flags;
+ unsigned int mode;
+ int val;
+
+ pm_runtime_get_sync(port->dev);
+ spin_lock_irqsave(&up->port.lock, flags);
+
+ /* Disable interrupts from this port */
+ mode = up->ier;
+ up->ier = 0;
+ serial_out(up, UART_IER, 0);
+
+ /* store new config */
+ up->rs485 = *rs485conf;
+
+ /* enable / disable rts */
+ val = (up->rs485.flags & SER_RS485_ENABLED) ?
+ SER_RS485_RTS_AFTER_SEND : SER_RS485_RTS_ON_SEND;
+ val = (up->rs485.flags & val) ? 1 : 0;
+ gpio_set_value(up->rts_gpio, val);
+
+ /* Enable interrupts */
+ up->ier = mode;
+ serial_out(up, UART_IER, up->ier);
+
+ spin_unlock_irqrestore(&up->port.lock, flags);
+ pm_runtime_mark_last_busy(port->dev);
+ pm_runtime_put_autosuspend(port->dev);
+}
+
+static int serial8250_ioctl(struct uart_port *port, unsigned int cmd,
+ unsigned long arg)
+{
+ struct serial_rs485 rs485conf;
+ struct uart_8250_port *up;
+
+ up = container_of(port, struct uart_8250_port, port);
+ switch (cmd) {
+ case TIOCSRS485:
+ if (!gpio_is_valid(up->rts_gpio))
+ return -ENODEV;
+ if (copy_from_user(&rs485conf, (void __user *) arg,
+ sizeof(rs485conf)))
+ return -EFAULT;
+
+ serial8250_config_rs485(port, &rs485conf);
+ break;
+
+ case TIOCGRS485:
+ if (!gpio_is_valid(up->rts_gpio))
+ return -ENODEV;
+ if (copy_to_user((void __user *) arg, &up->rs485,
+ sizeof(rs485conf)))
+ return -EFAULT;
+ break;
+
+ default:
+ return -ENOIOCTLCMD;
+ }
+ return 0;
+}
+
static const char *
serial8250_type(struct uart_port *port)
{
@@ -2840,6 +2999,7 @@ static struct uart_ops serial8250_pops = {
.request_port = serial8250_request_port,
.config_port = serial8250_config_port,
.verify_port = serial8250_verify_port,
+ .ioctl = serial8250_ioctl,
#ifdef CONFIG_CONSOLE_POLL
.poll_get_char = serial8250_get_poll_char,
.poll_put_char = serial8250_put_poll_char,
@@ -3375,6 +3535,17 @@ int serial8250_register_8250_port(struct uart_8250_port *up)
uart->tx_loadsz = up->tx_loadsz;
uart->capabilities = up->capabilities;

+ /*
+ * gpio_is_valid() is nice but if this struct wasn't initialized
+ * then it is 0 which is considered as valid. With the ioctl()
+ * which can enable the rs485 routine we could abuse a gpio.
+ */
+ if (up->rts_gpio_valid) {
+ uart->rs485 = up->rs485;
+ uart->rts_gpio = up->rts_gpio;
+ } else
+ uart->rts_gpio = -EINVAL;
+
/* Take tx_loadsz from fifosize if it wasn't set separately */
if (uart->port.fifosize && !uart->tx_loadsz)
uart->tx_loadsz = uart->port.fifosize;
diff --git a/include/linux/serial_8250.h b/include/linux/serial_8250.h
index 0ec21ec..056a73f 100644
--- a/include/linux/serial_8250.h
+++ b/include/linux/serial_8250.h
@@ -78,6 +78,7 @@ struct uart_8250_port {
unsigned char acr;
unsigned char ier;
unsigned char lcr;
+ unsigned char fcr;
unsigned char mcr;
unsigned char mcr_mask; /* mask of user bits */
unsigned char mcr_force; /* mask of forced bits */
@@ -94,6 +95,9 @@ struct uart_8250_port {
unsigned char msr_saved_flags;

struct uart_8250_dma *dma;
+ struct serial_rs485 rs485;
+ int rts_gpio;
+ bool rts_gpio_valid;

/* 8250 specific callbacks */
int (*dl_read)(struct uart_8250_port *);
@@ -107,6 +111,8 @@ void serial8250_resume_port(int line);

extern int early_serial_setup(struct uart_port *port);

+extern int serial8250_probe_rs485(struct uart_8250_port *up,
+ struct device *dev);
extern int serial8250_find_port(struct uart_port *p);
extern int serial8250_find_port_for_earlycon(void);
extern unsigned int serial8250_early_in(struct uart_port *port, int offset);
--
2.0.1

--
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/


https://patchwork.kernel.org/project/linux-arm-kernel/patch/1404928177-26554-6-git-send-email-bigeasy@linutronix.de/

http://lists.infradead.org/pipermail/linux-arm-kernel/2014-July/271071.html

离线

楼主 #2 2020-11-09 14:40:19

X3
会员
注册时间: 2020-04-08
已发帖子: 85
积分: 73.5

Re: 全志主线Linux UART串口使用8250驱动程序,但是没有实现RS485控制,有没有哪位打上了patch?

http://lists.infradead.org/pipermail/linux-arm-kernel/2014-July/271071.html

https://bbs.21ic.com/blog-41444-683796.html
AM3352 串口升级为8250模式后,RS485模式失效及处理

AM3352原来使用为TI官方提供的OMAP  serial port 串口驱动,使用一段时间后发现,这个串口是么有启动用DMA 模式发送的, 尽管你在设备树种声明可DNA通道,
这有啥问题呢,就是发送串口数据时,例如发送100个字节,大部分的情况下,这100个字节是连续发送出去的,但是遇到系统繁忙时,数据就不是连续发送了,中间会插入5-10ms的延时,这个时间是不确定的(由内核处理的事务多少而定),看了下OMAP  serial port 串口驱动代码。串口发送机制大概就是把数据分成若干帧(帧长度为FIFO大小)写满串口的FIFO ,然后FIFO空闲时中断再填充下一帧数据,直到数据发送完成。 这个方案遇到系统繁忙时,FIFO中断被延后就会出现数据发送不连续的问题。

然后有大神告知8250串口默认都是开启DMA模式的,便试试;
其实升级8250串口很简单,都不用几分钟就升级完成了
把串口设备由OMAP serial 改为8250 串口即可,见上图;
编译内核,更新内核。需要说明的是之后/dev/下的串口驱动名字会变成ttyS0,ttyS1。。。等,不再是原来的ttyO1,ttyO2;
所以需要把/etc目录下的inittab文件的控制台初始化语句改动掉

# Put a getty on the serial port
ttyS0::respawn:/sbin/getty -L  ttyS0 115200 vt100 # GENERIC_SERIAL

之后我们测试AM3352的串口,果然串口发送数据不连续的问题解决了。。。。  开心中。。。。;
正准备划水时,测试发现RS485串口不工作了,RST引脚一直为高电平,正常你妹呀;
RS485功能原来用omap serial port 是好的呀,这下可好了,把锅补出个大洞了。
后来查代码得知,8250的串口驱动压根就没有RS485的相关部分代码,必须自己添加
按照外网的连接对8250_core.c 打RS485的
补丁

http://lkml.iu.edu/hypermail/linux/kernel/1407.1/01890.html

然后编译内核,下载运行。
然而你要的RS485通讯功能并没有出现

应用程序初始化RS485串口函数打印出异常:printf("ioctl TIOCSRS485 error.\r\n");

哎。。。。免费的确实是最贵的;又开始填坑之类旅。。。

先看应用程序的异常打印代码:


if (ioctl (fd, TIOCSRS485, &rs485conf) <
0)    //将填充的结构体写入到文件描述符中

       {

              /* Error handling.*/

              printf("ioctl TIOCSRS485
error.\r\n");

       }

是ioctl函数引起的,我们就去8250_core.c哪里看看

 static int serial8250_ioctl(struct
uart_port *port, unsigned int cmd,

                   unsigned
long arg)

{

         struct
serial_rs485 rs485conf;

         struct
uart_8250_port *up;

 up
= container_of(port, struct uart_8250_port, port);

 if
(!up->rs485_config)

     
        return -ENOIOCTLCMD; 

         switch
(cmd)

         {

         case
TIOCSRS485:

                   if
(!gpio_is_valid(up->rts_gpio))

                            return
-ENODEV;

                   if
(copy_from_user(&rs485conf, (void __user *) arg,

                                               sizeof(rs485conf)))

                            return
-EFAULT;

 

                   serial8250_config_rs485(port,
&rs485conf);

                   break;

 

         case
TIOCGRS485:

                   if
(!gpio_is_valid(up->rts_gpio))

                            return
-ENODEV;

                   if
(copy_to_user((void __user *) arg, &up->rs485,

                                               sizeof(rs485conf)))

                            return
-EFAULT;

                   break;

 

         default:

                   return
-ENOIOCTLCMD;

         }

         return
0;

}

通过prink函数打印,得出:ioctl 写入的cmd值并没有等于TIOCSRS485 ;
反复检测应用程序,没有错呀;(应用程序基于omap-serial.c模式时,RS-485可是工作得好好的呢)
真是百思不得骑姐;
后来在
drivers\tty\serial\ serial_core.c找到串口真正的ioctl入口函数

/*

 *
Called via sys_ioctl.  We can use
spin_lock_irq() here.

 */

static int

uart_ioctl(struct tty_struct *tty, unsigned
int cmd,

            unsigned long arg)

{

         struct
uart_state *state = tty->driver_data;

         struct
tty_port *port = &state->port;

         void
__user *uarg = (void __user *)arg;

         int
ret = -ENOIOCTLCMD;

 

 

         /*

          * These ioctls don't rely on the hardware to
be present.

          */

         switch
(cmd) {

         case
TIOCGSERIAL:

                   ret
= uart_get_info_user(port, uarg);

                   break;

 

         case
TIOCSSERIAL:

                   down_write(&tty->termios_rwsem);

                   ret
= uart_set_info_user(tty, state, uarg);

                   up_write(&tty->termios_rwsem);

                   break;

 

         case
TIOCSERCONFIG:

                   down_write(&tty->termios_rwsem);

                   ret
= uart_do_autoconfig(tty, state);

                   up_write(&tty->termios_rwsem);

                   break;

 

         case
TIOCSERGWILD: /* obsolete */

         case
TIOCSERSWILD: /* obsolete */

                   ret
= 0;

                   break;

         }

 

         if
(ret != -ENOIOCTLCMD)

                   goto
out;

 

         if
(tty->flags & (1 << TTY_IO_ERROR)) {

                   ret
= -EIO;

                   goto
out;

         }

 

         /*

          * The following should only be used when
hardware is present.

          */

         switch
(cmd) {

         case
TIOCMIWAIT:

                   ret
= uart_wait_modem_status(state, arg);

                   break;

         }

 

         if
(ret != -ENOIOCTLCMD)

                   goto
out;

 

         mutex_lock(&port->mutex);

 

         if
(tty->flags & (1 << TTY_IO_ERROR)) {

                   ret
= -EIO;

                   goto
out_up;

         }

 

         /*

          * All these rely on hardware being present and
need to be

          * protected against the tty being hung up.

          */

 

         switch
(cmd) {

         case
TIOCSERGETLSR: /* Get line status register */

                   ret
= uart_get_lsr_info(tty, state, uarg);

                   break;

 

         case
TIOCGRS485:

                   ret
= uart_get_rs485_config(state->uart_port, uarg);

                   break;

 

         case
TIOCSRS485:

                   ret
= uart_set_rs485_config(state->uart_port, uarg);

                   break;

         default:
{

                   struct
uart_port *uport = state->uart_port;

                   if
(uport->ops->ioctl)

                            ret
= uport->ops->ioctl(uport, cmd, arg);

                   break;

         }

         }

out_up:

         mutex_unlock(&port->mutex);

out:

         return
ret;

}

原来这个函数中的case已经定义了TIOCGRS485、TIOCSRS485 的常量了;

只有这个函数没有定义的常量,才会调用default中的ret = uport->ops->ioctl(uport, cmd, arg);自定义ioctl函数 ,也就8250_core.c的ioctl函数;

把上边2个case的内容一屏蔽;
重新编译内核。。。。。应以程序还是报告"ioctl TIOCSRS485 error.;

再回去8250_core.c中看serial8250_ioctl() 的代码,
通过打印报告得出

         if
(!gpio_is_valid(up->rts_gpio))

                            return
-ENODEV;

函数判断RS-485 rts引脚无效,然后就退出了,看来解决了rts引脚注册的问题,就能解决问题了;

RS-485补丁中8250_core.c中有个serial8250_probe_rs485()函数, 负责根据设备树的配置信息去申请对应的gpio,来控制rts引脚

int serial8250_probe_rs485(struct
uart_8250_port *up,

                   struct
device *dev)

{

         struct
serial_rs485 *rs485conf = &up->rs485;

         struct
device_node *np = dev->of_node;

         u32
rs485_delay[2];

         enum
of_gpio_flags flags;

         int
ret;

 

         rs485conf->flags
= 0;

         if
(!np)

                   return
0;

 

         /*
check for tx enable gpio */

         up->rts_gpio
= of_get_named_gpio_flags(np, "rts-gpio", 0, &flags);

         if
(up->rts_gpio == -EPROBE_DEFER)

                   return
-EPROBE_DEFER;

         if
(!gpio_is_valid(up->rts_gpio))

                   return
0;

 

         ret
= devm_gpio_request(dev, up->rts_gpio, "serial_rts");

         if
(ret < 0)

                   return
ret;

         ret
= gpio_direction_output(up->rts_gpio,

                            flags
& SER_RS485_RTS_AFTER_SEND);

         if
(ret < 0)

                   return
ret;

 

         up->rts_gpio_valid
= true;

 

         if
(of_property_read_bool(np, "rs485-rts-active-high"))

                   rs485conf->flags
|= SER_RS485_RTS_ON_SEND;

         else

                   rs485conf->flags
|= SER_RS485_RTS_AFTER_SEND;

 

         if
(of_property_read_u32_array(np, "rs485-rts-delay",

                                     rs485_delay,
2) == 0) {

                   rs485conf->delay_rts_before_send
= rs485_delay[0];

                   rs485conf->delay_rts_after_send
= rs485_delay[1];

         }

 

         if
(of_property_read_bool(np, "rs485-rx-during-tx"))

                   rs485conf->flags
|= SER_RS485_RX_DURING_TX;

 

         if
(of_property_read_bool(np, "linux,rs485-enabled-at-boot-time"))

                   rs485conf->flags
|= SER_RS485_ENABLED;

 

         return
0;

}

EXPORT_SYMBOL_GPL(serial8250_probe_rs485);

奇怪的是在内核的源码中,居然没有找到调用这个函数的地方,
而且这个函数还是被声明为:EXPORT_SYMBOL_GPL的呢

分析串口相关的代码,理清串口注册的流程如下:

Drivers\tty\serola\8250_omap.c  负责把AM335X 的串口注册为8250模式串口

8250_omap.c 函数中 static int omap8250_probe(struct platform_device *pdev)

有这一行函数:         ret = serial8250_register_8250_port(&up);

serial8250_register_8250_port()函数位于8250_core.c中,为核心函数

这个函数就把AM3352的串口注册为 8250模式,

后来想serial8250_probe_rs485()函数不是声明为:EXPORT_SYMBOL_GPL 外部调用的函数么?

那我们在serial8250_register_8250_port()函数前面调用下serial8250_probe_rs485()那么RS485的RTS引脚不就申请成功了么;

按照这样修改,重新编译内核,RS485串口RST引脚注册成功,ioctl TIOCSRS485 操作通过,RS485串口运行正常。

QQ交流群:761781147

最近编辑记录 X3 (2020-11-09 14:47:08)

离线

楼主 #7 2020-11-12 16:29:08

X3
会员
注册时间: 2020-04-08
已发帖子: 85
积分: 73.5

Re: 全志主线Linux UART串口使用8250驱动程序,但是没有实现RS485控制,有没有哪位打上了patch?

zwanh 说:

楼主,给你个自动切换的电路,不用什么切换脚。发送接收都不用加延时
https://whycan.com/files/members/3750/485%E8%87%AA%E5%8A%A8%E5%88%87%E6%8D%A2.png

感谢大佬分享,这是甲方的要求,而且他们已经贴了几千片板子了, 只能死马做活马医了。

离线

楼主 #8 2020-11-12 16:30:52

X3
会员
注册时间: 2020-04-08
已发帖子: 85
积分: 73.5

Re: 全志主线Linux UART串口使用8250驱动程序,但是没有实现RS485控制,有没有哪位打上了patch?

离线

页脚

工信部备案:粤ICP备20025096号 Powered by FluxBB

感谢为中文互联网持续输出优质内容的各位老铁们。 QQ: 516333132, 微信(wechat): whycan_cn (哇酷网/挖坑网/填坑网) service@whycan.cn