使用STM32CubeMX移植FreeModbus到STM32G474RE方法

这几年在单片机的开发中经常用到FreeModbus,由于每个项目开发间隔时间长,选用的芯片型号不一样,故成贴记录本次利用STM32CubeMX与Keil2个工具移植的过程,减少下次移植的苦恼,以下内容从广大网友取经而来,如涉及侵权,敬请告知,立即整改。

/*更新纪要*/

/*20250322:将FreeModbus原CRC校验程序查表法改为STM32硬件CRC实现,在保证效率的前提下减少代码大小,详见:六、性能提升*/

/*20250401:在port.h中定义使用的串口为MB_UART为&hlpuart1和定时器MB_TIM为&htim7,portserial.c与porttimer.c文件中使用MB_UART与MB_TIM,方便在移植到其他芯片*/

一、STM32CubeMX的配置

开发时先实现FreeModbus的移植,后续再增加项目所需的其他功能,这里用了TIM7(这个定时器功能不能对外输出脉冲,正好用来给FreeModbus提供50uS的时钟)和LPUART1(开发使用的是NUCLEO-G474RE,MCU与调试器的虚拟串口连接的串口是LPUART1)。

1.系统配置

首先选择芯片,配置SYS模式和时钟树,选用Debug模式为Serial Wire,并时钟频率拉满,APB1频率为170MHz,对应2个外设TIM7和LPUART1的时钟也是170MHz在,这里需要记录一下,一会配置定时器还需要用来计算定时器周期。

2.定时器配置

 在mbrtu.c文件的eMBRTUInit函数里具体说明了串口波特率和定时器设置的关系:

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

    ( void )ucSlaveAddress;
    ENTER_CRITICAL_SECTION(  );

    /* Modbus RTU uses 8 Databits. */
    if( xMBPortSerialInit( ucPort, ulBaudRate, 8, eParity ) != TRUE )
    {
        eStatus = MB_EPORTERR;
    }
    else
    {
        /* If baudrate > 19200 then we should use the fixed timer values
         * t35 = 1750us. Otherwise t35 must be 3.5 times the character time.
         */
        if( ulBaudRate > 19200 )
        {
            usTimerT35_50us = 35;       /* 1800us. */
        }
        else
        {
            /* The timer reload value for a character is given by:
             *
             * ChTimeValue = Ticks_per_1s / ( Baudrate / 11 )
             *             = 11 * Ticks_per_1s / Baudrate
             *             = 220000 / Baudrate
             * The reload for t3.5 is 1.5 times this value and similary
             * for t3.5.
             */
            usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );
        }
        if( xMBPortTimersInit( ( USHORT ) usTimerT35_50us ) != TRUE )
        {
            eStatus = MB_EPORTERR;
        }
    }
    EXIT_CRITICAL_SECTION(  );

    return eStatus;
}

从上面代码的注释中可以看出,当波特率大于19200时,超时时间为1750us,当波特率小于19200时,超时时间为3.5个字符时间,项目使用115200的波特率,所以超时时间定为1750uS使用以下配置:

(1)进入TIM7配置界面,先得到50us的预分频(20kHz),即将预分频寄存器(PSC)设置为8500-1,计算方法如下:

(2)再配置计数周期为1750uS,即将自动重装载寄存器(AutoReload Register)设置为35,计算方法如下:

(3)自动重载预装载功能可以打开,如果后续串口波特率小于115200时需要用到。

  • 启用自动重载预装载:要是开启了自动重载预装载功能,写入到 ARR 寄存器的值不会马上生效,而是先存于预装载寄存器。直到更新事件发生时,预装载寄存器的值才会被加载到影子寄存器,从而影响后续的计数操作。
  • 禁用自动重载预装载:当禁用自动重载预装载功能时,写入到 ARR 寄存器的值会立刻被传送到影子寄存器,马上对计数操作产生影响。

(4)使能TIM7中断

3.LPUART1配置

(1)将通信模式配置为异步通信,参数为波特率115200,数据位8,检验NONE,停止位1

(2)使能LPUART中断

4.中断配置

TIM7的中断优先级必须比LPUART1低

5.项目参数配置

将代码生成规则设置成:将外设初始化为每个外设的一对 “. c/.h” 文件,然后生成代码。这样做方便后续进行移植改动初始化程序,不需要在main.c中有大的改动。

