Zi

Poke 49233, 0

By
on

解决 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 显然就不会起任何作用,因为我们只是反弹了一下。不过这个本来也没用。)

代码如下:usbd_cdc_if.c

/**
* @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 */
}