沁恒微蓝牙芯片 USB 应用开发入门

复制代码
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 个阶段:

  1. SETUP 阶段
    主机先发 8 字节标准命令包:
    bmRequestType(1) + bRequest(1) + wValue(2) + wIndex(2) + wLength(2) 如下图:
  2. DATA 阶段
    主机读数据时:设备把描述符 / 数据发给主机
    有些命令没有数据,这一步可以省略
  3. 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))

  1. 获取描述符 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 才有)
  1. 设置地址 USB_SET_ADDRESS

地址在 SET_ADDRESS 请求的状态阶段完成后 才生效。所以这里先存起来,等 UIS_TOKEN_IN 里再写。

c 复制代码
case USB_SET_ADDRESS:       //主机想设置设备地址
      // 注意:这里不立即写 R8_USB_DEV_AD,而是在 IN 事务完成后再写!
      SetupReqLen = (pSetupReqPak->wValue) & 0xff;    //将主机分发的位设备地址暂存在SetupReqLen中
      break;                                          //控制阶段会赋值给设备地址参数
  1. 获取/设置配置 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;
  1. 清除/设置特性 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_DESCRIPTORUSB_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 设备描述符 配置描述符 报表描述符》哦 (●'◡'●) )。

好了,本文就到这里。谢谢大家!

相关推荐
ZenasLDR12 天前
Type-C接口LDR多协议取电芯片
接口·芯片·usb
矜辰所致25 天前
沁恒微 RISC-V 蓝牙应用中常用蓝牙参数的设定和修改
蓝牙·沁恒微蓝牙·ble 蓝牙参数·连接参数设置·广播参数设置
ZenasLDR1 个月前
LDR系列PD协议控制芯片
接口·芯片·usb
CheungChunChiu1 个月前
USB‑C PD 充电系统完整解析(SC8886 + FUSB302)
linux·usb·type-c·充电
一个平凡而乐于分享的小比特2 个月前
USB Wi-Fi 三模式详解:Station、AP与Ad-Hoc
wifi·usb·ad-hoc·ap·station
遇雪长安2 个月前
高通安卓设备DIAG端口启用指南
android·adb·usb·dm·qpst·diag·qxdm
xhBruce2 个月前
Android USB 存储 冷启动(开机自动插着 U 盘)场景
android·usb·vold
Industio_触觉智能2 个月前
触觉智能RV1126B核心板配置USB复合设备(下)
串口·acm·开发板·usb·rv1126b·ums·usb存储
嵌入式×边缘AI:打怪升级日志3 个月前
USBX虚拟串口源码分析与改造笔记
笔记·学习笔记·usb