沁恒微蓝牙芯片 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 设备描述符 配置描述符 报表描述符》哦 (●'◡'●) )。

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

相关推荐
Championship.23.243 天前
Linux 3.0 USB机制深度解析:USB 3.0支持与传统外设驱动架构
linux·运维·架构·usb
ZenasLDR9 天前
Type-C接口iPad键盘皮套
接口·芯片·usb
smallerxuan9 天前
九、CherryUSB 设计架构与工作逻辑分析
usb·cherryusb·cherryusb分析
smallerxuan10 天前
二、USB协议中的设备类
usb·usb协议·usb设备类
smallerxuan12 天前
三、USB协议通信过程
usb·usb协议·usb通信过程
smallerxuan12 天前
七、USB协议中的事务
usb·usb协议·usb事务
smallerxuan12 天前
五、USB协议中的请求
usb·usb协议·usb请求
smallerxuan12 天前
八、USB协议分析与调试实战
usb·usb协议分析·usb协议·usb协议调测
smallerxuan12 天前
四、USB协议中的描述符
usb·usb协议·usb描述符
ZenasLDR15 天前
Type-C接口水冷散热器
接口·芯片·usb