HC32F460移植CherryUSB实现双CDC
2025-10-03
软件学习笔记
00

目录

CherryUSB简单移植
拷贝对应的文件
修改文件
识别类别名
实现usbgluehc.c
复制usb_config.h
简单测试
实现两个cdc的复合设备
USB端点分配
FIFO分配
描述符的修改
添加缓冲区
修改中断回调函数
编译烧录
收发函数对接
CDC1-数据发送
CDC2-对接命令行
完整代码
总结
参考

这次依旧是HC32相关:)
本次的目标是将CherryUSB移植到HC32F460上,并且实现两个CDC类虚拟串口,一个用于发送数据,另一个用于对接命令行进行调试。如果后面有时间的话大概率还会额外再加一个MSC(大容量存储设备),毕竟HC32F460那么大的FLASH不用白不用是不是(~ ̄▽ ̄)~

CherryUSB简单移植

本次移植的CherryUSB版本为1.5.2,HC32驱动库使用的是HC32F460_DDL_Rev3.3.0
对于CherryUSB不熟悉的可以先看看官方的使用指南,以及这篇移植教程

关于HC32的USB

HC32的USB使用的IP是DWC2,在从机模式下仅支持全速(FS,12Mb/s)模式,并且内置PHY。
拥有1个双向控制端点0;5个OUT端点,可以配置为支持批量传输、中断传输或同步传输;5个IN端点,可以配置为支持批量传输、中断传输或同步传输。
USBIP共有1.25KB(320字,1字=4Byte)专享FIFO,可配置为6个发送FIFO(大小可以自定义,每个IN端点一个FIFO),剩下的空间为一个接收FIFO(所有OUT端点共享这一个FIFO)。

拷贝对应的文件

HC32的USB IP是DWC2,要实现的类是CDC,那么按照上面的移植教程拷贝对应文件夹到工程中,文件夹结构如下:

展开代码
CherryUSB ├─class │ ├─cdc │ └─hub ├─common ├─core └─port └─dwc2

dwc2文件夹中应该保留的文件如下

展开代码
dwc2 ├─usb_dc_dwc2.c ├─usb_dwc2_param.h ├─usb_dwc2_reg.h └─usb_glue_hc.c

修改文件

识别类别名

为了CherryUSB中的变量类型名能被正确识别,需要在usb_dwc2_reg.h中包含hc32_ll.h

实现usb_glue_hc.c

相关信息

usb_glue_hc.c里面的glue是胶水的意思,胶水逻辑,一般是在不同模块里面耦合用的。这里需要实现和平台相关的函数。

注意

注意,不同版本的CherryUSB的usb_glue_xx.c文件中要实现的东西不一样。如果不是V1.5.2,请根据对应版本的usb_glue_xx.c文件自行编写usb_glue_hc.c

仓库中自带的usb_glue_hc.c文件中的内容并不完整,直接拿来编译会报一堆错误,基本都是xx函数未定义的错误。根据同目录下其他的usb_glue_xx.c可知要实现的内容:

c
展开代码
void usbd_dwc2_delay_ms(uint8_t ms) //获取用户配置,包括PHY类型,是否使用DMA,rx_fifo和tx_fifo的大小 void dwc2_get_user_params(uint32_t reg_base, struct dwc2_user_params *params) //对接HC32 USB中断的函数 void USB_IRQ_Handler(void) //HC32 USB外设的低级初始化 void usb_dc_low_level_init(uint8_t busid)

一顿操作猛如虎,得到完整的usb_glue_hc.c文件如下,其中USB的时钟源和中断向量序号可以根据你自己的需求修改(对于时钟源的讲解可以看我之前的博客),关于fifo的配置下面会具体讲解:

c
展开代码
#include "usb_config.h" #include "usb_dwc2_reg.h" #include "usbd_core.h" #include "usbh_core.h" #include "usb_dwc2_param.h" /* When using [GPIO_SetFunc(USBF_VBUS_PORT, USBF_VBUS_PIN, USBF_VBUS_FUNC);], there is no need to configure GOTGCTL */ #define USB_OTG_GLB ((DWC2_GlobalTypeDef *)(reg_base)) const struct dwc2_user_params param_pa11_pa12 = { .phy_type = DWC2_PHY_TYPE_PARAM_FS, .device_dma_enable = false, .device_dma_desc_enable = false, .device_rx_fifo_size = (320 - 16 - 32 - 8 - 16 - 8 - 32), .device_tx_fifo_size = { [0] = 16, // 64 byte [1] = 32, // 128 byte [2] = 8, // 32 byte [3] = 16, // 64 byte [4] = 8, // 32 byte [5] = 32, [6] = 0, [7] = 0, [8] = 0, [9] = 0, [10] = 0, [11] = 0, [12] = 0, [13] = 0, [14] = 0, [15] = 0}, .device_gccfg = 0, .total_fifo_size = 320, // 1280 byte .b_session_valid_override = true, }; void usbd_dwc2_delay_ms(uint8_t ms) { SysTick_Delay(ms); } void dwc2_get_user_params(uint32_t reg_base, struct dwc2_user_params *params) { memcpy(params, &param_pa11_pa12, sizeof(struct dwc2_user_params)); } void USB_IRQ_Handler(void) { extern void USBD_IRQHandler(uint8_t busid); USBD_IRQHandler(0); } /* USBFS Core*/ #define USBF_DP_PORT (GPIO_PORT_A) #define USBF_DP_PIN (GPIO_PIN_12) #define USBF_DM_PORT (GPIO_PORT_A) #define USBF_DM_PIN (GPIO_PIN_11) void usb_dc_low_level_init(uint8_t busid) { stc_gpio_init_t stcGpioCfg; CLK_SetUSBClockSrc(CLK_USBCLK_PLLXR); // UPLLR GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_11, GPIO_FUNC_10); // USBFS-DM GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_12, GPIO_FUNC_10); // USBFS-DP (void)GPIO_StructInit(&stcGpioCfg); stcGpioCfg.u16PinAttr = PIN_ATTR_ANALOG; (void)GPIO_Init(USBF_DM_PORT, USBF_DM_PIN, &stcGpioCfg); (void)GPIO_Init(USBF_DP_PORT, USBF_DP_PIN, &stcGpioCfg); FCG_Fcg1PeriphClockCmd(FCG1_PERIPH_USBFS, ENABLE); // enable usbfs clock stc_irq_signin_config_t stcIrqRegiConf; /* Register INT_SRC_USBFS_GLB Int to Vect.No.030 */ // stcIrqRegiConf.enIRQn = INT030_IRQn; stcIrqRegiConf.enIRQn = INT080_IRQn; /* Select interrupt function */ stcIrqRegiConf.enIntSrc = INT_SRC_USBFS_GLB; /* Callback function */ stcIrqRegiConf.pfnCallback = &USB_IRQ_Handler; /* Registration IRQ */ (void)INTC_IrqSignIn(&stcIrqRegiConf); /* Clear Pending */ NVIC_ClearPendingIRQ(stcIrqRegiConf.enIRQn); /* Set priority */ NVIC_SetPriority(stcIrqRegiConf.enIRQn, DDL_IRQ_PRIO_15); /* Enable NVIC */ NVIC_EnableIRQ(stcIrqRegiConf.enIRQn); } void usb_dc_low_level_deinit(uint8_t busid) { }

复制usb_config.h

将CherryUSB根目录下的cherryusb_config_template.h文件复制到工程中,并重命名为usb_config.h
这里不需要对usb_config.h进行修改,保持原样即可。

注意

注意,usb_config.h依旧是不通用的,请确保版本和文件对应。

usb_config.h中有打印日志的接口,默认是printf,可以直接重定向到串口输出日志。这里我为了少接几根线,稍微修改了下打印的接口,对接到了SEGGER的RTT上,通过SWD接口直接输出到RTT终端:

关于RTT的一个小坑

市面上的xx32一般都遵循某tm32做法,把RAM起始地址放到0x20000000,但是hc32不走寻常路,把flash的起始地址放到了0x00000000,RAM起始地址放到了0x1FFF8000。就会导致RTT Viewer找不到RTT缓冲区的位置,这时候需要到编译生成的.map文件中手动查找_SEGGER_RTT的地址,然后在RTT Viewer连接前选择手动输入地址:

以上的步骤完成后,整个工程再次编译应该就不会报错了。如果编译器还是提示xx函数未定义的错误,那就是编译器把未使用的代码也加入编译了,像下面一样写空实现就行了:

简单测试

将CherryUSB中demo文件夹下的cdc_acm_template.c拷贝到工程中,在main.c中调用初始化USB协议栈的cdc_acm_init函数:

c
展开代码
// USB Device CDC Init extern void cdc_acm_init(uint8_t busid, uint32_t reg_base); cdc_acm_init(0, CM_USBFS_BASE);

然后在while(1)中调用cdc_acm_data_send_with_dtr_test

c
展开代码
while(1){ extern void cdc_acm_data_send_with_dtr_test(uint8_t busid); cdc_acm_data_send_with_dtr_test(0); }

这时候编译烧录代码,再插上USB,设备管理器中就能识别到我们的设备了:
打开RTT Viewer连接单片机,也能看见输出的日志:

(已作废,是代码问题)关于HC32硬件上的坑

25.11.07更新:
已经找到问题,在param_pa11_pa12中未配置.b_session_valid_override = true,,导致某些寄存器未正确初始化,本文有关代码均已修改。

