6. USB:CDC_ACM设备(虚拟串口)

目录

配置

设备解析

设备描述符 

配置描述符 

一个基本的配置描述符

接口描述符

端点描述符

字符串描述符

语言ID描述符 

描述符的获取

标准设备请求

设备类的请求

设备的配置

通信框架分析

1.检查数据准备好函数

2.发送数据函数 

3.接收数据函数

4.应用示例 

总结


配置

打开usbd_conf.h文件,找到总配置开关,USE_USBD_CDCACM 配置为1。其他的为0,注意:USBD_INT_DEBUG 这个配置是在中断调试的配置,如果要运行这个工程,这个配置一定要配置为0。

设备解析

打开cdc_acm_core.c文件,对应的头文件是cdc_acm_core.h。在打开对应的配置的后,只会有一个设备类的文件生效。

设备描述符 

usb_desc_dev cdc_dev_desc =
{
    .header = 
     {
         .bLength          = USB_DEV_DESC_LEN, //描述符长度 0x12
         .bDescriptorType  = USB_DESCTYPE_DEV, //描述符类型 1:设备描述符
     },
    .bcdUSB                = 0x0200U,          //USB版本 0x0200 表示为USB2.0
    .bDeviceClass          = USB_CLASS_CDC,    //设备类的代码,CDC:0x02
    .bDeviceSubClass       = 0x00U,            //设备子类  
    .bDeviceProtocol       = 0x00U,            //设备协议
    .bMaxPacketSize0       = USBD_EP0_MAX_SIZE,//端点0的最大包大小,USB2.0全速设备中最大为64
    .idVendor              = USBD_VID,         //设备的制造商ID
    .idProduct             = USBD_PID,         //设备的产品ID
    .bcdDevice             = 0x0100U,          //设备版本 这个自己自定义的
    .iManufacturer         = STR_IDX_MFC,      //有关制造商的描述字符串描述符索引
    .iProduct              = STR_IDX_PRODUCT,  //有关产品的描述字符串描述符索引
    .iSerialNumber         = STR_IDX_SERIAL,   //有关产品唯一序列号的描述字符串描述符索引
    .bNumberConfigurations = USBD_CFG_MAX_NUM, //配置描述符的数量 这里为1
};

这里使用框架中自带了标准设备描述符的数据结构定义了一个 设备描述符,并初始化了相关数据,数据中表示的是设备的信息。

.header 描述符头数据结构

bLength

表示这个单独的描述符长度(包含头长度)

bDescriptorType

表示描述符的类型

配置描述符 

前面我们说过,配置描述符是一个描述符集。就是包含了多个基本描述符的描述符,所以这里的数据结构是不定的,根据使用的配置而定,在这里的话,在头文件定义了配置描述符的数据结构,描述符定义的顺序是有讲究的,基本的配置描述符一定在最前面,接着就是接口描述符,然后就是这个接口下面的一些描述符,比如端点和功能等等。

这里的配置描述符包含了:

一个基本的配置描述符

wTotalLength

整个配置描述符集的总长度,需要提前计算好

bNumInterfaces

这个配置下的接口数量

bConfigurationValue

当前配置描述符的配置索引,设备可以有多个配置

iConfiguration

这个配置的字符串描述符索引,可选

bmAttributes

配置属性,Bit7:保留(默认为1)
Bit6:0-总线供电 1-自供电
Bit5:0-支持远程唤醒 1-不支持远程唤醒

 bMaxPower:设备在当前配置下的最大功耗。

接口描述符

这个接口下包含了多个CDC协议下定义的描述符,不是标准的描述,还包含了一个端点描述符用于命令端点。

这是一个标准的接口描述符,在这里可以进一步表示的它的设备类和子类协议以及设备自身自定义的协议,所以USB会有复合设备的概念,因为你一个设备下可以有多个不同协议的接口。

bInterfaceNumber

当前配置中的接口索引

bAlternateSetting

接口号的备注  

bNumEndpoints

这个接口下的端点数量

bInterfaceClass

接口的协议类

bInterfaceSubClass

接口的协议子类

bInterfaceProtocol

接口的设备协议

