关注公众号,回复“入门资料”获取单片机入门到高级开挂教程
开发板带你入门,我们带你飞
文 | 无际(微信:2777492857)
全文约3753字,阅读大约需要 10 分钟
我第一次接触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:工业标准接口,支持长距离、多设备通讯。
典型硬件连接:
Plain Text |
4.2 软件实现
在单片机上实现Modbus,可以选择:
•开源库:如libmodbus,提供现成函数,适合快速开发。
•自行编写:从协议规范入手,灵活性更高,适合学习。
我们一般都是自己写,没用过开源库的。
实现步骤包括:
1.配置UART(波特率、数据位等)。
2.解析Modbus帧(地址、功能码、数据、CRC)。
3.处理功能码并响应。
4.3 示例项目:STM32实现Modbus从设备
让我们通过一个实例,看看如何用STM32单片机读取传感器数据并响应Modbus请求。
硬件准备
•STM32F103开发板
•RS-485模块
•温度传感器(如DS18B20,模拟输入寄存器)
项目目标
•单片机作为从设备,地址为1。
•支持功能码03(读保持寄存器),返回温度值。
代码实现
我们以STM32为例,实现一个简单的Modbus RTU从设备,功能是响应主设备的功能码03请求,返回一个固定的温度值(25.0°C)。
以下是伪代码,仅展示核心逻辑:
// 模拟的温度值(单位: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,不断处理请求。
测试方法
1.用电脑上的Modbus Poll软件模拟主设备,发送请求:
Plain Text |
○01:设备地址
○03:功能码
○00 00:起始地址0
○00 01:读取1个寄存器
○84 0A:CRC校验
2.单片机应该返回:
Plain Text |
○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协议简单实用,是工业自动化的入门钥匙,如果有帮助,记得安排三连啊!
end
下面是更多无际原创的个人成长经历、行业经验、技术干货。
1.电子工程师是怎样的成长之路?10年5000字总结
2.如何快速看懂别人的代码和思维
3.单片机开发项目全局变量太多怎么管理?
4.C语言开发单片机为什么大多数都采用全局变量的形式?
5.单片机怎么实现模块化编程?实用程度让人发指!
6.c语言回调函数的使用及实际作用详解
7.手把手教你c语言队列实现代码,通俗易懂超详细!
8.c语言指针用法详解,通俗易懂超详细!