用 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_HS
和 CDC_Control_FS
中没有实现 case CDC_SET_LINE_CODING
和 case 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 */ }