STM32单片机RS485 Modbus通讯协议实现

简介: STM32单片机RS485 Modbus通讯协议实现

一、系统概述

Modbus是一种工业级串行通信协议,支持RTU(二进制)和ASCII(文本)两种模式,其中Modbus RTU over RS485因高效、可靠,广泛应用于工业控制、传感器网络、PLC通信等场景。

RS485是物理层标准,采用差分信号传输,支持半双工通信(同一时刻单向传输),最大传输距离1200m,最多挂载32个从机(可扩展至256个)。

本方案基于STM32单片机(如F103/F407)实现Modbus RTU主站/从站功能,通过RS485接口与传感器、执行器等设备通信,支持读/写保持寄存器读输入寄存器线圈控制等核心功能。

二、系统架构

  • 主站:STM32作为控制核心,主动发送请求(如读数据、写指令),接收从机响应。
  • 从站:传感器、执行器等设备,被动响应主站请求,返回数据或执行操作。
  • RS485收发器:如MAX485/SP3485,将STM32的TTL电平转换为RS485差分信号,通过DE/RE引脚控制发送/接收方向。

三、硬件设计

1. 核心组件选型

组件 型号/规格 功能说明
主控芯片 STM32F103C8T6 32位ARM Cortex-M3,72MHz
RS485收发器 MAX485/CSP3485 半双工RS485转换,3.3V/5V兼容
隔离模块 ADuM1201(可选) 电气隔离,抗干扰
终端电阻 120Ω 匹配阻抗,减少信号反射
电源 3.3V/5V LDO 为STM32和RS485芯片供电

2. 硬件连接

(1)STM32与RS485收发器(MAX485)

MAX485引脚 STM32引脚 功能说明
RO PA10 (UART1_RX) 接收数据输出(TTL电平)
DI PA9 (UART1_TX) 发送数据输入(TTL电平)
DE/RE PB0 (GPIO) 方向控制(高=发送,低=接收)
VCC 3.3V/5V 电源(与STM32共电源)
GND GND
A A RS485差分线A(接从机A)
B B RS485差分线B(接从机B)

(2)RS485总线连接

  • A/B线:所有从机的A线并联,B线并联,终端电阻(120Ω)接在总线两端(首末从机)。
  • 注意:A/B线不可反接,否则通信失败;长距离传输需加屏蔽层,减少干扰。

四、软件设计

1. 开发环境

  • IDE:STM32CubeIDE 1.13.0
  • :STM32CubeF1 HAL库
  • 协议:Modbus RTU(功能码03读保持寄存器、06写单个寄存器、16写多个寄存器)

2. 核心原理

(1)Modbus RTU帧结构

[地址码(1B)][功能码(1B)][数据域(NB)][CRC校验(2B)]
  • 地址码:从机地址(1-247,0为广播地址)。
  • 功能码:操作类型(如03=读保持寄存器)。
  • 数据域:请求参数(如寄存器起始地址、数量)或响应数据。
  • CRC校验:16位循环冗余校验(低字节在前,高字节在后)。

(2)RS485方向控制

  • 发送模式:DE/RE=高电平,STM32通过UART发送数据,MAX485将TTL转为RS485差分信号。
  • 接收模式:DE/RE=低电平,MAX485将RS485差分信号转为TTL,STM32通过UART接收数据。

3. 核心代码实现

(1)UART与RS485初始化(rs485.c)

#include "rs485.h"
#include "usart.h"
#include "gpio.h"

UART_HandleTypeDef *rs485_huart = &huart1;
uint8_t rs485_rx_buf[256]; // 接收缓冲区
uint8_t rs485_tx_buf[256]; // 发送缓冲区

// 初始化RS485方向控制引脚
void RS485_Init(void) {
   
    GPIO_InitTypeDef GPIO_InitStruct = {
   0};
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 默认接收模式
}

// UART初始化(波特率9600,8N1)
void UART_Init(void) {
   
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 9600;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    HAL_UART_Init(&huart1);
    HAL_UART_Receive_IT(rs485_huart, rs485_rx_buf, 1); // 启动接收中断
}

(2)CRC16校验计算(modbus_crc.c)

Modbus RTU采用CRC16-IBM校验算法(多项式0xA001):

uint16_t Modbus_CRC16(uint8_t *data, uint8_t len) {
   
    uint16_t crc = 0xFFFF;
    for (uint8_t i = 0; i < len; i++) {
   
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
   
            if (crc & 0x0001) {
   
                crc >>= 1;
                crc ^= 0xA001;
            } else {
   
                crc >>= 1;
            }
        }
    }
    return crc; // 低字节在前,高字节在后
}

