admin 管理员组

文章数量: 887007

基于STM32 CDC模拟CH340

代码已开源在GitHub上,地址:STM32_USB_CH340

之前写过一篇使用STM32虚拟串口功能的文章:实现USB CDC通信,但是这个有个很大的问题,它的Windows驱动的数字签名过期了,我在我的电脑里搜索了一下,发现有两个驱动:

不过很可惜,这两个驱动都过期了:

这就直接导致在Windows上使用ST自己的虚拟串口需要强制跳过数字签名这一步,而每次电脑重启之后Windows就会恢复默认设置,最麻烦的是每次还必须通过重启设置,不过Linux下倒没这个问题,因为数字签名是Windows自己搞出来的东西。

但还是很烦,所以我决定抛弃ST官方的虚拟串口驱动,正好我找到了别人用STM32模拟CH341的代码:blackmiaool/STM32_USB_CH341 ,但这已经是五六年前的代码了,当时还是标准库,所以我决定把它用HAL库实现。

踩了一些坑,这玩意儿花了我三天时间,主要还是对USB协议不太熟悉,下面就按照我踩坑的时间顺序记录。

第一天:让电脑识别为CH340

这一步很简单,只需要改变设备描述符就行了,具体更改如下:

使用STM32CubeMX生成代码

这里修改以下PID和VID,然后字符串名称就随便写,点击生成代码。

修改设备描述符

usbd_desc.c里面,修改USBD_FS_DeviceDesc变量:

__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{0x12,   /* bLength */0x01,     /* bDescriptorType */0x10,0x01,   /* bcdUSB = 1.10 */0xff,   /* bDeviceClass: CDC */0x00,   /* bDeviceSubClass */0x00,   /* bDeviceProtocol */0x40,   /* bMaxPacketSize0 */0x86,0x1a,   /* idVendor = 0x1A86 */0x23,0x75,   /* idProduct = 0x7523 */0x63,0x02,   /* bcdDevice = 2.00 */1,              /* Index of string descriptor describing manufacturer */2,              /* Index of string descriptor describing product */1,              /* Index of string descriptor describing the device's serial number */0x01    /* bNumConfigurations */
};

修改设备配置描述符

然后修改usbd_cdc.c文件里面的USBD_CDC_CfgFSDesc变量(因为我配置的Full Speed,如果是其他速度就修改对应的变量就行):

__ALIGN_BEGIN uint8_t USBD_CDC_CfgFSDesc[0x27] __ALIGN_END =
{/*Configuation Descriptor*/0x09,   /* bLength: Configuation Descriptor size */0x02,      /* bDescriptorType: Configuration */0x27,       /* wTotalLength:no of returned bytes */0x00,0x01,   /* bNumInterfaces: 1 interface */0x01,   /* bConfigurationValue: Configuration value */0x00,   /* iConfiguration: Index of string descriptor describing the configuration */0x80,   /* bmAttributes: self powered */0x30,   /* MaxPower 0 mA *//*Interface Descriptor*/0x09,   /* bLength: Interface Descriptor size */0x04,  /* bDescriptorType: Interface *//* Interface descriptor type */0x00,   /* bInterfaceNumber: Number of Interface */0x00,   /* bAlternateSetting: Alternate setting */0x03,   /* bNumEndpoints: One endpoints used */0xff,   /* bInterfaceClass: Communication Interface Class */0x01,   /* bInterfaceSubClass: Abstract Control Model */0x02,   /* bInterfaceProtocol: Common AT commands */0x00,   /* iInterface: *//*Endpoint 2in Descriptor*/0x07,   /* bLength: Endpoint Descriptor size */0x05,   /* bDescriptorType: Endpoint */0x82,   /* bEndpointAddress: (IN2) */0x02,   /* bmAttributes: bulk */0x20,      0x00,   /* wMaxPacketSize: */0x00,   /* bInterval: *//*Endpoint 2out Descriptor*/0x07,   /* bLength: Endpoint Descriptor size */0x05,   /* bDescriptorType: Endpoint */0x02,   /* bEndpointAddress: (out2) */0x02,   /* bmAttributes: bulk */0x20,      0x00,   /* wMaxPacketSize: */0x00,   /* bInterval: *//*Endpoint 1in Descriptor*/0x07,   /* bLength: Endpoint Descriptor size */0x05,   /* bDescriptorType: Endpoint */0x81,   /* bEndpointAddress: (IN1) */0x03,   /* bmAttributes: Interrupt */0x08,      /* wMaxPacketSize: */0x00,0x01,   /* bInterval: */
};

