
目录
1. 通信协议
1.1 硬件层通信协议
负责实际的物理信号传输(电信号、光信号、无线电波等),将数据转换为适合传输介质的格式。诸如一些用于通信的硬件接口,IIC、SPI、UART、RS232、RS422和RS485等。
其通讯方式大概可以归为一下三类:
单工:数据传输仅能沿一个方向,不能实现反向传输,只有一条通信路线:

半双工:数据传输可以沿两个方向,但需要分时进行,也只有一条通信线路:

全双工:数据可以同时进行双向传输,具有两条通信线路:

而RS485就是半双工通信协议,如果要运用Modbus就需要使用一主多从模式,其也是半双工的一种,其特点就是:
① 在设备通讯中,只有一个设备作为主机
② 在设备通信过程中,从机不可以向主机主发送消息,从机想要给主机发送消息,需要主机先给从机发送,你可以发送消息的请求。
举个例子:课堂老师提问,张三、李四、王二都在举手想要回答问题,但需要老师点名来说谁来回答问题,例如老师点名说张三来回答,张三进行回答,李四和王二不能回答。
③ 任何一次数据交换都是用主机发起。
主机在同一时间内只能向一个从机发送请求,总线上每次只有一个数据进行传输,即主机发送,从机应答,主机不发送,总线上就没有数据通信。
1.2 软件层通信协议
你可以将硬件层当做设备间数据通信的“路”,而软件层就相当于这条路上能够通过的“车”(数据),两个设备之间约定只能通信什么样的车,就是约定了通信协议,不然加入设备一作为主机想要向设备二发送数据,跑过去一辆汽车,但是设备二,根本不知道汽车是干啥的,他们城市之接收货车,这样汽车跑过去,就没有作用。而如果我们都约定好了,只通行汽车,这样就能理解干什么了。
就像两个公司的产品,这个公司一个协议,那个公司一个协议,不利于设备之间的互通,因此约定俗成的一个协议便出现了,这样所有人都遵从这个协议,后续无论出现那种设备都方便互通。
如Modbus,TCP/IP等介属于此类。
2. MODBUS
2.1 简介
Modbus 是由 Modicon(现为施耐德电气公司的一个品牌)在 1979 年发明的,是全球第一个真正用于工业现场的总线协议。
ModBus 网络是一个工业通信系统,由带智能终端的可编程序控制器和计算机通过公用线路或局部专用线路连接而成。其系统结构既包括硬件、亦包括软件。它可应用于各种数据采集和过程监控。
为更好地普及和推动 Modbus 在基于以太网上的分布式应用,目前施耐德公司已将 Modbus 协议的所有权移交给 IDA(Interface for Distributed Automation,分布式自动化接口)组织,并成立了Modbus-IDA 组织,为 Modbus 今后的发展奠定了基础。
2.2 概述
Modbus是主从方式通信,且是一主多从的通信协议,也就是说,不能同步进行通信,总线上每次只有一个数据进行传输,即主机发送,从机应答,主机不发送,总线上就没有数据通信。从机不会自己发送消息给主站,只能回复从主机发送的消息请求。

Modbus并没有忙机制判断,比方说主机给从机发送命令, 从机没有收到或者正在处理其他东西,这时候就不能响应主机,因为modbus的总线只是传输数据,没有其他仲裁机制,所以需要通过软件的方式来判断是否正常接收。
在物理层,Modbus 串行链路系统可以使用不同的物理接口(RS485、RS232)。最常用的是 TIA/EIA-485 (RS485) 两线制接口。
2.3 主/从站协议原理
Modbus 串行链路协议是一个主-从协议。在同一时刻,只有一个主节点连接于总线,一个或多个子节点 (最大编号为 247 ) 连接于同一个串行总线。
Modbus 通信总是由主节点发起。子节点在没有收到来自主节点的请求时,从不会发送数据。子节点之间从不会互相通信。主节点在同一时刻只会发起一个Modbus 事务处理。

主节点以两种模式对子节点发出 Modbus 请求:单播模式和广播模式。
2.3.1 单播模式
主节点以特定地址访问某个子节点,子节点接到并处理完请求后,子节点向主节点返回一个报文(一个'应答')。在这种模式,一个 Modbus 事务处理包含 2 个报文:一个来自主节点的请求,一个来自子节点的应答。
每个子节点必须有唯一的地址 (1 到 247),这样才能区别于其它节点被独立的寻址。
2.3.2 广播模式
主节点向所有的子节点发送请求。对于主节点广播的请求没有应答返回。广播请求一般用于写命令。所有设备必须接受广播模式的写功能。地址 0 是专门用于表示广播数据的。
| 模式 | 目标地址 | 子节点响应 | 适用场景 |
|---|---|---|---|
| 单播 | 1–247 | 必须回复 | 需确认的读写操作(如读取传感器数据) |
| 广播 | 0 | 无回复 | 批量写操作(如同步控制多个设备) |
2.3.3 地址规则
Modbus 寻址空间有 256 个不同地址。
地址 0 为广播地址。所有的子节点必须识别广播地址。
Modbus 主节点没有地址,只有子节点必须有一个地址。 该地址必须在 Modbus 串行总线上唯一。
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0 | 广播地址 | 主节点发送广播命令时使用,所有子节点接收但不响应。 |
| 1–247 | 子节点单播地址 | 每个子节点必须配置唯一地址(如传感器、PLC等),主节点通过地址单独访问。 |
| 248–255 | 保留地址 | Modbus 协议未定义其用途,通常禁止使用,部分设备可能自定义用途(但非标准)。 |
2.4 帧描述
Modbus协议数据单元:

