s3c2440平台linux2.6.32双网卡驱动

s3c2440平台linux2.6.32双网卡驱动

网络设备驱动编写

前段时间由于一个工业以太网的需求,需要在S3C2440平台上实现双网卡。其中由DM9000构成的100M网络用于满足作为WEB服务器的需求,另外一路由于直接与设备相连,对带宽要求较低,另外为了接口方便选用了SPI接口的ENC28J60以实现一个10M的网络,并工作于混杂模式。

Linux下的网络设备驱动位于/linux/driver/net目录下,其中已经包含了许多常用的以太网控制器的驱动,包括CS8019DM9000等。由于ENC28J60是新出的一款专为单片机设计的以太网控制器,所以在现有内核驱动中还尚未得到支持,那么重写ENC28J60设备驱动成了必然的选择。首先无论是linux下的网卡驱动还是单片程序的裸机驱动,有几个地方是完全相同的,而且是必须做的工作:

以太网控制器正确初始化。对ENC28J60而言其初始化的过程包括:

1) S3C2440 SPI接口的初始化

2) ENC28J60复位

3) 发送控制寄存器、接收控制寄存器、发送和接收FIFO的设置以及物理地址的设置

以太网控制器发送数据。

接收数据函数,其中可能采用中断接收或查询方式。

关于底层的数据收发函数的实现,由于网上有较多的单片机代码可以参考这里就不详述了。有了最底层的sendreceive函数以后,接下来的问题就是如何将这些函数与linux内核联系起来,这也将是网络设备驱动与裸机驱动最本质的区别。接下来就以表格的形式列出网络驱动和字符设备的区别,如下表:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

类型

字符设备

网络设备

 

设备的数据结构

struct cdev

struct net_device

 

设备数据结构的分配

cdev_alloc();

struct net_device *alloc_netdev(int sizeof_priv,

constchar*name,

void(*setup)(struct net_device));

 

设备数据结构

初始化

void cdev_init(struct cdev*dev,

structfile_operations *f_ops)

对于以太网设备可以通过ether_setup(struct net_deice *dev)函数进行以太网的部分缺省设置,其他异构网络需要根据实际情况设置struct net_deice

 

 

驱动注册

int cdev_add(struct cdev*dev,dev_t num,unsigned int count);

 

 

int register_netdev(struct net_device *dev)设备注册之前必须保证网卡初始化可用。

 

设备操作方法

封装在struct file_operations中,包括open、write、read等

封装在struct net_device_ops中,

ndo_open、ndo_stop、ndo_start_xmit

ndo_tx_timeout、ndo_do_ioctl、

ndo_set_mac_address等

 

设备号

分为主、次设备号,访问通过设备节点完成

 

无主、次设备号及设备文件

 

获取上层数据

使用copy_from_user等从用户空间获取数据

通过读取网络层传递的struct sk_buff结构获取待发送数据,并释放struct sk_buff结构

 

向上层提交数据

使用copy_to_user等将内核空间数据提交至用户空间

将接受的数据封装在申请的struct sk_buff结构中,并使用int netif_rx(struct sk_buff *skb)将数据提交至网络层

上面将网络设备驱动和字符设备驱动做了比较,那么接下来我们不妨借鉴设计字符设备驱动的方法来重写网络设备驱动。

首先还是以模块的形式来加载,那么module_init是必须要做的工作,重写如下:

module_init(enc28j60_init_module);

module_exit(enc28j60_cleanup);

 

MODULE_AUTHOR("whut");

MODULE_DESCRIPTION("NE28j60 network driver");

MODULE_LICENSE("GPL");

 

接下来就是编写enc28j60_init_module的实现了,

 

805 int __init enc28j60_init_module (void)

