嵌入式操作系统实验:实验三 消息队列

本文介绍了在实验中通过创建TaskQSen和TaskQRec任务,实现实时操作系统中消息队列的初始化、消息发送与接收,强调了消息队列在任务间异步通信中的作用。作者通过实践掌握了OSQ相关API的使用和数据结构操作。
摘要由CSDN通过智能技术生成

实验三 消息队列

一、实验目的

理解消息队列的定义及其功能;

掌握消息队列相关数据结构定义及操作规则;

掌握初始化消息队列、建立消息队列、发消息到消息队列、等待消息队列中的消息的流程;

实现OS_QInit( )、OSQCreate( )、OSQPost( )、OSQPend( )。

二、实验内容

假设有任务TaskQSen和TaskQRec, TaskQSen在时间片1创建一个消息队列,然后每秒向消息队列中发邮件,其余时间延时。TaskQRec每2秒从消息队列中取邮件,然后延时。

三、对相关数据结构、函数(函数名、返回值,参数)及关键代码进行详细说明

(一)、主函数

int main(int argc, char **argv)
{
	int p[2],Experiment ;
	p[0]=0;
	p[1]=100;
	VCInit();
    printf("0.没有用户任务\n");
    printf("1.第一个例子,一个用户任务\n");
    printf("2.第二个例子,两个任务共享CPU交替运行\n");
    printf("3.第三个例子,任务的挂起和恢复\n");
    printf("4.第四个例子,信号量管理\n");
    printf("5.第五个例子,互斥信号量管理\n"); 
    printf("6.第六个例子,事件标志组\n");
    printf("7.第七个例子,消息邮箱\n");
    printf("8.第八个例子,消息队列\n");
    printf("9.第九个例子,内存管理\n"); 
	
    printf("请输入序号选择例子:\n");
	scanf("%d",&Experiment);
    if ((Experiment<0)||(Experiment>10))
	{
		printf("无效的输入!");
        return(1); 	
	}
	OSInit();
	OSTaskCreate(TaskStart, 0, &TaskStk[1][TASK_STK_SIZE-1], TaskStart_Prio);
	switch(Experiment) 
	{
		case 1://一个任务运行
			OSTaskCreate(FirstTask, 0, &TaskStk[5][TASK_STK_SIZE-1], 5);
			break;
		case 2://两个任务共享CPU
			OSTaskCreate(E2_task1, 0, &TaskStk[5][TASK_STK_SIZE-1], 5);
			OSTaskCreate(E2_task2, 0, &TaskStk[6][TASK_STK_SIZE-1], 6);
            break;
		case 3://任务的挂起和恢复
			OSTaskCreate(E3_task0, 0, &TaskStk[5][TASK_STK_SIZE-1], 5);
			OSTaskCreate(E3_task1, 0, &TaskStk[6][TASK_STK_SIZE-1], 6);
			OSTaskCreate(E3_task2, 0, &TaskStk[7][TASK_STK_SIZE-1], 7);
            break;
		case 4://信号量管理例程
			OSTaskCreate(UserTaskSemA, 0, &TaskStk[5][TASK_STK_SIZE-1], 7);
			OSTaskCreate(UserTaskSemB, 0, &TaskStk[6][TASK_STK_SIZE-1], 6);
			OSTaskCreate(UserTaskSemC, 0, &TaskStk[7][TASK_STK_SIZE-1], 5);
            break;
		case 5://互斥信号量管理例程
			OSTaskCreate(TaskMutex1, 0, &TaskStk[6][TASK_STK_SIZE-1], 6);
			OSTaskCreate(TaskMutex2, 0, &TaskStk[7][TASK_STK_SIZE-1], 50);
			OSTaskCreate(TaskPrint, 0, &TaskStk[8][TASK_STK_SIZE-1], 30);
            break;
		case 6://时间标志组管理例程
			OSTaskCreate(TaskDataProcess, 0, &TaskStk[5][TASK_STK_SIZE-1],5);
			OSTaskCreate(TaskIO1, 0, &TaskStk[6][TASK_STK_SIZE-1], 6);
			OSTaskCreate(TaskIO2, 0, &TaskStk[7][TASK_STK_SIZE-1], 7);
			OSTaskCreate(TaskIO3, 0, &TaskStk[8][TASK_STK_SIZE-1], 8);
			OSTaskCreate(TaskIO4, 0, &TaskStk[9][TASK_STK_SIZE-1], 9);
            break;
		case 7://消息邮箱
			OSTaskCreate(TaskMessageSen, 0, &TaskStk[6][TASK_STK_SIZE-1], 6);
			OSTaskCreate(TaskMessageRec, 0, &TaskStk[7][TASK_STK_SIZE-1], 7);
			break;
		case 8://消息队列
			 OSTaskCreate(TaskQSen, 0, &TaskStk[7][TASK_STK_SIZE-1], 5);
			 OSTaskCreate(TaskQRec, 0, &TaskStk[8][TASK_STK_SIZE-1], 6);
			 OSTaskCreate(TaskQRec, 0, &TaskStk[9][TASK_STK_SIZE-1], 7);
			break;
		case 9://内存管理
			 OSTaskCreate(TaskM, 0, &TaskStk[8][TASK_STK_SIZE-1], 6);
			break;
		default:           
			;
	}
 
	OSStart();	       
	return(0);
}

