Linux那些事儿之我是Hub(15)一个都不能少

烟波江声里,何处是江南.一晃神,一转眼,我们就这样垂垂老去.可是我们直到现在还根本就没有看明白hub驱动究竟是怎么工作的.但我相信,红莲即将绽放,双星终会汇聚,命运的轮转已经开始,我们只需耐心的等待.

2686,苦苦追寻之后,终于发现从这里开始针对端口进行分析了,有几个端口就对几个端口进行分析,分析每一个端口的状态变化,一个都不能少,很显然,这就是我们期待看到的代码,马上我们就可以知道,当我们把一个usb设备插入usb端口之后究竟会发生什么,知道usb设备提供的那些接口函数究竟是如何被调用的,特别是那个probe函数.这一刻,红领巾迎着太阳,阳光洒在海面上,水中鱼儿望着我们,悄悄的听我们愉快的歌唱,小船儿轻轻飘荡在水中,迎面吹来了凉爽的风!

bNbrports是前面我们获得的hub descriptor的一个成员,表征这个hub有几个端口.很显然,月可无光,星可无语,usb设备却不可以没有描述符.这里就是遍历每一个端口.busy_bitsstruct usb_hub的一个成员,unsigned long busy_bits[1],接下来的event_bits也是,change_bits也是,unsigned long event_bits[1],unsigned long change_bits[1],test_bit()我们太熟悉了,usb-storage里面到处都是,只不过当时我们测试的是us->flags里面的某个flag是否设置了,而这里我们要测试的有三个东东,首先测试busy_bits,这个flag实际上只有在resetresume的函数内部才会设置,所以这里我们先不用管,而这里的意思是,如果眼下这个端口正在执行reset或者resume操作,那么咱们就跳过去,不予理睬.

2689,测试change_bits.结合2690,2691,2692行一起看.如果这个端口对应的change_bits没有设置,event_bits没有设置过,hub->activating也为0,那么这里就执行continue,不过我们想都不用想,因为我们就是从hub_activate进来的.我们来的时候activating就是设置成了1,所以这里的continue我们是不用执行的.换言之,我们继续往下走.

2694,hub_port_status(),portstatusportchange是我们在hub_events()伊始定义的两个变量,u16 portstatus,u16 portchange,即两个都是16.尽管说了N遍了,但是我还是得说第N+1,这个函数仍然是来自drivers/usb/core/hub.c:

   1413 static int hub_port_status(struct usb_hub *hub, int port1,

   1414                                u16 *status, u16 *change)

   1415 {

   1416         int ret;

   1417

   1418         mutex_lock(&hub->status_mutex);

   1419         ret = get_port_status(hub->hdev, port1, &hub->status->port);

   1420         if (ret < 4) {

   1421                 dev_err (hub->intfdev,

   1422                         "%s failed (err = %d)/n", __FUNCTION__, ret);

   1423                 if (ret >= 0)

   1424                         ret = -EIO;

   1425         } else {

   1426                 *status = le16_to_cpu(hub->status->port.wPortStatus);

   1427                 *change = le16_to_cpu(hub->status->port.wPortChange);

   1428                 ret = 0;

   1429         }

   1430         mutex_unlock(&hub->status_mutex);

   1431         return ret;

   1432 }

重要的是其中的那个get_port_status()函数.

    300 /*

    301  * USB 2.0 spec Section 11.24.2.7

    302  */

    303 static int get_port_status(struct usb_device *hdev, int port1,

    304                 struct usb_port_status *data)

    305 {

    306         int i, status = -ETIMEDOUT;

    307

    308         for (i = 0; i < USB_STS_RETRIES && status == -ETIMEDOUT; i++) {

    309                 status = usb_control_msg(hdev, usb_rcvctrlpipe(hdev, 0),

    310                         USB_REQ_GET_STATUS, USB_DIR_IN | USB_RT_PORT, 0, port1,

    311                         data, sizeof(*data), USB_STS_TIMEOUT);

    312         }

    313         return status;

    314 }

一路从泥泞走到美景,我们再也不会对usb_control_msg()函数陌生了,这个函数做什么勾当我们完全是一目了然.Get Port StatusHub的一个标准请求,对我们来说就是一次控制传输就可以搞定.这个请求的格式如下图所示:

