1、枚举第一步:获取设备的描述符
1)先要允许数据传输完成中断;
从USB_init()开始
PowerOn(); //这句话执行完,主机检测到设备,并且能够响应复位中断了
_SetISTR(0); //清除挂起的中断
wInterupt_Mask = IMR_MSK;
_SetCNTR(wInterrupt_Mask); //设置中断屏蔽
//上面两句将允许所有USB中断
bDeviceState = UNCONNECTED; //设备状态值,当前为未连接状态
2)主机获取描述符;
主机进入控制传输的第一阶段:建立事务、发送setup令牌包、发送请求数据包、设备发送ACK包。
主机对地址0、端点0发出SETUP令牌包,首先端点0寄存器的第11位SETUP位置位,表明收到了setup令牌包。
由于此时端点0数据接收有效,所以接下来主机的请求数据包被SIE保存到端点0描述符表的
RxADDR里面,收到的字节数保存到RxCount里面。
端点0寄存器的CTR_RX被置位为1,ISTR的CTR置位为1,DIR=1,EP_ID=0,表示端点0
接收到主机来的请求数据。此时设备已经ACK主机,将触发正确传输完成中断,下面进入中断看一看。
//* Function Name : CTR_LP.
//* Description : Low priority Endpoint Correct Transfer interrupt's service routine.
void CTR_LP(void)
{
__IO uint16_t wEPVal = 0;
while (((wIstr = _GetISTR()) & ISTR_CTR) != 0) /* stay in loop while pending interrupts */
{
EPindex = (uint8_t)(wIstr & ISTR_EP_ID); /* extract low priority endpoint number :获取指针传输针对的端点号*/
if (EPindex == 0) /*如果是端点0,这里确实是端点0*/
{
/* Decode and service control endpoint interrupt calling related service routine
- (Setup0_Process, In0_Process, Out0_Process) */
SaveRState = _GetENDPOINT(ENDP0); /* save RX & TX status and set both to NAK:保存端点0状态,原本是有效状态 */
SaveTState = SaveRState & EPTX_STAT;
SaveRState &= EPRX_STAT;
_SetEPRxTxStatus(ENDP0,EP_RX_NAK,EP_TX_NAK); /*在本次数据处理好之前,对主机发来的数据包以NAK回应*/
if ((wIstr & ISTR_DIR) == 0) /* DIR bit = origin of the interrupt:如果是IN令牌包,数据被取走*/
{
/* DIR = 0 */
/* DIR = 0 => IN int :DIR = 0时,是输入中断*/
/* DIR = 0 implies that (EP_CTR_TX = 1) always :DIR为0总是意味着EP_CTR_TX为1*/
_ClearEP_CTR_TX(ENDP0); /*清除传输完成标志*/
In0_Process();
_SetEPRxTxStatus(ENDP0,SaveRState,SaveTState); /* before terminate set Tx & Rx status */
return;
}
else
/*DIR = 1时,要么是SETUP包,要么是OUT包*/
{
/* DIR = 1 */
/* DIR = 1 & CTR_RX => SETUP or OUT int:DIR = 1时,是输出中断 */
/* DIR = 1 & (CTR_TX | CTR_RX) => 2 int pending */
wEPVal = _GetENDPOINT(ENDP0); //获取端点0的状态
if ((wEPVal & EP_SETUP) != 0) //我们的程序会执行到这里
{
_ClearEP_CTR_RX(ENDP0); /* SETUP bit kept frozen while CTR_RX = 1 :*/
Setup0_Process(); //主要是调用该程序来处理主机请求
_SetEPRxTxStatus(ENDP0,SaveRState,SaveTState);/* before terminate set Tx & Rx status */
return;
}
else if ((wEPVal & EP_CTR_RX) != 0) //暂时不执行的代码先删除掉
{
_ClearEP_CTR_RX(ENDP0); /* clear int flag:清除接收端点0标志 */
Out0_Process();
_SetEPRxTxStatus(ENDP0,SaveRState,SaveTState);/* before terminate set Tx & Rx status */
return;
}
}
}
................................//其他端点处理代码
}
}
//* Function Name : CTR_HP.
//* Description : High Priority Endpoint Correct Transfer interrupt's service
routine.void CTR_HP(void)
{
uint32_t wEPVal = 0;
while (((wIstr = _GetISTR()) & ISTR_CTR) != 0)
{
_SetISTR((uint16_t)CLR_CTR); /* clear CTR flag:首先清除传输完成标志 */
EPindex = (uint8_t)(wIstr & ISTR_EP_ID);
/* extract highest priority endpoint number */ wEPVal = _GetENDPOINT(EPindex); /* process related endpoint register: */
if ((wEPVal & EP_CTR_RX) != 0)
{
_ClearEP_CTR_RX(EPindex);
/* clear int flag */ (*pEpInt_OUT[EPindex-1])();
/* call OUT service function */- }
else if ((wEPVal & EP_CTR_TX) != 0) /* if((wEPVal & EP_CTR_RX) */
{
_ClearEP_CTR_TX(EPindex); /* clear int flag */
(*pEpInt_IN[EPindex-1])();
/* call IN service function */- }
- }
}
3)Setup0_Process()函数的执行分析;
//* Function Name : Setup0_Process
//* Description : Get the device request data and dispatch to individual process.
//* Return : Post0_Process.
uint8_t Setup0_Process(void)
{
- union
{
uint8_t* b;
uint16_t* w;
} pBuf;
uint16_t offset = 1;
pBuf.b = PMAAddr + (uint8_t *)(_GetEPRxAddr(ENDP0) * 2); /* *2 for 32 bits addr :这是取得端点0接收缓冲区的起始地址*/ //PMAAddr是包缓冲区起始地址,_GetEPRxAddr(ENDP0)获得端点0描述符表里的接收缓冲区地址。
//乘以2的原因是,描述符里地址项为16位,使用的是相对偏移。
if (pInformation->ControlState != PAUSE)
{
pInformation->USBbmRequestType = *pBuf.b++; /* bmRequestType:请求类型,表示方向和接收对象(设备、接口还是端点)此时为80,表明设备到主机。*/
pInformation->USBbRequest = *pBuf.b++; /* bRequest:请求代码,第一次时应该为6,表明主机要获取设备描述符。 */
pBuf.w += offset; /* word not accessed because of 32 bits addressing */
pInformation->USBwValue = ByteSwap(*pBuf.w++); /* wValue */
pBuf.w += offset; /* word not accessed because of 32 bits addressing */
pInformation->USBwIndex = ByteSwap(*pBuf.w++); /* wIndex */
pBuf.w += offset; /* word not accessed because of 32 bits addressing */
pInformation->USBwLength = *pBuf.w; /* wLength */
}
pInformation->ControlState = SETTING_UP;
if (pInformation->USBwLength == 0)
{
NoData_Setup0(); /* Setup with no data stage */
}
else
{
Data_Setup0(); /* Setup with data stage:这次是数据传输的,所以有进入该函数 */
}
return Post0_Process();
}
这个函数执行的时候,主机发来的请求数据包已经存在RxADDR缓冲区了。大部分的标志位已经清除,除了SETUP位,这个位将由下一个令牌包自动清除。4)Data_Setup0()函数的执行分析;
//* Function Name : Data_Setup0.
//* Description : Proceed the processing of setup request with data stage.
void Data_Setup0(void)
{
uint8_t *(*CopyRoutine)(uint16_t);
RESULT Result;
uint32_t Request_No = pInformation->USBbRequest;
uint32_t Related_Endpoint, Reserved;
uint32_t wOffset, Status;
CopyRoutine = NULL; //这是一个函数指针,由用户提供
wOffset = 0;
if (Request_No == GET_DESCRIPTOR) //如果是获取描述符
{
if (Type_Recipient == (STANDARD_REQUEST | DEVICE_RECIPIENT))
{
uint8_t wValue1 = pInformation->USBwValue1;
if (wValue1 == DEVICE_DESCRIPTOR) //如果是获取设备描述符
{
CopyRoutine = pProperty->GetDeviceDescriptor; //获取设备描述符的操作由用户提供
}
else if (wValue1 == CONFIG_DESCRIPTOR) //如果是获取配置描述符
{
CopyRoutine = pProperty->GetConfigDescriptor;
}
else if (wValue1 == STRING_DESCRIPTOR) //如果是获取字符串描述符
{
CopyRoutine = pProperty->GetStringDescriptor;
} /* End of GET_DESCRIPTOR */
}
}
/*GET STATUS*/
else if ((Request_No == GET_STATUS) && (pInformation->USBwValue == 0)
&& (pInformation->USBwLength == 0x0002)
&& (pInformation->USBwIndex1 == 0))
{
....
}
/*GET CONFIGURATION*/
else if (Request_No == GET_CONFIGURATION)
{
....
}
/*GET INTERFACE*/
else if (Request_No == GET_INTERFACE)
{
....
}
if (CopyRoutine)
{
pInformation->Ctrl_Info.Usb_wOffset = wOffset;
pInformation->Ctrl_Info.CopyData = CopyRoutine;
/* sb in the original the cast to word was directly */
/* now the cast is made step by step */
(*CopyRoutine)(0); //这个函数在这里调用的目的是设置pInformation中需要写入的描述符的长度
Result = USB_SUCCESS;
}
else
{
....
}
if (pInformation->Ctrl_Info.Usb_wLength == 0xFFFF)
{
....
}
if ((Result == USB_UNSUPPORT) || (pInformation->Ctrl_Info.Usb_wLength == 0))
{
....
}
if (ValBit(pInformation->USBbmRequestType, 7)) /* Device ==> Host :此时为80,说明方向是IN*/
{
__IO uint32_t wLength = pInformation->USBwLength; //这个一般是64
if (pInformation->Ctrl_Info.Usb_wLength > wLength) /* Restrict the data length to be the one host asks */
{
pInformation->Ctrl_Info.Usb_wLength = wLength; //设备描述符长度18
}
else if (pInformation->Ctrl_Info.Usb_wLength < pInformation->USBwLength)
{
......
}
pInformation->Ctrl_Info.PacketSize = pProperty->MaxPacketSize;
DataStageIn(); //调用这个函数实现描述符的输出准备
}
else
{
....
}
return;
}
5)DataStageIn()函数的执行分析;
- //* Function Name : DataStageIn.
//* Description : Data stage of a Control Read Transfer.
void DataStageIn(void)
{
ENDPOINT_INFO *pEPinfo = &pInformation->Ctrl_Info;
uint32_t save_wLength = pEPinfo->Usb_wLength;
uint32_t ControlState = pInformation->ControlState;
uint8_t *DataBuffer;
uint32_t Length;
if ((save_wLength == 0) && (ControlState == LAST_IN_DATA))
{
if(Data_Mul_MaxPacketSize == TRUE)
{
/* No more data to send and empty packet */
Send0LengthData();
ControlState = LAST_IN_DATA;
Data_Mul_MaxPacketSize = FALSE;
}
else
{
/* No more data to send so STALL the TX Status*/
ControlState = WAIT_STATUS_OUT;
vSetEPTxStatus(EP_TX_STALL);
}
goto Expect_Status_Out;
}
Length = pEPinfo->PacketSize;
ControlState = (save_wLength <= Length) ? LAST_IN_DATA : IN_DATA;
if (Length > save_wLength)
{
Length = save_wLength;
}
//以下是主要执行代码:
DataBuffer = (*pEPinfo->CopyData)(Length); //取得用户描述符缓冲区的地址,这里共18个字节
- UserToPMABufferCopy(DataBuffer, GetEPTxAddr(ENDP0), Length); //将设备描述符复制到用户的发送缓冲区
- SetEPTxCount(ENDP0, Length); //设置发送字节的数目:18
pEPinfo->Usb_wLength -= Length; //等于0
pEPinfo->Usb_wOffset += Length; //偏移到18
vSetEPTxStatus(EP_TX_VALID); //使能端点发送,只要主机的IN令牌包一来,SIE就会将描述符返回给主机
USB_StatusOut();/* Expect the host to abort the data IN stage :使能接收也有效,主机可以取消IN*/
Expect_Status_Out:
pInformation->ControlState = ControlState;
}
6)执行流程返回到CTR_LP(void);
_SetEPRxStatus(ENDP0, SaveRState);
_SetEPTxStatus(ENDP0, SaveTState); //因为vSetEPTxStatus(EP_TX_VALID)实际改变了SaveTState,所以此时端点发送已经使能了。
return;
7)主机的IN令牌包;
获取描述符的控制传输进入第二阶段,主机首先发送一个IN令牌包,由于端点0发送有效,SIE将数据返回主机。
主机方返回一个ACK后,主机发送数据的CTR标志置位,DIR = 0,EP_ID = 0,表明主机正确收到了用户发过去的描述符。固件程序由此进入中断。
此时是由IN引起的。
主要是调用In0_Process()完成剩下的工作。
8)追踪进入函数In0_Process();
此时实际上设备返回描述符已经成功了。
这一次还是调用DataStageIn()函数,但是目的只是期待主机的0状态字节输出了。
//* Function Name : In0_Process
//* Description : Process the IN token on all default endpoint.
//* Return : Post0_Process.
uint8_t In0_Process(void)
{
uint32_t ControlState = pInformation->ControlState;
if ((ControlState == IN_DATA) || (ControlState == LAST_IN_DATA))
{
DataStageIn(); //第一次取设备描述符,只取一次。此次调用后,当前
//状态变为WAIT_STATUS_OUT,表明设备等待状态过程,主机输出0字节
ControlState = pInformation->ControlState; /* ControlState may be changed outside the function */
}
- else if (ControlState == WAIT_STATUS_IN)
{
- ....
}
- else
{
- ....
- }
- pInformation->ControlState = ControlState;
- return Post0_Process(); //返回时调用这个函数,没做什么事
}
9)进入状态过程;
主机收到18字节描述符后,进入状态事务过程,此时过程的令牌包为OUT,字节数为0,只需要用户返回一个ACK。
所以中断处理程序会进入Out0_Process()。
//* Function Name : Out0_Process
//* Description : Process the OUT token on all default endpoint.
//* Return : Post0_Process.
uint8_t Out0_Process(void)
{
uint32_t ControlState = pInformation->ControlState;
if ((ControlState == OUT_DATA) || (ControlState == LAST_OUT_DATA))
{
DataStageOut();
ControlState = pInformation->ControlState; /* may be changed outside the function */
}
//由于此时状态为WAIT_STATUS_OUT,所以执行下面代码
- else if (ControlState == WAIT_STATUS_OUT)
{
(*pProperty->Process_Status_OUT)(); //这个是空函数,什么都不做
ControlState = STALLED; //状态转为STALLED
}
- else if ((ControlState == IN_DATA) || (ControlState == LAST_IN_DATA))
{
/* host aborts the transfer before finish */
ControlState = STALLED;
}
- /* Unexpect state, STALL the endpoint */
else
{
ControlState = STALLED;
}
pInformation->ControlState = ControlState;
return Post0_Process();
}
获取设备描述符后,主机再次复位设备,设备又进入初始状态。五、USB的“JoyStickMouse”工作过程详细分析(3)
2、枚举第二步:设置地址
1)重新从复位状态开始;
在第一次获取设备描述符后,程序使端点0的发送和接收都无效,状态也设置为STALLED,所以主机
先发送一个复位,使得端点0接收有效。虽然在NAK和STALL状态下,端点任然可以响应和接收SETUP包。
2)设置地址的建立阶段;
主机先发一个SETUP令牌包,设备端EP0的SETUP标志置位。然后主机发了一个OUT包,共8字节,里面包含设置地址的要求。
设备在检验数据后,发一个ACK握手包。同时CTR_RX置位,CTR置位。数据已经保存到RxADDR指向的缓冲区。此时USB产生数据接收中断。
由于CTR_RX和SETUP同时置位,终端处理程序调用Setup0_Process(),所做的工作任然是先填充pInformation结构、获取请求特征码、请求代码和数据长度。
由于设置地址不会携带数据,所以接下来调用NoData_Setup0()。执行以下代码:
//* Function Name : NoData_Setup0.
//* Description : Proceed the processing of setup request without data stage.
void NoData_Setup0(void)
{
RESULT Result = USB_UNSUPPORT;
uint32_t RequestNo = pInformation->USBbRequest;
uint32_t ControlState;
if (Type_Recipient == (STANDARD_REQUEST | DEVICE_RECIPIENT))
{
/* Device Request*/
if (RequestNo == SET_CONFIGURATION) /* SET_CONFIGURATION*/
{
Result = Standard_SetConfiguration();
}
else if (RequestNo == SET_ADDRESS) /*SET ADDRESS*/
{
if ((pInformation->USBwValue0 > 127) || (pInformation->USBwValue1 != 0)
|| (pInformation->USBwIndex != 0)
|| (pInformation->Current_Configuration != 0))
/* Device Address should be 127 or less*/
{
ControlState = STALLED;
goto exit_NoData_Setup0;
}
//由于设置地址不会携带数据,执行以下代码
else
{
Result = USB_SUCCESS;
}
}
else if (RequestNo == SET_FEATURE) /*SET FEATURE for Device*/
{
- ......
}
else if (RequestNo == CLEAR_FEATURE) /*Clear FEATURE for Device */
{
- ......
}
- }
else if (Type_Recipient == (STANDARD_REQUEST | INTERFACE_RECIPIENT)) /* Interface Request*/
{
....
}
else if (Type_Recipient == (STANDARD_REQUEST | ENDPOINT_RECIPIENT)) /* EndPoint Request*/
{
- ....
}
else
{
Result = USB_UNSUPPORT;
}
- if (Result != USB_SUCCESS)
{
Result = (*pProperty->Class_NoData_Setup)(RequestNo);
if (Result == USB_NOT_READY)
{
ControlState = PAUSE;
goto exit_NoData_Setup0;
}
}
if (Result != USB_SUCCESS)
{
ControlState = STALLED;
goto exit_NoData_Setup0;
}
ControlState = WAIT_STATUS_IN;/* After no data stage SETUP:说明设置地址没有做任何工作 */
USB_StatusIn();
//上面这句话是关键,它是一个宏,实际是准备好发送0字节的状态数据包,因为地址设置没有数据过程,
//建立阶段后直接进入状态阶段,主机发IN令牌包,设备返回0字节数据包,主机再ACK。
- //它对应的宏是这样的:
- //#define USB_StatusIn() Send0LengthData() //准备发送0字节数据
- //#define Send0LengthData() { _SetEPTxCount(ENDP0, 0); vSetEPTxStatus(EP_TX_VALID);//设置发送地址,发送字节数为0}
exit_NoData_Setup0:
pInformation->ControlState = ControlState;
return;
}
3)设置地址的状态阶段;
前面把 状态设置为WAIT_STATUS_IN是给IN令牌包的处理提供提示。因为建立阶段结束后,主机接着发一个IN令牌包,设备返回0字节数据后,进入中断。
本次中断由IN0_Process()函数来处理,追踪进入,它执行以下代码:
else if(ControlState == WAIT_STATUS_IN)
{
if ((pInformation->USBbRequest == SET_ADDRESS) && (Type_Recipient==(STANDARD_REQUEST|DEVICE_RECIPIENT)))
{
SetDeviceAddress(pInformation->USBwValue0);
pUser_Standard_Requests->User_SetDeviceAddress(); //这个函数就一个赋值语句, bDeviceState = ADDRESSED。
}
(*pProperty->Process_Status_IN)(); //这是一个空函数。
ControlState = STALLED;
}
执行设置地址操作、采用新地址后,把设备的状态改为 STALLED。而在处理的出口中调用Post0_Process()函数,这个所做的工作是:
//* Function Name : Post0_Process
//* Description : Stall the Endpoint 0 in case of error.
//* Return : - 0 if the control State is in PAUSE;
- 1 if not.uint8_t Post0_Process(void)
{
SetEPRxCount(ENDP0, Device_Property.MaxPacketSize);
if (pInformation->ControlState == STALLED) //将端点0的缓冲区大小设置为64字节
{
vSetEPRxStatus(EP_RX_STALL);
vSetEPTxStatus(EP_TX_STALL); //将端点0的发送和接收都设置为:STALL,这样只接收SETUP令牌包。
}
return (pInformation->ControlState == PAUSE);
}
3、枚举第三步:从新地址获取设备描述符
1)上一阶段末尾的状态
端点0的发送和接收都设置为:STALL、只接收SETUP令牌包。
2)建立阶段:主机发送令牌包、数据包、设备ACK;
产生数据接收中断,且端点0的SETUP置位,调用Setup0_Process()函数进行处理。
在Setup0_Process()中,因为主机发送了请求数据8个字节,由调用Data_Setup()函数进行处理。首先是获取
设备描述符的长度、描述符起始地址、传送的最大字节数,根据这些参数确定本次能够传输的字节数,然后调用
DataSetageIn()函数进行实际的数据传输操作,设备描述符必须在本次中断中就写入发送缓冲区,因为很快就要
进入数据阶段了。
在函数处理的最后:
vSetEPTxStatus(EP_TX_VALID);
USB_StatusOut(); //本来期待IN令牌包,但用户可以取消数据阶段,一般不会用到
3)数据阶段:主机发IN包、设备返回数据,主机ACK;
本次操作会产生数据发送完成中断,由In0_Process(void)来处理中断,它也调用DataStageIn()函数进行处理。
如果数据已经发送完:
ControlState = WAIT_STATUS_OUT;
vSetEPTxStatus(EP_TX_STALL);
//转入状态阶段
有可能的话:
Send0LengthData();
ControlState = LAST_IN_DATA;
Data_Mul_MaxPacketSize = FALSE; //这一次发送0字节,状态转为最后的输入阶段。
否则,继续准备数据,调整剩余字节数、发送指针位置,等待主机的下一个IN令牌包。
4)状态阶段:主机发OUT包、0字节包,设备ACK;
数据发送完中断,调用Out0_Precess(void)函数进行处理,由于在数据阶段的末尾已经设置设备状态为:
WAIT_STATUS_OUT,所以处理函数基本上没做什么,就退出了,并将状态改为STALLED。
4、对配置描述符、字符串描述符获取
过程进行简单跟踪,过程就不一一叙述了
5、主机设置配置
1)建立阶段:主机发SETUP包、发请求数据包(DATA0包)、用户ACK;
进入CTR中断,用户调用Setup0_Precess()函数进行处理,取得请求数据后,由于没有数据传输阶段,
该函数调用NoData_Setup()函数进行处理。
判断为设置配置后,调用Standard_SetInterface()函数将设备状态结构体的当前配置改为主机数据中的
配置参数。同时调用用户的设置配置函数,将设备状态改为“ configured”。
退出时,将控制传输状态改为:ControlState = WAIT_STATUS_IN,进入状态阶段。设备期待主机的IN
令牌包,返回状态数据。
2)状态阶段:主机发IN令牌,设备返回0。
Setup0_Process()函数进行处理,取得请求数据后,由于没有数据传输阶段,该函数调用NoData_Setup()函数进行处理。
设置空闲时一个类特殊请求,其特征码为0x21,2表示类请求而不是标准请求,1表示接收对象是接口而不是设备。
USB的底层并不支持类特殊请求,它将调用上层函数提供的函数:
if(Result != USB_SUCCESS)
{
Result = (*pProperty->Class_NoData_Setup)(RequestNo);
//这里就是调用用户提供的类特殊请求的处理函数。结果发现用户提供的类特殊请求(针对无数据情况)只支持:
//SET_PROTOCOL。针对有数据情况只支持:GET_PROTOCOL。
if((Type_Recipient == (CLASS_REQUEST | INTERFACE_RECIPIENT)) && (RequestNo == SET_PROTOCOL))
{
return JoyStick_SetProtocol();
- }
- }
6、主机获取报告描述符
建立阶段:主机发送SETUP令牌包、请求数据包(DATA0包)、用户发送ACK。
if (Request_No == GET_DESCRIPTOR)
{
if (Type_Recipient == (STANDARD_REQUEST | DEVICE_RECIPIENT))
{
uint8_t wValue1 = pInformation->USBwValue1;
if (wValue1 == DEVICE_DESCRIPTOR)
{
CopyRoutine = pProperty->GetDeviceDescriptor;
}
else if (wValue1 == CONFIG_DESCRIPTOR)
{
CopyRoutine = pProperty->GetConfigDescriptor;
}
else if (wValue1 == STRING_DESCRIPTOR)
{
CopyRoutine = pProperty->GetStringDescriptor;
} /* End of GET_DESCRIPTOR */
}
}
进入CTR中断,获取描述符时一个标准请求,但是报告描述符并不是需要通用实现的,所以在底层函数中没有实现。
跟踪Setup0_Process(void)——进入Data_Setup(void)函数,它按照上面程序处理。
可见核心程序只支持设备描述符、配置描述符、字符串描述符。
最终该函数调用:
Result = (*pProperty->Class_Data_Setup)(pInformation->USBbRequest);
调用用户的类特殊实现来获取报告描述符,同时HID类描述符也是通过这种方式获取的。
7、主机从中断端点读取鼠标操作数据