SPC3开发包源码解析:PROFIBUS-DP从站初始化与GSD对齐
简介:面向工业自动化领域的嵌入式与现场总线开发者,SPC开发包源代码聚焦Profibus-DP从站通信功能的底层实现与定制开发,适合具备C语言基础并希望深入理解现场总线协议的中级工程师参考。压缩包共4个文件,由3个C源文件和1个H头文件组成,整体约21KB,体积轻量但结构紧凑,便于快速查阅协议处理、芯片驱动与用户接口之间的组织方式。已有1720人学习,说明其在Profibus-DP从站开发场景中具有一定参考价值。读者可从中了解SPC3通信控制器的源码架构,包括报文打包解包、主从站交互、中断处理与错误检测等关键环节,并据此修改通信服务、优化数据处理流程或增强故障恢复能力;结合硬件调试,还可对帧结构、通信时序与异常恢复路径进行验证,为摆脱现成库依赖、缩短从站开发周期并降低硬件适配成本提供实用起点。
1. SPC3 开发包源代码拆开看:1.5KB 双口 RAM 里到底装了什么
搜索框里敲下「SPC3 开发包源代码」的人,手里多半已经有一块画好的板子:一颗 PROFIBUS-DP 从站协议控制器、一片 RS-485 收发器、一颗单片机和一圈光耦隔离。这份源代码解决的不是“DP 协议怎么实现”——链路层状态机固化在芯片内部,主机侧能看到的只是 1.5KB 双口 RAM、二十来个寄存器和几根中断线——它真正解决的是“主机怎么把这颗芯片喂饱”:上电按什么顺序写寄存器、1536 字节 RAM 怎么切给七个缓冲区、中断来了先读哪一位、主站参数化失败时诊断字节该填什么值。
它适合三类人:正在做 DP 从站硬件、固件卡在初始化阶段的开发者;手里有旧项目要维护、需要一份能读懂的最小参考实现的人;以及想把从站功能塞进现有 MCU 工程、但不想从零啃手册的工程师。芯片本身不便宜,调试器一插一拔就是半天,这份代码最大的价值是把手册里散落在几十页的时序要求,压成了几个可以直接单步的函数。
2. 上电到能通信:SPC3 初始化顺序与缓冲区长度寄存器的写法
初始化这一段是全包最容易翻车的地方,因为它不像普通外设那样“配错一个位顶多功能不对”,而是配错一个字节,芯片就安安静静停在 POWER_ON,连个错误码都不给你。
2.1 复位、时钟与主机接口模式
先看硬件侧的三件事。第一是时钟,SPC3 的主时钟由外部晶振或 MCU 输出提供,常见做法是给到 24 MHz 这一档,走线要短、要远离 RS-485 的差分对,时钟抖动大了寄存器读出来会随机翻位,这种故障用示波器看波形还挺正常,只有连续读同一地址几百次才能发现偶发差异。第二是复位,RST 引脚低有效,低电平至少保持 1 ms,释放之后再等 100 µs 左右让内部 RAM 稳定,再去写第一个寄存器;有些板子把复位直接挂在 MCU 的复位网络上,结果 MCU 自己还在启动阶段就把 SPC3 的初始化代码跑了,读回来全是 0xFF。第三是主机接口模式,MODE 引脚决定芯片按 Intel 时序(/WR、/RD 分开)还是 Motorola 时序(R/W 加数据选通)响应,接反了的典型现象是写进去读不出来、或者地址每访问一次就自己加一。
这版开发包把硬件相关的动作单独放在一个文件里,初始化入口只做三件事:拉复位、等、按顺序写寄存器。下面这段可以直接对照自己板子改引脚宏:
/* spc3_init.c —— 上电初始化,函数内顺序不要调换 */ #include "spc3_reg.h" #include "spc3_def.h" static void spc3_hw_reset(void) { SPC3_RST_LOW(); /* RST 低有效 */ delay_us(2000); /* 手册要求 >=1 ms,这里留一倍余量 */ SPC3_RST_HIGH(); delay_us(100); /* 释放后等内部 RAM 稳定再访问 */ } void spc3_init(void) { spc3_hw_reset(); /* 1. 先清中断,避免复位后残留的中断标志打断初始化流程 */ SPC3_WR(REG_IR, 0x00); SPC3_WR(REG_MR1, 0x00); /* 2. 模式寄存器 0:DP 从站模式 + 最小 TSDR */ SPC3_WR(REG_MR0, MR0_DP_MODE | MR0_MIN_TSDR_11); /* 3. 先写长度,再写指针。顺序反了芯片会拿复位默认长度 0 去截断 */ SPC3_WR(REG_LEN_DDB, DDB_LEN); SPC3_WR(REG_LEN_DCB, DCB_LEN); SPC3_WR(REG_LEN_DPB, DPB_LEN); SPC3_WR(REG_LEN_DIN, DIN_LEN); SPC3_WR(REG_LEN_DOUT, DOUT_LEN); SPC3_WR(REG_LEN_SSA, SSA_LEN); /* 4. 再写各缓冲区起始地址,全部落在 0x040~0x5FF 之间且互不重叠 */ SPC3_WR(REG_PTR_DDB, ADDR_DDB); SPC3_WR(REG_PTR_DCB, ADDR_DCB); SPC3_WR(REG_PTR_DPB, ADDR_DPB); SPC3_WR(REG_PTR_DIN, ADDR_DIN); SPC3_WR(REG_PTR_DOUT, ADDR_DOUT); SPC3_WR(REG_PTR_SSA, ADDR_SSA); /* 5. 最后才开中断,顺序提前会在缓冲区没配好时进中断 */ SPC3_WR(REG_IR, IR_PRM | IR_CFG | IR_DX_OUT | IR_WD); SPC3_WR(REG_MR1, MR1_IRQ_EN | MR1_WD_EN); /* 6. 等状态机离开 POWER_ON,超时要有出口,别写成死等 */ { uint32_t t = 0; while (spc3_state() == ST_POWER_ON && ++t < 100000UL) { } } }逻辑上就三步:把芯片摁回默认态、把内存布局告诉它、把中断打开。参数里最需要盯的是MR0_MIN_TSDR_11这个宏,它决定从站应答的最小时隙,值太小会在高波特率下撞上主站超时,值太大又会让整个 DP 周期变长;网络里只有这一台从站时按最小值配,多从站时留一到两档余量。REG_*系列的寄存器偏移在各个封装里命名不同,但语义一致,改代码前先翻自己那份spc3_reg.h的注释,别照抄网上的地址表。
2.2 长度与指针:1536 字节怎么切
SPC3 内部是 1.5KB 双口 RAM,主机侧看到的是 11 位地址空间,其中 0x000 到 0x03F 是内部寄存器和保留区,真正的缓冲区从 0x040 开始。切法没有唯一答案,但有几条硬约束:每个缓冲区起始地址按偶地址对齐,避免 16 位访问时地址错位;各段之间不能重叠,重叠的后果不是编译报错,而是参数化数据把诊断缓冲区覆盖掉,表现为主站读到的诊断内容莫名其妙;总长不能越过 0x5FF。
我一般按下面这张表切,输入输出各留 64 字节,够覆盖大多数小型从站模块:
| 区域 | 起始地址 | 长度(字节) | 用途 |
|---|---|---|---|
| 内部寄存器与保留区 | 0x000 | 0x40 | 主机访问,不可挪用 |
| 诊断 DDB | 0x040 | 16 | 前 6 字节芯片填,后 10 字节用户扩展 |
| 配置 DCB | 0x050 | 16 | 必须与 GSD 的 Cfg_Data 逐字节一致 |
| 参数 DPB | 0x060 | 16 | 用户参数,主站下发 |
| SSA | 0x070 | 8 | Set_Slave_Address 用 |
| 输入 DIN | 0x080 | 64 | 从站到主站 |
| 输出 DOUT | 0x0C0 | 64 | 主站到从站 |
| 预留 | 0x100 | 0x500 | 后续加模块时扩展 |
长度寄存器写的是“我给这块区域留了多大”,不是“这次实际用了多大”。芯片在参数化和配置阶段会按这两个值做边界检查,写大了会越界读到别人的数据,写小了主站下发的配置字节直接被截断。开发包里一般会带一个运行时自检函数,把各段起止地址算一遍,发现重叠就返回错误码,上板第一天先把这函数跑通,比后面拿示波器抓帧省事得多。
还有一处容易忽略:SSA 缓冲区。Set_Slave_Address 报文用于主站远程改从站地址,有些主站组态时会自动发一次。SSA 段没配或者长度写 0,芯片收到这类报文后无法回写,主站侧就会报“地址分配失败”,而你在从站侧看不到任何异常。
2.3 中断使能与状态机推进
SPC3 的中断源不多,但每一位都对应一个必须处理的动作:参数化报文到达、配置报文到达、输出数据可用、看门狗超时。中断寄存器是“读后锁存、写回清除”的行为,处理函数里第一步必须是读出来存到局部变量,再按位处理,最后写回清除,中途插入任何一次其他寄存器访问都可能把标志弄丢。
状态寄存器给出的五个状态码,是排查问题的第一手线索:
| 状态码 | 含义 | 正常情况下会停留多久 |
|---|---|---|
| 0x00 | POWER_ON | 上电后极短,初始化完成即离开 |
| 0x01 | WAIT_PRM | 等主站下发参数化报文,可能一直停在这 |
| 0x02 | WAIT_CFG | 参数化通过,等配置报文 |
| 0x03 | DATA_EXCHANGE | 正常数据交换 |
| 0x04 | ERROR | 硬件或协议错误,需看诊断 |
停在 WAIT_PRM 说明主站压根没找到这台从站,问题多半在 GSD 文件、站地址或 RS-485 物理层,跟初始化代码无关;能进 WAIT_CFG 却过不去,问题出在配置字节比对;反复在 DATA_EXCHANGE 和 WAIT_PRM 之间跳,那是看门狗在起作用,往第 3 章和第 5 章看。
3. 数据交换主循环:输入输出缓冲区怎么搬才不打架
状态机跑到 DATA_EXCHANGE 只说明握手成功,真正决定现场稳不稳定的是主循环里那几十行搬运代码。
3.1 输出先取走,输入后组装
DP 的一个总线周期里,主站先写输出数据、再读输入数据。落在从站侧就是:输出缓冲区的中断先来,输入缓冲区要在主站读走之前准备好。所以主循环的顺序必须是“先拷出输出、再做业务运算、最后写输入”,写反了不会报错,但主站读到的永远是上一个周期的数据,现场表现是“按钮按下去电机慢半拍”,查起来非常折磨人。
下面这段是这版包里主循环的骨架,我按自己的习惯加了长度校验:
/* dp_slave.c —— 主循环,按状态机分派,搬运顺序不能反 */ void dp_slave_poll(void) { switch (spc3_state()) { case ST_WAIT_PRM: if (dp_check_prm() != 0) { /* 应用层校验用户参数 */ spc3_set_diag(DIAG_PRM_ERR, 0x01); } break; case ST_WAIT_CFG: if (dp_check_cfg() != 0) { /* 逐字节比对配置 */ spc3_set_diag(DIAG_CFG_ERR, 0x02); } break; case ST_DATA_EXCHANGE: dp_copy_outputs(); /* 1. 先把主站写的搬走 */ dp_run_app(); /* 2. 业务运算,别放耗时操作 */ dp_build_inputs(); /* 3. 组装本周期输入 */ spc3_arm_input_update(); /* 4. 通知芯片可以发新数据 */ break; case ST_ERROR: spc3_recover(); break; default: break; } } static void dp_copy_outputs(void) { uint8_t len = SPC3_RD(REG_LEN_DOUT_ACT); /* 芯片填的实际长度 */ if (len > DOUT_LEN) { len = DOUT_LEN; /* 防御性截断,别信主站 */ } memcpy(g_out_shadow, SPC3_PTR(REG_PTR_DOUT), len); g_out_len = len; }dp_copy_outputs里最关键的是读“实际长度”而不是“配置长度”。主站组态时可能只下发模块的一部分,或者因为诊断原因缩短了输出,按配置长度硬拷会把缓冲区尾部的旧数据当成新数据用。spc3_arm_input_update是通知芯片“我这边的输入缓冲区已经换了一版”,不同封装里可能叫 New_DIN 或者写一次命令寄存器,具体位定义看头文件,别自己猜。
3.2 诊断上报的三种情况与缓冲区格式
诊断缓冲区的前 6 个字节由芯片自己填,包括站状态、主站地址、Ident 号这些,用户扩展从偏移 6 开始,长度受 DCB 前面那个诊断长度寄存器约束。开发包里一般给了三个上报入口:参数化错误、配置错误、通道诊断。
参数化错误通常在dp_check_prm()里产生,比如用户参数里的量程上限小于下限、通道使能位自相矛盾,这种错误要把诊断字节 0 的对应位置起来,主站读到之后会停下来等人工处理。配置错误的来源更机械一些:主站下发的配置字节数量和内容跟 GSD 里写的不一致,或者跟 DCB 长度对不上。第三种是运行中产生的通道诊断,比如某一路输入断线,这种诊断要带上通道号和错误类型,长度超过 6 字节就得走扩展诊断区。
诊断内容更新之后,还要让芯片知道“有新诊断”,否则主站要等到下一个诊断轮询周期才看得到变化。这一步在代码里通常是一个置位动作,忘记写不会报错,只是现场排故时主站显示的一直是旧消息。
3.3 看门狗与掉线恢复
SPC3 内部看门狗监控的是“主站还在不在跟我说话”,超时时间由 GSD 里的两个系数乘出来,具体基准时间按 GSD 规范算,别拿毫秒硬套。超时之后芯片自动退回 WAIT_PRM,主站需要重新参数化才能恢复通信。
这里有个现场常见现象:设备运行几分钟到几十分钟掉一次线,重新上电又好了。原因往往不是看门狗时间配短了,而是主循环里塞了耗时操作——比如一次 Flash 擦写两百毫秒、一次串口打印几十毫秒——刚好跨过了看门狗窗口。我的做法是把看门狗时间按“最坏情况主循环耗时 × 3”来配,同时在长任务前后各喂一次狗,让芯片知道主机还活着。恢复逻辑不要写成“检测到掉线就整机复位”,从站被动重启会让主站侧累积一堆诊断,正确做法是只把应用状态清到安全值,等主站重新参数化。
4. GSD 与源码对不上:配置字节、用户参数、看门狗三处硬对齐
GSD 文件是主站认识这台从站的唯一入口,它和源代码之间的关系不是“文档描述实现”,而是“两边必须逐字节一致”。这一章的三处对不上,占了现场调试问题的一大半。
4.1 Cfg_Data 与 DCB 长度:一个字节都不能差
配置字节决定主站怎么切分输入输出模块。GSD 里写 32 字节输入加 32 字节输出,源代码里 DIN 和 DOUT 的长度寄存器也必须对应得上,中间的映射关系通常是一串十六进制:模块化设备按模块顺序排,紧凑型设备一般就是一两个字节。
| GSD 字段 | 示例值 | 源码对应 | 不一致时的表现 |
|---|---|---|---|
| Ident_Number | 0x1234 | spc3_set_ident()写入值 | 主站扫描不到设备 |
| Cfg_Data | 0xC0,0xC1 | DCB_LEN 与 DCB 内容 | 状态卡在 WAIT_CFG |
| User_Prm_Data_Len | 4 | DPB_LEN | 参数化失败,诊断位 0 置起 |
| MaxTsdr_9.6 | 60 | MR0 最小 TSDR 档位 | 低波特率偶发超时 |
| WD_Fact_1 / WD_Fact_2 | 3 / 10 | MR1 看门狗使能 | 周期性掉线 |
表里的 Ident 号是示例值,实际要向总线组织申请,别随手编一个跟知名设备撞号的数字。配置字节建议用 GSD 编辑器生成后直接粘进代码数组,手算的十六进制串十次里有三次会把输入输出的顺序搞反。
4.2 User_Prm_Data 与参数化校验
用户参数是主站下发给从站的应用层配置,比如量程、滤波时间、通道使能。GSD 里声明了User_Prm_Data_Len = 4,源代码里 DPB 长度就得是 4,主站发来的报文里也确实只有 4 个字节,多一个少一个都会让芯片判参数化失败。
校验函数别写成“长度对了就通过”。实际现场遇到过的坑是:主站组态软件升级之后,参数默认值变了,长度没变但内容变了,从站照单全收,结果按新参数跑出来的量程跟现场仪表对不上,排查了两天才发现是参数含义变了。稳妥做法是在dp_check_prm()里对每一个有取值范围的参数做区间检查,超范围就回诊断,让主站侧明确报错。
/* dp_prm.c —— 参数校验,长度对了不代表内容对 */ uint8_t dp_check_prm(void) { uint8_t *p = (uint8_t *)SPC3_PTR(REG_PTR_DPB); uint8_t len = SPC3_RD(REG_LEN_DPB_ACT); if (len != DPB_LEN) { return ERR_PRM_LEN; /* 长度不符,直接拒绝 */ } if (p[0] > 3) { /* 通道 0 量程档位,只允许 0~3 */ return ERR_PRM_RANGE; } if (p[1] < 10 || p[1] > 200) { /* 滤波时间 10~200 ms */ return ERR_PRM_RANGE; } g_cfg.range = p[0]; g_cfg.filter = p[1]; return 0; }参数说明:REG_LEN_DPB_ACT是芯片在收到参数化报文后填的实际长度,跟配置长度是两个寄存器;p[0]、p[1]的偏移跟 GSD 里User_Prm_Data的排列顺序一一对应,改了 GSD 里的顺序,这里的下标必须同步改。
4.3 波特率与 MaxTsdr:12 Mbit/s 下的时序余量
SPC3 支持从 9.6 kbit/s 到 12 Mbit/s 的自动波特率检测,但 GSD 里必须把支持的速率逐档列出来,主站只会按声明过的档位去试。漏写某一档,现场把主站拨到那一档就扫不到设备,看起来像硬件坏了。
高波特率下的时序余量主要落在两处。一处是MaxTsdr,它告诉主站“这台从站最慢多久能应答”,值给小了主站等不及就判超时,给大了会拖慢整个网络的循环周期。另一处是 RS-485 收发器的方向切换,从接收到发送的切换延迟如果超过一个位时间,高波特率下第一个字节就会丢,主站侧表现为偶发 CRC 错误。这个问题在 1.5 Mbit/s 以下几乎看不出来,一上 3 Mbit/s 就开始出现,我的做法是把方向切换放在定时器比较中断里,提前半个位时间拉高,而不是等发送函数里再去置引脚。
5. 避坑与排查:从站起不来、掉线、数据错位五类现场问题
下面这五条是按发生频率排的,每条都按“现象、原因、解决”写,遇到问题时按顺序对一遍,能省掉大部分示波器时间。
5.1 状态机始终停在 POWER_ON
现象:上电后读状态寄存器一直是 0x00,写进去的寄存器读回来不是原值,或者全 0、全 0xFF。
原因:九成出在硬件接口。MODE 引脚接反导致时序不匹配;片选信号没接或者地址译码错,主机访问的根本不是芯片;时钟没起振,芯片内部逻辑压根没跑;复位释放得太早,MCU 在芯片还没稳定时就去写寄存器。
解决:先用示波器确认时钟引脚有稳定波形、幅度够;再把 MODE 引脚按手册接死,别悬空;然后写一个最小测试——往某个可读写的寄存器写 0x55 再读回,读不回就说明主机接口这一段没通,别往下查协议。复位电路上加一个 RC 延时,保证 MCU 启动完成之后才释放芯片复位。
5.2 参数化能过,配置一直不过
现象:状态在 WAIT_PRM 短暂停留后进到 WAIT_CFG,然后停住不动,主站侧报配置错误,从站诊断字节 0 的对应位置起。
原因:DCB 长度寄存器和 GSD 里的Cfg_Data字节数对不上;或者配置缓冲区里预先填的内容跟主站发下来的不一致,芯片做逐字节比对时判失败;还有一种少见情况是长度寄存器写在了指针寄存器之后,芯片按默认长度 0 处理。
解决:把 GSD 里的Cfg_Data打印出来,跟DCB_LEN和 DCB 数组内容逐字节对一遍;确认初始化函数里长度寄存器的写入顺序在前面;如果用了模块化组态,检查模块顺序和Modul_Offset是否一致。
5.3 数据交换几秒到几分钟后掉线
现象:正常通信一段时间后自动退回 WAIT_PRM,主站报从站超时,重新参数化后又能撑一段时间,周而复始。
原因:看门狗超时。主循环里有阻塞操作,或者中断被长时间关闭,导致芯片在规定窗口内没被“喂”;也可能是 GSD 里两个看门狗系数配得太紧,正常抖动就超了。
解决:先测主循环最坏执行时间,把看门狗时间配到它的三倍以上;再把 Flash 擦写、长延时这类操作挪到状态机空闲阶段,或者拆成小片执行;中断里只做标志和搬运,别在中断里做浮点运算和字符串处理。
5.4 输入数据偶发错位或跳变
现象:主站读到的输入数据大部分正确,偶尔出现整段错位一个字节,或者某个字的高低位对调。
原因:输入缓冲区在组装到一半时被芯片读走,也就是所谓的半更新;或者缓冲区起始地址是奇地址,16 位访问时地址错位;还有一种是把uint8_t数组强制转换成uint16_t*访问,编译器对齐策略不一样导致偏移。
解决:先在局部数组里把整帧组装完,再一次性memcpy进 DIN,中间不要边算边写;所有缓冲区起始地址强制偶对齐,可以在宏里加一个编译期断言;跨字节访问统一用显式的移位拼装,别依赖指针强转。
5.5 高波特率下偶发丢帧、低波特率正常
现象:9.6 kbit/s 到 1.5 Mbit/s 都稳,切到 3 Mbit/s 以上开始出现 CRC 错误、重传增多,严重时直接掉站。
原因:RS-485 收发器方向切换延迟超过一个位时间;网络终端电阻缺失或阻值不对;MaxTsdr配得偏小,从站还在准备数据主站就已经超时;从站地址和网络里其他设备冲突。
解决:用示波器同时抓差分总线和方向控制引脚,看切换是否在帧间隙内完成;两端终端电阻按规范接;把MaxTsdr往上调一档再看是否消失;用主站的网络诊断功能扫一遍站地址,确认没有重复。
6. 二次开发进阶:把裸机轮询改成中断驱动并上板验证
包里的示例是轮询结构,主循环里不停读状态寄存器。这套写法在单从站、低速率场景够用,但一旦网络里从站变多、周期变短,轮询的响应延迟就会吃掉时序余量。改法不复杂:中断里只做三件事——读中断寄存器、清标志、往队列里投事件,耗时的搬运和业务运算全部留在主循环。
/* dp_isr.c —— 中断只负责记录事件,不搬数据 */ static volatile uint8_t ir_shadow; void SPC3_IRQHandler(void) { ir_shadow = SPC3_RD(REG_IR); /* 先读,读操作会锁存当前中断源 */ SPC3_WR(REG_IR, ir_shadow); /* 按手册写回清除对应位 */ if (ir_shadow & IR_DX_OUT) { q_push(Q_EVT_DX_OUT); } /* 输出数据到了 */ if (ir_shadow & IR_PRM) { q_push(Q_EVT_PRM); } /* 参数化请求 */ if (ir_shadow & IR_CFG) { q_push(Q_EVT_CFG); } /* 配置请求 */ if (ir_shadow & IR_WD) { q_push(Q_EVT_WD); } /* 看门狗超时 */ }队列深度按“一个 DP 周期内可能堆积的最大事件数”取,一般 8 到 16 就够;ir_shadow必须是volatile,否则主循环侧的编译器优化会把它缓存到寄存器里,读到永远不变的值。主循环侧按事件类型分派,处理完再回到等待,这样中断关闭时间能压到几个微秒。
上板验证我固定走一张表,按顺序做完再进现场:
| 顺序 | 验证项 | 通过标准 |
|---|---|---|
| 1 | 寄存器读写回环 | 写 0x55 读 0x55,连续 1000 次无差异 |
| 2 | 状态机流转 | POWER_ON → WAIT_PRM 在 10 ms 内完成 |
| 3 | 参数化与配置 | 主站下发后 200 ms 内进入 DATA_EXCHANGE |
| 4 | 输入输出一致性 | 主站写什么,从站读到什么,连续 1000 周期无错 |
| 5 | 看门狗掉线恢复 | 拔掉总线 5 s 后恢复,能自动重新参数化 |
| 6 | 高波特率压测 | 12 Mbit/s 下连续 1 小时无 CRC 错误 |
第 4 项我一般会写一个小工具,让主站侧发递增序列、从站侧回读比对,错一个就打印周期号;第 6 项没有条件做满一小时的话,至少跑十分钟并统计重传次数。
从那以后我每次拿到一份新的 SPC3 开发包源代码,都先把状态码表和长度寄存器对一遍再上电,先跑寄存器回环、再跑状态机、最后才接主站——这个顺序能保证出问题时你知道是哪一层的问题。希望帮到你。
本文还有配套的精品资源,点击获取