USB设备编程(二)

目录

前言

USB描述符

1.USB 设备状态切换图

2.标准设备请求

(1) SETUP事务的数据格式

(2) 标准设备请求

(3) 设备/配置/接口/端点

3.描述符

(1) 设备描述符

(2) 配置描述符

(3) 接口描述符

(4) 端点描述符

(5) 示例

4.设备枚举过程示例

USBX组件

1.Azure RTOS 介绍

2.USBX 层次

3.USBX 的基本配置

移植USBX实现虚拟串口

1.配置 USB

2.添加 USBX 代码

(1) 复制代码

(2) 添加进工程

3.添加 USBX APP 代码

4.修改 usb.c

5.创建 USBX 任务

6.设置 MDK-ARM 工程

7.添加使用串口的代码

8.上机实验

虚拟串口源码分析与改造

1.描述符的设置

2.数据收发函数

3.使用 FreeRTOS 改造代码


前言

USB设备编程我分成两篇博客,第一篇见USB设备编程(一)

我们把一个U盘插到电脑,电脑马上就能识别出它是一个U盘,为什么?
       
对于硬件本身,它存储有这个设备的信息,这些信息我们称之为描述符:简单理解成描述这个设备的、具有固定格式的数据。


USB描述符

1.USB 设备状态切换图

2.标准设备请求

(1) SETUP事务的数据格式

        Host 使用控制传输来识别设备、设置设备地址、启动设备的某些特性,对于控制传输,它首先发出"setup 事务",如下:

        在"setup 事务"中, 
① SETUP 令牌包:用来通知设备,"要开始传输了"
② DATA0 数据包:它含有固定的格式,用来告诉设备"是读还是写"、"读什么"、"写什么"

Host 通过 DATA0 数据包发送 8 字节数据给设备,它的格式如下图所示:

(2) 标准设备请求

控制传输的建立事务中,可以使用下列格式的数据:

上表中各个"宏"取值如下:

(3) 设备/配置/接口/端点

在 SETUP 事务的数据里,表示了要访问的是什么:Device(设备)?Interface(接口)?Endpoint(端点)? 
        对于一个 USB 设备,它可以多种配置(Configuration)。比如 4G 上网卡就有 2种配置:U 盘、上网卡。第 1 次把 4G 上网卡插入电脑时,它是一个 U 盘,可以按照里面的程序。装好程序后,把它再次插入电脑,它就是一个上网卡。驱动程序可以选择让它工作于哪种配置,同一时间只能有一种配置。大多数的 USB 设备只有一种配置。
        一个配置下,可以有多个接口(Interface),接口等同于功能(Function)。比如 USB 耳机有两个接口(功能):声音收发、按键控制。
        一个接口,可能有多个设置(Setting),比如默认设置下它使用较低的带宽,可以选择其他设置以使用更高带宽。
        一个接口,由一个或多个端点(Endpoint)组成。还是拿 USB 耳机举例:对于声音收发接口:Host可以往这个接口的一个端点写数据,然后从另一个端点读数据。端点是数据传输的单位。每个设备都有端点 0 ,端点 0 是双向的。接口还可以有其他端点,这些端点是单向的,要么是批量(Bulk)端点、要么是中断(Interrupt)端点、要么是同步(Isochronous)端点。

3.描述符

怎么描述设备、配置、接口、端点?
        
使用描述符(Descriptors),有设备描述符、配置描述符、接口描述符、端点描述符。所谓描述符,就是一些格式化的数据,用来描述信息。

        一个 USB 设备:
① 只有一个设备描述符:设备描述符用来表示设备的 ID、它有多少个配置、它的端点 0 一次最大能传输多少字节数据
② 可能有多个配置描述符:配置描述符用来表示它有多少个接口、供电方式、最大电流
③ 一个配置描述符下面,可能有多个接口描述符:接口描述符用来表示它是哪类接口、有几个设置(Setting)、有几个端点
④ 一个接口描述符符下面,可能有多个端点描述符:端点描述符用来表示端点号、方向(IN/OUT)、类型(批量/中断/同步)

        还有一些字符串描述符(String descriptors),它用可读的文字来描述设备,是可选的。

下面给出设备、配置、接口、端点对应的描述符格式,只做了解,不需要深入。 

(1) 设备描述符

