linux 2.6.28之spi用户接口初始化spidev.c的代码

spi.c是spi子系统初始化的核心代码,由内核负责初始化
spidev.c是spi用户接口初始化的代码,编译的时候需要选择该模块
spi_sam.c是平台驱动的初始化代码,编译时需要选择spi s3c64xx模块

spi.c也就是spi子系统的核心了,
spi_sam.c是s3c64xx系列芯片的SPI controller驱动,它向更上层的SPI核心层(spi.c)提供接口用来控制芯片的SPI controller,是一个被其他驱动使用的驱动。
spidev.c是在核心层基础之上将SPI controller模拟成一个字符型的驱动,向文件系统提供标准的文件系统接口,用来操作对应的SPI controller

module_init(spidev_init);
/-------------------------------------------------------------------------/

static int __init spidev_init(void)
{
int status;

/* Claim our 256 reserved device numbers.  Then register a class
 * that will key udev/mdev to add/remove /dev nodes.  Last, register
 * the driver which manages those device numbers.
 */
BUILD_BUG_ON(N_SPI_MINORS > 256);

//字符设备注册,将SPI注册为字符设备,绑定操作函数集合spidev_fops
//注册字符设备,参数spidev_fops是struct file_operations的实例,这里就可以知道,用户程序的open、//write等操作最终会调用这里面的函数
status = register_chrdev(SPIDEV_MAJOR, “spi”, &spidev_fops);
if (status < 0)
return status;

spidev_class = class_create(THIS_MODULE, "spidev");
if (IS_ERR(spidev_class)) {
    unregister_chrdev(SPIDEV_MAJOR, spidev_spi.driver.name);
    return PTR_ERR(spidev_class);
}

/注册spidev的driver,可与modalias字段为"spidev"的spi_device匹配/
status = spi_register_driver(&spidev_spi);
if (status < 0) {
class_destroy(spidev_class);
unregister_chrdev(SPIDEV_MAJOR, spidev_spi.driver.name);
}
return status;
}

static struct file_operations spidev_fops = {
.owner = THIS_MODULE,
/* REVISIT switch to aio primitives, so that userspace
* gets more complete API coverage. It’ll simplify things
* too, except for the locking.
*/
.write = spidev_write,
.read = spidev_read,
.unlocked_ioctl = spidev_ioctl,
.open = spidev_open,
.release = spidev_release,
};

第12章 SPI只是一种总线,spi_driver的作用只是将SPI外设挂接在总线上,因此在spi_driver的probe()成员函数中,将注册SPI外设本身所属设备驱动的类型

static struct spi_driver spidev_spi = {
/spidev的driver,可与modalias字段为"spidev"的spi_device匹配/
//
//static struct spi_board_info __initdata sam_spi_devs[] = {
// [0] = { .modalias = “spidev”, /* Test Interface */

.driver = {
    .name =        "spidev",
    .owner =    THIS_MODULE,
},
.probe =    spidev_probe,
.remove =    __devexit_p(spidev_remove),

/* NOTE:  suspend/resume methods are not necessary here.
 * We don't do anything except pass the requests to/from
 * the underlying controller.  The refrigerator handles
 * most issues; the controller driver handles the rest.
 */

};

spi子系统初始化的核心代码在spi.c里

/**

  • spi_register_driver - register a SPI driver
  • @sdrv: the driver to register
  • Context: can sleep
    */
    int spi_register_driver(struct spi_driver *sdrv)
    {
    //spi注册驱动的.bus注意看看
    sdrv->driver.bus = &spi_bus_type;
    if (sdrv->probe)
    sdrv->driver.probe = spi_drv_probe;
    if (sdrv->remove)
    sdrv->driver.remove = spi_drv_remove;
    if (sdrv->shutdown)
    sdrv->driver.shutdown = spi_drv_shutdown;
    return driver_register(&sdrv->driver);
    }

    spi子系统初始化的核心代码在spi.c里

spi_bus_type对应SPI中的SPI BUS总线
struct bus_type spi_bus_type = {
.name = “spi”,
.dev_attrs = spi_dev_attrs,
//驱动和设备匹配时调用.match函数进行匹配
//匹配成功后调用驱动的.probe函数
.match = spi_match_device,
.uevent = spi_uevent,
.suspend = spi_suspend,
.resume = spi_resume,
};

