简介:MCP251XFD是一份用C语言编写的硬件无关CAN控制器驱动资源,面向嵌入式开发者,用于配置和检查MCP2517FD/MCP2518FD内部寄存器并与之通信。驱动具备设备自动识别能力,可通过I2C转SPI转换器控制芯片,允许不同设备独立配置,方便移植到各类MCU平台。包内共259个文件,以204个h头文件和32个c源码文件为核心,另有cpp示例、配置模板、说明文档及工程配置文件,整体约1.06MB,结构清晰,便于查阅和二次开发。资源还包含基于SAM V71的演示工程片段,涉及USART、CAN扩展功能、PMC、PIO等模块,读者可对照示例理解驱动调用与底层移植方法。目前已有5412人学习/下载,适合需要启用MCP2517FD/2518FD的嵌入式开发者参考。
1. MCP251XFD 驱动:一颗外挂 CAN FD 控制器怎么让你的主控跑满 5Mbps
做嵌入式通信方案时,如果 MCU 内部带的是传统 CAN 控制器,而你又必须把链路升级成 CAN FD,MCP2517FD 和 MCP2518FD 外挂控制器的驱动就绕不过去。这套 MCP251XFD 驱动程序要解决的问题很具体:通过 SPI 让一颗普通 MCU 获得完整的 CAN FD 收发能力,而不是替换掉整块主控。适合谁看?正在做车载诊断、工业设备互联或电池管理通信的工程师,手头 MCU 不带 CAN FD 外设,又不想因为一个通信口就换主控平台。这类项目的坑不在协议本身,而在驱动与硬件时序的配合,寄存器配置差一位,整条总线就是静默的。下面按选型、驱动实现、调试避坑、验证技巧这条线往下讲。
2. MCP2517FD 与 MCP2518FD 的选型决策:引脚、RAM 与驱动可移植性的取舍
2.1 两款芯片的硬件差异从哪里影响你的设计
MCP2517FD 和 MCP2518FD 在驱动层面几乎可以共用一套代码,这是很多人直接选 2517 的理由,但硬件设计时它们并不是完全等效的。2517FD 的封装更大,引脚数量更多,适合做评估板和需要引出调试信号的场景;2518FD 封装更小,是为量产设备压缩 PCB 面积准备的。如果项目只有一路 CAN FD,又没有示波器探针需求,2518FD 更划算。反过来,如果板子还在迭代阶段、经常要改电阻电容,2517FD 的引脚间距会让你少焊坏几块板子。
另一个实际差异是散热。封装越小,芯片附近铺铜和过孔就越讲究,我在一个项目里遇到过 2518FD 长时间满负载发送后帧错误率略微上升的情况,后来把底层接地焊盘的过孔补满才恢复正常。驱动层面要注意的不是寄存器差异,而是中断引脚和时钟输出引脚的复用关系,2518FD 引脚少,CLKO 和中断功能经常挤在同一个引脚上,选型时先确认主控还有没有多余 IO 来接中断,别让驱动被迫走轮询。
从软件可移植性角度,我的惯例是:无论用哪一颗,头文件里只留一个MCP251XFD_HW_REV宏,驱动代码不做大分支。两芯片的寄存器映射和命令格式基本一致,真正需要区分的是初始化时要不要允许某些增强功能,以及帧发送完成中断的向量号。把差异收敛在配置层,后续换芯片只改宏,不改收发逻辑。
2.2 寄存器模型与对象 RAM:驱动只需维护一张内存表
整颗芯片的寄存器可以分为控制类、状态类、位时序类和报文缓冲类。前几类跟普通 SPI 外设没什么区别,麻烦在报文缓冲。MCP251XFD 不是把接收 FIFO 直接映射成几个寄存器,而是把一大块对象 RAM 切成若干块,由软件在初始化时配置成发送 FIFO、接收 FIFO、发送事件 FIFO(TEF)和用于调试的 RAM 空间。驱动上电时做的第一件事,不是去写波特率,而是先规划好这块 RAM 的布局。
这块 RAM 的分配结果会直接决定你中断处理的写法。比如把对象 RAM 前 16 个槽位配成发送 FIFO,中间 32 个槽位配成接收 FIFO,剩下几个槽位给 TEF 和 ECC 计数。驱动维护的其实是一张表:每个 FIFO 的基地址、深度、报文大小、是否启用中断。这张表在初始化时一次性写入,之后收发路径只需要按编号找地址,不需要每次都重新计算偏移。这也是 MCP251XFD 驱动比起老式 CAN 控制器驱动最大的进步:中断里拷走一帧数据,只需要两三次 SPI 读,效率高很多。
驱动结构体我一般这样定义:
typedef struct { spi_dev_t spi; gpio_t cs_pin; gpio_t int_pin; uint8_t hw_rev; /* 0 = MCP2517FD, 1 = MCP2518FD */ uint32_t osc_hz; /* 晶振频率,常见 40M/20M */ uint32_t nominal_br; /* 仲裁段波特率 */ uint32_t data_br; /* 数据段波特率 */ uint16_t rx_fifo_base; /* 接收 FIFO 在对象 RAM 中的首地址 */ uint8_t rx_fifo_depth; uint16_t tx_fifo_base; uint8_t tx_fifo_depth; } mcp251xfd_dev_t;spi_dev_t和gpio_t是平台抽象层,驱动内部只认这两个句柄,不直接操作寄存器。hw_rev字段不是启动时自动识别的,需要板级配置文件手工指定,因为两芯片的 ID 寄存器值在部分批次上无法可靠区分。把基地址和深度放进设备结构体,是为了让 FIFO 配置函数只依赖传入参数,不依赖全局变量,这样写单元测试也好写。
2.3 最小硬件接线与 SPI 时序初设
驱动写多少代码,很大程度取决于硬件接线给不给力。MCP251XFD 是纯 SPI 从机,需要四根线:SCK、SI、SO、CS,外加一个 INT 输出通知主控有中断。INT 必须接 MCU 的外部中断引脚,电平触发和边沿触发看初始化配置,我习惯用下降沿。晶振电路不要省,那种内部时钟凑合跑的做法在 CAN FD 上很容易翻车,数据段到了 2Mbps 以上,时钟精度不够直接体现在位错误上。
SPI 时钟我建议按芯片手册上限的七折左右初始。芯片 SPI 时钟标称能到 20MHz,但主控的 SPI 外设往往在高频下时序余量变小,而且 PCB 走线一长,信号过冲和振铃会让寄存器读写悄悄出错。保守做法:先按 10MHz 调通逻辑,再往上拉频率,每提升一档就做一次 10 万帧压力测试。驱动里 SPI 读写函数不要做任何延迟,延迟会让中断响应时间变差,高负载下直接丢帧。
给驱动用的 SPI 读写原语,至少要保证一次 CS 拉低期间完成“命令 + 地址 + 数据”的完整交换,不能让每次字节传输都单独拉 CS。很多 CAN 控制器对 CS 的时序要求很严,分开传输会把芯片内部的地址指针搞乱。下面这段是驱动底层的寄存器写接口:
#define MCP251XFD_CMD_RESET 0x00 #define MCP251XFD_CMD_WRITE 0x02 #define MCP251XFD_CMD_READ 0x03 #define MCP251XFD_CMD_READ_CRC 0x0B #define MCP251XFD_CMD_WRITE_CRC 0x0A static int mcp251xfd_spi_write(mcp251xfd_dev_t *dev, uint16_t addr, uint32_t val, uint8_t len) { uint8_t hdr[3]; uint8_t i; hdr[0] = MCP251XFD_CMD_WRITE; hdr[1] = (addr >> 8) & 0xFF; hdr[2] = addr & 0xFF; gpio_write(dev->cs_pin, 0); for (i = 0; i < 3; i++) spi_xfer(dev->spi, hdr[i]); for (i = 0; i < len; i++) spi_xfer(dev->spi, (val >> (8 * i)) & 0xFF); gpio_write(dev->cs_pin, 1); return 0; }这段代码逻辑很直白:先发命令字和 16 位地址,再按小端字节序发数据。CAN FD 控制器的寄存器基本都是 32 位对齐,len传 4 是常态,个别控制寄存器只用到低字节,传 1 或 2 减少 SPI 总线占用。注意这里没有加 CRC 指令,普通调试阶段用不到WRITE_CRC,量产阶段如果总线环境恶劣,再切换到带 CRC 的读写命令,能发现偶发的 SPI 数据翻转。
3. 驱动实现三板斧:SPI 指令层、位时序配置与对象 RAM 收发路径
3.1 SPI 指令层:不是所有驱动都需要 CRC,但你的读写必须加地址边界保护
MCP251XFD 的 SPI 指令集不算复杂,常用指令就那么几条,驱动底层真正要处理的是“地址对齐”和“批量传输”。很多开发者第一次写完驱动,发现寄存器读回乱七八糟,问题往往不是芯片坏了,而是发送地址时 16 位拆分写反了高字节低字节。芯片手册里地址字段的字节序和普通 SPI Flash 不同,这一点至少要踩一次坑才能记住。
我写的驱动在读写前会对地址做一次边界检查:寄存器区的地址必须在当前芯片支持的地址范围内,对象 RAM 的地址必须落在初始化配置好的 FIFO 区间内。这个检查看起来多余,但能救你一次:有一次我把接收 FIFO 基地址少算了两个字,正好落到相邻发送 FIFO 的区间上,数据错乱后整条总线都在报错,排查了很久才发现是地址越界而不是协议问题。边界检查的粒度可以按 FIFO 的槽位数算,不精确但够用。
指令层的性能直接决定驱动能跑多快。最影响速率的是中断响应里的批量读,一次接收中断要读 4 字节的报文描述符和最多 64 字节的数据。如果每个字节都调一次 SPI 传输函数,函数调用开销会比 SPI 本身还大。正确做法是准备一个 256 字节的 SPI DMA 缓冲,整块突发读完。驱动里尽量把spi_xfer做成内联函数,或者直接映射到平台层的 DMA 接口。
下面给一个带 CRC 校验的读接口,量产固件里我更倾向用它:
static int mcp251xfd_spi_read_crc(mcp251xfd_dev_t *dev, uint16_t addr, uint8_t *buf, uint8_t len) { uint8_t hdr[3] = { MCP251XFD_CMD_READ_CRC, addr >> 8, addr & 0xFF }; uint8_t crc_read; uint8_t crc_calc; gpio_write(dev->cs_pin, 0); for (uint8_t i = 0; i < 3; i++) spi_xfer(dev->spi, hdr[i]); for (uint8_t i = 0; i < len; i++) buf[i] = spi_xfer(dev->spi, 0xFF); crc_read = spi_xfer(dev->spi, 0xFF); gpio_write(dev->cs_pin, 1); crc_calc = mcp251xfd_crc8(buf, len); if (crc_read != crc_calc) return -1; return 0; }这里读了两遍 CRC:第一遍是芯片计算好的 CRC 值,第二遍是本地对收到的报文数据重新算一遍,两者相等才认为这帧数据没有在 SPI 传输中翻转。mcp251xfd_crc8是查表实现,表的生成多项式按芯片手册定义的 CRC8 算法来,不要在项目里自己发明多项式。CRC 模式会多花一个 SPI 时钟周期,但换来的是总线偶发干扰下不会把坏数据交给上层协议栈。
3.2 CAN FD 位时序计算:仲裁段和数据段必须分开配,别指望一个公式走天下
CAN FD 相比传统 CAN,最大的变化是仲裁段和数据段可以有不同的波特率。这就意味着驱动初始化要写两套位时序寄存器,而且数据段的位时间计算要考虑发送延迟补偿(TDC)。很多移植失败的项目,都是直接用传统 CAN 的波特率计算工具,套到 FD 数据段上,结果数据段采样点偏得离谱,帧发完对端根本收不到。
位时间的基本关系式是:波特率 = 时钟频率 / (BRP + 1) / (TSEG1 + TSEG2 + 1)。TSEG1 是传播段和相位缓冲段 1 之和,TSEG2 是相位缓冲段 2,外加固定一个同步段。采样点就在 TSEG1 结束的位置:采样点百分比 = (TSEG1 + 1) / (TSEG1 + TSEG2 + 1)。驱动要做的就是从目标波特率反推出合适的 BRP 和 TSEG 组合,约束条件包括 TSEG1 不小于 TSEG2、TSEG2 不小于 SJW、总时间量子数在 8 到 25 之间。
40MHz 晶振下我常用的一组配置是:仲裁段 500kbps,BRP 设为 3,TSEG1 寄存器值 9,TSEG2 寄存器值 9,采样点 50%;数据段 2Mbps,BRP 设为 0,TSEG1 寄存器值 9,TSEG2 寄存器值 9;数据段拉到 5Mbps 时,TSEG1 变为 3,TSEG2 变为 3。这里寄存器值和实际时间量子段数差 1,因为内部会加回同步段。采样点 50% 在大多数收发器组合下都能工作,但如果你用的收发器传播延迟偏大,要把采样点往后挪到 60% 到 70%。
驱动里我习惯把计算函数和配置函数拆开,调试时可以单独打印计算参数:
typedef struct { uint8_t brp; uint8_t tseg1; uint8_t tseg2; uint8_t sjw; } mcp251xfd_bit_timing_t; static int mcp251xfd_calc_timing(uint32_t osc_hz, uint32_t target_br, mcp251xfd_bit_timing_t *t) { uint32_t total_tq = osc_hz / target_br; uint8_t tseg_all; for (uint8_t brp = 1; brp <= 64; brp++) { tseg_all = (uint8_t)(total_tq / brp); if (tseg_all < 8 || tseg_all > 25) continue; t->brp = brp - 1; t->tseg1 = (tseg_all / 2) - 1; /* 先按 50% 采样点预置 */ t->tseg2 = tseg_all - (tseg_all / 2) - 1; t->sjw = 1; return 0; } return -1; }计算函数先从频率比值枚举出所有可能的 BRP,再筛选时间量子总数落在合法区间内的组合。tseg1先按二分设置,采样点就是 50%,实际项目里如果网络中有老的 CAN 节点,建议再手动往大了调,让采样点后移。需要注意total_tq是整数除法,振荡器频率和目标波特率不是整倍数时会出现截断误差,这就是某些速率的 CAN FD 帧偶发出错的根源。遇到这种情况,优先调 BRP 让总时间量子数更接近整数,而不是硬调 TSEG。
配置函数拿到计算结果后,分别写入仲裁段和数据段两套寄存器,还要把 TDC 使能位打开。TDC 打开后芯片会在帧发送时自动补偿收发器的环回延迟,这个功能在数据段超过 2Mbps 时是必须的,不开启的话长线缆上会出现位错误。TDC 的补偿上限跟晶振频率有关,40MHz 下一般能覆盖到几十纳秒的延迟,对于板内走线足够,但如果用了外接延长线,要重新评估。
3.3 发送路径与接收路径:FIFO 深度决定你能扛住多大的突发流量
对象 RAM 的布局是驱动初始化阶段最需要耐心的一步。发送侧我建议分两个发送 FIFO,一个给普通周期报文,一个给事件触发的紧急报文,两个 FIFO 的优先级要错开。接收侧至少配 8 个槽位,如果主控 SPI 频率不高,接收 FIFO 深度干脆配到 16 或 32,宁可牺牲一点 TEF 空间,也不能让总线上的帧因为软件来不及搬而丢掉。
发送一帧的软件路径是:应用层往发送 FIFO 写数据,然后设置发送请求位,芯片自动处理仲裁和重发。驱动只负责把数据搬到对象 RAM 的对应槽位。关键点在于槽位地址不是连续递增的,而是每个槽位固定占用 16 个 32 位字,里面前 4 个字是报文描述符,后面是数据。如果只写数据不碰描述符,发送请求会被芯片忽略。
我写发送函数时的习惯是先清掉发送 FIFO 的状态位,再逐字写数据,最后写控制字触发发送。顺序反过来的话,芯片可能在数据还没写完时就认为这是一帧完整报文并开始发送,那就会发出一个全是随机数据的错误帧。
static int mcp251xfd_send_frame(mcp251xfd_dev_t *dev, uint8_t fifo_idx, uint32_t id, const uint8_t *data, uint8_t len) { uint16_t base = dev->tx_fifo_base + fifo_idx * MCP251XFD_OBJ_RAM_SLOT_SIZE; uint32_t word[16] = {0}; uint8_t dlc = mcp251xfd_len_to_dlc(len); word[0] = (id & 0x1FFFFFFF); /* 标准帧/扩展帧 ID 按位写入 */ word[1] = (dlc << 16); /* DLC 在第二个字的高字节 */ memcpy(&word[2], data, len); /* 数据从第三个字开始 */ mcp251xfd_spi_write(dev, base + 0, word[0], 4); mcp251xfd_spi_write(dev, base + 4, word[1], 4); mcp251xfd_spi_write(dev, base + 8, word[2], len > 4 ? 4 : len); if (len > 4) mcp251xfd_spi_write(dev, base + 12, word[3], len - 4 < 4 ? len - 4 : 4); /* 置位发送请求位,芯片接管后续总线竞争 */ mcp251xfd_set_tx_req(dev, fifo_idx); return 0; }这段代码把报文 ID、DLC 和 64 字节数据拆成几个 32 位字分批写入对象 RAM。注意 DLC 不是直接填数据长度,CAN FD 有固定的 DLC 编码表,比如 12 字节填 9、16 字节填 10、32 字节填 12、64 字节填 15,所以mcp251xfd_len_to_dlc这个映射函数必须实现,否则对端收到的 DLC 和实际数据长度对不上,会直接丢帧。
接收方向,中断服务函数里要做的第一件事不是读数据,而是读中断向量寄存器,确认到底是接收 FIFO 有数据还是 TEF 有发送完成反馈。很多新手在这里会犯一个错:把所有可能的中断位一次性清掉,结果接收中断和发送中断同时挂起时,只处理了其中一个,另一个就丢了。正确做法是循环读取现场,逐个中断向量处理,直到没有 pending 状态为止。
接收数据的搬运可以完全复用一个批量读函数,把报文描述符和数据一次性读进结构体。这里为了减少中断占用时间,我一般把标志位解析和数据拷贝拆成两部:中断里只读描述符并标记缓冲区可用,真正的协议解析放到主循环里做。如果应用层响应要求不高,整个接收都轮询也没问题,但注意轮询周期必须小于发送节奏,否则 FIFO 会堆满。
4. MCP251XFD 驱动调试避坑实录:五个我踩过的坑
4.1 现象:SPI 能写不能读,寄存器回读全是 0xFF
有一次新板子回来,写配置寄存器没有一点报错,但读任何寄存器都是 0xFF。当时觉得芯片没坏,因为写指令没有让 SPI 总线异常,但读回就是死数据。这个现象你如果遇到,先不要怀疑芯片虚焊,多半是 SPI 模式不匹配。芯片要求的 SPI 时钟极性和相位跟主控默认配置不一致,写操作有时碰巧能成功,读操作在错误的采样沿上抓到的全是高电平。
排查方法很简单:用逻辑分析仪抓 SCK、SI、SO 三条线的相对关系,跟数据手册里的时序图一格格对。我那次是 SCK 空闲电平反了,芯片在主控采样沿之前就把数据释放掉了,改成 CPOL=1 立即恢复。注意不同批次芯片手册对 SPI 模式的定义可能写得不一致,要以时序图为准而不是文字描述。解决后顺手把初始化代码里加了一个读 ID 的自检步骤,读回值不对就直接报错,不继续往下配置。
4.2 现象:晶振正常,但 CAN 总线完全无波形
板子能写寄存器,中断也能触发,但示波器挂在 CAN_H 和 CAN_L 上看不到任何波形。这个坑的核心在时钟稳定标志:MCP251XFD 上电后需要一段时间等待晶振起振稳定,这段时间内芯片内部时钟还没锁定,但你写配置寄存器是能成功的,因为寄存器读写走的是 SPI 域。如果在时钟未稳定时配置波特率并开启发送,芯片会直接忽略你的发送请求。
解决方法是上电后先轮询时钟稳定状态位,等到它就绪再继续初始化。更稳妥的做法是同时配置 CLKO 引脚输出时钟信号,用示波器看 CLKO 的频率是否正确,频率差了十万八千里就说明晶振没起振或者负载电容不匹配。我那个板子是晶振负载电容焊错封装,焊了个 18pF 当 8pF 用,起振时间被拉长到几十毫秒,驱动里没有等标志位,第一次发送就石沉大海。
4.3 现象:FD 帧能发不能收,普通 CAN 帧正常
项目里用两颗同样的芯片做回环测试,普通帧收发正常,一切成 FD 帧就收不到。这个问题表面看是数据段波特率配置不对,实际要查两个地方:一是对端的 TDC 有没有开启,二是两边数据段波特率计算用的晶振频率是否一致。FD 帧在数据段要用更高波特率,芯片内部靠 TDC 测量发送延迟来调整采样点,如果 TDC 补偿量超限,接收端会认为数据段位错误。
排查步骤是先确认双方仲裁段都正常,然后把数据段波特率降到和仲裁段一样,FD 帧如果能通了,说明问题就出在高速数据段的时序上,再逐步把数据段速率往上拉。还有一个隐蔽原因:收发器工作在只听模式或静默模式,传统 CAN 帧因为帧结构简单偶尔能过去,FD 帧的高速位流触发收发器内部锁相环异常,表现为收不到。这种时候直接换一个已知没问题的节点板做对端,能快速定位问题在驱动还是收发器。
4.4 现象:高负载下丢失帧,但报文 FIFO 没有溢出标志
压力测试跑了两个小时,应用层统计发现丢了十几帧,但读取 FIFO 溢出标志始终为零。这个是驱动最阴险的坑:芯片的溢出标志只在 FIFO 完全满且又来新帧时才会置位,但如果你的接收中断服务函数搬数据太慢,新帧到达时槽位已经被覆盖,芯片会直接丢弃最老的一帧而不报告溢出。也就是说,丢帧发生时 FIFO 可能一直是半满状态,标志位自然永远不亮。
解决这类丢帧要分两头下手:一是把接收 FIFO 深度加大,让突发流量有缓冲余量;二是把中断服务函数里的数据搬运时间压缩到最小,启用 SPI 批量读,不要逐字节传输。最有效的验证方法是临时把 FIFO 深度从 16 改到 32 再跑同一压力脚本,如果丢帧率明显下降,就证明瓶颈在软件搬运而不在网络错误。量产前这类压力测试一定要做,别等现场出了问题再回来查。
4.5 现象:低功耗唤醒后第一次发送永远失败
带低功耗功能的产品,每次从休眠唤醒后,第一次发送的帧必然失败,之后的帧又全都正常。这个现象很典型,原因是唤醒时 SPI 主控先开始跑,芯片内部的时钟和状态机还没从休眠模式恢复,主控发出去的 SPI 写操作要么被忽略,要么只写了一半。芯片数据手册里唤醒时序写得清楚,但初版驱动容易忽略。
解决办法是在唤醒中断服务函数里加一段延时,等待芯片时钟稳定后再做软复位,重新初始化整个对象 RAM 布局。注意不是重读全部配置就能解决,关键是先执行 RESET 命令,把芯片内部的 SPI 状态机恢复到已知状态。我还会在唤醒路径里做一个发送健康检查:发送一个空帧,如果失败就再复位一次,最多重试三次,这样即使某些板子晶振起振慢,也不会在唤醒后有持续几分钟的通信盲区。
5. 进阶验证技巧:回环测试只能证明活着,时序窗口才能证明能用
驱动调通后,大家第一反应是做回环测试,但这只能证明芯片没坏、SPI 读写没跑飞。真正决定量产可靠性的,是时序余量。我常用的一个验证方法分三层:第一层芯片内部回环,验证基础收发路径;第二层把两个节点用短接线连起来,验证物理层和波特率配置;第三层在数据段高速模式下读取 TDC 测量值,用这个值反推实际线缆和收发器延迟,判断还有多少余量。
第三层是很多人忽略的。驱动初始化时打开 TDC,芯片会在发送过程中自动测量从总线边沿回到控制器的时间差。这个测量值存放在 TDC 相关寄存器里,单位是时间量子。你可以写一小段调试代码,在运行几十秒后把这个值读出来,换算成纳秒:实际延迟 = TDC 寄存器值 × (BRP + 1) / 晶振频率。如果这个值超过数据段位时间的一半,说明你的布线或 PCB 走线已经把时序余量吃光了,再调寄存器也补不回来,只能缩短走线或换更低延迟的收发器。
我现在做新板子,都会在驱动里保留一个调试开关,编进去之后串口可以随时打印当前 TDC 值、错误帧计数和 CRC 错误计数。这个功能量产固件里去掉,但开发阶段必须一直留着。它能让你在客户现场不用接示波器就能判断是驱动问题还是硬件布线问题,省掉一大半来回跑路的功夫。驱动调了一天没头绪的时候,我最后悔的就是没把测量功能提前写好。希望这一套流程能帮你把 MCP251XFD 项目一次打通,少走几个来回调板子的弯路。
本文还有配套的精品资源,点击获取