STM32的USB例程JoyStickMouse代码分析

模块九:mouse、key、joystick模块 mouse、key、joystick这三个模块分别用于管理鼠标、键盘、操纵杆。这三个模块的函数就是一些控制鼠标、键盘、操纵杆的函数。下面分别介绍一下各个模块的函数。 mouse模块中的函数: 函数 描述 pygame.mouse.get_pressed 获取鼠标按钮的状态 pygame.mouse.get_pos 获取鼠标光标的位置 pygame.mouse.get_rel 获取鼠标移动量 pygame.mouse.set_pos 设置鼠标光标位置 pygame.mouse. 阅读详情
一、USB的“JoyStickMouse”例程结构分析
1、例程的结构
(1)底层结构
包括5个文件:usb_core.c(USB总线数据处理的核心文件),usb_init.c,usb_int.c(用于端点数据输入输入中断处理),usb_mem.c(用于缓冲区操作),usb_regs.c(用于寄存器操作)。它们都包含了头文件“usb_lib.h”。在这个头文件中,又有以下定义:
#include "usb_type.h"
#include "usb_regs.h"
#include "usb_def.h"
#include "usb_core.h"
#include "usb_init.h"
#include "usb_mem.h"
#include "usb_int.h"
usb_lib.h中又包含了七个头文件,其中usb_type.h中主要是用typedef为stm32支持的数据类型取一些新的名称。usb_def.h中主要是定义一些相关的数据类型。
还有一个未包含在usb_lib.h中的头文件,usb_conf.h用于USB设备的配置。
(2)上层结构
上层结构总共5个文件:hw_config.c(用于USB硬件配置)、usb_pwr.c(用于USB连接、断开操作)、usb_istr.c(直接处理USB中断)、usb_prop.c(用于上层协议处理,比如HID协议,大容量存储设备协议)、usb_desc.c(具体设备的相关描述符定义和处理)。
可见,ST的USB操作库结构十分清晰明了,我先不准备直接阅读源代码。而是先利用MDK的软件模拟器仿真执行,先了解一下设备初始化的流程。
2、设备初始化所做的工作
(1)Set_System(void)
这个是main函数中首先调用的函数,它位于hw_config.c文件中。它的主要功能是初始化时钟系统、使能相关的外围设备电源。
配置了JoyStickMouse所用到的5个按键,并且配置了两个EXTI中断,一个是用于把USB从挂起模式唤醒,还有一个用途未知。
(2)USB_Interrupts_Config();
这个是main函数中调用的第二个函数,它也位于hw_config.c文件中。主要功能是配置USB所用到的中断。
跟踪到代码中,主要设配置了USB低优先级中断和唤醒中断,又有一个EXTI中断功能未知。
(3)Set_USBClock()
这个是main函数中调用的第三个函数,它也位于hw_config.c文件中。它的功能是配置和使能USB时钟。
(4)USB_Init(void)
这个是main函数中调用的第四个函数,它也位于usb_init.c文件中。它初始化了三个全局指针,指向DEVICE_INFO、USER_STANDARD_REQUESTS和DEVICE_PROP结构体。
后面两个是函数指针结构体,里面都是USB请求实现、功能实现的函数指针。
void USB_Init(void)
{

pInformation = &Device_Info;

pInformation->ControlState = 2;


pProperty = &Device_Property;


pUser_Standard_Requests = &User_Standard_Requests;

/* Initialize devices one by one */


pProperty->Init();
}
这三个结构体都是与具体设备枚举和功能实现相关的,定义在usb_prop.c和usb_desc.c文件中。
DEVICE_PROP Device_Property =

{

Joystick_init,

Joystick_Reset,

Joystick_Status_In,

Joystick_Status_Out,

Joystick_Data_Setup,

Joystick_NoData_Setup,

Joystick_Get_Interface_Setting,

Joystick_GetDeviceDescriptor,

Joystick_GetConfigDescriptor,

Joystick_GetStringDescriptor,

0,

0x40 /*MAX PACKET SIZE*/

};
USER_STANDARD_REQUESTS User_Standard_Requests =

