基于stm32的电压检测(Modbus通信协议)
项目简介:核心部分由STM32实时电压采集和本地界面显示功能, 并将采集到的电压数据通过Modbus RTU协议发送给ESP32,实现数据的实时传输, ESP32微控制器作为数据转发和处理中心,接收来自STM32的电压数据,并利用Modbus TCP协议将数据写入到Modbus TCP从机上,实现远程监控和数据存储功能。
有关Modbus协议的讲解:Modbus通信协议
STM32 从机实现(HAL库实现):
STM32与ESP32通过串口连接走Modbus RTU
通信,硬件配置方面不做过多介绍。
定义Modbus处理结构体:
typedef struct
{
uint8_t Addr; //本设备地址(从设备)
uint8_t RxBuffer[100]; //接收到主机的数据缓冲区
uint16_t RxPointer; //串口接收指针,端口已经收到的字节个数
uint16_t TimeOut; //modbus的数据断续时间
uint16_t Timer_Start; //modbus的定时器是否开始计时
uint8_t Rx_Flag; //接收是否完成标志
uint8_t TxBuffer[100]; //modbus的发送缓冲区
}MODBUS;
extern MODBUS modbus;
串口中断回调函数:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) //串口字节接收中断
{
if(huart->Instance == USART1)
{
if(modbus.Rx_Flag == 1) //RX_Flag = 1表示正在处理上一帧数据,未处理完之前不进行接收
{
return;
}
modbus.RxBuffer[modbus.RxPointer++] = Uart_Rx; //将接收到的数据进行保存
modbus.TimeOut = 0; //数据接收停止计时置0
if(modbus.RxPointer==1) //接收到主机发来的第一个字节
{
modbus.Timer_Start = 1; //启动定时器
}
HAL_UART_Receive_IT(&huart1,&Uart_Rx,1);
}
}
大致流程如下:当处理器正在处理上一次请求时串口不进行新的接收,当上一次请求完成后进行数据接收,当接收到第一帧数据的时候打开计时器,用于判断一帧数据是否接收完成。
定时器中断回调函数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) //定时器中断回调函数
{
if(htim->Instance == TIM3)
{
if(modbus.Timer_Start != 0)
{
modbus.TimeOut++;
if(modbus.TimeOut >= 8) //间隔时间超过了约定的时间,说明一帧数据接收完成,那么关闭定时器
{
modbus.Timer_Start = 0; //关闭定时器
modbus.Rx_Flag = 1; //数据接收完成
}
}
}
}
定时器的任务很简单,用于判断请求的数据是否全部接收完成。
当最后一个字节接收完成后4ms内没有数据发来,表示一个请求接收完成,为了规避掉硬件缺陷我们保守使用8ms的时间,接收完成后把标志置1,关闭定时器。
请求处理函数Modbus_Event():
void Modbus_Event()
{
uint16_t crc;
uint16_t Rx_Crc;
if(modbus.Rx_Flag == 0) //没有接收到数据包
{
return;
}
crc = crc16(&modbus.RxBuffer[0],modbus.RxPointer-2); //计算出来的校验码
Rx_Crc = modbus.RxBuffer[modbus.RxPointer-2]*256+modbus.RxBuffer[modbus.RxPointer-1]; //收到的校验码
if(crc == Rx_Crc) //收到一次完整的数据包
{
double adc = get_adc(&hadc2)*3.3/4096;
uint16_t intValue = (uint16_t)((int)(adc*100));
Reg[0] = intValue;
switch(modbus.RxBuffer[1])
{
case 0: break;
case 1: break;
case 2: break;
case 3: Modbus_fun_3(); break; //3号功能码的处理
case 4: break;
case 5: break;
case 6: Modbus_fun_6(); break; //6号功能码的处理
case 7: break;
}
}
modbus.Rx_Flag = 0;
modbus.RxPointer = 0;
}
把接收到的数据本地进行CRC校验,然后与接收的CRC校验进行对比,相同表示数据无误进行功能码实现,本次实验只实现了03和06功能码的实现。
03功能码实现(读取保持寄存器):
void Modbus_fun_3() //主机要读取从设备的寄存器
{
uint16_t RegAddr;
uint16_t Reglen;
uint16_t Tx_Point=0;
uint16_t byte;
uint16_t Rx_crc;
uint16_t j;
RegAddr = modbus.RxBuffer[2]*256+modbus.RxBuffer[3]; //要读取的寄存器的首地址
Reglen = modbus.RxBuffer[4]*256+modbus.RxBuffer[5]; //要读取的寄存器的长度
modbus.TxBuffer[Tx_Point++] = modbus.Addr;
modbus.TxBuffer[Tx_Point++] = 0x03; //功能码
byte = Reglen * 2; //要返回的字节数
modbus.TxBuffer[Tx_Point++] = byte/256;
modbus.TxBuffer[Tx_Point++] = byte%256;
for(j = 0; j<Reglen;j++)
{
modbus.TxBuffer[Tx_Point++] = Reg[RegAddr+j]/256;
modbus.TxBuffer[Tx_Point++] = Reg[RegAddr+j]%256;
}
Rx_crc = crc16(modbus.TxBuffer,Tx_Point);
modbus.TxBuffer[Tx_Point++] = Rx_crc/256;
modbus.TxBuffer[Tx_Point++] = Rx_crc%256;
HAL_UART_Transmit(&huart1,modbus.TxBuffer,Tx_Point,50);
memset(modbus.TxBuffer,0,Tx_Point);
}
06功能码实现(写入保持寄存器):
void Modbus_fun_6()
{
uint16_t RegAddr;
uint16_t val;
uint16_t Tx_Point=0;
uint16_t Rx_crc;
RegAddr = modbus.RxBuffer[2]*256+modbus.RxBuffer[3]; //得到要修改的地址
val = modbus.RxBuffer[4]*256+modbus.RxBuffer[5]; //修改后的值
Reg[RegAddr] = val; // 修改本设备的的寄存器
//以下代码回应主机
modbus.TxBuffer[Tx_Point++] = modbus.Addr;
modbus.TxBuffer[Tx_Point++] = 0x06; //功能码
modbus.TxBuffer[Tx_Point++] = RegAddr/256;
modbus.TxBuffer[Tx_Point++] = RegAddr%256;
modbus.TxBuffer[Tx_Point++] = val/256;
modbus.TxBuffer[Tx_Point++] = val%256;
Rx_crc = crc16(modbus.TxBuffer,Tx_Point);
modbus.TxBuffer[Tx_Point++] = Rx_crc/256;
modbus.TxBuffer[Tx_Point++] = Rx_crc%256;
HAL_UART_Transmit(&huart1,modbus.TxBuffer,Tx_Point,50);
memset(modbus.TxBuffer,0,Tx_Point);
}
Modbus 主机实现
ESP32通过wifi连接走Modbus TCP
通信。
ESP32端代码使用的是eModbus库。
#include <Arduino.h>
#include "HardwareSerial.h"
#include <WiFi.h>
#include "ModbusClientTCP.h"
#include "ModbusClientRTU.h"
//构造一个TCP客户端
WiFiClient Esp32_Client;
// 创建Modbus RTU客户端实例和TCP实例
ModbusClientRTU MB_RTU;
ModbusClientTCP MB_TCP(Esp32_Client);
#ifndef MY_SSID
#define MY_SSID "xxxxxxxxxx"
#endif
#ifndef MY_PASS
#define MY_PASS "xxxxxxxxxx"
#endif
char ssid[] = MY_SSID; // SSID and ...
char pass[] = MY_PASS; // password for the WiFi network used
uint16_t RTU_Buffer[] = { 0, 0, 0, 0};
//定义一个onData处理函数来接收常规响应
//参数为Modbus服务器ID、请求的功能码、消息数据及其长度;
//加上一个用户提供的令牌来标识引起请求
void handleData(ModbusMessage response, uint32_t token)
{
Serial.printf("Response: serverID=%d, FC=%d, Token=%08X, length=%d:\n", response.getServerID(), response.getFunctionCode(), token, response.size());
for (auto& byte : response) {
Serial.printf("%02X ", byte);
}
if(response.getServerID() == 4 && response.getFunctionCode() == READ_HOLD_REGISTER)
{
for (int i = 0; i < 4; i++) {
// 跳过前3个字节(从机地址、功能码和字节计数)
RTU_Buffer[i] = (response[i * 2 + 3] << 8) | response[i * 2 + 4];
}
}
Serial.println("");
}
//接收错误响应
void handleError(Error error, uint32_t token)
{
// ModbusError包装错误代码并为其提供可读的错误消息
ModbusError me(error);
Serial.printf("Error response: %02X - %s\n", (int)me, (const char *)me);
}
void setup() {
// 初始化串口监视器
Serial.begin(9600);
while (!Serial) {}
Serial.println("__ OK __");
//连接wifi
WiFi.begin(ssid, pass);
delay(200);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(". ");
delay(1000);
}
IPAddress wIP = WiFi.localIP();
Serial.printf("WIFi IP address: %u.%u.%u.%u\n", wIP[0], wIP[1], wIP[2], wIP[3]);
// 设置串口参数,用于与Modbus RTU设备通信
RTUutils::prepareHardwareSerial(Serial2);
Serial2.begin(9600, SERIAL_8N1, GPIO_NUM_16, GPIO_NUM_17);
// 设置 ModbusRTU 客户端。
//提供数据处理程序功能
//消息超时时间
MB_RTU.setTimeout(2000);
// 启动Modbus RTU客户端的后台任务
MB_RTU.begin(Serial2);
//设置ModbusRTU客户端
MB_RTU.onDataHandler(&handleData);
MB_RTU.onErrorHandler(&handleError);
MB_RTU.setTimeout(2000);
MB_RTU.begin(Serial2);
//设置ModbusTCP客户端
MB_TCP.onDataHandler(&handleData);
MB_TCP.onErrorHandler(&handleError);
MB_TCP.setTimeout(2000, 200);
MB_TCP.begin();
//设置从机IP地址和端口
MB_TCP.setTarget(IPAddress(192, 168, 1, 107), 502);
}
void loop() {
uint32_t Token = 1111;
//每5秒读取一次RTU从机保持寄存器
Error err = MB_RTU.addRequest(Token++, 4, READ_HOLD_REGISTER, 0, 4);
if (err!=SUCCESS) {
ModbusError e(err);
Serial.printf("Error creating request: %02X - %s\n", (int)e, (const char *)e);
}
err = MB_TCP.addRequest(Token++, 20, WRITE_MULT_REGISTERS, 0, 4, 8, RTU_Buffer);
if (err!=SUCCESS) {
ModbusError e(err);
Serial.printf("Error creating request: %02X - %s\n", (int)e, (const char *)e);
}
delay(5000);
}
内部实现不做过多介绍,有兴趣的可以去下载源码进行分析,TCP通信和RTU通信本质上是一个东西,只是把串口通信转变为TCP网络通信,都是进行请求码和回应码的构造然后进行传输。
实验现象:
串口监视器数据:
使用modbus slave 模拟Modbus TCP 从机: