linux下的蓝牙驱动程序详解

http://blog.csdn.net/gotowu/article/details/46687633


1、首先要做Bluez协议栈的移植,这样在开发板上才可以用hciconfig, hcitool等命令。关于bluez协议栈的移植步骤网上很多。

2、该驱动是USB蓝牙设备驱动,分析根据蓝牙驱动的写的顺序进行。因为只是要做数据的传输,所以讲用于语音的等时传输部分去掉了。

首先,定义一个结构体

[cpp]  view plain  copy
  1. struct bcm_data ={  
  2.     struct usb_endpoint_descriptor *intr_ep;  
  3.     struct usb_endpoint_descriptor *bulk_tx_ep;     //批量传输的收端点  
  4.     struct usb_endpoint_descriptor *bulk_rx_ep;    //批量传输的收端点  
  5.   
  6.     struct usb_anchor tx_anchor;             //用于阻塞操作  
  7.     struct usb_anchor intr_anchor;  
  8.     struct usb_anchor bulk_anchor;  
  9.   
  10.     struct usb_device *udev;  
  11.     struct usb_interface *intf;  
  12.   
  13.     unsigned long flags;  
  14.   
  15.     __u8 cmdreq_type;  
  16. }  

接下来是入口函数和出口函数

[cpp]  view plain  copy
  1. static int __init bcm_driver_init(void)  
  2. {  
  3.     usb_register(&bcm_driver);  
  4.     return 0;  
  5. }  
  6.   
  7. static void __exit bcm_driver_exit(void)  
  8. {  
  9.     usb_deregister(&bcm_driver);  
  10. }  
  11. module_init(bcm_driver_init);  
  12. module_exit(bcm_driver_exit);  
  13. MODULE_LICENSE("GPL");  
  14. MODULE_AUTHOR("WillwWu")  

入口函数和出口函数是对该USB设备进行注册和注销的操作。

然后是定义struct usb_driver,并对其成员进行填充。

[cpp]  view plain  copy
  1. static struct usb_driver bcm_driver={  
  2.     .name           = "BCMT",  
  3.     .probe      = bcm_probe,       //探测函数  
  4.     .disconnect = bcm_disconnect,  
  5.     .id_table       = bcm_table,        //所支持的USB设备表  
  6.     .supports_autosuspend = 1,        //支持自动挂起,若是设置为0则不支持  
  7.     .disable_hub_initiated_lpm = 1,    //允许低功率态的传输  
  8. };  

支持的USB设备表

[cpp]  view plain  copy
  1. static usb_device_id bcm_table[]={  
  2.     {   USB_DEVICE(0x0a5c, 0x2148)},  
  3.         {},  
  4. }  
  5. MODULE_DEVICE_TABLE(usb, bcm_table);  

MODULE_DEVICE_TABLE用于输出到用户空间,以便于知道支持什么设备,第一个参数是所支持的类型,此处为USB

下面来看看探测函数

