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

news2025/5/10 13:31:11

文章目录

  • 前言
  • 一、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.9
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
	GPIO_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;//PA10
	GPIO_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 ;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
	NVIC_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); //初始化串口1
  USART_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_tim
	TIM_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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2372335.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

对称加密算法(AES、ChaCha20和SM4)Python实现——密码学基础(Python出现No module named “Crypto” 解决方案)

文章目录 一、对称加密算法基础1.1 对称加密算法的基本原理1.2 对称加密的主要工作模式 二、AES加密算法详解2.1 AES基本介绍2.2 AES加密过程2.3 Python中实现AES加密Python出现No module named “Crypto” 解决方案 2.4 AES的安全考量 三、ChaCha20加密算法3.1 ChaCha20基本介…

【软件设计师:存储】16.计算机存储系统

一、主存储器 存储器是计算机系统中的记忆设备,用来存放程序和数据。 计算机中全部信息,包括输入的原始数据、计算机程序、中间运 行结果和最终运行结果都保存在存储器中。 存储器分为: 寄存器Cache(高速缓冲存储器)主存储器辅存储器一、存储器的存取方式 二、存储器的性…

WebRTC通信原理与流程

1、服务器与协议相关 1.1 STUN服务器 图1.1.1 STUN服务器在通信中的位置图 1.1.1 STUN服务简介 STUN&#xff08;Session Traversal Utilities for NAT&#xff0c;NAT会话穿越应用程序&#xff09;是一种网络协议&#xff0c;它允许位于NAT&#xff08;或多重 NAT&#xff09;…

Java版ERP管理系统源码(springboot+VUE+Uniapp)

ERP系统是企业资源计划&#xff08;Enterprise Resource Planning&#xff09;系统的缩写&#xff0c;它是一种集成的软件解决方案&#xff0c;用于协调和管理企业内各种关键业务流程和功能&#xff0c;如财务、供应链、生产、人力资源等。它的目标是帮助企业实现资源的高效利用…

Redis总结(六)redis持久化

本文将简单介绍redis持久化的两种方式 redis提供了两种不同级别的持久化方式&#xff1a; RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储.AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保…

PMIC电源管理模块的PCB设计

目录 PMU模块简介 PMU的PCB设计 PMU模块简介 PMIC&#xff08;电源管理集成电路&#xff09;是现代电子设备的核心模块&#xff0c;负责高效协调多路电源的转换、分配与监控。它通过集成DC-DC降压/升压、LDO线性稳压、电池充电管理、功耗状态切换等功能&#xff0c;替代传统分…

华为云Flexus+DeepSeek征文|DeepSeek-V3商用服务开通教程

目录 DeepSeek-V3/R1商用服务开通使用感受 DeepSeek-V3/R1商用服务开通 1、首先需要访问ModelArts Studio_MaaS_大模型即服务_华为云 2、在网站右上角登陆自己的华为云账号&#xff0c;如果没有华为云账号的话&#xff0c;则需要自己先注册一个。 3、接着点击ModelArts Stu…

Qt—鼠标移动事件的趣味小程序:会移动的按钮

1.项目目标 本次根据Qt的鼠标移动事件实现一个趣味小程序&#xff1a;当鼠标移动到按钮时&#xff0c;按钮就会随机出现在置&#xff0c;以至于根本点击不到按钮。​​​​​ 2.项目步骤 首先现在ui界面设计控件(也可以用代码的方式创建&#xff0c;就不多说了) 第一个按钮不需…

鞋样设计软件

Sxy 64鞋样设计软件是一款专业级鞋类设计工具 专为鞋业设计师与制鞋企业开发 该软件提供全面的鞋样设计功能 包括二维开版 三维建模 放码排料等核心模块 支持从草图构思到成品输出的完整设计流程 内置丰富的鞋型数据库与部件库 可快速生成各种鞋款模板 软件采用智能放码技术 精…

LeRobot 项目部署运行逻辑(六)——visualize_dataset_html.py/visualize_dataset.py

可视化脚本包括了两个方法&#xff1a;远程下载 huggingface 上的数据集和使用本地数据集 脚本主要使用两个&#xff1a; 目前来说&#xff0c;ACT 采集训练用的是统一时间长度的数据集&#xff0c;此外&#xff0c;这两个脚本最大的问题在于不能裁剪&#xff0c;这也是比较好…

