| @@ -46,7 +46,7 @@ | |||||
| UART_HandleTypeDef huart1; | UART_HandleTypeDef huart1; | ||||
| DMA_HandleTypeDef hdma_usart1_rx; | DMA_HandleTypeDef hdma_usart1_rx; | ||||
| DMA_HandleTypeDef hdma_usart1_tx; | DMA_HandleTypeDef hdma_usart1_tx; | ||||
| static uint8_t ConnectFlag; | |||||
| static volatile uint8_t ConnectFlag; | |||||
| /* USER CODE BEGIN PV */ | /* USER CODE BEGIN PV */ | ||||
| static OS_STK AppTaskStartStk[APP_TASK_START_STK_SIZE]; | 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(); | HistorySaveToRegisters(); | ||||
| @@ -262,7 +263,7 @@ static void MX_USART1_UART_Init(void) | |||||
| /* USER CODE END USART1_Init 1 */ | /* USER CODE END USART1_Init 1 */ | ||||
| huart1.Instance = USART1; | huart1.Instance = USART1; | ||||
| huart1.Init.BaudRate = 115200; | |||||
| huart1.Init.BaudRate = 9600; | |||||
| huart1.Init.WordLength = UART_WORDLENGTH_9B; | huart1.Init.WordLength = UART_WORDLENGTH_9B; | ||||
| huart1.Init.StopBits = UART_STOPBITS_1; | huart1.Init.StopBits = UART_STOPBITS_1; | ||||
| huart1.Init.Parity = UART_PARITY_EVEN; | huart1.Init.Parity = UART_PARITY_EVEN; | ||||
| @@ -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 | | |||||
| @@ -1,27 +0,0 @@ | |||||
| # Modbus相关知识点 | |||||
| ## 基本概念 | |||||
| - Modbus基本概念 | |||||
| - 应用场景 | |||||
| - 三种通信模式 | |||||
| - 广播模式与单播模式 | |||||
| - 事务处理流程 | |||||
| - 寄存器相关概念 | |||||
| ## 数据帧格式 | |||||
| - 0x01功能码 | |||||
| · 请求帧格式 | |||||
| · 应答帧格式 | |||||
| - 0x03功能码 | |||||
| · 请求帧格式 | |||||
| · 应答帧格式 | |||||
| - 0x0f功能码 | |||||
| · 请求帧格式 | |||||
| · 应答帧格式 | |||||
| - 0x10功能码 | |||||
| · 请求帧格式 | |||||
| · 应答帧格式 | |||||
| ## 嵌入式平台 | |||||
| - 串口通信 | |||||
| - 网口通信 | |||||
| - 人机界面 | |||||
| @@ -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 区域。 | |||||
| @@ -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); | |||||
| } | |||||
| ``` | |||||
| @@ -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字节帧后,下一帧仍应正常通信 | |||||
| @@ -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`,因此当前从站直接忽略该帧,不返回正常响应或异常响应。 | |||||
| @@ -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 | |||||
| ``` | |||||
| @@ -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, | |||||
| ) | |||||
| @@ -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, | |||||
| ) | |||||
| @@ -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芯片内部仍可能缓存或合并字节,界面中的线路静默时间仍是估算值。正式验证时应使用逻辑分析仪测量线路上的字节间隔。 | |||||
| @@ -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')]) | |||||
| @@ -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) | |||||
| @@ -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')]) | |||||
| @@ -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) | |||||
| @@ -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() | |||||
| @@ -0,0 +1 @@ | |||||
| pyserial==3.5 | |||||
| @@ -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 | |||||
| @@ -28,7 +28,11 @@ extern "C" | |||||
| #define HMI_REG_DEVICE_ID (0U) | #define HMI_REG_DEVICE_ID (0U) | ||||
| #define HMI_REG_UPTIME_SECONDS (1U) | #define HMI_REG_UPTIME_SECONDS (1U) | ||||
| #define HMI_REG_RX_FRAME_COUNT (2U) | #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_REG_WRITE_TEST (10U) | ||||
| #define HMI_COIL_WRITE_TEST (0U) | #define HMI_COIL_WRITE_TEST (0U) | ||||
| @@ -4,10 +4,10 @@ | |||||
| #define MODBUS_RTU_ADU_SIZE_MAX (256U) // Modbus RTU 最大 ADU 长度,单位为字节 | #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) // 广播地址 | #define MODBUS_BROADCAST_ADDRESS (0U) // 广播地址 | ||||
| @@ -28,9 +28,11 @@ static uint8_t ModbusSlaveAddress; | |||||
| static void ModbusHistorySave(uint32_t tick, const uint8_t *request, | static void ModbusHistorySave(uint32_t tick, const uint8_t *request, | ||||
| uint16_t requestLength, const uint8_t *response, | uint16_t requestLength, const uint8_t *response, | ||||
| uint16_t responseLength); | uint16_t responseLength); | ||||
| static HAL_StatusTypeDef ModbusStartReceive(void); | |||||
| static MODBUS_HISTORY_ITEM ModbusHistory[16]; | static MODBUS_HISTORY_ITEM ModbusHistory[16]; | ||||
| static uint16_t ModbusHistoryWriteIndex; | static uint16_t ModbusHistoryWriteIndex; | ||||
| /*最近一次字节尾到字节头的间隔周期数*/ | |||||
| static volatile uint32_t ModbusLastInterFrameGapCycles; | |||||
| /** | /** | ||||
| * DMA 缓冲区只由 DMA 写入;帧缓冲区在接收回调中完成一次快照, | * DMA 缓冲区只由 DMA 写入;帧缓冲区在接收回调中完成一次快照, | ||||
| * 随后由任务解析,避免 DMA 重启后覆盖尚未处理的数据 | * 随后由任务解析,避免 DMA 重启后覆盖尚未处理的数据 | ||||
| @@ -45,12 +47,12 @@ static uint16_t ModbusRetainedSnapshot[MODBUS_RETAINED_D_COUNT]; | |||||
| static volatile uint16_t ModbusRxFrameLength; | static volatile uint16_t ModbusRxFrameLength; | ||||
| static volatile uint8_t ModbusRxFrameReady; | static volatile uint8_t ModbusRxFrameReady; | ||||
| static volatile uint8_t ModbusTxBusy; | 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 uint32_t ModbusLastValidFrameTick; | ||||
| static volatile uint8_t ModbusHasReceivedValidFrame; | static volatile uint8_t ModbusHasReceivedValidFrame; | ||||
| static volatile MODBUS_BACKUP_DATA *ModbusBackupData = | static volatile MODBUS_BACKUP_DATA *ModbusBackupData = | ||||
| @@ -269,6 +271,13 @@ static void ModbusTryFinalizeReceive(void) | |||||
| __disable_irq(); | __disable_irq(); | ||||
| ModbusRxAssemblyFinalize(); | ModbusRxAssemblyFinalize(); | ||||
| __enable_irq(); | __enable_irq(); | ||||
| /* 无效帧被丢弃后,重新启动DMA接收。 */ | |||||
| if (ModbusRxFrameReady == 0U) | |||||
| { | |||||
| (void)ModbusStartReceive(); | |||||
| } | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -532,9 +541,9 @@ static uint16_t ModbusProcessWriteSingleRegister(const uint8_t *request, | |||||
| if (requestLength != 8U) | if (requestLength != 8U) | ||||
| { | { | ||||
| ModbusSlaveStatistics.illegalValueCount++; | 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]); | address = ModbusGetU16Be(&request[2]); | ||||
| @@ -543,9 +552,9 @@ static uint16_t ModbusProcessWriteSingleRegister(const uint8_t *request, | |||||
| if (address >= MODBUS_MAP_ITEM_COUNT) | if (address >= MODBUS_MAP_ITEM_COUNT) | ||||
| { | { | ||||
| ModbusSlaveStatistics.illegalAddressCount++; | 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; | ModbusHoldingRegisters[address] = value; | ||||
| @@ -578,9 +587,9 @@ static uint16_t ModbusProcessWriteSingleCoil(const uint8_t *request, | |||||
| if (requestLength != 8U) | if (requestLength != 8U) | ||||
| { | { | ||||
| ModbusSlaveStatistics.illegalValueCount++; | 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]); | 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)) | if ((value != MODBUS_COIL_VALUE_ON) && (value != MODBUS_COIL_VALUE_OFF)) | ||||
| { | { | ||||
| ModbusSlaveStatistics.illegalValueCount++; | 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) | if (address >= MODBUS_MAP_ITEM_COUNT) | ||||
| { | { | ||||
| ModbusSlaveStatistics.illegalAddressCount++; | 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); | ModbusCoilSetUnchecked(address, (value == MODBUS_COIL_VALUE_ON) ? 1U : 0U); | ||||
| @@ -639,9 +648,9 @@ static uint16_t ModbusProcessWriteMultipleCoils(const uint8_t *request, | |||||
| if (requestLength < 9U) | if (requestLength < 9U) | ||||
| { | { | ||||
| ModbusSlaveStatistics.illegalValueCount++; | 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]); | start = ModbusGetU16Be(&request[2]); | ||||
| @@ -654,25 +663,25 @@ static uint16_t ModbusProcessWriteMultipleCoils(const uint8_t *request, | |||||
| || (requestLength != (uint16_t)(9U + byteCount))) | || (requestLength != (uint16_t)(9U + byteCount))) | ||||
| { | { | ||||
| ModbusSlaveStatistics.illegalValueCount++; | 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) | if (ModbusAddressRangeIsValid(start, quantity) == 0U) | ||||
| { | { | ||||
| ModbusSlaveStatistics.illegalAddressCount++; | 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++) | for (index = 0U; index < quantity; index++) | ||||
| { | { | ||||
| uint8_t state; | 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); | ModbusCoilSetUnchecked((uint16_t)(start + index), state); | ||||
| } | } | ||||
| @@ -707,9 +716,9 @@ static uint16_t ModbusProcessWriteMultipleRegisters(const uint8_t *request, | |||||
| if (requestLength < 9U) | if (requestLength < 9U) | ||||
| { | { | ||||
| ModbusSlaveStatistics.illegalValueCount++; | 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]); | start = ModbusGetU16Be(&request[2]); | ||||
| @@ -721,17 +730,17 @@ static uint16_t ModbusProcessWriteMultipleRegisters(const uint8_t *request, | |||||
| || (requestLength != (uint16_t)(9U + byteCount))) | || (requestLength != (uint16_t)(9U + byteCount))) | ||||
| { | { | ||||
| ModbusSlaveStatistics.illegalValueCount++; | 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) | if (ModbusAddressRangeIsValid(start, quantity) == 0U) | ||||
| { | { | ||||
| ModbusSlaveStatistics.illegalAddressCount++; | 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++) | for (index = 0U; index < quantity; index++) | ||||
| @@ -906,7 +915,9 @@ static uint16_t ModbusProcessRequest(const uint8_t *request, | |||||
| isBroadcast); | isBroadcast); | ||||
| case 0x48U: // 读大地址保持寄存器 | case 0x48U: // 读大地址保持寄存器 | ||||
| return ModbusProcessReadBigHolding(request, requestLength); | |||||
| return (isBroadcast != 0U) | |||||
| ? 0U | |||||
| : ModbusProcessReadBigHolding(request, requestLength); | |||||
| default: // 未知功能码 | default: // 未知功能码 | ||||
| ModbusSlaveStatistics.illegalFunctionCount++; | ModbusSlaveStatistics.illegalFunctionCount++; | ||||
| @@ -931,7 +942,7 @@ HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart, | |||||
| ModbusTxBusy = 0U; | ModbusTxBusy = 0U; | ||||
| ModbusRtuTimingInit(huart->Init.BaudRate); | ModbusRtuTimingInit(huart->Init.BaudRate); | ||||
| ModbusHoldingRegisters[HMI_REG_DEVICE_ID] = 0xF407U; | |||||
| //ModbusHoldingRegisters[HMI_REG_DEVICE_ID] = 0xF407U; | |||||
| return ModbusStartReceive(); | return ModbusStartReceive(); | ||||
| } | } | ||||
| @@ -1013,7 +1024,7 @@ void ModbusSlaveOnRxEvent(UART_HandleTypeDef *huart, uint16_t size) | |||||
| if (ModbusRxAssemblyLength > 0U) | if (ModbusRxAssemblyLength > 0U) | ||||
| { | { | ||||
| interFrameGap = (uint32_t)(firstByteCycle - ModbusRxLastByteCycle); | interFrameGap = (uint32_t)(firstByteCycle - ModbusRxLastByteCycle); | ||||
| ModbusLastInterFrameGapCycles = interFrameGap; | |||||
| if (interFrameGap >= ModbusRtuT35Cycles) | if (interFrameGap >= ModbusRtuT35Cycles) | ||||
| { | { | ||||
| /* 间隔达到T3.5,结束上一帧,本片段作为新帧开始 */ | /* 间隔达到T3.5,结束上一帧,本片段作为新帧开始 */ | ||||