/** @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 */
之前写过一篇使用STM32虚拟串口功能的文章:实现USB CDC通信,但是这个有个很大的问题,它的Windows驱动的数字签名过期了,我在我的电脑里搜索了一下,发现有两个驱动:
不过很可惜,这两个驱动都过期了:
这就直接导致在Windows上使用ST自己的虚拟串口需要强制跳过数字签名这一步,而每次电脑重启之后Windows就会恢复默认设置,最麻烦的是每次还必须通过重启设置,不过Linux下倒没这个问题,因为数字签名是Windows自己搞出来的东西。
但还是很烦,所以我决定抛弃ST官方的虚拟串口驱动,正好我找到了别人用STM32模拟CH341的代码:blackmiaool/STM32_USB_CH341 ,但这已经是五六年前的代码了,当时还是标准库,所以我决定把它用HAL库实现。
踩了一些坑,这玩意儿花了我三天时间,主要还是对USB协议不太熟悉,下面就按照我踩坑的时间顺序记录。
第一天:让电脑识别为CH340
这一步很简单,只需要改变设备描述符就行了,具体更改如下:
使用STM32CubeMX生成代码
这里修改以下PID和VID,然后字符串名称就随便写,点击生成代码。
修改设备描述符
在
usbd_desc.c
里面,修改USBD_FS_DeviceDesc
变量:修改设备配置描述符
然后修改
usbd_cdc.c
文件里面的USBD_CDC_CfgFSDesc
变量(因为我配置的Full Speed,如果是其他速度就修改对应的变量就行):然后编译下载,插上USB,电脑就会识别到CH340了:![image](https://user-images.githubusercontent.com/35989223/69861398-439d4080-12d3-11ea-9290-81ed973c2d43.png)
不过这里有感叹号,点开发现是因为Windows有的请求失败,这是显然的,因为我们还没有写相关的东西呢。![image](https://user-images.githubusercontent.com/35989223/69861484-6cbdd100-12d3-11ea-9e60-a831b9dbd777.png)
第二天:响应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命令的结构:
生成的代码中处理USB请求的代码在
usbd_cdc.c
中:经过代码分析可以发现,官方代码并没有处理“厂商”的请求,即
bmRequest
的五六位为10的请求,所以需要修改一下:然后编译下载,插上USB,哈哈,感叹号就没了,其实之前是因为厂商的请求代码都将其处理为错误情况,所以Windows会请求失败。
但其实我们还是没有处理Windows的请求,所以真正使用时连串口都打不开:![image](https://user-images.githubusercontent.com/35989223/69862577-b14a6c00-12d5-11ea-8382-c626e462171b.png)
这里我参考了CH340的Linux驱动源码,当中有以下函数:
可以看到,上位机对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;
}
然后将这个处理函数添加到之前的请求处理函数中:
然后编译下载,插上USB,打开串口助手,成功打开串口!
但是还有问题,打开了串口却发送不了数据:![image](https://user-images.githubusercontent.com/35989223/69865256-abf02000-12db-11ea-87f3-ab66ef26893b.png)
第三天:实现串口收发
这问题卡了我挺久的,甚至还跑去之前用标准库模拟CH341那哥们那里请教,结果他这样回复:![image](https://user-images.githubusercontent.com/35989223/69865355-ece83480-12db-11ea-8369-329abd63f5b5.png)
而且作者还顺手把仓库设置为只读模式,啊,看来只能看我自己了。
我选择了
USBlyzer
工具进行USB抓包,看看究竟是通信中的哪个步骤出了问题(软件下载地址)抓到的结果如下:![QQ图片20191129121819](https://user-images.githubusercontent.com/35989223/69865536-56684300-12dc-11ea-82d1-d5f896d7fe1e.png)
可以看出这里并不是USB的请求出了问题,而是数据传输阶段出了问题,经过一系列查找资料后我突然意识到,可能是终端开的不对。
再回头看前面改的设备描述符中是这样写的:
我发现这里使用的是
EndPoint 1 IN
、EndPoint 2 IN
、EndPoint 2 OUT
、而ST自己的代码里默认使用的是:显然对不上号,所以我这里把
CDC_OUT_EP
改为0x02U
(EP2 OUT):编译下载,插上USB,发送数据,成功!
现在串口的接收功能已经实现了,然后实现串口的发送功能。
ST官方代码的发送使用的是EP1,但CH340应该使用EP2,这里修改
usbd_cdc.c
中的USBD_CDC_TransmitPacket
函数,将其中的CDC_IN_EP
改为CDC_CMD_EP
:最后为了测试,参照实现USB CDC通信实现一个复读机:
编译下载,插上USB,发送数据:![image](https://user-images.githubusercontent.com/35989223/69866783-07241180-12e0-11ea-8c37-21c3a592717e.png)
成功!
参考