利用STM32实现Modbus通信(RTU从机方案)

一、系统概述

Modbus是工业领域广泛应用的串行通信协议,支持RTU(二进制)和ASCII(文本)模式,本方案基于STM32F103C8T6(Cortex-M3,72MHz)实现Modbus RTU从机,通过RS485总线与主机(如PLC、PC)通信,支持保持寄存器(0x03/0x06/0x10)、线圈(0x01/0x05/0x0F) 读写,可扩展至其他功能码。核心功能包括:Modbus协议解析、寄存器管理、RS485收发控制、CRC校验。

二、硬件设计

2.1 核心组件

模块 型号/参数 功能说明
主控 STM32F103C8T6(64KB Flash,20KB RAM) Modbus协议处理、寄存器管理、串口通信
通信接口 MAX485(RS485物理层) 实现Modbus RTU差分信号传输(半双工)
串口 USART1(PA9-TX,PA10-RX,115200bps) 连接MAX485,配置8N1(8数据位、无校验、1停止位)
定时器 TIM2(16位通用定时器) 计算Modbus RTU帧超时(3.5字符时间,如9600bps时≈3.64ms)
控制引脚 PB0(DE/RE) 控制MAX485收发模式(高电平发送,低电平接收)

2.2 硬件连接

模块 引脚(STM32F103C8T6) 说明
USART1 PA9(TX)→ MAX485 DI 发送数据
PA10(RX)← MAX485 RO 接收数据
MAX485 DE/RE → PB0 收发控制(高=发送,低=接收)
RS485总线 A/B端子 连接主机(A-A,B-B)

三、软件设计(STM32标准库3.5)

3.1 系统架构

RS485
数据接收
解析请求
返回响应
数据发送
RS485
3.5T超时中断
主机(Modbus RTU)
STM32 USART1
FreeModbus协议栈
寄存器管理(保持寄存器/线圈)
TIM2

3.2 核心原理

  1. Modbus RTU帧格式[地址码(1)][功能码(1)][数据(n)][CRC16(2)],CRC16校验确保数据完整性。

  2. 从机工作流程

  • 接收主机请求帧,校验CRC;

  • 解析功能码,操作对应寄存器(读/写);

  • 生成响应帧(含数据+CRC),通过RS485返回主机。

3.3 开发步骤

3.3.1 FreeModbus协议栈移植
  • 获取源码 :从FreeModbus官网下载V1.6稳定版,关键文件:

  • modbus.c/modbus.h:协议核心(从机模式);

  • port/portserial.c/portserial.h:串口操作(需移植);

  • port/porttimer.c/porttimer.h:定时器操作(需移植);

  • port/portother.c/portother.h:临界区保护(关中断/开中断)。

3.3.2 STM32硬件初始化
(1)时钟与GPIO配置
c 复制代码
#include "stm32f10x.h"

void RCC_Config(void) {
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART1 | RCC_APB1Periph_TIM2, ENABLE); // 串口1+定时器2时钟
}

void GPIO_Config(void) {
  GPIO_InitTypeDef GPIO_InitStruct;
  
  // USART1_TX(PA9):复用推挽输出
  GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
  GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &GPIO_InitStruct);
  
  // USART1_RX(PA10):浮空输入
  GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
  GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  GPIO_Init(GPIOA, &GPIO_InitStruct);
  
  // MAX485_DE/RE(PB0):推挽输出(高=发送,低=接收)
  GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
  GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOB, &GPIO_InitStruct);
  GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 初始接收模式
}
(2)USART1初始化(Modbus RTU,115200bps,8N1)
c 复制代码
void USART1_Init(void) {
  USART_InitTypeDef USART_InitStruct;
  USART_InitStruct.USART_BaudRate = 115200;
  USART_InitStruct.USART_WordLength = USART_WordLength_8b;
  USART_InitStruct.USART_StopBits = USART_StopBits_1;
  USART_InitStruct.USART_Parity = USART_Parity_No;
  USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  USART_Init(USART1, &USART_InitStruct);
  USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收中断
  USART_Cmd(USART1, ENABLE);
}
(3)TIM2初始化(3.5T超时定时器,1ms基准)
c 复制代码
void TIM2_Init(void) {
  TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
  TIM_TimeBaseStruct.TIM_Period = 999;         // 1ms重装值(72MHz/72=1MHz,1ms=1000计数,重装值999)
  TIM_TimeBaseStruct.TIM_Prescaler = 71;        // 预分频72(72MHz/72=1MHz)
  TIM_TimeBaseStruct.TIM_ClockDivision = 0;
  TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
  TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStruct);
  TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);   // 使能更新中断
  TIM_Cmd(TIM2, DISABLE);                      // 初始关闭,接收数据时启动
}
3.3.3 FreeModbus接口移植
(1)串口操作(portserial.c)
c 复制代码
#include "portserial.h"
#include "stm32f10x_usart.h"