拿上图的标准设备描述符举例: 

不同的描述符有它自己的规范格式,就是通过这些描述符来识别到底是什么设备的。 

(2) 配置描述符

(3) 接口描述符

(4) 端点描述符

(5) 示例

在 Ubuntu 中可以执行 lsusb -v 查看 USB 设备的描述符信息:

也间接证明了设备/配置/接口/端点之间的关系。

4.设备枚举过程示例

        使用"usbprotocolsuite"打开,可以看到设备的枚举过程:
① 使用控制传输,读取设备信息(设备描述符):第一次读取时,它只需要得到 8 字节数据,因为第 8 个数据表示端点 0 能传输的最大数据长度。

一个USB设备插入Host后,Host会发起一个控制传输

② Host 分配地址给设备,然后把新地址发给设备:

 

③ 使用新地址,重新读取设备描述符,设备描述符长度是 18: 

读取配置描述符:它传入的长度是 255,想一次性把当前配置描述符、它下面的接口描述符、端点描述符全部读出来

读取字符描述符


USBX组件

1.Azure RTOS 介绍

        Azure RTOS 平台是运行实时解决方案的集合,包括 Azure RTOS ThreadX、Azure RTOS NetX 和 NetX Duo、Azure RTOS FileX、Azure RTOS GUIX 和 Azure RTOS USBX

        Azure RTOS ThreadX 是专用于深度嵌入式应用程序的高级实时操作系统 (RTOS)。Azure RTOS ThreadX 具有多种优势,其中包括高级调度设施、消息传递、中断管理和消息服务。 Azure RTOS ThreadX 具有许多高级功能,其中包括 picokernel 体系结构、抢占式阈值调度、事件链和一系列丰富的系统服务。

        USBX 是 Azure®RTOS USB 主机和 USB 设备嵌入式堆栈。它与 ThreadX 紧密耦合。在某些类中,它需要 FileX 和 NetX Duo 堆栈。它允许使用具有多种配置的 USB 设备、复合设备和USB OTG 进行操作。它支持 USB 电源管理。

        USBX 为 USB 主机和 USB 设备堆栈提供了大量的 USB 类。一旦低级驱动程序能够响应USBX 请求,模块化架构就可以更容易地移植到不同的 USB 硬件 IP 上。

        所有 STM32 USB IP(主机、设备、OTG、高速和全速)均由 USBX 通过通用 STM32 HAL驱动程序 API 透明支持。

2.USBX 层次

参考资料:https://wiki.stmicroelectronics.cn/stm32mcu/wiki/Introduction_to_USBX

        USBX 分为三层,如下图所示:
① 控制器层:最底层,USB 设备控制器的驱动程序,通常是 HAL 库
② stack layer:实现 USB 设备的基本操作,比如描述符的操作、使用 endpoint 进行数据传输
③ Class layer:实现各类 USB 设备的操作,比如 HID 设备、音频设备、虚拟串口,给 APP提供接口

在 STM32 的固件中,可以看到 USBX 目录,比如:

移植 Controller layer、stack layer、Class layer 并不复杂,重点在于 2 点: 
① 怎么初始化硬件以确保 Controller layer 可以正常运行?
② 怎么编写 APP:提供设备信息、传输数据?

3.USBX 的基本配置

        USBX 依赖于 Azure®RTOS ThreadX,但是也可以单独使用 USBX,这需要配置。通常在“ux_user.h”里进行配置,配置项如下:
① 使用单独模式或 RTOS 模式:

/* Defined, this macro will enable the standalone mode of usbx. */
#define UX_STANDALONE

        当没有定义“UX_STANDALONE”时就是使用 RTOS 模式,可以使用 ThreadX 提供的互斥量函数实现阻塞式读写(“blocking”),比如对于 USB 虚拟串口,可以使用如下函数: 

UINT _ux_device_class_cdc_acm_read(UX_SLAVE_CLASS_CDC_ACM *cdc_acm,                                                                 UCHAR *buffer,

                                                                ULONG requested_length,

                                                                ULONG *actual_length);

UINT _ux_device_class_cdc_acm_write(UX_SLAVE_CLASS_CDC_ACM *cdc_acm,                                                                             UCHAR *buffer,

                                                                ULONG requested_length,

                                                                ULONG *actual_length);

         这 2 个函数发起数据传输,在传输过程中线程阻塞,传输完成后线程被唤醒。

        当定义“UX_STANDALONE”时就是使用单独模式,不能再使用上面的阻塞函数,而要使用非阻塞的函数(non-blocke):

