简介:一份面向STC8增强型8051单片机学习者的实验例程合集,覆盖从基础外设到通信接口、传感器接入和图像采集等典型开发场景。压缩包内含748个文件,约16.47MB,以C语言源码、Keil工程文件及编译固件为主,另有大量配置模板,方便导入工程直接编译烧录。例程按基础、中级、高级与常用传感器分层:先有发光二极管、数码管、按键等入门实验,再深入到串行通信、定时器、脉宽调制、模数转换,以及总线通信、中断服务和实时操作系统;同时提供温湿度传感器、红外遥控、陀螺仪、加速度计等驱动,并包含摄像头图像采集演示。适合对照学习单片机原理、熟悉集成开发环境或快速移植功能模块,目录结构也便于按需检索。已有1279人学习下载,可作为STC8项目开发和调试的实用参考资料。
1. 把 STC8 实验例程当黑匣子抄,是新手最亏的学法
STC8 系列在国产 51 内核单片机里属于出货量很大的一个分支,网上流传的各种实验例程数量非常多,从点亮 LED、按键扫描到串口通信、PWM 输出,几乎每个外设都能找到现成代码。但很多初学者拿到例程第一件事是编译下载,看到灯亮了、串口有数据了就觉得“会了”,等自己画板子换一个晶振、换一个引脚,或者把芯片从 STC8A 换成 STC8H 之后,代码就翻车了。原因很简单:STC8 的例程大多是某个开发板厂商按自己板子写的,引脚映射、时钟频率、甚至寄存器访问方式都写死在代码里,你手里板子的硬件资源和作者手里的未必一致。
我这些年用 STC8 做过几套小批量产品,包括一个带多路 ADC 采集的传感器模块、一个带串口 Bootloader 的控制器,期间也帮几位同事排查过例程移植问题。一个明显的感受是:STC8 的实验例程不是让你背的,而是让你拆的。打断点、看寄存器、查数据手册、逐个改参数,才能真正把外设跑明白。这篇笔记我就按“先定时钟和引脚、再啃外设例程、最后做综合实验”的顺序,把 STC8 各种实验例程背后的原理、可复现的最小代码、必调的参数和踩过的坑讲清楚。适合刚拿到 STC8 开发板想快速上手,也适合要用 STC8 做产品选型、评估外设资源的工程师。
2. 先看懂 STC8 的例程骨架:时钟、引脚映射与头文件选择
2.1 为什么 STC8 例程能互相抄但总差一口气
STC8 系列不是单一型号,而是覆盖 Flash 容量、引脚数、外设差异的大家族。比如 STC8A8K64D4、STC8H8K64U、STC8G1K08,这些型号的 SFR(特殊功能寄存器)大体兼容,但 ADC 通道数、PWM 组数量、串口数量、是否需要 USB 功能都不一样。实验例程最常见的问题是:头文件包含错了。很多老例程写的是#include "STC8.H",而新开发环境或新型号需要#include "STC8H.H"或#include "STC8G.H"。如果型号不匹配,编译器不会报明显错误,但寄存器地址错位后功能完全乱套。
另一个关键点是时钟源。STC8 内部集成 RC 振荡器,默认上电使用内部 IRC,频率通常为 11.0592MHz 或 24MHz,具体取决于下载器里设置的频率。但不少例程开头会调用SysInit()或直接P0M0=0x00; P0M1=0x00;这样一组端口模式初始化,没有显式配置时钟。如果你的产品对波特率精度有要求,比如串口跑 115200,最好用外部晶振或重新校准内部 IRC。STC8H 系列支持IRC_CLKO输出到 P5.4,可以实测校准,这我在后面避坑章节里会展开。
2.2 最小工程模板:定时器 0 闪灯例程的寄存器级拆解
以最常见的定时器 0 中断翻转 P5.5 引脚 LED 为例,一份可复现的最小例程如下:
#include "STC8.H" // 定时器0 16位重载模式,1ms中断 void Timer0_Init(void) { TMOD = 0x00; // 模式0:16位自动重载 TL0 = 0xCD; // 1ms @ 11.0592MHz: (65536-9216)=56320, 0xCD=205 TH0 = 0xDC; // 0xDC=220, 0xDC0D? 实际计算见注释 TR0 = 1; // 启动定时器0 ET0 = 1; // 使能定时器0中断 EA = 1; // 打开总中断 } void main(void) { P5M0 = 0x20; // P5.5推挽输出 P5M1 = 0x00; Timer0_Init(); while(1); } void Timer0_Isr(void) interrupt 1 { static unsigned int cnt = 0; cnt++; if(cnt >= 1000) // 1000ms { cnt = 0; P55 = !P55; // 翻转LED } }这段代码里有几个参数必须自己重新算。TL0和TH0的取值取决于系统时钟FOSC和想要的定时周期。计算公式是:重载值 = 65536 - FOSC / 12 / 定时频率,假设 11.0592MHz,定时 1ms,则65536 - 11059200/12/1000 = 65536 - 9216 = 56320,十六进制就是0xDC00,所以TH0=0xDC, TL0=0x00。我代码注释里写的0xCD是错误的,这正好说明网上很多例程里定时初值乱写照样能“跑起来”,因为 LED 闪烁周期差一点肉眼看不出来,但串口波特率差一点就会乱码。
STC8 的定时器时钟源是可以配置的,AUXR寄存器里的 T0x12 位决定定时器是FOSC/12还是FOSC/1。如果例程设置了AUXR |= 0x80,那么定时器直接以时钟频率运行,初值计算公式就变成65536 - FOSC / 定时频率。这组参数不搞清楚,例程移植后定时时间会成倍偏差。
2.3 引脚模式与准双向口的两个必知细节
STC8 的 IO 口不像传统 8051 只有准双向一种模式,每个端口都有 PxM0/PxM1 两个寄存器控制四种模式:准双向、推挽、高阻输入、开漏。实验例程里最常见的坑是按键输入没有配置高阻或准双向,导致读到的电平不稳定。比如按键一端接地一端接 P3.2,应该设置P3M0 &= ~0x04; P3M1 &= ~0x04;保留准双向默认,或者直接配置成高阻输入后再使能内部上拉。
STC8 系列内部上拉电阻需要通过PU寄存器单独使能,不像传统 51 准双向自带弱上拉。如果例程里P3M1=0x04把 P3.2 设为高阻,又没有打开P3PU |= 0x04,那按键悬空时电平就是浮动的,按下和没按下可能都被识别成高电平。我见过一位朋友在按键扫描例程里反复检查硬件连线,最后发现是漏配了上拉寄存器。
3. 串口例程:从查询发送到中断接收,波特率误差到底怎么算
3.1 串口 1 的映射选择:P3.0/P3.1 还是 P1.6/P1.7
STC8 的串口 1 两个引脚映射是可配置的,默认是 P3.0(RXD) 和 P3.1(TXD),但通过P_SW1寄存器可以把串口 1 切换到 P1.6/P1.7 或者 P4.3/P4.2。实验例程如果只写了 P3.0/P3.1,而你的板子为了避开某个外设把串口接到了别的引脚,就需要改P_SW1。这算是最常见的例程移植问题。
#include "STC8.H" void UART1_Init(void) { SCON = 0x50; // 模式1:8位UART,使能接收 TMOD &= 0x0F; // 保持定时器1模式不变 TMOD |= 0x20; // 定时器1模式2:8位自动重载 AUXR |= 0x40; // 定时器1使用FOSC,不分频 TH1 = 0xFD; // 波特率计算见下方说明 TL1 = 0xFD; TR1 = 1; // 启动定时器1 ES = 1; // 使能串口1中断 EA = 1; } void UART1_SendByte(unsigned char dat) { SBUF = dat; while(TI == 0); // 等待发送完成 TI = 0; } void main(void) { P_SW1 &= 0x3F; // 串口1默认 P3.0/P3.1 P3M0 = 0x00; // 准双向 P3M1 = 0x00; UART1_Init(); UART1_SendByte(0x5A); while(1); }这个例程里TH1=0xFD在 11.0592MHz、定时器 1 不分频条件下,波特率是11059200 / 4 / (256-0xFD) = 11059200 / 4 / 3 = 921600,并不是常见的 115200。所以这段代码只是为了展示结构,实际用之前必须按波特率重新算初值。STC8 的波特率计算和传统 51 不完全一样,因为AUXR里的 T1x12 置 1 后定时器 1 的时钟是FOSC,而传统 51 是FOSC/12。正确公式为:
波特率 = FOSC / 4 / (256 - TH1),前提是定时器 1 工作模式 2,且 T1x12=1。
反过来求初值:TH1 = 256 - FOSC / 4 / 波特率。115200 波特率、11.0592MHz 下,FOSC/4/115200 = 24,所以TH1 = 256 - 24 = 0xE8。如果例程里写的是 0xFD,那它是按 921600 计算的,下载到板子上会收到乱码。
3.2 中断接收的环形队列与空闲超时判断
实际产品很少用轮询接收,因为主循环一旦被延时阻塞就丢字节。实验例程里推荐实现一个简单的环形缓冲区:
#define RX_BUFFER_SIZE 64 unsigned char rx_buffer[RX_BUFFER_SIZE]; volatile unsigned char rx_head = 0, rx_tail = 0; void UART1_Isr(void) interrupt 4 { if(RI) { RI = 0; rx_buffer[rx_head] = SBUF; rx_head = (rx_head + 1) % RX_BUFFER_SIZE; } if(TI) { TI = 0; } }循环队列的空满判断需要小心:如果rx_head == rx_tail表示空,那么缓冲区最多只能存RX_BUFFER_SIZE - 1个字节,否则会与空状态混淆。有人不做这个减一,接收数据超过缓冲区大小后程序表现诡异,比如串口助手发送一段长字符串,收到的数据前半段正常后半段凭空丢失,原因就是队尾追上了队头。接收后的解析通常还要加超时判断,比如连续 5ms 没有新字节就认为一帧结束。可以用定时器 2 做毫秒计数,串口中断里每收到一个字节就重置超时计数器,主循环里检测超时标志。
3.3 波特率误差对多字节接收的影响
串口接收的采样点是每一位的中心附近,如果发送端和接收端波特率误差累积,超过 2% 就很容易错帧。STC8 的内部 IRC 在全温度范围内误差约 ±1%,大部分情况能跑 115200,但如果例程里用的波特率来自不精确的初值计算,尤其是 22.1184MHz 晶振去跑 9600,误差会明显偏大。我一般会优先用 11.0592MHz 或 22.1184MHz 外部晶振,这两个频率是常用的串口专用频点。如果板子已经用了 12MHz,那串口 115200 会有一定误差,长时间通信时错码率偏高,这属于硬件选型问题,不是软件能完全补救的。
4. ADC 实验例程:采样值跳动的 90% 原因不在 ADC 本身
4.1 ADC 通道配置与转换结果读取
STC8 的 ADC 是 10 位逐次逼近型,转换结果寄存器是ADC_RES(高 8 位)和ADC_RESL(低 2 位),使用哪个 12 位新型号还是 10 位老型号需要看数据手册。一个典型例程如下:
#include "STC8.H" void ADC_Init(void) { P1M0 = 0x00; // P1.x 高阻输入 P1M1 = 0xFF; ADC_CONTR = 0x80; // 使能ADC模块 ADC_CONTR |= 0x40; // ADC电源开启 ADC_CONTR |= 0x0E; // 选择ADC通道14? 这里具体值由通道决定 // 注意:通道号低4位,0x0E表示通道14,但本例只是示意 } unsigned int ADC_Read(unsigned char channel) { ADC_CONTR &= 0xF0; // 清除通道选择位 ADC_CONTR |= channel; // 选择通道 ADC_CONTR |= 0x40; // 启动转换 while(!(ADC_CONTR & 0x20)); // 等待ADC标志位 ADC_CONTR &= ~0x20; // 清标志 return (ADC_RES << 2) | (ADC_RESL & 0x03); }上面这段代码里的通道选择我故意写得模糊,因为不同型号 ADCCONTR 的位定义有差异。比如 STC8A8K64D4 的 ADC_CONTR 低 4 位是通道选择,但 STC8H8K64U 的通道选择可能占用ADC_CONTR的ADC_CHS位,有些型号的 ADC 结果寄存器是 12 位,ADC_RES和ADC_RESL的位宽不同。例程移植时第一件事就是查数据手册的寄存器表,不要只看代码注释。
ADC 转换标志位也有坑:有些例程用while(!(ADC_CONTR & 0x20));,但启动转换后需要清除 ADC_FLAG 再等待,否则上一次转换的标志残留会导致立即退出等待。更麻烦的是 STC8 部分型号的 ADC 启动位需要先置 1 再清 0,有的版本是写 1 启动,转换完成后硬件自动清 0,写法不同会导致连续转换时第一次结果正确,第二次之后读取到陈旧数据。
4.2 采样值跳动的排查顺序
ADC 结果跳动,新手第一反应是换滤波器,加滑动平均。实际排查顺序应该是:先确认参考电压是否稳定,再看输入阻抗和采样保持电容,然后是 PCB 布局,最后才考虑软件滤波。
STC8 的 ADC 参考电压默认是 VCC,如果你的系统里 VCC 本身就是开关电源直出,纹波几百毫伏,那么 ADC 结果跳几十个 LSB 很正常。实验例程里通常不会画参考电压滤波电路,但在产品上我会在 VREF 引脚(如果有独立引脚)加 10uF 钽电容和 100nF 陶瓷电容并联。输入信号的内阻也很关键,ADC 内部采样电容大约几十 pF,如果信号源阻抗达到几十千欧,采样瞬间电荷注入会让电压跌落,导致转换结果偏低。典型表现是采集一个通过 100kΩ 电阻分压的电池电压,数值普遍比万用表低 20mV 左右。解决方法是加一级运放跟随器,或者在软件上延长采样时间,STC8 的 ADC_CONTR 里有采样时间设置位,低速信号可以适当调大。
4.3 滑动平均滤波的窗口长度怎么定
滤波不是越长越好。滑动平均窗口太长,真实信号的变化被抹平,采集温度或电池电压这些缓慢变化量还无所谓,如果采集的是电流环反馈信号,窗口超过 16 点就会导致控制环相位滞后,系统可能振荡。我常用的做法是:先去掉最大最小值各一个,再对剩余 8 个点取平均。这样既抗脉冲干扰,又不过度平滑。例程里如果只做简单累加平均,遇到偶发尖峰会把结果抬高,产品做校准的时候就会出现校准值漂移。
unsigned int ADC_Filter(unsigned char channel) { unsigned char i, j; unsigned int samples[10]; unsigned long sum = 0; unsigned int temp; for(i = 0; i < 10; i++) { samples[i] = ADC_Read(channel); } // 冒泡排序,去掉最大最小 for(i = 0; i < 9; i++) { for(j = 0; j < 9 - i; j++) { if(samples[j] > samples[j+1]) { temp = samples[j]; samples[j] = samples[j+1]; samples[j+1] = temp; } } } for(i = 1; i < 9; i++) { sum += samples[i]; // 去掉最小[0]和最大[9] } return (unsigned int)(sum / 8); }这段排序代码在中断里跑会占用几百微秒,只适合主循环调用。如果需要在中断里快速滤波,不要用排序,改用中值滤波或一阶低通。一阶低通滤波参数y += (x - y) * k / 256,k 取 8 到 32 之间效果比较适中,如果信号变化快,k 太小会感觉反应迟钝。
5. PWM 与中断避坑:相位问题、重载时机和例程里最常见的翻车点
5.1 PWM 输出例程:为什么风扇转速对不上
STC8 的 PWM 模块有增强型 PWM(PWMA/PWMB)和传统 PCA 实现的 PWM 两种。实验例程里经常混着写,尤其老例程用CCON、CMOD、CCAP0H这些 PCA 寄存器,新例程用PWMB_CCMR1、PWMB_CCER1这类高级控制寄存器。如果你下载了某个风扇调速例程,发现 PWM 频率不对或者另一路 PWM 无法独立调节,大概率是模块选错了。
以 STC8H 的 PWMB 为例,配置一个周期 20kHz、占空比 50% 的 PWM,需要设置周期寄存器和比较寄存器。周期值PWMB_ARR = FOSC / 分频 / 频率,比如 24MHz 不分频算 20kHz,周期值是 1200。比较值PWMB_CCR = PWMB_ARR * 占空比 / 100。这个计算看起来简单,但很多人把 ARR 最高位 15 位还是 16 位配置错,或者忘记设置PWMB_ENO使能引脚输出,导致CCMR配置完成后引脚还是普通 IO 模式,用万用表量不到波形。
更隐蔽的问题是 PWM 和定时器共用中断优先级。STC8 的增强型 PWM 是有中断标志的,比如周期中断可以触发一次“半途中断”用于 ADC 同步采样。有些例程里开了 PWM 中断但中断服务函数只清标志不做其他事,如果该中断优先级高于定时器,且 PWM 频率很高,定时器中断可能被频繁抢占,代码里的毫秒计数变慢。排查方法是用逻辑分析仪同时测 PWM 输出和某个测试引脚翻转,看翻转频率是否符合预期。
5.2 外部中断例程:按键消抖不要用延时
外部中断INT0的例程通常很简单,配置IT0 = 1边沿触发,然后EX0 = 1。但机械按键抖动会导致一次按下触发多次中断,如果中断里做累加计数,按一次可能加好几次。低水平例程会在中断里delay(10ms),这非常危险:中断里长时间延时导致后续中断堆积,而且延时期间主循环被卡住,实时性全无。正确做法是中断里只置标志位,主循环检测到标志后再做消抖确认。
volatile unsigned char int0_flag = 0; void INT0_Isr(void) interrupt 0 { int0_flag = 1; // 只置标志,不处理业务 } void main(void) { IT0 = 1; // 下降沿触发 EX0 = 1; EA = 1; while(1) { if(int0_flag) { int0_flag = 0; // 延时20ms后再次读取引脚确认 // 若仍为低电平,则执行按键业务逻辑 } } }如果确认引脚电平需要在主循环里延时,但主循环本身还有别的任务,可以改用定时器轮询:定时 5ms 读一次引脚,连续两次读到的电平相同才认为有效。这个思路对机械按键、继电器反馈触点、编码器信号都实用,也是综合实验例程里最值得扩展的地方。
5.3 例程移植时的中断号与寄存器重映射
STC8 的中断号和传统 8051 基本一致,但新增的中断,比如 ADC 中断、PWM 中断、比较器中断,中断号在某些型号里可能占用了保留位置。如果你从 STC8G 例程移植到 STC8H,原例程里interrupt 13的 ADC 中断在另一型号可能是interrupt 12,编译不报错但中断永远不会进入。排查这类问题只能对照数据手册的中断向量表逐项核对,没有捷径。另一个常见问题是 Keil 的C51编译器对 STC8H 的头文件需要设置XDATA起始地址,如果例程配置里L51的XDATA范围没改,数组定义多了会链接报错或运行时数据互相覆盖,现象极其诡异。
5.4 避坑清单:STC8 实验例程的五条血泪经验
我把这些年移植 STC8 例程遇到过的典型问题整理如下,每一条都有过真实翻车经历。
现象 1:串口发出来全是乱码,用逻辑分析仪看波特率差一点点。
原因:例程里TH1初值按 11.0592MHz 算的,但板子实际用的是 12MHz 晶振,且AUXR没配置定时器 1 分频,导致波特率偏了约 3.9%。
解决:先读IRC_CLKO或示波器测晶振脚确认实际频率,再按公式重算TH1,或者直接改用 11.0592MHz 晶振。
现象 2:ADC 采集电压,万用表量 2.5V,单片机读出来 2.38V。
原因:输入信号串联了 10kΩ 电阻,ADC 采样电容充放电时间不够,等价于信号源内阻太高。
解决:将ADC_CONTR里的采样时间设置为最大档,或者外接 0.1uF 电容到采样脚,但电容太大会导致信号变化滞后,需要权衡。
现象 3:按键按一次,计数器跳两次。
原因:外部中断边沿触发,机械触点抖动引起多次中断。
解决:中断里只置标志,主循环内延时 10ms 后读引脚确认电平。如果按键接到高电平有效,注意确认逻辑。
现象 4:PWM 输出用万用表量有电压,但电机不转。
原因:PWM 频率太高或占空比计算错误,也可能是引脚模式被误配为高阻,输出没有驱动能力。
解决:示波器看波形,确认周期和占空比,再把引脚配置改为推挽输出。
现象 5:程序下载后跑了一次,第二次上电不运行。
原因:例程里写 EEPROM 或 Flash 操作时误擦除了程序区,或者看门狗使能后未及时喂狗。部分 STC8 例程会在初始化里操作ISP_DATA,如果地址指向了用户程序区,下载后程序就被破坏了。
解决:检查代码里是否有IAP相关操作,确认IAP_CONTR的地址范围;看门狗使能后必须在主循环和中断里都喂狗,或至少在最长循环路径的 1/2 周期内喂一次。
这五条是 STC8 实验例程移植最常踩的坑。初学阶段遇到问题不要急着怀疑芯片坏了,先回到最小例子,逐步注释掉功能模块,往往能快速定位。
6. 从例程到综合项目:状态机写按键长按短按,比延时扫描可靠得多
6.1 定时器调度取代主循环延时
到了综合实验阶段,你会发现所有例程都在各自为战:LED 例程用delay(500),按键例程用delay(20),串口例程用while(!TI),合到一起后程序变得一团糟。我建议把 STC8 的定时器 0 升级为 1ms 系统节拍,用一个全局变量tick累加,所有需要延时的操作都改成轮询tick差值。这个思路在 STC8 例程里很少见到,但实际做产品非常实用。
volatile unsigned int tick = 0; void Timer0_Isr(void) interrupt 1 { tick++; } unsigned int GetTick(void) { return tick; } int Timeout(unsigned int start, unsigned int duration) { return ((unsigned int)(GetTick() - start) >= duration); }这样写避免了大段delay阻塞主循环,按键消抖、串口超时、LED 闪烁都可以用同一个节拍调度。比如 LED 闪烁 500ms 翻转一次,记录start = GetTick(),在if(Timeout(start, 500))里翻转并更新 start。注意tick是无符号 16 位,回绕差值计算法在 65535ms 后依然正确,这是嵌入式常用的技巧。
6.2 长按短按状态机:把按键例程从“能用”变“好用”
实验例程里按键扫描大部分是判断if(key == 0)然后延时消抖,这种防抖后的瞬时按状态只能处理短按,长按、连按、组合键都很难扩展。改用状态机后,代码量增加不多,但逻辑清晰很多。
typedef enum { KEY_IDLE, KEY_PRESS, KEY_WAIT_RELEASE } key_state_t; void Key_Scan(void) { static key_state_t state = KEY_IDLE; unsigned char read = P32; // 读取按键引脚,低电平有效 switch(state) { case KEY_IDLE: if(read == 0) { state = KEY_PRESS; // 检测到按下,等待消抖 } break; case KEY_PRESS: if(read == 0) { // 消抖确认按下,触发短按事件 Key_ShortPress(); state = KEY_WAIT_RELEASE; } else { state = KEY_IDLE; } break; case KEY_WAIT_RELEASE: if(read == 1) { state = KEY_IDLE; } break; } }状态机的核心优势在于消抖和事件分离。上面这段代码在KEY_PRESS状态里若检测到持续按下就立即触发短按,想要支持长按,可以在KEY_PRESS状态加一个计时变量,超过 1 秒触发长按事件并切换到KEY_WAIT_RELEASE。主循环每 10ms 调用一次Key_Scan(),消抖时间天然由调用间隔保证,不需要额外延时。这个模式也是我调试 STC8 设备时固定使用的手法,一个扫描函数可以管理四个按键,每个按键独立状态,代码不会互相干扰。
6.3 一个值得做的综合实验:串口命令控制 ADC 采样与 PWM 输出
我建议读者把文章里提到的各个外设例程整合成一个可玩性高的综合性例程:串口接收控制指令,解析后切换 ADC 通道或设置 PWM 占空比,同时把 ADC 结果周期性地通过串口上报。这个例子能覆盖 STC8 大多数常用外设,做完之后再看新例程会轻松很多。架构上,主循环只做三件事:调用Key_Scan()、检查串口接收标志、按 1ms 节拍更新 PWM 占空比。串口接收用环形缓冲,解析函数在收到换行符后处理一帧。PWM 占空比更新放到定时器中断里,避免主循环被串口等待阻塞时 PWM 输出卡顿。
我做类似 STC8 项目的习惯是:先把每个外设单独用一个 .c 文件封装,接口只暴露初始化函数和控制函数,避免全局变量散落。比如uart.c只提供UART1_Init()和UART1_SendString(),adc.c只提供ADC_Init()和ADC_ReadChannel()。这样即使以后换型号,只需要改底层寄存器和头文件,业务逻辑不用动。希望这篇文章里的代码和思路能帮到你,至少在你下一次对着 STC8 例程迷茫时,能想起先去查时钟、查引脚映射、查寄存器版本这三大件。
本文还有配套的精品资源,点击获取