当前位置: 首页 > news >正文

FreeModbus RTU 移植指南

FreeModbus 简介

FreeModbus 是一个免费的软件协议栈,实现了 Modbus 从机功能:

  • 纯 C 语言
  • 支持 Modbus RTU/ASCII
  • 支持 Modbus TCP

本文介绍 Modbus RTU 移植。

移植环境:

  • 裸机
  • Keil MDK 编译器
  • Cortex-M3 内核芯片(LPC1778/88)

移植概述

1.体系架构相关

项目描述
INLINE宏,编译器相关,内联指令或关键字
PR_BEGIN_EXTERN_C
PR_END_EXTERN_C
宏,按照 C 代码编译
ENTER_CRITICAL_SECTION( )
EXIT_CRITICAL_SECTION( )
宏,进入临界区和退出临界区
BOOL
UCHAR
CHAR
USHORT
SHORT
ULONG
LONG
数据类型
TRUE
FALSE
宏,BOOL 类型变量的值

2.定时器
需要移植的定时器函数

定时器函数描述
BOOL xMBPortTimersInit( USHORT usTim1Timerout50us )初始化,由协议栈回调, usTim1Timerout50us 的单位是 50us
void vMBPortTimersEnable( )使能定时器,协议栈回调
定时器计数器清零,然后开始计数
void vMBPortTimersDisable( )禁止定时器,由协议栈回调
定时器计数器清零,停止计数
void prvvTIMERExpiredISR( void )通知协议栈定时器中断发生,需手动安装到定时器中断服务函数中

3.串口
需要移植的函数

定时器函数描述
BOOL xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )初始化串口硬件,由协议栈回调
void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )使能/禁止串口发送和接收,由协议栈回调
BOOL xMBPortSerialPutByte( CHAR ucByte )通过串口发送一字节数据
BOOL xMBPortSerialGetByte( CHAR * pucByte )从串口接收一字节数据
void prvvUARTRxISR( void )通知协议栈串口接收中断发生,协议栈会进行数据接收处理。需手动安装到串口接收中断服务函数中
void prvvUARTTxReadyISR( void )通知协议栈串口发送中断发生,协议栈会进行数据发送。需手动安装到串口发送中断服务函数中

4.事件
事件相关回调函数需要移植:

事件回调函数描述
BOOL xMBPortEventInit( void )初始化
BOOL xMBPortEventPost( eMBEventType eEvent )事件投递
可以在这个函数中解析事件,并执行自己的事件函数。
BOOL xMBPortEventGet( eMBEventType * eEvent )获取事件

mb_config.h 文件属于协议栈的一部分,直接修改不合理
assert,直接调用 C 标准库函数, 但这个依赖硬件

移植细节

并不是所有函数都需要重头编写,协议栈 \freemodbus\demo\BARE\port\ 文件夹下给出了移植框架:

port
|---- port.h :体系架构相关
|---- porttimer.c :定时器相关
|---- portserial.c :串口相关
|---- portevent.c :事件相关

1.体系架构
port.h 文件:

#include <assert.h>
#include <stdint.h>
#include "cmsis_compiler.h"

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

#ifndef assert
#define assert(ignore) ((void)0)
#endif

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

typedef uint8_t BOOL;

typedef unsigned char UCHAR;
typedef char CHAR;

typedef uint16_t USHORT;
typedef int16_t SHORT;

typedef uint32_t ULONG;
typedef int32_t LONG;

#ifndef TRUE
#define TRUE            1
#endif

#ifndef FALSE
#define FALSE           0
#endif

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

进入和退出临界区函数,实际上是开关中断,这部分点击这里可以获取详细的信息。我们新建一个 port.c 文件,在这个文件中实现一个可以嵌套使用的进入和退出临界区代码:

#include "cmsis_compiler.h"

static uint32_t nesting_count = 0;
static uint32_t old_state;

void EnterCriticalSection(void)
{
    uint32_t cur_state;
    
    cur_state = __get_PRIMASK();
    __disable_irq();
    if(nesting_count == 0)
        old_state = cur_state;
    nesting_count ++;
}

void ExitCriticalSection(void)
{
    nesting_count --;
    if(0 == nesting_count)
        __set_PRIMASK(old_state);
}

