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 位保持寄存器 |
常用功能码举例:
-
读取温度传感器数值
使用功能码 0x04 从输入寄存器中读取某一路模拟量温度传感器值。 -
控制继电器开关
使用功能码 0x05 或 0x0F 向线圈地址写入开关状态,实现继电器控制。 -
配置设备参数
使用功能码 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 0B | CRC 校验(低位在前) |
从站响应帧(共 9 字节):01 03 04 00 0A 01 2C B8 44
字段 | 含义 |
---|---|
01 | 从站地址 |
03 | 功能码 |
04 | 数据长度(4 字节 = 2 个寄存器) |
00 0A | 第一个寄存器值(0x000A) |
01 2C | 第二个寄存器值(0x012C) |
B8 44 | CRC 校验 |
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 51 | CRC 校验 |
从站响应帧:
与请求帧一致,表示写入成功(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 8F | CRC 校验 |
从站响应帧: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
相关文章:
Modbus RTU 详解 + FreeMODBUS移植(附项目源码)
文章目录 前言一、Modbus RTU1.1 通信方式1.2 模式特点1.3 数据模型1.4 常用功能码说明1.5 异常响应码1.6 通信帧格式1.6.1 示例一:读取保持寄存器(功能码 0x03)1.6.2 示例二:写单个线圈(功能码 0x05)1.6.3…...
新闻发稿筛选媒体核心标准:影响力、适配性与合规性
1. 评估媒体影响力 权威性与公信力:优先选择央级媒体,其报道常被其他平台转载,传播链条长,加分权重高。 传播数据:参考定海区融媒体中心的赋分办法,关注媒体的阅读量、视频播放量等指标,如阅读…...
豆包:基于多模态交互的智能心理咨询机器人系统设计与效果评估——情感计算框架下的对话机制创新
豆包:基于多模态交互的智能心理咨询机器人系统设计与效果评估——情感计算框架下的对话机制创新 摘要 随着人工智能在心理健康领域的应用深化,本文提出一种融合情感计算与动态对话管理的智能心理咨询机器人系统架构。通过构建“用户状态-情感响应-策略生成”三层模型,结合…...
坐席业绩可视化分析工具
这个交互式的坐席业绩分析工具具有以下特点: 数据导入功能:支持上传 CSV 文件,自动解析并展示数据多维度分析:可按日 / 周 / 月分析业绩数据,支持切换不同业绩指标(接通时长 / 外呼次数 / 接通次数&#x…...
MATLAB制作柱状图与条图:数据可视化的基础利器
一、什么是柱状图与条图? 柱状图和条图都是用来表示分类数据的常见图表形式,它们的核心目的是通过矩形的长度来比较各类别的数值大小。条图其实就是“横着的柱状图”,它们的本质是一样的:用矩形的长度表示数值大小,不同…...
com.fasterxml.jackson.dataformat.xml.XmlMapper把对象转换xml格式,属性放到标签<>里边
之前从没用过xml和对象相互转换,最近项目接了政府相关的。需要用xml格式数据进行相互转换。有些小问题,困扰了我一下下。 1.有些属性需要放到标签里边,有的需要放到标签子集。 2.xml需要加<?xml version"1.0" encoding"…...
在js中大量接口调用并发批量请求处理器
并发批量请求处理器 ✨ 设计目标 该类用于批量异步请求处理,支持: 自定义并发数请求节拍控制(延时)失败重试机制进度回调通知 🔧 构造函数参数 new BulkRequestHandler({dataList, // 要处理的数据列表r…...
Azure资源创建与部署指南
本文将指导您如何在Azure平台上创建和配置必要的资源,以部署基于OpenAI的应用程序。 资源组创建 资源组是管理和组织Azure资源的逻辑容器。 在Azure门户顶端的查询框中输入"Resource groups"(英文环境)或"资源组"(中文环境)在搜索结果中点击"资…...
图解gpt之神经概率语言模型与循环神经网络
上节课我们聊了词向量表示,像Word2Vec这样的模型,它确实能捕捉到词语之间的语义关系,但问题在于,它本质上还是在孤立地看待每个词。英文的“Apple”,可以指苹果公司,也可以指水果。这种一词多义的特性&…...
Jenkins linux安装
jenkins启动 service jenkins start 重启 service jenkins restart 停止 service jenkins stop jenkins安装 命令切换到自己的下载目录 直接用命令下载 wget http://pkg.jenkins-ci.org/redhat-stable/jenkins-2.190.3-1.1.noarch.rpm 下载直接安装 rpm -ivh jenkins-2.190.3-…...
android 修改单GPS,单北斗,单伽利略等
从hal层入手,代码如下: 各个类型如下: typedef enum {MTK_CONFIG_GPS_GLONASS 0,MTK_CONFIG_GPS_BEIDOU,MTK_CONFIG_GPS_GLONASS_BEIDOU,MTK_CONFIG_GPS_ONLY,MTK_CONFIG_BEIDOU_ONLY,MTK_CONFIG_GLONASS_ONLY,MTK_CONFIG_GPS_GLONASS_BEIDO…...
CNG汽车加气站操作工岗位职责
CNG(压缩天然气)汽车加气站操作工是负责天然气加气设备操作、维护及安全管理的重要岗位。以下是该岗位的职责、技能要求、安全注意事项及职业发展方向的详细说明: *主要职责 加气操作 按照规程为车辆加注CNG,检查车辆气瓶合格证…...
纯Java实现反向传播算法:零依赖神经网络实战
在深度学习框架泛滥的今天,理解算法底层实现变得愈发重要。反向传播(Backpropagation)作为神经网络训练的基石算法,其实现往往被各种框架封装。本文将突破常规,仅用Java标准库实现完整BP算法,帮助开发者: 1) 深入理解…...
springboot3 + mybatis-plus3 创建web项目实现表增删改查
Idea创建项目 环境配置说明 在现代化的企业级应用开发中,合适的开发环境配置能够极大提升开发效率和应用性能。本文介绍的环境配置为: 操作系统:Windows 11JDK:JDK 21Maven:Maven 3.9.xIDE:IntelliJ IDEA…...
多模型协同预测在风机故障预测的应用(demo)
数据加载和预处理的真实性: 下面的代码中,DummyDataset 和数据加载部分仍然是高度简化和占位的。为了让这个训练循环真正有效,您必须用您自己的数据加载逻辑替换它。这意味着您需要创建一个 torch.utils.data.Dataset 的子类,它能…...
韩媒聚焦Lazarus攻击手段升级,CertiK联创顾荣辉详解应对之道
近日,韩国知名科技媒体《韩国IT时报》(Korea IT Times)刊文引述了CertiK联合创始人兼CEO顾荣辉教授的专业见解,聚焦黑客组织Lazarus在Web3.0领域攻击手段的持续升级,分析这一威胁的严峻性,并探讨了提升行业…...
5.9-selcct_poll_epoll 和 reactor 的模拟实现
5.9-select_poll_epoll 本文演示 select 等 io 多路复用函数的应用方法,函数具体介绍可以参考我过去写的博客。 先绑定监听的文件描述符 int sockfd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serveraddr; memset(&serveraddr, 0, sizeof(struc…...
图上思维:基于知识图的大型语言模型的深层可靠推理
摘要 尽管大型语言模型(LLM)在各种任务中取得了巨大的成功,但它们经常与幻觉问题作斗争,特别是在需要深入和负责任的推理的场景中。这些问题可以通过在LLM推理中引入外部知识图(KG)来部分解决。在本文中&am…...
37-智慧医疗服务平台(在线接诊/问诊)
系统功能特点: 技术栈: springBootVueMysql 功能点: 医生端 用户端 管理员端 医生端: 科室信息管理、在线挂号管理、预约体检管理、体检报告管理、药品信息管理、处方信息管理、缴费信息管理、病历信息管理、智能导诊管理、在线接诊患者功能 (和患者1V1沟通) 用户…...
【新品发布】VXI可重构信号处理系统模块系列
VXI可重构信号处理系统模块概述 VXI可重构信号处理系统模块包括了 GPU 模块,CPU 模块,射频模块、IO 模块、DSP模块、高速存储模块、交换模块,采集处理模块、回放处理模块等,全套组件为单体3U VPX架构,可自由组合到多槽…...
React 第三十八节 Router 中useRoutes 的使用详解及注意事项
前言 useRoutes 是 React Router v6 引入的一个钩子函数,允许通过 JavaScript 对象(而非传统的 JSX 语法)定义路由配置。这种方式更适合复杂路由结构,且代码更简洁易维护。 一、基础使用 1.1、useRoutes路由配置对象 useRoute…...
Redhat 系统详解
Red Hat 系统深度解析:从企业级架构到核心组件 一、Red Hat 概述:企业级 Linux 的标杆 Red Hat 是全球领先的开源解决方案供应商,其核心产品 Red Hat Enterprise Linux(RHEL) 是企业级 Linux 的黄金标准。RHEL 以 稳…...
docker常用命令总结
常用命令含义docker info查看docker 服务的信息-------------------------镜像篇docker pull XXX从官网上拉取名为XXX的镜像docker login -u name登录自己的dockerhub账号docker push XXX将XXX镜像上传到自己的dockerhub账户中(XXX的命名必须是用户名/镜像名&#x…...
【el-admin】el-admin关联数据字典
数据字典使用 一、新增数据字典1、新增【图书状态】和【图书类型】数据字典2、编辑字典值 二、代码生成配置1、表单设置2、关联字典3、验证关联数据字典 三、查询操作1、模糊查询2、按类别查询(下拉框) 四、数据校验 一、新增数据字典 1、新增【图书状态…...
component :is是什么?
问: component :is是什么? 是组件? 那我们是不是就不需要自己创建组件了?还是什么意思?component :is和什么功能是类似的,同时和类似功能相比对什么时候用component :is…...
适老化洗浴辅具产业:在技术迭代与需求升级中重塑银发经济新生态
随着中国人口老龄化程度的不断加深,老年群体对于适老化产品的需求日益增长。 适老化洗浴辅具作为保障老年人洗浴安全与舒适的关键产品,其发展状况备受关注。 深入剖析中国适老化洗浴辅具的发展现状,并探寻助力产业发展的有效路径࿰…...
『Python学习笔记』ubuntu解决matplotlit中文乱码的问题!
ubuntu解决matplotlit中文乱码的问题! 文章目录 simhei.ttf字体下载链接:http://xiazaiziti.com/210356.html将字体放到合适的地方 sudo cp SimHei.ttf /usr/share/fonts/(base) zkfzkf:~$ fc-list | grep -i "SimHei" /usr/local/share/font…...
从AI到新能源:猎板PCB的HDI技术如何定义高端制造新标准?
2025年,随着AI服务器、新能源汽车、折叠屏设备等新兴领域的爆发式增长,高密度互连(HDI)电路板成为电子制造业的“必争之地”。HDI板凭借微孔、细线宽和高层间对位精度,能够实现电子设备的高集成化与微型化,…...
汽车制造行业的数字化转型
嘿,大家好!今天来和大家聊聊汽车制造行业的数字化转型,这可是当下非常热门的话题哦! 随着科技的飞速发展,传统的汽车制造行业正经历着一场深刻的变革。数字化技术已经不再是“锦上添花”,而是车企能否在未…...
Redis 常见数据类型
Redis 常见数据类型 一、基本全局命令详解与实操 1. KEYS 命令 功能:按模式匹配返回所有符合条件的键(生产环境慎用,可能导致阻塞)。 语法: KEYS pattern 模式规则: h?llo:匹配 hello, ha…...
【计算机网络-传输层】传输层协议-TCP核心机制与可靠性保障
📚 博主的专栏 🐧 Linux | 🖥️ C | 📊 数据结构 | 💡C 算法 | 🅒 C 语言 | 🌐 计算机网络 上篇文章:传输层协议-UDP 下篇文章: 网络层 我们的讲解顺序是&…...
对golang中CSP的理解
概念: CSP模型,即通信顺序进程模型,是由英国计算机科学家C.A.R. Hoare于1978年提出的。该模型强调进程之间通过通道(channel)进行通信,并通过消息传递来协调并发执行的进程。CSP模型的核心思想是“不要通过…...
嵌入式openharmony标准系统中HDF框架底层原理分析
1、案例简介 该程序是基于OpenHarmony标准系统编写的基础外设类:简易HDF驱动。 2、基础知识 2.1、OpenHarmony HDF开发简介 HDF(Hardware Driver Foundation)驱动框架,为驱动开发者提供驱动框架能力,包括驱动加载、驱动服务管理、驱动消息机制和配置管理。旨在构建统一…...
238.除自身以外数组的乘积
给你一个数组,求出第 i 个元素以外的数组元素的乘积,不能使用除法,且时间复杂度O(n), 对于一个数,如果知道了前缀元素的乘积和后缀元素的乘积,就知道了这个元素以外的数组元素的乘积,所以现在的问题是如何…...
AI文旅|暴雨打造旅游新体验
今年"五一"假期,全国文旅市场迎来爆发式增长,从丈崖瀑布的磅礴水雾到城市商区的璀璨霓虹,从山野民宿的静谧悠然到主题乐园的欢腾喧嚣,处处人潮涌动。在这火热的景象背后,一股“无形之力”正悄然改变旅游体验…...
学习心得《How Global AI Policy and Regulations Will Impact Your Enterprise》Gartner
AI时代来临,然而与之对应的是海量的数据的安全性和合规性如何保障,如何平衡个人与智能体的利益,恰巧,最近Gartner发布了《How Global AI Policy and Regulations Will Impact Your Enterprise》,我们就其中的观点一起进行探讨。 战略规划假设 我们首先关注的是关键的战略…...
JAVA将一个同步方法改为异步执行
目的: 这么做的目的就是为了使一个高频率执行的方法能不阻塞整个程序,将该方法丢入到线程池中让线程去做异步执行,既提高了程序整体运行速度,也使得在高并发环境下程序能够更加健壮(同步执行可能会使得请求堆积以致系…...
对遗传算法思想的理解与实例详解
目录 一、概述 二、实例详解 1)问题描述与分析 2)初始化种群 3)计算种群适应度 4)遗传操作 5)基因交叉操作 6)变异操作 三、计算结果 四、总结 一、概述 遗传算法在求解最优解的问题中最为常用&a…...
数据可视化大屏——物流大数据服务平台(二)
代码分析: 物流大数据平台代码分析 这是一个基于 Bootstrap 和 ECharts 构建的物流大数据平台前端页面,设计采用了经典的三栏布局,主要展示河南省及全国的物流数据可视化内容。下面从多个维度进行分析: 1. 页面结构分析 整体采…...
MindSpore框架学习项目-ResNet药物分类-构建模型
目录 2.构建模型 2.1定义模型类 2.1.1 基础块ResidualBlockBase ResidualBlockBase代码解析 2.1.2 瓶颈块ResidualBlock ResidualBlock代码解释 2.1.3 构建层 构建层代码说明 2.1.4 定义不同组合(block,layer_nums)的ResNet网络实现 ResNet组建类代码解析…...
ChatTempMail - AI驱动的免费临时邮箱服务
在当今数字世界中,保护在线隐私的需求日益增长。ChatTempMail应运而生,作为一款融合人工智能技术的新一代临时邮箱服务,它不仅提供传统临时邮箱的基本功能,还通过AI技术大幅提升了用户体验。 核心功能与特性 1. AI驱动的智能邮件…...
(leetcode) 力扣100 9.找到字符串中所有字母异位词(滑动窗口)
题目 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 数据范围 1 < s.length, p.length < 3 * 104 s 和 p 仅包含小写字母 样例 示例 1: 输入: s "cbaebabacd", p &quo…...
深入了解 Stable Diffusion:AI 图像生成的奥秘
一、引言 AI 艺术与图像生成技术的兴起改变了我们创造和体验视觉内容的方式。在过去几年里,深度学习模型已经能够创造出令人惊叹的艺术作品,这些作品不仅模仿了人类艺术家的风格,甚至还能创造出前所未有的新风格。在这个领域,Sta…...
场外期权平值期权 实值期权 虚值期权有什么区别?收益如何计算?
期权汇 场外期权按价值状态分为平值、虚值、实值期权。 01|实值期权对于看涨期权而言,如果行权价格低于标的市场价格,则该期权处于实值状态;对于看跌期权,如果行权价格高于标的市场价格,则处于实值状态…...
微软系统 红帽系统 网络故障排查:ping、traceroute、netstat
在微软(Windows)和红帽(Red Hat Enterprise Linux,RHEL)等系统中,网络故障排查是确保系统正常运行的重要环节。 ping、traceroute(在Windows中为tracert)和netstat是三个常用的网络…...
HOT 100 | 【子串】76.最小覆盖子串、【普通数组】53.最大子数组和、【普通数组】56.合并区间
一、【子串】76.最小覆盖子串 1. 解题思路 定义两个哈希表分别用于 t 统计字符串 t 的字符个数,另一个sub_s用于统计字符串 t 在 s 的子串里面字符出现的频率。 为了降低时间复杂度,定义一个变量t_count用于统计 t 哈希表中元素的个数。哈希表sub_s是一…...
基于CNN的猫狗图像分类系统
一、系统概述 本系统是基于PyTorch框架构建的智能图像分类系统,专门针对CIFAR-10数据集中的猫(类别3)和狗(类别5)进行分类任务。系统采用卷积神经网络(CNN)作为核心算法,结合图形用…...
《时序数据库全球格局:国产与国外主流方案的对比分析》
引言 时序数据库(Time Series Database, TSDB)是专门用于存储、查询和分析时间序列数据的数据库系统,广泛应用于物联网(IoT)、金融、工业监控、智能运维等领域。近年来,随着大数据和物联网技术的发展&…...
力扣-2.两数相加
题目描述 给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。 请你将两个数相加,并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外,这两个数都…...
富乐德传感技术盘古信息 | 锚定“未来工厂”新坐标,开启传感器制造行业数字化转型新征程
在数字化浪潮下,制造业正经历深刻变革。 传感器作为智能制造的核心基础部件,正面临着质量精度要求升级、交付周期缩短、成本管控严苛等多重挑战。传统依赖人工纸质管理、设备数据孤岛化的生产模式,已成为制约高端传感器制造突破“高精度、高…...