admin 管理员组文章数量: 887007
基于STM32 CDC模拟CH340
代码已开源在GitHub上,地址:STM32_USB_CH340
之前写过一篇使用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
变量:
__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{0x12, /* bLength */0x01, /* bDescriptorType */0x10,0x01, /* bcdUSB = 1.10 */0xff, /* bDeviceClass: CDC */0x00, /* bDeviceSubClass */0x00, /* bDeviceProtocol */0x40, /* bMaxPacketSize0 */0x86,0x1a, /* idVendor = 0x1A86 */0x23,0x75, /* idProduct = 0x7523 */0x63,0x02, /* bcdDevice = 2.00 */1, /* Index of string descriptor describing manufacturer */2, /* Index of string descriptor describing product */1, /* Index of string descriptor describing the device's serial number */0x01 /* bNumConfigurations */
};
修改设备配置描述符
然后修改usbd_cdc.c
文件里面的USBD_CDC_CfgFSDesc
变量(因为我配置的Full Speed,如果是其他速度就修改对应的变量就行):
__ALIGN_BEGIN uint8_t USBD_CDC_CfgFSDesc[0x27] __ALIGN_END =
{/*Configuation Descriptor*/0x09, /* bLength: Configuation Descriptor size */0x02, /* bDescriptorType: Configuration */0x27, /* wTotalLength:no of returned bytes */0x00,0x01, /* bNumInterfaces: 1 interface */0x01, /* bConfigurationValue: Configuration value */0x00, /* iConfiguration: Index of string descriptor describing the configuration */0x80, /* bmAttributes: self powered */0x30, /* MaxPower 0 mA *//*Interface Descriptor*/0x09, /* bLength: Interface Descriptor size */0x04, /* bDescriptorType: Interface *//* Interface descriptor type */0x00, /* bInterfaceNumber: Number of Interface */0x00, /* bAlternateSetting: Alternate setting */0x03, /* bNumEndpoints: One endpoints used */0xff, /* bInterfaceClass: Communication Interface Class */0x01, /* bInterfaceSubClass: Abstract Control Model */0x02, /* bInterfaceProtocol: Common AT commands */0x00, /* iInterface: *//*Endpoint 2in Descriptor*/0x07, /* bLength: Endpoint Descriptor size */0x05, /* bDescriptorType: Endpoint */0x82, /* bEndpointAddress: (IN2) */0x02, /* bmAttributes: bulk */0x20, 0x00, /* wMaxPacketSize: */0x00, /* bInterval: *//*Endpoint 2out Descriptor*/0x07, /* bLength: Endpoint Descriptor size */0x05, /* bDescriptorType: Endpoint */0x02, /* bEndpointAddress: (out2) */0x02, /* bmAttributes: bulk */0x20, 0x00, /* wMaxPacketSize: */0x00, /* bInterval: *//*Endpoint 1in Descriptor*/0x07, /* bLength: Endpoint Descriptor size */0x05, /* bDescriptorType: Endpoint */0x81, /* bEndpointAddress: (IN1) */0x03, /* bmAttributes: Interrupt */0x08, /* wMaxPacketSize: */0x00,0x01, /* bInterval: */
};
然后编译下载,插上USB,电脑就会识别到CH340了:
不过这里有感叹号,点开发现是因为Windows有的请求失败,这是显然的,因为我们还没有写相关的东西呢。
第二天:响应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命令的结构:
偏移量 | 域 | 长度(字节) | 值 | 描述 |
---|---|---|---|---|
0 | bmRequestType | 0 | 位图 | 请求特征:D7:传输方向(0=主机至设备 1=设备至主机);D6…5:种类(0=标准 1=类 2=厂商 3=保留) |
1 | bRequest | 1 | 值 | 命令类型编码值 |
2 | wValue | 2 | 值 | 根据不同的命令,含义也不同 |
4 | wIndex | 2 | 索引或偏移 | 根据不同的命令,含义也不同,主要用于传送索引或偏移 |
6 | wLength | 2 | 值 | 如有数据传送阶段,此为数据字节数 |
生成的代码中处理USB请求的代码在usbd_cdc.c
中:
/*** @brief USBD_CDC_Setup* Handle the CDC specific requests* @param pdev: instance* @param req: usb requests* @retval status*/
static uint8_t USBD_CDC_Setup(USBD_HandleTypeDef *pdev,USBD_SetupReqTypedef *req)
{USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *) pdev->pClassData;uint8_t ifalt = 0U;uint16_t status_info = 0U;uint8_t ret = USBD_OK;switch (req->bmRequest & USB_REQ_TYPE_MASK){case USB_REQ_TYPE_CLASS :if (req->wLength){if (req->bmRequest & 0x80U){((USBD_CDC_ItfTypeDef *)pdev->pUserData)->Control(req->bRequest,(uint8_t *)(void *)hcdc->data,req->wLength);USBD_CtlSendData(pdev, (uint8_t *)(void *)hcdc->data, req->wLength);}else{hcdc->CmdOpCode = req->bRequest;hcdc->CmdLength = (uint8_t)req->wLength;USBD_CtlPrepareRx(pdev, (uint8_t *)(void *)hcdc->data, req->wLength);}}else{((USBD_CDC_ItfTypeDef *)pdev->pUserData)->Control(req->bRequest,(uint8_t *)(void *)req, 0U);}break;case USB_REQ_TYPE_STANDARD:switch (req->bRequest){case USB_REQ_GET_STATUS:if (pdev->dev_state == USBD_STATE_CONFIGURED){USBD_CtlSendData(pdev, (uint8_t *)(void *)&status_info, 2U);}else{USBD_CtlError(pdev, req);ret = USBD_FAIL;}break;case USB_REQ_GET_INTERFACE:if (pdev->dev_state == USBD_STATE_CONFIGURED){USBD_CtlSendData(pdev, &ifalt, 1U);}else{USBD_CtlError(pdev, req);ret = USBD_FAIL;}break;case USB_REQ_SET_INTERFACE:if (pdev->dev_state != USBD_STATE_CONFIGURED){USBD_CtlError(pdev, req);ret = USBD_FAIL;}break;default:USBD_CtlError(pdev, req);ret = USBD_FAIL;break;}break;default:USBD_CtlError(pdev, req);ret = USBD_FAIL;break;}return ret;
}
经过代码分析可以发现,官方代码并没有处理“厂商”的请求,即bmRequest
的五六位为10的请求,所以需要修改一下:
static uint8_t USBD_CDC_Setup(USBD_HandleTypeDef *pdev,USBD_SetupReqTypedef *req)
{USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *) pdev->pClassData;uint8_t ifalt = 0U;uint16_t status_info = 0U;uint8_t ret = USBD_OK;switch (req->bmRequest & USB_REQ_TYPE_MASK){// 省略其他代码...case USB_REQ_TYPE_VENDOR: //处理来自厂商的请求break;default:USBD_CtlError(pdev, req);ret = USBD_FAIL;break;}return ret;
}
然后编译下载,插上USB,哈哈,感叹号就没了,其实之前是因为厂商的请求代码都将其处理为错误情况,所以Windows会请求失败。
但其实我们还是没有处理Windows的请求,所以真正使用时连串口都打不开:
这里我参考了CH340的Linux驱动源码,当中有以下函数:
int ch341_configure(struct usb_device *dev, struct ch341_private *priv)
{ char *buffer; int r = -ENOMEM; const unsigned size = 8; dbg("ch341_configure()"); buffer = kmalloc(size, GFP_KERNEL); if (!buffer) goto out; /* expect two bytes 0x27 0x00 */ r = ch341_control_in(dev, 0x5f, 0, 0, buffer, size); if (r < 0) goto out; r = ch341_control_out(dev, 0xa1, 0, 0); if (r < 0) goto out; r = ch341_set_baudrate(dev, priv); if (r < 0) goto out; /* expect two bytes 0x56 0x00 */ r = ch341_control_in(dev, 0x95, 0x2518, 0, buffer, size); if (r < 0) goto out; r = ch341_control_out(dev, 0x9a, 0x2518, 0x0050); if (r < 0) goto out; /* expect 0xff 0xee */ r = ch341_get_status(dev); if (r < 0) goto out; r = ch341_control_out(dev, 0xa1, 0x501f, 0xd90a); if (r < 0) goto out; r = ch341_set_baudrate(dev, priv); if (r < 0) goto out; r = ch341_set_handshake(dev, priv); if (r < 0) goto out; /* expect 0x9f 0xee */ r = ch341_get_status(dev); out: kfree(buffer); return r;
}
可以看到,上位机对CH340的初始化有几步,会发送好几个请求,其中任何一个不成功都会导致初始化失败,于是我根据这些请求编写了对应的处理函数,具体如下。
我将所有的处理代码都放在了一个单独的文件ch340.c
:
- ch340.c
#include "ch340.h"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;switch(req->bRequest){case CH341_VERSION:USBD_CtlSendData(pdev, buf1, req->wLength);break;case CH341_REQ_READ_REG:if(wValue == 0x2518)USBD_CtlSendData(pdev, buf2, req->wLength);else if(wValue == 0x0706)USBD_CtlSendData(pdev, (uint8_t *)&ch341_state, req->wLength);break;case CH341_MODEM_OUT:USBD_CtlSendData(pdev, (uint8_t *)&ch341_state, req->wLength);break;default:USBD_CtlSendData(pdev, (uint8_t *)&zero, req->wLength);break;}return;
}
- ch340.h
#ifndef CH340_H
#define CH340_H#include "usbd_def.h"
#include "usbd_cdc.h"#define CH341_MODEM_OUT 0xA4
#define CH341_REQ_READ_REG 0x95
#define CH341_VERSION 0x5Fvoid CH340_Requset_Handle(USBD_HandleTypeDef *pdev, USBD_CDC_HandleTypeDef *hcdc, USBD_SetupReqTypedef *req);#endif
然后将这个处理函数添加到之前的请求处理函数中:
static uint8_t USBD_CDC_Setup(USBD_HandleTypeDef *pdev,USBD_SetupReqTypedef *req)
{USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *) pdev->pClassData;uint8_t ifalt = 0U;uint16_t status_info = 0U;uint8_t ret = USBD_OK;switch (req->bmRequest & USB_REQ_TYPE_MASK){// 省略其他代码...case USB_REQ_TYPE_VENDOR: //处理来自厂商的请求CH340_Requset_Handle(pdev, hcdc, req);break;default:USBD_CtlError(pdev, req);ret = USBD_FAIL;break;}return ret;
}
然后编译下载,插上USB,打开串口助手,成功打开串口!
但是还有问题,打开了串口却发送不了数据:
第三天:实现串口收发
这问题卡了我挺久的,甚至还跑去之前用标准库模拟CH341那哥们那里请教,结果他这样回复:
而且作者还顺手把仓库设置为只读模式,啊,看来只能看我自己了。
我选择了USBlyzer
工具进行USB抓包,看看究竟是通信中的哪个步骤出了问题(软件下载地址)
抓到的结果如下:
可以看出这里并不是USB的请求出了问题,而是数据传输阶段出了问题,经过一系列查找资料后我突然意识到,可能是终端开的不对。
再回头看前面改的设备描述符中是这样写的:
/*Endpoint 2in Descriptor*/0x07, /* bLength: Endpoint Descriptor size */0x05, /* bDescriptorType: Endpoint */0x82, /* bEndpointAddress: (IN2) */0x02, /* bmAttributes: bulk */0x20, 0x00, /* wMaxPacketSize: */0x00, /* bInterval: *//*Endpoint 2out Descriptor*/0x07, /* bLength: Endpoint Descriptor size */0x05, /* bDescriptorType: Endpoint */0x02, /* bEndpointAddress: (out2) */0x02, /* bmAttributes: bulk */0x20, 0x00, /* wMaxPacketSize: */0x00, /* bInterval: *//*Endpoint 1in Descriptor*/0x07, /* bLength: Endpoint Descriptor size */0x05, /* bDescriptorType: Endpoint */0x81, /* bEndpointAddress: (IN1) */0x03, /* bmAttributes: Interrupt */0x08, /* wMaxPacketSize: */0x00,0x01, /* bInterval: */
我发现这里使用的是EndPoint 1 IN
、EndPoint 2 IN
、EndPoint 2 OUT
、而ST自己的代码里默认使用的是:
/** @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 */
编译下载,插上USB,发送数据,成功!
注意:这里的IN和OUT是相对于上位机而言,即IN代表数据从单片机到上位机,OUT代表数据从上位机到单片机
现在串口的接收功能已经实现了,然后实现串口的发送功能。
ST官方代码的发送使用的是EP1,但CH340应该使用EP2,这里修改usbd_cdc.c
中的USBD_CDC_TransmitPacket
函数,将其中的CDC_IN_EP
改为CDC_CMD_EP
:
/*** @brief USBD_CDC_TransmitPacket* Transmit packet on IN endpoint* @param pdev: device instance* @retval status*/
uint8_t USBD_CDC_TransmitPacket(USBD_HandleTypeDef *pdev)
{USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *) pdev->pClassData;if (pdev->pClassData != NULL){if (hcdc->TxState == 0U){/* Tx Transfer in progress */hcdc->TxState = 1U;/* Update the packet total length */pdev->ep_in[CDC_CMD_EP & 0xFU].total_length = hcdc->TxLength;/* Transmit next packet */USBD_LL_Transmit(pdev, CDC_CMD_EP, hcdc->TxBuffer,(uint16_t)hcdc->TxLength);return USBD_OK;}else{return USBD_BUSY;}}else{return USBD_FAIL;}
}
最后为了测试,参照实现USB CDC通信实现一个复读机:
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{/* USER CODE BEGIN 6 */USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);USBD_CDC_ReceivePacket(&hUsbDeviceFS);CDC_Transmit_FS(Buf, *Len);return (USBD_OK);/* USER CODE END 6 */
}
编译下载,插上USB,发送数据:
成功!
参考
- usb-request - 喝水的面包 - CSDN博客
- 授人以渔 第二节:基于stm32f407usb例程讲解USB运行机制(下)
- USB CDC从理论到实践
- STM32 之 USB 虚拟串口
- blackmiaool/STM32_USB_CH341
本文标签: 基于STM32 CDC模拟CH340
版权声明:本文标题:基于STM32 CDC模拟CH340 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1732352280h1533432.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论