二、FreeModus源文件的添加

1.下载代码包

代码来于embedded-experts的官网(embedded-experts),该网页其中给出了源码的github仓库地址:https://github.com/cwalter-at/freemodbus,这里为方便大家下载我复制了一份在我的GitCode(FreeModbus_v1.6)。

2.复制相关代码文件到项目文件

(1)代码包文件树如下:

|-- Changelog.txt
|-- bsd.txt
|-- demo(案例,相关功能码的实现不完全,移植时复制一个空白的案例即BARE文件夹)
|-- doc 说明文档
|-- gpl.txt
|-- lgpl.txt
|-- modbus (存放源代码,需要复制的文件夹)
|   |-- ascii(Modbus-ASCII协议源码)
|   |-- functions (功能码对应的处理函数)
|   |-- include(头文件)
|   |-- mb.c(MODBUS协议栈的主文件框架,与具体的协议无关)
|   |-- rtu(Modbus-RTU协议源码)
|   `-- tcp(Modbus-TCP协议源码)
`-- tools

(2)确定项目使用的协议类型

Modbus的协议包含以下3种:

  • Modbus-RTU
  • Modbus-ASCII
  • Modbus-TCP
  • 本项目只使用了Modbus-RTU,后续根据协议类型修改相关代码。

(3)复制相关代码文件到项目文件中,并在Keil中添加,为方便识别将demo.c文件改名为STM32_FreeModus.c。

  

  

(4)重新设置头文件路径,将代码包的头文件包含进去。

  

三、代码的修改

修改内容及原因在代码中进行注释。

1.修改STM32_FreeModbus.c文件(原demo.c文件)

将main函数名改为slave,防止与项目中main.c文件里的main函数重复;

int
main( void )
{
    eMBErrorCode    eStatus;

    eStatus = eMBInit( MB_RTU, 0x0A, 0, 38400, MB_PAR_EVEN );

    /* Enable the Modbus Protocol Stack. */
    eStatus = eMBEnable(  );

    for( ;; )
    {
        ( void )eMBPoll(  );

        /* Here we simply count the number of poll cycles. */
        usRegInputBuf[0]++;
    }
}

/*该main函数与main.c里面的main函数重新,需要修改成下面代码
下面slave函数,在实际代码中并不运行,可直接删除。
*/
int
slave( void )
{
    eMBErrorCode    eStatus;

    eStatus = eMBInit( MB_RTU, 0x0A, 0, 38400, MB_PAR_EVEN );

    /* Enable the Modbus Protocol Stack. */
    eStatus = eMBEnable(  );

    for( ;; )
    {
        ( void )eMBPoll(  );

        /* Here we simply count the number of poll cycles. */
        usRegInputBuf[0]++;
    }
}

2.修改mbconfig.h文件

/*! \brief If Modbus ASCII support is enabled. */
#define MB_ASCII_ENABLED                        (  1 )

/*! \brief If Modbus RTU support is enabled. */
#define MB_RTU_ENABLED                          (  1 )

/*! \brief If Modbus TCP support is enabled. */
#define MB_TCP_ENABLED                          (  0 )

/*取消不用的功能,源代码默认支持Modbus ASCII和Modbus RTU功能
  这里我们只需要用到Modbus RTU功能,修改成下面代码
*/

/*! \brief If Modbus ASCII support is enabled. */
#define MB_ASCII_ENABLED                        (  0 )

/*! \brief If Modbus RTU support is enabled. */
#define MB_RTU_ENABLED                          (  1 )

/*! \brief If Modbus TCP support is enabled. */
#define MB_TCP_ENABLED                          (  0 )

编译不报错,表示工程建立完成,可进行下一步修改。

  

3.修改port文件夹里的代码

port文件夹里的C文件需要我们根据不同平台进行修改

(1)port.h文件

#include <assert.h>
#include <inttypes.h>

#include "usart.h"  //添加串口相关头文件
#include "tim.h"    //添加定时器相关头文件
#include "crc.h"    //添加CRC相关头文件,使用硬件CRC需要用到,不使用不需要添加

#define MB_UART &huart2   //定义使用的串口端口
#define MB_TIM &htim7   //定义使用的定时器

#define	INLINE                      inline
#define PR_BEGIN_EXTERN_C           extern "C" {
#define	PR_END_EXTERN_C             }