spi子系统初始化的核心代码在spi.c里

/* modalias support makes “modprobe $MODALIAS” new-style hotplug work,

  • and the sysfs version makes coldplug work too.
    */
    //匹配函数
    static int spi_match_device(struct device *dev, struct device_driver *drv)
    {
    const struct spi_device *spi = to_spi_device(dev);

    return strncmp(spi->modalias, drv->name, BUS_ID_SIZE) == 0;

/spidev的driver,可与modalias字段为"spidev"的spi_device匹配/
//
//static struct spi_board_info __initdata sam_spi_devs[] = {
// [0] = { .modalias = “spidev”, /* Test Interface */
//
//static struct spi_driver spidev_spi = { .driver = { .name = “spidev”,
}

///

/*

  • INTERFACES between SPI master-side drivers and SPI infrastructure.
  • (There’s no SPI slave support for Linux yet…)
    */
    extern struct bus_type spi_bus_type;

/**

  • struct spi_device - Master side proxy for an SPI slave device
  • @dev: Driver model representation of the device.
  • @master: SPI controller used with the device.
  • @max_speed_hz: Maximum clock rate to be used with this chip
  • (on this board); may be changed by the device’s driver.
  • The spi_transfer.speed_hz can override this for each transfer.
  • @chip_select: Chipselect, distinguishing chips handled by @master.
  • @mode: The spi mode defines how data is clocked out and in.
  • This may be changed by the device’s driver.
  • The “active low” default for chipselect mode can be overridden
  • (by specifying SPI_CS_HIGH) as can the “MSB first” default for
  • each word in a transfer (by specifying SPI_LSB_FIRST).
  • @bits_per_word: Data transfers involve one or more words; word sizes
  • like eight or 12 bits are common. In-memory wordsizes are
  • powers of two bytes (e.g. 20 bit samples use 32 bits).
  • This may be changed by the device’s driver, or left at the
  • default (0) indicating protocol words are eight bit bytes.
  • The spi_transfer.bits_per_word can override this for each transfer.
  • @irq: Negative, or the number passed to request_irq() to receive
  • interrupts from this device.
  • @controller_state: Controller’s runtime state
  • @controller_data: Board-specific definitions for controller, such as
  • FIFO initialization parameters; from board_info.controller_data
  • @modalias: Name of the driver to use with this device, or an alias
  • for that name. This appears in the sysfs “modalias” attribute
  • for driver coldplugging, and in uevents used for hotplugging
  • A @spi_device is used to interchange data between an SPI slave
  • (usually a discrete chip) and CPU memory.
  • In @dev, the platform_data is used to hold information about this
  • device that’s meaningful to the device’s protocol driver, but not
  • to its controller. One example might be an identifier for a chip
  • variant with slightly different functionality; another might be
  • information about how this particular board wires the chip’s pins.
    */
    //用spi_device来描述一个SPI外设设备
    struct spi_device {
    struct device dev;
    // .modalias = "spidev"的一对(设备和驱动)module_init(spidev_init)
    //通过内部包含struct spi_master *master;成员
    //与.name = “sam-spi”,的一对(平台设备和驱动)module_init(sam_spi_init);联系起来,
    //平台设备驱动通过samspi_probe 函数------>spi_alloc_master产生并赋值struct spi_master实例
    //见static inline int spi_async(struct spi_device *spi, struct spi_message *message)
    struct spi_master master;//对应的控制器指针
    u32 max_speed_hz; //spi通信的时钟
    u8 chip_select; //片选,用于区分同一总线上的不同设备
    u8 mode;
    #define SPI_CPHA 0x01 /
    clock phase /
    #define SPI_CPOL 0x02 /
    clock polarity /
    #define SPI_MODE_0 (0|0) /
    (original MicroWire) /
    #define SPI_MODE_1 (0|SPI_CPHA)
    #define SPI_MODE_2 (SPI_CPOL|0)
    #define SPI_MODE_3 (SPI_CPOL|SPI_CPHA)
    #define SPI_CS_HIGH 0x04 /
    chipselect active high? /
    #define SPI_LSB_FIRST 0x08 /
    per-word bits-on-wire /
    #define SPI_3WIRE 0x10 /
    SI/SO signals shared /
    #define SPI_LOOP 0x20 /
    loopback mode /
    #define SPI_SLAVE 0x40 /
    SLAVE mode if this bit is set */

#define SPIDEV_MAX_BUFFSIZE 16384

u8            bits_per_word;  //每个字长的比特数 
int            irq; //使用的中断  
void            *controller_state;
void            *controller_data;
char            modalias[32]; //名字  ,用来匹配

/*
 * likely need more hooks for more protocol options affecting how
 * the controller talks to each chip, like:
 *  - memory packing (12 bit samples into low bits, others zeroed)
 *  - priority
 *  - drop chipselect after each word
 *  - chipselect delays
 *  - ...
 */

};
///

