MODBUS通讯协议学习总结


最近学习了Modbus协议,通过UART驱动来实现控制板与上位机之间的通讯。这里把Modbus里须注意的点总结一下。
前面六点主要是对Modbus协议知识点的总结与归纳,后面记录了在开发过程中遇到的一些问题及相应的解决办法,如RTU模式下1.5/3.5字符超时时间如何处理等。

1 Modbus协议

1.1 协议概要

Modbus协议是一种单主/多从的协议,说白了,同一时间,总线上只能有一个主机,从机可以有多个(最多247个)。Modbus通讯的建立只能由主机发起,从机没收到来自主机的请求时,不会主动发送数据。从机之间不能通信,主机只能同时启动一个Modbus访问请求。见下图。
在这里插入图片描述
主机可以通过广播/单播模式发送Modbus请求报文。

单播模式广播模式
主机寻址单个从机主机寻址多个从机
报文可读可写报文只能写
从机地址任意(1至247)从机地址只能为0
从机必须应答从机无须应答

1.2 请求

主机发送的的请求报文包括从机地址、功能码、数据及CRC校验。
功能码 告知被选中的从机要执行什么功能;
数据段 里包含了从机要执行的功能的附加信息,这些信息必须包含要告诉从机的内容:从哪个寄存器开始读/写及要读的寄存器的数量;
CRC校验 验证数据是否正确。

1.3 应答

从机响应的应答报文包括从机地址、功能码、数据及CRC校验。
功能码 肯定报文中与请求报文中功能码一致;
数据段 包含了从机反馈的信息,如寄存器的状态或值。
CRC校验验证数据是否正确。

1.4 报文格式

Modbus里定义了数据传输的格式——PDU,PDU再加上另外的功能域构成ADU,如下图所示。
在这里插入图片描述
PDU:功能码+数据
ADU:地址+功能码+数据+CRC校验
受硬件限制(RS485/RS232的ADU最大为256字节),PDU最大内存空间为:256 - 从机地址(1字节)- CRC校验(2字节)= 253字节
所以完整的Modbus报文的构成就是:地址+功能码+数据+CRC校验,举例来讲:x向y发了一帧报文,报文内容为A+B+C+D,可以理解为x想从y的A地址里通过B命令获取C的值,D负责对A/B/C进行校验。

2 Modbus寄存器

2.1 寄存器种类

Modbus中的寄存器是一个宽泛的概念,并没有指MCU中某一特定的寄存器,可能是某一块内存区域。寄存器地址由开发者自己决定(实际开发过程中会有负责需求的同事整理好寄存器地址给到开发者,无须自己担心)。寄存器分类如下表。

寄存器种类说明与PLC类比举例
线圈状态输出端口,可读可写DO(数字量输出)电磁阀输出、MOSFET输出、LED显示等
离散输入状态输入端口,可读不可写DI(数字量输入)拨码开关、接近开关等
保持寄存器输出参数或保持参数,控制器运行时被设定的参数,可读可写AO(模拟量输出)模拟量输出设定值,PID运行参数,传感器报警值上、下限
输入寄存器输入参数,可读不可写AI(模拟量输入)模拟量输入

2.2 寄存器地址

Modbus寄存器地址分配如下表所示,仍然参照了PLC寄存器地址的分配方式。

寄存器种类寄存器PLC地址寄存器Modbus协议地址简称读写状态
线圈状态00001~099990000H~FFFFH0x可读可写
离散输入状态10001~199990000H~FFFFH1x可读
保持寄存器40001~499990000H~FFFFH2x可读可写
输入寄存器30001~399990000H~FFFFH3x可读

该表中的PLC地址可以理解为Modbus地址的变种,在触摸屏和PLC编程中应用较广泛。寄存器协议地址指的是通信时使用的寄存器寻址地址,例如PLC地址40001对应寻址地址0x0000,40002对应寻址地址0x0001。寄存器协议地址一般用16进制表示。另外,虽然PLC寄存器地址00001和10001对应的寄存器协议地址都是0000H,但是因为不同寄存器的功能码不同,需要使用不同的功能码进行访问,所以实际使用起来不会发生冲突。

3 Modbus串行消息帧格式

3.1 ASCII消息格式