806 {

807 int result;

808 base_addr=ioremap(S3C2410_PA_SPI,0x18);

809 PRINTK("io map sucess!!/n");

810 init_SPI();   //25M

811 //init_SPI(BandRate_10Mbps);

812 strcpy (enc28j60.name, "eth%d");

813 //enc28j60.name="NET28J60";

814 enc_device_init(); //enc28j60初始化

815 if ((result = register_netdev (&enc28j60))) {

816 PRINTK ("enc28j60: Error %d  initializing enc28j60 based device",result);

817 return result;

818 }

819 return 0;

820 }

810行就是初始化S3C2440SPI模块以适应ENC28J60要求

814行明显将要做enc28j60的硬件初始化,以及我们刚才所说的struct net_device机构的初始化,以便815行的register_netdev调用。

815行注册成功过后,设备就进入了正式工作状态,同时proc/device中将出现他的身影。

[enc_device_init()]

717 void enc_device_init(void)

718 {

719

720 enc28j60WriteOp(ENC28J60_SOFT_RESET, 0, ENC28J60_SOFT_RESET);

721 udelay(1000);   //复位后的延时

722

723 enc28j60PhyWrite(PHLCON, 0x3412);

724

725     enc28j60SetBank(ECON1);

726

727     NextPacketPtr = RXSTART_INIT;

728     enc28j60Write(ERXSTL, RXSTART_INIT&0xFF);

729     enc28j60Write(ERXSTH, RXSTART_INIT>>8);

730     // set receive pointer address

731     enc28j60Write(ERXRDPTL, RXSTART_INIT&0xFF);

732     enc28j60Write(ERXRDPTH, RXSTART_INIT>>8);

733     

734 enc28j60Write(ERDPTL, RXSTART_INIT&0xFF);

735 enc28j60Write(ERDPTH, RXSTART_INIT>>8);

736     //   enc28j60Write(ERXWRPTL,RXSTART_INIT&0xFF); //set the wr pointer or receive area

737     //   enc28j60Write(ERXWRPTH,RXSTART_INIT>>8);   

738 // enc28j60Write(ERXRDPTL,RXSTART_INIT&0xFF); 

739 // enc28j60Write(ERXRDPTH,RXSTART_INIT);      

740     

741     // set receive buffer end:

742     // ERXND defaults to 0x1FFF (end of ram)

743     enc28j60Write(ERXNDL, RXSTOP_INIT&0xFF);

744     enc28j60Write(ERXNDH, RXSTOP_INIT>>8);

745     // set transmit buffer start

746     // ETXST defaults to 0x0000 (beginnging of ram)

747     enc28j60Write(ETXSTL, TXSTART_INIT&0xFF);

748     enc28j60Write(ETXSTH, TXSTART_INIT>>8);

749

750     PRINTK("bank2 enc28j60 init process!/n");

751

752     // do bank 2 stuff

753     enc28j60Write(MACON1, MACON1_MARXEN|MACON1_TXPAUS|MACON1_RXPAUS);

754     // bring MAC out of reset

755     enc28j60Write(MACON2, 0x00);

756     // enable automatic padding and CRC operations

757 //   enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, MACON3, MACON3_PADCFG0|MACON3_TXCRCEN|MACON3_FRMLNEN);  //b s g

758 enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, MACON3, MACON3_PADCFG0|MACON3_TXCRCEN|MACON3_FRMLNEN|MACON3_FULDPX);  //q s g

759     enc28j60Write(MACON3, MACON3_PADCFG0|MACON3_TXCRCEN|MACON3_FRMLNEN);    

760     // set inter-frame gap (non-back-to-back)    

761     enc28j60Write(MAIPGL, 0x12);    

762     enc28j60Write(MAIPGH, 0x0C);    

763     // set inter-frame gap (back-to-back)    

764 // enc28j60Write(MABBIPG, 0x12);     // b s g  半双工

765     enc28j60Write(MABBIPG, 0x15);     // q s g   全双工

766     

767     enc28j60PhyWrite(PHCON1,0x0100); //q s g

768     

769     // Set the maximum packet size which the controller will accept    

770     enc28j60Write(MAMXFLL, MAX_FRAMELEN&0xFF);      