#define ENTER_CRITICAL_SECTION( )   __set_PRIMASK(1) 	 //关总中断
#define EXIT_CRITICAL_SECTION( )    __set_PRIMASK(0)     //开总中断

(2)portserial.c文件

/*
 * 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"

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

/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
    /* If xRXEnable enable serial receive interrupts. If xTxENable enable
     * transmitter empty interrupts.
     */
    if (xRxEnable)															//根据输入参数,使能串口接收(xRxEnable)和发送(xTxEnable),中断模式,项目使用LPUART1,故为&hlpuart1
    {
        __HAL_UART_ENABLE_IT(MB_UART, UART_IT_RXNE);	
    }
    else
    {
        __HAL_UART_DISABLE_IT(MB_UART, UART_IT_RXNE);
    }

    if (xTxEnable)
    {
        __HAL_UART_ENABLE_IT(MB_UART, UART_IT_TXE);
    }
    else
    {
        __HAL_UART_DISABLE_IT(MB_UART, UART_IT_TXE);
    }
}

BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
    return TRUE;//串口初始化在usart.c定义,在mian函数中完成,改为TURE,后续再修改成用该函数初始化,方便修改串口通信参数
}

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. */
    if(HAL_UART_Transmit (MB_UART, (uint8_t *)&ucByte, 1, 0x01) != HAL_OK )	//添加发送一个字符
        return FALSE ;
    else
        return TRUE;
}

BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
    /* Return the byte in the UARTs receive buffer. This function is called
     * by the protocol stack after pxMBFrameCBByteReceived( ) has been called.
     */
    if(HAL_UART_Receive (MB_UART, (uint8_t *)pucByte, 1, 0x01) != HAL_OK ) //添加接收一位字符
        return FALSE ;
    else
        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用于修饰全局变量和函数时,它们的作用域会被限制在定义它们的源文件内,其他源文件无法访问这些全局变量和函数。这有助于避免命名冲突,提高代码的模块化程度。为方便在中断函数中调用这里取消掉。文件开头的声明也需要注释掉,不然编译还是不通过。*/
//static 
void prvvUARTTxReadyISR( void )
{
    pxMBFrameCBTransmitterEmpty(  );
}

/* Create an interrupt handler for the receive interrupt for your target
 * processor. This function should then call pxMBFrameCBByteReceived( ). The
 * protocol stack will then call xMBPortSerialGetByte( ) to retrieve the
 * character.
 */
/*当static用于修饰全局变量和函数时,它们的作用域会被限制在定义它们的源文件内,其他源文件无法访问这些全局变量和函数。这有助于避免命名冲突,提高代码的模块化程度。为方便在中断函数中调用这里取消掉。文件开头的声明也需要注释掉,不然编译还是不通过。*/
//static 
void prvvUARTRxISR( void )
{
    pxMBFrameCBByteReceived(  );
}

(3)porttimer.c文件

/*
 * 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$
 */

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

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

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

/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
    return TRUE;  //串口初始化在usart.c定义,在mian函数中完成,改为TURE,后续再修改成用该函数初始化,方便修改串口通信参数
}


inline void
vMBPortTimersEnable(  )
{
    /* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
    /*使能定时器7中断模式(更新中断),项目使用TIM7,故为&htim7*/
    __HAL_TIM_CLEAR_IT(MB_TIM, TIM_IT_UPDATE); //清除定时器中断标志位
    __HAL_TIM_ENABLE_IT(MB_TIM, TIM_IT_UPDATE); //使能定时器更新中断
    __HAL_TIM_SET_COUNTER(MB_TIM, 0); //定时器计数器置0
    __HAL_TIM_ENABLE(MB_TIM);//使能定时器
}

inline void
vMBPortTimersDisable(  )
{
    /* Disable any pending timers. */
    /*关闭定时器7*/
    __HAL_TIM_DISABLE(MB_TIM);
    __HAL_TIM_SET_COUNTER(MB_TIM, 0);
    __HAL_TIM_DISABLE_IT(MB_TIM, TIM_IT_UPDATE);
    __HAL_TIM_CLEAR_IT(MB_TIM, TIM_IT_UPDATE);
}

/* Create an ISR which is called whenever the timer has expired. This function
 * must then call pxMBPortCBTimerExpired( ) to notify the protocol stack that
 * the timer has expired.
 */