[cpp]  view plain  copy
  1. static int bcm_probe (struct usb_interface *intf ,const struct usb_device_id * id)  
  2. {  
  3.     struct usb_endpoint_descriptor *ep_desc;  
  4.     struct hci_dev  *hdev;  
  5.     struct bcm_data *data;  
  6.     int  i,err;  
  7.   
  8.     if(intf->cur_altsetting->desc.bInterfaceNumber !=0)   //该接口的编号,端点0保留  
  9.         return -ENODEV;  
  10.     data=kzalloc( sizeof(*data) ,  GFP_KERNEL)  
  11.         if(!data)  
  12.             return -ENOMEM;  
  13.     for(i=0;i<intf->cur_altsetting->desc.bNumEndpoints;i++){   //对端点描述符进行分配  
  14.             ep_desc = &intf->cur_altsetting->endpoint[i].desc;  
  15.             if(!data->intr_ep && usb_endpoint_is_int_in(ep_desc)){  
  16.                 data->intr_ep=ep_desc;  
  17.                 }  
  18.             if(!data->bulk_tx_ep && usb_endpoint_is_bulk_out(ep_desc)){  
  19.   
  20.                 data->bulk_tx_ep=ep_desc;  
  21.                 }  
  22.             if(!data->bulk_rx_ep && usb_endpoint_is_bulk_in(ep_desc)){  
  23.                 data->bulk_rx_ep=ep_desc;  
  24.                 }  
  25.             if(!data->intr_ep||!data->bulk_tx_ep||!data->bulk_rx_ep){  
  26.                 kfree(data);  
  27.                 return -ENODEV;  
  28.         }     
  29.         }  
  30.     data->cmdreq_type=USB_TYPE_CLASS;  
  31.     data->udev=interface_to_usbdev(intf); //从接口描述符获取usb_device结构体信息并赋值  
  32.     data->intf=intf;  
  33.   
  34.     init_usb_anchor(&data->tx_anchor);    //初始化阻塞  
  35.     init_usb_anchor(&data->intr_anchor);  
  36.     init_usb_anchor(&data->bulk_anchor);  
  37.   
  38.     hdev=hci_alloc_dev();        //申请一个hci_dev  
  39.     if(!hdev){  
  40.         kfree(data);  
  41.         return -ENOMEM;  
  42.         }  
  43.     hdev->bus = HCI_USB;  
  44.     hci_set_drvdata(hdev, data);    //将data中的数据保存到hdev中  
  45.     data->hdev=hdev;  
  46.     SET_HCIDEV_DEV(hdev, intf->dev);  
  47.     /*设置hdev的各成员的函数指针*/  
  48.     hdev->open = bcm_open;    
  49.     hdev->close = bcm_close;  
  50.     hdev->flush  = bcm_flush  
  51.     hdev->send  =bcm_send;  
  52.       
  53.     if (!reset)  
  54.         set_bit(HCI_QUIRK_NO_RESET, &hdev->quirks);   
  55.     err=hci_register_dev(hdev) //注册hci_dev  
  56.     if (err < 0) {  
  57.         hci_free_dev(hdev);  
  58.         kfree(data);  
  59.         return err;  
  60.             }  
  61.     usb_set_intfdata(intf, data);  //将data中的数据保存到intf中  
  62.       
  63.     return 0;  
  64. }  

要区分一下的是:

bNumInterfaces : 配置所支持的接口数.指该配置配备的接口数量,也表示该配置下接口描述符数量.

bInterfaceNumber: 该接口的编号.

bNumEndpoint : 使用的端点数目.端点0除外.

[cpp]  view plain  copy
  1. static void bcm_disconnect(struct usb_interface *intf)  
  2. {  
  3.     struct bcm_data *data;  
  4.     struct hci_dev *hdev;  
  5.   
  6.     if(!data)  
  7.         return ;  
  8.     hdev = data->hdev;  
  9.     intf = data->intf;  
  10.     usb_set_intfdata(intf, NULL);  
  11.     hci_unregister_dev( hdev);  
  12.     hci_free_dev( hdev);  
  13.     kfree(data);  
  14. }  

该函数所做的就是对probe函数中的注册等一系列操作的反操作。

[cpp]  view plain  copy
  1. static int bcm_open(struct hci_dev *hdev)  
  2. {  
  3.     ……  
  4.     if(test_and_set_bit(HCI_RUNNING, &hdev->flags))  
  5.         return 0;  
  6.     if(test_and_set_bit(BCM_INTR_RUNNING,&data->flags))//BCM_INTR_RUNNING=0  
  7.         return 0;  
  8.     err=bcm_submit_intr_urb(hdev,GFP_KERNEL);  
  9.     if(err<0)  
  10.         goto error;  
  11.     set_bit(BCM_BULK_RUNNING,&data->flags);    //BCM_BULK_RUNNING=1                  
  12.     err=bcm_submit_bulk_urb(hdev,GFP_KERNEL);  
  13. ……  
  14. error:  
  15.     clear_bit(HCI_RUNNING, &hdev->flags);  
  16.     clear_bit(BCM_INTR_RUNNING,&data->flags);  
  17.     clear_bit(BCM_BULK_RUNNING,&data->flags);  
  18.     return err;  
  19. }  

这个函数是probe中对hdev结构体成员的填充的。主要做就是设置data中的flags参数。其中要说的是set_bit函数,例如set0&a)指的是对a中的第0位设置为1.

这个函数的作用其实也是在做接收函数的初始化的操作,首先我们先看看err=bcm_submit_intr_urb(hdev,GFP_KERNEL);

