嵌入式USB主机设计(硬件设计和软件设计)

2023-06-06  

嵌入式USB主机硬件设计
选用廉价的51系列单片机(89C52)控制USB主机接口芯片SL811HS,同时,通过MAX232芯片与PC机通信。硬件接线示意图如下所示:

嵌入式USB主机软件设计
下面将从底层到高层详细地介绍本系统的软件设计。

4.2.1单片机读写SL811HS

4.2.1.1读取SL811HS内存的数据
根据SL811HS的读写时序要求,读取数据前首先发送地址:
void SetHostAddress(char AddressP)
{
 P_BUS=AddressP;/*数据总线发送地址,但此时地址还不会被SL811HS接收*/
 P_CTRL=0x90;/*设置总线控制信号为SL811HS接收地址信号,具体含义如下所示:*/
   /*P_CTRL  ;0x90, 98, F0
   P_CTRL.0  ;  -  - -
   P_CTRL.1  ;  -  - -
   P_CTRL.2  ;  -  - -
   A0    ;  0  1 0
   nRST   ;  1  1 1
   nCS    ;  0  0 1
   nWR   ;  0  0 1
   nRD    ;  1  1 1 */
 nWR=1;
 nCS=1; /*地址传输完毕后,关闭片选、写等信号*/
 P_CTRL=0xF0;
}
地址发送完毕之后,SL811HS就接到了需要读取的内存单元地址(包括寄存器的地址)。紧接着单片机就可以读取数据:
unsigned char HostRead(void)
{
 A0=1; /* 满足SL811HS的时序要求,先保证A0和nCS的有效*/
 NCS=0;
 P_CTRL=0x58; /*设置控制位信号,读取SL811HS内的数据*/
   /*P_CTRL  ;0x90, 58, F0
   P_CTRL.0  ;  -  - -
   P_CTRL.1  ;  -  - -
   P_CTRL.2  ;  -  - -
   A0    ;  0  1 0
   nRST   ;  1  1 1
   nCS    ;  0  0 1
   nWR   ;  0  1 1
   nRD    ;  1  0 1 */
 return P_BUS; /*函数返回读取的SL811HS内存的数据*/
}

4.2.1.2写入数据到SL811HS内存
与读数据类似,单片机要往SL811HS的内存单元写数据时,也要首先发送地址,然后再发送要写入的数据。为了简便起见,写数据过程中发送地址和数据的功能都放在一个函数中执行。
Void HostWrite(char HostWriteAddress, char WriteConstent)
{ /*参数HostWriteAddress输入要写入数据的SL811HS内存的地址,WriteConstent为要写入的地址*/
 P_BUS=HostWriteAddress; /*准备好需要发送的地址*/
 P_CTRL=0x90;
   /*P_CTRL  ;0x90, 98, F0
   P_CTRL.0  ;  -  - -
   P_CTRL.1  ;  -  - -
   P_CTRL.2  ;  -  - -
   A0    ;  0  1 0
   nRST   ;  1  1 1
   nCS    ;  0  0 1
   nWR   ;  0  0 1
   nRD    ;  1  1 1 */
 nWR=1;
 nCS=1;
 P_BUS=WriteContent; /*准备好需要发送的数据*/
 P_CTRL=0x98; /*重新安排好控制信号,发送数据*/
 P_CTRL=0xF4;
}

4.2.1.3读写批量数据
有时单片机和SL811HS之间要进行批量数据的传输,为方便操作,设计了能够批量进行数据读或写的函数:
/*批量写*/
void HostBulkWrite(char addr, unsigned char *s, char c)
{ /*参数addr为SL811HS中写入数据的起始地址,*s为单片机内存放的需要写入的数据缓冲区,c为总共要写入的字节数*/
 if(c<=0) return;
 while(c--)
 {
  HostWrite(addr++,*s++);
 }
}
/*批量读*/
void HostBulkRead(char addr, unsigned char *s, char c)
{ /*参数addr为SL811HS中读取数据的起始地址,*s为单片机内存放读取来的数据的数据缓冲区,c为总共要读入的字节数*/
 if(c<=0) return;
 while(c--)
 {
  SetHostAddress(addr++);
  *s++=HostRead();
 }
}

4.2.2阶段USB传输的实现

4.2.2.1 SL811HS的初始化
初始化主要是对SL811HS的部分内部寄存器进行设置:
Void SL811HS_Init(void)

 HostWrite(IntEna,0x20);
 HostWrite(CSOFcnt, 0xAE);
HostWrite(CtrlReg, 0x08);
HostWrite(CtrlReg, 0x00);
 HostWrite(CSOFcnt, 0xAE);
HostWrite(CtrlReg, 0x08);
DelayMs(10);
HostWrite(CtrlReg, 0x00);
DelayMs(1);
HostWrite(IntStatus, 0xFF);
}