UINT _ux_device_class_cdc_acm_read_run(UX_SLAVE_CLASS_CDC_ACM *cdc_acm,                                                                            UCHAR *buffer,

                                                                        ULONG requested_length,

                                                                        ULONG *actual_length);

UINT _ux_device_class_cdc_acm_write_run(UX_SLAVE_CLASS_CDC_ACM *cdc_acm,

                                                                        UCHAR *buffer,

                                                                        ULONG requested_length,

                                                                        ULONG *actual_length);

        它们只是发起传输,然后就即刻返回。需要提供回调函数,在回调函数里分辨数据是否传输完成。 

② 非阻塞模式:

/* Defined, this macro disables CDC ACM non-blocking transmission support. */
//#define UX_DEVICE_CLASS_CDC_ACM_TRANSMISSION_DISABLE

        定义 UX_DEVICE_CLASS_CDC_ACM_TRANSMISSION_DISABLE 是,就禁止了“非阻塞模式”, 这时只能使用基于 RTOS 的阻塞函数。
        换句话说,要使用单独模式的非阻塞函数,就不能定义这个配置项。

③ USB HOST/Device 模式

/* Defined, this value will only enable the host side of usbx. */
/* #define UX_HOST_SIDE_ONLY */

/* Defined, this value will only enable the device side of usbx. */
#define UX_DEVICE_SIDE_ONLY

本课程定义“UX_DEVICE_SIDE_ONLY”,仅作为 USB Device


移植USBX实现虚拟串口

1.配置 USB

2.添加 USBX 代码

(1) 复制代码

找到固件库,如下:

把 usbx 整个目录复制到工程“Middlewares\Third_Party”目录下,如下:

(2) 添加进工程

        需要添加 USBX 的 3 层源码。
        先仿照下图添加“Class layer”源码,添加含有“ux_device_class_cdc_acm”前缀的 C 文件:

再仿照下图添加“stack layer”源码,可以从文件名的前面看出它们的作用,比如“ux_device_stack”表示这是 stack 源码,“ux_utility”表示这是辅助函数,“ux_system”表示是这是系统函数:

最后仿照下图添加“Controller layer”,添加“ux_dcd_stm32”前缀的 C 文件: 

3.添加 USBX APP 代码

参考工程:
① GIT 仓库:https://github.com/STMicroelectronics/STM32CubeH5.git
② 工程路径:

STM32CubeH5\Projects\NUCLEO-H563ZI\Applications\USBX\Ux_Device_HID_CDC_ACM

把 app 文件夹复制到工程的“Middlewares\Third_Party\usbx”目录下,如下图所示:

        各个文件的作用为:
① ux_user.h:配置 USBX
② ux_stm32_config.h:里面含有配置项,表示 STM32 支持多少个 endpoint
③ ux_device_descriptors.c/h:USB 虚拟串口的描述符信息
④ ux_device_cdc_acm.c:USB 串口的 Activate/DeActivate 函数
⑤ app_usbx_device.c:调用 stack layer 函数,模拟 USB 串口 

在工程里添加上述文件,如下图所示:

4.修改 usb.c

        使用 STM32CubeMX 配置 usb 后生成的 usb.c 里,只是初始化了 USB 控制器,并未启动它,也没有跟 USBX 建立联系,需要修改代码。
        代码如下:

