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
#endifvoid 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 。
何时使能定时器?
- 启动协议栈(
eMBRTUStart
) - 接收到 1 字节数据(
xMBRTUReceiveFSM
):复位计数器,重新开始计时
何时关闭定时器?
- 停止协议栈(
eMBRTUStop
) - 超时发生(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;
}
在发送事件处就可以完成的功能,为什么要绕一圈非得用事件来完成呢?
方便解耦。
对于裸机环境,使用事件将处理过程从中断转移到主循环,从而使中断服务函数简单。
对于有操作系统的应用,事件可以方便的实现操作系统移植层,实现协议栈进程与中断之间的通讯。协议栈进程会因为等待事件而进入阻塞状态。
相关文章:

FreeModbus RTU 移植指南
FreeModbus 简介 FreeModbus 是一个免费的软件协议栈,实现了 Modbus 从机功能: 纯 C 语言支持 Modbus RTU/ASCII支持 Modbus TCP 本文介绍 Modbus RTU 移植。 移植环境: 裸机Keil MDK 编译器Cortex-M3 内核芯片(LPC1778/88&…...

《唐诗三百首》数据源网络下载
2023年的 元宵之夜,这场以“长安”为主题的音乐会火了!在抖音,超过2300万人次观看了直播,在线同赏唐诗与交响乐的融合。许多网友惊呼,上学时那些害怕背诵的诗句,原来还可以有这么美的表达这场近80分钟的音乐…...

(深度学习快速入门)第五章第一节2:GAN经典案例之MNIST手写数字生成
获取pdf:密码7281 文章目录一:数据集介绍二:GAN简介(1)简介(2)损失函数三:代码编写(1)参数及数据预处理(2)生成器与判别器模型&#x…...

雁过留痕,竟是病毒的痕迹?
凌恩生物全新升级宏病毒组分析流程;聚焦DNA,RNA病毒组研究热点;高灵敏度检测vOTUs;多软件整合,精准鉴定病毒序列;直击地化循环关键环节,助力宏病毒组科研成功!期刊:Micro…...

Linux基本功系列之sort命令实战
文章目录前言一. sort命令介绍二. 语法格式及常用选项三. 参考案例3.1 按照文本默认排序3.2 忽略相同的行3.3 按数字大小进行排序3.4 检查文件是否已经按照顺序排序3.5 将第3列按照数字大小进行排序3.6 将排序结果输出到文件四. 探讨 -k的高级用法总结前言 大家好,…...

【笔记】移动端自动化:adb调试工具+appium+UIAutomatorViewer
学习源: https://www.bilibili.com/video/BV11p4y197HQ https://blog.csdn.net/weixin_47498728/category_11818905.html 一、移动端测试环境搭建 学习目标 1.能够搭建java 环境 2.能够搭建android 环境 (一)整体思路 我们的目标是Andr…...

面试复习题--性能检测原理
1、布局性能检测 Systrace,内存优化工具中也用到了 Systrace,这里关注 Systrace 中的 Frames 页面,正常情况下圆点为绿色,当出现黄色或者红色的圆点时,表现出现了丢帧。 Layout Inspector,是 AndroidStudio 自带工具…...

@LoadBalanced 和 @RefreshScope 同时使用,负载均衡失效分析
背景 最近引入了 Nacos Config 配置管理能力,说起来用法很简单,还是踩了三个坑。 Nacos Config 的 nacos 的帐号密码加密配置后,怎么解密而且在 NacosConfigBootstrapConfiguration 真正注入 Nacos Config 注入之前,而且不能触发…...

2023年个人计划
2023年个人计划 可能是最近太清闲,感觉生活很无聊,就胡乱做下新年的规划吧,扰乱下烦闷的心 1 二宝健健康康,活泼可爱 目前老婆已经怀孕5周左右了,二宝将在进行年中降生,希望老婆少受点罪,二宝…...

加拿大访问学者家属如何办理探亲签证?
由于大多数访问学者的访学期限都为一年,家人来访不仅可以缓解访学的寂寞生活,而且也是家人到加拿大体验国外风情的好机会。家属在国内申请赴加签证时,如果材料齐全,一般上午递交了申请,下午就可以拿到签证。以下是家人…...

操作系统基础---多线程
文章目录操作系统基础---多线程1.为何引入线程程序并发的时空开销线程的设计思路线程的状态和线程控制块TCB2.线程与进程的比较3.线程的实现⭐1.内核支持线程KST2.用户级线程3.组合方式操作系统基础—多线程 1.为何引入线程 利用传统的进程概念和设计方法已经难以设计出适合于…...

2022-12-10青少年软件编程(C语言)等级考试试卷(六级)解析
2022-12-10青少年软件编程(C语言)等级考试试卷(六级)解析T1、区间合并 给定 n 个闭区间 [ai; bi],其中i1,2,...,n。任意两个相邻或相交的闭区间可以合并为一个闭区间。例如,[1;2] 和 [2;3] 可以合并为 [1;3…...

太酷了,用Python实现一个动态条形图!
大家好,我是小F~说起动态条形图,小F之前推荐过两个Python库,比如「Bar Chart Race」、「Pandas_Alive」,都可以实现。今天就给大家再介绍一个新的Python库「pynimate」,一样可以制作动态条形图,…...

单元测试junit+mock
单元测试 是什么? 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。至于“单元”的大小或范围,并没有一个明确的标准,“单元”可以是一个方法、类、功能模块或者子系统。 单元测试通…...

2022Q4手机银行新版本聚焦提升客群专属、财富开放平台、智能化能力,活跃用户规模6.91亿人
易观:2022年第4季度,手机银行APP迭代升级加快,手机银行作为零售银行服务及经营的主阵地,与零售银行业务发展的联系日益紧密。迭代升级一方面可以顺应零售银行发展战略及方向,对手机银行业务布局进行针对性调整优化&…...

YOLO-V1~V3经典物体检测算法介绍
大名鼎鼎的YOLO物体检测算法如今已经出现了V8版本,我们先来了解一下它前几代版本都做了什么吧。本篇文章介绍v1-v3,后续会继续更新。一、节深度学习经典检测方法概述1.1 检测任务中阶段的意义我们所学的深度学习经典检测方法 ,有些是单阶段的…...

SparkSQL 核心编程
文章目录SparkSQL 核心编程1、新的起点2、SQL 语法1) 读取 json 文件创建 DataFrame2) 对 DataFrame 创建一个临时表3) 通过SQL语句实现查询全表3、DSL 语法1) 创建一个DataFrame2) 查看DataFrame的Schema信息3) 只查看"username"列数据4) 查看"username"列…...

