linux串口编程说明

1.参考文章1
2.linux手册参考
3.详解linux下的串口通讯开发
在linux下所有的设备都是文件,串口也不例外,所以对串口的操作也是open,close,write,read这几个操作,只不过串口通信要想正常沟通,还需要设置正确的属性。

一 必备知识

1.1 头文件

#include <termios.h>

1.2 主要结构struct termios

struct termios {
tcflag_t c_cflag  /* 控制标志  */
tcflag_t c_iflag;  /* 输入标志  */
tcflag_t c_oflag;  /* 输出标志  */
tcflag_t c_lflag;  /* 本地标志  */
tcflag_t c_cc[NCCS];  /* 控制字符  */
};

1.3 function

    int tcgetattr(int fd, struct termios *termios_p);
    int tcsetattr(int fd, int optional_actions, struct termios *termios_p);
    int tcflush(int fd, int queue_selector);
    speed_t cfgetispeed(struct termios *termios_p);
    speed_t cfgetospeed(struct termios *termios_p);
    int cfsetispeed(struct termios *termios_p, speed_t speed);
    int cfsetospeed(struct termios *termios_p, speed_t speed);

二 串口的基本操作

2.1 打开串口

int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY |O_NDELAY);
if (fd < 0) {
    perror("open uart device error\n");
}

O_NOCTTY:如果打开的串口是一个终端设备,这个程序不会成为对应这个端口的控制终端,如果没有该标志,任何一个输入,例如键盘中止信号(ctrl+c)等,都将影响进程。
O_NDELAY:告诉系统此程序不关心DCD信号线状态,即其他端口是否运行,不说明这个标志的话,该程序就会在DCD信号线为低电平时一直睡眠。
O_NONBLOCK: 该标志与O_NDELAY(早期使用)标志作用差不多,允许多次打开时必须设成非阻塞模式.

2.2 读写关闭串口

unsigned char buf[11];
int len = read(fd, buf, sizeof(buf));
if (len < 0){
    printf("reading data faile \n");
}
len = write(fd, buf, sizeof(buf));
if (len < 0) {
    printf("write data to serial failed! \n");
}
close(fd);

如果操作端口设置成原数据模式(raw data mode),每次read调用都会返回从串口输入缓冲区中实际得到的字符的个数。在读取不到数据的情况下,read调用就会一直等着,直到端口上有新的字符可以读取或者发生超时和错误。如果需要read函数迅速返回的话,可以使用下面这个方式改变设备属性:
fcntl(fd, F_SETFL, FNDELAY);
标志FNDELAY可以保证read函数在端口上读不到字符的时候返回0。需要回到正常(阻塞)模式的时候,需要再次调用fcntl函数设置成不带FNDELAY标志的情况:
fcntl(fd, F_SETFL, 0);
当然,如果你最初就是以O_NDELAY标志打开串口的,你也可在之后使用这个方法改变读取的行为方式。

三 串口属性设置

  • 属性的设置示例
    主要就是修改termios成员,它的各参数具体看下第4小节

       newtio.c_cflag = B115200 | CS8 | CLOCAL | CREAD;
       newtio.c_iflag = IGNPAR | IGNCR;
       newtio.c_oflag = 0;
       newtio.c_lflag &= ~(ICANON|ECHO|ECHOE|ISIG);
       newtio.c_cc[VMIN]=1;
       newtio.c_cc[VTIME]=0;
       tcflush(fd, TCIFLUSH);/*丢弃所有驱动以接收但还没读取的数据 ,可选项还有TCOFLUSH,TCIOFLUSH*/
       tcsetattr(fd,TCSANOW,&newtio);
    
  • tcgetattrtcsetattr说明

    int tcgetattr(int fd, struct termios *termptr);/* 获取终端属性*/
    int tcsetattr(int fd, int opt, const struct termios *termptr);/* 设置终端属性*/   
    

    在串口驱动程序里有输入缓冲区和输出缓冲区。在改变串口属性时, 缓冲区可能有数据存在,如何处理缓冲区中的数据,可通过 opt 参数实现:

    • TCSANOW: 更改立即发生;
    • TCSADRAIN: 发送了所有输出后更改才发生,若更改输出参数则应用此选项;
    • TCSAFLUSH: 发送了所有输出后更改才发生, 在更改发生时未读的所有输入数据被删除(Flush) 。

    上述两函数执行时,若成功则返回 0,若出错则返回-1。

  • tcflush()queue_selector
    • TCIFLUSH
      刷新串口输入缓冲中收到但还未被读取的数据
    • TCOFLUSH
      刷新串口输出缓冲中写入但还没发送出去的数据
    • TCIOFLUSH
      刷新串口中所有缓存的数据