Windows Server 2025开启GPU分区(GPU-P)部署DoraCloud云桌面

本文描述在ShareStation工作站虚拟化方案的部署过程。 将服务器上部署 Windows Server、DoraCloud&#xff0c;并创建带有vGPU的虚拟桌面。 GPU分区技术介绍 GPU-P&#xff08;GPU Partitioning&#xff09; 是微软在 Windows 虚拟化平台&#xff08;如 Hyper-V&#xff09;中…

TCP套接字通信核心要点

TCP套接字通信核心要点 通信模型架构 客户端-服务端模型 CS架构&#xff1a;客户端发起请求&#xff0c;服务端响应和处理请求双向通道&#xff1a;建立连接后实现全双工通信 服务端搭建流程 核心步骤 创建套接字 int server socket(AF_INET, SOCK_STREAM, 0); 参数说明&am…

【C】初阶数据结构15 -- 计数排序与稳定性分析

本文主要讲解七大排序算法之外的另一种排序算法 -- 计数排序 目录 1 计数排序 1&#xff09; 算法思想 2&#xff09; 代码 3&#xff09; 时间复杂度与空间复杂度分析 &#xff08;1&#xff09; 时间复杂度 &#xff08;2&#xff09; 空间复杂度 4&#xff09; 计…

高性能Python Web 框架--FastAPI 学习「基础 → 进阶 → 生产级」

以下是针对 FastAPI 的保姆级教程&#xff0c;包含核心概念、完整案例和关键注意事项&#xff0c;采用「基础 → 进阶 → 生产级」的三阶段教学法&#xff1a; 一、FastAPI介绍 FastAPI 是一个现代化的、高性能的 Python Web 框架&#xff0c;专门用于构建 APIs&#xff08;应…

Qt QML自定义LIstView

QML ListView组合拳做列表&#xff0c;代码不可直接复制使用&#xff0c;需要小改 先上图看效果 样式1 样式2 样式3 原理&#xff1a;操作&#xff1a;技术点:代码片段&#xff1a; 先上图看效果 样式1 三个表格组合成要给&#xff0c;上下滚动时&#xff0c;三个同时滚动&am…

C++进阶--红黑树的实现

文章目录 红黑树的实现红黑树的概念红黑树的规则红黑树的效率 红黑树的实现红黑树的结构红黑树的插入变色单旋&#xff08;变色&#xff09;双旋&#xff08;变色&#xff09; 红黑树的查找红黑树的验证 总结&#xff1a;结语 很高兴和大家见面&#xff0c;给生活加点impetus&a…

WPF之值转换器

文章目录 目录什么是值转换器IValueConverter接口Convert方法ConvertBack方法 创建和使用值转换器定义转换器类在XAML中使用转换器转换器参数&#xff08;ConverterParameter&#xff09; 常用转换器实现布尔值转可见性&#xff08;BoolToVisibilityConverter&#xff09;数值转…

qml中的TextArea使用QSyntaxHighlighter显示高亮语法

效果图&#xff0c;左侧显示行号&#xff0c;右侧用TextArea显示文本内容&#xff0c;并且语法高亮。 2025年5月8号更新 1、多行文本注释 多行文本注释跟普通的高亮规则代码不太一样&#xff0c;代码需要修改&#xff0c;这里以JavaScript举例。 先制定多行文本注释规则&…

Transformer编码器+SHAP分析,模型可解释创新表达!

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 基本介绍 基于SHAP分析的特征选择和贡献度计算&#xff0c;Matlab2023b代码实现&#xff1b;基于MATLAB的SHAP可解释Transformer编码器回归模型&#xff0c;敏感性分析方法。 详细介绍 引言 在正向渗透&#xff08…

[特殊字符]适合母亲节的SVG模版[特殊字符]

宝藏模版 往期推荐&#xff08;点击阅读&#xff09;&#xff1a; 趣味效果&#xff5c;高大上&#xff5c;可爱风&#xff5c;年终总结I&#xff5c;年终总结II&#xff5c;循环特效&#xff5c;情人节I&#xff5c;情人节II&#xff5c;情人节IIII&#xff5c;妇女节I&…