
这是我的本科毕业设计项目,基于 STM32 与 Modbus RTU 协议设计了一套分布式 IO 拓展系统。系统采用模块化硬件架构和封装式软件设计,可根据具体应用场景灵活组合 IO 功能。

项目背景
在工业 4.0 与中国制造 2025 的背景下,工业控制系统对可扩展性和可维护性的要求越来越高。传统集中式 IO 系统在面对复杂生产设备时,布线成本高、扩展困难。分布式 IO 系统因其灵活部署和高可靠性的特点,被广泛应用于智能制造、能源管理、楼宇自动化等领域。
然而,市场上的分布式 IO 产品(如西门子 ET200SP、施耐德 Modicon Edge IO 等)价格较高,且不同厂商的产品之间兼容性有限。本项目旨在设计一套低成本、可灵活配置的分布式 IO 系统框架。
系统架构
整个系统由三类模块组成,通过统一的 PH2.0 2×20P 排针排母接口和内部 RS-485 总线连接:

核心板
系统的计算与控制核心,基于 STM32F103C8T6 最小系统设计。
- 128K Flash + 20K SRAM
- 引出 UART、SPI、I2C、CAN 等外设接口
- 外部 5V 供电,板载 AMS1117 LDO 稳压至 3.3V
- 双 LED 指示(运行状态 + 总线状态)

核心板通过标准接口与不同功能板配合,实现不同的 IO 功能。
Modbus RTU 接口板
作为系统网关,连接外部 PLC 与内部扩展总线。
- 外部 24V DC 供电(支持 18-36V 宽压输入)
- 板载防雷保护、ESD 防护、防反接整流、自恢复保险丝
- DCDC 降压(LGS5145,4.5-55V 输入)为核心板供 5V
- 双路 UART 转 RS-485(MAX485),分别用于外部通讯和内部总线
- 外部接口兼容西门子 S7-1200/S7-1500 PLC

数字量扩展板
实现具体的 IO 功能,挂载在内部总线上。
数字量输入板:
- 8 路晶体管数字量输入,带 LED 状态指示
- 1 路高速脉冲输入(≥1KHz,计数误差 ≤0.3%)
- 输入信号光耦隔离(TLP281-4,隔离度 ≥1000V DC)
数字量输出板:
- 8 路晶体管数字量输出,带 LED 状态指示
- 1 路高速脉冲输出(≥5KHz,误差 ≤0.3%),支持频率和占空比可调
- 输出信号光耦隔离

软件设计
软件架构采用封装式设计,便于移植到不同硬件平台。
Modbus RTU 协议栈
自行实现了完整的 Modbus RTU 从站协议栈,支持以下功能码:
| 功能码 | 功能 |
|---|---|
| 01 | 读取线圈寄存器 |
| 02 | 读取离散输入寄存器 |
| 03 | 读取保持寄存器 |
| 05 | 写单个线圈寄存器 |
| 06 | 写单个保持寄存器 |
| 0F | 写多个线圈寄存器 |
| 10 | 写多个保持寄存器 |
协议栈采用分层设计:底层串口收发 → 帧解析与 CRC 校验 → 功能码分发 → 寄存器读写回调。各模块只需实现自己的寄存器回调函数即可。
Modbus 帧解析与功能码分发核心逻辑:
void Modbus_Process(uint8_t *frame, uint16_t len) {
uint8_t addr = frame[0];
uint8_t func = frame[1];
// CRC 校验
uint16_t crc_recv = (frame[len-1] << 8) | frame[len-2];
uint16_t crc_calc = Modbus_CRC16(frame, len - 2);
if (crc_recv != crc_calc) return;
// 地址过滤
if (addr != local_addr) return;
// 功能码分发
switch (func) {
case 0x01: Modbus_ReadCoils(frame); break;
case 0x02: Modbus_ReadDiscreteInputs(frame); break;
case 0x03: Modbus_ReadHoldingRegs(frame); break;
case 0x05: Modbus_WriteSingleCoil(frame); break;
case 0x06: Modbus_WriteSingleReg(frame); break;
case 0x0F: Modbus_WriteMultiCoils(frame); break;
case 0x10: Modbus_WriteMultiRegs(frame); break;
default: Modbus_ErrorResponse(addr, func, 0x01); break;
}
}