{

Joystick_GetConfiguration,

Joystick_SetConfiguration,

Joystick_GetInterface,

Joystick_SetInterface,

Joystick_GetStatus,

Joystick_ClearFeature,

Joystick_SetEndPointFeature,

Joystick_SetDeviceFeature,

Joystick_SetDeviceAddress

};
Usb_init()函数调用pProperty->Init()(实质上就是Joystick_init)完成设备的初始化。
上层程序调用下次函数是常规性的操作。而下层函数(usb_init相对于usb_prop是输入底层操作文件)调用上层文件函数我们称之为回调。
回调函数的意义在于同一种操作模式、提供不同的回调函数则可以实现不同的功能。Windows中处理消息,好像也用到了这种模式。
回调函数的实现方法是函数指针数组。这是指针的高级应用。
这是函数的代码:
void Joystick_init(void)
{/* Update the serial number string descriptor with the data from the unique
ID*/


Get_SerialNum();
//
获取设备序列号,转变为unicode字符串

pInformation->Current_Configuration = 0;

/* Connect the device */

PowerOn();
//连接USB设备,实质是能让主机检测到了。

/* USB interrupts initialization */

_SetISTR(0);
/* clear pending interrupts */

wInterrupt_Mask = IMR_MSK;

_SetCNTR(wInterrupt_Mask); /* set interrupts mask */

bDeviceState = UNCONNECTED;
}
实质上,代码执行到这里,开发板已经可以响应主机发来的数据了。但我还是先把main()函数的代码看完吧。
(5)SysTick_Config();
这个函数调用主要是为程序中用到的精确延时作配置。
3、进入主循环
进入主循环的工作就两个:
Joystick_Send(JoyState())。
JoyState()用来获取按键的状态。
Joystick_Send(JoyState())用来把按键状态发到主机。当然这里真正的发送工作并不是由该代码完成的。它的工作只是将数据写入IN端点缓冲区,主机的IN令牌包来的时候,SIE负责把它返回给主机。
主要代码如下:

UserToPMABufferCopy(Mouse_Buffer, GetEPTxAddr(ENDP1), 4);
//
从用户复制四个字节到端点1缓冲区,控制端点的输入缓冲区。

SetEPTxValid(ENDP1); /* enable endpoint for transmission */
4、中断处理过程大致理解
(1)usb_istr()函数中的中断处理简单分析
有用的代码大概以下几段,首先是处理复位的代码,调用设备结构中的复位处理函数。

wIstr = _GetISTR();

if (wIstr & ISTR_RESET & wInterrupt_Mask)

{

_SetISTR((u16)CLR_RESET); //
清复位中断


Device_Property.Reset();

}
处理唤醒的代码:

if (wIstr & ISTR_WKUP & wInterrupt_Mask)

{

_SetISTR((u16)CLR_WKUP);

Resume(RESUME_EXTERNAL);

}
处理总线挂起的代码:

if (wIstr & ISTR_SUSP & wInterrupt_Mask)

{

if (fSuspendEnabled) /* check if SUSPEND is possible */

{

Suspend();

}

else

{

/* if not possible then resume after xx ms */


Resume(RESUME_LATER);

}

/* clear of the ISTR bit must be done after setting of CNTR_FSUSP */

_SetISTR((u16)CLR_SUSP);

}
处理端点传输完成的代码,这段是最重要的,它调用底层usb_int.c()文件中的CTR_LP()函数来处理端点数据传输完成中断。

if (wIstr & ISTR_CTR & wInterrupt_Mask)

{

CTR_LP(); /* servicing of the endpoint correct transfer interrupt */

}


