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