
去年做伺服驱动器的现场总线扩展时客户一句话把项目定了调“别的变频器都支持EtherNet/IP你们这款伺服什么时候能接AB的PLC”老产品的功率板和控制板早就冻型了DSP的算力要留给电流环和位置环指望它再跑一套完整的EtherNet/IP协议栈不太现实换主控更不现实。最后定下来的方案是在驱动器内部塞一块嵌入式通信小板由小板专职负责EtherNet/IP协议栈主控板跟小板之间用SPI把输入输出数据周期性地搬来搬去。这套方案落到固件工程师手里第一步往往就是“把厂家给的固件Demo程序适配到自己的主控板上”。这活儿听起来只是改改引脚、调调参数实际动手才会发现坑全藏在数据组织、时序握手和协议映射这些不容易一眼看到的地方。这篇文章把我这次适配改造的完整过程和排查经验整理出来给准备做“伺服内嵌EtherNet/IP”或者“通信小板加SPI”项目的朋友一个参考。文章不是原理性科普是我个人项目里真实踩过的路径和结论偏实践的成分居多。1. 为什么是“内嵌小板SPI”而不是网关或一体化SoC1.1 三种加EtherNet/IP的路线我为什么选了中间这条接到需求后我先把可行方案列了一遍。市面上给老设备加EtherNet/IP无非三条路外挂独立网关、设备内部塞通信小板、直接换一颗能跑协议栈的大SoC主控。方案硬件成本开发工作量通信延迟现场安装外挂网关EtherNet/IP转Modbus之类每台设备多一个模块的钱小基本是配置工作多一跳转换延迟稍大占用柜内空间多了外部接线内部嵌通信小板主控SPI访问只多一块小PCB的钱中主要工作量在固件适配SPI搬运周期可做到1ms以内藏在壳子内部用户无感换主控SoC协议栈直接跑在主控上主控价格高、硬件改动大大涉及整板重新设计和认证最低无感外挂网关最大的问题不是性能而是这东西一旦卖给终端客户客户看到驱动器旁边还挂着一个铁盒子第一反应就是“你们这个方案是不是临时拼的”。而且网关模式还要单独供电、单独接通讯线故障点直接翻倍。换一体式SoC当然是最干净的但老产品的控制板不可能在现有生命周期内重新画板ODVA的一致性测试、协议栈授权这些都是实打实的成本和时间。内嵌小板方案基本是唯一能在不改动主控板的前提下把EtherNet/IP“长”进驱动器里的办法。我这次用的就是这个方案一块比硬币大不了多少的通信板通过排针或者连接器直接压在驱动器的控制板上。1.2 SPI为什么够用算一笔时间账有人一听说SPI就有点发怵觉得和EtherNet/IP动辄百兆的带宽没法比。这里要分清一个概念EtherNet/IP的物理层是标准以太网那是通信小板自己处理的事主控和小板之间的SPI只负责搬运“应用层”的周期数据这个数据量小得可怜。以伺服来说PLC每个周期发给驱动器的无非是控制字、速度/位置给定驱动器回给PLC的是状态字、实际位置、实际电流。假设输入输出各32字节一个周期总共64字节。SPI跑10MHz完整传64字节需要的时间是64×8/10MHz约51微秒。就算把字节间的间隙、片选切换、以及两次读写的间隔都算进去按100微秒的裕量来算对比1ms的通讯周期只占10%。而且这还是在8位传输模式下算的如果模块和主控都支持16位模式64字节只需要32次传输开销还能更小。SPI和IIC的区别在这里体现得很明显。IIC是半双工、带应答位、需要从机地址适合去配置寄存器、读传感器这类低速存取但要在一个固定周期内大批量搬运数据IIC的效率和时序开销都吃亏。SPI全双工、四根线、没有地址仲裁同一时刻收发同时进行天然适合“主控周期性把一块数据扔给小板、同时从小板拿回一块数据”这种搬运场景。伺服内部的通讯就是这样不需要什么花哨的总线协议要的就是一个稳定、高速、可控的数据管道。1.3 核心思路把协议栈当成“黑盒外设”来用内嵌小板方案的本质是把所有EtherNet/IP的协议复杂度全部关在小板里。小板内部跑着完整的CIP协议栈对外处理网络连接关系对内则以一个双口存储器DPM的形态暴露给主控。主控侧的固件根本不需要去理解EtherNet/IP的连接管理、CIP对象实例这些深水区内容只需要做到一件事把DPM里的数据按时读出来、把要发出去的数据按时写进去。这个思路决定了后面整个适配改造的工作内容。我得到的那个Demo程序本质上就是厂家示范“如何读写DPM”的样例不是完整的伺服应用。所以这篇博文后续讲的所有适配动作核心都是在回答三个问题Demo程序里那一套SPI驱动怎么移植到我的主控上、DPM的寄存器语义怎么跟我的伺服数据对上、PLC组态里的数据组织和我的固件定义怎么保持完全一致。2. 拿到Demo程序之后先别急着改代码2.1 Demo程序的典型组成先认清手上有什么厂家的Demo程序一般不是一个孤立的文件而是一套完整的示例工程加配套文档。我这次拿到的压缩包里大致有这些东西通信小板的协议栈固件或者预烧录固件默认已经跑起来了不需要动主控示例工程用的是厂家评估板上的某个MCU演示了SPI初始化和DPM读写一张寄存器地图文档定义了DPM里每个地址的含义EDS文件这是EtherNet/IP的灵魂——PLC组态工具靠它识别设备少量应用笔记说明上电时序、看门狗、配置工具使用方法。拿到这些文件最容易犯的错就是直接打开示例工程看看SPI初始化代码然后把引脚改成自己的板子就开始编译。Demo工程里的MCU型号、引脚、时钟树、中断优先级全部是厂家的习惯直接移植大概率能跑通一半然后卡在一个很隐蔽的时序问题上。我建议把Demo当成“行为参考”而不是“代码蓝本”先花半天把它的设计逻辑读明白再动手。2.2 先画出DPM寄存器地图再动一行代码双口存储器是理解整个方案的地图。以我这次用的某款模块为例这类模块的DPM设计大同小异主控通过SPI访问一个连续的寄存器空间按16位字宽组织大致是这个样子字地址偏移方向含义0x00主控写命令寄存器主机向模块下发控制命令0x01主控读状态寄存器模块向主机反馈通讯状态0x02主控写控制请求往模块申请刷新输入数据0x03主控读事件标志上报EtherNet/IP连接状态变更0x10~0x1F主控读输出数据缓冲区PLC发给伺服的数据放在这里0x20~0x2F主控写输入数据缓冲区伺服要发给PLC的数据写到这里不同厂家的地址分配差别不小有的模块还会把某些状态位合并在同一个字里但大体的“命令/状态区在前、数据缓冲区在后”的结构是一致的。拿到Demo后第一件事不是看SPI收发函数而是把这张寄存器地图抄到自己的设计笔记里并标清楚哪些位是“模块置位、主控清除”哪些是“主控置位、模块清除”。这两个方向搞反后面调试会极其痛苦。2.3 三段式握手逻辑必须彻底搞懂DPM访问不是简单的“读一块、写一块”核心是那段握手逻辑。我这次遇到的模块它的流程是这样的模块从EtherNet/IP网络上收到PLC发来的一组新输出数据后会把输出缓冲区对应的新数据标志位置位主控发现标志位置位读取输出缓冲区然后写入输入缓冲区主控写命令寄存器的应答位通知模块“数据我已经拿走了输入数据也准备好了”模块清除新数据标志。如果主控读完数据不清标志模块会认为上一次数据还没被消费下一周期可能不更新或者干脆把通讯连接判定为异常。反过来如果主控在模块还没更新数据时就重复读同一块地址拿到的就是上上周期的旧数据。有些模块还加了主控看门狗机制主控必须在规定时间内周期写一次命令寄存器的看门狗位否则模块会认为主控已死主动断开与PLC的EtherNet/IP连接。这个看门狗周期一般是几十到几百毫秒Demo程序里通常放在主循环里移植的时候要特别小心——如果伺服的主循环因为某些阻塞操作停顿超过看门狗时间整条连接就掉了这是现场很常见的隐性故障。我的做法是单独开一个定时器中断在中断里只做看门狗喂狗操作和正常的SPI数据搬运解耦。3. 按自己的主控平台移植SPI驱动与通讯任务3.1 SPI模式、位宽和片选先和模块“对表”SPI最经典的问题就是模式不匹配。CPOL决定时钟空闲电平CPHA决定数据采样沿组合起来有四种模式SPI模式CPOLCPHA数据采样边沿Mode 000上升沿采样Mode 101下降沿采样Mode 210下降沿采样Mode 311上升沿采样通讯小板作为从机它的时序是固定的主控必须配合它。判断模式最可靠的办法就是查模块手册里的SPI时序图它会明确画出SCLK空闲电平是低是高、数据在哪个边沿稳定。如果手册不完整可以在SPI总线上挂个逻辑分析仪让模块上电后随便发点状态信息看它输出的MISO数据在哪个边沿对齐就倒推出正确的模式了。我这次遇到的模块是Mode 0但另一个供应商的模块用的是Mode 3完全没有规律可循千万别想当然。位宽方面模块DPM通常是16位字宽建议SPI也用16位传输模式一次传一个寄存器地址自动递增读写效率会好很多。STM32用STM32CubeMX配置SPI时记得把Frame Size设成16 bits数据长度、极性、相位在初始化结构体里一改就能生效方便得很。片选这里单独说一下“硬件片选与软件片选”的选择。硬件片选NSS硬件管理的好处是极简但很多MCU的硬件NSS在SPI连续传输时无法灵活控制片选保持时间尤其是地址递增的多字节突发访问场景模块往往要求整个读写序列期间CS始终保持低电平。软件片选用普通GPIO控制CS引脚灵活度最高可以精确控制拉低时序、保持时间、拉高时间。虽然要多写两行代码但对于适配不同模块来说软件片选几乎是必备能力。我所有项目里只要涉及外部从机模块一律软件GPIO控制CS从没后悔过。3.2 从轮询到“查询超时错误计数”的改造厂家Demo为了演示简单SPI收发函数往往是清一色的轮询查询标志位。这在评估板上没问题但放在伺服主控里就有隐患轮询是一种阻塞式等待在等待SPI完成的过程中整个控制任务被卡住电流环中断要是抢不到CPU就不是闹着玩的了。我移植后的思路是SPI传输本身还是用查询方式做但外层包上“超时保护”和“错误统计”并且整个SPI事务放在定时器驱动的通讯周期里执行而不是放在主循环里。uint16_t spi_xfer_word(SPI_TypeDef *spi, uint16_t data) { uint32_t timeout 10000; while (SPI_I2S_GetFlagStatus(spi, SPI_I2S_FLAG_TXE) RESET) { if (--timeout 0) { comm_error_count; return 0xFFFF; } } SPI_I2S_SendData(spi, data); timeout 10000; while (SPI_I2S_GetFlagStatus(spi, SPI_I2S_FLAG_RXNE) RESET) { if (--timeout 0) { comm_error_count; return 0xFFFF; } } return SPI_I2S_ReceiveData(spi); }为什么不用DMA加中断对于1ms周期、64字节级别的数据量DMA的优势在于释放CPU但代价是代码复杂度、数据一致性处理DMA还没搬完主控又在改缓冲了以及调试难度都上来了。伺服主控在一毫秒内要干的活很多SPI这类查询方式只要把单次阻塞控制在几十微秒内对实时性影响是可控的。等你跑通整个链路之后如果想更稳一点再把SPI改成DMA模式也不迟DPM的寄存器访问逻辑不需要变变的只是底层传输实现。3.3 多字节连续读写最容易翻车的两个细节第一个细节是片选时序。有些模块要求CS拉低后经过最短建立时间才能来第一个时钟CS拉高后又有最短释放时间。软件GPIO片选的好处这时候就体现出来拉低CS后可以塞几个空操作延时保证建立时间整个读或写序列完成后先等最后一个时钟完全结束再拉高CS。不要一上来就追求极致速度把CS拉低后立即发数据这在实验室可能没事换一块板子、连线变长了可能就偶发失败。第二个细节是MISO线的电气状态。SPI从机的MISO在不被选中时通常是高阻态主控的MISO引脚如果内部没有上拉总线就悬空了读回来的数据可能随机跳变。最简单的办法是在主控MISO引脚上加上拉电阻或者把MISO配成内部上拉输入模式。如果同一根SPI总线上挂了多个从机这个上拉就更是必须的。3.4 上电时序和复位联动通讯小板内部有完整的嵌入式处理器和以太网物理层上电后完成自身初始化需要时间。主控不能一上电就按固定周期去读写DPM必须先去读状态寄存器的Ready位等模块就绪了再开始正常通讯。Demo程序一般是在初始化里写死一个Delay再开始但我建议改成轮询Ready位加超时报错这样无论模块启动快慢都能自适应。还有一个容易被忽略的问题如果主控板和小板共用同一个复位信号或者小板挂在主控的复位定时电路下面那么主控复位也会把小板一起复位。Reset信号释放后双方同时在启动就会存在一段“互相找不到对方”的时间窗口。我踩过一次这个坑主控拉复位后立刻跑通讯逻辑模块还没Ready结果是连接一直建不起来害得我以为是SPI时序错了查了半天。正确做法是主控复位后先等模块Ready信号再加一个安全余量然后再进通讯流程。4. EtherNet/IP数据组织与伺服寄存器映射4.1 Assembly实例PLC眼中的“输入输出”到底是什么EtherNet/IP的数据交换核心概念是Assembly对象。PLC作为Scanner扫描器和驱动器建立I/O连接后按配置的RPI周期隐式报文交换数据。PLC发过来的一包数据叫“输出Assembly”对应到伺服侧就是控制字、速度给定、位置给定这些驱动器发回PLC的“输入Assembly”就是状态字、实际位置、实际电流反馈。这里要特别留意一个合同级别的匹配问题PLC组态时选择的Assembly实例大小必须和通信小板固件里定义的完全一致。厂家Demo程序默认的Assembly可能只有4字节甚至更少但伺服至少要控制字加一个32位给定这就是8字节。改Assembly实例不是随便改改缓冲区长度就行有的模块支持在线配置有的需要在EDS工具里重新生成配置并烧写到模块里。我这次的做法是先在模块配置工具里把输出Assembly定义成符合伺服需求的结构——控制字2字节速度给定4字节状态字2字节位置反馈4字节再加上备用字节凑成对齐——然后让PLC组态工具加载对应的EDS文件去建立连接。整个过程中最怕的是主机和模块两边各自定义了一套结构结果连接建立后数据错位。后面第5节会详细说这种错位的排查。4.2 控制字、状态字的位定义与使能握手伺服通讯的“最后一公里”是控制字和状态字。PLC写的控制字到了伺服侧固件要把它拆成具体的动作伺服侧的状态字要能让PLC一眼看出设备当前处于什么状态。这里我采用了一种接近CiA 402标准的位定义很多伺服厂商都遵循这种风格上位机工程师上手也快。控制字的关键位定义位名称含义bit0切换ON准备就绪状态机进入待命bit1使能电压允许直流母线工作bit2快速停机1允许运行0执行快停bit3使能运行松开抱闸、允许输出力矩信号使能不能一次性把所有位置1标准做法是先写0x0006bit1、bit2为1等待状态字确认再写0x0007最后写0x000F全部置1让状态机按顺序推进。如果PLC梯形图直接给0x000F有些驱动器也能接受但为了兼容性和安全性建议固件侧把状态机跳转条件做严谨。状态字这边我们暴露给PLC的关键位是“准备好”、“运行使能”、“有故障”和“回到原点”这几个位是PLC判断下一步动作的基础。4.3 字节序和32位数据的坑大小端对不上EtherNet/IP的网络字节序是Big-Endian也就是高字节在前这是工业以太网的既定规则。而绝大多数主控MCU比如STM32、Kinetis、C2000内存里是Little-Endian低字节在前面。这个矛盾不会自动解决因为通讯小板的DPM里摆的是“从网络上收到的原始字节”它不做应用层转换。这意味着我的固件读取DPM里的速度给定时必须手动把两个16位寄存器拼成一个32位值同时调整字节序。我踩过这个坑后的教训是不要指望编译器或者库函数帮你处理直接在代码里写清楚转换逻辑经过充分测试后固定下来。uint32_t get_speed_ref_from_dpm(void) { uint16_t hi read_dpm_word(DPA_SPEED_REF 0); /* 高16位 */ uint16_t lo read_dpm_word(DPA_SPEED_REF 1); /* 低16位 */ uint32_t raw ((uint32_t)hi 16) | lo; return (int32_t)raw; /* 转成有符号数负值代表反转 */ }除了字节序还要和上位机约好“单位”。伺服项目里最常见的单位约定是速度用0.1rpm位置用编码器脉冲数电流用0.1A。PLC发来1500驱动器内部就要知道这是150.0rpm反馈位置是8421个脉冲PLC那边也知道这是第8421个脉冲。这种约定必须在文档里写死并且写进联动调试的测试表里逐项验证。哪怕单位差一位小数现场都是安全事故级的错误。4.4 通信断链后的安全行为这是安全审计必查项EtherNet/IP的连接都有RPI超时检查机制。如果PLC异常断电、网线被拔掉、或者主控与小板之间的SPI看门狗超时通讯小板会检测到连接丢失。此时伺服绝不能继续执行最后一次收到的给定值必须进入受控停机状态。我在固件里实现了两重保护第一重主控持续监控状态寄存器的“连接有效”位一旦该位清零立即触发通讯故障输出禁用、抱闸合闸第二重主控自己维护一个“数据新鲜度”计数器即使状态寄存器的连接位还亮着但如果主控连续N个周期发现新数据标志没有置位也判定数据失效。这两重保护在Demo程序里往往没有现成的需要自己加。联调的时候我就是故意拔掉网线来验证这个行为的测通之后才敢跟客户说“可以验收”。5. 联调实测从PLC扫不到站到字段错位的完整排查5.1 问题一PLC组态工具扫不到EtherNet/IP从站现象描述PLC侧用RSLogix现在叫Studio 5000扫描网络找不到通信小板的设备条目。排查链路我按这个顺序走第一步物理层和IP层。先确认小板的网口指示灯是否正常IP地址和PLC的网段是否一致。很多模块出厂默认是DHCP或者BootP获取IP而现场没有DHCP服务器模块就一直拿不到地址自然扫不到。解决办法是用厂家配置工具扫描模块的MAC地址给它设一个固定IP或者通过模块上的拨码开关选择静态IP模式。第二步EDS文件。如果网络能扫描到节点但扫描结果是一个“未知设备”那说明EDS文件没装好。EDS文件定义了设备的厂商ID、产品类型、产品代码和版本号PLC组态工具必须凭这些信息识别设备。我这里遇到的情况是模块固件升级过产品代码变了但调试电脑上的EDS还是老版本导致PLC识别成了Unrecognized Device。更新EDS后问题解决。第三步网络上的重复IP。这个比较隐蔽现场如果有多台设备被配置成了同一个IP扫描结果可能时有时无。在电脑上用Ping命令就能测出来——如果能Ping通但设备列表里没有出现优先怀疑EDS如果反复Ping不稳定优先怀疑IP冲突。5.2 问题二I/O连接建立成功但主控读回来的数据全是旧的现象描述PLC组态显示连接已建立PLC侧写数据也正常但伺服主控读到的给定值一直是0或者上一次的值完全没跟着PLC的写入变化。这个问题的隐蔽性很高因为它没有任何错误标志。我的排查链路是这样的先在主控侧观察状态寄存器的“新数据标志”位是否在周期性地翻转。如果这个标志根本不动说明模块压根没有把新的输出数据放进DPM。原因集中在两个方向要么是PLC侧虽然建立了连接但输出Assembly数据和模块固件预期的长度不匹配数据根本没被正确解析要么是模块内部的数据方向配置反了——某些模块需要配置哪个方向的数据才落地到DPM配置不对的话数据只存在于模块自己的内存里主控看不到。如果标志确实在翻转但读出来的内容不对那就是SPI读写地址或者字宽对不上。我这次遇到的就是一个经典错位案例模块手册里DPM地址是按16位字给的但我参考的Demo代码里某个偏移量用的是8位字节寻址结果从第3个寄存器开始所有字段全部错位。排查方法是抓SPI波形用逻辑分析仪把地址、数据逐字节拉出来和手册里的寄存器地图逐个对照两三个周期下来就能看出是哪一位地址计算错了。5.3 问题三控制字发过去没有反应状态字停在“待机”现象描述PLC把控制字写成了0x0007伺服侧状态字的“准备好”位置了但“运行使能”一直不置位。这个问题绕不开状态机时序。第一步确认伺服侧固件里收到的原始值是不是真的是0x0007。做法是在伺服固件里把收到的控制字原样回传到PLC的某个诊断字段在PLC里看看到底是多少。这里最容易出鬼的就是字节序——PLC组态工具里看着是0x0007经过小端解析之后固件拿到的可能变成了0x0700。第二步检查状态机跳转条件。如果控制字确实收到了但状态字不推进一步说明固件里的状态机要求按顺序跳转PLC一次给到0x000F中间跳过了低功耗和母线使能的确认步骤。我最后的解决办法是和上位机工程师约定让PLC按0x0006到0x0007到0x000F的顺序发送并且每一步等待状态字确认到位后再发下一步。这个流程反而是双方沟通成本最高的部分——PLC程序员习惯写一个“启动”按钮直接给全置位但安全标准要求状态机严格跳转。5.4 联调排错的工具和方法总结现象优先检查项常用工具/手段扫不到设备IP网段、EDS文件、重复IP厂家配置工具、Ping、网络扫描连接建立但数据不对Assembly长度匹配、DPM地址偏移、字节序逻辑分析仪抓SPI、回环诊断字段控制字发过去无响应状态机时序、控制字字节序、使能握手PLC诊断监视、固件内部回读变量数据偶发跳动SPI模式、CS时序、MISO上拉示波器抓CS与SCLK、加看门狗重试6. 几个容易被忽略的细节和往后的演进空间6.1 板级硬件的坑电平、地、干扰通信小板一般工作在3.3V老款伺服控制板如果还有5V的外设芯片SPI引脚电平匹配必须确认。如果模块IO支持5V容忍可以直接连如果不支持中间要加电平转换芯片不能靠分压电阻糊弄——SPI时钟跑几兆赫兹的时候分压电阻的波形上升沿会劣化导致数据采样出错。伺服驱动器是出了名的强干扰源功率部分IGBT开关瞬间产生的噪声会耦合到信号线上。通信小板和控制板之间的连接器一定要用带地引脚的多针排插不能只用几根杜邦线飞线调试。我最初用杜邦线联调时SPI偶尔出现CRC错误以为是代码问题后来换了排插后问题消失才意识到是共地阻抗和线间串扰造成的。6.2 参数持久化IP和配置别被主控“抢走”通信小板的IP地址、网关、Assembly配置一般存储在模块自身的非易失存储器里由厂家配置工具写入。主控固件在上电初始化时要做的就是等待模块加载完自身配置不要去主动改写。我见过一个项目主控为了图方便每次上电都往模块里写默认配置结果现场客户通过配置工具改好的IP地址一断电就被覆盖回出厂值最后排查半天才定位到主控在捣乱。正确做法是设计一个配置校验机制主控上电后读取模块的当前配置版本如果版本和固件要求的一致就继续正常通讯如果不一致才走配置下发流程。这样既兼顾产线批量写入又不破坏客户现场的手动配置。6.3 再往前一步从“黑盒模块”到完全自主可控内嵌小板方案跑通之后有人会问一个问题EtherNet/IP这个协议有源代码吗答案是有的现在市面上有商用EtherNet/IP协议栈源码也有基于开源硬件平台的自研实现。如果产品量大到能摊薄协议栈授权成本或者客户有深度定制需求可以考虑把协议栈从通信小板上移植回主控侧省掉一块小板。但这条路远比“适配Demo”复杂要做ODVA一致性测试、要维护CIP对象模型、要处理网络和电机控制两个子系统的实时调度。我的个人看法是量小的项目老老实实用模块量大到年出货几千上万台再考虑自主研发这个优先级基本不会错。另外一个演进方向是CIP Motion。现在这套方案里PLC看到的是普通I/O连接做点位控制够用但如果客户要求EtherNet/IP总线上的多轴同步插补就需要用到CIP Motion的运动控制对象。这个复杂度又上了一个量级两边都要支持运动控制行规通信小板的方案能不能做取决于选用的模块是否开放CIP Motion对象选型时要提前问清楚。最后说点个人体会。这种“主控加通讯小板”的项目真正决定进度的往往不是代码量而是对寄存器地图的熟悉程度。我前前后后花了两天时间把Demo程序吃透中间有半天几乎什么都没写就是对着DPM寄存器地图一遍遍推导数据流。结果后面移植和联调反而非常快。所以如果你手头也有类似的Demo要适配我的建议就一句话先别急着打开编译器把DPM的每一个位都吃透再动手写代码你会回来感谢我的。