前言

Modbus RTU作为工业自动化领域的“通用语言”,是嵌入式工程师必须掌握的通信协议之一。本文基于STM32F103C8T6开发板,从零实现一套完整的Modbus RTU从站协议栈,包含核心协议解析、CRC16校验、寄存器读写等关键功能,代码可直接移植到实际项目中。

一、项目概述

1.1 核心目标

基于STM32F103C8T6 + RS485模块,实现Modbus RTU从站功能,支持工业场景中最常用的03(读取保持寄存器)06(写入单个寄存器) 功能码,可与Modbus Poll等主站软件完成数据交互。

1.2 硬件需求

硬件名称 说明
STM32F103C8T6开发板 核心控制单元
RS485通信模块 工业级串行通信(需控制DE/RE引脚)
串口转USB模块 调试与数据监控
杜邦线 硬件连接

1.3 项目结构

modbus-stm32/
├── Inc/                  # 头文件目录
│   └── modbus.h          # Modbus协议头文件
├── Src/                  # 源文件目录
│   ├── modbus.c          # Modbus协议核心实现
│   ├── main.c            # 主程序(初始化+中断处理)
│   └── stm32f1xx_it.c    # 串口初始化
├── STM32F1xx/            # STM32底层驱动
│   ├── Core/
│   └── Drivers/
└── Makefile              # 编译脚本(或使用STM32CubeIDE)

二、核心代码实现

2.1 头文件定义(modbus.h)

该文件定义了Modbus协议的核心常量、寄存器地址、函数声明,是整个协议栈的“骨架”:

#ifndef __MODBUS_H
#define __MODBUS_H

#include "stm32f1xx_hal.h"

/* Modbus功能码 */
#define MODBUS_FUNC_READ_HOLD_REG      0x03  // 读取保持寄存器
#define MODBUS_FUNC_WRITE_SINGLE_REG   0x06  // 写入单个寄存器

/* Modbus错误码 */
#define MODBUS_EXCEPTION_ILLEGAL_FUNCTION  0x01  // 非法功能码
#define MODBUS_EXCEPTION_ILLEGAL_DATA_ADDR 0x02  // 非法寄存器地址

/* CRC16校验常量 */
#define CRC16_INIT 0xFFFF
#define CRC16_POLY 0xA001

/* 寄存器地址映射(模拟工业参数) */
#define REG_TEMP        0x0000  // 温度寄存器(2500 = 25.00℃)
#define REG_HUMIDITY    0x0001  // 湿度寄存器(6000 = 60.00%)
#define REG_PRESSURE    0x0002  // 压力寄存器(1013 hPa)
#define REG_STATUS      0x0003  // 状态寄存器(0x0001=运行中)

/* 从站地址与串口配置 */
#define MODBUS_SLAVE_ADDR 0x01
#define MODBUS_USART USART1

/* 全局缓冲区(中断接收/发送) */
extern uint8_t modbus_rx_buffer[256];
extern uint16_t modbus_rx_len;
extern uint8_t modbus_tx_buffer[256];
extern uint16_t modbus_tx_len;

/* 核心函数声明 */
void Modbus_Init(void);                // 寄存器初始化
void Modbus_Process(void);             // 协议帧解析
uint16_t Modbus_CRC16(uint8_t *data, uint16_t len); // CRC16校验
void Modbus_SendResponse(uint8_t *frame, uint16_t len); // 发送响应
uint16_t Modbus_ReadRegister(uint16_t reg_addr);   // 读寄存器
void Modbus_WriteRegister(uint16_t reg_addr, uint16_t value); // 写寄存器

#endif /* __MODBUS_H */

2.2 Modbus协议核心实现(modbus.c)

(1)基础初始化

初始化模拟寄存器的默认值,模拟工业现场的传感器初始数据:

#include "modbus.h"
#include <string.h>

/* 模拟寄存器存储(实际项目可替换为真实传感器数据) */
static uint16_t registers[100] = {0};
uint8_t modbus_rx_buffer[256] = {0};
uint16_t modbus_rx_len = 0;
uint8_t modbus_tx_buffer[256] = {0};
uint16_t modbus_tx_len = 0;

void Modbus_Init(void) {
    // 初始化默认参数
    registers[REG_TEMP] = 2500;    // 25.00°C
    registers[REG_HUMIDITY] = 6000; // 60.00%
    registers[REG_PRESSURE] = 1013; // 1013 hPa
    registers[REG_STATUS] = 0x0001; // 运行状态
}
(2)CRC16校验(Modbus必备)

Modbus RTU协议的核心校验算法,确保数据传输的完整性:

uint16_t Modbus_CRC16(uint8_t *data, uint16_t len) {
    uint16_t crc = CRC16_INIT;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc >>= 1;
                crc ^= CRC16_POLY;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}
(3)协议帧解析(核心逻辑)