/*当static用于修饰全局变量和函数时,它们的作用域会被限制在定义它们的源文件内,其他源文件无法访问这些全局变量和函数。这有助于避免命名冲突,提高代码的模块化程度。为方便在中断函数中调用这里取消掉。文件开头的声明也需要注释掉,不然编译还是不通过。*/
//static
void prvvTIMERExpiredISR( void )
{
    ( void )pxMBPortCBTimerExpired(  );
}

4.修改中断文件(stm32g4xx_it.c)

(1)添加三个函数声明,引用自portserial.c和porttimer.c

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN PFP */
/*将中断函数需要引用的三个文件进行声明*/
extern void prvvUARTTxReadyISR(void);
extern void prvvUARTRxISR(void);
extern void prvvTIMERExpiredISR( void );
/* USER CODE END PFP */

(2)修改串口中断函数,将相关流程连接至modbus中相关中断处理函数

/**
  * @brief This function handles LPUART1 global interrupt.
  */
void LPUART1_IRQHandler(void)
{
  /* USER CODE BEGIN LPUART1_IRQn 0 */

  /* USER CODE END LPUART1_IRQn 0 */
  HAL_UART_IRQHandler(&hlpuart1);
  /* USER CODE BEGIN LPUART1_IRQn 1 */
	if(__HAL_UART_GET_IT_SOURCE(&hlpuart1, UART_IT_RXNE)!= RESET) 
		{
			prvvUARTRxISR();//接收中断
		}

	if(__HAL_UART_GET_IT_SOURCE(&hlpuart1, UART_IT_TXE)!= RESET) 
		{
			prvvUARTTxReadyISR();//发送中断
		}
	
  HAL_NVIC_ClearPendingIRQ(LPUART1_IRQn);//清除LPUART1中断的挂起状态
  /* USER CODE END LPUART1_IRQn 1 */
}

(3)修改定时器中断函数,将相关流程连接至modbus中相关中断处理函数

void TIM7_DAC_IRQHandler(void)
{
    /* USER CODE BEGIN TIM7_DAC_IRQn 0 */

    /* USER CODE END TIM7_DAC_IRQn 0 */
    HAL_TIM_IRQHandler(&htim7);

    /* USER CODE BEGIN TIM7_DAC_IRQn 1 */
    if(__HAL_TIM_GET_FLAG(&htim7, TIM_FLAG_UPDATE))			// 更新中断标记被置位
    {
        __HAL_TIM_CLEAR_FLAG(&htim7, TIM_FLAG_UPDATE);		// 清除中断标记
        prvvTIMERExpiredISR();								// 通知modbus3.5个字符等待时间到
    }

    /* USER CODE END TIM7_DAC_IRQn 1 */
}

5.功能码处理程序

该代码参考了网上大神的作品,时间太过久远,忘记是参考谁的了,广大网友如有印象,烦请告知,我将在文中进行署名。

/*
 * FreeModbus Libary: BARE Demo Application
 * Copyright (C) 2006 Christian Walter <wolti@sil.at>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * File: $Id$
 */

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

/* ----------------------- Defines ------------------------------------------*/
//输入寄存器起始地址
#define REG_INPUT_START       0x0001
//输入寄存器数量
#define REG_INPUT_NREGS       10
//保持寄存器起始地址
#define REG_HOLDING_START     0x0001
//保持寄存器数量
#define REG_HOLDING_NREGS     10

//线圈起始地址
#define REG_COILS_START       0x0001
//线圈数量
#define REG_COILS_SIZE        16

//开关寄存器起始地址
#define REG_DISCRETE_START    0x0001
//开关寄存器数量
#define REG_DISCRETE_SIZE     16

/* ----------------------- Static variables ---------------------------------*/
//输入寄存器内容
uint16_t usRegInputBuf[REG_INPUT_NREGS] = {0};
//输入寄存器起始地址
uint16_t usRegInputStart = REG_INPUT_START;

//保持寄存器内容
uint16_t usRegHoldingBuf[REG_HOLDING_NREGS] = {1,2,3,4,5,6,7,8,9,10};
//保持寄存器起始地址
uint16_t usRegHoldingStart = REG_HOLDING_START;

//线圈状态
uint8_t ucRegCoilsBuf[REG_COILS_SIZE / 8] = {0};
//开关输入状态
uint8_t ucRegDiscreteBuf[REG_DISCRETE_SIZE / 8] = {0};