当控制器以ASCII格式通信时,消息中每个8位的字节都将以两个ASCII字符发送。这种方式的优点是字符发送的时间间隔可达到1s而不产生错误。
ASCII模式下,报文以(:)字符(ASCII码:0x3A)开始,以回车换行符结束(ASCII码:0x0A,0x0D)。报文里其他段的内容以十六进制的字符来表征。那么从机只要不断检测总线上是否冒号发过来且确认地址是否为发给自己的。报文中字符发送时间间隔不能超过1s,否则从机将会认为这帧报文无效。

3.2 RTU消息格式

主从机在收发报文的时候,接收对象需要知道在报文的起始处开始接收,同时也要知道报文何时结束。另外当接收到不完整的报文时,能够清晰的设置错误标志。
RTU消息格式中,报文的接收和发送以至少3.5个字符时间的停顿间隔为标志。实际使用中,从机不断侦测总线,计算字符间的停顿时间,判断报文的起始点。当接收到报文时,从机开始解析该报文是否发给自己的。在最后一个字符传输结束后,一个至少3.5字符的停顿标志了该帧报文的结束。另外,报文的发送必须保证连续性,在发送过程中,若两个字符之间停顿的时长超过1.5个字符时间,则从机认为该帧无效,将丢弃该帧。
如下图所示,Modbus通信时规定主机发送完一组报文必须间隔3.5个字符再发一组报文,这3.5个字符主要用于告诉从机该帧报文已经发送结束。
在这里插入图片描述
3.5个字符时间间隔采用如下计算方式:
串行通信中,1个字符包含1个起始位、8个数据位、1个校验位(如有)及1个停止位。那么一个字符就包含了11个位,3.5个字符就有3.5×11=38.5个位。
按照9600bps的波特率来算,38.5×(1000/9600)= 4.01041ms,也就是说上图中两个Modbus报文帧之间的间隔只要超过4.01041ms即可。
每个消息帧的格式如下图所示。
在这里插入图片描述

3.3 地址域

地址域,指的是Modbus通讯帧中的地址字段,内容为从设备地址。Modbus消息帧中的地址域包含两个两个字符(ASCII模式)或一个字节(RTU模式)。
报文中可能的从设备地址为0至247,其中,单个设备的实际地址是1~247,0用作广播地址。

3.4 功能码域

功能码域,用于表示当前报文的功能。由一个字节构成,取值范围由1~255。
常用的功能码有01、03、04、06、16等,其中03的作用为读保持寄存器的值,04的作用为读输入寄存器的值,06的作用为写单个保持寄存器,16的作用为写多个保持寄存器。
从设备根据执行情况,若执行成功,返回的功能码与接收的相同;若执行失败,返回的否定应答中功能码中最高位(MSB)须置1。
此外,对于主机发出的功能码,从机根据自身配置决定是否支持此功能码,若不支持,反馈异常响应。

3.5 数据域

数据域与功能码紧密相连,作为功能码域的补充,存放功能码域需要操作的具体细节。

4 差错校验

在Modbus通信中,根据传输模式不同(ASCII或者RTU),差错校验采用不同的方法。
ASCII模式
该模式下,报文包括一个错误校验字段。该字段由两个字符构成,具体的值基于对全部报文内容执行的纵向冗余校验(LRC)计算的结果而来,计算对象不包含起始的冒号和最后的回车换行符。
RTU模式
该模式下,报文同样包含一个错误校验字段。该字段由两个字节构成,具体的值基于对全部报文内容执行的循环冗余校验(CRC)计算的结果而来,计算对象包含差错校验域之前所有的报文。

4.1 LRC校验

4.2 CRC校验

在Modbus RTU通讯模式下,通讯报文包括了一个基于循环冗余校验方法的差错校验字段。
CRC全称是循环冗余校验,特点是检错能力极强,开销小,易于用编码器及检测电路实现。CRC校验包含了多个版本,常用的CRC校验有CRC-8、CRC-16、CRC-32、CRC-64 。
Modbus协议中,采用的是CRC-16的标准校验方法。在RTU模式下,CRC自身由两个字节构成,即CRC是一个16位的值。CRC字段校验针对整个报文的内容,无论报文中的单个字节采用何种奇偶校验方式,整个报文均采用CRC-16校验算法。
从机在接收报文时,会通过CRC算法重新计算接收到的报文的CRC值,并把计算得到的值与CRC字段中接收的实际值进行比较。若两者不同,则产生一个错误,并返回一个异常响应报文告知主机。
需要注意的是,CRC-16校验值由两个字节构成,所以涉及到哪个字节在前、哪个字节在后的问题,即大小端的问题。