/**

  • struct spi_driver - Host side “protocol” driver
  • @probe: Binds this driver to the spi device. Drivers can verify
  • that the device is actually present, and may need to configure
  • characteristics (such as bits_per_word) which weren’t needed for
  • the initial configuration done during system setup.
  • @remove: Unbinds this driver from the spi device
  • @shutdown: Standard shutdown callback used during system state
  • transitions such as powerdown/halt and kexec
  • @suspend: Standard suspend callback used during system state transitions
  • @resume: Standard resume callback used during system state transitions
  • @driver: SPI device drivers should initialize the name and owner
  • field of this structure.
  • This represents the kind of device driver that uses SPI messages to
  • interact with the hardware at the other end of a SPI link. It’s called
  • a “protocol” driver because it works through messages rather than talking
  • directly to SPI hardware (which is what the underlying SPI controller
  • driver does to pass those messages). These protocols are defined in the
  • specification for the device(s) supported by the driver.
  • As a rule, those device protocols represent the lowest level interface
  • supported by a driver, and it will support upper level interfaces too.
  • Examples of such upper levels include frameworks like MTD, networking,
  • MMC, RTC, filesystem character device nodes, and hardware monitoring.
    */
    //用spi_driver来描述一个SPI外设驱动
    struct spi_driver {
    int (*probe)(struct spi_device *spi);
    int (*remove)(struct spi_device *spi);
    void (*shutdown)(struct spi_device *spi);
    int (*suspend)(struct spi_device *spi, pm_message_t mesg);
    int (*resume)(struct spi_device *spi);
    struct device_driver driver;
    };
    图片
    //spi_device与spi_master是同一个父设备,这是在spi_new_device()–>spi_alloc_device函数中设定的,
    //一般这个设备是一个物理设备

/-------------------------------------------------------------------------/

/* Read-only message with current device setup */
static ssize_t
spidev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct spidev_data *spidev;
ssize_t status = 0;

/* chipselect only toggles at start or end of operation */
if (count > bufsiz)
    return -EMSGSIZE;

spidev = filp->private_data;

mutex_lock(&spidev->buf_lock);
status = spidev_sync_read(spidev, count);
if (status > 0) {
    unsigned long    missing;

    missing = copy_to_user(buf, spidev->buffer, status);
    if (missing == status)
        status = -EFAULT;
    else
        status = status - missing;
}
mutex_unlock(&spidev->buf_lock);

return status;

}

static long spidev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int err = 0;
int retval = 0;
struct spidev_data *spidev;
struct spi_device *spi;
u32 tmp;
unsigned n_ioc;
struct spi_ioc_transfer *ioc;

/* Check type and command number */
if (_IOC_TYPE(cmd) != SPI_IOC_MAGIC)
    return -ENOTTY;

/* Check access direction once here; don't repeat below.
 * IOC_DIR is from the user perspective, while access_ok is
 * from the kernel perspective; so they look reversed.
 */

//检查地址是否合法
if (_IOC_DIR(cmd) & _IOC_READ)
err = !access_ok(VERIFY_WRITE,
(void __user *)arg, _IOC_SIZE(cmd));
if (err == 0 && _IOC_DIR(cmd) & _IOC_WRITE)
err = !access_ok(VERIFY_READ,
(void __user *)arg, _IOC_SIZE(cmd));
if (err)
return -EFAULT;

/* guard against device removal before, or while,
 * we issue this ioctl.
 */
spidev = filp->private_data;
spin_lock_irq(&spidev->spi_lock);
spi = spi_dev_get(spidev->spi);
spin_unlock_irq(&spidev->spi_lock);