iInterface

描述这个接口的字符串描述符索引,为0就是没有

端点描述符

bEndpointAddress

端点地址,设备中端点的唯一标识,一个字节,最高位表示方向  1:IN方向  0:OUT方向,其余为地址。

bmAttributes

端点类型:控制、中断、批量、等时

wMaxPacketSize

端点的最大数据包大小

bInterval

中断端点的中断间隔   ms为单位

所以在配置描述符集可以看到若干个标准的描述符和协议特定的描述符,总的来说,一个配置描述符至少包含一个基本的配置描述符和一个接口描述符和一个端点描述符,其它都为可选可扩展的。 

字符串描述符

字符串描述符可以存在多个,甚至可以多定义都没有关系,它只是一个描述信息,返回主机时是自己操作。

unicode_string

用unicode编码的字符串,一个字符占两字节

语言ID描述符 

这是一个标准的描述符,有点类似字符串描述符,所以一般都和字符串描述符放在一起,所有字符串描述符的索引都从1开始,是因为0索引的字符串描述符是语言ID描述符,主机在获取字符串描述符前必须先获取语言ID描述符。

wLANGID

语言ID,

0x0409表示英文

0x0804表示中文

描述符的获取

所以可以看到,这个cdc_desc包含了所有的描述符信息,然后这个实例提供外部使用,接下来我们看一下获取描述符的请求是什么样子。 

1.当设备受到主机获取描述符的标准请求后会进入这里,这里可以发现会根据请求数据判定为获取设备描述符、获取配置描述符、获取字符串描述符三个大类进入 std_desc_get[ ]这个函数指针数组的函数。

2.当追溯到最底层函数发现,本质就是返回我们上面描述符集对应的位置的地址。 

所以描述符获取的本质就是,我们把实现定义好的数据地址配置到端点0传输缓冲区地址,然后通过端点0把数据发送到主机,这样主机就获取到了设备的信息。

标准设备请求

 在框架的标准设备请求器可以看到如下标准请求的定义。

上述都是标准设备请求才会触发的处理,每个处理有对应的函数,其中最常见的就是设置地址,获取描述符,设置配置这三个请求,包括我们这个ACM_CDC设备枚举阶段,主机在设置完地址后,开始获取大量的描述符,获取完没有问题后,发出设置配置请求后标准的设备请求就结束了。 

上面说了获取描述符的过程,接下来是设置配置的过程。

 这个函数中可以看出,判定设备处于什么状态进入什么设置,最终调用的函数无非就是这两个

(void)udev->class_core->init(udev, config);       设备类初始化

(void)udev->class_core->deinit(udev, udev->config);   设备类卸载

这两个函数都属于设备类的功能,在设备类文件中定义

 

设备类初始化函数主要完成除了0地址端点,一般0地址的管理是在框架中的,是不能修改的。剩下所有用到的端点初始化以及挂载事务处理函数,设置通信的数据结构。

卸载就比较简单了,就是去初始化的一部分,不过这个函数一般不会调用就是了,为了结构完整还是把它完成了,如果这个函数被调用,那么设备就不可能正常工作。 

设备类的请求

当设置配置完成后,主机会发送一些CDC类的请求,这些请求是进入不了标准设备请求,在请求处理器会进入 USB_REQTYPE_CLASS的请求。

追溯一下这个请求最终调用的是哪个函数,发现调用是设备类功能函数中的设备类请求处理函数,这个函数的定义也在设备类文件中定义。

 对于CDC_ACM设备,主机会获取连接代码,连接代码应该包含串口四要素的数据(波特率,数据位,停止位,校验位),然后设置连接代码,最后设置连接状态。

设备的配置

所以在设备类文件中会完成有关描述符以及设备类功能的数据结构设计以及对应的实例,并提供外部声明。

在应用时调用初始化函数时,将实例地址填进去就可以成功初始化并成功枚举。

从上述配置我们可以看出:在当前设置中,一共配置了4个端点(包含0端点),然后字符串描述符的数量为4,端点1和端点2为IN方向端点,端点3为OUT方向端点。

在我们枚举成功后,可以在PC的设备管理器看到我们的设备。