处理主站发来的请求帧,解析功能码并执行对应操作,返回响应帧:

void Modbus_Process(void) {
    if (modbus_rx_len < 3) return; // 帧长度不足,直接返回
    
    uint8_t slave_addr = modbus_rx_buffer[0];
    uint8_t func_code = modbus_rx_buffer[1];
    
    // 校验从站地址(只处理目标为本机的帧)
    if (slave_addr != MODBUS_SLAVE_ADDR) {
        modbus_rx_len = 0;
        return;
    }
    
    switch (func_code) {
        case MODBUS_FUNC_READ_HOLD_REG: {
            // 解析寄存器起始地址和数量
            uint16_t start_addr = (modbus_rx_buffer[2] << 8) | modbus_rx_buffer[3];
            uint16_t reg_count = (modbus_rx_buffer[4] << 8) | modbus_rx_buffer[5];
            
            // 校验寄存器地址范围
            if (start_addr >= 100 || (start_addr + reg_count) > 100) {
                // 返回非法地址异常
                modbus_tx_buffer[0] = MODBUS_SLAVE_ADDR;
                modbus_tx_buffer[1] = func_code | 0x80; // 异常标识(最高位置1)
                modbus_tx_buffer[2] = MODBUS_EXCEPTION_ILLEGAL_DATA_ADDR;
                modbus_tx_len = 3;
                Modbus_SendResponse(modbus_tx_buffer, modbus_tx_len);
                break;
            }
            
            // 构建响应帧
            modbus_tx_buffer[0] = MODBUS_SLAVE_ADDR;
            modbus_tx_buffer[1] = func_code;
            modbus_tx_buffer[2] = reg_count * 2; // 返回字节数(每个寄存器2字节)
            
            for (uint16_t i = 0; i < reg_count; i++) {
                uint16_t value = Modbus_ReadRegister(start_addr + i);
                modbus_tx_buffer[3 + i * 2] = (value >> 8) & 0xFF; // 高字节
                modbus_tx_buffer[4 + i * 2] = value & 0xFF;        // 低字节
            }
            
            modbus_tx_len = 3 + reg_count * 2;
            Modbus_SendResponse(modbus_tx_buffer, modbus_tx_len);
            break;
        }
        
        case MODBUS_FUNC_WRITE_SINGLE_REG: {
            // 解析目标寄存器地址和写入值
            uint16_t reg_addr = (modbus_rx_buffer[2] << 8) | modbus_rx_buffer[3];
            uint16_t value = (modbus_rx_buffer[4] << 8) | modbus_rx_buffer[5];
            
            // 校验地址范围
            if (reg_addr >= 100) {
                modbus_tx_buffer[0] = MODBUS_SLAVE_ADDR;
                modbus_tx_buffer[1] = func_code | 0x80;
                modbus_tx_buffer[2] = MODBUS_EXCEPTION_ILLEGAL_DATA_ADDR;
                modbus_tx_len = 3;
                Modbus_SendResponse(modbus_tx_buffer, modbus_tx_len);
                break;
            }
            
            // 写入寄存器
            Modbus_WriteRegister(reg_addr, value);
            
            // 返回确认帧(原样返回地址和值)
            modbus_tx_buffer[0] = MODBUS_SLAVE_ADDR;
            modbus_tx_buffer[1] = func_code;
            modbus_tx_buffer[2] = (reg_addr >> 8) & 0xFF;
            modbus_tx_buffer[3] = reg_addr & 0xFF;
            modbus_tx_buffer[4] = (value >> 8) & 0xFF;
            modbus_tx_buffer[5] = value & 0xFF;
            modbus_tx_len = 6;
            Modbus_SendResponse(modbus_tx_buffer, modbus_tx_len);
            break;
        }
        
        default:
            // 不支持的功能码,返回非法功能异常
            modbus_tx_buffer[0] = MODBUS_SLAVE_ADDR;
            modbus_tx_buffer[1] = func_code | 0x80;
            modbus_tx_buffer[2] = MODBUS_EXCEPTION_ILLEGAL_FUNCTION;
            modbus_tx_len = 3;
            Modbus_SendResponse(modbus_tx_buffer, modbus_tx_len);
            break;
    }
    
    modbus_rx_len = 0; // 清空接收缓冲区
}

2.3 主程序与中断处理(main.c)

通过串口中断接收数据,主循环处理协议帧,是STM32端的“入口”:

#include "main.h"
#include "modbus.h"
#include "usart.h"

uint8_t modbus_rx_buffer[256];
uint16_t modbus_rx_len = 0;

