Browse Source

新增ModbusRTU代码,测试通过,可以和触摸屏通信

undefined
ywh 1 week ago
parent
commit
3a44a1e033
8 changed files with 1360 additions and 30 deletions
  1. +50
    -30
      Core/Src/main.c
  2. BIN
      Document/Xinje_HMI_ModbusRTU_Function_Codes.pdf
  3. BIN
      Document/Xinje_TG765-MT_Installation_Manual_v1.1.pdf
  4. BIN
      Document/Xinje_TouchWin_User_Manual_v1.2.pdf
  5. +554
    -0
      Document/my_document/modbus学习文档.md
  6. +10
    -0
      EWARM/Modbus.ewp
  7. +80
    -0
      Modbus/Inc/modbus_rtu_slave.h
  8. +666
    -0
      Modbus/Src/modbus_rtu_slave.c

+ 50
- 30
Core/Src/main.c View File

@@ -24,6 +24,7 @@
/* USER CODE BEGIN Includes */
/* uC/OS-II的公开接口、任务类型和配置宏都由此头文件提供。 */
#include "ucos_ii.h"
#include "modbus_rtu_slave.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
@@ -47,12 +48,6 @@ DMA_HandleTypeDef hdma_usart1_rx;
DMA_HandleTypeDef hdma_usart1_tx;

/* USER CODE BEGIN PV */
static const uint8_t uart1_test_message[] = "USART1 DMA TX TEST\r\n";
static volatile uint32_t uart1_tx_ok_count;
static volatile uint32_t uart1_tx_busy_count;
static volatile uint32_t uart1_tx_error_count;
static volatile uint32_t uart1_tx_complete_count;

/*
* 每个任务必须拥有独立栈空间。
* OS_STK在本工程中为32位,因此256个OS_STK占用1024字节RAM。
@@ -83,37 +78,40 @@ static void AppTaskStart(void *p_arg);
*/
static void AppTaskStart(void *p_arg)
{
HAL_StatusTypeDef uart_status;

/* 当前没有给任务传递参数,显式转换可避免编译器产生未使用警告。 */
(void)p_arg;

/*
* F4作为Modbus RTU从站,触摸屏作为主站。
* 初始化函数会立即启动USART1的DMA空闲接收。
*/
if (ModbusSlave_Init(&huart1, MODBUS_SLAVE_DEFAULT_ADDRESS) != HAL_OK)
{
Error_Handler();
}

while (1)
{
/* DMA发送启动后立即返回,真正的发送完成由DMA中断回调通知。 */
uart_status = HAL_UART_Transmit_DMA(&huart1,
(uint8_t *)uart1_test_message,
sizeof(uart1_test_message) - 1U);

if (uart_status == HAL_OK)
{
uart1_tx_ok_count++;
}
else if (uart_status == HAL_BUSY)
{
uart1_tx_busy_count++;
}
else
{
uart1_tx_error_count++;
}
/*
* 维护几个供触摸屏首次联调读取的保持寄存器:
* 4x地址0:器件标识0xF407;
* 4x地址1:系统运行秒数;
* 4x地址2:收到的有效Modbus帧数;
* 4x地址10:触摸屏读写测试值,由协议层保存。
*/
(void)ModbusSlave_SetHoldingRegister(
HMI_REG_UPTIME_SECONDS,
(uint16_t)(OSTimeGet() / OS_TICKS_PER_SEC));
(void)ModbusSlave_SetHoldingRegister(
HMI_REG_RX_FRAME_COUNT,
(uint16_t)g_modbus_slave_stats.valid_frame_count);

/*
* 把当前任务延时OS_TICKS_PER_SEC个系统节拍。
* 当前OS_TICKS_PER_SEC为1000,SysTick周期为1ms,因此这里延时1秒。
* 延时期间任务处于等待态,内核会调度其他就绪任务(目前主要是空闲任务)。
* 协议解析和应答在任务上下文完成,中断只接收一帧并设置标志。
* 每1ms轮询一次,不在串口中断中执行耗时的CRC和寄存器操作。
*/
OSTimeDly(OS_TICKS_PER_SEC);
ModbusSlave_Poll();
OSTimeDly(1U);
}
}

@@ -336,7 +334,29 @@ void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
uart1_tx_complete_count++;
/* 应答发送完成后,协议模块重新启动DMA接收。 */
ModbusSlave_OnTxComplete(huart);
}
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1)
{
/*
* DMA遇到串口空闲事件后进入这里。
* 仅复制帧并重启DMA,协议解析由AppTaskStart任务完成。
*/
ModbusSlave_OnRxEvent(huart, Size);
}
}

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
/* 溢出、噪声、帧错误等异常后恢复USART1 DMA接收。 */
ModbusSlave_OnUartError(huart);
}
}



BIN
Document/Xinje_HMI_ModbusRTU_Function_Codes.pdf View File


BIN
Document/Xinje_TG765-MT_Installation_Manual_v1.1.pdf View File


BIN
Document/Xinje_TouchWin_User_Manual_v1.2.pdf View File


+ 554
- 0
Document/my_document/modbus学习文档.md View File

@@ -0,0 +1,554 @@
# Modbus 学习文档

## 基本概念

### Modbus 基本概念

Modbus 是一种工业通信协议,主要用于控制器、触摸屏、仪表、传感器和驱动器之间的数据交换。

Modbus 规定了通信双方如何组织地址、功能码、数据和校验信息,但不规定设备内部必须采用什么硬件或如何存储数据。某个寄存器代表温度、速度还是报警状态,需要由设备的通信说明书确定。

Modbus 具有以下特点:

- 协议结构简单,容易实现。
- 采用请求和应答方式通信。
- 支持位数据和 16 位寄存器数据。
- 支持串口和以太网。
- 不同厂商的工业设备可以按照统一报文格式交换数据。

在 Modbus 串口通信中,设备分为主站和从站:

- 主站主动发出请求,并等待从站应答。
- 从站接收并处理属于自己的请求,然后返回应答。
- 从站一般不能在没有请求的情况下主动发送业务数据。

### 应用场景

Modbus 常见于以下工业控制场景:

- PLC 与触摸屏之间交换状态和控制数据。
- PLC 读取温度、压力、流量等仪表数据。
- PLC 设置变频器频率并读取运行状态。
- 上位机监控多台 PLC 或现场设备。
- 多台从站通过 RS-485 总线连接到同一个主站。
- 工业网关在 Modbus RTU 与 Modbus TCP 之间转换。

Modbus 适合周期性读取和写入少量工业数据。它没有统一规定变量名称,因此通信前通常需要准备地址表,明确每个线圈和寄存器的用途、数据类型、读写权限及单位。

### 三种通信模式

#### Modbus RTU

Modbus RTU 使用二进制方式传输数据,通常运行在 RS-485 或 RS-232 接口上。

主要特点:

- 数据紧凑,传输效率较高。
- 使用 CRC16 校验。
- 通过总线静默时间判断帧边界。
- 是串口设备中最常见的 Modbus 模式。

#### Modbus ASCII

Modbus ASCII 把每个数据字节转换为两个 ASCII 字符发送。

例如:

```text
数据字节 0x3A -> 字符 '3' 和 'A'
```

主要特点:

- 报文以冒号 `:` 开始。
- 报文以回车和换行结束。
- 使用 LRC 校验。
- 便于通过文本终端观察,但传输效率低于 RTU。

#### Modbus TCP

Modbus TCP 运行在 TCP/IP 网络上,默认使用 TCP 端口 502。

主要特点:

- 使用以太网通信。
- 报文包含 MBAP 报文头。
- TCP 已经提供传输校验,因此不再附加 RTU 的 CRC16。
- 可以同时建立多个网络连接。

#### 三种模式对比

| 对比项 | Modbus RTU | Modbus ASCII | Modbus TCP |
|---|---|---|---|
| 常用接口 | RS-485、RS-232 | RS-485、RS-232 | 以太网 |
| 数据形式 | 二进制 | ASCII 字符 | 二进制 |
| 校验方式 | CRC16 | LRC | TCP/IP 校验 |
| 帧边界 | 静默时间 | 起止字符 | MBAP 长度字段 |
| 传输效率 | 高 | 较低 | 高 |

三种模式的外部封装不同,但功能码和主要数据含义基本一致。

### 广播模式与单播模式

#### 单播模式

主站使用从站地址 1~247访问指定从站。从站处理请求并返回应答。

```text
主站发送请求 -> 指定从站处理请求 -> 从站返回应答
```

主站可以根据应答判断本次事务是否成功。如果规定时间内没有收到应答,则可以进行超时或重试处理。

#### 广播模式

地址 0 是 Modbus 串口通信中的广播地址。主站向地址 0 发送请求时,所有从站都可以接收并执行。

广播模式具有以下特点:

- 广播只适合写操作。
- 从站执行广播写请求后不能返回应答。
- 广播读请求没有实际意义,应被从站忽略。
- 主站无法确认每个从站是否执行成功。

广播请求不能产生应答,是因为多个从站同时发送数据会造成总线冲突。

### 事务处理流程

一次 Modbus 请求和应答过程称为一个事务。

#### 主站事务处理流程

```text
确定从站地址和操作内容
|
生成请求数据
|
添加功能码和校验信息
|
发送请求帧
|
启动应答超时计时
|
接收从站应答
|
检查地址、功能码、长度和校验
|
处理正常应答或异常应答
```

主站需要处理以下情况:

- 收到正确的正常应答。
- 收到从站异常应答。
- 等待应答超时。
- 应答 CRC 或 LRC 错误。
- 应答地址与请求地址不一致。
- 应答功能码与请求功能码不匹配。
- 应答长度或字节数错误。

#### 从站事务处理流程

```text
接收请求帧
|
检查帧长度和校验
|
检查从站地址
|
检查功能码
|
检查数据地址、数量和字节数
|
执行读取或写入
|
生成正常应答或异常应答
|
添加校验并发送
```

从站应先完成报文校验和参数检查,再修改内部数据。CRC 错误、帧不完整或单播地址不属于本机的请求通常直接丢弃。

#### 超时与重试

主站发送请求后,如果在规定时间内没有收到完整且正确的应答,则本次事务超时。

主站可以进行有限次数重试:

```text
发送请求
-> 超时
-> 第一次重试
-> 仍然超时
-> 第二次重试
-> 仍然超时
-> 判断从站离线
```

重试次数不能无限增加,否则离线设备会长期占用通信总线。

### 寄存器相关概念

#### 四种数据区

Modbus 定义了四个相互独立的数据区。

| 数据区 | 数据宽度 | 访问权限 | 读取功能码 | 常用写入功能码 |
|---|---:|---|---|---|
| 线圈 Coils | 1 bit | 读/写 | 0x01 | 0x05、0x0F |
| 离散输入 Discrete Inputs | 1 bit | 只读 | 0x02 | 无 |
| 保持寄存器 Holding Registers | 16 bit | 读/写 | 0x03 | 0x06、0x10 |
| 输入寄存器 Input Registers | 16 bit | 只读 | 0x04 | 无 |

