【STM32】FreeModbus 移植Modbus-RTU从机协议到STM32详细过程

背景

FreeModbus是一款开源的Modbus协议栈,但是只有从机开源,主机源码是需要收费的。

第一步:下载源码

  1. 打开.embedded-experts的官网,连接如下:
    https://www.embedded-experts.at/en/freemodbus-downloads/
    其中给出了源码的github仓库地址:https://github.com/cwalter-at/freemodbus.
    在这里插入图片描述
    打开这个仓库将代码克隆到本地即可:
git clone https://github.com/cwalter-at/freemodbus.git

在这里插入图片描述
在Windows上的话就用git bash就行
在这里插入图片描述

如图时得到的freemodbus的源码:在这里插入图片描述

第二步:了解需要移植什么

Modbus的协议包含以下几种:

  • Modbus-RTU
  • Modbus-ASCII
  • Modbus-TCP

以上三种协议,一个设备只会有一种协议,如果你的设备使用的是Modbus-RTU,只需查看以下对应部分,一般来说大部分的设备都是Modbus-RTU协议的。
如图是源码的目录结构:

$ tree -L 2
.
|-- Changelog.txt
|-- bsd.txt
|-- demo 这个文件夹存放的是一些不同平台移植的例子
|-- doc 说明文档
|-- gpl.txt
|-- lgpl.txt
|-- modbus 存放源代码
|   |-- ascii Modbus-ASCII协议源码
|   |-- functions 
|   |-- include
|   |-- mb.c
|   |-- rtu Modbus-RTU协议源码
|   `-- tcp Modbus-TCP协议源码
`-- tools

在这里插入图片描述
因为我们需要移植的是Modbus-RTU,所以需要functions、include、port下的源码文件以及mb.c。其中port下的文件时更具自己使用的平台自己编写的,也就是用来适配移植到不同平台的,所以随便从一个接近目标普通的例子中复制过来修改即可,随便复制一个也可以,反正要自己改的。这里复制的是MSP430下面的例子,因为是这里是要移植无操作系统的情况,这个文件相对简洁一些。
在这里插入图片描述

为了方便展示我们使用了那些文件这里将用到的文件单独复制到一个文件夹,如下图所示为我们需要的文件:

在这里插入图片描述

第三步:根据平台实现对应的接口

如果你想要移植过程中编译运行看看结果可以先看第四步。

  1. 这里移植Modbus-RTU从机协议到无操作系统的STM32平台。

  2. 根据Modbus协议可以知道,移植实现Modbus协议最重要的就是两个东西:时间通信
    3.时间:
    3.1 其中时间是指MODBUS RTU的T3.5T1.5的时序,即在RTU模式,报文由时长至少3.5个字符时间的空间间隔区分,在收到最后一个字节的数据后超过T3.5(3.5个字符间隔时间)的时间没有收到新的数据判定一帧数据接收完成。如下图所示:
    在这里插入图片描述
    3.2 并且整个报文帧必须以连续的字符流发送,如果两个字符之间的空闲大于1.5个字符时间,则报文帧认为不完整,应该被接收点丢弃。
    在这里插入图片描述
    3.3 需要注意的是:RTU接收驱动程序的实现,由于T1.5T3.5的定时,隐含着大量对中断的管理。在高通信速率下,这导致CPU负担家中,因此,在<=19200bps时,这两个定时必须严格遵守;对于>19200bps的情形,应该使用2个定时的固定值,建议字符间的超时时间t1.5750us;帧间超时时间为1.75ms[ref]

  3. 通信
    3.1 通信涉及到的就是串口的发送接收数据,其中发送数据需要处理两个问题:
    一个是获取/释放总线使用权限,因为一般来说我们是使用RS485协议作为物理层协议的,程序里面需要通过一个IO口来控制是否在使用总线(这个不是协议本身是半双工的问题,二十物理层需要有这个总线权限获取释放的操作)。

    另一个问题就是避免串口发送数据的过程对程序造成阻塞,因为例如当使用9600的波特率的时候,发送一帧8个字节的主机请求,使用9600的波特率、8位数据位、1位奇偶校验位、1位停止位那么发送1字节数据就需要11个baud(加上起始位),发送这一帧数据就需要:
    1 s 9600 b a u d / s ∗ 1000 m s ∗ 11 b a u d ∗ 8 ≈ 9.17 m s \frac{1s}{9600baud/s}*1000ms*11baud*8\approx9.17ms 9600baud/s1s1000ms11baud89.17ms
    这显然是不可以接受的。而为了避免阻塞发送其实有很多的方法,例如有点MCU的串口外设自带了一定大小的硬件FIFO,只要单次连续发送的数据量不超过FIFO的大小就不会因为串口IO阻塞程序。或者DMA串口中断等加发送队列的方法都可以避免阻塞问题。FreeModbus使用的就是串口中断等加发送队列的方法来避免串口IO阻塞程序执行的问题的。

下面依次讲解port中各个文件要如何编写对应平台的代码的。

port.h

源码如下,其中值得关注的是ENTER_CRITICAL_SECTION( )和EXIT_CRITICAL_SECTION( ),这两个方法主要是在有RTOS的环境下进入和退出临界区,并且FreeModbus的实现部分使用的是ENTER_CRITICAL_SECTION和EXIT_CRITICAL_SECTION这两个符号。即使是不使用操作系统其实也是需要实现的,因为存在从终端中写入数据然后从主循环中读取的情况,即读写同一资源的情况。


#ifndef _PORT_H
#define _PORT_H

/* ----------------------- Platform includes --------------------------------*/

#include <msp430x16x.h>  //替换对应平台
#if defined (__GNUC__)
#include <signal.h>
#endif
#undef CHAR

/* ----------------------- Defines ------------------------------------------*/
#define	INLINE
#define PR_BEGIN_EXTERN_C           extern "C" {
#define	PR_END_EXTERN_C             }

#define ENTER_CRITICAL_SECTION( )   EnterCriticalSection( )
#define EXIT_CRITICAL_SECTION( )    ExitCriticalSection( )
#define assert( expr )

typedef char    BOOL;

typedef unsigned char UCHAR;

typedef char    CHAR;

typedef unsigned short USHORT;
typedef short   SHORT;

typedef unsigned long ULONG;
typedef long    LONG;

#ifndef TRUE
#define TRUE            1
#endif

#ifndef FALSE
#define FALSE           0
#endif

void            EnterCriticalSection( void );
void            ExitCriticalSection( void );


#endif

根据本文平台修改后:


#ifndef _PORT_H
#define _PORT_H

/* ----------------------- Platform includes --------------------------------*/

#include "main.h"  

