在这里插入图片描述

一、写在前面

1.1 文章回顾

该文章对【第一篇、基于CubeMX完成FreeModbus从机移植(无OS,裸机移植)】的所提出的代码弊端进行优化的方法实现

1.2 FreeModbus从机 数据接收优化版例程下载

移植例程下载戳我

   1. 芯片:STM32F103C8T6
   2. 串口引脚:USART1_TX(PB6)USART1_RX(PB7)
   3. RS485控制引脚:DIR1(PB8)
   4. 采用DMA+接收空闲中断的串口数据的接收(中断量最少、效率高) 
   5. 使用STM32CubeMX生成的Hal库程序
   6. 未使用RTOS

1.3 更新

博主精力有限,以下更新可能没有添加到下载资源中,若下载了资源,请自行对照修改!!!

1.3.1 使用DMA接受广播帧时,导致MODBUS异常

问题:由于modbus接收到广播帧以后,不需要回复任何数据,由于DMA状态重置在发送状态进行,因此没有重置DMA,导致异常
解决办法:在eMBErrorCode eMBPoll( void )的EV_EXECUTE状态中增加串口重启函数

BOOL xMBPortSerialRestart(void)
{
    HAL_UART_DMAStop(&SLAVE_PORT);
    vMBPortSerialEnable(TRUE, FALSE);
  return  TRUE;
}

1.3.2 使用DMA接收到异常数据,如不满足modbus任何条件的数据,导致MDOBUS异常

问题: 使用DMA接收到异常数据,如不满足modbus任何条件的数据,导致MDOBUS异常,这个原因是由于eMBErrorCode eMBPoll( void )没有处理帧异常时的措施
解决办法:在eMBErrorCode eMBPoll( void )的EV_FRAME_RECEIVED状态中增加串口重启函数

eMBErrorCode
eMBPoll( void )
{
    static UCHAR   *ucMBFrame;
    static UCHAR    ucRcvAddress;
    static UCHAR    ucFunctionCode;
    static USHORT   usLength;
    static eMBException eException;

    int             i;
    eMBErrorCode    eStatus = MB_ENOERR;
    eMBEventType    eEvent;

    /* Check if the protocol stack is ready. */
    if( eMBState != STATE_ENABLED )
    {
        return MB_EILLSTATE;
    }

    /* Check if there is a event available. If not return control to caller.
     * Otherwise we will handle the event. */
    if( xMBPortEventGet( &eEvent ) == TRUE )
    {
        switch ( eEvent )
        {
        case EV_READY:
            break;

        case EV_FRAME_RECEIVED:
            eStatus = peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength );
            if( eStatus == MB_ENOERR )
            {
                /* Check if the frame is for us. If not ignore the frame. */
                if( ( ucRcvAddress == ucMBAddress ) || ( ucRcvAddress == MB_ADDRESS_BROADCAST ) )
                {
                    ( void )xMBPortEventPost( EV_EXECUTE );
                }
            }
            if(eStatus == MB_EIO)            
            {
              printf("接收到错误帧\n");
              xMBPortSerialRestart();
            }
            break;

        case EV_EXECUTE:
            ucFunctionCode = ucMBFrame[MB_PDU_FUNC_OFF];
            eException = MB_EX_ILLEGAL_FUNCTION;
            for( i = 0; i < MB_FUNC_HANDLERS_MAX; i++ )
            {
                /* No more function handlers registered. Abort. */
                if( xFuncHandlers[i].ucFunctionCode == 0 )
                {
                    break;
                }
                else if( xFuncHandlers[i].ucFunctionCode == ucFunctionCode )
                {
                     eException = xFuncHandlers[i].pxHandler( ucMBFrame, &usLength );
                    break;
                }
            }

            /* If the request was not sent to the broadcast address we
             * return a reply. */
            if( ucRcvAddress != MB_ADDRESS_BROADCAST )
            {
                if( eException != MB_EX_NONE )
                {
                    /* An exception occured. Build an error frame. */
                    usLength = 0;
                    ucMBFrame[usLength++] = ( UCHAR )( ucFunctionCode | MB_FUNC_ERROR );
                    ucMBFrame[usLength++] = eException;
                }
                if( ( eMBCurrentMode == MB_ASCII ) && MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS )
                {
                    vMBPortTimersDelay( MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS );
                }                
                eStatus = peMBFrameSendCur( ucMBAddress, ucMBFrame, usLength );
            }else
            {
              printf("接收广播帧\n");
              xMBPortSerialRestart();
            }
            break;

        case EV_FRAME_SENT:
            break;
        }
    }
    return MB_ENOERR;
}