USB串行设备,这个描述是一个默认描述。可以看到COM15是我们的设备,它属于CDC_ACM设备,是虚拟串口,为什么我们描述符的产品信息没有被显示出来?而且USB转串口芯片的设备就可以识别出来不一样的信息?因为我们的设备是加载自带的驱动,自带的CDC驱动,这样我们就不用自己写驱动,用官方的标准驱动,但是CH340驱动是我们另外加载的,所以它才能够显示这些信息。

在上述的端点描述可知,地址1端点是IN方向的批量端点,批量端点的特性是在总线空闲时,主机会一直对端点IN令牌,如果这个端点有数据发送到主机,就可以在IN令牌后发送数据,达到及时响应的需求。但是我用逻辑分析看到,在枚举成功后,只收到了主机的帧起始包,并没有预想的IN令牌。 

说明主机在总线上没有活动,那我设备发送数据给主机不是无法发出吗?

在枚举阶段的最后有一个设置连接状态,那个设置就是表示主机和设备建立了连接,但是没有开始通信,所以主机不会对设备的IN批量端点发IN令牌,那什么时候开始建立通信?

在PC上打开串口助手,找到对应的串口号,在打开串口或关闭串口的时候,这个过程用逻辑分析可以看到,主机对设备的0号端点发送了几个设备类请求,获取连接代码、设置连接代码。设置连接状态。

所以可以推断,在打开串口的时候,设置了连接状态为通信状态,然后在用逻辑分析仪看IN方向的批量端点,可以发现主机几乎一直在对IN方向的批量端点发送IN令牌,这个时候发送数据和接收数据都可以正常进行。

通信框架分析

到了这里,对CDC_ACM设备的整个枚举过程就结束了,在配置中我们设置了一个数据接口,数据接口下有两个批量端点,一个IN,一个OUT,这是用于通信数据的端点。

在设备类中定义了一个数据结构,其中有一个64byte的数据缓冲区。在设备类的初始化函数中实例化。

1.检查数据准备好函数

uint8_t cdc_acm_check_ready(usb_dev *udev)
{
    if (udev->class_data[CDC_COM_INTERFACE] != NULL) {
        usb_cdc_handler *cdc = (usb_cdc_handler *)udev->class_data[CDC_COM_INTERFACE];

        if ((1U == cdc->packet_receive) && (1U == cdc->pre_packet_send)) {
            return 0U;
        }
    }

    return 5U;
}

当从主机接收到了数据到数据缓冲区,并且上一次接收的数据已经发出去后,返回0。

2.发送数据函数 

void cdc_acm_data_send (usb_dev *udev)
{
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->class_data[CDC_COM_INTERFACE];
    uint32_t data_len = cdc->receive_length;

    if ((0U != data_len) && (1U == cdc->packet_sent)) {
        cdc->packet_sent = 0U;
        usbd_ep_send(udev, CDC_IN_EP, (uint8_t*)(cdc->data), (uint16_t)data_len);
        cdc->receive_length = 0U;
    }
}

这里只有当 满足if条件的时候才能真正发送数据到主机,但是data_len只有再接收到主机的数据后才不为0,所以这里发送数据框架是只能在数据回显中才能发送出去的。

3.接收数据函数

void cdc_acm_data_receive(usb_dev *udev)
{
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->class_data[CDC_COM_INTERFACE];

    cdc->packet_receive = 0U;
    cdc->pre_packet_send = 0U;

    usbd_ep_recev(udev, CDC_OUT_EP, (uint8_t*)(cdc->data), USB_CDC_RX_LEN);
}

从OUT端点缓冲区读取数据到设计的通信框架中的数据缓冲区。

4.应用示例 

所以目前这个通信设计只能用于这个数据回显场景,其实本质就是用了一些标志位控制了而已,用户修改后可以实现任意的数据收发情景。 

usbd_ep_recev(udev, CDC_OUT_EP, (uint8_t*)(cdc->data), USB_CDC_RX_LEN);
usbd_ep_send(udev, CDC_IN_EP, (uint8_t*)(cdc->data), (uint16_t)data_len);