发起 Modbus 事务处理的客户端构造 Modbus PDU,然后添加附加的域以构造通信 PDU。

地址域:在 Modbus 串行链路,地址域只含有子节点地址。
如前文所述,合法的子节点地址为十进制 0 – 247。 每个子设备被赋予 1 – 247 范围中的地址。 主节点通过将子节点的地址放到报文的地址域对子节点寻址。当子节点返回应答时,它将自己的地址放到应答报文的地址域以让主节点知道哪个子节点在回答。
功能码:指明服务器要执行的动作。
数据域:功能码后面可跟有表示含有请求和响应参数的数据域。
错误检验域:是对报文内容执行 "冗余校验" 的计算结果。根据不同的传输模式 (RTU or ASCII) 使用两种不同的计算方法。
2.5 传输模式
有两种串行传输模式被定义:RTU 模式 和 ASCII 模式。它定义了报文域的位内容在线路上串行的传送。它确定了信息如何打包为报文和解码。Modbus 串行链路上所有设备的传输模式 (和串行口参数) 必须相同。
尽管在特定的领域 ASCII 模式是要求的,但达到 Modbus 设备之间的互操作性只有每个设备都有相同的模式: 所有设备必须实现 RTU 模式。ASCII 传输模式是选项。
这一点怎么理解呢?假如我们想要发送数据03:
- 对于RTU的方式,也就是十六进制,那么将要发送的数据为,十六进制0x03,转换成二进制也就是:0000 0011;
- 对于ASCII的方式03,查找ASCII码表值,可以发现字符“0”所对应的二进制数据为:0011 0000,字符“3”所对应的二进制数据为:0011 0011,也就是说实际要发送的数据为:0011 0000 0011 0011。
当设备使用 RTU (Remote Terminal Unit) 模式在 Modbus 串行链路通信,报文中每个 8 位字节含有两个 4 位十六进制字符。这种模式的主要优点是较高的数据密度,在相同的波特率下比 ASCII 模式有更高的吞吐率。每个报文必须以连续的字符流传送。
2.5.1 RTU模式
RTU 模式每个字节 ( 11 位 ) 的格式为:
| 起始位 | 数据位 | 奇偶校验位 | 停止位 |
| 1 | 8(首先发送最低有效位) | 1 | 1 |
偶校验是要求的, 其它模式 ( 奇校验, 无校验 ) 也可以使用。 为了保证与其它产品的最大兼容性,同时支持无校验模式是建议的。默认校验模式模式 必须为偶校验。
注:使用无校验要求 2 个停止位。
每个字符或字节均由此顺序发送(从左到右):最低有效位 (LSB) . . . 最高有效位 (MSB)

如果无奇偶校验,将传送一个附加的停止位以填充字符帧:

完整RTU报文帧:

这里需要注意一下,例如主机发送一串数据,从机接收,我们会发现在整个RTU的报文帧当中,并没有数据标明这一帧的数据结束了,那么从机如何判断主机发送的这一帧的数据结束了呢?对于MODBUS当中,根据两人回话的方式对数据进行了处理。
例如,两个人交流当一个人说话的时候,另一个人倾听,当说话人停止一段时间后,倾听的人就知道这一段话结束了,MODBUS就根据这一概念,引申出了t3.5的概念。
2.5.1.1 MODBUS报文RTU帧
由发送设备将 Modbus 报文构造为带有已知起始和结束标记的帧。这使设备可以在报文的开始接收新帧,并且知道何时报文结束。不完整的报文必须能够被检测到而错误标志必须作为结果被设置。
在 RTU 模式,报文帧由时长至少为 3.5 个字符时间的空闲间隔区分。在后续的部分,这个时间区间被称作 t3.5。
对于t3.5时间的计算,我们以波特率 9600 bps(bit/s)为例:
那么每发送一位数据,就是:
我们又知道RTU模式下,一个字节为11位,那么3.5个字符的时间就是:
也就是4ms左右的时间。

整个报文帧必须以连续的字符流发送。如果两个字符之间的空闲间隔大于 1.5 个字符时间,则报文帧被认为不完整应该被接收节点丢弃。

RTU 接收驱动程序的实现,由于 t1.5 和 t3.5 的定时,隐含着大量的对中断的管理。在高通信速率下,这导致 CPU 负担加重。因此,在通信速率等于或低于 19200 bps 时,这两个定时必须严格遵守;
对于波特率大于 19200 bps 的情形,应该使用 2 个定时的固定值:
建议的字符间超时时间(t1.5)为 750µs,
帧间的超时时间 (t1.5) 为 1.750ms。

