STM32 + MODBUS RTU + RS485 实现方案

一、系统架构与硬件连接

1.1 硬件连接表

STM32F103C8T6 RS485模块 说明
PA9 (TX1) DI 发送数据
PA10 (RX1) RO 接收数据
PA8 DE/RE 收发控制(高电平发送,低电平接收)
3.3V VCC 电源
GND GND 共地

1.2 MODBUS RTU协议帧格式

复制代码
[从机地址][功能码][数据起始地址][数据数量/数据值][CRC16校验]
  • 地址:1字节(1-247)
  • 功能码:1字节(03=读保持寄存器,06=写单个寄存器,16=写多个寄存器)
  • 数据:N字节
  • CRC16:2字节(低位在前)

二、代码实现

2.1 头文件(modbus_rtu.h)

c 复制代码
#ifndef __MODBUS_RTU_H
#define __MODBUS_RTU_H

#include "stm32f10x.h"

// MODBUS配置
#define MODBUS_SLAVE_ADDR   0x01    // 从机地址
#define MODBUS_BAUDRATE    9600     // 波特率
#define MODBUS_REG_NUM      10      // 寄存器数量

// 功能码定义
#define FUNC_READ_REG      0x03    // 读保持寄存器
#define FUNC_WRITE_SINGLE  0x06    // 写单个寄存器
#define FUNC_WRITE_MULTI  0x10    // 写多个寄存器

// 错误码定义
#define ERR_NONE           0x00     // 无错误
#define ERR_FUNC_CODE     0x01     // 非法功能码
#define ERR_REG_ADDR      0x02     // 非法数据地址
#define ERR_REG_VALUE     0x03     // 非法数据值

// 全局变量
extern uint16_t modbus_regs[MODBUS_REG_NUM];  // MODBUS寄存器数组
extern uint8_t modbus_rx_buf[256];            // 接收缓冲区
extern uint8_t modbus_tx_buf[256];            // 发送缓冲区
extern uint8_t modbus_rx_len;                 // 接收长度
extern uint8_t modbus_frame_flag;              // 帧接收完成标志

// 函数声明
void MODBUS_Init(void);
void MODBUS_ProcessFrame(void);
void MODBUS_SendResponse(uint8_t *data, uint8_t len);
uint16_t CRC16_Calculate(uint8_t *data, uint16_t len);
void MODBUS_UpdateDisplay(void);

#endif /* __MODBUS_RTU_H */

2.2 主程序(main.c)

c 复制代码
#include "stm32f10x.h"
#include "modbus_rtu.h"
#include "usart.h"
#include "delay.h"
#include "lcd.h"

// MODBUS寄存器数据
uint16_t modbus_regs[MODBUS_REG_NUM] = {
    0x1234, 0x5678, 0x9ABC, 0xDEF0, 0x1111,
    0x2222, 0x3333, 0x4444, 0x5555, 0x6666
};

// 通信缓冲区
uint8_t modbus_rx_buf[256];
uint8_t modbus_tx_buf[256];
uint8_t modbus_rx_len = 0;
uint8_t modbus_frame_flag = 0;

// 系统状态显示
typedef struct {
    uint32_t rx_count;       // 接收帧计数
    uint32_t tx_count;       // 发送帧计数
    uint16_t last_reg_addr;   // 最后操作的寄存器地址
    uint16_t last_reg_value;  // 最后操作的寄存器值
    uint8_t last_func_code;   // 最后功能码
} SystemStatus;

SystemStatus sys_status = {0};

int main(void)
{
    // 系统初始化
    SystemInit();
    Delay_Init();
    USART1_Init(9600);  // MODBUS通信串口
    USART2_Init(115200); // 调试输出串口
    LCD_Init();          // LCD显示屏
    
    printf("STM32 MODBUS RTU Slave Starting...\r\n");
    LCD_ShowString(0, 0, "MODBUS RTU Slave");
    LCD_ShowString(0, 20, "Addr: 0x01 Baud: 9600");
    
    // MODBUS初始化
    MODBUS_Init();
    
    uint32_t last_display_update = 0;
    
    while(1)
    {
        // 处理MODBUS帧
        if(modbus_frame_flag)
        {
            MODBUS_ProcessFrame();
            modbus_frame_flag = 0;
            modbus_rx_len = 0;
        }
        
        // 每秒更新显示
        if(millis() - last_display_update > 1000)
        {
            MODBUS_UpdateDisplay();
            last_display_update = millis();
        }
        
        Delay_ms(10);
    }
}

2.3 MODBUS核心实现(modbus_rtu.c)

c 复制代码
#include "modbus_rtu.h"
#include "string.h"

// GPIO控制RS485收发
#define RS485_TX_EN()   GPIO_SetBits(GPIOA, GPIO_Pin_8)   // PA8高电平,发送模式
#define RS485_RX_EN()   GPIO_ResetBits(GPIOA, GPIO_Pin_8) // PA8低电平,接收模式

/**
  * @brief  MODBUS初始化
  */
void MODBUS_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    
    // 1. 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
    
    // 2. 配置RS485收发控制引脚(PA8)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    RS485_RX_EN();  // 默认接收模式
    
    // 3. 配置USART1引脚(PA9-TX, PA10-RX)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    // 4. USART参数配置
    USART_InitStructure.USART_BaudRate = MODBUS_BAUDRATE;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_Init(USART1, &USART_InitStructure);
    
    // 5. 使能接收中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
    
    // 6. NVIC配置
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    // 7. 使能USART
    USART_Cmd(USART1, ENABLE);
    
    printf("MODBUS RTU Initialized: Addr=0x%02X, Baud=%d\r\n", 
           MODBUS_SLAVE_ADDR, MODBUS_BAUDRATE);
}

/**
  * @brief  CRC16计算
  * @param  data: 数据指针
  * @param  len: 数据长度
  * @retval CRC16校验值
  */
uint16_t CRC16_Calculate(uint8_t *data, uint16_t len)
{
    uint16_t crc = 0xFFFF;
    uint16_t i, j;
    
    for(i = 0; i < len; i++)
    {
        crc ^= data[i];
        for(j = 0; j < 8; j++)
        {
            if(crc & 0x0001)
            {
                crc >>= 1;
                crc ^= 0xA001;  // MODBUS CRC多项式
            }
            else
            {
                crc >>= 1;
            }
        }
    }
    return crc;
}

/**
  * @brief  发送MODBUS响应
  * @param  data: 发送数据指针
  * @param  len: 数据长度
  */
void MODBUS_SendResponse(uint8_t *data, uint8_t len)
{
    uint16_t crc;
    
    // 切换到发送模式
    RS485_TX_EN();
    Delay_us(10);
    
    // 发送数据
    for(uint8_t i = 0; i < len; i++)
    {
        USART_SendData(USART1, data[i]);
        while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    }
    
    // 等待发送完成
    while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
    
    // 切换回接收模式
    Delay_us(10);
    RS485_RX_EN();
    
    sys_status.tx_count++;
}

/**
  * @brief  处理MODBUS帧
  */
