FreeModbus RTU 移植指南

news2024/11/28 20:57:47

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/331369.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Intel x86_64 PMU简介

文章目录前言一、性能监控概述二、CPUID information三、架构性能监控3.1 架构性能监控 Version 13.1.1 架构性能监控 Version 1 Facilities3.1.2 预定义的体系结构性能事件3.1.3 cmask demo测试参考资料前言 Intel 64 和 IA-32 架构提供了 PMU&#xff08;Performance Monito…

Oracle Data Guard Apply服务

1. Apply服务介绍 Apply服务自动应用redo到备数据库来保持与主数据库的同步和允许事务一致性访问数据。 缺省情况下&#xff0c;Apply服务等待备redo日志文件进行归档&#xff0c;然后再应用归档日志文件包含的redo。然而&#xff0c;可以启用实时Apply&#xff0c;允许当前的…

CentOS8基础篇2:文件系统

一、文件系统概述 1.文件系统的基本概念 操作系统中负责管理和存储文件信息的软件机构称为文件管理系统&#xff0c;简称文件系统。它规定了文件的存储方式及文件索引方式等信息。文件系统主要由三部分组成&#xff0c;分别是与文件管理相关的软件、被管理的文件和实施文件管…

ip-guard如何查看客户端连接的服务器IP地址?

在客户端上通过“运行”输入win.ini打开文件(在目录C:\\Windows\\),可以从里面找到一个字段SIP,比如SIP=3232237616 再将其换算成16进制数为c0a80830

每天10个前端小知识 【Day 8】

前端面试基础知识题 1. Javascript中如何实现函数缓存&#xff1f;函数缓存有哪些应用场景&#xff1f; 函数缓存&#xff0c;就是将函数运算过的结果进行缓存。本质上就是用空间&#xff08;缓存存储&#xff09;换时间&#xff08;计算过程&#xff09;&#xff0c; 常用于…

在CANoe/CANalyzer中给CAN Log.asc/blf文件“瘦身”

案例背景&#xff08;共7页精讲&#xff09;&#xff1a; 该篇博文将告诉您&#xff0c;如何给离线文件CAN Log.asc/blf文件“瘦身”&#xff1a;批量删除/过滤 CAN Log中&#xff0c;不需要的CAN ID和CAN channel。 目录 1 准备工作 2 插入CAN Filter 3 保存“瘦身” 后的…

一种RK3399+MIPI+FPGA的高速工业相机的设计方案(一)

目 前 &#xff0c; 嵌 入 式 相 机 逐 渐 代 替 了 传 统 相 机 进 入 大 众 的 视 野 &#xff0c; 应 用 在 公 安 刑 侦 、 生 物 医 学和 文 物 保 护 等 诸 多 领 域 。 但 是 随 着 人 们 对 图 像 视 觉 成 像 质 量 追 求 的 提 升 &#xff0c; 图 像 传 感 器 的 特…

ESP32S3系列--SPI主机驱动详解(一)

一、目的SPI是一种串行同步接口&#xff0c;可用于与外围设备进行通信。ESP32S3自带4个SPI控制器外设&#xff08;Master&#xff09;&#xff0c;其中SPI0/SPI1内部专用,共用一组信号线,通过一个仲裁器访问外部Flash和PSRAM&#xff1b;SPI2/3各自使用一组信号线&#xff1b;开…

【C++】二叉树的前序中序后序非递归实现

文章目录二叉树的前序遍历二叉树的中序遍历二叉树的后序遍历总结二叉树的前序遍历 前序遍历的顺序是根、左、右。任何一颗树都可以认为分为左路节点&#xff0c;左路节点的右子树。先访问左路节点&#xff0c;再来访问左路节点的右子树。把访问左路节点的右子树看成一个子问题…

VUE3 插件的开发和使用

在构建 Vue 项目的过程中&#xff0c;离不开各种开箱即用的插件支持&#xff0c;用以快速完成需求&#xff0c;避免自己造轮子。 在 Vue 项目里&#xff0c;可以使用针对 Vue 定制开发的专属插件&#xff0c;也可以使用无框架依赖的通用 JS 插件&#xff0c;插件的表现形式也是…

