基于stm32的电压检测(Modbus通信协议)

基于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 从机:
在这里插入图片描述

本文章只用作学习笔记,并没有参考价值。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值