if (spi == NULL)
    return -ESHUTDOWN;

/* use the buffer lock here for triple duty:
 *  - prevent I/O (from us) so calling spi_setup() is safe;
 *  - prevent concurrent SPI_IOC_WR_* from morphing
 *    data fields while SPI_IOC_RD_* reads them;
 *  - SPI_IOC_MESSAGE needs the buffer locked "normally".
 */
mutex_lock(&spidev->buf_lock);

switch (cmd) {
/* read requests */

//设置SPI读模式
case SPI_IOC_RD_MODE:
retval = __put_user(spi->mode & SPI_MODE_MASK,
(__u8 __user *)arg);
break;
case SPI_IOC_RD_LSB_FIRST:
retval = __put_user((spi->mode & SPI_LSB_FIRST) ? 1 : 0,
(__u8 __user *)arg);
break;
case SPI_IOC_RD_BITS_PER_WORD:
retval = __put_user(spi->bits_per_word, (__u8 __user *)arg);
break;
case SPI_IOC_RD_MAX_SPEED_HZ:
retval = __put_user(spi->max_speed_hz, (__u32 __user *)arg);
break;

/* write requests */

//设置SPI成写模式
case SPI_IOC_WR_MODE:
retval = __get_user(tmp, (u8 __user *)arg);
if (retval == 0) {
//先将SPI之前的模式保存起来,一旦设置失败,进行回复
u8 save = spi->mode;

        if (tmp & ~SPI_MODE_MASK) {
            retval = -EINVAL;
            break;
        }

        tmp |= spi->mode & ~SPI_MODE_MASK;
        spi->mode = (u8)tmp;
        retval = spi_setup(spi);
        if (retval < 0)
            spi->mode = save;
        else
            dev_dbg(&spi->dev, "spi mode %02x\n", tmp);
    }
    break;
case SPI_IOC_WR_LSB_FIRST:
    retval = __get_user(tmp, (__u8 __user *)arg);
    if (retval == 0) {
        u8    save = spi->mode;

        if (tmp)
            spi->mode |= SPI_LSB_FIRST;
        else
            spi->mode &= ~SPI_LSB_FIRST;
        retval = spi_setup(spi);
        if (retval < 0)
            spi->mode = save;
        else
            dev_dbg(&spi->dev, "%csb first\n",
                    tmp ? 'l' : 'm');
    }
    break;
case SPI_IOC_WR_BITS_PER_WORD:
    retval = __get_user(tmp, (__u8 __user *)arg);
    if (retval == 0) {
        u8    save = spi->bits_per_word;

        spi->bits_per_word = tmp;
        retval = spi_setup(spi);
        if (retval < 0)
            spi->bits_per_word = save;
        else
            dev_dbg(&spi->dev, "%d bits per word\n", tmp);
    }
    break;
case SPI_IOC_WR_MAX_SPEED_HZ:
    retval = __get_user(tmp, (__u32 __user *)arg);
    if (retval == 0) {
        u32    save = spi->max_speed_hz;

        spi->max_speed_hz = tmp;
        retval = spi_setup(spi);
        if (retval < 0)
            spi->max_speed_hz = save;
        else
            dev_dbg(&spi->dev, "%d Hz (max)\n", tmp);
    }
    break;

default:
    /* segmented and/or full-duplex I/O request */
    if (_IOC_NR(cmd) != _IOC_NR(SPI_IOC_MESSAGE(0))
            || _IOC_DIR(cmd) != _IOC_WRITE) {
        retval = -ENOTTY;
        break;
    }

//得到用户空间数据的大小
tmp = _IOC_SIZE(cmd);
//如果这些数据不能分成spi_ioc_transfer的整数倍,则不能进行传输
if ((tmp % sizeof(struct spi_ioc_transfer)) != 0) {
retval = -EINVAL;
break;
}
//能分出的个数
n_ioc = tmp / sizeof(struct spi_ioc_transfer);
if (n_ioc == 0)
break;

//申请内存
/* copy into scratch area */
ioc = kmalloc(tmp, GFP_KERNEL);
if (!ioc) {
retval = -ENOMEM;
break;
}
//将用户空间的数据拷贝到内核空间来
if (__copy_from_user(ioc, (void __user )arg, tmp)) {
kfree(ioc);
retval = -EFAULT;
break;
}
//进行数据传输
/
translate to spi_message, execute */
retval = spidev_message(spidev, ioc, n_ioc);
kfree(ioc);
break;
}

