Browse Source

开始做PLSR项目

deepseek
ywh 1 month ago
parent
commit
4c45d6e670
61 changed files with 30207 additions and 1646 deletions
  1. +16
    -15
      Core/Src/main.c
  2. +0
    -158
      Document/0_Modbus任务要求.md
  3. +0
    -27
      Document/1_Modbus相关技术点.txt
  4. BIN
      Document/PLSR_document/任务要求.png
  5. BIN
      Document/PLSR_document/实现PLSR指令.pdf
  6. BIN
      Document/Xinje_HMI_ModbusRTU_Function_Codes.pdf
  7. BIN
      Document/Xinje_TG765-MT_Installation_Manual_v1.1.pdf
  8. BIN
      Document/Xinje_TouchWin_User_Manual_v1.2.pdf
  9. BIN
      Document/Xinje_XD_XL_PLC_Hardware_Manual_PD01_20240820_v1.3.2.pdf
  10. +0
    -286
      Document/my_document/IAR存储空间占用查看方法.md
  11. +0
    -336
      Document/my_document/Modbus 0x14收发ADU示例.md
  12. +0
    -141
      Document/my_document/Modbus RTU最大长度ADU测试.md
  13. +0
    -81
      Document/my_document/Modbus异常码实例.md
  14. +0
    -552
      Document/my_document/modbus学习文档.md
  15. BIN
      Document/my_document/~$需求规格书.docx
  16. BIN
      Document/my_document/框图/协议解析流程图.vsdx
  17. BIN
      Document/my_document/框图/应用模块流程图.vsdx
  18. BIN
      Document/my_document/框图/系统框图.vsdx
  19. BIN
      Document/my_document/测试大纲.docx
  20. BIN
      Document/my_document/测试报告.docx
  21. BIN
      Document/my_document/设计方案书.docx
  22. BIN
      Document/my_document/需求规格书.docx
  23. BIN
      HostComputer.zip
  24. +38
    -0
      HostComputer/ModbusTimingTester.spec
  25. +38
    -0
      HostComputer/ModbusTimingTester_3Baud.spec
  26. +37
    -0
      HostComputer/README.md
  27. BIN
      HostComputer/__pycache__/modbus_timing_tester.cpython-39.pyc
  28. +3202
    -0
      HostComputer/build/ModbusTimingTester/Analysis-00.toc
  29. BIN
      HostComputer/build/ModbusTimingTester/ModbusTimingTester.pkg
  30. +2795
    -0
      HostComputer/build/ModbusTimingTester/PKG-00.toc
  31. BIN
      HostComputer/build/ModbusTimingTester/PYZ-00.pyz
  32. +158
    -0
      HostComputer/build/ModbusTimingTester/PYZ-00.toc
  33. BIN
      HostComputer/build/ModbusTimingTester/base_library.zip
  34. BIN
      HostComputer/build/ModbusTimingTester/localpycs/pyimod01_archive.pyc
  35. BIN
      HostComputer/build/ModbusTimingTester/localpycs/pyimod02_importers.pyc
  36. BIN
      HostComputer/build/ModbusTimingTester/localpycs/pyimod03_ctypes.pyc
  37. BIN
      HostComputer/build/ModbusTimingTester/localpycs/pyimod04_pywin32.pyc
  38. BIN
      HostComputer/build/ModbusTimingTester/localpycs/struct.pyc
  39. +30
    -0
      HostComputer/build/ModbusTimingTester/warn-ModbusTimingTester.txt
  40. +7159
    -0
      HostComputer/build/ModbusTimingTester/xref-ModbusTimingTester.html
  41. +3202
    -0
      HostComputer/build/ModbusTimingTester_3Baud/Analysis-00.toc
  42. +2817
    -0
      HostComputer/build/ModbusTimingTester_3Baud/EXE-00.toc
  43. BIN
      HostComputer/build/ModbusTimingTester_3Baud/ModbusTimingTester_3Baud.pkg
  44. +2795
    -0
      HostComputer/build/ModbusTimingTester_3Baud/PKG-00.toc
  45. BIN
      HostComputer/build/ModbusTimingTester_3Baud/PYZ-00.pyz
  46. +158
    -0
      HostComputer/build/ModbusTimingTester_3Baud/PYZ-00.toc
  47. BIN
      HostComputer/build/ModbusTimingTester_3Baud/base_library.zip
  48. BIN
      HostComputer/build/ModbusTimingTester_3Baud/localpycs/pyimod01_archive.pyc
  49. BIN
      HostComputer/build/ModbusTimingTester_3Baud/localpycs/pyimod02_importers.pyc
  50. BIN
      HostComputer/build/ModbusTimingTester_3Baud/localpycs/pyimod03_ctypes.pyc
  51. BIN
      HostComputer/build/ModbusTimingTester_3Baud/localpycs/pyimod04_pywin32.pyc
  52. BIN
      HostComputer/build/ModbusTimingTester_3Baud/localpycs/struct.pyc
  53. +30
    -0
      HostComputer/build/ModbusTimingTester_3Baud/warn-ModbusTimingTester_3Baud.txt
  54. +7159
    -0
      HostComputer/build/ModbusTimingTester_3Baud/xref-ModbusTimingTester_3Baud.html
  55. BIN
      HostComputer/dist/ModbusTimingTester.exe
  56. BIN
      HostComputer/dist/ModbusTimingTester_3Baud.exe
  57. +491
    -0
      HostComputer/modbus_timing_tester.py
  58. +1
    -0
      HostComputer/requirements.txt
  59. +16
    -0
      HostComputer/run.bat
  60. +5
    -1
      Modbus/Inc/modbus_rtu_slave.h
  61. +60
    -49
      Modbus/Src/modbus_rtu_slave.c

+ 16
- 15
Core/Src/main.c View File

@@ -46,7 +46,7 @@
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
DMA_HandleTypeDef hdma_usart1_tx;
static uint8_t ConnectFlag;
static volatile uint8_t ConnectFlag;
/* USER CODE BEGIN PV */

static OS_STK AppTaskStartStk[APP_TASK_START_STK_SIZE];
@@ -82,22 +82,23 @@ static void AppTaskStart(void *pArg)
/*
* 维护几个供触摸屏首次联调读取的保持寄存器:
*/
(void)ModbusSlaveSetHoldingRegister( // OS时钟
HMI_REG_UPTIME_SECONDS, (uint16_t)(OSTimeGet() / OS_TICKS_PER_SEC));
(void)ModbusSlaveSetHoldingRegister( // 有效帧计数
HMI_REG_RX_FRAME_COUNT,
(uint16_t)ModbusSlaveStatistics.validFrameCount);
// (void)ModbusSlaveSetHoldingRegister( // OS时钟
// HMI_REG_UPTIME_SECONDS, (uint16_t)(OSTimeGet() / OS_TICKS_PER_SEC));
// (void)ModbusSlaveSetHoldingRegister( // 有效帧计数
// HMI_REG_RX_FRAME_COUNT,
// (uint16_t)ModbusSlaveStatistics.validFrameCount);

(void)ModbusSlaveSetHoldingRegister( // 从站地址不匹配计数
3, (uint16_t)ModbusSlaveStatistics.ignoredAddressCount);
(void)ModbusSlaveSetHoldingRegister( // 非法功能码计数
4, (uint16_t)ModbusSlaveStatistics.illegalFunctionCount);
// (void)ModbusSlaveSetHoldingRegister( // 从站地址不匹配计数
// D3, (uint16_t)ModbusSlaveStatistics.ignoredAddressCount);
// (void)ModbusSlaveSetHoldingRegister( // 非法功能码计数
// D4, (uint16_t)ModbusSlaveStatistics.illegalFunctionCount);

(void)ModbusSlaveSetHoldingRegister( // 非法地址计数
5, (uint16_t)ModbusSlaveStatistics.illegalAddressCount);
// (void)ModbusSlaveSetHoldingRegister( // 非法地址计数
// D5, (uint16_t)ModbusSlaveStatistics.illegalAddressCount);

(void)ModbusSlaveSetHoldingRegister( // 非法数据值计数
6, (uint16_t)ModbusSlaveStatistics.illegalValueCount);
// (void)ModbusSlaveSetHoldingRegister( // 非法数据值计数
// D6, (uint16_t)ModbusSlaveStatistics.illegalValueCount);

HistorySaveToRegisters();

@@ -262,7 +263,7 @@ static void MX_USART1_UART_Init(void)

/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.BaudRate = 9600;
huart1.Init.WordLength = UART_WORDLENGTH_9B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_EVEN;


+ 0
- 158
Document/0_Modbus任务要求.md View File

@@ -1,158 +0,0 @@
# Modbus通信
## 任务要求
- 了解Modbus的基本概念

- 熟悉功能码0x01、0x03、0x0f、0x10

- 书写规格书
· 使用word文档书写

- 书写方案书
· 使用Visio绘制流程图
· 使用word文档书写
- 书写测试大纲
· 书写测试大纲以保证自身产品的正确性、稳定性
· 使用word文档书写

- 实现ModbusRTU主站(触摸屏)
· 能够正常连接
· 能够判断连接断开
· 能够按要求生成请求帧
· 能够正确解析应答帧,并做相应处理
· 能够进行超时处理
· 拥有美观易操作的人机界面
- 实现ModbusRTU从站(PLC)
· 能够正常连接
· 能够判断连接断开
· 能够正确解析请求帧
· 对于正确请求帧给出正确应答帧
· 对于错误请求帧给出错误应答帧或按规格给出相应的处理

附加功能:
- 如果需要读取0x10000位置的寄存器该如何处理?
- 如果需要读取所有单数个寄存器,如何提高读取效率?
- 如果需要获取通信历史该如何处理?
- 如果从机数据需要断电不丢失该怎么处理?
- 提高人机交互的美观性及易用性。

注:
- 该任务需要基于ucos移植实现
- Modbus学习需要书写相关学习笔记
- 线圈和寄存器范围都是0~0x270F
- 要求方案书有明确的层级结构
- 最终实现代码需要与方案书相一致
- 代码书写需按照给定的代码规范完成
- 对于解析部分需要做对应的单元测试
- 代码管理空间为:``http://gitea.xinje.cc/TrainCamp_姓名全拼_Modbus.git``
- 目录结构如下图所示:
TrainCamp_姓名全拼_modbus
|---iar
| |---settings
| |---XXX.dep
| |---XXX.ewd
| |---XXX.ewp
| |---XXX.eww
| |---XXX.icf
| +---startup_stm32xxx.s
|---app
| |---main.h
| +---main.c
|---cmsis
| |---core_cm3.h
| | .
| | .
| | .
| +---tz_context.h
|---drivers
| |---inc
| | +---stm32f103_gpio.h
| +---src
| +---stm32f103_gpio.h
|---modbus
+---.gitignore

## 任务器材
| 器材 | 数量 |
| ---------- | --------- |
| XDM-60T4-E/XDM-60T10-E | 1台 |
| ST-LINK | 1个 |
| 电源线 | 1根 |
| USB-COM | 1个 |
| XVP线 | 2根 |
| TG765-MT | 1台 |

## XDM-60T4-E硬件信息

- 芯片型号: STM32F407IG ,晶振12MHz。
- 引脚信息

