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

Modbus RTU 详解 + FreeMODBUS移植(附项目源码)

文章目录

  • 前言
  • 一、Modbus RTU
    • 1.1 通信方式
    • 1.2 模式特点
    • 1.3 数据模型
    • 1.4 常用功能码说明
    • 1.5 异常响应码
    • 1.6 通信帧格式
      • 1.6.1 示例一:读取保持寄存器(功能码 0x03)
      • 1.6.2 示例二:写单个线圈(功能码 0x05)
      • 1.6.3 示例三:写多个保持寄存器(功能码 0x10)
  • 二、工程移植
    • 2.1 下载Modbus源码
    • 2.2 创建空白工程
    • 2.3 拷贝 MODBUS 源码文件
    • 2.4 添加工程文件分组及路径
    • 2.5 代码首次编译
    • 2.6 源码修改
      • 2.6.1 mbconfig.h
      • 2.6.2 mbrtu.c
      • 2.6.3 usart.c
      • 2.6.4 timer.c
      • 2.6.5 timer.h
      • 2.6.5 portserial.c
      • 2.6.6 porttimer.c
      • 2.6.7 demo.c
      • 2.6.8 main.c
    • 2.7 再次编译
  • 三、验证测试
  • 四、源码下载


前言

Modbus 是一种应用层通信协议,由 Modicon 公司于 1979 年为其 PLC(可编程逻辑控制器)产品开发。凭借其开放性、简单性和良好的可扩展性,Modbus 已广泛应用于工业自动化领域,成为各种工业设备之间通信的事实标准。

Modbus 协议采用主从通信模式(Master-Slave 或 Client-Server),主机发起请求,从设备响应请求。常见的使用场景包括:上位机读取传感器数据、配置仪表参数、PLC 之间互联等。

Modbus 协议主要有以下几种变体:

  • Modbus RTU(Remote Terminal Unit):基于串口(如 RS-485、RS-232)的二进制协议,传输效率高,常用于工业现场。

  • Modbus ASCII:也是串口通信,但数据以 ASCII 字符表示,抗干扰性较好,但效率不如 RTU。

  • Modbus TCP:基于以太网的 Modbus 协议,采用 TCP/IP 协议栈,适合现代工业网络系统。


一、Modbus RTU

1.1 通信方式

Modbus RTU 通常工作在 RS-485 总线上,采用半双工通信模式,具备以下几个显著特点:

  • 主从结构:一个主站可以连接多个从站(最多247个),主站轮询式地发起请求,从站被动响应。
  • 帧格式紧凑:采用二进制传输,每个字节为8位,帧间以时间间隔作为边界,数据密度高,传输效率优于 ASCII 模式。
  • 无握手机制:不需要建立连接或确认,仅靠数据帧格式和校验保证可靠性。
  • CRC 校验:使用 16 位 CRC 校验码进行帧校验,有效提高通信可靠性。

1.2 模式特点

  • 消息中每个8bit 字节包含两个4bit 的十六进制字符,因此,在波特率相同的情况下,传输效率比ascii 传输方式大
  • 1 个起始位、8 个数据位、1 个奇偶校验位和1 个停止位(或者两个停止位)
  • 错误检测域是CRC 检验
  • 消息发送至少要以3.5 个字符时间的停顿间隔开始。整个消息帧必须作为一连续的流传输。如果在帧完成之前有超过1.5 个字符时间的停顿时间, 接收设备将刷新不完整的消息并假定下一个字节是一个新消息的地址域。同样地, 如果一个新消息在小于3.5 个字符时间内接着前个消息开始,接收的设备将认为它是前一消息的延续。1.5~3.5 个字符间隔就算接收异常,只有超过3.5 个字符间隔才认为帧结束。

1.3 数据模型

Modbus RTU 中的数据通过地址空间进行分类,主要包括以下几种寄存器类型:

寄存器类型地址范围操作方式描述
线圈(Coils)00001~09999读/写单个位,相当于开关量,每个bit对应一个开关信号状态(可进行读写,列如通过1byte可控制8个IO口输出高低电平,此8个IO口电平可控可读)
离散输入(Discrete Inputs)10001~19999只读单个位,相当于开关量,每个bit对应一个开关信号状态(只能读取输入的开关量,无法通过应用程序进行更改,列如读取外部拨码开关的值。)
输入寄存器(Input Registers)30001~39999只读16 位, I/O 系统提供这种类型数据(无法通过应用程序进行更改的外部设备模拟量数据,如温度、气体浓度值等)
保持寄存器(Holding Registers)40001~49999读/写16 位,通过应用程序改变这种类型数据(例如可通过应用程序进行读写的当前设备RTC时钟值、设备运行模式等。)

1.4 常用功能码说明

在 Modbus RTU 协议中,每一条命令由一个功能码(Function Code)来标识主站想对从站执行的操作。功能码是一个 1 字节的十六进制数,决定了请求的类型(例如读写哪类寄存器)。以下是一些常见功能码及其说明:

功能码操作寄存器类型描述
0x01读线圈状态(Read Coils)线圈(0xxxx)读取一组输出线圈的当前开关状态
0x02读离散输入(Read Discrete Inputs)离散输入(1xxxx)读取一组输入通道的当前开关状态
0x03读保持寄存器(Read Holding Registers)保持寄存器(4xxxx)读取一组 16 位保持寄存器的数值
0x04读输入寄存器(Read Input Registers)输入寄存器(3xxxx)读取一组 16 位输入寄存器的只读数据
0x05写单个线圈(Write Single Coil)线圈(0xxxx)向某个输出线圈写入开关状态
0x06写单个保持寄存器(Write Single Register)保持寄存器(4xxxx)向某个保持寄存器写入一个 16 位数据
0x0F写多个线圈(Write Multiple Coils)线圈(0xxxx)批量写入一组线圈的开关状态
0x10写多个保持寄存器(Write Multiple Registers)保持寄存器(4xxxx)批量写入一组 16 位保持寄存器

常用功能码举例:

  1. 读取温度传感器数值
    使用功能码 0x04 从输入寄存器中读取某一路模拟量温度传感器值。

  2. 控制继电器开关
    使用功能码 0x05 或 0x0F 向线圈地址写入开关状态,实现继电器控制。

  3. 配置设备参数
    使用功能码 0x06 或 0x10 向保持寄存器写入设定值,比如 PID 参数、电机转速等。

1.5 异常响应码

如果从站检测到非法的功能码、地址或数据,它会返回一个异常响应,其功能码高位被置 1(如 0x83 表示对 0x03 的异常响应),并在数据域中返回一个异常码(Exception Code):

异常码含义
0x01非法功能码(Function Code)
0x02非法数据地址(Data Address)
0x03非法数据值(Data Value)
0x04从设备故障(Slave Device Failure)

1.6 通信帧格式

Modbus RTU 采用紧凑的二进制帧结构进行通信,通信帧以“起始静默时间”(≥ 3.5 个字符时间)开始,以“结束静默时间”作为边界。每一帧数据由以下字段组成:

通信帧格式结构(主→从或从→主)

地址 (1字节)功能码 (1字节)数据域 (N字节)CRC 校验 (2字节, 低位在前)
字段说明
地址目标从站地址(1~247)
功能码表示要执行的操作,例如读寄存器、写线圈等
数据域与功能码相关的附加信息,例如寄存器地址、数量、数据值等
CRC 校验循环冗余校验码,用于检测帧的完整性(先低字节,后高字节)

⚠️ Modbus RTU 没有帧头帧尾符号,依赖帧间间隔(起码 3.5 个字符时间)来区分帧。

1.6.1 示例一:读取保持寄存器(功能码 0x03)

目的:主站读取从站地址为 0x01,起始地址为 0x0000,共读取 2 个保持寄存器。

主站发送帧(共 8 字节):01 03 00 00 00 02 C4 0B

字段含义
01从站地址
03功能码(读保持寄存器)
00 00起始地址:0x0000
00 02寄存器数量:2个
C4 0BCRC 校验(低位在前)