2.定时器
Modbus RTU 使用超时机制判断数据帧结束:串口超过 3.5 个字符传输时间没有收到数据,则认为一帧结束。
这需要一个硬件定时器。
协议栈会根据传入的波特率自动计算 3.5 个字符传输时间是多少,单位是 50us,简化后的代码如下所示:

/* 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 );
}
xMBPortTimersInit( ( USHORT ) usTimerT35_50us )

所以就可以根据传入的 3.5 个字符传输时间 usTimerT35_50us 来初始化硬件定时器。我的系统刚好有个 50us 中断一次的定时器,所以我直接使用这个定时器来移植,移植代码在 porttime.c 文件中:

#include <stdbool.h>
/* ----------------------- Platform includes --------------------------------*/
#include "port.h"

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

static bool IsTimerEnable = false;
static USHORT Timerout50usCount = 0;
static USHORT Timerout50usCountCur = 0;

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

/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
    Timerout50usCount = usTim1Timerout50us;
    IsTimerEnable = false;
    return TRUE;
}


inline void
vMBPortTimersEnable(  )
{
    /* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
    IsTimerEnable = true;
    Timerout50usCountCur = 0;
}

inline void
vMBPortTimersDisable(  )
{
    /* Disable any pending timers. */
    IsTimerEnable = false;
    Timerout50usCountCur = 0;
}

/*需手动安装到定时器中断服务函数*/
void
vMBPortTimersISR(  )
{
    if(IsTimerEnable)
    {
        Timerout50usCountCur ++;
        if(Timerout50usCountCur >= Timerout50usCount)
            prvvTIMERExpiredISR();
    }
}

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

有一点我很好奇, 3.5 个字符传输时间 usTimerT35_50us 为什么要格式化成 50us 的倍数?
我注意到代码 xMBPortTimersInit( ( USHORT ) usTimerT35_50us ) 在传递参数时进行了一次数据强制转换,也就是协议栈使用的 USHORT 数据类型,一般这个数据类型最大值是 65536,如果不转换成 50us 的倍数,低波特率(比如 1200bps )必然会出现数据溢出现象。
那协议栈为什么又非要使用 USHORT 数据类型呢?
不清楚,大概是当时主流 MCU 还不是 32 位的,USHORT 数据类型可以更快更节省 RAM 。

何时使能定时器?

  1. 启动协议栈(eMBRTUStart
  2. 接收到 1 字节数据(xMBRTUReceiveFSM):复位计数器,重新开始计时

何时关闭定时器?

  1. 停止协议栈(eMBRTUStop
  2. 超时发生(3.5 个字符传输时间):收到新的数据帧,停止计时

定时器与接收关系密切,参与接收状态机的状态迁移:
在这里插入图片描述
3.串口
串口用于收发数据。移植代码在 portserial.c 中:

#include "port.h"

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

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

void down3_set_to_recv(void);
void down3_set_to_send(void);
void down3_put_byte( CHAR data);
void down3_get_byte(CHAR *pucByte);
void init_down3_uart2(UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity);

/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
    /* If xRXEnable enable serial receive interrupts. If xTxENable enable
     * transmitter empty interrupts.
     */
    if(xRxEnable)
    {
        down3_set_to_recv();
    }
    if(xTxEnable)
    {
        down3_set_to_send();
        prvvUARTTxReadyISR();
    }
}

BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
    init_down3_uart2(ucPORT, ulBaudRate, ucDataBits, eParity);
    return TRUE;
}

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. */
    down3_put_byte(ucByte);
    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.
     */
    down3_get_byte(pucByte);
    return TRUE;
}

/*需手动安装到串口接收中断服务函数*/
void
vMBPortSerialRecvISR(void)
{
    prvvUARTRxISR();
}

/*需手动安装到串口发送中断服务函数*/
void
vMBProtSerialSendISR(void)
{
    prvvUARTTxReadyISR();
}