/* ----------------------- Start implementation -----------------------------*/
int
slave( void )
{
    eMBErrorCode    eStatus;

    eStatus = eMBInit( MB_RTU, 0x0A, 0, 38400, MB_PAR_EVEN );

    /* Enable the Modbus Protocol Stack. */
    eStatus = eMBEnable(  );

    for( ;; )
    {
        ( void )eMBPoll(  );

        /* Here we simply count the number of poll cycles. */
        usRegInputBuf[0]++;
    }
}

/****************************************************************************
* 名	  称:eMBRegInputCB
* 功    能:读取输入寄存器,对应功能码是 04 eMBFuncReadInputRegister
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
*						usAddress: 寄存器地址
*						usNRegs: 要读取的寄存器个数
* 出口参数:
* 注	  意:上位机发来的 帧格式是: SlaveAddr(1 Byte)+FuncCode(1 Byte)
*								+StartAddrHiByte(1 Byte)+StartAddrLoByte(1 Byte)
*								+LenAddrHiByte(1 Byte)+LenAddrLoByte(1 Byte)+
*								+CRCAddrHiByte(1 Byte)+CRCAddrLoByte(1 Byte)
*							3 区
****************************************************************************/
eMBErrorCode
eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{
    eMBErrorCode    eStatus = MB_ENOERR;
    int             iRegIndex;

    if( ( usAddress >= REG_INPUT_START )
            && ( usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS ) )
    {
        iRegIndex = ( int )( usAddress - usRegInputStart );

        while( usNRegs > 0 )
        {
            *pucRegBuffer++ = ( UCHAR )( usRegInputBuf[iRegIndex] >> 8 );
            *pucRegBuffer++ = ( UCHAR )( usRegInputBuf[iRegIndex] & 0xFF );
            iRegIndex++;
            usNRegs--;
        }
    }
    else
    {
        eStatus = MB_ENOREG;
    }

    return eStatus;
}

/****************************************************************************
* 名	  称:eMBRegHoldingCB
* 功    能:对应功能码有:06 写保持寄存器 eMBFuncWriteHoldingRegister
*													16 写多个保持寄存器 eMBFuncWriteMultipleHoldingRegister
*													03 读保持寄存器 eMBFuncReadHoldingRegister
*													23 读写多个保持寄存器 eMBFuncReadWriteMultipleHoldingRegister
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
*						usAddress: 寄存器地址
*						usNRegs: 要读写的寄存器个数
*						eMode: 功能码
* 出口参数:
* 注	  意:4 区
****************************************************************************/
eMBErrorCode
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
{
    eMBErrorCode    eStatus = MB_ENOERR;
    int             iRegIndex;

    if((usAddress >= REG_HOLDING_START) && \
            ((usAddress + usNRegs) <= (REG_HOLDING_START + REG_HOLDING_NREGS)))
    {
        iRegIndex = (int)(usAddress - usRegHoldingStart );

        switch(eMode)
        {
            case MB_REG_READ://读 MB_REG_READ = 0
                while(usNRegs > 0)
                {
                    *pucRegBuffer++ = (uint8_t)(usRegHoldingBuf[iRegIndex] >> 8);
                    *pucRegBuffer++ = (uint8_t)(usRegHoldingBuf[iRegIndex] & 0xFF);
                    iRegIndex++;
                    usNRegs--;
                }

                break;

            case MB_REG_WRITE://写 MB_REG_WRITE = 0
                while(usNRegs > 0)
                {
                    usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
                    usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
                    iRegIndex++;
                    usNRegs--;
                }
        }

    }
    else//错误
    {
        eStatus = MB_ENOREG;
    }

    return eStatus;
}

extern void xMBUtilSetBits( UCHAR * ucByteBuf, USHORT usBitOffset, UCHAR ucNBits,
                            UCHAR ucValue );