这段代码是一个简化的实时操作系统的例程选择器,它允许用户选择不同的操作系统任务演示。代码的主要部分包括初始化、用户输入接收以及根据输入启动对应的任务。以下是对关键数据结构、函数和代码片段的详细说明:

数据结构int p[2]: 一个整数数组,用来存放相关数据,但在此代码片段中未见其它用途。TaskStk[][]: 一个二维数组,看起来像是为不同任务预设的栈空间。实时操作系统中每个任务通常有自己的栈。TASK_STK_SIZE: 预定义的常量,指定了任务栈的大小。

函数main函数类型: int

参数:int argc: 命令行参数的数量char **argv: 命令行参数的字符串数组指针。

返回值:程序退出码。0表示正常退出,非 0 值表示有错误发生。

VCInit:用于初始化线程的上下文。

OSInit:初始化操作系统环境的函数。它可能会设置一些关键的数据结构、定时器、任务调度器等。

OSTaskCreate:用来创建一个新的任务。

参数可能包括:任务函数指针:指向任务执行体的函数指针。传递给任务的可选参数。任务的栈顶指针:指指向任务栈顶的指针。任务的优先级。

OSStart:用来启动操作系统调度,使得创建的任务开始按照优先级和调度策略执行。

关键代码片段:

1.接收用户输入

scanf("%d", &Experiment);

if ((Experiment < 0) || (Experiment > 10)) {

    printf("无效的输入!");

    return (1);

}

此段代码要求用户输入一个数字来选择示例任务,并检查输入是否有效。

2.根据输入创建任务

switch(Experiment) {

    case 1: // 一个任务运行

        OSTaskCreate(...);

        break;

    // …后续case语句类似地创建不同的任务集合…

}

根据用户输入(通过Experiment变量),使用switch结构来选择创建和启动不同的任务例程。

(二)、VCInit函数

void VCInit(void)
{
	HANDLE cp,ct;
	Context.ContextFlags = CONTEXT_CONTROL;
	cp = GetCurrentProcess();	//得到当前进程句柄
	ct = GetCurrentThread();	//得到当前线程伪句柄
	DuplicateHandle(cp, ct, cp, &mainhandle, 0, TRUE, 2);	//伪句柄转换,得到线程真句柄
		
}

这段代码提供VCInit函数的实现,下面对该函数进行详细分析:

HANDLE cp, ct;: HANDLE是一个Windows API中用来指代资源(如进程、线程、文件等)的类型。在这里,`cp` 用来存储当前进程的句柄,而 ct 用来存储当前线程的句柄。

Context.ContextFlags = CONTEXT_CONTROL: CONTEXT_CONTROL通常表示我们只对线程的控制寄存器感兴趣,这可能包括程序计数器和栈指针。Context是一个结构体,可能在这个代码片段之外定义,用来存储线程上下文信息。

GetCurrentProcess();: 这个函数调用返回一个当前进程的句柄。

GetCurrentThread();: 返回一个当前线程的伪句柄,所谓的“伪句柄”是一个特殊值,它只对拥有它的进程有效,并且在该进程的上下文中可以代表它引用的对象(这里是指线程)。