不同数据区相互独立,可以拥有相同的地址。例如,线圈地址 0 和保持寄存器地址 0 不冲突,因为访问它们所使用的功能码不同。

#### 线圈

一个线圈只保存一个二进制状态:

```text
0 = OFF
1 = ON
```

多个线圈在报文中按位打包。请求中的第一个线圈放在第一个数据字节的最低位 bit0。

#### 寄存器

一个 Modbus 寄存器为 16 位,占 2 字节。

- 无符号数范围:0~65535。
- 有符号数范围:-32768~32767。

Modbus 只规定寄存器宽度,不规定寄存器中的数据类型。一个实际变量可以使用一个或多个寄存器表示:

- 16 位整数:占用 1 个寄存器。
- 32 位整数:占用 2 个寄存器。
- 32 位浮点数:占用 2 个寄存器。
- 字符串:占用多个连续寄存器。

使用多个寄存器表示一个数据时,通信双方必须约定高低字顺序。

#### 逻辑编号与协议地址

部分资料使用 `00001` 表示第一个线圈,使用 `40001` 表示第一个保持寄存器。这些是便于阅读的逻辑编号,不一定是报文中的实际地址。

例如:

```text
逻辑编号 40001 -> 报文地址通常为 0x0000
逻辑编号 40002 -> 报文地址通常为 0x0001
```

不同设备软件可能采用从 0 开始或从 1 开始的显示方式。通信时应以设备手册和实际发送的报文地址为准。

## 数据帧格式

### Modbus RTU 基本帧格式

Modbus RTU 报文由以下字段组成:

| 字段 | 长度 | 说明 |
|---|---:|---|
| 从站地址 | 1 字节 | 单播地址 1~247,广播地址 0 |
| 功能码 | 1 字节 | 表示读取或写入操作 |
| 数据 | 可变 | 起始地址、数量、字节数和数据值 |
| CRC16 | 2 字节 | 低字节先发送,高字节后发送 |

Modbus RTU ADU 的最大长度为 256 字节。

地址、数量和寄存器值等 16 位字段采用高字节在前:

```text
数值 0x1234 -> 发送 12 34
```

CRC 在线路上采用低字节在前:

```text
CRC 计算结果 0x0A84 -> 发送 84 0A
```

#### CRC16

Modbus RTU 的 CRC16 参数如下:

- 初始值:`0xFFFF`。
- 多项式:`0xA001`。
- 计算范围:从站地址、功能码和数据字段。
- 接收方重新计算 CRC,并与报文最后两个字节比较。

CRC 不正确的报文应直接丢弃,不能返回异常应答。

#### T1.5 与 T3.5

Modbus RTU 通过静默时间区分报文:

- T1.5:1.5 个字符的传输时间。
- T3.5:3.5 个字符的传输时间。

一帧内部的字节应连续发送。如果相邻字节间隔超过 T1.5,当前报文可能被判定为不完整。两帧之间至少保持 T3.5 的静默时间。

波特率高于 19200 bit/s 时,通常建议采用:

- T1.5:750 μs。
- T3.5:1.75 ms。

### 0x01 功能码

功能码 `0x01` 用于读取线圈。

#### 请求帧格式

| 字段 | 长度 |
|---|---:|
| 从站地址 | 1 字节 |
| 功能码 0x01 | 1 字节 |
| 起始地址 | 2 字节 |
| 线圈数量 | 2 字节 |
| CRC | 2 字节 |

请求读取从站 1 的线圈地址 0,共读取 1 个线圈:

```text
01 01 00 00 00 01 FD CA
```

字段解释:

```text
01 从站地址
01 功能码
00 00 起始地址
00 01 读取数量
FD CA CRC
```

线圈读取数量的标准范围为 1~2000。

#### 应答帧格式

| 字段 | 长度 |
|---|---:|
| 从站地址 | 1 字节 |
| 功能码 0x01 | 1 字节 |
| 数据字节数 | 1 字节 |
| 线圈数据 | N 字节 |
| CRC | 2 字节 |

如果线圈地址 0 为 ON:

```text
01 01 01 01 90 48
```

线圈按最低位优先排列:

```text
数据字节 bit0 -> 请求中的第 1 个线圈
数据字节 bit1 -> 请求中的第 2 个线圈
...
数据字节 bit7 -> 请求中的第 8 个线圈
```

最后一个数据字节中没有使用的高位应置 0。

### 0x03 功能码

功能码 `0x03` 用于读取保持寄存器。

#### 请求帧格式

| 字段 | 长度 |
|---|---:|
| 从站地址 | 1 字节 |
| 功能码 0x03 | 1 字节 |
| 起始地址 | 2 字节 |
| 寄存器数量 | 2 字节 |
| CRC | 2 字节 |

请求读取从站 1 的保持寄存器地址 0,共读取 1 个寄存器:

```text
01 03 00 00 00 01 84 0A
```

保持寄存器读取数量的标准范围为 1~125。

#### 应答帧格式

| 字段 | 长度 |
|---|---:|
| 从站地址 | 1 字节 |
| 功能码 0x03 | 1 字节 |
| 数据字节数 | 1 字节 |
| 寄存器数据 | N 字节 |
| CRC | 2 字节 |

假设保持寄存器地址 0 的值为 `0x1234`:

```text
01 03 02 12 34 B5 33
```

其中:

```text
02 后续数据占 2 字节
12 34 寄存器值
```

读取多个寄存器时,数据按照寄存器地址递增顺序排列。

### 0x0F 功能码

功能码 `0x0F` 用于写多个线圈。

#### 请求帧格式

