ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32实现USB虚拟串口与HID复合设备完整教程

STM32实现USB虚拟串口与HID复合设备完整教程 简介面向基于STM32C8T6ARM Cortex-M0内核进行USB外设开发的嵌入式工程师与单片机学习者内容围绕USB虚拟串口、HID设备、USB键盘与鼠标四类典型应用覆盖从固件代码、报告描述符示例到STM32CubeMX/HAL库配置的完整过程可直接用于交互式设备与数据通信方案的参考设计。压缩包共576个文件以h/c源码文件为主辅以s汇编、o目标文件、hex烧录文件及txt说明文档等整体16.02MB目录结构清晰例程采用MDK工程结构便于按模块查找并直接编译验证。目前已有1624人学习浏览。通过研究其中MyUSB_HID_Test2、USB_VIRTUAL_COM等工程读者可以理解USB协议栈、中断传输与鼠标/键盘报告生成的具体实现并快速迁移到智能家居、工业控制或物联网节点等场景中。1. 从STM32C8T6的USB外设说起为什么一枚C8T6能同时当键盘、鼠标和串口STM32C8T6 的 USB 虚拟串口 HID 复合设备是在 64KB Flash、20KB RAM 的小芯片上把 USB 键盘、USB 鼠标和虚拟串口同时塞进一根 USB 线的做法。常见的误解是这三种设备分别对应三个 USB 控制器实际上 STM32F103C8T6 内部只有一个 USB 2.0 全速设备外设所谓“复合设备”是通过描述符和端点规划让主机看到一个多功能设备再把功能拆分给不同的驱动。这个方案适合做工具板、上位机调试口、3D 打印机控制板这类既需要命令行交互又要反馈按键状态的场景。从 CubeMX 生成的最小 CDC 工程出发把 HID 描述符和接口规划并进去就能在 Windows 下同时看到 COM 口、键盘和鼠标三个节点。2. USB设备栈选型与工程初始化HAL库CubeMX跑通最小枚举2.1 时钟树和USB供电48MHz是枚举的命门在深入改描述符之前要先把 USB 的时钟伺候好。STM32F103C8T6 的 USB 外设是 FSFull Speed模式工作频率必须精确锁在 48MHz。USB 预分频器把 PLL 主时钟做 1 分频或 1.5 分频通常系统时钟配置为 72MHzUSB 时钟 72 / 1.5 48MHz。CubeMX 的 Clock Configuration 里会自动锁住这个联动关系但如果你在裸寄存器工程里手动配 RCC忘记开 HSE 或 PLL 选择错误枚举会表现为“设备描述符请求失败”或完全没有插入反应。供电和不必要的上拉电阻也是常见的迷惑点。F103 的 USB 外设内置了 D 上拉电阻驱动枚举时启用所以 PA12USB_DP上不需要外接 1.5kΩ 上拉到 3.3V一些开发板为了兼容旧设计会外置上拉也无害只是注意不要由 GPIO 去控制上拉否则复位时序和库的上拉时序冲突时枚举会反复插拔。USB_DP 和 USB_DM 走线尽量短、等长线长控制在 20cm 以内对 12Mbps 全速信号基本不需要做阻抗配对。USB 的 3.3V 电源建议单独用 LDO 供给 VDDUSB 引脚附近放 1μF 和 100nF 电容避免电机驱动或继电器动作时造成 USB 掉线。这个在 CNC 和 3D 打印机主控上尤其关键因为步进驱动器启动瞬间的电流抽动很可能把共用的 3.3V 拉出毛刺。2.2 CubeMX生成最小CDC虚拟串口工程打开 STM32CubeMX选 STM32F103C8Tx走以下几步RCC → HSE → Crystal/Ceramic Resonator。时钟树里确认 HCLK 为 72MHzUSB Prescaler 自动选择 PLLCLK 除 1.5得到 48MHz。左侧 USB_DEVICE → Mode 选择 Device (FS)F103 没有 OTG FS只有这个外设。中间 USB_DEVICE 折叠菜单选 Communication Device Class (Virtual Port Com)这一步会生成 CDC 虚拟串口的例程骨架。Project Manager 里选工具链生成代码推荐用 STM32CubeIDE 或 MDK-ARM。生成后的核心文件有几个usbd_cdc_if.c 是用户接口USB 设备类注册在 usbd_conf.c 里配置描述符在 usbd_desc.c 和 usbd_cdc.c 里。先把工程编译下载把板子插到电脑设备管理器里出现一个“USB 串行设备COMx”说明 CDC 最小链路已经通了。如果这里连最小链路都通不过不要急着往下加 HID。先检查两点一是实际晶振是不是 8MHz有些最小系统板用 12MHz时钟树就全错了二是板子有没有把 D 或 D- 接错到 PA11/PA12C8T6 上这两个引脚是 USB_DM 和 USB_DP不可重映射。2.3 最小验证先让串口被识别再谈复合验证用一段很短的代码即可不需要写复杂的收发逻辑。在 while(1) 里周期调用 CDC_Transmit_FS向主机发一串字符uint8_t hello[] cv12\r\n; while (1) { USBD_CDC_HandleTypeDef *hcdc (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData; if (hcdc-TxState 0) { CDC_Transmit_FS(hello, sizeof(hello)-1); } HAL_Delay(1000); }说明TxState 是 CDC 内部的上一次发送完成标志等于 0 表示愿意接收新数据。CDC_Transmit_FS 内部实现会在上一次发送未完成时直接返回 USBD_BUSY 并丢弃数据所以先查 TxState 再调用是主循环发送的安全姿势。参数里的 sizeof(hello)-1 是去掉字符串结尾的 \0CDC 发送的是裸字节。打开串口助手连上这个 COM 口能看到每秒一条的 cv12 输出USB 虚拟串口这条路就活了。从这步开始再往工程里并入 HID 功能。3. 先把HID键盘和鼠标跑起来报表描述符是灵魂3.1 为什么键盘和鼠标要合并进一个HID接口HID 设备在 USB 看来是一组“报表”Report键盘和鼠标本质上是不同的报表格式。如果想让键盘和鼠标独立出现第一种做法是给它们各自分配一个 HID 接口但 STM32 的 USB Device Library 对 HID 类只维护了一个全局实例要为两个接口复制类驱动并不算难却会给后续升级带来很多不必要的麻烦。更省事、也够用的做法是把键盘和鼠标写进同一个 HID 接口的报表描述符靠 Report ID 区分两个顶层集合。主机端会把这个接口解析成两个独立的顶层集合Windows 的设备管理器里照样能看到一个“USB 输入设备”下挂出键盘和鼠标两个节点实际驱动是分开的。USB 键盘和 USB 鼠标的 HID 接口类代码都是 0x03区别主要在报表描述符内部的组织方式。键鼠合并接口时bInterfaceClass 0x03bInterfaceSubClass 0x01Boot InterfacebInterfaceProtocol 可以根据主偏好设成 1键盘或 2鼠标对复合报表来说设 0 更准确表示不固定给哪个协议。3.2 键盘鼠标的复合报表描述符报表描述符是单个字节流每一项都有含义需要逐段对齐。下面这份是带 Report ID 的键鼠合并报表键盘 ID1鼠标 ID2__ALIGN_BEGIN static uint8_t HID_ReportDesc[] __ALIGN_END { /* 键盘顶层集合 */ 0x05, 0x01, /* Usage Page (Generic Desktop) */ 0x09, 0x06, /* Usage (Keyboard) */ 0xA1, 0x01, /* Collection (Application) */ 0x85, 0x01, /* Report ID (1) */ 0x05, 0x07, /* Usage Page (Keyboard) */ 0x19, 0xE0, /* Usage Minimum (Left Control) */ 0x29, 0xE7, /* Usage Maximum (Right GUI) */ 0x15, 0x00, /* Logical Minimum (0) */ 0x25, 0x01, /* Logical Maximum (1) */ 0x75, 0x01, /* Report Size (1) */ 0x95, 0x08, /* Report Count (8) */ 0x81, 0x02, /* Input (Data, Variable, Absolute) */ 0x95, 0x01, /* Report Count (1) */ 0x75, 0x08, /* Report Size (8) */ 0x81, 0x01, /* Input (Constant) */ 0x95, 0x05, /* Report Count (5) */ 0x75, 0x01, /* Report Size (1) */ 0x05, 0x08, /* Usage Page (LEDs) */ 0x19, 0x01, /* Usage Minimum (Num Lock) */ 0x29, 0x05, /* Usage Maximum (Kana) */ 0x91, 0x02, /* Output (Data, Variable, Absolute) */ 0x95, 0x01, /* Report Count (1) */ 0x75, 0x03, /* Report Size (3) */ 0x91, 0x01, /* Output (Constant) */ 0x95, 0x06, /* Report Count (6) */ 0x75, 0x08, /* Report Size (8) */ 0x15, 0x00, /* Logical Minimum (0) */ 0x25, 0x65, /* Logical Maximum (101) */ 0x05, 0x07, /* Usage Page (Keyboard) */ 0x19, 0x00, /* Usage Minimum (0) */ 0x29, 0x65, /* Usage Maximum (101) */ 0x81, 0x00, /* Input (Data, Array) */ 0xC0, /* End Collection */ /* 鼠标顶层集合 */ 0x05, 0x01, /* Usage Page (Generic Desktop) */ 0x09, 0x02, /* Usage (Mouse) */ 0xA1, 0x01, /* Collection (Application) */ 0x85, 0x02, /* Report ID (2) */ 0x09, 0x01, /* Usage (Pointer) */ 0xA1, 0x00, /* Collection (Physical) */ 0x05, 0x09, /* Usage Page (Button) */ 0x19, 0x01, /* Usage Minimum (1) */ 0x29, 0x03, /* Usage Maximum (3) */ 0x15, 0x00, /* Logical Minimum (0) */ 0x25, 0x01, /* Logical Maximum (1) */ 0x95, 0x03, /* Report Count (3) */ 0x75, 0x01, /* Report Size (1) */ 0x81, 0x02, /* Input (Data, Variable, Absolute) */ 0x95, 0x01, /* Report Count (1) */ 0x75, 0x05, /* Report Size (5) */ 0x81, 0x01, /* Input (Constant) */ 0x05, 0x01, /* Usage Page (Generic Desktop) */ 0x09, 0x30, /* Usage (X) */ 0x09, 0x31, /* Usage (Y) */ 0x09, 0x38, /* Usage (Wheel) */ 0x15, 0x81, /* Logical Minimum (-127) */ 0x25, 0x7F, /* Logical Maximum (127) */ 0x75, 0x08, /* Report Size (8) */ 0x95, 0x03, /* Report Count (3) */ 0x81, 0x06, /* Input (Data, Variable, Relative) */ 0xC0, /* End Collection */ 0xC0 /* End Collection */ };逻辑说明报表描述符描述的是“字节布局”。键盘部分是经典的 Boot 键盘布局加一个 0x85 Report ID 前缀这样实际输入报表为 9 字节第 1 字节是 Report ID0x01后面 8 字节依次是修饰键、保留、6 个按键。鼠标部分是 5 字节Report ID0x02、3 个按钮位 5 个填充位、X 位移、Y 位移、滚轮位移。使用 Report ID 后每次发送的数据都要带上 ID主机才能区分是哪个集合的上报。几个参数值得注意键盘 Logical Maximum 设 101是因为 USB 键盘按键索引范围是 0~101超出后按 0 处理。鼠标的 X/Y 是相对位移所以 Input 项用 Relative0x06而不是 Absolute。Wheel 通常也是相对位移方向约定上滚为正值下滚为负值。3.3 上报节奏与LED状态回传HID 键盘和鼠标的中断端点查询间隔由端点描述符里的 bInterval 决定。键盘放 10ms即 100Hz 轮询足够人眼和操作系统都感知不到延迟鼠标如果想要接近物理鼠标的手感可以把 bInterval 设成 2ms但在 USB 总线上会多占用带宽对复合设备里的 CDC 串口有一定影响。折中方案是都放 10ms串口吞吐优先。上报时机不能写错上电后键盘至少要主动发一次空报告9 字节全 0否则主机认为 LEFT CTRL 等修饰键处于未知状态Caps Lock 的 LED 也会失灵。每次发送前还是查一下 HID 类的状态或者直接依赖 USBD_HID_SendReport 的返回值它返回 USBD_BUSY 时说明上一次中断上报还没有完成直接丢帧比排队堆积更合理。USBD_HID_SendReport(hUsbDeviceFS, (uint8_t*)key_report, 9) 的第一个参数是全局 USB 设备句柄在 CubeMX 生成的 main.c 里已经定义第二个参数是带 Report ID 的报表缓冲地址第三个是报表长度必须和端点描述符支持的最大包长相符。键盘和鼠标共用一个端点缓冲区大小按 9 字节算。4. 虚拟串口与HID合成一个复合设备IAD描述符与端点规划4.1 IAD把两个CDC接口捆成一个功能CDC 虚拟串口在 USB 描述符里不是单接口而是两个接口一个管理接口Interface 0用于请求行编码和通知和一个数据接口Interface 1用于收发串口数据。Windows 如果没有额外提示会把这两个接口当成两个独立功能导致虚拟串口不出现或出现两个半残设备。IADInterface Association Descriptor就是解决这个问题的它把一段连续接口声明为一个功能告诉 USB 系统“接口 0 和接口 1 是一个整体”。所以复合设备最终是三个接口接口 0 和接口 1 组成 CDC接口 2 是 HID 键鼠。设备描述符的 bDeviceClass 也要配合改通常设为 0xEF、bDeviceSubClass 为 0x02、bDeviceProtocol 为 0x01语义是“符合 USB 混合设备规范接口描述符里带 IAD”。CubeMX 不会自动生成这种结构需要在生成的 usbd_desc.c 里手工改设备描述符再在 usbd_cdc.c 的全局配置描述符数组里重排接口。4.2 三个接口五个端点的配置描述符配置描述符是所有功能能否被识别的最关键一段数据结构。下面这份 100 字节的配置描述符数组是经过裁剪但可以直接用的版本端点分配为EP1 IN 给 HIDEP3 IN 和 EP4 OUT 给 CDC 数据EP5 IN 给 CDC 通知__ALIGN_BEGIN uint8_t USBD_FS_CfgDesc[100] __ALIGN_END { /* 配置描述符 */ 0x09, 0x02, 0x64, 0x00, 0x03, 0x01, 0x00, 0x80, 0x32, /* IAD接口0-1组合为CDC */ 0x08, 0x0B, 0x00, 0x02, 0x02, 0x02, 0x01, 0x00, /* 接口0CDC ACM管理接口 */ 0x09, 0x04, 0x00, 0x00, 0x01, 0x02, 0x02, 0x01, 0x00, 0x05, 0x24, 0x00, 0x10, 0x01, /* Header */ 0x05, 0x24, 0x01, 0x00, 0x00, /* Call Mgmt */ 0x04, 0x24, 0x02, 0x02, /* ACM */ 0x05, 0x24, 0x06, 0x00, 0x01, /* Union */ 0x07, 0x05, 0x85, 0x03, 0x08, 0x0A, 0x00, /* EP5 IN */ /* 接口1CDC数据接口 */ 0x09, 0x04, 0x01, 0x00, 0x02, 0x0A, 0x00, 0x00, 0x00, 0x07, 0x05, 0x83, 0x02, 0x40, 0x00, 0x00, /* EP3 IN */ 0x07, 0x05, 0x04, 0x02, 0x40, 0x00, 0x00, /* EP4 OUT */ /* 接口2HID键鼠 */ 0x09, 0x04, 0x02, 0x00, 0x01, 0x03, 0x01, 0x00, 0x00, 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x77, 0x00, 0x07, 0x05, 0x81, 0x03, 0x10, 0x0A, 0x00, /* EP1 IN */ };参数说明第一行的 0x64, 0x00 是 wTotalLength 的小端表示表示整个配置描述符的总长度为 100必须与数组实际长度一致否则主机枚举时会报“配置描述符无效”。接口 0 的 bNumEndpoints1 是通知端点接口 1 的 bNumEndpoints2 是收发各一个接口 2 的 bInterfaceClass0x03 是 HID最后那个 0x77, 0x00 是报表描述符长度 119 字节的小端表示必须和 3.2 的 HID_ReportDesc 实际字节数严格一致。CDC 功能描述符里的四个子描述符顺序不能乱Header 指定 CDC 版本 1.10Call Management 声明支持呼叫管理ACM 声明支持串行线状态、Line Coding 等命令Union 把管理接口0和数据接口1关联起来。IAD 的 bFirstInterface0、bInterfaceCount2 与 Union 描述符互相验证主机靠这两处才能识别出虚拟串口。4.3 512字节USB SRAM的分配与双缓冲取舍STM32F103C8T6 的 USB 外设带 512 字节专用数据内存USB SRAM所有端点的收发缓冲区都从这里划分。C8T6 本身 20KB 通用 RAM 不参与 USB 传输缓冲区这一点和很多 ARM 芯片不一样设计时要把 512 字节当作一个独立预算。按上文配置缓冲区占用分布如下表端点用途方向缓冲区大小EP0 IN/OUT控制传输双向64 64EP1 INHID 键鼠输入16EP3 INCDC 数据输入64EP4 OUTCDC 数据输出64EP5 INCDC 通知输入8合计约 280 字节在 512 字节预算内剩余空间足够再开一个 64 字节的调试端点。如果项目还要加 USB 大容量存储或自定义类就要收紧 CDC 数据端点到 32 字节把省出来的 64 字节给其它端点。F103 的 USB 库默认不开双缓冲只靠单缓冲加软件应答吞吐率对 12Mbps 全速设备来说仍然够用。双缓冲模式能提升连续传输吞吐但对键鼠这种小报文中断传输没有收益反而让端点缓冲区占用翻倍不建议在 C8T6 上为 HID 开双缓冲。虚拟串口如果要在 DMA 场景下持续灌数据可以在 CDC 数据端点上考虑双缓冲前提是你对 512 字节预算有精确计算。5. 把USART1嵌进虚拟串口双向回环与阻塞问题5.1 环形缓冲区接收UART数据USB 虚拟串口的典型落地方式是和 STM32 的物理 UART 做桥接USB 收到的字节发到 UART 外设UART 收到的字节发回 USB。这样 PC 端的串口助手就能隔着一根 USB 线操作板子上的实际串口设备比如 C8T6 配合 DM542 步进驱动器的 3D 打印主控板上位机通过虚拟串口下 G 代码中间不用再插一根 TTL 线。UART 接收侧最忌讳逐字节调用 CDC_Transmit_FS因为 USB 中断上报是毫秒级粒度每字节一次回调会把总线打成碎片。做法是开一个环形缓冲区UART 中断只负责往里写主循环攒一批再发。受 CDC 数据端点 64 字节限制每次攒满 64 字节或遇到空闲窗口就发送。#define RBUF_SIZE 256 static volatile uint8_t rbuf[RBUF_SIZE]; static volatile uint16_t rhead 0, rtail 0; void USART1_IRQHandler(void) { if (USART1-SR USART_SR_RXNE) { uint8_t ch (uint8_t)(USART1-DR 0xFF); uint16_t next (rhead 1) % RBUF_SIZE; if (next ! rtail) { rbuf[rhead] ch; rhead next; } } }说明中断里用 SR 寄存器的 RXNE 位判断接收寄存器非空读 DR 时硬件自动清零。环形缓冲区满时直接丢弃新字节不阻塞中断这是串口桥接里最常见的策略丢数据好过死锁。主循环侧每次从 rtail 取出数据一次性交给 CDC。while (1) { if (rhead ! rtail) { uint8_t chunk[64]; uint16_t filled 0; while (rhead ! rtail filled 64) { chunk[filled] rbuf[rtail]; rtail (rtail 1) % RBUF_SIZE; } CDC_Transmit_FS(chunk, filled); } }CDC_Transmit_FS 如果返回 USBD_BUSY这一批就会丢掉。为了减少丢包把判定条件从“有数据就发”改成“有数据并且上次发送完成”USBD_CDC_HandleTypeDef *hcdc (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData; if (rhead ! rtail hcdc-TxState 0) { ... }5.2 CDC下发到UART回调里的单字节陷阱从 PC 往板子发数据底层回调是 CDC_Receive_FS。CubeMX 生成的模板里它已经给了你 Buf 和 Len很多初学者在回调里直接 HAL_UART_Transmit 就完事但回调运行在 USB 中断上下文HAL_UART_Transmit 的阻塞等待会拖住整个 USB 协议栈。稳妥的做法是先用一个全局标志置位在主循环里做真实发送static uint8_t rx_buf[64]; static uint32_t rx_len 0; static volatile uint8_t rx_pending 0; int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len) { rx_len *Len; memcpy(rx_buf, Buf, rx_len); rx_pending 1; USBD_CDC_SetRxBuffer(hUsbDeviceFS, Buf); USBD_CDC_ReceivePacket(hUsbDeviceFS); return USBD_OK; }说明rx_buf 是静态缓冲主循环消费完后才允许下一次接收覆盖所以回调里同时重新武装了接收缓冲。主循环检测到 rx_pending 后调用 HAL_UART_Transmit(huart1, rx_buf, rx_len, 100)发送完成后再清标志。注意 Buf 是 USB 库的接收缓冲长度固定拷贝出来再交给 UART是因为下一次 USBD_CDC_ReceivePacket 会复用同一块内存。5.3 常见场景C8T6 DM542 的G代码透传一个实际组合C8T6 作为 3D 打印机或小型雕刻机主控DM542 步进驱动器接在 TIM2/PA0-1、PA8-11 这类引脚上USB 虚拟串口作为唯一的控制口。上位机下发的文本行是 G 代码特征是“行不完整但有持续字节流”。这时候环形缓冲区的 256 字节可能不够一行动辄上百字节可以把 RBUF_SIZE 提到 1024C8T6 的 20KB RAM 支撑得住。这类应用的另一个体会是虚拟串口下发到 UART 时不需要逐字节透传最好按行转发。在 CDC_Receive_FS 里做行检测遇到 \n 就设一个“有完整一行”的标志主循环读到标志后把整行交给 UART 的 DMA 发送能明显降低步进驱动的抖动因为 USB 微帧的抖动会被整行缓冲抹平。6. 调试复合设备的一线工具与三板斧6.1 设备管理器里的三类节点插上复合设备后设备管理器应该能看到端口COM 和 LPT下的 USB 串行设备COMx人体学输入设备下的 USB 输入设备展开子节点会有符合 HID 标准的键盘和鼠标。如果只看到 COM 口而键盘鼠标不出来先怀疑配置描述符的 bNumInterfaces 写错或者是 HID 报表描述符被读成了一个空集合。如果只看到 HID 而没有 COM多半是 IAD 的字节顺序或 Union 描述符写错Windows 拒绝把接口 0/1 组合成 CDC。6.2 USBlyzer 看枚举时序的三处重点USB 协议分析软件PC 端抓包工具常见的有 USBlyzer 和 Bus Hound能把枚举时的每个请求都摊开。第一次跑复合设备重点看三处Get Descriptor(Device) 返回的设备描述符里 bDeviceClass 是否为 0xEFGet Descriptor(Configuration) 的 wTotalLength 是否和数组长度一致Set Configuration 之后主机有没有发出针对 HID 键盘、鼠标的 GET_REPORT 请求。没有 GET_REPORT 不一定错但如果有说明主机已经接受了你的 HID 集合。Windows 对配置描述符的容错很低一个字节的长度错误都会直接让枚举失败没有抓包工具时可以用一个最土的办法把配置描述符数组长度写小一点如果设备从完全无法识别变成“无法识别的 USB 设备”至少说明主机读到了部分配置再逐步加回来比较。6.3 一个绕不开的发送完成检查复合设备下最容易忽略的坑是发送完成检查。HID 上报和 CDC 上报共享 USB 外设如果主循环里死循环调用 USBD_HID_SendReport 而不管返回值USB 库的端点状态会被你自己的下一次调用覆盖主机端表现为键盘和鼠标间歇性失灵。发送端口的统一写法是先检查对应类的 TxStateHID 没有暴露 TxState就检查 USBD_HID_SendReport 的返回值等于 USBD_OK 才继续发起下一次上报否则跳过这一帧。键盘这种事件型设备丢一帧没感知鼠标连续移动丢一帧就是肉眼可见的卡顿。最后的技巧给复合设备加一个运行状态灯把 USB 枚举成功和 CDC 收发活动都用 GPIO 点亮比任何日志都直观。调试时把 HID 键盘空报告改成每 100ms 发一次能在 USBlyzer 里看到持续的中断 IN 包证明 HID 链路还活着。本文还有配套的精品资源点击获取
返回列表