51单片机实验例程:汇编/C对照与Keil调试避坑指南
简介《单片机实验.zip》是一份面向51单片机初学者的上机练习包适合电子类学生、嵌入式入门者通过汇编与C语言对照实验理解8051的硬件结构与编程方法。压缩包共28个文件以18个C源文件、9个汇编源文件为主另附1份docx说明整体约28KB体量轻便便于在Keil等环境中直接打开、编译和调试。内容围绕LED点亮、按键控继电器、外部中断、定时器、串行通信收发、数码管静态与动态显示等常见实验展开汇编代码可帮助观察指令级操作C代码则便于理解模块化写法docx文档可辅助梳理实验要求与流程。已有149人学习适合课堂实验、自学练手或课程设计前期打基础也能为后续嵌入式开发积累调试经验。1. 实验室里最常见的一幕板子插上电灯死活不亮Keil 里编译却是 0 Error 0 Warning51 单片机的上机实验就是这么个东西——语法一百分现象零分。这份《单片机实验.zip》收的正是从汇编到 C 语言的整套上机例程覆盖 GPIO、定时器、中断、数码管、矩阵键盘、串口这些必做题目每个实验都能单独编译、单独烧录也能直接拿来当二次开发的骨架。它适合两类人刚上手 51、需要照着现象反复核对代码的新手以及写过不少上层业务、想把机器周期和时序重新捋一遍的开发者。汇编和 C 双份对照着看是这份资源里最值钱的地方——同一件事两种写法摆在一起中间差的那几十个机器周期就是玄学出现的地方。2. 实验包的结构一份 51 上机资源通常怎么组织目录拿到压缩包先别急着双击工程文件先看目录。我拆过的这类实验包组织逻辑基本都是「按实验编号横切、按语言纵切」横切保证每个实验独立可跑纵切保证汇编和 C 能一一对照。目录结构一旦看明白你想找哪个实验、想改哪个外设三十秒内就能定位到文件而不是在几十个 main.c 里逐个打开猜。2.1 目录骨架汇编、C、硬件资料、实验指导四块一个合格的实验包目录形状大概是下面这样。不同资源里文件夹名字会变但这四块内容基本跑不掉。单片机实验/ ├── 01_ASM例程/ # 汇编版按实验编号排列 │ ├── EXP01_LED/ │ │ ├── EXP01.asm # 源文件A51 汇编 │ │ ├── EXP01.uvproj # Keil 工程文件 │ │ └── EXP01.hex # 编译产物烧录用 │ ├── EXP02_TIMER/ │ └── EXP03_UART/ ├── 02_C例程/ # C 版与汇编目录一一对应 │ ├── EXP01_LED/ │ │ ├── main.c │ │ ├── STARTUP.A51 # C 工程启动文件必须加进工程 │ │ └── EXP01.uvproj │ ├── EXP02_TIMER/ │ └── EXP03_UART/ ├── 03_硬件资料/ │ ├── 开发板原理图.pdf # 关键查引脚到底接在哪 │ └── 器件手册/ ├── 04_实验指导/ │ └── 实验一 流水灯.md └── README.txt01_ASM例程和02_C例程的编号是对齐的EXP01 在两边都是流水灯EXP02 都是定时器。这个对齐很关键你在 C 里写不出效果可以直接抄汇编版的时序去比对看是逻辑错了还是延时不准。03_硬件资料里的原理图是最容易被忽略、也最容易救命的东西。同一个叫 P1 的口这块板子接的是 LED换块板子接的可能是矩阵键盘的行线。代码没错、现象不对九成是引脚定义和原理图对不上。2.2 汇编和 C 各留一份的实际价值很多人觉得汇编那份是历史遗留其实不是。汇编版能让你看到每条指令占几个机器周期DJNZ、MOV、LCALL这些指令在 12MHz 晶振下的耗时是可以精确数出来的C 版则胜在可读性和可移植性改个外设只要换个头文件和引脚定义。我一般的做法是做逻辑用 C对时序有疑问的时候回去翻汇编。比如串口波特率算出来有误差、数码管扫描出现闪烁这类问题在 C 里被编译器优化糊住看汇编生成的.lst列表文件反而更清楚——.lst会把每一行 C 代码对应的汇编指令全部列出来等于自带一个黑匣子。顺带提一句C 工程的STARTUP.A51别删。这个文件负责在main()之前清内部 RAM、初始化堆栈指针删了之后变量初值全是随机的调试起来能让人怀疑人生。2.3 工程文件、HEX 和 LST 各管什么一个实验文件夹里通常有四五种文件职责分得很清文件作用什么时候需要动.uvproj/.uvprojxKeil 工程记录器件型号、源文件列表、编译选项换器件、加源文件时必须改.asm/.c源码天天改.hex编译产物烧录工具真正吃进去的东西每次改完代码重新生成.lst汇编列表C 代码与汇编的对照表查时序、查编译器优化时看.A51启动代码一般不动但必须存在于工程里注意.hex是编译产物不是源码。有些包里直接带了旧的 hex你烧进去发现现象和代码对不上八成是烧了别人之前编好的版本。养成习惯——改完代码先 Build看 Build Output 窗口确认重新生成了 hex再打开烧录工具。这一步省不得我见过太多次「代码明明改了但板子没反应」最后发现烧的是两小时前的 hex。3. 跑起来Keil 工程配置、最小系统和第一个流水灯这一章的目标很明确——让板子上第一个 LED 按你的意愿亮起来。别小看这一步从装工具链到看见灯闪中间有四五个地方能卡住人而且卡住的时候报错信息往往什么也不说。3.1 工具链三件套和最小系统检查工具链就三样Keil C51uVision 4 或 5 都行关键是装了 C51 编译器而不是 MDK 的 ARM 编译器、烧录软件按你的芯片型号选、一根下载线。器件型号在 Keil 里选Atmel - AT89C52或AT89S52就够即便你板子上焊的是 STC 的兼容型号——生成 hex 的原理是一样的具体烧录交给烧录软件去识别。烧之前先肉眼过一遍最小系统这四个地方任何一处不对后面全白搭晶振两个脚各接一个 20~30pF 电容到地晶振本体紧贴芯片。复位脚接 10uF 电容到 VCC、10k 电阻到 GND构成上电复位。EA脚必须接 VCC否则芯片从外部存储器取指令内部程序根本不执行。P0 口做输出时要有 10k 上拉排阻P0 是开漏结构没有上拉出不来高电平。第四条是血泪经验。P1、P2、P3 都有内部上拉唯独 P0 没有很多板子把 LED 接在 P0 上结果 C 代码写P0 0xFF灯不灭还以为是代码问题。3.2 建工程时必须改的四个选项新建工程Project → New uVision Project、选好器件、把源文件加进 Source Group然后一定要打开 Options for Target 改这四项Output 页勾选 Create HEX File。不勾就永远没有 hex编译再成功也烧不进去。这是新手第一大坑。Target 页的 Xtal 填实际晶振频率11.0592MHz 或 12MHz。它不影响生成的代码时序但影响软件仿真时的计时填错了仿真出来的波形全是错的。C51 页的 Optimization 先设成 Level 0。调试阶段关掉优化单步执行时变量值和代码行才对得上等逻辑稳定了再开到 Level 8 压体积。汇编工程不需要STARTUP.A51但源文件后缀必须是.asmKeil 会自动调 A51 汇编器C 工程必须把STARTUP.A51加进去。改完编译一次Build Output 窗口出现0 Error(s), 0 Warning(s)并且提示creating hex file...才算这一步过了。3.3 第一个实验汇编版流水灯汇编版的好处是把「端口输出」这件事剥得干干净净没有任何库函数挡在中间。ORG 0000H ; 复位后从这里开始执行 LJMP MAIN ; 跳过中断向量区 ORG 0030H ; 主程序放到向量区之后 MAIN: MOV A, #0FEH ; 1111 1110B最低位为 0对应 LED 点亮 LOOP: MOV P1, A ; 送到 P1 口低电平点亮共阳接法 LCALL DELAY ; 调用延时让人眼看到流水 RL A ; 累加器循环左移那个 0 就往高位走 SJMP LOOP ; 死循环单片机没有操作系统 DELAY: MOV R7, #200 ; 外层 200 次 D1: MOV R6, #250 ; 内层 250 次 D2: DJNZ R6, D2 ; R6 减 1 不为 0 就跳回去 DJNZ R7, D1 RET END逐段说逻辑ORG 0000H加LJMP MAIN是 51 的固定起手式0000H 到 002FH 是中断向量区主程序必须跳过去否则你写了中断之后会互相踩。MOV P1, A一次写整个端口比逐位操作快得多。RL A是循环左移0FEH左移一次变0FDH再移变0FBH那个低电平位就沿着 P1.0 到 P1.7 跑一圈再回到起点形成循环。延时子程序是纯软件延时两层DJNZ嵌套。时间怎么算DJNZ是双周期指令内层 250 次约 500 个机器周期外层 200 次合计约 10 万个机器周期。12MHz 晶振下 1 个机器周期 1 微秒所以大约 0.1 秒。要调快调慢改R7的值就行R7加大变慢减小变快。3.4 第二个实验C 版流水灯和软件延时同一件事用 C 写代码短一半但有两处必须注意。#include reg52.h /* 51 特殊功能寄存器定义P1、TMOD 这些都在里面 */ #include intrins.h /* _crol_ 循环左移函数在这个头文件里 */ #define LED_PORT P1 /* 换板子只改这一行别到处找 P1 */ /* 11.0592MHz 下的粗略毫秒延时110 是凑出来的经验系数 */ void delay_ms(unsigned int ms) { unsigned int i, j; for (i ms; i 0; i--) for (j 110; j 0; j--); } void main(void) { unsigned char led 0xFE; /* 初始状态最低位点亮 */ while (1) { /* 单片机主循环永不返回 */ LED_PORT led; delay_ms(300); /* 300ms 换一次太快人眼看不出来 */ led _crol_(led, 1); /* 循环左移一位等价于汇编的 RL A */ } }逻辑说明reg52.h把 P0~P3、TMOD、SCON 这些地址用sfr定义了不 include 直接写P1 0xFE会报未定义。_crol_是 intrins.h 里的循环移位内联函数和汇编的RL A完全等价注意它不是——用移位八次之后变量会变成 0灯就全灭了。参数说明delay_ms的形参是毫秒数但它是软件延时不精确。系数 110 是在 11.0592MHz、优化等级 0 的条件下实测凑出来的换晶振、换优化等级、换编译器版本都会漂。想要准的定时就得上定时器软件延时只适合点灯、按键消抖这种场合。3.5 烧录和现象验证编译出 hex 之后打开烧录软件选对芯片型号和串口号加载 hex点下载然后给板子重新上电很多芯片需要冷启动才会进入下载模式。烧录成功会回显一串校验信息。验证顺序建议这样走先确认电源指示灯亮再确认你改的那个 LED 有反应最后才去调延时长短。如果灯全亮或者全灭不动先别改代码——拔掉下载线单独供电试一次很多问题是下载器占着串口或者板子供电不足。4. 核心模块拆解定时器、中断、数码管、键盘、串口流水灯只能证明工具链通了。真正撑起 51 上机实验的是这五块内容而且它们互相咬合——数码管靠定时器刷新键盘靠中断或者扫描串口靠定时器做波特率发生器。把下面几段代码理解透实验包里的其他例程基本都是这些的组合变形。4.1 定时器0方式1 的初值计算和手动重装定时器的核心就一个公式计数值 65536 − 定时时间 / 机器周期。12MHz 晶振下机器周期 1 微秒要 50ms 就是 50000 个计数65536 − 50000 15536换成十六进制是0x3CB0所以TH0 0x3C、TL0 0xB0。#include reg52.h sbit LED P1^0; unsigned char tick 0; /* 50ms 计数20 次凑 1 秒 */ void timer0_init(void) { TMOD 0xF0; /* 先清掉低 4 位别影响定时器 1 */ TMOD | 0x01; /* 定时器 0方式 116 位定时不自动重装 */ TH0 0x3C; /* 50ms 高 8 位 */ TL0 0xB0; /* 50ms 低 8 位 */ ET0 1; /* 允许定时器 0 中断 */ EA 1; /* 开总中断少了这行中断永远不进 */ TR0 1; /* 启动计数 */ } void timer0_isr(void) interrupt 1 { TH0 0x3C; /* 方式 1 不自动重装必须手动补初值 */ TL0 0xB0; if (tick 20) { /* 20 × 50ms 1s */ tick 0; LED ~LED; /* 取反1Hz 闪烁 */ } } void main(void) { timer0_init(); while (1); /* 主循环空转活都让中断干 */ }逻辑说明TMOD 0xF0这一步很多人省掉直接TMOD 0x01结果串口用的定时器 1 配置被一起清掉了。后面TMOD | 0x20配串口时现象就变得莫名其妙。先与后或是这种共享寄存器操作的标准写法。interrupt 1是定时器 0 的中断号51 的中断号是固定的外部中断 0 是 0、定时器 0 是 1、外部中断 1 是 2、定时器 1 是 3、串口是 4。写错编号编译器不报错但中断永远进不去属于典型的静默失败。注意方式 1 是 16 位不自动重装进一次中断就得补一次TH0/TL0中间还夹着中断响应延迟所以实际周期会比理论值略长几个机器周期。要更准就用方式 28 位自动重装代价是只能定 256 个机器周期需要更大的分频得自己在外层计数。4.2 中断服务函数写短、别阻塞、别忘清标志中断函数有三个铁律执行时间越短越好、不要在里面做长延时、用到的标志位记得清。原因很实在——51 只有两级中断优先级中断里再延时其他中断全被挂起串口收到的数据会因为来不及读 SBUF 而丢失。/* 外部中断 0按键接 P3.2下降沿触发 */ void ext0_isr(void) interrupt 0 { static unsigned char cnt 0; cnt; if (cnt 9) cnt 0; /* 只改一个变量显示刷新交给主循环或者定时器 */ } void ext0_init(void) { IT0 1; /* 下降沿触发0 是低电平触发会反复进 */ EX0 1; /* 允许外部中断 0 */ EA 1; }参数说明IT0 0是低电平触发按键按住不放会不停地进中断现象是「按一下加了十几次」改成IT0 1下降沿触发一次按下只进一次。这一位设错新手能调一下午。还有一个容易漏的点中断函数不能用return带返回值也不能被主程序直接调用声明时要加interrupt n。Keil 会自动在函数出入口压栈出栈但只保护它认为需要的寄存器所以中断里尽量别写太复杂。4.3 数码管动态扫描段码、位选和消隐动态扫描的本质是「骗眼睛」一次只点亮一位靠切换速度让人以为是同时亮的。扫描频率低于 50Hz 会看到闪烁高于 200Hz 亮度又不够一般定在 1~2ms 切换一次。#include reg52.h /* 共阴数码管 0~9 段码位序 dp g f e d c b a */ code unsigned char SEG_CODE[10] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; sbit DIG1 P2^0; /* 位选低电平选中按原理图改 */ sbit DIG2 P2^1; unsigned char disp_buf[2] {0, 0}; /* 要显示的两个数字 */ /* 放在定时器中断里每 2ms 调一次 */ void display_scan(void) { static unsigned char idx 0; P0 0x00; /* 先消隐段码全灭 */ DIG1 1; /* 位选全关 */ DIG2 1; if (idx 0) { P0 SEG_CODE[disp_buf[0]]; DIG1 0; /* 开第一位 */ } else { P0 SEG_CODE[disp_buf[1]]; DIG2 0; /* 开第二位 */ } idx ^ 1; /* 0/1 交替 */ }逻辑说明先P0 0x00消隐再开位选这个顺序不能反。如果先开位选再换段码切换的那一瞬间上一位的数字会短暂出现在这一位上看起来就是重影。这个细节几乎所有教程都提但真到自己焊板子写代码的时候忘了写消隐的居多。参数说明SEG_CODE前面的code关键字把数组放到程序存储器Flash里不占宝贵的 128 字节内部 RAM。51 的 RAM 小得可怜一个 10 字节数组不算多但项目一大就捉襟见肘。共阳数码管的段码要把这些值全部取反0x3F变0xC0。disp_buf是显示缓冲主程序只管往里写数字扫描函数只负责输出两边通过这个数组解耦不会互相干扰。4.4 矩阵键盘行扫描、消抖、等松手4×4 矩阵键盘 16 个键只占 8 个 IO。扫描思路是「逐行拉低、读列线」哪一列读到 0 就说明哪一列的键被按下。unsigned char key_scan(void) { unsigned char row, col; P1 0xF0; /* 行线(P1.4~P1.7)输出 0列线(P1.0~P1.3)读入 */ if ((P1 0x0F) 0x0F) return 0xFF; /* 列线全高没有键按下 */ delay_ms(10); /* 消抖机械触点抖动 5~20ms */ if ((P1 0x0F) 0x0F) return 0xFF; /* 10ms 后还在才算真按下 */ for (row 0; row 4; row) { P1 ~(0x10 row); /* 第 row 行拉低其余行拉高 */ for (col 0; col 4; col) { if (((P1 col) 0x01) 0) { while (((P1 col) 0x01) 0); /* 等松手防止连击 */ return row * 4 col; /* 返回 0~15 键值 */ } } } return 0xFF; /* 没扫到返回无效键值 */ }逻辑说明0x10 row依次得到0x10、0x20、0x40、0x80取反后就是「只有这一位是 0」。这个技巧比 case 分支干净得多也省代码空间。参数说明返回0xFF表示无键按下调用方要先判断再处理。row * 4 col的映射关系要和你的硬件接线一致——如果键盘是行列反接的键值会错位但不会报错只能靠自己按一遍逐个核对。那个while死等松手简单但危险如果按键卡住或者接触不良程序就永远停在这儿。工程上更稳的做法是把扫描放进定时器中断用状态机做「按下—确认—松手」三态主循环完全不阻塞。实验阶段先用这种写法把现象跑出来后面再改成状态机。4.5 串口为什么教程都让你用 11.0592MHz 晶振波特率公式是TH1 256 − (2^SMOD × fosc) / (12 × 32 × baud)。代入 11.0592MHz、9600、SMOD011059200 / 384 / 9600 3正好整除TH1 253 0xFD误差 0%。换成 12MHz 算出来是 2.9只能取 3实际波特率变成 8928误差 7%——超过串口能容忍的 2%~3%收数据必乱码。void uart_init(void) { TMOD 0x0F; TMOD | 0x20; /* 定时器 1方式 28 位自动重装做波特率发生器 */ TH1 0xFD; /* 11.0592MHzSMOD09600bps误差 0 */ TL1 0xFD; TR1 1; SCON 0x50; /* 方式 18 位 UARTREN1 允许接收 */ EA 1; ES 1; /* 串口中断使能 */ } void uart_send_byte(unsigned char dat) { SBUF dat; /* 写 SBUF 就触发发送 */ while (TI 0); /* 等硬件把 TI 置 1别用延时凑 */ TI 0; /* TI 必须软件清零 */ } void uart_isr(void) interrupt 4 { unsigned char dat; if (RI) { RI 0; /* 读走 SBUF 后清接收标志 */ dat SBUF; SBUF dat; /* 直接回填硬件自动开始发实现回显 */ } if (TI) { TI 0; /* 发送完成标志不清中断会一直重进 */ } }逻辑说明SBUF是两个物理上独立的寄存器共用一个地址写的时候进发送缓冲读的时候出接收缓冲硬件自己区分不用你操心。TI和RI都是硬件置位、软件清零这一点和定时器的TF0一样忘了清就等着中断风暴。参数说明SCON 0x50拆开看是0101 0000高两位01表示方式 18 位数据、1 位停止位、无校验REN位bit4为 1 表示允许接收。如果要 8 位数据加校验位就用方式 3。SMOD 位在PCON的最高位置 1 可以把波特率翻倍一般不需要动。5. 排查与避坑51 上机最容易翻车的五个现场调试 51 有个特点——大部分错误不会给你任何提示编译通过、烧录成功、板子就是不动。这种时候靠猜没用得有一套固定的排查顺序。下面五条是我自己踩过、也在别人板子上反复见到的。5.1 现象编译 0 Error烧录也提示成功LED 就是不亮原因三种可能按概率排——Options for Target 里的 Create HEX File 没勾烧的是上一次的旧 hexEA脚没接高电平芯片压根没执行内部程序P0 口没接上拉排阻输出不了高电平。解决先看实验文件夹里的 hex 修改时间和源文件改完的时间对不上就重新 Build 一次。再用万用表量EA脚对地电压不是接近 VCC 就是硬件问题。最后确认 LED 接在哪个口P0 必须加上拉。三步走完九成的「不亮」都能定位到。5.2 现象定时器中断只进一次之后程序像死了一样原因定时器工作在方式 116 位不自动重装中断函数里没有重新给TH0/TL0赋初值。计数器从中断点继续往上数到 65536 溢出这一趟可能要走几十毫秒甚至几秒看起来就像卡死。解决在中断函数第一行补上TH0和TL0。更好的办法是改用方式 28 位自动重装硬件自动把TH0的值重装进TL0代码里一行都不用写。代价是定时上限只有 256 个机器周期长定时得靠软件计数。5.3 现象数码管显示发虚、有重影或者亮度忽明忽暗原因换位之前没有消隐上一位的段码还挂在 P0 上或者扫描间隔不均匀——主循环里夹了别的耗时操作导致某一位停留时间明显长于其他位。解决扫描函数开头先P0 0x00关段码、位选全置高再开下一位。扫描必须放进定时器中断固定 1~2ms 一次不要放在主循环里——主循环一旦有延时函数刷新率立刻掉下来。5.4 现象串口助手收到的全是乱码或者干脆收不到原因波特率不匹配。要么晶振不是 11.0592MHz 导致 TH1 算出来有误差要么SMOD位和公式里的2^SMOD没对上要么串口助手的波特率和代码里不一致。解决先确认板子上的晶振丝印是 12.000 还是 11.0592。12MHz 就老老实实把波特率降到 4800 或者 2400误差能压到可接受范围。然后按公式重算 TH111.0592MHz 9600 就是0xFD这个组合是教科书级的稳定。最后检查串口助手数据位 8、停止位 1、无校验、波特率 9600。5.5 现象按一下按键程序里加了三四次原因机械按键触点在闭合瞬间会抖动 5~20msCPU 在这个窗口里读到的电平是乱的或者中断设成了低电平触发IT0 0按住不放就会反复进中断。解决软件消抖——读到按下后延时 10ms 再读一次两次一致才认。中断触发方式改成下降沿IT0 1。另外记得加松手检测否则长按会被当成连续按键。提示调试这类「现象和代码对不上」的问题最有效的工具不是改代码是串口。在关键分支里往串口打一行标记看看代码到底走到哪儿了比盯着板子猜快十倍。6. 二次开发把这套实验包改成自己的外设模板库实验包跑通之后多数人的下一步是接自己的项目。我的一般做法不是直接在例程上改而是抽一层出来——把延时、数码管、按键、串口这几个高频模块做成bsp.c/bsp.h实验例程只保留业务逻辑。这么做的好处是换块板子只改 bsp 里的引脚定义上层代码一行不动。/* bsp.h所有外设的引脚定义集中在这一处 */ #ifndef __BSP_H__ #define __BSP_H__ #include reg52.h #define LED_PORT P1 #define SEG_PORT P0 sbit DIG1 P2^0; sbit DIG2 P2^1; sbit KEY_UP P3^2; void bsp_init(void); void delay_ms(unsigned int ms); void display_scan(void); /* 放定时器中断里2ms 一次 */ unsigned char key_scan(void); #endif抽取的粒度按「换硬件要不要动」来判断引脚定义、初始化顺序、时序参数这些换板子就要改的进 bsp业务逻辑比如「按一下切换显示模式」留在 main 里。改造前后可以用一个笨办法验证——把 bsp.h 里的引脚全部换一组重新编译烧录如果除显示和按键之外其他代码一行没改就跑起来了说明分层是干净的。几个实验的可改造方向供参考实验可复用模块改造方向流水灯led_ctrl()换成 PWM 调光做呼吸灯效果定时器timer0_init()抽出timer0_set_us()参数化定时长度数码管display_scan()disp_buf增加符号位和小数点处理做成通用显示层矩阵键盘key_scan()改成状态机 定时器扫描彻底去掉阻塞串口uart_init()/uart_send_byte()加环形缓冲区收发都不阻塞主循环还有一个我个人很在意的点汇编那部分别急着删。当你改完 C 代码发现某个引脚上的波形时序不对最快的定位办法是把.lst文件打开看编译器把那几行 C 生成成了什么指令数一数机器周期。没有这个对照你只能靠改参数试试十次也未必试到根上。从那以后我每次动 51 的定时和串口都强制走一遍「算初值 → 看 lst 核对周期 → 上板测量」这三步再急也不跳。省下来的调试时间比省下的这三分钟多得多。希望帮到你。本文还有配套的精品资源点击获取