四 struct termios各成员详解

4.1 c_cflag 控制选项

  • 作用:可设置串口的波特率、数据位、奇偶校验、停止位以及
    硬件流控制

  • 波特率标志常量

    标 志说 明标 志说 明
    B00 位/秒(挂起)B96009600 位/秒
    B110100 位/秒B1920019200 位/秒
    B134134 位/秒B5760057600 位/秒
    B12001200 位/秒B115200115200 位/秒
    B24002400 位/秒B460800460800 位/秒
    B48004800 位/秒

    波特率系统提供了单独的函数来设置,一般来说,输入、输出的波特率应该是一致的。

    speed_t cfgetispeed(struct termios *termios_p);
    speed_t cfgetospeed(struct termios *termios_p);
    int cfsetispeed(struct termios *termios_p, speed_t speed);
    int cfsetospeed(struct termios *termios_p, speed_t speed);
  • 数据位,奇偶校验、停止位,硬件流控制开关标志常量

    标 志说 明标 志说 明
    CSIZE数据位屏蔽CS77 位数据位
    CS55 位数据位CS88 位数据位
    CS66 位数据位CLOCAL忽略 modem 控制线
    PARENB进行奇偶校验,否则不校验PARODD奇校验,否则为偶校验,需先开启前者
    CSTOPB设2位停止位,否则1位CREAD打开接受者
    CRTSCTS硬件流控制

4.2 c_iflag 输入设置

  • 负责控制串口输入数据的处理

    标 志说 明标 志说 明
    IGNPAR忽略桢错误和奇偶校验错IGNBRK忽略 BREAK 条件
    INPCK打开输入奇偶校验PARMRK标记奇偶错,只有设置了INPCK并且没有设置 IGNPAR 才有效
    ISTRIP去掉字符第8位IGNCR忽略输入中的回车CR
    ICRNL将输入的CR转换为 NL,除非设了IGNCRINLCR将输入的NL(换行)转换为CR
    IXON启用输出的 XON/XOFF 流控制IXOFF启用输入的 XON/XOFF 流控制
    IXANY尝试任何字符可做重启输出信号,默认只能START字符恢复输出IUCLC(not in POSIX)将输入中的大写字母映射为小写字母
    • 使用软件流控制是启用 IXON、IXOFF 和 IXANY 选项:
      options.c_iflag |= (IXON | IXOFF | IXANY);
    • 相反,要禁用软件流控制是禁止上面的选项:
      options.c_iflag &= ~(IXON | IXOFF | IXANY);

      什么是流控制 ?
      两个串行接口之间的传输数据流通常需要协调一致才行。这可能是由于用以通信的某个串行接口或者某些存储介质的中间串行通信链路的限制造成的。对于异步数据这里有两个方法做到这一点。
      第一种方法通常被叫做“软件”流控制。这种方法采用特殊字符来开始(XON,DC1,八进制数021)或者结束(XOFF,DC3或者八进制数023)数据流。而这些字符都在ASCII中定义好了。虽然这些编码对于传输文本信息非常有用,但是它们却不能被用于在特殊程序中的其他类型的信息。
      第二种方法叫做“硬件”流控制。这种方法使用RS-232标准的CTS和RTS信号来取代之前提到的特殊字符。当准备就绪时,接受一方会将CTS信号设置成为space电压,而尚未准备就绪时它会被设置成为mark电压。相应得,发送方会在准备就绪的情况下将RTS设置成space电压。正因为硬件流控制使用了于数据分隔的信号,所以与需要传输特殊字符的软件流控制相比它的速度很快。但是,并不是所有的硬件和操作系统都支持CTS/RTS流控制。