最终调用是端点的数据收发函数,这里可以根据需要自由设计,到了这里就是任意的数据收发了。

发送数据:当调用了端点发送数据函数,会根据数据长度填充到端点缓冲区,一次最多64byte,然后等待IN令牌到来时自动发送之后触发传输完成中断。

 if (USBD_EPxCS(ep_num) & EPxCS_TX_ST) {
                    /* clear successful transmit interrupt flag */
                    USBD_EP_TX_ST_CLEAR(ep_num);

                    usb_transc *transc = &udev->transc_in[ep_num];

                    if (0U == transc->xfer_len) {
                        if (udev->ep_transc[ep_num][TRANSC_IN]) {
                            udev->ep_transc[ep_num][TRANSC_IN](udev, ep_num);
                        }
                    } else {
                        usbd_ep_send(udev, ep_num, transc->xfer_buf, transc->xfer_len);
                    }
                }

当数据没有传输完时,继续从传输数据缓冲区搬运数据到端点数据缓冲区,知道数据传输完后调用对应的端点IN事务,这个事务函数在设备类文件中定义。

static void cdc_acm_data_in (usb_dev *udev, uint8_t ep_num)
{
    usb_transc *transc = &udev->transc_in[ep_num];
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->class_data[CDC_COM_INTERFACE];

    if (transc->xfer_count == transc->max_len) {
        usbd_ep_send(udev, EP_ID(ep_num), NULL, 0U);
    } else {
        cdc->packet_sent = 1U;
        cdc->pre_packet_send = 1U;
    }
}

当数据发送完时,最后一个包是一个完整的64byte包,需要补一个0数据包,表示这次传输结束,因为主机会自动检测短包,当收到短包就表示数据结束了,所以完整包后结束数据需要补0数据包,这是一个特殊的数据包,表示数据结束。

这样一来,发送数据的自动处理就设计好了,可以实现自动分包和补包,所以具体需求用户可以修改设备类中功能的设计,来达到自己的需求,中断的处理不建议修改,中断处理设计的非常完美。

接收数据:当调用了端点接收数据函数后,会配置好接收数据的长度和缓冲区,并且使能对应OUT端点的接收,当设备接收到主机数据到端点缓冲区时,将数据从端点缓冲区搬运到数据接收缓冲区,一次最多64byte,搬运完判断是否达到需要的接受量或接收到的数据小于一个包的最大数据,如果达到则调用设备类回调告诉数据接收完成,并返回计数。

usb_transc *transc = &udev->transc_out[ep_num];

                        uint16_t count = udev->drv_handler->ep_read (transc->xfer_buf, ep_num, (uint8_t)EP_BUF_SNG);
                        //缓存区里的端点缓存区描述符接收计数是硬件设置的 调试软件没有任何意义 
                        //count返回的是这次数据包的长度 每次数据包都会返回
                        transc->xfer_buf += count;
                        transc->xfer_count += count;

                        if ((transc->xfer_count >= transc->xfer_len) || (count < transc->max_len)) {
                            if (udev->ep_transc[ep_num][TRANSC_OUT]) {
                                udev->ep_transc[ep_num][TRANSC_OUT](udev, ep_num);
                            }
                        } else {
                            udev->drv_handler->ep_rx_enable(udev, ep_num);//使能将触发中断
                        }

如果单次搬运后并没有达到预计的接收数据量且预计的接收数据量大于等于一个包,则会再次使能端点的接收,当下次数据到来时再继续搬运直到达到预计的接收量。

所以接收数据的分包是需要时间来处理的。当主机发送大于一个包的最大数据量的数据时会自动分包,首先主机发送 数据包1,设备成功接收并应答(设备开始处理这个数据),主机继续发送数据包2,设备在处理数据,并没有使能端点的接收(在收到数据后会自动失能),所以不应答主机,主机就会一直发送数据包2,直到设备应答为止,以此类推实现分包数据并保证设备成功接收。



总结

文章以实现cdc_acm设备为demo进行了虚拟串口的测试,并通过demo解析了整个驱动框架的核心,以及中断处理和请求处理。整个中断的设计是非常合理的,以及请求的处理,但是设备类下的文件实现需要用户自己实现,也可以基于demo进行修改。比如描述符和设备类功能接口下函数。以及通信框架。