extern UCHAR xMBUtilGetBits( UCHAR * ucByteBuf, USHORT usBitOffset, UCHAR ucNBits );
/****************************************************************************
* 名	  称:eMBRegCoilsCB
* 功    能:对应功能码有:01 读线圈 eMBFuncReadCoils
*													05 写线圈 eMBFuncWriteCoil
*													15 写多个线圈 eMBFuncWriteMultipleCoils
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
*						usAddress: 线圈地址
*						usNCoils: 要读写的线圈个数
*						eMode: 功能码
* 出口参数:
* 注	  意:如继电器
*						0 区
****************************************************************************/
eMBErrorCode
eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils,
               eMBRegisterMode eMode )
{
    //错误状态
    eMBErrorCode eStatus = MB_ENOERR;
    //寄存器个数
    int16_t iNCoils = ( int16_t )usNCoils;
    //寄存器偏移量
    int16_t usBitOffset;

    //检查寄存器是否在指定范围内
    if( ( (int16_t)usAddress >= REG_COILS_START ) &&
            ( usAddress + usNCoils <= REG_COILS_START + REG_COILS_SIZE ) )
    {
        //计算寄存器偏移量
        usBitOffset = ( int16_t )( usAddress - REG_COILS_START );

        switch ( eMode )
        {
            //读操作
            case MB_REG_READ:
                while( iNCoils > 0 )
                {
                    *pucRegBuffer++ = xMBUtilGetBits( ucRegCoilsBuf, usBitOffset,
                                                      ( uint8_t )( iNCoils > 8 ? 8 : iNCoils ) );
                    iNCoils -= 8;
                    usBitOffset += 8;
                }

                break;

            //写操作
            case MB_REG_WRITE:
                while( iNCoils > 0 )
                {
                    xMBUtilSetBits( ucRegCoilsBuf, usBitOffset,
                                    ( uint8_t )( iNCoils > 8 ? 8 : iNCoils ),
                                    *pucRegBuffer++ );
                    iNCoils -= 8;
                }

                break;
        }

    }
    else
    {
        eStatus = MB_ENOREG;
    }

    return eStatus;
}

/****************************************************************************
* 名	  称:eMBRegDiscreteCB
* 功    能:读取离散寄存器,对应功能码有:02 读离散寄存器 eMBFuncReadDiscreteInputs
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
*						usAddress: 寄存器地址
*						usNDiscrete: 要读取的寄存器个数
* 出口参数:
* 注	  意:1 区
****************************************************************************/
eMBErrorCode
eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{
    //错误状态
    eMBErrorCode eStatus = MB_ENOERR;
    //操作寄存器个数
    int16_t iNDiscrete = ( int16_t )usNDiscrete;
    //偏移量
    uint16_t usBitOffset;

    //判断寄存器时候再制定范围内
    if( ( (int16_t)usAddress >= REG_DISCRETE_START ) &&
            ( usAddress + usNDiscrete <= REG_DISCRETE_START + REG_DISCRETE_SIZE ) )
    {
        //获得偏移量
        usBitOffset = ( uint16_t )( usAddress - REG_DISCRETE_START );

        while( iNDiscrete > 0 )
        {
            *pucRegBuffer++ = xMBUtilGetBits( ucRegDiscreteBuf, usBitOffset,
                                              ( uint8_t)( iNDiscrete > 8 ? 8 : iNDiscrete ) );
            iNDiscrete -= 8;
            usBitOffset += 8;
        }

    }
    else
    {
        eStatus = MB_ENOREG;
    }

    return eStatus;
}

6.启动modbus

(1)在main.c文件中添加下面2个头文件

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "mb.h"
#include "mbport.h"
/* USER CODE END Includes */

(2)初始化及使能modbus,并在主循环中开始侦听

    /* USER CODE BEGIN 2 */
    eMBInit( MB_RTU, 0x01, 1, 115200, MB_PAR_NONE);//初始化modbus,走modbusRTU,从站地址为0x01,端口为1。
    eMBEnable(  );//使能modbus
    /* USER CODE END 2 */

    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1)
    {
			( void )eMBPoll(  );//启动modbus侦听
        /* USER CODE END WHILE */

        /* USER CODE BEGIN 3 */
    }

    /* USER CODE END 3 */

四、编译项目

编译成功,下载到单片机中

五、系统调试

调试使用的是Modbus Poll工具,不过需要激活使用,这里试用了一下,一款很不错的软件。

1.设置好通信参数及端口

需要设置端口、波特率、数据位、奇偶校验、停止位、模式等。

2.定义读写要求

需要设置从站地址,功能码(测试功能码03),寄存器起始地址,寄存器数量,扫描速率等。

3.通信结果

(1)使用03功能码,正常读取出保存寄存器数据。