接口板网关逻辑
接口板核心板运行网关转发程序,负责:
- 接收外部 PLC 的 Modbus RTU 请求
- 根据从站地址转发到内部总线上对应的扩展板
- 将扩展板的响应回传给 PLC
void Gateway_Forward(uint8_t *plc_frame, uint16_t len) {
uint8_t target_addr = plc_frame[0];
// 查询寄存器偏移表,定位目标扩展板
uint8_t bus_addr = RegOffsetTable_Lookup(target_addr);
if (bus_addr == 0xFF) {
// 无设备响应
return;
}
// 转发到内部 RS-485 总线
IBUS_Send(plc_frame, len);
// 等待扩展板响应(超时机制)
uint16_t resp_len = IBUS_WaitResponse(resp_buf, TIMEOUT_MS);
if (resp_len > 0) {
// 回传给外部 PLC
EBUS_Send(resp_buf, resp_len);
}
}
扩展板 IO 控制
- 数字量输入:GPIO 配置为输入模式,通过 IDR 寄存器读取引脚电平,映射到离散输入寄存器
- 高速脉冲输入:TIM 配置为外部时钟模式,上升沿触发 CNT 计数,映射到保持寄存器
- 数字量输出:GPIO 配置为推挽输出,通过 ODR 寄存器控制,映射到线圈寄存器
- 高速脉冲输出:TIM 配置为 PWM 比较输出模式,72MHz 主频预分频至 1MHz,通过 ARR/CCR 寄存器控制频率和占空比
数字量输入——读取 8 路 GPIO 状态并映射到离散输入寄存器:
// 02 功能码回调:读取离散输入寄存器
uint8_t Modbus_ReadDiscreteInputs_CB(uint16_t addr, uint16_t qty) {
uint8_t state = Get_GPIO_Ports(); // 读取全部 IO 状态
// 按寄存器地址偏移返回对应位
return (state >> addr) & ((1 << qty) - 1);
}
uint8_t Get_GPIO_Ports(void) {
uint8_t state = 0;
state |= ((GPIOB->IDR >> 10) & 0x01) << 0; // PB10 → Port1
state |= ((GPIOB->IDR >> 11) & 0x01) << 1; // PB11 → Port2
state |= ((GPIOB->IDR >> 12) & 0x01) << 2; // PB12 → Port3
// ... 其余端口类似
return state;
}
高速脉冲输出——TIM PWM 配置与保持寄存器控制:
void TIM_PWM_Init(void) {
// 72MHz / (71+1) = 1MHz 计数频率
htim.Init.Prescaler = 71;
htim.Init.CounterMode = TIM_COUNTERMODE_UP;
htim.Init.Period = 0xFFFF; // ARR: 控制输出频率
htim.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_PWM_Init(&htim);
// PWM 通道配置
sConfig.OCMode = TIM_OCMODE_PWM1;
sConfig.Pulse = 0; // CCR: 控制占空比
HAL_TIM_PWM_ConfigChannel(&htim, &sConfig, TIM_CHANNEL_3);
}
// 03/06/10 功能码回调:读写保持寄存器
void Modbus_HoldingReg_CB(uint16_t addr, uint16_t value, uint8_t rw) {
if (rw == WRITE) {
switch (addr) {
case 0x00: TIM3->ARR = value; break; // 频率控制
case 0x01: TIM3->CCR3 = value; break; // 占空比控制
case 0x02: // 脉冲数量
if (value > 0) HAL_TIM_PWM_Start_IT(&htim, TIM_CHANNEL_3);
break;
}
} else {
// 读取对应寄存器值返回
}
}
总线寻址
每块扩展板通过硬件拨码开关设置从站地址,ADC 读取分压电阻值识别地址,支持总线上多设备挂载。
uint8_t Get_Bus_Address(void) {
uint16_t adc_val = HAL_ADC_GetValue(&hadc1); // 读取 AIN 引脚电压
// 根据分压电阻阶梯映射到地址 1~8
if (adc_val < 512) return 1;
if (adc_val < 1024) return 2;
if (adc_val < 1536) return 3;
if (adc_val < 2048) return 4;
// ...
return 0xFF; // 无效地址
}
硬件设计
所有 PCB 使用嘉立创 EDA 设计,核心板与功能板通过统一的排针排母接口对接。
主要设计要点:
- 电源保护:防雷空气放电管 + ESD 保护二极管 + 防反接整流 + 自恢复保险丝
- 信号隔离:输入输出均采用光耦隔离(TLP281-4),隔离度 ≥1000V DC
- 电平转换:UART 转 RS-485 采用 MAX485 芯片
- 模块化接口:统一 PH2.0 2×20P 连接器,核心板可即插即用
测试结果

- 数字量输入输出功能正常,LED 指示准确
- 高速脉冲输入计数频率 ≥1KHz,误差在 0.3% 以内
- 高速脉冲输出频率 ≥5KHz,误差在 0.3% 以内
- 与西门子 S7-1200 PLC 通过 Modbus RTU 通讯正常
- 系统在 18-36V 供电范围内稳定运行

总结
这个项目让我完整经历了从需求分析、硬件选型、原理图设计、PCB Layout、焊接调试到固件开发的全流程。模块化的设计思路使系统具备良好的可扩展性——后续只需设计新的功能板(如模拟量输入输出板),挂载到内部总线上即可。软件的封装式设计也使协议栈和驱动代码可以方便地移植到其他 MCU 平台。