二、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)
控制寄存器CNTR
传输完成中断允许位。CTRM,1有效,如果SIE置位传输完成标志,则相应的数据传输完成中断发生。
第15位
包缓冲区溢出中断允许位
错误中断允许位
唤醒中断允许位。WKUPM。1有效,如果唤醒请求标志位置位,则产生唤醒中断。
挂起中断允许位。SUSPM,1有效,当总线挂起标志置位时,发生挂起中断。
复位中断允许位。RESETM。1有效,软件强制复位和总线复位信号,都能触发复位中断。
帧首中断允许位
期望帧首中断允许位。ESOFM。它的含义是没有收到帧首信号,允许发生中断。
第8位
向主机发送的唤醒请求,RESUME。1有效,主机收到该信号,将唤醒设备。这个由软件置位。
第4位
强制挂起控制,FSUSP。1有效。与由于总线无活动引起挂起的效果相同。
低功耗模式。前提是先进入挂起状态。由软件设置,一般又硬件复位(被唤醒后自动清零)。
断电模式控制位。PDWN。此位为1时,USB模块关闭。
强制复位控制。FRES。与总线上的复位信号产生相同的效果。也能产生复位中断.
第0位。
(2)
中断状态寄存器ISTR
这个寄存器主要是反映USB模块当前的状态的。第15-8为与控制寄存器的中断允许是意义对应的。相应的标志位置位,且中断未屏蔽,则向CPU发出对应的中断。
CTR标志,数据传输完成后硬件置1.
PMAOVR标志
ERR标志
WKUP请求,总线检测到主机唤醒请求时由硬件置位。
SUSP请求标志位。
RESET请求标志位。
SOF帧首标志
ESOF,期待帧首标志。
DIR传输方向,此位由硬件控制。IN时为0,OUT为1.
第4位。
发生数据传输的端点的地址。
(3) USB设备地址寄存器
第7位,EF,USB模块允许位。如果EF=0,则USB模块将停止工作。
第6-0位。USB当前使用的地址。复位时为0.
(4)
端点状态和配置寄存器,8个寄存器,支持8个双向端点和16个单向端点。
CTR_RX,正确接收标志位。
第15位。
DTOG_RX,用于检测的数据翻转位。一般由硬件自动设置,软件写1可使其手动翻转。
STAT_RX,占据两位。
00表示该端点不可用,无回应。
01表示响应STALL
10响应NAK
11表示端点有效,可接收数据。
SETUP标志。收到SETUP令牌包时置位。用户收到数据后需检查次位。
第11位。
EP_TYPE,两位,表示端点类型。
00表示批量端点。
01表示控制端点
10表示等时端点。
11表示中断端点。
EP_KIND,端点特殊类型。在EP_TYPE=01时,表示设备期望主机的0字节状态包。
CTR_TX。
正确发送标志。主机的IN包之后。
第7位。
DTOG_TX,用于检测的数据翻转位。一般由硬件自动设置,软件写1可使其手动翻转。
STAT_TX,占据两位。
00表示该端点不可用,无回应。
01表示响应STALL
10响应NAK
11表示端点有效,可发送数据。
端点地址:EA【3:0】,表明该寄存器对应的端点号码。比如1、2号寄存器都可以对应端点1(在双缓冲情况下)。
第3-0位。
    
(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唤醒事件时用。

EXTI_InitStructure.EXTI_Line = EXTI_Line18; // USB resume from suspend mode

EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;

EXTI_InitStructure.EXTI_LineCmd = ENABLE;

EXTI_Init(&EXTI_InitStructure);
(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()的工作过程
这个代码就两句话:

RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5);

RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB, ENABLE);
作用是设置并使能USB时钟,从RCC输出可以看到,USB时钟是48MHz。
(4)USB_Init()的工作过程
void USB_Init(void)
{

pInformation = &Device_Info;

pInformation->ControlState = 2;

pProperty = &Device_Property;
//
这个是设备本身支持的属性和方法

pUser_Standard_Requests = &User_Standard_Requests; //
这个是主机请求的实现方法。


pProperty->Init();

/
/回调设备的初始化例程。
}
这个主要是初始化了三个全局结构体指针,pInformation表明当前连接的状态和信息,pProperty表明设备支持的方法,pUser_Standard_Requests是主机请求实现的函数指针数组。
Device_Info是一个结构体,包括11个成员变量。这里是将它的ControlState设为2,意义现在还不十分明了。
typedef struct _DEVICE_INFO
{

u8 USBbmRequestType;
/* bmRequestType */

u8 USBbRequest;
/* bRequest */

u16_u8 USBwValues;
/* wValue */

u16_u8 USBwIndexs;
/* wIndex */

u16_u8 USBwLengths;
/* wLength */

u8 ControlState;
/* of type CONTROL_STATE */

u8 Current_Feature;

u8 Current_Configuration;
/* Selected configuration */

u8 Current_Interface;
/* Selected interface of current configuration */

u8 Current_AlternateSetting;/* Selected Alternate Setting of current

interface*/

ENDPOINT_INFO Ctrl_Info;
//
端点信息结构体
}DEVICE_INFO;
最后调用pProperty->Init(),实质就是调用Joystick_init(void)。
在这个函数中,首先获取设备版本,并转换为Unicode存入版本号字符串。

——Get_SerialNum();
设备当前配置置为0。然后调用PowerOn(),这个函数实质上将D+上拉,此时USB设备就能被集线器检测到了。因此分析进入下一个流程。
2、进入设备检测状态
(1)在PowerOn()中执行的情况。
在USB_init()中调用PowerOn(),而它先调用USB_Cable_Config(ENABLE),这个函数实质上将USB连接控制线设置为低电平,然后设备就可以检测到设备了。
当集线器报告设备连接状态,并收到主机指令后,会复位USB总线,这需要一定的时间(这段时间内设备应该准备好处理复位指令)。但是现在设备初始化程序将继续往下进行,因为它还没有使能复位中断。

wRegVal = CNTR_FRES;