| 芯片引脚 | 实际作用 |
| --------- | -------- |
| I0 | PH10 |
| I1 | PH11 |
| I2 | PG8 |
| I3 | PB4 |
| I4 | PB5 |
| I5 | PG12 |
| I6 | PB6 |
| I7 | PB7 |
| I10 | PF10 |
| I11 | PA15 |
| I12 | PB3 |
| I13 | PA14 |
| I14 | PI4 |
| I15 | PI5 |
| I16 | PI6 |
| I17 | PI7 |
| I20 | PE2 |
| Q0 | PF6 |
| Q1 | PF8 |
| Q2 | PF7 |
| Q3 | PF9 |
| Q4 | PI8 |
| Q5 | PE6 |
| Q6 | PE5 |
| Q7 | PE4 |
| Q10 | PG7 |
| Q11 | PG6 |
| Q12 | PH9 |
| Q13 | PH8 |
| Q14 | PH7 |
| Q15 | PH6 |
| Q16 | PF11 |
| Q17 | PB0 |
| Q20 | PH5 |
| USART1_TX | PA9 |
| USART1_RX | PA10 |

## XDM-60T10-E硬件信息

- 芯片型号:STM32F407ZG,晶振12MHz。
- 引脚信息

| 芯片引脚 | 实际作用 |
| --------- | -------- |
| I0 | PA0 |
| I1 | PA1 |
| I2 | PG8 |
| I3 | PB4 |
| I4 | PB5 |
| I5 | PG12 |
| I6 | PB6 |
| I7 | PB7 |
| I10 | PF10 |
| I11 | PA15 |
| I12 | PB3 |
| I13 | PA14 |
| Q0 | PF6 |
| Q1 | PF8 |
| Q2 | PF7 |
| Q3 | PF9 |
| USART1_TX | PA9 |
| USART1_RX | PA10 |


+ 0
- 27
Document/1_Modbus相关技术点.txt View File

@@ -1,27 +0,0 @@
# Modbus相关知识点
## 基本概念
- Modbus基本概念
- 应用场景
- 三种通信模式
- 广播模式与单播模式
- 事务处理流程
- 寄存器相关概念

## 数据帧格式
- 0x01功能码
· 请求帧格式
· 应答帧格式
- 0x03功能码
· 请求帧格式
· 应答帧格式
- 0x0f功能码
· 请求帧格式
· 应答帧格式
- 0x10功能码
· 请求帧格式
· 应答帧格式
## 嵌入式平台
- 串口通信
- 网口通信
- 人机界面

BIN
Document/PLSR_document/任务要求.png View File

Before After
Width: 647  |  Height: 208  |  Size: 58 KiB

BIN
Document/PLSR_document/实现PLSR指令.pdf View File


BIN
Document/Xinje_HMI_ModbusRTU_Function_Codes.pdf View File


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


BIN
Document/Xinje_TouchWin_User_Manual_v1.2.pdf View File


BIN
Document/Xinje_XD_XL_PLC_Hardware_Manual_PD01_20240820_v1.3.2.pdf View File


+ 0
- 286
Document/my_document/IAR存储空间占用查看方法.md View File

@@ -1,286 +0,0 @@
# IAR 中 Flash、SRAM 和 CCMRAM 占用的查看方法

## 1. 从哪里查看

IAR 编译、链接完成后,会生成 `.map` 文件。该文件记录了程序代码、常量、全局变量、静态变量、栈以及自定义段的地址和大小。

本工程的 Map 文件位于:

```text
EWARM\Modbus\List\Modbus.map
```

如果工程没有生成 Map 文件,可以在 IAR 中依次打开:

```text
Project → Options → Linker → List
```

勾选生成 linker map file 的选项,然后重新执行 `Rebuild All`。

查看本工程占用情况时,打开 `Modbus.map`,主要搜索以下三个位置:

```text
PLACEMENT SUMMARY
Unused ranges
MODULE SUMMARY
```

## 2. 本工程的存储区域

在 Map 文件的 `PLACEMENT SUMMARY` 中可以看到:

```text
"P1": place in [from 0x08000000 to 0x080FFFFF] { ro };

"P2": place in [from 0x20000000 to 0x2001FFFF] {
rw, block CSTACK, block HEAP };

"P3": place in [from 0x10000000 to 0x1000FFFF] {
section .ccmram };
```

各区域含义如下:

| 区域 | 地址范围 | 容量 | 主要内容 |
|---|---|---:|---|
| P1 | `0x08000000~0x080FFFFF` | 1 MiB | Flash 中的程序代码、常量和变量初始值 |
| P2 | `0x20000000~0x2001FFFF` | 128 KiB | 普通 SRAM 中的全局变量、静态变量和栈 |
| P3 | `0x10000000~0x1000FFFF` | 64 KiB | CCMRAM 中的变量 |

说明:这里的 `KiB` 按 `1 KiB = 1024 Byte` 计算。

## 3. Flash 占用怎么看

### 3.1 查看方法

在 Map 文件的 `MODULE SUMMARY` 末尾可以看到:

```text
Grand Total: 33'398 751 163'114
ro code ro data rw data
```

其中:

- `ro code`:只读程序代码,存放在 Flash;
- `ro data`:只读数据、常量和初始化数据,存放在 Flash;
- `rw data`:运行时可读写数据,主要占用 RAM。

本工程的 Flash 占用为:

```text
Flash占用 = ro code + ro data
= 33,398 + 751
= 34,149 Byte
≈ 33.35 KiB
```

Flash 总容量为 1 MiB,因此占用率为:

```text
34,149 ÷ 1,048,576 × 100%
≈ 3.26%
```

### 3.2 通过地址验证

Map 文件中还可以看到:

```text
.intvec:0x188 Byte
"P1": 0x83DD Byte
```

因此:

```text
0x188 + 0x83DD = 0x8565
```

即 Flash 已经从 `0x08000000` 使用到 `0x08008564`,下一个空闲地址是 `0x08008565`。

`Unused ranges` 中也会显示:

```text
0x08008565~0x080FFFFF,剩余 0xF7A9B Byte
```

## 4. 普通 SRAM 占用怎么看

普通 SRAM 对应 `P2`,地址范围是:

```text
0x20000000~0x2001FFFF
```

在 `PLACEMENT SUMMARY` 中,P2 被分为三部分:

```text
"P2", part 1 of 3: 0xFC
"P2", part 2 of 3: 0x18DD0
"P2", part 3 of 3: 0x400
```

其中:

- `part 1`:已经初始化的全局变量和静态变量;
- `part 2`:未初始化或清零的全局变量和静态变量;
- `part 3`:`CSTACK`,即主栈,本工程为 `0x400 = 1024 Byte`。

按照链接后占用的地址范围计算:

```text
普通SRAM占用空间
= 0xFC + 0x18DD0 + 0x400
= 103,116 Byte
≈ 100.70 KiB
```

占用率约为:

```text
103,116 ÷ 131,072 × 100%
≈ 78.67%
```

Map 文件的 `Unused ranges` 中显示普通 SRAM 剩余:

```text
0x20018ECC~0x20018ECF:4 Byte
0x200192D0~0x2001FFFF:0x6D30 Byte
```

因此普通 SRAM 总剩余空间为:

```text
4 + 0x6D30
= 27,956 Byte
≈ 27.30 KiB
```

### 4.1 为什么有时会相差几字节

`MODULE SUMMARY` 统计的是各变量和数据段本身的大小;`PLACEMENT SUMMARY` 还可能包含为了满足地址对齐而留下的填充空间。

本工程普通 SRAM 中的有效读写数据约为 `103,114 Byte`,而链接后实际占用的地址空间为 `103,116 Byte`,两者相差的 `2 Byte` 属于段间对齐产生的空隙。

判断变量本身有多大时看 `MODULE SUMMARY`;判断 RAM 是否还能放下新数据时,应结合 `Unused ranges` 查看真正剩余的连续空间。

## 5. CCMRAM 占用怎么看

CCMRAM 对应 `P3`,地址范围是:

```text
0x10000000~0x1000FFFF
```

Map 文件中显示:

```text
"P3": 0xEA60
.ccmram 0x10000000~0x1000EA5F
```

因此 CCMRAM 占用为:

```text
0xEA60 = 60,000 Byte
≈ 58.59 KiB
```

CCMRAM 总容量为 64 KiB,因此占用率为:

```text
60,000 ÷ 65,536 × 100%
≈ 91.55%
```

`Unused ranges` 中显示:

```text
0x1000EA60~0x1000FFFF:0x15A0 Byte
```

所以 CCMRAM 剩余:

```text
0x15A0 = 5,536 Byte
≈ 5.41 KiB
```

本工程的 CCMRAM 占用已经超过 90%,后续增加 `.ccmram` 数组时需要特别注意。

## 6. RAM 合计怎么看

RAM 合计应包含:

```text
RAM合计 = 普通SRAM中的读写数据 + CCMRAM中的读写数据
```

在 `MODULE SUMMARY` 中,本工程的 `Grand Total rw data` 为:

```text
rw data = 163,114 Byte
≈ 159.29 KiB
```

芯片可用的这两块 RAM 总容量为:

```text
128 KiB普通SRAM + 64 KiB CCMRAM
= 192 KiB
= 196,608 Byte
```

因此 RAM 合计占用率约为:

```text
163,114 ÷ 196,608 × 100%
≈ 82.96%
```

需要注意,RAM 合计只能说明总体使用量。普通 SRAM 和 CCMRAM 地址不连续,不能因为两者合计还有空间,就认为某个大数组一定能放下。放置新数组前仍要分别检查 P2 和 P3 的剩余连续空间。

## 7. 本工程当前占用汇总

| 存储区 | 已使用 | 总容量 | 占用率 | 剩余空间 |
|---|---:|---:|---:|---:|
| Flash | 34,149 Byte(33.35 KiB) | 1 MiB | 3.26% | 约 990.65 KiB |
| 普通 SRAM | 约 103,116 Byte(100.70 KiB) | 128 KiB | 78.67% | 约 27.30 KiB |
| CCMRAM | 60,000 Byte(58.59 KiB) | 64 KiB | 91.55% | 约 5.41 KiB |
| RAM 合计 | 163,114 Byte(159.29 KiB) | 192 KiB | 82.96% | 约 32.71 KiB |

以上数值只对应当前一次编译结果。代码、数组大小、任务栈或链接配置发生变化后,需要重新编译并再次查看 Map 文件。

## 8. 如何查某个变量放在哪里

在 Map 文件中按 `Ctrl+F` 搜索变量名,可以查看它所属的段、起始地址和大小。

可以通过地址判断变量所在区域:

| 地址开头 | 所在区域 |
|---|---|
| `0x080...` | Flash |
| `0x200...` | 普通 SRAM |
| `0x100...` | CCMRAM |

例如,本工程的 CCMRAM 数组会显示在:

```text
.ccmram 0x10000000 ...
```

这说明该数组已经被链接到 CCMRAM,而不是普通 SRAM。

## 9. 每次编译后的推荐检查步骤

1. 执行 `Rebuild All`,确保 Map 文件是最新的。
2. 搜索 `Grand Total`,查看 `ro code`、`ro data` 和 `rw data`。
3. 用 `ro code + ro data` 计算 Flash 占用。
4. 搜索 `"P2"`,查看普通 SRAM 各部分的占用。
5. 搜索 `"P3"`,查看 CCMRAM 占用。
6. 搜索 `Unused ranges`,确认各存储区真正剩余的连续空间。
7. 新增大数组后搜索数组名,确认它被链接到了预期的 RAM 区域。


+ 0
- 336
Document/my_document/Modbus 0x14收发ADU示例.md View File

@@ -1,336 +0,0 @@
# Modbus RTU 0x14 收发 ADU 示例

## 1. 示例条件

本示例使用功能码 `0x14` 读取文件记录,按照当前工程只处理一个子请求的格式编写。

```text
从站地址: 1
引用类型: 0x06
文件号: 0
起始记录号: 0
读取数量: 2个寄存器
第一个寄存器值: 0x1234
第二个寄存器值: 0x5678
```

