工业通讯标准:Modbus协议的C语言实现详解

原创 2023-11-26 21:05 浙江
文章的分类 通信协议

前言

常见的工业通讯协议

协议优点缺点应用场景
Modbus- 简单易用- 安全性有限- 工业自动化
- 支持多种物理层- 速度较慢- 能源管理和建筑自动化
Profinet- 实时性强- 配置复杂- 机器人控制
- 支持工业以太网- 需要高带宽网络基础设施- 自动化生产线
CAN- 实时性强- 通信速率较低- 汽车内部通信
- 适用于恶劣环境- 网络拓扑复杂- 工业自动化
Ethernet/IP- 高速数据传输- 需要高带宽网络基础设施- 制造业、能源领域
- 具备开放性- 配置和管理复杂- 智能工厂、物联网(IoT)

深入理解Modbus协议及其C语言实现

Modbus是一种广泛应用于工业自动化的通信协议。在本文中,我们将深入探讨Modbus协议,并通过C语言代码示例来理解其在实际应用中的实现。

Modbus 协议概述

Modbus协议是一种基于主从架构的通信协议,支持多种类型的寄存器和操作。它可以通过串行通信(Modbus RTU)或基于TCP/IP的网络(Modbus TCP)进行数据传输。

寄存器和功能码

  • 寄存器类型

    • 线圈寄存器:用于读写操作。
    • 离散输入寄存器:仅读取操作。
    • 输入寄存器:仅读取操作。
    • 保持寄存器:用于读写操作。
  • 常用功能码

    • 读线圈寄存器(01H)
    • 读离散输入寄存器(02H)
    • 读保持寄存器(03H)
    • 读输入寄存器(04H)
    • 写单个线圈寄存器(05H)
    • 写单个保持寄存器(06H)
    • 写多个线圈寄存器(0FH)
    • 写多个保持寄存器(10H)

C语言代码分析

以下是Modbus协议C语言编写的功能函数代码的一部分。这段代码是用于实现Modbus RTU协议的从机底层驱动程序。方便理深入理解

文件形式

  • .c