其中这个GET_STATUS的对应具体的数值可以在下面这张表中对比得到,

而关于各种请求,咱们在include/linux/usb/ch9.h中也定义了相应的宏,

     72 /*

     73  * Standard requests, for the bRequest field of a SETUP packet.

     74  *

     75  * These are qualified by the bRequestType field, so that for example

     76  * TYPE_CLASS or TYPE_VENDOR specific feature flags could be retrieved

     77  * by a GET_STATUS request.

     78  */

     79 #define USB_REQ_GET_STATUS              0x00

     80 #define USB_REQ_CLEAR_FEATURE           0x01

     81 #define USB_REQ_SET_FEATURE             0x03

     82 #define USB_REQ_SET_ADDRESS             0x05

     83 #define USB_REQ_GET_DESCRIPTOR          0x06

     84 #define USB_REQ_SET_DESCRIPTOR          0x07

     85 #define USB_REQ_GET_CONFIGURATION       0x08

     86 #define USB_REQ_SET_CONFIGURATION       0x09

     87 #define USB_REQ_GET_INTERFACE           0x0A

     88 #define USB_REQ_SET_INTERFACE           0x0B

     89 #define USB_REQ_SYNCH_FRAME             0x0C

     90

     91 #define USB_REQ_SET_ENCRYPTION          0x0D    /* Wireless USB */

     92 #define USB_REQ_GET_ENCRYPTION          0x0E

     93 #define USB_REQ_RPIPE_ABORT             0x0E

     94 #define USB_REQ_SET_HANDSHAKE           0x0F

     95 #define USB_REQ_RPIPE_RESET             0x0F

     96 #define USB_REQ_GET_HANDSHAKE           0x10

     97 #define USB_REQ_SET_CONNECTION          0x11

     98 #define USB_REQ_SET_SECURITY_DATA       0x12

     99 #define USB_REQ_GET_SECURITY_DATA       0x13

    100 #define USB_REQ_SET_WUSB_DATA           0x14

    101 #define USB_REQ_LOOPBACK_DATA_WRITE     0x15

    102 #define USB_REQ_LOOPBACK_DATA_READ      0x16

    103 #define USB_REQ_SET_INTERFACE_DS        0x17

比如这里咱们传递给usb_control_msgrequest就是USB_REQ_GET_STATUS,它的值为0,usb spec中定义的GET_STATUS的值是对应的.这个请求返回两个咚咚,一个称为Port Status,一个称为Port Change Status.usb_control_msg()的调用注定了返回值将保存在struct usb_port_status *data里面,这个结构体定义与drivers/usb/core/hub.h:

     58 /*

     59  * Hub Status and Hub Change results

     60  * See USB 2.0 spec Table 11-19 and Table 11-20

     61  */

     62 struct usb_port_status {

     63         __le16 wPortStatus;

     64         __le16 wPortChange;

     65 } __attribute__ ((packed));

很显然这个格式是和实际的spec规范对应的,我们给get_port_status()传递的实参是&hub->status->port,port也是一个struct usb_port_status结构体变量,所以在hub_port_status()里面,14261427两行我们就得到了Status BitsStatus Change Bits.get_port_status()返回值就是GET PORT STATUS请求的返回数据的长度,它至少应该能够保存wPortStatuswPortChange,所以至少不能小于4,所以1420行有这么一个错误判断.这样,hub_port_status()就返回了,statuschange这两个指针也算是满载而归了,正常的话返回值就是0.

