手把手教你用STM32单片机实现Modbus RTU从站(基于RS485,附完整工程源码)
2026/5/4 4:16:03 网站建设 项目流程

STM32实战:从零构建Modbus RTU从站(RS485通信+完整源码解析)

第一次接触工业通信协议时,我被Modbus协议简洁高效的魅力所吸引。记得三年前在自动化产线调试现场,看着PLC通过两根双绞线就能控制数十台设备,这种看似简单却异常可靠的通信方式让我着迷。今天,我们就用STM32F103这颗经典芯片,亲手搭建一个能接入工业控制系统的Modbus从站设备。

1. 硬件准备与环境搭建

1.1 硬件选型与连接

手头准备一块STM32开发板(我用的是正点原子MiniSTM32),外加一个RS485转换模块。市面上常见的MAX485芯片模块价格不到10元,却能在工业环境中稳定工作。接线时特别注意:

  • A/B线:RS485差分信号线,必须双绞且远离电源线
  • RE/DE:收发控制引脚,接STM32任意GPIO(我用的PC9)
  • 终端电阻:长距离通信时在总线两端接120Ω电阻
// GPIO初始化示例(CubeMX生成) void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOC_CLK_ENABLE(); // RS485收发控制引脚 GPIO_InitStruct.Pin = GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_9, GPIO_PIN_RESET); // 默认接收模式 }

1.2 开发环境配置

推荐使用STM32CubeIDE,它自动生成的代码框架能省去大量底层配置时间。关键配置步骤:

  1. 在Pinout视图启用USART2(或其他可用串口)
  2. 参数设置:波特率9600,8数据位,无校验,1停止位
  3. 开启串口全局中断
  4. 生成代码前勾选"Generate peripheral initialization as a pair of .c/.h files"

注意:如果使用Keil MDK,记得在Options for Target的C/C++选项卡添加USE_HAL_DRIVER宏定义

2. Modbus协议核心实现

2.1 寄存器映射设计

Modbus协议通过4种寄存器与设备交互,我们需在STM32内存中建立映射关系:

寄存器类型功能码地址范围映射目标
线圈寄存器0x010x0000-0xFFFFGPIO输出状态
离散输入0x020x0000-0xFFFFGPIO输入状态
保持寄存器0x030x0000-0xFFFF内部变量数组
输入寄存器0x040x0000-0xFFFFADC采样值
// 寄存器映射示例 typedef struct { uint8_t coil[COIL_REG_SIZE]; // 可读写位寄存器 uint8_t input[INPUT_REG_SIZE]; // 只读位寄存器 uint16_t holding[HOLDING_REG_SIZE]; // 可读写字寄存器 uint16_t input_reg[INPUT_REG_SIZE]; // 只读字寄存器 } ModbusRegMap; ModbusRegMap mb_regs = {0};

2.2 CRC16校验实现

Modbus RTU必须的CRC校验算法,这个优化版本比查表法更节省内存:

uint16_t ModbusCRC16(uint8_t *pdata, uint16_t len) { uint16_t crc = 0xFFFF; while (len--) { crc ^= *pdata++; for (uint8_t i = 0; i < 8; i++) { if (crc & 0x0001) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return (crc << 8) | (crc >> 8); // 高低字节交换 }

3. 功能码处理实战

3.1 读取保持寄存器(0x03)

这是最常用的功能码,上位机通过它获取设备数据。响应报文格式:

[设备地址][功能码][字节数][数据1][数据2]...[CRC16]
void Handle_ReadHoldingRegisters(uint8_t *request, uint8_t *response) { uint16_t startAddr = (request[2] << 8) | request[3]; uint16_t regCount = (request[4] << 8) | request[5]; // 异常检查 if (regCount > 125) { BuildExceptionResponse(request, response, ILLEGAL_DATA_VALUE); return; } response[0] = request[0]; // 设备地址 response[1] = request[1]; // 功能码 response[2] = regCount * 2; // 字节数 for (uint16_t i = 0; i < regCount; i++) { uint16_t regValue = mb_regs.holding[startAddr + i]; response[3 + i*2] = regValue >> 8; response[4 + i*2] = regValue & 0xFF; } uint16_t crc = ModbusCRC16(response, 3 + regCount*2); response[3 + regCount*2] = crc & 0xFF; response[4 + regCount*2] = crc >> 8; }

3.2 预设单个寄存器(0x06)

PLC常用此功能码修改设备参数。典型请求报文:

[01][06][00][01][00][03][CRC16]

表示将地址0x0001的保持寄存器值设为0x0003

void Handle_PresetSingleRegister(uint8_t *request, uint8_t *response) { uint16_t regAddr = (request[2] << 8) | request[3]; uint16_t regValue = (request[4] << 8) | request[5]; if (regAddr >= HOLDING_REG_SIZE) { BuildExceptionResponse(request, response, ILLEGAL_DATA_ADDRESS); return; } mb_regs.holding[regAddr] = regValue; // 回显相同数据作为响应 memcpy(response, request, 6); uint16_t crc = ModbusCRC16(response, 6); response[6] = crc & 0xFF; response[7] = crc >> 8; }

4. 通信优化与故障排查

4.1 超时管理机制

工业现场必须考虑通信异常情况,建议实现以下保护措施:

  1. 帧间隔超时:3.5个字符时间(9600bps时约4ms)
  2. 响应超时:从收到完整请求到开始响应不超过1秒
  3. 静默超时:持续2秒无通信自动复位接收状态
// 使用HAL库的定时器实现 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim == &htim3) { // 10ms定时器 static uint16_t silenceTimeout = 0; if (uartRxState == RX_BUSY) { if (++silenceTimeout > 200) { // 2秒超时 uartRxState = RX_IDLE; silenceTimeout = 0; } } } }

4.2 典型故障处理方案

现象可能原因解决方案
通信时好时坏终端电阻未接在总线两端接120Ω电阻
全部从站无响应A/B线接反调换A、B线位置
CRC校验失败波特率偏差检查双方波特率设置
偶发数据错误电磁干扰使用屏蔽双绞线,远离变频器

5. 工程源码架构解析

完整项目包含这些关键文件:

├── Core │ ├── Src │ │ ├── main.c // 主循环和初始化 │ │ ├── modbus_rtu.c // Modbus协议处理 │ │ └── rs485.c // 硬件驱动层 │ └── Inc │ ├── modbus_rtu.h │ └── rs485.h ├── Drivers └── STM32F1xx_HAL_Driver

modbus_rtu.c的核心处理流程:

void Modbus_ProcessRequest(uint8_t *request, uint16_t len) { // 校验CRC uint16_t crc = ModbusCRC16(request, len - 2); if (crc != ((request[len-1] << 8) | request[len-2])) { return; // 丢弃CRC错误帧 } // 检查设备地址 if (request[0] != DEVICE_ADDRESS) { return; // 非本设备报文 } switch (request[1]) { // 功能码分发 case 0x01: Handle_ReadCoils(request, response); break; case 0x03: Handle_ReadHoldingRegisters(request, response); break; case 0x06: Handle_PresetSingleRegister(request, response); break; // 其他功能码处理... default: BuildExceptionResponse(request, response, ILLEGAL_FUNCTION); } RS485_Send(response, GetResponseLength(response)); }

6. 上位机测试技巧

使用Modbus Poll软件测试时,这几个参数必须匹配:

  1. 传输模式:RTU
  2. 串口参数:波特率、数据位、停止位
  3. 从站地址:与代码中DEVICE_ADDRESS一致
  4. 扫描间隔:建议初始设置为1000ms

测试用例设计示例:

  1. 读取40001-40005保持寄存器(功能码0x03)
  2. 写入40001寄存器值为0x55AA(功能码0x06)
  3. 读取40001验证写入结果
  4. 故意发送错误CRC测试容错性
# 简单的Python测试脚本示例 import serial import struct ser = serial.Serial('COM3', 9600, timeout=1) def build_rtu_frame(addr, func, data): frame = bytes([addr, func]) + data crc = struct.pack('<H', ModbusCRC16(frame)) return frame + crc # 读取保持寄存器40001-40003 request = build_rtu_frame(0x01, 0x03, bytes([0x00, 0x00, 0x00, 0x03])) ser.write(request) response = ser.read(9) # 预期返回7字节+2字节CRC

调试阶段建议在关键位置添加日志输出:

printf("[Modbus] RX: "); for(int i=0; i<len; i++) printf("%02X ", buf[i]); printf("\r\n");

通过示波器观察RS485信号质量时,健康波形应具备:

  • 差分电压幅值大于1.5V
  • 上升/下降沿陡峭无振铃
  • 逻辑电平转换期间无毛刺

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询