1.3.3 (20260513更新)使用xMBPortSerialPutByte( CHAR ucByte )进行数据发送时,发生断帧现象

现象描述:当主站对T3.5字符间隔要求严格时,由于原发送方式是单字符中断发送,有时候可能上一个字符与下一个字符的发送间隔会超过T3.5,导致主机认为从机帧发送接收。从而引发失败。
解决办法:在xMBRTUTransmitFSM中修改发送方式为HAL_UART_Transmit

BOOL
xMBRTUTransmitFSM( void )
{
    BOOL            xNeedPoll = FALSE; // 标记是否需要轮询

    switch ( eSndState )
    {
        /* 如果发送器处于空闲状态,我们不应该收到发送器事件。  */
    case STATE_TX_IDLE:
        /* 启用接收器/禁用发送器。 */
        vMBPortSerialEnable( TRUE, FALSE ); // 使能串口接收,禁用串口发送
        break;

    case STATE_TX_XMIT:
        /* 检查我们是否完成发送。 */
        ENTER_CRITICAL_SECTION(  ); // 进入临界区
        HAL_UART_Transmit(&huart1,pucSndBufferCur,usSndBufferCount,0xff);
        usSndBufferCount=0;
        EXIT_CRITICAL_SECTION(  ); // 退出临界区
        xNeedPoll = xMBPortEventPost( EV_FRAME_SENT ); // 发送帧发送完成事件
        vMBPortSerialEnable( TRUE, FALSE ); // 使能串口接收,禁用串口发送
        eSndState = STATE_TX_IDLE; // 设置发送器状态为空闲状态

        break;
    }

    return xNeedPoll; // 返回是否需要轮询的标记
}

二、生成CubeMX工程

在【第一篇、基于CubeMX完成FreeModbus从机移植(无OS,裸机移植)】工程基础上,进行以下改动

1、使能DMA

在这里插入图片描述

2、取消定时器

在这里插入图片描述

三、FreeModbus文件修改

1、portserial.c

1、增加DMA相关代码
2、引入ucRTUBuf和usRcvBufferPos变量
3、新增本机地址变量,用于触发接收空闲中断时,判断地址是否为本机地址,如果不是,直接忽略,

/*
 * FreeModbus Libary: BARE Port
 * Copyright (C) 2006 Christian Walter <wolti@sil.at>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * File: $Id$
 */

#include "port.h"

/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"

/* ----------------------- serial port --------------------------------------*/
extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_rx; // 新增 DMA 句柄
extern DMA_HandleTypeDef hdma_usart1_tx; // 新增发送 DMA 句柄
#define SLAVE_PORT huart1
#define rs485_dir_recv()  HAL_GPIO_WritePin(DIR1_GPIO_Port,DIR1_Pin,GPIO_PIN_RESET)
#define rs485_dir_send() HAL_GPIO_WritePin(DIR1_GPIO_Port,DIR1_Pin,GPIO_PIN_SET)

volatile uint8_t ucLocalSlaveAddress = 1; //本机modbus从地址
#define MB_SER_PDU_SIZE_MAX     256     /*!< Modbus RTU 帧的最大尺寸 */
extern volatile UCHAR  ucRTUBuf[MB_SER_PDU_SIZE_MAX]; // 用于存储接收的 Modbus RTU 数据的缓冲区
extern volatile USHORT usRcvBufferPos; // 接收缓冲区中已存储数据的位置

/* ----------------------- static functions ---------------------------------*/
static void prvvUARTTxReadyISR( void );

