简介:面向已有单片机与RTOS基础、希望系统掌握Modbus通信的嵌入式开发者,这份PDF教程围绕FreeModbus协议栈展开,重点解决从机开源而主机源码收费、优质开源主机协议栈难寻的问题。内容结合STM32H743与FreeRTOS的移植实践,讲解主站与从站保持统一风格和接口、主从同栈运行、阻塞与非阻塞请求模式及自定义超时等调用方式,并覆盖常用Modbus方法。资料还配有主机使用说明、主从流程图、协议文档整理、工程文件结构图以及调试工具说明,便于对照理解协议栈分层与移植要点。包内共1个PDF文件,约120KB,以文档形式汇总教程与资料索引,轻量便于查阅。已有505人学习,适合需要快速建立FreeModbus整体认识并落地到STM32+FreeRTOS项目的读者。
1. 从一根 RS485 线说起:FreeModbus 到底是个什么东西
很多新手第一次听到 Modbus,是在一块 STM32 开发板上:两根线接出去,一头是自己刚烧好的程序,另一头是不知道型号的仪表,然后就卡住了——谁先说话?发出去那串十六进制是啥意思?其实你可以把它想象成对讲机:一条线上同时只允许一个人讲,讲完必须说“完毕”,下一个人才开口。Modbus 就是这套“谁先说、说什么、说完怎么回”的规矩,而 FreeModbus 就是把规矩写好的现成代码。这份资源提供的是 FreeModbus 在 STM32 + FreeRTOS 上的主站、从站例程,注释写得细,最难的“主站”部分也补齐了。哪怕你连寄存器是什么都不知道,也能照着跑出第一帧数据,这是它最值钱的地方。
2. 为什么零基础也值得学:三个实在的理由
挑技术方向这件事,最怕学完发现用不上。Modbus 属于那种“你以后一定会碰到”的东西:只要你继续做嵌入式、做设备、做数据采集,早晚要跟它打交道。下面三个理由,是我建议新手从这一份入手,而不是从别的协议入手的原因。
2.1 它是设备之间最通用的“普通话”
工业现场的仪表、变频器、电表、温控器、PLC,几乎都支持 Modbus。原因很简单——这套规矩定得早、足够简单、够用,于是被大量设备沿用下来。这意味着会了它,你能看懂一大半现场设备的通讯手册:上面写着“支持 03 功能码,读保持寄存器 40001~40010,9600 8N1”,你立刻知道该发什么、收什么。
对零基础的人来说,这一点特别重要。学一个“只有某一家在用”的私有协议,学完就锁死在某个产品里;学 Modbus,等于给自己开了一把通用钥匙。而且它足够小——帧格式一共就那么几个字段,没有加密握手,没有复杂的会话管理,一个下午能弄明白结构,这对新手非常友好。
2.2 从机侧开源代码多,主机侧一直是坎
FreeModbus 本身是一款开源的 Modbus 协议栈,但原版有一个明显的缺口:从机(等着别人来问的那一方)源码开源,主机(主动去问别人的那一方)源码是要收费的。网上去找开源的主机实现,能直接拿来用的也不多,要么只有半截,要么风格跟从机完全不搭,接进来像两种语言。
这份资源的作者就是被这个缺口逼出来的——在实际工作里需要在 STM32H743 + FreeRTOS 上跑主机,找不到顺手的,就自己把主机模式补上了。补的时候刻意做了一件事:新增的主机源码,跟原有从机的风格和接口保持一致。听起来像小事,对新手却是大事:你在从机例程里学会的那套回调写法,换到主机能直接复用,不用推翻重学。
它还支持主机和从机在同一个协议栈里同时运行,支持 FreeRTOS,为应用提供阻塞和非阻塞两种请求模式,超时时间可以自己定。这些词现在看着抽象,等你在第 4 章跑第一个例子的时候,就明白它们解决的是什么问题了。
2.3 注释详尽的例程,等于有人陪你读代码
新手看开源代码最痛苦的不是看不懂语法,而是看着看着不知道这一段是干嘛的。一堆if判断、一堆宏定义,读三行就迷失了。注释详尽的例程能救你:它会告诉你“这里是算超时”“这里是校验 CRC”“这里是把数据搬到应用缓冲区”。
这份资源的例程是 STM32 + FreeRTOS 的完整 DEMO,不是伪代码、不是片段。完整的工程意味着你能编译、能烧录、能看现象,出了问题也能对比——这一点比读十篇原理文章都管用。我在带新人的时候一直强调一个习惯:先跑通,再读懂,最后才改。跑不通的代码,读一百遍都是玄学;跑通了再回头看,很多疑惑自己就消了。这份例程的价值就在于它跑得通,而且注释愿意跟你解释为什么这么写。
3. 动手前的地基:三样东西各管一段
很多新手一上来就去啃协议栈源码,结果被一堆eMB开头的函数劝退。其实你不需要先看懂协议栈,你需要先搞清楚“谁在干什么活”。把这三样东西的角色分清楚,后面读代码会顺很多。
3.1 STM32 是身体,FreeRTOS 是日程表,FreeModbus 是嘴
STM32 是那块芯片,负责计算、跑代码、控制外设。你可以把它当成身体。
FreeRTOS 是跑在这块芯片上的实时操作系统,管的是“什么时候轮到谁干活”:串口收数据是一个任务,闪灯是一个任务,Modbus 协议栈是一个任务,大家按优先级轮流跑,中间还能互相等待。你可以把它当成一张日程表。
FreeModbus 是协议栈,负责把应用想传的数据打包成一帧符合 Modbus 规矩的字节流,丢给串口发出去;也负责把串口收到的字节流拆开、校验、翻译成应用能懂的数据。你可以把它当成嘴和耳朵。
三者分开看,脑子就清晰了:你要写的应用逻辑在“身体”里,调度靠“日程表”,说话办事靠“嘴”。上手时最容易犯的错是把三者的边界搞混,比如在串口中断里直接去处理一整帧数据——那既不归中断管,也容易把系统卡死。第 4 章会专门讲任务该放在哪。
3.2 四种寄存器和七个功能码:一张表就装得下
Modbus 里所有数据都放在“寄存器”里,但寄存器不是芯片里那种物理寄存器,而是一块约定的数据区,你可以理解成一排编号的格子,每个格子装 16 位(两个字节)。格子分四种,功能码就是“我要对这些格子做什么”。
| 寄存器类型 | 编号习惯 | 读写 | 典型用途 | 常用功能码 |
|---|---|---|---|---|
| 线圈 Coil | 0xxxx | 读写 | 开关量输出,如继电器 | 01 读、05 写单个、15 写多个 |
| 离散输入 Discrete Input | 1xxxx | 只读 | 开关量输入,如限位开关 | 02 读 |
| 输入寄存器 Input Register | 3xxxx | 只读 | 传感器测量值 | 04 读 |
| 保持寄存器 Holding Register | 4xxxx | 读写 | 参数设定、运行数据 | 03 读、06 写单个、16 写多个 |
新手看到这张表,第一反应往往是“这么多怎么记得住”。其实你只要记两件事:开关量走线圈和离散输入,数值走输入寄存器和保持寄存器;只读的用 02 和 04,能读能写的用 01、03、05、06、15、16。真正上手以后你会发现日常用的就是 03 和 06 两个,其余遇到再查。
有个坑必须先说清楚:手册上写的 40001 是“从 1 开始数”的人类编号,代码里传的地址通常是从 0 开始的。所以读 40001 这个格子,函数参数一般写 0。这个差 1 的问题,能坑掉一大批新手,后面第 5 章还会专门讲。
3.3 主站问、从站答:一句话都不能抢
Modbus RTU 的一帧数据长这样:从站地址 1 字节 + 功能码 1 字节 + 数据 N 字节 + CRC 校验 2 字节。地址范围一般是 1~247,0 是广播(发了没人回)。
规矩的核心是一问一答,绝不抢话。主站发一帧出去,从站收到后校验地址是不是自己、CRC 对不对,都对才回一帧;主站等回帧,超时就重发或者报错。整个过程里从站不会主动开口,哪怕它的数据变了,也得等主站来问。
这条规矩决定了很多实现细节。比如主站必须管超时——不能发完就一直等;比如从站必须一直“在听”——协议栈得被反复调用,不能睡过去;再比如一帧数据内部字节之间不能停太久,连续空闲超过 3.5 个字符时间,接收方就认为这帧结束了。所以你会看到协议栈里到处是计时、状态机、缓冲区,它们全是在伺候这条“一问一答”的规矩。
理解这一点,你就理解了这个协议栈 80% 的设计动机。
4. 第一个例子怎么跑:从打开工程到看见数据
前面都是铺垫,这一章开始动手。我建议的顺序是:先用从机跑通,再用主站去读它,最后才把两者丢进 FreeRTOS 任务里。这个顺序不是随便定的——从机是被动方,调试时你能完全控制节奏,出问题容易定位。
提示:不同移植版本的函数名可能略有出入,下面的代码是按 FreeModbus 一贯的命名风格写的,你打开手上源码的头文件对照着找同名或近似的函数即可,不必逐字对。
4.1 打开工程先别编译:把三层结构看清楚
拿到一个嵌入式工程,新手最常见的动作是直接点编译,然后被一堆报错淹没。先花十分钟看目录,能省下两个小时。
一份 FreeModbus 的移植工程,通常分成三层:
第一层是协议栈本体,就是 FreeModbus 那部分源码,跟芯片无关,改都不用改。这一层里你会看到eMB开头的一堆函数,它们是协议栈对外的门面。
第二层是移植层,负责把协议栈和具体芯片接起来:串口怎么收发、定时器怎么计时、临界区怎么开关中断。这一层是每个芯片都不一样的地方,也是新手最该先看的地方——因为出问题十有八九出在这。
第三层是应用层,也就是你的业务代码:读传感器、控继电器、跟屏幕通信。这一层最薄,也最好改。
看目录的时候顺手做一件事:把工程里跟“流程图”配套的文档翻一遍,先建立整体印象,再钻代码。资源里带了主机、从机两份流程图,配合代码看,你会少走很多弯路。
编译时如果报错,先确认三件事:芯片型号选对了没、启动文件和链接脚本对不对、FreeRTOS 的配置文件里堆栈够不够。这三样占新手上手报错的大头。
4.2 从机第一个例子:让上位机读出你的一个变量
从机要干的活其实就一件:把应用里的数据摆到寄存器格子里,等主站来读。你需要在应用层准备一块 16 位的数据区,然后实现一个回调函数,协议栈在收到读请求时会自动调它。
/* 应用自己的数据区,一个格子 16 位,这就是主站能读到的"保持寄存器" */ static USHORT usRegHoldingBuf[REG_HOLDING_NREGS]; /* 协议栈收到"读/写保持寄存器"的请求时,会回调这个函数 */ eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { USHORT i; /* 关键:协议传过来的地址从 1 开始,数组下标从 0 开始,必须减 1 */ usAddress = usAddress - 1; /* 越界一定要挡住,否则数组一写飞,现象就是随机死机 */ if ((usAddress + usNRegs) > REG_HOLDING_NREGS) { return MB_ENOREG; } if (eMode == MB_REG_READ) { /* 主站要读:把数据按"高字节在前"塞进缓冲区 */ for (i = 0; i < usNRegs; i++) { *pucRegBuffer++ = (UCHAR)(usRegHoldingBuf[usAddress + i] >> 8); *pucRegBuffer++ = (UCHAR)(usRegHoldingBuf[usAddress + i] & 0xFF); } } else { /* 主站要写:把收到的两个字节拼回一个 16 位数 */ for (i = 0; i < usNRegs; i++) { usRegHoldingBuf[usAddress + i] = (USHORT)(*pucRegBuffer++ << 8); usRegHoldingBuf[usAddress + i] |= (USHORT)(*pucRegBuffer++); } } return MB_ENOERR; }这段代码里有三个参数值得掰开说。pucRegBuffer是协议栈给的缓冲区指针,读的时候往里写,写的时候从里读,你不需要自己管它有多长——长度由usNRegs决定,协议栈保证够用。usAddress是主站请求的起始格子编号,注意它从 1 开始。eMode告诉你这次是读还是写,同一个回调处理两个方向,这也是 FreeModbus 一贯的省事设计。
MB_ENOREG这个返回值的意思是“你要的格子不存在”,主站会收到一个异常应答,而不是拿到一堆垃圾数据。这个越界检查千万别省,我在现场见过因为少写这一行、主站一读越界地址就把整个板子跑飞的案例。血泪经验,宁可多写三行判断。
数据要动起来,还得靠应用的业务代码往usRegHoldingBuf里填值。比如你的温度是 25.6 度,乘 10 存成 256,主站读出来再除以 10,这就是最常见的定点数处理方式——Modbus 的格子只能装整数,小数得靠约定。
最后是初始化和轮询,这是从机跑起来的最小骨架:
/* 从机初始化:RTU 模式,本机地址 1,串口 0,9600 波特率,无校验 */ eMBInit(MB_RTU, 0x01, 0, 9600, MB_PAR_NONE); eMBEnable(); /* 打开协议栈,开始监听总线 */ for (;;) { eMBPoll(); /* 收一帧、解析一帧、回一帧,全靠它 */ }eMBPoll()是协议栈的心脏,它内部是一个状态机:看看有没有新字节、有没有凑够一帧、CRC 对不对、地址是不是自己、该不该回。你只要保证它被不停地调用就行,剩下的它自己会办。新手最容易犯的错是把它放在if里条件调用,或者忘了调用,然后抱怨“收不到数据”——其实协议栈根本没被喂到。
4.3 主站第一个例子:主动去读别人的数据
从机跑通了,轮到主站。主站要干的活是“按需发问”:什么时候问、问谁、问几个、等多久,全由应用决定。这份资源的主机部分提供了多种请求模式,你可以选阻塞(发完等结果,代码写得直白)或非阻塞(发完先干别的,结果回头再说),超时时间也能自己定。
/* 主站请求:读 1 号从站的保持寄存器,从 0 号格子起读 2 个,超时 200ms */ eMBMasterReqErrCode eErr; eErr = eMBMasterReqReadHoldingRegister(0x01, /* 目标从站地址 */ 0x0000, /* 起始地址,从 0 算 */ 2, /* 读几个格子 */ 200); /* 超时时间 */ if (eErr != MB_MRE_NO_ERR) { /* 请求没发出去:可能是总线忙、上一帧还没结束,稍后重试即可 */ }数据回来以后,协议栈会回调主站的寄存器处理函数,你在那里把值抄进自己的变量:
/* 主站收到应答后回调这里,此刻"对方的数据已经到了" */ eMBErrorCode eMBMasterRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { /* 把两个字节拼成一个 16 位数,高字节在前 */ usRemoteValue = (USHORT)((pucRegBuffer[0] << 8) | pucRegBuffer[1]); return MB_ENOERR; }新手看到这里会问:为什么读个数据要拆成两段?因为总线是异步的。你发出请求的那一瞬间,数据还在路上,函数只能立刻返回一个“请求已受理”的结果。真正的数据要等几十毫秒之后才会到,那时你的代码早就跑到别的地方去了,所以只能靠回调把数据接住。
理解了这个异步性,你就理解了为什么超时时间是个必填参数:万一对方掉线、地址写错、线接反了,回调永远不会来,你总得有个兜底。超时设太短,正常设备也来不及回,你会误判成故障;设太长,一个坏节点能把整个轮询周期拖死。我一般从 200ms 起步,用示波器或抓包看一眼实际往返时间再调。这套"先给个保守值,再按实测收紧"的思路,比死记某个数字有用得多。
还有个细节:主机和从机能跑在同一个协议栈里。这意味着你可以做一台"既是主又是从"的设备——对上被别的设备查询,对下主动去采集。省掉一个串口,也省掉一份代码。
4.4 丢进 FreeRTOS 任务里跑起来
前面两段代码的for(;;)循环,放到 FreeRTOS 里就变成一个任务:
/* 协议栈任务:优先级别给太高,也别给太低 */ void vModbusTask(void *pvParameters) { eMBInit(MB_RTU, 0x01, 0, 9600, MB_PAR_NONE); eMBEnable(); for (;;) { eMBPoll(); /* 处理收发和超时 */ vTaskDelay(pdMS_TO_TICKS(1)); /* 让出 CPU,别的任务才跑得动 */ } }vTaskDelay(1)这一行是精髓。不写它,这个任务会把 CPU 占满,别的任务永远轮不上,现象是“能通但系统像卡住了”。写了它,协议栈每秒被调用上千次,完全够用——Modbus 一帧最多几百字节,9600 波特率下一帧也要几十毫秒才发完,1ms 的轮询间隔远远跟得上。
任务优先级怎么定?我的建议是比串口收发任务低、比界面刷新的任务高。太低的话总线上一忙,协议栈就反应迟钝;太高的话它会把实时性要求更高的任务挤掉。这个值没有标准答案,跑起来看现象调,卡顿就降一点,超时就升一点。
另外提一句,别在串口中断里跑协议栈。中断该做的事只有一件:把收到的字节丢进环形缓冲区,然后赶紧退出。解析、校验、回帧这些重活,交给上面这个任务。这条边界守住了,系统就稳;守不住,你就等着处理各种随机死机吧。
5. 新手避坑:五个最常翻车的地方
这一章全是“别人踩过、你可能也会踩”的坑。每条都按“现象 → 原因 → 解决”写,出了问题时对着查,比到处问人快得多。
5.1 现象:通信一直超时,抓包看不到任何回应
现象很典型:主站发了请求,等半天没动静,超时错误一个接一个。
原因通常有三个层次。最外层是硬件:A、B 两根线接反了,或者终端电阻没接、接了但阻值不对,或者共地没做好。中间层是串口参数:波特率、数据位、校验位、停止位跟对方不一致,最常见的就是对方是 8E1(偶校验)而你按 8N1 配。最内层是协议栈没被喂到:eMBPoll()忘了调用,或者被放在了某个不常进的分支里。
解决办法按顺序排查:先用示波器或抓包工具看一眼线上到底有没有波形,没有波形就是硬件或串口配置问题,别再折腾代码;有波形但对方不回,就核对串口参数,尤其是校验位;都有还没反应,就在eMBPoll()上加个计数器或者打个断点,确认它真的在跑。这个“从物理层往上一层层排除”的顺序,比乱改代码高效十倍。
5.2 现象:读回来的数永远是 0,或者乱跳
主站能收到回帧,但数值不对——要么一直 0,要么几个数来回蹦。
原因基本跑不出两个。一是地址错位:你按手册写了 40001,代码里也传了 40001,结果读到了 40002 的格子,或者压根越界。前面说过,协议里的地址从 1 开始数,数组从 0 开始数,中间那道减 1 的坎必须跨过去。二是字节序:Modbus 规定高字节在前,你要是拼反了,256 会变成 1,65535 会变成 65535(这个碰巧不变),小数值看着就像乱跳。
解决办法:先用一个固定值做测试,把 0x1234 写进格子,读回来看看是不是 0x1234,这样字节序问题一眼就能看出来。地址问题就一个格子一个格子试,从 0 试到 9,看哪一格变。定下来以后在代码里写清注释,别让下一个人再猜。
5.3 现象:跑几分钟就死机,或者卡在 HardFault
这个最吓人。刚上电好好的,跑一阵子突然挂了,重启又好,过一会儿又挂。
原因多半是内存。三个高发点:中断里的缓冲区开小了,一帧数据没接完就溢出;任务堆栈给小了,协议栈内部递归或者局部变量把栈撑爆;临界区保护没做好,主站在收数据的同时应用在改寄存器数组,两边打架。
解决办法:先把栈加大一倍试试,如果问题消失,就是栈不够,这是最快的定位手段。然后检查接收缓冲区大小,按“最大帧长 + 余量”来给,别抠。最后检查共享数据的保护,凡是中断和任务都会碰的变量,该关中断就关中断,该用队列就用队列。这类问题没有后悔药,只能靠事前留余量。
5.4 现象:主站和从站都开了,结果互相打架
协议栈支持主机从机同时跑,你兴冲冲地两边都开了,结果谁都不正常。
原因是总线冲突。主站发问的时候,从机也在等别人来问,如果两边共用同一个串口又没有仲裁机制,很容易出现“自己发的帧被自己收回来”的尴尬场面。轻则CRC 报错,重则状态机错乱。
解决办法:先想清楚你的设备在总线上到底是什么角色。如果既要被查又要查别人,那就得严格分时——主站只在空闲时发问,问完立刻切回监听;或者干脆用两个串口,物理上分开。别指望协议栈帮你仲裁,它只负责打包和解包,不管总线归谁。
5.5 现象:CRC 校验通不过,收到的全是垃圾
现象是协议栈一直报 CRC 错误,或者干脆丢弃所有帧。
原因依次往下:波特率偏差太大,比如芯片主频配错导致实际波特率跟设定值差了几个百分点,短帧还能凑合,长帧必错;线路干扰,屏蔽没做好、走线跟动力线捆在一起;还有就是帧间间隔判断错了,把两帧粘成一帧,或者把一帧劈成两半。
解决办法:先量实际波特率,用示波器测一位的宽度,误差超过 2% 就该查时钟配置了。然后看接线,双绞、屏蔽、单点接地,工业现场这几样不做,玄学问题会一直跟着你。最后核对空闲间隔的判断逻辑,确认它是按“连续空闲超过 3.5 个字符时间”算的,而不是拍脑袋定了个毫秒数。
6. 下一步学什么:从跑通到能自己改
跑通第一个例子只是入场,接下来怎么走,我按自己的经验给个顺序。
6.1 三个台阶,一级一级上
第一级:改得动。拿从机例程,加一个新的保持寄存器,让它存一个你自己算出来的值(比如 ADC 采样),让上位机读出来。这一步的目的是把“应用数据 → 寄存器格子 → 回调 → 线上字节”这条链路彻底走熟。走熟之后你会发现,所谓的协议开发,大半时间在搬数据。
第二级:看得见。在协议栈的收发函数里加打印或者翻转 IO,用示波器或抓包工具对照着看:发出去的字节跟你想的是不是一样?CRC 算对了吗?异常应答是什么时候触发的?把黑匣子打开一次,比读十遍文档管用。
第三级:扛得住。故意制造故障:拔线、断电、让从站不回应、让主站连发。看你的程序会不会卡死、会不会内存泄漏、能不能自己恢复。工业现场没人保证线路永远好,能自愈的代码才叫能用。
一个可以照着走的两周安排:
| 时间 | 做什么 | 验收标准 |
|---|---|---|
| 第 1~2 天 | 编译烧录从机例程,用上位机软件读默认寄存器 | 能稳定读到数据,改值能看到变化 |
| 第 3~4 天 | 加两个自己的寄存器,一个只读一个可写 | 读写都正常,越界返回异常 |
| 第 5~6 天 | 编译烧录主机例程,去读从机 | 主站能读到从机刚加的值 |
| 第 7~8 天 | 主站加超时重试和错误统计 | 拔掉从机线,主站不卡死,能报错 |
| 第 9~10 天 | 主机从机同栈运行,分时调度 | 两边长时间跑不冲突 |
| 第 11~14 天 | 移植到自己的板子,换串口换引脚 | 在自己的硬件上跑通全流程 |
这张表不是让你赶进度,是让你每天都有个明确的“今天算完成”的标准。没有验收标准的学习,最容易变成“看起来在学,实际上在原地转”。
6.2 一个具体技巧:先写测试,再改协议栈
如果你打算动协议栈的源码,我劝你先做一件事:准备一个能随时跑的最小测试场景。比如固定读 1 号从站的 2 个寄存器,输出结果对不对。改完一次源码就跑一次,对上了再继续改。
这个习惯看着笨,实际能救你。协议栈代码状态机套状态机,改一处很容易牵动别处,等你改完五处再一起测,出了错根本不知道是哪一处引起的。一步一步验证,虽然慢,但不会返工。
资源里的主机、从机流程图和多份协议文档,正好配合这个习惯用:改之前先对着流程图确认自己要动的是哪一段,改完再对照流程图看有没有破坏原有路径。文档不是拿来供着的,是拿来对答案的。
最后说个我自己的教训。刚接触这套东西的时候,我觉得注释啰嗦,看两眼就跳过去,凭感觉改。结果一个地址减 1 的问题折腾了两天,最后翻回注释,第一行就写着“注意地址从 1 开始”。从那以后我每次改协议栈相关代码,都强制走一遍:先读注释,再对流程图,再写测试,三步走完才动手。慢是慢了点,但再没熬过那种夜。希望帮到你。
这份资源里的例程、流程图和协议文档是配套的,建议你直接下载下来,对着第 4 章的顺序跑一遍,跑通了再回头看第 3 章那张表,很多当时看不懂的地方会自己亮起来。
本文还有配套的精品资源,点击获取