| 字段 | 长度 |
|---|---:|
| 从站地址 | 1 字节 |
| 功能码 0x0F | 1 字节 |
| 起始地址 | 2 字节 |
| 线圈数量 | 2 字节 |
| 数据字节数 | 1 字节 |
| 线圈数据 | N 字节 |
| CRC | 2 字节 |

把地址 0~7 的 8 个线圈写成 `0x55`:

```text
01 0F 00 00 00 08 01 55 3E AA
```

字段解释:

```text
01 从站地址
0F 功能码
00 00 起始地址
00 08 写入 8 个线圈
01 数据字节数
55 线圈数据
3E AA CRC
```

线圈写入数量的标准范围为 1~1968。

数据字节数的计算方法:

```text
数据字节数 = (线圈数量 + 7) / 8,采用整数除法
```

#### 应答帧格式

正常应答回显起始地址和实际写入数量:

| 字段 | 长度 |
|---|---:|
| 从站地址 | 1 字节 |
| 功能码 0x0F | 1 字节 |
| 起始地址 | 2 字节 |
| 写入数量 | 2 字节 |
| CRC | 2 字节 |

应答示例:

```text
01 0F 00 00 00 08 54 0D
```

### 0x10 功能码

功能码 `0x10` 用于写多个保持寄存器。

#### 请求帧格式

| 字段 | 长度 |
|---|---:|
| 从站地址 | 1 字节 |
| 功能码 0x10 | 1 字节 |
| 起始地址 | 2 字节 |
| 寄存器数量 | 2 字节 |
| 数据字节数 | 1 字节 |
| 寄存器数据 | N 字节 |
| CRC | 2 字节 |

从地址 10 开始写入两个寄存器,数据分别为 `0x1234` 和 `0x5678`:

```text
01 10 00 0A 00 02 04 12 34 56 78 08 E4
```

字段解释:

```text
01 从站地址
10 功能码
00 0A 起始地址
00 02 写入 2 个寄存器
04 数据字节数
12 34 第一个寄存器的数据
56 78 第二个寄存器的数据
08 E4 CRC
```

寄存器写入数量的标准范围为 1~123,数据字节数必须等于寄存器数量乘以 2。

#### 应答帧格式

正常应答回显起始地址和实际写入数量:

| 字段 | 长度 |
|---|---:|
| 从站地址 | 1 字节 |
| 功能码 0x10 | 1 字节 |
| 起始地址 | 2 字节 |
| 写入数量 | 2 字节 |
| CRC | 2 字节 |

应答示例:

```text
01 10 00 0A 00 02 61 CA
```

### 异常应答帧

当请求报文校验正确,但从站无法执行请求时,从站可以返回异常应答。

异常功能码等于原功能码加 `0x80`:

```text
0x01 -> 0x81
0x03 -> 0x83
0x0F -> 0x8F
0x10 -> 0x90
```

异常应答格式:

| 字段 | 长度 |
|---|---:|
| 从站地址 | 1 字节 |
| 异常功能码 | 1 字节 |
| 异常码 | 1 字节 |
| CRC | 2 字节 |

常见异常码:

| 异常码 | 名称 | 含义 |
|---:|---|---|
| 0x01 | 非法功能 | 从站不支持该功能码 |
| 0x02 | 非法数据地址 | 请求地址不存在或访问范围越界 |
| 0x03 | 非法数据值 | 数量、字节数或数据值不合法 |
| 0x04 | 从站设备故障 | 从站执行请求时发生内部错误 |
| 0x06 | 从站设备忙 | 从站暂时不能处理请求 |

例如,从站 1 对 0x03 请求返回非法数据地址:

```text
01 83 02 C0 F1
```

+ 10
- 0
EWARM/Modbus.ewp View File

@@ -359,6 +359,7 @@
<state>$PROJ_DIR$/../Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc</state>
<state>$PROJ_DIR$/../Drivers/CMSIS/Device/ST/STM32F4xx/Include</state>
<state>$PROJ_DIR$/../Drivers/CMSIS/Include</state>
<state>$PROJ_DIR$\..\Modbus\Inc</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\Config</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Source</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Ports\ARM-Cortex-M4\IAR</state>
@@ -1180,6 +1181,15 @@
</file>
</group>
</group>
<group>
<name>Modbus</name>
<file>
<name>$PROJ_DIR$\..\Modbus\Inc\modbus_rtu_slave.h</name>
</file>
<file>
<name>$PROJ_DIR$\..\Modbus\Src\modbus_rtu_slave.c</name>
</file>
</group>
<group>
<name>Micrium</name>
<group>


+ 80
- 0
Modbus/Inc/modbus_rtu_slave.h View File

@@ -0,0 +1,80 @@
#ifndef MODBUS_RTU_SLAVE_H
#define MODBUS_RTU_SLAVE_H

#include "stm32f4xx_hal.h"

