STM32 MODBUS协议-简介及接入 FreeMODBUS

本文深入解析Modbus协议的工作原理,包括其物理连接、软件架构及现场总线概念。并通过实例演示如何在STM32平台上移植Modbus协议栈,实现主机与从机之间的通信。

简介

随着近年来物联网行业的迅速发展,工业物联网领域也成为了最大子领域之一。另外的领域包括运输业物联网、基础设施物联网、消费者物联网。

受制于体积、功耗、成本等因素,一部分设备无法直接接入物联网服务。对于这种设备,目前行业的解决方案通常是单独设置一个网关设备,无法直接接入网络的设备通过有线连接到网关,通过一定的协议将数据通过网关转发到上层网络。

这种连接方式和协议一起叫做现场总线(Field bus),现场总线的协议和一般的单片机常用UART、I2C、SPI、SDMMC等协议不同。现场总线协议要求更高的容错纠错率、抗干扰和易部署性,通常现场总线的长度都在几十米以上,普通的UART、I2C、SPI协议无法在这么长的长度上进行工作。

常用的现场总线协议有:
1.Modbus
2.CAN (常用于汽车)
3.Foundation Fieldbus




Modbus协议介绍

物理连接

Modbus的物理连接方式如下图所示:
在这里插入图片描述



软件架构

如果通过OSI七层网络模型来说的话,Modbus协议仅仅位于第二层:数据链路层。
在这里插入图片描述



因为处于的层次非常低,几乎不涉及到其他协议(实际上还涉及到串口UART协议,后面会讲),所以Modbus协议非常的单纯,几乎只是把物理层的电信号进行了一下封装。

当然这也不意味着Modbus协议就非常简单,实际上,如果你大学是学计算机专业的,一定学过一门叫做计算机网络的课,这门课通常的课时是50个小时左右,而这门课的内容大部分是在讲处于应用层的TCP/UDP协议。所以理论上来讲,学习Modbus协议至少也要20个小时的课时吧。(所以前两遍学不会也不要慌张,很正常O(∩_∩)O哈哈~)

废话少说,言归正传。看一下下面这张图:
在这里插入图片描述
上图表示的是:Modbus线上的信号是0/1的形式,Modbus协议会将多个0/1信号进行划分和组合,形成一个Modbus帧。一个Modbus帧就是一次Modbus请求或者Modbus回应

实际上,Modbus还利用了串口UART协议,可以理解为Modbus是建立在UART协议之上的协议。但是由于其实在是不复杂,所以我个人认为还是把其定义为数据链路层比较OK。因此Modbus中也有波特率、数据位、校验位、停止位的概念,Modbus协议只取串口UART协议中的数据位作为数据。 由于串口协议数据位通常是8位,所以Modbus帧的数据划分也是以8位、16位这样进行划分的。

Modbus和http协议很像,一次请求,一次回应。
只能主机向从机发送一次Modbus请求,然后从机响应一次Modbus回应。
从机不能主动向主机发送任何信息。
在这里插入图片描述


寄存器

Modbus协议还定义了寄存器的概念。Modbus主机向从机发送的查询请求帧的数据部分,并不能像tcp/http协议那样自定义数据内容,而是只能是固定的格式:16位寄存器起始地址➕16位寄存器数量。

同时从机也只会固定的按照这种格式去解析来自主机的请求。解析请求之后,一般会提供给开发者一个这样的回调函数:
eMBErrorCode eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
开发者需要手动重写这个函数,来返回给主机期望的数据。

总而言之,可以理解Modbus请求还在应用层帮我们定义了一层寄存器的概念,所有Modbus数帧都必须按照寄存器的概念来发送/响应数据。这样加强了Modbus协议的通用性,同时我们在开发时也不用自己去想应用层的协议了。


举个实际的例子:

主机向一台设备地址为3的从机读取输入寄存器0~输入寄存器10的数据。波特率9600,数据位8,校验位无,停止位1:

  1. 首先主机会向从机发送一个起始信号,起始信号持续一定时间(3.5个字符周期)

  2. 然后开始发送数据,通过UART串口,依次发送以下电信号
    (注意串口是高位在前低位在后,最后的1代表1位停止位)
    11000001 (数据为0x03) // 代表设备地址为3
    00100001 (数据为0x04) // 代表功能码为0x04,读输入寄存器
    00000001 (数据为0x00) //
    00000001 (数据为0x00) // 和上一数据一起代表寄存器起始地址为0
    00000001 (数据为0x00) //
    01010001 (数据为0x0A) // 和上一数据一起代表寄存器数量为10
    111101111 (数据为0xEF) //
    100011101 (数据为0x71) // 和上一数据一起代表CRC校验码

    (转换为16进制顺序为:03 04 00 00 00 0A EF 71)
    你甚至可以通过串口直接发送 0x03 0x04 0x00 0x00 0x00 0x0A 0xEF 0x71,但是实际上你会发现可能有时候不行,因为Modbus协议在各种地方还做了一些时序的限制,比如3.5字符周期的起始信号、1.5字符周期的数据间隔等。经过作者我的实际测试,大部分情况下都是可以直接通过串口发数据的。