从站响应帧(共 9 字节):01 03 04 00 0A 01 2C B8 44

字段含义
01从站地址
03功能码
04数据长度(4 字节 = 2 个寄存器)
00 0A第一个寄存器值(0x000A)
01 2C第二个寄存器值(0x012C)
B8 44CRC 校验

1.6.2 示例二:写单个线圈(功能码 0x05)

目的:主站控制从站地址为 0x11 的设备,将线圈地址 0x000A 置为 ON(0xFF00)

主站发送帧:11 05 00 0A FF 00 8E 51

字段含义
11从站地址
05写单个线圈
00 0A线圈地址:0x000A
FF 00写入值:0xFF00(表示 ON)
8E 51CRC 校验

从站响应帧:
与请求帧一致,表示写入成功(Echo back):11 05 00 0A FF 00 8E 51

1.6.3 示例三:写多个保持寄存器(功能码 0x10)

目的:主站向从站 0x01 的地址 0x0000 开始写入两个保持寄存器,分别写入 0x0011 和 0x0022。

主站发送帧:01 10 00 00 00 02 04 00 11 00 22 3E 8F

字段含义
01从站地址
10功能码:写多个保持寄存器
00 00起始地址:0x0000
00 02寄存器数量:2
04数据字节数:4 字节
00 11 00 22要写入的数据
3E 8FCRC 校验

从站响应帧:01 10 00 00 00 02 C1 0E
表示接收并成功写入了两个寄存器。

二、工程移植

2.1 下载Modbus源码