771     enc28j60Write(MAMXFLH, MAX_FRAMELEN>>8);    

772     

773     PRINTK("bank3 enc28j60 init process!/n");    

774     // do bank 3 stuff    

775     // write MAC address    

776     // NOTE: MAC address in ENC28J60 is byte-backward    

777     enc28j60Write(MAADR5, ENC28J60_MAC0);    

778     enc28j60Write(MAADR4, ENC28J60_MAC1);    

779     enc28j60Write(MAADR3, ENC28J60_MAC2);    

780     enc28j60Write(MAADR2, ENC28J60_MAC3);    

781     enc28j60Write(MAADR1, ENC28J60_MAC4);    

782     enc28j60Write(MAADR0, ENC28J60_MAC5);    

783     

784     // no loopback of transmitted frames    

785     PRINTK("phy enc28j60 init process!/n");    

786     enc28j60PhyWrite(PHCON2, PHCON2_HDLDIS);    

787     // enable interrutps    

788     // enable packet reception    

789     

790     enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_RXEN);    

791 //   enc28j60Write(ERXFCON, 0x00);

792     enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, EIR, EIR_PKTIF|EIR_TXIF|EIR_RXERIF|EIR_TXERIF); 

793     PRINTK("enabling interupts!/n");

794     enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, EIE, EIE_INTIE|EIE_PKTIE|EIE_TXIE|EIE_RXERIE|EIE_TXERIE); 

795

796     // Reenable receive logic

797     enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_RXEN);

798     PRINTK("enc28j60 device initialized/n");

799 }

这段代码无疑是一段赤裸裸的裸机驱动程序,复位enc28j60、设置FIFO、设置发送接收控制寄存器。

硬件初始化了,但是struct net_device结构却迟迟未能出现,其实这里填充了一个struct net_deviceinit方法,这是在注册struct net_device的时候会提前调用的。

803 struct net_device enc28j60 = {init: enc28j60_init};

进入enc28j60_init函数

659 int __init enc28j60_init (struct net_device *dev)

660 {

661 board_info_t *db;

662 u16 temp;

663 db=(void *)(kmalloc(sizeof(*db),GFP_KERNEL));

664 memset(db,0,sizeof(*db));

665 dev->priv = db ;

666 memset(dev->priv, 0, sizeof(struct net_device_stats));

667 dev->get_stats = get_stats;

668 enc_dev    = dev;

669 dev->open = &enc28j60_open;

670 dev->stop = &enc28j60_release;

671 dev->hard_start_xmit = &enc28j60_xmit;

672 dev->tx_timeout =&net_timeout;

673 dev->watchdog_timeo=5*HZ;

674 dev->irq=IRQ_EINT0;

675 spin_lock_init(&lplock);

676 dev->dev_addr[0] = 0x80;// 0x11;i

677 dev->dev_addr[1] = 0xe2;

678 dev->dev_addr[2] = 0x66;

679 dev->dev_addr[3] = 0x60;

680 dev->dev_addr[4] = 0x00;

681 dev->dev_addr[5] = 0x01;

682 nicSetMacAddress(dev->dev_addr);

683 {

684 u8 checkaddr[6];

685 nicGetMacAddress(checkaddr);

686 if(memcmp(checkaddr, dev->dev_addr, 6) != 0)

687 {

688 kfree(dev->priv);

689 dev->priv = NULL;

690 PRINTK("enc28j60 error! may be no ic(%2X:%2X:%2X:%2X:%2X:%2X)!/n", checkaddr[0], checkaddr[0], checkaddr[1], checkaddr[2], checkaddr[3], checkaddr[4], checkaddr[5]);

691 return -ENODEV;

692 }

693 }

694 PRINTK("set macaddr sucess!/n");

695 ether_setup(dev);

696 temp=enc28j60PhyRead(PHHID1);

697 if(temp==0x83)

698 PRINTK("EN28J60 init ok!/n");

699 #ifdef DEBUG

700 PRINTK("NET ID:%d/n",temp);

701 #endif

702      PRINTK("dev->hard_header_len: 0x%02x/n",dev->hard_header_len);

703     // do bank 0 stuff

704     // initialize receive buffer

705     // 16-bit transfers, must write low byte first

706     // set receive buffer start address

707     PRINTK("starting enc28j60 init process!/n");

708 return 0;

709 }

