STM32仿三菱PLC完整教程:从电路设计到指令解释器实现 📅 发布时间:2026/9/1 2:35:38 👁 浏览次数: 简介本资源是一套基于STM32F103实现三菱FX系列PLC功能的完整嵌入式开发方案面向嵌入式初学者、自动化专业学生及工业控制开发者解决PLC原理理解难、软硬协同设计缺实例、国产替代PLC开发无参考等实际问题。压缩包含159个文件总计4.46MB涵盖29个C源文件如main.c、stm32f10x_tim.c等核心驱动与逻辑实现、29个汇编目标文件.o、29个依赖文件.d、6个头文件.h及2个PDF文档含三菱PLC内部电路图详解另有工程配置文件.uvprojx、.uvoptx、调试符号.axf、.hex和编译中间产物结构完整可直接导入Keil MDK环境编译运行。已有5337人学习下载资源提供从硬件原理图设计、C/C底层逻辑编程、定时器/IO/中断仿真到MODBUS通信基础的全链路实现特别适合通过动手复现经典PLC架构来深入理解工业控制时序逻辑与嵌入式实时响应机制。 STM32仿三菱PLC这个玩法我琢磨了好一阵子。老实说一开始我也觉得这是个“大工程”毕竟PLC在工控圈混了这么多年稳定性、可靠性不是随便一个单片机就能比的。但真正把原理图、程序跑通之后你会发现自己对PLC的理解完全上了一个台阶——原来梯形图背后就是位操作、定时器就是一个计数器、通信口就是串口加收发芯片。这篇文章我就把这个项目的完整思路、电路细节、代码框架和踩坑记录全部摊开来讲给同样想动手的朋友一条能走通的路。1. 项目整体设计与思路拆解1.1 为什么用STM32去仿三菱PLC先说动机。市面上的三菱FX系列PLC比如FX3U一台也要几百上千块而且它的编程环境GX Works绑定的是自家生态想深入看硬件原理基本不可能。而STM32是开源生态最成熟的MCU之一有海量的库函数、例程、社区方案用C语言直接操作寄存器完全可以把PLC那一套逻辑在单片机上复刻出来。更重要的是三菱PLC的编程语言本质上是“梯形图”而梯形图最终会被编译成指令表Instruction List。指令表无非就是LD、AND、OR、OUT、SET、RST这些指令对应到C语言就是位变量与位变量的与、或、非、赋值。也就是说只要你把软元件X输入、Y输出、M中间继电器、T定时器、C计数器、D数据寄存器映射成内存变量再用一个解释器去执行指令表一台“软PLC”就成型了。STM32主频72MHz处理这些逻辑运算绰绰有余扫描周期能做到毫秒级甚至微秒级。所以这个项目的定位不是去硬刚三菱原厂的性能而是用最低的成本、最直观的方式把PLC的底层运行机制拆开看明白。适合谁玩一是想搞懂PLC内部原理的电气工程师二是学STM32但觉得普通例程太无聊的嵌入式爱好者三是实验室要做小型自动化教学平台的学生。1.2 整体架构硬件三层 软件两层我把整个系统拆成了硬件三层和软件两层。硬件三层指的是电源板、主板、IO板。电源板负责把外部24V工业电源转成5V和3.3V主板是核心放STM32、晶振、下载接口、通信芯片IO板处理输入信号和输出驱动。之所以分开是为了调试方便电源坏了换电源板IO烧了换IO板不用整块报废。软件两层则是指“系统层”和“应用层”。系统层包括时钟配置、GPIO初始化、定时器中断、串口DMA收发这一层直接用STM32标准外设库或HAL库搞定应用层是核心包括软元件映射、指令解释器、扫描周期主循环、Modbus协议栈。分层之后你想换一个更快的MCU比如从F103换成F407只需要动系统层应用层可以整体平移。1.3 功能规划先做减法再做加法实际动手前一定要控制功能范围。我第一次做的时候贪多想连浮点运算、PID、高速计数全部塞进去结果写出来的代码自己都看不懂。后来砍到只剩以下核心功能基本位逻辑指令LD、LDI、AND、ANI、OR、ORI、ANI、ANB、ORB、OUT、SET、RST、PLS、PLF定时器指令TON延时导通、TOF延时断开计数器指令CTU递增计数、CTD递减计数比较指令CMP、MOV、ADD、SUB通信功能Modbus RTU从站支持03/06功能码这基本覆盖了80%的PLC应用场景。一个电机正反转控制、一条小型传送带的分拣逻辑、一个恒压供水系统的逻辑控制用这套指令完全够写。等这些功能稳定之后再慢慢往里面添加高级指令整个过程相对顺畅。2. 三菱PLC内部电路图解构参考与致敬2.1 三菱FX系列内部电路的经典框架拆过三菱FX系列PLC的朋友应该清楚它的电路板其实不算复杂核心就是“电源模块 CPU模块 IO处理模块 通信模块”四块分立设计。CPU模块用的是三菱自家的专用芯片外面挂着SRAM和Flash用来存储用户程序和软元件状态。IO处理模块则通过总线桥接芯片与CPU通信输入侧经过光耦隔离后进锁存器输出侧则通过锁存器驱动继电器或晶体管。这个结构直接启发了我STM32完全可以通过GPIO模拟锁存器的读写逻辑输入侧用一个74HC165并转串、输出侧用一个74HC595串转并这样就能大量节省MCU的引脚开销。实际上我用的是更简单粗暴的直连方式——输入直接接光耦输出输出直接接达林顿管驱动因为STM32的引脚数量足以覆盖小型PLC的IO规模。2.2 输入电路的秘密光耦隔离与RC滤波这是三菱PLC内部电路的精髓之一。PLC工作在工业环境里按钮、传感器、接触器的开关信号经常带有抖动而且容易受到电机启停带来的电磁干扰。三菱的做法是外部信号先经过RC低通滤波再进入光耦隔离之后才送到CPU引脚。我抄了这个设计并且把参数算了一遍。光耦选PC817或EL357N输入端串联一个1kΩ限流电阻外部24V信号经过电阻和光耦内部LED形成回路电流大约24V-1.2V/1kΩ≈22.8mA这在PC817的额定范围内。光耦输出侧上拉一个10kΩ电阻到3.3V再并联一个0.1μF电容做滤波。实际测试下来10ms以下的随机毛刺基本被滤掉了。对于要求更高的场合可以在RC滤波上再加强。有一点要特别提醒光耦的电流传输比CTR是随温度变化的如果你把限流电阻选得太大导致LED电流只有几毫安那在高温环境下输出侧可能拉不低输入信号就会误判。所以限流电阻宁可取小一点保证光耦一直工作在过驱动状态也不要卡在临界值。2.3 输出电路的三种方案继电器、晶体管、达林顿三菱FX系列有继电器输出FX3U-32MR和晶体管输出FX3U-32MT两个版本。继电器版的好处是触点耐压高能直接驱动220V交流负载晶体管版则响应快适合控制步进驱动器和伺服驱动器的脉冲信号。我的仿真板两个都做了。继电器输出用ULN2803达林顿管驱动每组输出外部接一个5V继电器线圈两端并联了一个1N4007续流二极管防止关断瞬间的反向电动势打坏ULN2803——这是很多人不做就烧芯片的坑。晶体管输出则直接用ULN2803的开漏输出接负载负载另一端接24V电源注意ULN2803的耐压是50V电流是500mA驱动小型接触器线圈和中继继电器没问题。如果你要做大功率输出比如直接驱动1kW的电机接触器建议在继电器输出外再接一个交流接触器用PLC的输出触点点亮接触器线圈让接触器的主触点去切换大电流这样才能保证长期可靠性。2.4 通信电路RS485的完整参考设计三菱FX系列标配一个编程口和一个RS422/RS485通信口。RS485通信是工控现场最常用的方式它支持多点挂接抗干扰能力远强于RS232。我的STM32仿真板直接做了一个RS485接口控制芯片用MAX485或SP3485很简单RO接STM32的RX引脚DI接TX引脚RE和DE短接后接到一个普通GPIO这个GPIO控制收发方向。这里有个细节RS485总线在空闲状态下需要处于一个确定电平所以A、B两端必须加上下拉电阻并在末端并接120Ω匹配电阻。终端电阻不是随便加的只有总线两端才需要加中间节点接了反而会加重反射。如果你不确定自己是不是末端节点可以先不焊匹配电阻等通信不稳定再补上。实战中我见过很多人在每个节点都焊了120Ω结果信号直接衰减到没法用。另外为了提升共模干扰抑制能力A、B之间可以再接一个TVS管和两个钳位二极管到地和电源。这属于电路设计冗余成本不高但对长期稳定运行帮助很大。做工业应用多一分保护少一分出差。3. 核心程序实现用C语言模拟PLC运行机制3.1 PLC扫描周期一个无限循环的理解PLC的一个核心概念就是扫描周期CPU循环执行“读输入→执行用户程序→写输出→处理通信”这四件事每循环一次就是一个扫描周期。三菱FX3U的扫描周期大概是纳秒级到微秒级——它的位逻辑指令用专用芯片硬算。而用STM32模拟我们就要在软件里把这一步做出来。我的实现方法很直接——一个while (1)大循环循环内依次调用四个函数while (1) { input_scan(); // 读取所有输入引脚刷新X元件区 execute_program(); // 执行用户梯形图对应的指令表 output_update(); // 将Y元件区的状态写入输出引脚 modbus_poll(); // 处理通信请求更新D寄存器 }这里有个问题如果程序执行时间很长比如几百条指令再加上串口通信处理扫描周期可能达到几十毫秒。这在控制接触器、气缸等动作上没问题但如果你要控制步进电机脉冲就需要把脉冲生成放到定时器中断里不要在扫描循环里用延时函数否则整个系统的响应就废了。扫描周期的稳定性跟任务的划分密切相关。我建议把必须保证时序的任务脉冲输出、高速计数放进中断把逻辑执行、通信解析放进主循环。PLC也是这么干的——它的高速计数器就是硬件计数器不占用扫描周期。3.2 软元件映射把PLC指令翻译成C语言软元件是三菱PLC的精华概念。X是输入继电器对应到我的板子上就是PC817光耦输出侧的引脚状态Y是输出继电器对应ULN2803输入端的状态M是内部继电器就是一块内存里的位T是定时器C是计数器D是16位数据寄存器。在C语言里我的做法是定义几个全局数组#include stdint.h #define X_MAX 8 // 8路输入 #define Y_MAX 6 // 6路输出 #define M_MAX 64 // 64个中间继电器 #define T_MAX 16 // 16个定时器 #define C_MAX 8 // 8个计数器 #define D_MAX 32 // 32个数据寄存器 volatile uint8_t X_Map[X_MAX]; // 输入映像区 volatile uint8_t Y_Map[Y_MAX]; // 输出映像区 volatile uint8_t M_Map[M_MAX]; // 内部继电器 volatile uint16_t T_Map[T_MAX]; // 定时器当前值 volatile uint8_t T_Bit[T_MAX]; // 定时器触点状态 volatile uint16_t C_Map[C_MAX]; // 计数器当前值 volatile uint8_t C_Bit[C_MAX]; // 计数器触点状态 volatile uint16_t D_Map[D_MAX]; // 数据寄存器在input_scan()里直接读GPIO把电平状态写入X_Map。void input_scan(void) { for (int i 0; i X_MAX; i) { X_Map[i] HAL_GPIO_ReadPin(GPIOB, 1 i); } }在output_update()里把Y_Map写回GPIO。你可能会问为什么不直接读GPIO当X直接写GPIO当Y而要中间经过一个数组这就是PLC的“映像区”思想保证整个扫描周期内程序看到的输入状态是启动扫描那一刻的统一快照而不是执行到某条指令时随时变化的实时电平。这样做能保证程序的可预测性避免因为一条指令执行到一半时输入变了导致逻辑混乱。3.3 指令解释器从梯形图到指令表的落地梯形图不能直接在STM32里跑所以要么在PC端把梯形图编译成指令表再把这个指令表下载到下位机要么直接用一套指令集在PC端用助记符写“软PLC程序”。为了降低门槛我的方案是后者用一套类似三菱指令表的助记符直接写在STM32的Flash里。每条指令是一个结构体有一个操作码和操作数。typedef struct { uint8_t opcode; uint8_t operand_type; uint16_t operand_addr; } PLC_Instruction;执行的时候就是一个巨大的switch-casevoid execute_program(void) { const PLC_Instruction *inst user_program; uint8_t stack_result 0; uint8_t current_result 0; while (inst-opcode ! OP_END) { switch (inst-opcode) { case OP_LD: current_result get_operand(inst); break; case OP_AND: current_result current_result get_operand(inst); break; case OP_OR: current_result current_result || get_operand(inst); break; case OP_OUT: set_operand(inst, current_result); break; case OP_TON: timer_process(inst); // 定时器特殊处理 break; // ... 其他指令 } inst; } }get_operand和set_operand就是根据操作数类型从X_Map、Y_Map、M_Map、D_Map 里读写数据。这里有一个关键点梯形图是从左到右、从上到下执行的每一行的逻辑结果通过“堆栈”传递。三菱的ANB和ORB指令就是用来处理两个触点块的串联和并联的。在C语言实现里我用一个局部变量current_result保存当前行逻辑遇到分支时用一个栈暂存结果。比如并联一个子块时先把子块的结果算出来存进栈再和当前结果做OR。扫描周期里跑完所有指令后定时器还需要单独处理。PLC的定时器本质上是“对扫描周期计数”每扫描一次当前值加1当当前值达到设定值触点动作。但因为扫描周期不是固定的这种计数方式的定时不精确。更合理的做法是在定时器中断里固定增加一个时间基准比如每1ms扫描一次定时器模块把需要计时的定时器统一加1。我采用的是后者选一个1ms的SysTick中断中断里把每个正在计时的定时器的当前值加1到达设定值就置位T_Bit。这样定时精度是1ms完全够工控用。3.4 通信协议栈Modbus RTU让外设互联三菱PLC最常干的事就是和触摸屏、变频器、上位机通信。三菱FX系列走的是专用协议但很多国产设备兼容的是Modbus协议。我的STM32仿真板直接做了Modbus RTU从站因为Modbus协议公开、简单、通用性强随便找个上位机组态软件或串口调试助手就能连上。Modbus RTU的帧格式是地址码 功能码 数据 CRC16校验。地址码1字节功能码1字节数据若干字节CRC16双字节低字节在前。核心实现就是串口接收中断里把收到的字节存进一个缓冲区当检测到两个字节间隔超过3.5个字符时间就认为一帧接收完毕。然后解析帧头、功能码、CRC如果地址匹配并且CRC校验通过就执行相应的读写操作再组一帧回复。功能码我只实现了两个03读保持寄存器对应读D寄存器06写单个保持寄存器对应写D寄存器另外加了一个05功能码写单个线圈对应写Y输出和M中间继电器。这样你用电脑上的Modbus调试工具就能远程控制STM32的输出引脚了。这个功能非常实用调试电机正反转的时候我天天用它。CRC16的代码网上铺天盖地但需要注意查表法和按位运算法两种方式。查表法快适合在中断里用按位法则省Flash。我用的查表法把表放在Flash里不占RAM。uint16_t modbus_crc16(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; while (len--) { crc ^ *data; for (int i 0; i 8; i) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }3.5 把梯形图逻辑“翻译”成C宏定义写指令解释器还不够最终用户是要用梯形图编程的。为了方便演示我把几个最常用的三菱指令宏定义出来了直接用C代码写逻辑然后编译进STM32。// 梯形图逻辑示例X0启动X1停止控制Y0输出 while (1) { // 起保停电路 M_Map[0] X_Map[0] || (M_Map[0] !X_Map[1]); Y_Map[0] M_Map[0]; }虽然这只是用C语言直接模拟了梯形图的逻辑不是真正的梯形图编译器但用来学习编程思路、做一个演示系统绰绰有余。如果你想做完整的梯形图编程环境那要另起炉灶在上位机写一个梯形图解析器把梯形图转换成指令表再通过串口下载到STM32。这是个更庞大的工程但做出来绝对惊艳。附一个实用示例代码实现电机正反转互锁控制// 正转按钮X0、反转按钮X1、停止按钮X2 // 正转接触器Y0、反转接触器Y1、热继电器X3 void logic_motor(void) { // 自锁互锁控制 Y_Map[0] (X_Map[0] || Y_Map[0]) !X_Map[2] !X_Map[3] !Y_Map[1]; Y_Map[1] (X_Map[1] || Y_Map[1]) !X_Map[2] !X_Map[3] !Y_Map[0]; }在这段代码里!Y_Map[1]和!Y_Map[0]就是互锁——正转和反转不可能同时接通这是电机控制里绝对要保证的安全条件。热继电器常闭触点接入X3一旦过载断开就立即切断所有输出这是最最基本的保护逻辑。4. 实操过程与硬件调试记录4.1 物料清单与选型避坑实际采购的时候以下几个物料容易踩坑STM32F103C8T6最经典的“最小系统板”某宝十几块钱注意买带Flash的版本那种C8T6是64KB用来跑PLC逻辑够用但想塞大协议栈就紧张了。PC817光耦注意引脚顺序。自己画板子的时候很多人把1脚和4脚搞反导致上电后输入侧直接烧LED。PC817的1脚是阳极2脚是阴极3脚是发射极4脚是集电极。ULN2803输入是高电平有效内部是达林顿管加续流二极管输出端接继电器线圈线圈的另外一端接5V或24V电源。AMS1117-3.3注意功耗。工业电源是24V如果你直接用AMS1117把24V降到3.3V那功耗全烧在这个芯片上发热巨大。必须先用DC-DC降压模块把24V降到5V再用AMS1117降到3.3V。物料清单表模块物料型号/规格数量电源DC-DC降压模块MP1584/LM25961电源3.3V LDOAMS1117-3.31主控STM32最小系统板STM32F103C8T61输入光耦PC8178输入限流电阻1kΩ 06038输出达林顿管ULN28031输出继电器SRD-05VDC6通信RS485芯片MAX4851通信终端电阻120Ω14.2 手工焊接与测试流程有了物料之后我是在洞洞板上完成的第一版。这里分享一个经验先焊电源再焊MCU最小系统最后焊外围IO。电源焊完之后上电测24V→5V→3.3V电压是否正常确认无误后再焊后面的电路。千万别一上来就把所有元器件全焊上去如果电源有问题后续全白做排查起来也头大。MCU部分焊完后第一件事是用ST-Link连接SWD端口如果能识别到芯片并能擦除Flash说明MCU没问题。ST-Link Utility这个工具可以用来查看芯片ID和Flash容量确认买到的是不是翻新片。检测到之后先把一个LED闪烁程序烧进去看GPIO输出是否正常。然后再焊输入光耦、输出ULN2803和通信芯片。输入测试方法用一根杜邦线把24V正极接到某个输入端子看MCU对应引脚是否是低电平——光耦导通后输出侧被拉低。如果发现输入状态一直是高或一直是低多半是限流电阻虚焊或者光耦引脚焊反了。输出测试方法写一个简单的程序让某个Y引脚输出高电平然后用万用表量ULN2803输出端和GND之间的电压再听继电器有没有咔哒声。如果继电器工作正常就说明输出链路完整。最后测试通信用USB转485模块连接STM32的A、B线打开串口助手发送Modbus地址01功能码03读寄存器的请求看能不能收到正确回复。第一次联调如果失败建议把波特率降到9600默认发送频率设慢一点排除线材干扰。4.3 完整调试记录一个电机正反转控制的诞生我拿这个仿真板做了一个实际的验证项目通过两个按钮控制一台三相交流接触器电机正反转。系统接线是X0接正转按钮常开X1接反转按钮常开X2接停止按钮常闭X3接热继电器常闭Y0接正转交流接触器Y1接反转交流接触器。梯形图逻辑在C语言里写出来后我频繁遇到一个问题正转的时候点反转有时候要按两三次才生效。排查发现是按钮抖动引起的。虽然光耦输入侧有RC滤波但按钮的机械抖动发生在RC滤波之后依然会给CPU带来多次边沿触发。解决方式是在软件里加一个防抖连续读到X0为真超过30ms才认为有效否则忽略。有了这个30ms防抖之后整个系统就非常干脆了。热继电器动作测试我也做了用螺丝刀把热继电器的复位按钮压下去相当于模拟过载断开常闭触点这时X3变成高电平触点断开程序立即切断Y0和Y1接触器跳闸。这个保护逻辑实测下来响应时间是扫描周期级别最多1-2ms完全合格。5. 常见问题与排查技巧实录5.1 问题速查表现象可能原因排查/解决方法上电后STM32无反应不跑程序晶振未焊接或虚焊BOOT0引脚电平不对检查晶振和两个20pF电容确认BOOT0接GND输入信号一直为高光耦第1/2脚焊反限流电阻开路测量光耦LED两端电压正常约1.2V输出继电器不动作ULN2803输入侧虚焊继电器线圈供电不足量ULN2803输入引脚电压再用万用表量线圈两端串口通信乱码波特率不匹配RS485 A/B接反确认上位机和下位机波特率一致交换A、B线定时器延时不准使用了DWT或普通延时函数未用SysTick改用SysTick中断中断里累加时间片程序下载失败ST-Link接触不良SWD引脚被复用按住复位键再下载或避开PB3/PB4引脚继电器频繁烧毁续流二极管未并联或极性接反线圈两端并1N4007阴极接电源正极5.2 经验技巧那些文档里不会写的东西第一输入侧和输出侧的供电一定要隔离。我第一版把输入光耦的输出侧和MCU共地了结果外部干扰一来MCU直接复位。后来把输入光耦的输出侧单独用了一路5V供电地线分开只在一点共地抗干扰能力立刻好很多。第二STM32的GPIO驱动能力有限推挽输出最大也就20mA。如果你直接用GPIO驱动LED没问题但驱动光耦输入、继电器这种耗电稍大的器件就要加三极管或驱动芯片。我踩过直接用GPIO驱动LED限流电阻太小导致引脚烧掉的坑。第三别把下载引脚复用掉。STM32的SWD下载用的是PA13和PA14这两个引脚如果被你的程序复用成普通GPIO下载器就失效了。解决办法就是下载时按住复位键不放等下载器连接上再松开。但最好是程序中始终保留这两个引脚不碰。第四工业环境里电源的波动比你想的大得多。我实测过一个24V开关电源在接触器吸合的瞬间电压会跌到18V而且伴随大量毛刺。如果你的STM32板子和接触器共用电源非常容易复位。解决办法是给控制板单独加一个DC-DC模块或者至少加一个大容量的电解电容470μF以上在24V入口做缓冲。第五RS485通信一定要用双绞线或者屏蔽线不要用普通杜邦线。杜邦线之间距离近干扰直接串扰。我的板子用双绞线后通信丢帧率降了至少十倍。5.3 软件层面的排障心得代码跑飞或运行异常时我习惯用串口打印大法在关键函数入口打印一个调试字符通过观察打印输出的位置来定位问题。如果打印到某个函数后程序死掉问题大概率就在这个函数内部。还有一个方法很实用利用STM32的硬件定时器做一个看门狗在主循环里喂狗如果程序卡死或者陷入死循环看门狗自动复位。这在调试指令解释器这种复杂逻辑的时候尤其好用至少保证了系统不会长期锁死。5.4 稳定性的深入思考很多朋友问过用STM32仿真PLC到底能不能直接用于生产我的回答是如果你的负载是纯电阻负载、小型继电器、指示灯响应时间几百毫秒就行那这个方案是可行的但如果你要驱动伺服电机、步进电机高速脉冲、做PID闭环那还是老实买原厂PLC吧。STM32的GPIO响应、PWM精度和原厂PLC的硬逻辑电路还是有差距的特别是定时器中断在高负载情况下的抖动会让运动控制效果大打折扣。不过作为一个学习平台、一个原型验证平台这个方案的优势非常明显——便宜、透明、可改性强。通过它你理解了PLC的底子后面再去用三菱、西门子这些大厂的设备心里会多几分底。6. 后续可扩展的方向这个项目延伸出去能玩的东西实在太多。我的下一步计划是做一块完整的四层PCB板把电源、MCU、光耦隔离、继电器输出、RS485全部集成到一块板子上尺寸控制在名片大小外壳用3D打印做一个导轨安装卡扣这样它就更像一台真正的小型PLC了。软件方面我打算写一个简单的上位机在PC端用类似梯形图的界面编辑逻辑然后编译成指令表通过串口下发给STM32这样就基本复刻了PLC的完整编程体验。如果你对这个方向有兴趣不妨从“指令解释器 Modbus从站”这两个核心模块做起一步步迭代收获绝对不小。本文还有配套的精品资源点击获取