diff --git a/Core/Src/main.c b/Core/Src/main.c
index 83b3578..9d2c44c 100644
--- a/Core/Src/main.c
+++ b/Core/Src/main.c
@@ -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);
}
}
diff --git a/Document/Xinje_HMI_ModbusRTU_Function_Codes.pdf b/Document/Xinje_HMI_ModbusRTU_Function_Codes.pdf
new file mode 100644
index 0000000..54ccec1
Binary files /dev/null and b/Document/Xinje_HMI_ModbusRTU_Function_Codes.pdf differ
diff --git a/Document/Xinje_TG765-MT_Installation_Manual_v1.1.pdf b/Document/Xinje_TG765-MT_Installation_Manual_v1.1.pdf
new file mode 100644
index 0000000..7fa1198
Binary files /dev/null and b/Document/Xinje_TG765-MT_Installation_Manual_v1.1.pdf differ
diff --git a/Document/Xinje_TouchWin_User_Manual_v1.2.pdf b/Document/Xinje_TouchWin_User_Manual_v1.2.pdf
new file mode 100644
index 0000000..e378a60
Binary files /dev/null and b/Document/Xinje_TouchWin_User_Manual_v1.2.pdf differ
diff --git a/Document/my_document/modbus学习文档.md b/Document/my_document/modbus学习文档.md
new file mode 100644
index 0000000..eb1b315
--- /dev/null
+++ b/Document/my_document/modbus学习文档.md
@@ -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
+```
diff --git a/EWARM/Modbus.ewp b/EWARM/Modbus.ewp
index 55c8775..dc25351 100644
--- a/EWARM/Modbus.ewp
+++ b/EWARM/Modbus.ewp
@@ -359,6 +359,7 @@
$PROJ_DIR$/../Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc
$PROJ_DIR$/../Drivers/CMSIS/Device/ST/STM32F4xx/Include
$PROJ_DIR$/../Drivers/CMSIS/Include
+ $PROJ_DIR$\..\Modbus\Inc
$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\Config
$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Source
$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Ports\ARM-Cortex-M4\IAR
@@ -1180,6 +1181,15 @@
+
+ Modbus
+
+ $PROJ_DIR$\..\Modbus\Inc\modbus_rtu_slave.h
+
+
+ $PROJ_DIR$\..\Modbus\Src\modbus_rtu_slave.c
+
+
Micrium
diff --git a/Modbus/Inc/modbus_rtu_slave.h b/Modbus/Inc/modbus_rtu_slave.h
new file mode 100644
index 0000000..e76ba54
--- /dev/null
+++ b/Modbus/Inc/modbus_rtu_slave.h
@@ -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
diff --git a/Modbus/Src/modbus_rtu_slave.c b/Modbus/Src/modbus_rtu_slave.c
new file mode 100644
index 0000000..f9d380b
--- /dev/null
+++ b/Modbus/Src/modbus_rtu_slave.c
@@ -0,0 +1,666 @@
+#include "modbus_rtu_slave.h"
+
+#include
+
+/* 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;
+}