Android核心开发【UI绘制流程解析+原理】
一、UI如何进行具体绘制 UI从数据加载到具体展现的过程: 进程间的启动协作: 二、如何加载到数据 应用从启动到onCreate的过程: Activity生产过程详解: 核心对象 绘制流程源码路径 1、Activity加载ViewRootImpl ActivityThread…...

计算机组成原理第七章笔记记录
仅仅作为笔记记录,B站视频链接,若有错误请指出,谢谢 基本概念 演变过程 I/O系统基本组成 I/O软件 包括驱动程序、用户程序、管理程序、升级补丁等 下面的两种方式是用来实现CPU和I/O设备的信息交换的 I/O指令 CPU指令的一部分,由操作码,命令码,设备…...

ORB-SLAM2编译、安装等问题汇总大全(Ubuntu20.04、eigen3、pangolin0.5、opencv3.4.10)
ORB-SLAM2编译、安装等问题汇总大全(Ubuntu20.04、eigen3、pangolin0.5、opencv3.4.10) 1:环境说明: 使用的Linux发行版本为Ubuntu 20.04 SLAM2下载地址为:git clone https://github.com/raulmur/ORB_SLAM2.git ORB_SLAM2 2&a…...

QuickBuck:一款专为安全研究人员设计的勒索软件模拟器
关于QuickBuck QuickBuck是一款基于Golang开发的勒索软件模拟工具,在该工具的帮助下,广大研究人员可以通过更简单的方法来判断反病毒保护方案是否能够有效地预防勒索软件的攻击。 功能介绍 该工具能够模拟下列勒索软件典型行为,其中包括&a…...