_SetCNTR(wRegVal);
//
这句话实际上使能了USB模块的电源,因为上电复位时,CNTR寄存器的断电控制为PDWN位是1,模块是断电的。
这句话虽然将强制复位USB模块,但由于复位中断允许位没有使能,不会引起复位中断,而间接上由使PDWN=0,模块开始工作。
_SetCNTR是一个宏,将wRegVal赋值给CNTR寄存器,此时所有的中断被屏蔽。
再接下来两句指令又将清除复位信号。
然后清除所有的状态位。——_SetISTR(0);
接下来是很关键的两句话:

wInterrupt_Mask=
CNTR_RESETM| CNTR_SUSPM | CNTR_WKUPM;

_SetCNTR(wInterrupt_Mask);
后面一个语句执行后,复位中断已经被允许,而此时集线器多半已经开始复位端口了。或者说稍微有限延迟,设备固件还能继续初始化一些部件,但已经不会影响整个工作流程了。
所以接下来,分析直接进入复位中断。
(2)复位中断的处理。
当复位中断允许、且总线被集线器复位的时候,固件程序进入USB_LP中断。
中断程序直接调用USB_Istr(void)程序。
接下来讲对中断位进行判断:

if (wIstr & ISTR_RESET & wInterrupt_Mask)

{

_SetISTR((u16)CLR_RESET);
//
先清除复位中断位


Device_Property.Reset();
//
进入设备定义的复位过程。实际上是调用JoyStick_Reset()函数进行处理。

}
(3)JoyStick_Reset()函数的处理。
这里将一句句来分析:
void Joystick_Reset(void)
{

pInformation->Current_Configuration = 0;
//
当前配置为0

pInformation->Current_Interface = 0;/
当前接口为0

pInformation->Current_Feature = Joystick_ConfigDescriptor[7];


//
需要总线供电

SetBTABLE(BTABLE_ADDRESS);
//设置包缓冲区地址。

SetEPType(ENDP0, EP_CONTROL);
//
端点0为控制端点

SetEPTxStatus(ENDP0, EP_TX_STALL);
//
端点状态为发送无效,也就是主机IN令牌包来的时候,回送一个STALL。


SetEPRxAddr(ENDP0, ENDP0_RXADDR); //
设置端点0描述符表,包括接收缓冲区地址、最大允许接收的字节数、发送缓冲区地址三个量。

SetEPTxAddr(ENDP0, ENDP0_TXADDR); //
这是发送缓冲区地址

Clear_Status_Out(ENDP0);
//
清除EP_KIND的STATUS_OUT位,如果改位被设置,在控制模式下只对0字节数据包相应。其它的都返回STALL。主要用于控制传输的状态过程。

SetEPRxCount(ENDP0, Device_Property.MaxPacketSize); //
接收缓冲区支持64个字节。

SetEPRxValid(ENDP0);
//
使能端点0的接收,因为很快就要接收SETUP令牌包后面跟着的数据包了。

SetEPType(ENDP1, EP_INTERRUPT);
/
/端点1为中断端点。

SetEPTxAddr(ENDP1, ENDP1_TXADDR); //
设置发送缓冲区地址。

SetEPTxCount(ENDP1, 4);
//
每次发送四个字节

SetEPRxStatus(ENDP1, EP_RX_DIS);
//
接收禁止,只发送Mouse信息,而不从主机接收。

SetEPTxStatus(ENDP1, EP_TX_NAK); /
/现在发送端点还不允许发送数据。

bDeviceState = ATTACHED;
//
连接状态改为已经连接,默认地址状态。

SetDeviceAddress(0); //
地址默认为0.
}
复位中断执行完成后,开发板的USB接口能够以默认地址对主机来的数据包进行响应了。这个阶段的分析到此结束,下一个阶段就是正式分析代码实现的枚举过程了。


四、USB的“JoyStickMouse”工作过程详细分析
1、枚举第一步:获取设备的描述符
从USB_init()开始
(1)先要允许数据传输完成中断
在poweron()函数后面紧跟着几句话:


PowerOn();
//
这句执行完,设备被主机检测到,并且能够响应复位中断了。

_SetISTR(0);
/* clear pending interrupts */


wInterrupt_Mask = IMR_MSK;

_SetCNTR(wInterrupt_Mask); /* set interrupts 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主机,将触发正确传输完成中断,下面就进入中断看一看。
_SetISTR((u16)CLR_CTR); /*首先清除传输完成标志 */
EPindex = (u8)(wIstr & ISTR_EP_ID); //获取数据传输针对的端点号。
if (EPindex == 0)
//
如果是端点0,这里的确是端点0