(3)主站功能实现(master.c)

主站主动向从机发送请求,读取/写入数据:

// 发送Modbus RTU请求
void Modbus_Master_Send(uint8_t slave_addr, uint8_t func_code, uint16_t reg_addr, uint16_t reg_num, uint16_t *write_data) {
   
    uint8_t tx_len = 0;
    rs485_tx_buf[tx_len++] = slave_addr; // 地址码
    rs485_tx_buf[tx_len++] = func_code; // 功能码

    switch (func_code) {
   
        case 0x03: // 读保持寄存器
            rs485_tx_buf[tx_len++] = reg_addr >> 8; // 寄存器地址高字节
            rs485_tx_buf[tx_len++] = reg_addr & 0xFF; // 低字节
            rs485_tx_buf[tx_len++] = reg_num >> 8; // 数量高字节
            rs485_tx_buf[tx_len++] = reg_num & 0xFF; // 低字节
            break;
        case 0x06: // 写单个寄存器
            rs485_tx_buf[tx_len++] = reg_addr >> 8;
            rs485_tx_buf[tx_len++] = reg_addr & 0xFF;
            rs485_tx_buf[tx_len++] = write_data[0] >> 8;
            rs485_tx_buf[tx_len++] = write_data[0] & 0xFF;
            break;
        default: break;
    }

    // 计算CRC并添加到帧尾
    uint16_t crc = Modbus_CRC16(rs485_tx_buf, tx_len);
    rs485_tx_buf[tx_len++] = crc & 0xFF; // 低字节
    rs485_tx_buf[tx_len++] = crc >> 8; // 高字节

    // 发送数据(切换到发送模式)
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // DE=1(发送)
    HAL_UART_Transmit(rs485_huart, rs485_tx_buf, tx_len, 100);
    HAL_Delay(1); // 等待发送完成
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // DE=0(接收)
}

// 接收从机响应并解析(以读保持寄存器为例)
uint8_t Modbus_Master_Read(uint8_t slave_addr, uint16_t *reg_data) {
   
    uint8_t rx_len = 0;
    HAL_UART_Receive(rs485_huart, rs485_rx_buf, 256, 100); // 阻塞接收

    // 校验帧头(地址码+功能码)
    if (rs485_rx_buf[0] != slave_addr || rs485_rx_buf[1] != 0x03) {
   
        return 0; // 帧错误
    }

    // 校验CRC
    uint16_t crc_received = (rs485_rx_buf[rx_len-1] << 8) | rs485_rx_buf[rx_len-2];
    uint16_t crc_calculated = Modbus_CRC16(rs485_rx_buf, rx_len-2);
    if (crc_received != crc_calculated) {
   
        return 0; // CRC错误
    }

    // 解析数据(字节数=寄存器数量×2)
    uint8_t byte_count = rs485_rx_buf[2];
    for (uint8_t i = 0; i < byte_count/2; i++) {
   
        reg_data[i] = (rs485_rx_buf[3+2*i] << 8) | rs485_rx_buf[4+2*i];
    }
    return byte_count/2; // 返回寄存器数量
}

(4)从站功能实现(slave.c)

从站被动响应主站请求,处理读/写操作:

// 保持寄存器映射(示例:10个寄存器,地址0x0000-0x0009)
uint16_t holding_regs[10] = {
   0};