(2)加快扫描速率进行测试,没有报错

4.modbus相关功能码

功能码名称描述
1读取线圈状态读取一个或多个线圈(离散输出)的状态,返回值为 0 或 1,表示线圈的断开或闭合
2读取离散输入状态读取一个或多个离散输入的状态,返回值为 0 或 1,表示输入的断开或闭合
3读取保持寄存器读取一个或多个保持寄存器的值,用于获取设备的配置信息、测量值等
4读取输入寄存器读取一个或多个输入寄存器的值,通常用于获取只读的测量值或状态信息
5写单个线圈将单个线圈设置为指定的状态(0 或 1),用于控制设备的输出
6写单个寄存器将单个保持寄存器设置为指定的值,用于配置设备的参数
15写多个线圈同时将多个线圈设置为指定的状态,通过一次操作控制多个输出
16写多个寄存器同时将多个保持寄存器设置为指定的值,用于批量

六、优化改进

1.使用STM32硬件CRC实现MODBUS-CRC校验

采用STM32硬件CRC进行校验,所有的设置选项均要保持一致才能完成MODBUS-CRC校验。

关于该部分内容可参考老李的森林-全栈工程师的文章嵌入式开发--STM32软件和硬件CRC的使用_stm32 crc-CSDN博客文章浏览阅读1.5w次,点赞20次,收藏129次。STM32硬件CRC的使用STM32硬件的CRC不占用MCU的计算资源,和软件查表计算消耗的存储空间。但其结果与平常使用的CRC不一样,导致很多人还是在用软件计算CRC。其实结果的差别,只是由于计算方式导致的,调整计算方式以后也可以输出普通计算的结果。异同与普通计算方式相同,以CRC32举例,其默认多项式也是X32 + X26 + X23 + X22 + X16 + X12 + X11 + X10 +X8 + X7 + X5 + X4 + X2+ X +1,但也可以自定义。计算的初值不同:STM_stm32 crc https://blog.csdn.net/13011803189/article/details/122366072?fromshare=blogdetail&sharetype=blogdetail&sharerId=122366072&sharerefer=PC&sharesource=xcs101&sharefrom=from_link

如上图完成CRC设置后,找到mbcrc.c,将usMBCRC16( UCHAR * pucFrame, USHORT usLen )修改如下,其余内容删除或注释掉即可。

USHORT
usMBCRC16( UCHAR * pucFrame, USHORT usLen )
{
  /*改用STM32硬件CRC进行校验*/
  return ( USHORT )(HAL_CRC_Calculate(&hcrc, (ULONG*)pucFrame, usLen));
}

此外,在port.h文件中添加,编译成功后即可进行测试。

#include "crc.h"    //添加CRC相关头文件

2.程序轻量化裁剪

为满足小内存芯片使用,可根据工作场景需要对程序进行裁剪,以下是对相关的文件进行修改

(1)修改mbconfig.h中的定义,取消不需要的通信模式和功能码定义,比如这里我只启用了启用 Modbus RTU 通信模式和11、03、06等3个功能码