## 2. 请求帧

### 2.1 完整请求 ADU

以下内容可以直接复制到十六进制串口助手发送:

```text
01 14 07 06 00 00 00 00 00 02 78 E5
```

请求帧长度为:

```text
12字节
```

### 2.2 请求帧解析

| 字节 | 含义 | 说明 |
|---|---|---|
| `01` | 从站地址 | 从站地址为1 |
| `14` | 功能码 | 读取文件记录 |
| `07` | 后续字节数 | 一个子请求固定占7字节 |
| `06` | 引用类型 | Modbus文件记录规定为`0x06` |
| `00 00` | 文件号 | 文件0 |
| `00 00` | 起始记录号 | 从记录0开始 |
| `00 02` | 记录长度 | 读取2个16位寄存器 |
| `78 E5` | CRC16 | CRC低字节`78`在前,高字节`E5`在后 |

请求帧中参与CRC计算的是:

```text
01 14 07 06 00 00 00 00 00 02
```

计算得到:

```text
CRC16 = 0xE578
```

Modbus RTU发送CRC时低字节在前,因此帧尾为:

```text
78 E5
```

## 3. 正常响应帧

假设文件0的记录0和记录1分别保存:

```text
记录0 = 0x1234
记录1 = 0x5678
```

### 3.1 完整响应 ADU

```text
01 14 06 05 06 12 34 56 78 92 FE
```

响应帧长度为:

```text
11字节
```

### 3.2 响应帧解析

| 字节 | 含义 | 说明 |
|---|---|---|
| `01` | 从站地址 | 响应来自从站1 |
| `14` | 功能码 | 读取文件记录 |
| `06` |响应数据总字节数 | 后续子响应共6字节 |
| `05` | 子响应长度 | 引用类型1字节加寄存器数据4字节 |
| `06` | 引用类型 | 固定为`0x06` |
| `12 34` | 第一个寄存器 | 值为`0x1234` |
| `56 78` | 第二个寄存器 | 值为`0x5678` |
| `92 FE` | CRC16 | CRC低字节`92`在前,高字节`FE`在后 |

响应帧中参与CRC计算的是:

```text
01 14 06 05 06 12 34 56 78
```

计算得到:

```text
CRC16 = 0xFE92
```

因此响应帧尾发送:

```text
92 FE
```

## 4. 长度计算

读取数量为 `quantity` 时,单子请求的请求帧长度固定为:

```text
12字节
```

正常响应中的字段为:

```text
子响应长度 = 1 + quantity × 2
响应数据总字节数 = 2 + quantity × 2
完整响应ADU长度 = 7 + quantity × 2
```

本例读取2个寄存器:

```text
子响应长度 = 1 + 2 × 2 = 5字节
响应数据总字节数 = 2 + 2 × 2 = 6字节
完整响应ADU长度 = 7 + 2 × 2 = 11字节
```

## 5. 与当前代码的对应关系

请求帧中的参数由以下代码解析:

```c
fileNumber = ModbusGetU16Be(&request[4]);
recordNumber = ModbusGetU16Be(&request[6]);
quantity = ModbusGetU16Be(&request[8]);
```

响应帧字段对应:

```c
ModbusTxFrame[0] = ModbusSlaveAddress;
ModbusTxFrame[1] = 0x14U;
ModbusTxFrame[2] = (uint8_t)(2U + quantity * 2U);
ModbusTxFrame[3] = (uint8_t)(1U + quantity * 2U);
ModbusTxFrame[4] = 0x06U;
```

如果后续将文件号改为标准的一基编号,即文件号使用 `1~16`,测试请求中的文件号需要由:

```text
00 00
```

改为:

```text
00 01
```

同时重新计算请求帧CRC。

## 6. 读取124个寄存器的示例

### 6.1 请求条件

```text
从站地址: 1
引用类型: 0x06
文件号: 0
起始记录号: 0
读取数量: 124个寄存器,即0x007C
```

### 6.2 完整请求ADU

以下请求可以直接复制到十六进制串口助手发送:

```text
01 14 07 06 00 00 00 00 00 7C F8 C5
```

请求帧长度为:

```text
12字节
```

请求帧解析如下:

| 字节 | 含义 |
|---|---|
| `01` | 从站地址为1 |
| `14` | 读取文件记录功能码 |
| `07` | 当前子请求包含7个字节 |
| `06` | 引用类型 |
| `00 00` | 文件号0 |
| `00 00` | 从记录0开始读取 |
| `00 7C` | 读取124个寄存器 |
| `F8 C5` | CRC低字节、CRC高字节 |

参与CRC计算的内容为:

```text
01 14 07 06 00 00 00 00 00 7C
```

计算结果为:

```text
CRC16 = 0xC5F8
```

因此请求帧末尾按照低字节在前的顺序发送:

```text
F8 C5
```

### 6.3 正常响应格式

读取124个寄存器时,寄存器数据占用:

```text
124 × 2 = 248字节 = 0xF8字节
```

子响应长度为:

```text
引用类型1字节 + 寄存器数据248字节
= 249字节
= 0xF9字节
```

响应数据总字节数为:

```text
子响应长度字段1字节 + 子响应249字节
= 250字节
= 0xFA字节
```

所以正常响应格式为:

```text
01 14 FA F9 06 [248字节寄存器数据] CRC低 CRC高
```

完整响应ADU长度为:

```text
从站地址1字节
+ 功能码1字节
+ 响应数据总字节数字段1字节
+ 子响应长度字段1字节
+ 引用类型1字节
+ 寄存器数据248字节
+ CRC 2字节
= 255字节
```

该响应可以放入当前工程的256字节发送缓冲区:

```c
static uint8_t ModbusTxFrame[256];
```

### 6.4 所有寄存器值均为0时的响应

假设读取的124个寄存器全部为 `0x0000`,响应帧的开头为:

```text
01 14 FA F9 06
```

随后是248个 `00` 数据字节,帧尾CRC为:

```text
6C 45
```

即:

```text
01 14 FA F9 06 [248个00] 6C 45
```

该响应的CRC计算值为:

```text
CRC16 = 0x456C
```

Modbus RTU按照低字节在前发送,因此CRC字段为:

```text
6C 45
```

### 6.5 对应的代码限制

发送缓冲区长度为256字节时,单个0x14子请求最多读取124个寄存器,因此数量检查应为:

```c
if ((quantity == 0U) || (quantity > 124U))
{
ModbusSlaveStatistics.illegalValueCount++;
return ModbusBuildException(
request[1],
MODBUS_EX_ILLEGAL_VALUE);
}
```

如果每个 `MODBUS_HISTORY_ITEM` 为520字节,即260个16位寄存器,还必须保证本次读取不超过当前文件:

```c
if (((uint32_t)recordNumber + quantity) > 260UL)
{
ModbusSlaveStatistics.illegalAddressCount++;
return ModbusBuildException(
request[1],
MODBUS_EX_ILLEGAL_ADDRESS);
}
```

+ 0
- 141
Document/my_document/Modbus RTU最大长度ADU测试.md View File

@@ -1,141 +0,0 @@
# Modbus RTU最大长度ADU测试

串口助手请选择HEX发送。下列报文已经完整展开,可以直接复制。

测试条件:

- 从站地址为`0x01`
- 先执行0x0F写线圈,再执行0x01读线圈
- 先执行0x10写保持寄存器,再执行0x03读保持寄存器
- 0x10和0x03使用D6000起始地址,避免覆盖掉电保持区和通信历史区
- 文档中的全零读取响应以相关线圈和保持寄存器初始值均为0为前提
- 0x0F一次最多写1968个线圈,不能单次清零全部2000个待读线圈;测试0x01前还应确认线圈1968~1999为0
- 0x10一次最多写123个寄存器,不能单次清零全部125个待读寄存器;测试0x03前还应确认D6123~D6124为0

## 0x0F写入1968个线圈

含义:

- `01`:从站地址
- `0F`:写多个线圈功能码
- `00 00`:起始线圈地址0
- `07 B0`:写入数量1968
- `F6`:后续线圈数据长度246字节
- 连续246个`00`:将线圈0~1967全部写为OFF
- `A6 FE`:CRC低字节、CRC高字节
- 测试目的:用255字节合法请求帧检查DMA接收缓冲区边界和最大线圈写入数量

请求帧长度:255字节

请求帧:

```text
01 0F 00 00 07 B0 F6 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A6 FE
```

预期响应帧长度:8字节

预期响应帧:

```text
01 0F 00 00 07 B0 56 4F
```

## 0x01读取2000个线圈

含义:

- `01`:从站地址
- `01`:读线圈功能码
- `00 00`:起始线圈地址0
- `07 D0`:读取数量2000
- `3F A6`:CRC低字节、CRC高字节
- 响应中的`FA`表示后续包含250字节线圈状态,因为`2000 ÷ 8 = 250`
- 响应中的250个`00`表示线圈0~1999全部为OFF
- `F5 AF`:全零响应对应的CRC低字节、CRC高字节
- 测试目的:检查最大线圈读取数量以及255字节响应帧的DMA发送

请求帧长度:8字节

请求帧:

```text
01 01 00 00 07 D0 3F A6
```

预期响应帧长度:255字节

预期响应帧:

```text
01 01 FA 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F5 AF
```

## 0x10写入123个保持寄存器

含义:

- `01`:从站地址
- `10`:写多个保持寄存器功能码
- `17 70`:起始地址6000,即D6000
- `00 7B`:写入数量123
- `F6`:后续寄存器数据长度246字节
- 连续246个`00`:将D6000~D6122全部写为0
- `76 06`:CRC低字节、CRC高字节
- 正常响应回显起始地址`17 70`和写入数量`00 7B`
- 测试目的:用255字节合法请求帧检查DMA接收缓冲区边界和最大寄存器写入数量

请求帧长度:255字节

请求帧:

```text
01 10 17 70 00 7B F6 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 76 06
```

预期响应帧长度:8字节

预期响应帧:

```text
01 10 17 70 00 7B 84 45
```

## 0x03读取125个保持寄存器

含义:

- `01`:从站地址
- `03`:读保持寄存器功能码
- `17 70`:起始地址6000,即D6000
- `00 7D`:读取数量125
- `81 84`:CRC低字节、CRC高字节
- 响应中的`FA`表示后续包含250字节寄存器数据,因为`125 × 2 = 250`
- 响应中的250个`00`表示D6000~D6124全部为0
- `08 E8`:全零响应对应的CRC低字节、CRC高字节
- 测试目的:检查最大保持寄存器读取数量以及255字节响应帧的DMA发送

请求帧长度:8字节

请求帧:

```text
01 03 17 70 00 7D 81 84
```

预期响应帧长度:255字节

预期响应帧:

```text
01 03 FA 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 E8
```

## 检查项

- 0x0F和0x10请求的`ModbusRxFrameLength`应为255
- 0x01和0x03正常响应的`responseLength`应为255
- `droppedFrameCount`不应增加
- `uartErrorCount`不应增加
- 不应发生HardFault、DMA越界或接收停止
- 完成255字节帧后,下一帧仍应正常通信

+ 0
- 81
Document/my_document/Modbus异常码实例.md View File

@@ -1,81 +0,0 @@
# Modbus异常码实例

## 非法功能码

- 请求:`01 02 00 00 00 01 B9 CA`
- 请求帧长度:8字节
- 返回帧:`01 82 01 81 60`
- 返回帧长度:5字节
- 解释:从站未实现功能码`0x02`,返回功能码`0x82`和异常码`0x01`,表示非法功能。

## 03功能码读取非法地址

