前言
常见的工业通讯协议
协议 | 优点 | 缺点 | 应用场景 |
---|---|---|---|
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);
总体处理流程
- 接收数据 :
接收Modbus请求数据。这部分通常涉及串行通信或网络通信,根据硬件和通信方式而定。 - 解析请求 :
使用MB_Parse_Data
函数解析接收到的请求数据。这个函数将提取功能码、寄存器地址、寄存器数量等关键信息。 - 分析请求 :
使用MB_Analyze_Execute
函数对解析出的请求数据进行进一步分析,检查是否存在异常。 - 准备响应 :
根据分析结果,使用MB_RSP
函数准备响应数据。该函数将根据功能码调用相应的处理函数,计算CRC校验码,并准备响应数据。 - 发送响应 :
通过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 CF | 01 03 02 12 34 B5 33 |
主机发送 | 注释 |
---|---|
01 | 从机地址,指定目标设备的地址 |
03 | 功能码,表示这是一个读保持寄存器的请求 |
00 10 | 寄存器起始地址(高字节和低字节),这里是0010H |
00 01 | 要读取的寄存器数量(高字节和低字节),这里指示读取1个寄存器 |
85 CF | CRC校验码,用于检查数据传输过程中的错误 |
从机应答 | 注释 |
---|---|
01 | 从机地址,与请求中的从机地址相同 |
03 | 功能码,与请求中的功能码相同,表示这是一个读保持寄存器的响应 |
02 | 字节计数,表示后续跟随的数据字节数,这里是2字节 |
12 34 | 保持寄存器的值,这里是寄存器0010H的值,分为高字节和低字节 |
B5 33 | CRC校验码,用于检查数据传输过程中的错误 |
限于篇幅,对源码感兴趣研究的可以下载
学习
违法犯罪举报平台
大佬主题开源吗
通俗易懂
2
看看
你自己写的?
Z
1
1