基于 STM32 的 Modbus RTU 串口通讯程序

一、Modbus RTU 协议核心

1、帧格式

复制代码
[地址] [功能码] [数据] [CRC16] [3.5字符静默]

2、常用功能码

功能码 名称 操作
0x03 读保持寄存器 读取1-125个寄存器
0x06 写单个寄存器 写1个寄存器
0x10 写多个寄存器 写1-123个寄存器

二、STM32 硬件配置

1、串口配置

c 复制代码
// CubeMX 配置:
// USART1: 波特率9600/19200/38400, 8N1
// DMA: RX/TX 循环模式
// NVIC: 串口空闲中断

2、引脚定义

复制代码
PA9  → USART1_TX
PA10 → USART1_RX

三、Modbus 数据结构

c 复制代码
// modbus.h
#ifndef __MODBUS_H
#define __MODBUS_H

#include "stm32f1xx_hal.h"

// Modbus 寄存器定义
#define REG_INPUT_START     0x0000
#define REG_INPUT_NREGS     100
#define REG_HOLDING_START   0x1000
#define REG_HOLDING_NREGS   100

// 设备地址
#define MODBUS_SLAVE_ADDR   0x01

// 功能码
#define MODBUS_FC_READ_COILS          0x01
#define MODBUS_FC_READ_DISCRETE       0x02
#define MODBUS_FC_READ_HOLDING        0x03
#define MODBUS_FC_READ_INPUT          0x04
#define MODBUS_FC_WRITE_SINGLE        0x06
#define MODBUS_FC_WRITE_MULTIPLE      0x10

// 异常码
#define MODBUS_EX_ILLEGAL_FUNCTION    0x01
#define MODBUS_EX_ILLEGAL_ADDRESS     0x02
#define MODBUS_EX_ILLEGAL_VALUE       0x03
#define MODBUS_EX_SLAVE_FAILURE       0x04

// 缓冲区大小
#define MODBUS_RX_BUFFER_SIZE  256
#define MODBUS_TX_BUFFER_SIZE  256

// Modbus 从站结构体
typedef struct {
    // 寄存器数组
    uint16_t holding_regs[REG_HOLDING_NREGS];
    uint16_t input_regs[REG_INPUT_NREGS];
    
    // 缓冲区
    uint8_t rx_buf[MODBUS_RX_BUFFER_SIZE];
    uint8_t tx_buf[MODBUS_TX_BUFFER_SIZE];
    
    // 状态
    uint8_t slave_addr;
    uint16_t rx_length;
    uint16_t tx_length;
    uint8_t is_busy;
} Modbus_Slave_t;

// 函数声明
void Modbus_Init(Modbus_Slave_t *slave, uint8_t addr);
void Modbus_Process(Modbus_Slave_t *slave);
void Modbus_UART_Idle_Callback(UART_HandleTypeDef *huart);
uint16_t Modbus_CRC16(uint8_t *data, uint16_t length);

#endif

四、CRC16 校验(核心)

c 复制代码
// modbus_crc.c
#include "modbus.h"

// CRC16 表(查表法,速度快)
static const uint16_t crc16_table[256] = {
    0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
    0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
    // ... 完整表格共256个值
};

uint16_t Modbus_CRC16(uint8_t *data, uint16_t length)
{
    uint8_t tmp;
    uint16_t crc = 0xFFFF;
    
    while (length--) {
        tmp = *data++ ^ (uint8_t)crc;
        crc >>= 8;
        crc ^= crc16_table[tmp];
    }
    
    return crc;
}

五、Modbus 从站处理(核心)

c 复制代码
// modbus_slave.c
#include "modbus.h"

// 初始化
void Modbus_Init(Modbus_Slave_t *slave, uint8_t addr)
{
    slave->slave_addr = addr;
    slave->rx_length = 0;
    slave->tx_length = 0;
    slave->is_busy = 0;
    
    // 初始化寄存器
    for (int i = 0; i < REG_HOLDING_NREGS; i++) {
        slave->holding_regs[i] = 0;
    }
    for (int i = 0; i < REG_INPUT_NREGS; i++) {
        slave->input_regs[i] = 0;
    }
}