{

SaveRState = _GetEPRxStatus(ENDP0); //
保存端点0状态,原本是有效状态。

SaveTState = _GetEPTxStatus(ENDP0);

_SetEPRxStatus(ENDP0, EP_RX_NAK); //
在本次数据处理好之前,对主机发来的数据包以NAK回应

_SetEPTxStatus(ENDP0, EP_TX_NAK);

if ((wIstr & ISTR_DIR) == 0) //
如果是IN令牌,数据被取走

{

_ClearEP_CTR_TX(ENDP0);

In0_Process();
//
调用该程序处理固件数据输出后的工作。

_SetEPRxStatus(ENDP0, SaveRState);

_SetEPTxStatus(ENDP0, SaveTState);

return;

}

Else
//
DIR=1时,要么是SETUP包,要么是OUT包。

{
//
这里先分析SETUP包。

wEPVal = _GetENDPOINT(ENDP0);
//
获取整个端点0状态

if ((wEPVal & EP_CTR_TX) != 0)
//
这种情况一般不太可能,

{
//
如果出现表示同时 TX和RX 同时置位。

}

else if ((wEPVal &EP_SETUP) != 0)
//我们的程序会执行到这里

{

_ClearEP_CTR_RX(ENDP0);


Setup0_Process();

//
主要是调用该程序来处理主机请求。

_SetEPRxStatus(ENDP0, SaveRState);

_SetEPTxStatus(ENDP0, SaveTState);


return;

}

else if ((wEPVal & EP_CTR_RX) != 0) //
暂时不执行的代码先删除掉。

{

}

}

}/* if(EPindex == 0) */


后面处理其他端点的代码就先不看了。

}/* while(...) */
(3)Setup0_Process()函数的执行分析
这个函数执行的时候,主机发来的请求数据包已经存在于RxADDR缓冲区了。大部分的标志位已经清除,除了SETUP位,这个味将由下一个令牌包自动清除。
进入处理函数:
pBuf.b = PMAAddr + (u8 *)(_GetEPRxAddr(ENDP0) * 2); //这是取得端点0接收缓冲区的起始地址。
PMAAddr是包缓冲区起始地址,_GetEPRxAddr(ENDP0)获得端点0描述符表里的接收缓冲区地址,为什么要乘以2呢?大概因为描述符表里地址项为16位,使用的是相对偏移。

if (pInformation->ControlState != PAUSE)

{

pInformation->USBbmRequestType = *pBuf.b++; //
请求类型,表明方向和接收对象(设备、接口还是端点)此时为80,表明设备到主机

pInformation->USBbRequest = *pBuf.b++; /*
请求代码,第一次时应该是6,表明主机要获取设备描述符。 */

pBuf.w++;

pInformation->USBwValue = ByteSwap(*pBuf.w++); /* wValue */

pBuf.w++;
//我觉得这里可能有些问题。

pInformation->USBwIndex
= ByteSwap(*pBuf.w++); /* wIndex */

pBuf.w++;

pInformation->USBwLength = *pBuf.w; /* wLength */

}

pInformation->ControlState = SETTING_UP;

if (pInformation->USBwLength == 0)

{

NoData_Setup0();


}

else

{

Data_Setup0();
//
这次是有数据传输的,所以有进入该该函数。

}

return Post0_Process();
(4)Data_Setup0()函数的执行分析

CopyRoutine = NULL; //
这是一个函数指针,由用户提供。

wOffset = 0;

if (Request_No == GET_DESCRIPTOR) /
/如果是获取设备描述符