/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
    /* If xRXEnable enable serial receive interrupts. If xTxENable enable
     * transmitter empty interrupts.
     */
    if (xRxEnable)
    {
         while(__HAL_UART_GET_FLAG(&SLAVE_PORT,UART_FLAG_TC)!=SET)    //等待发送结束
        {
            /* code */
        }
        rs485_dir_recv();
        if (HAL_UART_Receive_DMA(&SLAVE_PORT, (uint8_t *)ucRTUBuf, MB_SER_PDU_SIZE_MAX) != HAL_OK)
        {
            // 处理错误
        }
        __HAL_UART_ENABLE_IT(&SLAVE_PORT, UART_IT_IDLE); // 使能空闲中断
    }
    else
    {
        rs485_dir_send();
        HAL_UART_DMAStop(&SLAVE_PORT); // 停止 DMA 接收
        __HAL_UART_DISABLE_IT(&SLAVE_PORT, UART_IT_IDLE); // 禁止空闲中断
    }
		
    if (xTxEnable)
    {        
        __HAL_UART_ENABLE_IT(&SLAVE_PORT, UART_IT_TXE);    // 使能发送空中断 
    }
    else
    {
        __HAL_UART_DISABLE_IT(&SLAVE_PORT, UART_IT_TXE);    // 禁止发送空中断 
    }
}

BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
    SLAVE_PORT.Instance = USART1;
    SLAVE_PORT.Init.BaudRate = ulBaudRate;
    SLAVE_PORT.Init.StopBits = UART_STOPBITS_1;
    SLAVE_PORT.Init.Mode = UART_MODE_TX_RX;
    SLAVE_PORT.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    SLAVE_PORT.Init.OverSampling = UART_OVERSAMPLING_16;

    switch(eParity)
    {
    // 奇校验
    case MB_PAR_ODD:
        SLAVE_PORT.Init.Parity = UART_PARITY_ODD;
        SLAVE_PORT.Init.WordLength = UART_WORDLENGTH_9B;            // 带奇偶校验数据位为9bits
        break;

    // 偶校验
    case MB_PAR_EVEN:
        SLAVE_PORT.Init.Parity = UART_PARITY_EVEN;
        SLAVE_PORT.Init.WordLength = UART_WORDLENGTH_9B;            // 带奇偶校验数据位为9bits
        break;

    // 无校验
    default:
        SLAVE_PORT.Init.Parity = UART_PARITY_NONE;
        SLAVE_PORT.Init.WordLength = UART_WORDLENGTH_8B;            // 无奇偶校验数据位为8bits
        break;
    }

    // 初始化 DMA
    SLAVE_PORT.hdmarx = &hdma_usart1_rx;
    hdma_usart1_rx.Instance = DMA1_Channel5; // 根据实际情况修改
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_NORMAL;
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
    if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
    {
        return FALSE;
    }
    __HAL_LINKDMA(&SLAVE_PORT, hdmarx, hdma_usart1_rx);

    return HAL_UART_Init(&SLAVE_PORT) == HAL_OK ? TRUE : FALSE;
}

BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
    /* Put a byte in the UARTs transmit buffer. This function is called
     * by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
     * called. */
    USART1->DR = ucByte;
    return TRUE;
}

/* Create an interrupt handler for the transmit buffer empty interrupt
 * (or an equivalent) for your target processor. This function should then
 * call pxMBFrameCBTransmitterEmpty( ) which tells the protocol stack that
 * a new character can be sent. The protocol stack will then call 
 * xMBPortSerialPutByte( ) to send the character.
 */
static void prvvUARTTxReadyISR( void )
{
    pxMBFrameCBTransmitterEmpty(  );
}

/*
 * Add your serial port interrupt handler
 *
 */
#include <string.h> // 包含 memset 函数所在的头文件

// 定义结构体来记录不同类型中断的次数
typedef struct {
    uint32_t idle_irq_count;  // 空闲中断次数
    uint32_t txe_irq_count;   // 发送空中断次数
    uint32_t rxne_irq_count;  // 接收缓冲区非空中断次数
    uint32_t error_irq_count; // 错误中断次数
} UART1_IRQ_Counters;

// 全局变量,用于存储中断计数
UART1_IRQ_Counters uart1_irq_counters = {0, 0, 0, 0};

