
简介面向STM32入门开发者的一套智能风扇项目资源基于C语言实现完整源码并附带演示视频可帮助理解如何通过温度传感器自动控制风扇转速。资源共325个文件主要包含C/H语言源码文件、Keil工程文件、编译生成的中间文件与可执行文件等整体压缩包仅14.46MB结构紧凑便于下载学习。目前已有528人学习浏览适合正在练习STM32外设驱动的读者。项目提供Human和Auto两种模式手动档位和自动温度调速通过4个按键切换在Auto模式下DS18B20采集环境温度程序据此改变PWM占空比从而调节风扇风速。源码中涵盖了定时器PWM输出、单总线温度读取、按键扫描等常用模块并辅以演示视频直观展示运行效果为进一步加入线性调速或PID控制留出了清晰的改造空间。1. 把风扇调速做成闭环才算是真正用熟了定时器不少人在 STM32 上点灯、跑串口都顺手但一到用 PWM 控制电机就容易卡住要么风扇只转不调速要么一开电机单片机就复位。这个基于 STM32F103C8T6 的智能风扇项目正好把定时器输出比较、PWM 占空比调节、ADC 或单总线温度采集、按键状态机这几块硬骨头串在了一起。它的核心逻辑并不复杂——Human 模式下手动切三档Auto 模式下根据 DS18B20 读到的温度动态改变 PWM 的 CCR 值来调整风扇转速。但恰恰因为没用 PID整个控制链路反而更透明适合拿来理解传感器 - 控制策略 - 执行器的完整闭环。对于正在做课程设计、或者想把手头的 STM32 开发板玩出实际用途的工程师来说这个项目的源码结构和硬件接线都足够典型值得拆开来看。2. 硬件链路与关键选型DS18B20、L298N 和定时器通道的配合2.1 为什么是 DS18B20 而不是 NTC 或 LM75项目里温度传感器用的是 DS18B20这是一颗单总线数字传感器测量范围 -55°C 到 125°C12 位分辨率下精度 ±0.5°C。相比 NTC 热敏电阻需要自己搭放大电路和查表校准DS18B20 直接输出数字量只需要一根数据线就能完成通信这对简化嵌入式系统的硬件设计帮助很大。更关键的是DS18B20 的读取时序是标准的单总线协议用 GPIO 模拟时序即可不占用片上外设资源。常见的做法是用一个普通推挽输出引脚比如 PA0接 DQ 线再在 DQ 和 VCC 之间接一个 4.7kΩ 上拉电阻因为单总线要求开漏或三态输出上拉电阻是保证空闲态为高电平的必要条件。DS18B20 的供电方式有两种寄生供电和数据线供电。本项目用的是外部 3.3V 供电这种情况下 VDD 接 3.3VVCC 和 GND 之间需要加一个 0.1μF 去耦电容。如果使用寄生供电模式DQ 线在高电平期间给内部电容充电转换期间总线必须被主机拉高否则转换会失败——不少人在做的时候在这里踩坑表现为读到的温度始终是 85°C其实就是转换期间供电不足导致的默认上电值。单总线的时序要求比较严格初始化时主机拉低总线至少 480μs 再释放然后等待 DS18B20 拉低 60~240μs 作为存在脉冲。读时序和写时序的时隙都是 60μs 左右写 1 是拉低后 1~15μs 内释放写 0 是拉低整个 60μs。这些微秒级的延时不能用 delay_ms 糊弄最好基于 SysTick 写一个精确到微秒的延时函数。下面这段代码是标准库下 DS18B20 初始化并读取温度的核心流程uint8_t DS18B20_Reset(void) { uint8_t presence; GPIO_ResetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); // 拉低 DQ delay_us(480); // 主机复位脉冲 GPIO_SetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); // 释放总线 delay_us(60); // 等待从设备响应窗口 presence GPIO_ReadInputDataBit(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); delay_us(420); // 存在脉冲剩余时间 return presence; // 0 表示设备存在 } void DS18B20_WriteByte(uint8_t data) { for (uint8_t i 0; i 8; i) { GPIO_ResetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); delay_us(2); if (data 0x01) GPIO_SetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); // 写 1 delay_us(60); GPIO_SetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); data 1; } }上面的代码里有个容易忽略的点读写时序之间必须保证 GPIO 方向正确。标准库下如果是推挽输出读数据前要把引脚模式切换为输入浮空或输入上拉否则读到的永远是引脚自身的输出电平。在实际工程里我一般会让 DS18B20 的 DQ 引脚保持开漏输出模式并外加上拉电阻这样读写只需要切换 CRL 寄存器里的 MODE 位逻辑更清晰。另一个细节是写字节时先写低位还是高位DS18B20 规定先发送最低有效位因此循环里用的是data 0x01然后右移方向反了会导致温度数据读取为乱码。2.2 L298N 驱动风扇的接线逻辑风扇电机驱动用的是 L298N 双 H 桥驱动芯片虽然它更多被用于直流电机或步进电机但驱动一个 5V 或 12V 的直流风扇完全没有问题。L298N 的逻辑电源 VSS 接 5V电机电源 VS 根据风扇额定电压接入外部电源比如 12V 风扇就接 12V。单片机本身不直接驱动电机而是通过 L298N 的 IN1、IN2 引脚控制风扇的转动和方向ENA 引脚接收 PWM 信号来控制转速。这里有必要说清楚一个很多人搞混的点如果风扇只需要单向转动、只调转速那么 IN1 固定为高电平、IN2 固定为低电平PWM 信号接在 ENA 上即可。L298N 内部逻辑是 ENA 为高时输出随 IN 状态ENA 为低时输出关断。所以把 PWM 接到 ENA等于用 PWM 的占空比控制电机平均电压——占空比越高风扇转速越快。下面是 L298N 与 STM32 的接线对应关系STM32 引脚L298N 引脚功能说明PB6ENA定时器 4 通道 1PWM 输出控制转速PB7IN1方向控制固定高电平PB8IN2方向控制固定低电平5VVSS逻辑电源12V 外部VS电机电源GNDGND与 STM32 共地共地是很容易被忽视的关键点。L298N 的 GND 必须和 STM32 的 GND 连在一起否则 PWM 信号没有参考电平驱动芯片接收不到有效逻辑电平表现就是风扇完全不转。如果你用的是 12V 风扇建议 VS 单独用稳压电源供电不要把 12V 引到 STM32 的 5V 引脚上否则稳压芯片会过热甚至烧毁。调试的时候可以用万用表量一下 ENA 引脚的对地电压占空比 50% 时大约 1.65V3.3V 逻辑这说明 PWM 信号是正常的。3. 定时器 PWM 输出与档位架构从 CCR 寄存器到占空比换算3.1 用定时器产生 PWM 的寄存器级配置在这个项目里PWM 输出是控制风扇转速的核心手段。STM32F103C8T6 有 4 个定时器其中 TIM1 和 TIM8 是高级定时器TIM2~TIM4 是通用定时器。通用定时器的通道 1 到通道 4 都可以映射到不同的 GPIO 引脚。项目源码里有stm32f10x_tim.c说明用的就是标准外设库的定时器接口。以 TIM4 通道 1PB6为例PWM 输出配置需要以下几个步骤每一步在标准库中都有对应的结构体字段void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_InitStructure; TIM_OCInitTypeDef OC_InitStructure; // 1. 使能时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 2. PB6 配置为复用推挽输出速度 50MHz GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 3. 定时器基础配置72MHz / 72 / 1000 1kHz PWM TIM_InitStructure.TIM_Prescaler 72 - 1; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_InitStructure.TIM_Period 1000 - 1; TIM_InitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM4, TIM_InitStructure); // 4. 通道 1 配置为 PWM1 模式 OC_InitStructure.TIM_OCMode TIM_OCMode_PWM1; OC_InitStructure.TIM_OutputState TIM_OutputState_Enable; OC_InitStructure.TIM_Pulse 0; // 初始占空比 0% OC_InitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM4, OC_InitStructure); TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable); TIM_Cmd(TIM4, ENABLE); }这段代码里的关键是分频系数和自动重装载值的搭配。系统主频是 72MHzAPB1 总线时钟最大 36MHz但定时器时钟倍频后仍然是 72MHz。预分频器设置为 71即 72MHz 除以 72 得到 1MHz 的计数频率自动重装载值 ARR 为 999即计数从 0 到 999 共 1000 个周期最终 PWM 频率是 1MHz / 1000 1kHz。这个频率对于风扇电机来说是可以接受的——低于 500Hz 会听到明显的啸叫超过 20kHz 又会增加 MOSFET 开关损耗。1kHz 是一个折中的选择。PWM 的占空比由捕获比较寄存器 CCR 决定。在 PWM1 模式下当计数值小于 CCR 时输出有效电平高电平当计数值大于等于 CCR 时输出无效电平低电平。所以 CCR 的值等于占空比乘以 ARR比如 ARR 为 999 时CCR 设为 500 就是 50% 占空比。调整风速只需要不断更新 CCR 值在标准库中对应TIM_SetCompare1(TIM4, duty);函数。如果你用的是 HAL 库对应的是__HAL_TIM_SET_COMPARE(htim4, TIM_CHANNEL_1, duty);原理完全相同。3.2 手动三档和自动模式的档位映射项目摘要里说明Human 模式下手动调节风速有 3 个档位Auto 模式根据温度自动改变 PWM 值。这里需要思考的是手动三档和自动模式在代码架构上如何统一。合理的做法是定义一个全局状态变量表示当前模式再为三种手动档位和自动计算各分配一个目标占空比生成函数。这里给出一个我常用的档位映射方案状态温度区间占空比对应 CCR 值ARR999一档低 26°C30%300二档中26°C ~ 32°C60%600三档高 32°C100%999这个方案是分段式的温度到达阈值时占空比跳变。原作者说如果让风扇转速和温度对应变化还有个方法就是通过计算让两者呈线性关系指的就是在最低温度和最高温度之间做线性插值。两种映射各有适用场景分段映射适合需要明确档位反馈的场景LED 指示、蜂鸣器提示都更直观线性映射适合温度越高风越大的平滑体验但用户对转速变化不敏感。Linear 映射的实现很简单——把温度范围映射到占空比范围。设定最低温度 T_min 20°C 对应占空比 20%最高温度 T_max 38°C 对应占空比 100%中间用线性公式duty 20 (temp - 20) * 80 / 18。最终限幅在 20~100 之间。这个公式的本质是把温度区间线性拉伸到占空比区间中间没有滞回温度在阈值附近时风扇转速会跟着温度抖动——这就是不用 PID 的代价也是原作者特别说明并没有用 PID的原因。后面第 5 章会专门谈这个问题。4. 按键扫描与主循环控制逻辑状态机设计及其边界4.1 按键去抖与模式切换的常见实现项目使用了 4 个按键分别对应模式转换、一档风速、二档风速、三档风速。按键扫描是嵌入式系统中最常见也最容易出问题的地方。一种粗暴的做法是在 while 循环里直接读取 GPIO检测到低电平就立即执行动作。但机械按键的抖动时间约 5~10ms如果不做去抖处理一次按下可能被识别成多次导致模式在 Human 和 Auto 之间来回跳变。常见的做法有两种阻塞式延时去抖和状态机扫描。前者实现简单在检测到按键按下后延时 10ms 再检测一次如果仍然是按下状态就确认是有效按键。这个方案在单片机没有其他实时任务时足够可靠。后者适合系统中有多个任务需要并发处理的场景——用定时器中断每隔 1ms 扫描一次按键通过状态转移来区分按下、抖动、稳定按下和释放。对于本项目这种低速控制场景用阻塞式去抖写在主循环里即可但要注意一个细节延时去抖的 10ms 会导致主循环阻塞如果这时候 DS18B20 正在做温度转换最长 750ms需要避免在温度读取的中途被按键打断。下面给出按键扫描的代码示例包含首次按下加速识别和一档二档三档的独立处理逻辑#define KEY_MODE_GPIO GPIOA #define KEY_MODE_PIN GPIO_Pin_0 #define KEY_1_GPIO GPIOA #define KEY_1_PIN GPIO_Pin_1 #define KEY_2_GPIO GPIOA #define KEY_2_PIN GPIO_Pin_2 #define KEY_3_GPIO GPIOA #define KEY_3_PIN GPIO_Pin_3 uint8_t Key_Scan(void) { uint8_t key_value 0; if (GPIO_ReadInputDataBit(KEY_MODE_GPIO, KEY_MODE_PIN) Bit_RESET) { delay_ms(10); // 消抖 if (GPIO_ReadInputDataBit(KEY_MODE_GPIO, KEY_MODE_PIN) Bit_RESET) { key_value KEY_MODE_PRESSED; while (GPIO_ReadInputDataBit(KEY_MODE_GPIO, KEY_MODE_PIN) Bit_RESET); } } // 另外三个按键用相同模式检测分别返回 KEY_1/2/3_PRESSED return key_value; }这种扫描方式的关键问题在于while等待释放是阻塞的。如果用户按住按键不松手主循环就卡在这里温度采集和 PWM 更新全部暂停。更稳妥的做法是检测到下降沿就返回按键事件不等待释放。你可以记录上一次按键状态当前为低且上次为高时视为一次有效按下释放状态留给下一次扫描去判断。上拉输入模式下按键按下时引脚为低电平未按下为高电平这个极性关系如果写反按键会完全失灵。4.2 主循环的组织方式与温度读取节奏主循环的逻辑相当直观扫描按键根据按键结果更新模式或档位如果是 Auto 模式读取 DS18B20 温度计算占空比并更新 TIM4 的 CCR。但有一个性能问题需要处理——DS18B20 的转换时间最长是 750ms12 位分辨率如果在主循环里每次迭代都调用完整的温度转换和读取流程整个系统的响应时间会被温度转换拖慢。一个可行的做法是降低温度采样频率比如每 500ms 才发起一次转换其余时间主循环只处理按键和 PWM 更新。下面给出一个简化版主循环的实现框架while (1) { uint8_t key Key_Scan(); switch (key) { case KEY_MODE_PRESSED: mode (mode MODE_MANUAL) ? MODE_AUTO : MODE_MANUAL; break; case KEY_1_PRESSED: if (mode MODE_MANUAL) manual_level 1; break; case KEY_2_PRESSED: if (mode MODE_MANUAL) manual_level 2; break; case KEY_3_PRESSED: if (mode MODE_MANUAL) manual_level 3; break; default: break; } if (mode MODE_MANUAL) { duty manual_duty[manual_level]; // 查表如 {0, 300, 600, 999} } else { temp DS18B20_ReadTemp(); // 读取内部温度值 duty TempToDuty(temp); // 线性映射或分段映射 } TIM_SetCompare1(TIM4, duty); }这个主循环里有一个值得留意的细节手动模式下一档二档三档对应的是一个查表数组manual_duty[3]而不是用 if-else 堆叠。查表的好处是新增档位只需要扩展数组不需要改动控制逻辑也让占空比参数集中在同一处便于调参。如果你把表中档位的值改成 {200, 500, 900}风扇在三档下不会满速运行这样在测试时可以避免风扇全速时的噪音干扰也方便对比不同占空比下风扇的实际转速。Auto 模式下的TempToDuty函数先判断温度是否低于下限或高于上限再做线性映射或查分段表。项目源码里没有 PID 控制器所以这个函数是纯计算不需要反馈校正。但这种情况下面临一个现实问题DS18B20 的测量值是带有噪声的温度在阈值附近时风扇会突然加速又减速。你可以对温度值做简单的滑动平均滤波比如维护一个长度为 5 的环形缓冲区取其平均值用于计算占空比能明显减少转速抖动。5. 从 0 到 1 复现这个项目从工程文件到硬件调试的完整闭环5.1 基于标准外设库的工程搭建要点源码包里的stm32f10x_tim.c、stm32f10x_flash.c、stm32f10x_rcc.c、stm32f10x_adc.c等文件说明这棵工程是基于 STM32 标准外设库Standard Peripheral Library构建的。标准库虽然已经被 ST 官方停止维护但 STM32F103 系列的项目存量很大不少公司和课程设计仍然沿用这套代码。如果你是从零开始复现推荐直接使用 STM32CubeMX 生成初始化代码然后移植逻辑或者从源码包里已有的工程文件上修改引脚映射。工程配置中你需要重点检查三个时钟相关项系统时钟是否为 72MHz、APB1 预分频是否为 /2这样定时器时钟才能真正以 72MHz 运行、以及 GPIO 时钟是否正确使能。标准库中 RCC 配置在stm32f10x_rcc.c中完成。系统启动后会执行SystemInit()它读取 flash 中的时钟配置字将 SYSCLK 设置为 72MHz。如果你的SystemInit()没有正确执行系统时钟可能停留在 8MHz 内部时钟PWM 频率就会按比例缩小风扇转速会明显变慢——这是非常常见的风扇转速不对的原因。DS18B20 的 GPIO 引脚选择也有讲究。建议选一个 5V 容忍的引脚因为 DS18B20 在寄生供电模式下可能把 DQ 拉到接近 5V虽然大多数 3.3V 输出的单片机也能识别但长期运行有风险。STM32F103 的大部分引脚是 5V 容忍的但 ADC 相关的 PA0~PA7 在某些数据手册中被标注为不是 5V 容忍需要查阅具体引脚定义。稳妥的做法是用 PA8 或 PB5 这类通用 IO 来连接 DS18B20避免与 ADC 通道混淆。5.2 实验现象验证示波器观测 PWM 波形与温控响应代码烧录后不要急着装风扇先用示波器探针夹在 PB6 上验证 PWM 波形。手动模式下一档对应 30% 占空比、二档 60%、三档 100%示波器上应该能直观看到脉冲宽度变化。没有示波器的话可以用万用表测量 PB6 的平均电压3.3V 乘以占空比一档约 0.99V、二档约 1.98V、三档约 3.3V。如果测到的电压恒为 0V 或恒为 3.3V说明定时器没有进入 PWM 模式需要检查TIM_OC1Init的TIM_OCMode字段是否配置为TIM_OCMode_PWM1。接下来做温控实验。用打火机或电烙铁靠近 DS18B20观察 Auto 模式下风扇转速是否随温度升高而加快。这里一个重要验证点是温度转换需要时间从打火机靠近到风扇转速变化之间应该有 750ms 左右的延迟这是正常的。如果延迟超过 2 秒说明你的delay_us函数精度有问题或者主循环中某个阻塞操作占用了太多时间。如果温度变化导致风扇转速跳变而非平滑变化说明你用的是分段表而不是线性映射这符合 4.2 节中两种方案的行为差异。代码中stm32f10x_flash.c的存在暗示项目可能将温度校准参数或运行模式存储在了 Flash 中。标准库中 Flash 操作的典型场景是保存用户配置比如记住上次关机时的档位。如果你在实现中想加入断电记忆功能需要使用FLASH_Unlock、FLASH_ErasePage、FLASH_ProgramHalfWord这一组接口。注意写 Flash 前必须先擦除擦除以页为单位而写入是半字16 位单位FLASH_Unlock(); FLASH_ErasePage(0x0800F000); // 擦除最后一页 FLASH_ProgramHalfWord(0x0800F000, mode); // 写入 16 位数据 FLASH_Lock();这段代码中的擦除地址 0x0800F000 是 STM32F103C8T6 的最后一个 Flash 页起始地址芯片内置 64KB Flash按 1KB 一页计算最后一页是第 63 页即 0x0800FC00 开始。不过如果项目中只用了默认启动配置不建议随意往 Flash 写数据代码放得不好会覆盖中断向量表导致程序跑飞。DS18B20 的代码实现里还有一个需要注意的参数放到了表格中参数推荐值说明上拉电阻4.7kΩ单总线空闲时拉高电平转换分辨率12 位转换时间约 750ms精度 0.0625°C读取间隔≥750ms转换未完成时读到的温度是旧值错误值0x8585°C初始化失败或供电不足时的默认值调试时如果读到固定 85°C优先检查 DQ 引脚的上拉电阻是否接好、GPIO 模式是否为开漏、延时函数是否准确。另外要注意DS18B20 的 ROM 码匹配在单设备场景下不需要做直接发跳过 ROM 指令 0xCC 就可以。如果总线上挂了多个 DS18B20 才开始需要寻址匹配这个项目只有一个传感器代码中不会出现DS18B20_SearchRom的逻辑。6. 把线性温控曲线做得更平滑限幅滤波与准 PID预期管理Auto 模式下让温度与 PWM 占空比呈线性关系理论上是 y kx b 的一次函数但在实际系统里直接套公式往往效果不理想。原因在于 DS18B20 的读数噪声和模数转换的量化噪声会叠加在控制信号上风扇转速会出现肉眼可见的抖动。一个低成本改进是给温度数据做限幅滤波当前温度与上次温度之差超过 2°C 才更新目标占空比否则沿用旧值。这样既保留了线性控制的连续性又抑制了微小波动导致的占空比频繁变化比单纯滑动平均更简单有效。uint16_t TempToDuty(int16_t temp) { static int16_t last_temp 25; int16_t delta temp - last_temp; if (delta 2 || delta -2) { last_temp temp; } if (last_temp 20) return 300; // 下限 20°C 对应 30% if (last_temp 38) return 999; // 上限 38°C 对应 100% // 线性插值20°C - 300, 38°C - 999 return 300 (last_temp - 20) * (999 - 300) / (38 - 20); }这段代码把温度下限 20°C 映射到 30% 占空比38°C 映射到 100% 占空比斜率为 38.8 CCR/°C。2°C 的限幅阈值需要根据你的温度传感器的实际噪声水平调整——如果 DS18B20 的读数稳定在 ±0.5°C 以内阈值设 2°C 足够如果散热环境导致温度波动剧烈可以适当放宽到 3°C。注意这个限幅是对温差做滤波不是对温度绝对值做处理所以初始上电时 last_temp 的赋值要合理否则第一帧输出可能跳变。进一步说原作者说并没有用 PID是个值得重视的预期管理。PID 的引入需要明确的控制目标——你要控的是转速恒定的闭环系统还是风扇跟随温度的开环系统如果是跟随温度根本不需要 PID线性映射本身就是正确的控制律。只有当你想让风扇转速不受电源电压波动和负载扰动影响、精确稳定在设定转速时才需要引入测速反馈和 PI 调节器。这个项目的价值恰恰在于它展示了一个不做过度设计的样本传感器读数、映射函数、PWM 输出三层逻辑清晰可追溯没有任何黑盒。验证线性映射是否合理的另一个技巧是打印拟合曲线。用串口以 1Hz 频率输出当前温度和占空比然后用串口助手的数据记录功能导出到 Excel画一张温度-占空比散点图。如果散点全部落在 y30038.8*(x-20) 的直线上说明映射逻辑正确如果出现阶梯状分布说明限幅滤波的阈值太大或者 DS18B20 的读数精度不够把分辨率从 12 位降低到 10 位转换时间 187.5ms会让问题更明显。这一步的结论会直接影响风扇是平滑升速还是一格一格跳变——对使用体验来说前者显然更自然。演示视频中展示的完整工作流程可以帮助你校准自己的预期手动模式下一二三档逐级提速时风扇声音有明显区别切到 Auto 模式后用热源靠近风扇会在几秒内自动加速移开热源后逐步回落。如果你复现的板子没有达到这种效果优先排查 PWM 频率是否因为时钟配置错误变成了 125Hz8MHz 内部时钟除以 64这个频率下风扇电机会发出明显的啸叫上位机逻辑完全正常但执行机构表现异常。把 SystemInit 配置好、用RCC_GetClocksFreq确认 SYSCLK 为 72MHz 后整个系统的表现会和预期一致。本文还有配套的精品资源点击获取