LWIP(stm32+lwip+freertos)

前言

本文是学习之余的记录,后续内容有时间补充

代码:git地址

一、LWIP配置

1.1 Project Manager

填写名称和选择路径,然后IDE选择MDK-ARM
Code Generator下选择.c和.h文件分开编译

在这里插入图片描述

1.2 基本外设选择

可以自行添加其他外设,本文主要进行基本配置

1.2.1 SYS与RCC

sys选择SWD接口
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.2.1 ETH

选择RMII,后进行ETH对应的GPIO配置
GPIO Settings 9个引脚一个一个对比,下图是正常情况下的配置,根据自己开发板的原理图修改对应配置

在这里插入图片描述

/* RMII引脚配置(一般是7个引脚) */
RMII_TXD0				PB12	//发送数据位0
RMII_TXD1				PB13	//发送数据位1
RMII_TX_EN				PB11	//发送数据的有效信号
RMII_RXD0				PC4		//接收数据位0
RMII_RXD1				PC5		//接收数据位1
RMII_CRS_DV				PA7		//接收数据的有效信号
RMII_REF_CLK			PA1		//参考时钟
/* 两个SMI引脚 */
MDC						PC1		//为PHY芯片提供参考信号
MDIO					PA2		//MCU与PHY芯片之间传输数据
下图是我自己开发板(STM32F407ZGT6)的配置

在这里插入图片描述

1.2.3 USART

串口主要用来测试使用,尽量配置一下(点击配置为异步模式即可)

在这里插入图片描述

1.3 LWIP

配置其PHY芯片,大部分单片机都是LAN8742,芯片是LAN后缀不是8742的也可以选择LAN8742

在这里插入图片描述

1.失能DHCP
2.配置IP
IP_ADDRESS	单片机IP
NETMAS		子网掩码
GATEWAY		路由IP

在这里插入图片描述

配置完上文的就可以生成keil文件然后打开

1.4 keil与电脑IP

在usart.c下加入头文件和串口重定向代码,然后再到main函数中添加打印测试代码

#include <stdio.h>
//串口重定向代码
int fputc(int ch, FILE *f)
{
	while ((USART1->SR & 0x40) == 0);
	USART1->DR = (uint8_t) ch;
	return ch;
}

假如电脑网络连接显示如下,则表明需要在软件上进行复位操作,如果显示有连接则直接进行下一步
在这里插入图片描述
以太网未识别到网线

查询原理图中ETH_RESET对应的引脚(我的板子是PE2),在STM32CubeMX和keil中的配置如下所示:
在这里插入图片描述
在这里插入图片描述

//软件复位操作---ETH_RESET
HAL_GPIO_WritePin(ETH_RESET_GPIO_Port, ETH_RESET_Pin, GPIO_PIN_RESET);
HAL_Delay(50);
HAL_GPIO_WritePin(ETH_RESET_GPIO_Port, ETH_RESET_Pin, GPIO_PIN_SET);

以太网识别到网络电缆显示未识别的网络或者其他内容

配置以太网的IPV4与单片机的IP在用一个网段即可,上文配置单片机IP为192.168.1.100

在这里插入图片描述
main.c的主循环中加入这句MX_LWIP_Process(); 然后下载程序到单片机中。win+R输入cmd,在窗口下ping单片机的IP,成功则表示LWIP配置成功
在这里插入图片描述
在这里插入图片描述

1.4 其他补充

//加快烧录速度
Debug	//生成可执行文件
Create	//生成HEX文件
Browse	//生成调试信息用于跳转

在这里插入图片描述

二、LWIP实现

2.1 TCP回响服务器

1.移植tcp_echoserver.c和tcp_echoserver.h文件到程序文件中
2.加入函数tcp_echoserver_init();到主函数中初始化中(需要先添加头文件)
3.打开串口调试工具,因为单片机是作为TCP Server端,所以电脑作为TCP Client端,配置IP和端口号
4.发送消息回传一样的,则TCP回响实现成功

在这里插入图片描述

2.2 加入Free RTOS