// 原有的中断处理函数,添加中断计数逻辑
void USART1_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&SLAVE_PORT, UART_FLAG_IDLE)) // 空闲中断标记被置位
    {
        uart1_irq_counters.idle_irq_count++;                                           // 空闲中断次数加 1
        __HAL_UART_CLEAR_IDLEFLAG(&SLAVE_PORT);                                        // 清除空闲中断标记
        usRcvBufferPos = MB_SER_PDU_SIZE_MAX - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); // 计算接收到的数据长度

        // 将接收到的从机地址存储到指定位置
        if (ucRTUBuf[0] == ucLocalSlaveAddress || ucRTUBuf[0] == MB_ADDRESS_BROADCAST)
        {
            xMBPortEventPost(EV_FRAME_RECEIVED); // 通知协议栈有新帧接收
        }
        else
        {
            // 停止 DMA 接收
            HAL_UART_DMAStop(&SLAVE_PORT);
            vMBPortSerialEnable(TRUE, FALSE);
        }
    }
    else if(__HAL_UART_GET_FLAG(&SLAVE_PORT, UART_FLAG_TXE)) // 发送为空中断标记被置位
    {
        uart1_irq_counters.txe_irq_count++; // 发送空中断次数加 1
        __HAL_UART_CLEAR_FLAG(&SLAVE_PORT, UART_FLAG_TXE); // 清除中断标记
        prvvUARTTxReadyISR(); // 通知modbus数据可以发送
    }
    else if(__HAL_UART_GET_FLAG(&SLAVE_PORT, UART_FLAG_RXNE)) // 接收缓冲区非空中断标记被置位
    {
        uart1_irq_counters.rxne_irq_count++; // 接收缓冲区非空中断次数加 1
        __HAL_UART_CLEAR_FLAG(&SLAVE_PORT, UART_FLAG_RXNE); // 清除中断标记
        // 若有需要可添加接收数据处理逻辑
    }
    else if(__HAL_UART_GET_FLAG(&SLAVE_PORT, UART_FLAG_ORE) || // 溢出错误
            __HAL_UART_GET_FLAG(&SLAVE_PORT, UART_FLAG_NE)  || // 噪声错误
            __HAL_UART_GET_FLAG(&SLAVE_PORT, UART_FLAG_FE)  || // 帧错误
            __HAL_UART_GET_FLAG(&SLAVE_PORT, UART_FLAG_PE))    // 奇偶校验错误
    {
        uart1_irq_counters.error_irq_count++; // 错误中断次数加 1
        __HAL_UART_CLEAR_FLAG(&SLAVE_PORT, UART_FLAG_ORE | UART_FLAG_NE | UART_FLAG_FE | UART_FLAG_PE); // 清除错误中断标记
        // 若有需要可添加错误处理逻辑
    }
}

2、mbrtu.c

删除 eMBRTUStart、eMBRTUStop 和 xMBRTUTimerT35Expired 函数中与定时器相关的代码,同时移除 xMBPortTimersInit、vMBPortTimersEnable 和 vMBPortTimersDisable 函数调用。

/* 
 * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU.
 * Copyright (c) 2006-2018 Christian Walter <cwalter@embedded-solutions.at>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

/* ----------------------- System includes ----------------------------------*/
#include "stdlib.h"
#include "string.h"

/* ----------------------- Platform includes --------------------------------*/
#include "port.h"

/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbrtu.h"
#include "mbframe.h"

#include "mbcrc.h"
#include "mbport.h"

/* ----------------------- Defines ------------------------------------------*/
#define MB_SER_PDU_SIZE_MIN     4       /*!< Modbus RTU 帧的最小尺寸 */
#define MB_SER_PDU_SIZE_MAX     256     /*!< Modbus RTU 帧的最大尺寸 */
#define MB_SER_PDU_SIZE_CRC     2       /*!< PDU 中 CRC 字段的大小 */
#define MB_SER_PDU_ADDR_OFF     0       /*!< 从机地址在串行 PDU 中的偏移量 */
#define MB_SER_PDU_PDU_OFF      1       /*!< Modbus-PDU 在串行 PDU 中的偏移量 */

/* ----------------------- Type definitions ---------------------------------*/
typedef enum
{
    STATE_RX_INIT,              /*!< 接收器处于初始状态 */
    STATE_RX_IDLE,              /*!< 接收器处于空闲状态 */
    STATE_RX_RCV,               /*!< 正在接收帧 */
    STATE_RX_ERROR              /*!< 如果帧无效 */
} eMBRcvState;

typedef enum
{
    STATE_TX_IDLE,              /*!< 发送器处于空闲状态 */
    STATE_TX_XMIT               /*!< 发送器处于传输状态 */
} eMBSndState;

/* ----------------------- Static variables ---------------------------------*/
static volatile eMBSndState eSndState; // 发送器状态,volatile 确保在多线程或中断环境下能正确访问
static volatile eMBRcvState eRcvState; // 接收器状态,volatile 确保在多线程或中断环境下能正确访问