[cpp]  view plain  copy
  1. static int bcm_submit_intr_urb(struct hci_dev *hdev, gfp_t mem_flags)  
  2. {  
  3.     struct bcm_data *data=hci_get_drvdata(hdev) //获取data数据  
  4.     struct urb *urb;  
  5.     unsigned char *buf;  
  6.     unsigned int pipe;  
  7.     int err,size;  
  8.   
  9.     if (!data->intr_ep)  
  10.         return -ENODEV;  
  11.     urb=usb_alloc_urb(0, mem_flags);    分配一个urb  
  12.     if(!urb)  
  13.         return -ENOMEM;  
  14.     size=le16_to_cpu(data->intr_ep->wMaxPacketSize);   //设置最大包的长度大小  
  15.     buf=kzalloc(size, mem_flags);                 //分配一个缓冲区  
  16.     pipe=usb_rcvintpipe(data->udev, data->intr_ep->bEndpointAddress); //设置USB的接收端点  
  17.     usb_fill_int_urb(urb, data->udev, pipe, buf, size, bcm_intr_complete, hdev ,data->intr_ep->bInterval);     //这个时候就要对urb进行填充了,使用了中断urb  
  18.     urb->transfer_flags |=URB_FREE_BUFFER;//Free transfer buffer with the URB  
  19.     usb_anchor_urb(urb, &data->intr_anchor);  
  20.     err = usb_submit_urb(urb, mem_flags); //将填充的urb提交给usb core处理。  
  21.     if(err<0)  
  22.         usb_unanchor_urb(urb);  
  23.     usb_free_urb(urb);   //防止重复提交,先进行释放。  
  24.     return err;  
  25. }  

usb_fill_int_urb中有个回调函数,当提交了urb后,将调用该回调函数bcm_intr_complete

[cpp]  view plain  copy
  1. static void bcm_intr_complete(struct urb *)  
  2. {  
  3.     struct hci_dev *hdev = urb->context;  
  4.     struct bcm_data *data = hci_get_drvdata(hdev);  
  5.     int err;  
  6.   
  7.     if(test_bit(HCI_RUNNING, &hdev->flags))  
  8.         return   
  9. /*判断urb是否发送成功,若status为0,则表示数据被发送或者接受成功*/  
  10.     if(urb->status==0){  
  11.         hdev->stat.byte_rx+=urb->actual_length;  
  12.         if(hci_recv_fragment( hdev,HCI_EVENT_PKT, urb->transfer_buffer, urb->actual_length)<0)  
  13.             hdev->stat.err_rx++;  
  14.         }  
  15.     if(!test_bit(BCM_INTR_RUNNING, &data->flags));  
  16.         return;  
  17.     usb_anchor_urb(urb, &data->intr_anchor);  
  18.     err=usb_submit_urb(urb, GFP_KERNEL);  
  19.     if(err<0){  
  20.         usb_unanchor_urb(urb);  
  21.     }  
  22. }  

帧的类型:

1) HCI_EVENT_PKT:     hci_event_packet() 处理来自Controller的事件 

2) HCI_ACLDATA_PKT: hci_acldata_packet() 处理ACL类型的数据包 

3) HCI_SCODATA_PKT: hci_scodata_packet() 处理SCO类型的数据包

hci_recv_fragmentbt协议栈数据接收函数。 hci_recv_fragmen 将数据帧放到hci_dev->rx_q链表尾部

[cpp]  view plain  copy
  1. int hci_recv_fragment(struct hci_dev *hdev, int type, void *data, int count)  
  2. {  
  3.     int rem = 0;  
  4.   
  5.     if (type < HCI_ACLDATA_PKT || type > HCI_EVENT_PKT)  
  6.         return -EILSEQ;  
  7.   
  8.     while (count) {  
  9.         rem = hci_reassembly(hdev, type, data, count, type - 1);  
  10.         if (rem < 0)  
  11.             return rem;  
  12.   
  13.         data += (count - rem);  
  14.         count = rem;  
  15.     }  
  16.   
  17.     return rem;  
  18. }  

下面是批量传输的bulk_urb的初始化操作