#ifdef __cplusplus
extern "C" {
#endif

/*
* 任务文档规定线圈和保持寄存器地址范围均为0~0x270F。
* 因此地址总数是0x2710,即10000个。
*/
#define MODBUS_MAP_LAST_ADDRESS 0x270FU
#define MODBUS_MAP_ITEM_COUNT 0x2710U

#define MODBUS_SLAVE_DEFAULT_ADDRESS 1U
#define MODBUS_CONNECTION_TIMEOUT_MS 1000U

/*
* 供第一次与触摸屏联调使用的演示地址。
* TouchWin选择“起始地址为0”的Modbus RTU主站协议后,可直接访问这些地址。
*/
#define HMI_REG_DEVICE_ID 0U
#define HMI_REG_UPTIME_SECONDS 1U
#define HMI_REG_RX_FRAME_COUNT 2U
#define HMI_REG_WRITE_TEST 10U
#define HMI_COIL_WRITE_TEST 0U

typedef struct
{
uint32_t rx_event_count;
uint32_t valid_frame_count;
uint32_t tx_frame_count;
uint32_t crc_error_count;
uint32_t ignored_address_count;
uint32_t illegal_function_count;
uint32_t illegal_address_count;
uint32_t illegal_value_count;
uint32_t dropped_frame_count;
uint32_t uart_error_count;
} ModbusSlaveStats;

extern volatile ModbusSlaveStats g_modbus_slave_stats;

/*
* 初始化从站并启动USART DMA空闲接收。
* 必须在MX_USART1_UART_Init()和DMA初始化完成后调用。
*/
HAL_StatusTypeDef ModbusSlave_Init(UART_HandleTypeDef *huart,
uint8_t slave_address);

/*
* 在uC/OS-II任务中周期调用,用于校验并处理一帧请求。
* 中断只负责搬运数据,协议解析留在任务上下文中执行。
*/
void ModbusSlave_Poll(void);

/* 以下三个接口由STM32 HAL串口回调调用。 */
void ModbusSlave_OnRxEvent(UART_HandleTypeDef *huart, uint16_t size);
void ModbusSlave_OnTxComplete(UART_HandleTypeDef *huart);
void ModbusSlave_OnUartError(UART_HandleTypeDef *huart);

/* 应用层访问保持寄存器和线圈的接口。返回0表示地址非法。 */
uint8_t ModbusSlave_SetHoldingRegister(uint16_t address, uint16_t value);
uint8_t ModbusSlave_GetHoldingRegister(uint16_t address, uint16_t *value);
uint8_t ModbusSlave_SetCoil(uint16_t address, uint8_t state);
uint8_t ModbusSlave_GetCoil(uint16_t address, uint8_t *state);

/*
* 在timeout_ms时间内收到过有效请求即认为触摸屏在线。
* 该状态适用于实现任务要求中的连接/断线判断。
*/
uint8_t ModbusSlave_IsConnected(uint32_t timeout_ms);

#ifdef __cplusplus
}
#endif

#endif

+ 666
- 0
Modbus/Src/modbus_rtu_slave.c View File

@@ -0,0 +1,666 @@
#include "modbus_rtu_slave.h"

#include <string.h>

/* Modbus RTU最大ADU为256字节。 */
#define MODBUS_RTU_ADU_SIZE_MAX 256U

/*
* 当前串口为115200 bit/s。Modbus规范建议波特率高于19200时,
* 帧间隔固定使用1.75ms,这里向上取整为2ms。
*/
#define MODBUS_RTU_FRAME_GAP_MS 2U

#define MODBUS_BROADCAST_ADDRESS 0U

#define MODBUS_FC_READ_COILS 0x01U
#define MODBUS_FC_READ_HOLDING_REGS 0x03U
#define MODBUS_FC_WRITE_SINGLE_REG 0x06U
#define MODBUS_FC_WRITE_MULTIPLE_COILS 0x0FU
#define MODBUS_FC_WRITE_MULTIPLE_REGS 0x10U

#define MODBUS_EX_ILLEGAL_FUNCTION 0x01U
#define MODBUS_EX_ILLEGAL_ADDRESS 0x02U
#define MODBUS_EX_ILLEGAL_VALUE 0x03U

#define MODBUS_READ_COILS_MAX 2000U
#define MODBUS_READ_REGS_MAX 125U
#define MODBUS_WRITE_COILS_MAX 1968U
#define MODBUS_WRITE_REGS_MAX 123U

static UART_HandleTypeDef *s_uart;
static uint8_t s_slave_address;

/*
* DMA缓冲区只由DMA写入;帧缓冲区在接收回调中完成一次快照,
* 随后由任务解析,避免DMA重启后覆盖尚未处理的数据。
*/
static uint8_t s_rx_dma_buffer[MODBUS_RTU_ADU_SIZE_MAX];
static uint8_t s_rx_frame[MODBUS_RTU_ADU_SIZE_MAX];
static uint8_t s_tx_frame[MODBUS_RTU_ADU_SIZE_MAX];

static volatile uint16_t s_rx_frame_length;
static volatile uint8_t s_rx_frame_ready;
static volatile uint8_t s_tx_busy;
static volatile uint32_t s_rx_event_tick;
static volatile uint32_t s_last_valid_frame_tick;
static volatile uint8_t s_has_received_valid_frame;

/* 10000个保持寄存器占20KB;10000个线圈按位存储,占1250字节。 */
static uint16_t s_holding_registers[MODBUS_MAP_ITEM_COUNT];
static uint8_t s_coils[(MODBUS_MAP_ITEM_COUNT + 7U) / 8U];

volatile ModbusSlaveStats g_modbus_slave_stats;

static uint16_t Modbus_Crc16(const uint8_t *data, uint16_t length)
{
uint16_t crc = 0xFFFFU;
uint16_t index;
uint8_t bit;

for (index = 0U; index < length; index++)
{
crc ^= data[index];

for (bit = 0U; bit < 8U; bit++)
{
if ((crc & 0x0001U) != 0U)
{
crc = (uint16_t)((crc >> 1U) ^ 0xA001U);
}
else
{
crc >>= 1U;
}
}
}

return crc;
}

static uint16_t Modbus_GetU16Be(const uint8_t *data)
{
return (uint16_t)(((uint16_t)data[0] << 8U) | data[1]);
}