4.3 c_oflag 输出设置

  • 负责控制串口输出数据的处理

    标 志说 明标 志说 明
    OPOST执行输出处理OFILL对于延迟使用填充符
    ONLCR将NL转换为CR-NLOCRNL将输出的CR转换为NL
    ONLRET不输出CR回车ONOCR在0列(行首)不输出CR
    FFDLY换页延迟屏蔽CRDLYCR 延迟屏蔽
    OFDEL填充符为DEL,否则为 NULLOXTABS将制表符扩充为空格
    BSDLY退格延迟屏蔽OLCUC将输出的小写字符转换为大写字符
    CMSPAR标志或空奇偶性
  • 启用输出处理
    启用输出处理需要在 c_oflag 成员中启用 OPOST 选项,其操作方法如下:

    options.c_oflag |= OPOST;
  • 使用原始输出
    就是禁用输出处理,使数据能不经过处理、过滤地完整地输出到串口。
    当 OPOST 被禁止,c_oflag 其它选项也被忽略,其操作方法如下:

    options.c_oflag &= ~OPOST;

4.4 c_lflag 本地设置

  • 控制串口驱动怎样控制输入字符

    标 志说 明标 志说 明
    ISIG当接受到字符 INTR, QUIT, SUSP, 或 DSUSP 时,产生相应的信号NOFLSH在中断或退出键后禁用刷清
    ICANON启用规范输入,默认开启IEXTEN启用扩充的输入字符处理
    XCASE如果同时设置了 ICANON,终端只有大写ECHOCTL如果设置了 ECHO,除了 TAB, NL, START, 和 STOP 之外的 ASCII 控制信号被回显为^X字符形式,
    ECHOPRT硬拷贝的可见擦除方式ECHO回送输入字符
    ECHOE如果设置了 ICANON,字符 ERASE 擦除前一个输入字符,WERASE 擦除前一个词ECHONL如果设置了 ICANON,回送NL
    ECHOK如果设置了 ICANON,字符KILL删除当前行。ECHOKE如果设置了 ICANON,回显 KILL 时将删除一行中的每个字符,如同指定了 ECHOE 和 ECHOPRT 一样
    PENDIN在读入下一个字符时,输入队列中所有字符被重新输出TOSTOP对于后台输出发送SIGTTOU信号
  • 经典输入
    经典输入是以面向行设计的。输入字符会被放入一个缓冲之中,这样可以与用户以交互的方式编辑缓冲的内容,直到收到CR(carriage return)或者LF(line feed)字符。

    options.c_lflag |= (ICANON | ECHO | ECHOE);
  • 原始输入
    输入字符只是被原封不动的接收。一般情况中,如果要使用原始输入模式,程序中需要去掉ICANON,ECHO,ECHOE和ISIG选项:

    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

4.5 c_cc[NCCS] 控制字符

  • 控制字符

    标 志说 明标 志说 明
    VINTR中断VEOL行结束
    VQUIT退出VEOF行结束
    VMIN需读取的最小字节数VERASE擦除
    VTIME与“VMIN”配合使用,是指限定的传输或等待的最长时间

    MIN是指一次read调用期望返回的最小字节数。 VTIME说明等待数据到达的分秒数 (秒的 1/10 为分秒) 。TIME 与 MIN 组合使用的具体含义分为以下四种情形:

    • 当 MIN > 0,TIME > 0 时
      计时器在收到第一个字节后启动, 在计时器超时之前 (TIME 的时间到) , 若已收到 MIN个字节,则 read 返回 MIN 个字节,否则,在计时器超时后返回实际接收到的字节。
      注意:因为只有在接收到第一个字节时才开始计时,所以至少可以返回 1 个字节。这种情形中,在接到第一个字节之前,调用者阻塞。如果在调用 read 时数据已经可用,则如同在 read 后数据立即被接到一样。
    • 当 MIN > 0,TIME = 0 时
      MIN 个字节完整接收后,read 才返回,这可能会造成 read 无限期地阻塞。
    • 当 MIN = 0, TIME > 0 时
      TIME 为允许等待的最大时间,计时器在调用 read 时立即启动,在串口接到 1 字节数据或者计时器超时后即返回,如果是计时器超时,则返回 0。
    • 当 MIN = 0,TIME = 0 时
      如果有数据可用,则 read 最多返回所要求的字节数,如果无数据可用,则 read 立即返回 0。