4.2.2.2三种阶段USB传输的实现
三种阶段USB传输都可以由这个函数(下称“阶段传输实现函数”)实现:
void USB_Transaction(unsigned char PID, unsigned char EP_Address, unsigned char Address, int Length, char *pDataBuf);

4.2.2.2.1发送或接收前的准备工作

4.2.2.2.1.1设置EP0Status寄存器
本设计涉及到了多种阶段的USB传输,但这里需要考虑的只有3种,分别是建立(SETUP)、数据输入(IN)和数据输出(OUT)阶段。阶段传输实现函数的输入参数中,PID就是用来区别这3种传输阶段。
#define PID_SETUP 0x2D
#define PID_IN 0x69
#define PID_OUT 0xE1
阶段传输实现函数的第二个需要输入的参数就是端点号EP_Address,大小为1字节(实际只有低4位有效,高4位为0),类型为unsigned char 。U盘等类似的USB Mass Storage类设备一般具有3个端点:一个是端点0,用于处理控制传输;另一个是批量输出Bulk_OUT端点,该端点用于接收主机发来的批量数据,端点号有设备定义;还有一个就是批量输入Bulk_IN端点,用于给÷向主机发送批量数据,其端点号也由设备定义。
从硬件角度来讲,程序需要把PID和EP_Address组合在一起后写入EP0Status寄存器。
unsigned char PID_EPA; /*用于储存PIN和EP_Address的组合值*/
PID_EPA=PID&0x0F; /*PID的高4位位校验码,低4位为有效值*/
PID_EPA=(PID_EPA<<4)+EP_Address; /*按照EP0Status寄存器的要求合并PID和EP_Address*/
HostWrite(EP0Status,PID_EPA);

4.2.2.2.2设置EP0Counter寄存器
阶段传输实现函数的第三个需要输入的参数是设备的地址Address,大小为1字节,类型为unsigned char。
HostWrite(EP0Counter,Address);/*设备地址的D7位值为0,D6~D0位代表地址*/

4.2.2.2.3设置EP0XferLen寄存器
第四个参数是发送或接收的数据的长度Length,大小为2字节,类型为int。这个长度还需要和相应端点的最大包尺寸MaxPacketSize进行比较。如果Length小于MaxPacketSize,就说明需要发送或接收的数据长度比相应端点的最大包尺寸还小,因此,主机和该端点之间只要进行一次数据传输就可以实现数据的发送或接收。反之,如果Length大于MaxPacketSize,那么就需要将发送或接收的数据进行分割,第一批发送或接收的数据长度就是MaxPacketSize,剩下的数据就利用 SL811HS的“乒乓”机制进行发送。最后需要把实际要发送的数据长度写入SL811HS的EP0XferLen寄存器中。
/*定义变量CurentLength,用于保存当前需要发送的数据长度*/
if(Length>MaxPacketSize)
{
 CurrentLength=MaxPacketSize;
}
else
 CurrentLength=Length;
HostWrith(EP0XferLen,(unsigned char)CurrentLength);/*注意这里的变量类型转换*/

4.2.2.2.4设置EP0Address寄存器
最后一个需要确定是发送或接收数据的缓冲地址*pDataBuf,大小为1字节。
在这里pDataBuf是指向单片机内存单元的指针,但实际读写数据是要以SL811HS的数据缓冲区作为中介的。
为加快数据传输,把SL811HS的数据缓冲区分成两部分:SL811HS_Buf0和SL811HS_Buf1。SL811HS_Buf0的起始地址就可以定为0x10。而SL811HS_Buf1的地址就根据端点最大包尺寸进行调整。

Unsigned char SL811_HS_Buf0 =0x10, SL811HS_Buf1;
If(Length>MaxPackeSize)
{
 SL811HS_Buf1= SL811HS_Buf0+MaxPacketSize;
}
HostWrite(EP0Address, SL811HS_Buf0);/*当前数据发送从SL811HS_Buf 0开始*/
如果主机要发送数据给设备,就需要把*pDataBuf中的数据复制到SL811HS的数据缓冲区中:
HostBulkWrite(SL811HS_Buf0,pDataBuf,CurrentLength);
如果是主机接收数据,那么在以下的处理中,就会把SL811HS缓冲区中的接收到的设备的数据通过HostBulkRead()函数复制到单片机的缓冲区中。
HostBulkRead(SL811HS_Buf0,pDataBuf,CurrentLength);