DuplicateHandle(cp, ct, cp, &mainhandle, 0, TRUE, 2);: 该函数用来复制对象的句柄,使得句柄有更多的权限或者在新的进程中有效。参数列表意义如下:cp: 源句柄所在的进程,这里是当前进程。ct: 需要被复制的句柄,这里是当前线程的伪句柄。cp: 目标句柄所在的进程,依然是当前进程。&mainhandle: 一个指向 `HANDLE` 类型的指针,复制成功后这里将保存新句柄。0: 新句柄的所需访问权限,这里设为0表示默认。TRUE: 是否继承标志,在新进程中是否可以继承该句柄。2: 选项标志,`DUPLICATE_SAME_ACCESS` 的值通常定义为2,意味着复制的句柄将和原句柄有相同的访问权限。

(三)、TaskStart函数

void TaskStart(void * pParam) 
{	
	char err;	
	OS_EVENT *sem1;
    
	/*模拟设置定时器中断。开启一个定时器线程,每秒中断100次,中断服务程序OSTickISRuser*/
	timeSetEvent(1000/OS_TICKS_PER_SEC, 0, OSTickISRuser, 0, TIME_PERIODIC);
	OSStatInit(); /*统计任务初始化*/
	sem1 = OSSemCreate(0); 
	OSSemPend(sem1, 0, &err);   //等待事件发生,被阻塞;
}

该函数详细说明如下:

void * pParam: 是一个指向任意数据类型的指针,通常用于传递用户定义数据给任务。在此函数实现中并没有使用这个参数。char err: 用于接收错误代码,它在 OSSemPend函数调用时作为错误状态的输出。

OS_EVENT *sem1: 变量 sem1是指向 OS_EVENT` 类型的指针,OS_EVENT是一种用来处理各种事件的结构体,在这里特指一个信号量。

函数内部的重要操作说明如下:timeSetEvent(1000/OS_TICKS_PER_SEC, 0, OSTickISRuser, 0, TIME_PERIODIC): 这行代码调用了一个函数 timeSetEvent,目的是设置一个定时器,以便周期性地调用中断服务例OSTickISRuser。这个定时器每 1000/OS_TICKS_PER_SEC`毫秒,也就是 OS_TICKS_PER_SEC 次/秒触发一次。这里 TIME_PERIODIC表示定时器是周期性的,也就是说,它会按照指定的间隔无限期地重复触发。OS_TICKS_PER_SEC是一个常量,它说明了操作系统的时钟节拍频率,即每秒钟时钟中断的次数。OSTickISRuser可能是用户定义的中断服务例程,它将在每个时钟节拍被调用。OSStatInit(): 这是一个初始化操作系统统计任务的函数调用。它可能会允许系统收集关于任务执行的统计数据,例如CPU占用和任务切换频率等。

sem1 = OSSemCreate(0): 此调用创建一个新的信号量并将其计数器初始化为0,信号量是用于线程或任务同步的一种手段。OSSemPend(sem1, 0, &err): 这行代码将任务置于等待状态,直到信号量 sem1 变得可用,也就是它的计数器超过0。第二个参数表示任务在获取不到信号量时无限期等待。如果等待过程中出现错误,err将存储相应的错误代码。

四、实验结果与分析

五、问题与解决方法、总结。