[cpp]  view plain  copy
  1. static int bcm_submit_bulk_urb(struct hci_dev *hdev, gfp_t mem_flags)  
  2. {  
  3.     struct bcm_data *data=hci_get_drvdata(hdev);  
  4.     struct urb *urb;  
  5.     unsigned *buf;  
  6.     unsigned int pipe;  
  7.     int err,size = HCI_MAX_FRAME_SIZE;  
  8.   
  9.     if(!data->bulk_rx_ep)  
  10.         return -ENODEV;  
  11.     urb=usb_alloc_urb(0, mem_flags);  
  12.     if(!urb)  
  13.         return -ENOMEM;  
  14.     buf=kzalloc(size, mem_flags);  
  15.     pipe=usb_rcvbulkpipe(data->udev, data->bulk_rx_ep->bEndpointAddress);  
  16.     usb_fill_bulk_urb(urb, data->udev, pipe, buf, size, bcm_bulk_complete, hdev);  
  17.     usb_anchor_urb(urb, &data->bulk_anchor);  
  18.     err=usb_submit_urb(urb, mem_flags);  
  19.     if(err<0)  
  20.         usb_unanchor_urb( urb)  
  21.     usb_free_urb(urb);  
  22.     return err;  
  23.   
  24. }  

该函数的操作与上面那个中断的几乎相同,就是在usb_fill_bulk_urb时使用了批量urb

[cpp]  view plain  copy
  1. static void bcm_bulk_complete(struct urb *)  
  2. {  
  3.     struct hci_dev *hdev = urb->context;  
  4.     struct bcm_data *data = hci_get_drvdata(hdev);  
  5.     int err;  
  6.   
  7.     if(test_bit(HCI_RUNNING, &hdev->flags))  
  8.         return   
  9.     if(urb->status==0){  
  10.         hdev->stat.byte_rx+=urb->actual_length;  
  11.         if(hci_recv_fragment( hdev,HCI_ACLDATA_PKT, urb->transfer_buffer, urb->actual_length)<0)  
  12.             hdev->stat.err_rx++;  
  13.         }  
  14.     if(!test_bit(BCM_BULK_RUNNING, &data->flags));  
  15.         return;  
  16.     usb_anchor_urb(urb,& data->bulk_anchor);  
  17.     err=usb_submit_urb(urb, GFP_KERNEL);  
  18.     if(err<0){  
  19.         usb_unanchor_urb(urb);  
  20.     }  
  21. }  

此处也与中断的一样。

下面来看看对于发送函数时如何进行操作的。在Linux中,定义了五种HCI数据包类型 

COMMAND/ACLDATA/SCODATA/EVENT/VENDOR,我们此处只对其中的COMMANDACLDATA进行发送。bcm_send于提供给HCI去发送数据包。

[cpp]  view plain  copy
  1. static int bcm_send (struct sk_buff *skb)  
  2. {  
  3.     struct hci_dev *hdev = (struct hci_dev *) skb->dev;  
  4.     struct bcm_data *data=hci_get_drvdata( hdev);  
  5.     struct urb *urb;  
  6.     struct usb_ctrlrequest *cr;  
  7.     unsigned int pipe;  
  8.   
  9.     if(!test_bit(HCI_RUNNING,&hdev->flags))     //每一步都要首先检测是否正在运行  
  10.         return -EBUSY;  
  11.     switch(bt_cb(skb)->pkt_type){           //从skb中的控制buffer中取出包的类型  
  12.         case HCI_COMMAND_PKT:  
  13.             urb=usb_alloc_urb(0, GFP_ATOMIC);  
  14.             if(!urb)  
  15.                 return -ENOMEM;  
  16.             cr=kmalloc(sizeof(*cr), GFP_ATOMIC);  
  17.             if(!cr){  
  18.                 usb_free_urb(urb);  
  19.                 return -ENOMEM;  
  20.                 }  
  21.             cr->bRequestType = data->cmdreq_type;  
  22.             cr->bRequest     = 0;  
  23.             cr->wIndex       = 0;  
  24.             cr->wValue       = 0;  
  25.             cr->wLength      = __cpu_to_le16(skb->len);  
  26.   
  27.             pipe = usb_sndctrlpipe(data->udev, 0x00);  
  28.  /*填充控制URB,这里我们需要注意的是,此处的数据缓冲区和数据的长度,都是由skb中的结构体成员进行设置的*/  
  29.             usb_fill_control_urb(urb, data->udev, pipe, (void *) cr,skb->data, skb->len, bcm_tx_complete, skb);   
  30.             hdev->stat.cmd_tx++;  
  31.             break;  
  32.         case HCI_ACLDATA_PKT  
  33.             urb=usb_alloc_urb(0, GFP_ATOMIC);  
  34.             if(!urb)  
  35.                 return -ENOMEM;  
  36.             pipe=usb_sndbulkpipe(data->udev, data->bulk_rx_ep->bEndpointAddress);  
  37.             usb_fill_bulk_urb( urb, data->udev, pipe, skb->data, skb->len, bcm_tx_complete, skb);   //填充批量URB  
  38.             hdev->stat.acl_tx++;  
  39.                     break;  
  40.         default:  
  41.             return -EILSEQ;  
  42.         }  
  43.         usb_anchor_urb(urb, &data->tx_anchor);  
  44.         err=usb_submit_urb(urb,GFP_ATOMIC);  
  45.         if(err<0){  
  46.             kfree(urb->setup_packet);  
  47.             usb_unanchor_urb(urb);  
  48.             }  
  49.         return err;  
  50. }  