`/* 私有函数原形 --------------------------------------------------------------*/`
// CRC校验计算
static uint8_t MB_JudgeAddr(uint16_t _Addr,uint16_t _RegNum);
// 判断操作的数量是否符合协议范围
static uint8_t MB_JudgeNum(uint16_t _RegNum,uint8_t _FunCode,uint16_t _ByteNum);
// 功能码
static uint16_t MB_RSP_01H(uint16_t _TxCount,uint16_t _AddrOffset ,uint16_t _CoilNum );
static uint16_t MB_RSP_02H(uint16_t _TxCount,uint16_t _AddrOffset ,uint16_t _CoilNum);
static uint8_t MB_RSP_03H(uint16_t _TxCount,uint16_t *_AddrOffset,uint16_t _RegNum );
static uint8_t MB_RSP_04H(uint16_t _TxCount,uint16_t _AddrOffset,uint16_t _RegNum );
static uint8_t MB_RSP_05H(uint16_t _TxCount,uint16_t _AddrOffset ,uint16_t _RegDATA);
static uint8_t MB_RSP_06H(uint16_t _TxCount,uint16_t _AddrOffset ,uint16_t _RegNum ,uint16_t *_AddrAbs);
static uint8_t MB_RSP_10H(uint16_t _TxCount,uint16_t _AddrOffset,uint16_t _RegNum ,uint16_t *_AddrAbs ,uint8_t* _Datebuf);```

这些函数使用了 static 关键字。在C或C++中,使用 static 关键字来声明函数时,表示这些函数的作用域被限定在当前源文件内,不能被其他源文件访问。这些函数只能在同一源文件中调用

  • .h
/* 函数声明 ------------------------------------------------------------------*/
// CRC校验计算
uint16_t CRC16_MODBUS(uint8_t *_pbuf, uint8_t _uslen);
// 提取数据帧,进行解析数据帧
void MB_Parse_Data(void);
// 对接收到的信息进行分析并执行
uint8_t MB_Analyze_Execute(void );
// 判断操作的数量是否符合协议范围
uint8_t MB_JudgeNum(uint16_t _Num,uint8_t _FunCode,uint16_t ByteNum);
// 判断地址是否符合协议范围
uint8_t MB_JudgeAddr(uint16_t _Addr,uint16_t _Num);
// 异常响应
void MB_Exception_RSP(uint8_t _FunCode,uint8_t _ExCode);
// 正常响应
void MB_RSP(uint8_t _FunCode);

总体处理流程

  1. 接收数据 :
    接收Modbus请求数据。这部分通常涉及串行通信或网络通信,根据硬件和通信方式而定。
  2. 解析请求 :
    使用 MB_Parse_Data 函数解析接收到的请求数据。这个函数将提取功能码、寄存器地址、寄存器数量等关键信息。
  3. 分析请求 :
    使用 MB_Analyze_Execute 函数对解析出的请求数据进行进一步分析,检查是否存在异常。
  4. 准备响应 :
    根据分析结果,使用 MB_RSP 函数准备响应数据。该函数将根据功能码调用相应的处理函数,计算CRC校验码,并准备响应数据。
  5. 发送响应 :
    通过 UART_Tx 发送准备好的响应数据。

示例

第一步:定义全局变量和结构

// 全局变量和结构已定义
extern uint8_t Rx_Buf[256]; // 接收缓冲区
extern uint8_t Tx_Buf[256]; // 发送缓冲区
extern PDUData_TypeDef PduData; // Modbus PDU 数据结构

#define MB_SLAVEADDR         0x01 // 从站地址
#define FUN_CODE_03H            0x03  //功能码03H

#define IS_NOT_FUNCODE(code)  (!((code == FUN_CODE_01H)||\
                                 (code == FUN_CODE_02H)||\
                                 (code == FUN_CODE_03H)||\
                                 (code == FUN_CODE_04H)||\
// ... 其他必要的定义 ...

第二步:解析请求函数

/**
 * 函数功能:提取数据帧,进行解析数据帧
 * 输入参数:无
 * 返 回 值:无
 * 说    明:无
 */
void MB_Parse_Data()
{
  PduData.Code = Rx_Buf[1];                                         // 功能码
  PduData.Addr = ((Rx_Buf[2] << 8) | Rx_Buf[3]);                    // 寄存器起始地址
  PduData.Num = ((Rx_Buf[4] << 8) | Rx_Buf[5]);                     // 数量(Coil,Input,Holding Reg,Input Reg)
  PduData._CRC = CRC16_MODBUS((uint8_t *)&Rx_Buf, RxCount - 2);     // CRC校验码
  PduData.byteNums = Rx_Buf[6];                                     // 获得字节数
  PduData.ValueReg = (uint8_t *)&Rx_Buf[7];                         // 写多个寄存器起始地址
  PduData.PtrHoldingOffset = PduData.PtrHoldingbase + PduData.Addr; // 保持寄存器的起始地址
}

第三步:分析和执行请求

/**
 * 函数功能:对接收到的信息进行分析并执行
 * 输入参数:无
 * 返 回 值:异常码0x00
 * 说    明:判断功能码,验证地址,数据是否溢出,数据没错误发送响应信号
 */
uint8_t MB_Analyze_Execute(void){
  uint16_t ExCode = EX_CODE_NONE; // 0x00
  /* 校验码功能 */
  if (IS_NOT_FUNCODE(PduData.Code)) // 不支持的功能码
  {
    /* MODBUS异常响应 */
    ExCode = EX_CODE_01H;
    return ExCode;
  }
  /* 根据功能码分别判断 */
  switch (PduData.Code){
    /* ---- 01H  02H 读离散输入寄存器(Coil Input)----------- */
    
    /* ---- 03H  04H 读保持/读输入寄存器---------------------- */
    case FUN_CODE_03H:
    case FUN_CODE_04H:
      /* 判断寄存器数量 */
      ExCode = MB_JudgeNum(PduData.Num, PduData.Code, PduData.byteNums);
      if (ExCode != EX_CODE_NONE)
      {
        return ExCode;
      }
      /* 判断地址*/
      ExCode = MB_JudgeAddr(PduData.Addr, PduData.Num);
      if (ExCode != EX_CODE_NONE)
      {
        return ExCode;
      }
      break;

      /* ---- 05H 写单个线圈---------------------- */

      /* ---- 06H 写单个保持 ---------------------- */
  }
  /* 数据帧没有异常 */
  return ExCode; //   EX_CODE_NONE
}

第四步:响应函数

/**
 * 函数功能:正常响应
 * 输入参数:_FunCode 功能码
 * 返 回 值:无
 * 说    明:当通讯数据帧没有异常时并且成功执行后,发送响应数据帧
 */
void MB_RSP(uint8_t _FunCode)
{
  uint16_t TxCount = 0;
  uint16_t crc = 0;
  Tx_Buf[TxCount++] = MB_SLAVEADDR; /* 从站地址 */
  Tx_Buf[TxCount++] = _FunCode;     /* 功能码   */
  switch (_FunCode)
  {
  case FUN_CODE_01H:
    /* 读取线圈状态 */
    TxCount = MB_RSP_01H(TxCount, PduData.Addr, PduData.Num);
    break;
  case FUN_CODE_02H:
    /* 读取离散输入 */
    TxCount = MB_RSP_02H(TxCount, PduData.Addr, PduData.Num);
    break;
  case FUN_CODE_03H:
    /* 读取保持寄存器 */
    TxCount = MB_RSP_03H(TxCount, (uint16_t *)PduData.PtrHoldingOffset, PduData.Num);
    break;
  case FUN_CODE_04H:
    /* 读取输入寄存器 */
    TxCount = MB_RSP_04H(TxCount, PduData.Addr, PduData.Num);
    break;
  case FUN_CODE_05H:
    /* 写单个线圈 */
    TxCount = MB_RSP_05H(TxCount, PduData.Addr, PduData.Num);
    break;
  case FUN_CODE_06H:
    /* 写单个保持寄存器 */
    TxCount = MB_RSP_06H(TxCount, PduData.Addr, PduData.Num, (uint16_t *)PduData.PtrHoldingOffset);
    break;
  case FUN_CODE_10H:
    /* 写多个保持寄存器 */
    TxCount = MB_RSP_10H(TxCount, PduData.Addr, PduData.Num, (uint16_t *)PduData.PtrHoldingOffset, (uint8_t *)PduData.ValueReg);
    break;
  }
  // 计算CRC校验码
  crc = CRC16_MODBUS((uint8_t *)&Tx_Buf, TxCount);

  // 将CRC校验码添加到发送缓冲区
  Tx_Buf[TxCount++] = crc & 0xFF;      // CRC低字节
  Tx_Buf[TxCount++] = (crc >> 8) & 0xFF; // CRC高字节

  // 发送响应
  UART_Tx((uint8_t *)Tx_Buf, TxCount);
}

第五步:主处理流程

void processModbusRequest() {
    // 假设 Rx_Buf 已经接收到数据

    // 解析请求数据
    MB_Parse_Data();

    // 分析请求数据并执行相应操作
    uint8_t ExCode = MB_Analyze_Execute();
    if (ExCode != EX_CODE_NONE) {
        // 如果有异常,可以在这里处理,例如发送异常响应
        // MB_Exception_RSP(PduData.Code, ExCode);
        return;
    }

    // 准备并发送响应数据
    MB_RSP(PduData.Code);
}

第六步:主函数或调用入口

int main() {
    // 接收Modbus请求数据到 Rx_Buf
    // ... 代码接收数据 ...

    // 处理接收到的Modbus请求
    processModbusRequest();

    // ... 其他代码 ...
    return 0;
}

示例便于理解底层逻辑,我的代码并不一定正确,流程大概是这个样子,实际应用还需要进行各种错误格式检查,以及异常处理等,当前省略了这些步骤。如果还看不懂的话,那么:

主机发送数据

字段说明备注
从机地址 01H
功能码 03H
寄存器地址高字节 00H
寄存器地址低字节 10H
寄存器数量高字节 00H
寄存器数量低字节 02H读取两个寄存器
CRC校验高字节 C5H
CRC校验低字节 CEH

从机应答数据

字段说明备注
从机地址 01H
功能码 03H
字节数 04H
数据1高字节寄存器0010H的值12H
数据1低字节寄存器0010H的值34H
数据2高字节寄存器0011H的值02H
数据2低字节寄存器0011H的值03H
CRC校验高字节 FFH
CRC校验低字节 F4H

示例:读取单个保持寄存器

主机发送从机应答
01 03 00 10 00 01 85 CF01 03 02 12 34 B5 33
主机发送注释
01从机地址,指定目标设备的地址
03功能码,表示这是一个读保持寄存器的请求
00 10寄存器起始地址(高字节和低字节),这里是0010H
00 01要读取的寄存器数量(高字节和低字节),这里指示读取1个寄存器
85 CFCRC校验码,用于检查数据传输过程中的错误
从机应答注释
01从机地址,与请求中的从机地址相同
03功能码,与请求中的功能码相同,表示这是一个读保持寄存器的响应
02字节计数,表示后续跟随的数据字节数,这里是2字节
12 34保持寄存器的值,这里是寄存器0010H的值,分为高字节和低字节
B5 33CRC校验码,用于检查数据传输过程中的错误

限于篇幅,对源码感兴趣研究的可以下载

隐藏内容,需要留言后方可查看



THE END


分享
赞赏
精选留言 写留言
    1. 仓年墨月 来自黑龙江省牡丹江市电信 访客 头像

      学习

      2023年12月24日
    1. 张宇芳 来自美国 访客 头像
      2023年12月21日
    1. Atman 来自上海市鹏博士宽带 访客 头像

      大佬主题开源吗

      2023年12月14日
    1. 来自浙江省杭州市华数 访客 头像

      通俗易懂

      2023年12月14日
    1. qq1920965933 来自浙江省金华市移动 访客 头像

      2

      2023年12月07日
    1. A 来自安徽省阜阳市联通 访客 头像

      看看

      2023年12月03日
    1. 来自浙江省宁波市联通 访客 头像

      你自己写的?

      2023年11月30日
    1. 阿祖 来自河南省济源市联通 访客 头像

      Z

      2023年11月27日
    1. 来自河南省漯河市联通 访客 头像

      1

      2023年11月27日
    1. sky 来自广东省佛山市电信 访客 头像

      1

      2023年11月26日