5 大小端

大端存储模式:数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端存储模式:数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中;
我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

6 功能码

6.1(0x01)01读取线圈输出值

6.2(0x02)02读取离散输入值

6.3(0x03)03读取保持寄存器值

6.3.1 功能说明

该功能码用于读取保持寄存器的值,不支持广播模式。报文中指定了读取的保持寄存器的起始地址和数量

6.3.2 查询报文

主机发送的查询报文中,必须指定查询寄存器的数量和起始地址。如:需要从设备地址为07的从机中读取寄存器地址从0x00C8开始的3个寄存器中的值。见下表:

字段例(RTU模式)
地址域0x07
功能码域0x03
数据域(寄存器地址高位)0x00
数据域(寄存器地址低位)0xC8
数据域(寄存器数量高位)0x00
数据域(寄存器数量低位)0x03
差错校验CRC

此例中,寄存器地址由2个字节构成,取值范围为0x0000-0xFFFF;寄存器数量由2个字节构成,取值范围为0x0001-0x007D,即最多可以连续读125个寄存器的值。

6.3.3 响应报文

响应报文的构成和意义见下表。由于Modbus中保持寄存器和输入寄存器的值都是两个byte,因此上述例子中读取3个寄存器的值将返回6个byte。数据域里的值分别是6个字节数及3个寄存器里的数值。

字段例(RTU模式)
地址域0x07
功能码域0x03
数据域(字节数)0x06
数据域(数据1高位)0x03
数据域(数据1低位)0x53
数据域(数据2高位)0x01
数据域(数据2低位)0xF3
数据域(数据3高位)0x01
数据域(数据3低位)0x05
差错校验CRC

6.4(0x04)04读取输入寄存器

6.5(0x05)05写单个线圈/离散输出状态

6.5.1 功能说明

此功能码用于将单个线圈寄存器设置为ON或OFF。此功能支持广播模式,在广播模式下,总线上所有从机的同一寄存器地址的值会被统一修改。查询报文中的ON/OFF状态由报文数据字段的常数指定,0xFF00表示ON状态,0x00FF表示OFF状态,其他值均为非法值。

6.5.2 查询报文

查询报文需要指定从设备地址以及需要变更的线圈地址和设定的状态值。需要注意,查询报文中,线圈地址从0开始计数。如,要设置线圈地址为00150为ON状态,则查询报文中线圈地址设置为0x95(00149),见下表。

字段例(RTU模式)
地址域0x03
功能码域0x05
数据域(寄存器地址高位)0x00
数据域(寄存器地址低位)0x95
数据域(寄存器数量高位)0xFF
数据域(寄存器数量低位)0x00
差错校验CRC

6.5.3 响应报文

响应报文的各项构成和意义见下表。对于从机而言,在线圈或离散输出寄存器正常变更的情况下,则返回与查询报文一样的响应报文。若修改失败,则反馈异常响应。

字段例(RTU模式)
地址域0x03
功能码域0x05
数据域(寄存器地址高位)0x00
数据域(寄存器地址低位)0x95
数据域(寄存器数量高位)0xFF
数据域(寄存器数量低位)0x00
差错校验CRC

6.6(0x06)06写单个保持寄存器

6.6.1 功能说明

此功能码用于修改单个保持寄存器的值,支持广播模式。在广播模式下,所有从机相同寄存器地址的寄存器的值都会被统一修改。

6.6.2 查询报文

此类型报文需要指定要修改的寄存器地址及设定的值。需要注意,查询报文中,寄存器地址从0开始计数。如,要设置寄存器地址为40150的寄存器的值为1200(0x4B0),则查询报文中地址字段设置为0x95(149),见下表。

字段例(RTU模式)
地址域0x04
功能码域0x06
数据域(寄存器地址高位)0x00
数据域(寄存器地址低位)0x95
数据域(寄存器数值高位)0x04
数据域(寄存器数值低位)0xB0
差错校验CRC

本功能码中,起始地址由2个字节构成,取值范围为0x0000-0xFFFF;变更目标数据由2个字节构成,取值范围为0x0000-0xFFFF。

