ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

雷赛SMC6480运动控制卡C#上位机开发实战:从DllImport到插补

雷赛SMC6480运动控制卡C#上位机开发实战:从DllImport到插补 简介面向工业自动化开发者的雷赛SMC6480控制器C#编程实例主要用于在.NET环境下通过官方DLL建立与运动控制器的通信连接。压缩包内含完整WinForms示例程序涵盖窗体设计、运动控制逻辑封装、控制器参数读写等核心代码适合需要快速上手雷赛二次开发的工程师参考学习。资源共48个文件以C#源码(cs)和动态链接库(dll)为主体辅以可执行程序、项目文件及升级报告等辅助资料整体压缩包仅311KB轻量易用。已有329人学习下载代码目录包含Bin、Obj、Properties及备份等模块层次清晰通过示例可掌握初始化控制器、发送运动指令、读取运行状态等常用流程为实际工程项目提供可直接复用的适配模板。1. 从C#.rar到带电机的设备雷赛SMC6480控制器例子的落地路线拿到雷赛SMC6480的C#.rar例子包时别指望里面有一个能直接编译通过的完整上位机。大多数情况下你解压出来的是C/MFC示例工程、头文件、dll库以及一份说不清是哪个版本改的接线图。SMC6480是一块四轴DSP运动控制卡设备上要跑的点位运动、直线插补、圆弧插补都在卡里规划上位机只负责把运动指令和参数交给它。真正让C#开发者卡住的不是把电机转起来而是把C语言环境里的函数声明、结构体、回调机制搬到.NET里再把板卡的输入输出信号接对。这篇文章按我实际做过的方式把从解压rar到设备能动、再到能跑插补的全过程拆开讲先认硬件接口和软件栈再给出C#调用dll的最小代码然后讲怎么把示例里的C函数翻译成DllImport声明接着是我踩过的五个坑最后是插补和验证手段。适合正在用雷赛SMC6480做C#上位机、但手里只有旧例子的工程师照着走如果你是第一次接触运动控制卡也能借此理解整个调用链路。2. 先认硬件再写代码SMC6480的接口信号与C#调用方式写硬件之前先确认一点SMC6480的“控制器例子”既不是像单片机那样的离线烧录程序也不是一个必须逐条解释脉冲时序的裸寄存器驱动。例如包里提供的dll已经把脉冲生成、速度规划、IO扫描封装在板卡固件里上位机调函数只是往板卡下发指令。因此C#开发的重点是两条一是把dll的导出函数、参数类型、结构体声明翻译成C#能识别的形式二是把轴使能、原点、限位这几个信号的逻辑关系接对。两者缺一容易让设备“电脑上看着动了机台上就是不动”这是运动控制项目里最典型的返工原因我自己也在这上面翻过车。2.1 板卡接口与输入输出信号类似SMC6480这种四轴脉冲型运动控制卡机台上最常见的信号点大致如下。轴接口方面每个轴一组差分脉冲输出通常包含脉冲、脉冲-、方向、方向-有的还带编码器反馈输入A/A-/B/B-/Z/Z-。差分输出抗干扰强适合长线缆到伺服驱动器如果驱动器只支持单端脉冲需要把负端接到公共端并确认极性。增量式编码器反馈用于读取实际位置很多设备会把它同时接到伺服驱动器和运动控制卡上形成上位机、板卡、驱动器三层的脉冲视图排查时很有用。输入信号方面每轴通常有原点Home、正限位PEL、负限位MEL和急停等专用输入。这些输入在固件里做滤波触发方式分为常开和常闭两类接线时以设备的PLC逻辑为准不能照着示例包图纸直接抄。若把常闭的限位接成常开上电后板卡会认为限位始终被触发导致使能后一运动就报错。这个现象很容易被误判成软件Bug实际上改一下接线图就能解决。输出信号方面主要是报警清除和通用输出。报警清除通常接伺服驱动器的ALM复位端子通用输出可以用于控制气缸、电磁阀等简单执行器。规划C#代码时这部分的读写说明也要一并考虑因为设备动作流程通常需要“读到原点信号—开始运动—到位后打开输出”这样的联动。很多C#脚本因为只处理了运动函数、没处理IO联动联调时才发现流程断在半路。注意不同批次的SMC6480板卡端子定义可能不同。上电前先用万用表确认输入公共端电压再接线。曾经有同事把24V接到了脉冲输出端烧了板上驱动芯片这个错误在售后现场不止见过一次。2.2 C#调用厂商dll的三条常见路线常见做法有三条按工程规模选择。第一条是最直接的DllImport。自己声明雷赛dll的导出函数用using System.Runtime.InteropServices和[DllImport]标注。优点是没有中间层函数名和参数都自己掌控排错时可以直接看到底层错误码缺点是C里随手写的一句宏定义、一个union结构体在C#里都要手工翻译工作量大但对认真做设备的人来说是必经之路。第二条是使用示例包里自带的封装类。很多rar里会附带现成的C#工程里面已有人写好了Motion类把初始化、轴使能、点位运动封装成方法。如果你的设备要求和这个封装类一致直接用它最省时间。但封装类往往只能覆盖“点动、回原点、走绝对位置”这类基础功能且写法针对特定dll版本升级dll后可能编译不过改起来比从头写还麻烦。第三条是C/CLI包一层。把C例子工程编译成托管C程序集再由C#调用。好处是几乎可以原样使用C头文件里的结构体和宏适合需要用到大量私有结构体或回调的复杂场景。坏处是调试麻烦托管与非托管边界报错难以定位非必要不用。我一般只在项目要处理复杂IO联动和自定义回调时选它。应用场景推荐路线原因点位运动、回原点、IO读写DllImport代码透明好排错与示例包功能高度一致封装类改动最少上手最快复杂插补、自定义回调、私有结构体C/CLI保留原生代码语义2.3 最小可用代码从初始化到点动选DllImport路线的话第一步是确认dll文件名和导出函数。不同版本dll可能叫smc6480.dll或gts.dll以随包头文件为准。下面这段代码是一个最小可用示例完成“打开板卡、找到轴、使能、点动”四个动作。using System; using System.Runtime.InteropServices; using System.Threading; class Smc6480MinDemo { // 声明dll导出函数注意调用约定要用Cdecl参数要和头文件一致 [DllImport(smc6480.dll, CallingConvention CallingConvention.Cdecl)] private static extern short smc_board_init(int boardID); [DllImport(smc6480.dll, CallingConvention CallingConvention.Cdecl)] private static extern short smc_board_close(); [DllImport(smc6480.dll, CallingConvention CallingConvention.Cdecl)] private static extern short smc_axis_on(short axis); [DllImport(smc6480.dll, CallingConvention CallingConvention.Cdecl)] private static extern short smc_vmove(short axis, int direction, int speed); [DllImport(smc6480.dll, CallingConvention CallingConvention.Cdecl)] private static extern short smc_axis_off(short axis); static void Main() { short rtn smc_board_init(0); // boardID传0对应板卡拨码设置的ID号 if (rtn ! 0) { Console.WriteLine(初始化失败错误码: rtn); return; } short axis 0; // 用第0轴做演示 smc_axis_on(axis); // 使能电机此刻伺服应处于上电锁轴状态 // direction: 1正方向-1反方向speed单位与示例包配置相关先按1000跑 smc_vmove(axis, 1, 1000); Thread.Sleep(2000); // 让轴跑2秒直观判断方向 smc_vmove(axis, -1, 1000); Thread.Sleep(2000); smc_axis_off(axis); // 关闭使能 smc_board_close(); // 关闭板卡 } }这段代码的逻辑顺序是先初始化板卡错误码非0要立刻输出来看再对具体轴使能使能成功后运行连续运动指令smc_vmove让电机持续朝一个方向转便于确认脉冲方向和线序确认无误后停轴、关使能、关板卡。参数说明要按实际环境调整boardID不是固定0要看板卡上的地址拨码和dll内定义direction正负方向以实际电机旋转为准接错线时不要急着改代码先检查接线speed的数值范围在dll头文件的宏定义里常见做法是百到万级别单位通常是脉冲/秒。如果调完发现电机抖动或者根本没有输出先回头检查使能信号和脉冲模式不要急着改速度参数。对新手来说这段最小代码的价值在于把“好消息路径”先跑通后面再逐步加功能。3. 把示例的C源码翻译成C#点位运动完整移植这一章解决的是整个移植过程里最耗时的部分把C例子里的函数声明翻译成C#的DllImport并完成一次标准点位运动。翻译不是对着头文件一行行照抄而是先建立一套“C类型到C#类型”的换算表再处理结构体和常量最后落实到调用流程。整个过程思路理顺后剩下的是机械劳动。3.1 先读随包头文件找出函数声明、常量和结构体解压rar后第一步不是打开C#工程而是打开.h头文件例如SMC6480.h。运动控制卡的DllImport本质上是“让C#运行时知道原生函数长什么样”因此最权威的参考不是网上博客而是厂商随包的头文件。你要找三类信息函数导出名前缀、参数类型、与C#类型对应的宏定义。常见做法是搜索“EXTERN”和“WINAPI”等标记把与点位运动相关的几个函数抄出来。头文件里通常会有类似这样的声明short WINAPI smc_board_init(unsigned short boardID); short WINAPI smc_pmove(short axis, long targetPos, unsigned long posDir, long moveSpeed);把它抄出来后C#端只需要做一次机械翻译short对应shortunsigned short对应ushortlong在Windows平台是4字节对应intunsigned long对应uint。注意long在这里是个很容易踩的翻译陷阱——C的long在Windows下是32位与C#的long64位不是一回事翻译错会导致栈不平衡函数直接崩溃。我习惯把自己用到的函数全部抄进一个cs文件里“挂墙”需要哪个取消注释哪个这样DllImport和头文件一一对应出错了能逐条核对。3.2 函数签名对照表与常量转换以完整点位运动为例子我把“头文件里的C声明→C#的DllImport声明”整理成一张对照表表里的内容也可以作为你项目里的维护文档。这比每次临时翻头文件快得多尤其是项目过半年再维护时能一眼看出当时的参数约定。C 声明C# 声明short WINAPI smc_board_init(unsigned short boardID)public static extern short smc_board_init(ushort boardID)short WINAPI smc_axis_on(short axis)public static extern short smc_axis_on(short axis)short WINAPI smc_pmove(short axis, long targetPos, unsigned long posDir, long moveSpeed)public static extern short smc_pmove(short axis, int targetPos, uint posDir, int moveSpeed)short WINAPI smc_set_profile(short axis, double acc, double dec, double speed)public static extern short smc_set_profile(short axis, double acc, double dec, double speed)short WINAPI smc_get_position(short axis, ref long pos)public static extern short smc_get_position(short axis, ref int pos)常量方面C头文件里的#define不要直接复制到C#。最常见的是#define SMC_PEL 0x01、#define SMC_MEL 0x02这类位标志在C#里建议写成[Flags]枚举既保证可读性也方便以后用按位与来处理多个信号同时触发的情况。例如一个轴的限位和原点同时有效用枚举位操作一眼就能看清比在整数上做位运算直观得多。[Flags] public enum SmcIoFlag { None 0x00, Pel 0x01, // 正限位 Mel 0x02, // 负限位 Home 0x04 // 原点 }在C#里使用枚举还有一个实际好处如果头文件某次升级改变了常量定义只要改枚举值即可调用代码几乎不动。如果不做这个抽象后面排查别人写的代码时满屏魔法数字会非常痛苦。这算是我在这个项目里坚持的工程习惯之一前期多花十分钟后期省半天排查时间。3.3 点位运动完整流程与加减速参数设定点位运动建议按以下顺序来做设置曲线参数、使能轴、执行运动、查询状态、等待停止。下面代码用绝对定位方式把0轴从当前位置移动到20000脉冲处。运动控制卡的指令是异步的smc_pmove返回后板卡仍在执行运动所以必须轮询状态这个等待逻辑是C#上位机里最容易写错的地方。[DllImport(smc6480.dll, CallingConvention CallingConvention.Cdecl)] private static extern short smc_set_profile(short axis, double acc, double dec, double speed); [DllImport(smc6480.dll, CallingConvention CallingConvention.Cdecl)] private static extern short smc_pmove(short axis, int targetPos, uint posDir, int moveSpeed); // posDir: 0表示负方向1表示正方向。若是相对移动 // targetPos填相对偏移量posDir决定目标方向的符号。 short axis 0; smc_set_profile(axis, 0.5, 0.5, 2000); // acc/dec单位是脉冲/秒² smc_pmove(axis, 20000, 1, 2000); // 绝对位置20000正方向 int status 0; do { smc_get_sts(axis, ref status); // 读取轴状态位0是运动标志 Thread.Sleep(10); // 轮询间隔不要小于10ms } while ((status 0x01) ! 0); int pos 0; smc_get_position(axis, ref pos); // 运动结束后读位置做校验代码逻辑说明smc_set_profile设置梯形加减速曲线acc和dec对应加速段和减速段的斜率speed是最大速度。smc_pmove发出指令后函数迅速返回实际运动由板卡固件完成所以上位机要用smc_get_sts轮询运动状态直到运动标志被清除。轮询间隔设10ms是一个兼顾响应速度和CPU占用的经验值太快会让上位机CPU占用率飘高太慢会让流程衔接产生明显延迟实际项目中我一般把间隔固定在10ms用独立的C#后台线程去跑而不是占用UI线程。参数说明acc/dec过大电机启动瞬间有冲击过小则加减速时间过长影响节拍。常见习惯是先按厂商示例默认值跑通再逐步增大speed观察设备振动和声音是否可接受。位置校验这里用smc_get_position读到的值是板卡发出的命令位置不代表实际机械位置要拿真值必须接编码器反馈。如果在轮询过程中发现状态一直不变也就是运动标志永远不清除先查返回错误码smc_get_rc大多数情况是目标位置超限被板卡内部限位逻辑拦截了。4. SMC6480 C#开发的避坑清单五个常见问题的现象与排查运动控制卡开发有一个特点软件在PC上一切正常不代表机台会动机台动了也不代表每次都会动对。下面这五个问题是我在实际项目里遇到最多、也被人问过最多的全都按“现象→原因→解决”写排查顺序也可以照这个顺序来。4.1 初始化总是返回-1现象smc_board_init返回值不是0程序稳定打印-1板卡指示灯正常但就是初始化不过。原因最常见的是dll位数和进程位数不匹配。32位dll被64位进程加载初始化会返回错误其次是驱动版本和dll不匹配再有一种是电脑里插了两块板卡boardID拨码重复初始化时无法区分。解决先确认dll是32位还是64位用dumpbin或corflags查看然后把C#工程的目标平台显式设为x86或x64不要用AnyCPU。如果确认进程位数没问题重新安装随包驱动并把板卡拨码设成唯一ID后再试。最后用厂商自带的调试工具初始化一次能通过说明硬件链路没问题问题在工程配置。这一步是排查顺序的开头很多C#新手喜欢直接翻DllImport的调用约定反而浪费了时间。4.2 轴能打开电机却完全不动现象使能轴后伺服驱动器的使能灯亮了执行点位指令也不报错但电机纹丝不动。原因多数情况是脉冲信号根本没到驱动器。差分脉冲接线方向接反、驱动器的脉冲模式设成了单端而板卡输出的是差分信号、又或者板卡输出被固件默认禁止都会造成“上位机以为在发脉冲驱动器没收到”的假象。解决先用示波器或万用表测量脉冲输出端的正负差分电压运行时应该有持续的脉冲再确认驱动器接收脉冲的模式参数和板卡输出模式一致。很多时候C#代码本身没问题问题在线序动代码前先把信号测一遍。这个问题在项目刚联调阶段非常多见属于“看起来像软件问题其实是硬件问题”的代表。4.3 方向开关接反导致回零撞限位现象手动正方向正常执行回原点却往反方向冲直到压上限位才停。原因原点搜索方向配置和限位开关极性不匹配。C示例里默认朝正方向找原点如果你的接线把正限位接到了机械负侧程序就会往错的方向找限位输入若按常开处理而硬接线是常闭固件会把它识别成“限位已在触发中”。解决先短接各限位输入确认板卡读到的IO状态与接线图一致再在参数配置里把原点搜索方向和限位极性与实际机械方向对齐。不要靠“改回零方向”掩盖接线错误那样下次换线后还会再翻车。这个问题的危害不在于报错而在于它可能不报错——设备直接顶到机械限位才停轻则损坏机械结构重则伤到人。我现在的习惯是每次接线后先手动触发所有限位和原点信号验证输入逻辑正确后再让程序自动回零。4.4 64位系统下DllImport偶尔崩溃或返回乱码现象程序运行几分钟后突然崩溃或者某些函数返回的错误码明显不对重启后又能正常一会儿。原因DllImport声明里的参数类型和C函数实际签名不一致是最常见原因。上面说过C的long是32位C#里如果写成了long栈帧就会错位另一个原因是调用约定错误雷赛的dll一般用Cdecl有的工程误写成Stdcall函数调用后栈不平衡必崩。解决逐个函数对照头文件核对形参类型把指针对应的参数改成ref关键字把long改成int把bool改成int调用约定统一设成CallingConvention.Cdecl。如果函数表很长在调试时把所有调用包一层try-catch并记录函数名哪个函数出问题就先检查哪个。还有一个细节C里的BOOL是4字节C#里的bool是1字节混用时内存布局错乱遇到这种函数要用[MarshalAs(UnmanagedType.Bool)]显式声明或干脆用int代替。4.5 原点信号抖动导致回零位置不重复现象回零动作能完成但每次停下的位置差几个脉冲到几十个脉冲重复性差。原因原点开关动作瞬间机械振动会让输入信号产生多次跳变固件的滤波参数默认值压不住或者回零速度太快位置在信号有效和电机停止之间错过了太多脉冲。解决降低回零速度并启用固件的输入滤波或软件去抖接线层面保证原点开关供电稳定避免与脉冲线走同一线槽。如果精度要求高建议回零后再加一段“向同一方向低速逼近原点”的二次对准能显著改善重复性。这个问题容易被归类为“玄学”其实大部分是机械振动和速度的交互问题数据测出来就能定位。我见过最简单有效的方案是把原点开关的安装位置移到机械结构刚性最好的地方再配合固件滤波重复精度提升一个数量级都不奇怪。5. 从点位到插补SMC6480直线插补与圆弧插补的C#调用SMC6480这种四轴运动控制卡的核心价值在于板卡能做多轴插补上位机不需要实时计算每个脉冲只需要下发目标点和速度卡固件完成轨迹规划。C#侧调用方式与点位运动基本一致只是函数参数从“单轴位置”变成了“一组轴坐标”但里面仍然有几个容易让人栽跟头的点。5.1 直线插补速度规划与起点终点坐标直线插补在两轴或多轴间同时协调运动使合成轨迹沿直线方向。以两轴为例典型调用方式如下先分别使能两个轴然后用一个坐标数组描述目标点。这里最容易犯的错误是只使能了其中一个轴另一个轴没使能结果跑起来只有单轴动作从代码上看却像插补函数失效。[DllImport(smc6480.dll, CallingConvention CallingConvention.Cdecl)] private static extern short smc_linear_move(short dim2, int[] targetPos, int moveSpeed); short axisCount 2; int[] target new int[] { 10000, 20000 }; // 第0轴到10000第1轴到20000 smc_linear_move(axisCount, target, 3000);代码逻辑说明axisCount参数告诉固件本次插补参与几个轴targetPos按轴顺序给出目标坐标moveSpeed是合成速度。固件会按比例对每个轴分配脉冲频率保证合成轨迹是直线。参与插补的轴必须先分别使能如果某一轴没有使能指令会成功返回但设备只有一个轴在动。参数说明moveSpeed是合成速度不是每个轴的速度。如果某个轴坐标很大而另一个轴坐标很小速度快的轴会按比例减速这是正常现象若希望插补全程匀速应把moveSpeed理解成矢量模长。插补前最好把各轴的acc和dec设成一致的数值。固件在做直线插补时通常按参与轴中最小的加速度执行如果两个轴的加减速参数差异很大轨迹在起点和终点会有突跳机械上表现为抖动。我一般会在插补开始前统一调用smc_set_profile把参与轴的加速、减速、目标速度全部设成同一组值而不是依赖各轴之前点位运动遗留的参数。5.2 圆弧插补圆心位置与方向判定圆弧插补需要三组信息目标点、圆心坐标、以及走顺时针还是逆时针。圆心有两种定义方式绝对圆心坐标或相对起点的偏移实际使用前要确认dll采用哪种。这个确认动作不能省两种定义之间只差一个数写错后轨迹会走成完全不同的圆弧而且不一定报错。[DllImport(smc6480.dll, CallingConvention CallingConvention.Cdecl)] private static extern short smc_arc_move(short axisCount, int[] targetPos, int[] centerPos, short dirFlag); // 圆心坐标按相对起点偏移传入axisCount固定为2 int[] target new int[] { 0, 10000 }; // 目标点 int[] center new int[] { 0, 0 }; // 相对起点的圆心偏移 smc_arc_move(2, target, center, 1); // dirFlag1顺时针0逆时针代码逻辑说明圆弧插补的所有坐标都在同一平面内因此axisCount传2即可。centerPos是否包含目标点在各个版本里定义不同有的版本圆心坐标就是绝对坐标有的版本是相对起点的增量坐标写代码前必须用头文件里的结构体定义或注释确认。dirFlag取值不匹配时轨迹会走成反向的半圆这是圆弧插补最先出现的错误。参数说明圆弧没有覆盖到目标点时大概率是圆心坐标计算不对或方向选反。软件上交换dirFlag能临时解决但要彻底搞对还是要画出轨迹对比圆心位置后再校准。如果设备上正好有触摸屏或示波器我建议先用低速度跑一小段把实际轨迹描出来看一眼比反复改参数快得多。这个习惯帮我省过不少时间尤其是两张图纸对不上的老设备改装场合。5.3 插补缓冲区与运动状态查询连续插补多段轨迹时上位机不能等上一段完全停止再发下一段那样会有明显停顿。SMC6480这类板卡在固件里有运动缓冲队列上位机可以连续下发多段插补指令板卡自动衔接。此时C#代码有两个注意点查询缓冲区剩余量以及设置段间过渡模式。缓冲区满时函数会返回特定错误码此时等待缓冲有空间再继续下发。段间过渡模式一般有急停和连续速度两种连续速度模式适合“轨迹不停顿”的场景急停模式适合每段末尾要求定位锁死的场景。调试时先从慢速开始确认每段轨迹都正常再逐步提高速度避免第一版直接跑高速时发生机械冲击。int[][] path new int[][] { new int[] { 0, 10000 }, new int[] { 10000, 10000 }, new int[] { 10000, 0 } }; foreach (int[] pt in path) { int rtn smc_linear_move(2, pt, 3000); while (rtn BufferFullError) // 缓冲满等待后重试 { Thread.Sleep(5); rtn smc_linear_move(2, pt, 3000); } if (rtn ! 0) { /* 其他错误码处理 */ } }BufferFullError是头文件里定义的错误码常量实际值以dll为准。重试前不要清空缓冲区否则会丢失前面排队的轨迹。这种“填满缓冲区”的下发策略让上位机始终维持缓冲区接近满的状态又不至于溢出是连续轨迹加工类设备的标准做法。对比点位运动的等待式调用这里完全是另一套思路点位关注“是否到位”插补关注“队列是否还能塞下下一条”C#代码里一定要区分这两种模式不能在插补循环里用Thread.Sleep等到位否则轨迹会被上位机卡断。6. 怎样确认设备动得对波形观测与位置校验技巧最后说一个我每次都要做的验证动作把上位机发的脉冲数和实际机械位置对上。验证手段很朴素但能挡住八成以上的装配错误。最直接的办法是观察伺服驱动器的反馈位置。手动发一段固定脉冲数的点位运动例如发1000个脉冲看驱动器显示的编码器计数值是否是1000。如果照例一致再放大到10000确认没有累计误差。若有偏差先检查电子齿轮比和编码器分辨率换算不要急着动C#代码。另一种是用固件提供的命令位置和编码器位置做对照分别读两个数值再相减差值稳定且很小说明轴回零和方向都没问题。我这里习惯写一段校验函数每次设备重启后先执行结果合格才允许进入生产模式。这个习惯是被一次血泪教训换来的。那台机器调试时上位机显示位置正常但产品每次到同一个工位都偏一点查了两天才发现编码器反馈线有一相接触不良偶发丢脉冲。从那以后我每次验收前都会做一次“脉冲计数对照”用增量编码器的Z相或者驱动器反馈做二次确认绝不只信运动控制卡里的命令位置。SMC6480的C#开发整体来说不复杂但细节密度高。把标题里那份rar当成一个待拆解的工程包来看先读头文件、再写DllImport、然后带着方向接线的意识去调试大部分问题都能在半小时内定位。如果你手头正好有这套板卡希望上面这些过程能帮你少走几步弯路也希望帮到你。本文还有配套的精品资源点击获取
返回列表