FX3U复刻到STM32F103VET6:国产PLC替代方案设计与实现 简介基于STM32F103VET6单片机实现的国产三菱FX3U全套设计资料以硬件原理图、PCB版图与软件源码为核心面向PLC开发工程师、嵌入式学习者及相关专业毕业生适合用作物联网设备、工装夹具或小型自动化项目中的控制器原型参考也可作为理解国产PLC软硬件协同设计的入门范本。整个zip压缩包共含532个文件大小约24.16MB文件内容涵盖原理图与PCB设计源文件、C程序源码与头文件、Keil工程配置、编译过程中间文件以及多个历史版本备份目录其中schdoc、pcbdoc可完整还原硬件设计c/h文件则对应固件逻辑。源码程序展示了输入输出端口初始化、RTC时钟配置、断电数据保持与恢复、串口和485通信等PLC典型功能模块能够直观看到从底层寄存器配置到应用层逻辑的完整实现路径。硬件设计部分通过原理图和PCB呈现板级走线、元器件布局和接口定义方便对照软件进行联调排查。目前已有一千二百八十四人学习浏览适合在开发调试时作为高风险项目的设计参考或工程模板。1. 把三菱 FX3U 复刻到 STM32F103VET6 上一套可学习也可改造的国产 PLC 方案车间里 FX3U 老设备出故障多半不是 IO 坏了就是通信口被浪涌打掉。原厂板子价格高、周期长直接换新机型又要重写梯形图、重新调参数。基于 STM32F103VET6 的国产 FX3U 复刻方案解决的正是这个维护痛点用一颗 512KB Flash 的 Cortex-M3 芯片把 FX3U 的输入输出映像、D/M 软元件、485 编程口协议这些核心行为重新实现一遍外部接线和用户梯形图不改就能把旧程序接力跑起来。这套 V8.0 资料包含完整硬件原理图、PCB 文件和 Keil 工程源码适合自动化设备改造工程师拿来评估替代方案也适合嵌入式开发者研究 PLC 的扫描执行模型和 Modbus 通信实现。全文就是围绕这两条主线把硬件选型、固件启动、通信与掉电保持、烧录验证逐个拆开讲透。2. 硬件层拆解电源轨、IO 光耦、PCB 走线里的 EMC 设计细节拿到 V8.0 资料先不要急着打开源码。硬件原理图和 PCB 决定了这套方案能跑在什么样的工业环境里也决定了后期改板时你会不会掉进坑。STM32F103VET6 本身不是 PLC电源、隔离、接口保护才是 PLC 与开发板的真正区别。下面按“电源→IO→PCB”三个层次把板子过一遍这几个位置对排查上电异常帮助最直接。2.1 电源轨设计24V 输入怎么变成 3.3V 给 MCUFX3U 兼容板的外部供电绝大多数是 24V 直流而 MCU 内核和片上外设需要 3.3V中间至少隔两级转换。原理图第一眼应该先看电源树而不是看 CPU 引脚。电源轨典型来源主要负载设计注意点24V_IN外部开关电源DCDC、继电器线圈、输入光耦入口并 TVS如 SMBJ24A和防反接二极管5VDCDC如 MP1584、LM2596485 收发器、光耦输出侧、继电器驱动与 3.3V 之间用单点接地连接3.3VLDO如 AMS1117、RT9013STM32F103VET6、RTC、晶振电路靠近 VDDA 引脚加磁珠和 0.1uF 去耦STM32F103VET6 的工作电压是 2.03.6V板上 3.3V 由 5V 经 LDO 再降一级得到。这套“DCDCLDO”的组合在工控板上很常见DCDC 负责把 24V 高效降到 5V 并承受输入波动LDO 再给 MCU 提供低纹波的 3.3V避免 DC-DC 开关噪声直接串进 ADC 和复位电路。如果原理图里只看到一颗 LDO 从 24V 直接降到 3.3V就要注意压差发热24V 输入时线性稳压器会消耗掉大部分功率。电源走线在原理图阶段就要圈出来24V、5V、3.3V、GND 和 PGND 这五类网络是重点。GND 和 PGND 通常在继电器或光耦附近用 0 欧电阻或磁珠做单点连接。我一般会先在原理图上把这五类网络标上颜色再生成 BOM 和网络表检查隔离两侧地线是否被错误短接避免后期 PCB 改板时带着问题投产。2.2 IO 通道电路输入分压、光耦 CTR 和输出续流输入侧FX3U 的数字量输入是 24V 漏型/源型两用的。复刻板上常见做法是 24V 信号先经过几个电阻分压限流再驱动 PC817 这类三极管输出光耦光耦输出侧直接接 STM32 的 GPIO 并配上拉电阻。光耦的电流传输比CTR决定上拉电阻取值PC817 在 1mA 输入下 CTR 只有 50% 左右设计输入电流 510mA、上拉电阻留 23 倍裕量比较稳妥留小了低温下容易导通不稳定。输出侧继电器输出比晶体管输出更容易被接受因为负载回路和 MCU 完全隔离。驱动继电器线圈要反向并联续流二极管否则断电瞬间的感生电动势会倒灌击穿驱动管。如果采用 ULN2803 这类达林顿阵列内部已经集成续流二极管但外部加一颗 TVS 到电源轨仍然必要尤其是带接触器、电磁阀这类感性负载时触点断开的瞬态会直接作用在输出端子上。2.3 PCB 布局与走线从网表里先抓出弱信号PCB 层数方面2 层板能省成本但 485、晶振、电源这些信号挤在同一层对铺地和回流控制要求比较高这类兼容 PLC 的板子如果条件允许做 4 层把第二层整层作为 GND 平面走线和 EMC 调试都会轻松很多。RS-485 差分线要成对走、保持间距一致收发器尽量靠近端子TVS 管放在连接器入口处让浪涌在进板前就被吸收而不是进板后再绕一圈。拿到 gerber 或 PCB 源文件后我会先做一轮最小线宽和间距的自动筛查而不是只看视觉效果。从 Altium 或嘉立创导出的网表里带上走线宽度字段用简单脚本就能过滤出风险项# 最小线宽与电源线宽度检查输入为原理图导出的网表 CSV import csv MIN_SIGNAL_WIDTH 0.2 # 信号线最小线宽单位 mm MIN_POWER_WIDTH 0.6 # 电源网络最小线宽 PWR_NETS {24V, 5V, 3V3, GND, PGND} with open(net_widths.csv, newline, encodingutf-8) as fp: rows list(csv.DictReader(fp)) bad [] for row in rows: width float(row[Width_mm]) net row[Net] if net in PWR_NETS and width MIN_POWER_WIDTH: bad.append(f{net}: {width}mm 电源线 {MIN_POWER_WIDTH}mm) if net not in PWR_NETS and width MIN_SIGNAL_WIDTH: bad.append(f{net}: {width}mm 信号线 {MIN_SIGNAL_WIDTH}mm) print(\n.join(bad) if bad else 线宽检查通过)Width_mm是 PCB 布线数据里的实际铜皮宽度不是封装引脚宽度PWR_NETS里的网络名要和原理图实际标签一致。脚本只做数值报警真正判断还要回到板上看回流路径例如 485 收发器下方不要打一排信号过孔光耦正下方避免铺铜形成寄生电容。gerber 导出后还要检查丝印层。丝印字符压到焊盘会造成贴片不良字符最小高度建议 0.8mm 以上板边 3mm 内不要放丝印和过孔。钻孔孔径与板厚比、最小孔环这些参数交给板厂前至少用在线 DFM 工具跑一遍出问题的是“看起来连上了、实际回流焊后虚焊”这一类批量良率问题原理图阶段看不出来。3. 固件启动流程main() 初始化、D8000 软元件区与扫描执行模型V8.0 的源码工程在 Keil MDK 下打开目标芯片 STM32F103VET6外设库基于标准外设库stm32f10x_tim.c、stm32f10x_rcc.c 这一套。源码目录里 CRJ_FX3U_V4.0.3.201610、CRJ_FX3U_V6.0.3.180921.0 这些文件夹是历次固件版本归档对比不同版本 main() 的启动顺序能看出外设配置的演进。固件整体框架并不复杂核心思路是让 IO 和时钟先稳定再恢复用户数据最后才打开通信。3.1 main() 里谁先谁后IO 配置、RTC、数据恢复、串口 485拿到源码第一件事是读 main()因为它把 PLC 的上电顺序讲得最清楚int main(void) { // rstFlg RCC-CSR; // 复位标志后面掉电判断会用到 PLC_IO_config(); // PLC 输入输出初始化先锁定引脚方向 #if RTC_FUNC RTC_Init(); // RTC 时钟配置掉电后时间戳保持 #endif data_init(); // D8000~D8126 系统软元件初始化 PLC_ReadKeepData(); // 从 Flash/BKP 恢复保持型数据 USART1_Configuration(); // 串口初始化编程口对应 USART1 bsp_Init(); // 485 收发方向控制、定时器、状态指示灯 HS_MOD_LOCAL_ADDR 1; // 本机 Modbus 从站地址默认 1 // 后续主循环读输入映像 - 执行梯形图 - 刷新输出 - 处理通信帧 while (1) { plc_scan_loop(); } }PLC_IO_config()放在最前面不是习惯而是必须后续 RTC、串口初始化的过程中若有引脚电平跳变没有配置的 GPIO 处于浮空输入外部接线一旦给到高电平可能误触发还没配置好的外设。data_init()和PLC_ReadKeepData()的顺序也值得注意先初始化系统软元件区再从非易失介质回读用户数据两者不能反过来。被注释掉的RCC-CSR值得解释。RCC-CSR是复位状态寄存器可以区分上电复位、外部复位、看门狗复位和欠压复位。在掉电保持方案里判断“这次启动是不是掉电后的恢复”就是读这个寄存器典型写法是uint32_t rst_flags RCC-CSR; if (rst_flags RCC_CSR_PORRSTF) { // 上电复位保持数据区从 Flash 整块恢复 } else if (rst_flags RCC_CSR_PINRSTF) { // 外部手动复位保持数据维持 RAM 缓存不重读 Flash }HS_MOD_LOCAL_ADDR是通信从站地址默认 1对应 FX3U 编程口在 Modbus 场景下的站号。这个变量放在 RAM 里如果要断电保持应该在PLC_ReadKeepData()阶段从保持区回读并覆盖默认值。另外RTC 功能依赖板上的 32.768kHz 晶振如果 LSE 没焊或虚焊RTC_Init()可能一直停在同步等待上表现为主程序卡在初始化阶段串口没有任何输出这是上电“假死”的第一排查点。3.2 stm32f10x_tim.c 与扫描周期定时器中断如何驱动 PLC 任务PLC 的执行模型和 FX3U 一样循环扫描。一次扫描周期依次完成输入采样、用户程序执行、输出刷新、通信服务四个阶段。梯形图在固件里的表达方式通行做法是先把每个网络编译成中间指令表ladder.c 在扫描时逐条解释执行这种做法的执行速度比逐节点递归遍历快又比 ARM 原生代码方式容易移植到其他型号。ladder.c 内部维护一个软元件映像区X 输入、Y 输出、M 中间继电器、D 数据寄存器都映射到 RAM 数组。梯形图执行时只读写这些数组不直接操作 GPIO这样输出刷新可以统一做一次也避免用户程序高频翻转单个 IO 导致外部负载抖动。扫描周期由定时器保障源码里用到 stm32f10x_tim.c 的 TIM2 作为时基中断// TIM2 1ms 中断作为 PLC 扫描节拍与看门狗时基 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); ms_tick; if ((ms_tick 0x03) 0) // 每 4ms 递减一次扫描看门狗 scan_watchdog--; if (scan_watchdog 0) // 用户程序死循环或跑飞 system_fault_handler(); } }TIM_TimeBaseInitTypeDef 里的预分频和自动重载值决定中断周期72MHz 主频下产生 1ms 中断一般取prescaler71, period999。如果应用有高速计数需求TIM2 可能被复用为计数通道这时扫描时基要换到 TIM3 或 TIM4改板时容易漏。主循环里的plc_scan_loop()只是按状态机顺序调用各阶段函数真正的时间精度由定时器中断保证梯形图中的延时定时器指令、脉冲输出指令都以ms_tick为基准。3.3 D8000~D8126 系统软元件区为什么不能当普通 D 寄存器用D8000 开始的系统软元件区是 FX3U 体系里的系统状态字里面存放扫描时间、错误代码、版本信息等运行状态。data_init()先把它写到默认值再由通信任务和扫描任务在运行中持续刷新。用户程序读写这一区间要谨慎例如扫描周期统计值由内核按实际扫描时间周期写入用户往里面写数据下一轮扫描就会被覆盖等于白写。开发应用时临时变量和数据缓存放在普通 D 区D8000 起的一段只读用于诊断。范围内容用户程序可写D0~D511保持型数据寄存器掉电由 PLC_ReadKeepData() 恢复可读写D512~D7999非保持型数据寄存器上电清零可读写D8000~D8003系统信息、版本、错误代码data_init 初始化后建议只读D8100~D8126扩展与外设参数可在配置阶段写入保持区的具体范围在系统参数里可配V8.0 默认按 D0~D511 处理。调试时如果发现某个 D 寄存器断电不保存先确认地址是否落在保持范围内再查PLC_ReadKeepData()的实现而不是急着改硬件。4. 485 通信与掉电保持Modbus RTU 帧解析、CRC16 和保存区策略FX3U 原机编程口是 422复刻板改为 485这是硬件上的取舍因为 485 是工业现场更普遍的物理层。上位机把编程口协议封装成 Modbus RTU 帧通过 UART 发送到板子。V8.0 固件里USART1_Configuration()负责打开串口bsp_Init()把 485 方向控制引脚、收发切换准备好。这章把通信和掉电保持放在一起讲因为它们的共同点是都要处理异步、时序和极端条件。4.1 Modbus RTU 帧的接收地址过滤、超时断帧和 CRC 校验485 是半双工一条总线上多个从站共享线路靠地址区分。HS_MOD_LOCAL_ADDR1决定了本机只处理地址为 1 的帧。从站在收到非本机地址帧时直接丢弃不清空接收缓冲这样不会抢占总线处理时间也不会因为总线上其他站的数据导致误判。Modbus RTU 帧格式固定为字段长度说明从站地址1 字节0x01~0xF70x00 为广播地址功能码1 字节0x01 读线圈、0x03 读寄存器、0x06 写单个寄存器数据区N 字节寄存器起始地址、数量或写值CRC162 字节低字节在前对地址到数据结尾做 CRC 校验帧结束判定有两条路一种靠串口空闲中断IDLE接收线空闲一个字符时间产生另一种靠定时器超时。标准库里没有现成的 IDLE 中断常见做法是开一个 3~5 个字符时间的定时器窗口来断帧下面这个精简实现可以跑在任意 F103 工程里// 485 帧接收地址过滤 长度保护 超时断帧 #define RX_BUF_MAX 64 volatile uint8_t rx_buf[RX_BUF_MAX]; volatile uint16_t rx_len; volatile uint8_t rx_timeout; void USART1_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { ch USART_ReceiveData(USART1); rx_timeout 30; // 收到一个字节就喂超时计数器 if (rx_len 0 ch ! HS_MOD_LOCAL_ADDR) { return; // 第一字节地址不匹配整帧丢弃 } if (rx_len RX_BUF_MAX) rx_buf[rx_len] ch; } } // 在 1ms 定时器中断里递减 rx_timeout归零表示 30ms 无新数据判定帧结束rx_timeout是帧间隙超时9600bps 下 30ms 足够宽松也兼容一些上位机逐字节发送的方式。地址不匹配后不调用rx_len 0保持缓冲为空让下一条地址匹配的帧从头开始存。广播地址 0x00 任何从站都要响应处理方式是在地址判断里单独加一个分支不能直接丢帧。CRC16 校验使用 Modbus 标准多项式 0xA001校验范围从地址字节开始到数据区结束不含 CRC 本身uint16_t modbus_crc16(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ data[i]; for (uint8_t bit 0; bit 8; bit) { crc (crc 0x0001) ? (crc 1) ^ 0xA001 : crc 1; } } return crc; // 返回值为低字节在前的两个字节 }调用时把rx_buf和rx_len - 2传进去再和帧尾两个字节比对不匹配就丢弃。CRC 初值固定 0xFFFF0xA001 是 Modbus 标准对多项式 0x8005 的反射形式不能用查表法里的其他初值替换否则和上位机对不上。串口参数方面FX3U 编程口默认是 9600bps、8 数据位、无校验、1 停止位改波特率要同时改USART1_Configuration()里的 BRR 分频值和上位机组态软件的通信参数。4.2 掉电数据恢复BKP 备份寄存器、Flash 写入和复位标志配合FX3U 的 D 寄存器有保持区和非保持区保持区数据断电后要能恢复。PLC_ReadKeepData()在这里是恢复入口它的保存介质有两个来源内部 Flash 划出的扇区或 RTC 域里的备份数据寄存器BKP。两种介质的可靠性模型完全不同保存介质容量写寿命典型用途STM32 内部 Flash 扇区1KB 到 128KB 不等约 1 万次擦写掉电瞬间批量保存RTC 备份寄存器BKP42×16 位无写次数限制频繁更新的小批量数据外挂铁电 FRAMKB 到 MB 级百亿次量产方案更合适内部 Flash 寿命只有一万次擦写绝不能每次 D 寄存器变化都写。合理做法是数据在 RAM 里累积软件检测到 24V 跌落或收到保存指令时一次性写入 Flash 扇区上电PLC_ReadKeepData()只读一次。如果数据修改频繁且量不大优先用 BKP 寄存器它没有写寿命问题但需要 VBAT 引脚接电池或大电容否则断电后内容同样丢失。BKP 写入在标准库里的用法是PWR_BackupAccessCmd(ENABLE); // 打开备份域写访问 BKP_WriteBackupRegister(RTC_BKP_DR1, d0); // 把 D0 的值写进备份寄存器 1 BKP_WriteBackupRegister(RTC_BKP_DR2, d1); // D1 - 备份寄存器 2 BKP_WriteBackupRegister(RTC_BKP_DR3, d2); // D2 - 备份寄存器 3PWR_BackupAccessCmd必须在写 BKP 之前调用它使能备份域的写保护读取 BKP 不需要这个指令但上电后要等备份域时钟稳定。备份寄存器数量有限F103 系列 42 个 16 位寄存器能装下 42 个保持 D 寄存器或 21 个 32 位变量适合保存通信参数、设备地址这类关键数据大批量配方数据仍要落到 Flash。V8.0 里rstFlg RCC-CSR被注释掉其实是另一个配合点掉电恢复时用复位标志区分“上电冷启动”还是“从掉电模式唤醒”决定是否重新从 Flash 读取保持数据。如果每次上电都无条件重读Flash 里恰好是旧数据就可能覆盖掉电前 BKP 里的最新值。上电先读复位标志发现欠压复位BOR才执行整块恢复可以避免这种错误覆盖。5. 上电验证三步走分散加载文件、485 回环和掉电数据检查资料包根目录里有CRJ_FX3U_V6_0_3_180921_sct.Bak这个文件.Bak后缀说明工程调试阶段改过分散加载文件。分散加载文件.sct决定代码、只读数据、RW 数据落到 Flash 的哪些地址改它往往是为了把保持数据区固定到一个独立区域避免与固件升级互相覆盖。这个文件值得在烧录前看一眼。5.1 用分散加载文件把保持数据隔离到独立 Flash 区域STM32F103VET6 的 Flash 是 512KB扇区大小并不一致前 4 个扇区是 16KB后面有大扇区。要隔离保持数据不如直接把代码区限制在前 384KB把最后一个 128KB 大扇区整个划出去这样擦写数据区时不会碰到代码LR_IROM1 0x08000000 0x00060000 { ; 384KB 代码区 ER_IROM1 0x08000000 0x00060000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00010000 { .ANY (RW ZI) } } LR_KEEP 0x08060000 0x00020000 { ; 最后 128KB 扇区独立数据区 ER_KEEP 0x08060000 0x00020000 { keep.o (RW ZI) } }LR_KEEP建立独立的加载域keep.o对应编译单元被指定进去用户程序升级或擦写其他区域时不会误伤保持数据。要注意的是擦除粒度F103 的大扇区最小擦除单位就是 128KB如果你把保持数据放在代码区中间一擦就全没了把代码区上限缩到 0x00060000最后一个扇区整块留给数据才算真正隔离。改完 .sct 要验证一次地址映射编译后在 Keil 的 MAP 文件里搜keep确认数据段落在 0x08060000 这个区间。注意在工程配置里必须关闭Use Memory Layout from Target Dialog否则你的 .sct 文件会被忽略保持数据又回到默认布局里。5.2 485 回环验证与掉电恢复确认烧录后第一次上电按顺序验证三件事IO 是否受控、RTC 是否走时、485 是否响应。485 验证不需要上位机组态软件用 USB-485 转换器直连 A/B串口助手发一帧 Modbus 读保持寄存器命令发送01 03 00 00 00 02 C4 0B 返回01 03 04 xx xx xx xx CRC_LO CRC_HI01是设备地址对应代码里HS_MOD_LOCAL_ADDR103是读保持寄存器功能码00 00代表从 D0 开始读00 02读两个寄存器。返回段里 4 个xx就是 D0 和 D1 的内容可以和硬件侧写入的状态对应。如果返回的是01 83 02 C0 F1这类异常帧83是异常功能码02表示非法数据地址先核对固件里启用了哪些 Modbus 功能码再确认 D0 起始地址没超出保持区范围。掉电恢复的验证方法用可调电源给板子供 24V先把 D0 写成一个非零值比如 0x5A5A断开 24V 等待 5 秒重新上电立刻读 D0确认返回值还是 0x5A5A。如果读回 0先查PLC_ReadKeepData()有没有被条件编译关掉再看保持数据用的是 Flash 还是 BKP——BKP 方案要求 VBAT 一直有电断开 24V 的同时把电池也断开数据必然丢失。实际现场测试不能直接把 24V 拉掉拆线前先断负载避免继电器触点拉弧反灌到主板损坏 IO 通道。本文还有配套的精品资源点击获取