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

SeanLib系列函数库-ModbusRTU


查看其它库函数说明,请点击此处跳转到SeanLib主页

1. 本篇内容

本篇讲解ModbusRTU库的用法,单片机使用此库可作为主站或从站与远程设置进行通讯,本库自动完成数据解析、打包和发送,大大简化了用户代码。

2. 使用说明

要使用本库,请将ModbusRTU.lib和ModbusRTU.h添加到项目工程中

2.1 头文件介绍

头文件中定义了一个RTU设备类型:

typedefstruct//定义一个Modbus RTU数据包结构体类型{unsignedcharIsMaster;//表示本设备是否是Modbus主站unsignedcharSite;//从站有效,表示本站站号unsignedshortCoilCount;//从站有效,存放允许主站访问的线圈点数unsignedshortRegCount;//从站有效,存放允许主站访问的寄存器数量unsignedshortBufSize;//RTU自身使用的数据缓存区大小unsignedchar*Buffer;//RTU自身要使用的数据缓存区指针unsignedchar*Coils;//从站有效,存放允许主站访问的线圈首地址,每个变量表示一个线圈unsignedshort*Regs;//从站有效,存放允许主站访问的寄存器首址,每个变量表示一个寄存器//从站专用,解析主站发送过来的数据,并做出响应RTU_Status_t(*SlaveHandle)(unsignedchar*,unsignedchar);//主站专用,读线圈函数,参数:从站站号、线圈地址、读取数量、存放线圈值的数组(char类型,1表示ON)RTU_Status_t(*ReadCoils)(unsignedchar,unsignedshort,unsignedchar,unsignedchar*);//主站专用,写一个线圈,参数:从站站号、线圈地址、线圈值(char类型,1表示ON)RTU_Status_t(*WriteCoil)(unsignedchar,unsignedshort,unsignedchar);//主站专用,写多个线圈,参数:从站站号、线圈地址、写入数量、要写入的线圈数组(char类型,1表示ON)RTU_Status_t(*WriteCoils)(unsignedchar,unsignedshort,unsignedchar,unsignedchar*);//主站专用,读寄存器, 参数:从站站号、起始地址、读取数量、存放寄存器的数组(short类型)RTU_Status_t(*ReadRegs)(unsignedchar,unsignedshort,unsignedchar,unsignedshort*);//主站专用,写一个寄存器,参数:从站站号、寄存器地址、要写入的数据RTU_Status_t(*WriteReg)(unsignedchar,unsignedshort,unsignedshort);//主站专用,写多个寄存器,参数:从站站号、起始地址、写入数量、要写入的寄存器数组(short类型)RTU_Status_t(*WriteRegs)(unsignedchar,unsignedshort,unsignedchar,unsignedshort*);}RTU_Device_t;

结构体中的成员都是私有,RTU设备创建之后就不要修改。
Modbus通讯需要区分主站和从站,本库分别提供了创建主站和从站设备的方法:

/******************************************************************************* * 功 能: 创建一个Modbus RTU主站设备 * 参 数: * BufSize : 指定为设备开辟的缓存区大小,用于存储通讯中发送和接收使用的数据包 * 返回值: 创建成功返回设备指针,否则返回空指针NULL *******************************************************************************/externRTU_Device_t*NewRtuMaster(unsignedshortBufSize);/******************************************************************************* * 功 能: 创建一个Modbus RTU从站设备 * 参 数: * Site : 本站站号 * BufSize : 指定为设备开辟的缓存区大小,用于存储通讯中发送和接收使用的数据包 * Coils : 指定要与主站同步的线圈起始地址,每个元素表示一个线圈,1表示ON,0表示OFF * CoilCount : 指定从站支持访问的线圈总数量 * Regs : 指定要与主站同步的寄存器起始地址 * RegCount : 指定从站支持访问的寄存器总数量 * 返回值: 创建成功返回设备指针,否则返回空指针NULL *******************************************************************************/externRTU_Device_t*NewRtuSlave(unsignedcharSite,unsignedshortBufSize,unsignedchar*Coils,unsignedshortCoilCount,unsignedshort*Regs,unsignedshortRegCount);