// 串口初始化(已在USART1_Init中实现)
BOOL xMBPortSerialInit(UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity) {
  return TRUE;
}

// 使能串口收发(DE/RE控制)
void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable) {
  if (xRxEnable) {
    GPIO_ResetBits(GPIOB, GPIO_Pin_0); // MAX485接收模式(DE=0)
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收中断
  } else {
    USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
  }
  
  if (xTxEnable) {
    GPIO_SetBits(GPIOB, GPIO_Pin_0); // MAX485发送模式(DE=1)
  } else {
    GPIO_ResetBits(GPIOB, GPIO_Pin_0);
  }
}

// 发送单个字节
BOOL xMBPortSerialPutByte(CHAR ucByte) {
  USART_SendData(USART1, ucByte);
  while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送完成
  return TRUE;
}

// 接收单个字节(中断中调用)
BOOL xMBPortSerialGetByte(CHAR *pucByte) {
  *pucByte = USART_ReceiveData(USART1);
  return TRUE;
}
(2)定时器操作(porttimer.c)
c 复制代码
#include "porttimer.h"
#include "stm32f10x_tim.h"

// 定时器初始化(3.5T超时,单位:50μs,115200bps时3.5T≈364μs=7.28×50μs→取整8)
BOOL xMBPortTimersInit(USHORT usTim1Timerout50us) {
  return TRUE; // 已在TIM2_Init中配置1ms基准,此处无需额外初始化
}

// 启动定时器(超时时间=usTim1Timerout50us×50μs)
void vMBPortTimersEnable(void) {
  TIM_SetCounter(TIM2, 0);       // 清零计数器
  TIM_Cmd(TIM2, ENABLE);          // 启动定时器
}

// 关闭定时器
void vMBPortTimersDisable(void) {
  TIM_Cmd(TIM2, DISABLE);         // 关闭定时器
  TIM_SetCounter(TIM2, 0);       // 清零计数器
}

// TIM2中断服务函数(1ms触发一次,累计超时)
void TIM2_IRQHandler(void) {
  if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    pxMBPortCBTimerExpired(); // 通知FreeModbus帧超时
  }
}
(3)寄存器定义(modbus_slave.c)

定义保持寄存器(0x03功能码)和线圈(0x01功能码):

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

// 保持寄存器(地址0-9,16位整数)
USHORT usRegHoldingBuf[10] = {0x1234, 0x5678, 0x9ABC, 0xDEF0, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006};

// 线圈(地址0-7,1位布尔值)
UCHAR ucRegCoilsBuf[1] = {0x01}; // 线圈0=ON(bit0=1),其余OFF

// 保持寄存器回调函数(读/写)
eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) {
  if (usAddress + usNRegs > 10) return MB_ENOREG; // 地址越界
  if (eMode == MB_REG_READ) {
    memcpy(pucRegBuffer, &usRegHoldingBuf[usAddress], usNRegs * 2); // 16位寄存器,2字节/个
  } else { // 写
    memcpy(&usRegHoldingBuf[usAddress], pucRegBuffer, usNRegs * 2);
  }
  return MB_ENOERR;
}

// 线圈回调函数(读/写)
eMBErrorCode eMBRegCoilsCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode) {
  if (usAddress + usNCoils > 8) return MB_ENOREG; // 地址越界(8个线圈)
  if (eMode == MB_REG_READ) {
    for (int i=0; i<usNCoils; i++) {
      UCHAR bit = (ucRegCoilsBuf[usAddress/8] >> (usAddress%8)) & 0x01;
      pucRegBuffer[i/8] |= (bit << (i%8));
      usAddress++;
    }
  } else { // 写
    for (int i=0; i<usNCoils; i++) {
      if (pucRegBuffer[i/8] & (1 << (i%8))) {
        ucRegCoilsBuf[usAddress/8] |= (1 << (usAddress%8));
      } else {
        ucRegCoilsBuf[usAddress/8] &= ~(1 << (usAddress%8));
      }
      usAddress++;
    }
  }
  return MB_ENOERR;
}
3.3.4 主函数(Modbus轮询)
c 复制代码
#include "stm32f10x.h"
#include "modbus.h"

