基于 STM32 与脉冲电源的 Modbus RTU 通信项目详解
本文内容是工作中实际遇到的一个项目
一、引言
在工业控制和自动化领域,设备之间的可靠通信至关重要。Modbus RTU 协议作为一种广泛应用的串行通信协议,因其简单、高效且开放的特性,成为连接各种工业设备的理想选择。本文将围绕使用 STM32 单片机与脉冲电源通过 Modbus RTU 协议进行通信的项目展开,详细介绍 Modbus RTU 协议的原理、通信设置、消息帧格式、功能代码以及寄存器地址表等内容,并结合实际项目进行说明。
二、Modbus RTU 协议概述
2.1 基本概念
Modbus 是一种主 - 从式协议,在本项目中,上位机作为主站发起通信请求,整流机(脉冲电源)作为从站进行应答。采用 RTU(十六进制数)传输模式,数据以二进制代码形式传输,通过 CRC16 循环冗余校验确保数据传输的准确性。
2.2 通信口设置
2.2.1 通讯方式
采用异步串行通讯接口 RS - 485,这种接口具有抗干扰能力强、传输距离远等优点,适合工业现场环境。
2.2.2 波特率
默认值为 19200bps,且可在 4800 - 115200 的范围内进行设置。波特率决定了数据传输的速率,需要根据实际通信距离和设备性能进行选择。
2.2.3 字节数据格式
- 起始位:1 位,用于标识数据帧的开始。
- 数据位:可设置为 7 位或 8 位,本项目默认采用 8 位数据位,能传输更多的数据信息。
- 停止位:可设置为 1 位或 2 位,本项目默认采用 1 位停止位,用于标识数据帧的结束。
- 校验位:可设置为偶校验、无校验或奇校验,本项目默认采用偶校验,用于检测数据传输过程中是否发生错误。
字节数据格式如下:
1 * * * * * * * * 1 *
起始位 数据位(从低到高) 校验位 停止位
三、消息帧格式
3.1 读寄存器帧
主站向从站发送读寄存器请求时,消息帧格式如下:
从站地址 | 功能代码 | 首寄存器地址 | 寄存器数 N | CRC16 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 |
例如,要读取从站地址为 01H 的设备中,从地址 0000H 开始的 5 个寄存器的值,读寄存器帧如下:
01H 03H 0000H 0005H CrcL, CrcH
3.2 读寄存器返回帧
从站接收到读寄存器请求并处理后,向主站返回的消息帧格式如下:
从站地址 | 功能代码 | 字节数 | 寄存器数据 | CRC16 |
---|---|---|---|---|
1 字节 | 1 字节 | 1 字节 | N * 2 字节 | 2 字节 |
假设成功读取 5 个寄存器的值,读寄存器返回帧如下:
01H 03H 0AH [寄存器数据] CrcL, CrcH
其中,字节数为 0AH(即 10 字节,因为每个寄存器占 2 字节,5 个寄存器共 10 字节)。
3.3 写寄存器帧
主站向从站发送写寄存器请求时,消息帧格式如下:
从站地址 | 功能代码 | 首寄存器地址 | 寄存器数 N | 字节数 | 寄存器数据 | CRC16 |
---|---|---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 1 字节 | N * 2 字节 | 2 字节 |
例如,要向从站地址为 01H 的设备中,从地址 0005H 开始的 5 个寄存器写入数据,写寄存器帧如下:
01H 10H 0005H 0005H 0AH [寄存器数据] CrcL, CrcH
3.4 写寄存器返回帧
从站接收到写寄存器请求并处理成功后,向主站返回的消息帧格式如下:
从站地址 | 功能代码 | 首寄存器地址 | 寄存器数 N | CRC16 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 |
写寄存器返回帧示例:
01H 10H 0005H 0005H CrcL, CrcH
四、功能代码
4.1 功能代码列表
功能代码 | ModBus 名 | 功能名 | 广播 | 数量 |
---|---|---|---|---|
03H | Read Holding Registers | 读 N 个寄存器值 | No | 5 |
10H | Write Multiple Registers | 写 N 个寄存器值 | No | 5 |
4.2 功能代码说明
- 03H:用于主站向从站读取多个保持寄存器的值。主站指定起始寄存器地址和要读取的寄存器数量,从站将相应寄存器的值返回给主站。
- 10H:用于主站向从站写入多个保持寄存器的值。主站指定起始寄存器地址、要写入的寄存器数量和具体的寄存器数据,从站将数据写入相应的寄存器。
五、寄存器地址表
5.1 寄存器地址表内容
编号 | 参数符号 | 参数名 | 地址 | 类型 | 数据类型 | 数值范围 | 备注 |
---|---|---|---|---|---|---|---|
1 | V | 电压显示值 | 0 | 读 | 16 位无符号(World) | 0 - 1200 (10 进制) | 电压有一位小数点,读到 800 代表 80.0V |
2 | I | 电流显示值 | 1 | 读 | 16 位无符号(World) | 0 - 2000 (10 进制) | 电流有 2 个小数点,读 1500 代表 15.00A |
3 | 状态显示 | 2 | 读 | 16 位无符号(World) | 0 - 4 (10 进制) | 0——正常 1——过热 2——过流 3——其它 | |
4 | F | 频率显示 | 3 | 读 | 16 位无符号(World) | 额定值范围内(10 进制) | 读到 6000 代表 6000HZ |
5 | D | 占空比显示 | 4 | 读 | 16 位无符号(World) | 0 - 100 (10 进制) | 读到 100 代表 100% |
6 | FV | 电压设置值 | 5 | 写 | 16 位无符号(World) | 0 - 1200 (10 进制) | 电压有一位小数点,写入 1000 代表 100.0V |
7 | FI | 电流设置值 | 6 | 写 | 16 位无符号(World) | 0 - 2000 (10 进制) | 电流有 2 个小数点,写入 1000 代 10.00A |
8 | FF | 频率设置 | 7 | 写 | 16 位无符号(World) | 写入 3000 代表 3000HZ | |
9 | FD | 占空比设置 | 8 | 写 | 16 位无符号(World) | 0 - 100 | 写入 50 代表 50% |
10 | RUN | 启停命令 | 9 | 写 | 16 位无符号(World) | 0 - 1 | 0 - 关机 1 - 开机 |
11 | 备用 | 10 | 16 位无符号(World) | ||||
12 | VC/CC | 稳压稳流 | 11 | 写 | 16 位无符号(World) | 0 - 1 | 0 - 稳流 1 - 稳压 |
13 | 脉冲个数设置 | 12 | 写 | 16 位无符号(World) | 1 - 60000 | 写入 200 代表 200 个 | |
14 | 间隔时间 | 13 | 写 | 16 位无符号(World) | 1 - 99999 | 写入 100 代表 100 秒 | |
15 | 循环次数 | 14 | 写 | 16 位无符号(World) | 1 - 99999 | 写入 100 代表 100 次 | |
16 | 脉冲启动 | 15 | 写 | 16 位无符号(World) | 0 - 1 | 0 - 关闭脉冲 1 - 启动脉冲 |
5.2 寄存器地址表说明
- 数据类型:所有寄存器的数据类型均为 16 位无符号整型(两字节)。
- 小数点处理:通信传输中带小数点的数据全部用整数代替,例如 27.9 代替为 279。
- 数据传输顺序:全部寄存器数据在传输过程中用十六进制数表示,先传高字节,再传低字节。例如,传送 279 时,先传 01H,再传 17H。
六、CRC16 校验
6.1 校验原理
CRC16 是一种循环冗余校验方法,用于检测数据在传输过程中是否发生错误。在本项目中,CRC16 校验从从站地址到数据区最后一个字节,计算多项式码为 A001(hex)。
6.2 校验计算示例
在 STM32 中实现 CRC16 校验计算可以通过以下代码示例:
#include <stdint.h>// CRC16 计算函数
uint16_t crc16(uint8_t *data, uint16_t length) {uint16_t crc = 0xFFFF;uint16_t i, j;for (i = 0; i < length; i++) {crc ^= (uint16_t)data[i];for (j = 0; j < 8; j++) {if (crc & 0x0001) {crc >>= 1;crc ^= 0xA001;} else {crc >>= 1;}}}return crc;
}
使用示例:
uint8_t message[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x05};
uint16_t crc = crc16(message, sizeof(message));
uint8_t crcL = (uint8_t)(crc & 0xFF);
uint8_t crcH = (uint8_t)(crc >> 8);
七、STM32 + KEIL5 + 固件库举例说明 ModbusRTU 协议
以下是使用STM32标准固件库实现Modbus RTU协议通信的详细步骤和代码示例。
1. 硬件连接
与使用HAL库时的硬件连接方式相同,将STM32的串口(如USART1)与RS - 485转换器连接,RS - 485转换器的A、B线连接到Modbus RTU从站设备的对应接口,同时确保所有设备共地。
2. 工程准备
在Keil等开发环境中创建一个基于STM32标准固件库的工程,将标准固件库文件添加到工程中。
3. 代码实现
3.1 串口初始化
使用标准固件库函数对串口进行初始化,设置通信参数。
#include "stm32f10x.h"// 串口初始化函数
void USART1_Init(void) {GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;// 使能GPIOA和USART1时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);// 配置USART1 Tx (PA9)为复用推挽输出GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);// 配置USART1 Rx (PA10)为浮空输入GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init(GPIOA, &GPIO_InitStructure);// USART1配置USART_InitStructure.USART_BaudRate = 19200;USART_InitStructure.USART_WordLength = USART_WordLength_8b;USART_InitStructure.USART_StopBits = USART_StopBits_1;USART_InitStructure.USART_Parity = USART_Parity_No;USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;USART_Init(USART1, &USART_InitStructure);// 使能USART1接收中断USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);// 使能USART1USART_Cmd(USART1, ENABLE);// 配置NVICNVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStructure);
}
3.2 CRC16校验函数
实现CRC16校验函数,用于计算和验证消息帧的CRC校验值。
#include <stdint.h>// CRC16计算函数
uint16_t crc16(uint8_t *data, uint16_t length) {uint16_t crc = 0xFFFF;uint16_t i, j;for (i = 0; i < length; i++) {crc ^= (uint16_t)data[i];for (j = 0; j < 8; j++) {if (crc & 0x0001) {crc >>= 1;crc ^= 0xA001;} else {crc >>= 1;}}}return crc;
}
3.3 发送读寄存器请求
编写函数来发送读寄存器请求消息帧,并计算CRC校验值。
// 发送读寄存器请求
void sendReadRequest(uint8_t slaveAddress, uint16_t startAddress, uint16_t registerCount) {uint8_t request[8];request[0] = slaveAddress;request[1] = 0x03;request[2] = (uint8_t)(startAddress >> 8);request[3] = (uint8_t)(startAddress & 0xFF);request[4] = (uint8_t)(registerCount >> 8);request[5] = (uint8_t)(registerCount & 0xFF);uint16_t crc = crc16(request, 6);request[6] = (uint8_t)(crc & 0xFF);request[7] = (uint8_t)(crc >> 8);for (int i = 0; i < 8; i++) {while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);USART_SendData(USART1, request[i]);}
}
3.4 接收并处理响应
在串口接收中断服务函数中接收从站的响应消息帧,并验证CRC校验值。
#define BUFFER_SIZE 256
uint8_t receiveBuffer[BUFFER_SIZE];
uint8_t receiveIndex = 0;// 串口1中断服务函数
void USART1_IRQHandler(void) {if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {receiveBuffer[receiveIndex++] = USART_ReceiveData(USART1);// 这里可以根据实际情况添加接收完成判断条件// 例如,根据消息帧长度判断USART_ClearITPendingBit(USART1, USART_IT_RXNE);}
}// 处理接收到的响应
void processResponse() {// 验证CRC校验uint16_t receivedCrc = (uint16_t)(receiveBuffer[receiveIndex - 2]) | ((uint16_t)(receiveBuffer[receiveIndex - 1]) << 8);uint16_t calculatedCrc = crc16(receiveBuffer, receiveIndex - 2);if (receivedCrc == calculatedCrc) {// 处理响应数据// 这里可以根据响应消息帧的格式解析数据// 例如,获取寄存器数据等} else {// CRC校验失败}receiveIndex = 0; // 清空接收缓冲区
}
4. 主函数调用示例
int main(void) {USART1_Init();// 发送读寄存器请求sendReadRequest(0x01, 0x0000, 0x0005);while (1) {// 可以在合适的时机调用processResponse函数处理接收到的响应if (receiveIndex > 0) {processResponse();}}
}
5. 关键部分解释
- 串口初始化:
USART1_Init
函数使用标准固件库的GPIO和USART初始化函数对串口进行配置,设置波特率、数据位、停止位、校验位等参数,并使能接收中断。 - CRC16校验:
crc16
函数根据Modbus RTU协议的规则计算CRC校验值,用于保证消息帧的完整性。 - 消息帧发送:
sendReadRequest
函数构建读寄存器请求消息帧,计算CRC校验值,并通过串口发送出去。 - 消息帧接收:在
USART1_IRQHandler
中断服务函数中接收从站的响应消息帧,将数据存储在receiveBuffer
中。processResponse
函数用于处理接收到的响应,验证CRC校验值并解析数据。
通过以上步骤,可以使用STM32标准固件库实现基于Modbus RTU协议的通信。需要注意的是,实际应用中可能需要根据具体情况对代码进行调整和优化,例如添加超时处理、错误处理等功能。
八、总结
通过本项目,深入了解 Modbus RTU 协议的原理、通信设置、消息帧格式、功能代码和寄存器地址表等内容,并使用 STM32 单片机实现与脉冲电源的通信。Modbus RTU 协议以其简单、可靠的特点,为工业设备之间的通信提供了有效的解决方案。在实际应用中,需要根据具体需求进行通信参数的设置和错误处理,以确保通信的稳定性和准确性。同时,通过合理的硬件设计和软件编程,可以实现更复杂的工业控制和自动化应用。