Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于STM32 CDC模拟CH340 #97

Open
imuncle opened this issue Nov 29, 2019 · 3 comments
Open

基于STM32 CDC模拟CH340 #97

imuncle opened this issue Nov 29, 2019 · 3 comments
Labels
RM 参加RoboMaster比赛中的成长记录 STM32 STM32学习开发记录

Comments

@imuncle
Copy link
Owner

imuncle commented Nov 29, 2019

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

image

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

image

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

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

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

第一天:让电脑识别为CH340

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

使用STM32CubeMX生成代码

image

这里修改以下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了:
image

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

第二天:响应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命令的结构:

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

生成的代码中处理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会请求失败。

image

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

这里我参考了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		 			 0x5F

void 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,打开串口助手,成功打开串口!

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

第三天:实现串口收发

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

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

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

抓到的结果如下:
QQ图片20191129121819

可以看出这里并不是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,发送数据:
image

成功!

参考

@imuncle imuncle added RM 参加RoboMaster比赛中的成长记录 STM32 STM32学习开发记录 labels Nov 29, 2019
@zhifangs
Copy link

可以使用。不错。发现几个bug 但我不会修复。只能正常发送14个字节。多了会导致数据出错,并在重新上电前不可还原正常状态。

@leovs
Copy link

leovs commented Aug 4, 2022

linux 好像无法正常驱动

@blackmiaool
Copy link

好详细

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
RM 参加RoboMaster比赛中的成长记录 STM32 STM32学习开发记录
Projects
None yet
Development

No branches or pull requests

4 participants