通过本次实验,我对消息队列的概念有了更直观的理解。实验中我创建了两个任务,TaskQSen负责定时发送消息至队列,TaskQRec则从队列中接收消息。这个过程深刻展示了消息队列在任务间通信中的异步性和缓冲能力。我体会到了,在嵌入式实时系统中,消息队列是一种重要的数据交换手段,它可以有效地解耦任务的生产者和消费者。对消息队列进行编程操作的过程中,我学习到了如何使用 OSQCreate和 OSQPend等API,不仅理解了函数背后的逻辑,也对应用编程接口的使用有了更加深入的了解。

  • 19
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
#include #include #include "vxWorks.h" #include "msgQLib.h" #include "taskLib.h" /*#include "memPartLib.h"*/ #include "memLib.h" /*宏定义*/ #define MAX_MSGS (10) /* the length of msg*/ #define MAX_MSG_LEN sizeof(MESSAGE) /*the length of message*/ #define STACK_SIZE 20000 /*the stack size of task*/ #define DELAY_TICKS 50 /*the time of sending message*/ #define MAX_point 5 /*用户从系统内存池中获得内存的最大次数*/ #define size_1 30 /*用户分区的分配的大小*/ #define size_2 40 /*全局变量*/ int tidtask1; int tidtask2; int tidtask3; SEM_ID syncSemId; SEM_ID waitSemId; MSG_Q_ID myMsgQId1; MSG_Q_ID myMsgQId2; MSG_Q_ID myMsgQId3; typedef struct _MESSAGE { int mSendId; /*发送任务 ID*/ int mRecvId; /*接收任务 ID*/ int mData; /*消息中传递的数据*/ char Data[14]; } MESSAGE; /*内存管理*/ char* usermem1; char* usermem2; MESSAGE *point1[MAX_point]; MESSAGE *point2[MAX_point]; MESSAGE *point3[MAX_point]; int point1_index=0; int point2_index=0; int point3_index=0; PART_ID partid1; PART_ID partid2; #define MID_MESSAGE(id) (id) /*函数声明*/ int start(void); int task1(void); int task2(void); int task3(void); template T* mymalloc(unsigned nBytes); void myfree(void); void bye(void); /***************************************[progStart]*******************************************/ /*启动程序,创建息队例,任务*/ int start(void) { tidtask1=taskSpawn("tTask1", 220, 0, STACK_SIZE, (FUNCPTR)task1,0,0,0,0,0,0,0,0,0,0); usermem1=malloc(200); partid1=memPartCreate(usermem1,200); usermem2=malloc(400); partid2=memPartCreate(usermem2,400); return; } /**************************************[test_end]********************************************/ /*是否相等,相等返回1*/ int test_end(char *end,char *target) { int ret; if(!strcmp(end,target)) ret=1; else ret=0; return ret; } /****************************************[task1]***********************************************/ /*管理Task。负责系统启动时同步系统中其他Task的启动同步,利用信号量的semFlush()完成。同时接收各*/ /*Task的告警信息,告警信息需编号以logmsg方式输出。本task负责系统结束时的Task删除处理*/ int task1(void) { int singal; int message; MESSAGE *rxMsg=mymalloc(26); /*define messages,and alloc memory*/ memset(rxMsg,0,26); syncSemId=semBCreate(SEM_Q_FIFO,SEM_EMPTY); /*creat semaphore*/ waitSemId=semBCreate(SEM_Q_PRIORITY,SEM_EMPTY); myMsgQId1=msgQCreate(MAX_MSGS,MAX_MSG_LEN,MSG_Q_PRIORITY); /*create msgQ*/ myMsgQId2=msgQCreate(MAX_MSGS,MAX_MSG_LEN,MSG_Q_PRIORITY); myMsgQId3=msgQCreate(MAX_MSGS,MAX_MSG_LEN,MSG_Q_PRIORITY); tidtask2=taskSpawn("tTask2", 200, 0, STACK_SIZE, (FUNCPTR)task2,0,0,0,0,0,0,0,0,0,0); /*create task*/ tidtask3=taskSpawn("tTask3", 210, 0, STACK_SIZE, (FUNCPTR)task3,0,0,0,0,0,0,0,0,0,0); printf("Please input one of the following commands:add,sub,multiply,divide,testcommand\n"); /*the command we should put into the console*/ semFlush(syncSemId); /*release semaphore*/ semGive(waitSemId); while(1) { singal=1; msgQReceive(myMsgQId1,(char*)&rxMsg,sizeof(rxMsg),WAIT_FOREVER); if(rxMsg->mRecvId==MID_MESSAGE(3)) /*receive MsgQ from task3*/ { singal=test_end(rxMsg->Data,"wrong length")-1; logMsg("task3 receiveing a:%s\n",rxMsg->Data); /*put the warn from task3*/ logMsg("Please reput the other command!\n"); msgQReceive(myMsgQId1,(char*)&rxMsg,MAX_MSG_LEN,WAIT_FOREVER); /*recive MsgQ from task3*/ } if(rxMsg->mRecvId==MID_MESSAGE(2)) /*receive MsgQ from task2*/ { message=test_end(rxMsg->Data,"sysend"); if(message) { /*if the message from task2 is "sysend" and did not receive the warn from task3, close the system*/ if(singal) { bye(); } } else {/*if the message from task2 is "sysend" and receive the warn from task3, reput the command*/ if(singal) logMsg("task2 receiveing a %s\n",rxMsg->Data); logMsg("please reput the correct command!\n"); } } } return; } /********************************************************************************************/ int change_buf(char *command) { int ret; if(!strcmp(command,"add")) ret=1; else if(!strcmp(command,"sub")) ret=2; else if(!strcmp(command,"multiply")) ret=3; else if(!strcmp(command,"divide")) ret=4; else if(!strcmp(command,"testcommand")) ret=5; else ret=0; return ret; } /****************************************[task2]*********************************************/ /*console 命令行接收Task。接收并分析console发来的命令行及参数。自行设置5种以上命令,并根据命*/ /*令的内容向Task3发送激励消息。同时实现系统退出命令,使系统采用适当方式安全退出。收到非法命令*/ /*向Task1告警*/ int task2(void) { char buf[100]; int command; char *str=mymalloc(35); MESSAGE *txMsg=mymalloc(26); memset(str,0,35); memset(txMsg,0,26); txMsg->mSendId=MID_MESSAGE(2); txMsg->mRecvId=MID_MESSAGE(2); FOREVER { semTake(syncSemId,WAIT_FOREVER); semTake(waitSemId,WAIT_FOREVER); gets(buf); command=change_buf(buf);/*change the commands into numbers*/ switch(command) { case 0:/*receive uncorrect command*/ txMsg->mData=0; strcpy(txMsg->Data,"wrong command");/*send warn to task1*/ msgQSend(myMsgQId1,(char*)&txMsg,sizeof(txMsg),WAIT_FOREVER,MSG_PRI_NORMAL); break; case 1:/*receive add command*/ strcpy(str,"This an add caculate!\0"); txMsg->mData=1; break; case 2:/*receive sub command*/ strcpy(str,"This a sub caculate!\0"); txMsg->mData=2; break; case 3:/*receive multiply command*/ strcpy(str,"This a multiply caculate!\0"); txMsg->mData=3; break; case 4:/*receive divide command*/ strcpy(str,"This a divide caculate!\0"); txMsg->mData=4; break; case 5:/*receive testcommand,send a long string to task3*/ strcpy(str,"This a testcommand to warn task1!\0"); txMsg->mData=5; break; default: break; } if(txMsg->mData!=0) {/*send along string to task3,and send a message to taks3*/ msgQSend(myMsgQId3,(char*)&str,sizeof(str),WAIT_FOREVER,MSG_PRI_NORMAL); msgQSend(myMsgQId3,(char*)&txMsg,sizeof(txMsg),WAIT_FOREVER,MSG_PRI_NORMAL); } semGive(waitSemId); semGive(syncSemId); taskDelay(DELAY_TICKS); if(txMsg->mData!=0) {/*send sysend to task1 to let task1 close system*/ strcpy(txMsg->Data,"sysend"); msgQSend(myMsgQId1,(char*)&txMsg,sizeof(txMsg),WAIT_FOREVER,MSG_PRI_NORMAL); } } return; } /****************************************[task3]********************************************/ /*console输出Task。接收需打印输出的字串消息(命令),输出到console。收到长度为0或超常字串向*/ /*Task1告警*/ int task3(void) { int firstData=100; int secondData=10; MESSAGE *rxMsg=mymalloc(26); MESSAGE *txMsg=mymalloc(26); char *rstr=mymalloc(35); memset(txMsg,0,26); memset(txMsg,0,26); memset(rstr,0,35); txMsg->mSendId=MID_MESSAGE(3); txMsg->mRecvId=MID_MESSAGE(3); while(1) { semTake(syncSemId,WAIT_FOREVER); msgQReceive(myMsgQId3,(char*)&rstr,sizeof(rstr),WAIT_FOREVER); if(strlen(rstr)=26) {/*make sure whether the string is too long or short*/ strcpy(txMsg->Data,"wrong length"); msgQSend(myMsgQId1,(char*)&txMsg,sizeof(txMsg),WAIT_FOREVER,MSG_PRI_NORMAL); /*msgQReceive(myMsgQId3,(char*)&rxMsg,sizeof(rxMsg),WAIT_FOREVER);*/ } semTake(waitSemId,WAIT_FOREVER); msgQReceive(myMsgQId3,(char*)&rxMsg,sizeof(rxMsg),WAIT_FOREVER); if(rxMsg->mData!=5) {/*when it is not testcommand,printf these*/ printf("%s\n",rstr); printf("there are two datas!\n"); printf("firstData:100\n"); printf("secondData:10\n"); } switch(rxMsg->mData) { case 1:/*printf add caculate*/ printf("The result is:%d\n",firstData+secondData); break; case 2:/*printf sub caculate*/ printf("The result is:%d\n",firstData-secondData); break; case 3:/*printf multiply caculate*/ printf("The result is:%d\n",firstData*secondData); break; case 4:/*printf divide caculate*/ printf("The result is:%d\n",firstData/secondData); break; case 5: break; default: break; } semGive(waitSemId); semGive(syncSemId); taskDelay(DELAY_TICKS); } return; } template T* mymalloc(unsigned nBytes) { T* point; int i=0; /*用户分区一是否能分配的标志位*/ int j=0; /*用户分区二是否能分配的标志位*/ if(nBytes=size_1 && nBytes=size_2) && point3_index<MAX_point) /*若用户分区二不能分配,由系统内存池来分配,且只能从系统内存池中分配MAX_point次*/ { point=malloc(nBytes); point3[point3_index]=point; printf("the number of the point3_index is:%d\n",point3_index); point3_index++; } return point; } void myfree(void) { int i=0; for (i=0;i<point1_index;i++) { memPartFree(partid1,point1[i]); } for (i=0;i<point2_index;i++) { memPartFree(partid2,point2[i]); } for (i=0;i<point3_index;i++) { free(point3[i]); } free(usermem1); free(usermem2); printf("The memory have freed!\n"); } void bye(void) { myfree(); logMsg("Bye-bye\n"); taskDelete(tidtask2); taskDelete(tidtask3); msgQDelete(myMsgQId1); msgQDelete(myMsgQId2); msgQDelete(myMsgQId3); semDelete(syncSemId); taskDelete(tidtask1); }
在Python中调用具有上述C函数类型签名的函数时,可以使用`ctypes`模块来实现。以下是一个示例代码,展示了如何在Python中调用这样的函数: ```python import ctypes # 定义函数类型 FrameProcCbFunc = ctypes.CFUNCTYPE( ctypes.c_bool, # 返回值类型为bool ctypes.POINTER(ctypes.c_ushort), # pFrameBuf的类型为unsigned short * ctypes.c_uint16, # iFrameID的类型为uint16_t ctypes.c_int32, # iFrameWidth的类型为int32_t ctypes.c_int32, # iFrameHeight的类型为int32_t ctypes.c_void_p # pParam的类型为void * ) # 定义回调函数 def frame_proc_callback(pFrameBuf, iFrameID, iFrameWidth, iFrameHeight, pParam): print("Get a frame in short bytes") print(iFrameID) print(iFrameWidth) print('false') return False # 将Python函数转换为C函数指针 callback = FrameProcCbFunc(frame_proc_callback) # 调用C函数 # 假设有一个名为lib的C库,其中包含了一个名为process_frame的函数需要传入回调函数 lib = ctypes.CDLL("lib.so") # 替换为实际的库文件名或路径 lib.process_frame(callback) ``` 在上述代码中,我们首先使用`ctypes.CFUNCTYPE`定义了一个与C函数类型签名相匹配的函数类型`FrameProcCbFunc`。然后,我们定义了一个名为`frame_proc_callback`的Python回调函数,该函数与C函数类型签名一致。接下来,我们使用`FrameProcCbFunc`将Python回调函数转换为C函数指针`callback`。最后,我们可以将C函数和回调函数结合起来使用,例如调用C库中的函数`process_frame`并传入回调函数作为参数。 请注意,上述示例中的具体类型可能需要根据实际情况进行调整,例如根据C代码中的类型定义来选择正确的`ctypes`类型。另外,还需要替换示例中的库文件名或路径为实际的库文件名或路径。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值