int main(void) {
  RCC_Config();       // 时钟配置
  GPIO_Config();      // GPIO配置(USART1+PB0)
  USART1_Init();      // USART1初始化(115200bps)
  TIM2_Init();        // TIM2初始化(超时定时器)
  NVIC_Config();      // 中断优先级配置(USART1+TIM2)
  
  // 初始化FreeModbus从机(地址0x01,RTU模式,串口1,115200bps,无校验)
  eMBInit(MB_RTU, 0x01, 0, 115200, MB_PAR_NONE);
  eMBEnable();        // 使能Modbus
  
  while (1) {
    eMBPoll();        // 轮询Modbus事件(解析请求、发送响应)
  }
}

// 中断优先级配置(NVIC)
void NVIC_Config(void) {
  NVIC_InitTypeDef NVIC_InitStruct;
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 分组2(2位抢占,2位响应)
  
  // USART1中断(接收)
  NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
  NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
  NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStruct);
  
  // TIM2中断(超时)
  NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;
  NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
  NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
  NVIC_Init(&NVIC_InitStruct);
}

// USART1中断服务函数(接收数据)
void USART1_IRQHandler(void) {
  if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
    pxMBFrameCBByteReceived(); // 通知FreeModbus接收到一个字节
    USART_ClearITPendingBit(USART1, USART_IT_RXNE);
  }
}

参考代码 利用STM32单片机实现modbus通信 www.youwenfan.com/contentcst/56553.html

四、测试与验证

4.1 测试工具

  • Modbus Poll(PC端):发送0x03功能码读取保持寄存器(地址0-9),0x01功能码读取线圈(地址0-7);

  • 串口助手 :监控USART1数据,验证帧格式(如读寄存器请求:01 03 00 00 00 0A C5 CD,响应:01 03 14 12 34 56 78 9A BC DE F0 00 01 00 02 00 03 00 04 00 05 00 06 XX XX,XX为CRC)。

4.2 关键参数

  • 波特率:115200bps时,1字符=10位(8数据+1停止+1校验,无校验时为9位),3.5T≈3.5×9/115200≈273μs,TIM2中断1ms累计3次触发超时;

  • CRC校验:FreeModbus自动处理CRC16(多项式0x8005),无需手动计算。

五、关键问题与解决方案

5.1 通信乱码

  • 原因:波特率不匹配(如STM32用115200,主机用9600)、串口参数错误(数据位/校验位);

  • 解决 :用示波器测量USART_TX引脚,确认波特率(115200bps时1位≈8.68μs),检查USART_InitStruct配置。

5.2 帧超时错误

  • 原因:TIM2定时不准确(如预分频/重装值错误)、3.5T计算偏差;

  • 解决:根据波特率调整TIM2重装值(如9600bps时3.5T≈364μs,TIM2 1ms中断累计1次触发超时)。

5.3 寄存器读写异常

  • 原因:寄存器地址映射错误(如主机请求地址10,但从机仅定义0-9);

  • 解决 :在eMBRegHoldingCB中添加地址越界判断(if (usAddress + usNRegs > 10) return MB_ENOREG;)。

六、总结

基于STM32F103实现了Modbus RTU从机,核心是FreeModbus协议栈移植+RS485收发控制+寄存器管理。通过简单修改寄存器定义(如扩展保持寄存器数量、添加输入寄存器),可支持更多功能码(0x04读输入寄存器、0x05写单线圈等),适用于工业传感器、执行器等设备的通信接口。

相关推荐
cmpxr_3 小时前
【单片机】常用设计模式
单片机·嵌入式硬件·设计模式
杰杰桀桀桀3 小时前
4*4无时延矩阵键盘(非阻塞)--附代码链接
stm32·单片机·嵌入式硬件·矩阵·计算机外设·无时延矩阵键盘
奶茶拌火锅3 小时前
EB配置Tc27x——MCU
单片机·嵌入式硬件
cmpxr_3 小时前
【单片机】STM32Fxx启动模式怎么接
stm32·单片机·嵌入式硬件
篮子里的玫瑰3 小时前
STM32 时序计算指南:时钟周期与波特率深入剖析
stm32·单片机·嵌入式硬件
feifeigo1233 小时前
基于STM32F407和WM8978的MP3播放程序设计与实现
stm32·单片机·嵌入式硬件
点灯小铭4 小时前
基于单片机的智能感应式汽车雨刮器控制系统设计
单片机·嵌入式硬件·汽车·毕业设计·课程设计·期末大作业
独小乐4 小时前
007.GNU C内联汇编杂谈|千篇笔记实现嵌入式全栈/裸机篇
linux·c语言·汇编·单片机·嵌入式硬件·arm·gnu
清风6666664 小时前
基于单片机的自动存包柜设计
单片机·嵌入式硬件·mongodb·毕业设计·课程设计·期末大作业