/* ----------------------- Defines ------------------------------------------*/
#define	INLINE inline //实际没有用到
#define PR_BEGIN_EXTERN_C           extern "C" {
#define	PR_END_EXTERN_C             }

#define ENTER_CRITICAL_SECTION( )   EnterCriticalSection( )//这里没有使用RTOS,如果使用FreeRTOS可以直接替换为 taskENTER_CRITICAL()和 taskEXIT_CRITICAL()即可
#define EXIT_CRITICAL_SECTION( )   ExitCriticalSection( )
#define assert( expr ) //如果你自己实现了assert可以替换为你实现的,这里不使用

typedef char    BOOL; //如果你使用了FreeRTOS,那么添加FreeRTOS的头文件就行了。

typedef unsigned char UCHAR;

typedef char    CHAR;

typedef unsigned short USHORT;
typedef short   SHORT;

typedef unsigned long ULONG;
typedef long    LONG;

#ifndef TRUE
#define TRUE            1
#endif

#ifndef FALSE
#define FALSE           0
#endif

void            EnterCriticalSection( void );
void            ExitCriticalSection( void );

#endif

portserial.c

vMBPortSerialEnable

  • portserial.c中vMBPortSerialEnable这个函数对于理解FreeModbus的通信过程至关重要:
void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable)
{
    ENTER_CRITICAL_SECTION();
    EXIT_CRITICAL_SECTION();
}
xRxEnable比较好理解,当xRxEnable为TRUE时,抽象意义就是说现在本设备可以接收数据。
  • 一台这杯初始化以后都应当处理这种等待接收数据的状态,无论是主机还是从机,
  • 所以eMBRTUStart后xRxEnable = TRUE,eMBRTUStop后xRxEnable = FALSE。
  • 另外因为Modbus-RTU协议是半双工的,所以在接收数据的时候,本设备不能发送数据,所以不可能出现xRxEnable = TRUE && xTxEnable = TRUE的情况。
xTxEnable则只在eMBRTUSend中为TRUE,表示设备当前准备进行发送数据。注意是准备,也就是这个时候
  • 设备将获取总线的使用权限(如果需要这么做的话)。但是这个时候数据并没有开始发送。
那什么时候数据开始发送呢?
  • eMBRTUSend被传递给了peMBFrameSendCur这个函数指针,

  • 在ePoll中,当Modbus-RTU状态从收到主机请求变为准备发送响应的时候,就会调用这个函数。

  • eMBRTUSend则是将要回复给主机的数据计算出CRC后存放到ucRTUBuf缓存中,然后将eSndState设置为STATE_TX_XMIT,eSndState则是FreeModbus的一个全局变量,用于利用串口发送中断实现Modbus发送数据的状态机,避免了发送主句阻塞一个进程执行的情况。

  • 在Modbus-RTU模式下,eSndState由xMBRTUTransmitFSM函数控制,被赋值给函数指针pxMBFrameCBTransmitterEmpty

  • 然后pxMBFrameCBTransmitterEmpty在串口发送中断中被调用,实现状态机的更新。

然后这里又有一个重要的问题就是这个串口发送中断具体是那个发送中断呢?
  • 因为串口发送中断有很多,例如发送完成中断,发送空中断,发送非空中断等等。

  • 因为xMBRTUTransmitFSM使用xMBPortSerialPutByte来发送数据,即从pucSndBufferCur中

  • 获取数据,然后调用xMBPortSerialPutByte发送数据,待到这个字节的数据从串口外设的位移寄存器中发送完成后通过这个发送完成的中断来调用xMBRTUTransmitFSM来发送下一个数据。

  • 如果xMBRTUTransmitFSM发现没有数据了就将xTxEnable设置为FALSE,表示本设备不再发送数据。

  • 同时也就是在这个时候释放总线的。

  • 所以实际上可以使用STM32的串口发送完成中断或者发送寄存器为空中断来实现Modbus-RTU的发送。

对于接收中断也是同样的道理
  • 通过pxMBFrameCBByteReceived这个统一的函数指针作为接收回调函数
  • 来实现接收事件,因为需要记录每个字节接收到的时刻所以肯定是收到1个字节就需要中断一次。
  • 那么可以使用的中断就是串口外设接收寄存器非空中断。
注意:

需要注意的是串口发送完成中断不能在串口发送完成中断中关闭,因为FreeModbus并不是在每次发送一个字节的时候都启动一次串口发送完成中断。
每次回复数据的调用流程为:
eMBPoll轮询发现收到一帧数据需要回复->调用peMBFrameSendCur->调用eMBRTUSend->eMBRTUSend完成了从机地址和CRC编码的计算存放发送缓存ucRTUBuf,usSndBufferCount->调用vMBPortSerialEnable( FALSE, TRUE );将系统状态设置为发送数据的状态,这个时候就获取的总线的使用权限,然后启动了串口发送完成中断。->然后串口发送完成中断会调用pxMBFrameCBTransmitterEmpty,实际调用xMBRTUTransmitFSM->从下图可以知道这个过程是只在最开始启动了一次串口发送完成中断,发送完成数据后又关闭了。

下面就是精简后的portserial.c
/* ----------------------- Platform includes --------------------------------*/
#include "port.h"

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

/* ----------------------- Defines ------------------------------------------*/

/* ----------------------- Static variables ---------------------------------*/

/* ----------------------- Start implementation -----------------------------*/
void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable)
{
    ENTER_CRITICAL_SECTION();

    EXIT_CRITICAL_SECTION();
}

BOOL xMBPortSerialInit(UCHAR ucPort, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity)
{
    BOOL bInitialized = TRUE;

    return bInitialized;
}

BOOL xMBPortSerialPutByte(CHAR ucByte)
{

    return TRUE;
}

BOOL xMBPortSerialGetByte(CHAR *pucByte)
{

    return TRUE;
}

void serialReceiveOneByteISR(void)
{
    pxMBFrameCBByteReceived();
}

void serialSentOneByteISR(void)
{
    pxMBFrameCBTransmitterEmpty();
}

void EnterCriticalSection(void)
{
    __disable_irq();
}

void ExitCriticalSection(void)
{
    __enable_irq();
}

下面就是本文所使用平台的实现
/* ----------------------- Platform includes --------------------------------*/
#include "port.h"
#include "main.h"
#include "HDL_G4_Uart.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"

/* ----------------------- Defines ------------------------------------------*/
void serialReceiveOneByteISR(void);

void serialSentOneByteISR(void);

void RS485_ReleaseBus()
{
    LL_GPIO_SetOutputPin(RS485_CON1_GPIO_Port, RS485_CON1_Pin);
}

void RS485_TakeBus()
{
    LL_GPIO_ResetOutputPin(RS485_CON1_GPIO_Port, RS485_CON1_Pin);
}

