imuncle.github.io
imuncle.github.io copied to clipboard
基于STM32 CDC模拟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 0x5F
void 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,发送数据:
成功!
参考
可以使用。不错。发现几个bug 但我不会修复。只能正常发送14个字节。多了会导致数据出错,并在重新上电前不可还原正常状态。
linux 好像无法正常驱动
好详细