void MODBUS_ProcessFrame(void)
{
    uint8_t slave_addr = modbus_rx_buf[0];
    uint8_t func_code = modbus_rx_buf[1];
    uint16_t crc_received, crc_calculated;
    
    // 检查从机地址
    if(slave_addr != MODBUS_SLAVE_ADDR && slave_addr != 0x00)
    {
        return;  // 不是发给本机的帧
    }
    
    // 检查CRC
    crc_received = (modbus_rx_buf[modbus_rx_len-1] << 8) | modbus_rx_buf[modbus_rx_len-2];
    crc_calculated = CRC16_Calculate(modbus_rx_buf, modbus_rx_len-2);
    
    if(crc_received != crc_calculated)
    {
        printf("CRC Error: Received=0x%04X, Calculated=0x%04X\r\n", 
               crc_received, crc_calculated);
        return;
    }
    
    sys_status.last_func_code = func_code;
    sys_status.rx_count++;
    
    printf("RX Frame: ");
    for(uint8_t i = 0; i < modbus_rx_len; i++)
    {
        printf("%02X ", modbus_rx_buf[i]);
    }
    printf("\r\n");
    
    // 处理不同功能码
    switch(func_code)
    {
        case FUNC_READ_REG:  // 读保持寄存器
            Handle_ReadRegisters();
            break;
            
        case FUNC_WRITE_SINGLE:  // 写单个寄存器
            Handle_WriteSingleRegister();
            break;
            
        case FUNC_WRITE_MULTI:  // 写多个寄存器
            Handle_WriteMultipleRegisters();
            break;
            
        default:  // 非法功能码
            Send_ExceptionResponse(ERR_FUNC_CODE);
            break;
    }
}

/**
  * @brief  处理读寄存器请求
  */
void Handle_ReadRegisters(void)
{
    uint16_t start_addr = (modbus_rx_buf[2] << 8) | modbus_rx_buf[3];
    uint16_t reg_count = (modbus_rx_buf[4] << 8) | modbus_rx_buf[5];
    uint8_t resp_len = 0;
    uint16_t crc;
    
    // 检查地址合法性
    if(start_addr >= MODBUS_REG_NUM || 
       start_addr + reg_count > MODBUS_REG_NUM)
    {
        Send_ExceptionResponse(ERR_REG_ADDR);
        return;
    }
    
    // 构建响应帧
    modbus_tx_buf[resp_len++] = MODBUS_SLAVE_ADDR;
    modbus_tx_buf[resp_len++] = FUNC_READ_REG;
    modbus_tx_buf[resp_len++] = reg_count * 2;  // 字节数
    
    // 添加寄存器数据
    for(uint16_t i = 0; i < reg_count; i++)
    {
        modbus_tx_buf[resp_len++] = (modbus_regs[start_addr + i] >> 8) & 0xFF;
        modbus_tx_buf[resp_len++] = modbus_regs[start_addr + i] & 0xFF;
    }
    
    // 计算CRC
    crc = CRC16_Calculate(modbus_tx_buf, resp_len);
    modbus_tx_buf[resp_len++] = crc & 0xFF;
    modbus_tx_buf[resp_len++] = (crc >> 8) & 0xFF;
    
    // 发送响应
    MODBUS_SendResponse(modbus_tx_buf, resp_len);
    
    sys_status.last_reg_addr = start_addr;
    sys_status.last_reg_value = modbus_regs[start_addr];
    
    printf("Read Registers: Addr=%d, Count=%d\r\n", start_addr, reg_count);
}

/**
  * @brief  处理写单个寄存器请求
  */
void Handle_WriteSingleRegister(void)
{
    uint16_t reg_addr = (modbus_rx_buf[2] << 8) | modbus_rx_buf[3];
    uint16_t reg_value = (modbus_rx_buf[4] << 8) | modbus_rx_buf[5];
    uint8_t resp_len = 0;
    uint16_t crc;
    
    // 检查地址合法性
    if(reg_addr >= MODBUS_REG_NUM)
    {
        Send_ExceptionResponse(ERR_REG_ADDR);
        return;
    }
    
    // 写入寄存器
    modbus_regs[reg_addr] = reg_value;
    
    // 回显相同的数据作为响应
    memcpy(modbus_tx_buf, modbus_rx_buf, 6);
    resp_len = 6;
    
    // 计算CRC
    crc = CRC16_Calculate(modbus_tx_buf, resp_len);
    modbus_tx_buf[resp_len++] = crc & 0xFF;
    modbus_tx_buf[resp_len++] = (crc >> 8) & 0xFF;
    
    // 发送响应
    MODBUS_SendResponse(modbus_tx_buf, resp_len);
    
    sys_status.last_reg_addr = reg_addr;
    sys_status.last_reg_value = reg_value;
    
    printf("Write Register: Addr=%d, Value=0x%04X\r\n", reg_addr, reg_value);
}