/* ----------------------- Static variables ---------------------------------*/

/* ----------------------- Start implementation -----------------------------*/
void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable)
{
    if (xRxEnable)
    {
        LL_USART_EnableIT_RXNE(USART2);
    }
    else
    {
        LL_USART_DisableIT_RXNE(USART2);
    }

    if (xTxEnable)
    {
        //每次发送一个回复帧只会启动一次串口发送完成中断,所以不能在串口发送完成中断中关闭串口发送完成中断
        LL_USART_EnableIT_TC(USART2); // 使能发送完成中断
    }
    else
    {
        LL_USART_DisableIT_TC(USART2); // 禁能发送完成中断
    }

    if (xTxEnable)
    {
        RS485_TakeBus();
    }
    else
    {
        RS485_ReleaseBus();
    }
}

BOOL xMBPortSerialInit(UCHAR ucPort, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity)
{
    BOOL bInitialized = TRUE;
    /**
     * ucPort 用于区分不同的串口,这里没有使用。
     * ucDataBits 这里再Modbus-RTU下一定是8,所以不使用。这个可以在eMBRTUInit查看。
     * eParity Modbus-RTU要求一帧是11位,所以如果有奇偶校验,那么就是1位停止位,否侧使用2位停止位。
     */
    UNUSED(ucPort);
    uint32_t stopBit = LL_USART_STOPBITS_1;
    uint32_t parity = LL_USART_PARITY_NONE;

    stopBit = eParity == MB_PAR_NONE ? LL_USART_STOPBITS_2 : LL_USART_STOPBITS_1;
    switch (eParity)
    {
    case MB_PAR_NONE:
        parity = LL_USART_PARITY_NONE;
        break;
    case MB_PAR_ODD:
        parity = LL_USART_PARITY_ODD;
        break;
    case MB_PAR_EVEN:
        parity = LL_USART_PARITY_EVEN;
        break;
    default:
        break;
    }
    Uart_Init(COM2, ulBaudRate, LL_USART_DATAWIDTH_8B, stopBit, parity);

    //因为我使用的这个库已经自己实现了接收完成中断,所以这里没有单独列出中断函数来,按照文章说明的意思来即可
    Uart_RegisterReceiveCharCallback(COM2, serialReceiveOneByteISR);
    Uart_SetWriteOverCallback(COM2, serialSentOneByteISR);
    
    //如果初始化失败,应当返回FALSE
    return bInitialized; 
}

BOOL xMBPortSerialPutByte(CHAR ucByte)
{
    USART2->TDR = ucByte;
    return TRUE;
}

BOOL xMBPortSerialGetByte(CHAR *pucByte)
{
    *pucByte = USART2->RDR;
    return TRUE;
}

void serialReceiveOneByteISR(void)
{
    pxMBFrameCBByteReceived();
}

void serialSentOneByteISR(void)
{
    pxMBFrameCBTransmitterEmpty();
}

void EnterCriticalSection(void)
{
    __disable_irq();
}

void ExitCriticalSection(void)
{
    __enable_irq();
}

porttimer.c

T3.5和T1.5的判断对于Modbus-RTU协议接收数据至关重要,而对于时间的判断就需要是要到定时器。这里只需要理解Modbus-RTU需要有个方法通知这这两个事件:

  • 在从空闲状态接收到一个字节后进入接收数据的状态,距离上一个接收到一个字节是否已经超过T3.5个字符的时间了,这是一个事件,用来判断一帧数据接收完成。
  • 在从空闲状态接收到一个字节后进入接收数据的状态,距离上一个接收到一个字节是否已经超过T1.5个字符的时间了,这是用于判断帧是否正常接收。

对于porttimer.c至关重要的就是对pxMBPortCBTimerExpired的理解,FreeModebus的源码中考虑的是将T3.5这个事件计算为50us的个数,然后在设备从空闲状态收到一个字节后进入接收数据的状态后启动这个定时器开始计时,每次收到新的字节都重置这个定时器,直到一帧接收完成,距离上一个接收到一个字节是否已经超过T3.5个字符的时间了,就会进入定时器的超时中断,然后在中断中通过xMBRTUTimerT35Expired通知一帧数据已经接收完成了。

xMBRTUTimerT35Expired被赋给了全局的pxMBPortCBTimerExpired函数指针,会在一个定时器中断中被调用。实际上每次接收开始后指挥被调用一次。

另外值得注意的是对于T1.5的判断FreeModebus并没有去实现,只是在mbrtu.h中提供了一个xMBRTUTimerT15Expired的声明。这也是可以理解的,因为实际上对于T1.5的判断并非是必要的。

下面是精简的porttimer.c

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

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

/* ----------------------- Defines ------------------------------------------*/
void prvvTIMERExpiredISR(void);

/* ----------------------- Static variables ---------------------------------*/

/* ----------------------- Start implementation -----------------------------*/
BOOL xMBPortTimersInit(USHORT usTim1Timeout50us)
{
    BOOL bInitialized = FALSE;
    // 每次调用这个方法都会重置一次定时器

    // 初始化成功要返回TRUE通知初始化完成
    return bInitialized;
}

void vMBPortTimersEnable(void)
{
    // 每次调用这个方法都会重置一次到达超时中断的时间,这个都是在接收到一个字符的时候调用的
    //  调用这个函数同时意味着是能超时中断
}

void vMBPortTimersDisable(void)
{
    // 调用这个函数意味着关闭定时器中断或者关闭定时器
}

void
vMBPortTimersDelay( USHORT usTimeOutMS )
{
	//这个方法不必实现,因为这个方法仅仅在Madbus-ASCII模式中被使用到,这个不需要
}

void prvvTIMERExpiredISR(void)
{
    (void)pxMBPortCBTimerExpired();
}

下面就是本文所使用平台的实现

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

/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
#include "HDL_G4_CPU_Time.h"
/* ----------------------- Defines ------------------------------------------*/
void prvvTIMERExpiredISR(void);
static USHORT gusTim1Timeoutus  =0;
#define CPU_US_TIME_CH 0
/* ----------------------- Static variables ---------------------------------*/

/* ----------------------- Start implementation -----------------------------*/
BOOL xMBPortTimersInit(USHORT usTim1Timeout50us)
{
    BOOL bInitialized = TRUE;
    // 每次调用这个方法都会重置一次定时器
    HDL_G4_CPU_Time_Init();
    gusTim1Timeoutus = usTim1Timeout50us*50;
    // 初始化成功要返回TRUE通知初始化完成
    return bInitialized;
}

void vMBPortTimersEnable(void)
{
    // 每次调用这个方法都会重置一次到达超时中断的时间,这个都是在接收到一个字符的时候调用的
    //  调用这个函数同时意味着是能超时中断
    HDL_G4_CPU_Time_StartHardTimer(CPU_US_TIME_CH, gusTim1Timeoutus, prvvTIMERExpiredISR);
}