sys的时基选择TIM1

在这里插入图片描述

选择CMSIS_V1,然后配置栈空间为1024`在这里插入代码片`

在这里插入图片描述

配置完后,ETH的Rx Mode会自动变为Interrupt Mode,中断也会打开在这里插入代码片

在这里插入图片描述

2.3 TCP_Server

1.单片机socket_tcp_server.c代码编写
2.在freeRTOS的任务代码中加入TCP的代码函数vTCPServer_Task();
3.打开电脑端调试助手--选择TCP Client ip和端口选择代码设置的
4.发送小写转换为大写则服务端代码成功
/* 此时单片机作为服务器 IP:192.168.1.100 端口3333 
 * 电脑端网络调试助手选择TCP Client,让其连接服务器(单片机)
 * 所以此时电脑端的IP和端口号则和单片机相同
*/
/* socket_tcp_server.c */
#include "socket_tcp_server.h"
#include "lwip/sockets.h"
#include <ctype.h>

char ReadBuff[BUFF_SIZE];

/* TCP服务器端(server)任务 */
void vTCPServer_Task(void)
{ 
    int sfd, cfd;
    int read_len;                                // 读的数据长度
    struct sockaddr_in server_addr, client_addr; // 服务器和客户端的结构体
    socklen_t client_addr_len;
    // 创建socket
    sfd = socket(AF_INET, SOCK_STREAM, 0); // IPV4 socker连接 默认协议
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 从本机获取
    // 绑定socket
    bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 文件描述符 IP地址加端口号 addr长度 返回值
    // 监听socket
    listen(sfd, 5); // 文件描述符 握手队列总和
    // 等待客户端连接
    client_addr_len = sizeof(client_addr);
    cfd = accept(sfd, (struct sockaddr *)&client_addr, &client_addr_len); // 文件描述符 IP地址加端口号 返回值
    printf("client is connect cfd = %d\r\n", cfd);
    while (1) // 等待客户端发送数据,然后进行大小写转换,再写回客户端
    {
        read_len = read(cfd, ReadBuff, BUFF_SIZE);
        for (int i = 0; i < read_len; i++)
        {
            ReadBuff[i] = toupper(ReadBuff[i]); // 小写转换为大写
        }
        write(cfd, ReadBuff, read_len);
    }
}


/* socket_tcp_server.h */
#ifndef __SOCKET_TCP_SERVER_H
#define __SOCKET_TCP_SERVER_H

#define SERVER_PORT 3333 // 端口号
#define BUFF_SIZE 1024
#define SERVER_IP "192.168.1.100"
void vTCPServer_Task(void);

#endif
void StartDefaultTask(void const * argument)
{
  /* init code for LWIP */
  MX_LWIP_Init();
  /* USER CODE BEGIN StartDefaultTask */
	//运行成功
  puts("lwip and freertos init is ok");
  /* Infinite loop */
  for(;;)
  {
	vTCPServer_Task();	//加入任务的代码
    osDelay(1);
  }
  /* USER CODE END StartDefaultTask */
}

运行成功显示如下:
在这里插入图片描述

2.4 TCP_Client

1.单片机socket_tcp_client.c代码编写
2.在freeRTOS的任务代码中加入TCP的代码函数vTCPClient_Task();
3.打开电脑端调试助手--选择TCP Server ip和端口选择与电脑网线连接的IPV4
4.发送小写转换为大写则服务端代码成功
/* 此时单片机作为客户端,所以要选启动电脑端的网络调试助手
 * 选择TCP Server 端口选择代码中设置的,IP选择单片机与电脑相连接的以太网的IPV4
 * 单片机作为客户端 IP:192.168.1.100 端口3333 
 * 代码中提前写好了单片机需要连接的客户端IP和端口号,上电单片机会自动连接
*/
/* socket_tcp_client.c */
#include "socket_tcp_client.h"
#include "socket_tcp_server.h"
#include "lwip/sockets.h"
#include <ctype.h>

static char ReadBuff[BUFF_SIZE];