mutex_unlock(&spidev->buf_lock);
spi_dev_put(spi);
return retval;

}

static int spidev_open(struct inode *inode, struct file *filp)
{
struct spidev_data *spidev;
int status = -ENXIO;

lock_kernel();
mutex_lock(&device_list_lock);

// static LIST_HEAD(device_list);
//通过链表头device_list和节点 device_entry获取到包含成员device_entry的 struct spidev_data *spidev;,而且是遍历链表
list_for_each_entry(spidev, &device_list, device_entry) {
if (spidev->devt == inode->i_rdev) {
//遍历链表,找到了和这个文件节点绑定的设备相同的在链表里的那个设备了
status = 0;
break;
}
}
if (status == 0) {
if (!spidev->buffer) {
spidev->buffer = kmalloc(bufsiz, GFP_KERNEL);
if (!spidev->buffer) {
dev_dbg(&spidev->spi->dev, “open/ENOMEM\n”);
status = -ENOMEM;
}
}
if (status == 0) {
spidev->users++;
//spidev_read是从filp->private_data里取出spidev
filp->private_data = spidev;
nonseekable_open(inode, filp);
}
} else
pr_debug(“spidev: nothing for minor %d\n”, iminor(inode));

mutex_unlock(&device_list_lock);
unlock_kernel();
return status;

}

//-书中说probe是用来查询特定设备是否存在的函数
//驱动和设备匹配成功后会调用spidev_probe
//这个函数实现把spidev添加到device_list
static int spidev_probe(struct spi_device *spi)
{
struct spidev_data *spidev;
int status;
unsigned long minor;

/* Allocate driver data */
spidev = kzalloc(sizeof(*spidev), GFP_KERNEL);
if (!spidev)
    return -ENOMEM;

/* Initialize the driver data */
spidev->spi = spi;
spin_lock_init(&spidev->spi_lock);
mutex_init(&spidev->buf_lock);

INIT_LIST_HEAD(&spidev->device_entry);

/* If we can allocate a minor number, hook up this device.
 * Reusing minors is fine so long as udev or mdev is working.
 */
mutex_lock(&device_list_lock);
minor = find_first_zero_bit(minors, N_SPI_MINORS);
if (minor < N_SPI_MINORS) {
    struct device *dev;

    spidev->devt = MKDEV(SPIDEV_MAJOR, minor);
    dev = device_create(spidev_class, &spi->dev, spidev->devt,
                spidev, "spidev%d.%d",
                spi->master->bus_num, spi->chip_select);
    status = IS_ERR(dev) ? PTR_ERR(dev) : 0;
} else {
    dev_dbg(&spi->dev, "no minor number available!\n");
    status = -ENODEV;
}
if (status == 0) {
    set_bit(minor, minors);

//spidev->device_entry这个链表节点插入了device_list中,这样spidev就可以通过device_list而获取到了
list_add(&spidev->device_entry, &device_list);
}
mutex_unlock(&device_list_lock);

if (status == 0)
    spi_set_drvdata(spi, spidev);
else
    kfree(spidev);

return status;

}