void vMBPortTimersDisable(void)
{
    // 调用这个函数意味着关闭定时器中断或者关闭定时器
    HDL_G4_CPU_Time_StopHardTimer(CPU_US_TIME_CH);
}

void vMBPortTimersDelay( USHORT usTimeOutMS )
{
    UNUSED(usTimeOutMS);
}

void prvvTIMERExpiredISR(void)
{
    (void)pxMBPortCBTimerExpired();
}

portevent.c

portevent.c中的三个方法使用例子中给的其实就可以了,主要是获取各个事件,其中:

xMBPortEventInit只会在eMBInit调用时被调用一次。

xMBPortEventPost Post的事件可能来自于上述的定时器中断和串口发送中断函数和eMBPoll中。

所以要确保完全没有问题的话最好时有进出临界区的保护之类的,这里为了简单就没有实现了,但是不代表这种操作时没有问题的,因为我也没有仔细分析会不会出现中断打断ePoll调用xMBPortEventPost的情况,但是从xMBPortEventPost的实现来看这就是一个单一元素的队列,所以应该时不会有这种情况吧,懒得分析了。

xMBPortEventGet 的调用则时完全只发生在eMBPoll中。

下面的代码就是MSP430 Demo中的实现。

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

/* ----------------------- Variables ----------------------------------------*/
static eMBEventType eQueuedEvent;
static BOOL     xEventInQueue;

/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortEventInit( void )
{
    xEventInQueue = FALSE;
    return TRUE;
}

BOOL
xMBPortEventPost( eMBEventType eEvent )
{
    xEventInQueue = TRUE;
    eQueuedEvent = eEvent;
    return TRUE;
}

BOOL
xMBPortEventGet( eMBEventType * eEvent )
{
    BOOL            xEventHappened = FALSE;

    if( xEventInQueue )
    {
        *eEvent = eQueuedEvent;
        xEventInQueue = FALSE;
        xEventHappened = TRUE;
    }
    return xEventHappened;
}

最后就是mbconfig.h

下面是代码注释中文翻译后的结果,主要需要关注的点有:

  • 是否使能Modbus-ASCII、Modbus-RTU、Modbus-TCP,这是只使能Modbus-RTU。
  • 协议栈应支持的最大 Modbus 功能码数量。
/* 
 * FreeModbus库:用于Modbus ASCII/RTU的可移植实现。
 * 版权所有(c)2006-2018 Christian Walter <cwalter@embedded-solutions.at>
 * 保留所有权利。
 *
 * 允许在源代码和二进制形式中重新分发和使用,无论是否进行了
 * 修改,只要满足以下条件:
 * 1. 必须保留上述版权声明、条件列表和以下免责声明。
 * 2. 在分发的文档和/或其他提供的材料中必须重现上述版权声明、条件列表和以下免责声明。
 * 3. 未经特定书面许可,不得使用作者的名称来认可或推广与本软件派生的产品。
 *
 * 本软件是按原样提供的,任何明示或暗示的保证,
 * 包括但不限于适销性和适用于特定目的的暗示保证,
 * 都是被拒绝的。
 * 在任何情况下,作者均不对任何直接、间接、
 * 偶然、特殊、典型或间接损害(包括但不限于
 * 替代品或服务的采购;使用、数据或利润的损失;
 * 或业务中断)负责,无论是基于合同、严格责任还是侵权
 * (包括疏忽或其他原因),即使事先已被告知有此种可能性。
 *
 */

#ifndef _MB_CONFIG_H
#define _MB_CONFIG_H

#ifdef __cplusplus
PR_BEGIN_EXTERN_C
#endif
/* ----------------------- 定义 ------------------------------------------*/
/*! \defgroup modbus_cfg Modbus 配置
 *
 * 协议栈中的大多数模块是完全可选的,可以排除。
 * 如果目标资源非常有限且需要节省程序存储空间,
 * 则这点尤为重要。<br>
 *
 * 所有这些设置都在文件 <code>mbconfig.h</code> 中可用。
 */
/*! \addtogroup modbus_cfg
 *  @{
 */
/*! \brief 启用 Modbus ASCII 支持。 */
#define MB_ASCII_ENABLED                        (  1 )

/*! \brief 启用 Modbus RTU 支持。 */
#define MB_RTU_ENABLED                          (  1 )

/*! \brief 启用 Modbus TCP 支持。 */
#define MB_TCP_ENABLED                          (  0 )

/*! \brief Modbus ASCII 的字符超时值。
 *
 * 字符超时值对于 Modbus ASCII 不是固定的,因此是一个配置选项。
 * 它应该设置为网络的最大预期延迟时间。
 */
#define MB_ASCII_TIMEOUT_SEC                    (  1 )

/*! \brief 在启用 ASCII 之前等待启用串行发送的超时时间。
 *
 * 如果定义了该值,则函数调用 vMBPortSerialDelay,
 * 参数为 MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS,
 * 以允许在启用串行发送之前延迟一段时间。
 * 这是必需的,因为某些目标速度非常快,
 * 在接收和发送帧之间没有时间间隔。
 * 如果主站在启用其接收器时太慢,
 * 那么它将无法正确接收响应。
 */
#ifndef MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS
#define MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS    ( 0 )
#endif

/*! \brief 协议栈应支持的最大 Modbus 功能码数量。
 *
 * 支持的最大 Modbus 功能码数量必须大于此文件中所有启用的功能的总和和自定义功能处理程序的总和。
 * 如果设置得太小,添加更多功能将失败。
 */
#define MB_FUNC_HANDLERS_MAX                    ( 16 )

/*! \brief 为 <em>报告从机 ID </em>命令分配的字节数。
 *
 * 此数字限制了报告从机 ID 函数中附加段的最大大小。
 * 有关如何设置此值的更多信息,请参见 eMBSetSlaveID(  )。
 * 仅在 MB_FUNC_OTHER_REP_SLAVEID_ENABLED 设置为 <code>1</code> 时使用。
 */
#define MB_FUNC_OTHER_REP_SLAVEID_BUF           ( 32 )

/*! \brief 是否启用 <em>报告从机 ID</em> 功能。 */
#define MB_FUNC_OTHER_REP_SLAVEID_ENABLED       (  1 )

/*! \brief 是否启用 <em>读输入寄存器</em> 功能。 */
#define MB_FUNC_READ_INPUT_ENABLED              (  1 )

/*! \brief 是否启用 <em>读保持寄存器</em> 功能。 */
#define MB_FUNC_READ_HOLDING_ENABLED            (  1 )