/* TCP客户端端(client)任务 */
void vTCPClient_Task(void)
{
    int cfd;
    int read_len;                   // 读的数据长度
    struct sockaddr_in server_addr; // 服务器结构体
    // 创建socket
    cfd = socket(AF_INET, SOCK_STREAM, 0); // IPV4 socker连接 默认协议
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 本机IPV4
	
    // 连接到服务器
    connect(cfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 文件描述符 ip地址和端口号 大小
    printf("server is connect ok");
    printf("server is connect cfd = %d\r\n", cfd);
    while (1)
    {
        read_len = read(cfd, ReadBuff, BUFF_SIZE); // 等待服务器发送数据
        for (int i = 0; i < read_len; i++)
        {
            ReadBuff[i] = toupper(ReadBuff[i]); // 小写转换为大写
        }
        write(cfd, ReadBuff, read_len); // 再写回服务器
    }
}

/* socket_tcp_client.h */
#ifndef __SOCKET_TCP_CLIENT_H
#define __SOCKET_TCP_CLIENT_H

void vTCPClient_Task(void);

#endif
void StartDefaultTask(void const * argument)
{
  /* init code for LWIP */
  MX_LWIP_Init();
  /* USER CODE BEGIN StartDefaultTask */
	//运行成功
  puts("lwip and freertos init is ok");
  /* Infinite loop */
  for(;;)
  {
	vTCPClient_Task(); //加入任务的代码
    osDelay(1);
  }
  /* USER CODE END StartDefaultTask */
}

运行成功显示如下:
在这里插入图片描述

2.5 问题改进

此时对于单片机TCP连接的客户端和服务端都可以实现,但是代码中并没有处理好,开始连接和断开连接等细节问题
我做的处理是封装一个文件用于进行处理
下文分别是:
socket_wrap.c	socket_wrap.h	---封装的代码
socket_tcp_server.c	socket_tcp_server.h	---改进后server代码
socket_tcp_client.c	socket_tcp_client.h	---改进后client代码
/* socket_wrap.c */
#include "socket_wrap.h"

/**
 * @brief  创建套接字
 * @param  domain: 		协议族 选择IPV4或IPV6
 * @param  type: 		协议类型 选择TCP、UDP或RAW
 * @param  protocol: 	协议版本 一般为0
 * @retval 失败:		-1(申请创建套接字失败)
 *			成功:		非负整数(socket描述符可以链接到lwip_sock)
 */
int My_Socket(int domain, int type, int protocol)
{
	int fd; // socket的返回值
	fd = socket(domain, type, protocol);
	// 当返回值为-1,大概率是lwip的内存不够
	if (fd == -1)
	{
		puts("create socket error");
		vTaskDelete(NULL); // 删除自身进行任务调度
	}
	return fd;
}

/**
 * @brief  绑定套接字
 * @param  s: 			要绑定的socket套接字
 * @param  name: 		指向sockaddr的结构体指针(包含网卡IP、端口号等)
 * @param  namelen: 	name结构体长度
 * @retval 失败:		-1
 *			成功:		0
 */
int My_Bind(int s, const struct sockaddr *name, socklen_t namelen)
{
	int ret;
	ret = bind(s, name, namelen);
	// 绑定失败
	if (ret == -1)
	{
		puts("bind socket error");
		vTaskDelete(NULL); // 删除自身进行任务调度
	}
	return ret;
}

/**
 * @brief  在客户端中向服务器建立连接
 * @param  s: 			要连接的socket套接字
 * @param  name: 		指向sockaddr的结构体指针(包含网卡IP、端口号等)
 * @param  namelen: 	name结构体长度
 * @retval 失败:		-1
 *			成功:		0
 */
int My_Connect(int s, const struct sockaddr *name, socklen_t namelen)
{
	int ret;
	ret = lwip_connect(s, name, namelen);
	// 连接失败
	if (ret == -1)
	{
		puts("connect socket error");
		close(s); // 连接失败关闭当前的socket(内部会自动删除socket内存块)
	}
	return ret;
}

/**
 * @brief  监听套接字
 * @param  s: 			要监听的socket
 * @param  backlog: 	请求队列的大小
 * @retval 失败:		-1
 *			成功:		0
 */
int My_Listen(int s, int backlog)
{
	int ret;
	ret = listen(s, backlog);
	// 监听失败
	if (ret == -1)
	{
		puts("listen socket error");
		vTaskDelete(NULL); // 删除自身进行任务调度
	}
	return ret;
}

/**
 * @brief  等待客户端连接请求
 * @param  s: 			请求的socket
 * @param  addr: 		绑定的客户端地址、端口号等信息
 * @param  addrlen: 	地址结构体长度
 * @retval 失败:		-1
 *			成功:		非负整数(socket描述符可以链接到lwip_sock)
 */
int My_Accept(int s, struct sockaddr *addr, socklen_t *addrlen)
{
	int fd;
again_accept:
	// 阻塞函数,错误返回或者连接成功返回
	fd = accept(s, addr, addrlen);
	// 客户端连接错误
	if (fd == -1)
	{
		puts("accept connect error");
		goto again_accept; // 重新请求连接
	}
	return fd;
}

/**
 * @brief  向套接字发送数据
 * @param  s: 			发送数据到socket
 * @param  data: 		发送的缓冲区
 * @param  size: 		发送数据大小(单位字节)
 * @retval 失败:		-1
 *			成功:		返回一个新的套接字描述符(用于和已连接的客户端通信)
 */
int My_Write(int s, const void *data, size_t size)
{
	int ret;
	ret = write(s, data, size);
	// 发送失败(socket错误或者对方关闭连接)
	if (ret < 0)
	{
		puts("write data to socket error");
		close(s);
	}
	return ret;
}

/**
 * @brief  向套接字读取数据
 * @param  s: 			接收数据的socket
 * @param  data: 		接受的缓冲区
 * @param  size: 		接收数据大小(单位字节)
 * @retval 失败:		-1
 *			成功:		返回已经接收的数据长度
 */
int My_Read(int s, void *mem, size_t len)
{
	int ret;
	ret = read(s, mem, len);
	// 读取失败(socket错误或者对方关闭连接)
	if (ret <= 0)
	{
		puts("read data to socket error");
		close(s);
	}
	return ret;
}

/* socket_wrap.h */
#ifndef __SOCKET_WRAP_H
#define __SOCKET_WRAP_H

#include "lwip/sockets.h"
#include "FreeRTOS.h"
#include "task.h"

int My_Socket(int domain, int type, int protocol); //创建套接字
int My_Bind(int s, const struct sockaddr *name, socklen_t namelen); //绑定套接字
int My_Connect(int s, const struct sockaddr *name, socklen_t namelen); //在客户端中向服务器建立连接
int My_Listen(int s, int backlog); //监听套接字
int My_Accept(int s, struct sockaddr *addr, socklen_t *addrlen); //等待客户端连接请求
int My_Write(int s, const void *data, size_t size); //向套接字发送数据
int My_Read(int s, void *mem, size_t len); //向套接字读取数据

#endif
/* socket_tcp_server.c */
#include "socket_tcp_server.h"
#include "lwip/sockets.h"
#include <ctype.h>

char ReadBuff[BUFF_SIZE];

/* TCP服务器端(server)任务 */
void vTCPServer_Task(void)
{ 
    int sfd, cfd;
    int read_len;                                // 读的数据长度
    int write_len;
    struct sockaddr_in server_addr, client_addr; // 服务器和客户端的结构体
    socklen_t client_addr_len;
    // 创建socket
    sfd = My_Socket(AF_INET, SOCK_STREAM, 0); // IPV4 socker连接 默认协议
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 从本机获取
    // 绑定socket
    My_Bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 文件描述符 IP地址加端口号 addr长度 返回值
    // 监听socket
    My_Listen(sfd, 5); // 文件描述符 握手队列总和
    // 等待客户端连接
    client_addr_len = sizeof(client_addr);
again:
    cfd = My_Accept(sfd, (struct sockaddr *)&client_addr, &client_addr_len); // 文件描述符 IP地址加端口号 返回值
    printf("client is connect cfd = %d\r\n", cfd);
    while (1) // 等待客户端发送数据,然后进行大小写转换,再写回客户端
    {
        read_len = My_Read(cfd, ReadBuff, BUFF_SIZE);
        if (read_len == -1)
        {
            goto again;
        }
        for (int i = 0; i < read_len; i++)
        {
            ReadBuff[i] = toupper(ReadBuff[i]); // 小写转换为大写
        }
        write_len = My_Write(cfd, ReadBuff, read_len);
        if (write_len == -1)
        {
            goto again;
        }
    }
}

/* socket_tcp_server.h */
#ifndef __SOCKET_TCP_SERVER_H
#define __SOCKET_TCP_SERVER_H

#include "socket_wrap.h"

#define SERVER_PORT 5555 // 端口号
#define BUFF_SIZE 1024
/* 单片机作为客户端需要连接服务器,这是服务器的IP(socker_tcp_client被调用) */
#define SERVER_IP "192.168.1.10"	
void vTCPServer_Task(void);

#endif
/* socket_tcp_client.c */
#include "socket_tcp_client.h"
#include "socket_tcp_server.h"
#include "lwip/sockets.h"
#include <ctype.h>

static char ReadBuff[BUFF_SIZE];

/* TCP客户端端(client)任务 */
void vTCPClient_Task(void)
{
    int cfd;
    int read_len;                   // 读的数据长度
    int write_len;                  // 写的数据长度
    struct sockaddr_in server_addr; // 服务器结构体
    int ret;                        // 连接状态
again:
    // 创建socket
    cfd = My_Socket(AF_INET, SOCK_STREAM, 0); // IPV4 socker连接 默认协议
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 本机IPV4

    // 连接到服务器
    ret = My_Connect(cfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 文件描述符 ip地址和端口号 大小
    if (ret == -1)
    {
        vTaskDelay(100); // 100ms连接一次服务器
        goto again;
    }
    printf("server is connect ok");
    printf("server is connect cfd = %d\r\n", cfd);
    while (1)
    {
        read_len = My_Read(cfd, ReadBuff, BUFF_SIZE); // 等待服务器发送数据
        if (read_len == -1)
        {
            goto again;
        }
        for (int i = 0; i < read_len; i++)
        {
            ReadBuff[i] = toupper(ReadBuff[i]); // 小写转换为大写
        }
        write_len = My_Write(cfd, ReadBuff, read_len); // 再写回服务器
        if (write_len == -1)
        {
            goto again;
        }
    }
}

/* socket_tcp_client.h */
#ifndef __SOCKET_TCP_CLIENT_H
#define __SOCKET_TCP_CLIENT_H

void vTCPClient_Task(void);

#endif

2.6 UDP_Server

1.单片机socket_wrap.c(添加封装的读写代码)和socket_udp_server.c代码编写
2.在freeRTOS的任务代码中加入TCP的代码函数vUDPServer_Task();
3.打开电脑端调试助手--选择UDP ip选择与单片机同一网段的,端口随意写
4.发送小写转换为大写则服务端代码成功
/* 此时单片机作为UDP服务器 IP:192.168.1.100 端口3333 
*/
/**
 * @brief  发送数据到指定地址
 * @param  s: 			发送数据的套接字
 * @param  data: 		发送数据的起始地址
 * @param  size: 		数据长度
 * @param  flags: 		发送时的处理(默认为0)
 * @param  to: 			远端主机的IP和端口号
 * @param  tolen: 		信息长度
 * @retval 失败:		-1
 *		   成功:		返回已经发送的数据长度
 */
int My_Sendto(int s, const void *data, size_t size, int flags,
			  const struct sockaddr *to, socklen_t tolen)
{
	int ret;
again_sendto:
	ret = sendto(s, data, size, flags, to, tolen);
	if (ret == -1)
	{
		puts("sendto socket error");
		goto again_sendto;
	}
	return ret;
}

/**
 * @brief  接收数据到指定地址
 * @param  s: 			接收数据的套接字
 * @param  mem: 		接收数据的缓冲起始地址
 * @param  len: 		数据长度
 * @param  flags: 		接收时的处理(默认为0)
 * @param  from: 		远端主机的IP和端口号
 * @param  fromlen: 	信息长度
 * @retval 失败:		-1
 *		   成功:		返回已经接收的数据长度
 */
int My_Recvfrom(int s, void *mem, size_t len, int flags,
				struct sockaddr *from, socklen_t *fromlen)
{
	int ret;
again_recvfrom:
	ret = recvfrom(s, mem, len, flags, from, fromlen);
	if (ret == -1)
	{
		puts("sendto socket error");
		goto again_recvfrom;
	}
	return ret;
}
/* socket_udp_server.c */
#include "socket_udp_server.h"
#include "socket_tcp_server.h"
#include <ctype.h>

char UDPServer_ReadBuff[BUFF_SIZE];

/* UDP服务器端(server)任务 */
void vUDPServer_Task(void)
{ 
    int sfd;
    int read_len;                                // 读的数据长度
    struct sockaddr_in server_addr, client_addr; // 服务器和客户端的结构体
    socklen_t client_addr_len;
    // 创建socket
    sfd = My_Socket(AF_INET, SOCK_DGRAM, 0); // IPV4 通信方式 版本(默认0)
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 从本机获取
    // 绑定socket
    My_Bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 文件描述符 IP地址加端口号 addr长度 返回值

    client_addr_len = sizeof(client_addr);
    // 等待客户端连接
    while (1) // 等待客户端发送数据,然后进行大小写转换,再写回客户端
    {
        read_len = My_Recvfrom(sfd, UDPServer_ReadBuff, BUFF_SIZE, 0, (struct sockaddr *)&client_addr, &client_addr_len);
        for (int i = 0; i < read_len; i++)
        {
            UDPServer_ReadBuff[i] = toupper(UDPServer_ReadBuff[i]); // 小写转换为大写
        }
        My_Sendto(sfd, UDPServer_ReadBuff, BUFF_SIZE, 0, (struct sockaddr *)&client_addr, client_addr_len);
    }
}


/* socket_udp_server.h */
#ifndef __SOCKET_UDP_SERVER_H
#define __SOCKET_UDP_SERVER_H

void vUDPServer_Task(void);

#endif
实现STM32 FreeRTOS LwIP TCP服务器需要按照以下步骤进行操作: 1. 首先,需要配置LwIPFreeRTOS。可以在STM32CubeMX中选择配置相应的组件,生成对应的代码和初始化函数。 2. 在代码中创建任务来处理TCP服务器。通过创建一个任务,可以将其分配给特定的核心,以处理TCP请求和响应。 3. 在任务中,首先需要进行LwIPFreeRTOS的初始化。这样可以确保网络和操作系统的适当设置。需要调用lwip_init()和vTaskStartScheduler()函数。 4. 配置和创建TCP服务器的套接字。可以通过调用lwip_socket()函数创建一个TCP套接字,并使用lwip_bind()函数将其与特定的IP地址和端口绑定。 5. 通过调用lwip_listen()函数监听TCP套接字,等待客户端的连接。 6. 使用lwip_accept()函数接受客户端的连接请求,并获得一个新的套接字来处理与该客户端之间的通信。 7. 通过调用lwip_recv()和lwip_send()函数来接收和发送数据。可以使用这些函数接收来自客户端的数据,并发送响应数据给客户端。 8. 当与客户端的通信完成后,使用lwip_close()函数关闭套接字。 9. 循环进行步骤6-8,以处理其他客户端的连接和通信请求。 需要注意的是,STM32系列芯片的内存和处理能力有限,因此在编写代码时需要谨慎处理内存和资源的分配和释放,以确保程序的稳定性和性能。 总结:通过以上步骤,可以在STM32上使用FreeRTOSLwIP实现TCP服务器,使其能够接受和处理客户端的连接和通信请求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值