static int spidev_message(struct spidev_data *spidev,
struct spi_ioc_transfer *u_xfers, unsigned n_xfers)
{
struct spi_message msg;
struct spi_transfer *k_xfers;
struct spi_transfer *k_tmp;
struct spi_ioc_transfer *u_tmp;
unsigned n, total;
u8 *buf;
int status = -EFAULT;
//初始化spi_message的链表头
spi_message_init(&msg);
k_xfers = kcalloc(n_xfers, sizeof(*k_tmp), GFP_KERNEL);
if (k_xfers == NULL)
return -ENOMEM;

/* Construct spi_message, copying any tx data to bounce buffer.
 * We walk the array of user-provided transfers, using each one
 * to initialize a kernel version of the same transfer.
 */
buf = spidev->buffer;
total = 0;
for (n = n_xfers, k_tmp = k_xfers, u_tmp = u_xfers;
        n;
        n--, k_tmp++, u_tmp++) {
    k_tmp->len = u_tmp->len;

    total += k_tmp->len;
    if (total > bufsiz) {
        status = -EMSGSIZE;
        goto done;
    }

    if (u_tmp->rx_buf) {
        k_tmp->rx_buf = buf;
        if (!access_ok(VERIFY_WRITE, (u8 __user *)
                    (uintptr_t) u_tmp->rx_buf,
                    u_tmp->len))
            goto done;
    }
    if (u_tmp->tx_buf) {
        k_tmp->tx_buf = buf;
        if (copy_from_user(buf, (const u8 __user *)
                    (uintptr_t) u_tmp->tx_buf,
                u_tmp->len))
            goto done;
    }
    buf += k_tmp->len;

    k_tmp->cs_change = !!u_tmp->cs_change;
    k_tmp->bits_per_word = u_tmp->bits_per_word;
    k_tmp->delay_usecs = u_tmp->delay_usecs;
    k_tmp->speed_hz = u_tmp->speed_hz;

#ifdef VERBOSE
dev_dbg(&spi->dev,
" xfer len %zd %s%s%s%dbits %u usec %uHz\n",
u_tmp->len,
u_tmp->rx_buf ? "rx " : “”,
u_tmp->tx_buf ? "tx " : “”,
u_tmp->cs_change ? "cs " : “”,
u_tmp->bits_per_word ? : spi->bits_per_word,
u_tmp->delay_usecs,
u_tmp->speed_hz ? : spi->max_speed_hz);
#endif
//将spi_transfer类型的实体k_tmp通过transfer_list字段挂到spi_message的transfer队列上
spi_message_add_tail(k_tmp, &msg);
}
//调用底层的传输函数
status = spidev_sync(spidev, &msg);
if (status < 0)
goto done;

/* copy any rx data out of bounce buffer */
buf = spidev->buffer;
for (n = n_xfers, u_tmp = u_xfers; n; n--, u_tmp++) {
    if (u_tmp->rx_buf) {
        if (__copy_to_user((u8 __user *)
                (uintptr_t) u_tmp->rx_buf, buf,
                u_tmp->len)) {
            status = -EFAULT;
            goto done;
        }
    }
    buf += u_tmp->len;
}
status = total;

done:
kfree(k_xfers);
return status;
}

static ssize_t
spidev_sync(struct spidev_data *spidev, struct spi_message *message)
{
//声明并初始化一个完成量
DECLARE_COMPLETION_ONSTACK(done);
int status;
//指定spi_message结构实例的完成量complete函数指针赋值
//handle_msg函数有一行 if(msg->complete) msg->complete(msg->context);
//这行调用的便是spidev_complete这个函数
message->complete = spidev_complete;
message->context = &done;

spin_lock_irq(&spidev->spi_lock);
if (spidev->spi == NULL)
    status = -ESHUTDOWN;
else

//调用spi核心中的函数进行数据传输
status = spi_async(spidev->spi, message);
spin_unlock_irq(&spidev->spi_lock);

if (status == 0) {

//等待完成量被唤醒
//handle_msg函数有一行 if(msg->complete) msg->complete(msg->context);
//这行调用的便是spidev_complete这个函数 ,当msg->complete(msg->context);调用后此处被唤醒往下执行
wait_for_completion(&done);
status = message->status;
if (status == 0)
status = message->actual_length;
}
return status;
}

static inline int
spi_async(struct spi_device *spi, struct spi_message *message)
{
message->spi = spi;

//看看.name = "spidev"的这对设备和驱动
//在这里与.name = “sam-spi”,的一对平台设备和驱动联系起来
//module_init(spidev_init);与
// module_init(sam_spi_init);这两个模块的连接点在这行程序里体现
//struct spi_device里的master成员是什么时候赋值过去的?
//samspi_probe(struct platform_device *pdev)------>spi_register_master(master);
//---------->scan_boardinfo(master);------->spi_new_device(master, chip);--------->proxy = spi_alloc_device(master);
///在spi_alloc_device函数里有一行spi->master = master;,返回值 return spi;
/这个便是调用samspi_transfer函数了。数据的发送传输
return spi->master->transfer(spi, message);
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xx-xx-xxx-xxx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值