二、STM32处理器的USB接口 1、接口模块的内部结构
在书上有一个很好的USB内部接口模块内部结构图,比较好的解释了各个模块之间的关系,我这里试着用我自己的理解阐述一下吧。 首先在总线端(与D+、D-相连的那一端),通过模拟收发器与SIE连接。SIE使用48MHz的专用时钟。 与SIE相关的的有三大块:CPU内部控制、中断和端点控制寄存器,挂起定时器(这个好像是USB协议的要求,总线在一定时间内没有活动,SIE模块能够进入SUSPEND状态以节约电能),还有包缓冲区接口模块。 说到包缓冲区接口模块,这个对应的含义是,USB设备应该提供USB包缓冲区。这块缓冲区同时受到SIE和CPU核心的控制,用于CPU与SIE共享达到数据传输的目的。 所以CPU通过APB1总线接口访问,SIE通过包缓冲区接口模块访问,中间通过Arbiter来协调访问。
当然我们关注的中心点是控制、中断和端点控制寄存器。我们通过这些寄存器来获取总线传输的状态,控制各个端点的状态,并可以产生中断来让CPU处理当前的USB事件。 CPU可以通过APB1总线接口来访问这些寄存器。它们使用的都是PCLK1时钟。
2、USB模块的寄存器认识 (1)
(2) 这个寄存器主要是反映USB模块当前的状态的。第15-8为与控制寄存器的中断允许是意义对应的。相应的标志位置位,且中断未屏蔽,则向CPU发出对应的中断。
(3) USB设备地址寄存器 第7位,EF,USB模块允许位。如果EF=0,则USB模块将停止工作。 第6-0位。USB当前使用的地址。复位时为0.
(4)
(5) 首先有一个描述符表地址寄存器,指明了包缓冲区内端点描述符表的地址。 每一个端点都对应一个描述附表。描述符表也在包缓冲区内。每个端点寄存器对应的描述符表的地址可根据公式计算。 单缓冲、双向的端点描述符表有四项,每项占据两个字节:分别是端点n的发送缓冲区地址、发送字节数、接收缓冲区地址、接收字节数。
了解USB相关寄存器的知识以后,接下来就可以分析“JoyStickMouse”详细的工作过程了。
三、USB的“JoyStickMouse”工作过程详细分析 1、初始化过程叙述 从main()函数开始 (1)Set_System(void)的工作过程 由于这些代码都是采用库代码,所以我主要分析每个代码具体做了什么工作。有些常用、类似的代码这里就不列出来了。
先将RCC部分复位,系统使用内部振荡HSI,8MHz——RCC_DeInit();。 使能HSE——RCC_HSEConfig(RCC_HSE_ON); 设置HCLK = SYSCLK——RCC_HCLKConfig(RCC_SYSCLK_Div1); 设置PCLK2,PCLK1——RCC_PCLK2Config(RCC_HCLK_Div1);
设置PLL,使能PLL——PLL采用HSE,输出=HSE X 9; RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); 系统时钟采用PLL输出—— RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
使能PWR控制,目的是为了控制CPU的低功耗模式; 将所有输入口初始化为模拟输入——GPIO_AINConfig(); 使能USB上拉控制GPIO端口的时钟,这个端口设置为低电平时,USB外设会被集线器检测到,并报告给主机,这也是设备枚举的开始; 将这个端口的模式设置为开漏输出;
初始化上下左右四个按键为上下拉输入; 配置GPIOG8为EXTI8中断输入引脚,这个是在外部按键输入引起中断。 配置EXTI18中断。这个是发生USB唤醒事件时用。 (2)USB_Interrupts_Config(void)的工作过程 设置向量表位置在FLASH起始位置—— NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x00); 设置优先级分组,1位用于抢占组级别。其余用于子优先级—— NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); 接下来配置、使能了三个中断,包括USB低优先级中断、USB唤醒中断(EXTI18)、和EXTI8(按键控制)中断。 它的优先级设置有些问题,明明只有一位用于抢占优先级。它把EXTI8的抢占优先级设为2。结果在调试时发现,它的抢占优先级仍然是0。
(3)Set_USBClock()的工作过程 这个代码就两句话: 作用是设置并使能USB时钟,从RCC输出可以看到,USB时钟是48MHz。
(4)USB_Init()的工作过程 void USB_Init(void) { } 这个主要是初始化了三个全局结构体指针,pInformation表明当前连接的状态和信息,pProperty表明设备支持的方法,pUser_Standard_Requests是主机请求实现的函数指针数组。 Device_Info是一个结构体,包括11个成员变量。这里是将它的ControlState设为2,意义现在还不十分明了。 typedef struct _DEVICE_INFO {
}DEVICE_INFO;
最后调用pProperty->Init(),实质就是调用Joystick_init(void)。 在这个函数中,首先获取设备版本,并转换为Unicode存入版本号字符串。 设备当前配置置为0。然后调用PowerOn(),这个函数实质上将D+上拉,此时USB设备就能被集线器检测到了。因此分析进入下一个流程。
2、进入设备检测状态 (1)在PowerOn()中执行的情况。 在USB_init()中调用PowerOn(),而它先调用USB_Cable_Config(ENABLE),这个函数实质上将USB连接控制线设置为低电平,然后设备就可以检测到设备了。 当集线器报告设备连接状态,并收到主机指令后,会复位USB总线,这需要一定的时间(这段时间内设备应该准备好处理复位指令)。但是现在设备初始化程序将继续往下进行,因为它还没有使能复位中断。
这句话虽然将强制复位USB模块,但由于复位中断允许位没有使能,不会引起复位中断,而间接上由使PDWN=0,模块开始工作。 _SetCNTR是一个宏,将wRegVal赋值给CNTR寄存器,此时所有的中断被屏蔽。
再接下来两句指令又将清除复位信号。 然后清除所有的状态位。——_SetISTR(0); 接下来是很关键的两句话: 后面一个语句执行后,复位中断已经被允许,而此时集线器多半已经开始复位端口了。或者说稍微有限延迟,设备固件还能继续初始化一些部件,但已经不会影响整个工作流程了。 所以接下来,分析直接进入复位中断。 (2)复位中断的处理。 当复位中断允许、且总线被集线器复位的时候,固件程序进入USB_LP中断。 中断程序直接调用USB_Istr(void)程序。 接下来讲对中断位进行判断: (3)JoyStick_Reset()函数的处理。 这里将一句句来分析: void Joystick_Reset(void) {
}
复位中断执行完成后,开发板的USB接口能够以默认地址对主机来的数据包进行响应了。这个阶段的分析到此结束,下一个阶段就是正式分析代码实现的枚举过程了。 四、USB的“JoyStickMouse”工作过程详细分析 1、枚举第一步:获取设备的描述符 从USB_init()开始 (1)先要允许数据传输完成中断 在poweron()函数后面紧跟着几句话: //以上这两句话将允许所有的USB中断
(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主机,将触发正确传输完成中断,下面就进入中断看一看。
_SetISTR((u16)CLR_CTR); /*首先清除传输完成标志 */ EPindex = (u8)(wIstr & ISTR_EP_ID); //获取数据传输针对的端点号。
if (EPindex == 0)
(3)Setup0_Process()函数的执行分析 这个函数执行的时候,主机发来的请求数据包已经存在于RxADDR缓冲区了。大部分的标志位已经清除,除了SETUP位,这个味将由下一个令牌包自动清除。 进入处理函数:
pBuf.b = PMAAddr + (u8 *)(_GetEPRxAddr(ENDP0) * 2); //这是取得端点0接收缓冲区的起始地址。 PMAAddr是包缓冲区起始地址,_GetEPRxAddr(ENDP0)获得端点0描述符表里的接收缓冲区地址,为什么要乘以2呢?大概因为描述符表里地址项为16位,使用的是相对偏移。
(4)Data_Setup0()函数的执行分析
(5)DataStageIn()函数的执行分析 以下是主要执行代码:
Expect_Status_Out:
(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()完成剩下的工作。
(7)追踪进入函数In0_Process() 此时实际上设备返回描述符已经成功了。 这一次还是调用DataStageIn()函数,但是目的只是期待主机的0状态字节输出了。
(8)进入状态过程 主机收到18个字节的描述符后,进入状态事务过程,此过程的令牌包为OUT,字节数为0.只需要用户回一个ACK。 所以中断处理程序会进入Out0_Process()。 由于此时状态为WAIT_STATUS_OUT,所以执行以下这段。 获取设备描述符后,主机再一次复位设备。设备又进入初始状态。
上图很好地描述了枚举阶段“获取描述符”和“设置地址”两个阶段主机和设备数据交换的过程。 五、USB的“JoyStickMouse”工作过程详细分析 1、枚举第二步:设置地址 (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()。执行以下代码: 说明设置地址没有做任何工作。
它对应的宏是这样的: #define USB_StatusIn() Send0LengthData() //准备发送0字节数据 #define Send0LengthData() { _SetEPTxCount(ENDP0, 0); \
(3)设置地址的状态阶段:
而前面把状态设置为WAIT_STATUS_IN是给IN令牌包的处理提供指示。因为建立阶段结束以后,主机接着发一个IN令牌包,设备返回0字节数据包后,进入中断。 本次中断由IN0_Process()函数来处理,追踪进入,它执行以下代码: 执行设置地址操作、采用新地址后,把设备的状态改为STALLED。而在处理的出口中调用Post0_Process()函数,这个所做的工作是: 将端点0的发送和接收都设置为:STALL,这种状态下只接受SETUP令牌包。 2、枚举第三步:从新地址获取设备描述符 (1)上一阶段末尾的状态 端点0的发送和接收都设置为:STALL,只接收SETUP令牌包。 (2)建立阶段:主机发令牌包、数据包、设备ACK 产生数据接收中断,且端点0的SETUP置位,调用Setup0_Process()函数进行处理。 在Setup0_Process()中,因为主机发送了请求数据8个字节。由调用Data_Setup0()函数进行处理。首先是获取设备描述符的长度,描述符的起始地址,传送的最大字节数,根据这些参数确定本次能够传输的字节数,然后调用DataStageIn()函数进行实际的数据传输操作,设备描述符必须在本次中断中就写入发送缓冲区,因为很快就要进入数据阶段了。 在函数处理的最后: (3)数据阶段:主机发IN包,设备返回数据,主机ACK 本次操作会产生数据发送完成中断,由In0_Process(void)来处理中断,它也调用DataStageIn()函数来进行处理。 如果数据已经发送完: 有可能的话: 否则,继续准备数据,调整剩余字节数、发送指针位置,等待主机的下一个IN令牌包。 (4)状态阶段:主机发OUT包、0字节包,设备ACK 数据发送完成中断,调用Out0_Process(void)函数进行处理,由于在数据阶段的末尾已经设置设备状态为:WAIT_STATUS_OUT,所以处理函数基本上没有做什么事,就退出了。并将状态设为STALLED。
3、对配置描述符、字符串描述符获取过程进行简单跟踪,过程就不再一一叙述了。
4、主机设置配置。 建立阶段:主机发SETUP包、发请求数据包(DATA0包)、用户ACK。 进入CTR中断,用户调用Setup0_Process()函数进行处理,取得请求数据后,由于没有数据传输阶段,该函数调用NoData_Setup0()函数进行处理。 判断为设置配置后,调用Standard_SetInterface()函数将设备状态结构体的当前配置改为主机数据中的配置参数。同时调用用户的设置配置函数,将设备状态改为“configured”。 退出时,将控制传输状态改为:ControlState = WAIT_STATUS_IN,进入状态阶段。设备期待主机的IN令牌包,返回状态数据。 状态阶段:主机发IN令牌、设备返回0[size=12p]Setup0_Process()函数进行处理,取得请求数据后,由于没有数据传输阶段,该函数调用NoData_Setup0()函数进行处理。 设置空闲时一个类特殊请求,其特征码为0x21,2表示类请求而不是标准请求,1表示接收对象是接口而不是设备。 USB的底层并不支持类特殊请求,它将调用上层函数提供的函数:
6、主机获取报告描述符 建立阶段:主机发SETUP包、发请求数据包(DATA0包)、用户ACK。 进入CTR中断,获取描述符是一个标准请求,但是报告描述符并不是需要通用实现的,所以在底层函数中没有实现。跟踪Setup0_Process(void)——进入Data_Setup(void)函数,它是这么处理的: 可见核心函数只支持设备描述符、配置描述符以及字符串描述符。最终该函数将调用: 调用用户的类特殊实现来获取报告描述符,同时HID类描述符也是通过这种方式取得的。
7、主机从中断端点读取鼠标操作数据 主机会轮询设备,设备数据的准备在主函数中,用Joystick_Send(JoyState())函数来实现。 使能端点1的发送,当主机的IN令牌包来的时候,SIE将数据返回给主机。同时产生 CTR中断。 在中断处理程序中,执行下列代码: 这是在函数指针数组中调用函数,跟踪进入:发现这个函数什么也没有做。
经过对程序执行过程的跟踪和分析,我现在对USB设备HID类的工作有了大概的了解,对ST的USB库的工作也有了初步的概念。把所有文件的源代码粗略地浏览了一遍,心里大概有了些底。但现在我还不准备阅读源代码,我先把例程在智林开发板上移植好,再详细的阅读一遍源代码。 |
|