简介:STM32三菱FX3U PLC仿真实例源码,面向嵌入式开发者、自动化设备工程师以及工控项目学习者,核心目标是用ARM微控制器替代或模拟三菱FX3U小型PLC,在MDK4与MDK5环境中均可编译,仅有一个多余变量警告,亲测可用。源码完整覆盖串口通讯协议、梯形图指令解析、输入输出寄存器模拟、外部中断响应、异常错误处理等模块,既能帮助理解微控制器与PLC之间的交互机制,也能支撑低成本原型验证、教学实验、设备改造等实际场景;进一步阅读ladder、PLC_Com等主要源文件,还能理清扫描周期、软元件映射与指令执行的实现思路。压缩包共403个文件,以c/h源文件、o/crf编译中间文件为主,同时包含hex烧录文件、sct链接脚本、map映射文件,直观呈现编译产物与内存布局,便于二次开发与排错,整体大小为13.33MB。目前已有3937人学习下载,对希望深入掌握FX3U兼容实现,或在此基础上扩展网络连接和高级控制算法的开发者,参考价值较强。
1. STM32仿三菱FX3U源码:它到底是个什么东西
做过非标设备的人都知道,小型PLC里三菱FX3U是绕不开的型号,但正品FX3U的硬件成本和一个STM32板子差了好几倍。所以这几年「用STM32仿FX3U」的方案一直在工控圈里流传——不是破解,而是用STM32的引脚去模拟PLC的IO行为,跑一份兼容FX3U寄存器规范的固件,让GX Works2或者GX Developer能正常写梯形图、在线监控。这套源码就是把这件事做完了的产物,在MDK5里编译0错误,下载后配合外部继电器板和隔离电路,可以直接当FX3U用。
适合的人很明确:一是做小型设备出厂配套,想把PLC成本压下来的嵌入式工程师;二是给老旧设备做替代维护、手头有现成STM32开发板的技术员。它解决的问题也很直接——不用学专门的PLC固件框架,用你熟悉的STM32外设和定时器,就能实现一套带扫描周期、带软元件区、带485通信的PLC核心。源码里能看到完整的工程结构和底层驱动,不是那种只能跑个点灯的半成品。这篇文章会把工程结构、核心逻辑、通信协议、踩坑点一个个拆开,你把代码下下来,对照着这篇笔记,差不多一到两天就能把固件跑起来。
2. 硬件选型与工程结构:先搞懂这份源码跑在什么环境上
2.1 主控选型与外设分配
这套源码默认的硬件平台是STM32F103系列,资源占用主要集中在三块:IO映射、串口、定时器。IO映射是把FX3U的X输入和Y输出对应到STM32的GPIO上,源码里用的是类似这样的映射表结构:
typedef struct { uint8_t soft_elem; /* 软元件编号,如 X0~X7 */ GPIO_TypeDef* port; /* 对应的GPIO口 */ uint16_t pin; /* 引脚号 */ } IO_MAP_TABLE; const IO_MAP_TABLE x_input_map[] = { {0, GPIOA, GPIO_Pin_0}, {1, GPIOA, GPIO_Pin_1}, {2, GPIOA, GPIO_Pin_2}, {3, GPIOA, GPIO_Pin_3}, /* ... */ };这里有一个核心设计思路:PLC梯形图扫描的时候,不会直接去读GPIO寄存器,而是先把所有输入引脚的电平状态采样到输入映像区,运算执行完再统一把输出映像区写到GPIO上。这个做法和真正的PLC是一模一样的,叫「批量输入、批量输出」。主要好处是避免程序扫描过程中IO被中途改掉导致逻辑错乱,这是仿PLC固件必须遵守的第一条规则。
硬件选型上我建议直接用ZET6的最小系统板起步,有FSMC但用不上,关键是引脚够多。如果只控制16点输入16点输出,C8T6也能跑,但你要确认引脚分配不会冲突——源码默认的映射表是按ZET6的100引脚封装的,换芯片得重新改表。
2.2 工程文件组成与编译入口
下载解压后,整个工程基本是标准MDK5的目录结构,核心文件包括:
├── USER/main.c /* 主函数与初始化入口 */ ├── USER/stm32f10x_it.c /* 中断处理,包含定时器中断调用PLC扫描 */ ├── HARDWARE/plc_core.c /* 扫描引擎、指令解释、软元件读写 */ ├── HARDWARE/plc_comm.c /* 485通信,三菱专用协议解析 */ ├── SYSTEM/delay.c ├── SYSTEM/sys.c └── SYSTEM/usart.c编译前要在MDK里做三件事:选芯片型号、加宏定义、配下载器。芯片型号在Device里选STM32F103ZE或者和你板子对应的型号;C/C++选项卡的Define处需要填上STM32F10X_HD,这个是标准外设库判断芯片容量的宏,不写的话很多外设的初始化会出问题。
编译通过之后烧录,第一件事不是看灯,而是打开串口看启动日志。源码在main.c里有一段初始化自检,会打印软元件区的初始状态和通信端口的波特率参数,这个信息非常重要——如果打印乱码,不是代码问题,八成是外部晶振频率和代码里的宏定义不匹配。源码默认是8MHz外部晶振倍频到72MHz,如果你的板子上是25MHz晶振,要在system_stm32f10x.c里改PLL配置。
3. 梯形图扫描引擎:源码里最值钱的核心逻辑
3.1 从软元件区到扫描周期的映射
FX3U的软元件区,简单说就是一堆有名字的内存变量。源码在plc_core.c里用一个大数组把这些变量全部管理起来了,这也是整份源码的精华所在。看代码:
/* 软元件存储区,按照FX3U的寄存器规范定义 */ #define M_SOFT_SIZE (8 * 1024) /* 辅助继电器 M0~M8191 */ #define D_SOFT_SIZE (16 * 1024) /* 数据寄存器 D0~D32767 */ #define X_SOFT_SIZE 128 /* 输入映像 */ #define Y_SOFT_SIZE 128 /* 输出映像 */ uint8_t M_soft[M_SOFT_SIZE / 8]; /* M区按位存储 */ uint16_t D_soft[D_SOFT_SIZE]; /* D区按字存储 */ uint8_t X_img[X_SOFT_SIZE / 8]; /* 输入映像区 */ uint8_t Y_img[Y_SOFT_SIZE / 8]; /* 输出映像区 */M区按位存储这一点很关键。因为梯形图里的M0、M1这些辅助继电器本质上就是一个bit,如果用一个uint8_t去存一个bit,8K的M继电器就要8KB内存,但用位数组只需要1KB。源码里读取M区状态用的是类似这样的位操作:
uint8_t get_m_bit(uint16_t addr) { return (M_soft[addr >> 3] >> (addr & 0x07)) & 0x01; } void set_m_bit(uint16_t addr, uint8_t val) { if (val) { M_soft[addr >> 3] |= (1 << (addr & 0x07)); } else { M_soft[addr >> 3] &= ~(1 << (addr & 0x07)); } }地址右移3位相当于除以8,算出这个bit在第几个字节,再用低3位定位在字节内的位置。这套位寻址的写法在整个源码里到处都是,你看懂这两个函数,后面读通信协议解析代码会轻松很多。
3.2 基本指令的实现方式:LD、AND、OUT怎么变成C代码
梯形图里的常开触点、常闭触点、线圈输出,在源码里被实现为对软元件区的一串位操作。以最典型的输出线圈为例:
/* OUT指令:把运算结果写入输出映像区 */ void plc_out(uint16_t y_addr, uint8_t in_bit) { if (in_bit) { Y_img[y_addr >> 3] |= (1 << (y_addr & 0x07)); } else { Y_img[y_addr >> 3] &= ~(1 << (y_addr & 0x07)); } } /* 常开触点读取:返回值就是触点的通断状态 */ uint8_t plc_ld(uint16_t addr, uint8_t is_x_area) { if (is_x_area) { return (X_img[addr >> 3] >> (addr & 0x07)) & 0x01; } return get_m_bit(addr); }这其实就是把PLC的扫描执行过程搬进了C语言:每个扫描周期,从梯形图的第一步开始,按顺序执行触点的读取运算,最后把结果写到线圈。源码里预置的梯形图逻辑相当于一个示例程序,你可以顺着plc_core.c里的函数调用关系,把一条完整的扫描路径画出来——从输入采样开始,到LD读取,到AND串联,到OUT输出,再到输出刷新,这就是一个完整的扫描周期。
扫描周期的长度由定时器中断控制。源码里用的是TIM2产生一个周期性的中断,中断频率决定了扫描周期。一般设置到2ms到5ms比较合适,太快的扫描周期会让STM32的负担变大,太慢的话逻辑响应不行。改扫描周期去tim.c里改定时器的预装载值就行。
3.3 指令表与GX Works2的对接逻辑
这块是源码里最容易忽略、但恰恰是最见功力的一层。GX Works2写出的梯形图,编译后生成的指令表,实际上是一串类似LD X0、ANI X1、OUT Y0这样的文本指令。这套源码要做的事情,就是把这串指令转化成上面讲的那些C函数调用。
源码里用一个指令映射表完成了这个解析:
typedef struct { uint8_t cmd_id; /* 指令码 */ void (*handler)(void); /* 对应的处理函数 */ } CMD_TABLE; const CMD_TABLE cmd_table[] = { {0x00, op_ld}, /* 常开触点运算开始 */ {0x01, op_ldi}, /* 常闭触点运算开始 */ {0x02, op_and}, /* 常开串联 */ {0x03, op_ani}, /* 常闭串联 */ {0x04, op_or}, /* 常开并联 */ {0x05, op_ori}, /* 常闭并联 */ {0x06, op_out}, /* 线圈输出 */ /* ... */ };这套设计思路和真实的PLC固件是一脉相承的,用一张函数指针表代替一堆switch-case,后续扩展指令只需要往表里挂新的处理函数。但要注意一点,这份源码实现的指令集是FX3U的一个子集,常用指令基本覆盖了,但像INV取反、RST复位这类指令,建议你先去看cmd_table里到底有没有——拿到源码后第一件事就是把指令表完整打印出来,对着GX Works2的指令手册核对一遍,看哪些指令支持哪些不支持,以免写到一半发现编译不过去。
4. 通信协议实战:上位机怎么和STM32对话
4.1 三菱专用协议的帧格式解析
这套源码最大的价值之一就是带了一整套三菱专用协议的解析,这意味着GX Works2的在线监控是可以直接连上的。三菱FX系列编程口的通信协议是固定格式的帧结构,源码通信这块的核心是把协议帧和上面的软元件存储区对接起来。
标准的读命令帧是这样的:
帧头 命令码 起始地址 点数 帧尾校验 02h 30h 44h 30h 30h 30h 33h 03h 41h 42h其中30h是ASCII字符0,44h 30h是ASCII的D0,代表从D0开始读。把这段变成C代码,就长这样:
/* 解析上位机发来的读命令,返回要读的起始地址和点数 */ void parse_read_frame(uint8_t *buf, uint16_t len) { uint16_t start_addr = 0; uint16_t count = 0; /* 第3~4字节是起始地址的ASCII码,如 "D0" */ start_addr = (buf[3] - '0') * 10 + (buf[4] - '0'); /* 第5~6字节是读取点数 */ count = (buf[5] - '0') * 10 + (buf[6] - '0'); /* 根据地址前缀判断操作哪个软元件区 */ if (buf[3] == 'D') { read_from_d_soft(start_addr, count); } else if (buf[3] == 'M') { /* M区按bit操作,点数是位个数 */ read_from_m_soft(start_addr, count); } }注意这个解析写的很直白,因为上位机发过来的就是ASCII字符,比如要读D0到D9,传输的是D0和09的ASCII码。和单片机串口收发字符数据不太一样,协议里所有地址和参数都是半字节ASCII,所以解析的第一件事就是减0x30把字符还原成数字。
4.2 读到数据后怎么回帧
收到读命令,CPU会先计算出校验和,再拼装回帧。回帧的格式保持和请求帧一致的协议规范:
/* 生成读响应帧 */ void build_read_response(uint8_t *data, uint16_t data_len) { uint8_t frame[MAX_FRAME_LEN]; uint8_t sum = 0; uint8_t idx = 0; frame[idx++] = 0x02; /* 帧头 */ frame[idx++] = 0x30; /* 命令码 30 = 读 */ for (uint16_t i = 0; i < data_len; i++) { uint8_t hex[2]; byte_to_hex(data[i], hex); /* 1字节分成两个ASCII字符 */ frame[idx++] = hex[0]; frame[idx++] = hex[1]; sum += hex[0]; sum += hex[1]; } frame[idx++] = 0x03; /* 帧尾 */ frame[idx++] = sum; /* 校验和 */ uart_send_buffer(frame, idx); }byte_to_hex做的事情是把一个字节的高低四位分别转成0~F的ASCII字符,这样才能和上位机约定好的协议对上。校验和的算法是所有非帧头帧尾的字节相加取低8位,这一点很多初次接触三菱协议的人会算错——是加整个数据区的ASCII码,不是加原始二进制。
注意一个实际项目里经常翻车的细节:D区的数据是16位的,读一个D寄存器要回4个ASCII字符。源码中已经考虑到这个了,默认按一个D寄存器占两个字节来换算点数和长度,但你要是自己改协议去适配别家上位机,这点经常出问题。
4.3 波特率、站号与串口参数对照表
源码默认的通信参数是三菱FX3U出厂设置,连线之前先把这些参数记牢:
| 参数项 | 默认值 | 说明 |
|---|---|---|
| 波特率 | 9600 bps | 三菱默认值,部分上位机可改19200 |
| 数据位 | 8 | 固定 |
| 停止位 | 1 | 固定 |
| 校验位 | 无(偶校验可配) | 源码默认无校验 |
| 站号 | 0 | 多机485时区分设备 |
| 通信口 | USART2/3 | 看具体板子引脚,默认PA2/PA3需要改的话查宏定义 |
串口初始化时有一个常见误区:很多人直接把USART的USART_InitStructure.USART_Mode设置成USART_Mode_Rx | USART_Mode_Tx就结束了,但这套源码里通信和调试日志用的是两个串口,一个负责和上位机通信,一个负责打印调试信息,改串口引脚时要看清楚当前在操作哪一个,改错的话在线监控能连上但看不到调试信息,或者反过来。
和GX Works2联机的链路要注意接线方式,用485的话要接A/B线到232/485转换器,有些USB转485模块默认是自动收发切换的,不用额外控制DE/RE引脚。但如果你的板子用的是MAX3485这类需要手动切换方向的芯片,源码里已经预留了方向控制引脚的GPIO操作,没预留的话要自己加一个,否则通信时收不到回帧。
5. 避坑指南:这片源码最常见的五个翻车现场
5.1 现象:MDK编译报错找不到core_cm3.h
原因:MDK5默认不自带CM3内核文件,使用标准外设库时没有把CMSIS路径加到Include Path里。解决方法是打开Options for Target,在C/C++选项卡的Include Paths里手动加上..\Libraries\CMSIS\CM3\CoreSupport和..\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x两个路径。这是从MDK4升级过来的工程最常见的问题。
5.2 现象:GX Works2能搜到设备但读写超时
原因:九成是485方向切换引脚没初始化。源码一般在plc_comm_init里初始化485方向控制,但如果你的板子在复位瞬间485芯片默认处于接收状态,而程序还没来得及把方向切到发送,就会造成第一帧丢帧。解决方法是上电初始化时先强制拉一次发送方向再释放到接收,同时串口中断里要在收到完整帧之后延时一小段时间再回帧,给从机留出处理时间。
5.3 现象:输入信号一抖动,逻辑就误触发
原因:直接用GPIO读到的电平给输入映像区,没有做软件消抖。现场环境里按钮、继电器触点都有几毫秒的抖动,PLC本身有数字滤波,仿品也得做。解决方法是复用主扫描定时器,对输入信号做连续多次采样,比如连续4次读到同一电平才认为输入有效。具体改法是在输入采样函数里加一个计数器数组,电平变化时清零计数值,达到阈值才算稳定。
5.4 现象:开关电源供电时程序频繁重启
原因:STM32的IO口直接接PLC输入信号线过来的24V电,而输入信号的地和单片机的地之间电位差过大。解决方法是所有PLC输入信号必须经过光耦隔离后再进STM32,光耦的输入侧用24V供电,输出侧接3.3V和GND。不要想省这个成本,用电阻分压直接把24V降到3.3V,工业现场一接电机就废。
5.5 现象:在线监控时D寄存器写入不生效
原因:这个坑很多第一次接触这套源码的人都会踩。GX Works2在线写入D寄存器后,数值确实通过串口进到了RAM里,但PLC扫描周期下一轮又把初始值覆盖回去了。原因是源码里D区初始化逻辑是在主循环里循环执行的,或者你下载的工程里D值存储区的初始赋值写在扫描周期内。解决方法是把D区初始化的代码挪到主函数启动阶段,只执行一次,不要放在while(1)里。
6. 在线监控的实现技巧:把GX Works2的梯形图状态刷出来
这套源码固件跑通之后,你面对的下一个真实项目需求多半是:设备装到现场了,调试人员希望用GX Works2直接看程序跑到哪里。梯形图监控功能看起来复杂,其实不复杂,它的核心就是批量读取软元件状态并周期性刷新。
具体的监控流程是:GX Works2每隔一段时间发一条读命令,把梯形图里用到的X、Y、M、D软元件区的值全部读走,然后在电脑端把梯形图上每个触点和线圈的颜色刷新一遍。你的固件侧要做的事情就两件——把软元件区的数据整理成协议要求格式,以及保证高速串口通信下数据的一致性。
现场实测技巧是我建议你重点关注的:
第一,监控模式下通信量比编程下载时要大得多,如果9600波特率刷新不够流畅,把通信参数整体改成19200,源码里已经有对应的宏开关。
第二,梯形图监控显示Y输出状态和实际物理输出不一致,这种问题基本都出在输出映像区到物理输出的刷新逻辑上。源码默认是在扫描周期结束后一次性刷新全部Y输出。如果你把输出刷新放在了梯形图执行中途,就会出现监控状态超前或者滞后的情况。检查你的plc_scan函数调用顺序,必须是输入采样→梯形图执行→输出刷新→通信处理。
第三,如果你在程序里用了定时中断来扫描梯形图,而通信串口中断优先级设置得比扫描中断高,那么高频率的通信请求会不断打断扫描周期,导致扫描时间被拉长。反过来也会有竞争问题,扫描周期中途处理通信,读取到的软元件状态可能是半新半旧的。我一般把通信中断优先级设在扫描中断之下,确保扫描的原子性,通信慢一点没关系,PLC逻辑不能乱。
当初我第一次把这个工程下载到板子上之前,还做了一件后来证明很有用的验证——先用USB转TTL把板子串口接到电脑上,打开串口助手,手动发一条读命令的十六进制帧,看回帧是否符合预期。这一招建议你们也用上,它能帮你在不打开GX Works2的情况下快速验证通信链路和协议解析的正确性,排查问题的时候省太多时间了。
从那以后我每次拿到这类仿PLC固件源码,都会强制走一遍这套验证流程:先核对硬件映射表、再查指令表覆盖范围、然后验证串口收发帧、最后才接上位机联调。这套流程帮我把调试时间压缩了至少一半以上,希望帮到你。
本文还有配套的精品资源,点击获取