void MX_USB_PCD_Init(void)
{
  /* USER CODE BEGIN USB_Init 0 */
  UINT MX_USBX_Device_Init(void);
  MX_USBX_Device_Init();
  /* USER CODE END USB_Init 0 */

  /* USER CODE BEGIN USB_Init 1 */

  /* USER CODE END USB_Init 1 */
  hpcd_USB_DRD_FS.Instance = USB_DRD_FS;
  hpcd_USB_DRD_FS.Init.dev_endpoints = 8;
  hpcd_USB_DRD_FS.Init.speed = USBD_FS_SPEED;
  hpcd_USB_DRD_FS.Init.phy_itface = PCD_PHY_EMBEDDED;
  hpcd_USB_DRD_FS.Init.Sof_enable = DISABLE;
  hpcd_USB_DRD_FS.Init.low_power_enable = DISABLE;
  hpcd_USB_DRD_FS.Init.lpm_enable = DISABLE;
  hpcd_USB_DRD_FS.Init.battery_charging_enable = DISABLE;
  hpcd_USB_DRD_FS.Init.vbus_sensing_enable = DISABLE;
  hpcd_USB_DRD_FS.Init.bulk_doublebuffer_enable = DISABLE;
  hpcd_USB_DRD_FS.Init.iso_singlebuffer_enable = DISABLE;
  if (HAL_PCD_Init(&hpcd_USB_DRD_FS) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USB_Init 2 */
  HAL_PWREx_EnableVddUSB();
  HAL_PWREx_EnableUSBVoltageDetector();
  
  HAL_PCDEx_PMAConfig(&hpcd_USB_DRD_FS, 0x00, PCD_SNG_BUF, 0x14);
  HAL_PCDEx_PMAConfig(&hpcd_USB_DRD_FS, 0x80, PCD_SNG_BUF, 0x54);
  HAL_PCDEx_PMAConfig(&hpcd_USB_DRD_FS, USBD_CDCACM_EPINCMD_ADDR, PCD_SNG_BUF, 0x94);
  HAL_PCDEx_PMAConfig(&hpcd_USB_DRD_FS, USBD_CDCACM_EPOUT_ADDR, PCD_SNG_BUF, 0xD4);
  HAL_PCDEx_PMAConfig(&hpcd_USB_DRD_FS, USBD_CDCACM_EPIN_ADDR, PCD_SNG_BUF, 0x114);
  ux_dcd_stm32_initialize((ULONG)USB_DRD_FS, (ULONG)&hpcd_USB_DRD_FS);
 
  HAL_PCD_Start(&hpcd_USB_DRD_FS);
  /* USER CODE END USB_Init 2 */
}

第 38 行:调用 USBX 的函数,添加 USB 串口的支持。
第 60~61 行:使能 USB 控制器的电源。
第 63~67 行:设置 endpoint 的“Packet Buffer Memory”,这个概念可以参考:http://www.51hei.com/bbs/dpj-40953-1.html 
第 68 行 : 把 STM32 USB 控 制 器 的 句 柄 , 传 给 USBX 系统,“usbx_stm32_device_controllers”的代码会使用这个句柄来操作硬件。
第 70 行:启动 USB 控制器。

5.创建 USBX 任务

        使用单独模式( STANDALONE ) 时 , 需要创建一个任务,不断运行“_ux_system_tasks_run”函数。以下代码是在 FreeRTOS 的默认任务里运行和这个函数:

app_freertos.c里:

第 29 行,包含 USBX 的头文件。
第 189 行,调用 USBX 的系统函数。 

6.设置 MDK-ARM 工程

        如下图配置:
① 添加宏开关:UX_INCLUDE_USER_DEFINE_FILE(图中标号 2)
② 添加头文件目录(图中标号 5) 

(用逗号隔开)

7.添加使用串口的代码

app_freertos.c 里添加 USB 串口的发送测试代码:

static void SPILCDTaskFunction ( void *pvParameters )
{
	char buf[100];
	int cnt = 0;
	
	while(1)
	{
        /*构造字符串*/
		sprintf(buf, "LCD Task Test : %d", cnt++);
        /*声明函数*/
		int ux_device_cdc_acm_send(uint8_t *datas, uint32_t len, uint32_t timeout);
		ux_device_cdc_acm_send((uint8_t *)buf, strlen(buf), 1000);
		vTaskDelay(1000);
	}
}

 

第 79~80 行:使用 USB 串口发送数据。 

在“Middlewares\Third_Party\usbx\app\ux_device_cdc_acm.c”中,有如下代码:

static UINT ux_device_class_cdc_acm_read_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, UCHAR *data_pointer, ULONG length)
{
    int Draw_String(uint32_t x, uint32_t y, char *str, uint32_t front_color, uint32_t back_color);
    if (status == UX_SUCCESS)
    {
        data_pointer[length] = '\0';
        Draw_String(0, 0, (char *)data_pointer, 0x0000ff00, 0);
    }
    return 0;
}