BufSize:指定缓存区大小,创建设备时会动态申请一块内存,用于存放modbut通讯的数据帧,大小计算方法为:

  1. 先确定一个命令最多可能读写的线圈数据M和寄存器数量N
  2. 计算线圈帧最大大小Size_M = 9 + (M+7)/8 字节,(M+7)/8需要向上取整
  3. 计算寄存器帧最大大小Size_N = 9 + 2N 字节
  4. 取Size_M和Size_N的最大值作为BufSize即可。建议额外增加一些,防止内存溢出

比如,一个写10个寄存器的帧大小为:9+2*10=29字节
另外,本库依赖两个外部数据接收和发送函数,请在SeanLib.c中实现:

//1.数据发送函数:Sean_Send//参数:// Dev: 调用该函数的设备,函数中可根据该指针判断要将数据发送到哪个通讯端口// Data: 要发送的数据指针// Len: 要发送的数据长度//返回值:无voidSean_Send(void*Dev,unsignedchar*Data,unsignedshortLen){if(Dev==PLC){HAL_UART_Transmit(&UART_PLC,Data,Len,30);}}//2.数据接收函数:Sean_Recv(对于RTU库仅主站需要,从站不需要该函数)//参数:// Dev: 调用该函数的设备,可根据该指针判断要将数据发送到哪个通讯端口// Data: 接收到的数据的存放指针// Len: 最多允许接收的数据长度//返回值: 实际接收到的数据长度unsignedshortSean_Recv(void*Dev,unsignedchar*Data,unsignedshortMaxLen){uint16_tRecv_Len;HAL_UARTEx_ReceiveToIdle(&UART_PLC,Data,MaxLen,&Recv_Len,30);returnRecv_Len;}

2.2 作为主站时的用法

单片机做主站时,可以主动发起通讯指令,读取或写入从站设备的线圈或寄存器,常用于连接如温控器、PLC、压力传感器之类的从站设备

2.2.1 创建主站设备

示例代码:

RTU_Device_t*Motor;//RTU设备初始化voidRTU_Init(void){Motor=NewRtuDevice(0);//用站号0创建RTU设备,表示本机是主站if(Motor==NULL){printf("Creat RTU device failed!\r\n");while(1);}Motor->Site=SITE;//设置从站站号}MSH_INIT_EXPORT(2,RTU_Init,ModbusRTU Package initialization);

2.2.2 编写外部函数

作为从站时只需要实现Sean_Send即可,请在SeanLib.c中实现该函数:

//1.数据发送函数://参数:// Dev: 调用该函数的设备,函数中可根据该指针判断要将数据发送到哪个通讯端口// Data: 要发送的数据指针// Len: 要发送的数据长度//返回值:无voidSean_Send(void*Dev,unsignedchar*Data,unsignedshortLen){if(Dev==Motor){HAL_UART_Transmit(&huart2,Data,Len,Len);}}//2.数据接收函数:(对于RTU库仅主站需要,从站不需要该函数)//参数:// Dev: 调用该函数的设备,函数中可根据该指针判断要将数据发送到哪个通讯端口// Data: 接收到的数据的存放指针// Len: 最多允许接收的数据长度//返回值: 实际接收到的数据长度unsignedshortSean_Recv(void*Dev,unsignedchar*Data,unsignedshortMaxLen){unsignedshortLen=0;if(Dev==Motor){HAL_UARTEx_ReceiveToIdle(&huart2,Data,MaxLen,&Len,10000);}returnLen;}

2.2.3 读写从站寄存器或线圈

调用下列方法可以直接操作从站的寄存器和线圈:

//主站专用,读线圈函数,参数:从站站号、线圈地址、读取数量、存放线圈值的数组(char类型,1表示ON)RTU_Status_t(*ReadCoils)(unsignedchar,unsignedshort,unsignedchar,unsignedchar*);//主站专用,写一个线圈,参数:从站站号、线圈地址、线圈值(char类型,1表示ON)RTU_Status_t(*WriteCoil)(unsignedchar,unsignedshort,unsignedchar);//主站专用,写多个线圈,参数:从站站号、线圈地址、写入数量、要写入的线圈数组(char类型,1表示ON)RTU_Status_t(*WriteCoils)(unsignedchar,unsignedshort,unsignedchar,unsignedchar*);//主站专用,读寄存器, 参数:从站站号、起始地址、读取数量、存放寄存器的数组(short类型)RTU_Status_t(*ReadRegs)(unsignedchar,unsignedshort,unsignedchar,unsignedshort*);//主站专用,写一个寄存器,参数:从站站号、寄存器地址、要写入的数据RTU_Status_t(*WriteReg)(unsignedchar,unsignedshort,unsignedshort);//主站专用,写多个寄存器,参数:从站站号、起始地址、写入数量、要写入的寄存器数组(short类型)RTU_Status_t(*WriteRegs)(unsignedchar,unsignedshort,unsignedchar,unsignedshort*);