cdc_acm的框架相对其他的设备类来说比较复杂,但是可以从这个设备类入手,方便我们后续在此基础上改写为HID键盘设备WINUSB通用串行总线设备

虽然不用修改驱动框架和中断事务处理,但是在实现WINUSB设备时,需要优化一下驱动框架的请求处理,因为WINUSB设备类是特定厂商设备,所以在某一个标准设备请求下存在差异,所以这个时候就需要优化,前提是需要得看懂整个框架的处理

### 回答1: 要在Linux系统中开启cdc_acm模块,需要按照以下步骤进行操作: 1. 打开终端,以root用户或具有sudo权限的用户身份登录。 2. 使用文本编辑器打开"/etc/modules"文件,例如使用命令"sudo nano /etc/modules"。 3. 在文件末尾添加一行"cdc_acm",保存并关闭文件。 4. 使用文本编辑器打开"/etc/modprobe.d/blacklist.conf"文件,例如使用命令"sudo nano /etc/modprobe.d/blacklist.conf"。 5. 在文件末尾找到或添加以下行,并确保这些模块没有被列入黑名单(没有在前面加上"#"号): ``` blacklist usbserial blacklist option blacklist pl2303 ``` 6. 保存并关闭文件。 7. 重新启动系统,或者使用命令"sudo modprobe cdc_acm"加载模块。 完成上述步骤后,cdc_acm模块将被成功开启,并可以在Linux系统中使用相关的功能。 ### 回答2: 在Linux系统中开启cdc_acm功能,可以通过以下步骤完成: 首先,确保提前准备好了一个USB-to-serial编程线缆(也称为CDC-ACM设备),该设备将在连接到计算机时被识别为串行设备。 然后,将USB-to-serial编程线缆插入计算机的USB端口。插入成功后,系统会自动识别该设备,并分配一个设备文件名,如/dev/ttyACM0。 接下来,在终端中打开一个新的命令行窗口,以便执行以下命令。 首先,检查系统中的USB驱动是否已加载正确。输入以下命令: lsusb 该命令会列出所有连接到计算机的USB设备。如果你正确插入了USB-to-serial编程线缆,它应该能够在列表中找到。 然后,加载cdc_acm内核模块。输入以下命令: sudo modprobe cdc_acm 这将启用cdc_acm模块,将USB-to-serial编程线缆识别为串行设备。 最后,检查设备文件是否已创建。输入以下命令: ls /dev/ttyACM* 如果设备文件已正确创建,它将显示在终端中。 此时,你可以通过访问该设备文件来进行USB-to-serial通信。例如,可以使用minicom或者其他串口通信工具来与连接到USB-to-serial编程线缆上的外部设备进行通信。 注意,以上步骤可能需要使用root权限,可以使用sudo命令来获取临时的root权限。 ### 回答3: 在Linux中启用cdc_acm驱动需要经过以下几个步骤: 1. 首先,确保已经安装了USB设备支持的驱动程序。可以通过运行命令`lsusb`来检查是否识别到USB设备。 2. 运行命令`dmesg`,查看系统日志,确认是否存在与cdc_acm驱动相关的信息。如果有相关信息,则表示驱动已经加载成功。 3. 如果系统中尚未加载cdc_acm驱动,可以使用modprobe命令加载该驱动程序。运行命令`sudo modprobe cdc_acm`,系统将自动加载并启用cdc_acm驱动。 4. 为了确保驱动在每次启动时都能自动加载,可以在`/etc/modules-load.d/modules.conf`文件中添加一行`cdc_acm`,保存并退出文件。 5. 最后,重新启动系统以使更改生效。运行命令`sudo reboot`,系统将重新启动。 通过以上步骤,你可以在Linux系统中启用cdc_acm驱动,以便支持与CDC/ACM(Communication Device Class/Abstract Control Model)兼容的USB设备的连接和使用。请注意,具体步骤可能因不同的Linux发行版而有所不同,以上仅为一般步骤。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值