如果日志正常输出,但是插上USB后设备管理器中没有设备的话,可能是HC32硬件上的BUG,请测量DP引脚(PA12)上的电压,正常应该是接近3.1V的高电平,才能让主机正常识别到我们的设备。
USBFS从机的DP引脚用于插入检测,当主机检测到DP引脚为高电平时,才会和该接口上的从机进行握手: 在DWC2的IP中,PA12的引脚上集成了1.5K的上拉电阻,只需要将DCTL寄存器的SDIS位置零即可启用,cherryUSB的DWC2从机初始化中默认启用了这个内置的上拉电阻: 但是很奇怪的是,HC32F460烧录代码之后,PA12不但没有上拉,反而变成了一个接近15K电阻(同样是DWC2中集成的电阻)的下拉,导致PA12一直是低电平,所以主机一直识别不到设备。我不知道这个是不是HC32F460固有的问题,反正我烧录了俩片子都是这个情况,试了各种方法也没有搞出来1.5K上拉。(肿么这么多坑啊◢▆▅▄▃崩╰(〒皿〒)╯潰▃▄▅▇◣)
最后的解决方法是:手动给PA12飞一个1K的上拉电阻到3V3。

然后再打开串口调试工具,开启DTR硬件流控制,就能看见单片机往主机发送的一堆a了(忘记截图了,这个是实现了两个cdc之后的截图):

实现两个cdc的复合设备

USB复合设备是一个USB设备芯片实现了多个USB设备功能,是通过USB接口描述符来实现不同的设备功能。要实现两个cdc,就要在描述符上下手,并实现对应的接口(usbd_interface)。
参考demo文件夹中的cdc_acm_multi_template.c(这个例程是4个CDC复合设备),下面实现我们自己的usb_cdc2_msc.husb_cdc2_msc.c

USB端点分配

这里补充一下USB端点的知识:
USB的所有指令和数据的收发都是通过端点进行的,端点分为双向端点和单向端点。双向端点就是能收能发;单向端点就是只能收或者只能发,那么单向端点就分为这两类:IN端点OUT端点。IN和OUT的方向是相对于主机(HOST)来说的。也就是说数据从主机到单片机是OUT,单片机到主机是IN。
在文章的最开头提到了HC32拥有1个双向控制端点0,5个OUT端点和5个IN端点。其中端点0一般用于USB控制,是用来收发指令的,用户一般不控制该端点。那我们能使用的就是剩下的10个单向端点了。
CDC的端点要求如下:一个用于中断(INT)的IN端点,一个用于批量数据传输的IN端点和一个用于批量数据传输的OUT端点。两个CDC需要的就是4个IN端点,2个OUT端点。HC32的资源是能满足这个要求的。
在代码中,配置端点的分配如下:

c
展开代码
/*!< endpoint address */ #define CDC1_IN_EP 0x81 #define CDC1_INT_EP 0x82 #define CDC2_IN_EP 0x83 #define CDC2_INT_EP 0x84 #define MSC_IN_EP 0x85 #define CDC1_OUT_EP 0x01 #define CDC2_OUT_EP 0x02 #define MSC_OUT_EP 0x03

IN端点地址是0x80-0x8F,OUT端点地址是0x00-0x0F,其中0x80和0x00是端点0的地址。这里把IN端点1、2OUT端点1给CDC1,IN端点3、4OUT端点2给CDC2。

FIFO分配

对于每个IN端点,可以单独配置FIFO的大小。在我的应用中,CDC1负责发送大量数据,CDC2负责对接Letter Shell。那么,需要把CDC1的数据IN端点和MSC的数据IN端点1、5的FIFO弄大一点,CDC1和CDC2的中断IN端点2、4的FIFO弄小一点才比较合适。所以,在usb_glue_hc.c中的配置结构体中配置FIFO如下:

c
展开代码
const struct dwc2_user_params param_pa11_pa12 = { .phy_type = DWC2_PHY_TYPE_PARAM_FS, .device_dma_enable = false, .device_dma_desc_enable = false, .device_rx_fifo_size = (320 - 16 - 32 - 8 - 16 - 8 - 32), // OUT端点FIFO大小 .device_tx_fifo_size = { [0] = 16, // 64 byte, 端点0,IN方向 [1] = 32, // 128 byte,IN端点1 [2] = 8, // 32 byte, IN端点2 [3] = 16, // 64 byte, IN端点3 [4] = 8, // 32 byte, IN端点4 [5] = 32, // IN端点5 [6] = 0, [7] = 0, [8] = 0, [9] = 0, [10] = 0, [11] = 0, [12] = 0, [13] = 0, [14] = 0, [15] = 0}, .device_gccfg = 0, .total_fifo_size = 320, // 1280 byte .b_session_valid_override = true, };

.device_tx_fifo_size是IN端点的FIFO,按需分配。这里的大小是以字(word)为单位,一个字(word)是4字节(Byte),1.25KB就是1280B,即320字。
IN端点的FIFO分配完了,剩下的就全是OUT端点的FIFO了(所有OUT端点共享这一个FIFO),即.device_rx_fifo_size,把tx_fifo减掉即是OUT端点FIFO的大小。

描述符的修改

首先修改配置描述符的长度,两个CDC的长度。:

c
展开代码
/*!< config descriptor size */ #define USB_CONFIG_SIZE (9 + CDC_ACM_DESCRIPTOR_LEN*2)

然后是配置配置描述符具体内容的修改:

c
展开代码
static const uint8_t config_descriptor[] = { USB_CONFIG_DESCRIPTOR_INIT(USB_CONFIG_SIZE, 0x04, 0x01, USB_CONFIG_BUS_POWERED, USBD_MAX_POWER), CDC_ACM_DESCRIPTOR_INIT(0x00, CDC1_INT_EP, CDC1_OUT_EP, CDC1_IN_EP, CDC_MAX_MPS, 0x02), CDC_ACM_DESCRIPTOR_INIT(0x02, CDC2_INT_EP, CDC2_OUT_EP, CDC2_IN_EP, CDC_MAX_MPS, 0x02), };

剩下的描述符保持原样即可。

添加缓冲区

给每个CDC都配置单独的接收发送缓冲区,并在.h文件中extern出去:

c
展开代码
USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t read_buffer_cdc1[2048]; USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t write_buffer_cdc1[2048]; USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t read_buffer_cdc2[2048]; USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t write_buffer_cdc2[2048];

因为DWC2发送数据需要的缓冲区需要4byte对齐,所以USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX这个前缀不能删除,而且要发送数据之前,必须把数据拷贝到这个write_buffer缓冲区中,再把write_buffer传给USB才能正常发送。

修改中断回调函数

  1. 修改事务中断回调函数usbd_event_handler 在这里配置两个CDC的第一次读取:
c
展开代码
volatile bool ep1_tx_busy_flag = false; volatile bool ep2_tx_busy_flag = false; static void usbd_event_handler(uint8_t busid, uint8_t event) { switch (event) { case USBD_EVENT_RESET: break; case USBD_EVENT_CONNECTED: break; case USBD_EVENT_DISCONNECTED: break; case USBD_EVENT_RESUME: break; case USBD_EVENT_SUSPEND: break; case USBD_EVENT_CONFIGURED: ep1_tx_busy_flag = false; ep2_tx_busy_flag = false; /* setup first out ep read transfer */ usbd_ep_start_read(busid, CDC1_OUT_EP, read_buffer_cdc1, 2048); usbd_ep_start_read(busid, CDC2_OUT_EP, read_buffer_cdc2, 2048); break; case USBD_EVENT_SET_REMOTE_WAKEUP: break; case USBD_EVENT_CLR_REMOTE_WAKEUP: break; default: break; } }
  1. 修改发送完成和接收完成中断回调函数,这里给CDC1和CDC2配置不同回调函数usbd_cdc1_bulk_outusbd_cdc1_bulk_inusbd_cdc2_bulk_outusbd_cdc2_bulk_in
c
展开代码
// CDC1回调 void usbd_cdc1_bulk_out(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("CDC1 out len:%d\r\n", nbytes); // 处理CDC1接收的数据... // 重启CDC1的读取 usbd_ep_start_read(busid, CDC1_OUT_EP, read_buffer_cdc1, 2048); } void usbd_cdc1_bulk_in(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("CDC1 in len:%d\r\n", nbytes); if ((nbytes % usbd_get_ep_mps(busid, ep)) == 0 && nbytes) { /* send zlp */ usbd_ep_start_write(busid, CDC1_IN_EP, NULL, 0); } else { ep1_tx_busy_flag = false; } } // CDC2回调 void usbd_cdc2_bulk_out(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("CDC2 out len:%d\r\n", nbytes); // 处理CDC2接收的数据 // 重启CDC2的读取 usbd_ep_start_read(busid, CDC2_OUT_EP, read_buffer_cdc2, 2048); } void usbd_cdc2_bulk_in(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("CDC2 in len:%d\r\n", nbytes); if ((nbytes % usbd_get_ep_mps(busid, ep)) == 0 && nbytes) { /* send zlp */ usbd_ep_start_write(busid, CDC2_IN_EP, NULL, 0); } else { ep2_tx_busy_flag = false; } } /*!< endpoint call back */ struct usbd_endpoint cdc_out_ep1 = { .ep_addr = CDC1_OUT_EP, .ep_cb = usbd_cdc1_bulk_out }; struct usbd_endpoint cdc_in_ep1 = { .ep_addr = CDC1_IN_EP, .ep_cb = usbd_cdc1_bulk_in }; struct usbd_endpoint cdc_out_ep2 = { .ep_addr = CDC2_OUT_EP, .ep_cb = usbd_cdc2_bulk_out }; struct usbd_endpoint cdc_in_ep2 = { .ep_addr = CDC2_IN_EP, .ep_cb = usbd_cdc2_bulk_in };
  1. 添加接口。原本只有两个接口intf0和intf1,增加一个CDC后需要再添加两个接口,并初始化:
c
展开代码
struct usbd_interface intf0; struct usbd_interface intf1; struct usbd_interface intf2; struct usbd_interface intf3; void usb_cdc_init(uint8_t busid, uintptr_t reg_base) { usbd_desc_register(busid, &cdc_descriptor); usbd_add_interface(busid, usbd_cdc_acm_init_intf(busid, &intf0)); usbd_add_interface(busid, usbd_cdc_acm_init_intf(busid, &intf1)); usbd_add_endpoint(busid, &cdc_out_ep1); usbd_add_endpoint(busid, &cdc_in_ep1); usbd_add_interface(busid, usbd_cdc_acm_init_intf(busid, &intf2)); usbd_add_interface(busid, usbd_cdc_acm_init_intf(busid, &intf3)); usbd_add_endpoint(busid, &cdc_out_ep2); usbd_add_endpoint(busid, &cdc_in_ep2); usbd_initialize(busid, reg_base, usbd_event_handler); }

编译烧录

将以上的代码和原有的模板代码整合后,编译烧录后就能在设备管理器中看见单片机枚举出来两个CDC:

收发函数对接

上面的工作只完成了USBFS的初始化和枚举,下面将程序中其他部分代码的收发数据对接到USB上。
cherryUSB中收发数据的函数是:usbd_ep_start_read()usbd_ep_start_write()。但是这俩函数都是异步收发,即只负责将收发数据的长度和地址对接到FIFO上,具体数据的接收,数据的分包,发送都由更底层的代码或者USB收发器自行处理。所以在例程中发送使用busy_flag进行阻塞式发送的方式并不适合我的情况,因为两个CDC只要有一个FIFO被塞满被阻塞住,整个程序就卡死在阻塞里面了,肥肠滴不优雅
于是参考了cherryUSB作者的另一个项目cherrDAP中的代码,使用环形缓冲区(Ring Buffer)进行数据的流转。正好,cherryUSB的third_party文件夹中就有作者写好的cherryRB,一个.c,一个.h,直接拷到工程里面用。

CDC1-数据发送

上面提到,CDC1用于大量数据的发送,然后用VOFA+对数据进行显示,因此CDC1只需要对接发送函数即可。
首先初始化环形缓冲区:

c
展开代码
chry_ringbuffer_t vofa_tx; uint8_t rb_vofa_tx[1024]; chry_ringbuffer_init(&vofa_tx, rb_vofa_tx, 1024);

然后创建发送数据的函数,数据写入环形缓冲区:

c
展开代码
void vofaShow(uint8_t busid) { uint8_t temp[128]; uint16_t len = sprintf( temp, "%d,%d\n\r", Iron.Temperature.OriginalValue, Iron.Current.OriginalValue); if (chry_ringbuffer_get_free(&vofa_tx) >= CDC_MAX_MPS){ chry_ringbuffer_write(&vofa_tx, temp, len); } }

在while(1)中监控环形缓冲区的使用情况:

c
展开代码
uint32_t size; uint8_t *buffer; if (chry_ringbuffer_get_used(&vofa_tx) && (ep1_tx_busy_flag == 0)) { buffer = chry_ringbuffer_linear_read_setup(&vofa_tx, &size); memcpy(write_buffer_cdc1, buffer, size); ep1_tx_busy_flag = 1; usbd_ep_start_write(0, CDC1_IN_EP, write_buffer_cdc1, size); }

环形缓冲区的数据读取完成后移动环形缓冲区中读指针的索引,这里放到USB的数据发送完成中断中处理:

c
展开代码
void usbd_cdc1_bulk_in(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("CDC1 in len:%d\r\n", nbytes); chry_ringbuffer_linear_read_done(&vofa_tx, nbytes); if ((nbytes % usbd_get_ep_mps(busid, ep)) == 0 && nbytes) { /* send zlp */ usbd_ep_start_write(busid, CDC1_IN_EP, NULL, 0); } else { ep1_tx_busy_flag = false; } }

CDC2-对接命令行

这里对接的命令行是Letter Shell,不了解的可以看我之前的博客文章。
对于命令行来说,要对接收和发送两个函数。如法炮制即可。
首先初始化收发的环形缓冲区:

c
展开代码
chry_ringbuffer_init(&shellrx, rb_shellrx, 1024); chry_ringbuffer_init(&shelltx, rb_shelltx, 1024);

接收数据写入环形缓冲区放在USB的数据接收完成中断中处理:

c
展开代码
// CDC2回调 void usbd_cdc2_bulk_out(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("CDC2 out len:%d\r\n", nbytes); // 处理CDC2接收的数据,放入环形缓冲区 chry_ringbuffer_write(&shellrx,read_buffer_cdc2, nbytes); // 重启CDC2的读取 usbd_ep_start_read(busid, CDC2_OUT_EP, read_buffer_cdc2, 2048); }

在while(1)中监控shellrx的使用情况,有收到数据时shellHandler解析字符串,shellrx移动读指针的索引:

c
展开代码
if (chry_ringbuffer_get_used(&shellrx)) { buffer = chry_ringbuffer_linear_read_setup(&shellrx, &size); Shell_handle(buffer, size); } void Shell_handle(uint8_t *data, uint16_t len) { if( len > 0){ for (uint16_t i = 0; i < len; i++) { shellHandler(&cdcShell, data[i]); } } chry_ringbuffer_linear_read_done(&shellrx, len); }

shellHandler()在解析完字符后会立马调用shell的发送函数,这里将要发送的数据写入shelltx中:

c
展开代码
/** * @brief cdc shell写 * * @param data 数据 * @param len 数据长度 * * @return short 实际写入的数据长度 */ short cdcShellWrite(char *data, unsigned short len) { if(chry_ringbuffer_get_free(&shelltx) >= len){ chry_ringbuffer_write(&shelltx, data, len); } return len; }

同样在while(1)中监控shelltx的使用情况:

c
展开代码
if (chry_ringbuffer_get_used(&shelltx) && (ep2_tx_busy_flag == 0)) { buffer = chry_ringbuffer_linear_read_setup(&shelltx, &size); memcpy(write_buffer_cdc2, buffer, size); ep2_tx_busy_flag = 1; usbd_ep_start_write(0, CDC2_IN_EP, write_buffer_cdc2, size); }

然后在CDC2的数据发送完成中断中移动shelltx读指针的索引:

c
展开代码
void usbd_cdc2_bulk_in(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("CDC2 in len:%d\r\n", nbytes); chry_ringbuffer_linear_read_done(&shelltx, nbytes); if ((nbytes % usbd_get_ep_mps(busid, ep)) == 0 && nbytes) { /* send zlp */ usbd_ep_start_write(busid, CDC2_IN_EP, NULL, 0); } else { ep2_tx_busy_flag = false; } }

最后,在main()中对shell进行初始化:

c
展开代码
cdcShellInit(); // 初始化cdc Shell

至此,整个移植过程结束。
单片机连上电脑,VOFA+连上第一个CDC串口,MobaXterm连上第二个CDC串口,就能看见代码效果辣,这里录了个视频:https://www.bilibili.com/video/BV1VAHAzeExg

完整代码

这里贴一下完整的代码:
usb_cdc2_msc.h:

c
展开代码
#ifndef USB_CDC2_MSC_H #define USB_CDC2_MSC_H #include "usbd_core.h" #include "usbd_cdc_acm.h" #include "shell_port.h" /*!< endpoint address */ #define CDC1_IN_EP 0x81 #define CDC1_INT_EP 0x82 #define CDC2_IN_EP 0x83 #define CDC2_INT_EP 0x84 #define MSC_IN_EP 0x85 #define CDC1_OUT_EP 0x01 #define CDC2_OUT_EP 0x02 #define MSC_OUT_EP 0x03 extern volatile bool ep1_tx_busy_flag; extern volatile bool ep2_tx_busy_flag; extern uint8_t write_buffer_cdc1[2048]; extern uint8_t write_buffer_cdc2[2048]; void usb_cdc_init(uint8_t busid, uintptr_t reg_base); void vofaShow(uint8_t busid); #endif /* USB_CDC2_MSC_H */

usb_cdc2_msc.c:

c
展开代码
#include <stdbool.h> #include <stdint.h> #include "usb_cdc2_msc.h" #include "loop.h" #define USBD_VID 0xFFFF #define USBD_PID 0xFFFF #define USBD_MAX_POWER 100 #define USBD_LANGID_STRING 1033 /*!< config descriptor size */ #define USB_CONFIG_SIZE (9 + CDC_ACM_DESCRIPTOR_LEN*2) #ifdef CONFIG_USB_HS #define CDC_MAX_MPS 512 #else #define CDC_MAX_MPS 64 #endif static const uint8_t device_descriptor[] = { USB_DEVICE_DESCRIPTOR_INIT(USB_2_0, 0xEF, 0x02, 0x01, USBD_VID, USBD_PID, 0x0100, 0x01) }; static const uint8_t config_descriptor[] = { USB_CONFIG_DESCRIPTOR_INIT(USB_CONFIG_SIZE, 0x04, 0x01, USB_CONFIG_BUS_POWERED, USBD_MAX_POWER), CDC_ACM_DESCRIPTOR_INIT(0x00, CDC1_INT_EP, CDC1_OUT_EP, CDC1_IN_EP, CDC_MAX_MPS, 0x02), CDC_ACM_DESCRIPTOR_INIT(0x02, CDC2_INT_EP, CDC2_OUT_EP, CDC2_IN_EP, CDC_MAX_MPS, 0x02), }; static const uint8_t device_quality_descriptor[] = { /////////////////////////////////////// /// device qualifier descriptor /////////////////////////////////////// 0x0a, USB_DESCRIPTOR_TYPE_DEVICE_QUALIFIER, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, }; static const char *string_descriptors[] = { (const char[]){0x09, 0x04}, /* Langid */ "CherryUSB", /* Manufacturer */ "CherryUSB CDC DEMO", /* Product */ "2022123456", /* Serial Number */ }; static const uint8_t *device_descriptor_callback(uint8_t speed) { return device_descriptor; } static const uint8_t *config_descriptor_callback(uint8_t speed) { return config_descriptor; } static const uint8_t *device_quality_descriptor_callback(uint8_t speed) { return device_quality_descriptor; } static const char *string_descriptor_callback(uint8_t speed, uint8_t index) { if (index > 3) { return NULL; } return string_descriptors[index]; } const struct usb_descriptor cdc_descriptor = { .device_descriptor_callback = device_descriptor_callback, .config_descriptor_callback = config_descriptor_callback, .device_quality_descriptor_callback = device_quality_descriptor_callback, .string_descriptor_callback = string_descriptor_callback }; USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t read_buffer_cdc1[2048]; USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t write_buffer_cdc1[2048]; USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t read_buffer_cdc2[2048]; USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t write_buffer_cdc2[2048]; volatile bool ep1_tx_busy_flag = false; volatile bool ep2_tx_busy_flag = false; static void usbd_event_handler(uint8_t busid, uint8_t event) { switch (event) { case USBD_EVENT_RESET: break; case USBD_EVENT_CONNECTED: break; case USBD_EVENT_DISCONNECTED: break; case USBD_EVENT_RESUME: break; case USBD_EVENT_SUSPEND: break; case USBD_EVENT_CONFIGURED: ep1_tx_busy_flag = false; ep2_tx_busy_flag = false; /* setup first out ep read transfer */ usbd_ep_start_read(busid, CDC1_OUT_EP, read_buffer_cdc1, 2048); usbd_ep_start_read(busid, CDC2_OUT_EP, read_buffer_cdc2, 2048); break; case USBD_EVENT_SET_REMOTE_WAKEUP: break; case USBD_EVENT_CLR_REMOTE_WAKEUP: break; default: break; } } // CDC1回调 void usbd_cdc1_bulk_out(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("CDC1 out len:%d\r\n", nbytes); // 处理CDC1接收的数据... // 重启CDC1的读取 usbd_ep_start_read(busid, CDC1_OUT_EP, read_buffer_cdc1, 2048); } void usbd_cdc1_bulk_in(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("CDC1 in len:%d\r\n", nbytes); chry_ringbuffer_linear_read_done(&vofa_tx, nbytes); if ((nbytes % usbd_get_ep_mps(busid, ep)) == 0 && nbytes) { /* send zlp */ usbd_ep_start_write(busid, CDC1_IN_EP, NULL, 0); } else { ep1_tx_busy_flag = false; } } // CDC2回调 void usbd_cdc2_bulk_out(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("CDC2 out len:%d\r\n", nbytes); // 处理CDC2接收的数据,放入环形缓冲区 chry_ringbuffer_write(&shellrx,read_buffer_cdc2, nbytes); // 重启CDC2的读取 usbd_ep_start_read(busid, CDC2_OUT_EP, read_buffer_cdc2, 2048); } void usbd_cdc2_bulk_in(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("CDC2 in len:%d\r\n", nbytes); chry_ringbuffer_linear_read_done(&shelltx, nbytes); if ((nbytes % usbd_get_ep_mps(busid, ep)) == 0 && nbytes) { /* send zlp */ usbd_ep_start_write(busid, CDC2_IN_EP, NULL, 0); } else { ep2_tx_busy_flag = false; } } /*!< endpoint call back */ struct usbd_endpoint cdc_out_ep1 = { .ep_addr = CDC1_OUT_EP, .ep_cb = usbd_cdc1_bulk_out }; struct usbd_endpoint cdc_in_ep1 = { .ep_addr = CDC1_IN_EP, .ep_cb = usbd_cdc1_bulk_in }; struct usbd_endpoint cdc_out_ep2 = { .ep_addr = CDC2_OUT_EP, .ep_cb = usbd_cdc2_bulk_out }; struct usbd_endpoint cdc_in_ep2 = { .ep_addr = CDC2_IN_EP, .ep_cb = usbd_cdc2_bulk_in }; struct usbd_interface intf0; struct usbd_interface intf1; struct usbd_interface intf2; struct usbd_interface intf3; void usb_cdc_init(uint8_t busid, uintptr_t reg_base) { usbd_desc_register(busid, &cdc_descriptor); usbd_add_interface(busid, usbd_cdc_acm_init_intf(busid, &intf0)); usbd_add_interface(busid, usbd_cdc_acm_init_intf(busid, &intf1)); usbd_add_endpoint(busid, &cdc_out_ep1); usbd_add_endpoint(busid, &cdc_in_ep1); usbd_add_interface(busid, usbd_cdc_acm_init_intf(busid, &intf2)); usbd_add_interface(busid, usbd_cdc_acm_init_intf(busid, &intf3)); usbd_add_endpoint(busid, &cdc_out_ep2); usbd_add_endpoint(busid, &cdc_in_ep2); usbd_initialize(busid, reg_base, usbd_event_handler); } void vofaShow(uint8_t busid) { uint8_t temp[128]; uint16_t len = sprintf( temp, "%d,%d\n\r", Iron.Temperature.OriginalValue, Iron.Current.OriginalValue); if (chry_ringbuffer_get_free(&vofa_tx) >= CDC_MAX_MPS){ chry_ringbuffer_write(&vofa_tx, temp, len); } }

shell_port.h:

c
展开代码
/** * @file shell_port.h * @author Letter (NevermindZZT@gmail.com) * @brief * @version 0.1 * @date 2019-02-22 * * @copyright (c) 2019 Letter * */ /* 加在.rodata : _shell_command_start = .; KEEP (*(shellCommand)) _shell_command_end = .; */ #ifndef __SHELL_PORT_H__ #define __SHELL_PORT_H__ #include "shell.h" #include "chry_ringbuffer.h" extern Shell cdcShell; extern chry_ringbuffer_t shellrx; extern chry_ringbuffer_t shelltx; extern uint8_t rb_shellrx[1024]; // 1K缓冲区 extern uint8_t rb_shelltx[1024]; // 1K缓冲区 void cdcShellInit(void); short cdcShellRead(char *data, unsigned short len); short cdcShellWrite(char *data, unsigned short len); void Shell_handle(uint8_t *data, uint16_t len); #endif

shell_port.c:

c
展开代码
/** * @file shell_port.c * @author Letter (NevermindZZT@gmail.com) * @brief * @version 0.1 * @date 2019-02-22 * * @copyright (c) 2019 Letter * */ #include "shell.h" #include "shell_port.h" #include "usb_cdc2_msc.h" Shell cdcShell; char cdcShellBuffer[512]; chry_ringbuffer_t shellrx; chry_ringbuffer_t shelltx; uint8_t rb_shellrx[1024]; // 1K缓冲区 uint8_t rb_shelltx[1024]; // 1K缓冲区 /** * @brief cdc shell写 * * @param data 数据 * @param len 数据长度 * * @return short 实际写入的数据长度 */ short cdcShellWrite(char *data, unsigned short len) { if(chry_ringbuffer_get_free(&shelltx) >= len){ chry_ringbuffer_write(&shelltx, data, len); } return len; } /** * @brief cdc shell读 * * @param data 数据 * @param len 数据长度 * * @return short 实际读取的数据长度 */ short cdcShellRead(char *data, unsigned short len) { return 0; // 实际不会用到,所以直接返回0 } /** * @brief 用户shell初始化 * */ void cdcShellInit(void) { cdcShell.write = cdcShellWrite; cdcShell.read = cdcShellRead; shellInit(&cdcShell, cdcShellBuffer, 512); } void Shell_handle(uint8_t *data, uint16_t len) { if( len > 0){ for (uint16_t i = 0; i < len; i++) { shellHandler(&cdcShell, data[i]); } } chry_ringbuffer_linear_read_done(&shellrx, len); }

其余代码:
初始化:

c
展开代码
usb_cdc_init(0, CM_USBFS_BASE); chry_ringbuffer_init(&shellrx, rb_shellrx, 1024); chry_ringbuffer_init(&shelltx, rb_shelltx, 1024); chry_ringbuffer_init(&vofa_tx, rb_vofa_tx, 1024); cdcShellInit(); // 初始化cdc Shell

while(1)中调用USB_cdcHandle()以及vofaShow()

c
展开代码
chry_ringbuffer_t vofa_tx; uint8_t rb_vofa_tx[1024]; void USB_cdcHandle() { uint32_t size; uint8_t *buffer; if (chry_ringbuffer_get_used(&shellrx)) { buffer = chry_ringbuffer_linear_read_setup(&shellrx, &size); Shell_handle(buffer, size); } if (chry_ringbuffer_get_used(&vofa_tx) && (ep1_tx_busy_flag == 0)) { buffer = chry_ringbuffer_linear_read_setup(&vofa_tx, &size); memcpy(write_buffer_cdc1, buffer, size); ep1_tx_busy_flag = 1; usbd_ep_start_write(0, CDC1_IN_EP, write_buffer_cdc1, size); } if (chry_ringbuffer_get_used(&shelltx) && (ep2_tx_busy_flag == 0)) { buffer = chry_ringbuffer_linear_read_setup(&shelltx, &size); memcpy(write_buffer_cdc2, buffer, size); ep2_tx_busy_flag = 1; usbd_ep_start_write(0, CDC2_IN_EP, write_buffer_cdc2, size); } }

总结

cherrUSB还是很好用滴,我对USB协议栈啥都不懂,看了几篇教程,仔细研究一下就能很快上手,设备的初始化对着模板也能很快敲出来,不需要对USB了解很深就能发挥出USB的大半实力。

参考

CherryUSB移植笔记 APM32F407VGT6 DWC2移植
整合 LetterShell 和 CherryUSB 功能到 STM32
https://github.com/LHYHHD/My-USB-Study
华大HC32F460XXX移植cherryusb协议栈,实现USB CDC ACM
HC32 RTT打印无输出-文章最末有解决方法

本文作者:zxcli

本文链接:

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