【八大数据排序法】堆积树排序法的图形理解和案例实现 | C++
第二十一章 堆积树排序法 目录 第二十一章 堆积树排序法 ●前言 ●认识排序 1.简要介绍 2.图形理解 3.算法分析 ●二、案例实现 1.案例一 ● 总结 前言 排序算法是我们在程序设计中经常见到和使用的一种算法,它主要是将一堆不规则的数据按照递增…...

低代码开发平台|生产管理-生产加工搭建指南
1、简介1.1、案例简介本文将介绍,如何搭建生产管理-生产加工。1.2、应用场景在主生产计划列表中下达加工后,在加工单列表可操作领料、质检。2、设置方法2.1、表单搭建1)新建表单【产品结构清单(BOM)】,字段…...

Python类型-语句-函数
文章目录类型动态类型:变量类型会随着程序的运行发生改变注释控制台控制台输入input()运算符算术关系逻辑赋值总结语句判断语句while循环for循环函数链式调用和嵌套调用递归关键字传参在C/java中,整数除以整数结果还是整数,并不会将小数部分舍弃…...

真兰仪表在创业板开启申购:募资约20亿元,IPO市值约为78亿元
2月9日,上海真兰仪表科技股份有限公司(下称“真兰仪表”,SZ:301303)开启申购,将在深圳证券交易所创业板上市。本次上市,真兰仪表的发行价为26.80元/股,市盈率43.06倍。 据贝多财经了解…...

【2023】Prometheus-Prometheus与Alertmanager配置详解
记录一下Prometheus与Alertmanager的配置参数等内容 目录1.Prometheus1.1.prometheus.yml1.2.告警规则定义2.alertmanager2.1.alertmanager.yml2.1.1.global:全局配置2.1.1.1.以email方式作为告警发送方2.1.1.2.以wechat方式作为告警发送方2.1.1.3.以webhook方式作为…...

华为HCIE学习之openstack基础
文章目录一、Openstack各种文件位置二、Openstack命令操作1.使用帮助三、用命令发放云主机1、创建租户2、创建用户并与租户绑定3、注册镜像4、创建规格5、创建公有网络及其子网(做弹性IP用)6、创建私有网络及其子网7、创建路由并设置网关与端口8、创建安…...

Python实现贝叶斯优化器(Bayes_opt)优化BP神经网络分类模型(BP神经网络分类算法)项目实战
说明:这是一个机器学习实战项目(附带数据代码文档视频讲解),如需数据代码文档视频讲解可以直接到文章最后获取。1.项目背景贝叶斯优化器(BayesianOptimization) 是一种黑盒子优化器,用来寻找最优参数。贝叶斯优化器是基…...

Elasticsearch(九)搜索---搜索辅助功能(下)--搜索性能分析
一、前言 上篇文章我们学习了ES的搜索辅助功能的一部分–分别是指定搜索返回的字段,搜索结果计数,分页,那么本次我们来学习一下ES的性能分析相关功能。 二、ES性能分析 在使用ES的过程中,有的搜索请求的响应比较慢,…...

化繁为简|中信建投基于StarRocks构建统一查询服务平台
近年来,在证券服务逐渐互联网化,以及券商牌照红利逐渐消退的行业背景下,中信建投不断加大对数字化的投入,尤其重视数据基础设施的建设,期望在客户服务、经营管理等多方面由经验依赖向数据驱动转变,从而提高…...