本帖最后由 yang_alex 于 2018-5-4 19:57 编辑
USB的数据通讯都是由主机发起的,主机发给从机一个命令(或叫请求),告诉从机,你要发给我什么,你要干什么。所有的USB设备都要求对主机发给自己的控制命令作出响应。在USB规范中,这个命令(或叫请求)是有格式要求的。他的格式如下:
USB主机发给从机的命令(或叫请求)分为标准命令(请求)和HID设备类特定的命令(请求)。
USB主机发给从机的标准请求如下:
USB主机发给从机的HID设备类特定的请求如下:
在USB的数据传输中,相关信息是分层体现的,具体来说就是:一个【传输】由多个【事务】组成;一个【事务】又由一个或多个【包】组成。
【传输】(Transfer)包括4种不同类型的传输方式:控制传输(Control Transfer),批量传输(Bulk Transfer),中断传输(Interrupt Transfer)和实时传输(IsochTransfer)。在HID设备中用到了控制传输和中断传输。所有的HID设备通过USB的控制管道(默认管道,即端点0)和中断管道与主机通信。
【事务】(Transaction)可以分成三类:Setup transaction:主机用来向设备发送控制命令 ;Data IN transaction:主机用来从设备读取数据 ;Data OUT transaction:主机用来向设备发送数据 。
【包】(Packet)可以分成四类:令牌包(Token Packet)、数据包(Data Packet)、握手包(Hand Shake Packet)、特殊包(Special Packet)。 USB通讯是以包(Packet)为单位,包里包含包标识符PID(Packet Identifier),用来说明包的类型。
令牌包(Token Packet):令牌包用来启动一次USB传输。包括下面4种:
输出(OUT)令牌包:用来通知设备将要输出一个数据包
输入(IN)令牌包:用来通知设备返回一个数据包
建立(SETUP)令牌包:只用在控制传输中,和输出令牌包作用一样,也是通知设备将要输出一个数据包,两者区别在于:
SETUP令牌包后只使用DATA0数据包,且只能发送到设备的控制端点,并且设备必须要接收,而OUT令牌包没有这些限制
帧起始(SOF)令牌包:在每帧(或微帧)开始时发送,以广播的形式发送,所有USB全速设备和高速设备都可以接收到SOF包。
数据包(Data Packet)顾名思义数据包就是用来传输数据的,分为下面几种:
DATA0和DATA1(USB1.1规范中定义)
DATA2和MDATA包(USB2.0规范中增加)
握手包(Hand Shake Packet)用来识别传输是否被对方确认。包括下面4种:
ACK,NAK,STALL,NYET
特殊包(Special Packet)特殊场合使用的包。包括下面4种:
PRE,ERR,SPLIT,和PING
了解了上面的基础知识后,我们开始分析代码:
当检测到 USB 总线复位、USB 总线挂起或唤醒事件,或者当 USB 成功处理完数据发送或者数据接收后,USB 协议处理器都将设置相应的中断标志并产生中断请求。应用程序可以直接查询或在 USB中断服务程序中查询并分析中断标志寄存器 USB_INT_FG,根据 IF_BUS_RST 和 UIF_SUSPEND 进行相应的处理;
并且,如果UIF_TRANSFER 有效,那么还需要继续分析 USB 中断状态寄存器 USB_INT_ST,根据当前端点号 MASK_UIS_ENDP 和当前事务令牌 PID 标识 MASK_UIS_TOKEN 进行相应的处理。 UIS_TOKEN_IN、UIS_TOKEN_OUT、UIS_TOKEN_SETUP分别对应前面提到的【输入(IN)令牌包】、【输出(OUT)令牌包】和【建立(SETUP)令牌包】。
(注意,为了看起来清爽些,我删除了部分代码,留下主要框架)
- /*******************************************************************************
- * Function Name : DeviceInterrupt()
- * Description : CH559USB中断处理函数
- *******************************************************************************/
- void DeviceInterrupt( void ) interrupt INT_NO_USB using 1 //USB中断服务程序,使用寄存器组1
- {
- UINT8 len,i;
- if(UIF_TRANSFER) //USB传输完成标志
- {
- switch (USB_INT_ST & (MASK_UIS_TOKEN | MASK_UIS_ENDP))
- {
- case UIS_TOKEN_IN | 2: //endpoint 2# 端点批量上传
- case UIS_TOKEN_OUT | 2: //endpoint 2# 端点批量下传
- case UIS_TOKEN_SETUP | 0: //SETUP事务
- case UIS_TOKEN_IN | 0: //endpoint0 IN
- case UIS_TOKEN_OUT | 0: // endpoint0 OUT
- default:
- }
- UIF_TRANSFER = 0; //写0清空中断
- }
- if(UIF_BUS_RST) //设备模式USB总线复位中断
- {
- UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
- UEP1_CTRL = bUEP_AUTO_TOG | UEP_R_RES_ACK;
- UEP2_CTRL = bUEP_AUTO_TOG | UEP_R_RES_ACK | UEP_T_RES_NAK;
- USB_DEV_AD = 0x00;
- UIF_SUSPEND = 0;
- UIF_TRANSFER = 0;
- UIF_BUS_RST = 0; //清中断标志
- }
- if (UIF_SUSPEND) //USB总线挂起/唤醒完成
- {
- UIF_SUSPEND = 0;
- if ( USB_MIS_ST & bUMS_SUSPEND ) //挂起
- {
- }
- }
- else { //意外的中断,不可能发生的情况
- USB_INT_FG = 0xFF; //清中断标志
- }
- }
复制代码
非0端点的令牌包处理起来比较简单:
如果事先设定了各个端点的 OUT 事务的同步触发位 bUEP_R_TOG,那么可以通过 U_TOG_OK 或 bUIS_TOG_OK判断当前所接收到的数据包的同步触发位是否与该端点的同步触发位匹配,如果数据同步,则数据有效;如果数据不同步,则数据应该被丢弃。每次处理完USB 发送或者接收中断后,都应该正确修改相应端点的同步触发位,用于同步下次所发送的数据包和检测下次所接收的数据包是否同步;另外,通过设置 bUEP_AUTO_TOG 可以实现在发送成功或者接收成功后自动翻转相应的同步触发位。
- case UIS_TOKEN_IN | 2: //endpoint 2# 端点批量上传
- UEP2_T_LEN = 0; //预使用发送长度一定要清空
- // UEP1_CTRL ^= bUEP_T_TOG; //如果不设置自动翻转则需要手动翻转
- UEP2_CTRL = UEP2_CTRL & ~ MASK_UEP_T_RES | UEP_T_RES_NAK; //默认应答NAK
- break;
- case UIS_TOKEN_OUT | 2: //endpoint 2# 端点批量下传
- if ( U_TOG_OK ) // 不同步的数据包将丢弃
- {
- len = USB_RX_LEN; //接收数据长度,数据从Ep2Buffer首地址开始存放
- for ( i = 0; i < len; i ++ )
- {
- Ep2Buffer[MAX_PACKET_SIZE+i] = Ep2Buffer[i] ^ 0xFF; // OUT数据取反到IN由计算机验证
- }
- UEP2_T_LEN = len;
- UEP2_CTRL = UEP2_CTRL & ~ MASK_UEP_T_RES | UEP_T_RES_ACK; // 允许上传
- }
- break;
复制代码
端点0的令牌包处理起来复杂一些,这是因为设备的各种描述符和主机的命令都是通过端点0传输的。
先说端点0的建立(SETUP)令牌包处理:识别出建立令牌包后,获取USB接收数据长度,与建立请求的大小比较,如不符合,则错误,进行错误处理。如符合,则进一步处理。通过获取请求类型,来识别是HID类命令还是标准请求。(注意,为了看起来清爽些,我删除了部分代码,留下主要框架)
- case UIS_TOKEN_SETUP | 0: //SETUP事务
- len = USB_RX_LEN;
- if(len == (sizeof(USB_SETUP_REQ)))
- {
- SetupLen = UsbSetupBuf->wLengthL;
- len = 0; // 默认为成功并且上传0长度
- SetupReq = UsbSetupBuf->bRequest;
- if ( ( UsbSetupBuf->bRequestType & USB_REQ_TYP_MASK ) != USB_REQ_TYP_STANDARD )/*HID类命令*/
- {
- }
- else //标准请求
- {
- }
- }
- else
- {
- len = 0xff; //包长度错误
- }
- if(len == 0xff)
- {
- SetupReq = 0xFF;
- UEP0_CTRL = bUEP_R_TOG | bUEP_T_TOG | UEP_R_RES_STALL | UEP_T_RES_STALL;//STALL
- }
- else if(len <= THIS_ENDP0_SIZE) //上传数据或者状态阶段返回0长度包
- {
- UEP0_T_LEN = len;
- UEP0_CTRL = bUEP_R_TOG | bUEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;//默认数据包是DATA1,返回应答ACK
- }
- else
- {
- UEP0_T_LEN = 0; //虽然尚未到状态阶段,但是提前预置上传0长度数据包以防主机提前进入状态阶段
- UEP0_CTRL = bUEP_R_TOG | bUEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;//默认数据包是DATA1,返回应答ACK
- }
- break;
复制代码
HID类命令处理:(从代码中可以看出,程序只对HID的Get_Report请求做了简单处理)
- if ( ( UsbSetupBuf->bRequestType & USB_REQ_TYP_MASK ) != USB_REQ_TYP_STANDARD )/*HID类命令*/
- {
- switch( SetupReq )
- {
- case 0x01: //GetReport
- pDescr = UserEp2Buf; //控制端点上传输据
- if(SetupLen >= THIS_ENDP0_SIZE) //大于端点0大小,需要特殊处理
- {
- len = THIS_ENDP0_SIZE;
- }
- else
- {
- len = SetupLen;
- }
- break;
- case 0x02: //GetIdle
- break;
- case 0x03: //GetProtocol
- break;
- case 0x09: //SetReport
- break;
- case 0x0A: //SetIdle
- break;
- case 0x0B: //SetProtocol
- break;
- default:
- len = 0xFF; /*命令不支持*/
- break;
- }
- if ( SetupLen > len )
- {
- SetupLen = len; //限制总长度
- }
- len = SetupLen >= THIS_ENDP0_SIZE ? THIS_ENDP0_SIZE : SetupLen;//本次传输长度
- memcpy(Ep0Buffer,pDescr,len); //加载上传数据
- SetupLen -= len;
- pDescr += len;
- }
复制代码
标准请求处理:(从代码中可以看出,程序分别对GET_DESCRIPTOR、SET_ADDRESS、GET_CONFIGURATION、SET_CONFIGURATION、CLEAR_FEATURE、SET_FEATURE、GET_STATUS7种请求做了处理)
再详细看一下对GET_DESCRIPTOR(获得描述符请求)的处理。 GET_DESCRIPTOR请求的wValue字段的高字节表示要取得描述符类型,低字节表示描述符的索引值,描述的类型有:1表示设备描述符,2表示配置描述符,3表示字符串描述符,4表示接口描述符,5表示端点描述符,0x22表示报表描述符。
- case USB_GET_DESCRIPTOR:
- switch(UsbSetupBuf->wValueH)
- {
- case 1: //设备描述符
- pDescr = DevDesc; //把设备描述符送到要发送的缓冲区
- len = sizeof(DevDesc);
- break;
- case 2: //配置描述符
- pDescr = CfgDesc; //把设备描述符送到要发送的缓冲区
- len = sizeof(CfgDesc);
- break;
- case 0x22: //报表描述符
- pDescr = HIDRepDesc; //数据准备上传
- len = sizeof(HIDRepDesc);
- Ready = 1; //如果有更多接口,该标准位应该在最后一个接口配置完成后有效
- break;
- default:
- len = 0xff; //不支持的命令或者出错
- break;
- }
- if ( SetupLen > len )
- {
- SetupLen = len; //限制总长度
- }
- len = SetupLen >= THIS_ENDP0_SIZE ? THIS_ENDP0_SIZE : SetupLen;//本次传输长度
- memcpy(Ep0Buffer,pDescr,len); //加载上传数据
- SetupLen -= len;
- pDescr += len;
- break;
复制代码
端点0的输入和输出令牌包处理起来就简单一些了。
- case UIS_TOKEN_IN | 0: //endpoint0 IN
- switch(SetupReq)
- {
- case USB_GET_DESCRIPTOR:
- case HID_GET_REPORT:
- len = SetupLen >= THIS_ENDP0_SIZE ? THIS_ENDP0_SIZE : SetupLen; //本次传输长度
- memcpy( Ep0Buffer, pDescr, len ); //加载上传数据
- SetupLen -= len;
- pDescr += len;
- UEP0_T_LEN = len;
- UEP0_CTRL ^= bUEP_T_TOG; //同步标志位翻转
- break;
- case USB_SET_ADDRESS:
- USB_DEV_AD = USB_DEV_AD & bUDA_GP_BIT | SetupLen;
- UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
- break;
- default:
- UEP0_T_LEN = 0; //状态阶段完成中断或者是强制上传0长度数据包结束控制传输
- UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
- break;
- }
- break;
- case UIS_TOKEN_OUT | 0: // endpoint0 OUT
- len = USB_RX_LEN;
- if(SetupReq == 0x09)
- {
- if(Ep0Buffer[0])
- {
- printf("Light on Num Lock LED!\n");
- }
- else if(Ep0Buffer[0] == 0)
- {
- printf("Light off Num Lock LED!\n");
- }
- }
- UEP0_CTRL ^= bUEP_R_TOG; //同步标志位翻转
- break;
复制代码
到此,USB中断函数就分析完了。 但是大家会不会觉得奇怪:没看到数据包、握手包、帧起始包的处理啊?其实这些包的处理都是被MCU硬件处理了。我们在程序中设置或读取他们的状态标志就可以了。
烧录到评估板,并运行。找到USB设备(这个没名字,开始找了半天),可以发现PID 2007、VID 5131、 PVN 0000 正是我们前面设备描述符中设置的(0x31,0x51,0x07,0x20,0x00,0x00).通过窗口发送32个HEX数据,结果收到端点2返回64个数据。
- /*设备描述符*/
- UINT8C DevDesc[18] = {0x12,0x01,0x10,0x01,0x00,0x00,0x00,THIS_ENDP0_SIZE,
- 0x31,0x51,0x07,0x20,0x00,0x00,0x00,0x00,
- 0x00,0x01
- };
复制代码
用监视HID功能,发现发送的数据和返回的数据正好互补。
分析代码可知,是在下面代码处将收到的数据取反,然后放到发送的缓冲区。
- case UIS_TOKEN_IN | 2: //endpoint 2# 端点批量上传 UEP2_T_LEN = 0; //预使用发送长度一定要清空
- // UEP1_CTRL ^= bUEP_T_TOG; //如果不设置自动翻转则需要手动翻转
- UEP2_CTRL = UEP2_CTRL & ~ MASK_UEP_T_RES | UEP_T_RES_NAK; //默认应答NAK
- break;
- case UIS_TOKEN_OUT | 2: //endpoint 2# 端点批量下传
- if ( U_TOG_OK ) // 不同步的数据包将丢弃
- {
- len = USB_RX_LEN; //接收数据长度,数据从Ep2Buffer首地址开始存放
- for ( i = 0; i < len; i ++ )
- {
- Ep2Buffer[MAX_PACKET_SIZE+i] = Ep2Buffer ^ 0xFF; // ★★★OUT数据取反到IN由计算机验证★★★
- }
- UEP2_T_LEN = len;
- UEP2_CTRL = UEP2_CTRL & ~ MASK_UEP_T_RES | UEP_T_RES_ACK; // 允许上传
- }
- break;
复制代码
修改代码,不再取反。重新编译烧录,发现果然返回的数据和发送的数据一样。
- case UIS_TOKEN_IN | 2: //endpoint 2# 端点批量上传
- UEP2_T_LEN = 0; //预使用发送长度一定要清空
- // UEP1_CTRL ^= bUEP_T_TOG; //如果不设置自动翻转则需要手动翻转
- UEP2_CTRL = UEP2_CTRL & ~ MASK_UEP_T_RES | UEP_T_RES_NAK; //默认应答NAK
- break;
- case UIS_TOKEN_OUT | 2: //endpoint 2# 端点批量下传
- if ( U_TOG_OK ) // 不同步的数据包将丢弃
- {
- len = USB_RX_LEN; //接收数据长度,数据从Ep2Buffer首地址开始存放
- for ( i = 0; i < len; i ++ )
- {
- Ep2Buffer[MAX_PACKET_SIZE+i] = Ep2Buffer; // ★★★OUT数据发回计算机验证★★★
- }
- UEP2_T_LEN = len;
- UEP2_CTRL = UEP2_CTRL & ~ MASK_UEP_T_RES | UEP_T_RES_ACK; // 允许上传
- }
- break;
复制代码
此内容由EEWORLD论坛网友yang_alex原创,如需转载或用于商业用途需征得作者同意并注明出处