这段就是对struct net_device的初始化了,

669 dev->open = &enc28j60_open; //打开网络设备,将会通过netif_start_queue (dev); 启动接口的发送队列允许它接受发送报文 ), 一旦它准备好启动发送数据

670 dev->stop = &enc28j60_release; //关闭网络驱动,停止设备

671 dev->hard_start_xmit = &enc28j60_xmit;//发送数据包,对应裸机驱动的发送函数

dev->tx_timeout =&net_timeout; //发送超时时调用的处理函数

695行调用ether_setup(dev);对这个网络设备初始化为默认的以太网设备。

接下来就是到了opensendreceive的问题了。前面说过,对open来说就是初始化驱动需要支持的内容,比如定时器、工作队列、申请中断等等的初始化,并开启发送队列。

413 int enc28j60_open (struct net_device *dev)

414 {

415 int result;

416 board_info_t * db = (board_info_t *)dev->priv;

417 PRINTK("enc28j60 open!/n");

418 enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, EIE, EIE_INTIE|EIE_PKTIE|EIE_TXIE|EIE_RXERIE|EIE_TXERIE);  

419 //注册中断

420 if (request_irq(dev->irq, &enc28j60_Interrupt, IRQ_TYPE_EDGE_FALLING, "enc28j60", dev))//申请中断

421 {

422 PRINTK("enc28j60 IRQ%d request fail!/n",dev->irq);

423 return -EAGAIN;

424 }

425 #ifdef DEBUG

426 else

427 PRINTK("request irq sucess!!!/n");

428 #endif

429 enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, EIE, EIE_INTIE|EIE_PKTIE|EIE_TXIE|EIE_RXERIE|EIE_TXERIE);  

430 //打开EIE中断

431 /* set and active a timer process */

432 init_timer(&db->timer);

433 db->timer.expires  = DMFE_TIMER_WUT * 2;

434 db->timer.data  = (unsigned long)dev;

435 db->timer.function  = &enc_timer;

436 //add_timer(&db->timer);//打开定时中断

437 db->tx_pkt_cnt = 0;

438 db->queue_pkt_len = 0;

439 dev->trans_start = 0;

440 netif_start_queue (dev);

441 int i;

442 for(i=0;i<30;i++)

443 enc28j60_send();

444 return 0;

445 }

关于发送的实现就更加简单了,只需将裸机驱动中的发送数据替换成struct sk_buff中的内容即可。发送完成后将struct sk_buff释放。

487 static int enc28j60_xmit (struct sk_buff *skb, struct net_device *dev)

488 {

489 unsigned char stat;

490 //struct net_local *lp=(struct net_local *)dev->priv;

491 spin_lock_irq(&lplock);//互斥锁

492 netif_stop_queue(dev); //停止网络发送

493 #ifdef DEBUG

494 PRINTK("EN28J60 send!!/n");

495 #endif

496 /* Disable all interrupt */   

497 enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, EIE, EIE_INTIE);  

498 enc28j60Write(EWRPTL, TXSTART_INIT);  // 28j60缓冲区写入的位置

499 enc28j60Write(EWRPTH, TXSTART_INIT>>8);

500 enc28j60Write(ETXSTL, TXSTART_INIT);  //28j60缓冲区开始位置

501 enc28j60Write(ETXSTH, TXSTART_INIT>>8);

502 // Set the TXND pointer to correspond to the packet size given

503 enc28j60Write(ETXNDL, (TXSTART_INIT+skb->len)); //skb->len发送数据的长度

504 enc28j60Write(ETXNDH, (TXSTART_INIT+skb->len)>>8);