五 程序清单

来源自3.详解linux下的串口通讯开发

5.1 标准输入程序

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include<errno.h>

/* 波特率设定被定义在此 */
#define BAUDRATE B38400            
/* 定义正确的串口设备 */
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1 /* POSIX 系统兼容 */

#define FALSE 0
#define TRUE 1

volatile int STOP=FALSE; 

main()
{
  int fd,c, res;
  struct termios oldtio,newtio;
  char buf[255];
/* 
  开启数据机装置以读取并写入而不以控制终端tty的模式
  因为我们不想程序在送出 CTRL-C 后就被杀掉.
*/
 fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY ); 
 if (fd <0) {perror(MODEMDEVICE); exit(-1); }

 tcgetattr(fd,&oldtio); /* 储存目前的序列埠设定 */
 bzero(&newtio, sizeof(newtio)); /* 清除结构体以放入新的序列埠设定值 */

/* 
  BAUDRATE: 设定 bps 的速度. 你也可以用 cfsetispeed 及 cfsetospeed 来设定.
  CRTSCTS : 输出资料的硬件流量控制 (只能在具完整线路的缆线下工作,没有就不要设,不然无法用)
  CS8     : 8n1 (8 位元, 不做同位元检查,1 个终止位元)
  CLOCAL  : 本地连线, 不具数据机控制功能
  CREAD   : 致能接收字元
*/
 newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;

/*
  IGNPAR  : 忽略经同位元检查后, 错误的位元组
  ICRNL   : 比 CR 对应成 NL (否则当输入信号有 CR 时不会终止输入)
            在不然把装置设定成 raw 模式(没有其它的输入处理)
*/
 newtio.c_iflag = IGNPAR | ICRNL;

/*
 Raw 模式输出.
*/
 newtio.c_oflag = 0;

/*
  ICANON  : 致能标准输入, 使所有回应机能停用, 并不送出信号以叫用程序
*/
 newtio.c_lflag = ICANON;

/* 
  初始化所有的控制特性
  预设值可以在 /usr/include/termios.h 找到, 在注解中也有,但我们在这不需要看它们
*/
 newtio.c_cc[VINTR]    = 0;     /* Ctrl-c */ 
 newtio.c_cc[VQUIT]    = 0;     /* Ctrl-/ */
 newtio.c_cc[VERASE]   = 0;     /* del */
 newtio.c_cc[VKILL]    = 0;     /* @ */
 newtio.c_cc[VEOF]     = 4;     /* Ctrl-d */
 newtio.c_cc[VTIME]    = 0;     /* 不使用分割字元组的计时器 */
 newtio.c_cc[VMIN]     = 1;     /* 在读取到 1 个字元前先停止 */
 newtio.c_cc[VSWTC]    = 0;     /* '/0' */
 newtio.c_cc[VSTART]   = 0;     /* Ctrl-q */ 
 newtio.c_cc[VSTOP]    = 0;     /* Ctrl-s */
 newtio.c_cc[VSUSP]    = 0;     /* Ctrl-z */
 newtio.c_cc[VEOL]     = 0;     /* '/0' */
 newtio.c_cc[VREPRINT] = 0;     /* Ctrl-r */
 newtio.c_cc[VDISCARD] = 0;     /* Ctrl-u */
 newtio.c_cc[VWERASE]  = 0;     /* Ctrl-w */
 newtio.c_cc[VLNEXT]   = 0;     /* Ctrl-v */
 newtio.c_cc[VEOL2]    = 0;     /* '/0' */