int main(void) {
    /* 初始化HAL库 */
    HAL_Init();
    /* 配置系统时钟(72MHz) */
    SystemClock_Config();
    /* 初始化USART1(9600 8N1) */
    MX_USART1_UART_Init();
    /* 初始化Modbus寄存器 */
    Modbus_Init();
    
    /* 使能串口接收中断(每次接收1字节) */
    HAL_UART_Receive_IT(&huart1, modbus_rx_buffer, 1);
    
    /* 主循环 */
    while (1) {
        /* 接收到完整帧后处理 */
        if (modbus_rx_len >= 3) {
            Modbus_Process();
        }
        HAL_Delay(10); // 降低CPU占用
    }
}

/* 串口接收中断回调函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        // 存储接收到的字节
        modbus_rx_buffer[modbus_rx_len++] = modbus_rx_buffer[0];
        // 缓冲区溢出保护
        if (modbus_rx_len >= 256) modbus_rx_len = 0;
        // 重新开启中断,等待下一字节
        HAL_UART_Receive_IT(&huart1, modbus_rx_buffer, 1);
    }
}

三、硬件连接与配置

3.1 关键连接(RS485模块)

STM32引脚 RS485模块引脚 说明
PA9(USART1_TX) DI 数据发送
PA10(USART1_RX) RO 数据接收
PA8(GPIO) DE/RE 收发方向控制(高电平发送,低电平接收)
GND GND 共地(必须连接,否则通信异常)

3.2 串口配置

  • 波特率:9600(工业常用,可根据需求修改)
  • 数据位:8
  • 停止位:1
  • 校验位:无
  • 流控:无

四、测试步骤与效果

4.1 测试工具

推荐使用Modbus Poll(Windows)或QModMaster(跨平台),配置如下:

  1. 选择串口模式(RTU),绑定USB转串口的COM口;
  2. 波特率9600、数据位8、停止位1、无校验;
  3. 从站地址:1(与代码中MODBUS_SLAVE_ADDR一致)。

4.2 测试操作

测试项 操作步骤 预期结果
读取温度 功能码03,起始地址0,寄存器数量1 返回2500(对应25.00℃)
读取湿度 功能码03,起始地址1,寄存器数量1 返回6000(对应60.00%)
写入温度 功能码06,地址0,写入值2400 寄存器0的值变为2400(24.00℃)

五、扩展与优化建议

  1. 功能码扩展:增加01(读线圈)、02(读离散输入)、16(写多个寄存器)支持;
  2. 硬件优化:DE/RE引脚通过代码自动控制(发送前置高,发送完成后置低);
  3. 数据可靠性:添加帧超时判断(Modbus RTU帧间隔>3.5字符时间视为帧结束);
  4. 多从站支持:通过配置从站地址,实现单总线多设备通信;
  5. 实际传感器对接:将模拟寄存器替换为DS18B20(温度)、DHT11(温湿度)等真实传感器数据;
  6. Modbus TCP扩展:结合ETH模块,实现Modbus TCP/IP协议。

六、核心知识点总结

  1. Modbus RTU帧结构:从站地址(1B) + 功能码(1B) + 数据(NB) + CRC16(2B);
  2. 中断驱动通信:STM32串口中断接收避免轮询占用CPU,提高实时性;
  3. CRC16校验:Modbus协议的“安全锁”,确保数据传输不被篡改;
  4. 寄存器映射:工业协议的核心思想,通过地址映射实现数据交互标准化。

结语

本文实现的Modbus RTU从站协议栈是工业嵌入式开发的基础框架,掌握该协议不仅能应对工业通信场景,也能理解串行通信、协议解析、数据校验等核心嵌入式开发思想。实际项目中,可根据需求扩展功能、优化性能,适配不同的工业传感器和控制器。

如果本文对你有帮助,欢迎点赞+收藏+关注,后续会更新更多STM32工业通信实战内容!

🎁欢迎关注,获取更多技术干货!

公众号:BackCatK Chen,文章末尾可以扫码关注

🎁资料包亮点

这份资料包涵盖了从硬件电路设计STM32单片机开发,再到Linux系统学习的全链路内容,适合不同阶段的学习者:

  • 硬件基础:包含硬件电路合集、硬件设计开发工具包,帮你打牢底层基础。
  • STM32专项:从环境搭建、开发工具、传感器模块到项目实战,还有书籍和芯片手册,一站式搞定STM32学习。
  • C语言进阶:C语言学习资料包,助你掌握嵌入式开发的核心语言。
  • 面试求职:嵌入式面试题合集,提前备战技术面试。
  • Linux拓展:Linux相关学习资料包,拓宽技术视野。
📂资料包目录
  • 00-STM32单片机环境搭建
  • 01-硬件电路合集
  • 02-硬件设计开发工具包
  • 03-C语言学习资料包
  • 04-STM32单片机开发工具包
  • 05-STM32传感器模块合集
  • 06-STM32项目合集
  • 07-STM32单片机书籍&芯片手册
  • 08-Linux相关学习资料包
    在这里插入图片描述
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