4.2.2.2.5启动发送或接收
启动USB数据的发送或接收实际上是通过向SL811HS的EP0Control寄存器发送命令字CmdWord来实现的。
首先,PID等参数的不同,CmdWord的值也不同,根据EP0Control寄存器每一位的属性,有如下配置程序:
unsigned char CmdWord;
if(PID==PID_SETUP)
{
 CmdWord=0x03; /*控制传输的SETUP事务*/
}
else
{
 if(EP_Address==0)
 {
  if(PID==PID_IN)
  {
   CmdWord=0x47; /*控制传输的输入IN事务*/
  }
  else
  {
   CmdWord=0x43; /*控制传输的OUT事务*/
  }
 }
 else
 {
  if(PID==PID_IN)
  {
   CmdWord=0x07; /*批量传输IN事务*/
  }
  else
  {
   CmdWord=0x03; /*批量传输OUT事务*/
  }
 }
}
将CmdWord命令字发送到SL811HS的EP0Control寄存器后,就启动了数据包的发送或接收了:
HostWrite(IntStatus, 0xFF); /*清除中断状态位*/
HostWrite(EP0Control, CmdWord);
剩下的工作就是查询SL811HS的IntStatus寄存器,以查看发送或接收的完成情况,有需要时,最后还可以查看EP0status获取握手包的有关信息,但其实所有的握手包信息都是有硬件自动完成的。

4.2.3事务USB传输的实现

4.2.3.1控制传输
包含了三个阶段:建立阶段、可选数据阶段以及状态阶段。
Void Control_Transfer(pRequestCMD RequestCMD, unsigned char* pDataBuf_x);

4.2.3.1.1建立阶段的实现
任务就是发送建立的8字节请求命令,命令的数据结构为(注意该段定义是放在Control_Transfer()函数之外的):
typedef struct{
 unsigned char bmRequest Type;
 unsigned char bRequest;
 unsigned int wValue;
 unsigned int wIndex;
 unsigned int wLength;
}REQUESTCMD,*pRequestCMD;
在这里只需调用一次USB_Transaction()函数即可:
USB_Transaction(PID_SETUP, 0 , Device_Address, 0x08, (char *)RequestCMD);
/*发送的令牌为PID_SETUP,端点号为0,设备地址为Device_Address,发送数据长度为8字节,发送内容为相应的请求命令*/

4.2.3.1.2可选数据阶段的实现
注意数据传输方向,实现过程如下:
if(RequestCMD->wLength)
{
 if(RequestCMD->bmRequestType & 0x80) /*判断为PID_OUT*/
 {
  USB_Transaction(PID_OUT, 0, Device_Address, RequestCMD->wLength, pDataBuf_x);
 }
 else /*判断为PID_IN*/
 {
  USB_Transaction(PID_IN, 0, Device_Address, RequestCMD->wLength, pDataBuf_x);
 }
}

4.2.3.1.3状态信息阶段的实现
在需要时(如可选数据阶段为IN),主机发送控制传输的状态信息:
USB_Transaction(PID_OUT, 0, Device_Address, 0, pDataBuf_x);

4.2.3.2批量传输
类似与控制传输中的可选数据阶段,有两个函数,分别对应于批量传输IN和批量传输OUT:
void Bulk_Transfer_IN(int Length_bi, unsigned char* pDataBuf_bi)
{
 USB_Transaction(PID_IN,EP_Bulk_IN,Device_Address, Length_bi, pDataBuf_bi);
}
void Bulk_Transfer_OUT(int Length_bo, unsigned char* pDataBuf_bo)
{
 USB_Transaction(PID_OUT,EP_Bulk_OUT,Device_Address, Length_bi, pDataBuf_bo);
}

4.2.4 USB设备枚举的实现
现在,各种枚举所需的传输实现函数已经好了,要实现USB请求动作进而实现设备枚举是很容易的了。下面是两个典型请求动作的实现函数:
/*获取描述符请求命令*/
void Get_Descriptor(unsigned int wValue_d, unsigned char Length_d, unsigned char * pDataBuf_d)
{
 REQUESTCMD RequestCMD; /*建立该请求命令的结构*/
 RequestCMD.bmRequestType=0x80; /*填入该请求命令的内容*/
 RequestCMD.bRequest=GET_DESCRIPTOR;
 RequestCMD.wValue=wValue_d;
 RequestCMD.wIndex=0;
 RequestCMD.wLength=Length_d;
 Control_Transfer(&RequstCMD,pDataBuf_d);
}

/*设置设备地址请求命令*/
void Set_Address(unsigned int Device_address_e)
{
 REQUESTCMD RequestCMD; /*建立该请求命令的结构*/
 RequestCMD.bmRequestType=0x00; /*填入该请求命令的内容*/
 RequestCMD.bRequest=SET_ADDRESS;
 RequestCMD.wValue=w Device_address_e;
 RequestCMD.wIndex=0;
 RequestCMD.wLength=0;
 Control_Transfer(&RequstCMD,0);
}


文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。