505 stat=enc28j60Read(ESTAT);

506 if(stat&2)//检查是否有发送错误

507 {

508 PRINTK("send erro!/n");

509 spin_unlock_irq(&lplock);

510 return 1;

511 }

512 // write per-packet control byte   lht

513 enc28j60WriteOp(ENC28J60_WRITE_BUF_MEM, 0, 0x00);  //use MCON3  参考42

514 // copy the packet into the transmit buffer

515 enc28j60WriteBuffer(skb->len, skb->data);

516 //udelay(100);

517 // send the contents of the transmit buffer onto the network

518 enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRTS);

519 udelay(400);//可以调试,有可能影响稳定性 可以更改

520 spin_unlock_irq(&lplock);

521 dev->trans_start=jiffies;//加入时间戳

522 dev_kfree_skb(skb);//释放空间

523 //     netif_wake_queue(dev);

524 udelay(200);//可以调试,有可能影响稳定性 可以更改

525 enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, EIE, EIE_INTIE|EIE_PKTIE|EIE_TXIE|EIE_RXERIE|EIE_TXERIE); 

526 return 0;

527 }

528

529

530 /*****************************************************************************/

531 static int enc28j60_send (void)

532 {

533 unsigned char stat,i;

534 char buff[70];

535 unsigned char tx_length=69;

536 for(i=0;i<70;i++)

537 buff[i]=i;

538 spin_lock_irq(&lplock);//互斥锁

539 PRINTK("EN28J60 send!!/n");

540 /* Disable all interrupt */   

541 enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, EIE, EIE_INTIE);  

542 //   mc=enc28j60Read(MAADR5);

543 //printk("mac5=%x/n",mc);

544 // Set the write pointer to start of transmit buffer area  

545 enc28j60Write(EWRPTL, TXSTART_INIT);  // 28j60缓冲区写入的位置

546 enc28j60Write(EWRPTH, TXSTART_INIT>>8);

547

548 enc28j60Write(ETXSTL, TXSTART_INIT);  //28j60缓冲区开始位置

549 enc28j60Write(ETXSTH, TXSTART_INIT>>8);

550 // Set the TXND pointer to correspond to the packet size given

551 enc28j60Write(ETXNDL, (TXSTART_INIT+tx_length)); //skb->len发送数据的长度

552 enc28j60Write(ETXNDH, (TXSTART_INIT+tx_length)>>8);

553 stat=enc28j60Read(ESTAT);

554 // printk("ESTAT=%x/n",stat);

555 if(stat&2)//检查是否有发送错误

556 {

557 PRINTK("send erro!/n");

558 spin_unlock_irq(&lplock);

559 return 1;

560 }

561 // write per-packet control byte   lht

562 enc28j60WriteOp(ENC28J60_WRITE_BUF_MEM, 0, 0x06);  //use MCON3  参考42

563 // copy the packet into the transmit buffer

564 enc28j60WriteBuffer(tx_length, buff);

565 //udelay(100);

566 // send the contents of the transmit buffer onto the network

567 enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRTS);

568 udelay(400);//可以调试,有可能影响稳定性 可以更改

569 spin_unlock_irq(&lplock);

570 //     netif_wake_queue(dev);

571 udelay(200);//可以调试,有可能影响稳定性 可以更改

572 enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, EIE, EIE_INTIE|EIE_PKTIE|EIE_TXIE|EIE_RXERIE|EIE_TXERIE); 

573 return 0;

574 }

上面代码与裸机驱动差异只在struct sk_buff上面,所以实现起来也就较为简单了。

最后,接收的代码就出现在中断处理函数中了,收到数据需要重新申请struct sk_buff,然后根据接收的内容进行相应的填充,最后由netif_rx(skb);提交给网络层。

577 static irqreturn_t enc28j60_Interrupt(int irq, void *dev_id)