首先我们要来看看struct sk_buff 这个结构体。

sk_buffLinux网络代码中最重要的结构体之一。它是Linux在其协议栈里传送的结构体,也就是所谓的“包”,在他里面包含了各层协议的头部,比如ethernet, ip ,tcp ,udp等等。并且他是一个复杂的双向链表,在他结构中有next和prev指针,分别指向链表的下一个节点和前一个节点.

此处的回调函数是bcm_tx_complete

[cpp]  view plain  copy
  1. static void bcm_tx_complete(struct urb *)  
  2. {     
  3.     struct sk_buff *skb=urb->context;  
  4.     struct hci_dev *hdev = (struct hci_dev *)skb->dev;  
  5.     struct bcm_data *data= hci_get_drvdata(hdev);  
  6.   
  7.     if(!test_bit(HCI_RUNNING,&hdev->flags));  
  8.         goto done ;  
  9.     if(!urb->status)  
  10.         hdev->stat.byte_tx+=urb->transfer_buffer_length;  
  11.     else  
  12.         hdev->stat.err_tx++;  
  13. done:  
  14.     kfree(urb->setup_packet);  
  15.     kfree_skb(skb);  
  16. }  

最后是close函数

[cpp]  view plain  copy
  1. static int bcm_close(struct hci_dev *hdev)  
  2. {  
  3.     struct bcm_data *data = hci_get_drvdata(hdev);  
  4.     if(!test_and_clear_bit(HCI_RUNNING,&hdev->flags))  
  5.         return 0;  
  6.     clear_bit(BCM_INTR_RUNNING, &data->flags);  
  7.     clear_bit(BCM_BULK_RUNNING, &data->flags);  
  8.     data->intf->needs_remote_wakeup=0;  
  9.     return 0;  
  10. }  

就是针对dataflags进行位清零设置。

最后

