USB 应用开发基础知识 ...... 矜辰所致
前言
我们之前学习使用的沁恒微 RISC-V 蓝牙芯片都带有 USB 控制器,之前的文章虽然偶尔也有用到 USB 的地方,但是都是比较特定的应用,为了之后 USB 的应用,我们有必要从基础的概念起了解一下 USB 。
本文我们主要就是来了解一下与 USB 应用开发有关的基础概念以及 沁恒微蓝牙芯片 USB 应用的基本流程。
相关文章:
USB 设备描述符 配置描述符 报表描述符.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
目录
- 前言
- [一、 一些基本概念](#一、 一些基本概念)
-
- [1.1 USB 是什么?](#1.1 USB 是什么?)
- [1.2 USB 的基础结构](#1.2 USB 的基础结构)
-
- [1.2.1 USB 物理接口](#1.2.1 USB 物理接口)
- [1.2.2 USB 速度](#1.2.2 USB 速度)
- [1.3 USB 核心概念](#1.3 USB 核心概念)
-
- [1.3.1 端点(Endpoint)](#1.3.1 端点(Endpoint))
- [1.3.2 描述符(Descriptor)](#1.3.2 描述符(Descriptor))
- [1.3.3 控制传输(EP0 的通信规则)](#1.3.3 控制传输(EP0 的通信规则))
- [1.4 USB 枚举](#1.4 USB 枚举)
- [1.5 数据交互](#1.5 数据交互)
- [二、CH58x/CH59x USB应用](#二、CH58x/CH59x USB应用)
-
- [2.1 USB 应用基本步骤](#2.1 USB 应用基本步骤)
- [2.2 USB_DevTransProcess](#2.2 USB_DevTransProcess)
-
- [2.2.1 枚举](#2.2.1 枚举)
- [2.2.2 设备接收主机数据(OUT 方向)](#2.2.2 设备接收主机数据(OUT 方向))
- [2.3 设备发数据给主机(IN 方向)](#2.3 设备发数据给主机(IN 方向))
-
- [2.3.1 示例疑问说明](#2.3.1 示例疑问说明)
- 结语
一、 一些基本概念
说明,本文的目的是以应用为主 。
按照惯例,过一遍简单的基础概念。
1.1 USB 是什么?
USB :Universal Serial Bus (通用串行总线),就是主机(比如电脑 / 手机)和设备(比如U 盘、键盘、鼠标)之间通信的一套标准。
USB 角色分为 Host 和 Device:
Host(主机):
在 USB 总线通信中,Host(主机)永远处于主导地位,全权控制整个总线,所有通信都由主机发起,设备只能被动响应,不能主动发送数据。
常见的 Host :电脑 。支持 OTG 的手机平板可主机可从机 。
Device(设备):
Device 只能被动回复,设备自己不能主动上传数据。
Device(设备)必须等主机问才能进行数据交互 :主机发 IN → 设备才能回数据,主机发 OUT → 设备才能收数据。
常见的 Device :U 盘、键盘,鼠标等。
还有一种功能特殊的 Device :集线器 USB Hub , 也就是我们常用的一拖多扩展坞。 它既能被 Host 管理,又能帮 Host 转发数据并监控下游接口, Hub 是 Device,它必须挂在 Host 下面 。(本文倒是不需要太关注 Hub 设备)。
1.2 USB 的基础结构
本文讨论 USB 2.0,USB 3.0 不在本文讨论范围,所以下面的接口 和 速度都只列出 USB2.0 支持的范围。
1.2.1 USB 物理接口
USB 线里一共 4 根线(USB 2.0 ):
- VCC +5V
- D- 数据负
- D+ 数据正
- GND 地
USB 靠 D+、D- 差分信号进行数据传输,所以在 PCB 设计的时候,D+ ,D- 需要遵循差分走线的原则设计。
1.2.2 USB 速度
- Low-Speed 低速:1.5 Mbps
- Full-Speed 全速:12 Mbps
- High-Speed 高速:480 Mbps
USB 速度是由 USB 硬件控制器决定的,在枚举的时候通过 D+ D- 上的电平高速主机,这个在博主之前文章《 USB 设备描述符 配置描述符 报表描述符》 提到过,如下:

沁恒微 CH58x / CH59x 系列带有 USB 控制器,不需要自己额外加电阻。
1.3 USB 核心概念
1.3.1 端点(Endpoint)
端点 : USB 设备和主机之间用来收发数据的通道 。
每个 USB 设备都会有多个端点,编号从 0 开始:EP0、EP1、EP2......
每个端点有两个关键属性:
-
方向
IN:数据从 设备 → 主机(CH585 发给电脑)
OUT:数据从 主机 → 设备(电脑发给 CH585)
-
传输类型
传输类型决定这个通道怎么用,分为下面四种:
\
①、控制传输(Control)
只有 EP0 专用
用途:设备枚举、读取描述符、命令交互
②、中断传输(Interrupt)
用途:键盘、鼠标、游戏手柄等小数据、实时性高的设备
③、批量传输(Bulk)
用途:USB 虚拟串口、U 盘等大数据量、不追求极致实时的设备
④、同步传输(Isochronous)
用途:音频、视频设备
\
端点的固定规则:
- EP0 是默认控制端点,任何 USB 设备必须有 EP0,否则无法被主机识别
- EP1 及以上端点用于业务数据传输
- 一个端点号 + 方向 = 一个独立通道(例如 EP1 IN 和 EP1 OUT 是两个不同通道)
1.3.2 描述符(Descriptor)
描述符:设备的身份证,设备通过描述符告诉主机 自己是什么类型的设备,能做哪些工作,由哪些通道等等。
当你把 USB 设备插到电脑上时,主机完全不知道这是个什么设备。
它会通过 EP0 不停地问问题,设备必须用一套固定格式的数据来回答。
这套回答数据,就是描述符。
USB 标准里常用的描述符有 5 种:
- 设备描述符(Device Descriptor)
告诉主机:USB 版本、厂商 ID、产品 ID、端点 0 最大包长等 - 配置描述符(Configuration Descriptor)
告诉主机:设备功耗、接口数量、是否自供电等 - 接口描述符(Interface Descriptor)
告诉主机:这是 HID 键盘?还是串口?还是 U 盘? - 端点描述符(Endpoint Descriptor)
告诉主机:我用 EP1 IN 还是 OUT?是中断传输还是批量传输?包长多少? - 字符串描述符(String Descriptor)
厂商名、产品名、序列号等,给电脑显示用
枚举过程概括来说如下:
主机通过 EP0 发命令 → 设备依次返回各类描述符 → 主机识别成功 → 设备可以正常使用
具体描述符和枚举的更多说明可参考博主之前博文: USB 设备描述符 配置描述符 报表描述符
1.3.3 控制传输(EP0 的通信规则)
所有枚举、命令交互,都在 EP0 上通过控制传输完成。
下图是 USB 控制传输(Control Transfer)的完整结构:

一个完整的控制传输分为 3 个阶段:
- SETUP 阶段
主机先发 8 字节标准命令包:
bmRequestType(1) + bRequest(1) + wValue(2) + wIndex(2) + wLength(2)如下图:

- DATA 阶段
主机读数据时:设备把描述符 / 数据发给主机
有些命令没有数据,这一步可以省略 - STATUS 阶段
双方互相应答,表示本次传输完成
标志一次控制请求结束
比如主机读取设备描述符的典型命令:
0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x12, 0x00
含义就是:
0x06 = GET_DESCRIPTOR
0x0100 = 要设备描述符
0x0012 = 要读 18 字节
设备收到后,就把设备描述符通过 DATA 阶段发给主机,枚举就往前走了一步。
1.4 USB 枚举
枚举请直接参考博主之前文章:USB 设备描述符 配置描述符 报表描述符
这里就额外说明一下设备类型表格:
| Class 代码 | 名称 | 常见设备 |
|---|---|---|
| 0x01 | Audio | USB 耳机、麦克风、音箱 |
| 0x02 | CDC | 虚拟串口、Modem |
| 0x03 | HID | 键盘、鼠标、游戏手柄 |
| 0x08 | Mass Storage | U 盘、SD 读卡器 |
| 0x09 | Hub | USB 集线器 |
| 0x0A | CDC-Data | 配合 CDC 的数据接口 不算独立设备类,只是辅助 |
| 0xFF | Vendor Specific | 自定义设备 |
绝大多数设备类别 Class 代码写在 接口描述符 的 bInterfaceClass 字段,如下接口描述符结构(USB 标准 9 字节):
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0 | bLength |
描述符长度 = 9 |
| 1 | bDescriptorType |
类型 = 0x04 |
| 2 | bInterfaceNumber |
接口编号 (0, 1, 2...) |
| 3 | bAlternateSetting |
备用设置 |
| 4 | bNumEndpoints |
该接口有几个端点 |
| 5 | bInterfaceClass |
← Class 代码在这里 |
| 6 | bInterfaceSubClass |
子类 (如 CDC 有 ACM/NCM 等) |
| 7 | bInterfaceProtocol |
协议 (如 HID 有键盘/鼠标/游戏手柄) |
| 8 | iInterface |
字符串索引 |
其他比如 Hub 这种特殊设备类型会写在 设备描述符 里面,这里不做讨论。
1.5 数据交互
枚举完成以后,枚举完成后,主机已经知道设备有哪些端点(EP1、EP2...),方向 IN / OUT,传输类型(中断 / 批量),包大小等,主机就会按照这个规则,通过 IN 令牌包主动获取数据!
USB 始终遵循主机主导、设备被动响应的规则,数据通信不是随便发,不同传输类型行为如下表格:
| 传输类型 | 主机行为 | 设备需要做什么 |
|---|---|---|
| 控制传输 (Control) | 主机主动发起请求 | 设备被动响应,用于命令 / 枚举 |
| 中断传输 (Interrupt) | 主机按 bInterval 定时轮询 | 设备必须提前把数据放入缓冲区 |
| 批量传输 (Bulk) | 主机空闲 / 按需读取,不自动定时轮询 | 设备准备好数据,等待主机来取 |
| 同步传输 (Isochronous) | 每帧固定传输,保证带宽 | 数据必须按时准备,丢帧不重发 |
USB 标准只规定通道传输方式,不规定数据内容格式:
- HID 设备:数据格式由 HID 报告描述符定义(如键盘 8 字节格式)
- CDC 虚拟串口:应用层数据透明传输,但 USB 层面有 CDC 封装;控制命令有独立格式
- Vendor 自定义设备:完全由你自己定义格式(帧头、长度、数据、校验等)
注意:USB 硬件 CRC 只保护单个数据包,应用层建议加简单校验(如帧头+长度+校验和),防止分包重组错误或缓冲区溢出导致的数据异常。
其他说明:
- 主机发数据到设备(OUT):
①、主机主动发送数据到设备 OUT 端点
②、硬件接收并触发 USB 中断
③、设备必须及时读取数据,若不及时读,后续数据可能覆盖或丢失
④、缓冲区满时,硬件自动回复 NAK 告诉主机"忙",无需软件干预 - 在批量 / 中断传输中,必须注意一个关键点:
当发送的数据长度 = 端点最大包长(如 64 字节)时,必须额外发送一个 0 字节包(ZLP),ZLP 告诉主机'本次传输结束',否则主机会继续等待更多数据。
二、CH58x/CH59x USB应用
以 EVT 工程为例,说明一下 沁恒微蓝牙芯片上 USB 的使用框架。
本文只讨论全速 USB,沁恒微 蓝牙芯片基本都带有全速 USB,但是高速 USB 只存在于部分型号。
2.1 USB 应用基本步骤
第 1 步: 给每个端点指定缓冲区(自定义的数组)
c
/******** 用户自定义分配端点RAM ****************************************/
__attribute__((aligned(4))) uint8_t EP0_Databuf[64 + 64 + 64]; //ep0(64)+ep4_out(64)+ep4_in(64)
__attribute__((aligned(4))) uint8_t EP1_Databuf[64 + 64]; //ep1_out(64)+ep1_in(64)
//...初始化
HSECFG_Capacitance(HSECap_18p);
SetSysClock(SYSCLK_FREQ);
DebugInit(); //配置串口1用来prinft来debug
printf("start\n");
pEP0_RAM_Addr = EP0_Databuf; //配置缓存区64字节。
pEP1_RAM_Addr = EP1_Databuf;
用到几个端点,就定义几个端点缓冲区!
IN 和 OUT 共用一个缓冲区,所以是数组大小是 [64 + 64],第一个数组EP0_Databuf[64 + 64 + 64] 把 EP4 IN 也放在同一个缓冲区了,这里用到再来说明。
第 2 步: 调用官方底层初始化函数(配置寄存器、端点、DMA)
c
USB_DeviceInit();
void USB_DeviceInit(void)
{
R8_USB_CTRL = 0x00; // 先设定模式,取消 RB_UC_CLR_ALL
// 配置端点模式:允许收发, 0~4全开
R8_UEP4_1_MOD = RB_UEP4_RX_EN | RB_UEP4_TX_EN | RB_UEP1_RX_EN | RB_UEP1_TX_EN; // 端点4 OUT+IN,端点1 OUT+IN
R8_UEP2_3_MOD = RB_UEP2_RX_EN | RB_UEP2_TX_EN | RB_UEP3_RX_EN | RB_UEP3_TX_EN; // 端点2 OUT+IN,端点3 OUT+IN
// 绑定 DMA 地址(缓冲区硬件映射)
R32_UEP0_DMA = (uint32_t)pEP0_RAM_Addr;
R32_UEP1_DMA = (uint32_t)pEP1_RAM_Addr;
R32_UEP2_DMA = (uint32_t)pEP2_RAM_Addr;
R32_UEP3_DMA = (uint32_t)pEP3_RAM_Addr;
// 端点默认应答模式
/*
UEP_R_RES_ACK:OUT 主机发数据,设备默认应答 ACK
UEP_T_RES_NAK:IN 主机读数据,设备默认应答 NAK(表示还没准备好)
RB_UEP_AUTO_TOG:硬件自动处理数据同步位,你不用管
*/
R8_UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
R8_UEP1_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK | RB_UEP_AUTO_TOG;
R8_UEP2_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK | RB_UEP_AUTO_TOG;
R8_UEP3_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK | RB_UEP_AUTO_TOG;
R8_UEP4_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
R8_USB_DEV_AD = 0x00;
R8_USB_CTRL = RB_UC_DEV_PU_EN | RB_UC_INT_BUSY | RB_UC_DMA_EN; // 启动USB设备及DMA,在中断期间中断标志未清除前自动返回NAK
R16_PIN_CONFIG |= RB_PIN_USB_EN | RB_UDP_PU_EN; // 防止USB端口浮空及上拉电阻
R8_USB_INT_FG = 0xFF; // 清中断标志
R8_UDEV_CTRL = RB_UD_PD_DIS | RB_UD_PORT_EN; // 允许USB端口
R8_USB_INT_EN = RB_UIE_SUSPEND | RB_UIE_BUS_RST | RB_UIE_TRANSFER; // 使能中断:挂起、复位、传输完成
}
第 3 步: 打开 USB 中断
c
PFIC_EnableIRQ(USB_IRQn); //启用中断向量
第 4 步: 中断服务函数
中断服务函数里面就写了一个函数,这个函数就是所有 USB 总线事务处理的地方(当然你可以直接把交互逻辑写在 USB_IRQHandler 里面,封装成函数只是一种形式)。
c
void USB_IRQHandler(void)
{
USB_DevTransProcess(); // 官方库处理所有 USB 底层事务
}
第 5 步: USB事务处理 void USB_DevTransProcess( void )
此函数 就是 USB 枚举 + 数据交互的核心,主机发送的所有命令,请求,全部都在这里解析、处理、回复,它处理枚举 + 接收数据 (OUT) + 发送完成后的收尾工作 。
说明,设备 → 主机 (IN)的 发送不是在这里面进行的,
第 6 步: 发送数据
什么时候发送时应用层自己的事情,比如示例就在 while 里面循环发送做示例的。
把自己想要发送的数据放入端点的 IN buff,调用 DevEP1_IN_Deal (EP1 发送)函数 。
小结: 整体来说,使用起来就是先初始化,然后中断函数中调用 USB_DevTransProcess 进行事务处理,同时在你想要发送数据的地方把数据放入对应的 端点 IN buff:
c
//初始化
void app_usb_init()
{
pEP0_RAM_Addr = EP0_Databuf;
pEP1_RAM_Addr = EP1_Databuf;
pEP2_RAM_Addr = EP2_Databuf;
pEP3_RAM_Addr = EP3_Databuf;
USB_DeviceInit();
PFIC_EnableIRQ( USB_IRQn );
}
//中断函数
__INTERRUPT
__HIGH_CODE
void USB_IRQHandler( void ) /* USB中断服务程序,使用寄存器组1 */
{
USB_DevTransProcess();
}
//USB 传输处理函数
__HIGH_CODE
void USB_DevTransProcess( void )
{
//流程处理函数...
}
//发送数据,应用层循环发
memcpy(pEP1_IN_DataBuf, 数据, 长度);
DevEP1_IN_Deal(长度);
本文我们对几个关键的点进行说明: 枚举流程、收数据、发数据,让大家有个基础的认识。
首先大家要明确 USB 交互基本框架如下:
USB_DevTransProcess 负责 USB 总线协议本身,包括枚举、数据接收和发送后的状态恢复;
发送什么数据、什么时候发送,由用户应用程序自己决定。
2.2 USB_DevTransProcess
此函数在官方 CH585 EVT 的 USB HID 示例中的的代码有详细的注释,需要了解详情大家可以自行下载查看,如下图:


虽然这个示例函数内容很多,但是大家不要怕,因为官方已经写好了示例,所有 USB 的应用框架都是大差不差的,我们只需要学习在必要的地方修改就可以了。
2.2.1 枚举
对于同一型号甚至同一系列芯片,不同 USB 应用 枚举代码都是通用、固定的,应用中基本不用修改此处逻辑,只需要学会不同设备写不同的 描述符。
枚举部分,我们简单说明一下,枚举流程如下:
1.插入 → 2. 检测到 D+ 上拉 → 3. 总线复位 → 4. 获取设备描述符 → 5. 设置地址 → 6. 获取配置描述符 → 7. 设置配置 → 8. 设备就绪
第一步:总线复位(RB_UIF_BUS_RST)
主机行为:插入设备后,先复位总线,让设备回到初始状态。
c
else if(intflag & RB_UIF_BUS_RST) //判断_INT_FG中的总线复位标志位,为1触发
{
R8_USB_DEV_AD = 0; //设备地址写成0,待主机重新分配给设备一个新地址
R8_UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK; //把端点0的控制寄存器,写成:接收响应响应ACK表示正常收到,发送响应NAK表示没有数据要返回
R8_UEP1_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
R8_UEP2_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
R8_UEP3_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
R8_USB_INT_FG = RB_UIF_BUS_RST; //写1清中断标识
}
第二步:SETUP 包处理(RB_UIS_SETUP_ACT)
这是枚举核心,主机通过 EP0 发送 SETUP 包,设备解析后响应。
c
if(R8_USB_INT_ST & RB_UIS_SETUP_ACT) // Setup包处理
{
/*
准备 EP0 响应:DATA1 开始,接收 ACK,发送 NAK
R响应OUT事务期待为DATA1(DMA收到的数据包的PID要为DATA1,否则算数据错误要重传)和ACK(DMA相应内存中收到了数据,单片机验收正常)
T响应IN事务设定为DATA1(单片机有数据送入DMA相应内存,以DATA1发送出去)和NAK(单片机没有准备好数据)。
*/
R8_UEP0_CTRL = RB_UEP_R_TOG | RB_UEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_NAK;
// 解析 SETUP 包内容(8 字节标准格式)
SetupReqLen = pSetupReqPak->wLength; //主机想要多少数据
SetupReqCode = pSetupReqPak->bRequest; //命令的序号,什么命令(如 GET_DESCRIPTOR)
chtype = pSetupReqPak->bRequestType; //包含数据传输方向、命令的类型、接收的对象等信息
// ... 下面根据 SetupReqCode 分支处理
第三步:具体请求处理(switch(SetupReqCode))
- 获取描述符
USB_GET_DESCRIPTOR
主机要什么, 设备就给什么:
c
case USB_GET_DESCRIPTOR:
switch(((pSetupReqPak->wValue) >> 8)) // 看高 8 位,决定要什么描述符
{
case USB_DESCR_TYP_DEVICE: // 0x01:设备描述符
pDescr = MyDevDescr; // 指向设备描述符数组
len = MyDevDescr[0]; // 第 0 字节是长度
break;
case USB_DESCR_TYP_CONFIG: // 0x02:配置描述符
pDescr = MyCfgDescr; // 指向配置描述符数组
len = MyCfgDescr[2]; // 第 2、3 字节是总长度
break;
case USB_DESCR_TYP_STRING: // 0x03:字符串描述符
switch((pSetupReqPak->wValue) & 0xff) // 看低 8 位,字符串索引
{
case 0: pDescr = MyLangDescr; break; // 语言 ID
case 1: pDescr = MyManuInfo; break; // 厂商名
case 2: pDescr = MyProductInfo; break; // 产品名
// ...
}
break;
case USB_DESCR_TYP_HID: // 0x21:HID 类描述符(HID 设备才有)
// ...
break;
case USB_DESCR_TYP_REPORT: // 0x22:HID 报告描述符(HID 设备才有)
pDescr = HIDDescr;
len = sizeof(HIDDescr);
break;
}
// 准备发送:拷贝到 EP0 缓冲区
if(SetupReqLen > len) SetupReqLen = len; // 主机要的 > 实际有的,给实际长度
len = (SetupReqLen >= DevEP0SIZE) ? DevEP0SIZE : SetupReqLen; // 最多 64 字节
memcpy(pEP0_DataBuf, pDescr, len); // 拷到 DMA 缓冲区
pDescr += len; // 指针后移,为长数据分包准备
// 注意:这里只准备第一包,剩余的在 UIS_TOKEN_IN 里继续发
| 主机请求 | wValue 高 8 位 | 我们给 |
|---|---|---|
| 设备描述符 | 0x01 | MyDevDescr(VID/PID/版本等) |
| 配置描述符 | 0x02 | MyCfgDescr(接口、端点、类定义) |
| 字符串描述符 | 0x03 | 语言/厂商/产品名 |
| HID 描述符 | 0x21 | HID 类特定信息 |
| 报告描述符 | 0x22 | 按键格式定义(HID 才有) |
- 设置地址
USB_SET_ADDRESS
地址在 SET_ADDRESS 请求的状态阶段完成后 才生效。所以这里先存起来,等 UIS_TOKEN_IN 里再写。
c
case USB_SET_ADDRESS: //主机想设置设备地址
// 注意:这里不立即写 R8_USB_DEV_AD,而是在 IN 事务完成后再写!
SetupReqLen = (pSetupReqPak->wValue) & 0xff; //将主机分发的位设备地址暂存在SetupReqLen中
break; //控制阶段会赋值给设备地址参数
- 获取/设置配置
USB_GET_CONFIGURATION/USB_SET_CONFIGURATION
SET_CONFIGURATION 后,设备从 Address 状态 → Configured 状态,数据端点才能用。
c
case USB_GET_CONFIGURATION: // 主机问:你当前配置号是多少?
pEP0_DataBuf[0] = DevConfig; // 通常是 1
if(SetupReqLen > 1) SetupReqLen = 1;
break;
case USB_SET_CONFIGURATION: // 主机说:用配置号 x
DevConfig = (pSetupReqPak->wValue) & 0xff;
// 配置设置后,非 0 端点(EP1/2/3...)才真正激活!
Ready = 1; // 可选:标记枚举完成,可以开始发数据了
break;
- 清除/设置特性
USB_CLEAR_FEATURE/USB_SET_FEATUR
c
case USB_CLEAR_FEATURE: // 主机清除某个特性,如端点 STALL
if((pSetupReqPak->bRequestType & USB_REQ_RECIP_MASK) == USB_REQ_RECIP_ENDP)
{
switch((pSetupReqPak->wIndex) & 0xff) // 看端点号
{
case 0x81: // EP1 IN
R8_UEP1_CTRL = (R8_UEP1_CTRL & ~(RB_UEP_T_TOG | MASK_UEP_T_RES)) | UEP_T_RES_NAK;
// 清除 STALL,恢复 NAK
break;
case 0x01: // EP1 OUT
R8_UEP1_CTRL = (R8_UEP1_CTRL & ~(RB_UEP_R_TOG | MASK_UEP_R_RES)) | UEP_R_RES_ACK;
break;
}
}
break;
第四步:SETUP 处理后的状态机
c
if(errflag == 0xff) // 错误或不支持的请求
{
R8_UEP0_CTRL = RB_UEP_R_TOG | RB_UEP_T_TOG | UEP_R_RES_STALL | UEP_T_RES_STALL;
// STALL:告诉主机"我不支持这个"
}
else
{
if(chtype & 0x80) // 方向是 IN(设备→主机),准备上传数据
{
len = (SetupReqLen > DevEP0SIZE) ? DevEP0SIZE : SetupEP0Len;
SetupReqLen -= len; // 记录还剩多少要发(长数据分包)
}
else
len = 0; // 方向是 OUT,没有数据要上传
R8_UEP0_T_LEN = len; // 设置本次发送长度
R8_UEP0_CTRL = RB_UEP_R_TOG | RB_UEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
// 准备响应:接收 ACK,发送 ACK(有数据时)
}
第五步:IN 事务继续发送长数据(UIS_TOKEN_IN)
c
case UIS_TOKEN_IN: // EP0 IN,主机来取数据
switch(SetupReqCode)
{
case USB_GET_DESCRIPTOR: // 描述符太长,分包发
if(SetupReqLen > 0) // 还有剩余数据
{
len = SetupReqLen >= DevEP0SIZE ? DevEP0SIZE : SetupReqLen;
memcpy(pEP0_DataBuf, pDescr, len); // 拷下一包
SetupReqLen -= len;
pDescr += len;
R8_UEP0_T_LEN = len;
R8_UEP0_CTRL ^= RB_UEP_T_TOG; // 翻转 DATA0/1
// 注意:这里没加 | UEP_T_RES_ACK,因为硬件会自动响应
}
else // 发完了
{
R8_UEP0_T_LEN = 0; // 0 长度包结束
R8_UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
}
break;
case USB_SET_ADDRESS: // 延迟写地址,现在生效
R8_USB_DEV_AD = (R8_USB_DEV_AD & RB_UDA_GP_BIT) | SetupReqLen;
R8_UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
break;
default: // 其他请求的状态阶段
R8_UEP0_T_LEN = 0;
R8_UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
break;
}
break;
到这里枚举流程就说完了。
对于初学者,这里可能会有点懵,比如说,USB_GET_DESCRIPTOR 和 USB_SET_ADDRESS 分支有两处地方,到底哪里是获取描述符,哪里是真正设置地址的?
这里呢,就是上文 <1.3.3 控制传输(EP0 的通信规则)>小节说明的问题:

它们是同一个命令的不同阶段, USB 控制传输分阶段:SETUP 中断解析命令并准备数据,IN 中断实际发送数据,如下表格说明:
| case | SETUP 分支做什么 | IN 分支做什么 | 关系 |
|---|---|---|---|
USB_SET_ADDRESS |
保存地址到变量 | 写入地址寄存器 | 配合完成 |
USB_GET_DESCRIPTOR |
准备第一包数据 | 继续发剩余数据(或结束) | 配合完成 |
USB_SET_CONFIGURATION |
保存配置值 | (通常无操作,或置 Ready) | 配置生效 |
流程示意图如下:

2.2.2 设备接收主机数据(OUT 方向)
数据接收相对来说简单多了,设备接收到主机数据会触发 USB 中断 ,直接进入对应分支:
c
case UIS_TOKEN_OUT : // 端点0 OUT
case UIS_TOKEN_OUT | 1: // 端点1 OUT
case UIS_TOKEN_OUT | 2 : // 端点2 OUT
示例代码如下:
c
case UIS_TOKEN_OUT: // EP0 OUT,枚举用(如 SETUP 后的数据阶段)
{
len = R8_USB_RX_LEN; // 读取接收长度
// 处理 EP0 数据...
}
break;
case UIS_TOKEN_OUT | 1: // EP1 OUT,应用数据传输
{
// 检查数据同步(DATA0/DATA1 是否匹配预期)
if(R8_USB_INT_ST & RB_UIS_TOG_OK)
{
R8_UEP1_CTRL ^= RB_UEP_R_TOG; // 翻转 DATA0/1(无 AUTO_TOG 时)
len = R8_USB_RX_LEN; // 读取接收长度
DevEP1_OUT_Deal(len); // 你的处理函数!
// 硬件自动回 ACK,准备接收下一包
}
}
break;
示例中 R8_UEP1_CTRL ^= RB_UEP_R_TOG; 应该是多余的不需要,这个会在下文示例疑问说明里面分析(因为博主先写的下面部分= =!)
流程很简单,需要说明的是,数据处理要快,不要在这个中断函数中处理数据 ,建议搬运完成在主循环处理。
比如:
c
// 中断里只做一件事:快速搬运到安全缓冲区
void DevEP1_OUT_Deal(uint8_t len)
{
// 快速拷到环形缓冲区,立即返回
for(i = 0; i < len && !RingBuffer_Full(); i++) {
RingBuffer_Write(pEP1_OUT_DataBuf[i]);
}
// 复杂处理放到主循环,不在中断里做
}
// 主循环处理
while(1) {
if(RingBuffer_Available()) {
data = RingBuffer_Read();
Process_Data(data); // 耗时操作在这里做
}
}
2.3 设备发数据给主机(IN 方向)
前面说到了,什么时候发送数据时应用层的行为。 USB_DevTransProcess 里面只触发发送完成中断。
发数据是主动行为,
写缓冲区 → 清 NAK → 等主机来取 → 发完中断里再置 NAK
USB_DevTransProcess 里的 case UIS_TOKEN_IN | 1表示主机刚刚读完了 EP1 IN 的数据,数据发送完成,在这里面是发送完成收尾:数据发完了 变回 NAK → 等待下一次发送把状态恢复成 NAK,准备下一次发送。
在 HID 示例中,在主循环 while(1) 中循环执行:
c
//循环发送
while(1)
{//模拟传输4个字节的数据,实际传输根据用户需要自行修改
if(Ready)
{
Ready = 0;
DevHIDReport(0x05, 0x10, 0x20, 0x11);
}
mDelaymS(100);
...
//函数实现
void DevHIDReport(uint8_t data0,uint8_t data1,uint8_t data2,uint8_t data3)
{
HID_Buf[0] = data0;
HID_Buf[1] = data1;
HID_Buf[2] = data2;
HID_Buf[3] = data3;
memcpy(pEP1_IN_DataBuf, HID_Buf, sizeof(HID_Buf));
DevEP1_IN_Deal(DevEP1SIZE);
}
然后在USB_DevTransProcess 中的 UIS_TOKEN_IN | 1 中断分支处理发送完成的处理 :
c
case UIS_TOKEN_IN | 1: //令牌包的PID为IN,端点号为1
R8_UEP1_CTRL ^= RB_UEP_T_TOG; //IN事务的DATA切换一下。设定将要发送的包的PID。
R8_UEP1_CTRL = (R8_UEP1_CTRL & ~MASK_UEP_T_RES) | UEP_T_RES_NAK; //当DMA中没有由单片机更新数据时,将T响应IN事务置为NAK。更新了就发出数据。
Ready = 1;
PRINT("Ready_IN_EP1 = %d\n",Ready);
break;
2.3.1 示例疑问说明
上面面是 HID_CompliantDev 示例中的方式,其中应该是多了一句 R8_UEP1_CTRL ^= RB_UEP_T_TOG; ,因为在 USB 初始化的函数USB_DeviceInit 里面有这么一句:
c
R8_UEP1_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK | RB_UEP_AUTO_TOG;
已经开启了RB_UEP_AUTO_TOG 硬件自动反转,所以中断里面的手动反转应该是多余的,而且不能加,加了数据间歇性丢包或者其他一些问题(为什么反转:USB 发送必须,一次 DATA0,一次 DATA1,交替进行,这是 USB 协议的握手机制,用于检测丢包和重传。)。
我查阅了其他的一些使用到 USB 的例程,比如 BLE_USB 和 Direct_Test_Mode ,都是不带HID 示例那行代码,如下:
c
case UIS_TOKEN_IN | 1 :
R8_UEP1_CTRL = ( R8_UEP1_CTRL & ~MASK_UEP_T_RES ) | UEP_T_RES_NAK;
break;
只要初始化的时候带了RB_UEP_AUTO_TOG 参数,那么这个端点的发送完毕后就不需要手动 ^= RB_UEP_T_TOG 。
好了,这里疑问不影响我们说明 USB 的整体发送流程,发送数据的核心操作如下,在应用层自己想要发数据的时候直接调用即可 :
c
// 应用层随时调用,准备发送数据
memcpy(pEP1_IN_DataBuf, 你的数据, 长度); // 写入 DMA 缓冲区
DevEP1_IN_Deal(长度);
示例:
void DevEP1_OUT_Deal(uint8_t l)
{ /* 用户可自定义 */
uint8_t i;
for(i = 0; i < l; i++)
{
pEP1_IN_DataBuf[i] = ~pEP1_OUT_DataBuf[i];
}
DevEP1_IN_Deal(l);
}
结语
本文我们从最基本的概念,到应用流程说明了一下 USB 基本使用,同时结合示例代码说明了一下 USB 的应用代码结构 ,希望本文能让大家对沁恒微 USB 应用有一个清晰的认知。
与蓝牙应用类似,官方代码给我们搭建好了基本的框架,我们只要了解了这个现有框架,知道基本的流程,数据收发的处理,就已经可以满足大部分的基础应用了(当然要记得结合博主之前的讲解描述符的文章《 USB 设备描述符 配置描述符 报表描述符》哦 (●'◡'●) )。
好了,本文就到这里。谢谢大家!