当 USB 串口收到数据后,ux_device_class_cdc_acm_read_callback 函数被调用。
Draw_String 函数把接收到的数据在 LCD 上显示处来。

8.上机实验

        烧写运行程序后,接上 USB 线,在电脑上可以识别出 USB 串口,查看设备管理器,可以看到如下设备:

使用串口工具打开这个串口,可以连续不断接收到数据,如下所示:

在串口工具上发送数据时,在板子的 LCD 上会有显示。


虚拟串口源码分析与改造

1.描述符的设置

        在“Middlewares\Third_Party\usbx\app\ux_device_descriptors.c”有设备描述符、配置描述符、接口描述符、端点描述符的定义。
        比如,设备描述符在如下代码中设置: 

配置描述符在如下代码中设置:

2.数据收发函数

        涉及文件为:demo\Middlewares\Third_Party\usbx\app\ux_device_cdc_acm.c
        开发板通过 USB 串口发出数据时,使用以下函数: 
        使用 ux_device_class_cdc_acm_write_with_callback 来启动发送,它会立刻返回,它返回的时候并不表示数据已经发送完成了,当数据发送完毕会调用回调函数 ux_device_class_cdc_acm_write_callback :

/* 启动发送 */ 
UINT ux_device_class_cdc_acm_write_with_callback(UX_SLAVE_CLASS_CDC_ACM *cdc_acm, UCHAR *buffer, ULONG requested_length);

/* 发送完毕的回调函数 */
static UINT ux_device_class_cdc_acm_write_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, ULONG length);

我们将会实现如下函数,在里面调用上面的启动发送函数,然后改造回调函数。

int ux_device_cdc_acm_send(uint8_t *datas, uint32_t len, uint32_t timeout);

开发板接收到 USB 串口数据时,以下回调函数被调用:

static UINT ux_device_class_cdc_acm_read_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, UCHAR *data_pointer, ULONG length); 

仿照发送数据,我们可以实现下面这个函数,在里面调用上面收到数据的回调函数,并改造它,把接收到的数据写入队列。

int ux_device_cdc_acm_getchar(uint8_t *pData, uint32_t timeout);

3.使用 FreeRTOS 改造代码

在 ux_device_cdc_acm.c 里:

对于发送,实现以下函数:启动发送之后阻塞,等待回调函数唤醒或超时。

static SemaphoreHandle_t g_xUSBUARTSend;

static UINT ux_device_class_cdc_acm_write_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, ULONG length)
{
    /* 发送完数据后 Give 信号量 */
	xSemaphoreGive(g_xUSBUARTSend);
	return 0;
}

/* send data -> PC */
int ux_device_cdc_acm_send(uint8_t *datas, uint32_t len, uint32_t timeout)
{
    if(cdc_acm)
    {
        if(UX_SUCCESS == ux_device_class_cdc_acm_write_with_callback(cdc_acm,datas,len))
        {
            /* 等待回调函数的信号量,返回TRUE表示成功获取 */
			if (pdTRUE == xSemaphoreTake(g_xUSBUARTSend, timeout))
				return 0;
            /* 超时返回-1 */
			else
				return -1;
        }
        else
        {
            return -1;
        }
    }
    else
    {
        return -1;
    }
	return 0;
}

对于接收,实现以下函数:把接收到的数据写入队列。

/* 接收到数据的回调函数 */
static UINT ux_device_class_cdc_acm_read_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, UCHAR *data_pointer, ULONG length)
{
	for(int i = 0; i < length; i++)
	{
		xQueueSend(g_xUSBUART_RX_Queue, (const void *)&data_pointer[i], 0);
	}
	return 0;
}

/* PC's data -> STM32 */
int ux_device_cdc_acm_getchar(uint8_t *pData, uint32_t timeout)
{
	if (g_xUSBUART_RX_Queue)//队列非空才读数据,不然程序会卡死
	{
		if (pdPASS == xQueueReceive(g_xUSBUART_RX_Queue, pData, timeout))
			return 0;
		else
			return -1;
	}
	else
	{
		return -1;
	}
}

        以后在应用层调用getchar函数就能得到PC通过USB虚拟串口给板子发送的数据了,调用send函数也可以给PC发送数据。使用串口工具的时候,波特率多少都无所谓,因为这个波特率是虚的。所以叫虚拟串口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sakabu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值