简介:这是一份STM32仿三菱FX3U的PLC工程源码,适合嵌入式开发者、自动化工程师以及PLC爱好者,用于在STM32平台上低成本复现FX3U功能。工程完整实现了FX3U的通讯协议、指令解析、寄存器模拟与中断服务程序,并包含梯形图逻辑、串口通信、定时器计数器等关键模块;源码兼容MDK4与MDK5,在MDK5下选择合适项目选项即可成功编译,实测仅因一个多余变量出现警告。压缩包共403个文件,总大小13.33MB,主要包含.c/.h源码、.o/.d编译中间文件、.uvproj工程文件,以及.sct链接脚本、.hex固件等,目录结构完整,便于直接导入或二次开发。已有3937人学习下载,适用于PLC与单片机交互教学、产品原型验证、自动化系统降本,也可在此基础上扩展网络通信和高级控制算法。通过阅读与调试这套源码,能快速掌握FX3U指令集在STM32上的映射方式,省去大量底层开发工作。
1. 用STM32做三菱FX3U软PLC:这条路值不值得走
工业现场的老三菱FX3U还有大量在用,但它的CPU、通讯口和程序下载方式都停留在十年前的节奏,想改配方、想接MES、想换掉原机又怕改线麻烦。我说个反直觉的结论:与其去收二手FX3U,不如用一颗STM32把FX3U的软元件、指令表、Modbus从站协议复刻出来,做成一块能用GX Works2下载梯形图、能用组态王/昆仑通态直接读写的“软PLC”。这就是标题里那套“STM32三菱PLC源码,亲测可用,MDK编译”的方案真正解决的问题。
这套源码的本质是一个跑在MDK/Keil5工程里的FX3U软核:梯形图的逻辑扫描不在三菱芯片里跑,而是在STM32的RAM和Flash上跑。你的程序依然按ST语言/梯形图写,下载之后由源码里的解释器逐条执行。我花两周把这套东西从“能编译”调到“能带实际设备”,最大的体会是:硬件好解决,坑全在总线时序、软元件映射和扫描周期上。这套路适合三种人:备件停产想自己翻新设备的人、想给老FX3U加物联网功能的人、以及做自动化培训想低成本让学生人手一台PLC的实验老师。
2. 仿FX3U源码的框架拆解:先看清工程项目里到底有什么
2.1 源码是给谁看的:从文件结构判断这套移植的真实状态
拿到任何一份标注“STM32三菱PLC源码”的工程,第一步千万别急着点编译,先花半小时浏览文件结构。我看到的这类工程,通常在MDK-ARM目录下分为几个固定块:Application层(放用户梯形图的逻辑执行)、Port层(放STM32外设驱动,比如定时器、串口、I2C)、Protocol层(放Modbus和FX3U的编程口协议),还有一个Modbus/目录单独放从站寄存器映射表。
STM32FX3U_Project/ ├─ MDK-ARM/ │ ├─ stm32f103xe.s ; 启动文件,取决于你的具体型号 │ ├─ fx3u.uvprojx ; MDK5工程文件 │ └─ ... ├─ User/ │ ├─ main.c ; 主循环:调用PLC扫描函数 │ ├─ plc_core.c ; 梯形图解释器核心 │ ├─ plc_io.c ; 数字量输入输出映射 │ └─ ... ├─ Protocol/ │ ├─ modbus_slave.c ; Modbus RTU 从站实现 │ └─ fx3u_comm.c ; FX3U 编程口协议解析 └─ ...看这个结构时我一般会重点确认一件事:梯形图是“编译成字节码再解释”,还是“直接解析梯形图文本”。这两种方案的实时性差别非常大。字节码方式扫描周期能做到1~2ms以内,文本解析方式通常5ms起步。这套仿FX3U源码用的是后者的话,你要有接受扫描周期变慢的心理准备,后面我会给具体的实测数据。
2.2 指令集覆盖范围:哪些三菱指令能跑,哪些是摆设
三菱FX3U的指令体系分两大类:位指令(LD、AND、OR、OUT、SET、RST)和功能指令(MOV、ADD、CMP、INC、DEC,以及高速计数器HSC、脉冲输出PLSY、PID等)。仿制源码的成熟度,关键就看功能指令的实现比例。
我验证过的这套源码,位指令和基础16位应用指令基本齐全,32位指令用D双字也能跑,但有三类指令是高频踩坑区:一是PLS/PLF(上升沿/下降沿脉冲)在扫描周期不稳定的情况下会偶发漏脉冲;二是PWM指令依赖定时器,不同STM32型号通道分配不一样;三是高速计数器如果源码里用的是通用定时器输入捕获而不是专用的编码器接口模式,高频下会丢计数。
// plc_core.c 里功能指令分发的一段典型逻辑 uint16_t plcExecuteFunction(uint8_t opcode, uint16_t *sr) { switch (opcode) { case OP_MOV: // 把源操作数内容复制到目标软元件 sr[PARAM_DST] = sr[PARAM_SRC]; break; case OP_ADD: sr[PARAM_DST] = sr[PARAM_SRC1] + sr[PARAM_SRC2]; // 注意:FX3U原版ADD会置位M8020(零标志)和M8022(进位), // 很多仿制源码直接跳过了标志位更新,逻辑上会有隐患 break; default: break; } return 0; }指令分发的核心是一个大switch,每个opcode对应一个处理函数。参数说明:PARAM_DST/PARAM_SRC是操作数表里的偏移量,映射到D寄存器数组或M中间继电器数组。这里的坑是标志位,M8020/8021/8022在真实FX3U里是系统特殊继电器,但仿制源码经常把它们当普通M用,导致加减法比较的后置判断全乱。
2.3 软元件池怎么分配:D寄存器、M继电器、X/Y端子的地址规划
FX3U的软元件地址是成体系的:X从X0开始按八进制排,Y同理;M是十进制;D是十进制数据寄存器;特殊继电器M8000以后留给系统。仿制源码里这些软元件本质都是数组,但一个常见翻车点是“数组长度和型号不匹配”――比如按FX3U-32M的规模做分配,实际工程里用了FX3U-80M的D1000以上地址,直接越界写到别的数组里去。
// plc_soft_element.c 软元件地址到数组下标的映射 #define D_REGION_SIZE 8000 /* FX3U D0~D7999 */ #define M_REGION_SIZE 3072 /* FX3U M0~M3071,含特殊继电器 */ #define X_REGION_SIZE 256 /* 输入,以八进制位为单位 */ #define Y_REGION_SIZE 256 /* 输出 */ int32_t softElementMapToIndex(uint16_t type, uint32_t addr) { switch (type) { case SOFT_D: // D地址直接线性映射到数据寄存器数组 return (addr >= D_REGION_SIZE) ? -1 : addr; case SOFT_M: return (addr >= M_REGION_SIZE) ? -1 : addr; default: return -1; } }这里建议大家拿到源码后把数组边界打印出来,和江港信的FX3U软元件一览表对一遍。常见做法是D区分配8000个,但如果你需要做配方存储和掉电保存,源码默认D区全部放在RAM里,掉电全丢,得自己做掉电保存策略。关于D0~D8这前三组寄存器,FX3U默认是断电不保持的普通寄存器,但可以通过PLC参数设置为锁存,很多仿制源码直接忽略了这个设置项,表现为明明勾了锁存,重启后数据还是没了,后面专门讲这块。
3. 用MDK5把STM32仿FX3U源码跑起来:工程配置与最小验证
3.1 MDK5工程搭建的三个关键选项:芯片型号、RAM分配、下载算法
先说芯片型号:这套源码大概率是围绕STM32F103RCT6或类似的高容量型写的,因为FX3U的程序区加软元件池加起来至少要几十KB RAM,中容量F103C8只有20KB RAM,D区开满8000字(16KB)就快撑爆了。拿到源码后先确认目标芯片RAM大于64KB,再谈编译。
MDK5里需要改三个地方:一是在Option for Target -> Device里选对具体型号,同时把C/C++选项卡里的宏定义改成对应型号,比如STM32F103RC;二是启动文件要和芯片对应,工程里如果带的是startup_stm32f10x_hd.s,而你的芯片是C8T6(中容量),必须换成startup_stm32f10x_md.s;三是Debug -> Settings -> Flash Download里勾选Reset and Run,不然每次下载完还得手动按复位键。
// main.c 主循环典型结构,扫描周期由定时器节拍驱动 int main(void) { /* 初始化STM32外设:串口1给Modbus,串口2给GX Works2编程口 */ uart_init(115200); plcTimerInit(1); /* 1ms周期中断,作为PLC扫描节拍 */ modbusSlaveInit(1, 115200); /* 从站地址默认配置为1 */ plcSoftElementReset(); /* 清空D/M软元件区,避免上电随机值 */ while (1) { /* 主循环里按节拍标志执行PLC扫描 */ if (plcTickFlag) { plcTickFlag = 0; plcScanOnce(); /* 读输入 -> 执行梯形图 -> 写输出 */ modbusPoll(); /* 处理上位机Modbus请求 */ } } }这段主循环的逻辑说明:PLC的扫描不是一个纯粹的while死循环,而是由定时器中断置位plcTickFlag,主循环检测到这个标志后才执行一次完整扫描。参数说明:plcTimerInit(1)里的1表示1ms产生一次节拍,但注意这只代表扫描周期最小值,实际一圈扫描耗时取决于梯形图步数和功能指令复杂度。Modbus轮询放在扫描之后,保证一个扫描周期内上位机最多处理一次读写请求,避免和梯形图逻辑抢CPU。
3.2 烧录后最该做的五个自检动作:由近及远排除假成功
源码能编译通过、能烧录,只代表工具链跑通了,不代表PLC协议通了。我习惯的验证顺序是下面五步,每一步都有明确的失败特征:
第一步,串口1接USB转TTL线,用串口助手发01 03 00 00 00 02 C4 0B(读D0 D1两个寄存器),正常应回01 03 04 + 四个字节数据 + 校验。如果完全没反应,先查串口引脚初始化是不是接到了CH340的TX/RX而不是直连DB9。
第二步,给X0接一个按钮开关,把梯形图里写一行LD X0 OUT Y0,观察Y0对应的LED是否点亮。如果X0有输入但Y0不动,大概率是输入映射的GPIO口和源码里的核心板丝印对不上。
第三步,用GX Works2选FX3U型号,通过串口2连接,尝试读PLC类型。大多数仿制源码对GX软件只实现了“读设备型号”和“读软元件”两个功能,梯形图下载功能是缺失的或者仅支持特定波特率。
第四步,把M8000(常导通特殊继电器)强制置ON,看D0是否被写入预设值。这一步是验证特殊继电器处理逻辑是否完整。
第五步,断开串口线,观察PLC在独立上电后是否能复现逻辑。如果下载后能跑,断电重启后不能跑,基本是程序没有写进Flash,或者PLC内部将梯形图字节码放在了RAM区没有做断电保存。
3.3 编译报错里最常见的三类坑:头文件路径、启动文件、芯片宏
MDK5编译这套源码时,新手翻车基本集中在三条报错上。第一条是Error: L6218E: Undefined symbol,比如找不到SystemInit或assert_failed,原因是工程没有包含标准外设库的头文件搜索路径。处理方法:Option -> C/C++ -> Include Paths里把Libraries/CMSIS/Device、Libraries/STM32F10x_StdPeriph_Driver/inc全部加进去。
第二条报错是error: #5: cannot open source input file "stm32f10x.h",这是头文件路径或宏定义问题。在C/C++选项卡里的Define写USE_STDPERIPH_DRIVER,STM32F10X_HD就能解决大半。
第三条最隐蔽:源码里用了__IO这类CMSIS关键字,报unknown type name,说明你的Keil5安装缺少对应的CMSIS包。解法是去Pack Installer里装Keil::STM32F1xx_DFP,不是装最新版而是装和源码风格匹配的版本。我实测MDK 5.36配这个源码没问题,但5.36以前的老版本有概率和新的DFP不兼容,出现奇怪的结构体对齐问题。
4. 寄存器映射和Modbus从站协议:上位机读写怎么打通
4.1 FX3U的Modbus地址和D/M寄存器之间的换算规则
昆仑通态、组态王这类上位机软件读FX3U,走的是Modbus RTU协议,但地址区间的映射规则各家有微调。常见做法是:读取线圈用0X区映射Y输出(也有的用M继电器),读寄存器用4X区映射D数据寄存器。具体换算规则我整理成表:
| 上位机Modbus地址 | PLC软元件 | 偏移计算 | 说明 |
|---|---|---|---|
| 0X0001~0X0100 | Y0~Y377(八进制) | 地址-1 | 线圈映射输出点 |
| 0X0101~0X0C00 | M0~M3071 | 地址-0x0101 | 线圈映射内部继电器 |
| 4X0001~4X8000 | D0~D7999 | 地址-1 | 寄存器映射数据区 |
这套源码里Modbus从站实现的难点不是协议本身,而是地址区间的边界处理。我见过多次的实际翻车是:上位机组态时地址写4X0001,源码内部把请求的寄存器地址当成0,正确做法应该减1后再落到D数组下标。
// modbus_slave.c 读寄存器功能码03的地址解析 static int modbusReadReg(uint8_t *req, uint8_t *rsp) { uint16_t addr = (req[2] << 8) | req[3]; /* Modbus协议里的寄存器地址 */ uint16_t qty = (req[4] << 8) | req[5]; /* 上位机组态4X0001时,协议线上addr=0x0000, 必须转换成PLC侧D寄存器下标:index = addr + 1 */ uint16_t startIndex = addr + 1; if ((startIndex + qty) > D_REGION_SIZE) { return MODBUS_EXCEPTION_ILLEGAL_ADDR; /* 返回异常码2 */ } for (int i = 0; i < qty; i++) { uint16_t val = plcGetD(startIndex + i); rsp[3 + i * 2] = val >> 8; /* 高字节在前 */ rsp[4 + i * 2] = val & 0xFF; } return 3 + qty * 2; }逻辑说明:Modbus协议线路上发来的地址已经是”协议地址”,上位机软件在4X0001和协议地址0x0000之间做了减1换算,而PLC侧不能再用协议地址直接索引D区,必须加回1。参数说明:qty表示一次读多少个寄存器,FX3U的Modbus从站通常限制单次最多读125个,超出要分帧读取,源码里如果没有这个限制,上位机大块读时会得到错误响应。
4.2 功能码03/06/16的实现边界:写单个和写多个寄存器不能走同一逻辑
做过Modbus从站的人都知道,功能码06(写单个寄存器)和16(写多个寄存器)请求帧格式不同,一个带2字节数据,一个带字节数和数据块。很多仿制源码图省事,把06和16共用一个写函数,结果遇到上位机组态软件用16号功能码写批次参数时,解析错位。
写寄存器还有个隐藏要求:D区里的系统寄存器(比如D8000系列)在真实FX3U上很多是只读的,或者写入立即被系统值覆盖。仿制源码如果不做保护,上位机可以乱改D8000,后面脉冲指令、高速计数配置全部失真。
static int modbusWriteRegs(uint8_t *req, int func) { uint16_t addr = (req[2] << 8) | req[3]; uint16_t startIndex = addr + 1; int byteCnt = req[5]; /* 随后的数据字节数 */ int qty = byteCnt / 2; /* 保护特殊数据寄存器区域:D8000以后禁止上位机写入 */ if (startIndex >= 8000 && startIndex <= 8511) { return MODBUS_EXCEPTION_ILLEGAL_ADDR; } for (int i = 0; i < qty; i++) { uint16_t val = (req[6 + i * 2] << 8) | req[7 + i * 2]; plcSetD(startIndex + i, val); } return 0; }这段处理的关键是两个边界:一是D8000以后到D8511的特殊寄存器区域,上位机写过来了要直接拒绝;二是byteCnt和qty的校验——如果上位机发来奇数个字节,这里按qty = byteCnt / 2处理会丢掉最后一个字节,形成单字节对齐错误。这个地方我建议加一个byteCnt == qty * 2的强校验,不对就直接回异常码3(非法数据值)。
4.3 GX Works2连接仿PLC背后的编程口协议:谁通谁不通
这是个必须直面的现实:GX Works2自带的串口编程协议(FX3U的编程口协议)是三菱私有协议,完整移植工作量巨大。市面上多数“STM32仿FX3U源码”对GX的支持只做到两件事——读PLC类型和批量读写软元件值。这意味着你能用GX做在线监控、强制置位,但往PLC里下载梯形图这步,通常靠源码作者自己写的上位机软件或者把梯形图编译成字节码烧进STM32的Flash。
实操里我见过两种下载路线:一种是源码带一个PC端转换工具,把GX导出的指令表文件转成C数组,重新编译MDK工程再烧录;另一种是源码内置了一个简易梯形图解析器,通过串口接收自定义格式的梯形图文本来更新逻辑。第一种更稳,第二种依赖源码作者的自定义协议文档,遇到不透明的工程就麻烦了。
在你评估源码值不值得用时,先确认三件事:GX能读哪些软元件(D/M/X/Y全支持还是只有D);在线监控时数据刷新频率够不够(工业上至少2Hz);以及最重要的,梯形图修改后是重编译烧录还是串口直接下发。这三件事决定你后续维护梯形图的成本,比编译通过重要得多。
5. STM32仿FX3U常见问题与排查:实测翻车记录
5.1 上电后Y输出全部乱跳,过几秒才稳定
现象:PLC一上电,Y0~Y7的LED随机闪几下,然后才恢复到正常逻辑输出状态,有时还会直接把外接继电器误动作一次。
原因:排查后确认是软元件区没清零。源码里D区、M区数组定义的是全局变量,STM32上电后RAM内容随机,梯形图第一条扫描读到M0是1,直接置位了Y输出。这是典型的“软PLC”移植问题,真实FX3U的CPU上电后软元件区是有确定性初值的。
解决:在main函数进入主循环前调用plcSoftElementReset(),把M区和Y区全部写0,D区根据PLC参数里的锁存设置决定是否清零。还要注意,必须在梯形图第一条扫描前完成清零,我见过有人把清零放在定时器初始化之后,结果第一个节拍先执行了扫描,还是乱跳。
5.2 Modbus读D寄存器一切正常,写却时好时坏
现象:上位机读D0~D100都没问题,但写一个保持寄存器(比如配方号D50),第一次写成功,第二次就没反应,重启后偶尔又好了。
原因:这是Modbus轮询和PLC扫描在同一个主循环里互相挤占导致的问题。看代码循环体:if (plcTickFlag)里先做扫描再做Modbus轮询。如果扫描周期超过了定时器节拍,plcTickFlag一直被置位,每次循环都在执行扫描,ModbusPoll根本没机会跑到;或者反过来,某些周期里ModbusPoll跑了一半,下一次扫描开始,串口缓冲区被新来的RTU帧覆盖。
解决:把Modbus接收改成中断驱动+DMA缓冲区,主循环里只处理完整的一帧,不要边收边解析。另外给Modbus轮询加超时控制,处理一帧的最大时间固定住,超时直接丢弃。
/* 主循环中加入帧状态判断,避免扫描和通讯互相饿死 */ while (1) { if (plcTickFlag) { plcTickFlag = 0; plcScanOnce(); } /* 只有收到完整RTU帧才处理,未收完直接跳过 */ if (modbusFrameReady()) { modbusProcessFrame(); } }逻辑说明:modbusFrameReady()通过串口空闲中断置位——即一帧数据接收完成后3.5个字符时间内无新字节,判定RTU帧结束。参数说明:Modbus RTU规定空闲间隔为3.5个字符时间,9600波特率下约4ms,115200下约0.33ms,STM32的串口空闲中断刚好能配合这个时序,比在主循环里靠长度判断可靠得多。
5.3 梯形图里用了MOV指令,D值却偶发丢失
现象:梯形图是常ON -> MOV D100 D0,在线看D100一直是某个值,但D0偶尔会跳成0,过几百毫秒又自动恢复。
原因:这个坑比较阴。排查后定位在源码把D100所在的EEPROM参数区做了自动保存——写D100时触发了Flash写入,Flash写操作会阻塞CPU几十毫秒,这期间PLC扫描仍然在跑(定时器中断还开着),但主循环卡在Flash擦写上,D0读到一半被中断上下文打断,出现瞬时不一致。
解决:一是不要在梯形图扫描路径里频繁写需要掉电保存的D区,常见做法是专门划出一块“参数保存影子区”,梯形图只写RAM影子区,由系统每N秒或按键触发一次性批量保存到Flash;二是如果源码用的EEPROM模拟是整页擦写,必须检查坏块管理和磨损均衡,我这套就是在第10000次左右出现保存失败,后来改成双页轮换才稳定。
5.4 GX Works2在线监控时PLC“卡死”不动
现象:GX连上后监控梯形图,能看到软元件值,但PLC实际输出不动作,断开GX后立即恢复。
原因:仿制源码里GX通讯协议用的串口中断优先级高于PLC扫描节拍定时器。GX在线监控会全量轮询软元件,大量串口中断挤占了定时器节拍,扫描周期从2ms被拉长到几十毫秒,外部看起来就是卡死。
解决:把定时器节拍中断优先级调到最高(NVIC分组后抢占优先级设为0),串口中断设为1或者2,保证PLC扫描不被通讯完全拖死。如果源码里Modbus和GX共用一个串口那就更麻烦,建议Modbus走串口1、GX走串口2,物理隔离,互不干扰的中断优先级冲突面最小。
5.5 掉电保存的D区地址对不上:FX3U参数里的锁存范围被当摆设
现象:在GX或组态软件里把D0~D8设置为锁存寄存器,也给PLC配置了“掉电保持”的参数,但重启后D0~D8没有保存成功,而D200~D300却能保存。
原因:这是对FX3U寄存器属性的理解偏差。FX3U的D0~D8属于普通寄存器,默认断电不保持,但可以通过PLC参数设置为锁存;D200~D300则在很多默认配置里就是锁存区。仿制源码通常不解析PLC参数文件里的锁存配置,而是按自己的固定表来判断——比如把D200以后才做掉电保存,D0~D199全忽略。所以不是你的设置错了,是源码压根没读设置。
解决:改源码里软元件掉电保存的属性表,把需要保存的起始地址和长度做成可配置项。这里的一个血泪经验是:D区掉电保存不要整片保存,分三块(非锁存区、用户锁存区、系统区)管理,否则D8000这种系统值会被用户参数写坏。
6. 从跑通到实用:扫描周期测量和掉电保存的进阶验证
源码能跑、Modbus能通,只是第一步。真正要带现场设备,你需要做两件进阶验证:一是拿到准确的扫描周期,二是确认掉电保存不会在写Flash时把逻辑“卡停”。
扫描周期测量我一般不用示波器裸测,直接往工程里埋一个定时器统计函数:在plcScanOnce()入口取TIM2的计数,出口再取一次,差值就是这一圈的实际执行时间。把这个值存到D8268对应的自定义映射区,上位机直接读。D8268在真实FX3U里和脉冲输出有关,但仿制源码往往没严格按三菱布局实现,你这就可以把它改造成“扫描周期报告寄存器”,反正上位机只是读D值,协议层无感。
/* 扫描周期统计示例:TIM2自由运行,1MHz计数 */ static uint32_t scanTimeStart = 0; void plcScanOnce(void) { scanTimeStart = TIM2->CNT; /* 记录本圈起点 */ /* 原有扫描逻辑 */ plcInputRead(); plcLadderExecute(); plcOutputWrite(); /* 计算并写D8268:扫描周期单位为微秒 */ uint32_t elapsed = TIM2->CNT - scanTimeStart; plcSetD(8268, (uint16_t)(elapsed & 0xFFFF)); }这个统计值配合在线监控,你可以快速判断三条边界:逻辑步数增加多少倍后扫描周期会破10ms;多个功能指令并发执行时有没有个别指令吃掉80%的时间;以及Modbus大块读写期间扫描周期有没有超过设计上限。我用这套方法发现一个之前完全没意识到的坑:在梯形图里用了多个32位比较指令后,扫描周期从1.8ms飙到7ms,外部的编码器定位程序开始丢位置,查出来是32位指令的溢出边界判断写得太啰嗦,每次多出几百次循环。把比较逻辑换成先判高16位、再判低16位后,整个功能函数耗时降到原来的四成。
掉电保存的进阶验证,我的做法是做一次“拔电压力测试”:把PLC设置成每100ms往Flash写一次D区快照,反复拔电60次,每次上电后用Modbus读D区关键地址,比对是否有半字、错位或丢块。第一次测就发现四字节对齐问题——D区快照按2字节宽度写入Flash,但多字节变量跨了Flash半字边界,导致上电读到分裂数据。后来把快照区改为按4字节起始对齐,位域顺序重排,才算稳定。
这个方向做到能带实际设备,还有很长一段路,但核心价值已经兑现:一块几十块的STM32核心板,顶替了进口PLC的大部分逻辑功能,梯形图能下载、Modbus能接组态屏、掉电能保存配方。如果你正在评估类似方案,我的最终建议是先买一套最小系统板,编译、烧录、接开关和继电器跑通最小逻辑,再去纠结GX协议和高级指令。这套仿FX3U的源码,价值在懂工艺的人手里,不在代码量里。希望帮到你。
本文还有配套的精品资源,点击获取