/*! \brief 是否启用 <em>写单个寄存器</em> 功能。 */
#define MB_FUNC_WRITE_HOLDING_ENABLED           (  1 )

/*! \brief 是否启用 <em>写多个寄存器</em> 功能。 */
#define MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED  (  1 )

/*! \brief 是否启用 <em>读线圈</em> 功能。 */
#define MB_FUNC_READ_COILS_ENABLED              (  1 )

/*! \brief 是否启用 <em>写线圈</em> 功能。 */
#define MB_FUNC_WRITE_COIL_ENABLED              (  1 )

/*! \brief 是否启用 <em>写多个线圈</em> 功能。 */
#define MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED    (  1 )

/*! \brief 是否启用 <em>读离散输入</em> 功能。 */
#define MB_FUNC_READ_DISCRETE_INPUTS_ENABLED    (  1 )

/*! \brief 是否启用 <em>读/写多个寄存器</em> 功能。 */
#define MB_FUNC_READWRITE_HOLDING_ENABLED       (  1 )

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

添加从机的回调函数

到这里就算是移植完成了,但是为了实现从机的功能还需要了解并且完成以下四个回调函数

在FreeModbus从机协议栈中,这四个回调函数的存在和作用与Modbus-RTU协议中定义的功能码密切相关。以下是每个回调函数的功能及其与Modbus功能码的关联:

  • eMBRegInputCB:这个函数用于处理输入寄存器的读取请求。Modbus协议为输入寄存器提供了专门的功能码。当从站收到来自主站的请求读取输入寄存器时,协议栈会调用这个回调函数来获取所需的寄存器值。
    函数的参数包括一个指向存储寄存器值的缓冲区的指针(pucRegBuffer)、起始寄存器地址(usAddress)和需要读取的寄存器数量(usNRegs)。该函数需要将相应的寄存器值写入缓冲区,并返回相应的错误代码。

  • eMBRegHoldingCBB:此函数用于处理保持寄存器的读写操作。保持寄存器的功能码包括读取(例如功能码03)和写入(例如功能码06和16)。根据协议栈的需求,这个回调函数会被用来读取或更新保持寄存器的值。在实际应用中,保持寄存器的数据通常存储在一个数组中​​​​​​。

  • 根据操作模式,该函数要么从缓冲区读取数据更新寄存器值,要么将当前寄存器值写入缓冲区,并返回相应的错误代码。

  • eMBRegInputCB:这个函数处理线圈寄存器的读写操作。线圈寄存器的功能码(例如0x01,用于读取线圈状态)允许从远程设备中连续读取一定数量的线圈状态,此回调函数便用于此类操作。线圈的状态被打包在数据字段中,每个位代表一个线圈的打开或关闭状态​​。

  • 该函数需要根据操作模式处理线圈值(读取或写入),并返回相应的错误代码。

  • eMBRegDiscreteCB:此函数用于处理离散输入寄存器的读取操作。离散输入类似于线圈,但它们是只读的。功能码0x02用于读取离散输入的状态,而这个回调函数便被用于此操作​​。

  • 函数的参数包括一个指向缓冲区的指针、起始离散输入地址和离散输入数量。该函数需要将相应的离散输入值写入缓冲区,并返回相应的错误代码。

Modbus存储区

Modbus协议规定了4个存储区 分别是0 1 3 4区 其中1区和4区是可读可写,1区和3区是只读。

区号名称读写地址范围
0区输出线圈可读可写布尔量00001-09999
1区输入线圈只读布尔量10001-19999
3区输入寄存器只读寄存器30001-39999
4区保持寄存器可读可写寄存器40001-49999
03、06、16号命令使用的就是保持寄存器

Modbus-RTU协议

帧结构 = 从机地址 + 功能码+ 数据段 + 校验

从机地址功能码数据段CRC校验
1B1BN*2B2B

常用功能码:

03:读保持寄存器;eg.01 03 00 00 00 01 84 0A
从slave01的地址0x0000开始读1个寄存器的值
06:写保持寄存器;eg.01 06 00 00 00 FF C9 8A
向slave01的地址0x0000写入寄存器的值0x00FF
16:写多个寄存器;eg.01 10 00 00 00 03 06 00 FF 00 55 00 33 A2 91
从slave01的地址0x0000开始写入3个寄存器(6B)的值0xff 0x55 0x33

CRC(循环冗余)校验流程:

1、预置一个16位寄存器为0FFFFH(全1),称之为CRC寄存器。
2 、把数据帧中的第一个字节的8位与CRC寄存器中的低字节进行异或运算,结果存回CRC寄存器。
3、将CRC寄存器向右移一位,最高位填以0,最低位移出并检测。
4 、如果最低位为0:重复第三步(下一次移位);如果最低位为1:将CRC寄存器与一个预设的固定值(0A001H)进行异或运算。
5、重复第三步和第四步直到8次移位。这样处理完了一个完整的八位。
6 、重复第2步到第5步来处理下一个八位,直到所有的字节处理结束。
7、最终CRC寄存器的值就是CRC的值。

例子

下面是一个简单的实现:
功能码 03:读保持寄存器,可读多个或者单个寄存器内容。详细的应用例子参考另一篇文章:STM32 移植FreeModbus详细过程。这里我是单独将从机的功能放在了一个文件mbcb.c中,并且添加到了工程。

#include "mb.h"

// 保持寄存器起始地址
#define REG_HOLDING_START 0x0000
// 保持寄存器数量
#define REG_HOLDING_NREGS 8
// 保持寄存器内容
uint16_t usRegHoldingBuf[REG_HOLDING_NREGS] = {0x1234, 0x5678, 0x9ABC, 0xDEF0,
                                               0x1357, 0x2468, 0xACE0, 0xBDF0};
// 保持寄存器起始地址
uint16_t usRegHoldingStart = REG_HOLDING_START;

/**
 * @brief 读取输入寄存器,对应功能码是 04 eMBFuncReadInputRegister.
 * @note 上位机发来的 帧格式是:
 * SlaveAddr(1 Byte)+FuncCode(1 Byte)
 * +StartAddrHiByte(1 Byte)+StartAddrLoByte(1 Byte)
 * +LenAddrHiByte(1 Byte)+LenAddrLoByte(1 Byte)+
 * +CRCAddrHiByte(1 Byte)+CRCAddrLoByte(1 Byte)
 * @param pucRegBuffer 数据缓存区,用于响应主机
 * @param usAddress 寄存器地址
 * @param usNRegs 要读取的寄存器个数
 * @return eMBErrorCode
 */
eMBErrorCode
eMBRegInputCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs)
{
    eMBErrorCode eStatus = MB_ENOERR;
    return eStatus;
}

