解决STM32F4系列MCU在Windows 10上无法打开 Virtual COM Port 的问题

用 CubeMX 生成代码试图在 STM32F4 系列的单片机中加入 USB CDC 中间件时,会出现一个奇怪的 bug,即在 macOS, Linux 等系统下运行时,可以正常打开串口操作,而在 Windows 10 中虽然在设备管理器中可以看到插入之后已经显示为 USB Serial Device, 但并不能正常打开操作。

比如最近我用的 STM32F407VET6,使用 CubeMX 生成代码,其中 USB_Device 设置为 Communication Device Class,所有参数都用默认,然后在主循环中只用一个最基本的 CDC_Transmit_FS 函数,编译之后依然不行。稍微调试之后可以发现如果把 Linker 的 minimum heap size 调到一些特定的数值上, 则程序有时候会正常。但这个很麻烦,因为随便改动程序之后就又要重新调整,所以稍微认真地找了一下bug,发现应该是官方库的函数有一些 bug。

编译后连接电脑,使用 pyserial 模块在 python 中连接串口,打开时报错为

SerialException: Cannot configure port, something went wrong. Original message: OSError(22, 'The parameter is incorrect.', None, 87)

继续向下追查,发现返回的错误码 87 是由 Windows 10 新加的驱动 usbser.sys 提供的 API SetCommState() 返回 False 造成的,87代表 INVALID_PARAMETER。继续找下去可以看到这是因为在ST的官方库中,usbd_cdc_if.c 中的函数 CDC_Control_HSCDC_Control_FS 中没有实现 case CDC_SET_LINE_CODINGcase CDC_GET_LINE_CODING,而 Windows API 在打开 COM 口时会固定地设置一下 LINE CODING,然后再回读一次 LINE CODING,如果不一致将拒绝连接,因此造成虽然可以在设备管理器中正确识别到 VCP (设备管理器只管看 USB 设备自己是不是声明自己为 class 02 subclass 02 设备,不管实际上能不能收发),但是打开串口时就会返回异常。

为了解决这个 bug,我们要么自己实现一个串口驱动跳过检查 LINE CODING,要么在 usbd_cdc_if.c 中随便给他应付一下。最简单的办法就是直接在 CDC_Control_FS 这个函数中设置一个临时的 buffer 保存 SET_LINE_CODING 的数据,当 windows 要求检查 line coding 的时候,就再把之前保存下来的发回去诈骗一下windows就行。(注意,这样做之后在 windows 上设置 line coding 显然就不会起任何作用,因为我们只是反弹了一下。不过这个本来也没用。)

/**
  * @brief  Manage the CDC class requests
  * @param  cmd: Command code
  * @param  pbuf: Buffer containing command data (request parameters)
  * @param  length: Number of data to be sent (in bytes)
  * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
  */
static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length)
{
  /* USER CODE BEGIN 5 */
  // temporary buffer to store line coding
  uint8_t tbuf[7] = {0,0,0,0,0,0,0};
  switch(cmd)
  {
    case CDC_SEND_ENCAPSULATED_COMMAND:

    break;

    case CDC_GET_ENCAPSULATED_RESPONSE:

    break;

    case CDC_SET_COMM_FEATURE:

    break;

    case CDC_GET_COMM_FEATURE:

    break;

    case CDC_CLEAR_COMM_FEATURE:

    break;

  /*******************************************************************************/
  /* Line Coding Structure                                                       */
  /*-----------------------------------------------------------------------------*/
  /* Offset | Field       | Size | Value  | Description                          */
  /* 0      | dwDTERate   |   4  | Number |Data terminal rate, in bits per second*/
  /* 4      | bCharFormat |   1  | Number | Stop bits                            */
  /*                                        0 - 1 Stop bit                       */
  /*                                        1 - 1.5 Stop bits                    */
  /*                                        2 - 2 Stop bits                      */
  /* 5      | bParityType |  1   | Number | Parity                               */
  /*                                        0 - None                             */
  /*                                        1 - Odd                              */
  /*                                        2 - Even                             */
  /*                                        3 - Mark                             */
  /*                                        4 - Space                            */
  /* 6      | bDataBits  |   1   | Number Data bits (5, 6, 7, 8 or 16).          */
  /*******************************************************************************/
    case CDC_SET_LINE_CODING:
        // add these to store the buffer
    	tbuf[0] = pbuf[0];
   	tbuf[1] = pbuf[1];
   	tbuf[2] = pbuf[2];
   	tbuf[3] = pbuf[3];
   	tbuf[4] = pbuf[4];
   	tbuf[5] = pbuf[5];
	tbuf[6] = pbuf[6];
    break;

    case CDC_GET_LINE_CODING:
        // add these to send back what is stored, to cheat on windows API
    	pbuf[0] = tbuf[0];
    	pbuf[1] = tbuf[1];
   	pbuf[2] = tbuf[2];
   	pbuf[3] = tbuf[3];
   	pbuf[4] = tbuf[4];
   	pbuf[5] = tbuf[5];
   	pbuf[6] = tbuf[6];
    break;

    case CDC_SET_CONTROL_LINE_STATE:

    break;

    case CDC_SEND_BREAK:

    break;

  default:
    break;
  }

  return (USBD_OK);
  /* USER CODE END 5 */
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.