578 {

579 struct net_device *dev = dev_id;

580 struct sk_buff *skb;

581     u16 rxstat;

582 u16 len=0;

583 u16 status,PKCNT;

584 enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, EIE, EIE_INTIE);  //disable irq

585 status=enc28j60Read(EIR);

586 enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, EIR, status); 

587 if(status & EIR_TXIF) //发送中断

588     {

589 #ifdef DEBUG

590     PRINTK("tx packet ok!!/n");

591 #endif

592        enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, EIR, EIR_TXIF); 

593 netif_wake_queue(dev); 

594

595

596 if(status & EIR_PKTIF)  //接收中断

597 {

598 do{

599 NextPacketPtr  = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0); //下个数据包起始地址 参考45

600 NextPacketPtr |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0)<<8;

601 //printk("NextPacketPtr=%x/n",NextPacketPtr);

602 // re ad the packet length

603 len  = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);  //数据包长度

604 len |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0)<<8;

605

606 // printk("len=%x/n",len);

607

608 rxstat  = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);

609 rxstat |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0)<<8;

610 /

611

612 skb = dev_alloc_skb(len+2);

613 skb->dev = dev;

614 skb_reserve(skb,2);

615 skb_put(skb,len);

616 // copy the packet from the receive buffer

617 enc28j60ReadBuffer(len, skb->data);

618 // Move the RX read pointer to the start of the next received packet

619 // This frees the memory we just read out

620 #ifdef DEBUG

621 PRINTK("rx sucess!/n")

622 PRINTK("length=%d/n",len);

623 u16 i;

624 for(i=0;i<len;i++)

625 PRINTK("%2X    ",(u8)(*(skb->data+i)));

626 PRINTK("/n");

627 #endif

628 enc28j60Write(ERXRDPTL, (NextPacketPtr)); 

629 enc28j60Write(ERXRDPTH, (NextPacketPtr)>>8);

630 enc28j60Write(ERDPTL, (NextPacketPtr));

631 enc28j60Write(ERDPTH, (NextPacketPtr)>>8);   //不用.可以自动变化 ECON2.AUTOINC置位

632

633 // decrement the packet counter indicate we are done with this packet

634 enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_PKTDEC);

635 skb->protocol= eth_type_trans(skb,dev);

636 //skb->ip_summed = CHECKSUM_UNNECESSARY;

637 netif_rx(skb); //数据交给上层网络

638 //udelay(100);

639 dev->last_rx=jiffies;     

640 PKCNT=enc28j60Read(EPKTCNT); //缓冲区还有多少包?

641 }while(PKCNT!=0); //

642     }

643 if(status& EIE_TXERIE)

644 {

645 PRINTK("TX erro/n");

646 enc_device_init();

647 netif_wake_queue(dev); 

648 }

649 if(status& EIE_RXERIE) 

650 {

651 //enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, EIR, EIE_RXERIE); 

652 PRINTK("RX erro/n");;

653 enc_device_init();

654 netif_wake_queue(dev); 

655 }

656 enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, EIE, EIE_INTIE|EIE_PKTIE|EIE_TXIE|EIE_RXERIE|EIE_TXERIE); 

657 return IRQ_HANDLED;

658 }

以上就是一个网络设备驱动几个比较关键的环节了,最后的工作就是简单修改makefilekconfig的内容使其支持该驱动即可。以模块形式加载以后如果无异常,在proc/device中将出现设备的名字。通过ifconfig+设备名+ip。将激活网络设备。最后注意两张网卡不能处于同一网段,否则无法工作。一切完了之后,就可以用PING命令来测试网络连接了。

其实从上面的实现过程中我们不难发现,网络设备驱动与裸机驱动最大的区别就是需要填充几个网络层的接口函数,并且要通过相应的注册函数将内核指定的数据结构注册进内核。这一过程恰恰和字符设备的几个过程惊人的相似,所以参考最熟悉的字符设备驱动编写的几个流程,来编写网络设备驱动就异常清晰了。

源码链接http://download.csdn.net/source/2759030

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值