// 处理Modbus请求(从机模式)
void Modbus_Slave_Process(uint8_t *rx_buf, uint8_t rx_len) {
   
    uint8_t slave_addr = rx_buf[0];
    uint8_t func_code = rx_buf[1];
    uint16_t crc_received = (rx_buf[rx_len-1] << 8) | rx_buf[rx_len-2];
    uint16_t crc_calculated = Modbus_CRC16(rx_buf, rx_len-2);

    if (crc_received != crc_calculated) return; // CRC错误

    switch (func_code) {
   
        case 0x03: {
    // 读保持寄存器
            uint16_t reg_addr = (rx_buf[2] << 8) | rx_buf[3];
            uint16_t reg_num = (rx_buf[4] << 8) | rx_buf[5];
            uint8_t tx_len = 0;
            rs485_tx_buf[tx_len++] = slave_addr;
            rs485_tx_buf[tx_len++] = func_code;
            rs485_tx_buf[tx_len++] = reg_num * 2; // 字节数=寄存器数×2
            for (uint8_t i = 0; i < reg_num; i++) {
   
                rs485_tx_buf[tx_len++] = holding_regs[reg_addr+i] >> 8;
                rs485_tx_buf[tx_len++] = holding_regs[reg_addr+i] & 0xFF;
            }
            // 添加CRC
            uint16_t crc = Modbus_CRC16(rs485_tx_buf, tx_len);
            rs485_tx_buf[tx_len++] = crc & 0xFF;
            rs485_tx_buf[tx_len++] = crc >> 8;
            // 发送响应
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
            HAL_UART_Transmit(rs485_huart, rs485_tx_buf, tx_len, 100);
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
            break;
        }
        case 0x06: {
    // 写单个寄存器
            uint16_t reg_addr = (rx_buf[2] << 8) | rx_buf[3];
            uint16_t reg_val = (rx_buf[4] << 8) | rx_buf[5];
            holding_regs[reg_addr] = reg_val; // 更新寄存器
            // 响应=请求(原样返回)
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
            HAL_UART_Transmit(rs485_huart, rx_buf, rx_len, 100);
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
            break;
        }
        default: {
    // 不支持的功能码
            rs485_tx_buf[0] = slave_addr;
            rs485_tx_buf[1] = func_code | 0x80; // 错误标志
            rs485_tx_buf[2] = 0x01; // 错误码:非法功能
            uint8_t tx_len = 3;
            uint16_t crc = Modbus_CRC16(rs485_tx_buf, tx_len);
            rs485_tx_buf[tx_len++] = crc & 0xFF;
            rs485_tx_buf[tx_len++] = crc >> 8;
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
            HAL_UART_Transmit(rs485_huart, rs485_tx_buf, tx_len, 100);
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
            break;
        }
    }
}

(5)主程序(main.c)

#include "main.h"
#include "rs485.h"
#include "modbus_crc.h"
#include "master.h"
#include "slave.h"

int main(void) {
   
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    RS485_Init();
    UART_Init();

    // 主站模式示例:读取从机1的保持寄存器0x0000-0x0001
    uint16_t reg_data[2];
    Modbus_Master_Send(0x01, 0x03, 0x0000, 0x0002, NULL); // 发送读请求
    if (Modbus_Master_Read(0x01, reg_data) == 2) {
    // 接收并解析
        // 处理数据(如显示、控制)
    }

    // 从站模式示例:循环处理请求
    while (1) {
   
        uint8_t rx_len = 0;
        HAL_UART_Receive(&huart1, rs485_rx_buf, 1, 100); // 接收1字节
        if (rs485_rx_buf[0] == 0x01) {
    // 从机地址0x01
            // 继续接收完整帧(根据长度)
            // 调用Modbus_Slave_Process处理
        }
    }
}

参考代码 stm32单片机RS485 Modbus通讯协议程序原代码 www.youwenfan.com/contentali/183191.html

五、关键配置与优化

1. 参数配置

  • 波特率:常用9600、19200、38400(需主从一致),STM32 UART时钟需支持(如72MHz主频,分频后满足波特率)。
  • 数据格式:8位数据位、1位停止位、无校验(8N1)或偶校验(8E1)。
  • 从机地址:每个从机分配唯一地址(1-247),主站通过地址寻址。

2. 抗干扰优化

  • 终端电阻:总线两端接120Ω电阻,匹配阻抗,减少信号反射。
  • 电气隔离:使用ADuM1201等隔离模块,隔离STM32与RS485总线,避免地环路干扰。
  • 软件滤波:接收数据时进行CRC校验,丢弃错误帧;添加超时重传机制(主站)。

六、测试与验证

1. 测试工具

  • 主站测试:使用Modbus Poll(PC软件)作为主站,读取STM32从机数据。
  • 从站测试:使用Modbus Slave(PC软件)作为从机,STM32主站读取其数据。
  • 硬件测试:用示波器观察A/B线差分信号,确保波形正常(无畸变、无干扰)。

2. 测试用例

测试项 主站操作 从站预期行为 结果
读保持寄存器 发送03功能码,读0x0000-0x0001 返回2个寄存器值,CRC正确
写单个寄存器 发送06功能码,写0x0000=0x1234 从机寄存器0x0000更新为0x1234
错误功能码 发送0x05(非法功能) 从机返回错误响应(功能码0x85)
广播写 发送06功能码,地址0x00 所有从机执行写操作

七、项目资源

  • 开发环境:STM32CubeIDE 1.13.0,HAL库
  • 参考文档
    • 《Modbus Application Protocol Specification V1.1b3》
    • 《MAX485 Datasheet》
    • STM32F103xx Reference Manual
  • 测试工具:Modbus Poll/Slave,示波器,逻辑分析仪

八、总结

本方案基于STM32实现了RS485 Modbus RTU主从通信,核心包括UART配置RS485方向控制Modbus帧解析CRC校验。通过合理设计硬件(如隔离、终端电阻)和优化软件(如超时重传、数据校验),可确保工业环境下的可靠通信。