继续往下走,2699,children[i-1],这么一个冬冬我们从没有见过,但是我想白痴都知道,正是像parentchildren这样的指针才能把USB树给建立起来,而我们才刚上路,肯定还没有设置children,所以对我们来说,至少目前children数组肯定为空,而我们又知道hub->activating这时候肯定为1,所以就看第三个条件了,portstatus&USB_PORT_STAT_CONNECTION,这是啥意思?稍有悟性的人就能看出来,这表明这个端口连了设备,没错,USB_PORT_STAT_CONNECTION这个宏定义于drivers/usb/core/hub.h:

     67 /*

     68  * wPortStatus bit field

     69  * See USB 2.0 spec Table 11-21

     70  */

     71 #define USB_PORT_STAT_CONNECTION        0x0001

     72 #define USB_PORT_STAT_ENABLE            0x0002

     73 #define USB_PORT_STAT_SUSPEND           0x0004

     74 #define USB_PORT_STAT_OVERCURRENT       0x0008

     75 #define USB_PORT_STAT_RESET             0x0010

     76 /* bits 5 to 7 are reserved */

     77 #define USB_PORT_STAT_POWER             0x0100

     78 #define USB_PORT_STAT_LOW_SPEED         0x0200

     79 #define USB_PORT_STAT_HIGH_SPEED        0x0400

     80 #define USB_PORT_STAT_TEST              0x0800

     81 #define USB_PORT_STAT_INDICATOR         0x1000

     82 /* bits 13 to 15 are reserved */

     83

     84 /*

     85  * wPortChange bit field

     86  * See USB 2.0 spec Table 11-22

     87  * Bits 0 to 4 shown, bits 5 to 15 are reserved

     88  */

     89 #define USB_PORT_STAT_C_CONNECTION      0x0001

     90 #define USB_PORT_STAT_C_ENABLE          0x0002

     91 #define USB_PORT_STAT_C_SUSPEND         0x0004

     92 #define USB_PORT_STAT_C_OVERCURRENT     0x0008

     93 #define USB_PORT_STAT_C_RESET           0x0010

这都是这两个变量对应的宏,usb 2.0spec里面对这些宏的意义说得很清楚,USB_PORT_STAT_CONNECTION的意思的确是表征是否有设备连接在这个端口上,我们不妨假设有,那么portstatus和它相与的结果就是1,usb spec里面,这一位叫做Current Connect Status,于是这里我们会看到connect_change被设置成了1.而接下来,USB_PORT_STAT_C_CONNECTION则是表征这个端口的Current Connect Status位是否有变化,如果有变化,那么portchangeUSB_PORT_STAT_C_CONNECTION相与的结果就是1,对于这种情况,我们需要发送另一个请求以清除这个flag,并且将connect_change也设置为1.这个请求叫做Clear Port Feature.这个请求也是Hub的标准请求,

它的作用就是reset hub端口的某种feature.clear_port_feature()定义于drivers/usb/core/hub.c:

    162 /*

    163  * USB 2.0 spec Section 11.24.2.2

    164  */

    165 static int clear_port_feature(struct usb_device *hdev, int port1, int feature)

    166 {

    167         return usb_control_msg(hdev, usb_sndctrlpipe(hdev, 0),

    168                 USB_REQ_CLEAR_FEATURE, USB_RT_PORT, feature, port1,

    169                 NULL, 0, 1000);

    170 }

对比上面贴出来的定义可知,USB_REQ_CLEAR_FEATUREusb spec中的CLEAR_FEATURE这个请求是对应的,那么一共有些什么Feature?drivers/usb/core/hub.h中是这样定义的,

     38 /*

     39  * Port feature numbers

     40  * See USB 2.0 spec Table 11-17

     41  */

     42 #define USB_PORT_FEAT_CONNECTION        0

     43 #define USB_PORT_FEAT_ENABLE            1

     44 #define USB_PORT_FEAT_SUSPEND           2

     45 #define USB_PORT_FEAT_OVER_CURRENT      3

     46 #define USB_PORT_FEAT_RESET             4

     47 #define USB_PORT_FEAT_POWER             8

     48 #define USB_PORT_FEAT_LOWSPEED          9

     49 #define USB_PORT_FEAT_HIGHSPEED         10

     50 #define USB_PORT_FEAT_C_CONNECTION      16

     51 #define USB_PORT_FEAT_C_ENABLE          17

     52 #define USB_PORT_FEAT_C_SUSPEND         18

     53 #define USB_PORT_FEAT_C_OVER_CURRENT    19

     54 #define USB_PORT_FEAT_C_RESET           20

     55 #define USB_PORT_FEAT_TEST              21

     56 #define USB_PORT_FEAT_INDICATOR         22

而在usb spec,有一张与之相对应的表格,定义了许多的feature,如图所示:

所以,我们清除的是C_PORT_CONNECTION这一个feature.spec里面说了,清除一个状态改变的feature就等于承认这么一个feature.(clearing that status change acknowledges the change)理由很简单,每次你检测到一个flag被设置之后,你都应该清除掉它,以便下次人家一设你就知道是人家设了,否则你不清你下次判断你就不知道是不是又有人设了.同理,接下来的每个与portchange相关的判断语句都要这么做.所以如果portchange与上USB_PORT_STAT_C_CONNECTION确实为1,那么我们就要清除这个feature.同时我们当然也要记录connect_change1.

继续,每个端口都有一个开关,这叫做enable或者disable一个端口.portchangeUSB_PORT_STAT_C_ENABLE相与如果为1的话,说明端口开关有变化.和刚才一样,首先我们要做的是,清除掉这个变化的feature.但是这里需要注意,spec里对这个feature是这样规定的,如果portchangeUSB_PORT_STAT_C_ENABLE1,说明这个port是从enable状态进入了disable状态.为什么呢?因为在spec规定了,Hub的端口是不可以直接设置成enable.通常让Hub端口enable的方法是reset hub port.spec的话说,这叫做发送另一个request,名为SET_FEATURE.SET_FEATURECLEAR_FEATURE是对应的,一个设置一个清除. 对于PORT_ENABLE这一位,spec里的话说,This bit may be set only as a result of a SetPortFeature(PORT_RESET) request,PORT_RESET是为hub定义的众多feature中的一种.最后提醒一点,27112715这段if语句仅仅是为了打印调试信息的,就是说如果port enable改变了,但是端口连接没有改变,那么打印出信息来通知调试者,不要把clear_port_feature这一行也纳入到if语句里去了.因为port enable的改变有多种可能,其中一种可能就是由于检测到了disconnection,但是对于这种情况,我们下面要处理的,所以甭急.

下面这段代码就比较帅了,电磁干扰都给扯进来了.EMI,就是电磁干扰.就是说有的时候hub portenable变成disable有可能是由于电磁干扰造成的,这个if条件判断的是,端口被disable,但是连接没有变化,并且hdev->children[i]还有值,这就说明明明有子设备连在端口上,可是端口却被disable,基本上这种情况就是电磁干扰造成的,否则hub端口不会有这种抽风的举动.那么这种情况就设置connect_change1.因为接下来我们会看到对于connect_change1的情况,我们会专门进行处理,而更犀利更一针见血的说法就是,hub_events()其实最重要的任务就是对于端口连接有变化的情况进行处理,或者说进行响应.

再往下,portchangeUSB_PORT_STAT_C_SUSPEND相与如果为1,表明连在该端口的设备的suspend状态有变化,并且是从suspended状态出来,也就是说resume完成.(别问我为什么,spec就这么规定的,没什么理由,一定要问理由那你问郑源去,他不是唱那什么如果你真的需要什么理由,一万个够不够吗.)那么首先我们就调用clear_port_feature清掉这个feature.接下来这个if牵扯到的东西比较高深,涉及到电源管理中很华丽的部分,我们只能先跳过.否则深陷其中难免会走火入魔欲罢不能.总之这里做的就是对于该端口连了子设备的情况就把子设备唤醒,否则如果端口没有连子设备,那么就把端口disable.

2754,portchange如果和USB_PORT_STAT_C_OVERCURRENT相与结果为1的话,说明这个端口可能曾经存在电流过大的情况,而现在这种情况不存在了,或者本来不存在而现在存在了.对此我们能做的就是首先清除这个feature.有一种比较特别的情况是,如果其它的端口电流过大,那么将会导致本端口断电,hub上一个端口出现over-current条件将有可能引起hub上其它端口陷入powered off的状态.不管怎么说,对于over-current的情况我们都把hub重新上电,执行hub_power_on().

2763,portchange如果和USB_PORT_STAT_C_RESET相与为1的话,这叫做一个端口从Resetting状态进入到Enabled状态.

