一个完整的Modbus RTU 主机库
-
一个完整的Modbus RTU 主机库
-
1.1 Modbus RTU 超时机制
-
1.2 Modbus 的错误机制
-
-
2. 协议栈代码解析
-
2.1 核心数据结构
-
2.2 主要功能码支持
-
2.3 工作流程
-
-
3. 移植指南
-
4. Modbus主机库使用示例
-
4.1 基础配置和初始化
-
2. 使用示例代码
-
-
5. 完整代码
一个完整的Modbus RTU 主机库
1.1 Modbus RTU 超时机制
1.2 Modbus 的错误机制
2. 协议栈代码解析
2.1 核心数据结构
2.2 主要功能码支持
2.3 工作流程
3. 移植指南
4. Modbus主机库使用示例
4.1 基础配置和初始化
2. 使用示例代码
5. 完整代码
• “Modbus RTU没有固定的帧头帧尾,依靠3.5个字符时间的静默间隔来区分数据帧” 这句话描述了Modbus RTU协议中的一个关键技术特点。
• 工作原理示意
帧1: [字符1][字符2][字符3]...[字符N] 3.5字符静默 帧2: [字符1][字符2]...
↑帧内间隔≤1.5字符 ↑帧结束检测点 ↑静默≥3.5字符 ↑新帧开始
• 不同波特率下3.5字符时间
| 波特率 | 每字符时间 | 3.5字符时间 |
|---|---|---|
| 2400 bps | 4.17 ms | 14.6 ms |
| 4800 bps | 2.08 ms | 7.29 ms |
| 9600 bps | 1.04 ms | 3.65 ms |
| 19200 bps | 0.52 ms | 1.82 ms |
| 38400 bps | 0.26 ms | 0.91 ms |
这一机制在代码中使用硬件定时器完成
/* 硬件定时器 TIM2 START 最小1us时间精度 使用 恩富莱 的代码*/
/* 保存 TIM定时中断到后执行的回调函数指针 */
static void (*s_TIM_CallBack1)(void);
static void (*s_TIM_CallBack2)(void);
static void (*s_TIM_CallBack3)(void);
static void (*s_TIM_CallBack4)(void);
/**
* @brief 初始化硬件定时器
*
*/
void bsp_InitHardTimer(void)
{
TIM_HandleTypeDef TimHandle = {0};
uint32_t usPeriod;
uint16_t usPrescaler;
uint32_t uiTIMxCLK;
__HAL_RCC_TIM2_CLK_ENABLE();
uiTIMxCLK = SystemCoreClock;
usPrescaler = uiTIMxCLK / 1000000 - 1; /* 分频比 = 1 */
usPeriod = 0xFFFF;
/*
设置分频为usPrescaler后,那么定时器计数器计1次就是1us
而参数usPeriod的值是决定了最大计数:
usPeriod = 0xFFFF 表示最大0xFFFF微秒。
usPeriod = 0xFFFFFFFF 表示最大0xFFFFFFFF微秒。
*/
TimHandle.Instance = TIM2;
TimHandle.Init.Prescaler = usPrescaler;
TimHandle.Init.Period = usPeriod;
TimHandle.Init.ClockDivision = 0;
TimHandle.Init.CounterMode = TIM_COUNTERMODE_UP;
TimHandle.Init.RepetitionCounter = 0;
TimHandle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&TimHandle) != HAL_OK)
{
// Error_Handler(__FILE__, __LINE__);
DEBUG_PRINTF(LEVEL_ERROR, "HAL_TIM_Base_Init error\n");
}
/* 配置定时器中断,给CC捕获比较中断使用 */
{
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 2);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
/* 启动定时器 */
HAL_TIM_Base_Start(&TimHandle);
}
/**
* @brief 启动硬件定时器
* 定时时间到后执行回调函数。可以同时启动4个定时器通道,互不干扰。
* 是16位定时器,最大定时时间为65535us,即65.535ms
*
* @param _CC 定时器通道 1~4
* @param _uiTimeOut 超时时间, 单位 1us. 对于16位定时器,最大 65.5ms; 对于32位定时器,最大 4294秒
* @param _pCallBack 定时时间到后,被执行的函数
*/
void bsp_StartHardTimer(uint8_t _CC, uint32_t _uiTimeOut, void * _pCallBack)
{
uint32_t cnt_now;
uint32_t cnt_tar;
TIM_TypeDef* TIMx = TIM2;
/* 无需补偿延迟,实测精度正负1us */
cnt_now = TIMx->CNT;
cnt_tar = cnt_now + _uiTimeOut; /* 计算捕获的计数器值 */
if (_CC == 1)
{
s_TIM_CallBack1 = (void (*)(void))_pCallBack;
TIMx->CCR1 = cnt_tar; /* 设置捕获比较计数器CC1 */
TIMx->SR = (uint16_t)~TIM_IT_CC1; /* 清除CC1中断标志 */
TIMx->DIER |= TIM_IT_CC1; /* 使能CC1中断 */
}
else if (_CC == 2)
{
s_TIM_CallBack2 = (void (*)(void))_pCallBack;
TIMx->CCR2 = cnt_tar; /* 设置捕获比较计数器CC2 */
TIMx->SR = (uint16_t)~TIM_IT_CC2; /* 清除CC2中断标志 */
TIMx->DIER |= TIM_IT_CC2; /* 使能CC2中断 */
}
else if (_CC == 3)
{
s_TIM_CallBack3 = (void (*)(void))_pCallBack;
TIMx->CCR3 = cnt_tar; /* 设置捕获比较计数器CC3 */
TIMx->SR = (uint16_t)~TIM_IT_CC3; /* 清除CC3中断标志 */
TIMx->DIER |= TIM_IT_CC3; /* 使能CC3中断 */
}
else if (_CC == 4)
{
s_TIM_CallBack4 = (void (*)(void))_pCallBack;
TIMx->CCR4 = cnt_tar; /* 设置捕获比较计数器CC4 */
TIMx->SR = (uint16_t)~TIM_IT_CC4; /* 清除CC4中断标志 */
TIMx->DIER |= TIM_IT_CC4; /* 使能CC4中断 */
}
else
{
return;
}
}
/**
* @brief TIM2中断处理函数
*
*/
void TIM2_IRQHandler(void)
{
uint16_t itstatus = 0x0, itenable = 0x0;
TIM_TypeDef* TIMx = TIM2;
itstatus = TIMx->SR & TIM_IT_CC1;
itenable = TIMx->DIER & TIM_IT_CC1;
if ((itstatus != (uint16_t)RESET) && (itenable != (uint16_t)RESET))
{
TIMx->SR = (uint16_t)~TIM_IT_CC1;
TIMx->DIER &= (uint16_t)~TIM_IT_CC1; /* 禁能CC1中断 */
// DEBUG_PRINTF(LEVEL_DEBUG, "TIM2->CCR1: %d\n", TIMx->CCR1);
/* 先关闭中断,再执行回调函数。因为回调函数可能需要重启定时器 */
s_TIM_CallBack1();
}
itstatus = TIMx->SR & TIM_IT_CC2;
itenable = TIMx->DIER & TIM_IT_CC2;
if ((itstatus != (uint16_t)RESET) && (itenable != (uint16_t)RESET))
{
TIMx->SR = (uint16_t)~TIM_IT_CC2;
TIMx->DIER &= (uint16_t)~TIM_IT_CC2; /* 禁能CC2中断 */
/* 先关闭中断,再执行回调函数。因为回调函数可能需要重启定时器 */
s_TIM_CallBack2();
}
itstatus = TIMx->SR & TIM_IT_CC3;
itenable = TIMx->DIER & TIM_IT_CC3;
if ((itstatus != (uint16_t)RESET) && (itenable != (uint16_t)RESET))
{
TIMx->SR = (uint16_t)~TIM_IT_CC3;
TIMx->DIER &= (uint16_t)~TIM_IT_CC3; /* 禁能CC2中断 */
/* 先关闭中断,再执行回调函数。因为回调函数可能需要重启定时器 */
s_TIM_CallBack3();
}
itstatus = TIMx->SR & TIM_IT_CC4;
itenable = TIMx->DIER & TIM_IT_CC4;
if ((itstatus != (uint16_t)RESET) && (itenable != (uint16_t)RESET))
{
TIMx->SR = (uint16_t)~TIM_IT_CC4;
TIMx->DIER &= (uint16_t)~TIM_IT_CC4; /* 禁能CC4中断 */
/* 先关闭中断,再执行回调函数。因为回调函数可能需要重启定时器 */
s_TIM_CallBack4();
}
}
• 错误码如下:我需要在代码中完善这些
| 错误码(Hex) | 名称 | 说明 |
|---|---|---|
| 01 | Illegal Function | 从站不支持请求的功能码(如设备不支持 0x05 写线圈)。 |
| 02 | Illegal Data Address | 请求的寄存器/线圈地址无效(如超出设备支持的范围)。 |
| 03 | Illegal Data Value | 请求的数据值非法(如写入的寄存器值超出允许范围)。 |
| 04 | Slave Device Failure | 从站设备执行请求时发生内部错误(如硬件故障)。 |
| 05 | Acknowledge | 从站已接收请求,但需要较长时间处理(通常用于编程命令)。 |
| 06 | Slave Device Busy | 从站正忙,无法处理当前请求(需重试)。 |
| 08 | Memory Parity Error | 从站检测到存储器奇偶校验错误(罕见,通常需硬件修复)。 |
| 0A | Gateway Path Unavailable | 网关路径不可用(仅适用于 Modbus 网关设备)。 |
| 0B | Gateway Target Failed | 网关目标设备无响应(仅适用于 Modbus 网关设备)。 |
typedef struct {
uint8_t RxBuf[H_RX_BUF_SIZE]; // 接收缓冲区
uint8_t TxBuf[H_TX_BUF_SIZE]; // 发送缓冲区
uint8_t RxCount; // 接收计数
uint8_t TxCount; // 发送计数
// 寄存器地址存储(用于匹配响应)
uint16_t Reg01H, Reg02H, Reg03H, Reg04H;
uint8_t RegNum; // 请求的寄存器数量
// 应答标志位
uint8_t fAck01H, fAck02H, fAck03H, fAck04H;
uint8_t fAck05H, fAck06H, fAck10H;
int16_t *RegBuf; // 存储读取到的寄存器数据
uint8_t address; // 当前操作的从机地址
} MODH_T;
| 功能码 | 名称 | 描述 |
|---|---|---|
| 0x01 | 读线圈状态 | 读取1-n个线圈的ON/OFF状态 |
| 0x02 | 读离散输入 | 读取1-n个离散输入的ON/OFF状态 |
| 0x03 | 读保持寄存器 | 读取1-n个保持寄存器的值 |
| 0x04 | 读输入寄存器 | 读取1-n个输入寄存器的值 |
| 0x05 | 写单个线圈 | 强制单个线圈为ON/OFF |
| 0x06 | 写单个寄存器 | 写入单个保持寄存器的值 |
| 0x10 | 写多个寄存器 | 写入多个保持寄存器的值 |
1. 数据发送流程
// 以03H功能码为例
void MODH_Send03H(uint8_t _addr, uint16_t _reg, uint16_t _num, int16_t *_buf)
{
// 1. 填充发送缓冲区
g_tModH.TxBuf[0] = _addr; // 从站地址
g_tModH.TxBuf[1] = 0x03; // 功能码
g_tModH.TxBuf[2] = _reg >> 8; // 寄存器地址高字节
g_tModH.TxBuf[3] = _reg; // 寄存器地址低字节
g_tModH.TxBuf[4] = _num >> 8; // 寄存器数量高字节
g_tModH.TxBuf[5] = _num; // 寄存器数量低字节
// 2. 计算并添加CRC校验
crc = CRC16_Modbus(g_tModH.TxBuf, g_tModH.TxCount);
g_tModH.TxBuf[g_tModH.TxCount++] = crc >> 8;
g_tModH.TxBuf[g_tModH.TxCount++] = crc;
// 3. 通过串口发送数据
MODH_SendPacket(g_tModH.TxBuf, g_tModH.TxCount);
// 4. 保存上下文信息用于后续解析
g_tModH.fAck03H = 0; // 清空应答标志
g_tModH.RegNum = _num; // 保存寄存器数量
g_tModH.Reg03H = _reg; // 保存寄存器起始地址
g_tModH.RegBuf = _buf; // 保存数据缓冲区指针
}
2. RTU 模式帧同步:依靠3.5个字符时间的静默间隔来区分数据帧
void MODH_ReciveNew(uint8_t _data)
{
// 1. 根据波特率获取超时时间(3.5字符时间)
// 9600bps → 4.0ms
// 19200bps → 2.0ms
// 38400bps及以上 → 1.75ms
// 2. 启动硬件定时器
bsp_StartHardTimer(2, timeout, MODH_RxTimeOut);
// 3. 存储接收到的字节
if (g_tModH.RxCount < H_RX_BUF_SIZE) {
g_tModH.RxBuf[g_tModH.RxCount++] = _data;
}
}
static void MODH_RxTimeOut(void)
{
// 定时器超时,表示接收到完整帧
g_modh_timeout = 1;
}
3. 数据解析流程
void MODH_Poll(void)
{
if (g_modh_timeout == 0) return; // 未收到完整帧
// 1. 基本长度检查(至少4字节)
if (g_tModH.RxCount < 4) goto err_ret;
// 2. CRC校验
crc1 = CRC16_Modbus(g_tModH.RxBuf, g_tModH.RxCount);
if (crc1 != 0) goto err_ret; // CRC错误
// 3. 应用层协议解析
MODH_AnalyzeApp();
err_ret:
g_tModH.RxCount = 0; // 重置接收计数器
}
4. 应用层响应解析
static void MODH_Read_03H(void)
{
// 1. 获取数据长度
bytes = g_tModH.RxBuf[2]; // 第3字节是数据字节数
// 2. 验证数据长度匹配
if (bytes == (g_tModH.RegNum * 2)) {
// 3. 解析数据(大端序转换)
p = &g_tModH.RxBuf[3];
for (uint8_t i = 0; i < g_tModH.RegNum; i++) {
g_tModH.RegBuf[i] = BEBufToUint16(p); // 大端序转整型
p += 2;
}
// 4. 设置应答标志
g_tModH.fAck03H = 1;
}
}
1. 移植串口,将创建串口,并且打开串口中断,在中断中将数据压入
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
HAL_UART_Receive_IT(&huart2, &g_usart2Rx, 1);
MODH_ReciveNew(g_usart2Rx);
}
}
2. 移植硬件定时器,完善如下硬件定时器的函数,更改为适应单片机的架构。
/* 硬件定时器 TIM2 START 最小1us时间精度 使用 恩富莱 的代码*/
/* 保存 TIM定时中断到后执行的回调函数指针 */
static void (*s_TIM_CallBack1)(void);
static void (*s_TIM_CallBack2)(void);
static void (*s_TIM_CallBack3)(void);
static void (*s_TIM_CallBack4)(void);
/**
* @brief 初始化硬件定时器
*
*/
void bsp_InitHardTimer(void)
{
TIM_HandleTypeDef TimHandle = {0};
uint32_t usPeriod;
uint16_t usPrescaler;
uint32_t uiTIMxCLK;
__HAL_RCC_TIM2_CLK_ENABLE();
uiTIMxCLK = SystemCoreClock;
usPrescaler = uiTIMxCLK / 1000000 - 1; /* 分频比 = 1 */
usPeriod = 0xFFFF;
/*
设置分频为usPrescaler后,那么定时器计数器计1次就是1us
而参数usPeriod的值是决定了最大计数:
usPeriod = 0xFFFF 表示最大0xFFFF微秒。
usPeriod = 0xFFFFFFFF 表示最大0xFFFFFFFF微秒。
*/
TimHandle.Instance = TIM2;
TimHandle.Init.Prescaler = usPrescaler;
TimHandle.Init.Period = usPeriod;
TimHandle.Init.ClockDivision = 0;
TimHandle.Init.CounterMode = TIM_COUNTERMODE_UP;
TimHandle.Init.RepetitionCounter = 0;
TimHandle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&TimHandle) != HAL_OK)
{
// Error_Handler(__FILE__, __LINE__);
DEBUG_PRINTF(LEVEL_ERROR, "HAL_TIM_Base_Init error\n");
}
/* 配置定时器中断,给CC捕获比较中断使用 */
{
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 2);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
/* 启动定时器 */
HAL_TIM_Base_Start(&TimHandle);
}
/**
* @brief 启动硬件定时器
* 定时时间到后执行回调函数。可以同时启动4个定时器通道,互不干扰。
* 是16位定时器,最大定时时间为65535us,即65.535ms
*
* @param _CC 定时器通道 1~4
* @param _uiTimeOut 超时时间, 单位 1us. 对于16位定时器,最大 65.5ms; 对于32位定时器,最大 4294秒
* @param _pCallBack 定时时间到后,被执行的函数
*/
void bsp_StartHardTimer(uint8_t _CC, uint32_t _uiTimeOut, void * _pCallBack)
{
uint32_t cnt_now;
uint32_t cnt_tar;
TIM_TypeDef* TIMx = TIM2;
/* 无需补偿延迟,实测精度正负1us */
cnt_now = TIMx->CNT;
cnt_tar = cnt_now + _uiTimeOut; /* 计算捕获的计数器值 */
if (_CC == 1)
{
s_TIM_CallBack1 = (void (*)(void))_pCallBack;
TIMx->CCR1 = cnt_tar; /* 设置捕获比较计数器CC1 */
TIMx->SR = (uint16_t)~TIM_IT_CC1; /* 清除CC1中断标志 */
TIMx->DIER |= TIM_IT_CC1; /* 使能CC1中断 */
}
else if (_CC == 2)
{
s_TIM_CallBack2 = (void (*)(void))_pCallBack;
TIMx->CCR2 = cnt_tar; /* 设置捕获比较计数器CC2 */
TIMx->SR = (uint16_t)~TIM_IT_CC2; /* 清除CC2中断标志 */
TIMx->DIER |= TIM_IT_CC2; /* 使能CC2中断 */
}
else if (_CC == 3)
{
s_TIM_CallBack3 = (void (*)(void))_pCallBack;
TIMx->CCR3 = cnt_tar; /* 设置捕获比较计数器CC3 */
TIMx->SR = (uint16_t)~TIM_IT_CC3; /* 清除CC3中断标志 */
TIMx->DIER |= TIM_IT_CC3; /* 使能CC3中断 */
}
else if (_CC == 4)
{
s_TIM_CallBack4 = (void (*)(void))_pCallBack;
TIMx->CCR4 = cnt_tar; /* 设置捕获比较计数器CC4 */
TIMx->SR = (uint16_t)~TIM_IT_CC4; /* 清除CC4中断标志 */
TIMx->DIER |= TIM_IT_CC4; /* 使能CC4中断 */
}
else
{
return;
}
}
/**
* @brief TIM2中断处理函数
*
*/
void TIM2_IRQHandler(void)
{
uint16_t itstatus = 0x0, itenable = 0x0;
TIM_TypeDef* TIMx = TIM2;
itstatus = TIMx->SR & TIM_IT_CC1;
itenable = TIMx->DIER & TIM_IT_CC1;
if ((itstatus != (uint16_t)RESET) && (itenable != (uint16_t)RESET))
{
TIMx->SR = (uint16_t)~TIM_IT_CC1;
TIMx->DIER &= (uint16_t)~TIM_IT_CC1; /* 禁能CC1中断 */
// DEBUG_PRINTF(LEVEL_DEBUG, "TIM2->CCR1: %d\n", TIMx->CCR1);
/* 先关闭中断,再执行回调函数。因为回调函数可能需要重启定时器 */
s_TIM_CallBack1();
}
itstatus = TIMx->SR & TIM_IT_CC2;
itenable = TIMx->DIER & TIM_IT_CC2;
if ((itstatus != (uint16_t)RESET) && (itenable != (uint16_t)RESET))
{
TIMx->SR = (uint16_t)~TIM_IT_CC2;
TIMx->DIER &= (uint16_t)~TIM_IT_CC2; /* 禁能CC2中断 */
/* 先关闭中断,再执行回调函数。因为回调函数可能需要重启定时器 */
s_TIM_CallBack2();
}
itstatus = TIMx->SR & TIM_IT_CC3;
itenable = TIMx->DIER & TIM_IT_CC3;
if ((itstatus != (uint16_t)RESET) && (itenable != (uint16_t)RESET))
{
TIMx->SR = (uint16_t)~TIM_IT_CC3;
TIMx->DIER &= (uint16_t)~TIM_IT_CC3; /* 禁能CC2中断 */
/* 先关闭中断,再执行回调函数。因为回调函数可能需要重启定时器 */
s_TIM_CallBack3();
}
itstatus = TIMx->SR & TIM_IT_CC4;
itenable = TIMx->DIER & TIM_IT_CC4;
if ((itstatus != (uint16_t)RESET) && (itenable != (uint16_t)RESET))
{
TIMx->SR = (uint16_t)~TIM_IT_CC4;
TIMx->DIER &= (uint16_t)~TIM_IT_CC4; /* 禁能CC4中断 */
/* 先关闭中断,再执行回调函数。因为回调函数可能需要重启定时器 */
s_TIM_CallBack4();
}
}
3. 在 main 函数中周期调用 MODH_Poll();
while (1)
{
BSP_Timer_Process();
MODH_Poll(); /* 空闲处理 */
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
4. 移植完成。
/* 1. 包含头文件 */
#include "modbus_host.h"
#include "usart.h"
/* 2. 定义设备地址(根据实际设备设置) */
#define PLC_ADDRESS 0x01 // PLC从站地址
#define TEMP_SENSOR_ADDR 0x02 // 温度传感器地址
#define IO_MODULE_ADDR 0x03 // IO模块地址
/* 3. 定义寄存器映射(根据设备手册) */
// 保持寄存器地址
#define REG_MOTOR_SPEED 0x1000 // 电机速度设定
#define REG_TEMPERATURE 0x1001 // 温度设定值
#define REG_PRESSURE 0x1002 // 压力设定值
#define REG_FLOW_RATE 0x1003 // 流量设定值
// 输入寄存器地址
#define REG_ACTUAL_TEMP 0x0000 // 实际温度
#define REG_ACTUAL_PRESS 0x0001 // 实际压力
#define REG_ACTUAL_FLOW 0x0002 // 实际流量
// 线圈寄存器地址
#define RELAY_1 0x0000 // 继电器1
#define RELAY_2 0x0001 // 继电器2
#define RELAY_3 0x0002 // 继电器3
#define RELAY_4 0x0003 // 继电器4
// 离散输入地址
#define SWITCH_1 0x0000 // 开关1状态
#define SWITCH_2 0x0001 // 开关2状态
/* 4. 定义数据缓冲区 */
int16_t holding_registers[10]; // 保持寄存器数据
int16_t input_registers[10]; // 输入寄存器数据
int16_t coil_states[10]; // 线圈状态数据
int16_t discrete_inputs[10]; // 离散输入数据
/* 5. 初始化Modbus(通常在main函数中调用) */
void Modbus_Init(void)
{
// 初始化串口(波特率9600,8位数据,无校验,1位停止位)
USART2_Init(9600);
// 初始化硬件定时器用于Modbus超时检测
bsp_InitTimer();
// 其他必要的初始化代码...
}
/* 工业控制系统Modbus通信示例 */
void IndustrialControl_Example(void)
{
uint8_t retry_count = 0;
uint8_t success = 0;
/* 示例1: 读取温度传感器的当前温度(功能码04H) */
int16_t temperature_data[1] = {0};
printf("=== 读取温度传感器数据 ===\n");
// 方法1: 直接调用(带超时和重试)
if (MODH_ReadParam_04H(TEMP_SENSOR_ADDR, REG_ACTUAL_TEMP, 1, temperature_data) == 1)
{
printf("当前温度: %.1f°C\n", temperature_data[0] / 10.0);
}
else
{
printf("温度读取失败,请检查设备连接\n");
}
/* 示例2: 读取多个模拟量输入(功能码04H) */
int16_t analog_inputs[3] = {0}; // 温度、压力、流量
printf("\n=== 读取所有模拟量输入 ===\n");
// 手动实现带重试机制的读取
for (retry_count = 0; retry_count < 3; retry_count++)
{
success = MODH_ReadParam_04H(PLC_ADDRESS, REG_ACTUAL_TEMP, 3, analog_inputs);
if (success == 1)
{
printf("读取成功(第%d次重试):\n", retry_count + 1);
printf(" 温度: %.1f°C\n", analog_inputs[0] / 10.0);
printf(" 压力: %.1f kPa\n", analog_inputs[1] / 100.0);
printf(" 流量: %.1f L/min\n", analog_inputs[2] / 10.0);
break;
}
else
{
printf("第%d次读取失败,等待500ms后重试...\n", retry_count + 1);
Delay_ms(500); // 等待500ms后重试
}
}
if (success == 0)
{
printf("模拟量读取失败,进入安全模式\n");
Emergency_Shutdown();
}
/* 示例3: 写入电机速度设定值(功能码06H) */
printf("\n=== 设置电机速度 ===\n");
int16_t motor_speed = 1500; // 1500 RPM
if (MODH_WriteParam_06H(PLC_ADDRESS, REG_MOTOR_SPEED, motor_speed) == 1)
{
printf("电机速度已设置为: %d RPM\n", motor_speed);
// 验证写入是否成功
int16_t verify_speed = 0;
if (MODH_ReadParam_03H(PLC_ADDRESS, REG_MOTOR_SPEED, 1, &verify_speed) == 1)
{
if (verify_speed == motor_speed)
{
printf("验证成功: 设定值与实际值一致\n");
}
else
{
printf("警告: 设定值(%d)与实际值(%d)不符\n",
motor_speed, verify_speed);
}
}
}
else
{
printf("电机速度设置失败\n");
}
/* 示例4: 批量写入多个参数(功能码10H) */
printf("\n=== 批量设置工艺参数 ===\n");
int16_t process_params[3] = {500, 2000, 100}; // 温度500,压力2000,流量100
if (MODH_WriteParam_10H(PLC_ADDRESS, REG_TEMPERATURE, 3, process_params) == 1)
{
printf("工艺参数批量设置成功\n");
// 读取验证
int16_t verify_params[3] = {0};
if (MODH_ReadParam_03H(PLC_ADDRESS, REG_TEMPERATURE, 3, verify_params) == 1)
{
for (int i = 0; i < 3; i++)
{
if (process_params[i] == verify_params[i])
{
printf(" 参数%d: 设置成功\n", i+1);
}
else
{
printf(" 参数%d: 设置失败(期望:%d,实际:%d)\n",
i+1, process_params[i], verify_params[i]);
}
}
}
}
/* 示例5: 读取和写入线圈(功能码01H和05H) */
printf("\n=== 控制继电器状态 ===\n");
// 读取当前所有继电器状态
int16_t relay_status[2] = {0}; // 每个int16可存储16个继电器的状态
if (MODH_ReadParam_01H(IO_MODULE_ADDR, RELAY_1, 16, relay_status) == 1)
{
printf("当前继电器状态:\n");
for (int i = 0; i < 16; i++)
{
int byte_index = i / 8;
int bit_index = i % 8;
int state = (relay_status[byte_index] >> bit_index) & 0x01;
printf(" 继电器%d: %s\n", i+1, state ? "ON" : "OFF");
}
}
// 控制单个继电器(打开继电器1)
if (MODH_WriteParam_05H(IO_MODULE_ADDR, RELAY_1, MODH_ON) == 1)
{
printf("继电器1已打开\n");
}
// 读取开关状态(功能码02H)
printf("\n=== 读取开关状态 ===\n");
int16_t switch_states[1] = {0};
if (MODH_ReadParam_02H(IO_MODULE_ADDR, SWITCH_1, 8, switch_states) == 1)
{
for (int i = 0; i < 8; i++)
{
int state = (switch_states[0] >> i) & 0x01;
printf(" 开关%d: %s\n", i+1, state ? "闭合" : "断开");
}
}
}
/* 示例6: 多设备轮询监控 */
void MultiDevice_Monitoring(void)
{
typedef struct {
uint8_t address;
char* name;
uint16_t reg_address;
int16_t last_value;
uint8_t error_count;
} DeviceInfo;
// 设备列表
DeviceInfo devices[] = {
{0x01, "主控制器", 0x1000, 0, 0},
{0x02, "温度传感器", 0x0000, 0, 0},
{0x03, "压力传感器", 0x0000, 0, 0},
{0x04, "流量计", 0x0000, 0, 0},
};
int16_t current_value;
printf("=== 多设备监控轮询 ===\n");
for (int i = 0; i < sizeof(devices)/sizeof(devices[0]); i++)
{
if (MODH_ReadParam_03H(devices[i].address, devices[i].reg_address, 1, ¤t_value) == 1)
{
devices[i].error_count = 0; // 清除错误计数
if (current_value != devices[i].last_value)
{
printf("[%s] 值变化: %d -> %d\n",
devices[i].name, devices[i].last_value, current_value);
devices[i].last_value = current_value;
}
}
else
{
devices[i].error_count++;
printf("[%s] 通信错误,错误计数: %d\n",
devices[i].name, devices[i].error_count);
if (devices[i].error_count >= 3)
{
printf("[%s] 设备故障,触发报警\n", devices[i].name);
// 触发报警处理
}
}
Delay_ms(100); // 每个设备间隔100ms
}
}
/* 示例7: 数据记录器 */
void DataLogger_Example(void)
{
typedef struct {
uint32_t timestamp;
int16_t temperature;
int16_t pressure;
int16_t flow_rate;
uint8_t device_status;
} LogEntry;
LogEntry log_data;
int16_t sensor_data[3];
// 获取时间戳
log_data.timestamp = GetSystemTick();
// 读取所有传感器数据
if (MODH_ReadParam_04H(PLC_ADDRESS, 0x0000, 3, sensor_data) == 1)
{
log_data.temperature = sensor_data[0];
log_data.pressure = sensor_data[1];
log_data.flow_rate = sensor_data[2];
// 读取设备状态
int16_t status_bits;
if (MODH_ReadParam_01H(PLC_ADDRESS, 0x0000, 1, &status_bits) == 1)
{
log_data.device_status = (uint8_t)status_bits;
}
// 保存到日志(模拟)
SaveToLog(&log_data);
printf("数据记录完成:\n");
printf(" 时间: %lu\n", log_data.timestamp);
printf(" 温度: %d\n", log_data.temperature);
printf(" 压力: %d\n", log_data.pressure);
printf(" 流量: %d\n", log_data.flow_rate);
printf(" 状态: 0x%02X\n", log_data.device_status);
}
}
/* 示例8: 错误处理和恢复 */
void Modbus_ErrorHandling_Example(void)
{
uint8_t i;
int16_t data[4];
printf("=== Modbus通信错误处理示例 ===\n");
// 尝试读取数据,最多重试3次
for (i = 0; i < 3; i++)
{
uint8_t result = MODH_ReadParam_03H(0x01, 0x1000, 4, data);
switch (result)
{
case 1: // 成功
printf("第%d次尝试: 读取成功\n", i+1);
Process_Data(data);
return; // 成功则退出
case 0: // 超时
printf("第%d次尝试: 通信超时\n", i+1);
if (i < 2) // 不是最后一次尝试
{
printf(" 等待%d秒后重试...\n", i+1);
Delay_ms((i+1) * 1000); // 指数退避
// 尝试重新初始化串口
USART2_Reinit();
}
break;
default: // 其他错误
printf("第%d次尝试: 错误代码 %d\n", i+1, result);
break;
}
}
// 所有重试都失败
printf("通信失败,切换到备用设备\n");
SwitchToBackupDevice();
}
/* 示例9: 设备配置和校准 */
void Device_Configuration_Example(void)
{
printf("=== 设备配置流程 ===\n");
/* 步骤1: 读取设备信息 */
int16_t device_info[4];
if (MODH_ReadParam_03H(0x01, 0x9990, 4, device_info) == 1)
{
printf("设备信息:\n");
printf(" 型号: 0x%04X\n", device_info[0]);
printf(" 版本: %d.%d\n", device_info[1] >> 8, device_info[1] & 0xFF);
printf(" 序列号: %d\n", (device_info[2] << 16) | device_info[3]);
}
/* 步骤2: 进入校准模式 */
printf("\n进入校准模式...\n");
if (MODH_WriteParam_06H(0x01, 0x8000, 0x5555) == 1)
{
printf("校准模式已激活\n");
Delay_ms(1000); // 等待设备准备
/* 步骤3: 设置零点校准 */
printf("执行零点校准...\n");
if (MODH_WriteParam_06H(0x01, 0x8001, 0x0001) == 1)
{
printf("零点校准完成\n");
}
/* 步骤4: 设置满量程校准值 */
printf("设置满量程值...\n");
if (MODH_WriteParam_06H(0x01, 0x8002, 1000) == 1)
{
printf("满量程设置完成\n");
}
/* 步骤5: 退出校准模式 */
printf("退出校准模式...\n");
if (MODH_WriteParam_06H(0x01, 0x8000, 0xAAAA) == 1)
{
printf("校准完成,设备返回正常模式\n");
}
}
}
/* 主循环中的Modbus任务调度 */
void Modbus_Task_Scheduler(void)
{
static uint32_t last_read_time = 0;
static uint8_t poll_phase = 0;
uint32_t current_time = GetTimerMs();
// 每100ms执行一次Modbus轮询
if (bsp_CheckRunTime(last_read_time) > 100)
{
last_read_time = current_time;
// 分阶段轮询不同设备
switch (poll_phase)
{
case 0:
// 阶段0: 读取快速变化的数据(如开关状态)
Read_Switch_States();
break;
case 1:
// 阶段1: 读取模拟量数据(如温度、压力)
Read_Analog_Values();
break;
case 2:
// 阶段2: 写入控制命令
Write_Control_Commands();
break;
case 3:
// 阶段3: 设备状态检查
Check_Device_Status();
break;
}
poll_phase = (poll_phase + 1) % 4; // 4个阶段循环
}
// 必须调用Modbus轮询函数处理接收
MODH_Poll();
}
/* 示例10: 实用函数封装 */
// 读取温度值(带单位转换)
float Read_Temperature_Celsius(uint8_t device_addr)
{
int16_t raw_data;
if (MODH_ReadParam_04H(device_addr, REG_ACTUAL_TEMP, 1, &raw_data) == 1)
{
// 假设原始数据是0.1°C单位
return raw_data / 10.0;
}
return -999.9; // 错误值
}
// 控制继电器(带状态反馈)
uint8_t Control_Relay(uint8_t device_addr, uint16_t relay_num, uint8_t state)
{
uint16_t value = (state ? MODH_ON : MODH_OFF);
if (MODH_WriteParam_05H(device_addr, relay_num, value) == 1)
{
// 等待并验证状态
Delay_ms(50);
int16_t verify_state;
if (MODH_ReadParam_01H(device_addr, relay_num, 1, &verify_state) == 1)
{
uint8_t actual_state = (verify_state == MODH_ON ? 1 : 0);
if (actual_state == state)
{
return 1; // 成功
}
}
}
return 0; // 失败
}
/* 主函数中的使用示例 */
int main(void)
{
// 系统初始化
System_Init();
Modbus_Init();
printf("Modbus主机通信系统启动\n");
printf("系统时间: %s\n", GetSystemTime());
// 设备自检
printf("\n=== 设备自检 ===\n");
Device_SelfTest();
// 主循环
while (1)
{
// 任务1: 监控数据(每秒一次)
static uint32_t last_monitor = 0;
if (bsp_CheckRunTime(last_monitor) > 1000)
{
last_monitor = GetTimerMs();
IndustrialControl_Example();
}
// 任务2: 多设备轮询(每5秒一次)
static uint32_t last_poll = 0;
if (bsp_CheckRunTime(last_poll) > 5000)
{
last_poll = GetTimerMs();
MultiDevice_Monitoring();
}
// 任务3: 数据记录(每10秒一次)
static uint32_t last_log = 0;
if (bsp_CheckRunTime(last_log) > 10000)
{
last_log = GetTimerMs();
DataLogger_Example();
}
// 必须不断调用Modbus轮询
MODH_Poll();
// 系统空闲处理
BSP_Idle();
}
}