然后编译下载,插上USB,电脑就会识别到CH340了:

不过这里有感叹号,点开发现是因为Windows有的请求失败,这是显然的,因为我们还没有写相关的东西呢。

第二天:响应Windows请求

这里先介绍一下USB的请求类型。

USB规范定义了11个标准命令,它们分别是:Clear_Feature、Get_Configuration、Get_Descriptor、Get_Interface、Get_Status、Set_Address、Set_Configuration、Set_Descriptor、Set_Interface、Set_Feature、Synch_Frame。所有USB设备都必须支持这些命令(个别命令除外,如Set_Descriptor、Synch_Frame)。

所有的命令虽然有不同的数据和使用目的,有的USB命令结构是一样的。下表所示为USB命令的结构:

偏移量长度(字节)描述
0bmRequestType0位图请求特征:D7:传输方向(0=主机至设备 1=设备至主机);D6…5:种类(0=标准 1=类 2=厂商 3=保留)
1bRequest1命令类型编码值
2wValue2根据不同的命令,含义也不同
4wIndex2索引或偏移根据不同的命令,含义也不同,主要用于传送索引或偏移
6wLength2如有数据传送阶段,此为数据字节数

生成的代码中处理USB请求的代码在usbd_cdc.c中:

/*** @brief  USBD_CDC_Setup*         Handle the CDC specific requests* @param  pdev: instance* @param  req: usb requests* @retval status*/
static uint8_t  USBD_CDC_Setup(USBD_HandleTypeDef *pdev,USBD_SetupReqTypedef *req)
{USBD_CDC_HandleTypeDef   *hcdc = (USBD_CDC_HandleTypeDef *) pdev->pClassData;uint8_t ifalt = 0U;uint16_t status_info = 0U;uint8_t ret = USBD_OK;switch (req->bmRequest & USB_REQ_TYPE_MASK){case USB_REQ_TYPE_CLASS :if (req->wLength){if (req->bmRequest & 0x80U){((USBD_CDC_ItfTypeDef *)pdev->pUserData)->Control(req->bRequest,(uint8_t *)(void *)hcdc->data,req->wLength);USBD_CtlSendData(pdev, (uint8_t *)(void *)hcdc->data, req->wLength);}else{hcdc->CmdOpCode = req->bRequest;hcdc->CmdLength = (uint8_t)req->wLength;USBD_CtlPrepareRx(pdev, (uint8_t *)(void *)hcdc->data, req->wLength);}}else{((USBD_CDC_ItfTypeDef *)pdev->pUserData)->Control(req->bRequest,(uint8_t *)(void *)req, 0U);}break;case USB_REQ_TYPE_STANDARD:switch (req->bRequest){case USB_REQ_GET_STATUS:if (pdev->dev_state == USBD_STATE_CONFIGURED){USBD_CtlSendData(pdev, (uint8_t *)(void *)&status_info, 2U);}else{USBD_CtlError(pdev, req);ret = USBD_FAIL;}break;case USB_REQ_GET_INTERFACE:if (pdev->dev_state == USBD_STATE_CONFIGURED){USBD_CtlSendData(pdev, &ifalt, 1U);}else{USBD_CtlError(pdev, req);ret = USBD_FAIL;}break;case USB_REQ_SET_INTERFACE:if (pdev->dev_state != USBD_STATE_CONFIGURED){USBD_CtlError(pdev, req);ret = USBD_FAIL;}break;default:USBD_CtlError(pdev, req);ret = USBD_FAIL;break;}break;default:USBD_CtlError(pdev, req);ret = USBD_FAIL;break;}return ret;
}

经过代码分析可以发现,官方代码并没有处理“厂商”的请求,即bmRequest的五六位为10的请求,所以需要修改一下:

static uint8_t  USBD_CDC_Setup(USBD_HandleTypeDef *pdev,USBD_SetupReqTypedef *req)
{USBD_CDC_HandleTypeDef   *hcdc = (USBD_CDC_HandleTypeDef *) pdev->pClassData;uint8_t ifalt = 0U;uint16_t status_info = 0U;uint8_t ret = USBD_OK;switch (req->bmRequest & USB_REQ_TYPE_MASK){// 省略其他代码...case USB_REQ_TYPE_VENDOR:  //处理来自厂商的请求break;default:USBD_CtlError(pdev, req);ret = USBD_FAIL;break;}return ret;
}

然后编译下载,插上USB,哈哈,感叹号就没了,其实之前是因为厂商的请求代码都将其处理为错误情况,所以Windows会请求失败。

但其实我们还是没有处理Windows的请求,所以真正使用时连串口都打不开:

这里我参考了CH340的Linux驱动源码,当中有以下函数:

int ch341_configure(struct usb_device *dev, struct ch341_private *priv) 
{ char *buffer; int r = -ENOMEM; const unsigned size = 8; dbg("ch341_configure()"); buffer = kmalloc(size, GFP_KERNEL); if (!buffer) goto out; /* expect two bytes 0x27 0x00 */ r = ch341_control_in(dev, 0x5f, 0, 0, buffer, size); if (r < 0) goto out; r = ch341_control_out(dev, 0xa1, 0, 0); if (r < 0) goto out; r = ch341_set_baudrate(dev, priv); if (r < 0) goto out; /* expect two bytes 0x56 0x00 */ r = ch341_control_in(dev, 0x95, 0x2518, 0, buffer, size); if (r < 0) goto out; r = ch341_control_out(dev, 0x9a, 0x2518, 0x0050); if (r < 0) goto out; /* expect 0xff 0xee */ r = ch341_get_status(dev); if (r < 0) goto out; r = ch341_control_out(dev, 0xa1, 0x501f, 0xd90a); if (r < 0) goto out; r = ch341_set_baudrate(dev, priv); if (r < 0) goto out; r = ch341_set_handshake(dev, priv); if (r < 0) goto out; /* expect 0x9f 0xee */ r = ch341_get_status(dev); out: kfree(buffer); return r; 
}

可以看到,上位机对CH340的初始化有几步,会发送好几个请求,其中任何一个不成功都会导致初始化失败,于是我根据这些请求编写了对应的处理函数,具体如下。

我将所有的处理代码都放在了一个单独的文件ch340.c

  • ch340.c
#include "ch340.h"uint32_t ch341_state = 0xdeff;
static uint8_t buf1[2] = {0x30, 0};
static uint8_t buf2[2] = {0xc3, 0};
static uint8_t zero[2] = {0, 0};void CH340_Requset_Handle(USBD_HandleTypeDef *pdev, USBD_CDC_HandleTypeDef *hcdc, USBD_SetupReqTypedef *req)
{uint16_t wValue = req->wValue;switch(req->bRequest){case CH341_VERSION:USBD_CtlSendData(pdev, buf1, req->wLength);break;case CH341_REQ_READ_REG:if(wValue == 0x2518)USBD_CtlSendData(pdev, buf2, req->wLength);else if(wValue == 0x0706)USBD_CtlSendData(pdev, (uint8_t *)&ch341_state, req->wLength);break;case CH341_MODEM_OUT:USBD_CtlSendData(pdev, (uint8_t *)&ch341_state, req->wLength);break;default:USBD_CtlSendData(pdev, (uint8_t *)&zero, req->wLength);break;}return;
}
  • ch340.h
#ifndef CH340_H
#define CH340_H#include "usbd_def.h"
#include "usbd_cdc.h"#define CH341_MODEM_OUT 			 0xA4
#define CH341_REQ_READ_REG     0x95
#define CH341_VERSION		 			 0x5Fvoid CH340_Requset_Handle(USBD_HandleTypeDef *pdev, USBD_CDC_HandleTypeDef *hcdc, USBD_SetupReqTypedef *req);#endif

然后将这个处理函数添加到之前的请求处理函数中:

static uint8_t  USBD_CDC_Setup(USBD_HandleTypeDef *pdev,USBD_SetupReqTypedef *req)
{USBD_CDC_HandleTypeDef   *hcdc = (USBD_CDC_HandleTypeDef *) pdev->pClassData;uint8_t ifalt = 0U;uint16_t status_info = 0U;uint8_t ret = USBD_OK;switch (req->bmRequest & USB_REQ_TYPE_MASK){// 省略其他代码...case USB_REQ_TYPE_VENDOR:  //处理来自厂商的请求CH340_Requset_Handle(pdev, hcdc, req);break;default:USBD_CtlError(pdev, req);ret = USBD_FAIL;break;}return ret;
}

然后编译下载,插上USB,打开串口助手,成功打开串口!

但是还有问题,打开了串口却发送不了数据:

第三天:实现串口收发

这问题卡了我挺久的,甚至还跑去之前用标准库模拟CH341那哥们那里请教,结果他这样回复:

而且作者还顺手把仓库设置为只读模式,啊,看来只能看我自己了。

我选择了USBlyzer工具进行USB抓包,看看究竟是通信中的哪个步骤出了问题(软件下载地址)

抓到的结果如下:

可以看出这里并不是USB的请求出了问题,而是数据传输阶段出了问题,经过一系列查找资料后我突然意识到,可能是终端开的不对。

再回头看前面改的设备描述符中是这样写的:

/*Endpoint 2in Descriptor*/0x07,   /* bLength: Endpoint Descriptor size */0x05,   /* bDescriptorType: Endpoint */0x82,   /* bEndpointAddress: (IN2) */0x02,   /* bmAttributes: bulk */0x20,      0x00,   /* wMaxPacketSize: */0x00,   /* bInterval: *//*Endpoint 2out Descriptor*/0x07,   /* bLength: Endpoint Descriptor size */0x05,   /* bDescriptorType: Endpoint */0x02,   /* bEndpointAddress: (out2) */0x02,   /* bmAttributes: bulk */0x20,      0x00,   /* wMaxPacketSize: */0x00,   /* bInterval: *//*Endpoint 1in Descriptor*/0x07,   /* bLength: Endpoint Descriptor size */0x05,   /* bDescriptorType: Endpoint */0x81,   /* bEndpointAddress: (IN1) */0x03,   /* bmAttributes: Interrupt */0x08,      /* wMaxPacketSize: */0x00,0x01,   /* bInterval: */

我发现这里使用的是EndPoint 1 INEndPoint 2 INEndPoint 2 OUT、而ST自己的代码里默认使用的是:

/** @defgroup usbd_cdc_Exported_Defines* @{*/
#define CDC_IN_EP                                   0x81U  /* EP1 for data IN */
#define CDC_OUT_EP                                  0x01U  /* EP1 for data OUT */
#define CDC_CMD_EP                                  0x82U  /* EP2 for CDC commands */

显然对不上号,所以我这里把CDC_OUT_EP改为0x02U(EP2 OUT):

/** @defgroup usbd_cdc_Exported_Defines* @{*/
#define CDC_IN_EP                                   0x81U  /* EP1 for data IN */
#define CDC_OUT_EP                                  0x02U  /* EP1 for data OUT */
#define CDC_CMD_EP                                  0x82U  /* EP2 for CDC commands */

编译下载,插上USB,发送数据,成功!

注意:这里的IN和OUT是相对于上位机而言,即IN代表数据从单片机到上位机,OUT代表数据从上位机到单片机

现在串口的接收功能已经实现了,然后实现串口的发送功能。

ST官方代码的发送使用的是EP1,但CH340应该使用EP2,这里修改usbd_cdc.c中的USBD_CDC_TransmitPacket函数,将其中的CDC_IN_EP改为CDC_CMD_EP

/*** @brief  USBD_CDC_TransmitPacket*         Transmit packet on IN endpoint* @param  pdev: device instance* @retval status*/
uint8_t  USBD_CDC_TransmitPacket(USBD_HandleTypeDef *pdev)
{USBD_CDC_HandleTypeDef   *hcdc = (USBD_CDC_HandleTypeDef *) pdev->pClassData;if (pdev->pClassData != NULL){if (hcdc->TxState == 0U){/* Tx Transfer in progress */hcdc->TxState = 1U;/* Update the packet total length */pdev->ep_in[CDC_CMD_EP & 0xFU].total_length = hcdc->TxLength;/* Transmit next packet */USBD_LL_Transmit(pdev, CDC_CMD_EP, hcdc->TxBuffer,(uint16_t)hcdc->TxLength);return USBD_OK;}else{return USBD_BUSY;}}else{return USBD_FAIL;}
}

最后为了测试,参照实现USB CDC通信实现一个复读机:

static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{/* USER CODE BEGIN 6 */USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);USBD_CDC_ReceivePacket(&hUsbDeviceFS);CDC_Transmit_FS(Buf, *Len);return (USBD_OK);/* USER CODE END 6 */
}

编译下载,插上USB,发送数据:

成功!

参考

  • usb-request - 喝水的面包 - CSDN博客
  • 授人以渔 第二节:基于stm32f407usb例程讲解USB运行机制(下)
  • USB CDC从理论到实践
  • STM32 之 USB 虚拟串口
  • blackmiaool/STM32_USB_CH341

本文标签: 基于STM32 CDC模拟CH340