上面状态图的一些解释:
- 从 "初始" 态到 “空闲” 态转换需要 t3.5 定时超时: 这保证帧间延迟
- “空闲” 态是没有发送和接收报文要处理的正常状态。
- 在 RTU 模式, 当没有活动的传输的时间间隔达 3.5 个字符长时,通信链路被认为在 “空闲” 态。
- 当链路空闲时, 在链路上检测到的任何传输的字符被识别为帧起始。 链路变为 "活动" 状态。 然后,当链路上没有字符传输的时间间个达到 t3.5 后,被识别为帧结束。
- 检测到帧结束后,完成 CRC 计算和检验。然后,分析地址域以确定帧是否发往此设备,如果不是,则丢弃此帧。 为了减少接收处理时间,地址域可以在一接到就分析,而不需要等到整个帧结束。这样,CRC 计算只需要在帧寻址到该节点 (包括广播帧) 时进行。
2.5.1.2 CRC校验
在 RTU 模式包含一个对全部报文内容执行的,基于循环冗余校验 (CRC - Cyclical Redundancy Checking) 算法的错误检验域。CRC 域检验整个报文的内容。不管报文有无奇偶校验,均执行此检验。
CRC 域作为报文的最后的域附加在报文之后。计算后,首先附加低字节,然后是高字节。CRC 高字节为报文发送的最后一个子节。
附加在报文后面的 CRC 的值由发送设备计算。接收设备在接收报文时重新计算 CRC 的值,并将计算结果于实际接收到的 CRC 值相比较。如果两个值不相等,则为错误。
CRC 的计算,开始对一个 16 位寄存器预装全 1。然后将报文中的连续的 8 位子节对其进行后续的计算。只有字符中的 8 个数据位参与生成 CRC 的运算,起始位,停止位和校验位不参与 CRC 计算。CRC 的生成过程中, 每个 8–位字符与寄存器中的值异或。然后结果向最低有效位(LSB)方向移动(Shift) 1 位,而最高有效位(MSB)位置充零。 然后提取并检查 LSB:如果 LSB 为 1, 则寄存器中的值一个固定的预置值异或;如果 LSB 为 0, 则不进行异或操作。
这个过程将重复直到执行完 8 次移位。完成最后一次(第 8 次)移位及相关操作后,下一个 8 位字节与寄存器的当前值异或,然后又同上面描述过的一样重复 8 次。当所有报文中子节都运算之后得到的寄存器忠的最终值,就是 CRC。
详细了解可以参考:
2.5.2 ASCII模式
ASCII 模式每个字节 ( 10 位 ) 的格式为 :
| 起始位 | 数据位 | 奇偶校验位 | 停止位 |
| 1 | 7(首先发送最低有效位) | 1 | 1 |
偶校验是要求的, 其它模式 ( 奇校验, 无校验 ) 也可以使用。 为了保证与其它产品的最大兼容性,同时支持无校验模式是建议的。默认校验模式模式 必须为偶校验。
注:使用无校验要求 2 个停止位。
这里的一些可以参考RTU模式开头介绍的部分,除了数据位相差1,其他基本一样,不再做过多赘述。
2.5.2.1 MODBUS报文ASCII报文帧
由发送设备将 Modbus 报文构造为带有已知起始和结束标记的帧。这使设备可以在报文的开始接收新帧,并且知道何时报文结束。不完整的报文必须能够被检测到而错误标志必须作为结果被设置。报文帧的地址域含有两个字符。
在 ASCII 模式, 报文用特殊的字符区分帧起始和帧结束。一个报文必须以一个‘冒号’ ( : ) (ASCII 十六进制 3A )起始,以 ‘回车-换行’ (CR LF) 对 (ASCII 十六进制 0D 和 0A) 结束。
注 : LF 字符可以通过特定的 Modbus 应用命令 (参见 Modbus 应用协议规范) 改变。
对于所有的域,允许传送的字符为十六进制 0–9,A–F (ASCII 编码)。设备连续的监视总线上的‘冒号’ 字符。 当收到这个字符后,每个设备解码后续的字符一直到帧结束。
报文中字符间的时间间隔可以达一秒。如果有更大的间隔,则接受设备认为发生了错误。
举一个典型的报文帧:
| 起始帧 | 地址帧 | 功能码 | 数据帧 | LRC | 结束符 |
| 1 | 2 | 2 | 0~2*252 | 2 | 2 |
| : | CR,LF |
每个字符子节需要用两个字符编码。因此,为了确保 ASCII 模式 和 RTU 模式在 Modbus 应用级兼容,ASCII 数据域最大数据长度为 (2x252) 是 RTU 数据域 (252) 的两倍。必然的, Modbus ASCII 帧的最大尺寸为 513 个字符。

