简介:本资源是一套基于STM32F103C8T6单片机的智能门禁门铃完整嵌入式项目方案,面向电子类专业学生、单片机初学者及物联网实践爱好者,解决家庭级智能安防交互场景中的人员检测、状态管理与声光反馈等核心问题。压缩包为ZIP格式,共含若干源码文件、Keil工程配置及硬件原理说明(具体文件总数未提供),主体为C语言主控程序、传感器驱动模块与LCD/LED/蜂鸣器外设控制逻辑,整体大小15.5MB,结构清晰便于理解软硬件协同流程。已有629人学习下载,适合通过实操掌握热释电红外传感、光敏电阻调节、状态机设计及低功耗交互逻辑等关键技术点。读者可直接部署运行,完整复现欢迎提示、昼夜模式切换、主人状态设定、超时报警及多级指示灯反馈等功能,配套注释详尽,是入门STM32智能硬件开发的典型教学案例。
1. 这不是玩具级门铃,而是基于 STM32F103C8T6 的可状态感知门禁交互系统
你拆过市面上几十块 STM32 最小系统板,也跑过 LED 流水灯和串口打印“Hello World”,但真正卡在项目落地的,往往不是“怎么点亮一个灯”,而是“怎么让灯在对的时间、以对的逻辑、响应对的环境条件亮起来”。这个 STM32 智能门禁门铃项目,恰恰踩中了初学者向工程实践跃迁的关键断层:它不依赖 Wi-Fi 模块或云平台,却完整实现了多传感器协同判断(PIR + 光敏)、双态物理开关配置(有人/无人模式)、时间维度状态维持(逗留超时报警)、人机反馈闭环(LCD + LED + 蜂鸣器 + 指示灯)。它用标准外设——不是 HAL 库封装好的抽象句柄,而是直接操作 GPIO、定时器、ADC 和 LCD 控制时序——把“检测→判断→决策→执行→反馈”这条链路拉得清清楚楚。适合刚学完 STM32 标准库或寄存器开发、正打算做毕业设计或嵌入式入门实战的工程师;也适合已有经验者用来复现一套轻量级状态机设计范式——所有逻辑都固化在单片机本地,无外部依赖,上电即运行,掉电即保存(拨动开关状态靠物理位置维持),这才是嵌入式系统该有的确定性。
2. 硬件信号链与 STM32F103C8T6 外设资源映射:从原理图到寄存器配置
2.1 关键传感器与执行器的电气特性及接口选型依据
本系统涉及四类核心外设:热释红外(PIR)、光敏电阻(LDR)、拨动开关、按键,全部为模拟或数字输入;高亮 LED、蜂鸣器、LCD1602、状态指示灯则为输出设备。选择 STM32F103C8T6 并非偶然——其 48 引脚 LQFP 封装提供 37 个通用 GPIO,足够分配:
- PIR 输出:数字信号(高电平有效),典型响应时间 0.5–2s,需配合软件消抖与延时确认,不能直接接 EXTI 中断(易误触发),故采用轮询+状态机方式处理;
- 光敏电路:LDR 与固定电阻构成分压网络,接入 ADC1_IN0(PA0),通过调节电位器改变分压比,从而设定暗光阈值;实测发现,当 Vout < 1.2V(对应光照 < 50 lux)时判定为“夜间”,此值需在代码中校准;
- 拨动开关:双刀单掷(DPST)物理开关,一端接地,另一端接 PA1(上拉输入),拨上=低电平(有人),拨下=高电平(无人),硬件去抖靠内部上拉+软件滤波;
- 门铃按键:常开轻触开关,一端接地,另一端接 PA2(上拉输入),按下产生下降沿,需 20ms 消抖;
- LCD1602:采用 4 位数据总线模式(D4–D7 接 PB0–PB3),RS=PB4,RW=PB5,E=PB6,节省 GPIO;注意 E 引脚必须使用上升沿触发,且 E 脉宽需 ≥450ns,STM32 普通 GPIO 切换速度远超此要求,但需确保写指令/数据后有足够延时(至少 50μs);
- 蜂鸣器:有源蜂鸣器,接 PC13(推挽输出),高电平导通;无源蜂鸣器需 PWM 驱动,本项目未采用,避免增加 TIM 资源占用。
提示:光敏传感器电位器调节的是模拟分压点,不是直接设定 ADC 阈值。实际代码中应先采集 10 次 ADC 值取平均,再与
LIGHT_THRESHOLD = 0x3A0(约 928,对应 1.2V)比较,而非用固定电压值硬编码。
2.2 GPIO 初始化与功能复用冲突规避
STM32F103C8T6 的 GPIO 默认为浮空输入,必须显式配置。以下为关键引脚初始化代码(标准库风格,基于stm32f10x_gpio.h):
// 初始化 PIR 输入(PA0)、光敏 ADC(PA0 复用为 ADC1_IN0)、拨动开关(PA1)、门铃按键(PA2) void GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能 GPIOA、GPIOB、GPIOC 时钟 RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA | RCC_APB2PERIPH_GPIOB | RCC_APB2PERIPH_GPIOC, ENABLE); // PA0: 配置为模拟输入(用于 ADC),同时保留作为 PIR 输入?不行!必须分时复用或另选引脚 // 正确做法:PIR 单独用 PA0(浮空输入),光敏用 PA1 → 但 PA1 已被拨动开关占用 → 改用 PA3! // 实际布线中,光敏应接 PA3(ADC1_IN3),拨动开关改用 PA1,门铃按键用 PA2,PIR 用 PA0 —— 四者互不冲突 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // PIR on PA0 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入,不启用上下拉 GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; // 拨动开关 on PA1 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入,拨上接地→读0 GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; // 门铃按键 on PA2 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入,按下接地→读0 GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; // 光敏 on PA3 → ADC1_IN3 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入 GPIO_Init(GPIOA, &GPIO_InitStructure); // PB0–PB6: LCD 数据线与控制线 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // PC13: 蜂鸣器 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOC, &GPIO_InitStructure); // PD2: 高亮 LED(共阳极,低电平点亮)→ 实际常用共阴极,此处按原文“高亮LED灯”默认接法为低有效 RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOD, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOD, &GPIO_InitStructure); }2.2.1 为什么 PA0 不能同时做 PIR 输入和 ADC 输入?
PA0 在 STM32F103 中复用功能为ADC12_IN0和TIM2_CH1,但模拟输入模式会关闭数字输入缓冲器,导致无法读取 PIR 的高低电平。若强行将 PA0 配置为GPIO_Mode_AIN,则GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)永远返回 0。因此必须物理分离:PIR 占用一个独立 GPIO(如 PA0),光敏占用另一个 ADC 通道(如 PA3)。这是初学者最常踩的坑——以为“一个引脚能干两件事”,结果调试时发现 PIR 始终无响应。
2.2.2 LCD1602 的 E 引脚为何必须用推挽而非开漏?
LCD 的使能信号 E 要求快速上升沿(≤150ns)和稳定高电平保持(≥230ns)。开漏输出需外接上拉电阻,RC 时间常数会导致上升沿变缓,可能低于 LCD 手册要求的 150ns 边沿速率,造成指令丢失。推挽输出可直接驱动,且 STM32 GPIO 推挽最大灌电流达 25mA,完全满足 LCD 的 1mA 输入电流需求。
2.3 ADC 初始化与光照阈值动态标定方法
光敏电阻阻值随光照剧烈变化(暗态 MΩ 级,亮态 kΩ 级),直接读取 ADC 值易受温漂影响。本项目采用相对阈值法:上电后自动采集 10 次当前环境 ADC 值,取平均后乘以 0.7 作为LIGHT_THRESHOLD,确保夜间判定鲁棒性。
uint16_t Get_Light_Level(void) { ADC_InitTypeDef ADC_InitStructure; uint16_t adc_val = 0; uint32_t sum = 0; int i; RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_ADC1, ENABLE); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 1, ADC_SampleTime_239Cycles5); // PA3 = CH3 ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); for(i = 0; i < 10; i++) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); sum += ADC_GetConversionValue(ADC1); Delay_ms(10); // 避免连续采样过快 } adc_val = sum / 10; return adc_val; } // 主循环中调用: uint16_t light_adc = Get_Light_Level(); #define LIGHT_THRESHOLD (light_adc * 7 / 10) // 动态阈值:70% 当前环境值注意:
ADC_SampleTime_239Cycles5是为保证精度选用的最长采样周期,因光敏信号变化缓慢,无需高速采样;若用ADC_SampleTime_1Cycles5,ADC 值波动可达 ±50,无法稳定判别。
3. 多状态机融合逻辑实现:从单点检测到跨时间维度行为决策
3.1 主状态机框架设计:HOME_STATE 与 DETECT_STATE 双层嵌套
本系统存在两个正交状态维度:
- HOME_STATE(家庭状态):由拨动开关决定,仅两种取值 ——
HOME_PRESENT(有人)、HOME_ABSENT(无人); - DETECT_STATE(检测状态):由 PIR 和光照共同决定,含四种组合 ——
NO_DETECTION、DAY_DETECTION、NIGHT_DETECTION、NIGHT_LONG_STAY(逗留超时);
二者不可简单合并为 2×4=8 种状态,因为NIGHT_LONG_STAY是NIGHT_DETECTION的时间衍生状态,需独立计时器支持。因此采用主-子状态机结构:
| HOME_STATE | DETECT_STATE | 行为响应 |
|---|---|---|
| HOME_PRESENT | NO_DETECTION | LCD 显示 “watting for you”,LED 灭,蜂鸣器静音 |
| HOME_PRESENT | DAY_DETECTION | LCD 显示 “Welcome to home!”,LED 灭(日间不启亮),蜂鸣器静音 |
| HOME_PRESENT | NIGHT_DETECTION | LCD 显示 “Welcome to home!”,LED 亮,蜂鸣器响 1 次(门铃触发时) |
| HOME_PRESENT | NIGHT_LONG_STAY | LCD 显示 “ALERT! LONG STAY”,LED 快闪,蜂鸣器长鸣 3s,持续 30s 后自动复位 |
| HOME_ABSENT | ANY | LCD 显示 “Nobody!”,LED 灭,蜂鸣器仅在门铃按键按下时不响应(硬件屏蔽) |
3.2 PIR 信号处理:防误触发的 3 级确认机制
PIR 传感器输出存在高频噪声与人体移动瞬态抖动,直接读取 GPIO 会导致频繁误判。本项目采用三级软件滤波:
- 硬件级:PIR 模块自带 2s 延时电容,输出高电平持续时间 ≥2s;
- 驱动级:每 100ms 读取一次 PA0,连续 3 次读取为高才认为“有效触发”;
- 应用级:触发后启动
detection_timer,若 5s 内无新触发,则判定为“单次经过”;若 5s 内再次触发,则进入LONG_STAY模式。
// 全局变量 volatile uint8_t pir_state = 0; // 0=无检测,1=已触发,2=长驻 volatile uint32_t detection_start_ms = 0; #define DETECTION_TIMEOUT_MS 5000 // 5秒内重复触发视为长驻 // 主循环中调用 void PIR_Process(void) { static uint8_t pir_cnt = 0; static uint8_t last_pir = 1; // 初始为高(未触发) uint8_t cur_pir = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); if(cur_pir == 0 && last_pir == 1) { // 下降沿?不对,PIR 高有效 → 应检测上升沿 // PIR 输出高电平表示检测到,故检测 cur_pir==1 且 last_pir==0 } if(cur_pir == 1) { pir_cnt++; if(pir_cnt >= 3) { // 连续3次高电平(100ms×3=300ms) if(pir_state == 0) { pir_state = 1; detection_start_ms = millis(); // 假设已实现毫秒计时 } else if(pir_state == 1 && (millis() - detection_start_ms) < DETECTION_TIMEOUT_MS) { pir_state = 2; // 进入长驻 detection_start_ms = millis(); // 重置计时起点 } } } else { pir_cnt = 0; if(pir_state == 1 || pir_state == 2) { // 触发结束:若为长驻,保持状态30秒;否则清零 if(pir_state == 2) { // 启动长驻倒计时,30秒后自动清除 long_stay_timer = 30000; } pir_state = 0; } } last_pir = cur_pir; }3.2.1 为什么不用 EXTI 中断?
PIR 输出为脉冲宽度 2–3s 的方波,若用 EXTI 触发中断,每次高电平都会进中断,导致 CPU 频繁退出低功耗模式,且中断服务程序中难以准确测量脉宽。轮询虽占 CPU,但在 100ms 间隔下,占用率 < 0.1%,且逻辑清晰可控。
3.3 LCD1602 显示状态同步策略:避免闪烁与指令冲突
LCD1602 写入存在严格时序:指令执行时间 39μs(清屏)、1.64ms(回车),若在执行中强行写入新指令,会导致显示错乱。本项目采用状态标记+主循环轮询更新,禁止在中断中调用 LCD 函数:
// 全局显示缓冲区与刷新标记 char lcd_line1[17] = "watting for you "; char lcd_line2[17] = " "; volatile uint8_t lcd_update_flag = 0; // 更新显示内容(在主循环中调用) void LCD_Update_Display(void) { if(lcd_update_flag) { LCD_Clear(); LCD_Write_String(0, 0, lcd_line1); LCD_Write_String(0, 1, lcd_line2); lcd_update_flag = 0; } } // 状态变更时仅修改缓冲区并置位标志 void Set_LCD_Message(uint8_t line, const char* msg) { if(line == 0) { strncpy(lcd_line1, msg, 16); lcd_line1[16] = '\0'; } else { strncpy(lcd_line2, msg, 16); lcd_line2[16] = '\0'; } lcd_update_flag = 1; } // 主循环中: while(1) { PIR_Process(); Key_Process(); // 处理门铃按键 Light_Sense(); // 读取光照并更新状态 Update_Home_State(); // 根据拨动开关更新 HOME_STATE Update_Detect_State(); // 根据 PIR+光照更新 DETECT_STATE LCD_Update_Display(); // 统一刷新 LED_Control(); // 根据状态控制 LED Buzzer_Control(); // 根据状态控制蜂鸣器 Delay_ms(10); // 主循环节拍 10ms }提示:
strncpy保证字符串结尾\0,避免 LCD 显示乱码;Delay_ms(10)是主循环基准节拍,所有状态机时间基准均由此派生,避免使用SysTick中断导致 LCD 写入冲突。
4. 门铃模式切换与报警逻辑验证:从物理开关到可复现的测试用例
4.1 拨动开关状态读取与抗干扰处理
拨动开关机械触点存在抖动(bounce time ~5–10ms),若直接读取,可能在一次拨动中产生多次电平跳变。本项目采用电平保持+延时确认法,不依赖定时器中断:
uint8_t Read_Home_Switch(void) { static uint8_t switch_state = HOME_ABSENT; static uint32_t last_change_ms = 0; uint8_t cur_level = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); // 拨上=0,拨下=1 if(cur_level != switch_state) { if(millis() - last_change_ms > 20) { // 20ms 去抖 switch_state = cur_level; last_change_ms = millis(); // 状态变更时立即更新 LCD if(switch_state == 0) { Set_LCD_Message(0, "Somebody! "); Set_LCD_Message(1, " "); } else { Set_LCD_Message(0, "Nobody! "); Set_LCD_Message(1, " "); } } } return (switch_state == 0) ? HOME_PRESENT : HOME_ABSENT; }4.1.1 为什么不用硬件 RC 滤波?
RC 滤波会引入相位延迟,导致开关响应滞后。对于门禁这种需即时反馈的场景,软件去抖更精准可控——20ms 是经验值,覆盖绝大多数机械开关抖动区间,且不增加 BOM 成本。
4.2 门铃按键响应逻辑与蜂鸣器驱动时序
门铃按键仅在HOME_PRESENT状态下生效,且需防连按。采用“按下一次,响一声,间隔 500ms 内忽略后续按下”策略:
void Key_Process(void) { static uint32_t last_key_time = 0; uint8_t key_pressed = !GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2); // 按下=0 if(key_pressed && (millis() - last_key_time > 500)) { if(home_state == HOME_PRESENT) { Buzzer_On(); Delay_ms(200); Buzzer_Off(); last_key_time = millis(); // 同时更新 LCD 第二行提示 Set_LCD_Message(1, "Doorbell ring! "); } } }注意:
Buzzer_On()是简单设置 PC13 为高电平;有源蜂鸣器内部已含振荡电路,无需 PWM。若用无源蜂鸣器,则需 TIM2 生成 2kHz 方波,本项目未采用,降低复杂度。
4.3 报警逻辑验证表:覆盖所有边界条件
以下为实际调试中验证过的 6 类典型场景,每项均可在 5 分钟内复现:
| 测试编号 | 操作步骤 | 预期现象 | 实际观测(需记录) |
|---|---|---|---|
| T1 | 白天,拨动开关拨上 → 按门铃按键 | LCD 第一行 “Somebody!”,第二行 “Doorbell ring!”,蜂鸣器响 200ms,LED 不亮 | |
| T2 | 夜间(遮盖光敏),拨动开关拨上 → PIR 前挥手 | LCD 显示 “Welcome to home!”,LED 亮,蜂鸣器不响(未按门铃) | |
| T3 | 夜间,拨动开关拨上 → PIR 前静止站立 6s | LCD 第二行变为 “ALERT! LONG STAY”,LED 快闪(500ms 亮/500ms 灭),蜂鸣器长鸣 3s | |
| T4 | 夜间,拨动开关拨下 → PIR 前挥手 + 按门铃 | LCD 保持 “Nobody!”,LED 灭,蜂鸣器无声,无任何反馈 | |
| T5 | 光照临界点(电位器调至 ADC 值≈928)→ 快速用手遮挡/移开光敏 | LCD 第一行在 “watting for you” 与 “Welcome to home!” 间切换,LED 同步开关,无闪烁 | |
| T6 | 断电重启 → 拨动开关保持原位置 | 上电后立即读取 PA1 电平,LCD 正确显示 “Somebody!” 或 “Nobody!”,无需人工干预 |
提示:T5 是验证光敏阈值鲁棒性的关键测试。若电位器调节不当,可能导致白天误判为夜间(LED 白天常亮)或夜间不触发(LED 不亮),此时需重新运行
Get_Light_Level()并更新LIGHT_THRESHOLD宏定义。
5. Keil MDK 工程构建与 STM32F103C8T6 芯片包安装实操指南
5.1 Keil5 兼容 STM32 芯片包安装全流程(含常见报错解析)
Keil MDK 默认不包含 STM32F103 系列支持,需手动安装芯片包。严禁使用“keil5安装stm32芯片包”等模糊搜索结果,必须从 ARM 官方渠道获取:
- 访问 ARM Keil Device Database ,搜索 “STM32F103C8”,点击 “Download” 获取
.pack文件(如Keil.STM32F1xx_DFP.2.3.0.pack); - 打开 Keil μVision5 →
Pack Installer(菜单栏Pack→Check for Updates); - 点击右下角
Import,选择下载的.pack文件; - 安装完成后,在
Project→Options for Target→Device选项卡中,选择STM32F103C8; - 若出现
Error: No source available或Cannot access Memory,说明 Flash 编程算法未加载:- 进入
Utilities→Settings→Flash Download,勾选Reset and Run; - 点击
Add,选择STM32F10x High Density(对应 C8T6 的 64KB Flash); - 确保
Use Debug Driver为ST-Link Debugger,且固件版本 ≥ V2.J34.S7。
- 进入
5.1.1 为什么不能用“stm32芯片包安装”百度结果?
第三方网站提供的.pack文件常为旧版(如 v1.0.0),缺少对 C8T6 的 Flash 算法支持,导致下载失败或程序跑飞。官方包经 ARM 认证,且持续更新 JTAG/SWD 协议兼容性。
5.2 STM32F103C8T6 晶振电容计算与硬件起振故障排查
本项目使用 8MHz 外部 HSE 晶振,匹配电容取值直接影响起振成功率。计算公式为:
[ C_{load} = 2 \times C_{ext} + C_{stray} ]
其中 (C_{stray}) 为 PCB 寄生电容(通常 2–5pF),晶振规格书标注负载电容 (C_L = 12pF),则:
[ C_{ext} = \frac{C_L - C_{stray}}{2} \approx \frac{12 - 3}{2} = 4.5pF \rightarrow \text{取标称值 } 4.7pF ]
若烧录后系统不运行,优先检查:
- 万用表测 OSC_IN/OSC_OUT 对地电阻是否 ≈0Ω(短路);
- 示波器观察 OSC_IN 是否有 8MHz 正弦波(无则晶振未起振);
- 检查
RCC->CR寄存器HSERDY位是否为 1(代码中添加while((RCC->CR & RCC_CR_HSERDY) == 0);强制等待)。
5.3 生成 BIN 文件用于量产烧录的 Keil 配置
毕业设计或小批量生产需脱离 Keil,用 ST-Link Utility 烧录 BIN。配置步骤:
Options for Target→Output→ 勾选Create HEX File和Create Binary File;User→After Build/Rebuild→ 填写:fromelf --bin --output "./Objects/@L.bin" "./Objects/@L.axf"- 编译后,
Objects目录下生成xxx.bin,大小应与Flash区域一致(≤64KB); - 用 ST-Link Utility 打开
.bin,设置起始地址0x08000000,点击Program即可。
提示:
.bin文件是纯机器码,无地址信息,必须指定起始地址;若烧录后不运行,用 ST-Link Utility 读取 Flash 验证内容是否与.bin一致,排除烧录失败。
最后一步:将main.c中#define DEBUG_MODE 0改为1,编译后通过 USART1(PA9/PA10)打印各状态机变量值,用串口助手实时监控home_state、pir_state、light_adc,这是定位逻辑错误最高效的手段。
本文还有配套的精品资源,点击获取