文章目录
- 前言
- 一、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.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