diff --git a/Core/Src/main.c b/Core/Src/main.c index 9d2c44c..6765981 100644 --- a/Core/Src/main.c +++ b/Core/Src/main.c @@ -85,11 +85,7 @@ static void AppTaskStart(void *p_arg) * F4作为Modbus RTU从站,触摸屏作为主站。 * 初始化函数会立即启动USART1的DMA空闲接收。 */ - if (ModbusSlave_Init(&huart1, MODBUS_SLAVE_DEFAULT_ADDRESS) != HAL_OK) - { - Error_Handler(); - } - + ModbusSlaveInit(&huart1, MODBUS_SLAVE_DEFAULT_ADDRESS); while (1) { /* @@ -99,18 +95,18 @@ static void AppTaskStart(void *p_arg) * 4x地址2:收到的有效Modbus帧数; * 4x地址10:触摸屏读写测试值,由协议层保存。 */ - (void)ModbusSlave_SetHoldingRegister( + (void)ModbusSlaveSetHoldingRegister( HMI_REG_UPTIME_SECONDS, (uint16_t)(OSTimeGet() / OS_TICKS_PER_SEC)); - (void)ModbusSlave_SetHoldingRegister( + (void)ModbusSlaveSetHoldingRegister( HMI_REG_RX_FRAME_COUNT, - (uint16_t)g_modbus_slave_stats.valid_frame_count); + (uint16_t)ModbusSlaveStatistics.validFrameCount); /* * 协议解析和应答在任务上下文完成,中断只接收一帧并设置标志。 * 每1ms轮询一次,不在串口中断中执行耗时的CRC和寄存器操作。 */ - ModbusSlave_Poll(); + ModbusSlavePoll(); OSTimeDly(1U); } } @@ -335,7 +331,7 @@ void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) if (huart->Instance == USART1) { /* 应答发送完成后,协议模块重新启动DMA接收。 */ - ModbusSlave_OnTxComplete(huart); + ModbusSlaveOnTxComplete(huart); } } @@ -347,7 +343,7 @@ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) * DMA遇到串口空闲事件后进入这里。 * 仅复制帧并重启DMA,协议解析由AppTaskStart任务完成。 */ - ModbusSlave_OnRxEvent(huart, Size); + ModbusSlaveOnRxEvent(huart, Size); } } @@ -356,7 +352,7 @@ void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) if (huart->Instance == USART1) { /* 溢出、噪声、帧错误等异常后恢复USART1 DMA接收。 */ - ModbusSlave_OnUartError(huart); + ModbusSlaveOnUartError(huart); } } diff --git a/Document/0_Modbus任务要求.md b/Document/0_Modbus任务要求.md index f98b2f1..ddbd9d5 100644 --- a/Document/0_Modbus任务要求.md +++ b/Document/0_Modbus任务要求.md @@ -19,7 +19,7 @@ - 实现ModbusRTU主站(触摸屏) · 能够正常连接 -· 能够判断连接断开 + · 能够判断连接断开 · 能够按要求生成请求帧 · 能够正确解析应答帧,并做相应处理 · 能够进行超时处理 diff --git a/Document/c语言规范.pdf b/Document/c语言规范.pdf new file mode 100644 index 0000000..e688f19 Binary files /dev/null and b/Document/c语言规范.pdf differ diff --git a/Document/my_document/uCOS-II移植说明.md b/Document/my_document/uCOS-II移植说明.md new file mode 100644 index 0000000..c1e7eb5 --- /dev/null +++ b/Document/my_document/uCOS-II移植说明.md @@ -0,0 +1,935 @@ +# STM32F407 + IAR 工程 uC/OS-II 移植说明 + +## 1. 文档目的 + +本文记录本项目将 uC/OS-II 移植到 STM32F407IG + IAR 工程的完整过程,并结合当前已经能够正常调试、运行串口与 Modbus RTU 从站的实际代码进行说明。 + +本文不是通用示例,文中目录、文件名、任务优先级、时基和接口均以当前工程为准,主要用于: + +- 回顾本次 uC/OS-II 的移植步骤; +- 理解每个移植文件的作用; +- 在 CubeMX 重新生成代码后检查移植内容是否仍然完整; +- 后续增加任务、信号量、消息队列等功能; +- 排查任务不运行、延时不生效、无法切换任务等问题。 + +当前工程已经完成以下验证: + +- uC/OS-II 可以初始化并启动; +- 应用任务 `AppTaskStart` 可以运行; +- `OSTimeDly()` 和 `OSTimeGet()` 可以正常使用; +- HAL 和 uC/OS-II 共用 SysTick 时基; +- USART1 + DMA 可以正常通信; +- Modbus RTU 从站可以在任务中轮询处理请求。 + +--- + +## 2. 当前工程环境 + +| 项目 | 当前配置 | +|---|---| +| MCU | STM32F407IG | +| CPU 内核 | ARM Cortex-M4 | +| 工程生成工具 | STM32CubeMX | +| 编译器 | IAR Embedded Workbench for ARM | +| IAR 工程配置名 | `Modbus` | +| uC/OS-II 版本 | V2.92.11 | +| 操作系统节拍 | 1000 Hz,即 1 ms/节拍 | +| 串口 | USART1 | +| 串口参数 | 115200 bit/s、8 个有效数据位、偶校验、1 个停止位(8E1) | +| 串口接收 | DMA2 Stream2 Channel4 | +| 串口发送 | DMA2 Stream7 Channel4 | +| 应用任务 | `AppTaskStart` | +| 应用任务优先级 | 5 | +| 应用任务栈 | 256 个 `OS_STK` 单元 | + +IAR 命令行编译工具位于: + +```text +F:\IAR Systems\Embedded Workbench 8.3\common\bin +``` + +--- + +## 3. 移植工作的本质 + +将 uC/OS-II 移植到一个新的 MCU 工程,不只是把源码复制进来。完整移植至少包含以下五部分: + +1. **内核源码**:任务调度、延时、信号量、消息队列等通用功能; +2. **CPU 移植层**:任务栈初始化、临界区、寄存器保存和恢复; +3. **工程配置**:选择启用哪些内核功能、允许多少任务、系统节拍频率等; +4. **硬件连接**:将 SysTick 和 PendSV 正确接入操作系统; +5. **应用启动**:初始化内核、创建任务并启动调度器。 + +它们的关系如下: + +```text +应用任务 AppTaskStart + │ + │ 调用 OSTimeDly、OSTimeGet 等 API + ▼ +uC/OS-II 内核源码 + │ + │ 请求任务切换、初始化任务栈 + ▼ +Cortex-M4 + IAR 移植层 + │ + ├── SysTick:产生操作系统节拍 + └── PendSV:保存旧任务现场并恢复新任务现场 +``` + +--- + +## 4. 工程中的 uC/OS-II 目录 + +当前相关文件位于: + +```text +Middlewares/Third_Party/Micrium/ +├─ Config/ +│ ├─ app_cfg.h +│ ├─ cpu_cfg.h +│ └─ os_cfg.h +├─ uCOS-II/ +│ ├─ Source/ +│ │ ├─ ucos_ii.h +│ │ ├─ os_core.c +│ │ ├─ os_task.c +│ │ ├─ os_time.c +│ │ ├─ os_sem.c +│ │ ├─ os_mutex.c +│ │ ├─ os_q.c +│ │ ├─ os_mbox.c +│ │ ├─ os_flag.c +│ │ ├─ os_mem.c +│ │ └─ os_tmr.c +│ └─ Ports/ARM-Cortex-M4/IAR/ +│ ├─ os_cpu.h +│ ├─ os_cpu_a.asm +│ ├─ os_cpu_c.c +│ └─ os_dbg.c +├─ uC-CPU/ +└─ uC-LIB/ +``` + +### 4.1 各类文件的作用 + +| 文件或目录 | 作用 | +|---|---| +| `ucos_ii.h` | uC/OS-II 总头文件,应用程序通常只需包含它 | +| `os_core.c` | 内核初始化、调度和中断嵌套管理 | +| `os_task.c` | 任务创建、删除、挂起、恢复和栈检查 | +| `os_time.c` | 任务延时及系统节拍计数 | +| `os_sem.c` | 信号量 | +| `os_mutex.c` | 互斥信号量 | +| `os_q.c` | 消息队列 | +| `os_mbox.c` | 邮箱 | +| `os_flag.c` | 事件标志组 | +| `os_mem.c` | 固定大小内存分区 | +| `os_tmr.c` | 软件定时器 | +| `os_cpu.h` | Cortex-M4/IAR 的数据类型、临界区和任务切换宏 | +| `os_cpu_c.c` | 初始任务栈构造、CPU 钩子及 SysTick 接口 | +| `os_cpu_a.asm` | 上下文切换和寄存器保存/恢复的汇编实现 | +| `os_dbg.c` | 向调试器提供内核配置及数据尺寸信息 | +| `os_cfg.h` | 内核功能裁剪和资源数量配置 | +| `app_cfg.h` | 应用任务优先级和栈大小配置 | + +`uC-CPU` 和 `uC-LIB` 目录还提供 Micrium 的 CPU 抽象和通用库头文件。本工程已将相应目录加入头文件搜索路径。 + +--- + +## 5. IAR 工程配置 + +### 5.1 添加源码分组 + +在 IAR 工程中建立 `Micrium` 分组,并添加下列内容。 + +`Micrium Config`: + +```text +app_cfg.h +cpu_cfg.h +os_cfg.h +``` + +`uCOS-II Port`: + +```text +os_cpu_a.asm +os_cpu_c.c +os_dbg.c +``` + +`uCOS-II Source`: + +```text +os_core.c +os_flag.c +os_mbox.c +os_mem.c +os_mutex.c +os_q.c +os_sem.c +os_task.c +os_time.c +os_tmr.c +``` + +即使某项功能在 `os_cfg.h` 中被关闭,对应 `.c` 文件留在工程中通常也不会产生完整功能代码,因为源码内部会根据配置宏进行条件编译。 + +### 5.2 添加头文件搜索路径 + +在 IAR 中进入: + +```text +Project → Options → C/C++ Compiler → Preprocessor +``` + +将以下路径加入 `Additional include directories`: + +```text +$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 +$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uC-CPU +$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uC-CPU\ARM-Cortex-M4\IAR +$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uC-LIB +``` + +如果编译提示找不到 `ucos_ii.h`、`os_cpu.h`、`cpu.h` 或 `lib_def.h`,应首先检查这些搜索路径。 + +--- + +## 6. 内核和应用配置 + +### 6.1 `app_cfg.h` + +当前应用任务配置为: + +```c +#define APP_TASK_START_PRIO 5u +#define APP_TASK_TEST_PRIO 6u + +#define APP_TASK_START_STK_SIZE 256u +#define APP_TASK_TEST_STK_SIZE 256u +``` + +其中: + +- `APP_TASK_START_PRIO` 是启动任务优先级; +- `APP_TASK_TEST_PRIO` 是预留的测试任务优先级,当前还未创建该任务; +- `APP_TASK_START_STK_SIZE` 是启动任务栈的元素个数,不是字节数; +- Cortex-M4 移植中 `OS_STK` 为 32 位,因此 256 个栈单元约占 1024 字节 RAM。 + +### 6.2 uC/OS-II 的任务优先级规则 + +uC/OS-II V2 中: + +- 数值越小,任务优先级越高; +- 每个任务必须使用不同的优先级; +- 当前 `AppTaskStart` 使用优先级 5; +- 后续可让普通测试任务使用 6、7 等更低优先级; +- 不建议随意占用 0~4,可为以后对实时性要求更高的任务预留。 + +任务优先级 5 和 NVIC 中断优先级 5 是两个不同体系,不能直接比较大小。 + +### 6.3 `os_cfg.h` 的当前关键配置 + +| 配置项 | 当前值 | 含义 | +|---|---:|---| +| `OS_LOWEST_PRIO` | 63 | 应用可用的最低任务优先级范围 | +| `OS_MAX_TASKS` | 10 | 最大应用任务数量 | +| `OS_TICKS_PER_SEC` | 1000 | 每秒 1000 个系统节拍 | +| `OS_TASK_CREATE_EXT_EN` | 1 | 启用 `OSTaskCreateExt()` | +| `OS_TASK_STAT_EN` | 0 | 不启用统计任务 | +| `OS_MUTEX_EN` | 1 | 启用互斥信号量 | +| `OS_Q_EN` | 1 | 启用消息队列 | +| `OS_SEM_EN` | 1 | 启用信号量 | +| `OS_TIME_GET_SET_EN` | 1 | 允许读取和设置系统时间 | +| `OS_TIME_TICK_HOOK_EN` | 1 | 启用节拍钩子 | +| `OS_TMR_EN` | 0 | 不启用软件定时器 | +| `OS_MEM_EN` | 0 | 不启用固定内存分区 | +| `OS_CPU_HOOKS_EN` | 1 | 使用 CPU 移植层钩子 | +| `OS_DEBUG_EN` | 1 | 保留内核调试信息 | + +因为: + +```c +#define OS_TICKS_PER_SEC 1000u +``` + +所以: + +```c +OSTimeDly(1U); /* 延时约 1 ms */ +OSTimeDly(100U); /* 延时约 100 ms */ +OSTimeDly(1000U); /* 延时约 1 s */ +``` + +`OSTimeDly()` 的参数单位是“节拍”,只有在当前 1000 Hz 配置下才刚好等于毫秒。 + +--- + +## 7. SysTick 时基接入 + +### 7.1 为什么 HAL 和 uC/OS-II 都需要 SysTick + +STM32 HAL 使用 SysTick 维护自己的毫秒计数,`HAL_GetTick()`、HAL 超时判断等功能依赖 `HAL_IncTick()`。 + +uC/OS-II 也需要周期节拍,用于: + +- 更新任务延时; +- 唤醒延时到期的任务; +- 更新 `OSTimeGet()` 返回的系统节拍; +- 在需要时触发任务调度。 + +因此,本工程在同一个 `SysTick_Handler()` 中同时服务 HAL 和 uC/OS-II。 + +### 7.2 当前 SysTick 处理 + +`Core/Src/stm32f4xx_it.c` 中的核心代码为: + +```c +void SysTick_Handler(void) +{ + HAL_IncTick(); + + if (OSRunning == OS_TRUE) + { + OS_CPU_SysTickHandler(); + } +} +``` + +必须保留 `OSRunning` 判断。原因是 `HAL_Init()` 会先启动 SysTick,而此时 `OSInit()` 尚未执行。如果在内核未初始化时直接调用 uC/OS-II 的节拍处理,可能访问尚未准备好的内核状态。 + +`OS_CPU_SysTickHandler()` 内部的主要过程是: + +```text +进入 SysTick 中断 + ↓ +OSIntEnter() + ↓ +OSTimeTick():更新延时和系统节拍 + ↓ +OSIntExit():检查是否需要调度更高优先级任务 +``` + +不能只调用 `OS_CPU_SysTickHandler()` 而删除 `HAL_IncTick()`,否则 HAL 的毫秒时基和超时机制会停止。 + +--- + +## 8. PendSV 与任务上下文切换 + +### 8.1 PendSV 的作用 + +SysTick 负责告诉内核“时间过去了一个节拍”,PendSV 才真正执行任务上下文切换。 + +一次任务切换大致包括: + +1. 保存当前任务的 CPU 寄存器; +2. 将当前栈指针保存到任务控制块; +3. 选择当前最高优先级的就绪任务; +4. 取出新任务的栈指针; +5. 恢复新任务的 CPU 寄存器; +6. 从异常返回,新任务继续运行。 + +这些底层工作由 `os_cpu_a.asm` 中的 `OS_CPU_PendSVHandler` 完成。 + +### 8.2 当前工程的特殊接法 + +当前工程不是在 C 语言的空 `PendSV_Handler()` 中调用内核,而是直接修改了 IAR 启动文件: + +```text +EWARM/startup_stm32f407xx.s +``` + +启动文件中声明: + +```asm +EXTERN OS_CPU_PendSVHandler +``` + +并将向量表中的 PendSV 项直接设置为: + +```asm +DCD OS_CPU_PendSVHandler +``` + +所以 `Core/Src/stm32f4xx_it.c` 中虽然还有一个空的 `PendSV_Handler()`,实际 PendSV 向量并不指向它。 + +这是本工程移植中非常关键的一步。若 CubeMX 或其他工具重新生成、替换启动文件,必须确认 PendSV 向量仍然指向 `OS_CPU_PendSVHandler`。否则常见现象是: + +- 第一个任务似乎能够启动; +- 一旦调用 `OSTimeDly()` 或发生任务切换就卡住; +- 多任务无法正常调度。 + +不要同时让向量表指向两个 PendSV 实现,也不要在已有直接向量映射的情况下重复定义另一个同名强符号。 + +--- + +## 9. 在 `main.c` 中启动 uC/OS-II + +### 9.1 包含头文件 + +当前应用包含: + +```c +#include "ucos_ii.h" +#include "modbus_rtu_slave.h" +``` + +`ucos_ii.h` 内部会继续包含 `app_cfg.h`,因此 `main.c` 可以使用任务优先级和栈大小宏。 + +### 9.2 定义任务栈 + +```c +static OS_STK AppTaskStartStk[APP_TASK_START_STK_SIZE]; +``` + +任务栈不能定义成任务函数内的普通局部数组,否则函数退出或栈被覆盖后会破坏任务运行现场。这里使用 `static`,使内存在整个程序运行期间一直存在。 + +### 9.3 声明任务函数 + +```c +static void AppTaskStart(void *p_arg); +``` + +uC/OS-II 任务函数必须符合以下形式: + +```c +void TaskName(void *p_arg); +``` + +任务通常包含永久循环,不能执行完后直接返回。 + +### 9.4 正确的初始化顺序 + +本工程顺序为: + +```c +HAL_Init(); +SystemClock_Config(); + +MX_GPIO_Init(); +MX_DMA_Init(); +MX_USB_DEVICE_Init(); +MX_USART1_UART_Init(); + +OSInit(); + +/* 创建应用任务 */ +OSTaskCreateExt(...); + +OSStart(); +``` + +各步骤含义如下: + +1. `HAL_Init()`:初始化 HAL 和 SysTick; +2. `SystemClock_Config()`:配置芯片主频和总线时钟; +3. `MX_xxx_Init()`:初始化任务要使用的外设; +4. `OSInit()`:初始化 uC/OS-II 内核数据结构; +5. `OSTaskCreateExt()`:创建至少一个应用任务; +6. `OSStart()`:启动最高优先级就绪任务。 + +正常情况下 `OSStart()` 不会返回。CubeMX 在后面生成的 `while (1)` 仅作为程序结构保留,操作系统成功启动后不会再执行到那里。 + +### 9.5 当前任务创建代码 + +```c +os_err = OSTaskCreateExt(AppTaskStart, + 0, + &AppTaskStartStk[APP_TASK_START_STK_SIZE - 1u], + APP_TASK_START_PRIO, + APP_TASK_START_PRIO, + &AppTaskStartStk[0], + APP_TASK_START_STK_SIZE, + 0, + (OS_TASK_OPT_STK_CHK | + OS_TASK_OPT_STK_CLR)); + +if (os_err != OS_ERR_NONE) +{ + Error_Handler(); +} +``` + +参数说明: + +| 参数 | 当前传入值 | 作用 | +|---|---|---| +| 任务入口 | `AppTaskStart` | 任务开始执行的函数 | +| 任务参数 | `0` | 传给 `p_arg`,本任务不需要参数 | +| 初始栈顶 | 栈数组最后一个元素 | Cortex-M 栈向低地址增长 | +| 优先级 | `APP_TASK_START_PRIO` | 当前为 5 | +| 任务 ID | 同任务优先级 | 本工程用优先级作为 ID | +| 栈底 | 栈数组第一个元素 | 用于扩展创建和栈检查 | +| 栈大小 | `APP_TASK_START_STK_SIZE` | 256 个栈单元 | +| 扩展数据 | `0` | 当前不使用 | +| 任务选项 | `STK_CHK \| STK_CLR` | 启用栈检查并在创建时清零栈 | + +检查 `os_err` 非常重要。如果任务创建失败却继续调用 `OSStart()`,后续现象不容易定位。 + +--- + +## 10. 当前应用任务与 Modbus 的结合 + +当前启动任务主要代码如下: + +```c +static void AppTaskStart(void *p_arg) +{ + (void)p_arg; + + if (ModbusSlave_Init(&huart1, + MODBUS_SLAVE_DEFAULT_ADDRESS) != HAL_OK) + { + Error_Handler(); + } + + while (1) + { + (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); + + ModbusSlave_Poll(); + OSTimeDly(1U); + } +} +``` + +### 10.1 任务在做什么 + +1. 任务只在开始时调用一次 `ModbusSlave_Init()`; +2. 初始化 USART1 的 Modbus 从站和 DMA 接收; +3. 在永久循环中更新运行时间寄存器; +4. 更新有效接收帧计数寄存器; +5. 调用 `ModbusSlave_Poll()` 在任务上下文中解析已收到的帧; +6. 延时 1 个节拍,把 CPU 让给其他任务或空闲任务。 + +### 10.2 为什么要调用 `OSTimeDly(1U)` + +如果永久循环内没有延时、等待信号量或其他阻塞操作,优先级 5 的任务会一直占用 CPU,使更低优先级任务无法运行。 + +调用 `OSTimeDly(1U)` 后: + +- 当前任务进入延时状态; +- 调度器转而执行其他就绪任务; +- 1 ms 后 SysTick 更新延时计数; +- 当前任务重新进入就绪状态; +- 当它成为最高优先级就绪任务时继续运行。 + +--- + +## 11. 串口中断、DMA 与操作系统任务的关系 + +当前 Modbus 接收采用“中断/DMA 快速接收,任务中解析”的结构: + +```text +USART1 收到数据 + ↓ +DMA 自动搬运到接收缓冲区 + ↓ +串口空闲事件/接收事件中断 + ↓ +HAL_UARTEx_RxEventCallback() + ↓ +ModbusSlave_OnRxEvent() + │ + │ 仅记录数据、帧状态并尽快退出 + ▼ +AppTaskStart 周期执行 ModbusSlave_Poll() + ↓ +CRC 校验、功能码解析、寄存器读写、组织应答 + ↓ +USART1 DMA 发送应答 +``` + +当前回调接口包括: + +```c +HAL_UARTEx_RxEventCallback() + → ModbusSlave_OnRxEvent() + +HAL_UART_TxCpltCallback() + → ModbusSlave_OnTxComplete() + +HAL_UART_ErrorCallback() + → ModbusSlave_OnUartError() +``` + +这种结构的优点是: + +- 中断处理时间短; +- CRC 和协议解析不会长时间占用中断; +- 复杂业务逻辑运行在任务上下文,便于扩展; +- 串口接收与协议处理在执行过程上是异步的。 + +当前串口回调没有直接调用 uC/OS-II 的信号量、队列等服务,因此不需要额外在这些回调外层手工调用 `OSIntEnter()` 和 `OSIntExit()`。 + +如果后续在外设中断中直接调用 `OSSemPost()`、`OSQPost()` 等内核服务,则必须: + +- 确认该服务允许在中断中调用; +- 正确执行 uC/OS-II 的中断进入和退出处理; +- 检查 NVIC 中断优先级是否满足移植层要求; +- 不在中断中调用会阻塞的接口。 + +--- + +## 12. 常用 uC/OS-II 接口解释 + +### 12.1 `OSInit()` + +初始化内核全局变量、就绪表、任务链表,并建立空闲任务等内部对象。必须先调用它,再创建应用任务和启动调度器。 + +### 12.2 `OSTaskCreateExt()` + +创建一个扩展任务。与简化版创建函数相比,它可以传入栈底、栈大小和任务选项,因此可使用栈检查功能。 + +### 12.3 `OSStart()` + +启动调度器,并切换到当前最高优先级的就绪任务。成功后不会返回 `main()` 的普通执行流程。 + +### 12.4 `OSTimeDly()` + +使当前任务延时指定节拍。它是操作系统延时,不是原地空转: + +- 延时期间 CPU 可以执行其他任务; +- 不能在中断服务程序中调用; +- 参数为 0 时通常不会产生期望的延时。 + +### 12.5 `OSTimeGet()` + +读取内核从启动后累计的节拍数。当前节拍频率为 1000 Hz,因此: + +```c +seconds = OSTimeGet() / OS_TICKS_PER_SEC; +``` + +可得到近似运行秒数。计数最终会溢出并回绕,不能把它当作永久不溢出的实时时钟。 + +### 12.6 `OSTaskStkChk()` + +在任务使用了 `OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR` 时,可以检查任务栈使用情况,用于判断栈是否配置过大或过小。 + +--- + +## 13. 后续增加第二个任务 + +### 13.1 定义栈和任务函数 + +```c +static OS_STK AppTaskTestStk[APP_TASK_TEST_STK_SIZE]; + +static void AppTaskTest(void *p_arg) +{ + (void)p_arg; + + while (1) + { + /* 在这里编写周期性测试功能。 */ + + OSTimeDly(100U); + } +} +``` + +### 13.2 创建任务 + +在调用 `OSStart()` 之前增加: + +```c +os_err = OSTaskCreateExt(AppTaskTest, + 0, + &AppTaskTestStk[APP_TASK_TEST_STK_SIZE - 1u], + APP_TASK_TEST_PRIO, + APP_TASK_TEST_PRIO, + &AppTaskTestStk[0], + APP_TASK_TEST_STK_SIZE, + 0, + (OS_TASK_OPT_STK_CHK | + OS_TASK_OPT_STK_CLR)); + +if (os_err != OS_ERR_NONE) +{ + Error_Handler(); +} +``` + +也可以先只创建启动任务,再由启动任务创建其他任务。但无论采用哪种方式,都必须保证: + +- 每个任务有独立且长期有效的栈; +- 每个任务优先级唯一; +- 任务函数不能直接返回; +- 周期任务必须适当延时或等待事件; +- 总任务数不得超过 `OS_MAX_TASKS`。 + +--- + +## 14. CubeMX 重新生成代码时的注意事项 + +### 14.1 能够自动保留的内容 + +CubeMX 的 `Keep User Code when re-generating` 选项只能保留位于标准标记内的代码: + +```c +/* USER CODE BEGIN ... */ +/* USER CODE END ... */ +``` + +本工程下列自定义内容应放在对应的 USER CODE 区域内: + +- `main.c` 中的 uC/OS-II 和 Modbus 头文件; +- 任务栈、任务函数声明; +- `AppTaskStart()`; +- `OSInit()`、任务创建和 `OSStart()`; +- `stm32f4xx_it.c` 中的 `OS_CPU_SysTickHandler()` 调用; +- HAL 串口回调中的 Modbus 接口调用。 + +写在 USER CODE 标记之外的修改可能被覆盖。 + +### 14.2 需要手工重点检查的内容 + +每次 CubeMX 重新生成后,至少检查: + +- `EWARM/startup_stm32f407xx.s` 的 PendSV 向量是否仍指向 `OS_CPU_PendSVHandler`; +- `stm32f4xx_it.c` 是否仍调用 `HAL_IncTick()`; +- `stm32f4xx_it.c` 是否在 `OSRunning` 为真时调用 `OS_CPU_SysTickHandler()`; +- `main.c` 是否仍然调用 `OSInit()`、创建任务并调用 `OSStart()`; +- IAR 工程中的 Micrium 源文件分组是否仍存在; +- IAR 的 Micrium 头文件搜索路径是否仍存在; +- 串口 DMA、NVIC 和回调代码是否仍完整。 + +CubeMX 通常不会主动删除独立放置的 `Middlewares/Third_Party/Micrium` 源文件,但重新生成 IAR 工程文件时,有可能改变 `.ewp` 中手工添加的分组或路径,因此重新生成前应提交 Git 或备份工程。 + +--- + +## 15. 编译和运行验证 + +### 15.1 IAR 图形界面 + +在 IAR 中执行: + +```text +Project → Rebuild All +``` + +确认: + +- 0 errors; +- 0 warnings; +- `os_cpu_a.asm`、`os_cpu_c.c` 和内核源码均参与编译; +- 链接阶段没有重复的中断处理函数。 + +### 15.2 命令行完整编译 + +在工程根目录执行: + +```powershell +& 'F:\IAR Systems\Embedded Workbench 8.3\common\bin\IarBuild.exe' ` + 'EWARM\Modbus.ewp' ` + -build Modbus ` + -log all +``` + +链接映射文件位于: + +```text +EWARM\Modbus\List\Modbus.map +``` + +该文件可以查看代码、只读数据和读写数据的详细占用。 + +### 15.3 调试时可观察的内容 + +建议设置或观察: + +- 在 `AppTaskStart()` 入口设置断点,确认任务已进入; +- `OSRunning` 应在内核启动后变为真; +- `OSTimeGet()` 应持续增加; +- 执行 `OSTimeDly(1U)` 后任务应能再次被唤醒; +- Modbus 运行时间寄存器应每秒变化; +- 串口请求应能收到正常应答。 + +--- + +## 16. 常见故障排查 + +### 16.1 提示找不到 `ucos_ii.h` + +原因通常是 IAR 头文件搜索路径未添加或路径层级错误。检查: + +```text +Middlewares\Third_Party\Micrium\uCOS-II\Source +``` + +### 16.2 `OSStart()` 后卡死或任务不运行 + +依次检查: + +1. 是否先调用了 `OSInit()`; +2. `OSTaskCreateExt()` 返回值是否为 `OS_ERR_NONE`; +3. 任务栈顶是否传入数组最后一个元素; +4. `os_cpu_a.asm` 是否参与编译和链接; +5. 启动文件 PendSV 向量是否指向 `OS_CPU_PendSVHandler`; +6. 任务函数是否包含永久循环。 + +### 16.3 `OSTimeDly()` 后任务再也不运行 + +重点检查: + +- SysTick 中断是否正常进入; +- 是否保留 `OS_CPU_SysTickHandler()`; +- `OSRunning` 是否已经变为真; +- `OS_TICKS_PER_SEC` 是否与实际 SysTick 频率匹配; +- PendSV 向量是否被 CubeMX 恢复成空的 `PendSV_Handler()`。 + +### 16.4 HAL 延时或超时异常 + +检查 `SysTick_Handler()` 中是否仍调用: + +```c +HAL_IncTick(); +``` + +uC/OS-II 和 HAL 共用 SysTick 时,两边的节拍处理都要保留。 + +### 16.5 链接时出现重复的 PendSV 或 SysTick 符号 + +说明同一异常被多个强符号实现。应明确采用一种连接方式。 + +当前工程采用: + +- SysTick 向量指向 `SysTick_Handler()`,其中同时服务 HAL 和 uC/OS-II; +- PendSV 向量直接指向汇编函数 `OS_CPU_PendSVHandler`。 + +### 16.6 任务运行一段时间后进入 HardFault + +常见原因包括: + +- 任务栈过小; +- 使用了错误的 CPU/编译器移植层; +- 栈顶和栈底参数传反; +- 局部数组过大; +- 中断或任务越界写内存; +- 浮点上下文或编译选项与移植层不匹配。 + +应先增加任务栈并使用栈检查,再检查 HardFault 时的栈和寄存器。 + +### 16.7 高优先级任务导致其他任务不运行 + +uC/OS-II 是基于优先级的抢占式内核。若高优先级任务永久循环且从不延时、不挂起、也不等待事件,低优先级任务不会得到运行机会。 + +周期任务中应使用: + +- `OSTimeDly()`; +- 等待信号量; +- 等待消息队列; +- 等待事件标志; +- 或其他会让任务进入非就绪状态的接口。 + +### 16.8 修改了配置宏后出现未定义符号 + +例如打开信号量、队列、统计任务或软件定时器后,应同时确认: + +- 对应源码已加入工程; +- 相关最大数量配置不为 0; +- 所需钩子函数已经实现; +- RAM 和任务数量足够。 + +--- + +## 17. 当前移植的关键文件清单 + +| 文件 | 当前项目中的关键内容 | +|---|---| +| `Core/Src/main.c` | 内核初始化、任务创建、启动调度器、Modbus 应用任务 | +| `Core/Src/stm32f4xx_it.c` | HAL 与 uC/OS-II 共用的 SysTick 处理 | +| `EWARM/startup_stm32f407xx.s` | PendSV 向量直接指向 `OS_CPU_PendSVHandler` | +| `EWARM/Modbus.ewp` | Micrium 源文件分组及头文件搜索路径 | +| `Middlewares/Third_Party/Micrium/Config/app_cfg.h` | 应用任务优先级和栈大小 | +| `Middlewares/Third_Party/Micrium/Config/os_cfg.h` | 内核裁剪、节拍频率和资源数量 | +| `Middlewares/Third_Party/Micrium/uCOS-II/Ports/ARM-Cortex-M4/IAR/os_cpu_a.asm` | 上下文切换 | +| `Middlewares/Third_Party/Micrium/uCOS-II/Ports/ARM-Cortex-M4/IAR/os_cpu_c.c` | 任务栈初始化及 SysTick 接口 | +| `Modbus/Src/modbus_rtu_slave.c` | Modbus RTU 从站协议处理 | + +--- + +## 18. 移植完成检查表 + +### 源码与工程 + +- [ ] uC/OS-II 内核源码已加入 IAR 工程; +- [ ] Cortex-M4/IAR 移植层已加入工程; +- [ ] Micrium 配置文件已加入工程; +- [ ] 所有头文件搜索路径正确; +- [ ] `os_cpu_a.asm` 确实参与编译。 + +### 内核配置 + +- [ ] `OS_TICKS_PER_SEC` 与 SysTick 实际频率一致; +- [ ] `OS_MAX_TASKS` 足够; +- [ ] 每个任务优先级唯一; +- [ ] 所需信号量、队列、互斥量功能已启用; +- [ ] 每个任务栈大小合理。 + +### 中断连接 + +- [ ] SysTick 同时调用 `HAL_IncTick()` 和 `OS_CPU_SysTickHandler()`; +- [ ] 内核未运行时不会调用 OS 节拍处理; +- [ ] PendSV 向量指向 `OS_CPU_PendSVHandler`; +- [ ] 没有重复的 PendSV 强符号。 + +### 应用启动 + +- [ ] 外设初始化先于使用外设的任务; +- [ ] 已调用 `OSInit()`; +- [ ] 至少成功创建一个任务; +- [ ] 已检查任务创建返回值; +- [ ] 已调用 `OSStart()`; +- [ ] 所有任务均包含循环并能主动让出 CPU。 + +### 运行验证 + +- [ ] 工程编译为 0 errors、0 warnings; +- [ ] `AppTaskStart()` 可以进入; +- [ ] `OSRunning` 为真; +- [ ] 系统节拍持续增加; +- [ ] `OSTimeDly()` 后任务可以恢复; +- [ ] USART1 DMA 通信正常; +- [ ] Modbus 请求和应答正常。 + +--- + +## 19. 关于 uC/OS-II 授权 + +当前源码头部的授权说明允许评估、教学或研究等用途。若代码用于正式商业产品,应根据所使用版本核实 Micrium/版权方的商业授权要求,不能仅因为源码可以编译就默认具备商业发布许可。 + +--- + +## 20. 总结 + +本工程的 uC/OS-II 移植可以概括为: + +```text +复制内核和 Cortex-M4/IAR 移植层 + ↓ +加入 IAR 源文件与头文件路径 + ↓ +配置 os_cfg.h 和 app_cfg.h + ↓ +SysTick 同时服务 HAL 与 uC/OS-II + ↓ +启动文件 PendSV 向量接入 OS_CPU_PendSVHandler + ↓ +OSInit → 创建任务 → OSStart + ↓ +在任务中运行 ModbusSlave_Poll 和应用逻辑 +``` + +其中最容易在 CubeMX 重新生成后遗漏的是 **IAR 启动文件中的 PendSV 向量修改**;最容易在增加任务后遇到的问题是 **任务优先级重复、任务栈不足,以及高优先级任务没有延时或等待事件**。 diff --git a/Document/my_document/~$需求规格书.docx b/Document/my_document/~$需求规格书.docx new file mode 100644 index 0000000..d5cee5a Binary files /dev/null and b/Document/my_document/~$需求规格书.docx differ diff --git a/Document/my_document/框图/协议解析流程图.vsdx b/Document/my_document/框图/协议解析流程图.vsdx new file mode 100644 index 0000000..d833aae Binary files /dev/null and b/Document/my_document/框图/协议解析流程图.vsdx differ diff --git a/Document/my_document/框图/应用模块流程图.vsdx b/Document/my_document/框图/应用模块流程图.vsdx new file mode 100644 index 0000000..c1b2ee0 Binary files /dev/null and b/Document/my_document/框图/应用模块流程图.vsdx differ diff --git a/Document/my_document/框图/系统框图.vsdx b/Document/my_document/框图/系统框图.vsdx new file mode 100644 index 0000000..cf99d71 Binary files /dev/null and b/Document/my_document/框图/系统框图.vsdx differ diff --git a/Document/my_document/测试大纲.docx b/Document/my_document/测试大纲.docx new file mode 100644 index 0000000..514b39a Binary files /dev/null and b/Document/my_document/测试大纲.docx differ diff --git a/Document/my_document/设计方案书.docx b/Document/my_document/设计方案书.docx new file mode 100644 index 0000000..cfd0abb Binary files /dev/null and b/Document/my_document/设计方案书.docx differ diff --git a/Document/my_document/需求规格书.docx b/Document/my_document/需求规格书.docx new file mode 100644 index 0000000..c350e9b Binary files /dev/null and b/Document/my_document/需求规格书.docx differ diff --git a/Modbus/Inc/modbus_rtu_slave.h b/Modbus/Inc/modbus_rtu_slave.h index e76ba54..570c590 100644 --- a/Modbus/Inc/modbus_rtu_slave.h +++ b/Modbus/Inc/modbus_rtu_slave.h @@ -1,80 +1,137 @@ +/** + * @file modbus_rtu_slave.h + * @brief Modbus RTU 从站模块公开接口。 + */ + #ifndef MODBUS_RTU_SLAVE_H #define MODBUS_RTU_SLAVE_H #include "stm32f4xx_hal.h" #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif -/* - * 任务文档规定线圈和保持寄存器地址范围均为0~0x270F。 - * 因此地址总数是0x2710,即10000个。 +/** + * @brief Modbus 数据模型的最大地址。 + * + * 任务要求规定线圈和保持寄存器地址范围均为 0~0x270F, + * 因此每种数据共有 10000 项。 */ -#define MODBUS_MAP_LAST_ADDRESS 0x270FU -#define MODBUS_MAP_ITEM_COUNT 0x2710U +#define MODBUS_MAP_LAST_ADDRESS (0x270FU) +#define MODBUS_MAP_ITEM_COUNT (0x2710U) -#define MODBUS_SLAVE_DEFAULT_ADDRESS 1U -#define MODBUS_CONNECTION_TIMEOUT_MS 1000U +#define MODBUS_SLAVE_DEFAULT_ADDRESS (1U) +#define MODBUS_CONNECTION_TIMEOUT_MS (1000U) -/* - * 供第一次与触摸屏联调使用的演示地址。 - * TouchWin选择“起始地址为0”的Modbus RTU主站协议后,可直接访问这些地址。 +/** + * @brief 与 TouchWin 触摸屏联调使用的演示地址。 + * + * 可以直接访问以下地址。 */ -#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 +#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) +/** + * @brief Modbus 从站通信统计信息。 + */ 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初始化完成后调用。 + uint32_t rxEventCount; ///< 串口接收事件总数。 + uint32_t validFrameCount; ///< CRC 和从站地址均有效的帧数。 + uint32_t txFrameCount; ///< 成功启动 DMA 发送的帧数。 + uint32_t crcErrorCount; ///< CRC 校验错误帧数。 + uint32_t ignoredAddressCount; ///< 因从站地址不匹配而忽略的帧数。 + uint32_t illegalFunctionCount; ///< 非法功能码计数。 + uint32_t illegalAddressCount; ///< 非法数据地址计数。 + uint32_t illegalValueCount; ///< 非法数据值计数。 + uint32_t droppedFrameCount; ///< 接收槽占用或长度错误导致的丢帧数。 + uint32_t uartErrorCount; ///< HAL 串口错误计数。 +} MODBUS_SLAVE_STATS; + +/** @brief Modbus 从站通信统计数据。 */ +extern volatile MODBUS_SLAVE_STATS ModbusSlaveStatistics; + +/** + * @brief 初始化 Modbus 从站并启动 USART DMA 空闲接收。 + * @param[in] huart 已由 HAL 初始化完成的串口句柄。 + * @param[in] slaveAddress 从站地址,有效范围为 1~247。 + * @retval HAL_OK 初始化成功且 DMA 接收已启动。 + * @retval HAL_ERROR 输入参数无效。 + * @retval HAL_BUSY 串口或 DMA 当前忙。 + */ +HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart, + uint8_t slaveAddress); + +/** + * @brief 校验并处理一帧待处理请求。 + * + * 本函数应在 uC/OS-II 任务中周期调用。中断只负责接收数据, + * CRC 校验、协议解析和响应发送均在任务上下文中执行。 + */ +void ModbusSlavePoll(void); + +/** + * @brief 处理 HAL 串口 DMA 接收事件。 + * @param[in] huart 产生接收事件的串口句柄。 + * @param[in] size 本次接收到的字节数。 + */ +void ModbusSlaveOnRxEvent(UART_HandleTypeDef *huart, uint16_t size); + +/** + * @brief 处理 HAL 串口 DMA 发送完成事件。 + * @param[in] huart 完成发送的串口句柄。 + */ +void ModbusSlaveOnTxComplete(UART_HandleTypeDef *huart); + +/** + * @brief 处理 HAL 串口错误事件并恢复接收。 + * @param[in] huart 发生错误的串口句柄。 */ -HAL_StatusTypeDef ModbusSlave_Init(UART_HandleTypeDef *huart, - uint8_t slave_address); +void ModbusSlaveOnUartError(UART_HandleTypeDef *huart); -/* - * 在uC/OS-II任务中周期调用,用于校验并处理一帧请求。 - * 中断只负责搬运数据,协议解析留在任务上下文中执行。 +/** + * @brief 写入一个保持寄存器。 + * @param[in] address 保持寄存器地址。 + * @param[in] value 待写入的 16 位数据。 */ -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 ModbusSlaveSetHoldingRegister(uint16_t address, uint16_t value); + +/** + * @brief 读取一个保持寄存器。 + * @param[in] address 保持寄存器地址。 + * @param[out] value 用于保存读取结果的指针。 + */ +uint8_t ModbusSlaveGetHoldingRegister(uint16_t address, uint16_t *value); + +/** + * @brief 设置一个线圈的状态。 + * @param[in] address 线圈地址。 + * @param[in] state 0 表示复位,非 0 表示置位。 */ -uint8_t ModbusSlave_IsConnected(uint32_t timeout_ms); +uint8_t ModbusSlaveSetCoil(uint16_t address, uint8_t state); + +/** + * @brief 读取一个线圈的状态。 + * @param[in] address 线圈地址。 + * @param[out] state 用于保存线圈状态的指针,结果为 0 或 1。 + */ +uint8_t ModbusSlaveGetCoil(uint16_t address, uint8_t *state); + +/** + * @brief 判断指定时间内是否收到过有效请求。 + * @param[in] timeoutMs 连接超时时间,单位为毫秒。 + * @retval 1 从站在指定时间内收到过有效请求。 + * @retval 0 尚未收到有效请求或连接已经超时。 + */ +uint8_t ModbusSlaveIsConnected(uint32_t timeoutMs); #ifdef __cplusplus } #endif -#endif +#endif /* MODBUS_RTU_SLAVE_H */ diff --git a/Modbus/Src/modbus_rtu_slave.c b/Modbus/Src/modbus_rtu_slave.c index f9d380b..1f166b3 100644 --- a/Modbus/Src/modbus_rtu_slave.c +++ b/Modbus/Src/modbus_rtu_slave.c @@ -1,666 +1,824 @@ +/** + * @file modbus_rtu_slave.c + * @brief Modbus RTU 从站协议处理模块。 + */ + #include "modbus_rtu_slave.h" #include -/* Modbus RTU最大ADU为256字节。 */ -#define MODBUS_RTU_ADU_SIZE_MAX 256U +/** @brief Modbus RTU 最大 ADU 长度,单位为字节。 */ +#define MODBUS_RTU_ADU_SIZE_MAX (256U) -/* - * 当前串口为115200 bit/s。Modbus规范建议波特率高于19200时, - * 帧间隔固定使用1.75ms,这里向上取整为2ms。 +/** + * @brief 当前通信参数下使用的 RTU 帧间隔。 + * + * 当前波特率为 115200 bit/s。Modbus 规范建议波特率高于 + * 19200 bit/s 时采用固定 1.75 ms 帧间隔,此处向上取整为 2 ms。 */ -#define MODBUS_RTU_FRAME_GAP_MS 2U +#define MODBUS_RTU_FRAME_GAP_MS (2U) -#define MODBUS_BROADCAST_ADDRESS 0U +#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 +#define MODBUS_EX_ILLEGAL_FUNCTION (0x01U) +#define MODBUS_EX_ILLEGAL_ADDRESS (0x02U) +#define MODBUS_EX_ILLEGAL_VALUE (0x03U) -static UART_HandleTypeDef *s_uart; -static uint8_t s_slave_address; +#define MODBUS_READ_COILS_MAX (2000U) +#define MODBUS_READ_REGS_MAX (125U) +#define MODBUS_WRITE_COILS_MAX (1968U) +#define MODBUS_WRITE_REGS_MAX (123U) -/* - * 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]; +#define MODBUS_COIL_VALUE_ON (0xFF00U) +#define MODBUS_COIL_VALUE_OFF (0x0000U) -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; +static UART_HandleTypeDef *ModbusUart; +static uint8_t ModbusSlaveAddress; -/* 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]; +/** + * DMA 缓冲区只由 DMA 写入;帧缓冲区在接收回调中完成一次快照, + * 随后由任务解析,避免 DMA 重启后覆盖尚未处理的数据。 + */ +static uint8_t ModbusRxDmaBuffer[MODBUS_RTU_ADU_SIZE_MAX]; +static uint8_t ModbusRxFrame[MODBUS_RTU_ADU_SIZE_MAX]; +static uint8_t ModbusTxFrame[MODBUS_RTU_ADU_SIZE_MAX]; + +static volatile uint16_t ModbusRxFrameLength; +static volatile uint8_t ModbusRxFrameReady; +static volatile uint8_t ModbusTxBusy; +static volatile uint32_t ModbusRxEventTick; +static volatile uint32_t ModbusLastValidFrameTick; +static volatile uint8_t ModbusHasReceivedValidFrame; + +/** + * 10000 个保持寄存器占用 20000 字节;10000 个线圈按位存储, + * 占用 1250 字节。 + */ +static uint16_t ModbusHoldingRegisters[MODBUS_MAP_ITEM_COUNT]; +static uint8_t ModbusCoils[(MODBUS_MAP_ITEM_COUNT + 7U) / 8U]; -volatile ModbusSlaveStats g_modbus_slave_stats; +volatile MODBUS_SLAVE_STATS ModbusSlaveStatistics; -static uint16_t Modbus_Crc16(const uint8_t *data, uint16_t length) +/** + * @brief 计算 Modbus RTU CRC16 校验值。 + * @param[in] data 待校验数据。 + * @param[in] length 待校验数据长度。 + * @return CRC16 校验值。 + */ +static uint16_t ModbusCrc16(const uint8_t *data, uint16_t length) { - uint16_t crc = 0xFFFFU; - uint16_t index; - uint8_t bit; + 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++) + for (index = 0U; index < length; index++) { - if ((crc & 0x0001U) != 0U) - { - crc = (uint16_t)((crc >> 1U) ^ 0xA001U); - } - else - { - crc >>= 1U; - } + crc ^= data[index]; + + for (bit = 0U; bit < 8U; bit++) + { + if ((crc & 0x0001U) != 0U) + { + crc = (uint16_t)((crc >> 1U) ^ 0xA001U); + } + else + { + crc >>= 1U; + } + } } - } - return crc; + return crc; } -static uint16_t Modbus_GetU16Be(const uint8_t *data) +/** + * @brief 从网络字节序数据中读取一个 16 位无符号整数。 + * @param[in] data 至少包含两个字节的数据地址。 + * @return 转换后的 16 位无符号整数。 + */ +static uint16_t ModbusGetU16Be(const uint8_t *data) { - return (uint16_t)(((uint16_t)data[0] << 8U) | data[1]); + return (uint16_t)(((uint16_t)data[0] << 8U) | data[1]); } -static uint8_t Modbus_AddressRangeIsValid(uint16_t start, uint16_t quantity) +/** + * @brief 检查连续数据地址范围是否合法。 + * @param[in] start 起始地址。 + * @param[in] quantity 数据项数量。 + * @retval 1 地址范围合法。 + * @retval 0 地址范围非法。 + */ +static uint8_t ModbusAddressRangeIsValid(uint16_t start, uint16_t quantity) { - if ((quantity == 0U) || (start >= MODBUS_MAP_ITEM_COUNT)) - { - return 0U; - } + if ((quantity == 0U) || (start >= MODBUS_MAP_ITEM_COUNT)) + { + return 0U; + } - /* 先做减法再比较,避免start + quantity发生16位溢出。 */ - return (quantity <= (MODBUS_MAP_ITEM_COUNT - start)) ? 1U : 0U; + /* 先做减法再比较,避免 start + quantity 发生 16 位溢出。 */ + return (quantity <= (MODBUS_MAP_ITEM_COUNT - start)) ? 1U : 0U; } -static uint8_t Modbus_CoilGetUnchecked(uint16_t address) +/** + * @brief 读取已经确认地址合法的线圈。 + * @param[in] address 线圈地址。 + * @return 线圈状态,取值为 0 或 1。 + */ +static uint8_t ModbusCoilGetUnchecked(uint16_t address) { - uint8_t mask = (uint8_t)(1U << (address & 0x0007U)); + uint8_t mask = (uint8_t)(1U << (address & 0x0007U)); - return ((s_coils[address >> 3U] & mask) != 0U) ? 1U : 0U; + return ((ModbusCoils[address >> 3U] & mask) != 0U) ? 1U : 0U; } -static void Modbus_CoilSetUnchecked(uint16_t address, uint8_t state) +/** + * @brief 设置已经确认地址合法的线圈。 + * @param[in] address 线圈地址。 + * @param[in] state 0 表示复位,非 0 表示置位。 + */ +static void ModbusCoilSetUnchecked(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); - } + uint8_t mask = (uint8_t)(1U << (address & 0x0007U)); + + if (state != 0U) + { + ModbusCoils[address >> 3U] |= mask; + } + else + { + ModbusCoils[address >> 3U] &= (uint8_t)(~mask); + } } -static HAL_StatusTypeDef Modbus_StartReceive(void) +/** + * @brief 启动 USART DMA 空闲接收。 + * @retval HAL_OK DMA 接收启动成功。 + * @retval HAL_BUSY 串口未配置或发送尚未结束。 + * @return 其他 HAL 状态表示 DMA 接收启动失败。 + */ +static HAL_StatusTypeDef ModbusStartReceive(void) { - HAL_StatusTypeDef status; + HAL_StatusTypeDef status; - if ((s_uart == NULL) || (s_tx_busy != 0U)) - { - return HAL_BUSY; - } + if (ModbusTxBusy != 0U) + { + return HAL_BUSY; + } - status = HAL_UARTEx_ReceiveToIdle_DMA(s_uart, - s_rx_dma_buffer, - sizeof(s_rx_dma_buffer)); + status = HAL_UARTEx_ReceiveToIdle_DMA(ModbusUart, + ModbusRxDmaBuffer, + sizeof(ModbusRxDmaBuffer)); - if ((status == HAL_OK) && (s_uart->hdmarx != NULL)) - { - /* - * 普通Modbus帧只应在IDLE或缓冲区满时交给应用。 - * 关闭DMA半传输中断,避免长帧在一半位置被误认为完整帧。 - */ - __HAL_DMA_DISABLE_IT(s_uart->hdmarx, DMA_IT_HT); - } + if ((status == HAL_OK) && (ModbusUart->hdmarx != NULL)) + { + /* + * 普通 Modbus 帧只应在 IDLE 或缓冲区满时交给应用。 + * 关闭 DMA 半传输中断,避免长帧在一半位置被误认为完整帧。 + */ + __HAL_DMA_DISABLE_IT(ModbusUart->hdmarx, DMA_IT_HT); + } - return status; + return status; } -static void Modbus_StopReceive(void) +/** + * @brief 停止当前 USART DMA 接收。 + */ +static void ModbusStopReceive(void) { - if (s_uart != NULL) - { - (void)HAL_UART_AbortReceive(s_uart); - } + if (ModbusUart != NULL) + { + (void)HAL_UART_AbortReceive(ModbusUart); + } } -static void Modbus_AppendCrc(uint8_t *frame, uint16_t payload_length) +/** + * @brief 在 RTU 帧末尾追加 CRC 低字节和高字节。 + * @param[in,out] frame 待追加 CRC 的帧缓冲区。 + * @param[in] payloadLength 不包含 CRC 的有效载荷长度。 + */ +static void ModbusAppendCrc(uint8_t *frame, uint16_t payloadLength) { - uint16_t crc = Modbus_Crc16(frame, payload_length); + uint16_t crc = ModbusCrc16(frame, payloadLength); - /* Modbus RTU在线路上传输CRC低字节在前、高字节在后。 */ - frame[payload_length] = (uint8_t)(crc & 0x00FFU); - frame[payload_length + 1U] = (uint8_t)(crc >> 8U); + /* Modbus RTU 在线路上传输 CRC 低字节在前、高字节在后。 */ + frame[payloadLength] = (uint8_t)(crc & 0x00FFU); + frame[payloadLength + 1U] = (uint8_t)(crc >> 8U); } -static uint16_t Modbus_BuildException(uint8_t function, uint8_t exception) +/** + * @brief 构造 Modbus 异常响应。 + * @param[in] function 请求功能码。 + * @param[in] exception 异常码。 + * @return 异常响应 ADU 长度。 + */ +static uint16_t ModbusBuildException(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); + ModbusTxFrame[0] = ModbusSlaveAddress; + ModbusTxFrame[1] = (uint8_t)(function | 0x80U); + ModbusTxFrame[2] = exception; + ModbusAppendCrc(ModbusTxFrame, 3U); - return 5U; + return 5U; } -static uint16_t Modbus_ProcessReadCoils(const uint8_t *request, - uint16_t request_length) +/** + * @brief 处理读线圈功能码 0x01。 + * @param[in] request RTU 请求帧。 + * @param[in] requestLength 请求帧长度。 + * @return 待发送响应长度,异常请求返回异常响应长度。 + */ +static uint16_t ModbusProcessReadCoils(const uint8_t *request, + uint16_t requestLength) { - 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); + uint16_t start; + uint16_t quantity; + uint16_t index; + uint8_t byteCount; + + if (requestLength != 8U) + { + ModbusSlaveStatistics.illegalValueCount++; + return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + start = ModbusGetU16Be(&request[2]); + quantity = ModbusGetU16Be(&request[4]); + + if ((quantity == 0U) || (quantity > MODBUS_READ_COILS_MAX)) + { + ModbusSlaveStatistics.illegalValueCount++; + return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + if (ModbusAddressRangeIsValid(start, quantity) == 0U) + { + ModbusSlaveStatistics.illegalAddressCount++; + return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); + } + + byteCount = (uint8_t)((quantity + 7U) / 8U); + ModbusTxFrame[0] = ModbusSlaveAddress; + ModbusTxFrame[1] = 0X01; + ModbusTxFrame[2] = byteCount; + (void)memset(&ModbusTxFrame[3], 0, byteCount); + + /* Modbus 规定第一个线圈放在响应数据首字节的最低位。 */ + for (index = 0U; index < quantity; index++) + { + if (ModbusCoilGetUnchecked((uint16_t)(start + index)) != 0U) + { + ModbusTxFrame[3U + (index >> 3U)] |= + (uint8_t)(1U << (index & 0x0007U)); + } + } + + ModbusAppendCrc(ModbusTxFrame, (uint16_t)(3U + byteCount)); + return (uint16_t)(5U + byteCount); } -static uint16_t Modbus_ProcessReadHolding(const uint8_t *request, - uint16_t request_length) +/** + * @brief 处理读保持寄存器功能码 0x03。 + * @param[in] request RTU 请求帧。 + * @param[in] requestLength 请求帧长度。 + * @return 待发送响应长度,异常请求返回异常响应长度。 + */ +static uint16_t ModbusProcessReadHolding(const uint8_t *request, + uint16_t requestLength) { - 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); + uint16_t start; + uint16_t quantity; + uint16_t index; + uint16_t value; + + if (requestLength != 8U) + { + ModbusSlaveStatistics.illegalValueCount++; + return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + start = ModbusGetU16Be(&request[2]); + quantity = ModbusGetU16Be(&request[4]); + + if ((quantity == 0U) || (quantity > MODBUS_READ_REGS_MAX)) + { + ModbusSlaveStatistics.illegalValueCount++; + return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + if (ModbusAddressRangeIsValid(start, quantity) == 0U) + { + ModbusSlaveStatistics.illegalAddressCount++; + return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); + } + + ModbusTxFrame[0] = ModbusSlaveAddress; + ModbusTxFrame[1] = 0X03; + ModbusTxFrame[2] = (uint8_t)(quantity * 2U); + + for (index = 0U; index < quantity; index++) + { + value = ModbusHoldingRegisters[start + index]; + ModbusTxFrame[3U + index * 2U] = (uint8_t)(value >> 8U); + ModbusTxFrame[4U + index * 2U] = (uint8_t)(value & 0x00FFU); + } + + ModbusAppendCrc(ModbusTxFrame, (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) +/** + * @brief 处理写单个保持寄存器功能码 0x06。 + * @param[in] request RTU 请求帧。 + * @param[in] requestLength 请求帧长度。 + * @param[in] isBroadcast 非 0 表示当前请求为广播。 + * @return 单播响应长度;广播或无法响应时返回 0。 + */ +static uint16_t ModbusProcessWriteSingleRegister( + const uint8_t *request, + uint16_t requestLength, + uint8_t isBroadcast) { - 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; + uint16_t address; + uint16_t value; + + if (requestLength != 8U) + { + ModbusSlaveStatistics.illegalValueCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + address = ModbusGetU16Be(&request[2]); + value = ModbusGetU16Be(&request[4]); + + if (address >= MODBUS_MAP_ITEM_COUNT) + { + ModbusSlaveStatistics.illegalAddressCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); + } + + ModbusHoldingRegisters[address] = value; + + if (isBroadcast != 0U) + { + return 0U; + } + + /* 0x06 的正常响应必须原样回显请求的前 6 个字节。 */ + (void)memcpy(ModbusTxFrame, request, 6U); + ModbusAppendCrc(ModbusTxFrame, 6U); + return 8U; } -static uint16_t Modbus_ProcessWriteMultipleCoils(const uint8_t *request, - uint16_t request_length, - uint8_t is_broadcast) +/** + * @brief 处理写单个线圈功能码 0x05。 + * @param[in] request RTU 请求帧。 + * @param[in] requestLength 请求帧长度。 + * @param[in] isBroadcast 非 0 表示当前请求为广播。 + * @return 单播响应长度;广播或无法响应时返回 0。 + */ +static uint16_t ModbusProcessWriteSingleCoil( + const uint8_t *request, + uint16_t requestLength, + uint8_t isBroadcast) { - 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; + uint16_t address; + uint16_t value; + + if (requestLength != 8U) + { + ModbusSlaveStatistics.illegalValueCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + address = ModbusGetU16Be(&request[2]); + value = ModbusGetU16Be(&request[4]); + + /* + * 0x05 只接受 0xFF00(线圈置位)和 0x0000(线圈复位); + * 其他数值属于非法数据值。 + */ + if ((value != MODBUS_COIL_VALUE_ON) + && (value != MODBUS_COIL_VALUE_OFF)) + { + ModbusSlaveStatistics.illegalValueCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + if (address >= MODBUS_MAP_ITEM_COUNT) + { + ModbusSlaveStatistics.illegalAddressCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); + } + + ModbusCoilSetUnchecked( + address, + (value == MODBUS_COIL_VALUE_ON) ? 1U : 0U); + + if (isBroadcast != 0U) + { + return 0U; + } + + /* 0x05 正常应答原样回显请求的前 6 个字节,再追加 CRC。 */ + (void)memcpy(ModbusTxFrame, request, 6U); + ModbusAppendCrc(ModbusTxFrame, 6U); + return 8U; } -static uint16_t Modbus_ProcessWriteMultipleRegisters(const uint8_t *request, - uint16_t request_length, - uint8_t is_broadcast) +/** + * @brief 处理写多个线圈功能码 0x0F。 + * @param[in] request RTU 请求帧。 + * @param[in] requestLength 请求帧长度。 + * @param[in] isBroadcast 非 0 表示当前请求为广播。 + * @return 单播响应长度;广播或无法响应时返回 0。 + */ +static uint16_t ModbusProcessWriteMultipleCoils( + const uint8_t *request, + uint16_t requestLength, + uint8_t isBroadcast) { - 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; + uint16_t start; + uint16_t quantity; + uint16_t index; + uint8_t byteCount; + uint8_t expectedByteCount; + + if (requestLength < 9U) + { + ModbusSlaveStatistics.illegalValueCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + start = ModbusGetU16Be(&request[2]); + quantity = ModbusGetU16Be(&request[4]); + byteCount = request[6]; + expectedByteCount = (uint8_t)((quantity + 7U) / 8U); + + if ((quantity == 0U) + || (quantity > MODBUS_WRITE_COILS_MAX) + || (byteCount != expectedByteCount) + || (requestLength != (uint16_t)(9U + byteCount))) + { + ModbusSlaveStatistics.illegalValueCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + if (ModbusAddressRangeIsValid(start, quantity) == 0U) + { + ModbusSlaveStatistics.illegalAddressCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); + } + + for (index = 0U; index < quantity; index++) + { + uint8_t state; + + state = (uint8_t)((request[7U + (index >> 3U)] + >> (index & 0x0007U)) + & 0x01U); + ModbusCoilSetUnchecked((uint16_t)(start + index), state); + } + + if (isBroadcast != 0U) + { + return 0U; + } + + ModbusTxFrame[0] = ModbusSlaveAddress; + ModbusTxFrame[1] = 0X0F; + (void)memcpy(&ModbusTxFrame[2], &request[2], 4U); + ModbusAppendCrc(ModbusTxFrame, 6U); + return 8U; } -static uint16_t Modbus_ProcessRequest(const uint8_t *request, - uint16_t request_length) +/** + * @brief 处理写多个保持寄存器功能码 0x10。 + * @param[in] request RTU 请求帧。 + * @param[in] requestLength 请求帧长度。 + * @param[in] isBroadcast 非 0 表示当前请求为广播。 + * @return 单播响应长度;广播或无法响应时返回 0。 + */ +static uint16_t ModbusProcessWriteMultipleRegisters( + const uint8_t *request, + uint16_t requestLength, + uint8_t isBroadcast) { - 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); - } + uint16_t start; + uint16_t quantity; + uint16_t index; + uint8_t byteCount; + + if (requestLength < 9U) + { + ModbusSlaveStatistics.illegalValueCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + start = ModbusGetU16Be(&request[2]); + quantity = ModbusGetU16Be(&request[4]); + byteCount = request[6]; + + if ((quantity == 0U) + || (quantity > MODBUS_WRITE_REGS_MAX) + || (byteCount != (uint8_t)(quantity * 2U)) + || (requestLength != (uint16_t)(9U + byteCount))) + { + ModbusSlaveStatistics.illegalValueCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + if (ModbusAddressRangeIsValid(start, quantity) == 0U) + { + ModbusSlaveStatistics.illegalAddressCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); + } + + for (index = 0U; index < quantity; index++) + { + ModbusHoldingRegisters[start + index] = + ModbusGetU16Be(&request[7U + index * 2U]); + } + + if (isBroadcast != 0U) + { + return 0U; + } + + ModbusTxFrame[0] = ModbusSlaveAddress; + ModbusTxFrame[1] = 0X10; + (void)memcpy(&ModbusTxFrame[2], &request[2], 4U); + ModbusAppendCrc(ModbusTxFrame, 6U); + return 8U; +} + +/** + * @brief 校验并分发一帧 Modbus RTU 请求。 + * @param[in] request RTU 请求帧。 + * @param[in] requestLength 请求帧长度。 + * @return 待发送响应长度;无需响应时返回 0。 + */ +static uint16_t ModbusProcessRequest(const uint8_t *request, + uint16_t requestLength) +{ + uint16_t calculatedCrc; + uint16_t receivedCrc; + uint8_t isBroadcast; + + if (requestLength < 4U) + { + return 0U; + } + + calculatedCrc = ModbusCrc16(request, (uint16_t)(requestLength - 2U)); + receivedCrc = + (uint16_t)(request[requestLength - 2U] + | ((uint16_t)request[requestLength - 1U] << 8U)); + + if (calculatedCrc != receivedCrc) + { + ModbusSlaveStatistics.crcErrorCount++; + return 0U; + } + + if ((request[0] != ModbusSlaveAddress) + && (request[0] != MODBUS_BROADCAST_ADDRESS)) + { + ModbusSlaveStatistics.ignoredAddressCount++; + return 0U; + } + + isBroadcast = (request[0] == MODBUS_BROADCAST_ADDRESS) ? 1U : 0U; + ModbusSlaveStatistics.validFrameCount++; + ModbusLastValidFrameTick = HAL_GetTick(); + ModbusHasReceivedValidFrame = 1U; + + switch (request[1]) + { + case 0X01U://读线圈 + // 广播请求不允许读取,从站不作响应。 + return (isBroadcast != 0U) ? 0U : + ModbusProcessReadCoils(request, requestLength); + + case 0X03U://读保持寄存器 + return (isBroadcast != 0U) ? 0U : + ModbusProcessReadHolding(request, requestLength); + + case 0x05U://写单个线圈 + return ModbusProcessWriteSingleCoil(request, + requestLength, + isBroadcast); + + case 0x06U://写单个保持寄存器 + return ModbusProcessWriteSingleRegister(request, + requestLength, + isBroadcast); + + case 0x0FU://写多个线圈 + return ModbusProcessWriteMultipleCoils(request, + requestLength, + isBroadcast); + + case 0x10U://写多个保持寄存器 + return ModbusProcessWriteMultipleRegisters(request, + requestLength, + isBroadcast); + + default://未知功能码 + ModbusSlaveStatistics.illegalFunctionCount++; + return (isBroadcast != 0U) ? 0U : + ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_FUNCTION); + } } -HAL_StatusTypeDef ModbusSlave_Init(UART_HandleTypeDef *huart, - uint8_t slave_address) +HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart, + uint8_t slaveAddress) { - 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(); + + ModbusUart = huart; + ModbusSlaveAddress = slaveAddress; + + ModbusHoldingRegisters[HMI_REG_DEVICE_ID] = 0xF407U; + + return ModbusStartReceive(); } -void ModbusSlave_Poll(void) +void ModbusSlavePoll(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; + uint16_t responseLength; - if (HAL_UART_Transmit_DMA(s_uart, s_tx_frame, response_length) == HAL_OK) + if ((ModbusRxFrameReady == 0U) || (ModbusTxBusy != 0U)) { - g_modbus_slave_stats.tx_frame_count++; + return; } - else + + /* + * IDLE 事件在约 1 个字符静默后产生;再等待到 2 ms, + * 满足当前 115200 bit/s 下的 Modbus RTU 帧间隔要求。 + */ + // if ((HAL_GetTick() - ModbusRxEventTick) < MODBUS_RTU_FRAME_GAP_MS) + // { + // return; + // } + + responseLength = ModbusProcessRequest(ModbusRxFrame, ModbusRxFrameLength); + + + if (responseLength > 0U) { - s_tx_busy = 0U; - g_modbus_slave_stats.uart_error_count++; - (void)Modbus_StartReceive(); + /* + * 发送期间停止接收 + * + */ + ModbusStopReceive(); + ModbusTxBusy = 1U; + + if (HAL_UART_Transmit_DMA(ModbusUart, + ModbusTxFrame, + responseLength) == HAL_OK) + { + ModbusSlaveStatistics.txFrameCount++; + } + else + { + ModbusTxBusy = 0U; + ModbusSlaveStatistics.uartErrorCount++; + (void)ModbusStartReceive();//重启DMA接收 + } } - } - /* - * 处理期间保持ready=1,使中断不会覆盖当前帧; - * 到这里再释放帧槽。 - */ - s_rx_frame_ready = 0U; + /* + * 处理期间保持 ModbusRxFrameReady 为 1,使中断不会覆盖 + * 当前帧;处理完成后再释放帧槽。 + */ + ModbusRxFrameReady = 0U; } -void ModbusSlave_OnRxEvent(UART_HandleTypeDef *huart, uint16_t size) +void ModbusSlaveOnRxEvent(UART_HandleTypeDef *huart, uint16_t size) { - if ((huart != s_uart) || (s_uart == NULL)) - { - return; - } + if ((huart != ModbusUart) || (ModbusUart == NULL)) + { + return; + } - g_modbus_slave_stats.rx_event_count++; + ModbusSlaveStatistics.rxEventCount++;//串口接收事件计数 - if ((size > 0U) && (size <= MODBUS_RTU_ADU_SIZE_MAX)) - { - if (s_rx_frame_ready == 0U) + if ((size > 0U) && (size <= MODBUS_RTU_ADU_SIZE_MAX)) { - (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; + if (ModbusRxFrameReady == 0U) + { + (void)memcpy(ModbusRxFrame, ModbusRxDmaBuffer, size); + ModbusRxFrameLength = size; + ModbusRxEventTick = HAL_GetTick(); + ModbusRxFrameReady = 1U; + } + else + { + /* 单帧槽尚未处理完成,丢弃新帧而不覆盖旧帧。 */ + ModbusSlaveStatistics.droppedFrameCount++; + } } else { - /* 单帧槽尚未处理完成,丢弃新帧而不覆盖旧帧。 */ - g_modbus_slave_stats.dropped_frame_count++; + ModbusSlaveStatistics.droppedFrameCount++; } - } - else - { - g_modbus_slave_stats.dropped_frame_count++; - } - /* DMA普通模式在IDLE事件后已经停止,需要显式重新启动。 */ - if (s_tx_busy == 0U) - { - (void)Modbus_StartReceive(); - } + /* DMA 普通模式在 IDLE 事件后停止,需要重新启动。 */ + if (ModbusTxBusy == 0U) + { + (void)ModbusStartReceive(); + } } -void ModbusSlave_OnTxComplete(UART_HandleTypeDef *huart) +void ModbusSlaveOnTxComplete(UART_HandleTypeDef *huart) { - if ((huart != s_uart) || (s_uart == NULL)) - { - return; - } + if ((huart != ModbusUart) || (ModbusUart == NULL)) + { + return; + } - s_tx_busy = 0U; - (void)Modbus_StartReceive(); + ModbusTxBusy = 0U; + (void)ModbusStartReceive();//重启DMA接收 } -void ModbusSlave_OnUartError(UART_HandleTypeDef *huart) +void ModbusSlaveOnUartError(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(); + if ((huart != ModbusUart) || (ModbusUart == NULL)) + { + return; + } + + ModbusSlaveStatistics.uartErrorCount++; + ModbusTxBusy = 0U; + (void)HAL_UART_Abort(huart);//立即终止这个串口当前正在进行的发送和接收操作 + (void)ModbusStartReceive();//重启DMA接收 } -uint8_t ModbusSlave_SetHoldingRegister(uint16_t address, uint16_t value) +uint8_t ModbusSlaveSetHoldingRegister(uint16_t address, uint16_t value) { - if (address >= MODBUS_MAP_ITEM_COUNT) - { - return 0U; - } + if (address >= MODBUS_MAP_ITEM_COUNT) + { + return 0U; + } - s_holding_registers[address] = value; - return 1U; + ModbusHoldingRegisters[address] = value; + return 1U; } -uint8_t ModbusSlave_GetHoldingRegister(uint16_t address, uint16_t *value) +uint8_t ModbusSlaveGetHoldingRegister(uint16_t address, uint16_t *value) { - if ((address >= MODBUS_MAP_ITEM_COUNT) || (value == NULL)) - { - return 0U; - } + if ((address >= MODBUS_MAP_ITEM_COUNT) || (value == NULL)) + { + return 0U; + } - *value = s_holding_registers[address]; - return 1U; + *value = ModbusHoldingRegisters[address]; + return 1U; } -uint8_t ModbusSlave_SetCoil(uint16_t address, uint8_t state) +uint8_t ModbusSlaveSetCoil(uint16_t address, uint8_t state) { - if (address >= MODBUS_MAP_ITEM_COUNT) - { - return 0U; - } + if (address >= MODBUS_MAP_ITEM_COUNT) + { + return 0U; + } - Modbus_CoilSetUnchecked(address, state); - return 1U; + ModbusCoilSetUnchecked(address, state); + return 1U; } -uint8_t ModbusSlave_GetCoil(uint16_t address, uint8_t *state) +uint8_t ModbusSlaveGetCoil(uint16_t address, uint8_t *state) { - if ((address >= MODBUS_MAP_ITEM_COUNT) || (state == NULL)) - { - return 0U; - } + if ((address >= MODBUS_MAP_ITEM_COUNT) || (state == NULL)) + { + return 0U; + } - *state = Modbus_CoilGetUnchecked(address); - return 1U; + *state = ModbusCoilGetUnchecked(address); + return 1U; } -uint8_t ModbusSlave_IsConnected(uint32_t timeout_ms) +uint8_t ModbusSlaveIsConnected(uint32_t timeoutMs) { - if (s_has_received_valid_frame == 0U) - { - return 0U; - } + if (ModbusHasReceivedValidFrame == 0U) + { + return 0U; + } - return ((HAL_GetTick() - s_last_valid_frame_tick) <= timeout_ms) ? 1U : 0U; + return ((HAL_GetTick() - ModbusLastValidFrameTick) <= timeoutMs) + ? 1U + : 0U; }