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; +}