目录
相关文章
|
18天前
|
并行计算 算法 Serverless
基于MATLAB的语音信号时域特征提取实现
基于MATLAB的语音信号时域特征提取实现
59 1
|
18天前
|
人工智能 运维 安全
本地 AI 终端普及背景下网络安全威胁演化与防御策略研究
本文以2026台北电脑展AI PC产业变革为背景,系统分析本地AI终端带来的新型安全风险,涵盖AI辅助攻击、身份凭证窃取、钓鱼即服务、协议层DoS、容器逃逸等威胁,并提出覆盖权限管控、身份验证、协议加固、人员管理的分层防御体系。(239字)
90 3
|
JavaScript 小程序 前端开发
【手把手教教学物联网项目】01 视频大纲
《手把手教教学物联网项目》是一系列视频教程,旨在引导初学者掌握物联网技术。视频涵盖物联网基础,如物联网概述、架构和技术;STM32微控制器的介绍、编程及外设使用;网关开发,涉及ESP8266和ESP32;物联网通信协议如TCP、MQTT、Modbus等;物联网总线协议如单总线、CAN、IIC和SPI;OLED显示原理与驱动;MQTT服务器搭建;物联网云平台介绍,包括阿里云平台的使用;微信小程序开发入门及前端VUE项目实践。此外,教程还涉及UniAPP和SpringBoot后台开发,最后通过“智能取餐柜”项目将理论知识付诸实践。视频可在B站找到,适合学生、爱好者和开发人员学习物联网技术。
1328 12
【手把手教教学物联网项目】01 视频大纲
|
人工智能 自然语言处理 测试技术
什么是通义灵码?
什么是通义灵码?
2743 0
|
18天前
|
人工智能 运维 JavaScript
阿里云轻量应用服务器部署 OpenClaw 搭配Token Plan完整配置流程
OpenClaw是一款基于Node.js开发的开源AI智能体,具备多轮上下文对话、插件拓展、自动化任务执行、会话记忆等功能,适配个人日常交互、轻量办公辅助、小型自动化场景。阿里云轻量应用服务器具备部署简单、运维门槛低、网络配置便捷的特点,相比传统ECS实例,更适合新手以及非专业运维人员用来托管轻量化应用,也是运行OpenClaw的常用载体。
126 0
|
18天前
|
弹性计算 人工智能 运维
阿里云ECS云服务器部署 OpenClaw 并配置 Token Plan 完整实操流程
OpenClaw是一款功能完善的开源AI智能体,支持多轮对话、插件拓展、自动化任务执行与上下文记忆,在个人办公、日常交互、轻量自动化场景中应用广泛。该应用基于Node.js开发,运行稳定且拓展性强,多数使用者会选择阿里云ECS云服务器作为运行载体,依托云服务器持续在线、网络稳定、资源可控的特性,实现7×24小时不间断服务。
74 0
|
18天前
阿里云ICP备案服务码是什么?如何生成备案服务码?
阿里云ICP备案服务码是用于备案的验证码凭证,可替代云产品直接完成备案。免费版随符合条件的云服务器等自动生成;付费版100元/个,需绑定有效云产品后使用,有效期一年。阿里云备案官方系统:https://t.aliyun.com/U/YETxGF
|
2月前
|
开发框架 并行计算 安全
【全网最详细】.NET Framework 4.0下载安装图文教程 | .NET4.0环境搭建使用指南
.NET Framework 4.0是微软2010年发布的关键版本,引入并行计算、动态语言运行时等新特性,为.NET现代化奠定基础。虽已停止主流支持,但仍是维护老项目的重要工具。建议新项目选用.NET 4.8或.NET 8。
|
5月前
|
自然语言处理 前端开发 Windows
推荐一款很好用的VSCode变量翻译插件
本文介绍VSCode插件“var-translate-en”,可一键将中文翻译为英文并转为小驼峰等命名格式,支持百度、腾讯、阿里等翻译服务。通过简单配置与快捷键设置,提升变量命名效率,解决命名难题。
2131 0
|
Oracle 关系型数据库 Linux
oracle学习82-VMware出现配置文件 .vmx 是由VMware产品创建,但该产品与此版 VMware workstation 不兼容,因此无法使用(VMware版本不兼容问题)
oracle学习82-VMware出现配置文件 .vmx 是由VMware产品创建,但该产品与此版 VMware workstation 不兼容,因此无法使用(VMware版本不兼容问题)
1630 0
oracle学习82-VMware出现配置文件 .vmx 是由VMware产品创建,但该产品与此版 VMware workstation 不兼容,因此无法使用(VMware版本不兼容问题)