65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (6投票s)

2015年5月10日

CPOL

19分钟阅读

viewsIcon

24575

downloadIcon

428

获取第一个 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),其类是多接口功能(bDeviceClassbDeviceSubClassbDeviceProtocol 组合为 0xEF, 2, 1),这意味着我的摄像头有几个(而不是一个)视频接口,它们是通过接口关联描述符(我将在下一篇文章中讨论)分组的。我可以使用不超过 64 字节的数据包(bMaxPacketSize0)与控制端点通信。idVendoridProduct 在您支付费用后由 USB 论坛(USB 合法机构名称)分配,这些数字是唯一的,可以在设备管理器中看到,以及设备版本(bcdDevice)——设备标识符属性:

iManufactureriProductiSerialNumber 是读取字符串(人类可读)描述的索引,我没有这样做。最重要的是配置数量(bNumConfigurations),即 1,这意味着我的摄像头只有一个索引为 0(零)的配置,我将在下一篇文章中使用 0(零)作为参数来读取配置描述符。

结论

本文包含了 Arduino Due 中 USB 通信的最低基础知识。详细描述了一种使用中断驱动技术的传输。下一篇文章将重点介绍获取所有剩余描述符并构建某种设备映射。

源代码 在此处

第 6 部分 在这里

© . All rights reserved.