static uint8_t Modbus_AddressRangeIsValid(uint16_t start, uint16_t quantity)
{
if ((quantity == 0U) || (start >= MODBUS_MAP_ITEM_COUNT))
{
return 0U;
}

/* 先做减法再比较,避免start + quantity发生16位溢出。 */
return (quantity <= (MODBUS_MAP_ITEM_COUNT - start)) ? 1U : 0U;
}

static uint8_t Modbus_CoilGetUnchecked(uint16_t address)
{
uint8_t mask = (uint8_t)(1U << (address & 0x0007U));

return ((s_coils[address >> 3U] & mask) != 0U) ? 1U : 0U;
}

static void Modbus_CoilSetUnchecked(uint16_t address, uint8_t state)
{
uint8_t mask = (uint8_t)(1U << (address & 0x0007U));

if (state != 0U)
{
s_coils[address >> 3U] |= mask;
}
else
{
s_coils[address >> 3U] &= (uint8_t)(~mask);
}
}

static HAL_StatusTypeDef Modbus_StartReceive(void)
{
HAL_StatusTypeDef status;

if ((s_uart == NULL) || (s_tx_busy != 0U))
{
return HAL_BUSY;
}

status = HAL_UARTEx_ReceiveToIdle_DMA(s_uart,
s_rx_dma_buffer,
sizeof(s_rx_dma_buffer));

if ((status == HAL_OK) && (s_uart->hdmarx != NULL))
{
/*
* 普通Modbus帧只应在IDLE或缓冲区满时交给应用。
* 关闭DMA半传输中断,避免长帧在一半位置被误认为完整帧。
*/
__HAL_DMA_DISABLE_IT(s_uart->hdmarx, DMA_IT_HT);
}

return status;
}

static void Modbus_StopReceive(void)
{
if (s_uart != NULL)
{
(void)HAL_UART_AbortReceive(s_uart);
}
}

static void Modbus_AppendCrc(uint8_t *frame, uint16_t payload_length)
{
uint16_t crc = Modbus_Crc16(frame, payload_length);

/* Modbus RTU在线路上传输CRC低字节在前、高字节在后。 */
frame[payload_length] = (uint8_t)(crc & 0x00FFU);
frame[payload_length + 1U] = (uint8_t)(crc >> 8U);
}

static uint16_t Modbus_BuildException(uint8_t function, uint8_t exception)
{
s_tx_frame[0] = s_slave_address;
s_tx_frame[1] = (uint8_t)(function | 0x80U);
s_tx_frame[2] = exception;
Modbus_AppendCrc(s_tx_frame, 3U);

return 5U;
}