volatile UCHAR  ucRTUBuf[MB_SER_PDU_SIZE_MAX]; // 用于存储接收的 Modbus RTU 数据的缓冲区

static volatile UCHAR *pucSndBufferCur; // 指向发送缓冲区当前位置的指针
static volatile USHORT usSndBufferCount; // 发送缓冲区中剩余要发送的字节数

volatile USHORT usRcvBufferPos; // 接收缓冲区中已存储数据的位置

/* ----------------------- Start implementation -----------------------------*/
eMBErrorCode
eMBRTUInit( UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity )
{
    eMBErrorCode    eStatus = MB_ENOERR;

    ( void )ucSlaveAddress;
    ENTER_CRITICAL_SECTION(  );

    /* Modbus RTU uses 8 Databits. */
    if( xMBPortSerialInit( ucPort, ulBaudRate, 8, eParity ) != TRUE )
    {
        eStatus = MB_EPORTERR;
    }
    else
    {

    }
    EXIT_CRITICAL_SECTION(  );

    return eStatus;
}

void
eMBRTUStart( void )
{
    ENTER_CRITICAL_SECTION(  ); // 进入临界区
    eRcvState = STATE_RX_IDLE; // 设置接收器状态为初始状态
    vMBPortSerialEnable( TRUE, FALSE ); // 使能串口接收,禁用串口发送
    EXIT_CRITICAL_SECTION(  ); // 退出临界区
}

void
eMBRTUStop( void )
{
    ENTER_CRITICAL_SECTION(  ); // 进入临界区
    vMBPortSerialEnable( FALSE, FALSE ); // 禁用串口接收和发送
    EXIT_CRITICAL_SECTION(  ); // 退出临界区
}

eMBErrorCode
eMBRTUReceive( UCHAR * pucRcvAddress, UCHAR ** pucFrame, USHORT * pusLength )
{
    BOOL            xFrameReceived = FALSE; // 标记是否接收到有效帧
    eMBErrorCode    eStatus = MB_ENOERR; // 错误状态码,初始设为无错误

    ENTER_CRITICAL_SECTION(  ); // 进入临界区
    assert( usRcvBufferPos < MB_SER_PDU_SIZE_MAX ); // 断言确保接收缓冲区位置在有效范围内

    /* 长度和 CRC 检查 */
    if( ( usRcvBufferPos >= MB_SER_PDU_SIZE_MIN )
        && ( usMBCRC16( ( UCHAR * ) ucRTUBuf, usRcvBufferPos ) == 0 ) )
    {
        /* 保存地址字段。所有帧都传递到上层,
         * 是否使用该帧的决策在那里进行。
         */
        *pucRcvAddress = ucRTUBuf[MB_SER_PDU_ADDR_OFF]; // 将接收到的从机地址存储到指定位置

        /* Modbus-PDU 的总长度是 Modbus 串行线路 PDU 减去
         * 地址字段大小和 CRC 校验和。
         */
        *pusLength = ( USHORT )( usRcvBufferPos - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_CRC ); // 计算有效数据长度

        /* 将 Modbus PDU 的起始位置返回给调用者。 */
        *pucFrame = ( UCHAR * ) & ucRTUBuf[MB_SER_PDU_PDU_OFF]; // 设置指向有效数据的指针
        xFrameReceived = TRUE; // 标记接收到有效帧
    }
    else
    {
        eStatus = MB_EIO; // 如果帧无效,设置错误状态为输入输出错误
    }

    EXIT_CRITICAL_SECTION(  ); // 退出临界区
    return eStatus; // 返回错误状态码
}