/* 
  现在清除数据机线并启动序列埠的设定
*/
 tcflush(fd, TCIFLUSH);
 tcsetattr(fd,TCSANOW,&newtio);

/*
  终端机设定完成, 现在处理输入信号
  在这个范例, 在一行的开始处输入 'z' 会退出此程序.
*/
 while (STOP==FALSE) {     /* 回圈会在我们发出终止的信号后跳出 */
 /* 即使输入超过 255 个字元, 读取的程序段还是会一直等到行终结符出现才停止.
    如果读到的字元组低于正确存在的字元组, 则所剩的字元会在下一次读取时取得.
    res 用来存放真正读到的字元组个数 */
    res = read(fd,buf,255); 
    buf[res]=0;             /* 设定字串终止字元, 所以我们能用 printf */
    printf(":%s:%d/n", buf, res);
    if (buf[0]=='z') STOP=TRUE;
 }
 /* 回存旧的序列埠设定值 */
 tcsetattr(fd,TCSANOW,&oldtio);
}

5.2 非标准输入程序

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include<errno.h>

#define BAUDRATE B38400
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1 /* POSIX 系统兼容 */
#define FALSE 0
#define TRUE 1

volatile int STOP=FALSE; 

main()
{
  int fd,c, res;
  struct termios oldtio,newtio;
  char buf[255];

 fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY ); 
 if (fd <0) {perror(MODEMDEVICE); exit(-1); }

 tcgetattr(fd,&oldtio); /* 储存目前的序列埠设定 */

 bzero(&newtio, sizeof(newtio));
 newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
 newtio.c_iflag = IGNPAR;
 newtio.c_oflag = 0;

 /* 设定输入模式 (非标准型, 不回应,...) */
 newtio.c_lflag = 0;

 newtio.c_cc[VTIME]    = 0;   /* 不使用分割字元组计时器 */
 newtio.c_cc[VMIN]     = 5;   /* 在读取到 5 个字元前先停止 */

 tcflush(fd, TCIFLUSH);
 tcsetattr(fd,TCSANOW,&newtio);


 while (STOP==FALSE) {       /* 输入回圈 */
   res = read(fd,buf,255);   /* 在输入 5 个字元后即返回 */
   buf[res]=0;               /* 所以我们能用 printf... */
   printf(":%s:%d/n", buf, res);
   if (buf[0]=='z') STOP=TRUE;
 }
 tcsetattr(fd,TCSANOW,&oldtio);
}

5.3 非同步式输入

异步串口通信,使用软中断模式接收串口数据,主要用到了linux的信号相关知识

信号(signal),又称为软中断信号,用来通知进程发生了异步事件。进程之间可以互相发送软中断信号。 内核也可以因为内部事件而给进程发送信号, 通知进程发生了某个事件。
注意,信号只是用来通知进程发生了什么事件,并不给该进程传递任何数据。

此处通过sigaction(SIGIO,&saio,NULL);SIGIO信号安装了一个中断处理函数signal_handler_IO 。当串口有收发数据时就会发出一个SIGIO信号,系统就会调用信号中断处理函数了。

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>

#define BAUDRATE B38400
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1 /* POSIX 系统兼容 */
#define FALSE 0
#define TRUE 1

volatile int STOP=FALSE; 

void signal_handler_IO (int status);   /* 定义信号处理程序 */
int wait_flag=TRUE;                    /* 没收到信号的话就会是 TRUE */

