基于CubeMX+STM32+循环队列实现解析RS485 Modbus从机协议

文章目录

  • 一、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的频率查询数据,是完全没有问题的。可提供具体源码,有需要评论即可。
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小李学不懂C

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值