2025-04-29
电子
0

目录

移植 CherryUSB 协议栈
使用CubeMX创建工程
拉取 CherryUSB 协议栈源码
拷贝所需代码到项目源码
1. class文件夹:
2. common文件夹
3. core文件夹
4. port文件夹
使用fsdev
使用dwc2
添加 CherryUSB 协议栈初始化函数到main.c
编辑usb_config.h
使用fsdev
使用dwc2
添加CherryUSB协议栈初始化函数
对接LetterShell终端与USB CDC

近日,从前一篇文章的原作者那边了解到了一个名为 Letter Shell 的轻量型终端,可以非常方便地移植部署到 STM32 上。但是,由于使用的是J-Link RTT Viewer作为命令行终端进行交互,如果需要使用通用的终端软件(例如MobaXterm)时,就必需要借助一个SEGGER RTT转Telnet的转换软件,感觉甚是不便。

正好最近在研究 STM32USB 协议,USB 作为通用传输协议,可支持枚举非常多类型的设备,其中就有 CDC(虚拟串口) 设备,那么,我们是不是可以将 Letter Shell 终端对接到 USB 的 CDC 设备上就可以实现用一根 USB 连接线插入电脑后直接用支持串口连接的终端软件进行交互呢?

本文基于 CherryUSB 1.4.4 进行移植,开发环境使用 VSCode EIDE + MakeFile + GCC编译器 ,类似开发环境的可以作为参考。

本文所有内容均基于STM32裸机开发,没有使用任何OS。

作者并非专业软件开发者,文中难免有些地方用词或说法不严谨,仅供参考,还请见谅。

移植 CherryUSB 协议栈

本部分参考官方文档的基于现有demo快速验证>基于ST系列芯片

因为ST官方的USB协议栈中间层写的有些💩,所以我此次使用的是目前仍在持续更新的开源嵌入式USB主从协议栈—— CherryUSB官方文档写的还是比较详细的,但因为要兼容很多平台的嵌入式设备所以显得有些零碎,如果只是要移植单一平台的话还需要来回翻找,初学者 (没错就是我) 看起来可能会有点晕,所以本篇会手把手一步步地介绍移植过程。

提示

如果本身有较丰富开发经验的也可以直接参照官方文档进行移植,因为目前这个协议栈还在持续更新,所以本篇博客并不能保证长期有效性

此外还有一点需要注意,截至本篇博客撰写时,前文所引用的官方文档网页仍是1.4.3版本的使用文档,而GitHub仓库目前已更新至1.4.4版本,两版本间的移植流程有细微区别,建议直接参考拉取的最新项目源码文件夹内的 /docs/source/quick_start/ 文件夹下的文档进行移植。

目前互联网上我找到的比较详细的几篇 CherryUSB 移植教程:

这几篇文章的作者分别使用 CLionKeilVisual Studio 进行开发,前两篇分别涉及使用了fsdevdwc2IP的STM32芯片,可以先阅读以上文章进行了解。

使用CubeMX创建工程

这边没有太多可说的,请直接参考官方文档

唯二需要注意的是本文的例程使用 MakeFile+GCC 进行编译,所以在“Project Manager>Project”部分的“Toolchain/IDE”需要选择“MakeFile”;以及堆栈大小需要给大些。

image.png

拉取 CherryUSB 协议栈源码

首先确保电脑上已正确安装git,使用命令行将工作目录切换到用于保存CherryUSB源码的目录下,使用git clone命令拉取该项目GitHub仓库的源码:

bash
git clone https://github.com/cherry-embedded/CherryUSB.git

如果没有科学上网环境,也可以使用Gitee仓库拉取:

bash
git clone https://gitee.com/woodman_112/CherryUSB.git

这两个仓库均由官方开发者维护,但是GitHub仓库更新频率会更高些。

拉取的项目源码文件目录结构应当如下:

CherryUSB ├─.github │ ├─ISSUE_TEMPLATE │ └─workflows ├─class │ ├─adb │ ├─aoa │ ├─audio │ ├─cdc │ ├─dfu │ ├─hid │ ├─hub │ ├─midi │ ├─msc │ ├─template │ ├─vendor │ │ ├─net │ │ ├─serial │ │ ├─wifi │ │ └─xbox │ ├─video │ └─wireless ├─common ├─core ├─demo │ ├─adb │ └─bootuf2 ├─docs │ ├─assets │ └─source │ ├─api │ │ └─img │ ├─class │ ├─demo │ ├─quick_start │ │ └─img │ ├─support │ │ └─img │ ├─tools │ │ └─img │ ├─usb │ │ └─img │ └─usbip ├─osal │ └─idf ├─platform │ ├─lvgl │ ├─none │ ├─nuttx │ ├─rtthread │ └─threadx ├─port │ ├─aic │ ├─bouffalolab │ ├─ch32 │ ├─chipidea │ ├─dwc2 │ ├─ehci │ ├─fsdev │ ├─hpm │ ├─kinetis │ ├─musb │ ├─nuvoton │ ├─ohci │ ├─pusb2 │ │ ├─freertos │ │ ├─rt-thread │ │ └─standalone │ ├─rp2040 │ ├─template │ └─xhci │ └─phytium │ ├─freertos │ └─rt-thread ├─third_party │ ├─cherrymp │ ├─cherryrb │ ├─dhcp-server │ ├─dns-server │ ├─fatfs-0.14 │ │ └─source │ │ └─port │ ├─FreeRTOS-10.4 │ │ ├─include │ │ └─portable │ │ ├─GCC │ │ │ ├─ARM_CM0 │ │ │ ├─ARM_CM3 │ │ │ ├─ARM_CM3_MPU │ │ │ ├─ARM_CM4F │ │ │ ├─ARM_CM4_MPU │ │ │ └─ARM_CM7 │ │ │ └─r0p1 │ │ ├─IAR │ │ │ ├─ARM_CM0 │ │ │ ├─ARM_CM3 │ │ │ ├─ARM_CM4F │ │ │ ├─ARM_CM4F_MPU │ │ │ └─ARM_CM7 │ │ │ └─r0p1 │ │ ├─MemMang │ │ └─RVDS │ │ ├─ARM_CM0 │ │ ├─ARM_CM3 │ │ ├─ARM_CM4F │ │ ├─ARM_CM4_MPU │ │ └─ARM_CM7 │ │ └─r0p1 │ ├─lwip-2.1.2 │ ├─nimble-1.6.0 │ │ ├─nimble │ │ └─porting │ │ ├─nimble │ │ │ ├─include │ │ │ │ ├─mem │ │ │ │ ├─nimble │ │ │ │ ├─os │ │ │ │ ├─stats │ │ │ │ ├─sysflash │ │ │ │ └─sysinit │ │ │ └─src │ │ └─npl │ │ └─freertos │ │ ├─include │ │ │ ├─console │ │ │ ├─nimble │ │ │ └─syscfg │ │ └─src │ └─zephyr_bluetooth-2.7.5 │ └─zephyr_bluetooth └─tools ├─audacity ├─chryusb_configurator ├─packet capture ├─stm32_dfuse ├─test_srcipts └─uf2

拷贝所需代码到项目源码

拉取的项目源码中有非常多的文件,但是对于本部分,我们只需关注classcommoncoreport这几个文件夹下的内容。

文件夹名用途
classUSB枚举设备类型,里面有很多子文件夹,一个子文件夹对应一种USB设备类型,根据需要进行选择保留
commonCherryUSB内部使用的一些代码,我们对里面的内容无需做任何更改
coreCherryUSB核心代码,我们对里面的内容无需做任何更改
port用于对接设备 USB IP 的相关代码,与设备所使用的 USB PHY 类型相关,要根据实际进行选择,STM32 中所使用的为 dwc2fsdev 其中的一种,可根据 CubeMX 配置项或手册进行判断

虽然有的教程说可以一股脑把全部CherryUSB源码都复制到项目目录下,但是为了保证项目目录的整洁,我个人还是建议仅保留需要的源码文件到项目内即可。

1. class文件夹

本文参考 CherryUSB 例程实现在单一USB端口上枚举CDC(虚拟串口)和MSC(大容量存储设备)两种不同类型的USB设备。所以class目录下理应只需要保留cdcmsc两个文件夹即可,但是core文件夹下的usbh_core.h头文件中包含了hub文件夹中的文件,所以不论需要枚举什么类型的设备hub文件夹都应当保留,不然编译时可能会出现“无法找到对应文件”的错误。 image.png

2. common文件夹

不用删改,全部保留。

3. core文件夹

不用删改,全部保留。

4. port文件夹

根据设备实际的 USB IP 来选择。这里介绍根据CubeMX中配置项来判断的方法。

使用fsdev
  • 如果CubeMX中“Connectivity”选项栏下为“USB”选项,则该芯片使用的 USB IP 为fsdev。这里以STM32L443RCT6为例:

image.png

  • 如果芯片是使用这个IP的话就直接保留port/fsdev文件夹即可,不用做其他修改

  • 最后保留的目录结构应当如下:

CherryUSB ├─class │ ├─cdc │ │ └─hub ├─common ├─core └─port └─fsdev
  • 将保留的源码放到项目目录下合适位置,并且在EIDE工程配置中将上述文件夹添加到源码目录和包含目录即可。
