在 Arduino Due 上获取 USB 网络摄像头视频流 - 第五部分:获取标准 USB 设备描述符






4.75/5 (6投票s)
获取第一个 USB 设备描述符
引言
第 4 部分 在这里。
在本文中,我将展示如何与 USB 设备通信并从中获取标准 USB 设备描述符。在此过程中,我将解释中断驱动技术来链接函数调用。最后,将以人类可读的格式显示接收到的设备描述符。
注意:本文内容不仅适用于摄像头,也适用于所有 USB 设备,如鼠标、键盘,任何您拥有的设备。阅读各种设备的描述符应该很有趣,因为它能让我们对事物的工作原理有更低层次的理解。
控制传输
控制传输发生在控制管道(pipe)上,管道编号为零。设备连接并复位后(上一篇文章),它就可以接收发送到其默认端点(编号零)的标准请求。控制管道是主机和设备零端点之间的通信通道。控制管道通信称为控制传输。USB 中的所有传输都分阶段进行。一次控制传输包含 2 或 3 个阶段:SETUP、DATA 和 HANDSHAKE。DATA 阶段并非总是存在,因为某些传输包含 SETUP 阶段所需的所有必要信息。一个阶段包含一个或多个事务,特别是 SETUP 和 HANDSHAKE 阶段总是只有一个事务,而 DATA 阶段可以有多个。例如,控制管道的最大数据尺寸设置为 64 字节,这意味着如果设备需要发送 100 字节,则无法一次事务完成,因此首先发送 64 字节,然后第二个事务发送剩余的 36 字节。
在本文中,我将获取一个远小于 64 字节的描述符,因此 DATA 阶段将只包含一个事务。让我们看看控制传输的流程。
红色矩形是事务,红色矩形内的黑色矩形是数据包。深色数据包是主机发送的,白色数据包是设备发送的。每个数据包都有明确定义的结构,基本上它们是一组或多组通过电线串行发送的数字。
控制传输序列讨论
请注意,每个阶段都由主机启动。SETUP、IN 和 OUT 数据包定义了设备地址和端点地址,当设备刚刚连接和复位时,这两个地址都为零。SETUP 阶段的 DATA0 数据包定义了我们要执行的操作,换句话说,请求哪个描述符。一旦设备收到 DATA0 数据包,它会发出 ACK 以表示成功接收。如果设备没有回传 ACK,则说明有问题,主机应在一段时间后尝试重新开始发送 SETUP 事务。
一旦 SETUP 事务完成,主机将等待一定时间以允许设备处理请求。然后主机发出 IN 数据包并等待 DATA0 数据包。请注意,如果我们有多个 IN 事务,第二个数据包将是 DATA1,第三个是 DATA0,依此类推。主机收到 DATA0 后,它会确认 DATA 事务。DATA0/DATA1 的切换称为数据切换,SAM3X 会自动处理,它知道何时从 DATA0 或 DATA1 开始并跟踪切换过程。连续两个相同的 DATA0 或 DATA1 数据包表示错误,可能是数据包丢失。
DATA 阶段完成后,主机开始 HANDSHAKE 阶段。规则如下:主机必须发出一个与 DATA 阶段方向相反的空 IN 或 OUT 事务。在我们的例子中,DATA 阶段是 IN,所以我发出空(DATA1 数据包中无数据)的 OUT 事务。请注意,根据 USB 规范,此处使用了 DATA1 数据包。
数据包格式讨论
上述每个数据包根据 USB 规范进一步拆分。SETUP、IN 和 OUT 的格式如下
所有 PID 都在 **[2, p.196]** 的表 8-1 中指定。因此,SETUP 是 0b1101,OUT 是 0b0001,依此类推。CRC 是循环冗余校验,由 SAM3X 自动计算。ADDR 和 ENDP 是设备地址和端点编号,在我们的情况下均为零。DATA0/1 数据包的格式如下
对于我们的控制传输,DATA 限制为 64 字节。PID 是 DATA0 的 0b0011,DATA1 的 0b1011。CRC 由 SAM3X 自动计算。
Ack 数据包仅包含一个 8 位字段,PID 为 0b0010。
请注意,所有 PID 都是 8 位字段,前 4 位是 PID,后 4 位是前 4 位的一补码,用于错误检查,因为 CRC 不包括 PID 字段。
那么最终的控制传输看起来是这样的
我用黄色标记了这些数据块,我们每次都需要用新事务显式填充它们。
在这张图片上,您可以看到实际通过物理线路传输的是什么。最后要说明的一点是,USB 使用 LSB(最低有效位)优先顺序,这意味着最低有效位先发送。这对我们来说是件好事,因为 ARM 处理器也使用 LSB 顺序。
SETUP 阶段 DATA0 数据包
SETUP 阶段 DATA0 数据包的数据(黄色标记)将包含我们的请求信息,即获取标准 USB 设备描述符的标准请求。此类请求具有定义的结构
让我们定义标准值,以便填充请求结构。为此,向项目中添加新文件 USB_Specification.h。第一个字段 bmRequestType 由三个信息组成,它们占据不同的位字段。这些是方向、请求类型和请求接收者。
//USB request data transfer direction (bmRequestType) #define USB_REQ_DIR_OUT (0<<7) //Host to device #define USB_REQ_DIR_IN (1<<7) //Device to host //USB request types (bmRequestType) #define USB_REQ_TYPE_STANDARD (0<<5) //Standard request #define USB_REQ_TYPE_CLASS (1<<5) //Class-specific request #define USB_REQ_TYPE_VENDOR (2<<5) //Vendor-specific request //USB recipient codes (bmRequestType) #define USB_REQ_RECIP_DEVICE (0<<0) //Recipient device #define USB_REQ_RECIP_INTERFACE (1<<0) //Recipient interface #define USB_REQ_RECIP_ENDPOINT (2<<0) //Recipient endpoint #define USB_REQ_RECIP_OTHER (3<<0) //Recipient other
在本文中,方向将是从设备到主机,因为我想从设备中获取信息。类型将是标准的,之后我还会使用类特定请求(UVC 请求),并且永远不会使用供应商特定请求。接收者将是设备。稍后当我深入研究视频功能时,我将使用接口和端点。
**[2, p.251]** 表 9-4 中的值用于填充 bRequest 字段
//Standard USB requests (bRequest)
#define USB_REQ_GET_STATUS 0
#define USB_REQ_CLEAR_FEATURE 1
#define USB_REQ_SET_FEATURE 3
#define USB_REQ_SET_ADDRESS 5
#define USB_REQ_GET_DESCRIPTOR 6
#define USB_REQ_SET_DESCRIPTOR 7
#define USB_REQ_GET_CONFIGURATION 8
#define USB_REQ_SET_CONFIGURATION 9
#define USB_REQ_GET_INTERFACE 10
#define USB_REQ_SET_INTERFACE 11
#define USB_REQ_SYNCH_FRAME 12
在本文中,我将使用 USB_REQ_GET_DESCRIPTOR 来获取标准 USB 设备描述符。
wValue 字段的值取决于上面两个字段指定的内容。在本文的情况下,此字段将包含描述符类型和描述符索引。设备描述符的索引始终为零,而类型是
#define USB_DT_DEVICE 1
#define USB_DT_CONFIGURATION 2
#define USB_DT_STRING 3
#define USB_DT_INTERFACE 4
#define USB_DT_ENDPOINT 5
#define USB_DT_DEVICE_QUALIFIER 6
#define USB_DT_OTHER_SPEED_CONFIGURATION 7
#define USB_DT_INTERFACE_POWER 8
#define USB_DT_OTG 9
#define USB_DT_IAD 0x0B
#define USB_DT_BOS 0x0F
#define USB_DT_DEVICE_CAPABILITY 0x10
我将使用 USB_DT_DEVICE 作为标准 USB 设备描述符。此值放入 wValue 字段的高字节,索引放入低字节。
wIndex 字段的值始终为零(它用于获取各种语言的易读字符串描述)。
wLength 指定返回多少字节。标准 USB 设备描述符为 18 字节。
现在所有必要的数据都已为标准请求定义,让我们定义请求本身
//SETUP request
typedef struct
{
uint8_t bmRequestType;
uint8_t bRequest;
uint16_t wValue;
uint16_t wIndex;
uint16_t wLength;
} usb_setup_req_t;
DATA 阶段 DATA0 数据包 - 设备描述符
我们已经理解了 SETUP 阶段。在下一个 DATA 阶段,我们将从设备获得响应。黄色块将用标准设备描述符填充。
设备描述符提供有关整个设备的通用信息,并且只有一种此类描述符。下表提供了简短的字段说明
让我们定义这个描述符
//Standard USB device descriptor structure
typedef struct
{
uint8_t bLength;
uint8_t bDescriptorType;
uint16_t bcdUSB;
uint8_t bDeviceClass;
uint8_t bDeviceSubClass;
uint8_t bDeviceProtocol;
uint8_t bMaxPacketSize0;
uint16_t idVendor;
uint16_t idProduct;
uint16_t bcdDevice;
uint8_t iManufacturer;
uint8_t iProduct;
uint8_t iSerialNumber;
uint8_t bNumConfigurations;
} usb_dev_desc_t;
编写 HCD
此时,我已经定义了所有必要的数据结构,因此是时候编写一些代码了。有几个问题需要解决
- SAM3X 的 USB 模块需要额外的初始化。我只进行了通用初始化,但 USB 通信需要建立(控制)管道,因此需要进行管道初始化。
- 管道通信会引发中断,由于只有一个中断处理程序(一个入口点),因此需要代码来确定中断的管道编号。
- 如上所述,控制传输有一个明确定义的序列。传输中有阶段、事务和数据包。因此,主机必须始终了解传输在哪个点。换句话说,它必须知道当前是哪个阶段,哪个阶段内的哪个事务,以及哪个事务内的哪个数据包。为了解决这个问题,我将在 UHC.c 文件中创建一个附加的控制结构,其中包含管理控制传输所需的所有字段。
- 当 USB 处于 HS(高速)模式时,所有事情都应在由 SOF(帧起始)数据包分隔的微帧内完成。当 USB 总线激活(未挂起)时,SAM3X 会自动生成这些数据包。因此,我们应该知道 SOF 何时发生。这也能让我们创建延迟,为连接的设备提供处理请求的时间。微帧每 125us 发生一次,这意味着每 8 个 SOF 构成 1ms。我将使用此来指定延迟。
在解决上述问题之后,将具备编写中断驱动传输序列所需的所有基础设施。
一旦接收到描述符,我将使用先前编写的打印函数打印它。代码将分为两组文件:HCD.h/.c(低级)和 USBD.h/.c(高级)。高级函数将向接收到的描述符提供请求和接收的缓冲区指针。低级函数将接收请求,遍历所有阶段、事务、数据包,直到获取(或失败)请求的数据,并向高级函数指示传输完成。函数指针将用于实现这一点。
管道初始化
SAM3X 的 UOTGHS 模块有 10 个可用管道,从逻辑上讲,我将管道 0 作为控制管道(实际上,根据 SAM3X 规范 **[1, p.1069]**,管道 0 只能是控制管道)。
管道初始化包括管道激活、地址设置和管道中断启用。
根据 **[1, p.1098]** 的图形算法,管道激活被描述。首先需要启用管道,然后指定其参数。控制管道的中断请求频率无关紧要,因为它不是中断管道。端点编号始终为 0,类型为控制管道,令牌为 SETUP,大小为 64(这是 SAM3X 管道 0 的最大尺寸,参见 **[1, p.1069]**。值 3 对应 64 字节,参见 **[1, p.1181]**),银行数量为 1(对于视频流我将使用两个,一个用于接收数据,另一个用于读取,然后它们会切换)。
如果以上所有参数都正确指定并且管道可以被激活,则 HSTPIPISR0(主机管道和中断状态寄存器零)的 CFGOK(配置 OK)位将设置为 1。如果激活失败,我会在 RS-232 端口上打印错误消息并禁用管道。
由于管道是 USB 设备地址和端点编号的组合,因此还需要指定管道的 USB 设备地址。管道 0 的 USB 设备地址位于寄存器 HSTADDR1 的位 [0-6] 中。
最后一步是启用管道中断。在初始化函数中,我将启用 STALL 中断。STALL 是一种握手。到目前为止,我们熟悉 ACK,它表示“一切正常”,而 STALL 表示设备无法理解请求。例如,如果我们弄错了请求字段的值,我们将收到 STALL 令牌而不是 ACK。我还会启用管道错误中断,当管道数据出现问题时会触发此中断,然后我将启用全局管道 0 中断以使 STALL 和错误中断正常工作。
管道初始化被打包到一个函数中,该函数有一个参数:USB 设备地址。这是因为设备地址在枚举过程中会发生变化,因此一旦设置了设备地址,我将不得不重新调整它。
uint32_t HCD_InitiatePipeZero(uint8_t Address)
{
UOTGHS->UOTGHS_HSTPIP |= UOTGHS_HSTPIP_PEN0; //Enable pipe 0
//Pipe activation:
//Single-bank pipe
UOTGHS->UOTGHS_HSTPIPCFG[0] |= (UOTGHS_HSTPIPCFG_PBK_1_BANK & UOTGHS_HSTPIPCFG_PBK_Msk);
//64 bytes pipe (3 corresponds to 64 bytes)
UOTGHS->UOTGHS_HSTPIPCFG[0] |= ((3 << UOTGHS_HSTPIPCFG_PSIZE_Pos) & UOTGHS_HSTPIPCFG_PSIZE_Msk);
//Pipe token is SETUP
UOTGHS->UOTGHS_HSTPIPCFG[0] |= (UOTGHS_HSTPIPCFG_PTOKEN_SETUP & UOTGHS_HSTPIPCFG_PTOKEN_Msk);
//Pipe type of Pipe 0 is always CONTROL
UOTGHS->UOTGHS_HSTPIPCFG[0] |= (UOTGHS_HSTPIPCFG_PTYPE_CTRL & UOTGHS_HSTPIPCFG_PTYPE_Msk);
//Pipe endpoint number is zero
UOTGHS->UOTGHS_HSTPIPCFG[0] |= ((0 << UOTGHS_HSTPIPCFG_PEPNUM_Pos) & UOTGHS_HSTPIPCFG_PEPNUM_Msk);
//Pipe bank autoswitch is off for control pipe
UOTGHS->UOTGHS_HSTPIPCFG[0] &= ~UOTGHS_HSTPIPCFG_AUTOSW;
//Memory allocation
UOTGHS->UOTGHS_HSTPIPCFG[0] |= UOTGHS_HSTPIPCFG_ALLOC;
//Check if pipe was allocated OK
if(0 == (UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_CFGOK))
{
PrintStr("Pipe 0 has not been activated.\r\n");
UOTGHS->UOTGHS_HSTPIP &= ~UOTGHS_HSTPIP_PEN0; //Disable pipe 0
return 0;
}
//Pipe's USB device address
UOTGHS->UOTGHS_HSTADDR1 = (UOTGHS->UOTGHS_HSTADDR1 & ~UOTGHS_HSTADDR1_HSTADDRP0_Msk)
| (Address & UOTGHS_HSTADDR1_HSTADDRP0_Msk);
//Enables stall interrupt on pipe 0
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_RXSTALLDES;
//Enables pipe 0 data bank error interrupts
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_PERRES;
//Enables global pipe 0 interrupts
UOTGHS->UOTGHS_HSTIER = UOTGHS_HSTIER_PEP_0;
PrintStr("Pipe 0 has been initialized successfully.\r\n");
return 1;
}
在 HCD.c 文件的顶部添加函数声明,它不会从外部使用
#include "HCD.h"
#include "UART.h"
uint32_t HCD_InitiatePipeZero(uint8_t Address);
//...
管道 0 中断
要查看哪个管道中断被触发,应该检查 UOTGHS_HSTISR(主机全局中断状态寄存器)寄存器的 PEP_0、PEP_1、... PEP_9 位。我将使用 **if** 语句执行此操作,而且我知道在此应用程序中我将只使用管道 0(控制)和管道 1(流),因此我将仅提供两个管道的代码,并将首先测试管道 1,因为在此低处理能力(相对于视频流任务)的微控制器中,流是时间关键的。
uint8_t HCD_GetPipeInterruptNumber(void)
{
if(UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_PEP_1)
return 1;
if(UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_PEP_0)
return 0;
PrintStr("Unsupported interrupt number.\r\n");
return 100;
}
将 HCD_GetPipeInterruptNumber 声明放在 HCD_InitiatePipeZero 声明下方,这个声明也不会从外部使用。
现在让我们回顾 UOTGHS 中断处理程序(UOTGHS_Handler)。到目前为止,它区分了设备连接、断开连接、总线电源更改、总线电源错误和 SOF 中断。处理程序已经很长了,为了提高可读性,我将把管道 0 中断处理程序放在单独的函数 HCD_HandleControlPipeInterrupt 中,并在管道号 0 中断发生后调用它。
#include "HCD.h"
#include "UART.h"
uint32_t HCD_InitiatePipeZero(uint8_t Address);
void HCD_HandleControlPipeInterrupt(void);
//...
void UOTGHS_Handler()
{
//Manage SOF interrupt
if (0 != (UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_HSOFI))
{
UOTGHS->UOTGHS_HSTICR = UOTGHS_HSTICR_HSOFIC; //Ack SOF
return;
}
//Getting pipe interrupt number
uint8_t PipeInterruptNumber = HCD_GetPipeInterruptNumber();
if(PipeInterruptNumber == 0)
{
HCD_HandleControlPipeInterrupt(); //Pipe 0 interrupt
return;
}
//...
void HCD_HandleControlPipeInterrupt(void)
{
//STALL interrupt
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_RXSTALLDI)
{
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_RXSTALLDIC; //Ack STALL interrupt
return;
}
//Pipe error interrupt
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_PERRI)
{
//Getting error
uint32_t error = UOTGHS->UOTGHS_HSTPIPERR[0] &
(UOTGHS_HSTPIPERR_DATATGL | UOTGHS_HSTPIPERR_TIMEOUT
| UOTGHS_HSTPIPERR_PID | UOTGHS_HSTPIPERR_DATAPID);
UOTGHS->UOTGHS_HSTPIPERR[0] = 0UL; //Ack all errors
switch(error)
{
case UOTGHS_HSTPIPERR_DATATGL:
PrintStr("UOTGHS_HSTPIPERR_DATATGL\r\n");
break;
case UOTGHS_HSTPIPERR_TIMEOUT:
PrintStr("UOTGHS_HSTPIPERR_TIMEOUT\r\n");
break;
case UOTGHS_HSTPIPERR_DATAPID:
PrintStr("UOTGHS_HSTPIPERR_DATAPID\r\n");
break;
case UOTGHS_HSTPIPERR_PID:
PrintStr("UOTGHS_HSTPIPERR_PID\r\n");
break;
default:
PrintStr("UHD_TRANS_PIDFAILURE\r\n");
}
}
PrintStr("Uncaught Pipe 0 interrupt.\r\n");
}
顶部显示了位于 .c 文件中的声明。中间部分显示了先前创建的全局 UOTGHS 中断处理程序。我将 SOF 中断移到顶部,因为这将是最频繁的中断,并在其下方放置了管道中断号确定和对 HCD_HandleControlPipeInterrupt 的调用。为了清晰起见,其余的处理程序未显示。最后一部分是控制管道中断处理函数。到目前为止,它处理管道 0 上的 STALL 和一些错误。我放置了带有可能错误的 **switch** 语句,但我没有试图理解它们的含义。目前,它们的发生很重要,而不是它们的含义,因为如果一切都做得正确,应该没有任何错误中断。
控制结构
控制结构将根据方向跟踪控制传输阶段
//Definitions to store control transfer state machine
typedef enum
{
USB_CTRL_REQ_STAGE_SETUP = 0,
USB_CTRL_REQ_STAGE_DATA_OUT = 1,
USB_CTRL_REQ_STAGE_DATA_IN = 2,
USB_CTRL_REQ_STAGE_ZLP_IN = 3,
USB_CTRL_REQ_STAGE_ZLP_OUT = 4,
} hcd_ControlRequestStageType;
本文使用 SETUP、DATA_IN 和 ZLP_OUT。ZLP 表示零长度数据包,因此 ZLP_IN 和 ZLP_OUT 属于 HANDSHAKE 阶段。DATA_OUT 和 ZLP_IN 将在下一篇文章中使用。
除了上述 **enum** 外,控制结构还将存储 SOF 计数(每 1 毫秒提取一次),方向信息(IN 或 OUT),接收/发送缓冲区指针,已接收/发送的总字节数,接收/发送缓冲区索引以记住具有多个事务的数据阶段中的位置,毫秒延迟以延迟函数调用,以及三个函数指针:USB 设备复位后调用的函数,控制传输开始时(延迟结束后)调用的函数,以及控制传输完成后调用的函数。
typedef struct
{
uint8_t SOFCount; //To count every 1ms
uint8_t Direction;
hcd_ControlRequestStageType ControlRequestStage; //Current stage of control transaction
uint8_t *ptrBuffer; //Receive/send buffer
uint8_t ByteCount; //Receive/send data size
uint16_t Index; //Current position in *ptrBuffer
uint16_t Pause; //Over how many ms to call TransferStart or ResetEnd
void (*ResetEnd)(void); //Called after reset happened
void (*TransferStart)(void); //Called after Pause elapsed
void (*TransferEnd)(uint16_t); //Called when transfer is finished
} hcd_ControlStructureType;
这两个结构都在 HCD.h 文件中定义,hcd_ControlStructureType 类型变量的声明在 HCD.c 文件中。
#include "HCD.h"
#include "UART.h"
hcd_ControlStructureType hcd_ControlStructure;
//...
请注意函数指针的用法,如果您不了解它们,请阅读它们。
SOF 处理程序
SOF 处理程序将计算延迟。一旦延迟结束,如果已分配,将调用 TransferStart 函数。延迟以毫秒为单位。还将有可能立即启动传输,绕过 SOF 处理程序。让我们创建它。
void HCD_HandleSOFInterrupt(void)
{
hcd_ControlStructure.SOFCount++; //This happens every 0.125ms
if(8 == hcd_ControlStructure.SOFCount)
{
hcd_ControlStructure.SOFCount = 0; //This happens every 1ms
if(0 < hcd_ControlStructure.Pause) //If pause was specified
{
hcd_ControlStructure.Pause--;
if(0 == hcd_ControlStructure.Pause)
{
if(0 != hcd_ControlStructure.TransferStart)
{
(*hcd_ControlStructure.TransferStart)();
return;
}
}
}
}
}
将此函数的声明添加到 HCD.c 文件中,因为它不会从外部使用。
根据 **[2, p.243]**,复位后的 USB 设备枚举过程必须在不晚于 100ms 时开始。让我们修改全局 USB 中断处理程序以包含 SOF 中断处理程序、最小延迟和用于枚举开始的函数。
void UOTGHS_Handler()
{
//Manage SOF interrupt
if (0 != (UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_HSOFI))
{
UOTGHS->UOTGHS_HSTICR = UOTGHS_HSTICR_HSOFIC; //Ack SOF
HCD_HandleSOFInterrupt();
return;
}
//...
//USB bus reset detection
if (0 != (UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_RSTI))
{
UOTGHS->UOTGHS_HSTICR = UOTGHS_HSTICR_RSTIC; //Ack reset
PrintStr("Reset performed.\r\n");
hcd_ControlStructure.Pause = 100; //Pause before enumeration start
hcd_ControlStructure.TransferStart = hcd_ControlStructure.ResetEnd;
return;
}
PrintStr("Unmanaged Interrupt.\n\r"); //If I forgot to handle something
}
一旦 USB 设备连接并复位,就会指定 100ms 延迟和传输开始函数(每次插入 USB 设备时都会发生);SOF 中断处理程序开始倒计时延迟,当达到 0 时,调用 TransferStart 函数。此函数属于 USBD(USB 驱动程序),它通过请求第一个描述符(标准设备描述符)来启动枚举过程。我将在本文稍后编写此函数。
中断驱动传输
中断驱动传输意味着一旦开始,它就会触发各种中断,这些中断依次调用传输函数,传输函数再次触发中断。这种情况一直持续到传输完成其所有阶段、事务和数据包。
序列从调用发送 SETUP 数据包(SETUP 阶段,SETUP 数据包)的函数开始。SAM3X 发送 DATA0 数据包并自动接收设备的 ACK,无需我们干预,我们只需要指向 DATA0 数据包的数据所在位置。一旦 SETUP 数据包(带 DATA0)发送并收到 ACK,SAM3X 将触发“Transmitted SETUP”中断,将我们导向 HCD_HandleControlPipeInterrupt 函数。在此函数中,我借助控制结构确定方向是 IN,因此我发送 IN 数据包(DATA 阶段,IN 数据包)。发送 IN 数据包后,将触发“Received IN Data”中断,再次将我们导向 HCD_HandleControlPipeInterrupt 函数。当我确认它时,SAM3X 发送 ACK 数据包(DATA 阶段的最后一个灰色矩形)。此时,缓冲区数据被读取,然后我通过调用发送带有空 DATA1 数据包的事务的函数来进入 HANDSHAKE 阶段。一旦发送了空 OUT 数据包并且 SAM3X 从设备收到 ACK,将触发“Transmitted OUT Data”中断,这将再次将我们导向 HCD_HandleControlPipeInterrupt 函数。借助控制结构,我确定这是控制传输的结束,是时候调用 TransferEnd 函数了。
在编写上述函数并更改控制管道中断处理程序之前,让我用图形方式显示上一段的调用序列,并带有正确的函数名称。
发送带数据的 Setup 数据包
这是序列中的第一个函数。它填充控制结构和管道 0 内存库,其中包含将在 SETUP 阶段的 DATA0 数据包中发送的数据,并启用“Transmitted SETUP”中断,该中断在所有 SETUP 阶段数据包发送并从设备收到 ACK 后调用。最后,它启动传输并退出。
void HCD_SendCtrlSetupPacket(uint8_t EndpAddress, uint8_t Direction, usb_setup_req_t SetupPacketData,
uint8_t *ptrBuffer, uint16_t TransferByteCount,
void (*TransferEnd)(uint16_t ByteReceived))
{
//Setting up control structure, initial stage is SETUP
hcd_ControlStructure.ByteCount = TransferByteCount;
hcd_ControlStructure.Direction = Direction;
hcd_ControlStructure.ptrBuffer = ptrBuffer;
hcd_ControlStructure.Index = 0;
hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_SETUP;
hcd_ControlStructure.TransferEnd = TransferEnd;
//Token is SETUP
UOTGHS->UOTGHS_HSTPIPCFG[0] = (UOTGHS->UOTGHS_HSTPIPCFG[0] & ~UOTGHS_HSTPIPCFG_PTOKEN_Msk)
| (UOTGHS_HSTPIPCFG_PTOKEN_SETUP & UOTGHS_HSTPIPCFG_PTOKEN_Msk);
//Getting pointer to memory bank of pipe 0
volatile uint64_t *ptrRequestPayload =
(volatile uint64_t*)&(((volatile uint64_t(*)[0x8000])UOTGHS_RAM_ADDR)[0]);
//Loading it with data
*ptrRequestPayload = *((uint64_t*)&SetupPacketData);
PrintStr("Sending SETUP packet + DATA0.\r\n");
//Transmitted SETUP interrupt enable
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_TXSTPES;
//Ack that SETUP packet is in pipe and send if not frozen
UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_FIFOCONC;
//Send data if pipe was frozen due to error, reset or STALL
UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_PFREEZEC;
}
请注意,传输完成后的立即调用的函数的指针在此处指定为参数 void (*TransferEnd)(uint16_t ByteReceived))。
请求数据大小为 8 字节(64 位),并且对管道的内存库有 8 位、16 位、32 位和 64 位访问,因此方便地将 usb_setup_req_t 强制转换为 uint64_t 以便轻松复制。
中断(第一次)
HCD_SendCtrlSetupPacket 将在 SETUP 数据包、DATA0 数据包发送并收到设备的 ACK 数据包后触发控制管道上的中断。中断将到达 HCD_HandleControlPipeInterrupt 函数,因此需要对其进行更改以能够服务中断。中断将是“Transmitted SETUP interrupt”(UOTGHS_HSTPIPISR[0] 寄存器的 UOTGHS_HSTPIPISR_TXSTPI 位)。代码检查阶段和方向,如果它是 SETUP 和 IN,它会将阶段更改为 DATA_IN 并调用发送 IN 数据包的函数 - HCD_SendCtrlInPacket。这启动了传输的 DATA 阶段。
//...
//Transmitted SETUP interrupt
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_TXSTPI)
{
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_PFREEZES; //Freezes pipe 0
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_TXSTPIC; //Ack interrupt
//Start DATA stage
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_SETUP)
{
if(hcd_ControlStructure.Direction == USB_REQ_DIR_IN) //Now need to send IN packet
{
//change to IN phase
hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_DATA_IN;
HCD_SendCtrlInPacket();
return;
}
}
return;
}
//...
发送 DATA 阶段 IN 数据包
这是一个简单的函数,它只发送 IN 令牌并启用“Received IN Data”中断。
void HCD_SendCtrlInPacket(void)
{
//Reconfigure to IN token
UOTGHS->UOTGHS_HSTPIPCFG[0] = (UOTGHS->UOTGHS_HSTPIPCFG[0] & ~UOTGHS_HSTPIPCFG_PTOKEN_Msk)
| (UOTGHS_HSTPIPCFG_PTOKEN_IN & UOTGHS_HSTPIPCFG_PTOKEN_Msk);
//Clear Received IN Data interrupt to be able to receive it when IN data came
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_RXINIC;
//Enable IN Received interrupt
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_RXINES;
//Free buffer
UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_FIFOCONC;
UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_PFREEZEC;
}
请注意,最后两行(...FIFOCONC 和 ...PFREEZEC)对于所有发送都是相同的。
中断(第二次)
发送 IN 数据包将触发一个中断,从中应接收数据。中断处理程序 HCD_HandleControlPipeInterrupt 应该理解这一点。
//...
//Received IN Data interrupt
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_RXINI)
{
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_RXINIC; //Ack interrupt
//Data stage: IN token has been send and DATA0/1 came in
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_DATA_IN)
{
HCD_GetCtrlInDataPacket(); //Getting data
return;
}
return;
}
//...
中断函数的一部分检查阶段是否为 DATA_IN。如果是,则调用 HCD_GetCtrlInDataPacket,该函数实际接收数据。
接收 DATA 阶段数据
此函数提供对管道 0 内存库的 8 位访问,逐字节读取并将其复制到控制结构缓冲区。ByteReceived 将保存当前事务中返回的字节数。总字节数必须等于 SETUP 阶段指定的值。因此,如果 ByteReceived 等于该值,则表示已全部接收,函数必须返回。
返回的另一个条件是短数据包。如果 DATA 阶段有多个事务,每个 DATA 阶段数据包的大小必须相同,除了最后一个,因此短数据包总是表示最后一个数据包,无论一切是否正常,或者数据是否存在问题。
一旦所有数据都接收完毕,它将指定 ZLP_OUT 阶段(开始 HANDSHAKE 阶段)并调用实际发送带有空 DATA1 数据包的 OUT 数据包的函数 - HCD_SentCtrlOutEmptyPacket。
void HCD_GetCtrlInDataPacket(void)
{
uint32_t IsReceivedPacketShort = UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_SHORTPACKETI;
uint8_t ByteReceived = UOTGHS->UOTGHS_HSTPIPISR[0] >> UOTGHS_HSTPIPISR_PBYCT_Pos;
volatile uint8_t *ptrReceiveBuffer =
(uint8_t *)&(((volatile uint8_t(*)[0x8000])UOTGHS_RAM_ADDR)[0]);
//Copying bytes over from FIFO buffer
while(ByteReceived)
{
hcd_ControlStructure.ptrBuffer[hcd_ControlStructure.Index] = *ptrReceiveBuffer++;
hcd_ControlStructure.Index ++;
ByteReceived--;
}
//If following is true, must move to HANDSHAKE stage
if((hcd_ControlStructure.Index == hcd_ControlStructure.ByteCount) || IsReceivedPacketShort)
{
hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_ZLP_OUT;
HCD_SentCtrlOutEmptyPacket();
return;
}
//Next IN transaction should be initiated here.
}
请注意底部的注释:当所有数据不适合一个事务时,我将返回此函数。
发送带有空 DATA1 数据包的 HANDSHAKE OUT 数据包
它只是指定正确的令牌(OUT)并启用“Received OUT Data”中断。
void HCD_SentCtrlOutEmptyPacket(void)
{
//Changing pipe token to OUT
UOTGHS->UOTGHS_HSTPIPCFG[0] = (UOTGHS->UOTGHS_HSTPIPCFG[0] & ~UOTGHS_HSTPIPCFG_PTOKEN_Msk)
| (UOTGHS_HSTPIPCFG_PTOKEN_OUT & UOTGHS_HSTPIPCFG_PTOKEN_Msk);
//Clean OUT interrupt flag if it is set
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_TXOUTIC;
//Enables OUT interrupt
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_TXOUTES;
//Sends FIFO buffer data (which is none in this case)
UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_FIFOCONC;
//Enable pipe request generation
UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_PFREEZEC;
}
请注意,SAM3X 会自动识别要发送 DATA1,无需我们在任何地方指定。
中断(第三次)
一旦发送了 OUT 数据包和空 DATA1 数据包并从设备收到了 ACK 数据包,它将第三次(也是这次传输的最后一次)将我们带到 HCD_HandleControlPipeInterrupt 函数。在这里,它检查阶段,如果是 ZLP_OUT,则调用 TransferEnd 函数(还记得,它是在调用 HCD_SendCtrlSetupPacket 函数时指定的)。
//...
//Received OUT Data interrupt
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_TXOUTI)
{
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_TXOUTIC; //Ack interrupt
//HANDSHAKE stage, ACK for empty OUT is received
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_ZLP_OUT)
{
//Notifies about transaction completion
(*hcd_ControlStructure.TransferEnd)(hcd_ControlStructure.Index);
}
return;
}
//...
HCD 最后调整
到目前为止,HCD 部分已基本完成,可以处理接收具有小于或等于管道 0 内存库大小(64 字节)的返回数据量的控制传输 - 这意味着 DATA 阶段中只有一个事务的传输。
唯一缺少的是如何指定控制结构的 ResetEnd 函数指针。此指针在执行完设备复位后立即复制到 TransferStart 指针。我将创建一个函数来执行此操作。
void HCD_SetEnumerationStartFunction(void (*ResetEnd)(void))
{
hcd_ControlStructure.ResetEnd = ResetEnd;
}
并在调用 HCD_Init 之前在主文件中调用它。此函数的声明必须在 HCD.h 文件中,因为它会从外部调用。
编写 USBD
USBD(USB 驱动程序)函数将启动传输并获取返回的数据。它们将成对工作,一个函数是 ...Begin,另一个是 ...End - 第一个函数形成具有相应命令的请求结构,第二个函数获取结果。
获取标准设备描述符
首先是 ...Begin 函数。它形成标准请求结构:传输是接收数据(USB_REQ_DIR_IN),此请求是标准请求(USB_REQ_TYPE_STANDARD),而不是类或供应商特定请求,此请求是发往设备(USB_REQ_RECIP_DEVICE),而不是设备的接口或接口的端点,此请求获取描述符(USB_REQ_GET_DESCRIPTOR),不是任何描述符,而是设备描述符(USB_DT_DEVICE)。
此请求不使用 wIndex。wLength 为 18,这是应返回的字节数,也是标准设备描述符 usb_dev_desc_t 的大小。
由于这是第一次与 HCD 交互,必须初始化一个控制管道 - HCD_InitiatePipeZero。
然后调用 HCD_SendCtrlSetupPacket,它启动了上一节中描述的中断驱动控制传输。
void USBD_GetDeviceDescriptorBegin(void)
{
usb_setup_req_t ControlRequest;
ControlRequest.bmRequestType = USB_REQ_DIR_IN | USB_REQ_TYPE_STANDARD | USB_REQ_RECIP_DEVICE;
ControlRequest.bRequest = USB_REQ_GET_DESCRIPTOR;
ControlRequest.wValue = (USB_DT_DEVICE << 8);
ControlRequest.wIndex = 0;
ControlRequest.wLength = 18;
if(HCD_InitiatePipeZero(0))
{
HCD_SendCtrlSetupPacket(0, USB_REQ_DIR_IN, ControlRequest, Buffer, ControlRequest.wLength,
USBD_GetDeviceDescriptorEnd);
}
}
缓冲区在 USBD.h 文件中指定
uint8_t Buffer[1024];
一旦 HCD 处理完传输,它将调用 USBD_GetDeviceDescriptorEnd 函数。
void USBD_GetDeviceDescriptorEnd(uint16_t ByteReceived)
{
PrintStr("Standard device descriptor size is ");
PrintDEC(ByteReceived);
PrintStr(".\r\n");
PrintDeviceDescriptor(Buffer);
}
返回的数据在 Buffer 中,我将其指针传递给打印函数 PrintDeviceDescriptor,该函数是
void PrintDeviceDescriptor(uint8_t* ptrBuffer)
{
usb_dev_desc_t dev_desc;
memcpy(&dev_desc, ptrBuffer, 18);
PrintStr("\r\n------Standard Device Descriptor------\r\n");
PrintStr("bLength: \t\t");
PrintDEC(dev_desc.bLength);
PrintStr("\r\n");
PrintStr("bDescriptorType: \t");
PrintDEC(dev_desc.bDescriptorType);
PrintStr("\r\n");
PrintStr("bcdUSB: \t\t");
PrintHEX16(dev_desc.bcdUSB);
PrintStr("\r\n");
PrintStr("bDeviceClass: \t");
PrintHEX((uint8_t*)(&dev_desc.bDeviceClass), 1);
PrintStr("\r\n");
PrintStr("bDeviceSubClass: \t");
PrintDEC(dev_desc.bDeviceSubClass);
PrintStr("\r\n");
PrintStr("bDeviceProtocol: \t");
PrintDEC(dev_desc.bDeviceProtocol);
PrintStr("\r\n");
PrintStr("bMazPacketSize0: \t");
PrintDEC(dev_desc.bMaxPacketSize0);
PrintStr("\r\n");
PrintStr("idVendor: \t\t");
PrintHEX16(dev_desc.idVendor);
PrintStr("\r\n");
PrintStr("idProduct: \t\t");
PrintHEX16(dev_desc.idProduct);
PrintStr("\r\n");
PrintStr("bcdDevice: \t\t");
PrintHEX16(dev_desc.bcdDevice);
PrintStr("\r\n");
PrintStr("iManufacturer: \t");
PrintDEC(dev_desc.iManufacturer);
PrintStr("\r\n");
PrintStr("iProduct: \t\t");
PrintDEC(dev_desc.iProduct);
PrintStr("\r\n");
PrintStr("iSerialNumber: \t");
PrintDEC(dev_desc.iSerialNumber);
PrintStr("\r\n");
PrintStr("bNumConfigurations:\t");
PrintDEC(dev_desc.bNumConfigurations);
PrintStr("\r\n-------------------------------------\r\n");
}
让我们构建代码,上传并查看输出(您的值应该略有不同)。
我的摄像头符合 USB 2.0 (bcbUSB),其类是多接口功能(bDeviceClass、bDeviceSubClass 和 bDeviceProtocol 组合为 0xEF, 2, 1),这意味着我的摄像头有几个(而不是一个)视频接口,它们是通过接口关联描述符(我将在下一篇文章中讨论)分组的。我可以使用不超过 64 字节的数据包(bMaxPacketSize0)与控制端点通信。idVendor 和 idProduct 在您支付费用后由 USB 论坛(USB 合法机构名称)分配,这些数字是唯一的,可以在设备管理器中看到,以及设备版本(bcdDevice)——设备标识符属性:
iManufacturer、iProduct 和 iSerialNumber 是读取字符串(人类可读)描述的索引,我没有这样做。最重要的是配置数量(bNumConfigurations),即 1,这意味着我的摄像头只有一个索引为 0(零)的配置,我将在下一篇文章中使用 0(零)作为参数来读取配置描述符。
结论
本文包含了 Arduino Due 中 USB 通信的最低基础知识。详细描述了一种使用中断驱动技术的传输。下一篇文章将重点介绍获取所有剩余描述符并构建某种设备映射。
源代码 在此处。
第 6 部分 在这里。