/**
 * @brief 对应功能码有:
 * 06 写保持寄存器 eMBFuncWriteHoldingRegister
 * 16 写多个保持寄存器 eMBFuncWriteMultipleHoldingRegister
 * 03 读保持寄存器 eMBFuncReadHoldingRegister
 * 23 读写多个保持寄存器 eMBFuncReadWriteMultipleHoldingRegister
 *
 * @param pucRegBuffer 数据缓存区,用于响应主机
 * @param usAddress 寄存器地址
 * @param usNRegs 要读写的寄存器个数
 * @param eMode 功能码
 * @return eMBErrorCode
 */
eMBErrorCode
eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode)
{
    eMBErrorCode eStatus = MB_ENOERR;
    int iRegIndex = 0;
    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);
/**
 * @brief 对应功能码有:01 读线圈 eMBFuncReadCoils
 * 05 写线圈 eMBFuncWriteCoil
 * 15 写多个线圈 eMBFuncWriteMultipleCoils
 *
 * @note 如继电器
 *
 * @param pucRegBuffer 数据缓存区,用于响应主机
 * @param usAddress 线圈地址
 * @param usNCoils 要读写的线圈个数
 * @param eMode 功能码
 * @return eMBErrorCode
 */
eMBErrorCode
eMBRegCoilsCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNCoils,
              eMBRegisterMode eMode)
{
    // 错误状态
    eMBErrorCode eStatus = MB_ENOERR;
    // 寄存器个数
    int16_t iNCoils = (int16_t)usNCoils;
    // 寄存器偏移量
    int16_t usBitOffset;

    return eStatus;
}

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

    return eStatus;
}

源码注释翻译


/*! \ingroup modbus_registers
 * \brief 用于协议栈需要<em>输入寄存器</em>值的情况的回调函数。
 *   起始寄存器地址由 \c usAddress 给出,最后一个寄存器由
 *   <tt>usAddress + usNRegs - 1</tt> 给出。
 *
 * \param pucRegBuffer 回调函数应将当前的modbus寄存器值写入的缓冲区。
 * \param usAddress 寄存器的起始地址。输入寄存器
 *   在范围1 - 65535内。
 * \param usNRegs 回调函数必须提供的寄存器数量。
 *
 * \return 该函数必须返回以下错误代码之一:
 *   - eMBErrorCode::MB_ENOERR 如果没有发生错误。在这种情况下,将发送正常的
 *     Modbus响应。
 *   - eMBErrorCode::MB_ENOREG 如果应用程序无法提供此范围内的寄存器的值。
 *     在这种情况下,将作为响应发送 <b>ILLEGAL DATA ADDRESS</b> 异常帧。
 *   - eMBErrorCode::MB_ETIMEDOUT 如果请求的寄存器块目前不可用,且
 *     应用程序相关的响应超时将被违反。在这种情况下,将作为响应发送
 *     <b>SLAVE DEVICE BUSY</b> 异常。
 *   - eMBErrorCode::MB_EIO 如果发生不可恢复的错误。在这种情况下,将作为
 *     响应发送 <b>SLAVE DEVICE FAILURE</b> 异常。
 */
eMBErrorCode    eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress,
                               USHORT usNRegs );

/*! \ingroup modbus_registers
 * \brief 用于协议栈读取或写入<em>保持寄存器</em>值的回调函数。
 *   起始寄存器地址由 \c usAddress 给出,最后一个寄存器由
 *   <tt>usAddress + usNRegs - 1</tt> 给出。
 *
 * \param pucRegBuffer 如果应用程序寄存器的值应该从缓冲区的新寄存器值更新,
 *   则该缓冲区指向新寄存器值。如果协议栈需要当前值,
 *   则回调函数应将它们写入该缓冲区。
 * \param usAddress 寄存器的起始地址。
 * \param usNRegs 要读取或写入的寄存器数量。
 * \param eMode 如果 eMBRegisterMode::MB_REG_WRITE,则应从缓冲区的值更新
 *   应用程序寄存器值。例如,当Modbus主站发出<b>写单个寄存器</b>命令时,
 *   这将是情况。如果值为 eMBRegisterMode::MB_REG_READ,则应用程序应将
 *   当前值复制到缓冲区 \c pucRegBuffer 中。
 *
 * \return 该函数必须返回以下错误代码之一:
 *   - eMBErrorCode::MB_ENOERR 如果没有发生错误。在这种情况下,将发送正常的
 *     Modbus响应。
 *   - eMBErrorCode::MB_ENOREG 如果应用程序无法提供此范围内的寄存器的值。
 *     在这种情况下,将作为响应发送 <b>ILLEGAL DATA ADDRESS</b> 异常帧。
 *   - eMBErrorCode::MB_ETIMEDOUT 如果请求的寄存器块目前不可用,且
 *     应用程序相关的响应超时将被违反。在这种情况下,将作为响应发送
 *     <b>SLAVE DEVICE BUSY</b> 异常。
 *   - eMBErrorCode::MB_EIO 如果发生不可恢复的错误。在这种情况下,将作为
 *     响应发送 <b>SLAVE DEVICE FAILURE</b> 异常。
 */
eMBErrorCode    eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress,
                                 USHORT usNRegs, eMBRegisterMode eMode );

/*! \ingroup modbus_registers
 * \brief 用于协议栈读取或写入<em>线圈寄存器</em>值的回调函数。
 *   如果要使用这个函数,您可能会使用函数
 *   xMBUtilSetBits(  ) 和 xMBUtilGetBits(  ) 来处理位字段。
 *
 * \param pucRegBuffer 位被打包在字节中,其中第一个线圈
 *   从地址 \c usAddress 开始,存储在缓冲区 <code>pucRegBuffer</code> 中
 *   的第一个字节的LSB中。如果回调函数应该写入缓冲区,
 *   则未使用的线圈值(即如果不使用八个线圈的倍数,则未使用的线圈)
 *   应设置为零。
 * \param usAddress 第一个线圈的编号。
 * \param usNCoils 请求的线圈值数量。
 * \param eMode 如果 eMBRegisterMode::MB_REG_WRITE,则应用程序值应从缓冲区
 *   \c pucRegBuffer 中的值更新。例如,当Modbus主站发出
 *   <b>写单个线圈</b> 命令时,就会出现这种情况。
 *   如果值为 eMBRegisterMode::MB_REG_READ,则应用程序应将当前值存储
 *   到缓冲区 \c pucRegBuffer 中。
 *
 * \return 该函数必须返回以下错误代码之一:
 *   - eMBErrorCode::MB_ENOERR 如果没有发生错误。在这种情况下,将发送正常的
 *     Modbus响应。
 *   - eMBErrorCode::MB_ENOREG 如果应用程序未在请求的地址范围内映射任何线圈。
 *     在这种情况下,将作为响应发送 <b>ILLEGAL DATA ADDRESS</b> 异常帧。
 *   - eMBErrorCode::MB_ETIMEDOUT 如果请求的寄存器块目前不可用,且
 *     应用程序相关的响应超时将被违反。在这种情况下,将作为响应发送
 *     <b>SLAVE DEVICE BUSY</b> 异常。
 *   - eMBErrorCode::MB_EIO 如果发生不可恢复的错误。在这种情况下,将作为
 *     响应发送 <b>SLAVE DEVICE FAILURE</b> 异常。
 */