static uint16_t Modbus_ProcessReadCoils(const uint8_t *request,
uint16_t request_length)
{
uint16_t start;
uint16_t quantity;
uint16_t index;
uint8_t byte_count;

if (request_length != 8U)
{
g_modbus_slave_stats.illegal_value_count++;
return Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

start = Modbus_GetU16Be(&request[2]);
quantity = Modbus_GetU16Be(&request[4]);

if ((quantity == 0U) || (quantity > MODBUS_READ_COILS_MAX))
{
g_modbus_slave_stats.illegal_value_count++;
return Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

if (Modbus_AddressRangeIsValid(start, quantity) == 0U)
{
g_modbus_slave_stats.illegal_address_count++;
return Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS);
}

byte_count = (uint8_t)((quantity + 7U) / 8U);
s_tx_frame[0] = s_slave_address;
s_tx_frame[1] = MODBUS_FC_READ_COILS;
s_tx_frame[2] = byte_count;
(void)memset(&s_tx_frame[3], 0, byte_count);

/* Modbus规定请求的第一个线圈放在响应数据首字节的最低位。 */
for (index = 0U; index < quantity; index++)
{
if (Modbus_CoilGetUnchecked((uint16_t)(start + index)) != 0U)
{
s_tx_frame[3U + (index >> 3U)] |=
(uint8_t)(1U << (index & 0x0007U));
}
}

Modbus_AppendCrc(s_tx_frame, (uint16_t)(3U + byte_count));
return (uint16_t)(5U + byte_count);
}

static uint16_t Modbus_ProcessReadHolding(const uint8_t *request,
uint16_t request_length)
{
uint16_t start;
uint16_t quantity;
uint16_t index;
uint16_t value;

if (request_length != 8U)
{
g_modbus_slave_stats.illegal_value_count++;
return Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

start = Modbus_GetU16Be(&request[2]);
quantity = Modbus_GetU16Be(&request[4]);

if ((quantity == 0U) || (quantity > MODBUS_READ_REGS_MAX))
{
g_modbus_slave_stats.illegal_value_count++;
return Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

if (Modbus_AddressRangeIsValid(start, quantity) == 0U)
{
g_modbus_slave_stats.illegal_address_count++;
return Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS);
}

s_tx_frame[0] = s_slave_address;
s_tx_frame[1] = MODBUS_FC_READ_HOLDING_REGS;
s_tx_frame[2] = (uint8_t)(quantity * 2U);

for (index = 0U; index < quantity; index++)
{
value = s_holding_registers[start + index];
s_tx_frame[3U + index * 2U] = (uint8_t)(value >> 8U);
s_tx_frame[4U + index * 2U] = (uint8_t)(value & 0x00FFU);
}

Modbus_AppendCrc(s_tx_frame, (uint16_t)(3U + quantity * 2U));
return (uint16_t)(5U + quantity * 2U);
}

static uint16_t Modbus_ProcessWriteSingleRegister(const uint8_t *request,
uint16_t request_length,
uint8_t is_broadcast)
{
uint16_t address;
uint16_t value;

if (request_length != 8U)
{
g_modbus_slave_stats.illegal_value_count++;
return (is_broadcast != 0U) ? 0U :
Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

address = Modbus_GetU16Be(&request[2]);
value = Modbus_GetU16Be(&request[4]);

if (address >= MODBUS_MAP_ITEM_COUNT)
{
g_modbus_slave_stats.illegal_address_count++;
return (is_broadcast != 0U) ? 0U :
Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS);
}

s_holding_registers[address] = value;

if (is_broadcast != 0U)
{
return 0U;
}

/* 0x06的正常响应必须原样回显请求的前6个字节。 */
(void)memcpy(s_tx_frame, request, 6U);
Modbus_AppendCrc(s_tx_frame, 6U);
return 8U;
}

static uint16_t Modbus_ProcessWriteMultipleCoils(const uint8_t *request,
uint16_t request_length,
uint8_t is_broadcast)
{
uint16_t start;
uint16_t quantity;
uint16_t index;
uint8_t byte_count;
uint8_t expected_byte_count;

if (request_length < 9U)
{
g_modbus_slave_stats.illegal_value_count++;
return (is_broadcast != 0U) ? 0U :
Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

start = Modbus_GetU16Be(&request[2]);
quantity = Modbus_GetU16Be(&request[4]);
byte_count = request[6];
expected_byte_count = (uint8_t)((quantity + 7U) / 8U);

if ((quantity == 0U) ||
(quantity > MODBUS_WRITE_COILS_MAX) ||
(byte_count != expected_byte_count) ||
(request_length != (uint16_t)(9U + byte_count)))
{
g_modbus_slave_stats.illegal_value_count++;
return (is_broadcast != 0U) ? 0U :
Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

if (Modbus_AddressRangeIsValid(start, quantity) == 0U)
{
g_modbus_slave_stats.illegal_address_count++;
return (is_broadcast != 0U) ? 0U :
Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS);
}

for (index = 0U; index < quantity; index++)
{
Modbus_CoilSetUnchecked((uint16_t)(start + index),
(uint8_t)((request[7U + (index >> 3U)] >> (index & 0x0007U)) & 0x01U));
}

if (is_broadcast != 0U)
{
return 0U;
}

s_tx_frame[0] = s_slave_address;
s_tx_frame[1] = MODBUS_FC_WRITE_MULTIPLE_COILS;
(void)memcpy(&s_tx_frame[2], &request[2], 4U);
Modbus_AppendCrc(s_tx_frame, 6U);
return 8U;
}

static uint16_t Modbus_ProcessWriteMultipleRegisters(const uint8_t *request,
uint16_t request_length,
uint8_t is_broadcast)
{
uint16_t start;
uint16_t quantity;
uint16_t index;
uint8_t byte_count;

if (request_length < 9U)
{
g_modbus_slave_stats.illegal_value_count++;
return (is_broadcast != 0U) ? 0U :
Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

start = Modbus_GetU16Be(&request[2]);
quantity = Modbus_GetU16Be(&request[4]);
byte_count = request[6];

if ((quantity == 0U) ||
(quantity > MODBUS_WRITE_REGS_MAX) ||
(byte_count != (uint8_t)(quantity * 2U)) ||
(request_length != (uint16_t)(9U + byte_count)))
{
g_modbus_slave_stats.illegal_value_count++;
return (is_broadcast != 0U) ? 0U :
Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

if (Modbus_AddressRangeIsValid(start, quantity) == 0U)
{
g_modbus_slave_stats.illegal_address_count++;
return (is_broadcast != 0U) ? 0U :
Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS);
}

for (index = 0U; index < quantity; index++)
{
s_holding_registers[start + index] =
Modbus_GetU16Be(&request[7U + index * 2U]);
}

if (is_broadcast != 0U)
{
return 0U;
}

s_tx_frame[0] = s_slave_address;
s_tx_frame[1] = MODBUS_FC_WRITE_MULTIPLE_REGS;
(void)memcpy(&s_tx_frame[2], &request[2], 4U);
Modbus_AppendCrc(s_tx_frame, 6U);
return 8U;
}

static uint16_t Modbus_ProcessRequest(const uint8_t *request,
uint16_t request_length)
{
uint16_t calculated_crc;
uint16_t received_crc;
uint8_t is_broadcast;

if (request_length < 4U)
{
return 0U;
}

calculated_crc = Modbus_Crc16(request, (uint16_t)(request_length - 2U));
received_crc = (uint16_t)(request[request_length - 2U] |
((uint16_t)request[request_length - 1U] << 8U));

if (calculated_crc != received_crc)
{
g_modbus_slave_stats.crc_error_count++;
return 0U;
}

if ((request[0] != s_slave_address) &&
(request[0] != MODBUS_BROADCAST_ADDRESS))
{
g_modbus_slave_stats.ignored_address_count++;
return 0U;
}

is_broadcast = (request[0] == MODBUS_BROADCAST_ADDRESS) ? 1U : 0U;
g_modbus_slave_stats.valid_frame_count++;
s_last_valid_frame_tick = HAL_GetTick();
s_has_received_valid_frame = 1U;

switch (request[1])
{
case MODBUS_FC_READ_COILS:
/* 广播请求不允许读取,从站不作响应。 */
return (is_broadcast != 0U) ? 0U :
Modbus_ProcessReadCoils(request, request_length);

case MODBUS_FC_READ_HOLDING_REGS:
return (is_broadcast != 0U) ? 0U :
Modbus_ProcessReadHolding(request, request_length);

case MODBUS_FC_WRITE_SINGLE_REG:
return Modbus_ProcessWriteSingleRegister(request,
request_length,
is_broadcast);

case MODBUS_FC_WRITE_MULTIPLE_COILS:
return Modbus_ProcessWriteMultipleCoils(request,
request_length,
is_broadcast);

case MODBUS_FC_WRITE_MULTIPLE_REGS:
return Modbus_ProcessWriteMultipleRegisters(request,
request_length,
is_broadcast);

default:
g_modbus_slave_stats.illegal_function_count++;
return (is_broadcast != 0U) ? 0U :
Modbus_BuildException(request[1], MODBUS_EX_ILLEGAL_FUNCTION);
}
}

HAL_StatusTypeDef ModbusSlave_Init(UART_HandleTypeDef *huart,
uint8_t slave_address)
{
if ((huart == NULL) ||
(slave_address == MODBUS_BROADCAST_ADDRESS) ||
(slave_address > 247U))
{
return HAL_ERROR;
}

s_uart = huart;
s_slave_address = slave_address;
s_rx_frame_length = 0U;
s_rx_frame_ready = 0U;
s_tx_busy = 0U;
s_has_received_valid_frame = 0U;

(void)memset(s_holding_registers, 0, sizeof(s_holding_registers));
(void)memset(s_coils, 0, sizeof(s_coils));
(void)memset((void *)&g_modbus_slave_stats, 0,
sizeof(g_modbus_slave_stats));

s_holding_registers[HMI_REG_DEVICE_ID] = 0xF407U;

return Modbus_StartReceive();
}

void ModbusSlave_Poll(void)
{
uint16_t response_length;

if ((s_rx_frame_ready == 0U) || (s_tx_busy != 0U))
{
return;
}

/*
* IDLE事件在约1个字符静默后产生;再等待到2ms,
* 满足当前115200波特率下的Modbus RTU帧间隔要求。
*/
if ((HAL_GetTick() - s_rx_event_tick) < MODBUS_RTU_FRAME_GAP_MS)
{
return;
}

response_length = Modbus_ProcessRequest(s_rx_frame, s_rx_frame_length);

if (response_length > 0U)
{
/*
* 发送期间停止接收,避免自动收发电路可能产生的本机回波
* 被误当成下一条主站请求。
*/
Modbus_StopReceive();
s_tx_busy = 1U;

if (HAL_UART_Transmit_DMA(s_uart, s_tx_frame, response_length) == HAL_OK)
{
g_modbus_slave_stats.tx_frame_count++;
}
else
{
s_tx_busy = 0U;
g_modbus_slave_stats.uart_error_count++;
(void)Modbus_StartReceive();
}
}

/*
* 处理期间保持ready=1,使中断不会覆盖当前帧;
* 到这里再释放帧槽。
*/
s_rx_frame_ready = 0U;
}

void ModbusSlave_OnRxEvent(UART_HandleTypeDef *huart, uint16_t size)
{
if ((huart != s_uart) || (s_uart == NULL))
{
return;
}

g_modbus_slave_stats.rx_event_count++;

if ((size > 0U) && (size <= MODBUS_RTU_ADU_SIZE_MAX))
{
if (s_rx_frame_ready == 0U)
{
(void)memcpy(s_rx_frame, s_rx_dma_buffer, size);
s_rx_frame_length = size;
s_rx_event_tick = HAL_GetTick();
s_rx_frame_ready = 1U;
}
else
{
/* 单帧槽尚未处理完成,丢弃新帧而不覆盖旧帧。 */
g_modbus_slave_stats.dropped_frame_count++;
}
}
else
{
g_modbus_slave_stats.dropped_frame_count++;
}

/* DMA普通模式在IDLE事件后已经停止,需要显式重新启动。 */
if (s_tx_busy == 0U)
{
(void)Modbus_StartReceive();
}
}

void ModbusSlave_OnTxComplete(UART_HandleTypeDef *huart)
{
if ((huart != s_uart) || (s_uart == NULL))
{
return;
}

s_tx_busy = 0U;
(void)Modbus_StartReceive();
}

void ModbusSlave_OnUartError(UART_HandleTypeDef *huart)
{
if ((huart != s_uart) || (s_uart == NULL))
{
return;
}

g_modbus_slave_stats.uart_error_count++;
s_tx_busy = 0U;
(void)HAL_UART_Abort(huart);
(void)Modbus_StartReceive();
}

uint8_t ModbusSlave_SetHoldingRegister(uint16_t address, uint16_t value)
{
if (address >= MODBUS_MAP_ITEM_COUNT)
{
return 0U;
}

s_holding_registers[address] = value;
return 1U;
}

uint8_t ModbusSlave_GetHoldingRegister(uint16_t address, uint16_t *value)
{
if ((address >= MODBUS_MAP_ITEM_COUNT) || (value == NULL))
{
return 0U;
}

*value = s_holding_registers[address];
return 1U;
}

uint8_t ModbusSlave_SetCoil(uint16_t address, uint8_t state)
{
if (address >= MODBUS_MAP_ITEM_COUNT)
{
return 0U;
}

Modbus_CoilSetUnchecked(address, state);
return 1U;
}

uint8_t ModbusSlave_GetCoil(uint16_t address, uint8_t *state)
{
if ((address >= MODBUS_MAP_ITEM_COUNT) || (state == NULL))
{
return 0U;
}

*state = Modbus_CoilGetUnchecked(address);
return 1U;
}

uint8_t ModbusSlave_IsConnected(uint32_t timeout_ms)
{
if (s_has_received_valid_frame == 0U)
{
return 0U;
}

return ((HAL_GetTick() - s_last_valid_frame_tick) <= timeout_ms) ? 1U : 0U;
}

Loading…
Cancel
Save