imuncle / imuncle.github.io

大叔的个人小站
https://imuncle.github.io/
78 stars 17 forks source link

基于STM32 CDC模拟CH340 #97

Open imuncle opened 4 years ago

imuncle commented 4 years ago

之前写过一篇使用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

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
```c
#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

成功!

参考

zhifangs commented 3 years ago

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

leovs commented 1 year ago

linux 好像无法正常驱动

blackmiaool commented 4 months ago

好详细