目录
前言
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发送数据。使用串口工具的时候,波特率多少都无所谓,因为这个波特率是虚的。所以叫虚拟串口。