// 处理请求
void Modbus_Process(Modbus_Slave_t *slave)
{
    uint8_t *rx = slave->rx_buf;
    uint8_t *tx = slave->tx_buf;
    uint16_t crc, crc_recv;
    uint8_t exception = 0;
    
    // 1. 检查地址
    if (rx[0] != slave->slave_addr && rx[0] != 0xFF) {
        return;  // 地址不匹配
    }
    
    // 2. CRC 校验
    crc_recv = rx[slave->rx_length - 2] | (rx[slave->rx_length - 1] << 8);
    crc = Modbus_CRC16(rx, slave->rx_length - 2);
    if (crc != crc_recv) {
        return;  // CRC错误
    }
    
    // 3. 解析功能码
    uint8_t func = rx[1];
    uint16_t addr = (rx[2] << 8) | rx[3];
    uint16_t quantity = (rx[4] << 8) | rx[5];
    
    // 4. 根据功能码处理
    switch (func) {
        case MODBUS_FC_READ_HOLDING:  // 0x03
            if (quantity >= 1 && quantity <= 125) {
                tx[0] = rx[0];  // 地址
                tx[1] = func;   // 功能码
                tx[2] = quantity * 2;  // 字节数
                
                for (int i = 0; i < quantity; i++) {
                    if (addr + i < REG_HOLDING_NREGS) {
                        tx[3 + i*2] = slave->holding_regs[addr + i] >> 8;
                        tx[4 + i*2] = slave->holding_regs[addr + i] & 0xFF;
                    } else {
                        exception = MODBUS_EX_ILLEGAL_ADDRESS;
                        break;
                    }
                }
                
                if (exception == 0) {
                    slave->tx_length = 3 + quantity * 2;
                }
            } else {
                exception = MODBUS_EX_ILLEGAL_VALUE;
            }
            break;
            
        case MODBUS_FC_WRITE_SINGLE:  // 0x06
            if (addr < REG_HOLDING_NREGS) {
                uint16_t value = (rx[4] << 8) | rx[5];
                slave->holding_regs[addr] = value;
                
                // 响应(回显)
                memcpy(tx, rx, 6);
                slave->tx_length = 6;
            } else {
                exception = MODBUS_EX_ILLEGAL_ADDRESS;
            }
            break;
            
        case MODBUS_FC_WRITE_MULTIPLE:  // 0x10
        {
            uint8_t byte_count = rx[6];
            
            if (quantity >= 1 && quantity <= 123 &&
                byte_count == quantity * 2) {
                
                for (int i = 0; i < quantity; i++) {
                    if (addr + i < REG_HOLDING_NREGS) {
                        uint16_t value = (rx[7 + i*2] << 8) | rx[8 + i*2];
                        slave->holding_regs[addr + i] = value;
                    } else {
                        exception = MODBUS_EX_ILLEGAL_ADDRESS;
                        break;
                    }
                }
                
                if (exception == 0) {
                    // 响应:地址 + 寄存器数量
                    memcpy(tx, rx, 6);
                    slave->tx_length = 6;
                }
            } else {
                exception = MODBUS_EX_ILLEGAL_VALUE;
            }
            break;
        }
            
        default:
            exception = MODBUS_EX_ILLEGAL_FUNCTION;
            break;
    }
    
    // 5. 异常响应
    if (exception != 0) {
        tx[0] = rx[0];
        tx[1] = func | 0x80;  // 功能码最高位置1
        tx[2] = exception;
        slave->tx_length = 3;
    }
    
    // 6. 添加CRC
    if (slave->tx_length > 0) {
        crc = Modbus_CRC16(tx, slave->tx_length);
        tx[slave->tx_length] = crc & 0xFF;
        tx[slave->tx_length + 1] = crc >> 8;
        slave->tx_length += 2;
    }
}

六、串口空闲中断处理

c 复制代码
// stm32f1xx_it.c
#include "modbus.h"

extern Modbus_Slave_t mb_slave;
extern UART_HandleTypeDef huart1;

// 空闲中断回调
void Modbus_UART_Idle_Callback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        uint32_t temp;
        
        // 清除空闲中断标志
        __HAL_UART_CLEAR_IDLEFLAG(huart);
        
        // 获取接收数据长度
        temp = huart->RxXferSize - __HAL_DMA_GET_COUNTER(huart->hdmarx);
        mb_slave.rx_length = temp;
        
        // 停止DMA,防止数据被覆盖
        HAL_UART_DMAStop(huart);
        
        // 处理Modbus数据
        if (mb_slave.rx_length >= 4) {  // 最小帧长度
            Modbus_Process(&mb_slave);
            
            // 发送响应
            if (mb_slave.tx_length > 0) {
                // 等待3.5个字符时间 (Modbus要求)
                uint32_t delay = 3500000 / huart->Init.BaudRate;
                HAL_Delay(delay);
                
                // 发送响应
                HAL_UART_Transmit_DMA(huart, mb_slave.tx_buf, mb_slave.tx_length);
            }
        }
        
        // 重新启动DMA接收
        HAL_UART_Receive_DMA(huart, mb_slave.rx_buf, MODBUS_RX_BUFFER_SIZE);
    }
}

七、Modbus 主站(主机)实现

c 复制代码
// modbus_master.c
#include "modbus.h"

typedef struct {
    uint8_t tx_buf[MODBUS_TX_BUFFER_SIZE];
    uint8_t rx_buf[MODBUS_RX_BUFFER_SIZE];
    uint16_t rx_length;
    uint8_t slave_addr;
    uint32_t timeout;
} Modbus_Master_t;