51单片机学习笔记_11 蜂鸣器,识简谱,根据简谱编写蜂鸣器代码

蜂鸣器实验 蜂鸣器简单地说&#xff0c;就是电磁线圈和磁铁对振动膜的作用。 单片机的是无源蜂鸣器&#xff0c;不能一直充电&#xff0c;需要外部控制器发送震荡信号&#xff0c;可以改变频率产生不同的音色、音调。 大多数有源蜂鸣器则没有这个效果&#xff0c;有源蜂鸣器…

JavaScript(四)-全面详解(学习总结---从入门到深化)

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是小童&#xff0c;Java开发工程师&#xff0c;CSDN博客博主,Java领域新星创作者 &#x1f4d5;系列专栏&#xff1a;前端、Java、Java中间件大全、微信小程序、微信支付、若依框架、Spring全家桶 &#x1f4e7;如果文章…

JAVA开发(Redis的主从与集群)

现在web项目无处不在使用缓存技术&#xff0c;redis的身影可谓无处不在。但是又有多少项目使用到的是redis的集群&#xff1f;大概很多项目只是用到单机版的redis吧。作为缓存的一块&#xff0c;set &#xff0c;get数据。用的不亦乐乎。但是对于高可用系统来说&#xff0c;数据…

Tomcat简介

目录 一、Tomcat简介 二、下载安装Tomcat 三、利用Tomcat部署静态页面 一、Tomcat简介 Tomcat是一个HTTP服务器&#xff0c;可以按照HTTP的格式来解析请求来调用用户指定的相关代码然后按照HTTP的格式来构造返回数据。 二、下载安装Tomcat 进入Tomcat官网选择与自己电脑…

电子科技大学人工智能期末复习笔记(二):MDP与强化学习

目录 前言 期望最大搜索&#xff08;Expectimax Search&#xff09; ⭐马尔科夫决策&#xff08;MDP&#xff09;——offline&#xff08;超重点&#xff09; 先来看一个例子 基本概念 政策&#xff08;Policy&#xff09; 折扣&#xff08;Discounting&#xff09; 如…

Mysql中的事务

1. MyIsam是不支持事务的&#xff0c; InnoDB支持 2.事务的四大特性ACID 原子性&#xff08;Atomicity&#xff09;&#xff1a;一个事务中的所有操作&#xff0c;要么全部完成&#xff0c;要么全部不完成&#xff0c;不会结束在中间某个环节&#xff0c;而且事务在执行过程中…

PythonWeb开发基础(四 完)Response使用及wsgify装饰器

课程地址&#xff1a;Python 工程师进阶技术图谱 文章目录&#x1f33e; Response使用及wsgify装饰器1、Response的使用2、wsgify装饰器&#x1f33e; Response使用及wsgify装饰器 1、Response的使用 前面一节我们知道了&#xff0c;使用webob的Request模块可以很方便地对请求…

若依框架---PageHelper分页(十五)

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是小童&#xff0c;Java开发工程师&#xff0c;CSDN博客博主&#xff0c;Java领域新星创作者 &#x1f4d5;系列专栏&#xff1a;前端、Java、Java中间件大全、微信小程序、微信支付、若依框架、Spring全家桶 &#x1f4…

STM32开发(7)----CubeMX配置串口通讯(轮询方式)

CubeMX配置串口通讯&#xff08;轮询方式&#xff09;前言一、串口的介绍二、实验过程1.实验材料2.STM32CubeMX配置PWM3.代码实现重载printf轮询接收4.编译烧录5.硬件连接6.实验结果重载printf结果串口轮询接收结果总结前言 本章介绍使用STM32CubeMX对串口进行配置的方法&…

​力扣解法汇总1797. 设计一个验证系统

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣 描述&#xff1a; 你需要设计一个包含验证码的验证系统。每一次验证中&#xff0c;用户会收到一个…