6.6.3 响应报文

响应报文的各项构成和意义见下表。对于从机而言,在保持寄存器正常变更时,返回与查询报文完全相同的响应报文。若修改失败,则返回一个异常响应。

字段例(RTU模式)
地址域0x04
功能码域0x06
数据域(寄存器地址高位)0x00
数据域(寄存器地址低位)0x95
数据域(寄存器数值高位)0x04
数据域(寄存器数值低位)0xB0
差错校验CRC

6.7(0x08)08诊断功能

6.8(0x0F)15写多个线圈/离散输出状态

6.8.1 功能说明

此功能码用于将连续的多个线圈或离散输出设置为ON/OFF状态,支持广播模式,在广播模式下,所有从站设备的同一地址的值将会被统一修改。本功能码中,起始地址字段由2个字段构成,取值范围为0x0000~0xFFFF;而寄存器数量字段由2个字节构成,取值范围为0x0001至0x07B0。

6.9(0x10)16写多个保持寄存器

6.9.1 功能说明

此功能码用于修改多个连续的寄存器的值。数据域中须指定寄存器起始地址及数量、须设定的字节数及值。本功能码中,起始地址字段由2个字段构成,取值范围为0x0000~0xFFFF;而寄存器数量字段由2个字节构成,取值范围为0x0001至0x007B。

6.9.2 查询报文

查询报文中包含了请求数据字段。数据字段保存须设定的数值,各数据按每个寄存器2个字节存放。如,从机设备地址为1,需要将保持寄存器地址为40020~40022的寄存器设置0x0155、0x0156、0x0157的值。
由于值是双字节,须注意MCU的大小端布置。同时需要注意,在查询报文中,Modbus协议起始地址为19(0x13),即比寄存器起始地址20少1,见下表。

字段例(RTU模式)
地址域0x05
功能码域0x10
数据域(寄存器地址高位)0x00
数据域(寄存器地址低位)0x13
数据域(寄存器数量高位)0x00
数据域(寄存器数量低位)0x03
数据域(写入数据字节数)0x06
数据域(变更数据1高位)0x01
数据域(变更数据1低位)0x55
数据域(变更数据2高位)0x01
数据域(变更数据2低位)0x56
数据域(变更数据3高位)0x01
数据域(变更数据3低位)0x57
差错校验CRC

6.9.3 响应报文

对于从机而言,在正常情况下响应报文包括功能码、起始地址及写入的寄存器数量,见下表。

字段例(RTU模式)
地址域0x05
功能码域0x10
数据域(寄存器地址高位)0x00
数据域(寄存器地址低位)0x13
数据域(寄存器数量高位)0x00
数据域(寄存器数量低位)0x03
差错校验CRC

在实际开发过程中,功能码16(0x10)写多个寄存器”常常用于方便用户写入多字节类型的数据。

6.10 Modbus异常响应

03、06、16等功能码是比较常用的功能码,在通常情况下,从机设备将返回一个正常响应报文,但是在某些特殊情况下,从机将返回异常响应报文。
对于查询报文,存在以下4种处理反馈:
i) 正常接收,正常处理,返回正常响应报文;
ii)因为通信错误等原因,造成从机没有接收到查询报文,主机将按照超时处理;
iii)从机接收到的查询报文存在通信错误(如LRC、CRC错误等),此时从机将丢弃该帧报文,主机将按照超时处理;
iv)从机接收到正确的报文,但是超过处理范围(如不存在的功能码或寄存器),此时从机将返回包含异常码的响应报文。
异常响应报文由从站地址、功能码以及异常码构成。其中,功能码与正常响应报文不同,在异常响应报文中,功能码最高位(MSB)被设置为1。因为Modbus协议中功能码占用一个字节,所以异常功能码 = 正常功能码 + 0x80。
举例说明,如现在主机想要查询起始地址为0x012C的寄存器的值。若从机中不存在地址为0x012C的寄存器,则从机将返回一个异常响应报文。见下表
查询码

字段例(RTU模式)
地址域0x07
功能码域0x04
数据域(寄存器地址高位)0x01
数据域(寄存器地址低位)0x2C
数据域(寄存器数量高位)0x00
数据域(寄存器数量低位)0x03
差错校验CRC

响应码