eMBErrorCode
eMBRTUSend( UCHAR ucSlaveAddress, const UCHAR * pucFrame, USHORT usLength )
{
    eMBErrorCode    eStatus = MB_ENOERR; // 错误状态码,初始设为无错误
    USHORT          usCRC16; // 用于存储 CRC16 校验和

    ENTER_CRITICAL_SECTION(  ); // 进入临界区

    /* 检查接收器是否仍处于空闲状态。如果不是,说明我们处理接收到的帧太慢,
     * 并且主机在网络上发送了另一个帧。我们必须中止发送该帧。
     */
    if( eRcvState == STATE_RX_IDLE )
    {
        /* Modbus-PDU 之前的第一个字节是从机地址。 */
        pucSndBufferCur = ( UCHAR * ) pucFrame - 1; // 设置发送缓冲区指针指向从机地址位置
        usSndBufferCount = 1; // 初始化发送缓冲区计数为 1(从机地址占 1 字节)

        /* 现在将 Modbus-PDU 复制到 Modbus 串行线路 PDU 中。 */
        pucSndBufferCur[MB_SER_PDU_ADDR_OFF] = ucSlaveAddress; // 将从机地址存储到发送缓冲区
        usSndBufferCount += usLength; // 更新发送缓冲区计数为从机地址和有效数据长度之和

        /* 计算 Modbus 串行线路 PDU 的 CRC16 校验和。 */
        usCRC16 = usMBCRC16( ( UCHAR * ) pucSndBufferCur, usSndBufferCount ); // 计算 CRC16 校验和
        ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 & 0xFF ); // 将 CRC16 低字节存储到缓冲区
        ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 >> 8 ); // 将 CRC16 高字节存储到缓冲区

        /* 激活发送器。 */
        eSndState = STATE_TX_XMIT; // 设置发送器状态为传输状态
        vMBPortSerialEnable( FALSE, TRUE ); // 禁用串口接收,使能串口发送
    }
    else
    {
        eStatus = MB_EIO; // 如果接收器不处于空闲状态,设置错误状态为输入输出错误
    }
    EXIT_CRITICAL_SECTION(  ); // 退出临界区
    return eStatus; // 返回错误状态码
}

BOOL
xMBRTUReceiveFSM( void )
{
    BOOL            xTaskNeedSwitch = FALSE; // 标记是否需要任务切换
    assert( eSndState == STATE_TX_IDLE ); // 断言确保发送器处于空闲状态

    switch ( eRcvState )
    {
    case STATE_RX_INIT:
         eRcvState = STATE_RX_IDLE; // 直接切换到空闲状态
        break;

        /* 在错误状态下,我们等待直到损坏的帧中的所有字符都被传输。 */
    case STATE_RX_ERROR:
        eRcvState = STATE_RX_IDLE; // 直接切换到空闲状态
        break;

    case STATE_RX_IDLE:
       // 无需处理,等待空闲中断触发接收完成事件
        break;

    case STATE_RX_RCV:
        // 无需处理,等待空闲中断触发接收完成事件
        break;
    }
    return xTaskNeedSwitch; // 返回是否需要任务切换的标记
}

BOOL
xMBRTUTransmitFSM( void )
{
    BOOL            xNeedPoll = FALSE; // 标记是否需要轮询

    assert( eRcvState == STATE_RX_IDLE ); // 断言确保接收器处于空闲状态

    switch ( eSndState )
    {
        /* 如果发送器处于空闲状态,我们不应该收到发送器事件。  */
    case STATE_TX_IDLE:
        /* 启用接收器/禁用发送器。 */
        vMBPortSerialEnable( TRUE, FALSE ); // 使能串口接收,禁用串口发送
        break;

    case STATE_TX_XMIT:
        /* 检查我们是否完成发送。 */
        if( usSndBufferCount != 0 )
        {
            xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur ); // 发送缓冲区中的当前字节
            pucSndBufferCur++;  /* 发送缓冲区中的下一个字节。 */
            usSndBufferCount--; // 减少待发送字节数
        }
        else
        {
            xNeedPoll = xMBPortEventPost( EV_FRAME_SENT ); // 发送帧发送完成事件
            /* 禁用发送器。这可以防止另一个发送缓冲区为空中断。 */
            vMBPortSerialEnable( TRUE, FALSE ); // 使能串口接收,禁用串口发送
            eSndState = STATE_TX_IDLE; // 设置发送器状态为空闲状态
        }
        break;
    }

    return xNeedPoll; // 返回是否需要轮询的标记
}

3、porttimer.c

从工程中删除该代码。或取消编译

四、测试验证

uart1_irq_counters结构体记录了系统进入中断的次数,可以看到每收到一帧数据,系统进入一次串口空闲中断,大大减少了中断次数。但发送空闲中断进入次数较多,后续将继续优化
请添加图片描述

五、代码弊端

程序的发送严重依赖TXE中断,这种办法依旧会产生较多中断次数,如果能使用TC中断和DMA,则仅需进入一次中断即可。最终可以达到总线上收发一帧数据最多引起两次中断的效果,是最高效的串口收发方式。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