安卓设备的USB-HID通讯例程的开发 (4)
- HID设备对三大主要事务的处理(C代码)
本博文系JGB联合商务组的原创作品,引用请标明出处
在HID设备一侧,完整地理解C代码对三大主要事务(SETUP事务, IN事务, OUT事务)的处理是很重要的,本通讯例程的C源码均涉及到对这三个事务的处理。
由MCD应用团队提供的USB函数库有如下5个文件:
- usb基础支持函数: usb.h / usb.c
- usb各种描述符声明: usb_desc.h / usb_desc.c
- usb各种中断入口定义: usb_it.c
其中usb.h的头文件定义十分简练明了,库内的文件个数也只有五个,因此深受USB开发者的喜爱,其中重点代码是完成一次正确传送的回调函数:CTR_CallBack(),
这里的正确传送指的是当前收到或发送的包是正确的,包主要有三种:
令牌包,数据包,握手包。每当完成这三种包之一的正确发送或正确接收都会触发一次这个中断回调。
另外,这里的CTR_CallBack()函数中的正确传送(CT)是针对包来说,不要和前面的写NFC卡某个单元时提到的完整传输混为一谈了。传输 - 事务 - 包的分解及合成关系中,传输处于最顶层,而包处于最底层,另外不同的包用不同包标识符PID区分。
- 令牌包(总是OUT方向)分三种:
- 1. SETUP令牌包(SETUP事务用),端点0处理。
- 2. IN令牌包(非0端点的IN事务用,SIE硬件层面处理, C代码无处理)
- 3. OUT令牌包(非0端点的OUT事务用,SIE硬件层面处理, C代码无处理)
- 数据包按方向细分为两种: DATA_OUT包 , DATA_IN包。
- 握手包按方向也细分为两种 : STATUS_OUT包 , STATUS_IN包。
IN事务和OUT事务中的握手包也在SIE硬件层面处理, C代码无处理。
SETUP事务中的三个阶段的包都是有方向的,对于有数据阶段的SETUP事务可划分为如下两种情形:
(1) 请求设备接收: SETUP令牌 -> DATA OUT ->STATUS IN
(其中的 DATA OUT ->STATUS IN 可以重复进行多次)
( 图4-A )
(2)请求设备发送: SETUP令牌 -> DATA IN ->STATUS OUT
(其中的 DATA IN ->STATUS OUT 可以重复进行多次,例如主机请求某些超过64字节长度的HID报告描述符即Report_Descriptor时就是这样的情形)
( 图4-B )
其中的OUT表示主机发出的包,IN表示设备发出的包,当然令牌包总是由主机发出的。
只有理解了上述的每个阶段的包方向,才能顺着它的流程指向在如下的C代码中分别找到其相应的处理方式。
CTR_CallBack()正确传送回调函数(由MCD应用团队提供),代码很简短,所以我会尽可能给出了每行的注释。
理解此段代码前需先了解如下几个由MCD应用团队声明的全局变量
//这两个变量标记端点0接下来【应该】是处于哪种状态(根据下一个包的流程指向而定,应准确修改它们),
//当前正确传送处理完成后要恢复之。
u16 SaveRxStatus =0 ;
u16 SaveTxStatus=0;
//SETUP事务三个阶段,三种包(都有方向)
//端点0的接收包有三种: SETUP包 , DATA_OUT包 , STATUS_OUT包
//端点0的发送包有两种: DATA_IN包 , STATUS_IN包
//这个变量标记事务【即将】进入哪一个阶段(包加上WAIT_ 前缀即表示将要进入哪一个阶段)
//例如: WAIT_DATA_OUT 表示端点0即将进入等待主机发来数据包的阶段。
u8 ControlStatus=0;
//SETUP令牌包的指针形式
SETUP_DATA *token;
//端点0的临时接收区
u8 EP0_RxBuf[64]={0};
以下是CTR_CallBack()回调函数:
//完成一次正确的传送,CTR即正确传送寄存器之意
void CTR_CallBack(void)
{
//存放端点号
u16 EpNum;
//存放此端点号对应的端点寄存器值
u16 EpReg;
//临时短整数变量
u16 ix=0 ;
//取得端点号,ISTR中断状态寄存器的低四位是端点号,所以要掩码0x000f
EpNum = (*(u16 *)ISTR & 0x000f);
if(EpNum == 0)
{
//开始处理端点0完成的一次正确传送
SaveRxStatus = GetEPRxStatus(EP0);
SaveTxStatus = GetEPTxStatus(EP0);
//立即设置繁忙标记,处理完后才解除
SetEpTxStatus(EP0,EP_TX_NAK);
SetEpRxStatus(EP0,EP_RX_NAK);
//方向检查: 如果ISTR的bit4位为1,则EP_CTR_RX=1 表示为OUT方向 ,有包从主机流向设备
//例如: SETUP令牌,EP0收到数据(DATA_OUT),收到握手(STATUS_OUT) 这三个包都是OUT方向.
//反之,若ISTR的bit4位为0,则EP_CTR_TX=1 , 表示为IN方向,有包从设备流向主机
//例如: EP0发送数据(DATA_IN),发送握手(STATUS_IN) 这两个包都是IN方向.
if(*(u16 *)ISTR & ISTR_DIR)
{
// 这里开始单片机EP0的OUT方向处理
// 附注: 不要随意在EP0的SETUP包的处理中加入printf(),否则很可能会造成无法识别设备!!!!!!!!
// 先检查是否为SETUP令牌, 若不是那么就是EP0的: DATA_OUT 或STATUS_OUT
if((*(u16 *)EP0REG) & EP_SETUP)
{
//处理SETUP令牌包
//先清除正确接收
Clr_CTR_Rx(EP0);
//EP0接收缓冲区已经存放了SETUP令牌包,这里把它暂存到EP0_RxBuf临时接收区
GetEpRxBuf(EP0_RxBuf,EP0);
//以指针形式取回令牌包
token = (SETUP_DATA *)&EP0_RxBuf[0];
//开始检查此SETUP令牌包的各种特定标准请求 或 特定类/自定义类请求
if(token->bRequest == GET_DESCRIPTOR )
{
//处理有关: 获取描述符的标准请求
Do_GetDescriptor();
} //GET_DESCRIPTOR ... end
else if(token->bRequest == SET_ADDRESS)
{
//设置HID设备地址的标准请求
//接下来应该 : 等待本设备发出握手包
ControlStatus = WAIT_STATUS_IN;
//这句标记设置的方式有典型意义!!!
//同时放进设置地址的标记,也就是说当设备完成本次的握手动作后,将直接完成对本设备的地址设定,
//附注: 设置地址这个标准请求是没有数据阶段的,要设置的地址就放在SETUP令牌的wValue字段里
//参见后面的 IN 方向的 STATUS_IN 判定
ControlStatus |= WAIT_SET_ADDRESS;
//完成本设备发出握手包的标准动作(后面的代码还有很多次这样的动作): 发送的字节长度为0并令发送有效
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
} //SET_ADDRESS ... end
else if(token->bRequest == GET_CONFIGURATION)
{
//MCD应用团队提供的源码即为如此: 此处为空,有待研讨。
} // GET_CONFIGURATION ... end
else if(token->bRequest == SET_CONFIGURATION)
{
//此阶段很重要:到了这里,整个设备的枚举就完成了,设备已处于配置完成状态,可以进行USB通讯了
//这里EP0只需发出握手包即可
ControlStatus = WAIT_STATUS_IN;
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
} // SET_CONFIGURATION ... end
else if(token->bRequest == CLEAR_FEATURE)
{
//处理有关: 清除特性的标准请求
Do_ClearFeature();
} // CLEAR_FEATURE ... end
else if(token->bRequest == SET_FEATURE)
{
//处理有关: 设置特性的标准请求
//EP0只需发出握手包
ControlStatus = WAIT_STATUS_IN;
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
} // SET_FEATURE) ... end
//检查是否为类请求及接收者为接口
else if((token->bmRequestType & 0x7f) == (CLASS_REQUEST | INTERFACE_RECIPIENT))
{
//处理有关: 特定类请求和自定义类请求, 这里是本工程的核心(A)
Do_ClassRequest() ;
} // CLASS_REQUEST | INTERFACE_RECIPIENT ... end
else
{
printf("UnSupport RequestType\r\n");
}
} //if((*(u16 *)EP0REG) & EP_SETUP) SETUP令牌处理 End
else if((*(u16 *)EP0REG) & EP_CTR_RX)
{
//处理EP0接收正确
//先清除接收正确的标记
Clr_CTR_Rx(EP0);
//在这里的EP0接收正确只有两种可能: 不是STATUS_OUT 那么就是DATA_OUT
if(ControlStatus & WAIT_STATUS_OUT)
{
//到了这里表示: 本设备的的SIE(串行接口引擎)已收到主机发来的握手包(STATUS OUT),
//后面将处于TX/RX的熄火状态(STALL), 一个SETUP事务就结束了.
//其实在这里: 在等待主机发来握手包之后你应检查已发送的数据包字节数是否完整才能决定是否要熄火!!!
//例如: 发送超过64字节长度的HID报告描述符时因为其长度较长而分为两个包来发送,
//那么第一个包发出去后在这里收到了主机发来的第一个握手包(STATUS OUT),
//那么此刻你就不能熄火,还要继续发完第二个包,只有在收到了主机发来的第二个握手包(STATUS OUT)后才能熄火!!!
//附注: SETUP事务中允许在一个SETUP令牌后有多个数据包(当然每个数据包后都有握手确认包 ),
// 而中断类型端点的IN事务和OUT事务则不能这样,这两个事务的令牌之后只能有一个数据包, 然后就是握手确认包.
SaveRxStatus = EP_RX_STALL;
SaveTxStatus = EP_TX_STALL;
ControlStatus = STALLED;
}
else if(ControlStatus & WAIT_DATA_OUT)
{
//处理有关: EP0的OUT方向的数据包(即数据阶段由主机发给设备的数据包)
//暂时只有两种DATA OUT包的情形要处理 : (1)写NFC卡需要的三个参数 (2)读NFC卡需要的二个参数
Do_EP0DataOut();
}
} //if((*(u16 *)EP0REG) & EP_CTR_RX) end
} //这里是单片机EP0的OUT方向处理的结束点
else
{
//处理EP0发送正确
if((*(u16 *)EP0REG) & EP_CTR_TX)
{
// EP0 发送正确完成
//先清除发送正确的标记
Clr_CTR_Tx(EP0);
//在这里的EP0发送正确只有两种可能: 不是STATUS_IN ,就是 DATA_IN
if(ControlStatus & WAIT_STATUS_IN)
{
//到了这里表示: 本设备的的SIE(串行接口引擎)已向主机发送握手包(STATUS IN),
//后面将处于TX/RX的熄火状态(STALL), 一个SETUP事务就结束了.
if(ControlStatus & WAIT_SET_ADDRESS){
// 标准请求的本地操作:设置设备地址(该事务是无数据阶段的)
// 要设置的地址就放在SETUP令牌的wValue字段处,即第三个字节处。
// 因此在设备向主机发出握手确认后,直接就完成取数和设置地址!!!
SetDeviceAddr(EP0_RxBuf[2]);
}
//本设备TX/RX都处于熄火状态(STALL), 一个完整的SETUP事务就结束了.
//若某个事务是包含有多个由主机发来的数据阶段的,那么在这里熄火之前你应检查收到的数据包字节数是否完整才能决定是否要熄火!!!
SaveRxStatus = EP_RX_STALL;
SaveTxStatus = EP_TX_STALL;
ControlStatus = STALLED;
}
else if(ControlStatus & WAIT_DATA_IN)
{
//因为设备刚刚完成的DATA_IN阶段,已把数据送达主机了。
//那么这几句即表示下一步本设备的SIE(串行接口引擎)需等待主机发来握手包(STATUS_OUT),
ControlStatus = WAIT_STATUS_OUT;
//接收需打开
SaveRxStatus = EP_RX_VALID;
//发送不需要打开
SaveTxStatus = EP_TX_STALL;
}
} //if((*(u16 *)EP0REG) & EP_CTR_TX) end
} //if(*(u16 *)ISTR & ISTR_DIR)
//0端点的检查即将结束, 设置EP0接下来应该进入的状态后要返回 ,即后面的非0端点的事不用做了。
SetEpTxStatus(EP0,SaveTxStatus);
SetEpRxStatus(EP0,SaveRxStatus);
return;
} // EP0 的检查到此结束
else
{
//非0端点 (EP1/EP2) 的检查
//先取回端点n寄存器的值(n=1或2)
EpReg = GetEpReg(EpNum);
//得到端点号所对应的端点n寄存器之当前的值
//正确接收: EpReg高字节的最高位 =1 , EP_CTR_RX=0x8000是一个掩码常数
if(EpReg & EP_CTR_RX)
{
//处理非0端点的正确接收
//先清除此端点的正确接收标记
Clr_CTR_Rx(EpNum);
//从非0端点接收缓冲区复制数据到第一用户接收缓冲区EP_Buf
//转移最前面字节
USBRxCNT= 0 ;
GetEpRxBuf(EP_Buf,EpNum);
//再次复制数据到第二用户接收缓冲区USBBufRecei
//USBRxCNT : USB本次收到的字节数,在前面调用了 GetEpRxBuf(EP_Buf,EpNum) 函数后它就有值了.
USBBufRecei[EP1_PACKETSIZE]=0;
for (ix=0; ix<USBRxCNT; ix++){
USBBufRecei[ix]=EP_Buf[ix] ;
}
//最后一个字节放0,则表示本字符串到此结束
USBBufRecei[USBRxCNT]=0;
//设置接收完成的标记
USBRxOKFlag = 1;
//可以继续下一个的接收(中断端点: 仍需要等待OUT令牌的到来才能继续接收)
SetEpRxStatus(EpNum,EP_RX_VALID);
} //if(EpReg & EP_CTR_RX) end
//正确发送: EpReg低字节的最高位 =1 , EP_CTR_TX=0x0080是一个掩码常数
if(EpReg & EP_CTR_TX)
{
//处理非0端点的正确发送
//先清除此非0端点的正确发送标记
Clr_CTR_Tx(EpNum);
//发送完成的标记
USBTxOKFlag = 1;
}
} // ... 非0端点 (EP1/EP2) 的检查 end
} // void CTR_CallBack(void) end
我把上面代码块中的重点部分单独抽出放在这里,它完成的第一个判断是:
检查SETUP令牌包中的第一个入参(RequestType)同时满足: 类请求和接口为接收者时应跳到 Do_ClassRequest()函数。
//检查是否为类请求及接收者为接口
if((token->bmRequestType & 0x7f) == (CLASS_REQUEST | INTERFACE_RECIPIENT))
{
//处理有关: 特定类请求和自定义类请求, 这里是本工程的核心(A)
Do_ClassRequest() ;
}
而Do_ClassRequest()处理函数的完整定义如下:
//处理有关: 特定类请求和自定义类请求 (本HID设备的核心所在)
void Do_ClassRequest()
{
//先处理主机对自定义类:TEST_JGB01 的请求
if(token->bRequest == TEST_JGB01)
{
// 随意在EP0的通讯中加入printf()会造成无法识别设备!!!!!!!!
// printf("User-Req-OK\r\n");
if (token->wValue.w == (u16)EPStatus_Flag1)
{
//EPStatus_Flag=61
//用来返回三个端点的状态,8个字节
*(u16*)EPStatus = GetEPTxStatus(EP0) ;
*(u16*)(EPStatus+2) = GetEPRxStatus(EP0) ;
*(u16*)(EPStatus+4) = GetEPRxStatus(EP1) ;
*(u16*)(EPStatus+6) = GetEPTxStatus(EP2) ;
SetEpTxBuf(EPStatus,8,EP0);
SetEP0TxCount(8);
ControlStatus = WAIT_DATA_IN;
SaveTxStatus = EP_TX_VALID;
SaveRxStatus = EP_RX_VALID;
}
else if (token->wValue.w == (u16)EPMAX_Flag1)
{
//EPMAX_Flag1 = 60
//用来确认设备支持的最大逻辑单元数 : 3 ; 及端点的最大字节数: EP1_PACKETSIZE
MaxLun[0] =(u8)3;
MaxLun[1] =(u8)EP1_PACKETSIZE;
SetEpTxBuf(MaxLun,2,EP0);
SetEP0TxCount(2);
ControlStatus = WAIT_DATA_IN;
SaveTxStatus = EP_TX_VALID;
SaveRxStatus = EP_RX_VALID;
}
else if (token->wValue.w == (u16)NID_Flag1)
{
//NID_Flag1 = 71
//取NFC卡的唯一序列号
IsNIDFlag1 =1;
//由于[读取NFC卡的唯一序列号]的操作是无数据阶段的,因此必须在这里立即回应一个ACK包给主机,否则主机会一直在等待这个ACK包的到来,
//直到超时.此时表现的Bug是: 主机端的程序会出现反应很缓慢(比如点亮/熄灯要等4S后才有反应)
//这里是回应ACK包的标准格式:
ControlStatus = WAIT_STATUS_IN;
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
}
else if (token->wValue.w == (u16)NRX_Flag1)
{
// NRX_Flag1 = 72
// 读出NFC卡的某个地址单元的一个字节
//SETUP令牌之后将是主机发来的数据
ControlStatus = WAIT_DATA_OUT;
//需打开接收
SaveRxStatus = EP_RX_VALID;
//记录当前的指令值
//设置等待数据阶段的数据到来的标记(即接下来准备获取主机发来的2个字节的数据)
Wait_EP0RX_Flag = (u8)NRX_Flag1;
}
else if (token->wValue.w == (u16)NWX_Flag1)
{
//NWX_Flag1 = 73
//写入NFC卡的某个地址单元的一个字节
//SETUP令牌之后将是主机发来的数据
ControlStatus = WAIT_DATA_OUT;
//需打开接收
SaveRxStatus = EP_RX_VALID;
//记录当前的指令值
//设置等待数据阶段的数据到来的标记(即接下来准备获取主机发来的3个字节的数据)
Wait_EP0RX_Flag = (u8)NWX_Flag1;
}
else if (token->wValue.w == (u16)Status_Flag1)
{
//Status_Flag1 = 10
//取回状态标记
IsStatusFlag1 =1;
//必须在这里回应一个ACK包给主机,否则主机会一直在等待这个ACK包的到来,
//直到超时.此时表现的Bug是: 主机端的程序会出现反应很缓慢(比如点亮/熄灯要等4S后才有反应)
//这里是回应ACK包的标准格式:
ControlStatus = WAIT_STATUS_IN;
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
}
else if (token->wValue.w == (u16)Temp_Flag0)
{
//Temp_Flag0 = 20
//停止温控标记
IsTempFlag0 =1;
//必须在这里回应一个ACK包给主机,否则主机会一直在等待这个ACK包的到来,
//直到超时.此时表现的Bug是: 主机端的程序会出现反应很缓慢(比如点亮/熄灯要等4S后才有反应)
//这里是回应ACK包的标准格式:
ControlStatus = WAIT_STATUS_IN;
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
}
else if (token->wValue.w == (u16)Temp_Flag1)
{
//Temp_Flag1 = 30
//取回温控标记
IsTempFlag1 =1;
//必须在这里回应一个ACK包给主机,否则主机会一直在等待这个ACK包的到来,
//直到超时.此时表现的Bug是: 主机端的程序会出现反应很缓慢(比如点亮/熄灯要等4S后才有反应)
//这里是回应ACK包的标准格式:
ControlStatus = WAIT_STATUS_IN;
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
}
else if (token->wValue.w == (u16)Ledx_Flag0)
{
//Ledx_Flag0 = 40
//蓝灯熄灭
IsLedxFlag0 =1;
//必须在这里回应一个ACK包给主机,否则主机会一直在等待这个ACK包的到来,
//直到超时.此时表现的Bug是: 主机端的程序会出现反应很缓慢(比如点亮/熄灯要等4S后才有反应)
//这里是回应ACK包的标准格式:
ControlStatus = WAIT_STATUS_IN;
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
}
else if (token->wValue.w == (u16)Ledx_Flag1)
{
//Ledx_Flag1 = 50
//蓝灯亮起
IsLedxFlag1 =1;
//必须在这里回应一个ACK包给主机,否则主机会一直在等待这个ACK包的到来,
//直到超时.此时表现的Bug是: 主机端的程序会出现反应很缓慢(比如点亮/熄灯要等4S后才有反应)
//这里是回应ACK包的标准格式:
ControlStatus = WAIT_STATUS_IN;
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
} else {
// 不支持命令字, 但仍需回复一个确认包
IsNoFlag1 =1;
//必须在这里回应一个ACK包给主机,否则主机会一直在等待这个ACK包的到来,
//直到超时.此时表现的Bug是: 主机端的程序会出现反应很缓慢(比如点亮/熄灯要等4S后才有反应)
//这里是回应ACK包的标准格式:
ControlStatus = WAIT_STATUS_IN;
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
}
} // 自定义类: TEST_JGB01 处理结束
//再处理六个规范的特定类请求(这里都是空实现)
else if(token->bRequest == GET_REPORT)
{
printf("GET_REPORT\r\n");
}
else if(token->bRequest == GET_IDLE)
{
printf("GET_IDLE\r\n");
}
else if(token->bRequest == GET_PROTOCOL)
{
printf("GET_PROTOCOL\r\n");
}
else if(token->bRequest == SET_REPORT)
{
printf("SET_REPORT\r\n");
}
else if(token->bRequest == SET_IDLE)
{
printf("SET_IDLE\r\n");
}
else if(token->bRequest == SET_PROTOCOL)
{
printf("SET_PROTOCOL\r\n");
}
else
{
//不支持的类请求
printf("SET_UnSupport_ClassReq\r\n");
}
}
为方便读者理解,我这里也单独把写NFC卡单元的类请求处理抽出,很简短的几句代码,一看就非常清楚,主要就是请求值的检测完成后就应该进入OUT方向的数据阶段: WAIT_DATA_OUT .
//先处理主机对自定义类:TEST_JGB01 的请求
if(token->bRequest == TEST_JGB01)
{
//......
//再处理请求值
if (token->wValue.w == (u16)NWX_Flag1)
{
//NWX_Flag1 = 73 : 表示是【写入NFC卡的某个地址单元的一个字节】的请求值
//SETUP令牌之后将是主机发来的数据
ControlStatus = WAIT_DATA_OUT;
//需打开接收,因为主机发送过来的数据包马上就到了,见上面的图4-A
SaveRxStatus = EP_RX_VALID;
//记录当前的请求值
//设置等待数据阶段的数据到来的标记(即接下来准备获取主机发来的3个字节的数据)
Wait_EP0RX_Flag = (u8)NWX_Flag1;
}
//......
}
上面的这段代码中的重点:
ControlStatus = WAIT_DATA_OUT 表示接下来将进入事务中的数据阶段(OUT方向),流程参见上面的图4-A。
此后,我们在下一个CTR_CallBack()发生时将会看到此函数有如下动作:
先断言EP0接收正确,接着检查状态掩码后是否为:
WAIT_DATA_OUT或WAIT_STATUS_OUT,如果是WAIT_DATA_OUT则跳转去
执行Do_EP0DataOut()函数,处理主机发给EP0的数据。
//如果EP0接收正确
if((*(u16 *)EP0REG) & EP_CTR_RX)
{
//处理EP0接收正确
//先清除接收正确的标记
Clr_CTR_Rx(EP0);
if(ControlStatus & WAIT_STATUS_OUT){
//处理主机发来的握手
}
else if(ControlStatus & WAIT_DATA_OUT)
{
//处理有关: EP0的OUT方向的数据包(即数据阶段由主机发给设备的数据包)
//暂时只有两种DATA OUT包的情形要处理 : (1)写NFC卡需要的三个参数 (2)读NFC卡需要的二个参数
Do_EP0DataOut();
}
} //EP0接收正确处理完成
处理主机发来的数据是由Do_EP0DataOut() 函数来完成的:
//处理有关: EP0的OUT方向的数据包(即数据阶段由主机发给设备的数据)
//暂时有两个事务要处理 : (1)写NFC卡需要的三个参数 (2)读NFC卡需要的二个参数
void Do_EP0DataOut()
{
//到了这里表示: 本设备的端点0已收到了主机发来的数据,此时本设备应发出一个握手包(STATUS IN)
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
SaveRxStatus = EP_RX_STALL;
ControlStatus = WAIT_STATUS_IN;
//四个字节数据的NFC数据准备区(只用到前三个,第四个字节保留不用)
EP0RXTemp[0]=0;
EP0RXTemp[1]=0;
EP0RXTemp[2]=0;
EP0RXTemp[3]=0;
//收到的数据长度
EP0RXCounter = GetEP0RxCount() ;
//需检查EP0接收标记Wait_EP0RX_Flag 和收到的数据长度
if (( Wait_EP0RX_Flag == (u8)NWX_Flag1) && (EP0RXCounter==3)){
IsNWXFlag1 =1 ;
Wait_EP0RX_Flag = 0 ;
//取回EP0收到的3字节数据
GetEpRxBuf(EP0RXTemp,EP0);
//转放到NFC数据准备区
BulkAddrNFC = EP0RXTemp[0] ;
UnitAddrNFC = EP0RXTemp[1] ;
WRDataNFC = EP0RXTemp[2] ;
}
//需检查EP0接收标记Wait_EP0RX_Flag 和收到的数据长度
else if (( Wait_EP0RX_Flag == (u8)NRX_Flag1) && (EP0RXCounter == 2)) {
IsNRXFlag1 =1 ;
Wait_EP0RX_Flag = 0 ;
//取回EP0收到的2字节数据
GetEpRxBuf(EP0RXTemp,EP0);
//转放到NFC数据准备区
BulkAddrNFC = EP0RXTemp[0] ;
UnitAddrNFC = EP0RXTemp[1] ;
} else {
//收到了不能识别的数据
IsNoFlag1 =1 ;
//需清除此标记
Wait_EP0RX_Flag = 0 ;
}
}
上面的Do_EP0DataOut() 函数中处理收到的数据也很简单,其中比较有意思的是这段:
//到了这里表示: 本设备的端点0已收到了主机发来的数据,此时本设备应发出一个握手包(STATUS IN)
SetEP0TxCount(0);
SaveTxStatus = EP_TX_VALID;
SaveRxStatus = EP_RX_STALL;
ControlStatus = WAIT_STATUS_IN;
它表示,发出一个握手包(STATUS IN)的标准格式是: 设置要发送的字节数为0并置定发送标记为有效。
许多初入门的开发者常忘记发出 STATUS IN 握手包这一步: 导致主机一直在等待这个握手包的到来直到超时,表现为主机包括UI界面的反应非常迟缓卡顿。
同样,当本设备发出一个握手包的动作完成后会引发下一个CTR_CallBack()回调,该函数的执行代码如下:
//处理EP0发送正确
if((*(u16 *)EP0REG) & EP_CTR_TX)
{
// EP0 发送正确完成
//先清除发送正确的标记
Clr_CTR_Tx(EP0);
//在这里的EP0发送正确只有两种可能: 不是STATUS_IN ,就是 DATA_IN
if(ControlStatus & WAIT_STATUS_IN)
{
//到了这里表示: 本设备的的SIE(串行接口引擎)已向主机发送握手包(STATUS IN),
//此后本设备TX/RX都处于熄火状态(STALL), 一个完整的SETUP事务就结束了.
//若某个SETUP事务是包含有多个由主机发来的数据阶段的,那么在这里你应检查收到的数据包字节数是否完整才能决定是否要熄火!!!
//例如, 若还需要继续接收主机发来的数据那你应该这样做:
//SaveRxStatus = EP_RX_VALID 及 ControlStatus = WAIT_DATA_OUT;
SaveRxStatus = EP_RX_STALL;
SaveTxStatus = EP_TX_STALL;
ControlStatus = STALLED;
}
//......
}
- 附注: SETUP事务结束后,端点0的发送和接收均处于熄火(STALL), 即不收也不发。
到了这里,一个完整的请求设备接收数据的SETUP事务就结束了。
同样要想理解请求设备发送数据的SETUP事务也需要按上面的流程图4-B的指示,东一块西一块地在代码中跳转和解析三个阶段的每一个阶段的处理,这也是为何USB通讯协议中的SETUP事务较为复杂之原因。
而IN事务和OUT事务的处理则简单多了,它不需要在C代码中处理IN/OUT令牌和握手, 只需处理数据阶段, 基本上几句代码就解决了,读者可自行在CTR_CallBack()函数的末尾段落查看。
在USB协议体系中有一个芯片内的硬件外设扮演了一个很重要的角色却很少有人提及它,这个就是SIE(Serial Interface Engine)串行接口引擎,它主要完成如下几个功能:
- 最底层NRZI 编码/译码和CRC生成/校验.
- 并入串出和串入并出的高速移位操作, 完成在字节/位之间的互相转换,以便以串行方式在差分信号线D+/D-上输出包或把串行的位流转换为包.
- 检测和产生 SOP (即每个包的同步字段头)和 EOP (包尾)电子信号.
- 最重要的一点: 它报告不同PID的包最终触发的结果(例如产生中断和回应IN/OUT事务中的握手), 或将解包后的"感兴趣的数据"例如SETUP令牌等放入指定端点的接收缓冲区内,并置位各种寄存器(例如中断状态寄存器,控制寄存器和每个端点独有的端点寄存器等)的标记,以便由你编写的C代码对其进行处理.
理解了最后一点,我们也就很容易理解如下几个经常被人提及的基本概念:
(1) USB设备不能拒收主机的令牌
(2) 端点的发送和接收即使处于熄火状态也不会影响到令牌的接收
(3) USB各种中断的产生机制:由SIE触发的结果
(4) 未收到令牌之前无缘无故地将发送端点置为有效,此时发送缓冲区的数据位流并不会出现在串行引脚D+/D-上,因为没有令牌,SIE就不会执行类似的并入串出的移位操作。同样对于接收端点而言,只有在收到令牌后,你的C代码才应该根据需要是否令接收端点置为有效。
总之,你一但启用了芯片的USB功能即等同于使能了SIE硬件外设, 这个外设就会一直工作并履行它的职责,至于你的端点状态如何设置应根据当前事务到达哪一个阶段而定.
USB通讯体系十分庞大,仅靠几篇博文就想把它说清楚是不可能的,唯有在工程实践中不断地学习和探索,并把其中的经验教训记录成文,温故知新,这对于提高自己的技术水平是大有益处的,也可对后来的初学者有所启示,这也是本作者写此博文的初心。
本系列博文到此结束,谢谢! (完成日期: 2021-03-22)