上面状态图的一些解释:
- “空闲” 态是没有发送和接收报文要处理的正常状态。
- 每次接收到 ":" 字符表示新的报文的开始。如果在一个报文的接收过程中收到该字符,则当前地报文被认为不完整并被丢弃。而一个新的接收缓冲区被重新分配。
- 检测到帧结束后,完成 LRC 计算和检验。然后,分析地址域以确定帧是否发往此设备,如果不是,则丢弃此帧。 为了减少接收处理时间,地址域可以在一接到就分析,而不需要等到整个帧结束。
2.5.2.2 LRC校验
在 ASCII 模式,包含一个对全部报文内容执行的,基于纵向冗余校验 (LRC - Longitudinal
Redundancy Checking) 算法的错误检验域。LRC 域检验不包括起始“冒号”和结尾 CRLF 对的整个报文的内容。不管报文有无奇偶校验,均执行此检验。
LRC 域为一个子节,包含一个 8 位二进制值。LRC 值由发送设备计算,然后将 LRC 附在报文后面。接收设备在接收报文时重新计算 LRC 的值,并将计算结果于实际接收到的 LRC 值相比较。如果两个值不相等,则为错误。
LRC 的计算, 对报文中的所有的连续 8 位字节相加,忽略任何进位,然后求出其二进制补码。执行检验针对不包括起始“冒号”和结尾 CRLF 对的整个 ASCII 报文域的内容。在 ASCII 模式, LRC 的结果被 ASCII 编码为两个字节并放置于 ASCII 模式报文帧的结尾, CRLF 之前。
2.6 事务处理流程

一旦服务器处理请求,使用合适的 MODBUS 服务器事务建立 MODBUS 响应。根据处理结果,可以建立两种类型响应:
一个正确的 MODBUS 响应:响应功能码 = 请求功能码
一个 MODBUS 异常响应:
- 用来为客户机提供处理过程中与被发现的差错相关的信息;
- 响应功能码 = 请求功能码 + 0x80;
- 提供一个异常码来指示差错原因。
3. 代码编写
本文只完成对MODBUS-RTU部分的讲解,想要了解RS485裸机代码实现的可以通过下方链接跳转的RS485部分(只看裸机部分即可):
后续工程,我们采用移植好的工程,工程的移植也非常的简单,可以参考上方链接,就是江协的串口代码,增加RS485电路部分,加一个收发控制引脚即可:
对于MODBUS-RTU完整的功能码讲解,篇幅原因,文章未进行表述,详细可以了解:
本文只涉及到了03(读保持寄存器)和06(写单个保持寄存器)的功能码,可以先看这两个即可。
3.1 RS485收发数据验证
对于上述RS485工程,只有接收没有发送的代码我们添加一下,顺便验证一下,数据的收发是否正常,首先添加一些发送函数,简单来说就是江协的串口代码,增加一些收发引脚控制:
//串口发送一个字节
void Serial_SendByte(uint8_t Byte)
{
USART_REDE_TX_MODE_H;
USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
USART_REDE_RX_MODE_L;
}
//串口发送一个数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
USART_REDE_TX_MODE_H;
for (i = 0; i < Length; i ++) //遍历数组
{
Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据
}
USART_REDE_RX_MODE_L;
}
//串口发送一个字符串
void Serial_SendString(char *String)
{
uint8_t i;
USART_REDE_TX_MODE_H;
for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止
{
Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据
}
USART_REDE_RX_MODE_L;
}
//printf重定向
int fputc(int ch, FILE *f)
{
USART_REDE_TX_MODE_H;
/* 发送一个字节数据到串口 */
USART_SendData(USART1, (uint8_t) ch);
/* 等待发送完毕 */
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_REDE_RX_MODE_L;
return (ch);
}
在主函数,添加代码:
Serial_SendByte('A');
打开串口调试助手,看一下是否正常发送:

想要发慢点,可以加个延时,这里就不加了,可以自行验证。
对于接收,我们通过print打印出来:

可以看到数据正常接收。
3.2 定时器添加
我们上面也说了,Modbus-RTU协议为了判断报文结束,有一个t3.5的概念,也就是4ms,这就需要定时器进行完成,我们添加一个1ms定时器:
#include "stm32f10x.h" // Device header
#include "Timer.h"
/**
* 函 数:定时中断初始化
* 参 数:无
* 返 回 值:无
*/
void Timer_Init(void)//1ms产生一次更新事件
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
/*配置时钟源*/
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 1000 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*中断输出配置*/
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除定时器更新标志位
//TIM_TimeBaseInit函数末尾,手动产生了更新事件
//若不清除此标志位,则开启中断后,会立刻进入一次中断
//如果不介意此问题,则不清除此标志位也可
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启TIM2的更新中断
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //选择配置NVIC的TIM2线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //指定NVIC线路的抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
定时器完成了,我们就来处理一下一帧数据如何接收。
3.3 数据接收
首先我们通过上面知道,Modbus-RTU协议没有明确的帧头和帧尾去确定这一帧数据是否发送完成,然后引入了 t3.5 的概念,如果一个数据帧开始发送后,停下 t3.5 个时间间隔,没有后续数据继续发送,就证明这一帧数据发送完成了:

注意这一段可能比较绕,可以根据完整的定时器中断服务函数和串口接收中断服务函数进行理解,会在本小节最下面附上完整的。
那么我们利用这一特性如何实现一帧数据的接收呢?首先我们知道每一个从机设备都应该有一个设备地址,用于主机找到从机设备:
uint8_t Modbus_my_add; //本设备的地址
这里我们对本设备设置地址为4,然后记得在主函数初始化一下:
//modbus初始化
void Modbus_Init(void)
{
Modbus_my_add=4; //本机也就是自己的地址
}
然后对于接收的数据我们需要一个缓冲区进行存放,这里就创建一个数组,用于存放接收数据:
uint8_t Modbus_RX_BUFF[100]; //modbus的接收数据缓冲区
uint8_t Modbus_RX_recount; //modbus端口已经接收到的数据个数
接着对于一帧数据的接收,前面我们也提到了,需要累加,并且由于此时我们用的是9600bps的波特率,那么根据上方计算得知,一帧接收完成的时间间隔为4ms作用,因此我们上面声明了一个1ms的定时器,通过累加来判定是否有t3.5个间隔:
uint16_t Modbus_RX_timeout; //modbus的数据断续时间
不过考虑到4ms太卡点了,没那么精准,我们可以稍微往上加一点,这里我直接给它翻一倍,计8ms的时间间隔,在进行后续数据的处理:
/*
波特率 9600
1位数据的时间为 1000000us/9600bit/s = 104us
一个字节 11位=1起始位+8数据位+1校验位+1停止位
104us*11位=1180us
所以modbus确定一个数据帧完成的时间为:1180us*3.5=4ms
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
modbus.Modbus_RX_timeout++;
if(modbus.Modbus_RX_timeout>=8)//间隔时间达到了3.5个字节时间
{
}
}
}
不过这里我们需要考虑一个问题,如果只是按照上面的计数,那么它是一直在累加的,我们需要的是等数据接收结束以后再开始进行计数,那么我们就需要再在数据接收的地方进行一个计数器清零,找到USART1接收中断服务函数:
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
uint8_t RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
Modbus_RX_BUFF[Modbus_RX_recount++]=RxData;//数据接收
Modbus_RX_timeout=0;//定时器清零
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位
}
}
那么定时器何时进行计时呢?我们在引入一个标志位:
uint8_t Modbus_time_run; //modbus定时器是否计时
这样当我们我们接收到第一个字节的数据,接收数据个数就会开始累加,此时启动定时器开始计时:
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
uint8_t RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
Modbus_RX_BUFF[Modbus_RX_recount++]=RxData;//数据接收
Modbus_RX_timeout=0;//定时器清零
if(Modbus_RX_recount==1)//收到主机发来的一帧数据的第一字节
{
Modbus_time_run=1;//启动计时
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位
}
}
当启动定时器后,计数器开始累加,当超过一个计数周期后关闭计数器:
/*
波特率 9600
1位数据的时间为 1000000us/9600bit/s = 104us
一个字节 11位=1起始位+8数据位+1校验位+1停止位
104us*11位=1180us
所以modbus确定一个数据帧完成的时间为:1180us*3.5=4ms
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
if(Modbus_time_run!=0)
{
modbus.Modbus_RX_timeout++;
if(modbus.Modbus_RX_timeout>=8)//间隔时间达到了3.5个字节时间
{
Modbus_time_run=0;//关闭定时器,停止定时
}
}
}
}
并且这里因为数据一直会进入中断服务函数,因此计数器会一直被清零,只有当数据停止接收后,计数器才开始累加,累加到t3.5个时间后,一帧数据接收,当然我们不能在中断里进行后续数据的处理,此时我们在引入一个一帧数据接收完的处理标志位:
uint8_t Modbus_RX_Flag; //modbus收到一帧数据的标志
当一帧数据完成,标志位置1,通过判断标志位,判断一帧数据的接收:
/*
波特率 9600
1位数据的时间为 1000000us/9600bit/s = 104us
一个字节 11位=1起始位+8数据位+1校验位+1停止位
104us*11位=1180us
所以modbus确定一个数据帧完成的时间为:1180us*3.5=4ms
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
if(Modbus_time_run!=0)
{
modbus.Modbus_RX_timeout++;
if(modbus.Modbus_RX_timeout>=8)//间隔时间达到了3.5个字节时间
{
Modbus_time_run=0;//关闭定时器,停止定时
Modbus_RX_Flag=1;//接收到一帧数据
}
}
}
}
为了防止正在处理数据的时候新的数据接收扰乱数组数据,因此在数据接收这里在加个判断,如说有数据包正在处理则直接返回:
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
uint8_t RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
if(Modbus_RX_Flag==1)//有数据包正在处理
{
return ;
}
Modbus_RX_BUFF[Modbus_RX_recount++]=RxData;//数据接收
Modbus_RX_timeout=0;//定时器清零
if(Modbus_RX_recount==1)//收到主机发来的一帧数据的第一字节
{
Modbus_time_run=1;//启动计时
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位
}
}
上面声明的函数比较多,我们归置一下:
typedef struct
{
uint8_t Modbus_my_add; //本设备的地址
uint8_t Modbus_RX_BUFF[100]; //modbus的接收数据缓冲区
uint16_t Modbus_RX_timeout; //modbus的数据断续时间
uint8_t Modbus_RX_recount; //modbus端口已经接收到的数据个数
uint8_t Modbus_time_run; //modbus定时器是否计时
uint8_t Modbus_RX_Flag; //modbus收到一帧数据的标志
uint8_t Modbus_TX_BUFF[100]; //modbus的发送缓冲区
}MODBUS;
MODBUS modbus;
这样定时器最终函数:
/*
波特率 9600
1位数据的时间为 1000000us/9600bit/s = 104us
一个字节 11位=1起始位+8数据位+1校验位+1停止位
104us*11位=1180us
所以modbus确定一个数据帧完成的时间为:1180us*3.5=4ms
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
if(modbus.Modbus_time_run!=0)
{
modbus.Modbus_RX_timeout++;
if(modbus.Modbus_RX_timeout>=8)//间隔时间达到了3.5个字节时间
{
modbus.Modbus_time_run=0;//关闭定时器,停止定时
modbus.Modbus_RX_Flag=1;//接收到一帧数据
}
}
}
}
串口接收最终函数:
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
uint8_t RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
if(modbus.Modbus_RX_Flag==1)//有数据包正在处理
{
return ;
}
modbus.Modbus_RX_BUFF[modbus.Modbus_RX_recount++]=RxData;
modbus.Modbus_RX_timeout=0;
if(modbus.Modbus_RX_recount==1)//收到主机发来的一帧数据的第一字节
{
modbus.Modbus_time_run=1;//启动计时
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位
}
}
这里我们会发现一个事情参数Modbus_RX_recount的值一直处于累加状态,并没有清零,这么做主要为了方便后续计算CRC的值,下面数据处理会提到,至此数据接收的底层代码完毕。
我们来通过keil5的仿真来验证一下,这里需要一个Modbus调试软件,作为主机发送:
打开上方软件,链接到字节刚刚调试RS485的COM口,波特率我们设置的位9600,无校验位,8数据位,停止位,对于框住的部分此时可以随意填写:

然后找到代码,开启仿真,将Watch1窗口打开,查看此时数组数据:

对于keil5仿真的使用,不熟悉的可以查看:
此时可以看到数组内的数据全为0:

然后我们找到一帧数据接收完的标志位,打一个断点:

然后全速运行代码:

找到Modbus调试软件,打开串口,点击“读出”,可以发现此时数据被写入到寄存器当中:

3.4 CRC校验码
上面一帧数据已经接收完成了,我们就需要对后续数据进行处理,首先是CRC的值,我们前面也知道,Modbus-RTU是通过CRC进行校验的,对于CRC-16/Modbus的校验有两种方式,一种就是直接计算:
/**
* @brief 计算CRC16/MODBUS校验码
* @param pData 指向数据缓冲区的指针
* @param length 数据的长度(字节数)
* @return 计算出的CRC16校验码
*/
uint16_t Calculate_CRC16(const uint8_t *pData, uint32_t length)
{
uint16_t crc = 0xFFFF; // CRC初始值
uint8_t i;
while(length--) {
crc ^= *pData++; // 将数据与CRC的低8位进行异或
// 对每个字节进行8次移位处理
for (i = 0; i < 8; i++) {
if (crc & 0x0001) // 判断最低位是否为1
crc = (crc >> 1) ^ 0xA001; // 右移并与多项式异或
else
crc >>= 1; // 直接右移
}
}
return crc;
}
另一种是查表法:
/* CRC 高位字节值表 */
const uchar auchCRCHi[] = {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40
} ;
/* CRC低位字节值表*/
const uchar auchCRCLo[] = {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,
0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,
0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,
0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,
0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,
0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,
0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,
0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,
0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,
0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,
0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,
0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,
0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,
0x43, 0x83, 0x41, 0x81, 0x80, 0x40
} ;
uint crc16( uchar *puchMsg, uint usDataLen )
{
uchar uchCRCHi = 0xFF ; // 高CRC字节初始化
uchar uchCRCLo = 0xFF ; // 低CRC 字节初始化
unsigned long uIndex ; // CRC循环中的索引
while ( usDataLen-- ) // 传输消息缓冲区
{
uIndex = uchCRCHi ^ *puchMsg++ ; // 计算CRC
uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex] ;
uchCRCLo = auchCRCLo[uIndex] ;
}
return ( uchCRCHi << 8 | uchCRCLo ) ;
}
两种方法,直接计算需要的MCU的空间较小,但是运算时间比较长,查表法因为需要空间存储表中的值,因此内存占用较多,但是可以直接查找,运算时间较短,二者各有优缺点,这里我使用的是查表法。
那么我们来验证一下CRC校验能不能正常使用,首先创建一个处理函数,判断有没有接收到一帧数据包,若没有则直接跳出,若接收到了,则继续运行:
void Modbus_Data_Treat(void)
{
if(modbus.Modbus_RX_Flag == 0)//没有收到modbus的数据包
{
return;//直接跳出
}
}
然后声明两个变量,一个用来存放我们自己接收到数据计算出来的CRC的值,一个用来存放发送给我们的CRC的值,然后判断二者是否相等:
void Modbus_Data_Treat(void)
{
uint16_t CRC_count;//计算接收到的数据的CRC
uint16_t RX_CRC;//接收主从机发送的CRC值
if(modbus.Modbus_RX_Flag == 0)//没有收到modbus的数据包
{
return;//直接跳出
}
//计算的校验码,中断接收时,会将发送数据的CRC校验吗一并接收
//而我们CRC校验的是除本身以外的值
//所以计算从主机接收到的数据的CRC的值,需要将总数减2,用于与本机进行校验
CRC_count = crc16(&modbus.Modbus_RX_BUFF[0], modbus.Modbus_RX_recount-2);
//接收到的校验码,高八位乘以256,相当于左移8位,然后与第八位相加
RX_CRC = modbus.Modbus_RX_BUFF[modbus.Modbus_RX_recount-2]*256 + modbus.Modbus_RX_BUFF[modbus.Modbus_RX_recount-1];
if(CRC_count==RX_CRC)//判断接收的数据处理出来的CRC的值和发送的CRC的值是否相等
{
CRC_count = 0;//没啥用仅测试
}
modbus.Modbus_RX_recount = 0;//清楚接收数据个数标志位
modbus.Modbus_RX_Flag = 0;//清除数据接收标志位
}
可以发现我们这里也需要调用接收数据的个数,用来计算CRC的值,如果刚才在定时器清楚了,这里就不好操作了,因此等使用完,在这里清除一下。
我们来验证一下,如果接收的CRC值和计算的CRC的值相等,就会进入到测试的代码当中,我们进入仿真调试界面,在测试的那句话打一个断点,点击全速运行,通过Modbus调试软件进行发送数据,如果能够进入到调试语句,则证明CRC能正常校验,若不能,则不能正常校验:

我们找到Modbus调试软件,点击读出,可以发现程序运行到测试位置,CRC校验正确:

3.5 功能码调用
这一块就非常简单了,上面我们已经判断完CRC的值,数据接收完整,下面我们就需要判断,这个数据是不是给我的,如果给我的我应当使用什么功能码,并且判断这个是否是广播地址。
我们知道数据包的第一位为从机地址,设备首先判断接收到的数据第一位和自己本机的地址做对比,若相同则继续分析功能码(第二位为功能码),若不同则再次判断是不是广播地址,若是则进行广播地址处理,若不是则结束,这里我们只使用了0x03和0x06的功能码:
if(modbus.Modbus_RX_BUFF[0] == modbus.Modbus_my_add)//确认数据包是否是发给本设备的,接收到的数据第一个数据是我的地址
{
switch(modbus.Modbus_RX_BUFF[1])//分析功能码
{
case 0: break;
case 1: break;
case 2: break;
case 3: Modbus_03();break;//3号功能码处理,读多个寄存器
case 4: break;
case 5: break;
case 6: Modbus_06();break;//6号功能码处理,写单个寄存器
case 7: break;
default:break;
}
}
else if(modbus.Modbus_RX_BUFF[0] == 0)//广播地址
{
}
3.6 功能码编写-读保持寄存器03H
读保持寄存器,可读取单个或者多个保持寄存器。其功能码为0x03。其中寄存器的地址是我们自己声明的,用于存放我们自己的数据,假如我们声明寄存器地址位:
//本机设定的寄存器
uint16_t Modbus_Register[] =
{
0x0000,
0x0001,
0x0002,
0x0003,
0x0004,
0x0005,
0x0006,
0x0007,
0x0008,
0x0009,
0x000A,
0x000B,
};
此时你想要做一个环境检测传感器,那么你可以将温度传感器的数据放到0x0000,将湿度传感器的数据放到0x0001,将二氧化碳的数据放到0x0002等等,这样到主机想要检测那个数据,只需要告诉从机想要检测的起始地址,以及想要检测的数量即可:
//得到要读取的寄存器的首地址
start_add_03 = modbus.Modbus_RX_BUFF[2]*256+modbus.Modbus_RX_BUFF[3];//高位在前低位在后
//读取数据长度
read_len_03 = modbus.Modbus_RX_BUFF[4]*256+modbus.Modbus_RX_BUFF[5];
从机根据主机发送的想要检测的地址以及检测数量进行返回数据,发货数据包括,本机地址:
modbus.Modbus_TX_BUFF[arr_i++] = modbus.Modbus_my_add;//本机的地址
功能码:
modbus.Modbus_TX_BUFF[arr_i++] = 0x03;//功能码
要返回的字节数:
byte_03=read_len_03*2;//要返回的数据字节数
modbus.Modbus_TX_BUFF[arr_i++]=byte_03%256;
以及需要返回的数据:
for(CRC_j=0;CRC_j<read_len_03;CRC_j++)
{
modbus.Modbus_TX_BUFF[arr_i++] = Modbus_Register[start_add_03 + CRC_j] / 256;//高八位
modbus.Modbus_TX_BUFF[arr_i++] = Modbus_Register[start_add_03 + CRC_j] % 256;//低八位
}
然后将要返回的数据进行CRC计算,将计算值一并放松到主机:
CRC_03_count = crc16(modbus.Modbus_TX_BUFF, arr_i);//计算CRC
//将计算完的CRC重新插入
modbus.Modbus_TX_BUFF[arr_i++] = CRC_03_count / 256;//高八位
modbus.Modbus_TX_BUFF[arr_i++] = CRC_03_count % 256;//低八位
将完整的数据发送个主机:
USART_REDE_TX_MODE_H;//RS485转为发送状态
//发送数据
for(CRC_j = 0; CRC_j < arr_i+1; CRC_j++)
{
Serial_SendByte(modbus.Modbus_TX_BUFF[CRC_j]);
}
USART_REDE_RX_MODE_L;//RS485恢复接收状态
我们来测试代码此时03功能码能否正常使用,继续进入仿真,点击全速运行,我们现在先不更改设备地址看看会发生什么:

可以发现接收区域什么也没有,那是因为我们在上面初始化的时候将自己的地址规定为4,主机现在是给7发送数据,那么我们是不会进行给主机进行返回消息,我们将地址改为4会怎么样呢:

可以发现数据正常返回,我们来分析一下数据是不是就是我们主机想要的数据(高位在后,低位在前):
可以与03功能码相互印证一下,方便理解:

04:我们从机地址;
03:功能码;
04:所要传输的数据字节个数,高八位和低八位组成一个数据,刚好两个数据;
00 04 00 05:所传输的数据;
2E F1:前面数据CRC校验的值。
一个CRC校验的工具:

我们在换一下,读取4号地址,寄存器首地址位1,读取数据为4:

可以自己多测试几组数据分析一下,进行理解。
3.7 功能码编写-写单个保持寄存器06H
对寄存器地址进行修改,主机发送什么从机做什么应答,当主机对从机发送06功能码,想要修改从机,从机读取主机想要修改的寄存器首地址,将修改后的值存入其中:
start_add_06 = modbus.Modbus_RX_BUFF[2]*256 + modbus.Modbus_RX_BUFF[3];//得到要修改的寄存器的首地址
revise_sum_06 = modbus.Modbus_RX_BUFF[4]*256 + modbus.Modbus_RX_BUFF[5];//修改后的值
Modbus_Register[start_add_06] = revise_sum_06;//修改本设备相应的寄存器
此时寄存器的值已经修改完成,从机将修改后的数据返回主机,告诉主机已经修改完成:
modbus.Modbus_TX_BUFF[arr_06_i++] = modbus.Modbus_my_add;//本机的地址
modbus.Modbus_TX_BUFF[arr_06_i++] = 0x06;//功能码
//主机想要修改的数据
modbus.Modbus_TX_BUFF[arr_06_i++] = start_add_06/256;
modbus.Modbus_TX_BUFF[arr_06_i++] = start_add_06%256;
//从机修改后的数据
modbus.Modbus_TX_BUFF[arr_06_i++] = revise_sum_06/256;
modbus.Modbus_TX_BUFF[arr_06_i++] = revise_sum_06%256;
对上述值进行CRC校验一并发送个给主机,可以发现这一块的应答流程和03差不多:
CRC_06_count = crc16(modbus.Modbus_TX_BUFF,arr_06_i);//计算CRC
//将计算完的CRC重新插入
modbus.Modbus_TX_BUFF[arr_06_i++] = CRC_06_count/256;//高八位
modbus.Modbus_TX_BUFF[arr_06_i++] = CRC_06_count%256;//低八位
USART_REDE_TX_MODE_H;//RS485转为发送状态
//发送
for(CRC_06_j=0; CRC_06_j < arr_06_i + 1; CRC_06_j++)
{
Serial_SendByte(modbus.Modbus_TX_BUFF[CRC_06_j]);
}
USART_REDE_RX_MODE_L;//RS485恢复接收状态
我们来验证一下,代码是否可行,根据下方链接06码一起相互验证:
还是进入仿真,全速运行,我们先来查看一下开始的Modbus_Register的数据,打开窗口Watch1,可以看到此时地址未变:

找到我们的调试软件,我们对寄存器地址1,将其数值改为1111:

可以发现能够成功进行修改。
至此03和06功能码功能测试完成,想要其他功能可以根据上述思路,参考功能码协议,自己上手实践一下。
从机完整工程代码:



9194