- 请求:`01 03 27 10 00 01 8F 7B`
- 请求帧长度:8字节
- 返回帧:`01 83 02 C0 F1`
- 返回帧长度:5字节
- 解释:起始地址`0x2710`等于10000,超出标准保持寄存器有效地址`0~9999`,返回异常码`0x02`,表示非法数据地址。

## 03功能码读取数量为0

- 请求:`01 03 00 00 00 00 45 CA`
- 请求帧长度:8字节
- 返回帧:`01 83 03 01 31`
- 返回帧长度:5字节
- 解释:读取寄存器数量为0,不符合功能码要求,返回异常码`0x03`,表示非法数据值。

## 05功能码写入非法线圈值

- 请求:`01 05 00 00 12 34 C0 BD`
- 请求帧长度:8字节
- 返回帧:`01 85 03 02 91`
- 返回帧长度:5字节
- 解释:05功能码只允许使用`FF 00`表示ON或`00 00`表示OFF,写入值`12 34`非法,返回异常码`0x03`。

## 06功能码写入非法地址

- 请求:`01 06 27 10 00 01 43 7B`
- 请求帧长度:8字节
- 返回帧:`01 86 02 C3 A1`
- 返回帧长度:5字节
- 解释:写入地址`0x2710`等于10000,超出标准保持寄存器有效地址`0~9999`,返回异常码`0x02`。

## 0F功能码字节数错误

- 请求:`01 0F 00 00 00 09 01 01 6E 95`
- 请求帧长度:10字节
- 返回帧:`01 8F 03 04 31`
- 返回帧长度:5字节
- 解释:请求写入9个线圈时应携带2个数据字节,但帧中的字节数字段为1,返回异常码`0x03`。

## 10功能码写入数量为0

- 请求:`01 10 00 00 00 00 00 09 50`
- 请求帧长度:9字节
- 返回帧:`01 90 03 0C 01`
- 返回帧长度:5字节
- 解释:写入寄存器数量为0,不符合功能码要求,返回异常码`0x03`。

## 48功能码读取非法大地址

- 请求:`01 48 01 11 6F 00 01 CD 69`
- 请求帧长度:9字节
- 返回帧:`01 C8 02 F6 01`
- 返回帧长度:5字节
- 解释:起始地址`0x01116F`等于69999,超出扩展保持寄存器有效地址`0~69998`,返回异常码`0x02`。

## 48功能码读取数量为0

- 请求:`01 48 00 00 00 00 00 04 48`
- 请求帧长度:9字节
- 返回帧:`01 C8 03 37 C1`
- 返回帧长度:5字节
- 解释:读取寄存器数量为0,不符合功能码要求,返回异常码`0x03`。

## 从站地址不匹配

- 请求:`02 03 00 00 00 01 84 39`
- 请求帧长度:8字节
- 返回帧:无
- 返回帧长度:0字节
- 解释:当前从站地址为`0x01`,请求帧的目标从站地址为`0x02`,因此当前从站直接忽略该帧,不返回正常响应或异常响应。

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

@@ -1,552 +0,0 @@
# 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 个线圈:

```
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 | 非法数据值 | 数量、字节数或数据值不合法 |

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

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

BIN
Document/my_document/~$需求规格书.docx View File


BIN
Document/my_document/框图/协议解析流程图.vsdx View File


BIN
Document/my_document/框图/应用模块流程图.vsdx View File


BIN
Document/my_document/框图/系统框图.vsdx View File


BIN
Document/my_document/测试大纲.docx View File


BIN
Document/my_document/测试报告.docx View File


BIN
Document/my_document/设计方案书.docx View File


BIN
Document/my_document/需求规格书.docx View File


BIN
HostComputer.zip View File


+ 38
- 0
HostComputer/ModbusTimingTester.spec View File

@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
['modbus_timing_tester.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='ModbusTimingTester',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

+ 38
- 0
HostComputer/ModbusTimingTester_3Baud.spec View File

@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
['modbus_timing_tester.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='ModbusTimingTester_3Baud',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

+ 37
- 0
HostComputer/README.md View File

@@ -0,0 +1,37 @@
# Modbus RTU T1.5/T3.5测试上位机

## 直接运行EXE

不需要安装Python,直接双击`dist/ModbusTimingTester_3Baud.exe`即可运行。

## 运行方法

1. 将USB转RS-485模块连接到电脑和STM32从站。
2. 将STM32串口设置为9600、19200或115200,采用8位数据、偶校验、1位停止位。
3. 双击`run.bat`。
4. 选择USB转RS-485模块对应的COM口和与STM32一致的波特率。
5. 输入从站地址、起始线圈地址和读取数量后执行测试。

软件自动生成0x01请求帧并添加Modbus CRC。数值输入支持十进制和`0x`开头的十六进制。

界面中输入的是期望的RS-485线路静默时间。程序以每次`write()`开始时刻为基准,将下一次`write()`安排在“当前开始时刻+1146us字符时间+目标静默时间”。这种方式会测量并自动扣除Python循环以及`write+flush`已经消耗的时间。例如输入1833us时,目标头到头时间为2979us,估算线路静默时间为“实测头到头时间-1146us”。

## 9600、8E1对应时间

- 一个字符约为1146us。
- T1.2约为1375us,理论上仍是一帧。
- T1.5约为1719us。
- T3.5约为4010us。
- 软件使用2865us测试T1.5和T3.5之间的无效帧。
- 软件使用5000us测试超过T3.5后的残帧。

## 预期结果

- 正常连续帧:收到正常0x01响应。
- T1.2:理论上收到正常0x01响应。
- T1.5~T3.5:从站丢弃无效帧,不响应。
- 超过T3.5:各字节成为残帧,不响应。

## 精度说明

程序采用高精度计数器按绝对时刻调度,并把可测量的Python循环、`write()`和`flush()`耗时纳入计算。但Windows内核、USB驱动和USB转RS-485芯片内部仍可能缓存或合并字节,界面中的线路静默时间仍是估算值。正式验证时应使用逻辑分析仪测量线路上的字节间隔。

BIN
HostComputer/__pycache__/modbus_timing_tester.cpython-39.pyc View File


+ 3202
- 0
HostComputer/build/ModbusTimingTester/Analysis-00.toc
File diff suppressed because it is too large
View File


BIN
HostComputer/build/ModbusTimingTester/ModbusTimingTester.pkg View File


+ 2795
- 0
HostComputer/build/ModbusTimingTester/PKG-00.toc
File diff suppressed because it is too large
View File


BIN
HostComputer/build/ModbusTimingTester/PYZ-00.pyz View File


+ 158
- 0
HostComputer/build/ModbusTimingTester/PYZ-00.toc View File

@@ -0,0 +1,158 @@
('F:\\Xinje_Modbus_IAR\\TrainCamp_yuwenhao_modbus\\HostComputer\\build\\ModbusTimingTester\\PYZ-00.pyz',
[('__future__', 'D:\\anaconda3\\lib\\__future__.py', 'PYMODULE'),
('_compat_pickle', 'D:\\anaconda3\\lib\\_compat_pickle.py', 'PYMODULE'),
('_compression', 'D:\\anaconda3\\lib\\_compression.py', 'PYMODULE'),
('_py_abc', 'D:\\anaconda3\\lib\\_py_abc.py', 'PYMODULE'),
('_pydecimal', 'D:\\anaconda3\\lib\\_pydecimal.py', 'PYMODULE'),
('_strptime', 'D:\\anaconda3\\lib\\_strptime.py', 'PYMODULE'),
('_threading_local', 'D:\\anaconda3\\lib\\_threading_local.py', 'PYMODULE'),
('argparse', 'D:\\anaconda3\\lib\\argparse.py', 'PYMODULE'),
('base64', 'D:\\anaconda3\\lib\\base64.py', 'PYMODULE'),
('bisect', 'D:\\anaconda3\\lib\\bisect.py', 'PYMODULE'),
('bz2', 'D:\\anaconda3\\lib\\bz2.py', 'PYMODULE'),
('calendar', 'D:\\anaconda3\\lib\\calendar.py', 'PYMODULE'),
('configparser', 'D:\\anaconda3\\lib\\configparser.py', 'PYMODULE'),
('contextlib', 'D:\\anaconda3\\lib\\contextlib.py', 'PYMODULE'),
('contextvars', 'D:\\anaconda3\\lib\\contextvars.py', 'PYMODULE'),
('copy', 'D:\\anaconda3\\lib\\copy.py', 'PYMODULE'),
('csv', 'D:\\anaconda3\\lib\\csv.py', 'PYMODULE'),
('ctypes', 'D:\\anaconda3\\lib\\ctypes\\__init__.py', 'PYMODULE'),
('ctypes._endian', 'D:\\anaconda3\\lib\\ctypes\\_endian.py', 'PYMODULE'),
('ctypes.wintypes', 'D:\\anaconda3\\lib\\ctypes\\wintypes.py', 'PYMODULE'),
('datetime', 'D:\\anaconda3\\lib\\datetime.py', 'PYMODULE'),
('decimal', 'D:\\anaconda3\\lib\\decimal.py', 'PYMODULE'),
('email', 'D:\\anaconda3\\lib\\email\\__init__.py', 'PYMODULE'),
('email._encoded_words',
'D:\\anaconda3\\lib\\email\\_encoded_words.py',
'PYMODULE'),
('email._header_value_parser',
'D:\\anaconda3\\lib\\email\\_header_value_parser.py',
'PYMODULE'),
('email._parseaddr', 'D:\\anaconda3\\lib\\email\\_parseaddr.py', 'PYMODULE'),
('email._policybase',
'D:\\anaconda3\\lib\\email\\_policybase.py',
'PYMODULE'),
('email.base64mime', 'D:\\anaconda3\\lib\\email\\base64mime.py', 'PYMODULE'),
('email.charset', 'D:\\anaconda3\\lib\\email\\charset.py', 'PYMODULE'),
('email.contentmanager',
'D:\\anaconda3\\lib\\email\\contentmanager.py',
'PYMODULE'),
('email.encoders', 'D:\\anaconda3\\lib\\email\\encoders.py', 'PYMODULE'),
('email.errors', 'D:\\anaconda3\\lib\\email\\errors.py', 'PYMODULE'),
('email.feedparser', 'D:\\anaconda3\\lib\\email\\feedparser.py', 'PYMODULE'),
('email.generator', 'D:\\anaconda3\\lib\\email\\generator.py', 'PYMODULE'),
('email.header', 'D:\\anaconda3\\lib\\email\\header.py', 'PYMODULE'),
('email.headerregistry',
'D:\\anaconda3\\lib\\email\\headerregistry.py',
'PYMODULE'),
('email.iterators', 'D:\\anaconda3\\lib\\email\\iterators.py', 'PYMODULE'),
('email.message', 'D:\\anaconda3\\lib\\email\\message.py', 'PYMODULE'),
('email.parser', 'D:\\anaconda3\\lib\\email\\parser.py', 'PYMODULE'),
('email.policy', 'D:\\anaconda3\\lib\\email\\policy.py', 'PYMODULE'),
('email.quoprimime', 'D:\\anaconda3\\lib\\email\\quoprimime.py', 'PYMODULE'),
('email.utils', 'D:\\anaconda3\\lib\\email\\utils.py', 'PYMODULE'),
('fnmatch', 'D:\\anaconda3\\lib\\fnmatch.py', 'PYMODULE'),
('fractions', 'D:\\anaconda3\\lib\\fractions.py', 'PYMODULE'),
('getopt', 'D:\\anaconda3\\lib\\getopt.py', 'PYMODULE'),
('gettext', 'D:\\anaconda3\\lib\\gettext.py', 'PYMODULE'),
('glob', 'D:\\anaconda3\\lib\\glob.py', 'PYMODULE'),
('gzip', 'D:\\anaconda3\\lib\\gzip.py', 'PYMODULE'),
('hashlib', 'D:\\anaconda3\\lib\\hashlib.py', 'PYMODULE'),
('importlib', 'D:\\anaconda3\\lib\\importlib\\__init__.py', 'PYMODULE'),
('importlib._bootstrap',
'D:\\anaconda3\\lib\\importlib\\_bootstrap.py',
'PYMODULE'),
('importlib._bootstrap_external',
'D:\\anaconda3\\lib\\importlib\\_bootstrap_external.py',
'PYMODULE'),
('importlib.abc', 'D:\\anaconda3\\lib\\importlib\\abc.py', 'PYMODULE'),
('importlib.machinery',
'D:\\anaconda3\\lib\\importlib\\machinery.py',
'PYMODULE'),
('importlib.metadata',
'D:\\anaconda3\\lib\\importlib\\metadata.py',
'PYMODULE'),
('importlib.util', 'D:\\anaconda3\\lib\\importlib\\util.py', 'PYMODULE'),
('logging', 'D:\\anaconda3\\lib\\logging\\__init__.py', 'PYMODULE'),
('lzma', 'D:\\anaconda3\\lib\\lzma.py', 'PYMODULE'),
('numbers', 'D:\\anaconda3\\lib\\numbers.py', 'PYMODULE'),
('optparse', 'D:\\anaconda3\\lib\\optparse.py', 'PYMODULE'),
('pathlib', 'D:\\anaconda3\\lib\\pathlib.py', 'PYMODULE'),
('pickle', 'D:\\anaconda3\\lib\\pickle.py', 'PYMODULE'),
('pprint', 'D:\\anaconda3\\lib\\pprint.py', 'PYMODULE'),
('py_compile', 'D:\\anaconda3\\lib\\py_compile.py', 'PYMODULE'),
('queue', 'D:\\anaconda3\\lib\\queue.py', 'PYMODULE'),
('quopri', 'D:\\anaconda3\\lib\\quopri.py', 'PYMODULE'),
('random', 'D:\\anaconda3\\lib\\random.py', 'PYMODULE'),
('selectors', 'D:\\anaconda3\\lib\\selectors.py', 'PYMODULE'),
('serial',
'D:\\anaconda3\\lib\\site-packages\\serial\\__init__.py',
'PYMODULE'),
('serial.serialcli',
'D:\\anaconda3\\lib\\site-packages\\serial\\serialcli.py',
'PYMODULE'),
('serial.serialjava',
'D:\\anaconda3\\lib\\site-packages\\serial\\serialjava.py',
'PYMODULE'),
('serial.serialposix',
'D:\\anaconda3\\lib\\site-packages\\serial\\serialposix.py',
'PYMODULE'),
('serial.serialutil',
'D:\\anaconda3\\lib\\site-packages\\serial\\serialutil.py',
'PYMODULE'),
('serial.serialwin32',
'D:\\anaconda3\\lib\\site-packages\\serial\\serialwin32.py',
'PYMODULE'),
('serial.tools',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\__init__.py',
'PYMODULE'),
('serial.tools.list_ports',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports.py',
'PYMODULE'),
('serial.tools.list_ports_common',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports_common.py',
'PYMODULE'),
('serial.tools.list_ports_linux',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports_linux.py',
'PYMODULE'),
('serial.tools.list_ports_osx',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports_osx.py',
'PYMODULE'),
('serial.tools.list_ports_posix',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports_posix.py',
'PYMODULE'),
('serial.tools.list_ports_windows',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports_windows.py',
'PYMODULE'),
('serial.win32',
'D:\\anaconda3\\lib\\site-packages\\serial\\win32.py',
'PYMODULE'),
('shutil', 'D:\\anaconda3\\lib\\shutil.py', 'PYMODULE'),
('signal', 'D:\\anaconda3\\lib\\signal.py', 'PYMODULE'),
('socket', 'D:\\anaconda3\\lib\\socket.py', 'PYMODULE'),
('statistics', 'D:\\anaconda3\\lib\\statistics.py', 'PYMODULE'),
('string', 'D:\\anaconda3\\lib\\string.py', 'PYMODULE'),
('stringprep', 'D:\\anaconda3\\lib\\stringprep.py', 'PYMODULE'),
('subprocess', 'D:\\anaconda3\\lib\\subprocess.py', 'PYMODULE'),
('tarfile', 'D:\\anaconda3\\lib\\tarfile.py', 'PYMODULE'),
('textwrap', 'D:\\anaconda3\\lib\\textwrap.py', 'PYMODULE'),
('threading', 'D:\\anaconda3\\lib\\threading.py', 'PYMODULE'),
('tkinter', 'D:\\anaconda3\\lib\\tkinter\\__init__.py', 'PYMODULE'),
('tkinter.commondialog',
'D:\\anaconda3\\lib\\tkinter\\commondialog.py',
'PYMODULE'),
('tkinter.constants',
'D:\\anaconda3\\lib\\tkinter\\constants.py',
'PYMODULE'),
('tkinter.messagebox',
'D:\\anaconda3\\lib\\tkinter\\messagebox.py',
'PYMODULE'),
('tkinter.ttk', 'D:\\anaconda3\\lib\\tkinter\\ttk.py', 'PYMODULE'),
('token', 'D:\\anaconda3\\lib\\token.py', 'PYMODULE'),
('tokenize', 'D:\\anaconda3\\lib\\tokenize.py', 'PYMODULE'),
('tracemalloc', 'D:\\anaconda3\\lib\\tracemalloc.py', 'PYMODULE'),
('typing', 'D:\\anaconda3\\lib\\typing.py', 'PYMODULE'),
('urllib', 'D:\\anaconda3\\lib\\urllib\\__init__.py', 'PYMODULE'),
('urllib.parse', 'D:\\anaconda3\\lib\\urllib\\parse.py', 'PYMODULE'),
('uu', 'D:\\anaconda3\\lib\\uu.py', 'PYMODULE'),
('zipfile', 'D:\\anaconda3\\lib\\zipfile.py', 'PYMODULE')])

BIN
HostComputer/build/ModbusTimingTester/base_library.zip View File


BIN
HostComputer/build/ModbusTimingTester/localpycs/pyimod01_archive.pyc View File


BIN
HostComputer/build/ModbusTimingTester/localpycs/pyimod02_importers.pyc View File


BIN
HostComputer/build/ModbusTimingTester/localpycs/pyimod03_ctypes.pyc View File


BIN
HostComputer/build/ModbusTimingTester/localpycs/pyimod04_pywin32.pyc View File


BIN
HostComputer/build/ModbusTimingTester/localpycs/struct.pyc View File


+ 30
- 0
HostComputer/build/ModbusTimingTester/warn-ModbusTimingTester.txt View File

@@ -0,0 +1,30 @@

This file lists modules PyInstaller was not able to find. This does not
necessarily mean these modules are required for running your program. Both
Python's standard library and 3rd-party Python packages often conditionally
import optional modules, some of which may be available only on certain
platforms.

Types of import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement

IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!

missing module named pwd - imported by posixpath (delayed, conditional), subprocess (optional), shutil (optional), tarfile (optional), pathlib (delayed, conditional, optional)
missing module named 'org.python' - imported by copy (optional)
missing module named _posixsubprocess - imported by subprocess (optional)
missing module named grp - imported by subprocess (optional), shutil (optional), tarfile (optional), pathlib (delayed, optional)
missing module named posix - imported by os (conditional, optional), shutil (conditional), importlib._bootstrap_external (conditional)
missing module named resource - imported by posix (top-level)
missing module named org - imported by pickle (optional)
missing module named termios - imported by serial.serialposix (top-level)
missing module named fcntl - imported by serial.serialposix (top-level)
missing module named 'System.IO' - imported by serial.serialcli (top-level)
missing module named System - imported by serial.serialcli (top-level)
missing module named pep517 - imported by importlib.metadata (delayed)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional)

+ 7159
- 0
HostComputer/build/ModbusTimingTester/xref-ModbusTimingTester.html
File diff suppressed because it is too large
View File


+ 3202
- 0
HostComputer/build/ModbusTimingTester_3Baud/Analysis-00.toc
File diff suppressed because it is too large
View File


+ 2817
- 0
HostComputer/build/ModbusTimingTester_3Baud/EXE-00.toc
File diff suppressed because it is too large
View File


BIN
HostComputer/build/ModbusTimingTester_3Baud/ModbusTimingTester_3Baud.pkg View File


+ 2795
- 0
HostComputer/build/ModbusTimingTester_3Baud/PKG-00.toc
File diff suppressed because it is too large
View File


BIN
HostComputer/build/ModbusTimingTester_3Baud/PYZ-00.pyz View File


+ 158
- 0
HostComputer/build/ModbusTimingTester_3Baud/PYZ-00.toc View File

@@ -0,0 +1,158 @@
('F:\\Xinje_Modbus_IAR\\TrainCamp_yuwenhao_modbus\\HostComputer\\build\\ModbusTimingTester_3Baud\\PYZ-00.pyz',
[('__future__', 'D:\\anaconda3\\lib\\__future__.py', 'PYMODULE'),
('_compat_pickle', 'D:\\anaconda3\\lib\\_compat_pickle.py', 'PYMODULE'),
('_compression', 'D:\\anaconda3\\lib\\_compression.py', 'PYMODULE'),
('_py_abc', 'D:\\anaconda3\\lib\\_py_abc.py', 'PYMODULE'),
('_pydecimal', 'D:\\anaconda3\\lib\\_pydecimal.py', 'PYMODULE'),
('_strptime', 'D:\\anaconda3\\lib\\_strptime.py', 'PYMODULE'),
('_threading_local', 'D:\\anaconda3\\lib\\_threading_local.py', 'PYMODULE'),
('argparse', 'D:\\anaconda3\\lib\\argparse.py', 'PYMODULE'),
('base64', 'D:\\anaconda3\\lib\\base64.py', 'PYMODULE'),
('bisect', 'D:\\anaconda3\\lib\\bisect.py', 'PYMODULE'),
('bz2', 'D:\\anaconda3\\lib\\bz2.py', 'PYMODULE'),
('calendar', 'D:\\anaconda3\\lib\\calendar.py', 'PYMODULE'),
('configparser', 'D:\\anaconda3\\lib\\configparser.py', 'PYMODULE'),
('contextlib', 'D:\\anaconda3\\lib\\contextlib.py', 'PYMODULE'),
('contextvars', 'D:\\anaconda3\\lib\\contextvars.py', 'PYMODULE'),
('copy', 'D:\\anaconda3\\lib\\copy.py', 'PYMODULE'),
('csv', 'D:\\anaconda3\\lib\\csv.py', 'PYMODULE'),
('ctypes', 'D:\\anaconda3\\lib\\ctypes\\__init__.py', 'PYMODULE'),
('ctypes._endian', 'D:\\anaconda3\\lib\\ctypes\\_endian.py', 'PYMODULE'),
('ctypes.wintypes', 'D:\\anaconda3\\lib\\ctypes\\wintypes.py', 'PYMODULE'),
('datetime', 'D:\\anaconda3\\lib\\datetime.py', 'PYMODULE'),
('decimal', 'D:\\anaconda3\\lib\\decimal.py', 'PYMODULE'),
('email', 'D:\\anaconda3\\lib\\email\\__init__.py', 'PYMODULE'),
('email._encoded_words',
'D:\\anaconda3\\lib\\email\\_encoded_words.py',
'PYMODULE'),
('email._header_value_parser',
'D:\\anaconda3\\lib\\email\\_header_value_parser.py',
'PYMODULE'),
('email._parseaddr', 'D:\\anaconda3\\lib\\email\\_parseaddr.py', 'PYMODULE'),
('email._policybase',
'D:\\anaconda3\\lib\\email\\_policybase.py',
'PYMODULE'),
('email.base64mime', 'D:\\anaconda3\\lib\\email\\base64mime.py', 'PYMODULE'),
('email.charset', 'D:\\anaconda3\\lib\\email\\charset.py', 'PYMODULE'),
('email.contentmanager',
'D:\\anaconda3\\lib\\email\\contentmanager.py',
'PYMODULE'),
('email.encoders', 'D:\\anaconda3\\lib\\email\\encoders.py', 'PYMODULE'),
('email.errors', 'D:\\anaconda3\\lib\\email\\errors.py', 'PYMODULE'),
('email.feedparser', 'D:\\anaconda3\\lib\\email\\feedparser.py', 'PYMODULE'),
('email.generator', 'D:\\anaconda3\\lib\\email\\generator.py', 'PYMODULE'),
('email.header', 'D:\\anaconda3\\lib\\email\\header.py', 'PYMODULE'),
('email.headerregistry',
'D:\\anaconda3\\lib\\email\\headerregistry.py',
'PYMODULE'),
('email.iterators', 'D:\\anaconda3\\lib\\email\\iterators.py', 'PYMODULE'),
('email.message', 'D:\\anaconda3\\lib\\email\\message.py', 'PYMODULE'),
('email.parser', 'D:\\anaconda3\\lib\\email\\parser.py', 'PYMODULE'),
('email.policy', 'D:\\anaconda3\\lib\\email\\policy.py', 'PYMODULE'),
('email.quoprimime', 'D:\\anaconda3\\lib\\email\\quoprimime.py', 'PYMODULE'),
('email.utils', 'D:\\anaconda3\\lib\\email\\utils.py', 'PYMODULE'),
('fnmatch', 'D:\\anaconda3\\lib\\fnmatch.py', 'PYMODULE'),
('fractions', 'D:\\anaconda3\\lib\\fractions.py', 'PYMODULE'),
('getopt', 'D:\\anaconda3\\lib\\getopt.py', 'PYMODULE'),
('gettext', 'D:\\anaconda3\\lib\\gettext.py', 'PYMODULE'),
('glob', 'D:\\anaconda3\\lib\\glob.py', 'PYMODULE'),
('gzip', 'D:\\anaconda3\\lib\\gzip.py', 'PYMODULE'),
('hashlib', 'D:\\anaconda3\\lib\\hashlib.py', 'PYMODULE'),
('importlib', 'D:\\anaconda3\\lib\\importlib\\__init__.py', 'PYMODULE'),
('importlib._bootstrap',
'D:\\anaconda3\\lib\\importlib\\_bootstrap.py',
'PYMODULE'),
('importlib._bootstrap_external',
'D:\\anaconda3\\lib\\importlib\\_bootstrap_external.py',
'PYMODULE'),
('importlib.abc', 'D:\\anaconda3\\lib\\importlib\\abc.py', 'PYMODULE'),
('importlib.machinery',
'D:\\anaconda3\\lib\\importlib\\machinery.py',
'PYMODULE'),
('importlib.metadata',
'D:\\anaconda3\\lib\\importlib\\metadata.py',
'PYMODULE'),
('importlib.util', 'D:\\anaconda3\\lib\\importlib\\util.py', 'PYMODULE'),
('logging', 'D:\\anaconda3\\lib\\logging\\__init__.py', 'PYMODULE'),
('lzma', 'D:\\anaconda3\\lib\\lzma.py', 'PYMODULE'),
('numbers', 'D:\\anaconda3\\lib\\numbers.py', 'PYMODULE'),
('optparse', 'D:\\anaconda3\\lib\\optparse.py', 'PYMODULE'),
('pathlib', 'D:\\anaconda3\\lib\\pathlib.py', 'PYMODULE'),
('pickle', 'D:\\anaconda3\\lib\\pickle.py', 'PYMODULE'),
('pprint', 'D:\\anaconda3\\lib\\pprint.py', 'PYMODULE'),
('py_compile', 'D:\\anaconda3\\lib\\py_compile.py', 'PYMODULE'),
('queue', 'D:\\anaconda3\\lib\\queue.py', 'PYMODULE'),
('quopri', 'D:\\anaconda3\\lib\\quopri.py', 'PYMODULE'),
('random', 'D:\\anaconda3\\lib\\random.py', 'PYMODULE'),
('selectors', 'D:\\anaconda3\\lib\\selectors.py', 'PYMODULE'),
('serial',
'D:\\anaconda3\\lib\\site-packages\\serial\\__init__.py',
'PYMODULE'),
('serial.serialcli',
'D:\\anaconda3\\lib\\site-packages\\serial\\serialcli.py',
'PYMODULE'),
('serial.serialjava',
'D:\\anaconda3\\lib\\site-packages\\serial\\serialjava.py',
'PYMODULE'),
('serial.serialposix',
'D:\\anaconda3\\lib\\site-packages\\serial\\serialposix.py',
'PYMODULE'),
('serial.serialutil',
'D:\\anaconda3\\lib\\site-packages\\serial\\serialutil.py',
'PYMODULE'),
('serial.serialwin32',
'D:\\anaconda3\\lib\\site-packages\\serial\\serialwin32.py',
'PYMODULE'),
('serial.tools',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\__init__.py',
'PYMODULE'),
('serial.tools.list_ports',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports.py',
'PYMODULE'),
('serial.tools.list_ports_common',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports_common.py',
'PYMODULE'),
('serial.tools.list_ports_linux',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports_linux.py',
'PYMODULE'),
('serial.tools.list_ports_osx',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports_osx.py',
'PYMODULE'),
('serial.tools.list_ports_posix',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports_posix.py',
'PYMODULE'),
('serial.tools.list_ports_windows',
'D:\\anaconda3\\lib\\site-packages\\serial\\tools\\list_ports_windows.py',
'PYMODULE'),
('serial.win32',
'D:\\anaconda3\\lib\\site-packages\\serial\\win32.py',
'PYMODULE'),
('shutil', 'D:\\anaconda3\\lib\\shutil.py', 'PYMODULE'),
('signal', 'D:\\anaconda3\\lib\\signal.py', 'PYMODULE'),
('socket', 'D:\\anaconda3\\lib\\socket.py', 'PYMODULE'),
('statistics', 'D:\\anaconda3\\lib\\statistics.py', 'PYMODULE'),
('string', 'D:\\anaconda3\\lib\\string.py', 'PYMODULE'),
('stringprep', 'D:\\anaconda3\\lib\\stringprep.py', 'PYMODULE'),
('subprocess', 'D:\\anaconda3\\lib\\subprocess.py', 'PYMODULE'),
('tarfile', 'D:\\anaconda3\\lib\\tarfile.py', 'PYMODULE'),
('textwrap', 'D:\\anaconda3\\lib\\textwrap.py', 'PYMODULE'),
('threading', 'D:\\anaconda3\\lib\\threading.py', 'PYMODULE'),
('tkinter', 'D:\\anaconda3\\lib\\tkinter\\__init__.py', 'PYMODULE'),
('tkinter.commondialog',
'D:\\anaconda3\\lib\\tkinter\\commondialog.py',
'PYMODULE'),
('tkinter.constants',
'D:\\anaconda3\\lib\\tkinter\\constants.py',
'PYMODULE'),
('tkinter.messagebox',
'D:\\anaconda3\\lib\\tkinter\\messagebox.py',
'PYMODULE'),
('tkinter.ttk', 'D:\\anaconda3\\lib\\tkinter\\ttk.py', 'PYMODULE'),
('token', 'D:\\anaconda3\\lib\\token.py', 'PYMODULE'),
('tokenize', 'D:\\anaconda3\\lib\\tokenize.py', 'PYMODULE'),
('tracemalloc', 'D:\\anaconda3\\lib\\tracemalloc.py', 'PYMODULE'),
('typing', 'D:\\anaconda3\\lib\\typing.py', 'PYMODULE'),
('urllib', 'D:\\anaconda3\\lib\\urllib\\__init__.py', 'PYMODULE'),
('urllib.parse', 'D:\\anaconda3\\lib\\urllib\\parse.py', 'PYMODULE'),
('uu', 'D:\\anaconda3\\lib\\uu.py', 'PYMODULE'),
('zipfile', 'D:\\anaconda3\\lib\\zipfile.py', 'PYMODULE')])

BIN
HostComputer/build/ModbusTimingTester_3Baud/base_library.zip View File


BIN
HostComputer/build/ModbusTimingTester_3Baud/localpycs/pyimod01_archive.pyc View File


BIN
HostComputer/build/ModbusTimingTester_3Baud/localpycs/pyimod02_importers.pyc View File


BIN
HostComputer/build/ModbusTimingTester_3Baud/localpycs/pyimod03_ctypes.pyc View File


BIN
HostComputer/build/ModbusTimingTester_3Baud/localpycs/pyimod04_pywin32.pyc View File


BIN
HostComputer/build/ModbusTimingTester_3Baud/localpycs/struct.pyc View File


+ 30
- 0
HostComputer/build/ModbusTimingTester_3Baud/warn-ModbusTimingTester_3Baud.txt View File

@@ -0,0 +1,30 @@

This file lists modules PyInstaller was not able to find. This does not
necessarily mean these modules are required for running your program. Both
Python's standard library and 3rd-party Python packages often conditionally
import optional modules, some of which may be available only on certain
platforms.

Types of import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement

IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!

missing module named pwd - imported by posixpath (delayed, conditional), shutil (optional), tarfile (optional), pathlib (delayed, conditional, optional), subprocess (optional)
missing module named org - imported by copy (optional)
missing module named _posixsubprocess - imported by subprocess (optional)
missing module named grp - imported by shutil (optional), tarfile (optional), pathlib (delayed, optional), subprocess (optional)
missing module named posix - imported by os (conditional, optional), shutil (conditional), importlib._bootstrap_external (conditional)
missing module named resource - imported by posix (top-level)
missing module named 'org.python' - imported by pickle (optional)
missing module named termios - imported by serial.serialposix (top-level)
missing module named fcntl - imported by serial.serialposix (top-level)
missing module named 'System.IO' - imported by serial.serialcli (top-level)
missing module named System - imported by serial.serialcli (top-level)
missing module named pep517 - imported by importlib.metadata (delayed)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional)

+ 7159
- 0
HostComputer/build/ModbusTimingTester_3Baud/xref-ModbusTimingTester_3Baud.html
File diff suppressed because it is too large
View File


BIN
HostComputer/dist/ModbusTimingTester.exe View File


BIN
HostComputer/dist/ModbusTimingTester_3Baud.exe View File


+ 491
- 0
HostComputer/modbus_timing_tester.py View File

@@ -0,0 +1,491 @@
"""Modbus RTU T1.5/T3.5 test tool for the STM32 slave project."""

import argparse
import queue
import threading
import time
import tkinter as tk
from tkinter import messagebox, ttk

import serial
from serial.tools import list_ports


BAUD_RATES = (9600, 19200, 115200)
BITS_PER_CHARACTER = 11
NEW_FRAME_GAP_US = 5000


def calculate_timing(baud_rate):
"""Return character, T1.2, T1.5 and T3.5 times in microseconds."""
character_us = round(BITS_PER_CHARACTER * 1_000_000 / baud_rate)
t12_us = round(character_us * 1.2)

if baud_rate > 19200:
t15_us = 750
t35_us = 1750
else:
t15_us = round(character_us * 1.5)
t35_us = round(character_us * 3.5)

invalid_gap_us = round((t15_us + t35_us) / 2)
return character_us, t12_us, t15_us, t35_us, invalid_gap_us


def modbus_crc(data):
"""Return the Modbus RTU CRC-16 value."""
crc = 0xFFFF

for value in data:
crc ^= value

for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1

return crc


def build_read_coils_request(slave, address, quantity):
"""Build an 0x01 Read Coils RTU request."""
if not 1 <= slave <= 247:
raise ValueError("从站地址必须为1~247")
if not 0 <= address <= 0xFFFF:
raise ValueError("线圈地址必须为0~65535")
if not 1 <= quantity <= 2000:
raise ValueError("线圈数量必须为1~2000")
if address + quantity > 0x10000:
raise ValueError("地址与数量之和超过65536")

frame = bytearray(
(
slave,
0x01,
address >> 8,
address & 0xFF,
quantity >> 8,
quantity & 0xFF,
)
)
crc = modbus_crc(frame)
frame.extend((crc & 0xFF, crc >> 8))
return bytes(frame)


def send_request(
port_name, frame, gap_us, timeout_ms, baud_rate, character_us
):
"""Send one RTU request and return received bytes and host timing data."""
write_starts = []
write_costs = []

with serial.Serial(
port=port_name,
baudrate=baud_rate,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
timeout=0,
write_timeout=1,
) as port:
port.reset_input_buffer()
port.reset_output_buffer()

if gap_us is None:
port.write(frame)
port.flush()
else:
for index, value in enumerate(frame):
write_start = time.perf_counter_ns()
write_starts.append(write_start)
port.write(bytes((value,)))
port.flush()
write_costs.append(
(time.perf_counter_ns() - write_start) / 1000.0
)

if index + 1 < len(frame):
# 下一次write的目标时刻=本次write开始时刻+一个字符时间
# +目标静默时间。这样write、flush和循环开销都会被自动
# 计入并从剩余等待时间中扣除。
next_write_deadline = write_start + (
gap_us + character_us
) * 1000

while time.perf_counter_ns() < next_write_deadline:
pass

deadline = time.perf_counter() + timeout_ms / 1000.0
response = bytearray()

while time.perf_counter() < deadline:
waiting = port.in_waiting

if waiting:
response.extend(port.read(waiting))

time.sleep(0.001)

waiting = port.in_waiting
if waiting:
response.extend(port.read(waiting))

head_to_head = [
(write_starts[index] - write_starts[index - 1]) / 1000.0
for index in range(1, len(write_starts))
]
estimated_silence = [
interval - character_us for interval in head_to_head
]
timing = {
"baud_rate": baud_rate,
"character_us": character_us,
"write_costs": write_costs,
"head_to_head": head_to_head,
"estimated_silence": estimated_silence,
}
return bytes(response), timing


def describe_response(response, slave):
"""Perform basic RTU response checks and return a readable result."""
if not response:
return "未收到响应"
if len(response) < 5:
return "响应长度不足5字节"

received_crc = response[-2] | (response[-1] << 8)
calculated_crc = modbus_crc(response[:-2])
if received_crc != calculated_crc:
return "响应CRC错误"
if response[0] != slave:
return "响应从站地址不匹配"
if response[1] == 0x81:
return f"从站返回异常码0x{response[2]:02X}"
if response[1] != 0x01:
return f"响应功能码错误:0x{response[1]:02X}"

return "01响应格式及CRC正确"


class TimingTester:
"""Tkinter user interface for the timing tests."""

def __init__(self, root):
self.root = root
self.root.title("Modbus RTU T1.5/T3.5测试工具")
self.root.resizable(False, False)
self.result_queue = queue.Queue()

self.port_var = tk.StringVar()
self.baud_var = tk.StringVar(value="9600")
self.slave_var = tk.StringVar(value="1")
self.address_var = tk.StringVar(value="0")
self.quantity_var = tk.StringVar(value="1")
self.timeout_var = tk.StringVar(value="300")
self.custom_gap_var = tk.StringVar()
self.status_var = tk.StringVar(value="就绪")
self._update_timing_values()
self.custom_gap_var.set(str(self.t12_us))

self._build_ui()
self.refresh_ports()
self.root.after(50, self._poll_result)

def _build_ui(self):
settings = ttk.LabelFrame(self.root, text="通信参数(8E1)")
settings.grid(row=0, column=0, padx=10, pady=8, sticky="ew")

ttk.Label(settings, text="串口").grid(row=0, column=0, padx=5, pady=5)
self.port_box = ttk.Combobox(
settings, textvariable=self.port_var, width=14, state="readonly"
)
self.port_box.grid(row=0, column=1, padx=5, pady=5)
ttk.Button(settings, text="刷新", command=self.refresh_ports).grid(
row=0, column=2, padx=5, pady=5
)

ttk.Label(settings, text="波特率").grid(row=1, column=0, padx=5, pady=5)
self.baud_box = ttk.Combobox(
settings,
textvariable=self.baud_var,
values=[str(value) for value in BAUD_RATES],
width=14,
state="readonly",
)
self.baud_box.grid(row=1, column=1, padx=5, pady=5)
self.baud_box.bind("<<ComboboxSelected>>", self._on_baud_changed)

ttk.Label(settings, text="从站地址").grid(row=2, column=0, padx=5, pady=5)
ttk.Entry(settings, textvariable=self.slave_var, width=16).grid(
row=2, column=1, padx=5, pady=5
)
ttk.Label(settings, text="起始线圈地址").grid(
row=3, column=0, padx=5, pady=5
)
ttk.Entry(settings, textvariable=self.address_var, width=16).grid(
row=3, column=1, padx=5, pady=5
)
ttk.Label(settings, text="线圈数量").grid(row=4, column=0, padx=5, pady=5)
ttk.Entry(settings, textvariable=self.quantity_var, width=16).grid(
row=4, column=1, padx=5, pady=5
)
ttk.Label(settings, text="接收等待/ms").grid(row=5, column=0, padx=5, pady=5)
ttk.Entry(settings, textvariable=self.timeout_var, width=16).grid(
row=5, column=1, padx=5, pady=5
)

timing = ttk.LabelFrame(self.root, text="定时测试")
timing.grid(row=1, column=0, padx=10, pady=2, sticky="ew")

self.timing_info = ttk.Label(timing, text=self._timing_info_text())
self.timing_info.grid(
row=0, column=0, columnspan=3, padx=5, pady=6
)

ttk.Button(
timing,
text="正常连续帧",
command=lambda: self.start_test("正常连续帧", None),
width=20,
).grid(row=1, column=0, padx=5, pady=5)
self.t12_button = ttk.Button(
timing,
text=f"T1.2({self.t12_us}us)",
command=lambda: self.start_test("T1.2有效帧", self.t12_us),
width=20,
)
self.t12_button.grid(row=1, column=1, padx=5, pady=5)
self.invalid_button = ttk.Button(
timing,
text=f"T1.5~T3.5({self.invalid_gap_us}us)",
command=lambda: self.start_test(
"T1.5至T3.5无效帧", self.invalid_gap_us
),
width=20,
)
self.invalid_button.grid(row=2, column=0, padx=5, pady=5)
ttk.Button(
timing,
text=f">T3.5({NEW_FRAME_GAP_US}us)",
command=lambda: self.start_test(
"超过T3.5的残帧", NEW_FRAME_GAP_US
),
width=20,
).grid(row=2, column=1, padx=5, pady=5)

ttk.Label(timing, text="自定义字节间隔/us").grid(
row=3, column=0, padx=5, pady=5
)
ttk.Entry(timing, textvariable=self.custom_gap_var, width=12).grid(
row=3, column=1, padx=5, pady=5, sticky="w"
)
ttk.Button(timing, text="自定义测试", command=self.start_custom_test).grid(
row=3, column=2, padx=5, pady=5
)

output = ttk.LabelFrame(self.root, text="测试记录")
output.grid(row=2, column=0, padx=10, pady=8, sticky="ew")
self.log = tk.Text(output, width=76, height=16, state="disabled")
self.log.grid(row=0, column=0, padx=5, pady=5)
ttk.Label(self.root, textvariable=self.status_var).grid(
row=3, column=0, padx=10, pady=(0, 8), sticky="w"
)

def _update_timing_values(self):
baud_rate = int(self.baud_var.get())
(
self.character_us,
self.t12_us,
self.t15_us,
self.t35_us,
self.invalid_gap_us,
) = calculate_timing(baud_rate)

def _timing_info_text(self):
return (
f"1字符={self.character_us}us,T1.5={self.t15_us}us,"
f"T3.5={self.t35_us}us"
)

def _on_baud_changed(self, _event=None):
self._update_timing_values()
self.custom_gap_var.set(str(self.t12_us))
self.timing_info.configure(text=self._timing_info_text())
self.t12_button.configure(text=f"T1.2({self.t12_us}us)")
self.invalid_button.configure(
text=f"T1.5~T3.5({self.invalid_gap_us}us)"
)

def refresh_ports(self):
ports = [item.device for item in list_ports.comports()]
self.port_box["values"] = ports

if ports and self.port_var.get() not in ports:
self.port_var.set(ports[0])
elif not ports:
self.port_var.set("")

@staticmethod
def _parse_integer(value, name):
try:
return int(value.strip(), 0)
except ValueError as error:
raise ValueError(f"{name}不是有效整数") from error

def start_custom_test(self):
try:
gap_us = self._parse_integer(self.custom_gap_var.get(), "自定义间隔")
if gap_us < 0:
raise ValueError("自定义间隔不能小于0")
except ValueError as error:
messagebox.showerror("参数错误", str(error))
return

self.start_test("自定义间隔", gap_us)

def start_test(self, test_name, gap_us):
try:
port_name = self.port_var.get()
if not port_name:
raise ValueError("没有检测到串口,请连接USB转RS-485设备")

slave = self._parse_integer(self.slave_var.get(), "从站地址")
address = self._parse_integer(self.address_var.get(), "线圈地址")
quantity = self._parse_integer(self.quantity_var.get(), "线圈数量")
timeout_ms = self._parse_integer(self.timeout_var.get(), "接收等待时间")
baud_rate = self._parse_integer(self.baud_var.get(), "波特率")
if not 10 <= timeout_ms <= 10000:
raise ValueError("接收等待时间必须为10~10000ms")

frame = build_read_coils_request(slave, address, quantity)
except ValueError as error:
messagebox.showerror("参数错误", str(error))
return

self.status_var.set(f"正在执行:{test_name}")
worker = threading.Thread(
target=self._run_test,
args=(
test_name,
port_name,
slave,
frame,
gap_us,
timeout_ms,
baud_rate,
self.character_us,
),
daemon=True,
)
worker.start()

def _run_test(
self,
test_name,
port_name,
slave,
frame,
gap_us,
timeout_ms,
baud_rate,
character_us,
):
try:
response, timing = send_request(
port_name,
frame,
gap_us,
timeout_ms,
baud_rate,
character_us,
)
result = describe_response(response, slave)
self.result_queue.put(
(True, test_name, frame, response, gap_us, timing, result)
)
except (OSError, serial.SerialException) as error:
self.result_queue.put((False, str(error)))

def _poll_result(self):
try:
result = self.result_queue.get_nowait()
except queue.Empty:
self.root.after(50, self._poll_result)
return

if result[0]:
_, name, request, response, gap_us, timing, description = result
gap_text = "连续发送" if gap_us is None else f"设定{gap_us}us"
if timing["head_to_head"]:
costs = timing["write_costs"]
intervals = timing["head_to_head"]
silence = timing["estimated_silence"]
character_us = timing["character_us"]
gap_text += (
f"\nwrite+flush耗时{min(costs):.0f}~{max(costs):.0f}us"
f",主机头到头{min(intervals):.0f}~{max(intervals):.0f}us"
f",减去{character_us}us后估算线路静默"
f"{min(silence):.0f}~{max(silence):.0f}us"
)
lines = (
f"[{name}] {gap_text}\n"
f"TX: {request.hex(' ').upper()}\n"
f"RX: {response.hex(' ').upper() if response else '<无>'}\n"
f"结果: {description}\n\n"
)
self._append_log(lines)
self.status_var.set("测试完成")
else:
self._append_log(f"串口错误: {result[1]}\n\n")
self.status_var.set("测试失败")

self.root.after(50, self._poll_result)

def _append_log(self, text):
self.log.configure(state="normal")
self.log.insert("end", text)
self.log.see("end")
self.log.configure(state="disabled")


def self_test():
"""Check calculations without opening a serial port or GUI."""
request = build_read_coils_request(1, 0, 1)
expected = bytes.fromhex("01 01 00 00 00 01 FD CA")
if request != expected:
raise RuntimeError(
f"CRC self-test failed: {request.hex(' ').upper()}"
)
for baud_rate in BAUD_RATES:
character_us, t12_us, t15_us, t35_us, invalid_gap_us = (
calculate_timing(baud_rate)
)
if not (t12_us < t15_us < invalid_gap_us < t35_us < NEW_FRAME_GAP_US):
raise RuntimeError(f"Timing self-test failed at {baud_rate}")
print(
f"Baud={baud_rate}, Char={character_us}us, T1.2={t12_us}us, "
f"T1.5={t15_us}us, T3.5={t35_us}us"
)
print("Self-test passed")
print(f"Request: {request.hex(' ').upper()}")


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--self-test", action="store_true")
args = parser.parse_args()

if args.self_test:
self_test()
return

root = tk.Tk()
TimingTester(root)
root.mainloop()


if __name__ == "__main__":
main()

+ 1
- 0
HostComputer/requirements.txt View File

@@ -0,0 +1 @@
pyserial==3.5

+ 16
- 0
HostComputer/run.bat View File

@@ -0,0 +1,16 @@
@echo off
cd /d "%~dp0"

python -c "import serial" >nul 2>nul
if errorlevel 1 (
echo Installing pyserial...
python -m pip install -r requirements.txt
if errorlevel 1 (
echo Failed to install pyserial.
pause
exit /b 1
)
)

python modbus_timing_tester.py
if errorlevel 1 pause

+ 5
- 1
Modbus/Inc/modbus_rtu_slave.h View File

@@ -28,7 +28,11 @@ extern "C"
#define HMI_REG_DEVICE_ID (0U)
#define HMI_REG_UPTIME_SECONDS (1U)
#define HMI_REG_RX_FRAME_COUNT (2U)

#define D3 (3U)
#define D4 (4U)
#define D5 (5U)
#define D6 (6U)
#define D7 (7U)
#define HMI_REG_WRITE_TEST (10U)
#define HMI_COIL_WRITE_TEST (0U)



+ 60
- 49
Modbus/Src/modbus_rtu_slave.c View File

@@ -4,10 +4,10 @@

#define MODBUS_RTU_ADU_SIZE_MAX (256U) // Modbus RTU 最大 ADU 长度,单位为字节

#define MODBUS_RTU_T15_US (750UL) /* 高波特率下固定T1.5时间,单位us */
#define MODBUS_RTU_T35_US (1750UL) /* 高波特率下固定T3.5时间,单位us */
#define MODBUS_RTU_BITS_PER_CHAR (11UL) /* 8E1包含11个传输位 */
#define MODBUS_RTU_HIGH_BAUD_LIMIT (19200UL) /* 高低波特率计算方式的分界值 */
#define MODBUS_RTU_T15_US (750UL) // 高波特率下固定T1.5时间,单位us
#define MODBUS_RTU_T35_US (1750UL) // 高波特率下固定T3.5时间,单位us
#define MODBUS_RTU_BITS_PER_CHAR (11UL) // 8E1包含11个传输位
#define MODBUS_RTU_HIGH_BAUD_LIMIT (19200UL) // 高低波特率计算方式的分界值

#define MODBUS_BROADCAST_ADDRESS (0U) // 广播地址

@@ -28,9 +28,11 @@ static uint8_t ModbusSlaveAddress;
static void ModbusHistorySave(uint32_t tick, const uint8_t *request,
uint16_t requestLength, const uint8_t *response,
uint16_t responseLength);
static HAL_StatusTypeDef ModbusStartReceive(void);
static MODBUS_HISTORY_ITEM ModbusHistory[16];
static uint16_t ModbusHistoryWriteIndex;
/*最近一次字节尾到字节头的间隔周期数*/
static volatile uint32_t ModbusLastInterFrameGapCycles;
/**
* DMA 缓冲区只由 DMA 写入;帧缓冲区在接收回调中完成一次快照,
* 随后由任务解析,避免 DMA 重启后覆盖尚未处理的数据
@@ -45,12 +47,12 @@ static uint16_t ModbusRetainedSnapshot[MODBUS_RETAINED_D_COUNT];
static volatile uint16_t ModbusRxFrameLength;
static volatile uint8_t ModbusRxFrameReady;
static volatile uint8_t ModbusTxBusy;
static volatile uint16_t ModbusRxAssemblyLength; /* 当前拼帧长度 */
static volatile uint8_t ModbusRxAssemblyInvalid; /* 帧内间隔超过T1.5时置1 */
static volatile uint32_t ModbusRxLastByteCycle; /* 上一片段末字节结束时刻 */
static uint32_t ModbusRtuT15Cycles; /* T1.5对应的CPU周期数 */
static uint32_t ModbusRtuT35Cycles; /* T3.5对应的CPU周期数 */
static uint32_t ModbusRtuCharCycles; /* 一个UART字符对应的CPU周期数 */
static volatile uint16_t ModbusRxAssemblyLength; // 当前拼帧长度
static volatile uint8_t ModbusRxAssemblyInvalid; // 帧内间隔超过T1.5时置1
static volatile uint32_t ModbusRxLastByteCycle; // 上一片段末字节结束时刻
static uint32_t ModbusRtuT15Cycles; // T1.5对应的CPU周期数
static uint32_t ModbusRtuT35Cycles; // T3.5对应的CPU周期数
static uint32_t ModbusRtuCharCycles; // 一个UART字符对应的CPU周期数
static volatile uint32_t ModbusLastValidFrameTick;
static volatile uint8_t ModbusHasReceivedValidFrame;
static volatile MODBUS_BACKUP_DATA *ModbusBackupData =
@@ -269,6 +271,13 @@ static void ModbusTryFinalizeReceive(void)
__disable_irq();
ModbusRxAssemblyFinalize();
__enable_irq();
/* 无效帧被丢弃后,重新启动DMA接收。 */
if (ModbusRxFrameReady == 0U)
{
(void)ModbusStartReceive();
}


}

/**
@@ -532,9 +541,9 @@ static uint16_t ModbusProcessWriteSingleRegister(const uint8_t *request,
if (requestLength != 8U)
{
ModbusSlaveStatistics.illegalValueCount++;
return (isBroadcast != 0U)
? 0U
: ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
return (isBroadcast != 0U) ? 0U: ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

address = ModbusGetU16Be(&request[2]);
@@ -543,9 +552,9 @@ static uint16_t ModbusProcessWriteSingleRegister(const uint8_t *request,
if (address >= MODBUS_MAP_ITEM_COUNT)
{
ModbusSlaveStatistics.illegalAddressCount++;
return (isBroadcast != 0U) ? 0U
: ModbusBuildException(
request[1], MODBUS_EX_ILLEGAL_ADDRESS);
return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS);
}

ModbusHoldingRegisters[address] = value;
@@ -578,9 +587,9 @@ static uint16_t ModbusProcessWriteSingleCoil(const uint8_t *request,
if (requestLength != 8U)
{
ModbusSlaveStatistics.illegalValueCount++;
return (isBroadcast != 0U)
? 0U
: ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
return (isBroadcast != 0U)? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

address = ModbusGetU16Be(&request[2]);
@@ -593,17 +602,17 @@ static uint16_t ModbusProcessWriteSingleCoil(const uint8_t *request,
if ((value != MODBUS_COIL_VALUE_ON) && (value != MODBUS_COIL_VALUE_OFF))
{
ModbusSlaveStatistics.illegalValueCount++;
return (isBroadcast != 0U)
? 0U
: ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
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);
return (isBroadcast != 0U) ? 0U: ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS);
}

ModbusCoilSetUnchecked(address, (value == MODBUS_COIL_VALUE_ON) ? 1U : 0U);
@@ -639,9 +648,9 @@ static uint16_t ModbusProcessWriteMultipleCoils(const uint8_t *request,
if (requestLength < 9U)
{
ModbusSlaveStatistics.illegalValueCount++;
return (isBroadcast != 0U)
? 0U
: ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
return (isBroadcast != 0U) ? 0U: ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

start = ModbusGetU16Be(&request[2]);
@@ -654,25 +663,25 @@ static uint16_t ModbusProcessWriteMultipleCoils(const uint8_t *request,
|| (requestLength != (uint16_t)(9U + byteCount)))
{
ModbusSlaveStatistics.illegalValueCount++;
return (isBroadcast != 0U)
? 0U
: ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
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);
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);
state = (uint8_t)((request[7U + (index >> 3U)] >> (index & 0x0007U)) & 0x01U);
ModbusCoilSetUnchecked((uint16_t)(start + index), state);
}

@@ -707,9 +716,9 @@ static uint16_t ModbusProcessWriteMultipleRegisters(const uint8_t *request,
if (requestLength < 9U)
{
ModbusSlaveStatistics.illegalValueCount++;
return (isBroadcast != 0U)
? 0U
: ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
}

start = ModbusGetU16Be(&request[2]);
@@ -721,17 +730,17 @@ static uint16_t ModbusProcessWriteMultipleRegisters(const uint8_t *request,
|| (requestLength != (uint16_t)(9U + byteCount)))
{
ModbusSlaveStatistics.illegalValueCount++;
return (isBroadcast != 0U)
? 0U
: ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE);
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);
return (isBroadcast != 0U) ? 0U : ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS);
}

for (index = 0U; index < quantity; index++)
@@ -906,7 +915,9 @@ static uint16_t ModbusProcessRequest(const uint8_t *request,
isBroadcast);

case 0x48U: // 读大地址保持寄存器
return ModbusProcessReadBigHolding(request, requestLength);
return (isBroadcast != 0U)
? 0U
: ModbusProcessReadBigHolding(request, requestLength);

default: // 未知功能码
ModbusSlaveStatistics.illegalFunctionCount++;
@@ -931,7 +942,7 @@ HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart,
ModbusTxBusy = 0U;
ModbusRtuTimingInit(huart->Init.BaudRate);

ModbusHoldingRegisters[HMI_REG_DEVICE_ID] = 0xF407U;
//ModbusHoldingRegisters[HMI_REG_DEVICE_ID] = 0xF407U;

return ModbusStartReceive();
}
@@ -1013,7 +1024,7 @@ void ModbusSlaveOnRxEvent(UART_HandleTypeDef *huart, uint16_t size)
if (ModbusRxAssemblyLength > 0U)
{
interFrameGap = (uint32_t)(firstByteCycle - ModbusRxLastByteCycle);
ModbusLastInterFrameGapCycles = interFrameGap;
if (interFrameGap >= ModbusRtuT35Cycles)
{
/* 间隔达到T3.5,结束上一帧,本片段作为新帧开始 */


Loading…
Cancel
Save