{

if(Type_Recipient==(STANDARD_REQUEST| EVICE_RECIPIENT))

{

u8 wValue1 = pInformation->USBwValue1;

if (wValue1 == DEVICE_DESCRIPTOR)

{


CopyRoutine = pProperty->GetDeviceDescriptor;

} //
获取设备描述符的操作由用户提供。


if (CopyRoutine)

{

pInformation->Ctrl_Info.Usb_wOffset = wOffset;

pInformation->Ctrl_Info.CopyData = CopyRoutine;


(*CopyRoutine)(0); //
这个函数这里调用的目的只是设置了pInformation中需要写入的描述符的长度。

Result = USB_SUCCESS;

}

if (ValBit(pInformation->
USBbmRequestType, 7))
//
此时为80

{
//上面这个语句主要是判断传输方向。如果为1,则是设备到主机

vu32 wLength = pInformation->USBwLength;
这个一般是64

if (pInformation->
Ctrl_Info.Usb_wLength > wLength)

{
//
设备描述符长度18

pInformation->Ctrl_Info.Usb_wLength = wLength;

}

//
有些细节暂时先放着

pInformation->Ctrl_Info.PacketSize = pProperty->MaxPacketSize;


DataStageIn();
//
最主要是调用这个函数完成描述符的输出准备

}
(5)DataStageIn()函数的执行分析
以下是主要执行代码:

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();/* 这个实际上是使接收也有效,主机可取消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()完成剩下的工作。
(7)追踪进入函数In0_Process()
此时实际上设备返回描述符已经成功了。
这一次还是调用DataStageIn()函数,但是目的只是期待主机的0状态字节输出了。

if ((ControlState == IN_DATA) || (
ControlState == LAST_IN_DATA))

{
第一次取设备描述符只取一次。

DataStageIn();
/
/此次调用后,当前状态变成WAIT_STATUS_OUT,表明设备等待状态过程,主机输出0字节。

/* ControlState may be changed outside the function */

ControlState = pInformation->ControlState;

}
返回时调用Post0_Process(void)函数,这个函数没做什么事。
(8)进入状态过程
主机收到18个字节的描述符后,进入状态事务过程,此过程的令牌包为OUT,字节数为0.只需要用户回一个ACK。
所以中断处理程序会进入Out0_Process()。
由于此时状态为WAIT_STATUS_OUT,所以执行以下这段。

else if (ControlState ==
WAIT_STATUS_OUT)

{

(*pProperty->Process_Status_OUT)();
//
这是个空函数,什么也不做。


ControlState = STALLED;
//
状态转为STALLED。

}
获取设备描述符后,主机再一次复位设备。设备又进入初始状态。


五、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()。执行以下代码:

else if (RequestNo == SET_ADDRESS)

{

Result = USB_SUCCESS;

}
说明设置地址没有做任何工作。

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

}
(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()函数,这个所做的工作是:

SetEPRxCount(ENDP0, Device_Property.MaxPacketSize);
//
将端点0的缓冲区大小设置为64字节

if (
pInformation->ControlState == STALLED)

{

vSetEPRxStatus(EP_RX_STALL);

vSetEPTxStatus(EP_TX_STALL);

}
将端点0的发送和接收都设置为:STALL,这种状态下只接受SETUP令牌包。
2、枚举第三步:从新地址获取设备描述符
(1)上一阶段末尾的状态
端点0的发送和接收都设置为:STALL,只接收SETUP令牌包。
(2)建立阶段:主机发令牌包、数据包、设备ACK
产生数据接收中断,且端点0的SETUP置位,调用Setup0_Process()函数进行处理。
在Setup0_Process()中,因为主机发送了请求数据8个字节。由调用Data_Setup0()函数进行处理。首先是获取设备描述符的长度,描述符的起始地址,传送的最大字节数,根据这些参数确定本次能够传输的字节数,然后调用DataStageIn()函数进行实际的数据传输操作,设备描述符必须在本次中断中就写入发送缓冲区,因为很快就要进入数据阶段了。
在函数处理的最后:

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_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的底层并不支持类特殊请求,它将调用上层函数提供的函数:

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。
进入CTR中断,获取描述符是一个标准请求,但是报告描述符并不是需要通用实现的,所以在底层函数中没有实现。跟踪Setup0_Process(void)——进入Data_Setup(void)函数,它是这么处理的:

if
(Request_No == GET_DESCRIPTOR)

{

if(Type_Recipient==(STANDARD_REQUEST| EVICE_RECIPIENT))

{

u8 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 */

}

}
可见核心函数只支持设备描述符、配置描述符以及字符串描述符。最终该函数将调用:

Result= (*pProperty->Class_Data_Setup)(pInformation->USBbRequest);
调用用户的类特殊实现来获取报告描述符,同时HID类描述符也是通过这种方式取得的。
7、主机从中断端点读取鼠标操作数据
主机会轮询设备,设备数据的准备在主函数中,用Joystick_Send(JoyState())函数来实现。

Mouse_Buffer[1] = X;

Mouse_Buffer[2] = Y;

/*copy mouse position info in ENDP1 Tx Packet Memory Area*/

UserToPMABufferCopy(Mouse_Buffer, GetEPTxAddr(ENDP1), 4);

/* enable endpoint for transmission */

SetEPTxValid(ENDP1);
使能端点1的发送,当主机的IN令牌包来的时候,SIE将数据返回给主机。同时产生 CTR中断。
在中断处理程序中,执行下列代码:

if ((wEPVal & EP_CTR_TX) != 0)

{

/* clear int flag */

_ClearEP_CTR_TX(EPindex);

(*pEpInt_IN[EPindex-1])();

} /* if((wEPVal & EP_CTR_TX) != 0) */
这是在函数指针数组中调用函数,跟踪进入:发现这个函数什么也没有做。
经过对程序执行过程的跟踪和分析,我现在对USB设备HID类的工作有了大概的了解,对ST的USB库的工作也有了初步的概念。把所有文件的源代码粗略地浏览了一遍,心里大概有了些底。但现在我还不准备阅读源代码,我先把例程在智林开发板上移植好,再详细的阅读一遍源代码。

04 STM32F103 USB固件开发-移植JoyStickMouse-(3)(USB描述符详解) USB描述符结构如下(图片引用自USB中文网):**设备描述符:**主要向主机说明设备类型、USB版本号、PID、VID等,是主机读取的第一个配置,一个设备能有有一个。**配置描述符:**一个设备可以有一个或多个,描述设备的供电方式、最大功耗、接口数量。配置描述符在USB设备的枚举过程中,需要获取两次:第一次只获取配置描述符的基本长度9字节,获取后从wTotalLength字节中解析出配置描述符的总长度,然后再次获取全部的描述符。 阅读详情

相关推荐

STM32 USB joyStick详解

STM32 USB库例程JOYSTICK源码详解

stm32读取驾驶模拟器数据 stm32F407读取joystick数据

实习工作,老板要求用单片机读取驾驶模拟器返回的数据,驾驶模拟器usb输出,输出信息包括:方向盘转角、左右拨杆、按键等。

renzemingcsdn的博客 3651

[最新]stm32官方usb例程joystick详解.doc

[最新]stm32官方usb例程joystick详解.doc

JoystickMouse

我只是USB新手,我只是想叙述一下我如何 将JoystickMouse例程 改为 USB虚拟键盘的过程,以及一些仍未解决的疑惑。 我昨天补了1天的USB知识,对USB的枚举,描述符间的关系,控制传输有个大致清晰了解,但是并不深刻。(为此,我决定随后开始从PDIUSB12芯片开始学习起走) 首先,我们先打开JoystickMouse的例程,先下载进单片机看看,以确保单片机和USB之间的通信是

* . Benirus的专栏 4368

unity | 动画模块之滚动选项框

动画模块之滚动选项框

菌菌兹的博客 2074

手把手教你用Verilog实现AT24C64的I2C读写(附完整仿真流程)

本文详细介绍了如何使用Verilog HDL设计AT24C64 EEPROM的I2C控制器,并配合ModelSim完成完整的仿真验证流程。内容涵盖I2C协议解析、Verilog状态机设计、AT24C64读写操作实现,以及仿真波形分析和调试技巧,为FPGA开发者提供了一套完整的EEPROM控制解决方案。

3c4x5z6v7b的博客 844

STM32的USB例程JoyStickMouse代码分析(全)

一、USB的“JoyStickMouse”例程结构分析 1、例程的结构 (1)底层结构 包括5个文件:usb_core.c(USB总线数据处理的核心文件),usb_init.c,usb_int.c(用于端点数据输入输入中断处理),usb_mem.c(用于缓冲区操作),usb_regs.c(用于寄存器操作)。它们都包含了头文件“usb_lib.h”。在这个头文件中,又有以下定义: #include...

674

Android音频学习(十四)——加载音频设备

loadHwModule 是 Android 音频系统的核心初始化函数,负责加载音频硬件抽象层(HAL)的模块实现。它建立 AudioPolicyService 与底层硬件的桥梁,将配置文件(audio_policy_config.xml)中的标签<module>定义转化为可操作的硬件对象,加载的是音频硬件模块(HAL实现),每个模块对应一种音频设备类型,以动态库的形式存在,负责与底层音频硬件交互。

miemie2014的博客 1731

STM32的USB例程JoyStickMouse代码分析(全)--转载网友"追风"帖子

一、USB的“JoyStickMouse”例程结构分析 1、例程的结构 (1)底层结构 包括5个文件:usb_core.c(USB总线数据处理的核心文件),usb_init.c,usb_int.c(用于端点数据输入输入中断处理),usb_mem.c(用于缓冲区操作),usb_regs.c(用于寄存器操作)。它们都包含了头文件“usb_lib.h”。在这个头文件中,又有以下定义: #incl

不见不念 6688

Windows系统离线搭建PyTorch深度学习环境全攻略(CUDA+cudnn实战)

本文提供了一份在Windows系统下离线搭建PyTorch深度学习环境的完整实战指南。文章详细讲解了如何在没有网络连接的情况下,正确安装和配置CUDA、cudnn以及PyTorch,并构建独立的Python虚拟环境。重点强调了版本依赖关系的重要性,并提供了从驱动准备到环境验证的每一步操作,旨在帮助用户高效、稳定地部署可复用的GPU深度学习开发环境。

table的专栏 578

接口电路设计的关键要点

接口电路设计是嵌入式硬件开发的核心环节,本文系统阐述了五大关键设计要点:1)电平匹配技术,详细分析了3.3V/5V系统互连方案及转换电路计算;2)阻抗匹配原理,包括高速信号反射机制及差分信号处理;3)滤波与去耦设计,提出多级电容配置和"先电容后过孔"布局原则;4)接地设计规范,重点说明模拟/数字地分离及高速信号回流路径;5)防护电路设计,涵盖防反接、过流保护及ESD防护方案。文章通过具体计算公式、器件选型指南和布局技巧,为接口电路设计提供了系统化的解决方案,并附有设计检查清单和典型问题分

