文章目录
- 一、Modbus-RTU协议简介
- 二、循环队列简介
- 1.基本概念
- 1.循环队列在串口通讯中的应用
- 三、Modbus-RTU从机协议解析
- 总结
一、Modbus-RTU协议简介
Modbus-RTU协议是工业控制领域中常见的通讯协议,采用主从架构(Master/Slave),一问一答的形式,主设备发起请求,从设备响应。这里以03功能码为例,介绍下具体的帧格式,如下图所示。

根据不同产品的,协议的寄存器地址和从机地址有所不同。
二、循环队列简介
1.基本概念
循环队列是一种线性数据结构,通过固定大小的数组实现,利用头尾指针的模运算实现循环存储。当队列满时,尾指针会绕回到数组起始位置,避免频繁的数据搬移,适合处理高频数据流。下图则是对循环队列相关功能的一个简单图解。

下面是具体代码实现
UserQueue.h
#ifndef __USERQUEUE_H__
#define __USERQUEUE_H__
#include "main.h"
#define QUEUE_MAX_BUFFER_SIZE 256
typedef uint8_t elemType;
#pragma pack(1)
typedef struct
{
elemType dateBuffer[QUEUE_MAX_BUFFER_SIZE];//数据数组
uint8_t front;//头指针
uint8_t rear;//尾指针
uint16_t elemNum;//当前元素个数
}QUEUE;
#pragma pack()
extern void queueInit(QUEUE* pQueue);
extern uint8_t getQueueIsEmpty(QUEUE* pQueue);
extern uint8_t getQueueIsFull(QUEUE* pQueue);
extern void enQueue(QUEUE* pQueue, elemType date);
extern void dnQueue(QUEUE* pQueue, elemType* date);
#endif
UserQueue.c
/****************************************************************************************
* @file UserQueue.c
* @brief 循环队列的实现
* @author lwz
* @date 2025/10/21
* @version V1.0.0
* @note 1、队列初始化
2、队列判空
3、队列判满
4、获取队列当前元素数量
5、入队
6、出队
****************************************************************************************/
/*头文件添加区*/
#include "UserQueue.h"
#include <string.h>
/*宏定义定义区*/
/*全局变量定义区*/
/*函数声明区*/
void queueInit(QUEUE* pQueue);
uint8_t getQueueIsEmpty(QUEUE* pQueue);
uint8_t getQueueIsFull(QUEUE* pQueue);
void enQueue(QUEUE* pQueue, elemType date);
void dnQueue(QUEUE* pQueue, elemType* date);
/*函数定义区*/
/***************************************************************************************
* @brief 队列初始化
* @param pQueue 队列指针
* @retval void
* @note 1、初始化队列数组、队头指针、队尾指针
***************************************************************************************/
void queueInit(QUEUE* pQueue)
{
if(pQueue == NULL)
{
return;
}
memset(pQueue->dateBuffer, 0 , QUEUE_MAX_BUFFER_SIZE);
pQueue->front = 0;
pQueue->rear = 0;
pQueue->elemNum = 0;
}
/***************************************************************************************
* @brief 判断队列是否为空
* @param pQueue 队列指针
* @retval 0表示不为空,1表示为空,2表示队列指针为NULL
* @note
***************************************************************************************/
uint8_t getQueueIsEmpty(QUEUE* pQueue)
{
if(pQueue == NULL)
{
return 2;
}
if(pQueue->front == pQueue->rear)
{
return 1;
}
return 0;
}
/***************************************************************************************
* @brief 判断队列是否满
* @param pQueue 队列指针
* @retval 0表示不满,1表示为满,2表示队列指针为NULL
* @note
***************************************************************************************/
uint8_t getQueueIsFull(QUEUE* pQueue)
{
if(pQueue == NULL)
{
return 2;
}
if( (pQueue->rear+1)%QUEUE_MAX_BUFFER_SIZE == pQueue->front)
{
return 1;
}
return 0;
}
/***************************************************************************************
* @brief 一个元素入队
* @param pQueue 队列指针
* @retval
* @note 队列满则不入队,直接return
***************************************************************************************/
void enQueue(QUEUE* pQueue, elemType date)
{
if(pQueue == NULL || getQueueIsFull(pQueue))
{
return;
}
pQueue->dateBuffer[pQueue->rear] = date;
pQueue->rear = (pQueue->rear+1) % QUEUE_MAX_BUFFER_SIZE;
}
/***************************************************************************************
* @brief 一个元素出队
* @param pQueue 队列指针
* @retval
* @note 队列空则直接return
***************************************************************************************/
void dnQueue(QUEUE* pQueue, elemType* date)
{
if(pQueue == NULL || getQueueIsEmpty(pQueue))
{
return;
}
*date = pQueue->dateBuffer[pQueue->front];
pQueue->front = (pQueue->front+1) % QUEUE_MAX_BUFFER_SIZE;
}
1.循环队列在串口通讯中的应用
串口通信通常以字节流形式接收数据,数据到达速率不固定且可能突发。直接处理可能导致数据丢失或阻塞。循环队列的以下特性使其成为理想选择:
非阻塞缓冲:允许数据在未被处理时暂存,避免丢失。
高效读写:头尾指针操作的时间复杂度为O(1),适合实时系统。
内存预分配:固定大小缓冲区减少动态内存分配的开销。
具体代码使用场景如下:
在串口接收回调函数中完成数据入队操作
/***************************************************************************************
* @brief UART接收回调函数
* @param huart 串口句柄指针
* @retval void
* @note 1、完成串口数据的接收
2、接收到的数据放入对应的循环队列中
***************************************************************************************/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == UART4)
{
enQueue(&gs_rs485RxQueue, g_uart4RxDate);
gs_rs485RxQueue.elemNum++;
gs_uart4RxTick = HAL_GetTick();
HAL_UART_Receive_IT(huart, &g_uart4RxDate, sizeof(uint8_t));
}
}
三、Modbus-RTU从机协议解析
接下来介绍作为从机时,以03功能码为例,来解析接收到的查询数据帧,完成对应数据填充并发送
代码如下:
ModbusRTU.c
/****************************************************************************************
* @file ModbusRTU.c
* @brief ModbusRTU解析
* @author lwz
* @date 2025/10/22
* @version V1.0.0
* @note
****************************************************************************************/
/*头文件添加区*/
#include "ModbusRTU.h"
#include "UserQueue.h"
#include "UserUart.h"
#include "ModbusRTU_Cmd.h"
#include <string.h>
/*宏定义定义区*/
/*全局变量定义区*/
static Slave_State gs_ModbusSlaveState = SLAVE_IDLE;
static ModbusSendFrame gs_sendFrmae = {0};
/*函数声明区*/
static uint8_t Check_Frame(QUEUE* pQueue);
void Date_Parse(uint8_t* recvBuff);
/*函数定义区*/
/***************************************************************************************
* @brief modbusRTU协议解析
* @param void
* @retval void
* @note
***************************************************************************************/
void ModbusProcess(void)
{
QUEUE* pQueue = NULL;
pQueue = GetRs485QueuePointer();
switch(gs_ModbusSlaveState)
{
case SLAVE_IDLE:
{
if(GetReceiveIsOk())
{
gs_ModbusSlaveState = SLAVE_JUDGE;
}
break;
}
case SLAVE_JUDGE:
{
if(Check_Frame(pQueue) == 0)
{
gs_ModbusSlaveState = SLAVE_PROCESS;
}
else
{
gs_ModbusSlaveState = SLAVE_ERROR;
}
break;
}
case SLAVE_PROCESS:
{
memset(gs_sendFrmae.dateArr, 0, FRAME_DATE_SIZE);
Date_Parse(pQueue->dateBuffer);
gs_ModbusSlaveState = SLAVE_SEND;
queueInit(GetRs485QueuePointer());
break;
}
case SLAVE_SEND:
Rs485Send(&gs_sendFrmae);
gs_ModbusSlaveState = SLAVE_END;
break;
case SLAVE_ERROR:
{
queueInit(pQueue);
gs_ModbusSlaveState = SLAVE_IDLE;
break;
}
case SLAVE_END:
gs_ModbusSlaveState = SLAVE_IDLE;
break;
default:
break;
}
}
/***************************************************************************************
* @brief modbusRTU帧格式解析
* @param pQueue 队列指针
* @retval 0:解析成功 1:从机地址错误 2:功能码错误 3:CRC校验错误
* @note
***************************************************************************************/
void Date_Parse(uint8_t* recvBuff)
{
switch(recvBuff[1])
{
case FUNC_CODE02:
{
//ModbusCmd_Fucn02(recvBuff, SendBuff);
break;
}
case FUNC_CODE03:
{
gs_sendFrmae.dateFrame.slaveAddr = SLAVE_ADDR;
gs_sendFrmae.dateFrame.funcCode = FUNC_CODE03;
gs_sendFrmae.dateFrame.sendNum = ModbusCmd_Fucn03(recvBuff, gs_sendFrmae.dateFrame.sendBuff);
break;
}
case FUNC_CODE04:
{
//ModbusCmd_Fucn04(recvBuff, SendBuff);
break;
}
case FUNC_CODE06:
{
//ModbusCmd_Fucn06(recvBuff, SendBuff);
break;
}
case FUNC_CODE10:
{
//ModbusCmd_Fucn10(recvBuff, SendBuff);
break;
}
default:
break;
}
}
/***************************************************************************************
* @brief modbusRTU帧格式解析
* @param pQueue 队列指针
* @retval 0:解析成功 1:从机地址错误 2:功能码错误 3:CRC校验错误
* @note
***************************************************************************************/
static uint8_t Check_Frame(QUEUE* pQueue)
{
if(pQueue->dateBuffer[0] != SLAVE_ADDR)
{
return 1;
}
if(pQueue->dateBuffer[1] != FUNC_CODE02 &&
pQueue->dateBuffer[1] != FUNC_CODE03 &&
pQueue->dateBuffer[1] != FUNC_CODE04 &&
pQueue->dateBuffer[1] != FUNC_CODE06 &&
pQueue->dateBuffer[1] != FUNC_CODE10 )
{
return 2;
}
if((uint16_t)modbus_crc16(0xFFFF, pQueue->elemNum-2, pQueue->dateBuffer) != (uint16_t)(pQueue->dateBuffer[pQueue->elemNum-1]<<8 | pQueue->dateBuffer[pQueue->elemNum-2]))
{
return 3;
}
return 0;
}
ModbusRTU.h
#ifndef __MODBUSRTU_H__
#define __MODBUSRTU_H__
#include "main.h"
#define SLAVE_ADDR 0x01
#define FRAME_DATE_SIZE 256+6
typedef enum
{
SLAVE_IDLE, //空闲状态
SLAVE_JUDGE, //数据校验
SLAVE_PROCESS, //处理数据
SLAVE_SEND, //回复数据
SLAVE_ERROR, //出错
SLAVE_END,
}Slave_State;
enum
{
FUNC_CODE02 = 0x2,
FUNC_CODE03,
FUNC_CODE04,
FUNC_CODE06 = 0x6,
FUNC_CODE10 = 0x10,
};
typedef union
{
uint8_t dateArr[FRAME_DATE_SIZE];
#pragma pack(1)
struct
{
uint8_t slaveAddr; //从机地址
uint8_t funcCode; //功能码
uint8_t sendBuff[256]; //数据
uint16_t crc; //crc校验码
uint16_t sendNum; //发送数量
}dateFrame;
#pragma pack()
}ModbusSendFrame;
uint16_t modbus_crc16(uint32_t crc_now, uint8_t iLen, uint8_t *crc_begin);
void ModbusProcess(void);
#endif
ModbusRTU_Cmd.c
/****************************************************************************************
* @file ModbusRTU_Cmd.c
* @brief modbus功能码解析函数
* @author lwz
* @date 2025/10/22
* @version V1.0.0
* @note
****************************************************************************************/
/*头文件添加区*/
#include "ModbusRTU_Cmd.h"
#include "ModbusRTU.h"
/*宏定义定义区*/
#define HOLDING_REGISTERS_START_ADDR 100
#define HOLDING_REGISTERS_END_ADDR 300
/*全局变量定义区*/
/*函数声明区*/
uint16_t GetHoldingRegVal(uint16_t regAddr);
/*函数定义区*/
/***************************************************************************************
* @brief 03功能码解析函数
* @param recvBuff 接收数据指针 sendBuff 发送数据指针
* @retval i 返回返送数据字节数
* @note 解析接收数据,填充发送数组
***************************************************************************************/
uint16_t ModbusCmd_Fucn03(uint8_t* recvBuff, uint8_t* sendBuff)
{
uint16_t startRegNum = recvBuff[2]<<8 | recvBuff[3];
uint16_t regNum = recvBuff[4]<<8 | recvBuff[5];
uint16_t i = 0, crc = 0;
if(startRegNum < HOLDING_REGISTERS_START_ADDR || regNum > (HOLDING_REGISTERS_END_ADDR-HOLDING_REGISTERS_START_ADDR))
{
sendBuff[i++] = SLAVE_ADDR;
sendBuff[i++] = 0x83;
return i;
}
else
{
sendBuff[i++] = SLAVE_ADDR;
sendBuff[i++] = 0x3;
sendBuff[i++] = (uint8_t)(regNum*2);
}
for(uint8_t j=0; j<regNum; j++)
{
uint16_t retVal = 0;
retVal = GetHoldingRegVal(j);
sendBuff[i++] = (uint8_t)(retVal>>8);
sendBuff[i++] = (uint8_t)(retVal);
}
crc = modbus_crc16(0xFFFF, i, sendBuff);
sendBuff[i++] = (uint8_t)(crc>>8);
sendBuff[i++] = (uint8_t)(crc);
return i;
}
/***************************************************************************************
* @brief 保持寄存器数据读取
* @param regAddr 保持寄存器地址
* @retval ret 返回对应地址的数据
* @note None
***************************************************************************************/
uint16_t GetHoldingRegVal(uint16_t regAddr)
{
uint16_t ret = 0;
switch(regAddr)
{
case 100:
ret = 0x1;
break;
case 101:
ret = 0x2;
break;
default:
ret = 0;
break;
}
return ret;
}
总结
以上就是基于CubeMX+STM32+循环队列实现解析RS485 Modbus从机协议,经过实验,以100ms的频率查询数据,是完全没有问题的。可提供具体源码,有需要评论即可。

2916

被折叠的 条评论
为什么被折叠?