/* 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(  );
}

4.事件
协议栈使用前后台架构,中断产生 事件 ,主循环处理 事件

事件生产者消费者描述
EV_READY定时器中断 (porttimer.c)
prvvTIMERExpiredISR
主循环 (mb.c)
eMBPoll
协议栈初始化完毕
EV_FRAME_RECEIVED定时器中断 (porttimer.c)
prvvTIMERExpiredISR
主循环 (mb.c)
eMBPoll
接收到一帧数据
如果数据帧校验正确,则产生 EV_EXECUTE 事件
EV_EXECUTE主循环 (mb.c)
eMBPoll
主循环 (mb.c)
eMBPoll
解析命令,生成应答数据,添加 CRC ,启动数据发送,数据将由串口发送中断发送
EV_FRAME_SENT串口发送中断 (portserial.c)
prvvUARTTxReadyISR
主循环 (mb.c)
eMBPoll
应答数据全部发送完成

事件一般用队列实现,以便消费者来不及处理事件时,暂时保存事件。对于简单应用,如果满足消费者消费事件的速度 大于等于 生产者生产事件的速度,则可以使用协议栈 \freemodbus\demo\BARE\port\portevent.c 文件中的源码,直接使用,不用修改:

#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;
}

在发送事件处就可以完成的功能,为什么要绕一圈非得用事件来完成呢?
方便解耦。
对于裸机环境,使用事件将处理过程从中断转移到主循环,从而使中断服务函数简单。
对于有操作系统的应用,事件可以方便的实现操作系统移植层,实现协议栈进程与中断之间的通讯。协议栈进程会因为等待事件而进入阻塞状态。

相关文章:

  • C库函数文件操作(fopen、fread、fwrite、fclose)
  • QT之OpenGL深度测试
  • 初阶指针C
  • 【C++】CC++内存管理
  • 每日学术速递2.9
  • python selenium浏览器复用技术
  • Chapter4:机器人仿真
  • 我的网站上线了!
  • MySQL篇02-三大范式,多表查询
  • C进阶:预处理
  • ESP8266 + STC15基于AT指令通过TCP通讯协议获取时间
  • 编译与链接------《程序员的自我修养》
  • python(14)--集合
  • Minikube vs. kind vs. k3s vs k3d vs MicroK8s
  • SpringCloud 中 Config、Bus、Stream、Sleuth
  • 《深入 React 技术栈》
  • 【347天】每日项目总结系列085(2018.01.18)
  • 2017年终总结、随想
  • CSS 提示工具(Tooltip)
  • Docker容器管理
  • HashMap ConcurrentHashMap
  • JS变量作用域
  • linux学习笔记
  • Markdown 语法简单说明
  • Phpstorm怎样批量删除空行?
  • Python_OOP
  • Terraform入门 - 1. 安装Terraform
  • webpack入门学习手记(二)
  • 聊聊hikari连接池的leakDetectionThreshold
  • 手机app有了短信验证码还有没必要有图片验证码?
  • 远离DoS攻击 Windows Server 2016发布DNS政策
  • 【云吞铺子】性能抖动剖析(二)
  • PostgreSQL之连接数修改
  • ​【原创】基于SSM的酒店预约管理系统(酒店管理系统毕业设计)
  • #数学建模# 线性规划问题的Matlab求解
  • #我与Java虚拟机的故事#连载04:一本让自己没面子的书
  • (12)Hive调优——count distinct去重优化
  • (动手学习深度学习)第13章 计算机视觉---微调
  • (二)PySpark3:SparkSQL编程
  • (附源码)计算机毕业设计SSM教师教学质量评价系统
  • (三)mysql_MYSQL(三)
  • (提供数据集下载)基于大语言模型LangChain与ChatGLM3-6B本地知识库调优:数据集优化、参数调整、Prompt提示词优化实战
  • (原創) 如何讓IE7按第二次Ctrl + Tab時,回到原來的索引標籤? (Web) (IE) (OS) (Windows)...
  • (转)MVC3 类型“System.Web.Mvc.ModelClientValidationRule”同时存在
  • (转)从零实现3D图像引擎:(8)参数化直线与3D平面函数库
  • .class文件转换.java_从一个class文件深入理解Java字节码结构
  • .equals()到底是什么意思?
  • .NET 4 并行(多核)“.NET研究”编程系列之二 从Task开始
  • .NET C# 使用 SetWindowsHookEx 监听鼠标或键盘消息以及此方法的坑
  • .NET CORE使用Redis分布式锁续命(续期)问题
  • .NET Project Open Day(2011.11.13)
  • .NET/C# 中设置当发生某个特定异常时进入断点(不借助 Visual Studio 的纯代码实现)
  • .net遍历html中全部的中文,ASP.NET中遍历页面的所有button控件
  • .NET简谈设计模式之(单件模式)
  • [2544]最短路 (两种算法)(HDU)