2.3 作为从站时的用法

单片机做从站时,需要及时响应主站发送的通讯指令,根据指令发送或改写本地的线圈或寄存器的值,常用于连接如HMI、PC之类的主站设备。
本地需要创建一个线圈和寄存器的数组,如:

uint8_tCoils[16]={0};//定义允许Modbus访问的线圈组uint16_tRegs[10]={0};//定义允许Modbus访问的寄存器组

工作流程:
串口接收数据–>解析请求帧–>生成响应帧–>发送响应帧
根据这个流程非常适合用DMA来接收和发送串口数据,后面的例子均按DMA的方案进行介绍。

2.3.1 创建从站设备

voidRTU_Init(void){PLC=NewRtuSlave(Config.Site,100,Coils,16,Regs,10);if(PLC==NULL){Error_Handle(0,"Creat rtu device failed!");}}MSH_INIT_EXPORT(1,RTU_Init,Creat modbus rtu slave);

Config.Site为本机站号

2.3.2 串口数据接收

voidUSART_IT_Callback(UART_HandleTypeDef*huart){uint8_ttemp;BaseType_t TaskWoken=pdFALSE;//使用之前必须初始化为Falseuint32_tSR=huart->Instance->SR;//读SR寄存器,检查中断标志uint32_tCR1=huart->Instance->CR1;//读CR1寄存器,检查中断使能状态if(huart==&UART_PC){...}elseif(huart==&UART_HMI){...}elseif(huart==&UART_PLC){if((SR&USART_SR_IDLE)&&(CR1&USART_CR1_IDLEIE))//发生了空闲中断,说明数据接收完成{temp=(uint8_t)huart->Instance->DR;//读一次DR,清除中断标志xTaskNotifyFromISR(Task_Uart,EVENT_UART_PLC,eSetBits,&TaskWoken);//发送任务通知}if((SR&USART_SR_TC)&&(CR1&USART_CR1_TCIE))//发生了发送完成中断{//DMA会开启该中断,因此需要在这里清除中断标志,或直接关闭中断使能__HAL_UART_DISABLE_IT(huart,UART_IT_TC);}}portYIELD_FROM_ISR(TaskWoken);//如果TaskWoken为true,进行一次上下文切换}

2.3.3 编写串口数据处理任务

//线程:串口数据处理线程voidStartTaskUart(void*argument){uint8_tcmd_PLC[50];//定义PLC的接收缓存区uint8_tLen_PLC=0,temp;uint8_tsite,func;EventBits_t EventBits;RTU_Status_t Ret;__HAL_UART_ENABLE_IT(&UART_PLC,UART_IT_IDLE);//开启PLC串口的空闲中断HAL_UART_Receive_DMA(&UART_PLC,cmd_PLC,sizeof(cmd_PLC));//启动DMA接收while(1){xTaskNotifyWait(//等待有串口数据到达0x00,//进入时不清除任何标志位EVENT_UART_ALL,//退出时清除相关标志位&EventBits,portMAX_DELAY);if(EventBits&EVENT_UART_PC)//PC的数据接收完成,执行命令解析处理{...}if(EventBits&EVENT_UART_HMI)//如果有HMI接收完成的事件{...}if(EventBits&EVENT_UART_PLC)//PLC的数据接收完成,执行Modbus数据解析处理{Len_PLC=sizeof(cmd_PLC)-__HAL_DMA_GET_COUNTER(&hdma_usart3_rx);HAL_UART_DMAStop(&UART_PLC);site=cmd_PLC[0];//从本包数据中取出站号func=cmd_PLC[1];//从本包数据中取出功能码Ret=PLC->SlaveHandle(cmd_PLC,Len_PLC);//解析Modbus命令if(Ret==RTU_Status_OK){if(site==Config.Site)//如果本包数据是发给本站的,需要判断是否主站更改了本站的线圈和寄存器{if(func==0x05||func==0x06||func==0x10)//如果主站是写操作,则需要触发主任务进行处理{xTaskNotifyGive(Task_Main);//通知主任务线圈被更改了}}}HAL_UART_Receive_DMA(&UART_PLC,cmd_PLC,sizeof(cmd_PLC));//再次启动DMA接收}}}

SlaveHandle函数会自动识别站号、功能码,根据功能码操作前面定义的线圈和寄存器数组Coils、Regs。只是需要注意在主站改写线圈或寄存器后,用户代码记得进行处理即可。

2.3.4 编写外部函数

作为从站时只需要实现Sean_Send即可,请在SeanLib.c中实现该函数:

//1.数据发送函数:Sean_Send//参数:// Dev: 调用该函数的设备,函数中可根据该指针判断要将数据发送到哪个通讯端口// Data: 要发送的数据指针// Len: 要发送的数据长度//返回值:无voidSean_Send(void*Dev,unsignedchar*Data,unsignedshortLen){if(Dev==PLC){UART_PLC.gState=HAL_UART_STATE_READY;//手动恢复串口的状态UART_PLC.hdmatx->State=HAL_DMA_STATE_READY;//手动恢复DMA的状态__HAL_UNLOCK(&hdma_usart3_tx);//手动解锁DMAHAL_UART_Transmit_DMA(&UART_PLC,Data,Len);//用DMA发送数据}}
http://www.jsqmd.com/news/1154117/

相关文章:

  • UL/CSA C22.2 NO. 62133-2报告在亚马逊DV验证下的合规定位与申报要求
  • 从零搭建自动化运维监控平台:阿里云ECS + Prometheus + Grafana 实战指南
  • JMeter接口自动化测试框架搭建:从脚本到工程化实践
  • 从Codex核心原理到实战:AI编程助手完整指南与项目集成
  • 提取word所有图片至特定文件夹-宏命令
  • 重庆禾赞科技有限公司是一家智慧农业全链路服务商
  • 嵌入式软件架构设计:分层思想、设计规范与开源实践
  • APP开发公司哪家好?2026年外包供应商分类对比与D-coding选型观察
  • Linux用户管理与文件权限完全指南:用户/组管理、权限解读、chmod/chown实战
  • 【AI逆向】APT组织“蔓灵花”某恶意样本分析报告
  • 广州建博会看点:当家居品牌扎堆卷产品,奇兵到家带着460万+师傅卷售后
  • 2026 ITSM产品选型全景:四大核心产品对决,国产化方案如何重塑企业服务管理价值
  • ADP5350与STM32L152RE低功耗电源管理方案详解
  • 2026 湖南工程职称指导服务流程解析 | 从诊断到答辩,透明可查
  • 基于AI的科研论文写作全流程实战:从文献拆解到项目管理的效率革命
  • 2026最新CC-Switch配置DeepSeek安装包下载安装教程,完美适配Codex本地调用
  • 微信小程序源码解析3步指南:用unveilr深入理解小程序架构
  • Sunshine游戏串流服务器完整指南:搭建私人云游戏的终极教程
  • Krita Vision Tools:告别繁琐手动选区,AI智能工具如何重塑数字创作工作流
  • Awoo Installer:Switch游戏安装终极指南 - 简单三步搞定NSP/XCI格式
  • ⑦Pads Layout PCB转 嘉立创(专业版)_PCB
  • Gemini 2.5 Flash / Flash-Lite 高并发 API 选型:高 RPM 和低成本为什么比单次 benchmark 更重要
  • 终极指南:5分钟免费掌握AMD Ryzen处理器隐藏性能解锁工具
  • SOX2抗体如何解析垂体腺瘤干细胞特性调控?
  • 微信小程序反编译工具unveilr:技术分析与实战应用指南
  • 图形验证码接入效率与长期运维成本综合评估:自建方案与企讯通Qcaptcha等第三方服务的全链路对比
  • 爬虫代码质量提升:从能跑到能维护的进化之路
  • 计算机毕业设计之基于SSM农产品推广系统
  • 网页截图总是不完整?Chrome全屏截图插件一键搞定超长页面
  • 微信小程序源码提取神器:用unveilr轻松反编译小程序包