2771,connect_change如果为1,就执行hub_port_connect_change(),啥也不说了,这是每一个看hub驱动的人最期待的函数,因为这正是我们的原始动机,即当一个usb设备插入usb接口之后究竟会发生什么,usb设备驱动程序提供那个probe函数究竟是如何被调用的.这些疑问统统会在这个函数里得到答案.来自drivers/usb/core/hub.c:

   2404 /* Handle physical or logical connection change events.

   2405  * This routine is called when:

   2406  *      a port connection-change occurs;

   2407  *      a port enable-change occurs (often caused by EMI);

   2408  *      usb_reset_device() encounters changed descriptors (as from

   2409  *              a firmware download)

   2410  * caller already locked the hub

   2411  */

   2412 static void hub_port_connect_change(struct usb_hub *hub, int port1,

   2413                                         u16 portstatus, u16 portchange)

   2414 {

   2415         struct usb_device *hdev = hub->hdev;

   2416         struct device *hub_dev = hub->intfdev;

   2417         u16 wHubCharacteristics = le16_to_cpu(hub->descriptor->wHubCharacteristics);

   2418         int status, i;

   2419

   2420         dev_dbg (hub_dev,

   2421                 "port %d, status %04x, change %04x, %s/n",

   2422                 port1, portstatus, portchange, portspeed (portstatus));

   2423

   2424         if (hub->has_indicators) {

   2425                 set_port_led(hub, port1, HUB_LED_AUTO);

   2426                 hub->indicator[port1-1] = INDICATOR_AUTO;

   2427         }

   2428

   2429         /* Disconnect any existing devices under this port */

   2430         if (hdev->children[port1-1])

   2431                 usb_disconnect(&hdev->children[port1-1]);

   2432         clear_bit(port1, hub->change_bits);

   2433

   2434 #ifdef  CONFIG_USB_OTG

   2435         /* during HNP, don't repeat the debounce */

   2436         if (hdev->bus->is_b_host)

   2437                 portchange &= ~USB_PORT_STAT_C_CONNECTION;

   2438 #endif

   2439

   2440         if (portchange & USB_PORT_STAT_C_CONNECTION) {

   2441                 status = hub_port_debounce(hub, port1);

   2442                 if (status < 0) {

   2443                         if (printk_ratelimit())

   2444                                 dev_err (hub_dev, "connect-debounce failed, "

   2445                                                 "port %d disabled/n", port1);

   2446                         goto done;

   2447                 }

   2448                 portstatus = status;

   2449         }

   2450

   2451         /* Return now if nothing is connected */

   2452         if (!(portstatus & USB_PORT_STAT_CONNECTION)) {

   2453

   2454                 /* maybe switch power back on (e.g. root hub was reset) */

   2455                 if ((wHubCharacteristics & HUB_CHAR_LPSM) < 2

   2456                                 && !(portstatus & (1 << USB_PORT_FEAT_POWER)))

   2457                         set_port_feature(hdev, port1, USB_PORT_FEAT_POWER);

   2458

   2459                 if (portstatus & USB_PORT_STAT_ENABLE)

   2460                         goto done;

   2461                 return;

   2462         }

   2463

   2464 #ifdef  CONFIG_USB_SUSPEND

   2465         /* If something is connected, but the port is suspended, wake it up. */

   2466         if (portstatus & USB_PORT_STAT_SUSPEND) {

   2467                 status = hub_port_resume(hub, port1, NULL);

   2468                 if (status < 0) {

   2469                         dev_dbg(hub_dev,

   2470                                 "can't clear suspend on port %d; %d/n",

   2471                                 port1, status);

   2472                         goto done;

   2473                 }

   2474         }

   2475 #endif

   2476

   2477         for (i = 0; i < SET_CONFIG_TRIES; i++) {

   2478                 struct usb_device *udev;

   2479

   2480                 /* reallocate for each attempt, since references

   2481                  * to the previous one can escape in various ways

   2482                  */

   2483                 udev = usb_alloc_dev(hdev, hdev->bus, port1);

   2484                 if (!udev) {

   2485                         dev_err (hub_dev,

   2486                                 "couldn't allocate port %d usb_device/n",

   2487                                 port1);

   2488                         goto done;

   2489                 }

   2490

   2491                 usb_set_device_state(udev, USB_STATE_POWERED);

   2492                 udev->speed = USB_SPEED_UNKNOWN;

   2493                 udev->bus_mA = hub->mA_per_port;

   2494                 udev->level = hdev->level + 1;

   2495

   2496                 /* set the address */

   2497                 choose_address(udev);

   2498                 if (udev->devnum <= 0) {

   2499                         status = -ENOTCONN;     /* Don't retry */

   2500                         goto loop;

   2501                 }

   2502

   2503                 /* reset and get descriptor */

   2504                 status = hub_port_init(hub, udev, port1, i);

   2505                 if (status < 0)

   2506                         goto loop;

   2507

   2508                 /* consecutive bus-powered hubs aren't reliable; they can

   2509                  * violate the voltage drop budget.  if the new child has

   2510                  * a "powered" LED, users should notice we didn't enable it

   2511                  * (without reading syslog), even without per-port LEDs

   2512                  * on the parent.

   2513                  */

   2514                 if (udev->descriptor.bDeviceClass == USB_CLASS_HUB

   2515                                 && udev->bus_mA <= 100) {

   2516                         u16     devstat;

   2517

   2518                         status = usb_get_status(udev, USB_RECIP_DEVICE, 0,

   2519                                         &devstat);

   2520                         if (status < 2) {

   2521                                 dev_dbg(&udev->dev, "get status %d ?/n", status);

   2522                                 goto loop_disable;

   2523                         }

   2524                         le16_to_cpus(&devstat);

   2525                         if ((devstat & (1 << USB_DEVICE_SELF_POWERED)) == 0) {

   2526                                 dev_err(&udev->dev,

   2527                                         "can't connect bus-powered hub "

   2528                                         "to this port/n");

   2529                                 if (hub->has_indicators) {

   2530                                         hub->indicator[port1-1] =

   2531                                                 INDICATOR_AMBER_BLINK;

   2532                                         schedule_delayed_work (&hub->leds, 0);

   2533                                 }

   2534                                 status = -ENOTCONN;     /* Don't retry */

   2535                                 goto loop_disable;

   2536                         }

   2537                 }

   2538

   2539                 /* check for devices running slower than they could */

   2540                 if (le16_to_cpu(udev->descriptor.bcdUSB) >= 0x0200

   2541                                 && udev->speed == USB_SPEED_FULL

   2542                                 && highspeed_hubs != 0)

   2543                         check_highspeed (hub, udev, port1);

   2544

   2545                 /* Store the parent's children[] pointer.  At this point

   2546                  * udev becomes globally accessible, although presumably

   2547                  * no one will look at it until hdev is unlocked.

   2548                  */

   2549                 status = 0;

   2550

   2551                 /* We mustn't add new devices if the parent hub has

   2552                  * been disconnected; we would race with the

   2553                  * recursively_mark_NOTATTACHED() routine.

   2554                  */

   2555                 spin_lock_irq(&device_state_lock);

   2556                 if (hdev->state == USB_STATE_NOTATTACHED)

   2557                         status = -ENOTCONN;

   2558                 else

   2559                         hdev->children[port1-1] = udev;

   2560                 spin_unlock_irq(&device_state_lock);

   2561

   2562                 /* Run it through the hoops (find a driver, etc) */

   2563                 if (!status) {

   2564                         status = usb_new_device(udev);

   2565                         if (status) {

   2566                                 spin_lock_irq(&device_state_lock);

   2567                                 hdev->children[port1-1] = NULL;

   2568                                 spin_unlock_irq(&device_state_lock);

   2569                         }

   2570                 }

   2571

   2572                 if (status)

   2573                         goto loop_disable;

   2574

   2575                 status = hub_power_remaining(hub);

   2576                 if (status)

   2577                         dev_dbg(hub_dev, "%dmA power budget left/n", status);

   2578

   2579                 return;

   2580

   2581 loop_disable:

   2582                 hub_port_disable(hub, port1, 1);

   2583 loop:

   2584                 ep0_reinit(udev);

   2585                 release_address(udev);

   2586                 usb_put_dev(udev);

   2587                 if (status == -ENOTCONN)

   2588                         break;

   2589         }

   2590

   2591 done:

   2592         hub_port_disable(hub, port1, 1);

   2593 }

到今天我算是看明白了 , 内核里面这些函数 , 没有最变态只有更变态 , 变态哪都有 , 可是开源社区尤其多 ! 你们他妈的不是我的冤家派来故意玩我的吧 ? 面对这个函数 , 我真的想吐血 ! 我打算不像过去那样一行一行讲了 , 我必须先来个提纲挈领 , 必须先开门见山把这个函数的哲学思想讲清楚 , 否则一行一行往下讲肯定晕菜 . 
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值