3.从机收到之后,会对数据进行校验,校验的内容包括但不限于:设备地址是否是本机地址、CRC校验码是否正确等

4.校验如果通过,则会回复给主机相应的数据,数据格式和上面大同小异。这里就不累述了(写起太累啦)。







移植Modbus协议栈(主机部分)

看了我上面的介绍,你是不是会觉得好像自己手写一个Modbus请求也不是特别困难,百八十行代码就能解决。确实是这样,但是=你还需要考虑超时、接收数据CRC校验、总线冲突等一系列问题,所以再加上这些内容就不只几百行代码能搞定了,所以我们还是使用现成的Modbus协议栈一般来说Modbus协议都跑在RTOS操作系统之上。

目前有的Modbus协议栈有,在github上搜一搜:
https://github.com/search?l=C&o=desc&q=modbus&s=stars&type=Repositories

搜出来start多且是用C语言写的有如下几个

  • libmodbus: 使用c语言写的Modbus库。支持win、mac、linux平台,不支持arm平台。主要是由于在stm32使用的arm-none-eabi-gcc是阉割版本的gcc,部分内置函数、对象不支持。如果使用这个库的话,需要仔细阅读代码,手动替换不支持的部分代码。
  • FreeModbus:一个奥地利人写的Modbus库,从机模式免费,主机模式收费。官网https://www.embedded-experts.at/en/freemodbus/about/
  • FreeModbus_Slave-Master-RTT-STM32:也是C语言写的Modbus库,针对MCU平台。这个库是rtthread的主要作者armlink大神在FreeModbus免费的从机基础上添加了主机模式。中文文档,用户多,比较推荐。

接下来我们选择 FreeModbus_Slave-Master-RTT-STM32 进行开发。上面有说到,FreeModbus_Slave-Master-RTT-STM32 的作者是rtthread的主要作者,所以和rtthread有不潜的py关系,甚至直接提供了一套在rtthread上的移植。github地址:https://github.com/armink/FreeModbus_Slave-Master-RTT-STM32

使用的开发板为淘宝f103开发板
淘宝链接



1.使用STM32CubeMX生成工程

  • 选择你的MCU,这里我用的是STM32F103RC
  • RCC中开启外部HSE时钟,外部时钟比HSI更稳定些。
  • Clock中时钟设置为最高主频72MHz。
  • SYS中开启DEBUG JTAG 4线(也就是SWD)
  • 打开串口1用于打印调试信息,波特率115200,校验位0,停止位1
  • 打开串口2用于Modbus协议,波特率9600,校验位0,停止位1
  • NVIC中,串口1和串口2中断都勾选Enabled
  • NVIC-Code generation中
    串口1勾选Generate IRQ handle,不勾选Call HAL Handle
    串口2不勾选Generate IRQ handle,不勾选Call HAL Handle
  • 接入RT-thread:参考官方文档 基于 CubeMX 移植 RT-Thread Nano 根据官方文档,需要1.取消生成HardFault_Handler、PendSV_Handler、SysTick_Handler三个中断函数
  • Project-Manager-Project 勾选Do not generate main()。
    主要是因为main函数需要自己写,不需要生成。
  • Project-Manager-Advanced Settings 所有函数勾选 Not Generate Function Call, 取消勾选 Visibility。
    主要是因为接入了rt-thread后,初始化工作需要在rt-thread初始化时进行,所以取消自动生成,并且把函数设置为non-static,全局可见。
  • Toolchain/IDE选择MDK-Keil,点击生成工程。



2.修改部分函数适配RT-thread

由于Modbus协议基于RT-thread,所以需要先稍稍修改一下RT-thread:

  1. board.c/rt_hw_board_init 函数中对MCU进行初始化,更改的后的rt_hw_board_init函数如下:
#include "main.h"
extern void SystemClock_Config(void);
extern void MX_GPIO_Init(void);
extern void MX_USART1_UART_Init(void);
extern UART_HandleTypeDef huart1;
/* 调试串口1接收数据的消息队列buffer */
static uint8_t consoleInputBuffer[256];
struct rt_messagequeue consoleInputMQ;

void rt_hw_board_init()
{
   
   
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
   
    /* 使用串口1作为调试串口,初始化一个消息队列保存串口1接收到的数据,并手动开启串口中断 */
    rt_err_t error = rt_mq_init(&consoleInputMQ,"consoleInputMQ",consoleInputBuffer,
                                1,sizeof(consoleInputBuffer),RT_IPC_FLAG_FIFO);
    RT_ASSERT(error == RT_EOK);
    SET_BIT(huart1.Instance->CR1, USART_CR1_PEIE | USART_CR1_RXNEIE);
    
    /* System Clock Update */
    SystemCoreClockUpdate();
    /* System Tick Configuration */
    _SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);
    

    /* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INIT
    rt_components_board_init();
#endif

#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
    rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());
#endif
}

2.增加一个打印输出的函数 rt_hw_console_output ,位置随意,代码如下:

extern UART_HandleTypeDef huart1
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值