```c

#ifndef _MB_CONFIG_H
#define _MB_CONFIG_H

#ifdef __cplusplus
    PR_BEGIN_EXTERN_C
#endif
/* ----------------------- 宏定义 ------------------------------------------*/
/*! \defgroup modbus_cfg Modbus 协议栈配置分组
 *
 * 协议栈绝大部分功能模块均可按需裁剪关闭,
 * 当单片机资源紧张、需要节省程序Flash空间时该特性尤为实用。<br>
 *
 * 本文件 mbconfig.h 包含全部协议栈功能开关配置项。
 */
/*! \addtogroup modbus_cfg
 *  @{
 */
/*! \brief 是否启用 Modbus ASCII 通信模式 */
#define MB_ASCII_ENABLED (0)

/*! \brief 是否启用 Modbus RTU 通信模式 */
#define MB_RTU_ENABLED (1)

/*! \brief 是否启用 Modbus TCP 以太网通信模式 */
#define MB_TCP_ENABLED (0)

/*! \brief Modbus ASCII 模式字符间隔超时时间(单位:秒)
 *
 * ASCII 协议没有固定的帧间隔超时,因此通过该宏自定义配置;
 * 建议设置为现场网络通信预估最大延迟时长。
 */
#define MB_ASCII_TIMEOUT_SEC (1)

/*! \brief ASCII 模式开启串口发送前的等待延时
 *
 * 若定义该宏,协议栈会调用底层延时函数 vMBPortSerialDelay,
 * 在打开串口发送引脚前插入一段延时。
 * 部分单片机运行速度极快,接收完报文会立刻回复应答,
 * 若主机串口接收开启速度较慢,会丢失应答帧,因此需要该延时缓冲。
 */
#ifndef MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS
#define MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS (0)
#endif

/*! \brief 协议栈支持的最大Modbus功能码处理数量
 *
 * 该数值必须大于当前开启的所有内置功能码 + 自定义功能码总数;
 * 数值设置过小会导致新增功能码注册失败。
 */
#define MB_FUNC_HANDLERS_MAX (3)

/*! \brief “读取从站标识”功能的缓存分配字节长度
 *
 * 该宏限制从站自定义标识信息的最大存储长度;
 * 配套接口 eMBSetSlaveID() 配置标识内容;
 * 仅当 MB_FUNC_OTHER_REP_SLAVEID_ENABLED 置1时生效。
 */
#define MB_FUNC_OTHER_REP_SLAVEID_BUF (32)

/*! \brief 是否启用 0x11 读取从站标识 功能码 */
#define MB_FUNC_OTHER_REP_SLAVEID_ENABLED (1)

/*! \brief 是否启用 0x04 读输入寄存器 功能码 */
#define MB_FUNC_READ_INPUT_ENABLED (0)

/*! \brief 是否启用 0x03 读保持寄存器 功能码 */
#define MB_FUNC_READ_HOLDING_ENABLED (1)

/*! \brief 是否启用 0x06 写单个保持寄存器 功能码 */
#define MB_FUNC_WRITE_HOLDING_ENABLED (1)

/*! \brief 是否启用 0x10 批量写多个保持寄存器 功能码 */
#define MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED (0)

/*! \brief 是否启用 0x01 读线圈 功能码 */
#define MB_FUNC_READ_COILS_ENABLED (0)

/*! \brief 是否启用 0x05 写单个线圈 功能码 */
#define MB_FUNC_WRITE_COIL_ENABLED (0)

/*! \brief 是否启用 0x0F 批量写多个线圈 功能码 */
#define MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED (0)

/*! \brief 是否启用 0x02 读离散输入 功能码 */
#define MB_FUNC_READ_DISCRETE_INPUTS_ENABLED (0)

/*! \brief 是否启用 0x23 读写多个保持寄存器 功能码 */
#define MB_FUNC_READWRITE_HOLDING_ENABLED (0)

/*! @} */
#ifdef __cplusplus
        PR_END_EXTERN_C
#endif
#endif
```

(2)修改STM32_FreeModbus.c中各类寄存器、线圈的数量,注释掉没有用到的功能码处理函数

课程背景Modbus 协议是工业自动化控制系统中常见的通信协议,协议的全面理解是个痛点。本课程主讲老师集10多年在Modbus协议学习、使用中的经验心得,结合当前物联网浪潮下Modbus协议开发的痛点,推出这套面向Modbus 协议初学者的课程。本课程不同于以往市面课程只是协议讲解无实现代码,而是采用讲解与实践并重的方式,结合STM32F103ZET6开发板进行手把手编程实践,十分有利于初学者学习。涵盖了学习者在Modbus协议方面会遇到的方方面面的问题,是目前全网首个对Modbus协议进行全面总结的课程。课程名称   协议讲解及实现>>课程内容1、Modbus 协议的基础。2、Modbus协议栈函数编程实现。3、Modbus协议在串行链路编程实现。4、Modbus协议在以太网链路编程实现。5、常见问题的解决方法。带给您的价值通过学习本课程,您可以做到如下:1、全面彻底的理解Modbus协议。2、理解在串行链路,以太网链路的实现。3、掌握Modbus协议解析的函数编程方法,调试工具的使用。4、掌握多个串口,网口同时运行同一个Modbus协议栈的方法。5、掌握Modbus协议下,负数,浮点数等处理方法。讲师简介许孝刚,山东大学工程硕士,副高职称,技术总监。10多年丰富嵌入式系统开发经验,国家软考“嵌入式系统设计师”。2017年获得“华为开发者社区杰出贡献者”奖励。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值