| @@ -26,3 +26,4 @@ | |||
| .DS_Store | |||
| Thumbs.db | |||
| Desktop.ini | |||
| tmp/ | |||
| @@ -64,6 +64,8 @@ void TIM1_UP_TIM10_IRQHandler(void); | |||
| void TIM8_UP_TIM13_IRQHandler(void); | |||
| void TIM1_TRG_COM_TIM11_IRQHandler(void); | |||
| void TIM8_TRG_COM_TIM14_IRQHandler(void); | |||
| void TIM1_BRK_TIM9_IRQHandler(void); | |||
| void TIM8_BRK_TIM12_IRQHandler(void); | |||
| /* USER CODE END EFP */ | |||
| #ifdef __cplusplus | |||
| @@ -75,8 +75,12 @@ static void AppTaskStart(void *pArg) | |||
| while (1) | |||
| { | |||
| PlsrPoll1ms(); | |||
| ModbusSlavePoll(); | |||
| PlsrPoll1ms(); | |||
| if (ModbusSlaveIsIdle() != 0U) | |||
| { | |||
| PlsrServicePersistence(); | |||
| } | |||
| OSTimeDly(1U); | |||
| } | |||
| } | |||
| @@ -47,7 +47,7 @@ | |||
| /* Private function prototypes -----------------------------------------------*/ | |||
| /* USER CODE BEGIN PFP */ | |||
| extern void PlsrPlatformForceSafeOutputsFromFault(void); | |||
| /* USER CODE END PFP */ | |||
| /* Private user code ---------------------------------------------------------*/ | |||
| @@ -89,7 +89,7 @@ void NMI_Handler(void) | |||
| void HardFault_Handler(void) | |||
| { | |||
| /* USER CODE BEGIN HardFault_IRQn 0 */ | |||
| PlsrPlatformForceSafeOutputsFromFault(); | |||
| /* USER CODE END HardFault_IRQn 0 */ | |||
| while (1) | |||
| { | |||
| @@ -104,7 +104,7 @@ void HardFault_Handler(void) | |||
| void MemManage_Handler(void) | |||
| { | |||
| /* USER CODE BEGIN MemoryManagement_IRQn 0 */ | |||
| PlsrPlatformForceSafeOutputsFromFault(); | |||
| /* USER CODE END MemoryManagement_IRQn 0 */ | |||
| while (1) | |||
| { | |||
| @@ -119,7 +119,7 @@ void MemManage_Handler(void) | |||
| void BusFault_Handler(void) | |||
| { | |||
| /* USER CODE BEGIN BusFault_IRQn 0 */ | |||
| PlsrPlatformForceSafeOutputsFromFault(); | |||
| /* USER CODE END BusFault_IRQn 0 */ | |||
| while (1) | |||
| { | |||
| @@ -134,7 +134,7 @@ void BusFault_Handler(void) | |||
| void UsageFault_Handler(void) | |||
| { | |||
| /* USER CODE BEGIN UsageFault_IRQn 0 */ | |||
| PlsrPlatformForceSafeOutputsFromFault(); | |||
| /* USER CODE END UsageFault_IRQn 0 */ | |||
| while (1) | |||
| { | |||
| @@ -1,50 +1,121 @@ | |||
| # PLSR 地址映射 | |||
| # PLSR Modbus 地址映射(2026) | |||
| > 文档版本:V1.0 | |||
| > 适用轴:Y0~Y3 | |||
| > 文档版本:V2.0 | |||
| > 字节序:每个 32 位量均为低 16 位字在前、高 16 位字在后 | |||
| > 访问方式:配置和控制支持 FC06/FC16,状态和诊断仅支持 FC03 | |||
| ## 1 固定配置参数 | |||
| ## 1 固定配置区 `0x1000..0x1197` | |||
| | 地址 | 用途 | | |||
| |---|---| | |||
| | HSD460~539 | K0参数区 | | |||
| | SFD900~1419 | 公共参数及K1~K4参数区 | | |||
| ### 1.1 公共参数 `0x1000..0x1013` | |||
| ## 2 固定运行状态 | |||
| | 地址 | 类型 | 名称 | 取值/单位 | | |||
| |---|---|---|---| | |||
| | `0x1000` | U16 | 脉冲输出 | `0..3`,对应 Y0..Y3 | | |||
| | `0x1001` | U16 | 方向输出 | `0..3`,对应 Y12..Y15;AB 模式忽略 | | |||
| | `0x1002` | U16 | 等待输入 | `0=X4`,`1=X5` | | |||
| | `0x1003` | U16 | 外部中断输入 | `0=X4`,`1=X5` | | |||
| | `0x1004` | U16 | 发送模式 | `0=完整发送`,`1=后续发送` | | |||
| | `0x1005` | U16 | 方向延时 | ms;AB 模式忽略 | | |||
| | `0x1006` | U16 | 方向负逻辑 | `0/1`;AB 模式忽略 | | |||
| | `0x1007` | U16 | 曲线模式 | `0=直线`,`1=S 曲线`,`2=正弦曲线` | | |||
| | `0x1008` | U16 | 位置模式 | `0=相对`,`1=绝对` | | |||
| | `0x1009` | U16 | 有效段数 | `1..10` | | |||
| | `0x100A` | U16 | 起始段 | `1..10`,启动时不得超过有效段数 | | |||
| | `0x100B..0x100C` | U32 | 默认速度 | Hz,`1..100000` | | |||
| | `0x100D..0x100E` | U32 | 起始速度 | Hz,`0..100000` | | |||
| | `0x100F` | U16 | 保留 | 读为 0,仅允许写 0 | | |||
| | `0x1010..0x1011` | U32 | 停止速度 | Hz,`0..100000` | | |||
| | `0x1012` | U16 | 加速时间 | ms | | |||
| | `0x1013` | U16 | 减速时间 | ms | | |||
| | 地址 | 用途 | | |||
| |---|---| | |||
| | HSD0~15 | 累计脉冲状态 | | |||
| | SM1000、SM1001 | Y0运行和方向状态 | | |||
| | SM1020、SM1021 | Y1运行和方向状态 | | |||
| | SM1040、SM1041 | Y2运行和方向状态 | | |||
| | SM1060、SM1061 | Y3运行和方向状态 | | |||
| | SD1000~1011 | Y0实时运行状态 | | |||
| | SD1020~1031 | Y1实时运行状态 | | |||
| | SD1040~1051 | Y2实时运行状态 | | |||
| | SD1060~1071 | Y3实时运行状态 | | |||
| `0x1014..0x10FF` 为保留区,读为 0,仅允许写 0。32 位参数必须在同一次请求中完整访问两个寄存器。 | |||
| ## 3 固定事件入口 | |||
| ### 1.2 分段参数 `0x1100..0x1197` | |||
| | 地址 | 用途 | | |||
| |---|---| | |||
| | I6000~6399 | Y0~Y3每段事件入口 | | |||
| 第 `n` 段(`n=1..10`)首地址为 `0x1100 + (n-1)*0x10`。 | |||
| ## 4 动态数据地址 | |||
| | 段内偏移 | 类型 | 名称 | 取值/单位 | | |||
| |---|---|---|---| | |||
| | `+0x0..+0x1` | U32 | 段频率 | Hz,`1..100000` | | |||
| | `+0x2..+0x3` | I32 | 位移/脉冲数 | 正数为正向,负数为反向 | | |||
| | `+0x4` | U16 | 等待类型 | `0=定时等待`,`1=信号等待`,`2=定时动作`,`3=外部信号`,`4=外部或完成` | | |||
| | `+0x5` | U16 | 等待时间 | ms | | |||
| | `+0x6` | U16 | 动作时间 | ms | | |||
| | `+0x7` | U16 | 跳转段 | `0=不跳转`,`1..10=目标段` | | |||
| | `+0x8..+0xF` | U16 | 保留 | 读为 0,仅允许写 0 | | |||
| | 地址 | 用途 | | |||
| |---|---| | |||
| | S0 | 路径数据块,首地址动态指定 | | |||
| | S1 | 执行参数块,首地址动态指定 | | |||
| ## 2 扩展配置区 `0x1200` | |||
| 汇总 | |||
| | 地址 | 类型 | 名称 | 取值 | | |||
| |---|---|---|---| | |||
| | `0x1200` | U16 | 输出模式 | `0=PULSE/DIR`,`1=AB 正交脉冲` | | |||
| 该参数参与持久化。AB 模式只允许 `0x1000=0`(Y0/Y1)或 `0x1000=2`(Y2/Y3);其他组合返回非法数据值。运行期间不得修改输出模式或脉冲输出。 | |||
| | 类别 | 固定地址 | | |||
| |---|---:| | |||
| | K0参数 | HSD460~539 | | |||
| | 公共参数及K1~K4参数 | SFD900~1419 | | |||
| | 累计脉冲状态 | HSD0~15 | | |||
| | 正在发脉冲、方向标志 | SM1000~SM1001、SM1020~SM1021、SM1040~SM1041、SM1060~SM1061 | | |||
| | 实时运行状态 | SD1000~1011、SD1020~1031、SD1040~1051、SD1060~1071 | | |||
| | 每段事件入口 | I6000~6099、I6100~6199、I6200~6299、I6300~6399 | | |||
| AB 模式的频率单位是完整正交周期/秒,一个 `00` 起止的四状态周期等于一个指令脉冲: | |||
| - 正向:`00 -> 10 -> 11 -> 01 -> 00` | |||
| - 反向:`00 -> 01 -> 11 -> 10 -> 00` | |||
| 方向由段位移的符号决定。独立 DIR 输出、DIR 极性和 DIR 延时在 AB 模式中不参与输出。 | |||
| ## 3 固定状态区 `0x2000..0x2006`(只读) | |||
| | 地址 | 类型 | 名称 | 说明 | | |||
| |---|---|---|---| | |||
| | `0x2000..0x2001` | I32 | 当前位置 | 累计指令脉冲位置 | | |||
| | `0x2002..0x2003` | U32 | 当前频率 | Hz | | |||
| | `0x2004` | U16 | 运行状态 | `0=未初始化`,`1=空闲`,`2=加速`,`3=运行`,`4=减速`,`5=等待`,`6=暂停`,`7=完成`,`8=停止`,`9=错误` | | |||
| | `0x2005` | U16 | 当前段 | `0=无`,`1..10=段号` | | |||
| | `0x2006` | U16 | 错误码 | `0=无错误`,其余见 `PLSR_ERROR` 定义 | | |||
| ## 4 扩展诊断区 `0x2100..0x2119`(只读) | |||
| | 地址 | 类型 | 名称 | 说明 | | |||
| |---|---|---|---| | |||
| | `0x2100` | U16 | 诊断标志 | 位定义见下表 | | |||
| | `0x2101` | U16 | 首个锁存原因 | `0=无`,`1=计数`,`2=频率`,`3=曲线`,`4=无效采样` | | |||
| | `0x2102` | U16 | 段号 | 当前或末次诊断段 | | |||
| | `0x2103` | U16 | 模式与方向 | bit0..7=输出模式,bit8=`1` 正向/`0` 反向 | | |||
| | `0x2104..0x2105` | U32 | 期望脉冲数 | 当前或末次诊断段的完整指令脉冲数 | | |||
| | `0x2106..0x2107` | U32 | 实测脉冲数 | 当前或末次诊断段的实测值;目标板来自独立硬件计数链 | | |||
| | `0x2108..0x2109` | I32 | 计数误差 | `实测 - 期望` | | |||
| | `0x210A..0x210B` | U32 | 请求频率 | Hz,规划器本次请求值 | | |||
| | `0x210C..0x210D` | U32 | 期望定时器频率 | Hz,根据实际 PSC/ARR 可实现的量化值 | | |||
| | `0x210E..0x210F` | U32 | 活动定时器频率 | Hz,实际生效的定时器配置值 | | |||
| | `0x2110..0x2111` | I32 | 请求量化误差 | `期望定时器频率 - 请求频率`,Hz | | |||
| | `0x2112..0x2113` | U32 | 采样数 | 本次诊断累计的有效样本数 | | |||
| | `0x2114..0x2115` | U32 | 不匹配数 | 频率或曲线不匹配的累计样本数 | | |||
| | `0x2116..0x2117` | U32 | 最大频率误差 | `abs(活动定时器频率 - 期望定时器频率)`,Hz | | |||
| | `0x2118..0x2119` | U32 | 首个不匹配样本 | 无不匹配时为 `0xFFFFFFFF` | | |||
| `0x2100` 标志位: | |||
| | 位 | 名称 | 置位条件 | | |||
| |---|---|---| | |||
| | bit0 | `monitoring_active` | 正在采集诊断数据 | | |||
| | bit1 | `count_checked` | 已完成计数核对 | | |||
| | bit2 | `count_pass` | 计数核对通过 | | |||
| | bit3 | `frequency_checked` | 已执行频率核对 | | |||
| | bit4 | `frequency_pass` | 频率核对通过 | | |||
| | bit5 | `curve_checked` | 已执行加减速次序核对 | | |||
| | bit6 | `curve_pass` | 曲线核对通过 | | |||
| | bit7 | `fault_latched` | 至少一个诊断故障已锁存 | | |||
| 诊断使用定时器分频值、完整周期计数和频率变化次序核对输出配置。它能发现内部计数、频率和曲线不一致,但不能代替逻辑分析仪对端子占空比、窄脉冲或电气波形的验收。 | |||
| ## 5 控制区 | |||
| | 地址 | 类型 | 名称 | 写入值 | | |||
| |---|---|---|---| | |||
| | `0x3000` | U16 | PLSR 命令 | `0=无操作`,`1=START`,`2=STOP`,`4=CLEAR` | | |||
| | `0x3100` | U16 | 诊断控制 | `0=无操作`,`1=清除诊断锁存和统计` | | |||
| 控制寄存器读取恒为 0。除上述单一值外的位组合均返回非法数据值。 | |||
| 运行期间写 `0x3100=1` 返回设备忙,避免清除正在采集的诊断上下文。 | |||
| ## 6 地址边界 | |||
| - 原固定区 `0x1000..0x1197`、`0x2000..0x2006`、`0x3000` 保持不变。 | |||
| - 扩展区仅增加 `0x1200`、`0x2100..0x2119`、`0x3100`。 | |||
| - 完全落在 PLSR 地址之外的请求返回“不处理”,跨越 PLSR 地址与空洞的请求返回“非法地址”。 | |||
| @@ -0,0 +1,376 @@ | |||
| # PLSR 2026 重写测试报告 | |||
| > 测试日期:2026-08-12 | |||
| > | |||
| > 测试分支:`codex/plsr-2026-minimal` | |||
| > | |||
| > 测试代码基线:`bf314a2343002a3df78bce5049badb29d11eae2e` | |||
| > | |||
| > PLSR 实现提交:`c7b851659ea26f9af8563e146bf5332c3c8d6dfd` | |||
| > | |||
| > 分支起点:`e35595aae18690a46a03445865bd6218a166bd89` | |||
| > | |||
| > 结论口径:只记录本次 2026 PLSR 最小重写实际执行过的测试 | |||
| ## 1. 总结 | |||
| 本次重写的 Host 回归、Python 离线自检、IAR 全量构建、固件烧录、真实 Modbus RTU 板测、Y0~Y3 逐路选择、X4/X5 实体回环、硬复位持久化和最终默认状态复核均通过。 | |||
| | 类别 | 最终结果 | 数量或关键结果 | | |||
| |---|---|---| | |||
| | C Host 回归 | PASS | 44 个测试组,1,694,007 次断言,0 失败 | | |||
| | Python 产品测试脚本离线自检 | PASS | 未打开串口 | | |||
| | Python 控制面板离线自检 | PASS | 未打开 GUI 或串口 | | |||
| | IAR EWARM 全量构建 | PASS | 0 errors,0 warnings | | |||
| | 固件烧录和校验 | PASS | `Verification...OK`,SHA256 与板测固件一致 | | |||
| | Modbus RTU/运动板测 | PASS | 9 组 | | |||
| | X4/X5 实体回环板测 | PASS | 9 个用例 | | |||
| | 硬复位持久化 | PASS | 配置和位置恢复,运动不自动续跑 | | |||
| | 最终默认状态复核 | PASS | 位置、配置、输入和夹具输出均恢复 | | |||
| | 逻辑分析仪测试 | 未执行 | 不在本轮验收范围内 | | |||
| 这里的“Y0~Y3 验证”是**同一个逻辑 PLSR 依次选择 Y0、Y1、Y2、Y3 作为脉冲输出**,不是四轴同时运动或四轴联动。 | |||
| `1,694,007 assertions` 是逐脉冲检查、边界检查和随机算术比对产生的断言总数,不代表 169 万个独立测试场景。 | |||
| ## 2. 测试范围和口径 | |||
| ### 2.1 纳入本报告 | |||
| - 本次重写新增的固定产品寄存器区:`0x1000~0x1197`、`0x2000~0x2006`、`0x3000`。 | |||
| - 单个逻辑 PLSR 的配置、状态、控制、运动、短段曲线、动态调频和停止。 | |||
| - 单个逻辑 PLSR 对 Y0~Y3 的逐路选择。 | |||
| - X4/X5 的 WAIT、EXT 和 EXT_OR_COMPLETE 实体输入回环。 | |||
| - 产品配置与逻辑位置的保存、硬复位恢复和默认值恢复。 | |||
| - 最小 Modbus RTU 的真实串口访问和异常响应。 | |||
| - 当前代码基线的 Host、Python、IAR 和固件产物复核。 | |||
| ### 2.2 不纳入本报告 | |||
| 旧工程曾做过的四轴并发、双 AB、软限位、长稳、USB CDC 并发和逻辑分析仪波形测试属于另一套实现,不能作为本次重写的通过证据,因此没有合并到本报告。 | |||
| ## 3. 环境与产物 | |||
| | 项目 | 实际环境或结果 | | |||
| |---|---| | |||
| | 目标板 | 信捷 XDM-60T4-E,STM32F407IG | | |||
| | RTOS | uC/OS-II | | |||
| | 串口 | `COM5`,9600 bit/s,8E1,从站 1 | | |||
| | Host 编译器 | GCC 4.9.2 (`tdm64-1`) | | |||
| | Host 编译选项 | C99,`-Wall -Wextra -Werror`,`PLSR_HOST_TEST` | | |||
| | IAR | EWARM 8.3.4,配置 `Modbus` | | |||
| | IAR 工程 | `EWARM/Modbus.ewp` | | |||
| | HEX | `EWARM/Modbus/Exe/Modbus.hex` | | |||
| | HEX 文本文件大小 | 142,131 B | | |||
| | Intel HEX 有效数据 | 50,513 B | | |||
| | HEX 地址范围 | `0x08000000..0x0800C550` | | |||
| | SHA256 | `97956D9EF30A9B274140B9B82F412EDD77F9637F563FCC3F552BB4B140772343` | | |||
| IAR 链接结果: | |||
| ```text | |||
| 49,004 bytes of readonly code memory | |||
| 1,509 bytes of readonly data memory | |||
| 18,660 bytes of readwrite data memory | |||
| Errors: none | |||
| Warnings: none | |||
| ``` | |||
| ## 4. 最终自动化回归 | |||
| ### 4.1 C Host 回归 | |||
| 执行命令: | |||
| ```powershell | |||
| powershell.exe -NoProfile -ExecutionPolicy Bypass ` | |||
| -File .\tests\plsr_host\run_tests.ps1 | |||
| ``` | |||
| 最终输出: | |||
| ```text | |||
| SUMMARY tests=44 assertions=1694007 failures=0 | |||
| ``` | |||
| 44 个测试组全部 PASS: | |||
| | # | 测试组 | 实际验证内容 | | |||
| |---:|---|---| | |||
| | 1 | `u64_by_u32_division` | 用 8 个边界被除数、9 个边界除数和 250,000 组确定性伪随机数据,比对 64 位除 32 位算法与 C 原生除法的商和余数。 | | |||
| | 2 | `protocol_boundaries` | 覆盖区间外、跨区、地址溢出、零长度、空缓冲、只读区、保留字、32 位字段不完整写入、非法值和控制字宽度,并确认失败写入不破坏原值。 | | |||
| | 3 | `wait_zero_one_ms` | 验证 `WAIT_TIME=0` 按最小 1 ms 处理,先进入 WAITING,1 ms 后才进入下一段。 | | |||
| | 4 | `act_zero_no_pulse` | 验证 `ACT_TIME=0` 的段不产生脉冲并在下一次 1 ms 调度跳过,后续段正常执行。 | | |||
| | 5 | `relative_absolute_zero` | 验证相对位移 0、相对位移 +5,以及当前位置等于绝对目标时的零位移完成。 | | |||
| | 6 | `self_jump_stop` | 验证单段自跳转可循环运行,并能由 STOP 正常终止。 | | |||
| | 7 | `stop_deceleration` | 验证 STOP 从 1000 Hz 按 1000 ms 减速至 100 Hz 后停止;重复 STOP 不重启减速,最终位置 902。 | | |||
| | 8 | `stop_pending_boundary` | 验证最后脉冲已完成但 Poll 尚未处理时收到 STOP,最终为 STOPPED 而非 COMPLETED。 | | |||
| | 9 | `direction_delay_one_ms` | 验证 1 ms 方向建立延时结束前不输出脉冲,满 1 ms 后启动。 | | |||
| | 10 | `ext_edge_at_natural_boundary` | 验证 EXT 边沿和自然末脉冲同时发生时只处理一次,不重复切段或计数。 | | |||
| | 11 | `ext_cut_natural_boundary_interleavings` | 覆盖 EXT 切断与自然边界的两种临界交错顺序,均正确进入后续 2000 Hz 段。 | | |||
| | 12 | `ext_edge_during_direction_delay` | 验证方向延时期间的 EXT 边沿不会在首脉冲前错误切段。 | | |||
| | 13 | `stopped_clears_pending_ext` | 验证 STOP 清除方向延时期间挂起的 EXT 边沿,重新 START 不消费旧边沿。 | | |||
| | 14 | `ext_edge_seamless_epoch` | 验证上一段的 EXT 竞态不会误切无缝衔接后的下一段。 | | |||
| | 15 | `ramp_seamless_epoch` | 验证上一段遗留的调速更新不会重定向无缝衔接后的下一段。 | | |||
| | 16 | `short_profile_boundary_matrix` | 覆盖直线、平滑、正弦三曲线与 1、2、10、99、100、101 脉冲,检查频率范围、升降趋势、对称性、计数和理论时长。 | | |||
| | 17 | `short_final_no_poll` | 验证三种曲线的 10 脉冲最终短段在脉冲间无 1 ms Poll 时仍逐脉冲减速并准确完成。 | | |||
| | 18 | `zero_to_maximum_ramp_duration` | 验证三种曲线从 0 加速到 100 kHz、1000 脉冲、20 ms 配置的单调性、峰值和总时长。 | | |||
| | 19 | `maximum_pulse_ramp_duration` | 验证三种曲线在 65,535 脉冲和 1311 ms 加速条件下采用可达峰值并准确计数。 | | |||
| | 20 | `maximum_single_ramp_no_poll` | 验证三种曲线的 65,535 脉冲单段在无 Poll 时持续逐脉冲更新并准确完成。 | | |||
| | 21 | `short_timer_failure` | 注入短段频率更新失败,验证立即关闭输出并进入 `ERROR/TIMER`,位置保留为 1。 | | |||
| | 22 | `stop_after_subsequent_handoff` | 验证无缝切入第二段后立即 STOP 能从新段频率正常减速并停止。 | | |||
| | 23 | `short_zero_speed_edges` | 验证起始/停止速度均为 0 时,1 脉冲和 2 脉冲短段仍以有效非零硬件频率准确执行。 | | |||
| | 24 | `short_dynamic_retarget` | 验证 50 脉冲短段中把目标频率由 1000 改为 2000 Hz 后安全上升,随后 STOP 正常停止。 | | |||
| | 25 | `short_ext_cut` | 验证短段执行 3 脉冲后触发 EXT,在下一脉冲边界切断,最终位置 4。 | | |||
| | 26 | `current_frequency_dynamic` | 验证当前段动态频率按安全脉冲边界生效,状态反映实际输出,运行中改脉冲数不改变启动快照。 | | |||
| | 27 | `poll_mailbox_failure` | 注入动态调频失败,验证 Poll 仅提交更新,失败在脉冲边界转为 `ERROR/TIMER`。 | | |||
| | 28 | `future_frequency_on_arrival` | 普通逐段模式中修改未来第二段频率,当前段不受影响,到达第二段时采用新值。 | | |||
| | 29 | `subsequent_future_frequency_handoff` | 无缝模式中修改未来段频率,验证预装衔接重建并以新频率无缝进入第二段。 | | |||
| | 30 | `future_frequency_last_pulse_race` | 验证未来段调频与当前段最后脉冲竞态时,第二段仍以新频率启动并准确完成。 | | |||
| | 31 | `latched_update_before_future_frequency_commit` | 验证屏蔽中断期间锁存的物理脉冲先消费旧衔接,再提交新的未来频率预装。 | | |||
| | 32 | `subsequent_no_poll_gap` | 验证两段只靠 ISR 无缝衔接,中间不依赖 Poll,输出不停止,最终位置 7。 | | |||
| | 33 | `three_single_pulse_no_poll` | 验证三个各 1 脉冲、1000/2000/3000 Hz 的段在无 Poll 时连续衔接。 | | |||
| | 34 | `handoff_queue_failures` | 两种时序下注入下一段频率排队失败,均关闭输出并转为 `ERROR/TIMER`。 | | |||
| | 35 | `command_state_restrictions` | 验证 IDLE/RUNNING 状态下 START、STOP、CLEAR、静态/动态配置写入及非法组合的限制。 | | |||
| | 36 | `command_mailbox_start_snapshot` | 验证 START 延迟执行与配置快照,后续写入和重复 START 不覆盖首个快照。 | | |||
| | 37 | `command_mailbox_stop_clear_deferred` | 验证重复 STOP/CLEAR 可幂等入队,状态变化延迟到 Poll,冲突命令返回 BUSY。 | | |||
| | 38 | `command_mailbox_validation` | 验证 START 入队前检查起始段和绝对位置有效性,非法配置不启动。 | | |||
| | 39 | `command_mailbox_start_failure` | 注入底层启动失败,验证 Poll 后进入 `ERROR/INVALID_RESOURCE`,CLEAR 可恢复。 | | |||
| | 40 | `wait_signal_positive` | 验证段后进入 WAITING,选定输入有效后由下一次 Poll 结束等待。 | | |||
| | 41 | `persistence_reinit` | 在 Host 存储模拟中验证输出、方向、2345 Hz 配置和位置 4 跨 `PlsrInit()` 恢复,CLEAR 后位置保持 0。 | | |||
| | 42 | `position_flash_save` | 验证位置稳定约 1000 ms 后仅保存一次,CLEAR 后延时产生第二次保存。 | | |||
| | 43 | `busy_reset_absolute_gate` | 模拟运动中复位,保留位置但取消绝对位置可信标志;CLEAR 后恢复绝对运动。 | | |||
| | 44 | `position_overflow` | 验证 `INT32_MAX` 正向溢出时立即停脉冲并进入 `ERROR/COUNT`。 | | |||
| 说明:Host 测试直接调用寄存器 API 和平台替身,不能代替真实 RTU 串口板测;真实 RTU 结果见第 7 节。 | |||
| ### 4.2 Python 产品测试脚本离线自检 | |||
| 执行命令: | |||
| ```powershell | |||
| py -B HostComputer\plsr_modbus_product_test.py --self-test | |||
| ``` | |||
| 结果: | |||
| ```text | |||
| Self-test passed; no serial port was opened | |||
| ``` | |||
| 覆盖: | |||
| - Modbus CRC16 参考向量及线上低字节在前的字节序。 | |||
| - 6 组有符号 32 位数的双 WORD 拆分与合并。 | |||
| - 1200 Hz 请求的 1 Hz 容差边界。 | |||
| - ST-LINK GPIO 位读回文本解析。 | |||
| ### 4.3 Python 控制面板离线自检 | |||
| 执行命令: | |||
| ```powershell | |||
| py -B HostComputer\plsr_control_panel.py --self-test | |||
| ``` | |||
| 结果: | |||
| ```text | |||
| PLSR control panel self-test passed; no GUI or serial port was opened | |||
| ``` | |||
| 覆盖 9600 波特率、X4/X5 选项、10 段地址步进、整数解析和越界拒绝、公共区 20 WORD 编码、段区 8 WORD 编码、负脉冲补码及跳转段范围校验。 | |||
| ## 5. IAR 构建 | |||
| 执行命令: | |||
| ```powershell | |||
| & 'F:\IAR Systems\Embedded Workbench 8.3\common\bin\IarBuild.exe' ` | |||
| 'EWARM\Modbus.ewp' -build Modbus -log warnings | |||
| ``` | |||
| 最终结果: | |||
| ```text | |||
| Total number of errors: 0 | |||
| Total number of warnings: 0 | |||
| ``` | |||
| `EWARM/Modbus/List/Modbus.map` 同时报告 `Errors: none`、`Warnings: none`,内存数据见第 3 节。 | |||
| ## 6. 烧录与固件产物校验 | |||
| ### 6.1 烧录 | |||
| 最终烧录使用 `EWARM/Modbus/Exe/Modbus.hex`,只擦除并写入应用占用的 Sector 0~3。ST-LINK 最终输出: | |||
| ```text | |||
| Verification...OK | |||
| ``` | |||
| 开发过程中还用 STM32CubeProgrammer 执行过两次下载校验,用于确认当时固件可以下载和校验。它们属于中间版本的烧录链路记录,不是功能测试,也不作为最终交付固件的产物证据;最终结论以上述 ST-LINK 烧录、最终 SHA256 和后续完整板测为准。 | |||
| ### 6.2 持久化区保护 | |||
| 烧录前后均读取并检查 Sector 10/11。应用 HEX 地址上限为 `0x0800C550`,不覆盖持久化区;烧录流程也没有擦除 Sector 10/11。该检查用于确认后续持久化板测不是由重新烧录清空后伪造出的初始状态。 | |||
| ### 6.3 产物一致性 | |||
| 板测烧录件和当前分支重新构建产物的 SHA256 均为: | |||
| ```text | |||
| 97956D9EF30A9B274140B9B82F412EDD77F9637F563FCC3F552BB4B140772343 | |||
| ``` | |||
| ## 7. 真实 Modbus RTU 和运动板测 | |||
| 执行入口: | |||
| ```powershell | |||
| py -B HostComputer\plsr_modbus_product_test.py ` | |||
| --port COM5 --run --phase all --allow-motion | |||
| ``` | |||
| 最终 `phase=all` 的 9 组测试全部 PASS: | |||
| | # | 用例 | 实际验证内容 | 结果 | | |||
| |---:|---|---|---| | |||
| | 1 | `fixed_map_and_exceptions` | 真实 FC03/06/10 访问固定配置、段、状态和控制区;检查保留字读零、控制字读零;验证不支持 FC01 返回 0x01,非法/跨区/只读地址返回 0x02,保留字非零、半个 DWORD 和组合控制命令返回 0x03。 | PASS | | |||
| | 2 | `positive_negative_absolute_zero` | 验证正向相对、负向相对、绝对定位及零位移,检查方向、终态和逻辑位置。 | PASS | | |||
| | 3 | `all_outputs_100khz` | 同一个 PLSR 依次选择 Y0、Y1、Y2、Y3,每路请求 100 kHz、执行 1000 个逻辑脉冲并检查完成位置。不是四轴并发。 | PASS | | |||
| | 4 | `short_final_profiles` | 对直线、平滑、正弦三种曲线执行最终 10 脉冲短段,检查终态和准确位置。 | PASS | | |||
| | 5 | `wait_time` | 执行 10 脉冲后进入 WAITING,配置等待 120 ms;检查不会异常提前完成,之后正常完成。 | PASS | | |||
| | 6 | `act_time` | 2000 脉冲目标配合 80 ms ACT,检查时间到后在 0~2000 之间切断并正常完成。 | PASS | | |||
| | 7 | `dynamic_frequency_repeated_stop` | 当前段从 500 Hz 动态改为 1200 Hz;真实状态读回 1199 Hz,在允许的 1 Hz 容差内;运行中静态输出选择写入返回 BUSY;重复 STOP 正常减速停止且未跑满 3000 脉冲。 | PASS | | |||
| | 8 | `future_segment_frequency_on_arrival` | 普通逐段模式运行第一段时修改未来第二段频率,确认当前段仍为 400 Hz,进入第二段后采用 900 Hz。 | PASS | | |||
| | 9 | `subsequent_future_frequency_handoff` | SUBSEQUENT 模式运行第一段时修改未来段频率,确认当前段不变,第二段以 900 Hz 衔接并最终累计 400 脉冲。 | PASS | | |||
| 脚本在退出时停止仍在运行的任务,并恢复进入测试前保存的公共配置和前两段配置。 | |||
| 正式烧录后还在完整 `phase=all` 之前单独执行过一次 `--phase smoke`,固定映射和异常码检查 PASS。第 9 节恢复默认值并再次硬复位后又执行了一次 smoke,结果仍为 PASS。 | |||
| ## 8. X4/X5 实体回环板测 | |||
| ### 8.1 接线和方法 | |||
| 本轮保留的自动化回环接线: | |||
| ```text | |||
| Y4 -> X4 | |||
| Y5 -> X5 | |||
| ``` | |||
| 脚本通过 ST-LINK 临时把 Y4/Y5 对应 GPIO 配置为输出,驱动 PLC 输入并直接读回 X4/X5 输入位;产品协议和正式固件没有为测试夹具新增私有寄存器。 | |||
| 开发期夹具最初尝试写 GPIO 的只写 BSRR 寄存器。旧版 `ST-LINK_CLI 3.5.0` 会在写后回读校验,因 BSRR 不可按普通寄存器回读而返回码 4。夹具随后改用 PI8/PE6 的 ODR 位带地址 `0x424402A0`、`0x42420298` 驱动输出。该失败是调试工具与只写寄存器的校验方式不兼容,不是接线、X4/X5 输入或 PLSR 功能故障。 | |||
| 执行入口: | |||
| ```powershell | |||
| py -B HostComputer\plsr_modbus_product_test.py ` | |||
| --port COM5 --run --phase x45 --allow-motion | |||
| ``` | |||
| ### 8.2 九个用例 | |||
| | # | 用例 | 实际验证内容 | 结果 | | |||
| |---:|---|---|---| | |||
| | 1 | `x45_electrical_scan` | Y4/Y5 均关闭时 X4/X5 均为低;分别打开 Y4、Y5 时对应输入为高、另一路保持低;关闭后恢复低,验证双路电气扫描和隔离。 | PASS | | |||
| | 2 | `X4_wait_signal` | 选择 X4;50 脉冲后进入 WAITING,X4 低时保持,Y4 拉有效后完成,位置仍为 50。 | PASS | | |||
| | 3 | `X4_ext_signal` | 选择 X4;5000 脉冲运动中由 Y4 触发 EXT_SIGNAL,提前切段并正常完成。 | PASS | | |||
| | 4 | `X4_ext_or_complete_natural` | 选择 X4;不触发输入,50 脉冲自然完成。 | PASS | | |||
| | 5 | `X4_ext_or_complete_trigger` | 选择 X4;5000 脉冲运动中由 Y4 外部触发,提前完成。 | PASS | | |||
| | 6 | `X5_wait_signal` | 选择 X5;50 脉冲后进入 WAITING,X5 低时保持,Y5 拉有效后完成,位置仍为 50。 | PASS | | |||
| | 7 | `X5_ext_signal` | 选择 X5;5000 脉冲运动中由 Y5 触发 EXT_SIGNAL,提前切段并正常完成。 | PASS | | |||
| | 8 | `X5_ext_or_complete_natural` | 选择 X5;不触发输入,50 脉冲自然完成。 | PASS | | |||
| | 9 | `X5_ext_or_complete_trigger` | 选择 X5;5000 脉冲运动中由 Y5 外部触发,提前完成。 | PASS | | |||
| ODR 位带夹具修正后先完成独立电气扫描,再执行完整九项用例。异常清理顺序修正后又执行了第二次完整九项回归,结果仍全部 PASS。 | |||
| ### 8.3 最终夹具清理状态 | |||
| 测试结束或异常退出均按以下顺序清理:先关闭 Y4/Y5,再停止仍在运行的 PLSR,恢复测试前配置,最后把 PI8/PE6 恢复为输入高阻并清除上下拉。最终复核结果: | |||
| - Y4/Y5 均关闭。 | |||
| - PI8/PE6 均为输入高阻、无内部上下拉。 | |||
| - X4/X5 读回均为低。 | |||
| ## 9. 持久化与硬复位板测 | |||
| 持久化测试使用真实目标板和 100 ms 硬复位,不以 Host 内存模拟代替。 | |||
| ### 9.1 保存准备 | |||
| 执行 `persistence-prepare`: | |||
| - 清零逻辑位置。 | |||
| - 写入 500 Hz、7 脉冲的特征配置。 | |||
| - 执行运动并确认 COMPLETED、位置为 7。 | |||
| - 等待后台保存完成。 | |||
| ### 9.2 第一次硬复位和恢复检查 | |||
| 执行 100 ms 硬复位后运行 `persistence-verify --cleanup`,结果: | |||
| - 上电状态为 IDLE,没有自动续跑复位前任务。 | |||
| - 逻辑位置恢复为 7。 | |||
| - 第一段频率恢复为 500 Hz。 | |||
| - 第一段脉冲数恢复为 7。 | |||
| 随后执行 CLEAR,把位置恢复为 0;写回默认配置并等待保存。 | |||
| ### 9.3 第二次硬复位和默认状态检查 | |||
| 再次执行 100 ms 硬复位后运行 `defaults-verify`,结果: | |||
| - 状态为 IDLE,逻辑位置为 0。 | |||
| - 默认方向建立延时为 10 ms。 | |||
| - 默认速度为 1000 Hz,起始速度和停止速度均为 100 Hz。 | |||
| - 第 1 段为 1000 Hz、1000 脉冲。 | |||
| - 第 2~10 段频率均为 1000 Hz、脉冲数均为 0。 | |||
| 最后又执行一次固定映射 smoke,确认默认配置恢复后 RTU 访问和异常码仍通过。 | |||
| ## 10. 测试过程中出现的问题和修复回归 | |||
| 本节记录中间失败;这些结果不是最终失败,而是用于说明本轮调试实际发现并关闭了哪些问题。 | |||
| | 阶段 | 中间现象 | 原因与处理 | 修复后结果 | | |||
| |---|---|---|---| | |||
| | 旧架构 PDU 试验 | `length == 8U`、`TestStatusAndControl() == 0` 失败 | 试验基于待废弃的旧 PDU 架构。需求复核后没有继续补旧架构,而是按 2026 固定产品接口重写;该试验路线整体废弃。 | 新固定接口由 Host `protocol_boundaries` 和真实 `fixed_map_and_exceptions` 覆盖并通过。 | | |||
| | 初期 Host | `12 tests / 262 assertions / 2 failures` | 失败项是 `wait_zero_rejected`、`act_zero_rejected`。复核明确需求后确认正确语义为 `WAIT=0` 按 1 ms,`ACT=0` 零脉冲跳段,代码和测试按正确语义修正。 | `wait_zero_one_ms`、`act_zero_no_pulse` 通过;最终 44/1,694,007/0。 | | |||
| | 扩充 EXT 测试 | Host 编译失败,`PLSR_EXT_SIGNAL` 未声明 | 测试扩充时枚举和测试引用未同步,补齐枚举/接口后重跑。 | Host 编译和完整回归通过。 | | |||
| | Modbus 裁剪中间态 | IAR `10 errors / 2 warnings` | 头文件先精简,旧源文件与 `main.c` 尚未同步清理;不是运动算法失败。随后完成最小 RTU 源文件和入口同步。 | IAR 全量构建 `0 errors / 0 warnings`。 | | |||
| | Host 脚本启动 | Windows 默认策略阻止直接运行 `.ps1` | 改用 `powershell.exe -NoProfile -ExecutionPolicy Bypass -File ...`。 | Host 脚本正常执行并通过。 | | |||
| | X4/X5 夹具初版 | `ST-LINK_CLI 3.5.0` 写 BSRR 返回码 4 | BSRR 是只写寄存器,旧 CLI 的写后回读校验不适用。改用 PI8/PE6 的 ODR 位带地址驱动 Y4/Y5。 | 独立电气扫描和完整 X4/X5 九项通过;判定为夹具工具限制,不计为产品失败。 | | |||
| | X4/X5 夹具审查 | 异常退出清理顺序不够安全 | 调整为先关闭 Y4/Y5,再停止运动、恢复配置,最后释放 GPIO;保证清理期间不会继续保持输入触发。 | 完整 X4/X5 九项重新执行并通过,最终 GPIO/输入状态复核通过。 | | |||
| 中间失败均保留在本报告中,但最终交付结论以当前基线重新执行的结果为准。 | |||
| ## 11. 明确未执行和结论边界 | |||
| 以下项目本轮**未执行**,不得从本报告的 PASS 推导为已验证: | |||
| - 未使用逻辑分析仪,因此没有验证端子真实占空比、每路真实边沿数、毛刺、窄脉冲、方向建立时间或段间物理波形连续性。 | |||
| - 未对 MCU 中断最坏响应时间和 100 kHz 下的 ISR 最坏时延做仪器测量。 | |||
| - 未做四轴同时运动或四轴联动;本实现只有一个逻辑 PLSR,Y0~Y3 是逐路选择。 | |||
| - 未做 USB CDC 与 Modbus/运动并发长稳。 | |||
| - 未做本次最小重写的长时间压力、断线重连或坏 CRC 长稳矩阵。 | |||
| - 未做真实 VBAT 断主电源测试。本轮实际执行的是调试器控制的 100 ms 硬复位;它能证明复位恢复路径,不能等价证明 VBAT/完全掉电保持。 | |||
| - Host 的持久化用例使用内存模拟;真实 Flash/复位结论仅来自第 9 节板测。 | |||
| - Host 直接调用寄存器 API,不证明 RTU 物理链路;真实 RTU 覆盖仅限第 7~9 节列出的板测。 | |||
| - 未用 IAR 8.40 构建;本轮实际验证版本为 IAR EWARM 8.3.4。 | |||
| ## 12. 最终状态 | |||
| 测试结束后目标板处于以下状态: | |||
| - 固件为 SHA256 `97956D9EF30A9B274140B9B82F412EDD77F9637F563FCC3F552BB4B140772343` 对应版本。 | |||
| - PLSR 状态为 IDLE,逻辑位置为 0。 | |||
| - 固定产品寄存器配置已恢复默认值。 | |||
| - Y4/Y5 夹具输出已关闭,PI8/PE6 已释放为输入高阻。 | |||
| - X4/X5 均读回低。 | |||
| - 最终 `defaults-verify` 和固定映射 smoke 均 PASS。 | |||
| 本报告没有引用旧实现的四轴、双 AB、长稳或逻辑分析仪结果作为本次重写证据。 | |||
| @@ -0,0 +1,248 @@ | |||
| ## 实现PLSR指令(明确需求版) | |||
| ## 一、任务要求 | |||
| 参考信捷PLC设备PLSR指令手册,实现下位机PLSR指令。 | |||
| ### 1、整体任务要求 | |||
| - 书写项目文档,包含书写规格书、方案书、测试大纲。 | |||
| - 代码管理清晰,以及编写规范。 | |||
| ### 2、PLSR指令任务要求 | |||
| * 设计PLSR指令实现功能; | |||
| * 可脱离仿真器运行; | |||
| * 实现1路PLSR功能,脉冲输出端子在Y0~Y3中可选择,方向端子固定Y12~Y15; | |||
| * 脉冲发送频率、发送个数准确; | |||
| * 能记录累计发送的脉冲个数,累计值可通过通信方式清零; | |||
| * 脉冲输出方式; | |||
| * 单相(脉冲+方向)脉冲,脉冲个数为正时,正向发脉冲,脉冲方向端子为ON;脉冲个数为负时,反向发脉冲,脉冲方向端子为OFF; | |||
| * 脉冲发送模式: | |||
| - 支持完成模式、后续模式。 | |||
| * 运行模式; | |||
| * 支持相对模式、绝对模式; | |||
| * 绝对模式时,如果设定的脉冲个数与累计的脉冲个数相等时,不动作; | |||
| * 加减速:支持直线加减速、S曲线加减速、正弦曲线加减速; | |||
| * 脉冲速度:支持设置起始速度、终止速度、脉冲默认速度(匀速) | |||
| * PLSR多段脉冲输出功能实现; | |||
| * 脉冲总段数范围:1~10段; | |||
| * 起始执行段数:脉冲指令从“起始执行段数”开始执行; | |||
| * 等待条件 | |||
| - 当前段脉冲发送完成后,进入等待,“等待条件”满足后,跳转至指定段; | |||
| - 等待条件包括:Wait时间、Wait信号(X点)、ACT时间、EXT信号(X点)、EXT信号或脉冲发送完成; | |||
| * 单段脉冲参数配置: | |||
| * 脉冲频率,范围:1HZ~100KHZ,脉冲频率在脉冲指令执行过程中修改后立即生效; | |||
| * 脉冲个数,范围:-2147483648~2147483647,设置负数表示反向发脉冲; | |||
| * 跳转段数,如果设置为0,表示默认跳转至下一段脉冲,最后一段脉冲的跳转段数设置为0,表示结束脉冲输出功能; | |||
| * 断电保存数据: | |||
| * 系统参数数据、记录数据可掉电保持; | |||
| * 上电后自动加载保存数据,并进行参数配置; | |||
| * 配置参数; | |||
| * 脉冲方向端子可选择; | |||
| * 脉冲输出端子可选择; | |||
| * 脉冲总段数可设置; | |||
| * 起始执行段数可设置; | |||
| * 运行模式可设置; | |||
| * 加减速模式可设置; | |||
| * 等待条件可设置; | |||
| * 加速时间、减速时间可设置; | |||
| * 第n段的脉冲频率、脉冲个数、跳转段数可设置(n:1~10); | |||
| * PLSR指令功能自测; | |||
| ### 4、通信任务要求 | |||
| - 使用一种通信方式(modbus)监控和实时修改PLSR的参数; | |||
| - 自制触摸屏或者上位机界面,实现PLSR题目涉及的参数读、写功能。(二选一,需要触摸屏可申领) | |||
| ### 5、可选要求 | |||
| * 实现PLSR输出AB相脉冲,可控制输出方向; | |||
| * 支持对输出的脉冲信号进行诊断: | |||
| - 诊断输出脉冲的个数是否与设置一致; | |||
| - 诊断输出脉冲的频率、加减速曲线是否与设置一致; | |||
| ## 二、时间要求 | |||
| 1个月(4周) | |||
| ## 三、任务器材和软件工具 | |||
| | 序号 | 器材 | 数量 | | |||
| | ---- | ---------------------------------------- | ---- | | |||
| | 1 | XDM-60T4-E/XDM-60T10-E | 1台 | | |||
| | 2 | ST-LINK | 1台 | | |||
| | 3 | 电源线 | 1根 | | |||
| | 4 | XVP线 | 1根 | | |||
| | 5 | USB转串 | 1个 | | |||
| | 6 | DB9F侧线 | 1根 | | |||
| | 7 | 编码器线CP-SP-M-03 | 1根 | | |||
| | 8 | 动力线CM-P15-01 | 1根 | | |||
| | 9 | 开关电源 | 1个 | | |||
| | 10 | 驱动器DF3E-0410 | 1个 | | |||
| | 11 | 电机MF3S-60CS30B1-504 | 1个 | | |||
| | | **6~11是伺服相关的设备,一组有一套即可** | | | |||
| | 软件工具 | 版本 | | |||
| | -------- | ---- | | |||
| | | | | |||
| | | | | |||
| | IAR8.40 | 8.40 | | |||
| ## 四、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 | | |||
| ## 六、Modbus 参数地址表 | |||
| 本节整理自《参数表-2026.xlsx》。表内地址为 Modbus 寄存器地址;所有双字参数均占用两个连续的 | |||
| 16 位寄存器,并按“低地址存低 16 位、高地址存高 16 位”的顺序保存。 | |||
| ### 1、公共配置参数 | |||
| | Modbus 地址 | 参数 | 取值及说明 | | |||
| | --- | --- | --- | | |||
| | `0x1000` | 脉冲发送端子 | `0`:Y0;`1`:Y1;`2`:Y2;`3`:Y3 | | |||
| | `0x1001` | 脉冲方向端子 | `0`:Y12;`1`:Y13;`2`:Y14;`3`:Y15 | | |||
| | `0x1002` | WAIT 信号 | `0`:X4;`1`:X5 | | |||
| | `0x1003` | EXT 信号 | `0`:X4;`1`:X5 | | |||
| | `0x1004` | 脉冲发送模式 | `0`:完成模式;`1`:后续模式 | | |||
| | `0x1005` | 脉冲方向延时时间 | 单位:ms | | |||
| | `0x1006` | 脉冲方向逻辑 | `0`:正逻辑;`1`:负逻辑 | | |||
| | `0x1007` | 脉冲加减速模式 | `0`:直线;`1`:S 曲线;`2`:正弦曲线 | | |||
| | `0x1008` | 运行模式 | `0`:相对模式;`1`:绝对模式 | | |||
| | `0x1009` | 脉冲总段数 | 范围:1~10 | | |||
| | `0x100A` | 起始执行段数 | 指令从指定段开始执行 | | |||
| | `0x100B~0x100C` | 脉冲默认速度 | 32 位,单位:Hz | | |||
| | `0x100D~0x100E` | 脉冲起始速度 | 32 位,单位:Hz | | |||
| | `0x1010~0x1011` | 脉冲终止速度 | 32 位,单位:Hz | | |||
| | `0x1012` | 默认速度加速时间 | 单位:ms | | |||
| | `0x1013` | 默认速度减速时间 | 单位:ms | | |||
| ### 2、单段参数布局 | |||
| 每段占用一个 `0x10` 寄存器跨度。第 `n` 段的基地址为: | |||
| ```text | |||
| segment_base = 0x1100 + (n - 1) × 0x10,n = 1~10 | |||
| ``` | |||
| | 相对偏移 | 参数 | 取值及说明 | | |||
| | --- | --- | --- | | |||
| | `+0~+1` | 第 n 段脉冲频率 | 32 位,单位:Hz,范围:1 Hz~100 kHz | | |||
| | `+2~+3` | 第 n 段脉冲个数 | 32 位有符号数;负数表示反向 | | |||
| | `+4` | 第 n 段等待条件 | `0`:WAIT 时间;`1`:WAIT 信号;`2`:ACT 时间;`3`:EXT 信号;`4`:EXT 信号或脉冲发送完成 | | |||
| | `+5` | WAIT 时间 | 单位:ms | | |||
| | `+6` | ACT 时间 | 单位:ms | | |||
| | `+7` | 第 n 段跳转编号 | `0` 表示默认跳转至下一段;最后一段为 `0` 时结束输出 | | |||
| 各段具体地址如下: | |||
| | 段号 | 脉冲频率 | 脉冲个数 | 等待条件 | WAIT 时间 | ACT 时间 | 跳转编号 | | |||
| | ---: | --- | --- | --- | --- | --- | --- | | |||
| | 1 | `0x1100~0x1101` | `0x1102~0x1103` | `0x1104` | `0x1105` | `0x1106` | `0x1107` | | |||
| | 2 | `0x1110~0x1111` | `0x1112~0x1113` | `0x1114` | `0x1115` | `0x1116` | `0x1117` | | |||
| | 3 | `0x1120~0x1121` | `0x1122~0x1123` | `0x1124` | `0x1125` | `0x1126` | `0x1127` | | |||
| | 4 | `0x1130~0x1131` | `0x1132~0x1133` | `0x1134` | `0x1135` | `0x1136` | `0x1137` | | |||
| | 5 | `0x1140~0x1141` | `0x1142~0x1143` | `0x1144` | `0x1145` | `0x1146` | `0x1147` | | |||
| | 6 | `0x1150~0x1151` | `0x1152~0x1153` | `0x1154` | `0x1155` | `0x1156` | `0x1157` | | |||
| | 7 | `0x1160~0x1161` | `0x1162~0x1163` | `0x1164` | `0x1165` | `0x1166` | `0x1167` | | |||
| | 8 | `0x1170~0x1171` | `0x1172~0x1173` | `0x1174` | `0x1175` | `0x1176` | `0x1177` | | |||
| | 9 | `0x1180~0x1181` | `0x1182~0x1183` | `0x1184` | `0x1185` | `0x1186` | `0x1187` | | |||
| | 10 | `0x1190~0x1191` | `0x1192~0x1193` | `0x1194` | `0x1195` | `0x1196` | `0x1197` | | |||
| ### 3、运行监控 | |||
| | Modbus 地址 | 参数 | 说明 | | |||
| | --- | --- | --- | | |||
| | `0x2000~0x2001` | 脉冲累计数量监控 | 32 位有符号数,可读取负数 | | |||
| | `0x2002~0x2003` | 当前脉冲发送频率 | 32 位 | | |||
| | `0x2004` | 运行状态 | 状态编码由实现定义并在通信协议中说明 | | |||
| | `0x2005` | 当前执行段 | 当前正在执行的段号 | | |||
| | `0x2006` | 错误码 | 错误码定义由实现说明 | | |||
| ### 4、控制寄存器 | |||
| | Modbus 地址 | 位 | 功能 | | |||
| | --- | ---: | --- | | |||
| | `0x3000` | bit0 | 启动脉冲输出 | | |||
| | `0x3000` | bit1 | 停止脉冲输出(可参考) | | |||
| | `0x3000` | bit2 | 清除状态信息及脉冲累计值 | | |||
| ### 5、附加功能通信 | |||
| AB 相输出和脉冲诊断属于附加题,若实现,所需寄存器及协议由项目另行设计,不占用或改变上述 | |||
| 基础功能地址和语义。 | |||
| @@ -226,6 +226,7 @@ | |||
| <name>CCDefines</name> | |||
| <state>USE_HAL_DRIVER</state> | |||
| <state>STM32F407xx</state> | |||
| <state>PLSR_DEBUG_TIMING=1</state> | |||
| </option> | |||
| <option> | |||
| <name>CCPreprocFile</name> | |||
| @@ -1306,6 +1307,35 @@ | |||
| </file> | |||
| <file> | |||
| <name>$PROJ_DIR$\..\PLSR\Src\plsr_platform_f407.c</name> | |||
| <configuration> | |||
| <name>Modbus</name> | |||
| <settings> | |||
| <name>ICCARM</name> | |||
| <data> | |||
| <version>35</version> | |||
| <wantNonLocal>0</wantNonLocal> | |||
| <debug>1</debug> | |||
| <option> | |||
| <name>CCOptLevel</name> | |||
| <state>3</state> | |||
| </option> | |||
| <option> | |||
| <name>CCOptStrategy</name> | |||
| <version>0</version> | |||
| <state>2</state> | |||
| </option> | |||
| <option> | |||
| <name>CCOptLevelSlave</name> | |||
| <state>3</state> | |||
| </option> | |||
| <option> | |||
| <name>CCOptStrategySlave</name> | |||
| <version>0</version> | |||
| <state>2</state> | |||
| </option> | |||
| </data> | |||
| </settings> | |||
| </configuration> | |||
| </file> | |||
| </group> | |||
| <group> | |||
| @@ -21,11 +21,25 @@ from plsr_modbus_product_test import ( | |||
| COMMON_WORDS, | |||
| CONFIG_BASE, | |||
| CONTROL, | |||
| DIAGNOSTIC_CLEAR, | |||
| DIAGNOSTIC_CONTROL, | |||
| DIAG_COUNT_CHECKED, | |||
| DIAG_COUNT_PASS, | |||
| DIAG_CURVE_CHECKED, | |||
| DIAG_CURVE_PASS, | |||
| DIAG_FAULT_LATCHED, | |||
| DIAG_FREQUENCY_CHECKED, | |||
| DIAG_FREQUENCY_PASS, | |||
| DIAG_MONITORING, | |||
| ModbusException, | |||
| OUTPUT_AB, | |||
| OUTPUT_MODE, | |||
| OUTPUT_PULSE_DIR, | |||
| RtuClient, | |||
| SEGMENT_1_BASE, | |||
| SEGMENT_WORDS, | |||
| TestFailure, | |||
| read_diagnostic, | |||
| read_status, | |||
| split_i32, | |||
| split_u32, | |||
| @@ -39,6 +53,11 @@ SEGMENT_COUNT = 10 | |||
| SEGMENT_STRIDE = 0x10 | |||
| MAX_FREQUENCY_HZ = 100_000 | |||
| OUTPUT_MODE_OPTIONS = ( | |||
| ("PULSE/DIR (0)", OUTPUT_PULSE_DIR), | |||
| ("AB 正交脉冲 (1)", OUTPUT_AB), | |||
| ) | |||
| PULSE_OPTIONS = ( | |||
| ("Y0 (0)", 0), | |||
| ("Y1 (1)", 1), | |||
| @@ -88,6 +107,13 @@ ERROR_NAMES = { | |||
| 8: "急停", | |||
| 9: "内部错误", | |||
| } | |||
| DIAGNOSTIC_REASON_NAMES = { | |||
| 0: "无故障", | |||
| 1: "计数", | |||
| 2: "频率", | |||
| 3: "曲线", | |||
| 4: "无效采样", | |||
| } | |||
| def option_labels(options): | |||
| @@ -126,6 +152,15 @@ def parse_integer(text, field_name, minimum, maximum): | |||
| return value | |||
| def write_common_configuration(client, words, output_mode): | |||
| """Order writes so the intermediate configuration is always valid.""" | |||
| if output_mode == OUTPUT_PULSE_DIR: | |||
| client.write_single(OUTPUT_MODE, OUTPUT_PULSE_DIR) | |||
| client.write_multiple(CONFIG_BASE, words) | |||
| if output_mode == OUTPUT_AB: | |||
| client.write_single(OUTPUT_MODE, OUTPUT_AB) | |||
| class SerialWorker(threading.Thread): | |||
| """Own the serial port and report all results through a queue.""" | |||
| @@ -166,11 +201,17 @@ class SerialWorker(threading.Thread): | |||
| def _read_configuration(self): | |||
| client = self._require_client() | |||
| common = client.read_holding(CONFIG_BASE, COMMON_WORDS) | |||
| output_mode = client.read_holding(OUTPUT_MODE, 1)[0] | |||
| segments = [] | |||
| for index in range(SEGMENT_COUNT): | |||
| address = SEGMENT_1_BASE + index * SEGMENT_STRIDE | |||
| segments.append(client.read_holding(address, SEGMENT_WORDS)) | |||
| self._emit("configuration", common=common, segments=segments) | |||
| self._emit( | |||
| "configuration", | |||
| common=common, | |||
| output_mode=output_mode, | |||
| segments=segments, | |||
| ) | |||
| def _handle_connection_error(self, operation, error): | |||
| self._disconnect(notify=True, reason=str(error)) | |||
| @@ -215,9 +256,12 @@ class SerialWorker(threading.Thread): | |||
| self._read_configuration() | |||
| self._emit("operation", message="参数读取完成") | |||
| elif command == "write_common": | |||
| client.write_multiple(CONFIG_BASE, payload["words"]) | |||
| write_common_configuration( | |||
| client, payload["words"], payload["output_mode"] | |||
| ) | |||
| common = client.read_holding(CONFIG_BASE, COMMON_WORDS) | |||
| self._emit("common", words=common) | |||
| output_mode = client.read_holding(OUTPUT_MODE, 1)[0] | |||
| self._emit("common", words=common, output_mode=output_mode) | |||
| self._emit("operation", message="公共参数写入并回读完成") | |||
| elif command == "write_segments": | |||
| for index, words in enumerate(payload["segments"]): | |||
| @@ -232,6 +276,10 @@ class SerialWorker(threading.Thread): | |||
| elif command == "control": | |||
| client.write_single(CONTROL, payload["value"]) | |||
| self._emit("operation", message=payload["message"]) | |||
| elif command == "clear_diagnostic": | |||
| client.write_single(DIAGNOSTIC_CONTROL, DIAGNOSTIC_CLEAR) | |||
| self._emit("diagnostic", diagnostic=read_diagnostic(client)) | |||
| self._emit("operation", message="诊断锁存和统计已清除") | |||
| else: | |||
| raise RuntimeError("未知后台命令: %s" % command) | |||
| except (OSError, serial.SerialException) as error: | |||
| @@ -247,7 +295,9 @@ class SerialWorker(threading.Thread): | |||
| def _poll_status(self): | |||
| try: | |||
| status = read_status(self._require_client()) | |||
| diagnostic = read_diagnostic(self._require_client()) | |||
| self._emit("status", status=status) | |||
| self._emit("diagnostic", diagnostic=diagnostic) | |||
| self.next_status_poll = time.monotonic() + STATUS_POLL_SECONDS | |||
| except (OSError, serial.SerialException) as error: | |||
| self._handle_connection_error("状态轮询", error) | |||
| @@ -304,6 +354,16 @@ class PlsrControlPanel: | |||
| "segment": tk.StringVar(value="--"), | |||
| "error": tk.StringVar(value="--"), | |||
| } | |||
| self.diagnostic_vars = { | |||
| key: tk.StringVar(value="--") | |||
| for key in ( | |||
| "summary", "reason", "segment", "mode_direction", | |||
| "expected_count", "actual_count", "count_error", | |||
| "requested_hz", "expected_hz", "active_hz", | |||
| "request_error", "sample_count", "mismatch_count", | |||
| "maximum_error", "first_mismatch", | |||
| ) | |||
| } | |||
| self.common_vars = {} | |||
| self.segment_vars = [] | |||
| self.connection_widgets = [] | |||
| @@ -366,10 +426,13 @@ class PlsrControlPanel: | |||
| notebook.grid(row=2, column=0, padx=10, pady=6, sticky="nsew") | |||
| common_tab = ttk.Frame(notebook, padding=12) | |||
| segment_tab = ttk.Frame(notebook, padding=12) | |||
| diagnostic_tab = ttk.Frame(notebook, padding=12) | |||
| notebook.add(common_tab, text="公共参数 0x1000-0x1013") | |||
| notebook.add(segment_tab, text="10 段参数 0x1100-0x1197") | |||
| notebook.add(diagnostic_tab, text="输出诊断 0x2100-0x2119") | |||
| self._build_common_tab(common_tab) | |||
| self._build_segment_tab(segment_tab) | |||
| self._build_diagnostic_tab(diagnostic_tab) | |||
| footer = ttk.Frame(self.root) | |||
| footer.grid(row=3, column=0, padx=10, pady=(4, 10), sticky="ew") | |||
| @@ -480,6 +543,49 @@ class PlsrControlPanel: | |||
| self._add_entry(parent, 4, 2, "acceleration", "加速时间 ms") | |||
| self._add_entry(parent, 5, 0, "deceleration", "减速时间 ms") | |||
| self._add_combo( | |||
| parent, 5, 1, "output_mode", "输出模式 0x1200", OUTPUT_MODE_OPTIONS | |||
| ) | |||
| def _build_diagnostic_tab(self, parent): | |||
| parent.columnconfigure(1, weight=1) | |||
| parent.columnconfigure(3, weight=1) | |||
| parent.columnconfigure(5, weight=1) | |||
| fields = ( | |||
| ("综合结果", "summary"), | |||
| ("首个故障", "reason"), | |||
| ("段号", "segment"), | |||
| ("模式 / 方向", "mode_direction"), | |||
| ("期望脉冲", "expected_count"), | |||
| ("实测脉冲", "actual_count"), | |||
| ("计数误差", "count_error"), | |||
| ("请求频率", "requested_hz"), | |||
| ("量化频率", "expected_hz"), | |||
| ("活动频率", "active_hz"), | |||
| ("请求量化误差", "request_error"), | |||
| ("曲线采样数", "sample_count"), | |||
| ("不匹配数", "mismatch_count"), | |||
| ("最大频率误差", "maximum_error"), | |||
| ("首个不匹配样本", "first_mismatch"), | |||
| ) | |||
| for index, (label, key) in enumerate(fields): | |||
| row = index // 3 | |||
| group = index % 3 | |||
| column = group * 2 | |||
| ttk.Label(parent, text=label).grid( | |||
| row=row, column=column, padx=(8, 4), pady=9, sticky="e" | |||
| ) | |||
| ttk.Label( | |||
| parent, | |||
| textvariable=self.diagnostic_vars[key], | |||
| style="StatusValue.TLabel" if key == "summary" else "TLabel", | |||
| ).grid(row=row, column=column + 1, padx=(4, 18), pady=9, sticky="w") | |||
| clear_button = ttk.Button( | |||
| parent, text="清除诊断", command=self._clear_diagnostic | |||
| ) | |||
| clear_button.grid(row=5, column=5, padx=8, pady=(16, 4), sticky="e") | |||
| self.action_buttons.append(clear_button) | |||
| def _build_segment_tab(self, parent): | |||
| headers = ( | |||
| @@ -563,6 +669,8 @@ class PlsrControlPanel: | |||
| ) | |||
| for variable in self.status_vars.values(): | |||
| variable.set("--") | |||
| for variable in self.diagnostic_vars.values(): | |||
| variable.set("--") | |||
| self._update_action_state() | |||
| def _update_action_state(self): | |||
| @@ -738,12 +846,22 @@ class PlsrControlPanel: | |||
| def _write_common(self): | |||
| try: | |||
| words = self._common_words() | |||
| output_mode = option_value( | |||
| OUTPUT_MODE_OPTIONS, | |||
| self.common_vars["output_mode"].get(), | |||
| "输出模式", | |||
| ) | |||
| if output_mode == OUTPUT_AB and words[0] not in (0, 2): | |||
| raise ValueError("AB 模式的脉冲输出只能选择 Y0/Y1 或 Y2/Y3") | |||
| except ValueError as error: | |||
| messagebox.showerror("参数错误", str(error)) | |||
| return | |||
| if self._start_operation("正在写入公共参数"): | |||
| self.worker.submit( | |||
| "write_common", words=words, operation="写入公共参数" | |||
| "write_common", | |||
| words=words, | |||
| output_mode=output_mode, | |||
| operation="写入公共参数", | |||
| ) | |||
| def _write_segments(self): | |||
| @@ -775,6 +893,12 @@ class PlsrControlPanel: | |||
| if messagebox.askyesno("确认清零", "确认将累计位置清零?"): | |||
| self._send_control(COMMAND_CLEAR) | |||
| def _clear_diagnostic(self): | |||
| if self._start_operation("正在清除诊断锁存和统计"): | |||
| self.worker.submit( | |||
| "clear_diagnostic", operation="清除诊断" | |||
| ) | |||
| def _show_status(self, status): | |||
| self.status_vars["position"].set(str(status.position)) | |||
| self.status_vars["frequency"].set("%d Hz" % status.frequency_hz) | |||
| @@ -786,6 +910,51 @@ class PlsrControlPanel: | |||
| "%s (%d)" % (ERROR_NAMES.get(status.error, "未知"), status.error) | |||
| ) | |||
| def _show_diagnostic(self, diagnostic): | |||
| def check_name(checked_bit, pass_bit): | |||
| if not diagnostic.flags & checked_bit: | |||
| return "未检查" | |||
| return "通过" if diagnostic.flags & pass_bit else "失败" | |||
| checks = "计数 %s / 频率 %s / 曲线 %s" % ( | |||
| check_name(DIAG_COUNT_CHECKED, DIAG_COUNT_PASS), | |||
| check_name(DIAG_FREQUENCY_CHECKED, DIAG_FREQUENCY_PASS), | |||
| check_name(DIAG_CURVE_CHECKED, DIAG_CURVE_PASS), | |||
| ) | |||
| if diagnostic.flags & DIAG_MONITORING: | |||
| checks = "监测中 / " + checks | |||
| if diagnostic.flags & DIAG_FAULT_LATCHED: | |||
| checks = "故障锁存 / " + checks | |||
| direction = "正向" if diagnostic.direction_positive else "反向" | |||
| mode = "AB" if diagnostic.output_mode == OUTPUT_AB else "PULSE/DIR" | |||
| first_mismatch = ( | |||
| "无" if diagnostic.first_mismatch_sample == 0xFFFFFFFF | |||
| else str(diagnostic.first_mismatch_sample) | |||
| ) | |||
| values = self.diagnostic_vars | |||
| values["summary"].set(checks) | |||
| values["reason"].set( | |||
| "%s (%d)" % ( | |||
| DIAGNOSTIC_REASON_NAMES.get(diagnostic.reason, "未知"), | |||
| diagnostic.reason, | |||
| ) | |||
| ) | |||
| values["segment"].set(str(diagnostic.segment)) | |||
| values["mode_direction"].set("%s / %s" % (mode, direction)) | |||
| values["expected_count"].set(str(diagnostic.expected_pulses)) | |||
| values["actual_count"].set(str(diagnostic.actual_pulses)) | |||
| values["count_error"].set(str(diagnostic.count_error)) | |||
| values["requested_hz"].set("%d Hz" % diagnostic.requested_hz) | |||
| values["expected_hz"].set("%d Hz" % diagnostic.expected_timer_hz) | |||
| values["active_hz"].set("%d Hz" % diagnostic.active_timer_hz) | |||
| values["request_error"].set("%d Hz" % diagnostic.request_error_hz) | |||
| values["sample_count"].set(str(diagnostic.sample_count)) | |||
| values["mismatch_count"].set(str(diagnostic.mismatch_count)) | |||
| values["maximum_error"].set( | |||
| "%d Hz" % diagnostic.maximum_frequency_error_hz | |||
| ) | |||
| values["first_mismatch"].set(first_mismatch) | |||
| def _handle_result(self, event, payload): | |||
| if event == "connection": | |||
| if payload["connected"]: | |||
| @@ -800,14 +969,22 @@ class PlsrControlPanel: | |||
| self.footer_var.set("连接已断开" if not reason else "通信错误") | |||
| elif event == "configuration": | |||
| self._set_common_words(payload["common"]) | |||
| self.common_vars["output_mode"].set( | |||
| option_label(OUTPUT_MODE_OPTIONS, payload["output_mode"]) | |||
| ) | |||
| self._set_segment_words(payload["segments"]) | |||
| self._finish_operation("全部参数已读取") | |||
| elif event == "common": | |||
| self._set_common_words(payload["words"]) | |||
| self.common_vars["output_mode"].set( | |||
| option_label(OUTPUT_MODE_OPTIONS, payload["output_mode"]) | |||
| ) | |||
| elif event == "segments": | |||
| self._set_segment_words(payload["words"]) | |||
| elif event == "status": | |||
| self._show_status(payload["status"]) | |||
| elif event == "diagnostic": | |||
| self._show_diagnostic(payload["diagnostic"]) | |||
| elif event == "operation": | |||
| self._finish_operation(payload["message"]) | |||
| elif event == "error": | |||
| @@ -845,6 +1022,17 @@ class _SelfTestVariable: | |||
| self.value = str(value) | |||
| class _SelfTestClient: | |||
| def __init__(self): | |||
| self.calls = [] | |||
| def write_single(self, address, value): | |||
| self.calls.append(("single", address, value)) | |||
| def write_multiple(self, address, values): | |||
| self.calls.append(("multiple", address, list(values))) | |||
| def self_test(): | |||
| assert BAUD_RATE == 9600 | |||
| assert INPUT_OPTIONS == (("X4 (0)", 0), ("X5 (1)", 1)) | |||
| @@ -879,10 +1067,26 @@ def self_test(): | |||
| "stop_speed": _SelfTestVariable(65536), | |||
| "acceleration": _SelfTestVariable(1000), | |||
| "deceleration": _SelfTestVariable(2000), | |||
| "output_mode": _SelfTestVariable(OUTPUT_MODE_OPTIONS[1][0]), | |||
| } | |||
| common = panel._common_words() | |||
| assert common[:11] == [3, 3, 1, 0, 1, 25, 1, 2, 1, 10, 2] | |||
| assert common[11:20] == [34464, 1, 0, 0, 0, 0, 1, 1000, 2000] | |||
| assert option_value( | |||
| OUTPUT_MODE_OPTIONS, | |||
| panel.common_vars["output_mode"].get(), | |||
| "输出模式", | |||
| ) == OUTPUT_AB | |||
| assert OUTPUT_MODE == 0x1200 | |||
| assert DIAGNOSTIC_CONTROL == 0x3100 | |||
| client = _SelfTestClient() | |||
| write_common_configuration(client, common, OUTPUT_PULSE_DIR) | |||
| assert client.calls[0] == ("single", OUTPUT_MODE, OUTPUT_PULSE_DIR) | |||
| assert client.calls[1][0:2] == ("multiple", CONFIG_BASE) | |||
| client = _SelfTestClient() | |||
| write_common_configuration(client, common, OUTPUT_AB) | |||
| assert client.calls[0][0:2] == ("multiple", CONFIG_BASE) | |||
| assert client.calls[1] == ("single", OUTPUT_MODE, OUTPUT_AB) | |||
| panel.segment_vars = [] | |||
| for index in range(SEGMENT_COUNT): | |||
| @@ -9,13 +9,12 @@ | |||
| #define APP_TASK_TEST_PRIO 6u | |||
| /* | |||
| * 任务栈大小的单位是OS_STK元素。当前OS_STK为32位, | |||
| * 所以每个256元素的任务栈实际占用1024字节RAM。 | |||
| * 任务栈大小的单位是OS_STK元素。当前OS_STK为32位。 | |||
| */ | |||
| #define APP_TASK_START_STK_SIZE 256u | |||
| #define APP_TASK_START_STK_SIZE 512u | |||
| #define APP_TASK_TEST_STK_SIZE 256u | |||
| /* The short-profile pulse IRQ needs more than the port's 128-word default. */ | |||
| #define OS_CPU_EXCEPT_STK_SIZE 256u | |||
| #define OS_CPU_EXCEPT_STK_SIZE 512u | |||
| #endif | |||
| @@ -13,6 +13,7 @@ extern "C" { | |||
| HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart, | |||
| uint8_t slaveAddress); | |||
| void ModbusSlavePoll(void); | |||
| uint8_t ModbusSlaveIsIdle(void); | |||
| void ModbusSlaveOnRxEvent(UART_HandleTypeDef *huart, uint16_t size); | |||
| void ModbusSlaveOnTxComplete(UART_HandleTypeDef *huart); | |||
| void ModbusSlaveOnUartError(UART_HandleTypeDef *huart); | |||
| @@ -39,6 +39,7 @@ static volatile uint32_t ModbusRxLastByteCycle; | |||
| static volatile uint16_t ModbusRxFrameLength; | |||
| static volatile uint8_t ModbusRxFrameReady; | |||
| static volatile uint8_t ModbusTxBusy; | |||
| static volatile uint32_t ModbusLastActivityCycle; | |||
| static uint32_t ModbusRtuCharCycles; | |||
| static uint32_t ModbusRtuT15Cycles; | |||
| @@ -451,6 +452,7 @@ HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart, | |||
| ModbusRxFrameReady = 0U; | |||
| ModbusTxBusy = 0U; | |||
| ModbusRtuTimingInit(huart->Init.BaudRate); | |||
| ModbusLastActivityCycle = DWT->CYCCNT; | |||
| return ModbusStartReceive(); | |||
| } | |||
| @@ -496,6 +498,42 @@ void ModbusSlavePoll(void) | |||
| } | |||
| } | |||
| uint8_t ModbusSlaveIsIdle(void) | |||
| { | |||
| uint32_t primask; | |||
| uint32_t now; | |||
| uint8_t isIdle = 0U; | |||
| if ((ModbusUart == NULL) || (ModbusUart->hdmarx == NULL)) | |||
| { | |||
| return 0U; | |||
| } | |||
| primask = ModbusEnterCritical(); | |||
| if ((ModbusRxAssemblyLength == 0U) | |||
| && (ModbusRxAssemblyInvalid == 0U) | |||
| && (ModbusRxFrameReady == 0U) | |||
| && (ModbusTxBusy == 0U) | |||
| && (ModbusUart->RxState == HAL_UART_STATE_BUSY_RX) | |||
| && ((ModbusUart->Instance->CR3 & USART_CR3_DMAR) != 0U) | |||
| && ((ModbusUart->hdmarx->Instance->CR & DMA_SxCR_EN) != 0U) | |||
| && (__HAL_DMA_GET_COUNTER(ModbusUart->hdmarx) | |||
| == MODBUS_RTU_ADU_SIZE_MAX) | |||
| && ((ModbusUart->Instance->SR | |||
| & (USART_SR_PE | USART_SR_FE | USART_SR_NE | USART_SR_ORE | |||
| | USART_SR_IDLE | USART_SR_RXNE)) == 0U)) | |||
| { | |||
| now = DWT->CYCCNT; | |||
| if ((uint32_t)(now - ModbusLastActivityCycle) | |||
| >= ModbusRtuT35Cycles) | |||
| { | |||
| isIdle = 1U; | |||
| } | |||
| } | |||
| ModbusExitCritical(primask); | |||
| return isIdle; | |||
| } | |||
| void ModbusSlaveOnRxEvent(UART_HandleTypeDef *huart, uint16_t size) | |||
| { | |||
| HAL_UART_RxEventTypeTypeDef eventType; | |||
| @@ -510,6 +548,8 @@ void ModbusSlaveOnRxEvent(UART_HandleTypeDef *huart, uint16_t size) | |||
| return; | |||
| } | |||
| now = DWT->CYCCNT; | |||
| ModbusLastActivityCycle = now; | |||
| eventType = HAL_UARTEx_GetRxEventType(huart); | |||
| if (eventType == HAL_UART_RXEVENT_HT) | |||
| { | |||
| @@ -524,7 +564,6 @@ void ModbusSlaveOnRxEvent(UART_HandleTypeDef *huart, uint16_t size) | |||
| return; | |||
| } | |||
| now = DWT->CYCCNT; | |||
| lastByteCycle = now; | |||
| if (eventType == HAL_UART_RXEVENT_IDLE) | |||
| { | |||
| @@ -571,6 +610,7 @@ void ModbusSlaveOnTxComplete(UART_HandleTypeDef *huart) | |||
| return; | |||
| } | |||
| ModbusLastActivityCycle = DWT->CYCCNT; | |||
| ModbusTxBusy = 0U; | |||
| (void)ModbusStartReceive(); | |||
| } | |||
| @@ -582,6 +622,7 @@ void ModbusSlaveOnUartError(UART_HandleTypeDef *huart) | |||
| return; | |||
| } | |||
| ModbusLastActivityCycle = DWT->CYCCNT; | |||
| ModbusTxBusy = 0U; | |||
| ModbusRxAssemblyLength = 0U; | |||
| ModbusRxAssemblyInvalid = 0U; | |||
| @@ -13,6 +13,11 @@ extern "C" { | |||
| #define PLSR_STATUS_LAST_ADDRESS (0x2006U) | |||
| #define PLSR_CONTROL_ADDRESS (0x3000U) | |||
| #define PLSR_OUTPUT_MODE_ADDRESS (0x1200U) | |||
| #define PLSR_DIAGNOSTIC_FIRST_ADDRESS (0x2100U) | |||
| #define PLSR_DIAGNOSTIC_LAST_ADDRESS (0x2119U) | |||
| #define PLSR_DIAGNOSTIC_CONTROL_ADDRESS (0x3100U) | |||
| typedef enum | |||
| { | |||
| PLSR_MB_NOT_HANDLED = 0, | |||
| @@ -53,6 +58,7 @@ typedef enum | |||
| uint8_t PlsrInit(void); | |||
| void PlsrPoll1ms(void); | |||
| void PlsrServicePersistence(void); | |||
| PLSR_MB_RESULT PlsrModbusReadHolding(uint16_t startAddress, | |||
| uint16_t quantity, | |||
| @@ -63,6 +69,7 @@ PLSR_MB_RESULT PlsrModbusWriteHolding(uint16_t startAddress, | |||
| /* Called by the selected output timer update interrupt once per pulse. */ | |||
| void PlsrPulseTimerIrq(uint8_t pulseOutput); | |||
| void PlsrFinalArmJobIrq(uint8_t pulseOutput); | |||
| #ifdef PLSR_HOST_TEST | |||
| uint64_t PlsrTestDivideU64ByU32(uint64_t dividend, | |||
| @@ -74,19 +81,52 @@ void PlsrTestEmitPulseOnCriticalEntry(void); | |||
| void PlsrTestEmitPulseAfterCriticalEntries(uint8_t entriesToSkip); | |||
| void PlsrTestEmitPulseOnCriticalExit(void); | |||
| void PlsrTestLatchPulseOnCriticalEntry(void); | |||
| void PlsrTestLatchAbFinalQuarterOnNextStopArm(void); | |||
| void PlsrTestCompleteAbCycleOnNextQueueCommit(void); | |||
| void PlsrTestServicePendingPulse(void); | |||
| void PlsrTestServiceFinalArmJob(void); | |||
| void PlsrTestDeferFinalArmJob(uint8_t defer); | |||
| void PlsrTestFailNextStart(void); | |||
| void PlsrTestStaleNextFrequencyAtUpdate(void); | |||
| void PlsrTestFailNextFrequencyAtUpdate(void); | |||
| void PlsrTestFailNextStopRequest(void); | |||
| void PlsrTestEmitAbQuarters(uint32_t quarterCount); | |||
| uint8_t PlsrTestPulseIsActive(void); | |||
| uint32_t PlsrTestOutputFrequency(void); | |||
| uint32_t PlsrTestQueuedFrequency(void); | |||
| uint8_t PlsrTestDirectionLevel(void); | |||
| uint8_t PlsrTestDirectionPinLevel(uint8_t directionOutput); | |||
| uint32_t PlsrTestDirectionWriteCount(uint8_t directionOutput); | |||
| uint32_t PlsrTestDirectionTransitionCount(uint8_t directionOutput); | |||
| uint8_t PlsrTestAbPhase(void); | |||
| uint32_t PlsrTestAbTransitionCount(void); | |||
| uint32_t PlsrTestAbFastGateCount(void); | |||
| uint32_t PlsrTestAbCleanupCount(void); | |||
| void PlsrTestInjectCountOffset(int32_t offset); | |||
| void PlsrTestInjectActiveFrequencyOffset(int32_t offsetHz); | |||
| void PlsrTestInjectCurveMismatch(void); | |||
| void PlsrTestForceProfileQueueEmpty(void); | |||
| uint16_t PlsrTestProfileQueueCount(void); | |||
| uint32_t PlsrTestProfileQueueGeneration(void); | |||
| uint32_t PlsrTestProfileQueueReadIndex(void); | |||
| uint32_t PlsrTestProfileQueueWriteIndex(void); | |||
| uint32_t PlsrTestProfileQueueHeadFrequency(void); | |||
| uint16_t PlsrTestProfileProducerNextPeriod(void); | |||
| uint16_t PlsrTestActiveProfileNextPeriod(void); | |||
| uint32_t PlsrTestLastGateEvent(void); | |||
| uint32_t PlsrTestLastDiagnosticFinishEvent(void); | |||
| void PlsrTestSetPosition(int32_t position, uint8_t positionValid); | |||
| void PlsrTestClearPersistentStorage(void); | |||
| void PlsrTestResetSaveCount(void); | |||
| uint32_t PlsrTestSaveCount(void); | |||
| #endif | |||
| #if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) | |||
| extern volatile uint32_t PlsrProfileProducerItemCount; | |||
| extern volatile uint32_t PlsrProfileProducerTotalCycles; | |||
| extern volatile uint32_t PlsrProfileProducerMaxItemCycles; | |||
| #endif | |||
| #ifdef __cplusplus | |||
| } | |||
| #endif | |||
| @@ -6,6 +6,9 @@ | |||
| #define PLSR_SEGMENT_COUNT_MAX (10U) | |||
| #define PLSR_FREQUENCY_MAX_HZ (100000UL) | |||
| #define PLSR_OUTPUT_PULSE_DIR (0U) | |||
| #define PLSR_OUTPUT_AB (1U) | |||
| typedef struct | |||
| { | |||
| uint32_t frequencyHz; | |||
| @@ -35,6 +38,8 @@ typedef struct | |||
| uint16_t accelerationTimeMs; | |||
| uint16_t decelerationTimeMs; | |||
| PLSR_SEGMENT_CONFIG segments[PLSR_SEGMENT_COUNT_MAX]; | |||
| /* Kept at the end so version-2 persisted configurations remain a prefix. */ | |||
| uint16_t outputMode; | |||
| } PLSR_CONFIG; | |||
| typedef struct | |||
| @@ -4,29 +4,92 @@ | |||
| #include "plsr_internal.h" | |||
| #include <stdint.h> | |||
| typedef enum | |||
| { | |||
| PLSR_PLATFORM_STOP_COMPLETE = 0, | |||
| PLSR_PLATFORM_STOP_PENDING, | |||
| PLSR_PLATFORM_STOP_FORCED_FAULT | |||
| } PLSR_PLATFORM_STOP_RESULT; | |||
| typedef enum | |||
| { | |||
| PLSR_PLATFORM_QUEUE_FAILED = 0, | |||
| PLSR_PLATFORM_QUEUE_APPLIED, | |||
| PLSR_PLATFORM_QUEUE_STALE | |||
| } PLSR_PLATFORM_QUEUE_RESULT; | |||
| typedef enum | |||
| { | |||
| PLSR_PLATFORM_SERVICE_FAILED = 0, | |||
| PLSR_PLATFORM_SERVICE_READY, | |||
| PLSR_PLATFORM_SERVICE_DEFERRED | |||
| } PLSR_PLATFORM_SERVICE_RESULT; | |||
| typedef struct | |||
| { | |||
| uint32_t actualFrequencyHz; | |||
| uint16_t prescaler; | |||
| uint16_t pairPrescaler; | |||
| uint16_t period; | |||
| uint16_t compare; | |||
| } PLSR_PLATFORM_TIMER_SETTING; | |||
| uint8_t PlsrPlatformInit(void); | |||
| uint8_t PlsrPlatformPrepare(uint8_t pulseOutput, | |||
| uint8_t directionOutput, | |||
| uint8_t directionLevel); | |||
| uint8_t directionLevel, | |||
| uint8_t outputMode, | |||
| uint8_t directionPositive); | |||
| uint8_t PlsrPlatformStartPulse(uint8_t pulseOutput, | |||
| uint32_t firstFrequencyHz, | |||
| uint32_t queuedFrequencyHz, | |||
| uint32_t *actualFirstFrequencyHz, | |||
| uint32_t *actualQueuedFrequencyHz); | |||
| uint8_t PlsrPlatformQueueFrequency(uint8_t pulseOutput, | |||
| uint32_t frequencyHz, | |||
| uint32_t *actualFrequencyHz); | |||
| uint8_t PlsrPlatformBuildTimerSetting( | |||
| uint8_t pulseOutput, | |||
| uint8_t outputMode, | |||
| uint32_t requestedFrequencyHz, | |||
| PLSR_PLATFORM_TIMER_SETTING *setting); | |||
| uint8_t PlsrPlatformStartPrepared( | |||
| uint8_t pulseOutput, | |||
| const PLSR_PLATFORM_TIMER_SETTING *firstSetting, | |||
| const PLSR_PLATFORM_TIMER_SETTING *queuedSetting, | |||
| uint32_t *actualFirstFrequencyHz, | |||
| uint32_t *actualQueuedFrequencyHz); | |||
| PLSR_PLATFORM_QUEUE_RESULT PlsrPlatformLoadPreparedFromIrq( | |||
| uint8_t pulseOutput, | |||
| const PLSR_PLATFORM_TIMER_SETTING *setting, | |||
| uint32_t *actualFrequencyHz); | |||
| void PlsrPlatformGateFromIrq(uint8_t pulseOutput); | |||
| PLSR_PLATFORM_QUEUE_RESULT PlsrPlatformQueueFrequency( | |||
| uint8_t pulseOutput, | |||
| uint32_t frequencyHz, | |||
| uint32_t *actualFrequencyHz); | |||
| void PlsrPlatformDrainPendingPulse(uint8_t pulseOutput); | |||
| uint32_t PlsrPlatformActiveFrequency(uint8_t pulseOutput); | |||
| uint8_t PlsrPlatformExpectedFrequency(uint8_t pulseOutput, | |||
| uint8_t outputMode, | |||
| uint32_t requestedFrequencyHz, | |||
| uint32_t *actualFrequencyHz); | |||
| uint64_t PlsrPlatformObservedPulses(uint8_t pulseOutput); | |||
| /* Returns 0, PLSR diagnostic reason 2 (frequency), or 3 (curve/order). */ | |||
| uint16_t PlsrPlatformDiagnosticFault(void); | |||
| /* Caller must hold the platform critical section. */ | |||
| PLSR_PLATFORM_STOP_RESULT PlsrPlatformRequestStopLocked( | |||
| uint8_t pulseOutput, | |||
| uint8_t requireZeroBoundary); | |||
| uint8_t PlsrPlatformQueueFinalArmFromIrq(uint8_t pulseOutput); | |||
| void PlsrPlatformStopPulse(uint8_t pulseOutput); | |||
| uint8_t PlsrPlatformReadInput(uint8_t inputSelection); | |||
| uint8_t PlsrPlatformLoad(PLSR_PERSIST_PAYLOAD *payload); | |||
| PLSR_PLATFORM_SERVICE_RESULT PlsrPlatformServicePersistence(void); | |||
| uint8_t PlsrPlatformSave(const PLSR_PERSIST_PAYLOAD *payload); | |||
| void PlsrPlatformCheckpointConfig(const PLSR_CONFIG *config); | |||
| void PlsrPlatformCheckpointPosition(int32_t position, | |||
| uint8_t positionValid, | |||
| uint8_t wasBusy); | |||
| void PlsrPlatformForceSafeOutputsFromFault(void); | |||
| uint32_t PlsrPlatformEnterCritical(void); | |||
| void PlsrPlatformExitCritical(uint32_t state); | |||