使用dwc2
  • 如果 CubeMX 中“Connectivity”选项栏下为“USB_OTG_FS”和“USB_OTG_HS”之中的两项或一项,则该芯片使用的 USB IP 为dwc2。这里以STM32F401CEU6为例:

image.png

  • 如果芯片是使用这个IP的话则需要注意,在port/dwc2文件夹下,有多个名为usb_glue_xxx.c的文件,这是用于对接不同MCU平台的代码,对于STM32仅需保留ST结尾的glue源码文件,不然会导致在编译时出现一堆“未知变量类型”或“函数重复定义”之类的奇怪错误。

image.png

image.png

  • 最后保留的目录结构应当如下:
CherryUSB ├─class │ ├─cdc │ │ └─hub ├─common ├─core └─port └─dwc2
  • 将保留的源码放到项目目录下合适位置,并且在EIDE工程配置中将上述文件夹添加到源码目录和包含目录即可。

提示

对于使用了dwc2的 STM32 芯片这里还有一点需要注意,虽然有些型号的配置选项中有“USB_OTG_HS”选项,但是实际上 MCU 内集成的 USB PHY 仅支持 FS(Full Speed 12Mbps),而如果想要实现 HS(High Speed 480Mbps)必须要外挂支持 HS 的 PHY 芯片,并通过 ULPI(USB Link Power Interface)与 MCU 的 USB_OTG_HS 接口连接才能真正实现 High Speed USB。仅有少部分高端型号(F7、H7系列)拥有真正的内置HS USB PHY,而无需外挂PHY芯片来实现。

添加 CherryUSB 协议栈初始化函数到main.c

将CherryUSB源码准备完毕后即可准备将初始化函数添加到main.c中,从而实现使用CherryUSB协议栈初始化STM32的USB外设,让电脑之类的主设备(Host)正确识别STM32所枚举的USB从设备(Device)。

在这一步我们需要关注CherryUSB源码根目录下的 cherryusb_config_template.h 文件和 demo 文件夹下的内容。

编辑usb_config.h

将CherryUSB根目录下的cherryusb_config_template.h文件复制到 Core\ Inc 文件夹中,并重命名为usb_config.h

该文件用于保存CherryUSB的配置参数,对此我们只需关注如下部分配置项:

c
/* ================ USB Device Port Configuration ================*/ #ifndef CONFIG_USBDEV_MAX_BUS #define CONFIG_USBDEV_MAX_BUS 1 // for now, bus num must be 1 except hpm ip #endif #ifndef CONFIG_USBDEV_EP_NUM #define CONFIG_USBDEV_EP_NUM 8 #endif /* When your chip hardware supports high-speed and wants to initialize it in high-speed mode, the relevant IP will configure the internal or external high-speed PHY according to CONFIG_USB_HS. */ // #define CONFIG_USB_HS /* ---------------- FSDEV Configuration ---------------- */ //#define CONFIG_USBDEV_FSDEV_PMA_ACCESS 2 // maybe 1 or 2, many chips may have a difference /* ---------------- DWC2 Configuration ---------------- */ /* (5 * number of control endpoints + 8) + ((largest USB packet used / 4) + 1 for * status information) + (2 * number of OUT endpoints) + 1 for Global NAK */ // #define CONFIG_USB_DWC2_RXALL_FIFO_SIZE (1024 / 4) /* IN Endpoints Max packet Size / 4 */ // #define CONFIG_USB_DWC2_TX0_FIFO_SIZE (64 / 4) // #define CONFIG_USB_DWC2_TX1_FIFO_SIZE (1024 / 4) // #define CONFIG_USB_DWC2_TX2_FIFO_SIZE (64 / 4) // #define CONFIG_USB_DWC2_TX3_FIFO_SIZE (64 / 4) // #define CONFIG_USB_DWC2_TX4_FIFO_SIZE (0 / 4) // #define CONFIG_USB_DWC2_TX5_FIFO_SIZE (0 / 4) // #define CONFIG_USB_DWC2_TX6_FIFO_SIZE (0 / 4) // #define CONFIG_USB_DWC2_TX7_FIFO_SIZE (0 / 4) // #define CONFIG_USB_DWC2_TX8_FIFO_SIZE (0 / 4) // #define CONFIG_USB_DWC2_DMA_ENABLE /* ---------------- MUSB Configuration ---------------- */ // #define CONFIG_USB_MUSB_SUNXI

每一部分的作用注释已经标注清楚了。

使用fsdev
  • 如果设备使用的是 fsdev IP,那么需要在配置文件中实现以下宏:
c
#ifndef CONFIG_USBDEV_EP_NUM #define CONFIG_USBDEV_EP_NUM 8 // 设备支持的最大USB端点(End Point)数,STM32一般为8个,需要根据具体设备判断 #endif /* ---------------- FSDEV Configuration ---------------- */ #define CONFIG_USBDEV_FSDEV_PMA_ACCESS 2
使用dwc2
  • 如果设备使用的是 dwc2 IP,那么需要在配置文件中实现以下宏:
c
#ifndef CONFIG_USBDEV_EP_NUM #define CONFIG_USBDEV_EP_NUM 8 // 设备支持的最大USB端点(End Point)数,STM32一般为8个,需要根据具体设备判断 #endif /* ---------------- DWC2 Configuration ---------------- */ /* (5 * number of control endpoints + 8) + ((largest USB packet used / 4) + 1 for * status information) + (2 * number of OUT endpoints) + 1 for Global NAK */ #define CONFIG_USB_DWC2_RXALL_FIFO_SIZE (1024 / 4) /* IN Endpoints Max packet Size / 4 */ #define CONFIG_USB_DWC2_TX0_FIFO_SIZE (64 / 4) #define CONFIG_USB_DWC2_TX1_FIFO_SIZE (64 / 4) #define CONFIG_USB_DWC2_TX2_FIFO_SIZE (64 / 4) #define CONFIG_USB_DWC2_TX3_FIFO_SIZE (64 / 4) #define CONFIG_USB_DWC2_TX4_FIFO_SIZE (64 / 4) #define CONFIG_USB_DWC2_TX5_FIFO_SIZE (64 / 4) #define CONFIG_USB_DWC2_TX6_FIFO_SIZE (64 / 4) #define CONFIG_USB_DWC2_TX7_FIFO_SIZE (64 / 4) #define CONFIG_USB_DWC2_TX8_FIFO_SIZE (64 / 4) // 这边FIFO_SIZE的配置仅为最基础配置,想要达到硬件的全部性能还需要自己尝试修改

添加CherryUSB协议栈初始化函数

在CherryUSB源码中的demo目录下有很多“template”结尾的文件,这些都是开发者编写好的示例顶层函数,一般来说我们直接拿去用就行。

比如我这里是要实现USB CDC功能,那么就只要把cdc_acm_template.c文件复制到工程目录下就可以了,我是直接放到main.c文件的同目录下,另有一个cdc_ecm_template.c并不属于本例程的使用范畴。

提示

CDC ACM(Abstract Control Model)和 ECM(Ethernet Networking Control Model)是两种可以实现CDC功能的USB通信子类协议,其区别在于:

  • ACM:
    • 数据与AT指令通过独立通道传输,或混合在同一数据流中(需解析指令与数据的边界)。
    • 适用于需要双向控制指令交互的场景,例如通过AT指令配置蜂窝模块。
  • ECM:
    • 直接封装以太网帧并通过USB批量传输端点发送,无需额外解析协议,操作系统视其为标准网卡。
    • 每个数据包独立传输,效率受限(相比改进版NCM协议)。

cdc_acm_template.c中有一个名为void cdc_acm_init的函数,这个就是整个协议栈的初始化函数,只要在main.c中的系统开机初始化阶段调用这个函数就可以正常初始化对应功能的USB协议栈。

c
// CherryUSB协议栈初始化函数 void cdc_acm_init(uint8_t busid, uintptr_t reg_base) { ... }

因为这个 .c 文件没有对应的头文件来声明函数原型,所以可以直接在main.c中的系统初始化部分使用如下方法直接调用该函数:

c
/* Initialize all configured peripherals */ ... /* USER CODE BEGIN 2 */ // USB Device CDC Init extern void cdc_acm_init(uint8_t busid, uint32_t reg_base); cdc_acm_init(0, USB_OTG_FS_PERIPH_BASE); ... /* USER CODE END 2 */

添加完成后尝试进行编译,如果没有出现报错的话,那么理论上USB协议栈就已经可以正常工作了。此时如果将单片机的USB接口连接至电脑,应当会有设备插入的提示音,并且在(Windows)设备管理器的“端口(COM和LPT)”中应当就可以看到单片机枚举的CDC设备了。

image.png

并且如果你没有修改cdc_acm_template.c文件中开头部分的USB设备的描述符内容的话,在Windows 10的设置>设备>蓝牙和其他设备中应当能看到如下图所示的设备连接到你的电脑。

image.png

如果结果和我截图中所示相符,那么恭喜你,CherryUSB协议栈已经移植成功,CDC功能已经可以正常运行了!

对接LetterShell终端与USB CDC

这部分的工作主要就两件:

  1. 找到CDC的收发函数,并理清楚所需传入的参数;
  2. 修改LetterShell的收发函数,将其与CDC的收发函数对接。

本文作者:Polaris⭐

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 Polari_S_tation 版权所有 许可协议。转载请注明出处!