main()
{
  int fd,c, res;
  struct termios oldtio,newtio;
  struct sigaction saio;           /* definition of signal action */
  char buf[255];

  /* 开启装置为 non-blocking (读取功能会马上结束返回) */
  fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY | O_NONBLOCK);
  if (fd <0) {perror(MODEMDEVICE); exit(-1); }

  /* 在使装置非同步化前, 安装信号处理程序 */
  saio.sa_handler = signal_handler_IO;
  saio.sa_mask = 0;
  saio.sa_flags = 0;
  saio.sa_restorer = NULL;
  sigaction(SIGIO,&saio,NULL);

  /* 允许行程去接收 SIGIO 信号*/
  fcntl(fd, F_SETOWN, getpid());
  /* 使文档ake the file descriptor 非同步 (使用手册上说只有 O_APPEND 及 O_NONBLOCK, 而 F_SETFL 也可以用...) */
  fcntl(fd, F_SETFL, FASYNC);

  tcgetattr(fd,&oldtio); /* 储存目前的序列埠设定值 */
  /* 设定新的序列埠为标准输入程序 */
  newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
  newtio.c_iflag = IGNPAR | ICRNL;
  newtio.c_oflag = 0;
  newtio.c_lflag = ICANON;
  newtio.c_cc[VMIN]=1;
  newtio.c_cc[VTIME]=0;
  tcflush(fd, TCIFLUSH);
  tcsetattr(fd,TCSANOW,&newtio);

  /* 等待输入信号的回圈. 很多有用的事我们将在这做 */ 
  while (STOP==FALSE) {
    printf("./n");usleep(100000);
    /* 在收到 SIGIO 后, wait_flag = FALSE, 输入信号存在则可以被读取 */
    if (wait_flag==FALSE) { 
      res = read(fd,buf,255);
      buf[res]=0;
      printf(":%s:%d/n", buf, res);
      if (res==1) STOP=TRUE; /* 如果只输入 CR 则停止回圈 */
      wait_flag = TRUE;      /* 等待新的输入信号 */
    }
  }
  /* 回存旧的序列埠设定值 */
  tcsetattr(fd,TCSANOW,&oldtio);
}

/***************************************************************************
* 信号处理程序. 设定 wait_flag 为 FALSE, 以使上述的回圈能接收字元          *
***************************************************************************/

void signal_handler_IO (int status)
{
  printf("received SIGIO signal./n");
  wait_flag = FALSE;
}

上面的中断信号不能区分是写入还是发出数据时的中断,这个可以具体看看Linux的信号章节。或我写的信号函数sigaction说明

5.4 等待来自多个信号来源的输入

Linux下直接用read读串口可能会造成堵塞,或数据读出错误。然而用select先查询串口,再用read去读就可以避免,并且当串口延时时,程序可以退出,这样就不至于由于串口堵塞,程序就死了。利用select函数还可以实现多个串口的读写,

main()
{
    int    fd1, fd2;  /* 输入源 1 及 2 */
    fd_set readfs;    /* 文档叙述结构设定 */
    int    maxfd;     /* 最大可用的文档叙述结构 */
    int    loop=1;    /* 回圈在 TRUE 时成立 */ 
    int res;
    struct timeval Timeout;
    /* 设定输入回圈的逾时值 */
    Timeout.tv_usec = 0;  /* 毫秒 */
    Timeout.tv_sec  = 1;  /* 秒 */

   /* open_input_source 开启一个装置, 正确的设定好序列埠,
      并回传回此文档叙述结构体 */
   fd1 = open_input_source("/dev/ttyS1");   /* COM2 */
   if (fd1<0) exit(0);
   fd2 = open_input_source("/dev/ttyS2");   /* COM3 */
   if (fd2<0) exit(0);
   maxfd = MAX (fd1, fd2)+1;  /* 测试最大位元输入 (fd) */

   /* 输入回圈 */
   while (loop) {
     FD_ZERO(&readfs)//清除一个文件描述符集;
     FD_SET(fd1, &readfs);  /* 测试输入源 1 */
     FD_SET(fd2, &readfs);  /* 测试输入源 2 */
     /* block until input becomes available */
    res = select(maxfd, &readfs, NULL, NULL, &Timeout);
    if (res==0)
        continue;/*超时*/
    else if(res<0){
            if (errno == EINTR)    
                continue;    
            ERR_EXIT("select error");  
    }
    else{
        if (FD_ISSET(fd1))         /* 如果输入源 1 有信号 */
           handle_input_from_source1();
        if (FD_ISSET(fd2))         /* 如果输入源 2 有信号 */
           handle_input_from_source2();
       }
   }

}   
  • 3
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值