字段例(RTU模式)
地址域0x07
功能码域0x84
数据域(异常码)0x02
差错校验CRC

常见的异常码见下表。

异常码名称说明
01非法功能码从机不支持此功能码
02非法数据地址指定的数据地址在从站设备中不存在
03非法数据值指定的数据超过范围或者不允许使用
04从机设备故障从机处理响应的过程中,出现未知错误

7 开发过程中遇到的问题

7.1 1.5字符/3.5字符超时时间如何处理(附代码)

3.2节中提到,Modbus协议中规定了连续两个Modbus报文之间的时间间隔必须超过3.5个UART报文的发送时间(T3.5),连续两个UART报文之间时间间隔不能超过1.5个UART报文的发送时间(T1.5)。
其实Modbus的开发过程中尤其是在我在做从机开发时,这个T3.5和T1.5是不需要管的。
T1.5:从主机发的报文来看,实际在一个帧内部,两个UART报文是紧密相连的,不会存在有两个报文分开的情况,上一个UART报文的停止位和下一个UART报文的起始位之间的δ时间几乎是0;
T3.5:同样看主机发的报文,两帧Modbus报文之间的时间间隔都是30ms以上,远远超过了规定的0.29362ms。
但是T3.5可以用作判断报文是否接收完整的依据,作为从机来讲,不知道UART报文什么时候接收完成,所以可以利用这个T3.5去做文章。当我用中断去进行接收UART报文时,每一次从机接收一个BTYE的UART报文,那么就启动定时器,如果接下来还有报文进来,那么就清除前面累计的定时器计数,反过来,如果超过T3.5的时间过去了,都没有UART报文进来,那么就可以认为当前UART报文已经接收完成,也就是Modbus报文接收完成,那么我就可以对这段报文进行解析了。
附上代码:

	//Code for UART receive interrupt
	if(UART_GetRxDataAvailableIntStatus(UART))				//RX interrupt is triggered
	{
		TIMER_Stop(TIMER1);									//stop timer
		TIMER_ClearInt(TIMER1);								//clear timer interrupt flag		
		if(uart_message.rxindex < MODBUS_MAX_ADU_LENGTH)	//buffer is not full
		{
			//take data out of fifo
			uart_message.ptrRx[uart_message.rxindex] = UART_ReadByte(UART);
			uart_message.rxindex++;
			uart_message.uart_rx_state = UART_RX_BUSY;		//uart receive state is busy 
			TIMER_Run(TIMER1);
		}
		else
		{
			uart_message.rxindex = 0;
			uart_message.uart_rx_state = UART_STATE_ERROR;
		}
	}
	//Code for timer interrupt
	if(Timer_3Byte5_cnt++ > TimerPeridCount)				//3.5 uart message time reaches
	{
		uart_message.uart_rx_state = UART_RX_FINISHED;		//uart receive state finished
		uart_message.rxlength = uart_message.rxindex;		
		Timer_3Byte5_cnt = 0;
	}

在定时器中断函数里,当定时器计数达到3.5个UART报文发送时间时,认为此时Modbus报文已经接收完成,UART_State从BUSY变为FINISHED,同时把UART报文的length设置为rxindex,接下来就可以对接收到的报文进行解析了。

7.2 Modbus/UART模块化(附代码)

Modbus是应用层协议,Uart是底层的收发驱动,在具体实现时,需要把两个模块的内容分隔开。即Modbus协议相关内容的实现不要涉及UART。
代码里,对于Modbus和UART,都用了一个状态机去描述各自的状态。

typedef enum
{
	UART_TX_IDLE = 0,
	UART_TX_BUSY,
	UART_TX_FINISHED,
	UART_RX_IDLE,
	UART_RX_BUSY,
	UART_RX_FINISHED,
	UART_STATE_ERROR,
}uart_state_t;
//State machine of UART
typedef struct
{	
	uart_state_t uart_tx_state;
	uint8_t *ptrTx;
	uint8_t txlength;
	uint8_t txindex;
	
	uart_state_t uart_rx_state;
	uint8_t *ptrRx;
	uint8_t rxlength;
	uint8_t rxindex;	
}uart_message_t;

