我第一次接触Modbus时,是做一个电梯的配件,还有后面做PDU也做过。
网上资料一大堆,但零零散散,我一开始也完全不知道从哪下手,我翻开官方文档,满眼的专业术语和表格,头都大了。帧结构是什么?CRC校验怎么算?完全摸不着头脑。
其实找个简单的代码示例,把基础概念弄明白,再去看理论的东西,贼简单。
后来我找到突破口:从Modbus RTU入手,先搞懂帧结构和功能码,其他的慢慢补。
这篇文章带你从零开始,拆解Modbus的核心概念。
1. 引言
先大概介绍下,在工业自动化领域,设备之间的“对话”是实现智能控制的核心。Modbus通讯协议作为一种简单、开放的协议,自1979年诞生以来,广泛应用于PLC、传感器、变频器等设备的数据交换。它的设计初衷是为工业现场提供一种高效的通讯方式,如今已成为自动化领域的“通用语言”。
除了PLC以外,将Modbus协议与单片机结合,不仅能让单片机轻松与其他设备通讯,还能扩展其在工业场景中的应用,降低硬件成本,提高定制化能力。
本文将带你从零开始认识Modbus,并通过单片机开发的实例,助你迈出实践第一步。
2. Modbus协议基础
2.1 历史与背景
Modbus协议由Modicon公司于1979年开发,最初用于其可编程逻辑控制器(PLC)的通讯。
随着时间推移,它因免费开放和易于实现而成为工业标准。如今,Modbus已发展出多个版本,适应不同场景,如串行通讯和网络通讯。
2.2 协议类型
Modbus有以下几种常见形式:
-
Modbus RTU:采用二进制格式,适用于串行通讯(如RS-485),效率高,是工业现场的主流选择。
-
Modbus ASCII:使用ASCII字符,便于调试,但数据冗余较多。
-
Modbus TCP/IP:基于以太网的版本,适合远程访问和现代网络应用。
初学者通常从Modbus RTU入手,因为它简单直接,且硬件支持广泛,在嵌入式领域用这种类型用得很多。
2.3 通讯模式
Modbus采用主从模式:
-
主设备(Master):如PLC、单片机或上位机,负责发起请求。
-
从设备(Slave):如传感器或执行器,接收并响应请求。
一次通讯由主设备发送请求帧开始,从设备处理后返回响应帧。这种单向发起的机制简单可靠。
2.4 数据模型
Modbus将数据分为四种类型,存储在不同的“寄存器”中:
这些寄存器就像设备的“内存单元”,主设备通过操作它们实现数据交换。
3. Modbus RTU详解
由于Modbus RTU是工业中最常用的版本,我们深入了解它的细节。
3.1 帧结构
Modbus RTU的通讯帧由以下部分组成:
-
地址码(1字节):从设备的地址,取值1-247。
-
功能码(1字节):指定操作类型,如读取或写入。
-
数据区(长度可变):包含具体的操作数据。
-
CRC校验(2字节):循环冗余校验码,确保数据无误。
例如,一个读取寄存器的请求帧可能是:
01 03 00 00 00 02 C4 0B
-
01
:从设备地址 -
03
:功能码(读保持寄存器) -
00 00
:起始地址 -
00 02
:读取2个寄存器 -
C4 0B
:CRC校验
3.2 功能码
功能码定义了主设备要执行的操作,常见的有:
3.3 CRC校验
CRC(循环冗余校验)是Modbus RTU的关键,用于检测传输错误。发送方计算CRC并附加在帧尾,接收方重新计算并比对。单片机实现时,可用查表法快速完成计算。
这个算法也挺复杂的,分CRC16、CRC32,我们一般用CRC16就够了。
而且最关键的,网上其实有现成的算法代码,我第一次接触就傻乎乎的去研究公式,走了几个星期弯路,把我气的。
4. Modbus在单片机中的应用
以下从硬件、软件和实例三个方面展开。
4.1 硬件接口
单片机与Modbus设备的连接通常依赖:
-
UART:单片机的串行通讯模块,用于发送和接收数据。
-
RS-485:工业标准接口,支持长距离、多设备通讯。
典型硬件连接:
单片机UART TX/RX -> RS-485转换器 -> Modbus网络
4.2 软件实现
在单片机上实现Modbus,可以选择:
-
开源库:如
libmodbus
,提供现成函数,适合快速开发。 -
自行编写:从协议规范入手,灵活性更高,适合学习。
我们一般都是自己写,没用过开源库的。
实现步骤包括:
-
配置UART(波特率、数据位等)。
-
解析Modbus帧(地址、功能码、数据、CRC)。
-
处理功能码并响应。
4.3 示例项目:STM32实现Modbus从设备
让我们通过一个实例,看看如何用STM32单片机读取传感器数据并响应Modbus请求。
硬件准备
-
STM32F103开发板
-
RS-485模块
-
温度传感器(如DS18B20,模拟输入寄存器)
项目目标
-
单片机作为从设备,地址为1。
-
支持功能码03(读保持寄存器),返回温度值。
代码实现
我们以STM32为例,实现一个简单的Modbus RTU从设备,功能是响应主设备的功能码03请求,返回一个固定的温度值(25.0°C)。
以下是伪代码,仅展示核心逻辑:
#include "stm32f10x.h"
// 模拟的温度值(单位:0.1°C,例如250表示25.0°C)
//小数点不方便传输,一般我们是扩大10倍,接收端再减少10倍
uint16_t temperature = 250;
// UART发送函数(伪代码)
void uart_send(uint8_t *data, int len) {
// 这里是发送数据的代码,具体实现根据你的单片机调整
}
// UART接收函数(伪代码)
int uart_receive(uint8_t *buffer, int *len) {
// 这里是接收数据的代码
// 返回1表示收到数据,0表示没收到
return 0; // 假设暂时没收到数据
}
// CRC校验函数(简化版,实际需要完整的CRC16计算)
uint16_t calc_crc(uint8_t *data, int len) {
// 实际项目中,这里用CRC16算法计算校验值
// 为了简单,这里返回固定值
return 0xFFFF;
}
// 处理Modbus请求的主函数
void modbus_task() {
uint8_t rx_buffer[20]; // 用来存接收到的数据
int len = 0; // 接收到的数据长度
// 检查有没有收到数据
if (uart_receive(rx_buffer, &len)) {
// 解析收到的数据
uint8_t addr = rx_buffer[0]; // 第1个字节是地址
uint8_t func = rx_buffer[1]; // 第2个字节是功能码
// 只处理地址为1的请求(我们的设备地址是1)
if (addr == 0x01) {
// 只支持功能码03(读保持寄存器)
if (func == 0x03) {
// 从收到的数据中取出起始地址和寄存器数量
uint16_t start_addr = (rx_buffer[2] << 8) | rx_buffer[3]; // 第3、4字节是起始地址
uint16_t reg_count = (rx_buffer[4] << 8) | rx_buffer[5]; // 第5、6字节是寄存器数量
// 假设我们只支持读取地址0的1个寄存器
if (start_addr == 0 && reg_count == 1) {
// 准备发送的响应数据
uint8_t tx_buffer[10]; // 响应数据缓冲区
tx_buffer[0] = 0x01; // 地址(我们的设备地址)
tx_buffer[1] = 0x03; // 功能码(读保持寄存器)
tx_buffer[2] = 2; // 返回的数据字节数(温度值占2字节)
tx_buffer[3] = (temperature >> 8); // 温度值的高字节
tx_buffer[4] = temperature; // 温度值的低字节
// 计算CRC校验值
uint16_t crc = calc_crc(tx_buffer, 5); // 前5字节参与校验
tx_buffer[5] = crc & 0xFF; // CRC低字节
tx_buffer[6] = crc >> 8; // CRC高字节
// 发送响应给主设备
uart_send(tx_buffer, 7); // 总共7字节
}
}
}
}
}
// 主函数
int main() {
// 初始化UART,设置波特率为9600
uart_init(9600);
// 一直循环处理Modbus请求
while (1) {
modbus_task();
}
}
代码解释
1. 变量定义
-
temperature
:一个简单的变量,存储温度值,设为250(表示25.0°C)。我们用它来模拟Modbus的保持寄存器。
2. UART函数
-
uart_send
:发送数据的函数。这里是伪代码,实际中你需要用STM32的HAL库(比如HAL_UART_Transmit
)实现。 -
uart_receive
:接收数据的函数。返回1表示收到数据,0表示没收到。同样是伪代码,实际用HAL库实现。
3. CRC函数
-
calc_crc
:计算CRC校验值。这里为了简单,返回固定值0xFFFF
。实际项目中,需要用CRC16算法(查表法或计算法)来实现。
给大家分享我产品上一直在用的CRC16校验算法:
const unsigned short wCRCTalbeAbs[] =
{
0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400,
};
unsigned short mt_api_crc16(unsigned char *ptr, unsigned int len)
{
unsigned short wCRC = 0xFFFF;
unsigned short i;
unsigned char chChar;
unsigned char temp[2];
for (i = 0; i < len; i++)
{
chChar = *ptr++;
wCRC = wCRCTalbeAbs[(chChar ^ wCRC) & 15] ^ (wCRC >> 4);
wCRC = wCRCTalbeAbs[((chChar >> 4) ^ wCRC) & 15] ^ (wCRC >> 4);
}
temp[0] = wCRC&0xFF;
temp[1] = (wCRC>>8)&0xFF;
wCRC = (temp[0]<<8)|temp[1];
return wCRC;
}
再教大家一个技巧,怎么验证CRC16校验码是否正确?
把校验码和数据再进行一次CRC16的计算,得出来新的CRC16校验码是0,就代表正确。
4. Modbus任务函数(modbus_task)
这个函数是代码的核心,处理Modbus通信的步骤:
-
接收数据:用
rx_buffer
存储收到的数据。 -
解析数据:
-
addr
:第1个字节是设备地址,我们只处理地址为1的请求。 -
func
:第2个字节是功能码,我们只支持03(读保持寄存器)。 -
start_addr
:第3、4字节表示起始地址。 -
reg_count
:第5、6字节表示要读的寄存器数量。
-
-
检查请求:只支持读取地址0的1个寄存器。
-
构造响应:
-
tx_buffer[0]
:设备地址(1)。 -
tx_buffer[1]
:功能码(3)。 -
tx_buffer[2]
:返回的数据字节数(温度值占2字节)。 -
tx_buffer[3]
和tx_buffer[4]
:温度值的高低字节。 -
tx_buffer[5]
和tx_buffer[6]
:CRC校验值。
-
-
发送响应:把7字节数据发出去。
5. 主函数(main)
-
初始化UART,设置波特率为9600(Modbus RTU常用波特率)。
-
无限循环调用
modbus_task
,不断处理请求。
测试方法
-
用电脑上的Modbus Poll软件模拟主设备,发送请求:
01 03 00 00 00 01 84 0A
-
01
:设备地址 -
03
:功能码 -
00 00
:起始地址0 -
00 01
:读取1个寄存器 -
84 0A
:CRC校验
-
-
单片机应该返回:
01 03 02 00 FA XX XX
-
01
:设备地址 -
03
:功能码 -
02
:数据字节数 -
00 FA
:温度值250(16进制表示25.0°C) -
XX XX
:CRC校验值(简化版是0xFFFF)
-
4.4 调试与测试
开发中,调试工具必不可少:
-
Modbus Poll:模拟主设备,测试请求。
-
Modbus Slave:模拟从设备,验证逻辑。
-
串口助手:查看原始数据。
5. 常见问题与解决方案
5.1 通讯故障
-
地址冲突:多个从设备地址相同。解决:分配唯一地址。
-
CRC错误:传输干扰或计算错误。解决:检查线路和代码。
-
超时:从设备未响应。解决:确认电源和参数。
5.2 调试技巧
-
用示波器检查信号。
-
分步测试,从简单功能开始。
Modbus协议简单实用,是工业自动化的入门钥匙,如果有帮助,记得安排三连啊!
最近很多粉丝问我单片机怎么学,我根据自己从业十年经验,累积耗时一个月,精心整理一份「单
片机最佳学习路径+单片机入门到高级教程+工具包」,全部无偿分享给铁粉!!!
除此以外,再含泪分享我压箱底的22个热门开源项目,包含源码+原理图+PCB+说明文档,让你迅速进阶成高手!
教程资料包和详细的学习路径可以看我下面这篇文章的开头。