weiyvyy的博客 453

STM32的USB例程JoyStickMouse改成自定义HID设备

简介 USB HID类是USB设备的一个标准设备类,包括的设备非常多。HID类设备定义它属于人机交互操作的设备,用于控制计算机操作的一些方面,如USB鼠标、USB键盘、USB游戏操纵杆等。但HID设备类不一定要有人机接口,只要符合HID类别规范的设备都是HID设备。 USB HID设备的一个好处就是操作系统自带了HID类的驱动程序,而用户无需去开发驱动程序,只要使用API系统调用即可完成通信。 S...

chiqingjiao7503的博客 4262

高级技能-高可用和容灾-两地三中心、异地多活架构

两地三中心和异地多活是当前主流的跨地域高可用架构方案。两地三中心通过生产中心、同城灾备和异地灾备的组合,提供多级容灾能力,适合对数据安全要求高的行业。异地多活则采用单元化设计,多个站点同时服务,提升资源利用率,但需解决网络延迟、数据冲突等挑战。两种架构在RTO/RPO、一致性、改造成本等方面各具特点,企业应根据业务规模、一致性要求和预算进行选择,可考虑从同城双活起步,逐步向异地多活演进。混合架构可能是平衡容灾与多活的务实方案。

zhongjianfeng100的博客 168