官网下载源码(https://www.embedded-experts.at/en/freemodbus-downloads/),解压
在这里插入图片描述
在这里插入图片描述

2.2 创建空白工程

第一步:创建空白工程,用于移植Modbus RTU(我这里选用正点原子的定时器例程)
在这里插入图片描述
第二步:在项目中创建MODBUS文件夹
在这里插入图片描述

2.3 拷贝 MODBUS 源码文件

打开空白工程中创建的MODBUS 文件夹,拷贝如下文件:
在这里插入图片描述
在这里插入图片描述

2.4 添加工程文件分组及路径

Keil工程添加文件分组、路径
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.5 代码首次编译

完成如上操作后编译,发现3error,别急,往下看
在这里插入图片描述

2.6 源码修改

2.6.1 mbconfig.h

打开文件mbconfig.h,将 1 修改为 0
在这里插入图片描述

2.6.2 mbrtu.c

打开文件mbrtu.c,增加几行代码

//启动第一次发送,这样才可以进入发送完成中断
xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
pucSndBufferCur++;  /* next byte in sendbuffer. */
usSndBufferCount--;

在这里插入图片描述

2.6.3 usart.c

打开usart.c文件,只保留串口初始化函数
在这里插入图片描述

#include "sys.h"
#include "usart.h"	  void uart_init(u32 bound)
{//GPIO端口设置GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA时钟//USART1_TX   GPIOA.9GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9//USART1_RX	  GPIOA.10初始化GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10  //Usart1 NVIC 配置NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器//USART 初始化设置USART_InitStructure.USART_BaudRate = bound;//串口波特率USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式USART_Init(USART1, &USART_InitStructure); //初始化串口1USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断USART_Cmd(USART1, ENABLE);                    //使能串口1 }

2.6.4 timer.c

打开timer.c文件,只保留定时器初始化函数,并设置预分频系数,保证频率为20khz
在这里插入图片描述

#include "timer.h"void TIM3_Int_Init(u16 arr)
{TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能//定时器TIM3初始化TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	TIM_TimeBaseStructure.TIM_Prescaler =3600-1; //设置用来作为TIMx时钟频率除数的预分频值TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_timTIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3中断,允许更新中断//中断优先级NVIC设置NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;  //TIM3中断NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  //先占优先级0级NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;  //从优先级3级NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能NVIC_Init(&NVIC_InitStructure);  //初始化NVIC寄存器TIM_Cmd(TIM3, ENABLE);  //使能TIMx					 
}

2.6.5 timer.h

打开timer.h文件,修改函数定义,与timer.c保持一致
在这里插入图片描述

2.6.5 portserial.c

打开portserial.c文件
①添加头文件#include "usart.h"
在这里插入图片描述
②修改vMBPortSerialEnable函数
在这里插入图片描述
③修改xMBPortSerialInit函数
在这里插入图片描述

④修改xMBPortSerialPutByte函数
在这里插入图片描述

⑤修改xMBPortSerialGetByte函数
在这里插入图片描述

⑥增加串口中断函数
在这里插入图片描述
portserial.c

#include "port.h"/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"#include "usart.h"	  /* ----------------------- static functions ---------------------------------*/
static void prvvUARTTxReadyISR( void );
static void prvvUARTRxISR( void );/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{/* If xRXEnable enable serial receive interrupts. If xTxENable enable* transmitter empty interrupts.*/if (xRxEnable){//使能串口接收中断USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);}else{USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);}if (xTxEnable){//使能串口发送完成中断USART_ITConfig(USART1, USART_IT_TC, ENABLE);}else{USART_ITConfig(USART1, USART_IT_TC, DISABLE);}	
}BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{uart_init(ulBaudRate);return TRUE;
}BOOL
xMBPortSerialPutByte( CHAR ucByte )
{/* Put a byte in the UARTs transmit buffer. This function is called* by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been* called. */USART_SendData(USART1, ucByte);return TRUE;
}BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{/* Return the byte in the UARTs receive buffer. This function is called* by the protocol stack after pxMBFrameCBByteReceived( ) has been called.*/*pucByte = USART_ReceiveData(USART1);return TRUE;
}/* Create an interrupt handler for the transmit buffer empty interrupt* (or an equivalent) for your target processor. This function should then* call pxMBFrameCBTransmitterEmpty( ) which tells the protocol stack that* a new character can be sent. The protocol stack will then call * xMBPortSerialPutByte( ) to send the character.*/
static void prvvUARTTxReadyISR( void )
{pxMBFrameCBTransmitterEmpty(  );
}/* Create an interrupt handler for the receive interrupt for your target* processor. This function should then call pxMBFrameCBByteReceived( ). The* protocol stack will then call xMBPortSerialGetByte( ) to retrieve the* character.*/
static void prvvUARTRxISR( void )
{pxMBFrameCBByteReceived(  );
}void USART1_IRQHandler(void)                	//串口1中断服务程序
{if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)//接收中断{prvvUARTRxISR();USART_ClearITPendingBit(USART1, USART_IT_RXNE);}if (USART_GetITStatus(USART1, USART_IT_ORE) == SET) //接收溢出中断{USART_ClearITPendingBit(USART1, USART_IT_ORE);prvvUARTRxISR();}if (USART_GetITStatus(USART1, USART_IT_TC) == SET) //发送完成中断{prvvUARTTxReadyISR();USART_ClearITPendingBit(USART1, USART_IT_TC);//}
} 

2.6.6 porttimer.c

打开porttimer.c文件
①添加头文件#include "timer.h"
在这里插入图片描述
②去掉inline关键字
在这里插入图片描述
③修改xMBPortTimersInit函数
在这里插入图片描述

④修改vMBPortTimersEnable函数

在这里插入图片描述

⑤修改vMBPortTimersDisable函数
在这里插入图片描述

⑥增加定时器中断函数
在这里插入图片描述
porttimer.c

/* ----------------------- Platform includes --------------------------------*/
#include "port.h"/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"#include "timer.h"/* ----------------------- static functions ---------------------------------*/
static void prvvTIMERExpiredISR( void );/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{TIM3_Int_Init(usTim1Timerout50us);return TRUE;
}void
vMBPortTimersEnable(  )
{/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */TIM_ClearITPendingBit(TIM3, TIM_IT_Update);TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);TIM_SetCounter(TIM3, 0x0000);TIM_Cmd(TIM3, ENABLE);	
}void
vMBPortTimersDisable(  )
{/* Disable any pending timers. */TIM_ClearITPendingBit(TIM3, TIM_IT_Update);TIM_ITConfig(TIM3, TIM_IT_Update, DISABLE);TIM_SetCounter(TIM3, 0x0000);TIM_Cmd(TIM3, DISABLE);
}/* Create an ISR which is called whenever the timer has expired. This function* must then call pxMBPortCBTimerExpired( ) to notify the protocol stack that* the timer has expired.*/
static void prvvTIMERExpiredISR( void )
{( void )pxMBPortCBTimerExpired(  );
}//定时器3中断服务程序
void TIM3_IRQHandler(void)   //TIM3中断
{if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否{prvvTIMERExpiredISR();TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  //清除TIMx更新中断标志 }
}

2.6.7 demo.c

打开demo.c文件,修改为如下:

#include "mb.h"
#include "mbport.h"// 十路输入寄存器
#define REG_INPUT_SIZE  10
uint16_t REG_INPUT_BUF[REG_INPUT_SIZE];// 十路保持寄存器
#define REG_HOLD_SIZE   10
uint16_t REG_HOLD_BUF[REG_HOLD_SIZE];// 十路线圈
#define REG_COILS_SIZE 10
uint8_t REG_COILS_BUF[REG_COILS_SIZE] = {1, 1, 1, 1, 0, 0, 0, 0, 1, 1};// 十路离散量
#define REG_DISC_SIZE  10
uint8_t REG_DISC_BUF[REG_DISC_SIZE] = {1,1,1,1,0,0,0,0,1,1};/// CMD4命令处理回调函数
eMBErrorCode eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{USHORT usRegIndex = usAddress - 1;// 非法检测if((usRegIndex + usNRegs) > REG_INPUT_SIZE){return MB_ENOREG;}// 循环读取while( usNRegs > 0 ){*pucRegBuffer++ = ( unsigned char )( REG_INPUT_BUF[usRegIndex] >> 8 );*pucRegBuffer++ = ( unsigned char )( REG_INPUT_BUF[usRegIndex] & 0xFF );usRegIndex++;usNRegs--;}// 模拟输入寄存器被改变for(usRegIndex = 0; usRegIndex < REG_INPUT_SIZE; usRegIndex++){REG_INPUT_BUF[usRegIndex]++;}return MB_ENOERR;
}/// CMD6、3、16命令处理回调函数
eMBErrorCode eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
{USHORT usRegIndex = usAddress - 1;// 非法检测if((usRegIndex + usNRegs) > REG_HOLD_SIZE){return MB_ENOREG;}// 写寄存器if(eMode == MB_REG_WRITE){while( usNRegs > 0 ){REG_HOLD_BUF[usRegIndex] = (pucRegBuffer[0] << 8) | pucRegBuffer[1];pucRegBuffer += 2;usRegIndex++;usNRegs--;}}// 读寄存器else{while( usNRegs > 0 ){*pucRegBuffer++ = ( unsigned char )( REG_HOLD_BUF[usRegIndex] >> 8 );*pucRegBuffer++ = ( unsigned char )( REG_HOLD_BUF[usRegIndex] & 0xFF );usRegIndex++;usNRegs--;}}return MB_ENOERR;
}/// CMD1、5、15命令处理回调函数
eMBErrorCode eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode )
{USHORT usRegIndex   = usAddress - 1;UCHAR  ucBits       = 0;UCHAR  ucState      = 0;UCHAR  ucLoops      = 0;// 非法检测if((usRegIndex + usNCoils) > REG_COILS_SIZE){return MB_ENOREG;}if(eMode == MB_REG_WRITE){ucLoops = (usNCoils - 1) / 8 + 1;while(ucLoops != 0){ucState = *pucRegBuffer++;ucBits  = 0;while(usNCoils != 0 && ucBits < 8){REG_COILS_BUF[usRegIndex++] = (ucState >> ucBits) & 0X01;usNCoils--;ucBits++;}ucLoops--;}}else{ucLoops = (usNCoils - 1) / 8 + 1;while(ucLoops != 0){ucState = 0;ucBits  = 0;while(usNCoils != 0 && ucBits < 8){if(REG_COILS_BUF[usRegIndex]){ucState |= (1 << ucBits);}usNCoils--;usRegIndex++;ucBits++;}*pucRegBuffer++ = ucState;ucLoops--;}}return MB_ENOERR;
}/// CMD2命令处理回调函数
eMBErrorCode eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{USHORT usRegIndex   = usAddress - 1;UCHAR  ucBits       = 0;UCHAR  ucState      = 0;UCHAR  ucLoops      = 0;// 非法检测if((usRegIndex + usNDiscrete) > REG_DISC_SIZE){return MB_ENOREG;}ucLoops = (usNDiscrete - 1) / 8 + 1;while(ucLoops != 0){ucState = 0;ucBits  = 0;while(usNDiscrete != 0 && ucBits < 8){if(REG_DISC_BUF[usRegIndex]){ucState |= (1 << ucBits);}usNDiscrete--;usRegIndex++;ucBits++;}*pucRegBuffer++ = ucState;ucLoops--;}// 模拟离散量输入被改变for(usRegIndex = 0; usRegIndex < REG_DISC_SIZE; usRegIndex++){REG_DISC_BUF[usRegIndex] = !REG_DISC_BUF[usRegIndex];}return MB_ENOERR;
}

2.6.8 main.c

打开main.c文件,修改为如下:

#include "sys.h"
#include "mb.h"int main(void)
{	eMBInit(MB_RTU, 0x01, 0x00, 115200, MB_PAR_NONE);eMBEnable();while (1){eMBPoll();}
}

2.7 再次编译

在这里插入图片描述
至此,环境适配完成,准备烧录验证。

三、验证测试

下载烧录到开发板,连接串口1(PA9、PA10)到USB-TTL模块,打开串口调试助手发送“01 03 00 00 00 01 84 0A”,表示向开发板请求读取从地址 0x0000 开始的 1 个保持寄存器的值
在这里插入图片描述
可见,开发板回复“01 03 02 00 00 B8 44”,表示地址为 01 的从站(单片机)成功响应了主站(串口调试助手)的读取保持寄存器请求,返回了一个值为 0x0000 的数据,数据长度为 2 个字节,并且附带了 CRC 校验值供主站验证数据的完整性。

四、源码下载

网盘链接: https://pan.baidu.com/s/1TdXjEkvC2T1Hze18fgSvGQ
提取码: f1rm

相关文章:

  • 【算法】:滑动窗口
  • 常见图像融合算法(alpha和金字塔融合)
  • 使用智能表格做FMEDA
  • Mysql--基础知识点--91.1--慢查询日志
  • 日常知识点之随手问题整理(思考单播,组播,广播哪个更省带宽)
  • RocketMQ 深度解析:架构设计与最佳实践
  • 学习黑客认识数字取证与事件响应(DFIR)
  • 修改docker为国内源
  • 【笔记】BCEWithLogitsLoss
  • NVME / DoCA 是什么?
  • 2025年 全新 AI 编程工具 Cursor 安装使用教程
  • 【RAG官方大神笔记】检索增强生成 (RAG):Python AI 教程的详细介绍
  • FastChat部署大模型
  • tauri-plugin-store 这个插件将数据存在本地电脑哪个位置
  • 如何把win10 wsl的安装目录从c盘迁移到d盘
  • postgresql 参数wal_level
  • 《算法导论(第4版)》阅读笔记:p14-p16
  • centos 7 安装 java 运行环境
  • Python 打包时包含字库文件的方法
  • 信息系统项目管理师-软考高级(软考高项)​​​​​​​​​​​2025最新(十三)(1)
  • 人民日报钟声:平等对话是解决大国间问题的正确之道
  • 14岁女生瞒报年龄文身后洗不掉,法院判店铺承担六成责任
  • 国防部:奉劝有关国家不要引狼入室,甘当棋子
  • 特朗普政府拟终止太空污染研究,马斯克旗下太空公司将受益
  • 不主动上门检查,上海已制定14个细分领域“企业白名单”甄别规则
  • 潘功胜发布会答问五大要点:除了降准降息,这些政策“含金量”也很高