[cpp]  view plain  copy
  1. static int bcm_flush (struct hci_dev *hdev)  
  2. {  
  3.     struct bcm_data *data=hci_get_drvdata( hdev)  
  4.     usb_kill_anchored_urbs(&data->tx_anchor);  //取消传输请求  
  5.     return 0;  
  6. }  

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
摘 要:基于对Linux蓝牙协议栈BlueZ 源代码的分析,给出BlueZ的组织结构和特点。分析蓝牙USB 传输驱动机制和数据处理过程, 给出实现蓝牙设备驱动的重要数据结构和流程,并总结Linux 下开发蓝牙USB 设备驱动的一般方法和关键技术。 关键词:Linux 系统;蓝牙协议栈;设备驱动 USB Device Driver for Linux Bluetooth Stack LIANG Jun-xue, YU Bin (Institute of Electronic Technology, PLA Information Engineering University, Zhengzhou 450004) 【Abstract】This paper depicts the structure and characteristics of BlueZ based on analyzing the source code of Linux bluetooth stack BlueZ. It analyzes the implementation of bluetooth USB transport driver scheme and data processing procedure in detail, and gives the key data structure and implementation of bluetooth device driver. It summarizes the approach of developing Linux bluetooth USB device driver and the key technology. 【Key words】Linux system; bluetooth stack; device driver 计 算 机 工 程 Computer Engineering 第 34 卷 第 9 期 Vol.34 No.9 2008 年 5 月 May 2008 ·开发研究与设计技术· 文章编号:1000—3428(2008)09—0273—03 文献标识码:A 中图分类号:TP391 1 概述 蓝牙技术是开放式通信规范,而 Linux 是开放源码的操 作系统。廉价设备与免费软件的结合,促进了蓝牙技术和 Linux 的发展与融合。 Linux最早的蓝牙协议栈是由Axis Communication Inc在 1999 年发布的 OpenBT 协议栈。 随后, IBM 发布了 BlueDrekar 协议栈,但没有公开其源码。Qualcomm Incorporated 在 2001 年发布的 BlueZ 协议栈被接纳为 2.4.6 内核的一部分。此外, Rappore Technology 及 Nokia 的 Affix Bluetooth Stack 都是 Linux 系统下的蓝牙协议栈,应用在不同的设备和领域中。 BlueZ 是 Linux 的官方蓝牙协议栈,也是目前应用最广 泛的协议栈,几乎支持所有已通过认证的蓝牙设备。对于基 于主机的蓝牙应用,目前常见的硬件接口有 UART, USB 和 PC 卡等,USB 作为 PC 的标准外设接口,具有连接方便、兼 容性好和支持高速设备等特点,已广泛应用于蓝牙设备。 目前对 Linux 下 USB 设备驱动的研究已较为广泛而深 入[1-4] ,但对 Linux 下的蓝牙设备驱动还没有专门的研究。本 文在分析 USB 设备驱动和蓝牙协议栈的基础上,总结了 Linux 下开发蓝牙 USB 驱动程序的一般方法,并深入剖析了 其关键技术。 2 Linux 蓝牙协议栈 BlueZ 简介 BlueZ 目前已成为一个开放性的源码工程。它可以很好 地在 Linux 支持的各种体系的硬件平台下运行,包括各种单 处理器平台、多处理器平台及超线程系统。 BlueZ 由多个独立的模块组成,内核空间主要包括设备 驱动层、蓝牙核心及 HCI 层、L2CAP 与 SCO 音频层、 RFCOMM, BNEP, CMTP 与 HIDP 层、通用蓝牙 SDP 库和后 台服务及面向所有层的标准套接字接口;在用户空间提供了 蓝牙配置、测试及协议分析等工具。其组织结构如图 1 所示, BlueZ 没有实现专门的 SDP 层,而是将其实现为运行在后台 的蓝牙服务库例程(图 1 没有描述该后台服务)。 RFOMM 层支 持标准的套接口,并提供了串行仿真 TTY 接口,这使串行端 口应用程序和协议可以不加更改地运行在蓝牙设备上,例如 通过点对点协议 PPP 可实现基于 TCP/IP 协议簇的所有网络 应用。BNEP 层实现了蓝牙的以太网仿真,TCP/IP 可以直接 运行于其上。 USB设备驱动 (hci_usb.o) L2CAP层(l2cap.o) RFCOMM层 (rfcomm.o) BNEP层 (bnep.o) CMTP层 (cmtp.o) 串口设备驱动 (hci_uart.o) 虚拟串口设备驱动 (hci_vhci.o) 音频 socket RFCOMM socket BNEP socket CMTP socket L2CAP socket HCI socket 内核 空间 用户 空间 串口设备 CAPI设备 输入设备 网络设备 HDIP socket 音频设备 AF_BLUETOOTH socket 音频层(sco.o) PPP TCP/IP AF_INET socket BNEP层 (bnep.o) 其他设备驱动 (bluecard_cs.o等) BlueZ工具和实用程序 HDIP层 (hdip.o) BlueZ核心 及HCI层(bluez.o/bluetooth.o) 图 1 BlueZ 组织结构 3 蓝牙 USB 设备驱动 设备驱动程序Linux 内核中起着重要作用,它使某个 硬件能响应一个定义良好的内部编程接口。这些接口隐藏了 设备的工作细节,用户通过一组独立于特定驱动程序的标准 调用来操作设备。而将这些调用映射到作用于实际硬件设备 的特有操作上,则是驱动程序的任务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值