/**
  * @brief  发送异常响应
  */
void Send_ExceptionResponse(uint8_t error_code)
{
    uint8_t resp_len = 0;
    uint16_t crc;
    
    modbus_tx_buf[resp_len++] = MODBUS_SLAVE_ADDR;
    modbus_tx_buf[resp_len++] = 0x80 | modbus_rx_buf[1];  // 错误响应功能码
    modbus_tx_buf[resp_len++] = error_code;
    
    crc = CRC16_Calculate(modbus_tx_buf, resp_len);
    modbus_tx_buf[resp_len++] = crc & 0xFF;
    modbus_tx_buf[resp_len++] = (crc >> 8) & 0xFF;
    
    MODBUS_SendResponse(modbus_tx_buf, resp_len);
    
    printf("Exception Response: Error Code=%d\r\n", error_code);
}

/**
  * @brief  更新显示
  */
void MODBUS_UpdateDisplay(void)
{
    char display_buf[32];
    
    LCD_ShowString(0, 40, "MODBUS Status:");
    
    sprintf(display_buf, "RX: %lu TX: %lu", sys_status.rx_count, sys_status.tx_count);
    LCD_ShowString(0, 60, display_buf);
    
    sprintf(display_buf, "Func: 0x%02X", sys_status.last_func_code);
    LCD_ShowString(0, 80, display_buf);
    
    sprintf(display_buf, "Reg[%d]: 0x%04X", 
            sys_status.last_reg_addr, sys_status.last_reg_value);
    LCD_ShowString(0, 100, display_buf);
    
    // 显示部分寄存器值
    sprintf(display_buf, "R0:%04X R1:%04X", modbus_regs[0], modbus_regs[1]);
    LCD_ShowString(0, 120, display_buf);
}

2.4 串口中断处理(stm32f10x_it.c)

c 复制代码
#include "stm32f10x_it.h"
#include "modbus_rtu.h"

extern uint8_t modbus_rx_buf[];
extern uint8_t modbus_rx_len;
extern uint8_t modbus_frame_flag;

// 帧超时检测(3.5个字符时间)
#define FRAME_TIMEOUT   (3500000 / MODBUS_BAUDRATE)  // 微秒

void USART1_IRQHandler(void)
{
    static uint32_t last_rx_time = 0;
    uint8_t data;
    
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        data = USART_ReceiveData(USART1);
        
        // 检查缓冲区是否溢出
        if(modbus_rx_len < sizeof(modbus_rx_buf))
        {
            modbus_rx_buf[modbus_rx_len++] = data;
        }
        
        // 更新最后接收时间
        last_rx_time = micros();
        
        // 清除中断标志
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
    
    // 在主循环中检查帧超时
    if(modbus_rx_len > 0 && (micros() - last_rx_time) > FRAME_TIMEOUT)
    {
        modbus_frame_flag = 1;
    }
}

2.5 调试串口输出(usart.c)

c 复制代码
#include "usart.h"
#include <stdarg.h>
#include <stdio.h>

// 重定向printf到USART2
int fputc(int ch, FILE *f)
{
    USART_SendData(USART2, (uint8_t)ch);
    while(USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
    return ch;
}

void USART2_Init(uint32_t baudrate)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
    
    // PA2-TX, PA3-RX
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    USART_InitStructure.USART_BaudRate = baudrate;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_Init(USART2, &USART_InitStructure);
    
    USART_Cmd(USART2, ENABLE);
}

三、测试工具与验证

3.1 PC端测试工具

  1. Modbus Poll(Windows)

    • 连接:选择COM口,波特率9600,RTU模式
    • 功能码03:读取寄存器
    • 功能码06:写入单个寄存器
  2. QModMaster(跨平台)

    • 开源MODBUS主站工具
    • 支持RTU/TCP