eMBErrorCode    eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress,
                               USHORT usNCoils, eMBRegisterMode eMode );

/*! \ingroup modbus_registers
 * \brief 用于协议栈读取<em>输入离散寄存器</em>值的回调函数。
 *
 * 如果要使用这个函数,您可能会使用函数
 * xMBUtilSetBits(  ) 和 xMBUtilGetBits(  ) 来处理位字段。
 *
 * \param pucRegBuffer 缓冲区应该更新为当前
 *   线圈值。第一个离散输入从 \c usAddress 开始,必须存储在
 *   缓冲区的第一个字节的LSB中。如果请求的数量不是八个的倍数,
 *   则剩余的位应设置为零。
 * \param usAddress 第一个离散输入的起始地址。
 * \param usNDiscrete 离散输入值的数量。
 * \return 该函数必须返回以下错误代码之一:
 *   - eMBErrorCode::MB_ENOERR 如果没有发生错误。在这种情况下,将发送正常的
 *     Modbus响应。
 *   - eMBErrorCode::MB_ENOREG 如果没有这样的离散输入存在。
 *     在这种情况下,将作为响应发送 <b>ILLEGAL DATA ADDRESS</b> 异常帧。
 *   - eMBErrorCode::MB_ETIMEDOUT 如果请求的寄存器块目前不可用,且
 *     应用程序相关的响应超时将被违反。在这种情况下,将作为响应发送
 *     <b>SLAVE DEVICE BUSY</b> 异常。
 *   - eMBErrorCode::MB_EIO 如果发生不可恢复的错误。在这种情况下,将作为
 *     响应发送 <b>SLAVE DEVICE FAILURE</b> 异常。
 */
eMBErrorCode    eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress,
                                  USHORT usNDiscrete );


第四步:添加文件到Keil工程

这里为了方便吧我就把所有文件都添加到Keill工程的同一个文件夹下了,更具自己喜好了把,这个不影响使用,记得在工程的包含目录集合里面添加头文件所在的文件夹就行。

  1. 点击工程项管理添加一个工程文件夹freemodbus,并且添加所有.c文件
    在这里插入图片描述
    2. 添加所有.c文件
  2. 点击魔术棒图标打开工程选项->C/C+±>include Paths->添加包含freemodbus头文件的文件夹->确认
    在这里插入图片描述
    在这里插入图片描述
    这样工程就添加完成了。
    在这里插入图片描述

第五步:如何使用

如何初始化使用

#include "mb.h"
int main(void)
{
		/*
		* RTU模式 从机地址:0x01 串口:这里不起作用,随便写 波特率:9600 无奇偶校验位
		*/
		eMBInit(MB_RTU, 0x01, 0x01, 9600, MB_PAR_NONE);
		//使能Modbus RTU
		eMBEnable();
		for(;;)
		{
			//轮询
			eMBPoll();
		}
}

第六步:实验程序

如下:发送03查询命令,读取从地址00 00开始读取2个保持寄存器。

在这里插入图片描述

注意

需要注意的是串口发送完成中断不能在串口发送完成中断中关闭,因为FreeModbus并不是在每次发送一个字节的时候都启动一次串口发送完成中断。
每次回复数据的调用流程为:
eMBPoll轮询发现收到一帧数据需要回复->调用peMBFrameSendCur->调用eMBRTUSend->eMBRTUSend完成了从机地址和CRC编码的计算存放发送缓存ucRTUBuf,usSndBufferCount->调用vMBPortSerialEnable( FALSE, TRUE );将系统状态设置为发送数据的状态,这个时候就获取的总线的使用权限,然后启动了串口发送完成中断。->然后串口发送完成中断会调用pxMBFrameCBTransmitterEmpty,实际调用xMBRTUTransmitFSM->从下图可以知道这个过程是只在最开始启动了一次串口发送完成中断,发送完成数据后又关闭了。

在这里插入图片描述

如下是一个串口中断的实现。

/**
 * @brief This function handles USART2 global interrupt.
 */
void USART2_IRQHandler(void)
{
	// 接收寄存器非空标志位能且只能通过读取接收寄存器来清除,这里是检查是否是接收到新数据而进入中断
	if (LL_USART_IsActiveFlag_RXNE(USART2) != RESET)
	{
		if (com2_receive_char_callback_not_read != NULL)
		{
			com2_receive_char_callback_not_read();
		}
	}

	if (LL_USART_IsActiveFlag_ORE(USART2) != RESET)
	{
		LL_USART_ClearFlag_ORE(USART2);
	}

	if (LL_USART_IsActiveFlag_PE(USART2) != RESET)
	{
		LL_USART_ClearFlag_PE(USART2); // 奇偶校验错误清除
	}

	if (LL_USART_IsActiveFlag_FE(USART2) != RESET)
	{
		LL_USART_ClearFlag_FE(USART2); // Clear Framing Error Flag
	}

	if (LL_USART_IsEnabledIT_TC(USART2) && LL_USART_IsActiveFlag_TC(USART2))
	{
		//不能再这里关闭串口发送完成中断
		if (com2_write_over_callback != NULL)
		{
			com2_write_over_callback();
		}
	}
}

如果你使用的是HAL库的串口发送完成中断回调函数HAL_UART_TxCpltCallback,那么你需要在这个回调函数中重新启动串口接收完成中断,因为HAL库会在你每次调用HAL_UART_Transmit_IT进入中断后关闭串口发送完成中断。

HAL_UART_Transmit();//串口发送数据,使用超时管理机制
HAL_UART_Receive();//串口接收数据,使用超市管理机制
HAL_UART_Transmit_IT();//串口中断模式发送  
HAL_UART_Receive_IT();//串口中断模式接收
HAL_UART_Transmit_DMA();//串口DMA模式发送
HAL_UART_Transmit_DMA();//串口DMA模式接收

HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);

以下是HAL库的一个实现:

/*
 * 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"
#include "usart.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)
    {
		//
		// 如果使用了485控制芯片,那么在此处将485设置为接收模式
		//
		
		// MAX485_SetMode(MODE_RECV);
		
        __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);		// 使能接收非空中断
    }
    else
    {
        __HAL_UART_DISABLE_IT(&huart1, UART_IT_RXNE);		// 禁能接收非空中断
    }

    if(xTxEnable)
    {
		//
		// 如果使用了485控制芯片,那么在此处将485设置为发送模式
		//
		
		// MAX485_SetMode(MODE_SENT);
		
        __HAL_UART_ENABLE_IT(&huart1, UART_IT_TXE);			// 使能发送为空中断
    }
    else
    {
        __HAL_UART_DISABLE_IT(&huart1, UART_IT_TXE);		// 禁能发送为空中断
    }
}

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

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

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

    // 无校验
    default:
        huart1.Init.Parity = UART_PARITY_NONE;
        huart1.Init.WordLength = UART_WORDLENGTH_8B;			// 无奇偶校验数据位为8bits
        break;
    }
    return HAL_UART_Init(&huart1) == HAL_OK ? TRUE : FALSE;
}

BOOL xMBPortSerialPutByte( CHAR ucByte )
{
    USART1->DR = ucByte;
    return TRUE;
}

BOOL xMBPortSerialGetByte( CHAR * pucByte )
{
    *pucByte = (USART1->DR & (uint16_t)0x00FF);
    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(  );
}

/* 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 void prvvUARTRxISR( void )
{
    pxMBFrameCBByteReceived(  );
}

void USART1_IRQHandler(void)
{
    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE))			// 接收非空中断标记被置位
    {
        __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_RXNE);			// 清除中断标记
        prvvUARTRxISR();										// 通知modbus有数据到达
    }

    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE))				// 发送为空中断标记被置位
    {
        __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TXE);			// 清除中断标记
        prvvUARTTxReadyISR();									// 通知modbus数据可以发松
    }
}


完整的mbcb.c

#include "mb.h"

// 输入寄存器起始地址
#define REG_INPUT_START 0x0000
// 输入寄存器数量
#define REG_INPUT_NREGS 8
// 保持寄存器起始地址
#define REG_HOLDING_START 0x0000
// 保持寄存器数量
#define REG_HOLDING_NREGS 8

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

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

/* Private variables ---------------------------------------------------------*/
// 输入寄存器内容
uint16_t usRegInputBuf[REG_INPUT_NREGS] = {0x1234, 0x5678, 0x9abc, 0xdef0, 0x147b, 0x3f8e, 0x147b, 0x3f8e};
// 输入寄存器起始地址
uint16_t usRegInputStart = REG_INPUT_START;

// 保持寄存器内容
uint16_t usRegHoldingBuf[REG_HOLDING_NREGS] = {0xAABB, 0xCCDD, 0xEEFF, 0x0011, 0x2233, 0x4455, 0x6677, 0x8899};
// 保持寄存器起始地址
uint16_t usRegHoldingStart = REG_HOLDING_START;

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

/**
 * @brief 读取输入寄存器,对应功能码是 04 eMBFuncReadInputRegister.
 * @note 上位机发来的 帧格式是:
 * SlaveAddr(1 Byte)+FuncCode(1 Byte)
 * +StartAddrHiByte(1 Byte)+StartAddrLoByte(1 Byte)
 * +LenAddrHiByte(1 Byte)+LenAddrLoByte(1 Byte)+
 * +CRCAddrHiByte(1 Byte)+CRCAddrLoByte(1 Byte)
 * @param pucRegBuffer 数据缓存区,用于响应主机
 * @param usAddress 寄存器地址
 * @param usNRegs 要读取的寄存器个数
 * @return eMBErrorCode
 */
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;
}

/**
 * @brief 对应功能码有:
 * 06 写保持寄存器 eMBFuncWriteHoldingRegister
 * 16 写多个保持寄存器 eMBFuncWriteMultipleHoldingRegister
 * 03 读保持寄存器 eMBFuncReadHoldingRegister
 * 23 读写多个保持寄存器 eMBFuncReadWriteMultipleHoldingRegister
 *
 * @param pucRegBuffer 数据缓存区,用于响应主机
 * @param usAddress 寄存器地址
 * @param usNRegs 要读写的寄存器个数
 * @param eMode 功能码
 * @return eMBErrorCode
 */
eMBErrorCode
eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode)
{
    eMBErrorCode eStatus = MB_ENOERR;
    int iRegIndex = 0;
    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);
/**
 * @brief 对应功能码有:01 读线圈 eMBFuncReadCoils
 * 05 写线圈 eMBFuncWriteCoil
 * 15 写多个线圈 eMBFuncWriteMultipleCoils
 *
 * @note 如继电器
 *
 * @param pucRegBuffer 数据缓存区,用于响应主机
 * @param usAddress 线圈地址
 * @param usNCoils 要读写的线圈个数
 * @param eMode 功能码
 * @return eMBErrorCode
 */
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;
}

/**
 * @brief 读取离散寄存器,对应功能码有:
 *  02 读离散寄存器 eMBFuncReadDiscreteInputs
 *
 * @param pucRegBuffer 数据缓存区,用于响应主机
 *
 * @param usAddress 寄存器地址
 * @param usNDiscrete 要读取的寄存器个数
 * @return eMBErrorCode
 */
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;
}

  • 13
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
STM32是一种非常受欢迎的微控制器,广泛应用于各种嵌入式系统。在FreeRTOS系统下使用STM32的串口DMA接收方式对接FreeModbus-RTU协议栈,可以实现高效的数据传输,提高系统性能。下面分别从串口DMA和FreeModbus-RTU两部分介绍。 串口DMA是一种直接内存访问技术,能够实现数据的高效传输,极大地提高了系统性能。在STM32中,串口DMA接收方式可以实现在接收数据时不需要CPU干预,将接收到的数据直接存储到指定的内存区域中。这种方式可以大大降低CPU的负载,提高系统的并发处理能力。 FreeModbus-RTU是一个广泛应用于工控系统的通信协议栈,具有易于移植、高效、可靠等优点。通过STM32的串口DMA接收方式对接FreeModbus-RTU协议栈,可以实现快速、高效的通信。具体实现过程中,需要根据FreeModbus-RTU协议的规则进行数据包的解析和封装。在串口DMA接收到数据后,可以通过设置相关的中断来触发数据的解析和封装。 需要注意的是,在接收数据时,由于数据包的长度是不确定的,因此需要设置合适的缓冲区大小。同时,在封装数据包时,需要按照FreeModbus-RTU协议的规则进行封装,并且需要考虑到异步通信时数据包的压缩问题,以提高通信效率。 综上所述,通过STM32的串口DMA接收方式对接FreeModbus-RTU协议栈,可以实现高效、可靠的通信,并且可以在保证系统性能的同时提高通信效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值