// 读取保持寄存器
uint8_t Modbus_Read_Holding(Modbus_Master_t *master, uint8_t slave_addr,
                           uint16_t reg_addr, uint16_t reg_num,
                           uint16_t *data, uint32_t timeout)
{
    // 构造请求
    master->tx_buf[0] = slave_addr;        // 地址
    master->tx_buf[1] = 0x03;              // 功能码
    master->tx_buf[2] = reg_addr >> 8;     // 起始地址高字节
    master->tx_buf[3] = reg_addr & 0xFF;   // 起始地址低字节
    master->tx_buf[4] = reg_num >> 8;      // 寄存器数量高字节
    master->tx_buf[5] = reg_num & 0xFF;    // 寄存器数量低字节
    
    // CRC
    uint16_t crc = Modbus_CRC16(master->tx_buf, 6);
    master->tx_buf[6] = crc & 0xFF;
    master->tx_buf[7] = crc >> 8;
    
    // 发送请求
    HAL_UART_Transmit_DMA(&huart1, master->tx_buf, 8);
    
    // 等待响应
    uint32_t start = HAL_GetTick();
    while (HAL_GetTick() - start < timeout) {
        if (master->rx_length >= 5 + reg_num * 2) {
            // 校验响应
            if (master->rx_buf[0] == slave_addr &&
                master->rx_buf[1] == 0x03) {
                
                // 提取数据
                for (int i = 0; i < reg_num; i++) {
                    data[i] = (master->rx_buf[3 + i*2] << 8) |
                              master->rx_buf[4 + i*2];
                }
                return 1;  // 成功
            }
        }
    }
    
    return 0;  // 超时
}

八、主函数示例

c 复制代码
// main.c
#include "main.h"
#include "modbus.h"

Modbus_Slave_t mb_slave;
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
DMA_HandleTypeDef hdma_usart1_tx;

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    
    // 初始化外设
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_USART1_UART_Init();
    
    // 初始化Modbus从站
    Modbus_Init(&mb_slave, 0x01);
    
    // 启动DMA接收
    HAL_UART_Receive_DMA(&huart1, mb_slave.rx_buf, MODBUS_RX_BUFFER_SIZE);
    
    // 启用串口空闲中断
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
    
    while (1) {
        // 主循环(空闲中断处理Modbus)
        // 可以添加LED闪烁等指示
        HAL_Delay(1000);
    }
}

九、CubeMX 关键配置

1、USART 配置

c 复制代码
// 波特率: 9600/19200/38400
// Word Length: 8 bits
// Parity: None
// Stop Bits: 1
// Over Sampling: 16 Samples

2、DMA 配置

复制代码
USART1_RX:
  Mode: Circular
  Increment Address: Enable
  Data Width: Byte

USART1_TX:
  Mode: Normal
  Increment Address: Enable
  Data Width: Byte

3、NVIC 配置

复制代码
USART1_IRQn: Enable
DMA1_Channel4_5_IRQn: Enable

参考代码 Modbus串口通讯程序 www.youwenfan.com/contentcsu/69985.html

十、测试与调试

1、Modbus Poll 测试

复制代码
请求帧: 01 03 00 00 00 02 C4 0B
  - 地址: 0x01
  - 功能码: 0x03
  - 起始地址: 0x0000
  - 寄存器数量: 0x0002
  
响应帧: 01 03 04 00 00 00 00 FA 33
  - 字节数: 0x04
  - 数据: 0x0000 0x0000

2、调试函数

c 复制代码
void Modbus_Debug_Print(uint8_t *data, uint16_t len)
{
    printf("Modbus Frame (%d bytes): ", len);
    for (int i = 0; i < len; i++) {
        printf("%02X ", data[i]);
    }
    printf("\r\n");
}
相关推荐
fie88891 小时前
基于 STC15F104E 的 T12 白光烙铁控制器方案
stm32·单片机
yuan199971 小时前
基于 STM32 的工程级扫地机器人方案
stm32·嵌入式硬件·机器人
绿竹-大地2 小时前
韦东山开发板imx6ull移植WS1-CBS-Kit
嵌入式硬件·wifi
qq_411262422 小时前
wifi自适应
stm32·单片机·嵌入式硬件
洋九八3 小时前
STM32 (NVIC)中断
stm32·单片机·嵌入式硬件
12.=0.3 小时前
【stm32_9.2】FreeRTOS的任务管理:任务策略,调度器启用,任务创建、删除、挂起、恢复
c语言·stm32·单片机·嵌入式硬件
国产电子元器件3 小时前
ACS712国产替代推荐:电流检测芯片选型指南
单片机·嵌入式硬件·物联网
徐怀江4 小时前
ModusToolbox for vscode使用小记
ide·vscode·单片机·mcu·infineon
洋九八5 小时前
STM32 串口(USART)配置
stm32·单片机·嵌入式硬件