
简介STM32 RBT6 USB复合设备工程基于STM32F103RBT6实现HID鼠标与HID键盘两个接口的复合设备适合初学USB协议或需要快速实现自定义HID设备的嵌入式开发者。工程提供一套可直接编译运行的Keil项目共48个文件主体为23个.h头文件与15个.c源文件其中USB库配置、设备描述符、HID报告描述符、端点中断处理及系统延时/串口等模块均有清晰拆分库文件与用户代码分离便于按模块阅读另配套启动文件、Keil工程文件、hex烧录固件及工程清理脚本整体仅110KB内容紧凑。通过研究源码可理解复合设备中多个接口描述符的编排方法、多端点数据交互机制以及鼠标键盘数据并发上报的处理逻辑固件可直接烧录到RBT6开发板连接电脑后即可同时识别为鼠标与键盘。该资源已有866人学习适合想深入USB底层机制并动手实践的开发者作为参考模板。 我是一个喜欢把键盘鼠标都塞进一个小板子里的嵌入式爱好者。最近在 STM32F103RBT6 上折腾了一个 USB 复合设备把 HID 键盘和 HID 鼠标塞进了同一个设备描述符里共用一根 USB 线插上电脑直接识别成“键盘鼠标”两个设备。这个方案在做游戏外设、快捷操作面板、演示翻页器这类产品时非常实用而且 RBT6 这颗芯片资源刚好够用适合做入门到进阶的 USB 开发实践。如果你也卡在“不知道怎么在一块芯片上同时枚举出两个 HID 设备”或者是第一次接触 USB 复合设备配置这篇内容应该能帮你把整个流程捋顺。我会把描述符配置、报告发送、端点分配这些核心部分讲透再附上我在实际调试中遇到的坑和解决办法。1. 为什么要在 RBT6 上做 USB 复合设备1.1 复合设备的应用场景与价值先说场景。普通的 USB 键盘或者鼠标设备描述符里只有一个接口Interface操作系统加载对应的 HID 驱动后这个设备就只是键盘或者只是鼠标。但很多项目需要同时具备两种输入能力比如一个自定义小键盘上带一个摇杆或滚轮或者一个体感翻页器既要模拟键盘按键又要模拟鼠标移动。如果做成两个独立的 USB 设备就得占用两个 USB 口成本高、用户体验也差。复合设备Composite Device解决的就是这个问题在一个物理设备里把多个接口组合在一起每个接口可以是一个独立的 HID 功能模块。RBT6 的 USB 外设是全速 12Mbps支持 8 个端点理论上可以同时承载多个 HID 接口做两个 HID 功能绰绰有余。1.2 USB HID 协议基础回顾HIDHuman Interface Device是 USB 协议里专门为人机交互设备定义的一类设备。它的特点是使用中断传输Interrupt Transfer来周期性地上报数据延迟低、带宽占用小。在 HID 规范里设备通过 HID 报告描述符Report Descriptor告诉主机“我有哪些输入/输出项、每个项有多长、数据怎么解析”。键盘和鼠标的报告描述符结构完全不同这也是复合设备里最需要仔细配置的部分。具体到 STM32 的实现我们可以利用 ST 官方标准外设库SPL里的 USB 设备库或者 HAL 库里的 USB Device 中间件。两种方式我都试过SPL 的代码更直观适合理解 USB 枚举和数据传输的底层流程HAL 中间件封装得更好但自定义描述符时反而多了一层抽象。这里我以 ST 标准外设库为例讲解核心原理你在移植到 HAL 时思路是一样的。2. 硬件选型与开发环境准备2.1 STM32F103RBT6 资源盘点RBT6 属于 STM32F103 中容量系列Cortex-M3 内核主频最高 72MHzFlash 128KBSRAM 20KB。对于 USB 设备开发来说最关键的是它内置了 USB 全速设备控制器需要外接一个 1.5kΩ 上拉电阻到 D 线部分最小系统板已经集成。注意 RBT6 没有 USB OTG只有 USB Device 模式不过我们做 HID 复合设备Device 模式完全够用。引脚方面USB_DMPA11和 USB_DPPA12是固定的不能随意映射。另外要注意 RBT6 的 USB 控制器需要使用 48MHz 时钟一般通过 PLL 从 8MHz 外部晶振倍频得到或者使用内部 HSI 也可以但 USB 对时钟精度要求较高建议务必使用外部晶振。2.2 开发工具与调试验证工具准备代码编写我用的是 Keil MDK5配合 ST-Link V2 下载调试。这里有一个老生常谈但容易忽略的点如果你要用 ST-Link 在线调试而板子上同时又用了 PA11/PA12 做 USB两者一般不冲突但要注意有些 ST-Link 的 SWD 接口和 USB 座在供电上会互相干扰建议用隔离的 USB 线供电。USB 抓包工具是必备的。我用的是 Wireshark USBPcap也可以直接用 Bus Hound。枚举失败、描述符长度出错、报告格式不对这类问题用抓包工具看一眼就能定位是哪个环节的问题比盲猜快得多。另外Windows 的“设备管理器”也要学会看如果枚举正常但驱动报错设备状态里会给出具体错误码。3. 描述符配置复合设备的灵魂3.1 设备描述符与配置描述符的结构设计USB 设备枚举过程中主机首先读取设备描述符Device Descriptor然后读取配置描述符Configuration Descriptor。复合设备的“复合”体现在配置描述符里包含多个接口描述符而且每个接口描述符后面跟着自己的 HID 描述符和端点描述符。以 ST 标准库的 usb_desc.c 为例关键常量如下#define USB_DEVICE_DESCRIPTOR_TYPE 0x01 #define USB_CONFIGURATION_DESCRIPTOR_TYPE 0x02 #define USB_STRING_DESCRIPTOR_TYPE 0x03 #define USB_INTERFACE_DESCRIPTOR_TYPE 0x04 #define USB_HID_DESCRIPTOR_TYPE 0x21 #define USB_ENDPOINT_DESCRIPTOR_TYPE 0x05设备描述符里 bDeviceClass 必须设为 0x00表示设备类在接口描述符中定义。这一点很重要如果这里设成 0x03HID 类主机就会认为整个设备是一个 HID 设备而不是复合设备后续的多个接口就没法正确识别了。const uint8_t DeviceDescriptor[] { 0x12, // bLength USB_DEVICE_DESCRIPTOR_TYPE, // bDescriptorType 0x00, 0x02, // bcdUSB 2.00 0x00, // bDeviceClass: 0x00 表示由接口描述符定义 0x00, // bDeviceSubClass 0x00, // bDeviceProtocol 0x40, // bMaxPacketSize0 64 0x83, 0x04, // idVendor (自定义) 0x10, 0x10, // idProduct 0x00, 0x01, // bcdDevice 1, // iManufacturer 2, // iProduct 3, // iSerialNumber 0x01 // bNumConfigurations };3.2 配置描述符的字节排列一个都不能错配置描述符是整个复合设备里最容易出错的地方因为它是一长串连续排列的结构体包含配置描述符本身、每一个接口描述符、HID 描述符和对应的端点描述符。主机靠 bLength 和 bDescriptorType 逐个解析只要有一个字节的偏移不对后面的描述符全部错位。我的排列是配置描述符 接口0描述符 HID描述符 端点1描述符 接口1描述符 HID描述符 端点2描述符。具体数据const uint8_t ConfigDescriptor[] { // 配置描述符 0x09, // bLength USB_CONFIGURATION_DESCRIPTOR_TYPE, 0x29, 0x00, // wTotalLength: 41 字节 0x02, // bNumInterfaces 2 0x01, // bConfigurationValue 0x00, // iConfiguration 0x80, // bmAttributes: 总线供电 0x32, // bMaxPower 100mA // 接口0键盘 0x09, // bLength USB_INTERFACE_DESCRIPTOR_TYPE, 0x00, // bInterfaceNumber 0 0x00, // bAlternateSetting 0x01, // bNumEndpoints 1 0x03, // bInterfaceClass HID 0x01, // bInterfaceSubClass Boot 0x01, // bInterfaceProtocol Keyboard 0x00, // iInterface // HID 描述符 0x09, // bLength USB_HID_DESCRIPTOR_TYPE, 0x11, 0x01, // bcdHID 1.11 0x00, // bCountryCode 0x01, // bNumDescriptors 1 0x22, // bDescriptorType Report 0x3E, 0x00, // wDescriptorLength 62 字节 // 端点描述符 0x07, // bLength USB_ENDPOINT_DESCRIPTOR_TYPE, 0x81, // bEndpointAddress: 端点1 IN 0x03, // bmAttributes: Interrupt 0x08, 0x00, // wMaxPacketSize 8 0x0A, // bInterval 10ms // 接口1鼠标 0x09, USB_INTERFACE_DESCRIPTOR_TYPE, 0x01, // bInterfaceNumber 1 0x00, 0x01, // bNumEndpoints 1 0x03, // bInterfaceClass HID 0x01, // bInterfaceSubClass Boot 0x02, // bInterfaceProtocol Mouse 0x00, // HID 描述符 0x09, USB_HID_DESCRIPTOR_TYPE, 0x11, 0x01, 0x00, 0x01, 0x22, 0x34, 0x00, // wDescriptorLength 52 字节 // 端点描述符 0x07, USB_ENDPOINT_DESCRIPTOR_TYPE, 0x82, // bEndpointAddress: 端点2 IN 0x03, // Interrupt 0x08, 0x00, // wMaxPacketSize 8 0x0A // bInterval 10ms };注意 wTotalLength 是 41包含了所有内容这个值写错会导致主机只解析一部分描述符设备枚举到一半卡住。另外每个接口的 HID 描述符里 wDescriptorLength 必须和实际的报告描述符长度完全一致主机会按这个长度去读取报告描述符。3.3 HID 报告描述符键盘和鼠标各写各的报告描述符定义了设备上报的数据格式。键盘和鼠标的格式差异很大但有一点相同都通过 Report ID 来区分不同的报告类型。在复合设备里由于每个接口是独立的可以不使用 Report ID但我个人建议加上方便以后扩展比如在鼠标接口里再增加一个多媒体键报告。键盘的报告描述符标准 8 字节格式const uint8_t HID_ReportDesc_Keyboard[] { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 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) - LED指示灯 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 };这一个描述符有 62 字节对应接口0 HID 描述符里的 wDescriptorLength 0x003E。我踩过一个坑报告描述符里 Usage Maximum 写成了 0x64十进制100导致第 101 个键通常是右 Alt 旁边的按键上报无效后来改成 0x65 才正常。这类小错误只能靠逐个按键测试才能发现。鼠标的报告描述符标准 4 字节格式const uint8_t HID_ReportDesc_Mouse[] { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x02, // Usage (Mouse) 0xA1, 0x01, // Collection (Application) 0x09, 0x01, // Usage (Pointer) 0xA1, 0x00, // Collection (Physical) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (Button 1) 0x29, 0x03, // Usage Maximum (Button 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) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) 0x75, 0x08, // Report Size (8) 0x95, 0x02, // Report Count (2) 0x81, 0x06, // Input (Data, Variable, Relative) 0x09, 0x38, // Usage (Wheel) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) 0x75, 0x08, // Report Size (8) 0x95, 0x01, // Report Count (1) 0x81, 0x06, // Input (Data, Variable, Relative) 0xC0, // End Collection 0xC0 // End Collection };4. 软件实现数据上报与端点收发逻辑4.1 USB 中断与枚举回调处理在 ST 标准外设库中USB 的事件处理主要在 usb_istr.c 的中断服务函数里调度。你需要关注 USB_Reset、USB_Ep0_SETUP、USB_Suspend 这几个事件。设备复位时主机下发 Set Address 和 Set Configuration成功之后才开始正常的数据传输。关键是要在pProperty-Reset()里重新初始化端点状态。我习惯在 reset 回调里调用SetBTABLE()和几个SetEPType函数确保设备在热插拔时状态是干净的。如果你发现设备第一次插入能识别拔了再插就识别不了十有八九是 reset 回调里没把端点状态恢复好。void USB_Reset(void) { pInformation-Current_Configuration 0; pInformation-Current_Feature 0; SetBTABLE(BTABLE_ADDRESS); SetEPType(ENDP0, EP_CONTROL); SetEPTxStatus(ENDP0, EP_TX_NAK); SetEPRxStatus(ENDP0, EP_RX_VALID); SetEPType(ENDP1, EP_INTERRUPT); SetEPTxStatus(ENDP1, EP_TX_NAK); SetEPType(ENDP2, EP_INTERRUPT); SetEPTxStatus(ENDP2, EP_TX_NAK); SetEPTxCount(ENDP1, 8); SetEPTxCount(ENDP2, 8); SetCNTR(CNTR_RESETM | CNTR_CTRM | CNTR_SOFM | CNTR_SUSPM); }4.2 键盘和鼠标数据的发送实现键盘数据的发送核心是往端点 1 的发送缓冲区写入 8 字节报告然后调用SetEPTxValid(ENDP1)让 USB 外设在下一个 IN 令牌时把数据发出去。注意发送之前要检查端点是否处于空闲状态GetEPTxStatus(ENDP1) EP_TX_NAK否则会覆盖还没发完的数据导致丢键。uint8_t Keyboard_Buffer[8] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; void Keyboard_SendReport(void) { if (GetEPTxStatus(ENDP1) EP_TX_NAK) { UserToPMABufferCopy(Keyboard_Buffer, GetEPTxAddr(ENDP1), 8); SetEPTxCount(ENDP1, 8); SetEPTxValid(ENDP1); } }鼠标数据同理通过端点 2 发送缓冲区只有 4 字节uint8_t Mouse_Buffer[4] {0x00, 0x00, 0x00, 0x00}; void Mouse_SendReport(void) { if (GetEPTxStatus(ENDP2) EP_TX_NAK) { UserToPMABufferCopy(Mouse_Buffer, GetEPTxAddr(ENDP2), 4); SetEPTxCount(ENDP2, 4); SetEPTxValid(ENDP2); } }4.3 端点分配策略为什么键盘用端点 1鼠标用端点 2RBT6 的 USB 控制器有 8 个端点EP0-EP7EP0 固定用于控制传输剩下的可以自由分配。HID 设备的数据都是中断传输一个接口至少要一个 IN 端点。有人可能会问两个接口能不能共用一个端点从协议层面说接口和端点是一对多的关系但一个端点只能属于一个接口。所以键盘和鼠标必须各用一个端点这里我选了 EP1 和 EP2。端点地址的 bit7 是方向位1 表示 IN设备到主机所以 0x81 和 0x82 分别是端点1 IN、端点2 IN。最大包长我统一设为 8 字节对应键盘和鼠标报告的长度这样不会浪费带宽。bInterval 设为 10ms对应 100Hz 的轮询率对于键盘鼠标这种输入设备100Hz 完全够用而且对 USB 总线的占用比较小。如果你追求更低的延迟可以把 bInterval 改成 1ms也就是 1000Hz 轮询率。但要注意两个接口都改成 1ms 的话总线上中断传输占用的带宽会明显增加如果同时还有 U 盘这类设备在跑可能会有带宽不足的风险。我实际测试下来100Hz 下键盘敲击没有可感知的延迟1000Hz 更多是心理作用。5. 调试实录常见问题与排查技巧5.1 设备枚举失败0x80000040 错误怎么办我最开始调试时插上 USB 后电脑完全没反应设备管理器里出现一个未知设备属性里显示“该设备无法启动 (代码 10)”。用 Bus Hound 抓包发现设备对 Set Address 请求没有正确响应。最后定位到原因是 48MHz 时钟没有配好USB 外设的时钟不对导致枚举握手失败。排查时钟问题的思路是检查 RCC 配置是否正确进入了 PLL 模式然后确认 USB 时钟源是否来自 PLLCLK 经过二分频得到的 48MHz。用库函数写就是RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5); // 72MHz / 1.5 48MHz RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB, ENABLE);注意这里不是除以 1 或除以 2而是 1Div5。很多人栽在这里以为 PLLCLK 是 72MHz直接二分频得 36MHz就完全错了。USB 外设的时钟必须是 48MHz, 这是硬性要求。另一个常见问题是配置描述符里 wTotalLength 写错。主机读取配置描述符时前 9 字节里有 wTotalLength主机按这个值决定接下来读多少字节。如果你写小了后面的接口描述符主机根本没读到自然只识别出一个设备。我调试时用了一个笨办法把配置描述符数组打印出来手动按 bLength 字段一个个划分结构体检查每个结构体的边界是否对齐。推荐你也养成这个习惯描述符这东西肉眼检查远比靠工具猜来得快。5.2 键盘和鼠标只能识别出一个如果你描述符里两个接口都写了但电脑只识别出其中一个排查顺序一般是先确认 bNumInterfaces 是不是 2。如果是 1主机只会解析第一个接口后面全被忽略。确认接口描述符的 bInterfaceNumber 是不是从 0 开始连续编号0, 1, 2...如果有跳号主机的 PnP 管理器会认为接口不连续部分接口可能被丢弃。查每个接口的端点描述符地址是否冲突。我在一次实验中把两个接口的端点地址都写成了 0x81结果鼠标数据往端点 1 发被主机当成键盘报告导致鼠标乱动、键盘丢键。后来把鼠标的端点改成 0x82 就好了。第 3 点是个隐蔽的坑因为 USB 协议并没有强制要求端点地址必须唯一所以编译期不会报错只有运行时行为异常。建议你在设计初期就给每个接口分配独立的端点地址并且写进设计文档里避免后面改代码时混乱。5.3 键盘按键偶尔重复或丢失这个问题在我用实验板测试时出现过。键盘按键按下时发送按键码松开时发送空报告全 0这是标准的两个状态。如果因为总线繁忙按键按下和松开的报告在短时间内连续发送主机可能只处理了其中一个表现为按键重复或丢失。解决方法是在发送之前检查上一个报告是否已经成功发送。如果发送缓冲还忙就把当前报告缓存在一个全局变量里等上一个发送完成再发。我在代码里实现了一个简单的单缓冲队列volatile uint8_t Keyboard_Buffer[8]; volatile uint8_t Keyboard_Buffer_Pending 0; void Keyboard_SendReport(void) { if (GetEPTxStatus(ENDP1) EP_TX_NAK) { UserToPMABufferCopy((uint8_t*)Keyboard_Buffer, GetEPTxAddr(ENDP1), 8); SetEPTxCount(ENDP1, 8); SetEPTxValid(ENDP1); Keyboard_Buffer_Pending 0; } else { Keyboard_Buffer_Pending 1; } }在 USB 端点发送完成中断里如果Keyboard_Buffer_Pending为 1就立即再发一次。这样能保证每个报告都被发送不会丢。5.4 鼠标移动异常上下乱跳或加速度异常鼠标报告的 X 和 Y 是相对位移值类型是带符号的 int8_tLogical Minimum -127Logical Maximum 127。如果你在代码里把 X 值定义成了 uint8_t而实际值超过 127数据会被解释成很大的负数鼠标就会往反方向跑。我曾在一次测试中把鼠标移动量设成 200结果鼠标直接往右下角飞。后来把类型改成 int8_t超过 127 的位移做了饱和处理问题解决。另外 Windows 的“提高指针精确度”这个功能会把鼠标加速算法作用在你的 HID 输入上如果你测试时发现鼠标移动速度不一致可以去控制面板把这项关掉再测这样能更真实地反映你发送的数据。5.5 两个接口同时传输数据的带宽问题全速 USB 的中断传输带宽是有限的每个 1ms 帧里所有中断传输的总数据量不能超过 90% 的帧带宽。两个接口各占一个端点每个端点 8 字节每 10ms 各发一次总带宽占用是 2 * 8 / 10ms 1.6KB/s离上限还远得很所以正常情况下完全不用担心。但如果你的应用里还要同时跑一个虚拟串口CDC或者其他大流量传输就需要额外关注是否在一个帧里发生了过多传输。我的经验是HID 的中断间隔尽量保持在 4ms 以上bInterval4这对大多数交互设备足够又不会给总线造成压力。6. 经验总结与进阶扩展方向6.1 从 SPL 到 HAL 的移植要点如果你用的是 STM32CubeMX 加 HAL 库实现复合设备的思路是一样的但描述符的定义方式不同。HAL 中间件里你需要在 usbd_hid.c 里修改 HID 报告描述符和配置描述符。注意 HAL 的中间件默认只有一个接口要在 usbd_conf.c 里增加端点分配并且在 usbd_hid.c 里把USBD_HID_ConfigDesc扩展成包含两个接口的配置描述符结构。一个容易踩的坑是HAL 中间件里的HID_ReportDesc数组默认长度是 4 字节仅一个 LED 报告你把键盘报告描述符复制进去后必须同步修改HID_REPORT_DESC_SIZE这个宏的定义。这个宏在头文件里定义如果漏改主机读到的报告描述符会被截断导致键盘完全无法使用。6.2 设备状态的提示灯设计最后分享一个提升体验的小细节。我在项目里加了一个 LED 做连接状态提示USB 初始化完成且 Set Configuration 成功后点亮总线挂起时熄灭。实现方式是在USB_Configured_Callback()里置位一个标志主循环检测到标志后翻转 LED。这样调试的时候不用一直盯软件看灯就知道设备是否正常枚举。代码很简单void USB_Configured_Callback(void) { USB_Configured 1; } // 主循环中 if (USB_Configured) { GPIO_SetBits(GPIOB, GPIO_Pin_0); // 点亮 LED }6.3 个人实践后的整体感受STM32F103RBT6 做 HID 复合设备可以说是一个性价比极高的入门到进阶项目。它需要你理解 USB 描述符的层级结构和 HID 报告格式几乎是 USB 开发里最核心的知识点同时它的实现难度又远低于做一个高速 CDC 设备非常适合用来打通我对 USB 协议的整体认知。踩过了描述符错位、端点争用、时钟配置这些坑之后再看别的 USB 设备开发基本都能举一反三。如果你做完了这个键盘加鼠标的复合设备下一步可以尝试在同一个接口里通过 Report ID 扩展多媒体控制键或者把设备改成 USB HID 游戏手柄再或者在上位机通过 HID API 和设备做双向通信。这些方向都是在这次复合设备描述符的基础上一层层加东西所有底层逻辑你已经具备了。希望这篇分享能帮你少走一些弯路。本文还有配套的精品资源点击获取