1./**
2.* 定时器处理函数,1毫秒调用一次
3.* 在定时器中断中调用,判断帧是否结束.modbus要求帧结束的标志为总线上有3.5个字符传输时间空闲
5.void modbus_handle_timer(void)
7. if(modbus_rec.rec_timer_flag==START_REC_TIMER)
8. {
9. modbus_rec.rec_timer++;
10. }
11. else
12. {
13. modbus_rec.rec_timer=0;
14. }
16. if(modbus_rec.rec_timer>超时时间) //找到一帧数据
17. {
18. modbus_rec.rec_timer_flag=0;
19. modbus_rec.rec_timer=0;
20. modbus_rec.rec_end_flag=FARME_END_FALG;
22. //其它处理
23. }
3. Modbus TCP
Modbus协议在TCP/IP上的实现是在TCP/IP协议层上的应用,它需要一个完整的TCP/IP协议栈做支撑。
Modbus帧由TCP层提供,不需要像串行链路那样自己判断一帧是否结束,所有数据传输由TCP/IP层处理,Modbus帧结构见图3-1所示,最大帧数据长度为260字节。与串行链路相比,Modbus TCP帧多了MBAP报文头,少了地址码和校验字段。 因为TCP本身就是被设计为安全交付型协议,所以校验部分就交给TCP层来处理。Modbus TCP通过IP地址以及端口号来唯一确定一个设备,此外在MBAP报文头中也包含一个协议标识符字段,在获取到数据帧后,需要判断这个字段值是否为0。
其中,MBAP报文头包括下列字段,见表2-1
表2-1 MBAP报文头的字段
字 段 | 长 度 | 描 述 | 客 户 机 | 服 务 器 |
事务处理标识符 | 2字节 | Modbus请求/响应事务处理的识别 | 由客户机设置 | 服务器从接收的请求中重新复制 |
协议标识符 | 2字节 | 0=Modbus | 由客户机设置 | 服务器从接收的请求中重新复制 |
长度 | 2字节 | 随后字节的数量(包括单元标识符) | 由客户机设置 | 由服务器设置(响应) |
单元标识符 | 1字节 | 串行链路或其他总线上连接的远程从站的识别 | 由客户机设置 | 服务器从接收的请求中重新复制 |
3.1 lwIP协议栈
Modbus TCP需要一个完整的TCP/IP协议栈做支撑,目前公司使用TCP/IP协议栈多为lwIP协议。该协议栈专为嵌入式系统而设计,可在资源匮乏的微控制器上实现完整的可裁剪的TCP/IP协议。下面简单介绍一下lwIP移植。
3.1.1编写与编译器相关的头文件
头文件cc.h主要完成协议栈内部使用的数据类型定义,用户需要根据自己使用的编译器和处理器特性来定义好这些数据类型长度;除此之外,cc.h文件还要完成临界代码保护、协议栈调试信息输出相关宏、大小端定义等。
3.1.2编写与硬件的接口函数
lwIP协议栈已经在ethernetif.c文件中给出了硬件接口函数的原型,一共5个函数的框架,包括函数名、函数参数、函数内容等,用户需要完成这5个函数的编写。当然,你也可以按照自己的需求来编写底层硬件接口函数,不必和ethernetif.h文件中给出的函数相同。大多数应用是以这5个函数为基础的,所以我们这里来讨论一下这五个函数。
3.1.2.1 网卡初始化
1. static void low_level_init(struct netif *netif)
主要完成对底层硬件MAC和PHY芯片的初始化工作,此外还需要设置协议栈网络接口管理结构netif中与网卡属性相关的字段,比如网卡MAC地址长度等。
3.1.2.2 数据包发送
1. static err_t low_level_output(struct netif *netif,struct pbuf *p)
将内核数据结构pbuf描述的数据包发送出去。
3.1.2.3 数据包接收
1. static struct pbuf * low_level_input(struct netif *netif)
要将网卡接收的数据封装到内核能识别的pbuf形式。
3.1.2.4 接收数据初步解析
1. static void ethernetif_input(struct netif * netif)
调用数据包接收函数low_level_input从网卡处读取一个数据包,然后解析该数据包类型(ARP或IP包),最后将该数据包递交给相应的上层。对于一般无操作系统应用,该函数可以直接使用。
3.1.2.5 底层初始化
1. err_t ethernetif_init(struct netif * netif)
由协议栈自动调用该函数,主要完成netif结构中某些初始化,并最终调用low_level_init完成对网卡的初始化。对于一般应用,该函数可以直接使用。
3.2 lwIP 应用关键事项
对于Modbus TCP应用,使用lwIP协议栈还需要一些额外注意的点:
- 禁止Nagle算法,以便允许lwIP发送小数据包。如果不禁用,lwIP的默认机制会尽量等到更多的数据,再一起发送,这样不利于控制的实时性。lwIP提供了禁用Nagle算法的函数。
- 使能并更改保活机制。客户端和服务器建立连接后,如果二者长时间进行通讯,服务器会发送探测包,来检测客户端是否还在线,如果这时客户端没有响应服务器的探测包,服务器端会释放相应的资源,以便接受其它客户端连接。但默认情况下,lwIP并没有开启这个功能,需要手动开启。
4. Modbus应用层
无论是Modbus串行链路还是Modbus TCP接收到的一帧数据,都要将协议数据单元(PDU,即图2-1和图3-1的功能码及数据区域部分)从一帧中剥离出来提交给Modbus应用层处理,应用层根据功能码来执行相应的操作。Modbus有着众多功能码,其中某些功能码还具有子码,可以根据具体应用来实现这些功能码的全部或一部分,一般数据访问功能码都是需要实现的。
关于功能码的描述可以参考GB/T 19582.1-2008,下面介绍几个实现起来稍繁琐的功能码,并给出关键源代码。
4.1读线圈
主机或客户端使用该功能码从远程设备中读取1~2000个连续的线圈状态,请求帧中会指定第一个线圈地址和线圈数目。从机或服务器需要将每位一个线圈进行打包,第一个数据字节的LSB包含包含询问中所寻址的输出。其它线圈依次类推,一直到这个字节的高位为止,并在后续字节中按照从低位到高位的顺序排列。如果返回的输出数量不是8的倍数,将用零填充最后数据字节中的剩余位。
如果从机(客户端)设备的每一个线圈都占用1个字节存储,那么要应答读线圈是很简单的,这么做的好处是简化数据处理逻辑,提高响应速度;坏处是占用的RAM会增多。但如果线圈数量比较少,而微控制器的RAM又足够多的话,非常推荐这么处理。
1./**
2.* @brief 将要返回的线圈状态打包,在设备中每个线圈占1位
3.* @param coil_num:线圈数量
4.* @param src_data:指向第一个线圈所在的字节地址,按照字节读取
5.* @param send_buf:指向打包后的线圈存放地址,按照字节存放
7.void pack_coils(uint16_t coil_num,uint8_t *src_data,uint8_t *send_buf)
9. uint32_t tmp_data =0;
10. uint32_t data_addr=0;
11. uint32_t send_data_offset=0;
12. uint32_t i;
13. while(coil_num)
14. {
15. if(coil_num>=8)
16. {
17. for(i=0;i<8;i++)
18. {
19. if(src_data[data_addr++])
20. {
21. tmp_data += 1<<i ;
22. }
23. }
24. send_buf [send_data_offset++]=tmp_data;
25. tmp_data =0;
26. coil_num -= 8;
27. }
28. else
29. {
30. for(i=0;i<coil_num ;i++)
31. {
32. if(src_data[data_addr++])
33. {
34. tmp_data += 1<<i ;
35. }
36. }
37. send_buf [send_data_offset++]=tmp_data;
38. coil_num =0;
39. }
40. }
有些时候线圈数量非常多或者微控制器RAM资源紧张的情况下,一个线圈占用一个位存储空间,微控制器内的RAM通常是最小按照字节访问的,这样一字节存储空间可以放8个线圈。如果按照这种位存储,打包程序会变得繁琐,要考虑各各种情况,比如起始地址是否从整字节开始,线圈数量是否大于8个等等。下面给按位存储情况下的打包示例程序。
1./**
2.* @brief 将要返回的线圈状态打包,在设备中每个线圈占1位
3.* @param start_addr:线圈起始地址
4.* @param coil_num:线圈数量
5.* @param src_data:指向第一个线圈所在的字节地址,按照字节读取
6.* @param send_buf:指向打包后的线圈存放地址,按照字节存放
8.void pack_coils(uint16_t start_addr,uint16_t coil_num,uint8_t *src_data,
9. uint8_t *send_buf)
11. uint32_t data_addr=0;
12. uint32_t send_data_offset=0;
14. if(start_addr%8==0) //从整字节开始
15. {
16. while(coil_num)
17. {
18. if(coil_num>=8)
19. {
20. send_buf[send_data_offset++]=src_data[data_addr++];
21. coil_num-=8;
22. }
23. else
24. {
25. send_buf[send_data_offset]=src_data[data_addr];
26. send_buf[send_data_offset]&=((1<<coil_num)-1);
27. coil_num=0;
28. }
29. }
30. }
31. else //非整字节开始
32. {
33. uint32_t bit_nonint,bit_remainder,read_data,tmp_data;
35. bit_remainder =start_addr%8; //先处理非整字节的位
36. bit_nonint=8-bit_remainder;
37. if(bit_nonint<coil_num)
38. {
39. read_data=src_data[data_addr++];
40. tmp_data = read_data>>bit_remainder;
41. coil_num-=bit_nonint ;
42. while(coil_num)
43. {
44. if(coil_num>8)
45. {
46. read_data=src_data[data_addr++];
47. tmp_data+=read_data<<bit_nonint;
48. send_buf[send_data_offset++]=tmp_data; //够1字节则存盘
49. tmp_data=read_data>>bit_remainder;
50. coil_num-=8;
51. }
52. else
53. {
54. read_data=src_data[data_addr++];
55. if(coil_num>bit_remainder)
56. {
57. tmp_data+=read_data<<bit_nonint;
58. send_buf[send_data_offset++]=tmp_data;
59. tmp_data=read_data>>bit_remainder;
60. tmp_data &=((1UL<<(coil_num-bit_remainder))-1);
61. send_buf[send_data_offset]=tmp_data;
62. }
63. else
64. {
65. tmp_data+=(read_data & ((1UL<<coil_num)-1))<<bit_nonint ;
66. send_buf[send_data_offset++]=tmp_data;
67. }
68. coil_num=0;
69. }
70. }
71. }
72. else
73. {
74. read_data=src_data[data_addr];
75. tmp_data = read_data>>bit_remainder;
76. tmp_data &=((1UL<<coil_num)-1);
77. send_buf[send_data_offset]=tmp_data;
78. }
79. }
4.2 读离散量和写多个线圈
读离散量和写多个线圈这两个功能码的实现所遇到的问题跟读线圈类似,从机(服务器)设备都可以用不同的方法存储离散量和线圈,可以选择按字节存储,也可以选择按位存储,两种方法各有优点。如果设备条件允许,建议按照字节存储。由于实现方法类似于读线圈,所以不再给出打包(解包)源码。
5. 总结
本文介绍Modbus 串行链路和Modbus TCP的从机(服务器)设计的关键点,虽然有众多的文献、论文涉及到该内容,但本文不局限于文字,还以源码或伪代码的形式给出参考例程,可以帮助开发者更快的理解Modbus协议,缩短开发周期。
摘要:本文在国家标准GB/T 19582-2008的框架下,讨论Modbus协议在串行链路RS485以及TCP/IP上的实现过程和注意事项。涉及到Modbus帧界定、lwip协议栈移植等关键内容,对于难度较大的读写多个线圈命令,本文给出了关键源代码。1. 简介 从1979年开始,Modbus作为工业串行链路的事实标准,Modbus使成千上万的自动化设备能够通信。目前,对简
Modbus是一种串行通信协议,是Modicon公司(现为施耐德电气公司的一个品牌)于1979年为使用可编程逻辑控制器(PLC)通信而发表的。Modbus已经成为工业领域通信协议事实上的业界标准,并且现在是工业电子设备之间常用的连接方式。
【协议版本】
Modbus协议当前存在...
最近工作中需要用到
modbus通信,在查阅了相关资料后在stm32f1中实现了符合要求的
modbus协议。因为我的主机只需对保持寄存器(RW)进行单个或多个寄存器的读写,所以只需要实现对0x03(读寄存器)、0x06(写单个寄存器)、0x10(写多个寄存器)这三个功能码的响应。
我们首先要知道
modbus的命令帧结构如下:
关于
MODBUS - TCP协议,发现其在应用过程中很多人对其理解得五花八门,这里不妨再增加一门。
谈
MODBUS TCP协议肯定要分层看,
Modbus是应用层协议,其所依赖的网络层协议栈可以是TCP,也可以是UDP。而TCP又可以分为客户端和
服务器。有趣的是,
MODBUS-TCP由于其应用于全双工网络环境,注定其行为与
MODBUS-RTU/ASCII不同。
关于链接模式
常见的局域网链接模式,
MODBUS主机就是TCP客户端。
基于C#的Modbus TCP服务器端可以使用开源组件来实现。一个常用的组件是NuGet上的ModbusTcpNet。在使用之前,需要先进行实例化。
通过引用可以看到实例化ModbusTcpNet的示例代码。需要传入服务器的IP地址、端口号和站号。其中,IP地址为服务器的IP地址,端口号一般为502,站号可以设置为0-255。
引用提到,ModbusTcpNet组件可以方便地对Modbus TCP服务器进行读写操作。这个服务器可以是电脑端C#设计的,也可以是PLC实现的,或者是其他任何支持Modbus TCP通信协议的服务器。
如果不需要设置端口号和站号的话,可以使用引用中的示例代码进行实例化。只需要传入服务器的IP地址即可,默认端口号为502,站号为1。
总结起来,基于C#的Modbus TCP服务器端可以使用ModbusTcpNet组件来实现。实例化需要传入服务器的IP地址、端口号和站号。具体的代码示例可以参考引用中的示例代码。<span class="em">1</span><span class="em">2</span><span class="em">3</span>
#### 引用[.reference_title]
- *1* *2* *3* [C# ModBus Tcp读写数据 与服务器进行通讯](https://blog.csdn.net/weixin_30478923/article/details/95071862)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 100%"]
[ .reference_list ]