3.2 测试步骤

  1. 连接硬件:STM32通过RS485模块连接到PC

  2. 启动从机:STM32运行MODBUS从机程序

  3. 发送测试帧

    复制代码
    读寄存器:01 03 00 00 00 01 84 0A
    (从机地址01,功能码03,起始地址0000,读取1个寄存器)
  4. 观察响应

    复制代码
    响应帧:01 03 02 12 34 B5 33
    (从机地址01,功能码03,字节数2,数据1234,CRC校验)

3.3 LCD显示效果

复制代码
MODBUS RTU Slave
Addr: 0x01 Baud: 9600
MODBUS Status:
RX: 125 TX: 124
Func: 0x03
Reg[0]: 0x1234
R0:1234 R1:5678

四、常见问题解决

现象 原因 解决方案
无响应 地址不匹配 检查从机地址设置
CRC错误 波特率不一致 确认双方波特率相同
数据错乱 收发切换延迟不足 增加RS485收发切换延时
丢帧 中断处理不及时 优化中断服务程序
显示乱码 LCD初始化问题 检查LCD引脚连接

参考代码 STM32+MODEBUS+485代码,串口发送,实时显示 www.youwenfan.com/contentcsu/56209.html

五、扩展功能建议

5.1 增加更多功能码

c 复制代码
// 增加04功能码(读输入寄存器)
case 0x04:
    Handle_ReadInputRegisters();
    break;

// 增加05功能码(写单个线圈)
case 0x05:
    Handle_WriteSingleCoil();
    break;

5.2 数据记录与报警

c 复制代码
// 监控寄存器变化
if(modbus_regs[0] > 0xFF00)
{
    // 触发报警
    GPIO_SetBits(GPIOB, GPIO_Pin_0);  // 蜂鸣器报警
    printf("Alarm Triggered! Value=0x%04X\r\n", modbus_regs[0]);
}

5.3 多从机支持

c 复制代码
// 支持广播地址0x00
if(slave_addr == MODBUS_SLAVE_ADDR || slave_addr == 0x00)
{
    // 处理广播命令
    Process_BroadcastCommand();
}

六、总结

这套STM32+MODBUS+RS485方案具有以下特点:

完整协议实现 :支持03/06/16功能码
实时数据显示 :LCD实时显示通信状态
稳定可靠 :CRC校验、超时检测、错误处理
易于扩展:模块化设计,方便添加新功能

应用场景

  • 工业自动化控制系统
  • 智能仪表数据采集
  • 楼宇自动化系统
  • 远程监控系统

通过LCD实时显示MODBUS通信状态,可以直观地监控系统运行情况,便于调试和维护。

相关推荐
CinzWS2 小时前
BASETIMER(基本定时器) - 系统的时基:从时钟源、分频链到定时中断的确定性追求
单片机·嵌入式·basetimer
zy135380675732 小时前
6v/2.7A的H桥驱动芯片AH6227主要用于5v的适配器上
stm32·单片机·嵌入式硬件
维吉斯蔡2 小时前
【计算机是怎样跑起来的】(二)CPU、内存、I/O 和总线到底是什么?
笔记·stm32·单片机·物联网·计算机外设·51单片机
BT-BOX2 小时前
基于STM32的多参数物联网安防监测与远程报警系统
stm32·嵌入式硬件·物联网
云栖梦泽2 小时前
Linux内核与驱动:GPIO设备树与SPI设备树的区别
linux·运维·c++·嵌入式硬件
三品吉他手会点灯2 小时前
STM32 VSCode 开发-C语言程序运行后,终端中文乱码
c语言·ide·笔记·vscode·stm32·学习·编辑器
zmj3203242 小时前
单片机共地通信
单片机·嵌入式硬件·公共地·共地
2201_756206343 小时前
STM32L431 USART3 串口调试总结
单片机·嵌入式硬件
yugi9878383 小时前
STM32F407 + EC20 串口透传 TCP DTU 实现方案
stm32·嵌入式硬件·tcp/ip