STM32的USB程序JoyStickMouse源码分析

https://my.oschina.net/u/184090/blog/467826 一、USB的“JoyStickMouse”例程结构分析 1、例程的结构 (1)底层结构   包括5个文件:usb_core.c(USB总线数据处理的核心文件),usb_init.c,usb_int.c(用于端点数据输入输入中断处理),usb_mem.c(用于缓冲区操作),usb_regs.c(用于寄存器操作)。它们都包含了头文件“usb_lib.h”。在这个头文件中,又有以下定义: ...

oshan2012的博客 1129

2026年运维工程师常见面试选择题

这篇文章总结了30道Linux运维面试常见问题及解析,涵盖基础命令、系统监控、故障排查、数据库、缓存、容器编排等多个方面。主要内容包括: 基础命令使用:如df查看磁盘、free查看内存、top监控进程等 系统监控与故障排查:负载判断、端口检查、进程管理等 中间件相关:Nginx状态码、MySQL主从复制、Redis缓存问题 容器技术:Docker优势、Kubernetes作用 运维理念:高可用、监控告警、发布策略、备份容灾 工作原则:故障处理优先级、运维工程师核心能力 文章不仅提供标准答案,还解析了各命令的

不务正业的猿 191

OpenCloudOS9操作系统搭建Confluence8.2.1+Jira企业级WIKI

Atlassian Confluence是一个专业的企业知识管理与协同软件,可用于构建企业wiki

背锅神童的博客 1021

STM32 JoystickMouse USB游戏杆鼠标的实现

本次程序的实现是基于上次CustomHID修稿过来的,工程的架构就不在介绍,这里主要介绍下,如何修改。 首先当然是usb_desc.c的一些描述符了。 设备描述符需要修改下bMaxPacketSize(最大包长度)为0x08个字节,因为该工程通讯的长度为4字节,根据USB洗衣规范,最大包长度只能是8、16、64等,所以这里选择0x08,这里要注意,改为0x08时,在usb_prop.c

* . Benirus的专栏 7409

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

02 STM32F103 USB固件开发-移植JoyStickMouse(1)

下载的固件库是:en.stsw-stm32121.zip。

qq_45575942的博客 419

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

USB的“JoyStickMouse”例程分析——学习笔记(1)__目录

USB的“JoyStickMouse”例程分析——学习笔记(1)__目录

sz189981的博客 1739

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

USB的“JoyStickMouse”例程分析——学习笔记(2)__例程结构分析+芯片的接口

USB的“JoyStickMouse”例程分析——学习笔记(2)__例程结构分析+芯片的接口

sz189981的博客 1579
上一篇: 如何修改STM32的USB例程为自己所用
下一篇: STM32 USB-HID通信移植步骤
獠
獠
博客等级 码龄16年 43粉丝 6原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值