1. 项目概述MiniPLC_FX2N 是一款面向 ESP32 平台的轻量级可编程逻辑控制器PLC固件库专为 MiniPLC-32u 硬件平台设计。该库并非通用型 PLC 运行时环境而是以“FX2N 兼容指令集”为蓝本在资源受限的 ESP32 微控制器上实现工业级 PLC 的核心控制语义——包括继电器逻辑LD、定时器T0–T199、计数器C0–C199、数据寄存器D0–D999及基本位操作指令AND、OR、ANB、ORB、LDP、LDF 等。其本质是将 IEC 61131-3 中的 LD梯形图语言抽象层通过确定性状态机与紧凑内存映射在无操作系统或 FreeRTOS 环境下完成硬实时执行。项目命名中的 “FX2N” 并非指代三菱电机原厂 FX2N 系列 PLC 的硬件复刻而是明确声明其指令集兼容性与编程模型一致性工程师可使用 GX Works2 或其他支持 FX2N 指令集的编程软件编写梯形图逻辑经编译生成标准 .lst 或 .m90 格式指令列表后通过串口或 OTA 方式加载至 MiniPLC-32u 设备由 MiniPLC_FX2N 固件解析并周期性扫描执行。这种设计极大降低了嵌入式 PLC 的入门门槛使传统工控工程师无需学习 C/C 即可参与边缘控制开发。MiniPLC-32u 硬件平台基于 ESP32-WROVER 模组标配 4MB PSRAM 4MB Flash具备 32 路可配置 GPIO含 12 位 ADC × 12、DAC × 2、SPI/I2C/UART × 3支持以太网通过 LAN8720 PHY与 Wi-Fi 双网络接入。MiniPLC_FX2N 库充分利用 ESP32 的双核特性Core 0 专用于 PLC 扫描周期调度与指令执行Core 1 用于通信协议栈Modbus TCP/RTU、自定义串口协议、Web 配置服务及 OTA 升级管理实现控制与通信任务的物理隔离保障扫描周期抖动 50μs典型值1ms 周期下。该库当前发布版本为 v1.0.1相较初版 v1.0.0 主要修复了以下关键缺陷定时器预设值写入异常当通过 Modbus 写入 T0–T199 的 K 值时若新值小于当前计时值旧版未清零当前计时器状态导致下次启动时立即触发输出上升沿检测LDP在高速脉冲下误触发因未对输入信号进行 3 采样点同步消抖存在亚稳态传播风险D 寄存器批量读取越界访问ReadDRegisterRange(start, count)函数未校验start count ≤ 1000可能引发 PSRAM 访问异常。所有修复均通过静态分析与硬件在环HIL测试验证符合 IEC 61131-3 第 3 部分对 PLC 扫描周期行为的定义。2. 系统架构与内存映射2.1 整体架构分层MiniPLC_FX2N 采用四层架构设计严格遵循嵌入式实时系统分层原则层级名称关键组件实时性要求运行位置L0硬件抽象层HALgpio_hal.c,timer_ll.c,uart_ll.c纳秒级响应Core 0L1PLC 运行时层Runtimeplc_scan.c,ld_executor.c,timer_mgr.c,counter_mgr.c微秒级确定性Core 0L2通信协议层Protocolmodbus_tcp.c,modbus_rtu.c,fx2n_serial.c,web_server.c毫秒级吞吐Core 1L3应用接口层APIplc_api.h,register_access.h,config_store.h无硬实时要求Core 1其中L0 与 L1 层完全运行于 Core 0禁用所有可能导致不可预测延迟的操作如 malloc/free、浮点运算、Cache 未命中密集型循环L2 层通过 xQueueSendFromISR 向 Core 0 发送控制指令如“强制置位 Y10”Core 0 在下一个扫描周期起始处原子性更新输出映像区L3 层提供 C API 供用户扩展例如在 FreeRTOS 任务中调用PLC_WriteY(10, true)直接控制物理输出该调用最终通过队列通知 Runtime 层。2.2 内存映射与寄存器布局MiniPLC_FX2N 在 PSRAM 中划分出专用区域作为 PLC 数据区采用紧凑结构体布局以最小化内存占用与访问延迟。全部寄存器均按 32 位对齐支持字节/字/双字访问// plc_memory_map.h —— 物理内存布局定义地址偏移基于 PSRAM 起始 typedef struct { uint8_t X[32]; // 输入继电器 X0–X31bit-addressable映射至 GPIO 输入寄存器 uint8_t Y[32]; // 输出继电器 Y0–Y31bit-addressable映射至 GPIO 输出寄存器 uint8_t M[256]; // 辅助继电器 M0–M2047bit-addressable软件实现 uint16_t T[200]; // 定时器当前值 T0–T199单位ms16-bit unsigned uint16_t TC[200]; // 定时器预设值K 值T0–T19916-bit unsigned uint16_t C[200]; // 计数器当前值 C0–C19916-bit unsigned uint16_t CC[200]; // 计数器预设值K 值C0–C19916-bit unsigned uint16_t D[1000]; // 数据寄存器 D0–D99916-bit unsigned支持 MOV、ADD、SUB 等 } __attribute__((packed)) plc_data_t; // 实际分配PSRAM 地址 0x3F800000 static plc_data_t* const PLC_DATA (plc_data_t*)0x3F800000;关键设计说明X/Y 寄存器直连硬件X[0] 对应 GPIO0Y[0] 对应 GPIO2读取PLC_DATA-X[0]即读取 GPIO0 电平写入PLC_DATA-Y[0] 1即设置 GPIO2 为高电平。此设计消除中间映射表降低 I/O 延迟至单条GPIO_IN_REG读指令。T/TC 与 C/CC 分离存储避免定时器/计数器状态机在修改预设值时产生竞态。例如OUT T0 K100指令仅写入TC[0] 100Runtime 层在每个扫描周期检查T[0] TC[0]并置位M[8000]T0 触点。D 寄存器为 16-bit符合 FX2N 规范但底层支持 32-bit 运算通过D[i] 16 | D[i1]组合。所有算术指令ADD、SUB均以 D 寄存器为操作数结果写回指定 D 地址。该内存布局总占用 4,512 字节32322564004004004002000远低于 ESP32-WROVER 的 4MB PSRAM为未来扩展如浮点寄存器、文件寄存器预留充足空间。3. 核心功能与指令执行机制3.1 扫描周期与执行流程MiniPLC_FX2N 的核心是确定性的扫描周期Scan Cycle默认周期为 1ms可通过PLC_SetScanTimeMs(uint16_t ms)动态调整范围 0.1ms–100ms。每个周期严格按以下五阶段执行全程在 Core 0 上以中断屏蔽方式运行确保无上下文切换开销输入刷新Input Refresh读取所有配置为输入的 GPIO存入PLC_DATA-X[]。采用三重采样消抖连续 3 次读取间隔 10μs取多数值。代码片段如下static inline void refresh_inputs(void) { for (int i 0; i 32; i) { uint32_t pin gpio_x_map[i]; // GPIO 编号映射表 uint32_t s0 GPIO.IN (1U pin); ets_delay_us(10); uint32_t s1 GPIO.IN (1U pin); ets_delay_us(10); uint32_t s2 GPIO.IN (1U pin); PLC_DATA-X[i/8] ~(1U (i%8)); // 清位 if ((s0 s1) || (s1 s2) || (s0 s2)) { // 三取二 PLC_DATA-X[i/8] | (1U (i%8)); } } }程序执行Program Execution解析并执行加载的指令列表。指令以fx2n_inst_t结构体数组存储于 Flashtypedef struct { uint8_t opcode; // 指令码如 0x00LD, 0x01AND, 0x20OUT, 0x30TMR uint8_t operand; // 操作数低 8 位如 X0 的 0T0 的 0 uint16_t k_value; // K 值仅 TMR/CTR 指令有效 } fx2n_inst_t;执行器采用查表法跳转每条指令平均耗时 12–25 个 CPU 周期ESP32 240MHz。特殊功能处理Special Function更新定时器/计数器状态定时器对每个TC[i] 0的 T执行T[i]若T[i] TC[i]则置位对应 M 寄存器T0→M8000并保持T[i] TC[i]不溢出计数器检测PLC_DATA-X[operand]下降沿LDF触发减计数C[i]--当C[i] 0时置位 M8100i。输出刷新Output Refresh将PLC_DATA-Y[]位写入对应 GPIO 输出寄存器。注意Y 寄存器为“影子寄存器”物理输出仅在此阶段更新避免执行中输出毛刺。通信轮询Communication Poll检查 Core 1 发来的指令队列如 Modbus 写请求原子性更新 D/T/C 寄存器。此阶段耗时 5μs不影响周期稳定性。整个扫描周期实测最大抖动为 42μsv1.0.1满足大多数工业控制场景如步进电机启停、气缸顺序控制的时序要求。3.2 关键指令实现解析定时器指令TMRFX2N 的定时器为“通电延时型”TON指令格式OUT T0 K100表示 T0 在驱动条件成立后延时 100ms 置位。MiniPLC_FX2N 的实现严格遵循此语义// ld_executor.c 中 TMR 指令处理 case OPC_TMR: { uint8_t t_idx inst-operand; // T0 → t_idx 0 bool coil_on get_coil_state(); // 获取前一条指令的 RLO逻辑运行结果 if (coil_on) { if (PLC_DATA-T[t_idx] PLC_DATA-TC[t_idx]) { PLC_DATA-T[t_idx]; // 递增当前值 } // 置位 M8000t_idx 表示定时器完成 set_m_bit(8000 t_idx, PLC_DATA-T[t_idx] PLC_DATA-TC[t_idx]); } else { PLC_DATA-T[t_idx] 0; // 驱动断开清零当前值 set_m_bit(8000 t_idx, false); } break; }工程要点get_coil_state()返回的是上一指令的 RLO而非直接读取 X/Y 位。这保证了梯形图中“串联逻辑”的正确性——例如LD X0 AND X1 OUT T0 K100只有 X0 与 X1 同时为 ON 时 T0 才开始计时。上升沿检测LDPLDP X0指令在 X0 由 OFF→ON 时一个扫描周期内为 ON。其实现依赖于输入映像区的历史快照// plc_scan.c 中维护输入历史 static uint8_t X_prev[32/8]; // 上一周期 X 值 void scan_cycle_begin(void) { memcpy(X_prev, PLC_DATA-X, sizeof(X_prev)); // 保存上周期值 refresh_inputs(); // 刷新当前 X 值 } // ld_executor.c 中 LDP 处理 case OPC_LDP: { uint8_t x_idx inst-operand; uint8_t byte x_idx / 8; uint8_t bit x_idx % 8; bool now PLC_DATA-X[byte] (1U bit); bool prev X_prev[byte] (1U bit); set_rlo(now !prev); // 仅当 now1 且 prev0 时 RLO1 break; }此设计确保 LDP 严格在一个扫描周期内有效符合 FX2N 行为规范。4. API 接口与配置详解4.1 主要 API 函数说明MiniPLC_FX2N 提供两套 API底层寄存器直写 API适用于裸机开发与高层 PLC 控制 API适用于 FreeRTOS 项目。所有函数均声明于plc_api.h。函数签名功能说明参数说明返回值典型用例PLC_Init(const plc_config_t* config)初始化 PLC 运行时config: 指向配置结构体含扫描周期、GPIO 映射表等ESP_OK或错误码app_main()中首次调用PLC_LoadProgram(const fx2n_inst_t* prog, size_t len)加载指令列表至 Flashprog: 指令数组首地址len: 指令条数ESP_OK或ESP_ERR_INVALID_SIZEOTA 升级后调用PLC_Start(void)启动扫描周期启用 Core 0 定时器中断无void加载程序后调用PLC_Stop(void)停止扫描清零所有 T/C/M/Y无void紧急停机PLC_WriteY(uint8_t y_num, bool value)强制写入 Y 寄存器绕过梯形图y_num: Y 编号0–31value: 目标电平ESP_OK或ESP_ERR_INVALID_ARGHMI 按钮直接控制 Y10PLC_ReadD(uint16_t d_addr, uint16_t* value)读取 D 寄存器值d_addr: D 编号0–999value: 输出缓冲区指针ESP_OK或ESP_ERR_NOT_FOUND读取温度设定值 D100关键参数配置结构体plc_config_ttypedef struct { uint16_t scan_time_ms; // 扫描周期单位 ms0.1–100 uint8_t x_gpio_map[32]; // X0–X31 对应的 GPIO 编号0xFF 表示未使用 uint8_t y_gpio_map[32]; // Y0–Y31 对应的 GPIO 编号0xFF 表示未使用 uint8_t uart_port; // 串口通信端口UART_NUM_0/1/2 uint32_t uart_baud; // 串口波特率默认 9600 bool enable_ethernet; // 是否启用以太网Modbus TCP bool enable_wifi_ap; // 是否启用 Wi-Fi AP 模式用于配置 } plc_config_t;配置示例初始化 1ms 周期X0–X7 接 GPIO0–7Y0–Y7 接 GPIO16–23plc_config_t cfg { .scan_time_ms 1, .x_gpio_map {0,1,2,3,4,5,6,7, 0xFF,/*...*/}, .y_gpio_map {16,17,18,19,20,21,22,23, 0xFF,/*...*/}, .uart_port UART_NUM_2, .uart_baud 115200, .enable_ethernet true, .enable_wifi_ap false }; PLC_Init(cfg); PLC_LoadProgram(my_program, ARRAY_SIZE(my_program)); PLC_Start();4.2 通信协议集成MiniPLC_FX2N 内置三种通信模式均运行于 Core 1通过消息队列与 Core 0 交互FX2N 串口协议兼容原厂编程电缆电气特性RS-232/485波特率自适应9600/19200/38400。接收.m90文件后自动解析为fx2n_inst_t数组并调用PLC_LoadProgram()。Modbus RTU串口将 X/Y/M/D/T/C 映射为 Modbus 地址X0–X31 → 00001–00032只读线圈Y0–Y31 → 00033–00064读写线圈D0–D999 → 40001–41000读写保持寄存器Modbus TCP以太网/Wi-Fi相同地址映射支持标准 Modbus TCP 客户端如 QModMaster直接监控。Modbus 写入 D 寄存器的底层流程Core 1 的modbus_tcp_task()解析写请求提取address40100对应 D100、value1234构造plc_cmd_t命令结构体通过xQueueSend(cmd_queue, cmd, portMAX_DELAY)发送至 Core 0Core 0 的plc_cmd_handler()在扫描周期间隙Output Refresh 后调用PLC_DATA-D[100] 1234确保原子性。此设计避免了在 Core 0 上执行网络协议栈彻底消除网络抖动对控制周期的影响。5. 典型应用与工程实践5.1 气动装配站顺序控制某汽车零部件装配站需控制 4 个气缸A/B/C/D按“夹紧→钻孔→松开→退回”循环动作。使用 MiniPLC_FX2N 实现如下硬件连接X0启动按钮、X1停止按钮、X2A 缸到位、X3B 缸到位… Y0A 缸电磁阀、Y1B 缸电磁阀…梯形图逻辑GX Works2 编写导出 .m90LD X0 // 启动 ANI X1 // 未停止 ANI M0 // 未运行 OUT M0 // 置位运行标志 LD M0 // 运行中 AND X2 // A 缸已夹紧 OUT T0 K500 // 延时 500ms 启动钻孔 ...部署步骤将.m90文件通过串口下载至 MiniPLC-32u调用PLC_LoadProgram()加载PLC_Start()启动通过 Web 界面http://ip/status实时监控 D100当前步骤、T0 当前值。该方案替代了传统 PLC成本降低 60%且支持远程 OTA 升级工艺参数如修改K500为K300加快节拍。5.2 与 FreeRTOS 的协同开发在复杂系统中常需 PLC 控制与高级算法共存。例如温控系统PLC 负责加热器启停Y0、风扇控制Y1FreeRTOS 任务运行 PID 算法并动态调整 D 寄存器// pid_task.c —— 运行于 Core 1 void pid_control_task(void* pvParameters) { float setpoint 85.0f; float current_temp; while(1) { current_temp read_ds18b20(); // 读取温度传感器 float output pid_compute(setpoint, current_temp); // PID 计算 // 将输出映射为占空比写入 D200 供 PLC 使用 uint16_t duty (uint16_t)(output * 100.0f); PLC_WriteD(200, duty); vTaskDelay(pdMS_TO_TICKS(100)); } } // PLC 梯形图中使用 D200 控制 PWM // LD D200 K50 OUT Y0 // 温度50%时开启加热器此架构实现了控制逻辑PLC与算法FreeRTOS的解耦符合工业软件分层设计规范。6. 调试与故障排查6.1 关键调试接口MiniPLC_FX2N 提供硬件级调试支持无需 JTAG串口调试命令通过 UART 发送ATPLC?返回当前扫描周期、T/C 当前值、错误码LED 指示灯编码Y31 保留为状态指示闪烁模式表示不同状态长亮运行1Hz待机2Hz通信错误寄存器快照导出调用PLC_DumpMemory()将全部 X/Y/M/T/C/D 寄存器以 CSV 格式通过 UART 输出用于离线分析逻辑错误。6.2 常见问题处理现象可能原因解决方法扫描周期严重超时2msGPIO 输入引脚悬空导致高频抖动三重采样耗时剧增为所有 X 引脚添加 10kΩ 下拉电阻Modbus 读取 D 寄存器返回 0PLC_LoadProgram()未成功执行D 区域未初始化检查PLC_LoadProgram()返回值确认指令数组长度非零LDP 指令不触发X 引脚配置为开漏输出而非输入检查plc_config_t.x_gpio_map中对应 GPIO 是否被其他外设复用所有诊断信息均记录于plc_log.h定义的日志缓冲区可通过PLC_GetLogBuffer()获取最近 128 条事件如“T0 timeout”, “X5 edge detected”为现场快速排故提供依据。