```c
typedef enum
{
	MODBUS_TX_IDLE = 0,
	MODBUS_TX_BUSY,
	MODBUS_TX_FINISHED,

	MODBUS_RX_IDLE,
	MODBUS_RX_BUSY,
	MODBUS_RX_FINISHED,
	
	MODBUS_STATE_ERROE,
}modbus_state_t;
//State machine of Modbus
typedef struct
{
	uint8_t* ptrTx;
	uint8_t txlength;
	uint8_t txindex;
	modbus_state_t modbus_tx_state;

	uint8_t* ptrRx;
	uint8_t rxlength;
	uint8_t rxindex;
	modbus_state_t modbus_rx_state;
}modbus_message_t;

uart_state有IDLE、BUSY、FINISHED这几个状态,分别用于表征UART总线的状态。
对于UART的收发,都有完整的函数去实现。在完成UART报文的接收后,就要把UART的报文移交给Modbus,移交之后UART本身的状态进行初始化。

uint8_t Modbus_Receive()
{
	uint8_t length;
	if(uart_message.uart_rx_state == UART_RX_FINISHED)		//uart message receive finished
	{
		modbus_message.rxlength = uart_message.rxlength;	
		modbus_message.modbus_rx_state = MODBUS_RX_FINISHED;
		DataCopy(uart_message.ptrRx, modbus_message.ptrRx, modbus_message.rxlength);		
		Uart_RxMsg_Init();								
	    //uart message clear, ready to receive next message
		length = Modbus_Check_Integrity(uart_message.ptrRx, modbus_message.rxlength);
	}
	else if((uart_message.uart_rx_state == UART_RX_BUSY) || (uart_message.uart_rx_state == UART_RX_IDLE))
		return 0;
	return length;
}

Modbus_Receive()函数中进行了Uart message buffer与Modbus message buffer内容的交接,当从机把通过Uart收到的报文给到Modbus后,Uart模块本身会进行初始化,等待总线上下一帧报文的到来。

uint8_t Modbus_Send(uint8_t *req, uint8_t req_length)			//Message doesn't include crc
{
	uint8_t Uart_Message_Length;
	if(req[0] == MODBUS_BROADCAST_ADDRESS)
	{
		return 0;
	}
	else
	{
		Uart_Message_Length = Modbus_Send_Msg_Pre(req, req_length);
		if(Uart_Message_Length > 0)
		{
			if(modbus_message.modbus_tx_state == MODBUS_TX_IDLE)
			{
				modbus_message.modbus_tx_state = MODBUS_TX_BUSY;
				if(Uart_Send(req, Uart_Message_Length))
				{
					modbus_message.modbus_tx_state = MODBUS_TX_FINISHED;
					modbus_message.modbus_tx_state = MODBUS_TX_IDLE;					
				}
			}else
				return 0;
		}
	}
	return 1;
}

完成了两个模块的分割之后,这样Modbus相关的代码就可以轻松移植到其他项目中去,需要做的就是根据不同的芯片完成UART的收发代码。

  • 8
    点赞
  • 88
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Modbus通讯协议是一种常用的串行通信协议,用于实现控制板与上位机之间的通讯。它通过UART驱动来进行数据传输。在Modbus协议中,有一些需要注意的点。首先,协议概要包括了协议的基本信息和功能。其次,在开发过程中可能会遇到一些问题,比如在RTU模式下如何处理1.5/3.5字符超时时间等。为了解决这些问题,可以参考一些解决办法,比如在代码中使用switch语句来处理不同的状态,使用CRC16算法来计算校验和等。\[1\] 在代码中,可以看到一些与Modbus协议相关的函数和变量。例如,GetCRC16函数用于计算CRC16校验和,ModbusSlaveReadHldRegAnswer函数用于构造读取保持寄存器的应答帧。这些函数和变量的具体实现可以根据具体的开发需求进行调整和使用。\[2\]\[3\] 总之,Modbus通讯协议是一种常用的串行通信协议,通过UART驱动来实现控制板与上位机之间的通讯。在开发过程中,需要注意一些协议的知识点和可能遇到的问题,并根据具体需求使用相应的函数和变量来实现协议的解析和构造。 #### 引用[.reference_title] - *1* [MODBUS通讯协议学习总结](https://blog.csdn.net/qq_41769322/article/details/123986126)[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^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [DSP做的modbus rtu 主从站协议](https://blog.csdn.net/liboxiu/article/details/79030418)[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^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值