P14 硬件计数与四轴 100kHz 压力: - TIM9/TIM12 硬件计数接入(Q0/Q2、Q1/Q3 共享,16 位溢出扩展 counterBlockPulses,目标脉冲 CC1IF 判定) - 计数资源租约与软件回退(Q2/Q3 走源 UIF),counter_stress 脚本 + 规格文档 P15 软限位边界: - 制动距离改用真实硬件输出频率,2000Hz 高频采样提前 1 脉冲进入减速 - 四轴正/负 500/2000Hz 矩阵测试(验收 ±1 脉冲),soft_limit_matrix 脚本 P16 性能遥测: - PlsrProcess/TIM6/输出 ISR/计数 ISR 最坏执行时间遥测(DWT cycles + 分段峰值) - performance_test 脚本 + 规格文档(全部预算 PASS) 其余: - Modbus 命令扩展:SAVE/LOAD_CONFIG 持久化命令、错误码 5/6、RESET_ERROR - Flash 双槽 CRC+代数持久化(SFD900~1419,SAVE/LOAD 已接命令口) - CW/CCW 段尾停止机制(cwStopPending,物理下降沿停止) - AB 启动预热机制(abStartupPriming,首次启动 ISR 内释放引脚) - USB CDC 运行时诊断(Rx 重装失败计数等) - 上电自测改由 plsr_build_config.h 宏开关控制 - 新增真机测试脚本/文档:AB_STRESS、BIT_INPUT、LONG_STRESS、PERSISTENCE_BOARD - 交接提示词与问题总清单(2026-08-10)master
| @@ -18,6 +18,11 @@ | |||
| # Editor and operating-system files | |||
| .vscode/ | |||
| .idea/ | |||
| **/__pycache__/ | |||
| *.pyc | |||
| HostComputer/build/ | |||
| HostComputer/dist/ | |||
| HostComputer/long_stress_logs/ | |||
| *.user | |||
| *.suo | |||
| *.tmp | |||
| @@ -26,6 +26,7 @@ | |||
| #include "modbus_rtu_slave.h" | |||
| #include "plc_device.h" | |||
| #include "plsr_core.h" | |||
| #include "plsr_build_config.h" | |||
| #include "plsr_modbus_control.h" | |||
| #include "plsr_self_test.h" | |||
| #include "stdio.h" | |||
| @@ -38,12 +39,6 @@ | |||
| /* Private define ------------------------------------------------------------*/ | |||
| /* USER CODE BEGIN PD */ | |||
| #define PLSR_BOARD_TEST_CW_CCW (9U) | |||
| #define PLSR_BOARD_TEST_FAST_REFRESH (10U) | |||
| #define PLSR_BOARD_TEST_DYNAMIC_FREQ (11U) | |||
| #define PLSR_BOARD_TEST_MODBUS_DATA (12U) | |||
| #define PLSR_BOARD_TEST_MODBUS_CONTROL (13U) | |||
| #define PLSR_BOARD_TEST_SELECT PLSR_BOARD_TEST_MODBUS_CONTROL | |||
| #define PLSR_MODBUS_CONTROL_TEST_BASE (1200U) | |||
| /* USER CODE END PD */ | |||
| @@ -86,8 +81,12 @@ static void AppTaskStart(void *pArg) | |||
| /* | |||
| * F4作为Modbus RTU从站,触摸屏作为主站 | |||
| * 初始化函数会立即启动USART1的DMA空闲接收 | |||
| */ | |||
| (void)ModbusSlaveInit(&huart1, MODBUS_SLAVE_DEFAULT_ADDRESS); | |||
| */ | |||
| if (ModbusSlaveInit(&huart1, MODBUS_SLAVE_DEFAULT_ADDRESS) != HAL_OK) | |||
| { | |||
| Error_Handler(); | |||
| } | |||
| #if PLSR_ENABLE_BOARD_SELF_TEST != 0U | |||
| #if PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_MODBUS_DATA | |||
| /* P12 must be queued after the Modbus register store is ready. */ | |||
| if (PlsrModbusDataSelfTestQueue() != PLSR_RESULT_QUEUED) | |||
| @@ -95,13 +94,29 @@ static void AppTaskStart(void *pArg) | |||
| Error_Handler(); | |||
| } | |||
| #elif PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_MODBUS_CONTROL | |||
| if ((PlsrModbusControlSelfTestPrepare() != PLSR_RESULT_OK) | |||
| || (PlsrModbusControlInit(PLSR_MODBUS_CONTROL_TEST_BASE) | |||
| != PLSR_RESULT_OK)) | |||
| if (PlsrModbusControlSelfTestPrepare() != PLSR_RESULT_OK) | |||
| { | |||
| Error_Handler(); | |||
| } | |||
| #elif (PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_HW_COUNTER) \ | |||
| || (PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_DUAL_AB) | |||
| if (PlsrHardwareCounterSelfTestPrepare() != PLSR_RESULT_OK) | |||
| { | |||
| Error_Handler(); | |||
| } | |||
| #elif PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_LONG_STRESS | |||
| if (PlsrLongStressSelfTestPrepare() != PLSR_RESULT_OK) | |||
| { | |||
| Error_Handler(); | |||
| } | |||
| #endif | |||
| #endif | |||
| /* Production command/status service; never couple it to a board fixture. */ | |||
| if (PlsrModbusControlInit(PLSR_MODBUS_CONTROL_TEST_BASE) | |||
| != PLSR_RESULT_OK) | |||
| { | |||
| Error_Handler(); | |||
| } | |||
| //ModbusRetainedRegistersLoad(); | |||
| while (1) | |||
| { | |||
| @@ -181,7 +196,9 @@ int main(void) | |||
| /* Initialize all configured peripherals */ | |||
| MX_GPIO_Init(); | |||
| MX_DMA_Init(); | |||
| #if APP_ENABLE_USB_CDC != 0U | |||
| MX_USB_DEVICE_Init(); | |||
| #endif | |||
| MX_USART1_UART_Init(); | |||
| if (BackupSramInit() != HAL_OK) | |||
| { | |||
| @@ -224,6 +241,7 @@ int main(void) | |||
| } | |||
| /* 上电自测统一延时1s,当前P9配置见下方调用(验证后关闭)。 */ | |||
| #if PLSR_ENABLE_BOARD_SELF_TEST != 0U | |||
| HAL_Delay(1000U); | |||
| #if PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_CW_CCW | |||
| /* P9: Q0=CW, Q1=CCW; only one channel may pulse. */ | |||
| @@ -234,6 +252,7 @@ int main(void) | |||
| #elif PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_DYNAMIC_FREQ | |||
| /* P11: automatic live-frequency schedule without Watch edits. */ | |||
| (void)PlsrDynamicFrequencySelfTestQueue(); | |||
| #endif | |||
| #endif | |||
| //(void)PlsrDirectionLogicSelfTestQueue(); | |||
| OSStart(); | |||
| @@ -92,8 +92,8 @@ | |||
| | 硬限位输入和常开/常闭 | 31、61等 | 输入点及极性可配置 | 已纳入 | | |||
| | 正反向齿隙补偿 | 31、41~43 | V1.0实现 | 已纳入 | | |||
| | 补偿加减速时间 | 41 | S2参数 | 已纳入 | | |||
| | FOLLOW性能参数 | 23~24、44 | 范围1~100 | 已纳入 | | |||
| | FOLLOW前馈补偿 | 23~24、44 | 范围0~100% | 已纳入 | | |||
| | FOLLOW性能参数 | 23~24、44 | 范围1~100 | 共用S2字段的存储/快照/校验已纳入;FOLLOW执行属于独立指令,不属于PLSR | | |||
| | FOLLOW前馈补偿 | 23~24、44 | 范围0~100% | 共用S2字段的存储/快照/校验已纳入;FOLLOW执行属于独立指令,不属于PLSR | | |||
| | 0.1 ms/1 ms刷新 | 22~24、44 | S2配置,独立硬件控制周期 | 已纳入 | | |||
| ## 6. 监控、事件和本项目增强 | |||
| @@ -117,3 +117,20 @@ | |||
| 2. `待实机确认`项目已通过信捷实机或厂家书面资料关闭。 | |||
| 3. 所有`已纳入`和`等效实现`项目具有测试用例和结果。 | |||
| 4. Y0~Y3的PLSR固定软元件地址不得作为差异项;差异仅限编程界面或明确记录的增强行为。 | |||
| ## 8. 当前实现与验证状态(2026-08-10) | |||
| 本节只记录实现证据;第1~6节的“已纳入/等效实现”仍不等同于代码或板测完成,严格关闭仍以 | |||
| 第7节四项准则为准。 | |||
| | 范围 | 当前证据 | 状态 | | |||
| |---|---|---| | |||
| | 四轴PULSE/DIR与计数 | P14真机100kHz,Q0~Q3逻辑分析仪各严格200000个上升沿,无窄脉冲、启动毛刺和停止后残余 | 已板测关闭 | | |||
| | 实时预算 | P16 `PlsrProcess`自身53748 cycles(319.929us)<168000,响应85655 cycles;输出和计数ISR均在预算内 | 已板测关闭 | | |||
| | 软限位 | P15四轴正负方向、500/2000Hz矩阵,最终位置误差不超过±1脉冲 | 已板测关闭 | | |||
| | X/M/HM位设备 | 三套独立10000位映像、FC02读取X、标准线圈访问M、PLSR `readBit`已实现 | 代码完成;X物理GPIO映射与HM保持未关闭 | | |||
| | 双AB 100kHz | 安全首沿、TIM9/TIM12双硬件计数、00边界PAUSE、延后慢收尾及D1468完整快速窗口已通过Host/IAR | 待四通道真机验证,历史7501首沿问题尚不能关闭 | | |||
| | 通信长稳 | USB IRQ优先级4、Modbus 0x47、D1516~D1537 USB设备端统计、P18/K4夹具和自动长稳脚本已完成 | 待30分钟及更长Modbus/USB/四轴板测 | | |||
| | 持久化 | HSD双检查点、SFD Flash A/B+CRC、保守全轴有效位及分阶段掉电/故障工具已完成 | 待VBAT断电、坏CRC、异常复位和反复擦写板测 | | |||
| 因此截至本状态日仍不得宣称“严格功能对标全部完成”。 | |||
| @@ -3,7 +3,7 @@ | |||
| > 文档版本:V1.0 Rev.C | |||
| > 需求基线:[PLSR需求规格统计_V1.3.md](./PLSR需求规格统计_V1.3.md) Rev.B | |||
| > 目标平台:XDM-60T4-E / STM32F407IG / uC/OS-II | |||
| > 编制日期:2026-08-05 | |||
| > 编制日期:2026-08-10 | |||
| > 文档状态:设计基线;标记为“待上板验证”的项目在验证完成前不得关闭 | |||
| ## 目录 | |||
| @@ -68,7 +68,7 @@ V1.0 不实现多轴直线或圆弧插补、闭环位置修正、电子凸轮、 | |||
| | PLSR | 多段脉冲定位指令,逻辑调用形式为 `PLSR S0 S1 S2 D` | | |||
| | S0 | 动态首地址指定的路径数据块,保存总段数及各段频率、脉冲数、等待和跳转 | | |||
| | S1 | 动态首地址指定的用户参数块,保存相对/绝对模式和起始段 | | |||
| | S2 | K0~K4 运动参数套组,保存默认速度、速度限制、曲线、补偿、FOLLOW和刷新周期 | | |||
| | S2 | K0~K4 运动参数套组,保存默认速度、速度限制、曲线、补偿、刷新周期及与独立FOLLOW指令共用的字段 | | |||
| | D | 输出资源操作数,兼容语义下选择 Y0~Y3,本项目对应 Q0~Q3 高速资源 | | |||
| | PULSE/DIR | 一个端口输出脉冲,另一个端口输出方向电平 | | |||
| | AB 相 | 两路相差四分之一周期的正交输出,通过相位先后表示方向 | | |||
| @@ -129,9 +129,9 @@ Q2/Q3 固定点对,最多同时运行两轴。四轴之间不做插补,各 | |||
| | MCU | STM32F407IG,168 MHz | 保持现有时钟配置 | | |||
| | OS | uC/OS-II,Tick为1 ms | 不提高OS Tick | | |||
| | Modbus | USART1+DMA,任务轮询 | 作为调用与监控适配层 | | |||
| | USB | OTG FS中断当前优先级0 | 调低到运动中断之后,建议优先级6 | | |||
| | PLSR定时器 | 当前未初始化 | 新增TIM6控制基准、TIM9/TIM12硬件计数及TIM10/11/13/14脉冲输出 | | |||
| | Modbus历史 | 代码仍写入1000起始历史区 | 删除历史写入,释放给S0/S1动态数据池 | | |||
| | USB | OTG FS中断优先级已设为4 | 低于输出/计数中断优先级1和TIM6优先级2,仍需长稳并发板测 | | |||
| | PLSR定时器 | TIM6、TIM9/TIM12及TIM10/11/13/14已接入 | 保持固定资源映射并通过租约隔离冲突 | | |||
| | Modbus历史 | 1000起始历史区写入已删除 | S0/S1动态数据池不得恢复旧历史映射 | | |||
| | Backup SRAM | 已完成基础初始化 | 扩展为带版本和CRC的双检查点 | | |||
| | 急停 | 无外部硬件切断 | 仅实现软件立即关闭和状态锁存 | | |||
| @@ -312,7 +312,8 @@ S0校验内容: | |||
| - S0与S1不重叠。 | |||
| S1只包含定位模式和起始段。S2只允许K0~K4,并校验默认速度、最高速度、起止频率、曲线、 | |||
| FOLLOW、补偿和刷新周期。D按输出模式检查Y0~Y3或Y0/Y2固定点对。 | |||
| 补偿、刷新周期及FOLLOW共用字段的合法范围;FOLLOW共用字段不参与PLSR运算。D按输出模式检查 | |||
| Y0~Y3或Y0/Y2固定点对。 | |||
| #### 3.2.2 模块输入输出接口说明 | |||
| @@ -438,8 +439,9 @@ f(u) = f_start + (f_end - f_start) * g(u) | |||
| 下一段入口速度。反向段必须先减速到0再切换方向或相序。 | |||
| S2最高速度为0或超出硬件开放范围时拒绝启动。起始、目标和终止速度超过合法最高速度时限幅并发布 | |||
| `SPEED_CLAMPED`。FOLLOW性能范围为1~100,前馈补偿范围为0~100;具体运算公式在编码前按 | |||
| 信捷手册相应参数定义冻结,禁止自行发明含义。 | |||
| `SPEED_CLAMPED`。FOLLOW性能范围为1~100,前馈补偿范围为0~100;两者属于信捷独立 | |||
| FOLLOW/FOLLOW_AB指令使用的共用S2字段。本PLSR模块保留存储、快照和范围校验,但不把它们 | |||
| 代入PLSR速度曲线,也不自行定义FOLLOW运算公式。 | |||
| #### 3.4.2 模块输入输出接口说明 | |||
| @@ -561,6 +563,15 @@ AB相固定配对如下: | |||
| 完整AB周期,落后相第四次跳变完成后关闭最后一周期。一个完整四状态周期只计1个指令脉冲,四次 | |||
| 相位跳变不得分别计数。 | |||
| AB启动必须在GPIO仍持有安全低电平时完成以下顺序:停止两个定时器、强制OC无效并UG、把两个CNT | |||
| 装入严格大于CCR且保持四分之一周期差的位置、使能通道、切回AF、切入PWM1后再同时开放计数。 | |||
| 禁止在运行中重写CNT维持相位。双AB分别以Q0→TIM9和Q3→TIM12作为完整周期计数源;到达目标前 | |||
| 武装落后相的`00`边界快速门控,并通过D1468~D1469记录该快速路径最大周期数。上述新序列和门控 | |||
| 代码已完成Host/IAR验证。PAUSE同样只在下一真实`00`边界停表,已经开始的周期计入完成数;双组 | |||
| 近同时完成时,ISR只快速gate并锁存,GPIO、计数器释放和事件发布延后到任务态。D1468覆盖从IRQ | |||
| 入口到二次扫描及公共ISR收尾的完整快速窗口,而非局部函数时间。在四通道真机确认严格边沿数、 | |||
| 2.5us首相位间隔、变频及PAUSE/RESUME波形前仍标记为待上板验证。 | |||
| ##### 3.5.1.2 16位硬件计数扩展 | |||
| TIM9和TIM12均为16位计数器。计数器配置为外部时钟模式1,ARR保持为`0xFFFF`,每收到一次选定的 | |||
| @@ -819,6 +830,11 @@ PLSR内核通过统一设备接口访问逻辑软元件,Modbus只负责字节 | |||
| 32/64位状态采用版本化快照发布。Modbus断开不产生STOP;非法通信参数不得改变正在运行的任务。 | |||
| 本设计书不新增PLSR全局通信地址区,也不恢复已删除的Modbus历史功能。 | |||
| Modbus数据存储提供相互隔离的X、M、HM三套10000位映像。FC01/FC05/FC0F访问M,FC02只读访问X; | |||
| `PlsrModbusDataSourceInit()`把PLSR `readBit`连接到对应映像。该层只解决数据命名空间和协议一致性: | |||
| 当前工程尚无可据以冻结的X端子GPIO映射,HM映像也尚未接入经板测的保持介质,因此硬限位、WAIT、 | |||
| EXT的真实X输入链路和HM掉电保持不得宣称关闭。 | |||
| #### 3.8.2 模块输入输出接口说明 | |||
| ```c | |||
| @@ -856,7 +872,9 @@ void PlsrDevice_TriggerSegmentEvent(uint8_t axis, uint16_t segment); | |||
| `position_valid`。高速ISR只更新当前块计数;任务在块边界、段边界或检查点时合并64位数据。 | |||
| 发布HSD固定32位累计前检查INT32范围。超出范围时保持最近合法发布值,锁存 | |||
| `COUNTER_OVERFLOW`,不得回绕或静默截断。 | |||
| `COUNTER_OVERFLOW`,不得回绕或静默截断。HSD记录只有一个全局位置有效元数据位,因此检查点按 | |||
| 四轴`position_valid`且无发布溢出的保守AND写入;单独校准一轴不得把其他未校准轴一起标为可信, | |||
| 任何轴超出32位发布范围时也不得让旧HSD位置在重启后冒充当前可信坐标。 | |||
| 当量换算按每转指令脉冲数和每转移动量形成有理数比例,计算前约分并检查溢出,每轴保存换算余数, | |||
| 避免连续多段累计舍入漂移。任一分母为0时返回兼容错误码2。 | |||
| @@ -915,6 +933,13 @@ void PlsrPersist_SaveCheckpoint(uint32_t reason); | |||
| 诊断模块记录每轴目标/实际频率、PSC/ARR/CCR、当前计数模式、TIM9/TIM12块累计、软件脉冲数、 | |||
| 物理边沿诊断值、当前段、停止原因、状态机非法转换、资源冲突和ISR执行周期。 | |||
| P16诊断协议V7使用D1256~D1263发布整体/ISR最大周期,D1456~D1467发布六个任务阶段独立最大值, | |||
| D1468~D1469发布AB末周期快速门控最大值,D1470~D1515发布缓存的持久化诊断与受控验证应答, | |||
| D1516~D1537使用首尾generation发布USB CDC initialized及八个32位收发/错误统计。 | |||
| Modbus只读功能码0x47提供40 WORD运行诊断;各32位字段不会撕裂,整组字段允许跨相邻通信事件; | |||
| USB CDC另记录收发、BUSY、失败和重挂统计。USB OTG FS优先级为4,长稳脚本可并发施加Modbus轮询、 | |||
| 原子动态频率写入、坏CRC、计划断线及可选USB OUT流量。 | |||
| `SELF_TEST` 分为两级: | |||
| - 默认不输出自测:地址表、S0/S1边界、跳转、分频、状态机、计数和CRC; | |||
| @@ -1258,16 +1283,23 @@ Flash操作或通信发送,并使用DWT周期计数器测量最坏执行时间 | |||
| | RISK-01 | 四轴100 kHz仍有两轴可能采用软件计数,最坏约200k周期事件/秒 | 分别验证两硬件+两软件计数组合,记录ISR最坏周期、CPU占用和通信压力结果 | | |||
| | RISK-02 | TIM9/TIM12只能累计,不能反向自动关闭输出源 | 最终比较只武装尾边沿,逐数量边界验证无额外或窄脉冲 | | |||
| | RISK-03 | AB配对定时器时钟分别为168/84 MHz | 全频段测量两组AB频率、相位及方向 | | |||
| | RISK-04 | USB当前中断优先级0 | 调低优先级并完成USB压力测试 | | |||
| | RISK-04 | USB中断可能抢占运动实时路径 | 优先级已降为4;完成USB/Modbus/四轴长稳压力后关闭 | | |||
| | RISK-05 | 输出电路极性和可靠带宽待实测 | 测量Q0~Q3带载波形后冻结参数 | | |||
| | RISK-06 | 0.1 ms曲线计算量 | 定点化和预计算,DWT实测最坏执行时间小于周期预算 | | |||
| | RISK-07 | 16位通信写入造成32位动态频率撕裂 | 使用影子+COMMIT版本和内核二次校验 | | |||
| | RISK-08 | Backup SRAM不能保证掉电瞬间零误差 | 异常复位清position_valid,并记录恢复等级 | | |||
| | RISK-09 | 现有历史写入破坏S0/S1数据 | 删除历史写入并完成全地址越界测试 | | |||
| | RISK-10 | 信捷保留字段被扩展复用 | 固定描述表、静态断言和对标矩阵评审 | | |||
| | RISK-11 | FOLLOW公式尚未在需求中展开 | 编码前按手册冻结公式和测试向量 | | |||
| | RISK-11 | 将共用S2字段误当作PLSR的FOLLOW运算 | 已关闭:FOLLOW/FOLLOW_AB按信捷手册属于独立指令,不在PLSR V1.0执行范围 | | |||
| | RISK-12 | TIM9/TIM12内部OC事件相位和跨时钟域同步延迟 | 测量Q0~Q3的计数对应边沿、首脉冲、末脉冲和动态调频后累计一致性 | | |||
| 截至2026-08-10,P14端子实测Q0~Q3均为100kHz且各严格200000个上升沿;P16实测 | |||
| `PlsrProcess`自身最大53748 cycles(319.929us)、响应最大85655 cycles,P14/P16已关闭。P15四轴 | |||
| 软限位矩阵亦已板测通过。双AB安全首沿/双计数器/00边界PAUSE/D1468完整快速窗口、0x47及 | |||
| D1516~D1537 USB自动诊断、P18/K4长稳夹具和持久化分阶段掉电工具属于代码完成、Host/IAR已通过 | |||
| 但最终板测未关闭项;真实X GPIO映射和HM保持介质仍待硬件定义。上述新批次落盘后必须重跑 | |||
| P14/P15/P16回归,不能用修改前的通过记录替代发布回归。 | |||
| ### 5.2 错误输出信息 | |||
| #### 5.2.1 信捷兼容错误码 | |||
| @@ -1,7 +1,7 @@ | |||
| # PLSR 需求规格书 | |||
| > 文档版本:V1.3 Rev.B(正式需求规格基线)<br> | |||
| > 更新日期:2026-08-05<br> | |||
| > 更新日期:2026-08-10<br> | |||
| > 目标平台:XDM-60T4-E<br> | |||
| > 软件平台:STM32F407 + uC/OS-II<br> | |||
| > 对标基线:信捷 XD/XL 系列 PLC 定位控制手册 PLSR(印刷页 5~87,重点为 49~56) | |||
| @@ -38,6 +38,7 @@ | |||
| - [4.3 后续版本或加分项](#43-后续版本或加分项) | |||
| - [4.4 不在当前范围内](#44-不在当前范围内) | |||
| - [4.5 实施验证事项](#45-实施验证事项) | |||
| - [4.6 当前实现与验证快照](#46-当前实现与验证快照) | |||
| - [5 参考资料](#5-参考资料) | |||
| ## 1 概述 | |||
| @@ -97,7 +98,7 @@ V1.0 的目标范围如下: | |||
| | 后续方式 | 在上一段内提前变频,使段结束时已经达到下一段频率 | | |||
| | S0 | 路径数据块,保存段数、频率、脉冲数、等待和跳转参数 | | |||
| | S1 | 用户执行参数,仅保存定位模式和起始段 | | |||
| | S2 | K0~K4运动参数套组,保存速度限制、起止频率、加减速、补偿、FOLLOW和刷新周期 | | |||
| | S2 | K0~K4运动参数套组,保存速度限制、起止频率、加减速、补偿、刷新周期及与独立FOLLOW指令共用的参数字段 | | |||
| | D | 输出轴或脉冲输出点 | | |||
| | 控制周期 | PLSR 更新状态和速度规划的周期,由 S2 配置为 1 ms 或 0.1 ms | | |||
| | 检查点 | 为掉电恢复而保存的一致性累计数据快照 | | |||
| @@ -314,7 +315,7 @@ PULSE/DIR模式每个有效上升沿计1个指令脉冲;CW/CCW模式在当前 | |||
| | --- | --- | --- | | |||
| | S0 | 路径数据块 | 总段数、每段频率、脉冲数、等待及跳转 | | |||
| | S1 | 用户执行参数 | 仅包含相对/绝对模式和起始执行段;异步调用属于指令接口而非S1字段 | | |||
| | S2 | 运动参数套组 | K0~K4,包含默认频率、最高频率、起止频率、加减速、曲线、补偿、FOLLOW和刷新周期 | | |||
| | S2 | 运动参数套组 | K0~K4,包含默认频率、最高频率、起止频率、加减速、曲线、补偿、刷新周期及与独立FOLLOW指令共用的参数字段 | | |||
| | D | 输出轴 | 指定Y0~Y3脉冲轴;输出模式、方向点、极性和方向延时由该轴配置参数决定 | | |||
| #### 2.4.1 指令接口 | |||
| @@ -378,8 +379,8 @@ S0首地址由调用方动态指定。占用范围从`S0+0`开始,到最后一 | |||
| - 齿隙补偿加减速时间。 | |||
| - 直线、S形、正弦三种加减速模式。 | |||
| - 最高速度限制、起始速度和终止速度。 | |||
| - FOLLOW性能参数,范围1~100。 | |||
| - FOLLOW前馈补偿百分比,范围0~100。 | |||
| - FOLLOW性能参数,范围1~100;该字段供信捷独立FOLLOW/FOLLOW_AB指令使用,PLSR只负责兼容存储和范围校验。 | |||
| - FOLLOW前馈补偿百分比,范围0~100;该字段不参与PLSR运动曲线计算。 | |||
| - 脉冲频率刷新周期,0.1 ms或1 ms。 | |||
| K0参数固定使用`HSD460~539`中对应轴的地址;K1~K4及公共参数固定使用 | |||
| @@ -738,6 +739,11 @@ I6000~I6399按轴和段映射:第N段事件入口为该轴事件基址加`N- | |||
| 7. 运行中写影子参数不得改变当前任务,允许运行修改的目标频率除外。 | |||
| 8. 协议应包含版本号,新增字段时不得改变既有寄存器语义。 | |||
| 位设备访问必须保持X、M、HM三个独立命名空间,不得通过地址重叠或别名互相覆盖。标准Modbus线圈 | |||
| FC01/FC05/FC0F映射M位映像,离散输入FC02只读映射X位映像;PLSR的`readBit`按操作数类型读取 | |||
| 对应映像。X物理输入扫描必须由明确的板级GPIO映射写入X映像,HM若要求掉电保持则必须采用经 | |||
| 验证的保持存储策略;仅存在RAM映像不等于这两项已经上板关闭。 | |||
| PLSR内存地址、数据类型和固定占用规则见 | |||
| [PLSR地址映射](./PLSR地址映射.md)。地址分配必须避开现有演示、 | |||
| Backup | |||
| @@ -994,7 +1000,7 @@ Flash数据无效或保存失败 | |||
| | AT-17 | 四轴独立运行 | 四轴同时以100 kHz运行时无丢脉冲、重复脉冲和异常周期,且状态和累计数据互不串扰 | | |||
| | AT-18 | AB 相输出 | 两个AB轴可独立运行;一个完整正交周期计1个指令脉冲,周期频率、相位方向和数量正确 | | |||
| | AT-19 | S0等待与跳转 | 六种等待条件、动态参数来源、提前/延后触发和0/指定/自身跳转语义与信捷一致 | | |||
| | AT-20 | 公共参数 | 单位换算、软限位、方向延时、齿隙补偿和FOLLOW参数均通过测试 | | |||
| | AT-20 | 公共参数 | 单位换算、软限位、方向延时和齿隙补偿通过功能测试;FOLLOW共用字段通过存储及范围校验测试,不作为PLSR运算项 | | |||
| | AT-21 | 零值及边界 | INT32最小值、频率0、脉冲数0跳段和绝对零位移行为符合本规格 | | |||
| | AT-22 | 段事件和监控 | 每段事件、运行标志、当前段计数和累计计数可被稳定读取 | | |||
| | AT-23 | 固定软元件 | HSD、SM、SD和I6000~I6399的地址、位宽、轴映射及可观察语义与信捷一致 | | |||
| @@ -1052,6 +1058,21 @@ Flash数据无效或保存失败 | |||
| 5. Backup SRAM检查点的具体更新时机和诊断数据允许损失量。 | |||
| 6. 段脉冲数为0时无脉冲输出、跳过当前段并正确进入下一段或完成任务。 | |||
| ### 4.6 当前实现与验证快照 | |||
| 以下快照用于区分“需求已纳入”“代码已实现”和“已完成板测”,不改变第4.1节验收准则,也不能 | |||
| 单独作为“严格功能对标全部完成”的结论: | |||
| | 项目 | 截至2026-08-10的状态 | 关闭边界 | | |||
| | --- | --- | --- | | |||
| | P14四轴PULSE/DIR | 已板测通过 | Q0~Q3均为100kHz、各严格200000个物理上升沿,无窄脉冲、启动毛刺和停止后残余 | | |||
| | P15软限位矩阵 | 已板测通过 | 四轴正负方向、500/2000Hz均在目标软限位±1脉冲内停止 | | |||
| | P16实时性能 | 已板测通过 | `PlsrProcess`自身最大53748 cycles(319.929us),预算小于168000 cycles;响应最大85655 cycles | | |||
| | X/M/HM位数据源 | 数据结构和通信代码已实现 | 已分离三个10000位映像,M接FC01/05/0F、X接FC02、PLSR `readBit`已接入;实际X GPIO扫描映射及HM掉电保持尚未关闭 | | |||
| | 双AB 100kHz | 代码、Host及IAR阶段完成 | 安全首沿、TIM9/TIM12双硬件计数、真实00边界PAUSE、任务态延后收尾和D1468完整快速窗口已写;必须以四通道逻辑分析仪关闭首次边沿、相序、相位、严格周期数、动态调频、PAUSE/RESUME和独立停止 | | |||
| | Modbus/USB长稳 | 代码和自动脚本完成 | USB优先级已降为4,0x47运行诊断、D1516~D1537设备端USB统计和P18/K4无软限位台架夹具已提供;30分钟及更长并发板测尚未关闭 | | |||
| | HSD/SFD持久化 | A/B、CRC、诊断窗口及分阶段板测工具完成 | 检查点按四轴有效且无32位发布溢出的保守AND写入;VBAT断主电、Flash擦写/坏CRC、异常复位和反复保存的正式板测尚未关闭;破坏性诊断默认禁用 | | |||
| ## 5 参考资料 | |||
| 1. [PLSR 嵌入式方向任务要求](./任务要求.png) | |||
| @@ -0,0 +1,80 @@ | |||
| # PLSR 项目交接提示词(2026-08-10) | |||
| > **用途**:将此文件全文粘贴到新的 Codex/对话中,即可无缝接替上一段长对话(ZCode-deepseek 会话)的全部任务。 | |||
| > 交接人:ZCode 会话(deepseek);接收人:Codex 新会话。 | |||
| > 项目:信捷 XDM 兼容 PLSR 脉冲定位模块(STM32F407 + IAR EWARM 8.3 + uC/OS-II + Modbus RTU 从站)。 | |||
| --- | |||
| ## 一、项目一句话 | |||
| 在 STM32F407 上实现信捷 XD/XL 系列 PLC 的 `PLSR S0 S1 S2 D` 多段脉冲定位指令(对标 XDM-60T4-E),上板真机验证,当前处于**收尾阶段:只剩遗留精度验证与压力测试/清理**。 | |||
| - 仓库:`F:\Xinje_Modbus_IAR\TrainCamp_yuwenhao_modbus`(git,当前分支 `deepseek`,主分支 `master`) | |||
| - 指令语义:S0=段表(每段 10 字:频率/脉冲/等待码/跳转),S1=模式/起始段,S2=参数组(K0=HSD460-539,K1-K4=SFD950+),D=Y0-Y3 | |||
| - 十态状态机:UNINITIALIZED/IDLE/ACCEL/RUN/DECEL/WAIT/PAUSED/COMPLETED/STOPPED/ERROR | |||
| - 速度规划:Q32.32 定点,中断中无浮点,直线/S曲线/正弦加减速,动态调频 | |||
| - 输出特性:Y 输出为**集电极开路(OC/NPN 漏型)负逻辑**,正向=低电平=ON | |||
| - 参考手册:`Document/PLSR_document/信捷XD_XL系列PLC定位控制手册_PD02_20260510_V1.3.pdf` | |||
| ## 二、⚠️ 进入任务前必读(当前工作区状态) | |||
| 1. **工作区有 22 个文件未提交**(P14 硬件计数新增 437 行 + 软限位修复 + AB 预热机制等全在工作区,未 commit)——先 `git status` 查看,与老对话确认提交策略后再动工。 | |||
| 2. 最近提交:`fdbbe71` P13(Modbus 控制窗口)、`7750e43` P6(四轴并发自测)、`c776438` P5(软限位保护)。 | |||
| 3. 上板自测 `plsr_self_test.c` 仍在(收尾时要删)。 | |||
| 4. 真机验证环境:STM32F407 + 逻辑分析仪导出 16 通道 .bin(100MS/s,2字节/采样小端 uint16),波形分析脚本 `Document/PLSR_document/波形/plot_waveform.py`。 | |||
| ## 三、主线:5 批规划与完成度(已核实代码) | |||
| | 批次 | 内容 | 状态 | 证据 | | |||
| |---|---|---|---| | |||
| | ① | Modbus 数据源正式接入(真实 D/HD/FD → S0/S1/动态调频) | ✅ 完成 | `plsr_modbus_data.c` readWord=PlsrModbusReadWord;S0=D1600/S1=D1700 经 0x10 原子写入;动态调频 D1000/D1100 已真机验证 | | |||
| | ② | Modbus 命令与状态接口(START/STOP/PAUSE/状态/错误码/位置/计数) | ✅ 完成 | fdbbe71:COMMIT/START 分离、停止/暂停/继续/位置设置/清零/保存、重复序号幂等、四轴状态、32/64 位版本一致性;错误码 5/6 + RESET_ERROR 已验 | | |||
| | ③ | FOLLOW 与前馈补偿 | ✅ 关闭(不实现) | P14 按手册确认 FOLLOW/FOLLOW_AB 是**独立随动指令**,PLSR 仅保留参数字段存储校验,不实现随动运算。**勿再开发** | | |||
| | ④ | 遗留精度问题 | ⚠️ 见下表 | — | | |||
| | ⑤ | 压力测试与正式收尾 | ❌ 大部分未做 | 见第五节 | | |||
| ## 四、遗留精度问题(④批)——按优先级 | |||
| | # | 问题 | 状态 | 验证/修复方法 | | |||
| |---|---|---|---| | |||
| | 1 | **P14 硬件计数 -1**(Q0/Q1=199999 而非 200000;TIM9/TIM12 硬件计数;Q2/Q3 软件回退精确) | 🟡 代码已修**未上板验证**、未提交 | 修复:计数器延迟到源 OC 启动 UG 完成后再配置/使能,消除无物理脉冲的内部首沿;验收=四轴均 200000。用 `HostComputer/plsr_modbus_counter_stress_test.py`(四轴 100kHz 压力) | | |||
| | 2 | **AB 启动边沿**(段1 起步 A-R/B-R 同升,各多 1 沿 → 7501/7501;首周期从 11 开始非 00→10→11;稳定复现 6 版固件) | 🔴 预热机制 `abStartupPriming` 已写(plsr_hal_f407.c:845/893/1065)**未上板验证** | ① 两 ISR 各翻转调试 GPIO 抓先后;② `SetPwmMode1` 移到 `SetCc1e` 之前;③ 启动后读两路 CNT 确认初值。验收=严格 7500 | | |||
| | 3 | **CW/CCW 段尾截断窄脉冲**(段末最后脉冲仅 2.5~5µs 高,未走完完整周期) | 🟡 未关闭(主验收已过,微秒毛刺标准不达标) | 停止应延迟到最后脉冲 CC1 匹配(下降沿)之后 | | |||
| | 4 | 软限位边界 | ✅ 已关闭 | P15 高频采样补偿(2000Hz 提前 1 脉冲进入减速)→ 四轴正/负 500/2000Hz 矩阵 ±1 脉冲窗口全过 | | |||
| | 5 | AB 段3 反向切换 10ms 过渡期 / 方向信号 Q5 1µs 毛刺 | 🟢 低优先级,可暂缓 | — | | |||
| 详细记录见 `Document/PLSR_document/问题总清单_2026-08-10.md`、`已知问题清单_2026-08-09.md`。 | |||
| ## 五、压力测试与正式收尾(⑤批)——下一步主线 | |||
| 1. **上板验证**(立即):AB 预热(7500)、硬件计数 -1(四轴 200000)、软限位回归。 | |||
| 2. **四轴 100kHz AB 模式**:只测过 PULSE/DIR 100kHz;AB 高频加速段重定相干扰未实测。 | |||
| 3. **双 AB 并发**:Q0/Q1 + Q2/Q3 同时 AB 运行(独立轴停止互不影响已有实现,未实测并发)。 | |||
| 4. **Modbus 压力**:`HostComputer/plsr_modbus_counter_stress_test.py` 已有部分覆盖(12 次状态读取),补全并发读写压力。 | |||
| 5. **掉电恢复**:Flash 存储(SFD900~1419 片内 Flash 已实现)掉电/上电恢复未实测。 | |||
| 6. **性能测试**:`HostComputer/PLSR_MODBUS_PERFORMANCE_TEST.md` 已建,脚本未写。 | |||
| 7. **清自测收尾**:删 `plsr_self_test.c`(及 main 中的调用)、调试快照、tmp 资料清理、git 历史大对象(1GB bin 松散对象)清理。 | |||
| ## 六、已固化的关键决策(勿回退) | |||
| - Y 输出 OC 负逻辑(正向=低=ON);PULSE/DIR、AB、CW/CCW 三模式,CW/CCW 返回 NOT_SUPPORTED | |||
| - AB 仅允许 D=Y0/Y2(Q0/Q1、Q2/Q3),调频同时更新 A/B 不重置相位,正常完成只在完整 00 周期边界停止 | |||
| - 段间衔接仅方向变化时走 DIR_SETTLING(~2ms),否则直连 | |||
| - 每 tick `PlsrProfileSyncPulses` 校准 profile 虚拟计数到硬件实发数(段尾冻结修复) | |||
| - 地址映射固定:HSD 384B / SFD RAM 缓存 2080B / SD 192B / SM 4B,`plsr_address_map.h` | |||
| - PLSR 阶段不实现 FOLLOW 随动运算(见批次③) | |||
| ## 七、代码地图与测试体系 | |||
| - `PLSR/Inc|Src/`:plsr_core(状态机/命令)、plsr_job(任务快照解析/校验)、plsr_path(多段执行器)、plsr_profile(速度曲线)、plsr_hal_f407(定时器/GPIO/硬件计数)、plsr_modbus_control(命令)、plsr_modbus_data(D 区数据源)、plsr_persistence(Flash)、plsr_self_test(上电自测)、plsr_position、plsr_resource、plc_device | |||
| - **Host 测试**:`PLSR/Test/run_host_tests.ps1`(1268 项,`-std=c11 -Wall -Wextra -Werror -DPLSR_HOST_TEST -lm`);**IAR 编译**:`IarBuild.exe EWARM/Modbus.ewp -build Modbus`(0 错误 0 警告基线) | |||
| - **真机测试脚本**(HostComputer/):`plsr_modbus_control_test.py`(命令)、`plsr_modbus_counter_stress_test.py`(四轴 100kHz+计数)、`plsr_modbus_frequency_test.py`(动态调频)、`plsr_modbus_soft_limit_matrix_test.py`(限位矩阵)、`modbus_timing_tester.py`(时序) | |||
| - **波形验证**:`Document/PLSR_document/波形/*.bin` + `plot_waveform.py`(时频分析:总脉冲数/段结构/频率命中/段间 gap) | |||
| - **文档**:方案设计书 V1.0、需求规格统计 V1.3、对标追踪矩阵、问题总清单 2026-08-10 | |||
| ## 八、建议的下一步行动(新对话从这里开始) | |||
| 1. `git status` + `git log --oneline -5` 确认工作区 22 个未提交文件内容(P14 硬件计数批次),**先上板验证再提交**(或与用户确认提交策略)。 | |||
| 2. 上板验证三件套:硬件计数 -1(counter_stress 脚本)→ AB 预热(7500)→ 软限位回归(matrix 脚本)。 | |||
| 3. 通过后提交 P14 批次,然后按第五节 2→7 推进压力测试与收尾。 | |||
| @@ -0,0 +1,69 @@ | |||
| # 提示词:发给 Codex 老对话(请求输出交接总结) | |||
| > 用法:把下面【发送内容】整段复制,发给当前正在进行的 Codex 老对话。 | |||
| > 老对话回复的交接总结,将整段粘贴给新对话,作为新对话的唯一历史输入。 | |||
| > (仓库里另有一份兜底参考:`交接提示词_2026-08-10.md`,若老对话总结不完整,可一起给新对话。) | |||
| --- | |||
| 【发送内容】 | |||
| ``` | |||
| 我们即将结束本对话,把所有任务交接给一个全新的对话(新 Codex/ZCode 会话)。 | |||
| 新对话没有任何本对话的历史,它只能看到两样东西: | |||
| (1) 你下面这份回复(原样粘贴给它); | |||
| (2) 仓库 F:\Xinje_Modbus_IAR\TrainCamp_yuwenhao_modbus 中的文件。 | |||
| 请【一次性、直接】输出一份完整、自包含、Markdown 格式的交接总结。 | |||
| 不要提问、不要确认、不要寒暄、不要分多轮——就这一轮输出全部内容。 | |||
| 硬性要求: | |||
| 1. 不假设新对话知道任何背景:所有结论、数据、文件路径、命令、编号都写全。 | |||
| 2. 高密度:多用表格、编号、代码引用(文件:行号),少废话。 | |||
| 3. 必须覆盖以下章节(顺序可微调,但章节都要有): | |||
| ## A. 项目概况 | |||
| - 项目一句话(做什么、对标什么硬件、当前处于什么阶段) | |||
| - 仓库路径、git 分支、最近几个提交 | |||
| ## B. 主线进度(5 批规划逐项标注 ✅ 完成 / ⚠️ 部分 / ❌ 未做) | |||
| 1. Modbus 数据源正式接入(真实 D/HD/FD → S0/S1/动态调频) | |||
| 2. Modbus 命令与状态接口(START/STOP/PAUSE/状态/错误码/位置/计数) | |||
| 3. FOLLOW 与前馈补偿 | |||
| 4. 遗留精度问题(AB 首沿、软限位、硬件计数 -1 等) | |||
| 5. 压力测试与正式收尾(100kHz/双 AB/掉电/Modbus 压力/清自测) | |||
| 每项给一句证据(提交号 / 测试脚本 / 文档名)。 | |||
| ## C. ⚠️ 工作区当前状态(最重要,务必写全) | |||
| - git status 所有未提交文件清单,每个文件是什么内容(哪些是待上板验证的修复、哪些是测试脚本、哪些是文档) | |||
| - 哪些代码改动【已写但未上板验证】、哪些【已验证但未提交】、哪些【既未验证也未提交】 | |||
| ## D. 未完成任务清单 | |||
| 每个未完成任务给出: | |||
| - 现象(含实测数据,如 7501 vs 7500、199999 vs 200000) | |||
| - 影响/验收标准 | |||
| - 根因分析或假设 | |||
| - 代码现状(已写/未写,给出 文件:行号) | |||
| - 验证方法(用什么脚本/逻辑分析仪/命令) | |||
| - 若已写修复但未验证:修复思路一句话 | |||
| ## E. 已验证成果与固化决策(勿回退清单) | |||
| - 所有实测过的硬件特性(如 OC 负逻辑、AB 相序、段间衔接时序) | |||
| - 已修复并验证的 bug 列表 | |||
| - 明确的边界决定(如 FOLLOW 不实现、CW/CCW 不支持) | |||
| - 地址映射等固定约定 | |||
| ## F. 代码地图与测试体系 | |||
| - PLSR/ 各模块职责(core/job/path/profile/hal/modbus_control/modbus_data/persistence/self_test...) | |||
| - HostComputer/ 各测试脚本用途 | |||
| - 编译命令(IAR build、run_host_tests.ps1)与基线(当前 0 错误 0 警告?) | |||
| - 逻辑分析仪波形验证方法(bin 格式、采样率、plot_waveform.py 用法) | |||
| ## G. 下一步行动清单 | |||
| 按优先级排序的具体行动(从新对话开工第一件事开始排)。 | |||
| ## H. 仅存在于对话中的信息 | |||
| 凡"只在我们对话里出现过、没有落入任何文件"的关键信息(测试数据、踩坑经验、用户偏好、待确认问题),单独列一节,务必不要遗漏。 | |||
| ``` | |||
| 【发送内容结束】 | |||
| @@ -0,0 +1,99 @@ | |||
| # PLSR 问题总清单(2026-08-10) | |||
| > 汇总截至今日所有未关闭的问题、待验证项与待办功能。 | |||
| > 均为上板验证/代码扫描实证,非猜测。 | |||
| --- | |||
| ## 一、未解决 Bug(4 个主问题 + 2 个低优先级) | |||
| ### 1. AB 启动边沿(Q0/Q1 = 7501 而非 7500) | |||
| | 项 | 内容 | | |||
| |---|---| | |||
| | 现象 | 段1 起步瞬间 A-R 与 B-R 同刻上升(0.33~0.37µs),每相各多 1 个上升沿 → 7501/7501;首周期从 11 开始而非 00→10→11 | | |||
| | 复现 | 6 版固件稳定复现(00:06 / 13:41 / 13:57 / 13:59 / 14:15),现象、位置、结构完全一致 | | |||
| | 影响 | 启动瞬间 1 个非法跳变;多数驱动器正交解码忽略,位置误差最多 1 脉冲 | | |||
| | 已排除 | 调频/重定相路径无同升(稳定段 <10µs 间隔 0 个)——问题只在首次启动路径 | | |||
| | 代码现状 | `PlsrHwBeginAbOutput`:GPIO 保持 00 → UG → CNT 初值(lead 3/4T、lag 1/2T)→ 切 PWM1 → CEN → update 分时释放引脚;`abStartupPriming` 预热机制已写(首次启动吞周期)**待上板验证** | | |||
| | 根因方向 | 两路"释放"几乎同时 → 两路第一次 update 同时 → CNT 初值 1/4T 相位差未体现;或 CC1E 先于 PWM1 切换(RM0090:切模式瞬间 OCREF 跳变)+ 落后相 CNT 写入 = CCR 触发比较事件 | | |||
| | 验证方法 | ① 两 ISR 各翻转调试 GPIO 抓先后;② `SetPwmMode1` 移到 `SetCc1e` 之前;③ 启动后读两路 CNT 确认初值 | | |||
| | 状态 | 🔴 未关闭(验收:严格 7500) | | |||
| ### 2. 正软限位边界精度(502 脉冲 vs 验收 499~501) | |||
| | 项 | 内容 | | |||
| |---|---| | |||
| | 现象 | 目标 +10000、正软限位 +500、1000Hz、减速 100ms → 实测 Q0=502 上升沿,超停 1~2 个 | | |||
| | 波形结构 | 恒定段 460 个 @1000Hz → 减速段 42 个(84.5ms,535→134Hz 平滑)→ 停止后零边沿 | | |||
| | 偏差 | 验收"接近第 450 个脉冲开始减速"→ 实测 459 开始(晚 9 个);理论提前量 = 50 脉冲(应在 450 触发、正好停 500) | | |||
| | 影响 | 超停 1~2 个工程单位;软限位是安全功能,偏差为系统性(速度/限位配置变化时超停量随之变化) | | |||
| | 根因方向 | ① 限位判定位置源(hardwarePulses/虚拟计数/logicalPosition)与判定 tick 采样点;② 提前量(减速距离)计算与实际减速段脉冲数(42 vs 50)不一致;③ 收尾差 1 与 DONE vs 硬件计数边界可能同源 | | |||
| | 验证方法 | 调限位值/速度复测多组(+200/+1000、500Hz/2000Hz),统计触发位置偏差是否随参数线性变化 | | |||
| | 状态 | 🟡 已修复待上板:制动距离改用真实硬件输出频率,新增四轴正/负、500/2000Hz矩阵测试(验收±1脉冲) | | |||
| ### 3. CW/CCW 段尾截断窄脉冲(2.5µs / 5µs) | |||
| | 项 | 内容 | | |||
| |---|---| | |||
| | 现象 | 段1 最后脉冲只持续 2.5µs 高(0.167797→0.167800)、段2 最后脉冲 5.0µs——最后脉冲上升沿后即拉低,未走完完整周期 | | |||
| | 影响 | 计数不受影响(Q0=300、Q1=200 精确,中断计数不依赖脉冲宽度);但部分驱动器可能把窄脉冲当噪声忽略(物理少走最后 1 个脉冲) | | |||
| | 根因方向 | CW/CCW 停止时机在"最后脉冲上升沿 + 2.5~5µs"(比 CC1 下降沿早半个周期)——停止应延迟到最后脉冲 CC1 匹配(下降沿)之后 | | |||
| | 状态 | 🟡 未关闭(验收"微秒级毛刺 0 个"严格不达标;主验收已过) | | |||
| ### 4. P14 硬件计数少 1 个脉冲(Q0/Q1 = 199999 而非 200000)【新增】 | |||
| | 项 | 内容 | | |||
| |---|---| | |||
| | 现象 | 四轴 100kHz 压力测试:Q0/Q1(TIM9/TIM12 硬件计数)各 199999 个上升沿;Q2/Q3(软件计数回退)精确 200000 | | |||
| | 影响 | 每轴少 1 个脉冲(0.0005%),位置误差 -1 脉冲/次,**系统性**(非随机);影响硬件计数"转正" | | |||
| | 代码现状 | 硬件计数本轮新接入:TIM9 对 Q0/Q2、TIM12 对 Q1/Q3 累计;16 位溢出扩展(counterBlockPulses)+ 目标脉冲比较(CC1IF 判定,`PlsrHwOnCounterInterrupt`);本轮同时修复"启动 UG 事件被误计" | | |||
| | 根因方向 | 代码实证:`pulses = counterBlockPulses + CNT`,`CC1IF && pulses >= targetPulses` 判 DONE——**启动时计数器初值偏移 +1(CNT 从 1 开始或第一个 OC 事件被 UG 清除时序吞掉)→ pulses 提前达到 200000 → 提前 1 个停止**(实际只发 199999);注释"TIMx_OC rises at the PWM update boundary = 物理下降沿"——启动瞬间的计数边界对齐即嫌疑点 | | |||
| | 验证方法 | DONE 判定处打印硬件计数器最终值(199999 → 少计 1 个;200000 → 停止判定滞后);或对比 Q0(硬件)与 Q2(软件)启动前 3 个脉冲边沿 | | |||
| | 状态 | 🟡 已修复待上板:从计数器延迟到源OC启动UG完成后再配置/使能,消除无物理脉冲的内部首沿;验收仍为四轴均200000 | | |||
| ### 5.(低)AB 段3 反向切换 10ms 过渡期 | |||
| | 项 | 内容 | | |||
| |---|---| | |||
| | 现象 | 段2→段3 反向切换有 ~10ms 混合相序过渡(A-R→B-R→B-F→A-F)后才进入标准 B 超前 00→01→11→10→00;无毛刺、每状态时长正常 | | |||
| | 影响 | 若要求"反向也严格从 00 起步"则未达标;实际影响很小 | | |||
| | 状态 | 🟢 低优先级,可暂缓 | | |||
| ### 6.(低)方向信号 Q5 启动瞬间 1µs 毛刺 | |||
| | 项 | 内容 | | |||
| |---|---| | |||
| | 现象 | 方向逻辑测试(17:20)中 Q5 在方向建立瞬间出现 1µs 低→高→低毛刺(0.128780) | | |||
| | 影响 | 方向信号 1µs 抖动,一般不影响(方向在脉冲开始前已稳定 10ms) | | |||
| | 状态 | 🟢 低优先级,可暂缓 | | |||
| --- | |||
| ## 二、待验证项 | |||
| | 项 | 内容 | 状态 | | |||
| |---|---|---| | |||
| | AB 启动预热机制(abStartupPriming) | 代码已写(首次启动吞周期),未上板验证 | ⏳ 待测 | | |||
| | 四轴 100kHz AB 模式 | 本次只测了 PULSE/DIR 100kHz;AB 高频加速段重定相干扰未实测 | ⏳ 待测 | | |||
| | 双 AB 并发(Q0/Q1 + Q2/Q3 同时 AB) | 未实测 | ⏳ 待测 | | |||
| --- | |||
| ## 三、未实现功能(清单剩余 2 项) | |||
| | 项 | 现状 | 说明 | | |||
| |---|---|---| | |||
| | Modbus 剩余批次 | P13 控制窗口 ✅(已提交 fdbbe71)、P14 硬件计数 ✅(待提交) | 按 P14 说明"剩余 3 批",当前已完成 2 批 → **剩余约 2 批**(具体内容见 Codex 规划) | | |||
| | 收尾清理 | 未做 | 上电自测移除、调试快照(P4 后删除标记)、tmp 资料清理、git 历史重写(1GB bin 松散对象) | | |||
| > **FOLLOW/前馈已关闭(不再列入待办)**:P14 批按信捷手册明确 FOLLOW/FOLLOW_AB 为独立随动指令(输出 = 输入 × 乘系数/除系数),PLSR 仅保留共用参数字段(follow 1~100、feedforward 0~100)的存储与校验,不实现随动运算。 | |||
| --- | |||
| ## 四、优先级建议 | |||
| 1. **尽快修**:P14 硬件计数 -1(新路径转正门票,预期小改动) | |||
| 2. **次优先**:AB 启动边沿(预热机制已写,上板验证即可见分晓) | |||
| 3. **可并行**:Modbus 剩余批次、软限位/段尾截断按实验定位后修 | |||
| 4. **暂缓**:低优先级两项(反向过渡/方向毛刺)、FOLLOW 运算(等需求明确) | |||
| @@ -0,0 +1,41 @@ | |||
| # PLSR 验证与生产构建 | |||
| 构建开关集中在 `PLSR/Inc/plsr_build_config.h`。 | |||
| 当前仓库默认是验证构建,用于完成剩余板测: | |||
| - `PLSR_ENABLE_BOARD_SELF_TEST=1` | |||
| - `PLSR_BOARD_TEST_SELECT=PLSR_BOARD_TEST_DUAL_AB` | |||
| - `PLSR_ENABLE_HW_TRACE=1` | |||
| - `APP_ENABLE_USB_CDC=1` | |||
| - `PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG=0` | |||
| P14、P15、P17 共用一次参数准备,运动仍由上位机脚本发起,不会上电自启动。 | |||
| P18 长稳使用独立 K4 且关闭软/硬限位,只允许在脱开机构或已确认机械安全的台架上使用。在 IAR | |||
| 预处理器符号中临时覆盖: | |||
| ```text | |||
| PLSR_BOARD_TEST_SELECT=PLSR_BOARD_TEST_LONG_STRESS | |||
| ``` | |||
| 完成长稳后删除该覆盖,仓库默认会回到 P17 双 AB 验证配置。 | |||
| 正式发布时,在 IAR 的预处理器符号中覆盖: | |||
| ```text | |||
| PLSR_ENABLE_BOARD_SELF_TEST=0 | |||
| PLSR_ENABLE_HW_TRACE=0 | |||
| PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG=0 | |||
| ``` | |||
| USB 不使用时再增加: | |||
| ```text | |||
| APP_ENABLE_USB_CDC=0 | |||
| ``` | |||
| 关闭板测后,Modbus PLSR 控制窗口仍会初始化;这是正式控制链,不再依赖任何自测选择。破坏性持久化诊断默认始终关闭,只有专用验证固件、双重 magic、全轴空闲且双槽有效时才允许使能。 | |||
| 2026-08-10已实际将`PLSR_ENABLE_BOARD_SELF_TEST=0`(从而关闭HW trace)做过一次完整IAR链接, | |||
| 结果0 errors / 0 warnings;随后已恢复仓库默认P17验证配置并再次完整构建通过。 | |||
| @@ -0,0 +1,55 @@ | |||
| # PLSR 双 AB 100kHz 压力测试 | |||
| 本测试覆盖两组固定 AB 点对:Q0/Q1(轴 Y0)和 Q2/Q3(轴 Y2)。脚本使用 | |||
| K3 的 100kHz 零加减速配置,自动写入 S0/S1、COMMIT、START,并核对两组 | |||
| TIM9/TIM12 硬件计数租约、任务/位置/物理周期数和 DWT 实时预算。动态用例还会 | |||
| 执行100kHz→50kHz→100kHz原子变频以及Q0/Q1单组PAUSE/RESUME。 | |||
| > 当前状态(2026-08-10):安全首沿建立、双硬件计数租约和末周期快速门控代码已通过Host回归及IAR编译;尚未按本文件完成四通道真机波形验收,因此AB首次启动7501历史问题和双AB 100kHz压力项仍不得标记为关闭。 | |||
| ## 测试前提 | |||
| - 烧录当前工作区固件后执行硬复位,使 DWT 最大值从零开始。 | |||
| - `main.c` 的板测准备路径必须执行 `PlsrHardwareCounterSelfTestPrepare()`,以生成 K3。 | |||
| - Modbus RTU 默认 COM5、9600、8E1。 | |||
| - 逻辑分析仪四通道同步连接 CH0~CH3=Q0~Q3,建议 100MS/s。 | |||
| ## 自动测试 | |||
| 完整执行独立停止、等长并发、动态变频/暂停三个用例: | |||
| ```powershell | |||
| python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 | |||
| ``` | |||
| 为便于逻辑分析仪精确统计,推荐硬复位后分别采集: | |||
| ```powershell | |||
| python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 --case independent | |||
| python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 --case simultaneous | |||
| python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 --case dynamic | |||
| ``` | |||
| `independent` 要求 Q0/Q1 完成后计数冻结且保持低,同时 Q2/Q3 继续运行。 | |||
| `simultaneous` 使用相同的 200000 周期目标验证两组并发完成;两个 START 通过 | |||
| Modbus 串行提交,因此两组实际启动/结束时刻允许相差一次 RTU 事务时间。 | |||
| `dynamic` 使用每组300000周期,要求两组调频时保持相位和连续计数;Q0/Q1只在 | |||
| 完整`00`边界暂停并保持低,Q2/Q3不受影响,随后Q0/Q1从`00`恢复正确相序。 | |||
| 脚本还检查 D1468~D1469 发布的 AB 末周期快速门控最大周期数。168MHz 下必须 | |||
| 非零且小于 420 cycles(2.5us,一个 100kHz AB 周期的四分之一)。 | |||
| ## 逻辑分析仪验收 | |||
| - 独立停止用例:CH0=CH1=100000 个上升沿;CH2=CH3=200000 个上升沿。 | |||
| - 等长用例:CH0=CH1=CH2=CH3=200000 个上升沿。 | |||
| - 动态用例:CH0=CH1=CH2=CH3=300000 个上升沿;Q0/Q1含一个暂停低电平窗口。 | |||
| - 100kHz区间完整AB周期10.000us、各相高宽5.000us;50kHz区间周期20.000us、 | |||
| 各相高宽10.000us;不得有小于1us的窄脉冲。 | |||
| - 正向必须为 `00→10→11→01→00`,反向必须为逆序。 | |||
| - 调频前后保持严格±90°且不得增加边沿;PAUSE只能在`00`边界生效,RESUME从 | |||
| `00`重新建立相序,另一组全程不得停顿或串扰。 | |||
| - 启动前为 `00`;A/B 首个上升沿间隔 2.50us(建议容差 ±0.05us),不得同时上升。 | |||
| - 末周期完整回到 `00`;停止后至少 1ms 全低且无残余边沿。 | |||
| 脚本的内部 `physical_pulses` 只用于固件一致性检查,不能替代端子波形验收。 | |||
| @@ -0,0 +1,17 @@ | |||
| # PLSR Modbus 位数据源测试 | |||
| 运行: | |||
| ```powershell | |||
| python HostComputer\plsr_modbus_bit_input_test.py --port COM5 | |||
| ``` | |||
| 脚本自动完成以下检查: | |||
| 1. 使用 Modbus `0x05` 写 M123,并用 `0x01` 回读。 | |||
| 2. 使用 `0x02` 读取同地址 X123,确认 X/M 是两个独立位空间。 | |||
| 3. 原子写入一段 1000 Hz、500 脉冲的 S0/S1,段后等待 M123。 | |||
| 4. 验证 M123=0 时轴稳定处于 WAIT,任务计数严格为 500。 | |||
| 5. 将 M123 置 1,验证 WAIT 释放并进入 COMPLETED。 | |||
| 该测试关闭的是 M 位生产链。X 只提供只读映像和 FC02 协议入口;必须取得板卡“X 点号 → GPIO/输入扫描”映射后,才能验收实际 X、EXT 和正负硬限位。HM 当前也是独立映像,但尚未定义工程需要的掉电保持策略。 | |||
| @@ -1,6 +1,6 @@ | |||
| # PLSR P13 Modbus 命令与状态测试 | |||
| P13 使用可配置控制窗口,当前板测选择 D1200~D1455。该范围不属于信捷固定 PLSR 地址,仅是本工程上位机测试使用的动态通信窗口。 | |||
| P13 使用可配置控制窗口,当前板测选择 D1200~D1537。D1456~D1467 是 P16 V7 分阶段性能诊断扩展,D1468~D1469是双AB末周期快速门控诊断,D1470~D1515用于持久化只读状态和受控验证请求/应答,D1516~D1537用于generation保护的只读USB CDC运行统计。该范围不属于信捷固定 PLSR 地址,仅是本工程上位机测试使用的动态通信窗口。 | |||
| 运行: | |||
| @@ -0,0 +1,57 @@ | |||
| # PLSR P14 四轴高速计数压力测试 | |||
| ## 1 测试目的 | |||
| 验证四轴同时以100kHz输出时的脉冲计数、16位计数器跨界、计数资源冲突回退和Modbus并发读状态。P14按硬件连接动态分配计数资源:Q0/Q2共享TIM9,Q1/Q3共享TIM12;按Q0、Q1、Q2、Q3顺序启动时,Q0/Q1使用硬件计数,Q2/Q3使用软件回退。 | |||
| ## 2 准备 | |||
| 1. 使用验证构建编译并烧录当前工程:`PLSR_ENABLE_BOARD_SELF_TEST=1`,`PLSR_BOARD_TEST_SELECT`可选`PLSR_BOARD_TEST_HW_COUNTER`或`PLSR_BOARD_TEST_DUAL_AB`,两者都会准备P14所需K1参数。 | |||
| 2. 复位开发板。 | |||
| 3. 逻辑分析仪连接Q0、Q1、Q2、Q3和GND,采样率建议不低于10MS/s,采集时间至少5秒。 | |||
| 4. 保持Modbus RTU参数与工程一致:默认站号1、9600 bit/s、偶校验、1停止位。 | |||
| ## 3 运行 | |||
| ```powershell | |||
| python HostComputer\plsr_modbus_counter_stress_test.py --port COM5 | |||
| ``` | |||
| 脚本会自动完成四轴位置清零、S0/S1写入、COMMIT、START、运行期状态轮询和最终计数核对,不需要在Watch窗口手工修改变量。 | |||
| 当前固件同时为后续P15配置了±1000000软限位;P14每轴只运动200000脉冲,因此不会触发该限位。 | |||
| ## 4 自动判定标准 | |||
| - 四轴均进入运行态,频率均为100000Hz。 | |||
| - Q0/Q1运行期`hardwareCounter=1`,Q2/Q3为0。 | |||
| - 四轴均进入`PLSR_STATE_COMPLETED`。 | |||
| - 每轴`logicalPosition`和`taskPulses`均精确等于200000;`physicalPulses`是上电累计值,脚本以运行前基线为准,要求本次增量精确等于200000,不能直接比较累计绝对值。 | |||
| - 四轴`error=0`且最后执行结果为`PLSR_RESULT_OK`。 | |||
| 可选Watch观察项(只需观察,不需要修改): | |||
| - `PlsrHwCounterOwners[0]`:运行期为0,表示TIM9分配给Q0。 | |||
| - `PlsrHwCounterOwners[1]`:运行期为1,表示TIM12分配给Q1。 | |||
| - `PlsrHwAxes[0].counterBlockPulses`和`PlsrHwAxes[1].counterBlockPulses`:运行中依次跨过65536、131072、196608。 | |||
| - `PlsrHwAxes[0..3].hardwareCounterActive`:运行期依次为1、1、0、0;完成后租约释放并回到0。 | |||
| ## 5 波形判定标准 | |||
| - Q0、Q1、Q2、Q3各200000个上升沿。 | |||
| - 每路稳定频率100kHz,周期10us,占空比约50%。 | |||
| - 无额外首沿、段尾截断、窄脉冲或停止后的残余脉冲。 | |||
| - 四轴启动时刻允许因串行发送START命令而不同;这不是同步插补测试。 | |||
| 若脚本通过而硬件计数轴的波形数量不符,应优先检查TIM9/TIM12内部触发边沿与物理输出反相关系;Host测试无法覆盖这一项。 | |||
| ## 6 已关闭的板测记录(2026-08-10) | |||
| - P14脚本通过:四轴本次任务计数和`physicalPulses`增量均为200000。 | |||
| - 逻辑分析仪确认Q0~Q3各严格200000个物理上升沿,稳定频率100000.0Hz。 | |||
| - 周期中位数10.000us,实测范围9.990~10.010us;高电平宽度5.000us。 | |||
| - 无小于0.5us窄脉冲、无启动毛刺、无停止后残余,四路最终均为低电平。 | |||
| 上述记录关闭当前P14 PULSE/DIR计数验收,但不能替代双AB、掉电恢复或长稳并发专项验收。 | |||
| 该记录也是最终AB/持久化/USB批次合入前的基线;烧录当前最终工作区后应再执行一次本脚本和四通道 | |||
| 边沿统计,确认共享HAL与中断改动没有造成PULSE/DIR回退。 | |||
| @@ -0,0 +1,85 @@ | |||
| # PLSR Modbus / USB 长稳并发测试 | |||
| 本批测试用于在四轴 `PULSE/DIR` 持续运动时,同时施加 Modbus 状态轮询、32 位动态频率写入和可选 USB CDC OUT 流量。脚本不发送 `SAVE_CONFIG`,不会在运动中触发 Flash 擦写。 | |||
| > 当前状态(2026-08-10):0x47只读诊断、USB CDC统计、USB中断优先级4及自动长稳脚本已完成代码和静态构建验证;30分钟及更长的Modbus/USB/四轴真机并发仍待执行,不能以脚本存在替代板测结论。 | |||
| ## 固件诊断接口 | |||
| Modbus RTU 新增只读功能码 `0x47`。请求 PDU 与 `0x03` 一样包含 `start word` 和 `quantity`,完整快照为 40 WORD;请求示例为 `47 00 00 00 28`。不存在写入口,也不占用 D/HD/FD 地址。 | |||
| 主要字段如下(32 位字段均为低 WORD 在前): | |||
| | WORD | 内容 | | |||
| |---:|---| | |||
| | 0~3 | 签名 `0x4D42`、版本 1、长度 40、运行标志 | | |||
| | 4~9 | 当前 Tick、最后有效帧 Tick、最后帧间隔 DWT 周期 | | |||
| | 10~15 | DMA 接收重启次数、重启失败次数、最后 UART ErrorCode | | |||
| | 16~19 | 最近重启 HAL 状态、拼帧长度、待处理帧长度、连接超时口径 | | |||
| | 20~39 | 10 个完整 32 位统计:RX 事件、有效帧、TX 帧、CRC 错误、忽略站号、非法功能、非法地址、非法值、丢帧、UART 错误 | | |||
| 固件内部也可调用 `ModbusSlaveGetRuntimeDiagnostics()` 获取这些诊断值。Cortex-M4 对齐的 32 位及更小字段保证单字段不撕裂;为避免阻塞运动中断,完整结构允许跨相邻 UART 事件,不应解释为事务级原子快照。USB CDC 提供 `CDC_GetRuntimeDiagnostics()`,记录 OUT 包/字节、接收重挂失败、发送请求/忙/失败/完成;USB OTG FS 中断优先级为 4,低于 PLSR 输出定时器优先级 1 和 TIM6 优先级 2。 | |||
| 同一组USB统计自动发布到控制窗口D1516~D1537,32位字段均为低WORD在前。D1516~D1517与D1536~D1537是首尾generation,读取时必须相同且为偶数;D1518为USB诊断版本1,D1519为`initialized`;D1520~D1535依次为`rxPacketCount`、`rxByteCount`、`rxRearmFailureCount`、`txRequestCount`、`txByteCount`、`txBusyCount`、`txFailureCount`、`txCompleteCount`。Host回归构建发布相同布局、版本和generation,但`initialized`及八个统计值固定为0。 | |||
| ## 基本运行 | |||
| 本脚本只能配合专用 `P18 LONG_STRESS` 验证固件运行:编译前令 `PLSR_BOARD_TEST_SELECT=PLSR_BOARD_TEST_LONG_STRESS`,再编译、烧录并硬复位。P18 为四轴准备 K4 100 kHz 平台参数,保留 Q4~Q7 的 PULSE/DIR 方向映射,并明确禁用软限位和硬限位输入。不要使用 P14/P15/P17 固件运行长稳脚本;它们的 K1 软限位会在约 1,000,000 脉冲处按设计停止。 | |||
| P18 是无软限位的台架耐久测试配置,只能在确认机构脱开、运动范围安全或仅连接逻辑分析仪时使用。默认 COM5、100 kHz、30 分钟: | |||
| ```powershell | |||
| python HostComputer\plsr_modbus_long_stress_test.py | |||
| ``` | |||
| 快速冒烟测试: | |||
| ```powershell | |||
| python HostComputer\plsr_modbus_long_stress_test.py --port COM5 --duration 60 | |||
| ``` | |||
| 脚本固定选择 P18 的 K4,自动完成四轴位置清零、重复序号幂等检查、COMMIT/START、状态一致性轮询、`0x10` 原子动态频率切换,并在结束时对四轴执行软件立即停止。停止清理会逐轴尝试,不会因为某一轴已经停止或应答异常而跳过后续轴。不会要求在 IAR Watch 中改值。 | |||
| 证据默认写入 `HostComputer/long_stress_logs/`: | |||
| - CSV:每个采样点的四轴状态、频率、位置/任务/物理计数和 Modbus 32 位统计; | |||
| - JSON:参数、事件、Modbus与USB设备端起止诊断快照、统计增量、USB主机发送量和最终状态。 | |||
| ## 可选压力与恢复用例 | |||
| USB CDC OUT 与 Modbus 并发(将 COM8 替换为实际 USB 虚拟串口): | |||
| ```powershell | |||
| python HostComputer\plsr_modbus_long_stress_test.py --port COM5 --duration 1800 --usb-port COM8 --usb-rate 64000 | |||
| ``` | |||
| 每 60 秒注入一次仅 CRC 错误的只读请求;固件必须静默丢弃,随后正常通信,`crcErrorCount` 每次至少加 1: | |||
| ```powershell | |||
| python HostComputer\plsr_modbus_long_stress_test.py --port COM5 --duration 600 --bad-crc-period 60 | |||
| ``` | |||
| 运行 120 秒时由主机主动关闭 COM5 3 秒,再重新连接;断线期间四轴脉冲计数必须继续增长: | |||
| ```powershell | |||
| python HostComputer\plsr_modbus_long_stress_test.py --port COM5 --duration 600 --disconnect-at 120 --disconnect-duration 3 | |||
| ``` | |||
| 三个压力项也可组合。故障注入默认关闭,避免日常回归意外中断通信。 | |||
| ## 验收准则 | |||
| - 脚本最终输出 `长稳 PASS`,CSV/JSON 均成功生成; | |||
| - 全程轴 `error=0`、`last_result=0`,状态快照首尾 generation 相同且为偶数; | |||
| - 四轴 `physical_pulses` 单调不回退,动态频率由 FC16 一次写两个 WORD; | |||
| - 非故障注入基线下 `uart_errors`、`dropped_frames`、`restart_failures` 不增长;如增长,JSON 必须保留差值并按失败处理; | |||
| - 开启坏 CRC 时,只允许 `crc_errors` 按注入次数增长,不得出现运动错误或通信失联; | |||
| - 开启计划断线时,重连成功且断线期间四轴计数继续增长; | |||
| - 开启 USB 时,脚本自动读取D1516~D1537起止快照:结束快照必须`initialized=1`,`rxPacketCount`与`rxByteCount`必须增长,`rxRearmFailureCount`不得增长;同时保留主机`bytes_written`且要求`write_errors=0`,无需IAR Watch; | |||
| - 逻辑分析仪抽检 Q0~Q3:频率切换连续,无小于 1 us 窄脉冲、无串扰,停止后全低。 | |||
| 长稳结束后,若需要严格单任务脉冲总数,另跑 P14/P16 脚本和逻辑分析仪 200000 沿验收;本脚本以持续运行、通信恢复和计数单调性为目标。 | |||
| ## 持久化专项边界 | |||
| HSD Backup SRAM双检查点、SFD Flash A/B槽、generation、CRC和故障回退工具已完成代码与Host故障注入覆盖;破坏性持久化诊断在正式构建中默认关闭。真实VBAT断主电恢复、Flash擦写/坏CRC回退、异常复位清除`position_valid`以及反复保存对运动无抖动的板测仍未关闭,必须在所有轴安全停止时单独执行;本长稳脚本不会也不应在运动中发起Flash保存或擦除。 | |||
| @@ -0,0 +1,67 @@ | |||
| # PLSR P16 实时性能统计测试 | |||
| ## 1 功能 | |||
| 固件使用STM32F407的DWT周期计数器,持续记录以下路径从本次复位以来的最坏执行周期: | |||
| - `PlsrProcess`任务自身CPU执行时间; | |||
| - `PlsrProcess`包含高优先级中断抢占的墙钟响应时间; | |||
| - TIM6 10kHz控制刷新中断; | |||
| - TIM10/11/13/14输出定时器中断; | |||
| - TIM9/TIM12硬件计数中断; | |||
| - 双AB末周期快速门控路径。 | |||
| - `PlsrProcess`六个内部阶段的自身CPU时间,用于定位超过1ms的峰值路径。 | |||
| 统计结果通过D1200~D1537控制窗口中的D1256~D1263和D1456~D1469发布。D1205~D1206给出CPU计时频率,D1207为性能统计版本V7;V7保留D1470~D1515持久化诊断并增加D1516~D1537 USB CDC只读统计,但不改变P16各计时字段。该区域属于项目诊断增强,不对应信捷固定软元件。 | |||
| | 地址 | 内容 | | |||
| | --- | --- | | |||
| | D1256~D1257 | PlsrProcess自身CPU最大周期数(32位) | | |||
| | D1258~D1259 | PlsrProcess墙钟响应最大周期数(32位) | | |||
| | D1260~D1261 | TIM6控制ISR最大周期数(32位) | | |||
| | D1262 | 输出定时器ISR最大周期数(16位饱和) | | |||
| | D1263 | TIM9/TIM12计数ISR最大周期数(16位饱和) | | |||
| | D1456~D1457 | 脉冲合并与保护检查最大周期数(32位) | | |||
| | D1458~D1459 | 关键事件处理最大周期数(32位) | | |||
| | D1460~D1461 | 命令队列处理最大周期数(32位) | | |||
| | D1462~D1463 | 普通事件与方向批量提交最大周期数(32位) | | |||
| | D1464~D1465 | HAL tick、路径和Profile推进最大周期数(32位) | | |||
| | D1466~D1467 | 合并后的HSD Backup SRAM检查点最大周期数(32位) | | |||
| | D1468~D1469 | 双AB末周期快速门控最大周期数(32位) | | |||
| ## 2 测试方法 | |||
| 重新复位开发板后运行P14脚本: | |||
| ```powershell | |||
| python HostComputer\plsr_modbus_counter_stress_test.py --port COM5 | |||
| ``` | |||
| 脚本在四轴100kHz、Modbus持续轮询完成后自动读取P16数据,显示cycles和微秒,不需要手工读取Watch。 | |||
| ## 3 自动预算 | |||
| - `PlsrProcess`自身CPU时间必须小于1ms对应周期数。 | |||
| - `PlsrProcess`墙钟响应单独显示;超过1ms时给出抢占提示,不与函数自身WCET混算。 | |||
| - TIM6控制ISR必须小于0.1ms对应周期数。 | |||
| - 输出ISR和计数ISR必须小于100kHz的10us周期数。 | |||
| - 双AB压力测试中,末周期快速门控必须非零且小于420 cycles(168MHz下为2.5us)。P14 PULSE/DIR用例不要求该项非零。 | |||
| - P14所覆盖的五项必须都被实际执行,数值不得为0。 | |||
| - 仍须同时满足P14四轴计数精确、无毛刺和无残余输出;执行时间通过不能替代波形验收。 | |||
| 建议把脚本输出的五个总体最坏值、六个阶段值和五个流程检查点记录到测试报告。V7保留自身CPU时间和包含抢占的墙钟响应时间、六个分阶段独立最大值及合并后的HSD检查点,并保留D1468~D1469的双AB快速门控统计;阶段最大值来自不同轮次,不能直接相加作为总执行时间。 | |||
| ## 4 已关闭的P16板测记录(2026-08-10) | |||
| 硬复位清除DWT历史最大值后,四轴PULSE/DIR 100kHz、每轴200000脉冲及Modbus并发轮询实测: | |||
| - `PlsrProcess`自身最大53748 cycles(319.929us),低于168000 cycles预算; | |||
| - `PlsrProcess`响应最大85655 cycles(509.851us); | |||
| - TIM6控制ISR最大231 cycles(1.375us); | |||
| - 输出ISR最大893 cycles(5.315us); | |||
| - TIM9/TIM12计数ISR最大883 cycles(5.256us)。 | |||
| P14端子波形同时确认Q0~Q3各严格200000个上升沿,因此本次P16 PULSE/DIR性能闭环验收通过。D1468~D1469只属于双AB新路径,仍须随双AB 100kHz板测单独关闭。 | |||
| 该记录是本轮AB、持久化V7窗口和USB诊断落盘前的已通过基线。烧录最终工作区后仍须硬复位并重跑 | |||
| P14/P16;只有新输出继续满足预算且四轴计数/波形不回退,才可作为本批次发布回归证据。 | |||
| @@ -0,0 +1,41 @@ | |||
| # PLSR P15 四轴软限位精度测试 | |||
| ## 1 测试内容 | |||
| 本测试与P14复用同一固件和D1200控制窗口,不需要修改Watch变量。固件公共参数将四轴软限位配置为`-1000000~+1000000`;K1继续用于P14四轴100kHz测试,K2用于以下保护矩阵: | |||
| | 轴 | 方向 | 初始位置 | 频率 | 理论输出 | | |||
| | --- | --- | ---: | ---: | ---: | | |||
| | Q0 | 正 | +999800 | 500Hz | 约200脉冲 | | |||
| | Q1 | 正 | +999000 | 2000Hz | 约1000脉冲 | | |||
| | Q2 | 负 | -999800 | 500Hz | 约200脉冲 | | |||
| | Q3 | 负 | -999000 | 2000Hz | 约1000脉冲 | | |||
| 每组减速时间均为100ms。脚本逐轴执行,检查正负限位、逻辑位置、任务计数、物理累计、兼容停止原因和错误复位。 | |||
| ## 2 测试步骤 | |||
| 1. 编译并烧录当前工程,复位开发板。 | |||
| 2. 逻辑分析仪连接Q0、Q1、Q2、Q3和GND,采样率建议不低于1MS/s,采集至少8秒。 | |||
| 3. 执行: | |||
| ```powershell | |||
| python HostComputer\plsr_modbus_soft_limit_matrix_test.py --port COM5 | |||
| ``` | |||
| 脚本可在P14压力测试之后直接运行;每组开始前都会自动设置当前位置,并在结束后清除该轴错误。 | |||
| ## 3 验收标准 | |||
| - 四轴均因对应软限位进入`PLSR_STATE_STOPPED`,不得进入正常完成状态。 | |||
| - 最终逻辑位置与`+1000000`或`-1000000`的差值不超过1脉冲。 | |||
| - Q0/Q2上升沿分别为199~201;Q1/Q3分别为999~1001。 | |||
| - 正限位错误码为`PLSR_ERROR_LIMIT_POSITIVE`,负限位为`PLSR_ERROR_LIMIT_NEGATIVE`。 | |||
| - 每次`RESET_ERROR`后轴回到IDLE且错误清零。 | |||
| - 减速段连续、无窄脉冲、截断脉冲和停止后残余输出。 | |||
| 如果脚本位置通过而逻辑分析仪脉冲数不通过,应记录对应轴的上升沿数;这表示硬件内部计数边界仍与端子物理边沿不一致。 | |||
| ## 4 已关闭的板测记录(2026-08-10) | |||
| 四轴正负方向、500/2000Hz软限位矩阵已由用户真机确认通过。修复后的保护距离使用当前硬件实际频率并补偿1ms保护采样窗口,最终位置保持在目标软限位的±1脉冲范围内。后续修改保护、计数或停止路径时仍应重跑本脚本回归。 | |||
| @@ -0,0 +1,116 @@ | |||
| # PLSR HSD/SFD 真机持久化验收 | |||
| 本测试验证 STM32F407 板上的真实 Backup SRAM/VBAT 和单 Bank Flash 行为。脚本默认使用 `COM5`,阶段之间由操作者真实断主电或硬复位,不用软件复位代替掉电。 | |||
| ## 固件与诊断窗口 | |||
| 当前控制窗口为 D1200~D1537,D1470~D1499 是只读持久化快照;新增的D1516~D1537 USB诊断块不会移动持久化地址。固件只在 HSD/SFD 的加载、保存、擦除或显式破坏性诊断时计算 CRC;1ms 的 `PlsrModbusControlPoll()` 只复制缓存结果,不扫描 Backup SRAM/Flash,因此不会重新引入 P16 热点。 | |||
| | 地址 | 内容 | | |||
| | --- | --- | | |||
| | D1470~D1471 / D1498~D1499 | 一致性 generation(首尾相同且为偶数) | | |||
| | D1472 | 持久化诊断版本,当前 V1 | | |||
| | D1473 | 低字节 HSD valid mask,高字节 SFD valid mask | | |||
| | D1474 | 低字节 HSD newest mask,高字节 SFD newest mask | | |||
| | D1475 | bit0 HSD dirty;bit1 SFD dirty;bit2 restored position valid(四轴位置均有效时才为1);bit3 restored last busy;bit4 破坏性诊断已编译启用 | | |||
| | D1476~D1480 | HSD/SFD last load、last save、last erase 结果 | | |||
| | D1482~D1489 | HSD A/B 与 SFD A/B generation | | |||
| | D1490~D1493 | 本次上电后成功 HSD/SFD 实际保存次数 | | |||
| | D1494~D1497 | 当前选中 HSD/SFD 记录的已存 CRC32 | | |||
| `save_count` 只在真正完成记录提交时增加。SFD 为 clean 时执行 SAVE 是 no-op,不擦 Flash,也不增加次数。 | |||
| ## 依赖与烧录 | |||
| ```powershell | |||
| py -m pip install -r HostComputer\requirements.txt | |||
| & 'F:\IAR Systems\Embedded Workbench 8.3\common\bin\IarBuild.exe' ` | |||
| 'EWARM\Modbus.ewp' -build Modbus -log warnings | |||
| ``` | |||
| 烧录后硬复位。先确认诊断可读: | |||
| ```powershell | |||
| python HostComputer\plsr_persistence_board_test.py --phase diagnostics | |||
| ``` | |||
| 阶段状态默认保存在系统临时目录的 `plsr_persistence_board_state.json`;可用 `--state-file` 指定别处。 | |||
| ## 1. HSD + VBAT 正常停机恢复 | |||
| ```powershell | |||
| python HostComputer\plsr_persistence_board_test.py --phase hsd-prepare | |||
| ``` | |||
| 脚本自动给四轴写入互不相同的已知位置,等待 HSD 检查点真正完成,并保存期望值。看到 PASS 后关闭主电源,保持 VBAT,等待数秒,再上主电并执行: | |||
| ```powershell | |||
| python HostComputer\plsr_persistence_board_test.py --phase hsd-verify | |||
| ``` | |||
| 验收:四轴位置分别与脚本保存的期望值完全一致、四轴 `position_valid=1`、无脉冲输出、HSD load=OK、restored last busy=0。固件以四轴 `position_valid` 的保守 AND 写入全局元数据,单独校准一轴不会把其他未校准轴误判为可信。 | |||
| ## 2. 运行中掉电/复位安全恢复 | |||
| ```powershell | |||
| python HostComputer\plsr_persistence_board_test.py --phase busy-prepare | |||
| ``` | |||
| 脚本启动一个约 500 秒的长任务并确认端子正在输出。看到“运行中掉电已就绪”后,直接断主电或硬复位,不要发送 STOP。重启后执行: | |||
| ```powershell | |||
| python HostComputer\plsr_persistence_board_test.py --phase busy-verify | |||
| ``` | |||
| 验收:所有轴 IDLE、无自动续跑、无输出、所有轴 `position_valid=0`,诊断显示 restored last busy=1。 | |||
| ## 3. SFD Flash A/B + CRC 上电恢复 | |||
| 验证固件的 P14/P17 自测准备会在启动后写入确定的 SFD RAM 配置并置 dirty;正式固件也可由正常参数配置入口置 dirty。执行一次: | |||
| ```powershell | |||
| python HostComputer\plsr_persistence_board_test.py --phase sfd-save | |||
| ``` | |||
| 脚本只发送一次 SAVE_CONFIG。若 SFD dirty,要求 generation 前进、save_count 只增加 1、CRC 非零;若 SFD 已 clean,则要求 SAVE 正确 no-op,绝不重复擦写。随后硬复位或掉电重启,再执行: | |||
| ```powershell | |||
| python HostComputer\plsr_persistence_board_test.py --phase sfd-verify | |||
| ``` | |||
| 验收:启动时 SFD load=OK,选中槽的 generation 与 CRC 和保存阶段完全一致。 | |||
| ## 4. 运动中禁止 Flash 擦写 | |||
| ```powershell | |||
| python HostComputer\plsr_persistence_board_test.py --phase motion-save-busy | |||
| ``` | |||
| 脚本启动长任务,只发送一次 SAVE_CONFIG,要求内核执行结果为 BUSY 且 SFD save_count 不增加,随后自动 `STOP_IMMEDIATE` 清理。该测试不要求逻辑分析仪,但应观察运动过程中没有因 Flash 擦除产生输出停顿。 | |||
| ## 5. 可选 A/B 回退破坏性测试 | |||
| 正常构建的 `PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG=0`,请求一定返回 NOT_SUPPORTED。只有专用验证构建显式改为 1 时才允许使用。固件仍会同时检查: | |||
| - 两个固定 magic; | |||
| - 非零 sequence 及其 32 位反码; | |||
| - 固定 arm 字; | |||
| - 所有轴 idle 且无输出; | |||
| - 目标介质必须已有两个 CRC 有效槽; | |||
| - 只能失效 newest 槽,协议不接受任意 Flash/内存地址。 | |||
| 执行还需要主机侧双重显式确认: | |||
| ```powershell | |||
| python HostComputer\plsr_persistence_board_test.py ` | |||
| --phase invalidate-sfd ` | |||
| --allow-destructive ` | |||
| --confirm INVALIDATE-NEWEST-SLOT | |||
| ``` | |||
| HSD 对应用 `--phase invalidate-hsd`。失效后应只剩一个有效槽;立即重启/LOAD,确认回退到旧 generation。测试结束务必恢复默认宏并重新烧录生产固件。 | |||
| ## 记录要求 | |||
| 每个阶段保存完整终端输出,至少记录 valid mask、selected generation、CRC、save_count、last load/save 结果和 flags。掉电阶段同时记录 VBAT 是否保持、断电时长、复位方式与上电时间。不得把 Host 模拟测试替代为上述真实掉电证据。 | |||
| @@ -0,0 +1,492 @@ | |||
| #!/usr/bin/env python3 | |||
| """PLSR dual-AB 100 kHz hardware-counter and fast-gate stress test.""" | |||
| from __future__ import annotations | |||
| import argparse | |||
| import time | |||
| from dataclasses import dataclass | |||
| import serial | |||
| from plsr_modbus_counter_stress_test import ( | |||
| AB_GATE_PERFORMANCE_BASE, | |||
| AXIS_STATUS_WORDS, | |||
| CALL_COMMIT, | |||
| CALL_REQUEST, | |||
| CALL_RESPONSE, | |||
| CALL_START, | |||
| CMD_SET_POSITION, | |||
| CONTROL_BASE, | |||
| CONTROL_WINDOW_WORDS, | |||
| PERFORMANCE_BASE, | |||
| PERFORMANCE_VERSION, | |||
| RESULT_OK, | |||
| RESULT_QUEUED, | |||
| RtuClient, | |||
| S0_BASES, | |||
| S1_BASES, | |||
| check_result, | |||
| get_u32, | |||
| put_u32, | |||
| read_axis_status, | |||
| send_command, | |||
| wait_response, | |||
| ) | |||
| from plsr_modbus_frequency_test import choose_port | |||
| TEST_FREQUENCY_HZ = 100_000 | |||
| TEST_LOW_FREQUENCY_HZ = 50_000 | |||
| AB_OUTPUT_MODE = 1 | |||
| AB_S2_SET = 3 | |||
| AB_OWNERS = (0, 2) | |||
| CMD_PAUSE = 3 | |||
| CMD_RESUME = 4 | |||
| STATE_ACCEL = 2 | |||
| STATE_RUN = 3 | |||
| STATE_DECEL = 4 | |||
| STATE_PAUSED = 6 | |||
| STATE_COMPLETED = 7 | |||
| RUNNING_STATES = {STATE_ACCEL, STATE_RUN, STATE_DECEL} | |||
| @dataclass(frozen=True) | |||
| class AbCase: | |||
| name: str | |||
| pulses_axis0: int | |||
| pulses_axis2: int | |||
| require_independent_stop: bool | |||
| exercise_dynamic_pause: bool = False | |||
| CASES = { | |||
| "independent": AbCase( | |||
| name="独立停止(Q0/Q1先停,Q2/Q3继续)", | |||
| pulses_axis0=100_000, | |||
| pulses_axis2=-200_000, | |||
| require_independent_stop=True, | |||
| ), | |||
| "simultaneous": AbCase( | |||
| name="等长双AB并发完成(相同目标周期数)", | |||
| pulses_axis0=-200_000, | |||
| pulses_axis2=200_000, | |||
| require_independent_stop=False, | |||
| ), | |||
| "dynamic": AbCase( | |||
| name="双AB变频与单组PAUSE/RESUME", | |||
| pulses_axis0=300_000, | |||
| pulses_axis2=-300_000, | |||
| require_independent_stop=False, | |||
| exercise_dynamic_pause=True, | |||
| ), | |||
| } | |||
| def send_ab_call( | |||
| client: RtuClient, sequence: int, axis: int, operation: int | |||
| ) -> list[int]: | |||
| request = [0] * 16 | |||
| put_u32(request, 0, sequence) | |||
| request[2] = 0 # S0 device D | |||
| put_u32(request, 3, S0_BASES[axis]) | |||
| request[5] = 0 # S1 device D | |||
| put_u32(request, 6, S1_BASES[axis]) | |||
| request[8] = 0 # S2 constant | |||
| put_u32(request, 10, AB_S2_SET) | |||
| request[12] = axis | |||
| request[13] = AB_OUTPUT_MODE | |||
| request[14] = operation | |||
| client.write_multiple(CALL_REQUEST, request) | |||
| return wait_response(client, CALL_RESPONSE, 12, sequence) | |||
| def wait_command_applied( | |||
| client: RtuClient, axis: int, sequence: int, timeout: float = 3.0 | |||
| ) -> dict[str, int]: | |||
| deadline = time.monotonic() + timeout | |||
| latest: dict[str, int] | None = None | |||
| while time.monotonic() < deadline: | |||
| latest = read_axis_status(client, axis) | |||
| if latest["last_sequence"] == sequence: | |||
| if latest["last_result"] != RESULT_OK: | |||
| raise RuntimeError( | |||
| f"轴{axis}命令#{sequence}执行失败:{latest}" | |||
| ) | |||
| return latest | |||
| raise RuntimeError(f"等待轴{axis}命令#{sequence}执行超时:{latest}") | |||
| def wait_axis_state( | |||
| client: RtuClient, | |||
| axis: int, | |||
| expected: set[int], | |||
| timeout: float = 4.0, | |||
| ) -> dict[str, int]: | |||
| deadline = time.monotonic() + timeout | |||
| latest: dict[str, int] | None = None | |||
| while time.monotonic() < deadline: | |||
| latest = read_axis_status(client, axis) | |||
| if latest["error"] != 0: | |||
| raise RuntimeError(f"轴{axis}等待状态时进入错误:{latest}") | |||
| if latest["state"] in expected: | |||
| return latest | |||
| raise RuntimeError( | |||
| f"等待轴{axis}状态{sorted(expected)}超时,最后状态={latest}" | |||
| ) | |||
| def write_live_frequency(client: RtuClient, frequency_hz: int) -> None: | |||
| words = [0, 0] | |||
| put_u32(words, 0, frequency_hz) | |||
| for axis in AB_OWNERS: | |||
| # One FC16 writes the complete signed INT32 target atomically. | |||
| client.write_multiple(S0_BASES[axis] + 10, words) | |||
| def wait_live_frequency( | |||
| client: RtuClient, frequency_hz: int, timeout: float = 3.0 | |||
| ) -> dict[int, dict[str, int]]: | |||
| deadline = time.monotonic() + timeout | |||
| latest: dict[int, dict[str, int]] = {} | |||
| while time.monotonic() < deadline: | |||
| latest = {axis: read_axis_status(client, axis) for axis in AB_OWNERS} | |||
| if all( | |||
| status["state"] in RUNNING_STATES | |||
| and status["target_frequency"] == frequency_hz | |||
| and status["current_frequency"] == frequency_hz | |||
| for status in latest.values() | |||
| ): | |||
| return latest | |||
| raise RuntimeError( | |||
| f"双AB未稳定到{frequency_hz}Hz,最后状态={latest}" | |||
| ) | |||
| def exercise_dynamic_pause_resume( | |||
| client: RtuClient, sequence: int | |||
| ) -> int: | |||
| write_live_frequency(client, TEST_LOW_FREQUENCY_HZ) | |||
| slowed = wait_live_frequency(client, TEST_LOW_FREQUENCY_HZ) | |||
| time.sleep(0.05) | |||
| slowed_again = { | |||
| axis: read_axis_status(client, axis) for axis in AB_OWNERS | |||
| } | |||
| if any( | |||
| abs(slowed_again[axis]["task_pulses"]) | |||
| <= abs(slowed[axis]["task_pulses"]) | |||
| for axis in AB_OWNERS | |||
| ): | |||
| raise RuntimeError(f"双AB降频后计数未继续增长:{slowed_again}") | |||
| write_live_frequency(client, TEST_FREQUENCY_HZ) | |||
| wait_live_frequency(client, TEST_FREQUENCY_HZ) | |||
| response = send_command(client, sequence, 0, CMD_PAUSE, 0) | |||
| check_result(response, 4, RESULT_QUEUED, "轴0 AB PAUSE") | |||
| wait_command_applied(client, 0, sequence) | |||
| sequence += 1 | |||
| paused = wait_axis_state(client, 0, {STATE_PAUSED}) | |||
| other_before = read_axis_status(client, 2) | |||
| time.sleep(0.05) | |||
| paused_again = read_axis_status(client, 0) | |||
| other_after = read_axis_status(client, 2) | |||
| if ( | |||
| paused_again["state"] != STATE_PAUSED | |||
| or paused_again["physical_pulses"] != paused["physical_pulses"] | |||
| or abs(other_after["task_pulses"]) | |||
| <= abs(other_before["task_pulses"]) | |||
| ): | |||
| raise RuntimeError( | |||
| "AB PAUSE独立性失败:" | |||
| f"paused={paused}, paused_again={paused_again}, " | |||
| f"other_before={other_before}, other_after={other_after}" | |||
| ) | |||
| response = send_command(client, sequence, 0, CMD_RESUME, 0) | |||
| check_result(response, 4, RESULT_QUEUED, "轴0 AB RESUME") | |||
| wait_command_applied(client, 0, sequence) | |||
| sequence += 1 | |||
| resumed = wait_axis_state(client, 0, RUNNING_STATES) | |||
| resume_deadline = time.monotonic() + 1.0 | |||
| while time.monotonic() < resume_deadline: | |||
| resumed_again = read_axis_status(client, 0) | |||
| if abs(resumed_again["task_pulses"]) > abs(resumed["task_pulses"]): | |||
| break | |||
| else: | |||
| raise RuntimeError(f"AB RESUME后计数未恢复:{resumed_again}") | |||
| print("动态控制已通过:双AB 100k→50k→100k,Q0/Q1在00边界暂停并恢复") | |||
| return sequence | |||
| def prepare_job(client: RtuClient, axis: int, signed_pulses: int) -> None: | |||
| s0 = [0] * 20 | |||
| put_u32(s0, 0, 1) | |||
| put_u32(s0, 10, TEST_FREQUENCY_HZ) | |||
| put_u32(s0, 12, signed_pulses) | |||
| client.write_multiple(S0_BASES[axis], s0) | |||
| client.write_multiple(S1_BASES[axis], [0] * 4) | |||
| def validate_final_status( | |||
| axis: int, | |||
| status: dict[str, int], | |||
| signed_pulses: int, | |||
| physical_baseline: int, | |||
| ) -> None: | |||
| expected_physical = abs(signed_pulses) | |||
| checks = { | |||
| "状态": (status["state"], STATE_COMPLETED), | |||
| "错误": (status["error"], 0), | |||
| "执行结果": (status["last_result"], RESULT_OK), | |||
| "逻辑位置": (status["logical_position"], signed_pulses), | |||
| "任务周期数": (status["task_pulses"], signed_pulses), | |||
| "物理周期增量": ( | |||
| status["physical_pulses"] - physical_baseline, | |||
| expected_physical, | |||
| ), | |||
| } | |||
| bad = {name: value for name, value in checks.items() if value[0] != value[1]} | |||
| if bad: | |||
| raise RuntimeError(f"轴{axis} AB最终状态不正确:{bad};状态={status}") | |||
| def run_case( | |||
| client: RtuClient, case: AbCase, sequence: int | |||
| ) -> tuple[int, int]: | |||
| print(f"\n开始用例:{case.name}") | |||
| signed_targets = {0: case.pulses_axis0, 2: case.pulses_axis2} | |||
| for axis in AB_OWNERS: | |||
| response = send_command(client, sequence, axis, CMD_SET_POSITION, 0) | |||
| check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION") | |||
| wait_command_applied(client, axis, sequence) | |||
| sequence += 1 | |||
| prepare_job(client, axis, signed_targets[axis]) | |||
| for axis in AB_OWNERS: | |||
| response = send_ab_call(client, sequence, axis, CALL_COMMIT) | |||
| check_result(response, 3, RESULT_OK, f"轴{axis} AB COMMIT") | |||
| if response[11] != 1: | |||
| raise RuntimeError(f"轴{axis} AB COMMIT未建立有效快照") | |||
| sequence += 1 | |||
| baselines = { | |||
| axis: read_axis_status(client, axis)["physical_pulses"] | |||
| for axis in AB_OWNERS | |||
| } | |||
| started = time.monotonic() | |||
| for axis in AB_OWNERS: | |||
| response = send_ab_call(client, sequence, axis, CALL_START) | |||
| check_result(response, 3, RESULT_QUEUED, f"轴{axis} AB START") | |||
| sequence += 1 | |||
| running: dict[int, dict[str, int]] = {} | |||
| deadline = time.monotonic() + 4.0 | |||
| while time.monotonic() < deadline: | |||
| running = {axis: read_axis_status(client, axis) for axis in AB_OWNERS} | |||
| if all(item["state"] in RUNNING_STATES for item in running.values()): | |||
| break | |||
| else: | |||
| raise RuntimeError(f"双AB未同时进入运行态:{running}") | |||
| for axis, status in running.items(): | |||
| if status["counter_mode"] != 1: | |||
| raise RuntimeError(f"轴{axis}未取得AB硬件计数器:{status}") | |||
| if status["current_frequency"] != TEST_FREQUENCY_HZ: | |||
| raise RuntimeError(f"轴{axis}未达到100kHz:{status}") | |||
| print("运行期租约正确:Q0/Q1→TIM9,Q2/Q3→TIM12,双组均为硬件计数") | |||
| if case.exercise_dynamic_pause: | |||
| sequence = exercise_dynamic_pause_resume(client, sequence) | |||
| deadline = time.monotonic() + 8.0 | |||
| latest = running | |||
| independent_stop_seen = False | |||
| while time.monotonic() < deadline: | |||
| latest = {axis: read_axis_status(client, axis) for axis in AB_OWNERS} | |||
| if ( | |||
| case.require_independent_stop | |||
| and latest[0]["state"] == STATE_COMPLETED | |||
| and latest[2]["state"] in RUNNING_STATES | |||
| ): | |||
| q2_before = abs(latest[2]["task_pulses"]) | |||
| q0_physical = latest[0]["physical_pulses"] | |||
| time.sleep(0.05) | |||
| stopped_again = read_axis_status(client, 0) | |||
| running_again = read_axis_status(client, 2) | |||
| independent_stop_seen = ( | |||
| stopped_again["state"] == STATE_COMPLETED | |||
| and stopped_again["physical_pulses"] == q0_physical | |||
| and running_again["state"] in RUNNING_STATES | |||
| and abs(running_again["task_pulses"]) > q2_before | |||
| ) | |||
| if independent_stop_seen: | |||
| print("独立停止已观测:Q0/Q1保持低且计数冻结,Q2/Q3继续计数") | |||
| if all(item["state"] == STATE_COMPLETED for item in latest.values()): | |||
| break | |||
| else: | |||
| raise RuntimeError(f"等待双AB完成超时:{latest}") | |||
| if case.require_independent_stop and not independent_stop_seen: | |||
| raise RuntimeError("未观测到第一组停止后第二组继续运行的独立停止窗口") | |||
| for axis in AB_OWNERS: | |||
| validate_final_status( | |||
| axis, latest[axis], signed_targets[axis], baselines[axis] | |||
| ) | |||
| elapsed = time.monotonic() - started | |||
| print( | |||
| f"用例 PASS:Q0/Q1={abs(case.pulses_axis0)}完整AB周期," | |||
| f"Q2/Q3={abs(case.pulses_axis2)}完整AB周期,耗时{elapsed:.3f}s" | |||
| ) | |||
| return sequence, int(elapsed * 1_000) | |||
| def validate_dwt(client: RtuClient, header: list[int]) -> None: | |||
| core_clock_hz = get_u32(header, 5) | |||
| if core_clock_hz == 0 or header[7] != PERFORMANCE_VERSION: | |||
| raise RuntimeError( | |||
| f"P16诊断头无效:clock={core_clock_hz}, version={header[7]}" | |||
| ) | |||
| performance = client.read_holding(PERFORMANCE_BASE, 8) | |||
| performance_again = client.read_holding(PERFORMANCE_BASE, 8) | |||
| if performance != performance_again: | |||
| performance = performance_again | |||
| ab_gate_cycles = get_u32( | |||
| client.read_holding(AB_GATE_PERFORMANCE_BASE, 2), 0 | |||
| ) | |||
| values = { | |||
| "PlsrProcess自身": get_u32(performance, 0), | |||
| "PlsrProcess响应": get_u32(performance, 2), | |||
| "TIM6控制ISR": get_u32(performance, 4), | |||
| "输出定时器ISR": performance[6], | |||
| "TIM9/12计数ISR": performance[7], | |||
| "AB末周期快速门控": ab_gate_cycles, | |||
| } | |||
| budgets = { | |||
| "PlsrProcess自身": core_clock_hz // 1_000, | |||
| "TIM6控制ISR": core_clock_hz // 10_000, | |||
| "输出定时器ISR": core_clock_hz // TEST_FREQUENCY_HZ, | |||
| "TIM9/12计数ISR": core_clock_hz // TEST_FREQUENCY_HZ, | |||
| # Gate must finish within one 100kHz quarter-period (2.5us). | |||
| "AB末周期快速门控": core_clock_hz // 400_000, | |||
| } | |||
| print("\nP16/AB DWT最坏执行时间:") | |||
| for name, cycles in values.items(): | |||
| budget = budgets.get(name) | |||
| budget_text = f"预算<{budget}" if budget is not None else "观测项" | |||
| print( | |||
| f" {name:<18} {cycles:8d} cycles " | |||
| f"{cycles * 1_000_000.0 / core_clock_hz:8.3f}us {budget_text}" | |||
| ) | |||
| missing = [name for name, cycles in values.items() if cycles == 0] | |||
| if missing: | |||
| raise RuntimeError("DWT路径未实际执行:" + "、".join(missing)) | |||
| overruns = { | |||
| name: (values[name], budget) | |||
| for name, budget in budgets.items() | |||
| if values[name] >= budget | |||
| } | |||
| if overruns: | |||
| raise RuntimeError(f"AB实时预算超限:{overruns}") | |||
| def print_logic_analyzer_acceptance(selected: list[str]) -> None: | |||
| print("\n逻辑分析仪验收(CH0~CH3=Q0~Q3,建议100MS/s,四通道同步):") | |||
| if selected == ["independent"]: | |||
| print(" 上升沿:CH0=CH1=100000,CH2=CH3=200000。") | |||
| elif selected == ["simultaneous"]: | |||
| print(" 上升沿:CH0=CH1=CH2=CH3=200000。") | |||
| elif selected == ["dynamic"]: | |||
| print(" 上升沿:CH0=CH1=CH2=CH3=300000(含Q0/Q1暂停窗口)。") | |||
| else: | |||
| print( | |||
| " 连续采全部用例时累计上升沿:CH0=CH1=600000," | |||
| "CH2=CH3=700000;精确逐用例验收建议分别用 --case 采集。" | |||
| ) | |||
| print( | |||
| " 100kHz区间周期10.000us/高宽5.000us;dynamic的50kHz区间" | |||
| "周期20.000us/高宽10.000us;无<1us窄脉冲。" | |||
| ) | |||
| print(" 正向:00→10→11→01→00;反向:00→01→11→10→00。") | |||
| print( | |||
| " 独立停止用例:Q0/Q1正向且先停,Q2/Q3反向并继续;" | |||
| "等长用例方向相反。" | |||
| ) | |||
| print( | |||
| " dynamic用例:调频前后保持严格±90°且无额外边沿;Q0/Q1只在00" | |||
| "边界进入低电平暂停,Q2/Q3连续运行,RESUME从00重建相序。" | |||
| ) | |||
| print( | |||
| " 每组首跳前必须为00;A/B首个上升沿相隔2.50us(建议容差±0.05us)," | |||
| "不得近似同时上升。" | |||
| ) | |||
| print(" 末周期必须完整回到00,停止后至少1ms全低、无残余边沿。") | |||
| def main() -> int: | |||
| parser = argparse.ArgumentParser( | |||
| description="PLSR 双AB 100kHz硬件计数、独立停止与快速门控压力测试" | |||
| ) | |||
| parser.add_argument("--port", help="串口,例如COM5;只有一个串口时可省略") | |||
| parser.add_argument("--baud", type=int, default=9600) | |||
| parser.add_argument("--slave", type=int, default=1) | |||
| parser.add_argument( | |||
| "--case", | |||
| choices=("all", "independent", "simultaneous", "dynamic"), | |||
| default="all", | |||
| help="默认依次执行独立停止、等长并发、动态调频/暂停三个用例", | |||
| ) | |||
| args = parser.parse_args() | |||
| selected = ( | |||
| ["independent", "simultaneous", "dynamic"] | |||
| if args.case == "all" | |||
| else [args.case] | |||
| ) | |||
| with serial.Serial( | |||
| port=choose_port(args.port), | |||
| baudrate=args.baud, | |||
| bytesize=serial.EIGHTBITS, | |||
| parity=serial.PARITY_EVEN, | |||
| stopbits=serial.STOPBITS_ONE, | |||
| timeout=1.0, | |||
| write_timeout=1.0, | |||
| ) as uart: | |||
| client = RtuClient(uart, args.slave) | |||
| header = client.read_holding(CONTROL_BASE, 8) | |||
| if header[:5] != [ | |||
| 0x504C, | |||
| 0x5352, | |||
| 0x0100, | |||
| CONTROL_WINDOW_WORDS, | |||
| 0x0007, | |||
| ]: | |||
| raise RuntimeError( | |||
| f"AB控制窗口未就绪:{header};请烧录当前固件并硬复位" | |||
| ) | |||
| if header[7] != PERFORMANCE_VERSION: | |||
| raise RuntimeError( | |||
| f"AB测试要求性能统计V{PERFORMANCE_VERSION},当前V{header[7]}" | |||
| ) | |||
| if AXIS_STATUS_WORDS != 48: | |||
| raise RuntimeError("上位机轴状态结构版本不匹配") | |||
| print("双AB测试就绪:K3,100kHz,Q0/Q1与Q2/Q3") | |||
| sequence = max(1, (time.monotonic_ns() >> 20) & 0x7FFFFFFF) | |||
| for index, name in enumerate(selected): | |||
| sequence, _elapsed_ms = run_case(client, CASES[name], sequence) | |||
| if index + 1 < len(selected): | |||
| time.sleep(0.25) | |||
| validate_dwt(client, header) | |||
| print_logic_analyzer_acceptance(selected) | |||
| print("\n全部自动检查PASS;最终结论仍需逻辑分析仪四通道波形通过。") | |||
| return 0 | |||
| if __name__ == "__main__": | |||
| try: | |||
| raise SystemExit(main()) | |||
| except (RuntimeError, serial.SerialException) as error: | |||
| print(f"测试失败:{error}") | |||
| raise SystemExit(1) | |||
| @@ -0,0 +1,143 @@ | |||
| #!/usr/bin/env python3 | |||
| """Validate the production Modbus M -> PLSR WAIT bit-data path. | |||
| X is intentionally read-only through function 0x02. A real X/EXT/hard-limit | |||
| test is only possible after the board-specific GPIO-to-X mapping is supplied. | |||
| """ | |||
| from __future__ import annotations | |||
| import argparse | |||
| import struct | |||
| import time | |||
| import serial | |||
| from plsr_modbus_control_test import ( | |||
| CALL_COMMIT, | |||
| CALL_START, | |||
| CONTROL_BASE, | |||
| CONTROL_WINDOW_WORDS, | |||
| S0_BASE, | |||
| S1_BASE, | |||
| check_result, | |||
| read_axis_status, | |||
| send_call, | |||
| ) | |||
| from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words | |||
| RESULT_OK = 0 | |||
| STATE_WAIT = 5 | |||
| STATE_COMPLETED = 7 | |||
| M_TEST_POINT = 123 | |||
| def write_coil(client: RtuClient, address: int, value: bool) -> None: | |||
| encoded = 0xFF00 if value else 0x0000 | |||
| pdu = bytes((0x05,)) + struct.pack(">HH", address, encoded) | |||
| response = client.exchange(pdu, 8) | |||
| expected = bytes((client.slave, 0x05)) + struct.pack(">HH", address, encoded) | |||
| if response[:6] != expected: | |||
| raise RuntimeError(f"FC05 回显错误: {response.hex(' ')}") | |||
| def read_bits(client: RtuClient, function: int, address: int, count: int) -> list[int]: | |||
| byte_count = (count + 7) // 8 | |||
| pdu = bytes((function,)) + struct.pack(">HH", address, count) | |||
| response = client.exchange(pdu, 5 + byte_count) | |||
| if response[1] != function or response[2] != byte_count: | |||
| raise RuntimeError(f"FC{function:02X} 响应格式错误: {response.hex(' ')}") | |||
| return [ | |||
| (response[3 + (index // 8)] >> (index % 8)) & 1 | |||
| for index in range(count) | |||
| ] | |||
| def wait_for_state(client: RtuClient, expected: int, timeout: float) -> dict[str, int]: | |||
| deadline = time.monotonic() + timeout | |||
| latest: dict[str, int] | None = None | |||
| while time.monotonic() < deadline: | |||
| latest = read_axis_status(client) | |||
| if latest["state"] == expected: | |||
| return latest | |||
| raise RuntimeError(f"等待状态 {expected} 超时,最后状态: {latest}") | |||
| def prepare_wait_job(client: RtuClient) -> None: | |||
| words = [0] * 20 | |||
| words[0:2] = signed_dword_words(1) # one segment | |||
| words[10:12] = signed_dword_words(1000) # 1000 Hz | |||
| words[12:14] = signed_dword_words(500) # +500 pulses | |||
| words[14] = (2 << 8) | 5 # WAIT_SIGNAL, source M | |||
| words[15:17] = signed_dword_words(M_TEST_POINT) | |||
| words[17] = 0 # constant fall-through jump | |||
| words[18:20] = signed_dword_words(0) | |||
| client.write_multiple(S0_BASE, words) | |||
| client.write_multiple(S1_BASE, [0, 0, 0, 0]) | |||
| def main() -> int: | |||
| parser = argparse.ArgumentParser( | |||
| description="PLSR 真实 Modbus M 位源、WAIT 与 FC02 X 只读视图测试" | |||
| ) | |||
| parser.add_argument("--port", default="COM5") | |||
| parser.add_argument("--baud", type=int, default=9600) | |||
| parser.add_argument("--slave", type=int, default=1) | |||
| args = parser.parse_args() | |||
| with serial.Serial( | |||
| port=choose_port(args.port), | |||
| baudrate=args.baud, | |||
| bytesize=serial.EIGHTBITS, | |||
| parity=serial.PARITY_EVEN, | |||
| stopbits=serial.STOPBITS_ONE, | |||
| timeout=1.0, | |||
| write_timeout=1.0, | |||
| ) as uart: | |||
| client = RtuClient(uart, args.slave) | |||
| header = client.read_holding(CONTROL_BASE, 8) | |||
| if header[3] != CONTROL_WINDOW_WORDS: | |||
| raise RuntimeError( | |||
| f"控制窗口版本不匹配: firmware={header[3]}, script={CONTROL_WINDOW_WORDS}" | |||
| ) | |||
| # FC02 must exist and must not alias writable M coils. | |||
| x_before = read_bits(client, 0x02, M_TEST_POINT, 1)[0] | |||
| write_coil(client, M_TEST_POINT, False) | |||
| if read_bits(client, 0x01, M_TEST_POINT, 1) != [0]: | |||
| raise RuntimeError("FC05 写 M=0 后 FC01 回读不一致") | |||
| if read_bits(client, 0x02, M_TEST_POINT, 1)[0] != x_before: | |||
| raise RuntimeError("X 与 M 发生别名:写 M 意外改变了 FC02 X") | |||
| prepare_wait_job(client) | |||
| sequence = int(time.time()) & 0x7FFFFFFF | |||
| response = send_call(client, sequence, CALL_COMMIT) | |||
| check_result(response, RESULT_OK, "COMMIT") | |||
| response = send_call(client, sequence + 1, CALL_START) | |||
| check_result(response, RESULT_OK, "START") | |||
| waiting = wait_for_state(client, STATE_WAIT, 5.0) | |||
| if waiting["task_pulses"] != 500: | |||
| raise RuntimeError(f"进入 WAIT 时任务脉冲不是 500: {waiting}") | |||
| print("M123=0:500 脉冲完成后稳定进入 WAIT") | |||
| write_coil(client, M_TEST_POINT, True) | |||
| if read_bits(client, 0x01, M_TEST_POINT, 1) != [1]: | |||
| raise RuntimeError("FC05 写 M=1 后 FC01 回读不一致") | |||
| completed = wait_for_state(client, STATE_COMPLETED, 3.0) | |||
| if completed["task_pulses"] != 500: | |||
| raise RuntimeError(f"WAIT 释放后任务计数异常: {completed}") | |||
| print("M123 0->1:PLSR WAIT 已释放并正常 COMPLETED") | |||
| print(f"FC02 X123 只读值={x_before};写 M 不会改变 X") | |||
| print("PASS:Modbus FC05/FC01 -> M image -> PLSR readBit/WAIT 生产链通过") | |||
| print("待硬件映射后再测:实际 X 输入、EXT 上升沿及正/负硬限位。") | |||
| return 0 | |||
| if __name__ == "__main__": | |||
| try: | |||
| raise SystemExit(main()) | |||
| except (RuntimeError, serial.SerialException) as error: | |||
| print(f"测试失败:{error}") | |||
| raise SystemExit(1) | |||
| @@ -12,6 +12,7 @@ from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_word | |||
| CONTROL_BASE = 1200 | |||
| CONTROL_WINDOW_WORDS = 338 | |||
| S0_BASE = 1600 | |||
| S1_BASE = 1700 | |||
| CALL_REQUEST = CONTROL_BASE + 8 | |||
| @@ -122,6 +123,7 @@ def read_axis_status(client: RtuClient) -> dict[str, int]: | |||
| "logical_position": get_u64(words, 16, signed=True), | |||
| "task_pulses": get_u64(words, 20, signed=True), | |||
| "physical_pulses": get_u64(words, 28), | |||
| "counter_mode": words[37], | |||
| "current_frequency": get_u32(words, 38), | |||
| "target_frequency": get_u32(words, 40), | |||
| } | |||
| @@ -186,9 +188,15 @@ def main() -> int: | |||
| ) as uart: | |||
| client = RtuClient(uart, args.slave) | |||
| header = client.read_holding(CONTROL_BASE, 8) | |||
| if header[:5] != [0x504C, 0x5352, 0x0100, 256, 0x0007]: | |||
| if header[:5] != [ | |||
| 0x504C, | |||
| 0x5352, | |||
| 0x0100, | |||
| CONTROL_WINDOW_WORDS, | |||
| 0x0007, | |||
| ]: | |||
| raise RuntimeError(f"P13 控制窗口未就绪:{header}") | |||
| print("P13 控制窗口就绪:D1200~D1455,协议 V1.0") | |||
| print("P13 控制窗口就绪:D1200~D1537,协议 V1.0") | |||
| s0 = [0] * 20 | |||
| put_u32(s0, 0, 1) | |||
| @@ -0,0 +1,371 @@ | |||
| #!/usr/bin/env python3 | |||
| """PLSR P14 four-axis 100 kHz hardware-counter stress test.""" | |||
| from __future__ import annotations | |||
| import argparse | |||
| import time | |||
| import serial | |||
| from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words | |||
| CONTROL_BASE = 1200 | |||
| CONTROL_WINDOW_WORDS = 338 | |||
| CALL_REQUEST = CONTROL_BASE + 8 | |||
| CALL_RESPONSE = CONTROL_BASE + 24 | |||
| COMMAND_REQUEST = CONTROL_BASE + 40 | |||
| COMMAND_RESPONSE = CONTROL_BASE + 48 | |||
| AXIS_STATUS_BASE = CONTROL_BASE + 64 | |||
| AXIS_STATUS_WORDS = 48 | |||
| PERFORMANCE_BASE = CONTROL_BASE + 56 | |||
| STAGE_PERFORMANCE_BASE = CONTROL_BASE + 256 | |||
| AB_GATE_PERFORMANCE_BASE = CONTROL_BASE + 268 | |||
| PERFORMANCE_VERSION = 7 | |||
| STAGE_NAMES = ( | |||
| "脉冲合并/保护", | |||
| "关键事件", | |||
| "命令队列", | |||
| "普通事件/方向提交", | |||
| "HAL/路径/Profile", | |||
| "HSD检查点", | |||
| ) | |||
| STAGE_PERFORMANCE_WORDS = len(STAGE_NAMES) * 2 | |||
| S0_BASES = (1600, 1800, 2000, 2200) | |||
| S1_BASES = (1700, 1900, 2100, 2300) | |||
| TEST_FREQUENCY_HZ = 100_000 | |||
| TEST_PULSES = 200_000 | |||
| RESULT_OK = 0 | |||
| RESULT_QUEUED = 1 | |||
| STATE_ACCEL = 2 | |||
| STATE_RUN = 3 | |||
| STATE_COMPLETED = 7 | |||
| CALL_COMMIT = 1 | |||
| CALL_START = 2 | |||
| CMD_SET_POSITION = 5 | |||
| def put_u32(words: list[int], offset: int, value: int) -> None: | |||
| words[offset : offset + 2] = signed_dword_words(value) | |||
| def put_u64(words: list[int], offset: int, value: int) -> None: | |||
| raw = value & 0xFFFFFFFFFFFFFFFF | |||
| words[offset : offset + 4] = [ | |||
| (raw >> shift) & 0xFFFF for shift in (0, 16, 32, 48) | |||
| ] | |||
| def get_u32(words: list[int], offset: int) -> int: | |||
| return words[offset] | (words[offset + 1] << 16) | |||
| def get_u64(words: list[int], offset: int, signed: bool = False) -> int: | |||
| raw = sum(words[offset + index] << (16 * index) for index in range(4)) | |||
| if signed and raw & (1 << 63): | |||
| return raw - (1 << 64) | |||
| return raw | |||
| def wait_response( | |||
| client: RtuClient, address: int, words: int, sequence: int, timeout: float = 2.0 | |||
| ) -> list[int]: | |||
| deadline = time.monotonic() + timeout | |||
| while time.monotonic() < deadline: | |||
| response = client.read_holding(address, words) | |||
| if get_u32(response, 0) == sequence: | |||
| return response | |||
| raise RuntimeError(f"等待序号 {sequence} 的应答超时") | |||
| def send_command( | |||
| client: RtuClient, sequence: int, axis: int, opcode: int, argument: int = 0 | |||
| ) -> list[int]: | |||
| request = [0] * 8 | |||
| put_u32(request, 0, sequence) | |||
| request[2] = opcode | |||
| request[3] = axis | |||
| put_u64(request, 4, argument) | |||
| client.write_multiple(COMMAND_REQUEST, request) | |||
| return wait_response(client, COMMAND_RESPONSE, 8, sequence) | |||
| def send_call( | |||
| client: RtuClient, | |||
| sequence: int, | |||
| axis: int, | |||
| operation: int, | |||
| s2_set: int = 1, | |||
| ) -> list[int]: | |||
| request = [0] * 16 | |||
| put_u32(request, 0, sequence) | |||
| request[2] = 0 # S0 device D | |||
| put_u32(request, 3, S0_BASES[axis]) | |||
| request[5] = 0 # S1 device D | |||
| put_u32(request, 6, S1_BASES[axis]) | |||
| request[8] = 0 # S2 constant | |||
| put_u32(request, 10, s2_set) | |||
| request[12] = axis | |||
| request[13] = 0 # PULSE/DIR | |||
| request[14] = operation | |||
| client.write_multiple(CALL_REQUEST, request) | |||
| return wait_response(client, CALL_RESPONSE, 12, sequence) | |||
| def check_result(response: list[int], offset: int, expected: int, label: str) -> None: | |||
| if response[offset] != expected: | |||
| raise RuntimeError( | |||
| f"{label} 返回 {response[offset]},期望 {expected};应答={response}" | |||
| ) | |||
| def read_axis_status(client: RtuClient, axis: int) -> dict[str, int]: | |||
| address = AXIS_STATUS_BASE + axis * AXIS_STATUS_WORDS | |||
| words = client.read_holding(address, AXIS_STATUS_WORDS) | |||
| generation_begin = get_u32(words, 0) | |||
| generation_end = get_u32(words, 46) | |||
| if generation_begin != generation_end or generation_begin & 1: | |||
| raise RuntimeError( | |||
| f"轴{axis}状态快照不一致:begin={generation_begin}, end={generation_end}" | |||
| ) | |||
| return { | |||
| "state": words[2], | |||
| "flags": get_u32(words, 3), | |||
| "error": words[6], | |||
| "stop_reason": words[7], | |||
| "last_result": words[8], | |||
| "last_sequence": get_u32(words, 10), | |||
| "logical_position": get_u64(words, 16, signed=True), | |||
| "task_pulses": get_u64(words, 20, signed=True), | |||
| "physical_pulses": get_u64(words, 28), | |||
| "counter_mode": words[37], | |||
| "current_frequency": get_u32(words, 38), | |||
| "target_frequency": get_u32(words, 40), | |||
| } | |||
| def print_process_checkpoint(client: RtuClient, label: str) -> None: | |||
| words = client.read_holding(PERFORMANCE_BASE, 4) | |||
| stage_words = client.read_holding( | |||
| STAGE_PERFORMANCE_BASE, STAGE_PERFORMANCE_WORDS | |||
| ) | |||
| stages = ", ".join( | |||
| f"{name}={get_u32(stage_words, index * 2)}" | |||
| for index, name in enumerate(STAGE_NAMES) | |||
| ) | |||
| print( | |||
| f"P16阶段[{label}]:自身最大={get_u32(words, 0)} cycles," | |||
| f"响应最大={get_u32(words, 2)} cycles" | |||
| ) | |||
| print(f" 分段最大:{stages}") | |||
| def prepare_jobs(client: RtuClient) -> None: | |||
| for axis in range(4): | |||
| s0 = [0] * 20 | |||
| put_u32(s0, 0, 1) | |||
| put_u32(s0, 10, TEST_FREQUENCY_HZ) | |||
| put_u32(s0, 12, TEST_PULSES) | |||
| client.write_multiple(S0_BASES[axis], s0) | |||
| client.write_multiple(S1_BASES[axis], [0] * 4) | |||
| def main() -> int: | |||
| parser = argparse.ArgumentParser( | |||
| description="PLSR P14 四轴100kHz硬件计数与并发压力测试" | |||
| ) | |||
| parser.add_argument("--port", help="串口,例如 COM5;只有一个串口时可省略") | |||
| parser.add_argument("--baud", type=int, default=9600) | |||
| parser.add_argument("--slave", type=int, default=1) | |||
| args = parser.parse_args() | |||
| with serial.Serial( | |||
| port=choose_port(args.port), | |||
| baudrate=args.baud, | |||
| bytesize=serial.EIGHTBITS, | |||
| parity=serial.PARITY_EVEN, | |||
| stopbits=serial.STOPBITS_ONE, | |||
| timeout=1.0, | |||
| write_timeout=1.0, | |||
| ) as uart: | |||
| client = RtuClient(uart, args.slave) | |||
| header = client.read_holding(CONTROL_BASE, 8) | |||
| if header[:5] != [ | |||
| 0x504C, | |||
| 0x5352, | |||
| 0x0100, | |||
| CONTROL_WINDOW_WORDS, | |||
| 0x0007, | |||
| ]: | |||
| raise RuntimeError( | |||
| f"P14控制窗口未就绪:{header};请烧录当前固件并复位" | |||
| ) | |||
| print("P14 已就绪:四轴 PULSE/DIR,100kHz,200000脉冲/轴") | |||
| prepare_jobs(client) | |||
| sequence = 100 | |||
| for axis in range(4): | |||
| response = send_command( | |||
| client, sequence, axis, CMD_SET_POSITION, argument=0 | |||
| ) | |||
| check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION") | |||
| sequence += 1 | |||
| print("四轴位置已清零,S0/S1 已用 0x10 原子写入") | |||
| print_process_checkpoint(client, "位置清零") | |||
| for axis in range(4): | |||
| response = send_call(client, sequence, axis, CALL_COMMIT) | |||
| check_result(response, 3, RESULT_OK, f"轴{axis} COMMIT") | |||
| if response[11] != 1: | |||
| raise RuntimeError(f"轴{axis} COMMIT 未建立有效快照") | |||
| sequence += 1 | |||
| print("四轴 COMMIT 校验通过") | |||
| print_process_checkpoint(client, "COMMIT") | |||
| physical_baseline = [ | |||
| read_axis_status(client, axis)["physical_pulses"] | |||
| for axis in range(4) | |||
| ] | |||
| started = time.monotonic() | |||
| for axis in range(4): | |||
| response = send_call(client, sequence, axis, CALL_START) | |||
| check_result(response, 3, RESULT_QUEUED, f"轴{axis} START") | |||
| sequence += 1 | |||
| print("四轴 START 已排队;持续读取状态以施加 Modbus/任务并发压力") | |||
| print_process_checkpoint(client, "START") | |||
| running_status = [read_axis_status(client, axis) for axis in range(4)] | |||
| for axis, status in enumerate(running_status): | |||
| if status["state"] not in {STATE_ACCEL, STATE_RUN}: | |||
| raise RuntimeError(f"轴{axis} 未进入运行态:{status}") | |||
| expected_mode = 1 if axis < 2 else 0 | |||
| if status["counter_mode"] != expected_mode: | |||
| raise RuntimeError( | |||
| f"轴{axis}计数模式={status['counter_mode']},期望={expected_mode}" | |||
| ) | |||
| if status["current_frequency"] != TEST_FREQUENCY_HZ: | |||
| raise RuntimeError(f"轴{axis}频率不正确:{status}") | |||
| print("运行期计数租约正确:Q0/Q1=硬件,Q2/Q3=软件回退") | |||
| print_process_checkpoint(client, "进入运行态") | |||
| deadline = time.monotonic() + 10.0 | |||
| polls = 0 | |||
| final_status: list[dict[str, int]] = running_status | |||
| while time.monotonic() < deadline: | |||
| final_status = [read_axis_status(client, axis) for axis in range(4)] | |||
| polls += 4 | |||
| if all(item["state"] == STATE_COMPLETED for item in final_status): | |||
| break | |||
| else: | |||
| raise RuntimeError(f"等待四轴完成超时,最后状态:{final_status}") | |||
| for axis, status in enumerate(final_status): | |||
| expected = { | |||
| "logical_position": TEST_PULSES, | |||
| "task_pulses": TEST_PULSES, | |||
| "error": 0, | |||
| "last_result": RESULT_OK, | |||
| } | |||
| bad = {key: (status[key], value) for key, value in expected.items() | |||
| if status[key] != value} | |||
| physical_delta = ( | |||
| status["physical_pulses"] - physical_baseline[axis] | |||
| ) | |||
| if physical_delta != TEST_PULSES: | |||
| bad["physical_pulses_delta"] = ( | |||
| physical_delta, | |||
| TEST_PULSES, | |||
| ) | |||
| if bad: | |||
| raise RuntimeError(f"轴{axis}最终计数不正确:{bad};状态={status}") | |||
| elapsed = time.monotonic() - started | |||
| print_process_checkpoint(client, "运行完成") | |||
| performance = client.read_holding(PERFORMANCE_BASE, 8) | |||
| performance_again = client.read_holding(PERFORMANCE_BASE, 8) | |||
| if performance != performance_again: | |||
| performance = performance_again | |||
| stage_performance = client.read_holding( | |||
| STAGE_PERFORMANCE_BASE, STAGE_PERFORMANCE_WORDS | |||
| ) | |||
| ab_gate_cycles = get_u32( | |||
| client.read_holding(AB_GATE_PERFORMANCE_BASE, 2), 0 | |||
| ) | |||
| core_clock_hz = get_u32(header, 5) | |||
| performance_version = header[7] | |||
| cycle_values = { | |||
| "PlsrProcess自身": get_u32(performance, 0), | |||
| "PlsrProcess响应": get_u32(performance, 2), | |||
| "TIM6控制ISR": get_u32(performance, 4), | |||
| "输出定时器ISR": performance[6], | |||
| "TIM9/12计数ISR": performance[7], | |||
| } | |||
| if core_clock_hz == 0 or performance_version != PERFORMANCE_VERSION: | |||
| raise RuntimeError( | |||
| f"P16性能诊断头无效:clock={core_clock_hz}, " | |||
| f"version={performance_version}" | |||
| ) | |||
| if any(value == 0 for value in cycle_values.values()): | |||
| raise RuntimeError(f"P16性能计数未完整运行:{cycle_values}") | |||
| budgets = { | |||
| "PlsrProcess自身": core_clock_hz // 1_000, | |||
| "TIM6控制ISR": core_clock_hz // 10_000, | |||
| "输出定时器ISR": core_clock_hz // TEST_FREQUENCY_HZ, | |||
| "TIM9/12计数ISR": core_clock_hz // TEST_FREQUENCY_HZ, | |||
| } | |||
| overruns = { | |||
| name: (cycles, budgets[name]) | |||
| for name, cycles in cycle_values.items() | |||
| if name in budgets | |||
| if cycles >= budgets[name] | |||
| } | |||
| print("P16 DWT最坏执行时间:") | |||
| for name, cycles in cycle_values.items(): | |||
| microseconds = cycles * 1_000_000.0 / core_clock_hz | |||
| budget_text = ( | |||
| f"预算<{budgets[name]} cycles" | |||
| if name in budgets | |||
| else "观测项(含中断抢占)" | |||
| ) | |||
| print( | |||
| f" {name:<16} {cycles:8d} cycles " | |||
| f"{microseconds:8.3f}us {budget_text}" | |||
| ) | |||
| print("P16 PlsrProcess分段最大执行时间(各段独立峰值):") | |||
| for index, name in enumerate(STAGE_NAMES): | |||
| cycles = get_u32(stage_performance, index * 2) | |||
| microseconds = cycles * 1_000_000.0 / core_clock_hz | |||
| print(f" {name:<18} {cycles:8d} cycles {microseconds:8.3f}us") | |||
| print( | |||
| " AB末周期快速门控 " | |||
| f"{ab_gate_cycles:8d} cycles " | |||
| f"{ab_gate_cycles * 1_000_000.0 / core_clock_hz:8.3f}us " | |||
| "(PULSE/DIR用例未执行时允许为0)" | |||
| ) | |||
| if overruns: | |||
| raise RuntimeError(f"实时执行时间超过对应调度周期:{overruns}") | |||
| response_cycles = cycle_values["PlsrProcess响应"] | |||
| if response_cycles >= core_clock_hz // 1_000: | |||
| print( | |||
| "提示:PlsrProcess墙钟响应超过1ms,但自身CPU执行时间达标;" | |||
| "差值来自高优先级PLSR定时器中断抢占。" | |||
| ) | |||
| print( | |||
| f"全部 PASS:四轴均为 {TEST_PULSES} 脉冲," | |||
| f"耗时 {elapsed:.3f}s,运行期状态读取 {polls} 次" | |||
| ) | |||
| print("请再核对逻辑分析仪:Q0~Q3各200000个上升沿、100kHz、无窄脉冲。") | |||
| return 0 | |||
| if __name__ == "__main__": | |||
| try: | |||
| raise SystemExit(main()) | |||
| except (RuntimeError, serial.SerialException) as error: | |||
| print(f"测试失败:{error}") | |||
| raise SystemExit(1) | |||
| @@ -0,0 +1,912 @@ | |||
| #!/usr/bin/env python3 | |||
| """Long-duration four-axis PLSR/Modbus/optional USB concurrency test. | |||
| Requires the dedicated P18 firmware configuration (K4, soft limits disabled). | |||
| The tool creates one long PULSE/DIR segment on every axis, continuously reads | |||
| generation-protected status snapshots, alternates the live frequency using one | |||
| FC16 transaction per 32-bit value, and writes CSV plus JSON evidence. Optional | |||
| bad-CRC and planned-disconnect probes are disabled unless explicitly requested. | |||
| No persistence SAVE command is issued by this script. | |||
| """ | |||
| from __future__ import annotations | |||
| import argparse | |||
| import csv | |||
| import json | |||
| import math | |||
| import struct | |||
| import threading | |||
| import time | |||
| from datetime import datetime, timezone | |||
| from pathlib import Path | |||
| from typing import Any | |||
| import serial | |||
| from plsr_modbus_frequency_test import RtuClient, add_crc, signed_dword_words | |||
| CONTROL_BASE = 1200 | |||
| CONTROL_WINDOW_WORDS = 338 | |||
| PERFORMANCE_VERSION = 7 | |||
| CALL_REQUEST = CONTROL_BASE + 8 | |||
| CALL_RESPONSE = CONTROL_BASE + 24 | |||
| COMMAND_REQUEST = CONTROL_BASE + 40 | |||
| COMMAND_RESPONSE = CONTROL_BASE + 48 | |||
| AXIS_STATUS_BASE = CONTROL_BASE + 64 | |||
| AXIS_STATUS_WORDS = 48 | |||
| USB_DIAGNOSTICS_BASE = CONTROL_BASE + 316 | |||
| USB_DIAGNOSTICS_WORDS = 22 | |||
| USB_DIAGNOSTICS_VERSION = 1 | |||
| S0_BASES = (1600, 1800, 2000, 2200) | |||
| S1_BASES = (1700, 1900, 2100, 2300) | |||
| RUNTIME_DIAGNOSTICS_FUNCTION = 0x47 | |||
| RUNTIME_DIAGNOSTICS_SIGNATURE = 0x4D42 | |||
| RUNTIME_DIAGNOSTICS_VERSION = 1 | |||
| RUNTIME_DIAGNOSTICS_WORDS = 40 | |||
| RESULT_OK = 0 | |||
| RESULT_QUEUED = 1 | |||
| RESULT_INVALID_STATE = 4 | |||
| STATE_IDLE = 1 | |||
| STATE_ACCEL = 2 | |||
| STATE_RUN = 3 | |||
| STATE_DECEL = 4 | |||
| STATE_COMPLETED = 7 | |||
| STATE_STOPPED = 8 | |||
| STATE_ERROR = 9 | |||
| CALL_COMMIT = 1 | |||
| CALL_START = 2 | |||
| CMD_STOP_IMMEDIATE = 2 | |||
| CMD_SET_POSITION = 5 | |||
| CSV_FIELDS = ( | |||
| "host_time_utc", | |||
| "elapsed_s", | |||
| "sample", | |||
| "axis", | |||
| "state", | |||
| "flags", | |||
| "error", | |||
| "last_result", | |||
| "counter_mode", | |||
| "current_frequency", | |||
| "target_frequency", | |||
| "logical_position", | |||
| "task_pulses", | |||
| "physical_pulses", | |||
| "snapshot_retries", | |||
| "modbus_valid_frames", | |||
| "modbus_tx_frames", | |||
| "modbus_crc_errors", | |||
| "modbus_dropped_frames", | |||
| "modbus_uart_errors", | |||
| "modbus_restart_failures", | |||
| "modbus_last_uart_error", | |||
| ) | |||
| def put_u32(words: list[int], offset: int, value: int) -> None: | |||
| words[offset : offset + 2] = signed_dword_words(value) | |||
| def put_u64(words: list[int], offset: int, value: int) -> None: | |||
| raw = value & 0xFFFFFFFFFFFFFFFF | |||
| words[offset : offset + 4] = [ | |||
| (raw >> shift) & 0xFFFF for shift in (0, 16, 32, 48) | |||
| ] | |||
| def get_u32(words: list[int], offset: int) -> int: | |||
| return words[offset] | (words[offset + 1] << 16) | |||
| def get_u64(words: list[int], offset: int, *, signed: bool = False) -> int: | |||
| value = sum(words[offset + index] << (16 * index) for index in range(4)) | |||
| if signed and value & (1 << 63): | |||
| value -= 1 << 64 | |||
| return value | |||
| def wait_response( | |||
| client: RtuClient, | |||
| address: int, | |||
| quantity: int, | |||
| sequence: int, | |||
| timeout: float = 3.0, | |||
| ) -> list[int]: | |||
| deadline = time.monotonic() + timeout | |||
| latest: list[int] = [] | |||
| while time.monotonic() < deadline: | |||
| latest = client.read_holding(address, quantity) | |||
| if get_u32(latest, 0) == sequence: | |||
| return latest | |||
| raise RuntimeError(f"等待命令序号 {sequence} 应答超时;最后应答={latest}") | |||
| def send_command( | |||
| client: RtuClient, | |||
| sequence: int, | |||
| axis: int, | |||
| opcode: int, | |||
| argument: int = 0, | |||
| ) -> list[int]: | |||
| request = [0] * 8 | |||
| put_u32(request, 0, sequence) | |||
| request[2] = opcode | |||
| request[3] = axis | |||
| put_u64(request, 4, argument) | |||
| client.write_multiple(COMMAND_REQUEST, request) | |||
| return wait_response(client, COMMAND_RESPONSE, 8, sequence) | |||
| def send_call( | |||
| client: RtuClient, | |||
| sequence: int, | |||
| axis: int, | |||
| operation: int, | |||
| ) -> list[int]: | |||
| request = [0] * 16 | |||
| put_u32(request, 0, sequence) | |||
| request[2] = 0 # S0 is D | |||
| put_u32(request, 3, S0_BASES[axis]) | |||
| request[5] = 0 # S1 is D | |||
| put_u32(request, 6, S1_BASES[axis]) | |||
| request[8] = 0 # S2 is constant K4 (dedicated P18 long-stress setup) | |||
| put_u32(request, 10, 4) | |||
| request[12] = axis | |||
| request[13] = 0 # PULSE/DIR | |||
| request[14] = operation | |||
| client.write_multiple(CALL_REQUEST, request) | |||
| return wait_response(client, CALL_RESPONSE, 12, sequence) | |||
| def check_result( | |||
| response: list[int], offset: int, expected: int, label: str | |||
| ) -> None: | |||
| if response[offset] != expected: | |||
| raise RuntimeError( | |||
| f"{label} 返回 {response[offset]},期望 {expected};应答={response}" | |||
| ) | |||
| def read_axis_status( | |||
| client: RtuClient, axis: int, attempts: int = 4 | |||
| ) -> tuple[dict[str, int], int]: | |||
| """Read one coherent generation-guarded axis status snapshot.""" | |||
| address = AXIS_STATUS_BASE + axis * AXIS_STATUS_WORDS | |||
| for retry in range(attempts): | |||
| words = client.read_holding(address, AXIS_STATUS_WORDS) | |||
| generation_begin = get_u32(words, 0) | |||
| generation_end = get_u32(words, 46) | |||
| if generation_begin == generation_end and not generation_begin & 1: | |||
| return ( | |||
| { | |||
| "generation": generation_begin, | |||
| "state": words[2], | |||
| "flags": get_u32(words, 3), | |||
| "error": words[6], | |||
| "stop_reason": words[7], | |||
| "last_result": words[8], | |||
| "last_sequence": get_u32(words, 10), | |||
| "logical_position": get_u64(words, 16, signed=True), | |||
| "task_pulses": get_u64(words, 20, signed=True), | |||
| "physical_pulses": get_u64(words, 28), | |||
| "counter_mode": words[37], | |||
| "current_frequency": get_u32(words, 38), | |||
| "target_frequency": get_u32(words, 40), | |||
| }, | |||
| retry, | |||
| ) | |||
| raise RuntimeError(f"轴{axis}状态快照连续 {attempts} 次版本不一致") | |||
| def read_runtime_diagnostics(client: RtuClient) -> dict[str, Any]: | |||
| pdu = bytes((RUNTIME_DIAGNOSTICS_FUNCTION,)) + struct.pack( | |||
| ">HH", 0, RUNTIME_DIAGNOSTICS_WORDS | |||
| ) | |||
| response = client.exchange(pdu, 5 + RUNTIME_DIAGNOSTICS_WORDS * 2) | |||
| if ( | |||
| response[1] != RUNTIME_DIAGNOSTICS_FUNCTION | |||
| or response[2] != RUNTIME_DIAGNOSTICS_WORDS * 2 | |||
| ): | |||
| raise RuntimeError(f"0x47 诊断应答格式错误:{response.hex(' ')}") | |||
| words = list( | |||
| struct.unpack(f">{RUNTIME_DIAGNOSTICS_WORDS}H", response[3:-2]) | |||
| ) | |||
| if words[:3] != [ | |||
| RUNTIME_DIAGNOSTICS_SIGNATURE, | |||
| RUNTIME_DIAGNOSTICS_VERSION, | |||
| RUNTIME_DIAGNOSTICS_WORDS, | |||
| ]: | |||
| raise RuntimeError(f"Modbus 运行诊断版本不匹配:{words[:3]}") | |||
| stat_names = ( | |||
| "rx_events", | |||
| "valid_frames", | |||
| "tx_frames", | |||
| "crc_errors", | |||
| "ignored_addresses", | |||
| "illegal_functions", | |||
| "illegal_addresses", | |||
| "illegal_values", | |||
| "dropped_frames", | |||
| "uart_errors", | |||
| ) | |||
| statistics = { | |||
| name: get_u32(words, 20 + index * 2) | |||
| for index, name in enumerate(stat_names) | |||
| } | |||
| return { | |||
| "flags": words[3], | |||
| "initialized": bool(words[3] & (1 << 0)), | |||
| "connected": bool(words[3] & (1 << 2)), | |||
| "tx_busy": bool(words[3] & (1 << 3)), | |||
| "rx_restart_ok": bool(words[3] & (1 << 6)), | |||
| "current_tick": get_u32(words, 4), | |||
| "last_valid_frame_tick": get_u32(words, 6), | |||
| "last_inter_frame_gap_cycles": get_u32(words, 8), | |||
| "restart_attempts": get_u32(words, 10), | |||
| "restart_failures": get_u32(words, 12), | |||
| "last_uart_error": get_u32(words, 14), | |||
| "last_receive_start_status": words[16], | |||
| "rx_assembly_length": words[17], | |||
| "rx_frame_length": words[18], | |||
| "statistics": statistics, | |||
| } | |||
| def read_usb_diagnostics( | |||
| client: RtuClient, attempts: int = 4 | |||
| ) -> dict[str, Any]: | |||
| """Read one coherent generation-guarded USB CDC diagnostic block.""" | |||
| counter_names = ( | |||
| "rx_packet_count", | |||
| "rx_byte_count", | |||
| "rx_rearm_failure_count", | |||
| "tx_request_count", | |||
| "tx_byte_count", | |||
| "tx_busy_count", | |||
| "tx_failure_count", | |||
| "tx_complete_count", | |||
| ) | |||
| for retry in range(attempts): | |||
| words = client.read_holding(USB_DIAGNOSTICS_BASE, USB_DIAGNOSTICS_WORDS) | |||
| generation_begin = get_u32(words, 0) | |||
| generation_end = get_u32(words, 20) | |||
| if generation_begin == generation_end and not generation_begin & 1: | |||
| if words[2] != USB_DIAGNOSTICS_VERSION: | |||
| raise RuntimeError( | |||
| "USB CDC 诊断版本不匹配:" | |||
| f"读取={words[2]},要求={USB_DIAGNOSTICS_VERSION}" | |||
| ) | |||
| return { | |||
| "generation": generation_begin, | |||
| "version": words[2], | |||
| "initialized": bool(words[3]), | |||
| "snapshot_retries": retry, | |||
| "counters": { | |||
| name: get_u32(words, 4 + index * 2) | |||
| for index, name in enumerate(counter_names) | |||
| }, | |||
| } | |||
| raise RuntimeError(f"USB CDC 诊断块连续 {attempts} 次版本不一致") | |||
| def inject_bad_crc(port: serial.Serial, slave: int, baud: int) -> None: | |||
| """Send a read-only request with a deliberately invalid CRC.""" | |||
| valid = add_crc(bytes((slave, 0x03)) + struct.pack(">HH", CONTROL_BASE, 1)) | |||
| malformed = valid[:-1] + bytes((valid[-1] ^ 0x01,)) | |||
| port.reset_input_buffer() | |||
| port.write(malformed) | |||
| port.flush() | |||
| # Match the firmware's Modbus RTU timing rule and leave a small host margin. | |||
| t35_seconds = 0.00175 if baud > 19_200 else (3.5 * 11.0 / baud) | |||
| time.sleep(t35_seconds + 0.005) | |||
| def open_modbus(args: argparse.Namespace) -> tuple[serial.Serial, RtuClient]: | |||
| port = serial.Serial( | |||
| port=args.port, | |||
| baudrate=args.baud, | |||
| bytesize=serial.EIGHTBITS, | |||
| parity=serial.PARITY_EVEN, | |||
| stopbits=serial.STOPBITS_ONE, | |||
| timeout=args.timeout, | |||
| write_timeout=args.timeout, | |||
| ) | |||
| return port, RtuClient(port, args.slave) | |||
| def reopen_modbus(args: argparse.Namespace) -> tuple[serial.Serial, RtuClient]: | |||
| """Reopen a planned/lost link with a bounded number of host retries.""" | |||
| last_error: BaseException | None = None | |||
| attempts = max(1, args.max_communication_errors) | |||
| for _ in range(attempts): | |||
| try: | |||
| return open_modbus(args) | |||
| except (OSError, serial.SerialException) as error: | |||
| last_error = error | |||
| time.sleep(args.reconnect_delay) | |||
| raise RuntimeError( | |||
| f"连续 {attempts} 次无法重新打开 {args.port}:{last_error}" | |||
| ) from last_error | |||
| class UsbOutPressure: | |||
| def __init__(self, port: str | None, bytes_per_second: int) -> None: | |||
| self.port = port | |||
| self.bytes_per_second = bytes_per_second | |||
| self.bytes_written = 0 | |||
| self.write_errors = 0 | |||
| self.last_error = "" | |||
| self._stop = threading.Event() | |||
| self._thread: threading.Thread | None = None | |||
| def start(self) -> None: | |||
| if self.port is None: | |||
| return | |||
| self._thread = threading.Thread(target=self._run, daemon=True) | |||
| self._thread.start() | |||
| def stop(self) -> None: | |||
| self._stop.set() | |||
| if self._thread is not None: | |||
| self._thread.join(timeout=3.0) | |||
| def _run(self) -> None: | |||
| payload = (b"PLSR-USB-CDC-OUT-STRESS-" * 3)[:64] | |||
| try: | |||
| with serial.Serial( | |||
| self.port, | |||
| baudrate=115200, | |||
| timeout=0.2, | |||
| write_timeout=1.0, | |||
| ) as usb: | |||
| next_send = time.monotonic() | |||
| while not self._stop.is_set(): | |||
| usb.write(payload) | |||
| usb.flush() | |||
| self.bytes_written += len(payload) | |||
| if self.bytes_per_second > 0: | |||
| next_send += len(payload) / self.bytes_per_second | |||
| delay = next_send - time.monotonic() | |||
| if delay > 0: | |||
| self._stop.wait(delay) | |||
| elif delay < -1.0: | |||
| next_send = time.monotonic() | |||
| except (OSError, serial.SerialException) as error: | |||
| self.write_errors += 1 | |||
| self.last_error = str(error) | |||
| def summary(self) -> dict[str, Any]: | |||
| return { | |||
| "port": self.port, | |||
| "target_bytes_per_second": self.bytes_per_second, | |||
| "bytes_written": self.bytes_written, | |||
| "write_errors": self.write_errors, | |||
| "last_error": self.last_error, | |||
| } | |||
| def prepare_jobs(client: RtuClient, frequency: int, pulses: int) -> None: | |||
| for axis in range(4): | |||
| s0 = [0] * 20 | |||
| put_u32(s0, 0, 1) | |||
| put_u32(s0, 10, frequency) | |||
| put_u32(s0, 12, pulses) | |||
| client.write_multiple(S0_BASES[axis], s0) | |||
| client.write_multiple(S1_BASES[axis], [0] * 4) | |||
| def stop_all_axes(client: RtuClient, sequence: int) -> int: | |||
| send_failures: list[str] = [] | |||
| for axis in range(4): | |||
| try: | |||
| response = send_command( | |||
| client, sequence, axis, CMD_STOP_IMMEDIATE, argument=0 | |||
| ) | |||
| if response[4] not in { | |||
| RESULT_OK, | |||
| RESULT_QUEUED, | |||
| RESULT_INVALID_STATE, | |||
| }: | |||
| send_failures.append( | |||
| f"轴{axis} STOP_IMMEDIATE 返回 {response[4]}" | |||
| ) | |||
| except (RuntimeError, serial.SerialException, OSError) as error: | |||
| # Never let one failed/already-stopped axis prevent stop attempts | |||
| # for the remaining axes. | |||
| send_failures.append(f"轴{axis} STOP_IMMEDIATE 异常:{error}") | |||
| sequence += 1 | |||
| deadline = time.monotonic() + 8.0 | |||
| latest: dict[int, dict[str, int]] = {} | |||
| terminal_states = {STATE_IDLE, STATE_COMPLETED, STATE_STOPPED, STATE_ERROR} | |||
| while time.monotonic() < deadline: | |||
| for axis in range(4): | |||
| try: | |||
| latest[axis] = read_axis_status(client, axis)[0] | |||
| except (RuntimeError, serial.SerialException, OSError) as error: | |||
| send_failures.append(f"轴{axis}停止状态读取异常:{error}") | |||
| if len(latest) == 4 and all( | |||
| latest[axis]["state"] in terminal_states | |||
| and (latest[axis]["flags"] & (1 << 1)) == 0 | |||
| for axis in range(4) | |||
| ): | |||
| return sequence | |||
| raise RuntimeError( | |||
| "STOP_IMMEDIATE 后仍有轴未确认安全停止:" | |||
| f"status={latest};发送/读取异常={send_failures}" | |||
| ) | |||
| def delta32(end: int, start: int) -> int: | |||
| return (end - start) & 0xFFFFFFFF | |||
| def is_retryable_communication_error(error: BaseException) -> bool: | |||
| if isinstance(error, serial.SerialException): | |||
| return True | |||
| if not isinstance(error, RuntimeError): | |||
| return False | |||
| message = str(error) | |||
| return message.startswith("响应超时:") or message.startswith("响应 CRC 错误:") | |||
| def parse_args() -> argparse.Namespace: | |||
| parser = argparse.ArgumentParser(description="PLSR Modbus/USB 长稳并发测试") | |||
| parser.add_argument("--port", default="COM5", help="Modbus RTU 串口,默认 COM5") | |||
| parser.add_argument("--baud", type=int, default=9600) | |||
| parser.add_argument("--slave", type=int, default=1) | |||
| parser.add_argument("--timeout", type=float, default=1.5) | |||
| parser.add_argument("--duration", type=float, default=1800.0, help="运行秒数") | |||
| parser.add_argument("--frequency", type=int, default=100_000) | |||
| parser.add_argument( | |||
| "--low-frequency", | |||
| type=int, | |||
| help="动态频率低值,默认主频率的一半", | |||
| ) | |||
| parser.add_argument("--status-period", type=float, default=1.0) | |||
| parser.add_argument( | |||
| "--frequency-period", | |||
| type=float, | |||
| default=10.0, | |||
| help="动态频率切换周期;0 表示禁用", | |||
| ) | |||
| parser.add_argument( | |||
| "--bad-crc-period", | |||
| type=float, | |||
| default=0.0, | |||
| help="坏 CRC 注入周期;默认 0(禁用)", | |||
| ) | |||
| parser.add_argument( | |||
| "--disconnect-at", | |||
| type=float, | |||
| default=0.0, | |||
| help="运行到指定秒数时主动断开串口;默认 0(禁用)", | |||
| ) | |||
| parser.add_argument("--disconnect-duration", type=float, default=3.0) | |||
| parser.add_argument("--usb-port", help="可选 USB CDC 虚拟串口,例如 COM8") | |||
| parser.add_argument("--usb-rate", type=int, default=64_000, help="USB OUT B/s") | |||
| parser.add_argument("--max-communication-errors", type=int, default=5) | |||
| parser.add_argument("--reconnect-delay", type=float, default=1.0) | |||
| parser.add_argument( | |||
| "--output-dir", | |||
| type=Path, | |||
| default=Path("HostComputer/long_stress_logs"), | |||
| ) | |||
| return parser.parse_args() | |||
| def main() -> int: | |||
| args = parse_args() | |||
| if args.baud <= 0: | |||
| raise RuntimeError("baud 必须大于 0") | |||
| if not 1 <= args.slave <= 247: | |||
| raise RuntimeError("slave 必须为 1~247") | |||
| if args.timeout <= 0: | |||
| raise RuntimeError("timeout 必须大于 0") | |||
| if args.duration <= 0 or args.status_period <= 0: | |||
| raise RuntimeError("duration 和 status-period 必须大于 0") | |||
| if args.frequency_period < 0 or args.bad_crc_period < 0: | |||
| raise RuntimeError("frequency-period 和 bad-crc-period 不得为负数") | |||
| if args.disconnect_at < 0 or args.disconnect_duration < 0: | |||
| raise RuntimeError("disconnect-at 和 disconnect-duration 不得为负数") | |||
| if args.disconnect_at > 0 and args.disconnect_duration <= 0: | |||
| raise RuntimeError("启用计划断线时 disconnect-duration 必须大于 0") | |||
| if args.usb_rate < 0: | |||
| raise RuntimeError("usb-rate 不得为负数") | |||
| if args.usb_port and args.usb_rate == 0: | |||
| raise RuntimeError("启用 USB 压力时 usb-rate 必须大于 0") | |||
| if args.max_communication_errors < 0: | |||
| raise RuntimeError("max-communication-errors 不得为负数") | |||
| if args.reconnect_delay < 0: | |||
| raise RuntimeError("reconnect-delay 不得为负数") | |||
| if not 1 <= args.frequency <= 100_000: | |||
| raise RuntimeError("frequency 必须为 1~100000Hz(K4/P18 配置上限)") | |||
| low_frequency = args.low_frequency or max(1, args.frequency // 2) | |||
| if not 1 <= low_frequency <= args.frequency: | |||
| raise RuntimeError("low-frequency 必须为 1~frequency") | |||
| if args.usb_port and args.usb_port.upper() == args.port.upper(): | |||
| raise RuntimeError("USB CDC 串口不能与 Modbus 串口相同") | |||
| pulse_target = math.ceil( | |||
| (args.duration + max(0.0, args.disconnect_duration) + 120.0) | |||
| * args.frequency | |||
| ) | |||
| if pulse_target > 2_000_000_000: | |||
| raise RuntimeError("测试时长/频率使单段脉冲超过 20 亿;请降低时长或频率") | |||
| args.output_dir.mkdir(parents=True, exist_ok=True) | |||
| run_id = datetime.now().strftime("%Y%m%d_%H%M%S") | |||
| csv_path = args.output_dir / f"plsr_long_stress_{run_id}.csv" | |||
| json_path = args.output_dir / f"plsr_long_stress_{run_id}.json" | |||
| events: list[dict[str, Any]] = [] | |||
| summary: dict[str, Any] = { | |||
| "started_utc": datetime.now(timezone.utc).isoformat(), | |||
| "arguments": { | |||
| key: str(value) if isinstance(value, Path) else value | |||
| for key, value in vars(args).items() | |||
| }, | |||
| "pulse_target": pulse_target, | |||
| "low_frequency": low_frequency, | |||
| "events": events, | |||
| "result": "FAIL", | |||
| } | |||
| usb_pressure = UsbOutPressure(args.usb_port, args.usb_rate) | |||
| uart: serial.Serial | None = None | |||
| client: RtuClient | None = None | |||
| sequence = 1000 | |||
| failure: BaseException | None = None | |||
| last_status: list[dict[str, int]] = [] | |||
| axes_stopped = False | |||
| with csv_path.open("w", newline="", encoding="utf-8-sig") as csv_file: | |||
| writer = csv.DictWriter(csv_file, fieldnames=CSV_FIELDS) | |||
| writer.writeheader() | |||
| try: | |||
| uart, client = open_modbus(args) | |||
| header = client.read_holding(CONTROL_BASE, 8) | |||
| if ( | |||
| header[0:3] != [0x504C, 0x5352, 0x0100] | |||
| or header[3] != CONTROL_WINDOW_WORDS | |||
| or header[7] != PERFORMANCE_VERSION | |||
| ): | |||
| raise RuntimeError(f"PLSR 控制窗口未就绪:{header}") | |||
| diagnostics_start = read_runtime_diagnostics(client) | |||
| summary["diagnostics_start"] = diagnostics_start | |||
| usb_diagnostics_start: dict[str, Any] | None = None | |||
| if args.usb_port: | |||
| usb_diagnostics_start = read_usb_diagnostics(client) | |||
| summary["usb_device_diagnostics_start"] = usb_diagnostics_start | |||
| prepare_jobs(client, args.frequency, pulse_target) | |||
| for axis in range(4): | |||
| response = send_command(client, sequence, axis, CMD_SET_POSITION, 0) | |||
| check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION") | |||
| if axis == 0: | |||
| replay = send_command(client, sequence, axis, CMD_SET_POSITION, 0) | |||
| if replay != response: | |||
| raise RuntimeError( | |||
| f"重复命令序号未回放同一应答:首次={response},重复={replay}" | |||
| ) | |||
| events.append({"elapsed_s": 0.0, "event": "idempotency_pass"}) | |||
| sequence += 1 | |||
| for axis in range(4): | |||
| response = send_call(client, sequence, axis, CALL_COMMIT) | |||
| check_result(response, 3, RESULT_OK, f"轴{axis} COMMIT") | |||
| if response[11] != 1: | |||
| raise RuntimeError(f"轴{axis} COMMIT 未建立有效快照") | |||
| sequence += 1 | |||
| physical_baseline = [ | |||
| read_axis_status(client, axis)[0]["physical_pulses"] | |||
| for axis in range(4) | |||
| ] | |||
| for axis in range(4): | |||
| response = send_call(client, sequence, axis, CALL_START) | |||
| check_result(response, 3, RESULT_QUEUED, f"轴{axis} START") | |||
| sequence += 1 | |||
| usb_pressure.start() | |||
| started = time.monotonic() | |||
| next_sample = started | |||
| next_frequency = ( | |||
| started + args.frequency_period | |||
| if args.frequency_period > 0 | |||
| else float("inf") | |||
| ) | |||
| next_bad_crc = ( | |||
| started + args.bad_crc_period | |||
| if args.bad_crc_period > 0 | |||
| else float("inf") | |||
| ) | |||
| disconnected = False | |||
| communication_errors = 0 | |||
| sample_index = 0 | |||
| dynamic_frequency = args.frequency | |||
| previous_physical = physical_baseline[:] | |||
| while time.monotonic() - started < args.duration: | |||
| now = time.monotonic() | |||
| elapsed = now - started | |||
| if ( | |||
| args.disconnect_at > 0 | |||
| and not disconnected | |||
| and elapsed >= args.disconnect_at | |||
| ): | |||
| before_disconnect = previous_physical[:] | |||
| assert uart is not None | |||
| uart.close() | |||
| events.append( | |||
| {"elapsed_s": elapsed, "event": "planned_disconnect_start"} | |||
| ) | |||
| time.sleep(args.disconnect_duration) | |||
| uart, client = reopen_modbus(args) | |||
| disconnected = True | |||
| after_disconnect = [ | |||
| read_axis_status(client, axis)[0]["physical_pulses"] | |||
| for axis in range(4) | |||
| ] | |||
| if any( | |||
| after_disconnect[axis] <= before_disconnect[axis] | |||
| for axis in range(4) | |||
| ): | |||
| raise RuntimeError( | |||
| "计划断线期间存在轴脉冲未继续增长:" | |||
| f"before={before_disconnect}, after={after_disconnect}" | |||
| ) | |||
| previous_physical = after_disconnect | |||
| events.append( | |||
| { | |||
| "elapsed_s": time.monotonic() - started, | |||
| "event": "planned_disconnect_recovered", | |||
| "physical_pulses": after_disconnect, | |||
| } | |||
| ) | |||
| next_sample = time.monotonic() | |||
| continue | |||
| try: | |||
| assert client is not None and uart is not None | |||
| if now >= next_frequency: | |||
| dynamic_frequency = ( | |||
| low_frequency | |||
| if dynamic_frequency == args.frequency | |||
| else args.frequency | |||
| ) | |||
| for axis in range(4): | |||
| # FC16 writes both words of the signed INT32 atomically. | |||
| client.write_multiple( | |||
| S0_BASES[axis] + 10, | |||
| signed_dword_words(dynamic_frequency), | |||
| ) | |||
| events.append( | |||
| { | |||
| "elapsed_s": elapsed, | |||
| "event": "frequency_change", | |||
| "frequency_hz": dynamic_frequency, | |||
| } | |||
| ) | |||
| next_frequency += args.frequency_period | |||
| if now >= next_bad_crc: | |||
| before_crc = read_runtime_diagnostics(client)["statistics"][ | |||
| "crc_errors" | |||
| ] | |||
| inject_bad_crc(uart, args.slave, args.baud) | |||
| after_crc = read_runtime_diagnostics(client)["statistics"][ | |||
| "crc_errors" | |||
| ] | |||
| if delta32(after_crc, before_crc) < 1: | |||
| raise RuntimeError("注入坏 CRC 后 crcErrorCount 未增长") | |||
| events.append( | |||
| { | |||
| "elapsed_s": elapsed, | |||
| "event": "bad_crc_rejected", | |||
| "crc_count": after_crc, | |||
| } | |||
| ) | |||
| next_bad_crc += args.bad_crc_period | |||
| if now < next_sample: | |||
| time.sleep(min(next_sample - now, 0.05)) | |||
| continue | |||
| diagnostics = read_runtime_diagnostics(client) | |||
| if not diagnostics["connected"]: | |||
| raise RuntimeError(f"Modbus connected 标志丢失:{diagnostics}") | |||
| statuses: list[dict[str, int]] = [] | |||
| snapshot_retries: list[int] = [] | |||
| for axis in range(4): | |||
| status, retries = read_axis_status(client, axis) | |||
| statuses.append(status) | |||
| snapshot_retries.append(retries) | |||
| if status["error"] != 0 or status["last_result"] != RESULT_OK: | |||
| raise RuntimeError(f"轴{axis}进入错误状态:{status}") | |||
| if status["state"] not in {STATE_ACCEL, STATE_RUN, STATE_DECEL}: | |||
| raise RuntimeError(f"轴{axis}意外离开运行态:{status}") | |||
| if status["physical_pulses"] < previous_physical[axis]: | |||
| raise RuntimeError( | |||
| f"轴{axis}物理累计计数回退:" | |||
| f"{previous_physical[axis]} -> {status['physical_pulses']}" | |||
| ) | |||
| previous_physical[axis] = status["physical_pulses"] | |||
| timestamp = datetime.now(timezone.utc).isoformat() | |||
| stats = diagnostics["statistics"] | |||
| for axis, status in enumerate(statuses): | |||
| writer.writerow( | |||
| { | |||
| "host_time_utc": timestamp, | |||
| "elapsed_s": f"{elapsed:.6f}", | |||
| "sample": sample_index, | |||
| "axis": axis, | |||
| "state": status["state"], | |||
| "flags": status["flags"], | |||
| "error": status["error"], | |||
| "last_result": status["last_result"], | |||
| "counter_mode": status["counter_mode"], | |||
| "current_frequency": status["current_frequency"], | |||
| "target_frequency": status["target_frequency"], | |||
| "logical_position": status["logical_position"], | |||
| "task_pulses": status["task_pulses"], | |||
| "physical_pulses": status["physical_pulses"], | |||
| "snapshot_retries": snapshot_retries[axis], | |||
| "modbus_valid_frames": stats["valid_frames"], | |||
| "modbus_tx_frames": stats["tx_frames"], | |||
| "modbus_crc_errors": stats["crc_errors"], | |||
| "modbus_dropped_frames": stats["dropped_frames"], | |||
| "modbus_uart_errors": stats["uart_errors"], | |||
| "modbus_restart_failures": diagnostics[ | |||
| "restart_failures" | |||
| ], | |||
| "modbus_last_uart_error": diagnostics[ | |||
| "last_uart_error" | |||
| ], | |||
| } | |||
| ) | |||
| csv_file.flush() | |||
| last_status = statuses | |||
| sample_index += 1 | |||
| if sample_index % 30 == 0: | |||
| print( | |||
| f"{elapsed:8.1f}s:样本 {sample_index}," | |||
| f"Q0物理累计={statuses[0]['physical_pulses']}," | |||
| f"Modbus有效帧={stats['valid_frames']}" | |||
| ) | |||
| next_sample = max( | |||
| next_sample + args.status_period, time.monotonic() | |||
| ) | |||
| communication_errors = 0 | |||
| except (RuntimeError, serial.SerialException) as error: | |||
| if not is_retryable_communication_error(error): | |||
| raise | |||
| communication_errors += 1 | |||
| events.append( | |||
| { | |||
| "elapsed_s": time.monotonic() - started, | |||
| "event": "communication_error", | |||
| "count": communication_errors, | |||
| "message": str(error), | |||
| } | |||
| ) | |||
| if communication_errors > args.max_communication_errors: | |||
| raise | |||
| if uart is not None: | |||
| uart.close() | |||
| time.sleep(args.reconnect_delay) | |||
| uart, client = reopen_modbus(args) | |||
| next_sample = time.monotonic() | |||
| assert client is not None | |||
| sequence = stop_all_axes(client, sequence) | |||
| axes_stopped = True | |||
| usb_pressure.stop() | |||
| last_status = [read_axis_status(client, axis)[0] for axis in range(4)] | |||
| diagnostics_end = read_runtime_diagnostics(client) | |||
| summary["diagnostics_end"] = diagnostics_end | |||
| summary["diagnostics_delta"] = { | |||
| name: delta32( | |||
| diagnostics_end["statistics"][name], | |||
| diagnostics_start["statistics"][name], | |||
| ) | |||
| for name in diagnostics_end["statistics"] | |||
| } | |||
| summary["restart_failure_delta"] = delta32( | |||
| diagnostics_end["restart_failures"], | |||
| diagnostics_start["restart_failures"], | |||
| ) | |||
| usb_diagnostics_end: dict[str, Any] | None = None | |||
| usb_device_delta: dict[str, int] = {} | |||
| if args.usb_port: | |||
| assert usb_diagnostics_start is not None | |||
| usb_diagnostics_end = read_usb_diagnostics(client) | |||
| summary["usb_device_diagnostics_end"] = usb_diagnostics_end | |||
| usb_device_delta = { | |||
| name: delta32( | |||
| usb_diagnostics_end["counters"][name], | |||
| usb_diagnostics_start["counters"][name], | |||
| ) | |||
| for name in usb_diagnostics_end["counters"] | |||
| } | |||
| summary["usb_device_diagnostics_delta"] = usb_device_delta | |||
| injected_bad_crc = sum( | |||
| event.get("event") == "bad_crc_rejected" for event in events | |||
| ) | |||
| unhealthy = { | |||
| "uart_errors": summary["diagnostics_delta"]["uart_errors"], | |||
| "dropped_frames": summary["diagnostics_delta"]["dropped_frames"], | |||
| "restart_failures": summary["restart_failure_delta"], | |||
| "unexpected_crc_errors": ( | |||
| summary["diagnostics_delta"]["crc_errors"] | |||
| - injected_bad_crc | |||
| ), | |||
| "usb_write_errors": usb_pressure.write_errors, | |||
| } | |||
| if args.usb_port: | |||
| assert usb_diagnostics_start is not None | |||
| assert usb_diagnostics_end is not None | |||
| unhealthy.update( | |||
| { | |||
| "usb_target_not_initialized": not usb_diagnostics_end[ | |||
| "initialized" | |||
| ], | |||
| "usb_target_rx_packets_no_increment": ( | |||
| usb_device_delta["rx_packet_count"] == 0 | |||
| ), | |||
| "usb_target_rx_bytes_no_increment": ( | |||
| usb_device_delta["rx_byte_count"] == 0 | |||
| ), | |||
| "usb_target_rx_rearm_failures": usb_device_delta[ | |||
| "rx_rearm_failure_count" | |||
| ], | |||
| } | |||
| ) | |||
| unhealthy = {name: value for name, value in unhealthy.items() if value} | |||
| if unhealthy: | |||
| raise RuntimeError(f"长稳运行诊断出现异常增量:{unhealthy}") | |||
| summary["samples"] = sample_index | |||
| summary["final_status"] = last_status | |||
| summary["result"] = "PASS" | |||
| except (RuntimeError, serial.SerialException, OSError, KeyboardInterrupt) as error: | |||
| failure = error | |||
| summary["failure"] = str(error) | |||
| if client is not None and not axes_stopped: | |||
| try: | |||
| sequence = stop_all_axes(client, sequence) | |||
| except Exception as stop_error: # best-effort safety cleanup | |||
| summary["stop_cleanup_failure"] = str(stop_error) | |||
| finally: | |||
| usb_pressure.stop() | |||
| summary["usb_host_pressure"] = usb_pressure.summary() | |||
| summary["ended_utc"] = datetime.now(timezone.utc).isoformat() | |||
| if uart is not None and uart.is_open: | |||
| uart.close() | |||
| json_path.write_text( | |||
| json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8" | |||
| ) | |||
| print(f"CSV 证据:{csv_path}") | |||
| print(f"JSON 汇总:{json_path}") | |||
| if failure is not None: | |||
| if isinstance(failure, KeyboardInterrupt): | |||
| raise RuntimeError("用户中止测试,已尝试停止四轴") from failure | |||
| raise RuntimeError(str(failure)) from failure | |||
| print( | |||
| f"长稳 PASS:{summary['samples']} 个四轴一致性样本;" | |||
| f"最终计数={[item['physical_pulses'] for item in last_status]}" | |||
| ) | |||
| return 0 | |||
| if __name__ == "__main__": | |||
| try: | |||
| raise SystemExit(main()) | |||
| except (RuntimeError, serial.SerialException) as error: | |||
| print(f"测试失败:{error}") | |||
| raise SystemExit(1) | |||
| @@ -0,0 +1,664 @@ | |||
| #!/usr/bin/env python3 | |||
| """PLSR P16 Modbus 性能统计自动测试(对应规格:PLSR_MODBUS_PERFORMANCE_TEST.md)。 | |||
| 本脚本在四轴 100kHz 输出与 Modbus 持续轮询的并发压力下,测量并判定 | |||
| 规格中定义的各项性能指标,运行完成后自动读取 P16 DWT 统计并做预算检查: | |||
| - 版本号一致性(任务要求的“32/64 位版本号一致性检查”): | |||
| D1205~D1206 的 32 位 CPU 计时频率非零、D1207 性能统计版本为 V7、 | |||
| 控制窗口协议版本 0x0100;P16 主统计块(32 位周期数与 16 位饱和字段) | |||
| 连续两次读取必须完全一致。 | |||
| - 单条命令响应时间:0x03/0x10 完整往返耗时,判定预算 = 波特率折算的 | |||
| 线上传输时间(8E1,每字符 11 位)+ 10ms 处理余量。 | |||
| - 连续状态读取吞吐:四轴状态块(48 字×4)持续轮询速率,要求不低于 | |||
| 波特率理论上限的 50%。 | |||
| - 重复序号幂等:运行期重发完全相同序号的命令,固件必须只回放既有应答、 | |||
| 不得重复执行(沿用 P13 控制测试的 PAUSE 重复序号用例)。 | |||
| - 并发读写压力模拟:四轴 100kHz 期间持续读取轴状态,并穿插重写相同的 | |||
| S0/S1 数据(幂等写入)与读取 P16 统计块,验证并发下计数精确。 | |||
| - P16 自动预算:PlsrProcess 自身 < 1ms、TIM6 控制 ISR < 0.1ms、输出/计数 | |||
| ISR < 10us,四项均须实际执行(数值不得为 0);墙钟响应单独显示, | |||
| 超过 1ms 只给抢占提示、不判失败。 | |||
| 与规格/现有脚本的差异说明: | |||
| - 规格“测试方法”一节引用 P14 脚本;本脚本独立实现相同的四轴 100kHz + | |||
| 持续轮询场景(规格要求),并额外测量主机侧性能指标,满足规格 | |||
| “自动预算”与“记录到测试报告”的要求。 | |||
| - 规格要求“无毛刺、无残余输出”,该两项只能由逻辑分析仪验收,本脚本 | |||
| 按规格保留为人工核对提示,与 plsr_modbus_counter_stress_test.py 口径一致。 | |||
| - P16 主统计块读取比 P14 脚本更严格:必须连续两次读取一致(规格要求 | |||
| 统计快照一致性),三次尝试仍不一致即判定失败。 | |||
| """ | |||
| from __future__ import annotations | |||
| import argparse | |||
| import time | |||
| import serial | |||
| from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words | |||
| CONTROL_BASE = 1200 | |||
| CONTROL_WINDOW_WORDS = 338 | |||
| CALL_REQUEST = CONTROL_BASE + 8 | |||
| CALL_RESPONSE = CONTROL_BASE + 24 | |||
| COMMAND_REQUEST = CONTROL_BASE + 40 | |||
| COMMAND_RESPONSE = CONTROL_BASE + 48 | |||
| AXIS_STATUS_BASE = CONTROL_BASE + 64 | |||
| AXIS_STATUS_WORDS = 48 | |||
| PERFORMANCE_BASE = CONTROL_BASE + 56 # D1256:P16 主统计块 | |||
| STAGE_PERFORMANCE_BASE = CONTROL_BASE + 256 # D1456:P16 分阶段统计块 | |||
| AB_GATE_PERFORMANCE_BASE = CONTROL_BASE + 268 # D1468:AB末周期快速门控 | |||
| PERFORMANCE_VERSION = 7 | |||
| STAGE_NAMES = ( | |||
| "脉冲合并/保护", | |||
| "关键事件", | |||
| "命令队列", | |||
| "普通事件/方向提交", | |||
| "HAL/路径/Profile", | |||
| "HSD检查点", | |||
| ) | |||
| STAGE_PERFORMANCE_WORDS = len(STAGE_NAMES) * 2 | |||
| S0_BASES = (1600, 1800, 2000, 2200) | |||
| S1_BASES = (1700, 1900, 2100, 2300) | |||
| TEST_FREQUENCY_HZ = 100_000 | |||
| TEST_PULSES = 200_000 | |||
| RESULT_OK = 0 | |||
| RESULT_QUEUED = 1 | |||
| STATE_ACCEL = 2 | |||
| STATE_RUN = 3 | |||
| STATE_PAUSED = 6 | |||
| STATE_COMPLETED = 7 | |||
| CALL_COMMIT = 1 | |||
| CALL_START = 2 | |||
| CMD_PAUSE = 3 | |||
| CMD_RESUME = 4 | |||
| CMD_SET_POSITION = 5 | |||
| BITS_PER_CHARACTER = 11 # Modbus RTU 8E1:每字符 11 位 | |||
| PROCESSING_BUDGET_MS = 10.0 # 单条命令响应预算中的处理余量(主机+从站开销) | |||
| THROUGHPUT_MIN_RATIO = 0.5 # 实测吞吐不低于波特率理论上限的比例 | |||
| LATENCY_SAMPLES = 20 # 每种命令的响应时间采样次数 | |||
| RUN_DEADLINE_SECONDS = 15.0 # 四轴运行完成的等待上限 | |||
| METRICS: list[tuple[str, bool, str]] = [] | |||
| def put_u32(words: list[int], offset: int, value: int) -> None: | |||
| words[offset : offset + 2] = signed_dword_words(value) | |||
| def put_u64(words: list[int], offset: int, value: int) -> None: | |||
| raw = value & 0xFFFFFFFFFFFFFFFF | |||
| words[offset : offset + 4] = [ | |||
| (raw >> shift) & 0xFFFF for shift in (0, 16, 32, 48) | |||
| ] | |||
| def get_u32(words: list[int], offset: int) -> int: | |||
| return words[offset] | (words[offset + 1] << 16) | |||
| def get_u64(words: list[int], offset: int, signed: bool = False) -> int: | |||
| raw = sum(words[offset + index] << (16 * index) for index in range(4)) | |||
| if signed and raw & (1 << 63): | |||
| return raw - (1 << 64) | |||
| return raw | |||
| def wait_response( | |||
| client: RtuClient, address: int, words: int, sequence: int, timeout: float = 2.0 | |||
| ) -> list[int]: | |||
| deadline = time.monotonic() + timeout | |||
| while time.monotonic() < deadline: | |||
| response = client.read_holding(address, words) | |||
| if get_u32(response, 0) == sequence: | |||
| return response | |||
| raise RuntimeError(f"等待序号 {sequence} 的应答超时") | |||
| def send_command( | |||
| client: RtuClient, sequence: int, axis: int, opcode: int, argument: int = 0 | |||
| ) -> list[int]: | |||
| request = [0] * 8 | |||
| put_u32(request, 0, sequence) | |||
| request[2] = opcode | |||
| request[3] = axis | |||
| put_u64(request, 4, argument) | |||
| client.write_multiple(COMMAND_REQUEST, request) | |||
| return wait_response(client, COMMAND_RESPONSE, 8, sequence) | |||
| def send_call( | |||
| client: RtuClient, | |||
| sequence: int, | |||
| axis: int, | |||
| operation: int, | |||
| s2_set: int = 1, | |||
| ) -> list[int]: | |||
| request = [0] * 16 | |||
| put_u32(request, 0, sequence) | |||
| request[2] = 0 # S0 device D | |||
| put_u32(request, 3, S0_BASES[axis]) | |||
| request[5] = 0 # S1 device D | |||
| put_u32(request, 6, S1_BASES[axis]) | |||
| request[8] = 0 # S2 constant | |||
| put_u32(request, 10, s2_set) | |||
| request[12] = axis | |||
| request[13] = 0 # PULSE/DIR | |||
| request[14] = operation | |||
| client.write_multiple(CALL_REQUEST, request) | |||
| return wait_response(client, CALL_RESPONSE, 12, sequence) | |||
| def check_result(response: list[int], offset: int, expected: int, label: str) -> None: | |||
| if response[offset] != expected: | |||
| raise RuntimeError( | |||
| f"{label} 返回 {response[offset]},期望 {expected};应答={response}" | |||
| ) | |||
| def read_axis_status(client: RtuClient, axis: int) -> dict[str, int]: | |||
| address = AXIS_STATUS_BASE + axis * AXIS_STATUS_WORDS | |||
| words = client.read_holding(address, AXIS_STATUS_WORDS) | |||
| generation_begin = get_u32(words, 0) | |||
| generation_end = get_u32(words, 46) | |||
| if generation_begin != generation_end or generation_begin & 1: | |||
| raise RuntimeError( | |||
| f"轴{axis}状态快照不一致:begin={generation_begin}, end={generation_end}" | |||
| ) | |||
| return { | |||
| "state": words[2], | |||
| "flags": get_u32(words, 3), | |||
| "error": words[6], | |||
| "stop_reason": words[7], | |||
| "last_result": words[8], | |||
| "last_sequence": get_u32(words, 10), | |||
| "logical_position": get_u64(words, 16, signed=True), | |||
| "task_pulses": get_u64(words, 20, signed=True), | |||
| "physical_pulses": get_u64(words, 28), | |||
| "counter_mode": words[37], | |||
| "current_frequency": get_u32(words, 38), | |||
| "target_frequency": get_u32(words, 40), | |||
| } | |||
| def wait_status( | |||
| client: RtuClient, | |||
| axis: int, | |||
| states: set[int], | |||
| sequence: int | None = None, | |||
| timeout: float = 5.0, | |||
| ) -> dict[str, int]: | |||
| deadline = time.monotonic() + timeout | |||
| latest: dict[str, int] | None = None | |||
| while time.monotonic() < deadline: | |||
| latest = read_axis_status(client, axis) | |||
| sequence_ok = sequence is None or latest["last_sequence"] == sequence | |||
| if latest["state"] in states and sequence_ok: | |||
| return latest | |||
| raise RuntimeError(f"等待轴{axis}状态 {sorted(states)} 超时,最后状态:{latest}") | |||
| def wait_running_output( | |||
| client: RtuClient, axis: int, sequence: int, timeout: float = 5.0 | |||
| ) -> dict[str, int]: | |||
| """等待真实脉冲恢复,不能只依据 ACCEL/RUN 状态标签。""" | |||
| deadline = time.monotonic() + timeout | |||
| latest: dict[str, int] | None = None | |||
| while time.monotonic() < deadline: | |||
| latest = read_axis_status(client, axis) | |||
| pulse_active = (latest["flags"] & (1 << 1)) != 0 | |||
| if ( | |||
| latest["last_sequence"] == sequence | |||
| and latest["state"] in {STATE_ACCEL, STATE_RUN} | |||
| and pulse_active | |||
| and latest["current_frequency"] > 0 | |||
| ): | |||
| return latest | |||
| raise RuntimeError(f"等待轴{axis}实际脉冲恢复超时,最后状态:{latest}") | |||
| def print_process_checkpoint(client: RtuClient, label: str) -> None: | |||
| words = client.read_holding(PERFORMANCE_BASE, 4) | |||
| stage_words = client.read_holding( | |||
| STAGE_PERFORMANCE_BASE, STAGE_PERFORMANCE_WORDS | |||
| ) | |||
| stages = ", ".join( | |||
| f"{name}={get_u32(stage_words, index * 2)}" | |||
| for index, name in enumerate(STAGE_NAMES) | |||
| ) | |||
| print( | |||
| f"P16阶段[{label}]:自身最大={get_u32(words, 0)} cycles," | |||
| f"响应最大={get_u32(words, 2)} cycles" | |||
| ) | |||
| print(f" 分段最大:{stages}") | |||
| def read_performance_block(client: RtuClient) -> list[int] | None: | |||
| """读取 P16 主统计块;连续两次读取一致才算有效(快照一致性)。""" | |||
| for _ in range(3): | |||
| first = client.read_holding(PERFORMANCE_BASE, 8) | |||
| second = client.read_holding(PERFORMANCE_BASE, 8) | |||
| if first == second: | |||
| return first | |||
| return None | |||
| def wire_time_ms(baud: int, request_bytes: int, response_bytes: int) -> float: | |||
| """RTU 8E1 线上传输时间:每字符 11 位。""" | |||
| return (request_bytes + response_bytes) * BITS_PER_CHARACTER * 1000.0 / baud | |||
| def throughput_theory(baud: int) -> float: | |||
| """四轴状态轮询(4×48字,请求 8B + 响应 101B)的波特率理论速率(轮/s)。""" | |||
| bytes_per_poll = 4 * (8 + 101) | |||
| return baud / (bytes_per_poll * BITS_PER_CHARACTER) | |||
| def sample_round_trips( | |||
| client: RtuClient, | |||
| baud: int, | |||
| actions: list[tuple[str, int, int, object]], | |||
| samples: int, | |||
| ) -> dict[str, tuple[float, float, float, float]]: | |||
| """对每种命令采样往返耗时,返回 {名称: (最小, 中位, 最大, 预算) ms}。""" | |||
| results = {} | |||
| for name, request_bytes, response_bytes, action in actions: | |||
| collected = [] | |||
| for _ in range(samples): | |||
| started = time.perf_counter() | |||
| action() | |||
| collected.append((time.perf_counter() - started) * 1000.0) | |||
| ordered = sorted(collected) | |||
| results[name] = ( | |||
| ordered[0], | |||
| ordered[len(ordered) // 2], | |||
| ordered[-1], | |||
| wire_time_ms(baud, request_bytes, response_bytes) + PROCESSING_BUDGET_MS, | |||
| ) | |||
| return results | |||
| def build_s0_job() -> list[int]: | |||
| s0 = [0] * 20 | |||
| put_u32(s0, 0, 1) | |||
| put_u32(s0, 10, TEST_FREQUENCY_HZ) | |||
| put_u32(s0, 12, TEST_PULSES) | |||
| return s0 | |||
| def prepare_jobs(client: RtuClient) -> None: | |||
| for axis in range(4): | |||
| client.write_multiple(S0_BASES[axis], build_s0_job()) | |||
| client.write_multiple(S1_BASES[axis], [0] * 4) | |||
| def record_metric(name: str, passed: bool, detail: str) -> None: | |||
| """记录一项指标并实时打印 通过/失败 + 实测值。""" | |||
| METRICS.append((name, passed, detail)) | |||
| print(f" [{'通过' if passed else '失败'}] {name}:{detail}") | |||
| def main() -> int: | |||
| parser = argparse.ArgumentParser( | |||
| description="PLSR P16 Modbus 性能统计自动测试" | |||
| "(PLSR_MODBUS_PERFORMANCE_TEST.md)" | |||
| ) | |||
| parser.add_argument("--port", help="串口,例如 COM5;只有一个串口时可省略") | |||
| parser.add_argument("--baud", type=int, default=9600) | |||
| parser.add_argument("--slave", type=int, default=1) | |||
| parser.add_argument( | |||
| "--polls", | |||
| type=int, | |||
| default=10, | |||
| help="静态吞吐测试的完整四轴轮询轮数(默认 10)", | |||
| ) | |||
| args = parser.parse_args() | |||
| with serial.Serial( | |||
| port=choose_port(args.port), | |||
| baudrate=args.baud, | |||
| bytesize=serial.EIGHTBITS, | |||
| parity=serial.PARITY_EVEN, | |||
| stopbits=serial.STOPBITS_ONE, | |||
| timeout=1.0, | |||
| write_timeout=1.0, | |||
| ) as uart: | |||
| client = RtuClient(uart, args.slave) | |||
| header = client.read_holding(CONTROL_BASE, 8) | |||
| if header[:5] != [ | |||
| 0x504C, | |||
| 0x5352, | |||
| 0x0100, | |||
| CONTROL_WINDOW_WORDS, | |||
| 0x0007, | |||
| ]: | |||
| raise RuntimeError( | |||
| f"P16 控制窗口未就绪:{header};请烧录当前固件并复位" | |||
| ) | |||
| print("P16 控制窗口就绪:D1200~D1537,协议 V1.0") | |||
| # ---- 指标1:版本号一致性(32位CPU时钟 + 16位性能版本 + 统计块快照) ---- | |||
| core_clock_hz = get_u32(header, 5) | |||
| performance_version = header[7] | |||
| baseline_block = read_performance_block(client) | |||
| version_ok = ( | |||
| core_clock_hz != 0 | |||
| and performance_version == PERFORMANCE_VERSION | |||
| and baseline_block is not None | |||
| ) | |||
| consistency_text = ( | |||
| "一致" if baseline_block is not None else "连续三次读取不一致" | |||
| ) | |||
| record_metric( | |||
| "版本号一致性", | |||
| version_ok, | |||
| f"协议版本={header[2]:#06x},32位CPU计时频率(D1205~D1206)=" | |||
| f"{core_clock_hz}Hz,性能统计版本(D1207)=V{performance_version}," | |||
| f"P16统计块两次读取{consistency_text}", | |||
| ) | |||
| if not version_ok: | |||
| raise RuntimeError("版本号一致性检查失败,后续指标失去判定基准") | |||
| print_process_checkpoint(client, "复位后基线") | |||
| print("P16 已就绪:四轴 PULSE/DIR,100kHz,200000脉冲/轴") | |||
| # ---- 指标2:单条命令响应时间 ---- | |||
| s0_job = build_s0_job() | |||
| actions = [ | |||
| ( | |||
| "0x03读取控制头(8字)", | |||
| 8, | |||
| 21, | |||
| lambda: client.read_holding(CONTROL_BASE, 8), | |||
| ), | |||
| ( | |||
| "0x10写入S0(20字)", | |||
| 49, | |||
| 8, | |||
| lambda: client.write_multiple(S0_BASES[0], s0_job), | |||
| ), | |||
| ( | |||
| "0x10写入S1(4字)", | |||
| 17, | |||
| 8, | |||
| lambda: client.write_multiple(S1_BASES[0], [0] * 4), | |||
| ), | |||
| ] | |||
| latency = sample_round_trips(client, args.baud, actions, LATENCY_SAMPLES) | |||
| latency_ok = True | |||
| latency_parts = [] | |||
| for name, (minimum, median, maximum, budget) in latency.items(): | |||
| latency_ok = latency_ok and median <= budget | |||
| latency_parts.append( | |||
| f"{name} 中位{median:.1f}ms" | |||
| f"(最小{minimum:.1f}/最大{maximum:.1f},预算{budget:.1f}ms)" | |||
| ) | |||
| record_metric("单条命令响应时间", latency_ok, ";".join(latency_parts)) | |||
| # ---- 指标3:连续状态读取吞吐(静态轮询,无运行压力) ---- | |||
| static_started = time.monotonic() | |||
| for _ in range(args.polls): | |||
| for axis in range(4): | |||
| read_axis_status(client, axis) | |||
| static_elapsed = time.monotonic() - static_started | |||
| static_polls = args.polls * 4 | |||
| static_rate = static_polls / static_elapsed | |||
| theory = throughput_theory(args.baud) | |||
| ratio = static_rate / theory | |||
| record_metric( | |||
| "连续状态读取吞吐", | |||
| ratio >= THROUGHPUT_MIN_RATIO, | |||
| f"静态四轴轮询 {static_polls} 次耗时 {static_elapsed:.3f}s," | |||
| f"速率 {static_rate:.2f} 轮/s,波特率理论上限 {theory:.2f} 轮/s" | |||
| f"(占比 {ratio:.0%})", | |||
| ) | |||
| prepare_jobs(client) | |||
| sequence = 100 | |||
| for axis in range(4): | |||
| response = send_command( | |||
| client, sequence, axis, CMD_SET_POSITION, argument=0 | |||
| ) | |||
| check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION") | |||
| sequence += 1 | |||
| print("四轴位置已清零,S0/S1 已用 0x10 原子写入") | |||
| print_process_checkpoint(client, "位置清零") | |||
| for axis in range(4): | |||
| response = send_call(client, sequence, axis, CALL_COMMIT) | |||
| check_result(response, 3, RESULT_OK, f"轴{axis} COMMIT") | |||
| if response[11] != 1: | |||
| raise RuntimeError(f"轴{axis} COMMIT 未建立有效快照") | |||
| sequence += 1 | |||
| print("四轴 COMMIT 校验通过") | |||
| print_process_checkpoint(client, "COMMIT") | |||
| physical_baseline = [ | |||
| read_axis_status(client, axis)["physical_pulses"] | |||
| for axis in range(4) | |||
| ] | |||
| started = time.monotonic() | |||
| for axis in range(4): | |||
| response = send_call(client, sequence, axis, CALL_START) | |||
| check_result(response, 3, RESULT_QUEUED, f"轴{axis} START") | |||
| sequence += 1 | |||
| print("四轴 START 已排队;持续读取状态以施加 Modbus/任务并发压力") | |||
| print_process_checkpoint(client, "START") | |||
| running_status = [read_axis_status(client, axis) for axis in range(4)] | |||
| for axis, status in enumerate(running_status): | |||
| if status["state"] not in {STATE_ACCEL, STATE_RUN}: | |||
| raise RuntimeError(f"轴{axis} 未进入运行态:{status}") | |||
| expected_mode = 1 if axis < 2 else 0 | |||
| if status["counter_mode"] != expected_mode: | |||
| raise RuntimeError( | |||
| f"轴{axis}计数模式={status['counter_mode']},期望={expected_mode}" | |||
| ) | |||
| if status["current_frequency"] != TEST_FREQUENCY_HZ: | |||
| raise RuntimeError(f"轴{axis}频率不正确:{status}") | |||
| print("运行期计数租约正确:Q0/Q1=硬件,Q2/Q3=软件回退") | |||
| print_process_checkpoint(client, "进入运行态") | |||
| # ---- 指标4:重复序号幂等(轴0,运行期命令处理) ---- | |||
| pause_sequence = sequence | |||
| response = send_command(client, pause_sequence, 0, CMD_PAUSE) | |||
| check_result(response, 4, RESULT_QUEUED, f"轴0 PAUSE#{pause_sequence}") | |||
| status = wait_status(client, 0, {STATE_PAUSED}, sequence=pause_sequence) | |||
| # 重发完全相同的请求:必须只回放既有应答、不得重复执行 | |||
| replay = send_command(client, pause_sequence, 0, CMD_PAUSE) | |||
| status_again = read_axis_status(client, 0) | |||
| idem_ok = ( | |||
| replay == response | |||
| and status_again["state"] == STATE_PAUSED | |||
| and status_again["last_sequence"] == pause_sequence | |||
| ) | |||
| record_metric( | |||
| "重复序号幂等", | |||
| idem_ok, | |||
| f"重发 PAUSE#{pause_sequence}:应答与首次完全一致={replay == response}," | |||
| f"状态保持暂停,last_sequence={status_again['last_sequence']}" | |||
| f"(期望 {pause_sequence})", | |||
| ) | |||
| sequence += 1 | |||
| response = send_command(client, sequence, 0, CMD_RESUME) | |||
| check_result(response, 4, RESULT_QUEUED, f"轴0 RESUME#{sequence}") | |||
| status = wait_running_output(client, 0, sequence=sequence) | |||
| resumed_pulses = status["task_pulses"] | |||
| time.sleep(0.5) | |||
| status = read_axis_status(client, 0) | |||
| if status["task_pulses"] <= resumed_pulses: | |||
| raise RuntimeError(f"RESUME#{sequence} 后轴0脉冲计数未增长:{status}") | |||
| print( | |||
| f"轴0 PAUSE/RESUME 完成:恢复输出,当前 {status['current_frequency']}Hz" | |||
| ) | |||
| print_process_checkpoint(client, "暂停/恢复") | |||
| # ---- 指标5:并发读写压力模拟(运行期持续轮询 + 幂等写入) ---- | |||
| loop_started = time.monotonic() | |||
| deadline = loop_started + RUN_DEADLINE_SECONDS | |||
| polls = 0 | |||
| writes = 0 | |||
| final_status: list[dict[str, int]] = running_status | |||
| while time.monotonic() < deadline: | |||
| final_status = [read_axis_status(client, axis) for axis in range(4)] | |||
| polls += 4 | |||
| # 写压力:重写与 prepare_jobs 完全相同的 S0/S1(幂等, | |||
| # 不影响 COMMIT 后已建立的运行快照) | |||
| client.write_multiple(S0_BASES[0], s0_job) | |||
| client.write_multiple(S1_BASES[0], [0] * 4) | |||
| # 并发下验证 P16 统计块可正常读取 | |||
| client.read_holding(PERFORMANCE_BASE, 8) | |||
| writes += 2 | |||
| if all(item["state"] == STATE_COMPLETED for item in final_status): | |||
| break | |||
| else: | |||
| raise RuntimeError(f"等待四轴完成超时,最后状态:{final_status}") | |||
| run_elapsed = time.monotonic() - loop_started | |||
| # 并发压力下四轴计数精确性核对 | |||
| count_errors: list[str] = [] | |||
| for axis, status in enumerate(final_status): | |||
| expected = { | |||
| "logical_position": TEST_PULSES, | |||
| "task_pulses": TEST_PULSES, | |||
| "error": 0, | |||
| "last_result": RESULT_OK, | |||
| } | |||
| bad = { | |||
| key: (status[key], value) | |||
| for key, value in expected.items() | |||
| if status[key] != value | |||
| } | |||
| physical_delta = status["physical_pulses"] - physical_baseline[axis] | |||
| if physical_delta != TEST_PULSES: | |||
| bad["physical_pulses_delta"] = (physical_delta, TEST_PULSES) | |||
| if bad: | |||
| count_errors.append(f"轴{axis}: {bad}") | |||
| record_metric( | |||
| "并发读写压力", | |||
| not count_errors, | |||
| f"四轴100kHz运行期间读取状态 {polls} 次、穿插幂等写入 {writes} 次," | |||
| f"轮询吞吐 {polls / run_elapsed:.2f} 次/s;四轴计数均精确为 " | |||
| f"{TEST_PULSES}、error=0" | |||
| + (f";异常:{';'.join(count_errors)}" if count_errors else ""), | |||
| ) | |||
| elapsed = time.monotonic() - started | |||
| print_process_checkpoint(client, "运行完成") | |||
| # ---- 指标6:P16 统计块快照一致性(运行后,含基线单调性) ---- | |||
| block = read_performance_block(client) | |||
| if block is None: | |||
| record_metric( | |||
| "P16统计块快照一致性", False, "三次读取未得到连续一致结果" | |||
| ) | |||
| else: | |||
| regressions = [ | |||
| f"D{PERFORMANCE_BASE + offset}: {baseline_block[offset]}→{block[offset]}" | |||
| for offset in range(len(block)) | |||
| if block[offset] < baseline_block[offset] | |||
| ] | |||
| detail = "连续两次读取完全一致(32位周期数与16位饱和字段)" | |||
| if regressions: | |||
| detail += ";但相对复位后基线发生回退:" + "、".join(regressions) | |||
| record_metric("P16统计块快照一致性", not regressions, detail) | |||
| # ---- 指标7~10:P16 自动预算 ---- | |||
| stage_performance = client.read_holding( | |||
| STAGE_PERFORMANCE_BASE, STAGE_PERFORMANCE_WORDS | |||
| ) | |||
| ab_gate_cycles = get_u32( | |||
| client.read_holding(AB_GATE_PERFORMANCE_BASE, 2), 0 | |||
| ) | |||
| cycle_values: dict[str, int] = {} | |||
| if block is not None: | |||
| cycle_values = { | |||
| "PlsrProcess自身": get_u32(block, 0), | |||
| "PlsrProcess响应": get_u32(block, 2), | |||
| "TIM6控制ISR": get_u32(block, 4), | |||
| "输出定时器ISR": block[6], | |||
| "TIM9/12计数ISR": block[7], | |||
| } | |||
| print("P16 DWT最坏执行时间:") | |||
| for name, cycles in cycle_values.items(): | |||
| microseconds = cycles * 1_000_000.0 / core_clock_hz | |||
| print(f" {name:<16} {cycles:8d} cycles {microseconds:8.3f}us") | |||
| print("P16 PlsrProcess分段最大执行时间(各段独立峰值,来自不同轮次不能相加):") | |||
| for index, name in enumerate(STAGE_NAMES): | |||
| cycles = get_u32(stage_performance, index * 2) | |||
| microseconds = cycles * 1_000_000.0 / core_clock_hz | |||
| print(f" {name:<18} {cycles:8d} cycles {microseconds:8.3f}us") | |||
| print( | |||
| " AB末周期快速门控 " | |||
| f"{ab_gate_cycles:8d} cycles " | |||
| f"{ab_gate_cycles * 1_000_000.0 / core_clock_hz:8.3f}us " | |||
| "(本PULSE/DIR用例未执行时允许为0)" | |||
| ) | |||
| budgets = { | |||
| "PlsrProcess自身": (core_clock_hz // 1_000, "1ms"), | |||
| "TIM6控制ISR": (core_clock_hz // 10_000, "0.1ms"), | |||
| "输出定时器ISR": (core_clock_hz // TEST_FREQUENCY_HZ, "10us"), | |||
| "TIM9/12计数ISR": (core_clock_hz // TEST_FREQUENCY_HZ, "10us"), | |||
| } | |||
| for name, (budget, budget_text) in budgets.items(): | |||
| if block is None: | |||
| record_metric(f"P16 {name}预算", False, "统计块不可用,无法判定") | |||
| continue | |||
| cycles = cycle_values[name] | |||
| microseconds = cycles * 1_000_000.0 / core_clock_hz | |||
| passed = cycles != 0 and cycles < budget | |||
| detail = ( | |||
| f"实测 {cycles} cycles = {microseconds:.3f}us," | |||
| f"预算 < {budget} cycles({budget_text})" | |||
| ) | |||
| if cycles == 0: | |||
| detail += ";数值为 0,路径未被实际执行" | |||
| record_metric(f"P16 {name}预算", passed, detail) | |||
| # 墙钟响应与阶段峰值超过 1ms 时仅给提示(规格:不与自身 CPU 混算) | |||
| if block is not None: | |||
| response_cycles = cycle_values["PlsrProcess响应"] | |||
| if response_cycles >= core_clock_hz // 1_000: | |||
| print( | |||
| "提示:PlsrProcess墙钟响应超过1ms,但自身CPU执行时间达标;" | |||
| "差值来自高优先级PLSR定时器中断抢占。" | |||
| ) | |||
| for index, name in enumerate(STAGE_NAMES): | |||
| cycles = get_u32(stage_performance, index * 2) | |||
| if cycles >= core_clock_hz // 1_000: | |||
| print( | |||
| f"提示:阶段“{name}”峰值超过1ms,需定位峰值路径;" | |||
| "各段最大值来自不同轮次,不能直接相加。" | |||
| ) | |||
| # ---- 汇总 ---- | |||
| print("\n性能测试指标汇总:") | |||
| passed_count = sum(1 for _, passed, _ in METRICS if passed) | |||
| for name, passed, detail in METRICS: | |||
| print(f" [{'通过' if passed else '失败'}] {name}:{detail}") | |||
| print(f"最终判定:{passed_count}/{len(METRICS)} 项通过") | |||
| failed = [name for name, passed, _ in METRICS if not passed] | |||
| if failed: | |||
| raise RuntimeError("未通过指标:" + "、".join(failed)) | |||
| print( | |||
| f"全部 PASS:四轴均为 {TEST_PULSES} 脉冲,耗时 {elapsed:.3f}s," | |||
| f"运行期状态读取 {polls} 次" | |||
| ) | |||
| print("请再核对逻辑分析仪:Q0~Q3 各200000个上升沿、100kHz、无窄脉冲。") | |||
| return 0 | |||
| if __name__ == "__main__": | |||
| try: | |||
| raise SystemExit(main()) | |||
| except (RuntimeError, serial.SerialException) as error: | |||
| print(f"测试失败:{error}") | |||
| raise SystemExit(1) | |||
| @@ -0,0 +1,207 @@ | |||
| #!/usr/bin/env python3 | |||
| """PLSR P15 four-axis positive/negative soft-limit precision test.""" | |||
| from __future__ import annotations | |||
| import argparse | |||
| import time | |||
| import serial | |||
| from plsr_modbus_counter_stress_test import ( | |||
| CALL_COMMIT, | |||
| CALL_START, | |||
| CMD_SET_POSITION, | |||
| CONTROL_BASE, | |||
| CONTROL_WINDOW_WORDS, | |||
| RESULT_OK, | |||
| RESULT_QUEUED, | |||
| RtuClient, | |||
| S0_BASES, | |||
| S1_BASES, | |||
| check_result, | |||
| put_u32, | |||
| read_axis_status, | |||
| send_call, | |||
| send_command, | |||
| ) | |||
| from plsr_modbus_frequency_test import choose_port | |||
| STATE_ACCEL = 2 | |||
| STATE_RUN = 3 | |||
| STATE_STOPPED = 8 | |||
| STATE_IDLE = 1 | |||
| CMD_RESET_ERROR = 10 | |||
| ERROR_LIMIT_POSITIVE = 6 | |||
| ERROR_LIMIT_NEGATIVE = 7 | |||
| STOP_LIMIT_POSITIVE = 5 | |||
| STOP_LIMIT_NEGATIVE = 6 | |||
| SOFT_LIMIT = 1_000_000 | |||
| CASES = ( | |||
| # axis, start position, frequency, requested pulses, expected output pulses | |||
| (0, 999_800, 500, 10_000, 200), | |||
| (1, 999_000, 2_000, 10_000, 1_000), | |||
| (2, -999_800, 500, -10_000, 200), | |||
| (3, -999_000, 2_000, -10_000, 1_000), | |||
| ) | |||
| def wait_command_applied( | |||
| client: RtuClient, | |||
| axis: int, | |||
| sequence: int, | |||
| expected_result: int = RESULT_OK, | |||
| timeout: float = 3.0, | |||
| ) -> dict[str, int]: | |||
| deadline = time.monotonic() + timeout | |||
| latest: dict[str, int] | None = None | |||
| while time.monotonic() < deadline: | |||
| latest = read_axis_status(client, axis) | |||
| if latest["last_sequence"] == sequence: | |||
| if latest["last_result"] != expected_result: | |||
| raise RuntimeError( | |||
| f"轴{axis}命令#{sequence}执行结果={latest['last_result']}," | |||
| f"期望={expected_result};状态={latest}" | |||
| ) | |||
| return latest | |||
| raise RuntimeError(f"等待轴{axis}命令#{sequence}执行超时,最后状态={latest}") | |||
| def wait_state( | |||
| client: RtuClient, axis: int, expected: set[int], timeout: float = 6.0 | |||
| ) -> dict[str, int]: | |||
| deadline = time.monotonic() + timeout | |||
| latest: dict[str, int] | None = None | |||
| while time.monotonic() < deadline: | |||
| latest = read_axis_status(client, axis) | |||
| if latest["state"] in expected: | |||
| return latest | |||
| raise RuntimeError( | |||
| f"等待轴{axis}状态{sorted(expected)}超时,最后状态={latest}" | |||
| ) | |||
| def write_job( | |||
| client: RtuClient, axis: int, frequency_hz: int, signed_pulses: int | |||
| ) -> None: | |||
| s0 = [0] * 20 | |||
| put_u32(s0, 0, 1) | |||
| put_u32(s0, 10, frequency_hz) | |||
| put_u32(s0, 12, signed_pulses) | |||
| client.write_multiple(S0_BASES[axis], s0) | |||
| client.write_multiple(S1_BASES[axis], [0] * 4) | |||
| def main() -> int: | |||
| parser = argparse.ArgumentParser( | |||
| description="PLSR P15 四轴正负软限位边界精度测试" | |||
| ) | |||
| parser.add_argument("--port", help="串口,例如 COM5;只有一个串口时可省略") | |||
| parser.add_argument("--baud", type=int, default=9600) | |||
| parser.add_argument("--slave", type=int, default=1) | |||
| args = parser.parse_args() | |||
| with serial.Serial( | |||
| port=choose_port(args.port), | |||
| baudrate=args.baud, | |||
| bytesize=serial.EIGHTBITS, | |||
| parity=serial.PARITY_EVEN, | |||
| stopbits=serial.STOPBITS_ONE, | |||
| timeout=1.0, | |||
| write_timeout=1.0, | |||
| ) as uart: | |||
| client = RtuClient(uart, args.slave) | |||
| header = client.read_holding(CONTROL_BASE, 8) | |||
| if header[:5] != [ | |||
| 0x504C, | |||
| 0x5352, | |||
| 0x0100, | |||
| CONTROL_WINDOW_WORDS, | |||
| 0x0007, | |||
| ]: | |||
| raise RuntimeError( | |||
| f"P15控制窗口未就绪:{header};请烧录当前固件并复位" | |||
| ) | |||
| print("P15 已就绪:软限位±1000000,K2保护矩阵") | |||
| sequence = 500 | |||
| for axis, start_position, frequency, requested, expected_pulses in CASES: | |||
| positive = requested > 0 | |||
| label = "正限位" if positive else "负限位" | |||
| expected_position = SOFT_LIMIT if positive else -SOFT_LIMIT | |||
| expected_error = ( | |||
| ERROR_LIMIT_POSITIVE if positive else ERROR_LIMIT_NEGATIVE | |||
| ) | |||
| expected_reason = ( | |||
| STOP_LIMIT_POSITIVE if positive else STOP_LIMIT_NEGATIVE | |||
| ) | |||
| response = send_command( | |||
| client, sequence, axis, CMD_SET_POSITION, start_position | |||
| ) | |||
| check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION") | |||
| before = wait_command_applied(client, axis, sequence) | |||
| sequence += 1 | |||
| write_job(client, axis, frequency, requested) | |||
| response = send_call( | |||
| client, sequence, axis, CALL_COMMIT, s2_set=2 | |||
| ) | |||
| check_result(response, 3, RESULT_OK, f"轴{axis} COMMIT") | |||
| sequence += 1 | |||
| start_sequence = sequence | |||
| response = send_call( | |||
| client, start_sequence, axis, CALL_START, s2_set=2 | |||
| ) | |||
| check_result(response, 3, RESULT_QUEUED, f"轴{axis} START") | |||
| sequence += 1 | |||
| running = wait_state(client, axis, {STATE_ACCEL, STATE_RUN}) | |||
| if running["counter_mode"] != 1: | |||
| raise RuntimeError(f"轴{axis}未取得硬件计数器:{running}") | |||
| stopped = wait_state(client, axis, {STATE_STOPPED}) | |||
| physical_delta = stopped["physical_pulses"] - before["physical_pulses"] | |||
| expected_task = expected_pulses if positive else -expected_pulses | |||
| checks = { | |||
| "逻辑位置": (stopped["logical_position"], expected_position), | |||
| "任务脉冲": (stopped["task_pulses"], expected_task), | |||
| "物理脉冲增量": (physical_delta, expected_pulses), | |||
| } | |||
| bad = { | |||
| name: values | |||
| for name, values in checks.items() | |||
| if abs(values[0] - values[1]) > 1 | |||
| } | |||
| if bad: | |||
| raise RuntimeError(f"轴{axis}{label}边界超差:{bad};状态={stopped}") | |||
| if ( | |||
| stopped["error"] != expected_error | |||
| or stopped["stop_reason"] != expected_reason | |||
| ): | |||
| raise RuntimeError(f"轴{axis}{label}错误语义不正确:{stopped}") | |||
| print( | |||
| f"轴{axis} {label} {frequency:5d}Hz:" | |||
| f"输出={physical_delta},位置={stopped['logical_position']},PASS" | |||
| ) | |||
| response = send_command(client, sequence, axis, CMD_RESET_ERROR) | |||
| check_result(response, 4, RESULT_QUEUED, f"轴{axis} RESET_ERROR") | |||
| reset = wait_command_applied(client, axis, sequence) | |||
| sequence += 1 | |||
| if reset["state"] != STATE_IDLE or reset["error"] != 0: | |||
| raise RuntimeError(f"轴{axis}错误复位不完整:{reset}") | |||
| print("全部 PASS:四轴正/负软限位在±1脉冲窗口内停止,错误码及复位正确。") | |||
| print("请核对波形:Q0/Q2约200个上升沿,Q1/Q3约1000个上升沿。") | |||
| return 0 | |||
| if __name__ == "__main__": | |||
| try: | |||
| raise SystemExit(main()) | |||
| except (RuntimeError, serial.SerialException) as error: | |||
| print(f"测试失败:{error}") | |||
| raise SystemExit(1) | |||
| @@ -0,0 +1,520 @@ | |||
| #!/usr/bin/env python3 | |||
| """Phase-based PLSR HSD/SFD persistence board acceptance tool. | |||
| Normal phases never corrupt storage and issue at most one SFD SAVE per | |||
| invocation. Power removal/reset is intentionally manual so VBAT retention and | |||
| real reset behavior are tested rather than simulated by a software command. | |||
| """ | |||
| from __future__ import annotations | |||
| import argparse | |||
| import json | |||
| import tempfile | |||
| import time | |||
| from pathlib import Path | |||
| import serial | |||
| from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words | |||
| CONTROL_BASE = 1200 | |||
| CONTROL_WINDOW_WORDS = 338 | |||
| CALL_REQUEST = CONTROL_BASE + 8 | |||
| CALL_RESPONSE = CONTROL_BASE + 24 | |||
| COMMAND_REQUEST = CONTROL_BASE + 40 | |||
| COMMAND_RESPONSE = CONTROL_BASE + 48 | |||
| AXIS_STATUS_BASE = CONTROL_BASE + 64 | |||
| AXIS_STATUS_WORDS = 48 | |||
| PERSISTENCE_BASE = CONTROL_BASE + 270 | |||
| PERSISTENCE_WORDS = 30 | |||
| PERSISTENCE_REQUEST = CONTROL_BASE + 300 | |||
| PERSISTENCE_RESPONSE = CONTROL_BASE + 308 | |||
| PERSISTENCE_VERSION = 1 | |||
| PERFORMANCE_VERSION = 7 | |||
| S0_BASES = (1600, 1800, 2000, 2200) | |||
| S1_BASES = (1700, 1900, 2100, 2300) | |||
| KNOWN_POSITIONS = (123456, -234567, 345678, -456789) | |||
| RESULT_OK = 0 | |||
| RESULT_QUEUED = 1 | |||
| RESULT_BUSY = 8 | |||
| RESULT_NOT_SUPPORTED = 10 | |||
| PERSISTENCE_OK = 0 | |||
| PERSISTENCE_NOT_IMPLEMENTED = 4 | |||
| STATE_IDLE = 1 | |||
| STATE_ACCEL = 2 | |||
| STATE_RUN = 3 | |||
| STATE_DECEL = 4 | |||
| STATE_STOPPED = 8 | |||
| CALL_COMMIT = 1 | |||
| CALL_START = 2 | |||
| CMD_STOP_IMMEDIATE = 2 | |||
| CMD_SET_POSITION = 5 | |||
| CMD_SAVE_CONFIG = 8 | |||
| DESTRUCTIVE_MAGIC_A = 0xDA7A | |||
| DESTRUCTIVE_MAGIC_B = 0x51F0 | |||
| DESTRUCTIVE_ARM = 0xA55A | |||
| DESTRUCTIVE_CONFIRM = "INVALIDATE-NEWEST-SLOT" | |||
| def put_u32(words: list[int], offset: int, value: int) -> None: | |||
| words[offset : offset + 2] = signed_dword_words(value) | |||
| def put_u64(words: list[int], offset: int, value: int) -> None: | |||
| raw = value & 0xFFFFFFFFFFFFFFFF | |||
| words[offset : offset + 4] = [ | |||
| (raw >> shift) & 0xFFFF for shift in (0, 16, 32, 48) | |||
| ] | |||
| def get_u32(words: list[int], offset: int) -> int: | |||
| return words[offset] | (words[offset + 1] << 16) | |||
| def get_u64(words: list[int], offset: int, signed: bool = False) -> int: | |||
| raw = sum(words[offset + index] << (16 * index) for index in range(4)) | |||
| if signed and raw & (1 << 63): | |||
| return raw - (1 << 64) | |||
| return raw | |||
| def next_sequence() -> int: | |||
| value = int(time.time_ns() // 1_000_000) & 0x7FFFFFFF | |||
| return value if value != 0 else 1 | |||
| def wait_response( | |||
| client: RtuClient, address: int, count: int, sequence: int, timeout: float = 3.0 | |||
| ) -> list[int]: | |||
| deadline = time.monotonic() + timeout | |||
| latest: list[int] | None = None | |||
| while time.monotonic() < deadline: | |||
| latest = client.read_holding(address, count) | |||
| if get_u32(latest, 0) == sequence: | |||
| return latest | |||
| raise RuntimeError(f"等待序号 {sequence} 应答超时,最后应答={latest}") | |||
| def send_command( | |||
| client: RtuClient, sequence: int, axis: int, opcode: int, argument: int = 0 | |||
| ) -> list[int]: | |||
| request = [0] * 8 | |||
| put_u32(request, 0, sequence) | |||
| request[2] = opcode | |||
| request[3] = axis | |||
| put_u64(request, 4, argument) | |||
| client.write_multiple(COMMAND_REQUEST, request) | |||
| return wait_response(client, COMMAND_RESPONSE, 8, sequence) | |||
| def read_axis_status(client: RtuClient, axis: int) -> dict[str, int]: | |||
| words = client.read_holding( | |||
| AXIS_STATUS_BASE + axis * AXIS_STATUS_WORDS, AXIS_STATUS_WORDS | |||
| ) | |||
| generation_begin = get_u32(words, 0) | |||
| generation_end = get_u32(words, 46) | |||
| if generation_begin != generation_end or generation_begin & 1: | |||
| raise RuntimeError( | |||
| f"轴{axis}状态快照不一致:{generation_begin}/{generation_end}" | |||
| ) | |||
| return { | |||
| "state": words[2], | |||
| "flags": get_u32(words, 3), | |||
| "last_result": words[8], | |||
| "last_sequence": get_u32(words, 10), | |||
| "logical_position": get_u64(words, 16, signed=True), | |||
| "task_pulses": get_u64(words, 20, signed=True), | |||
| "current_frequency": get_u32(words, 38), | |||
| } | |||
| def wait_command_applied( | |||
| client: RtuClient, axis: int, sequence: int, timeout: float = 5.0 | |||
| ) -> dict[str, int]: | |||
| deadline = time.monotonic() + timeout | |||
| latest: dict[str, int] | None = None | |||
| while time.monotonic() < deadline: | |||
| latest = read_axis_status(client, axis) | |||
| if latest["last_sequence"] == sequence: | |||
| return latest | |||
| raise RuntimeError(f"等待命令 {sequence} 内核执行超时,最后状态={latest}") | |||
| def read_persistence(client: RtuClient) -> dict[str, int]: | |||
| latest: list[int] | None = None | |||
| for _ in range(5): | |||
| latest = client.read_holding(PERSISTENCE_BASE, PERSISTENCE_WORDS) | |||
| generation_begin = get_u32(latest, 0) | |||
| generation_end = get_u32(latest, 28) | |||
| if generation_begin == generation_end and not generation_begin & 1: | |||
| if latest[2] != PERSISTENCE_VERSION: | |||
| raise RuntimeError(f"持久化诊断版本错误:{latest[2]}") | |||
| hsd_newest = latest[4] & 0xFF | |||
| sfd_newest = (latest[4] >> 8) & 0xFF | |||
| hsd_generations = (get_u32(latest, 12), get_u32(latest, 14)) | |||
| sfd_generations = (get_u32(latest, 16), get_u32(latest, 18)) | |||
| return { | |||
| "generation": generation_begin, | |||
| "hsd_valid_mask": latest[3] & 0xFF, | |||
| "sfd_valid_mask": (latest[3] >> 8) & 0xFF, | |||
| "hsd_newest_mask": hsd_newest, | |||
| "sfd_newest_mask": sfd_newest, | |||
| "flags": latest[5], | |||
| "last_hsd_load": latest[6], | |||
| "last_sfd_load": latest[7], | |||
| "last_hsd_save": latest[8], | |||
| "last_sfd_save": latest[9], | |||
| "last_sfd_erase": latest[10], | |||
| "hsd_generation_a": hsd_generations[0], | |||
| "hsd_generation_b": hsd_generations[1], | |||
| "sfd_generation_a": sfd_generations[0], | |||
| "sfd_generation_b": sfd_generations[1], | |||
| "hsd_selected_generation": ( | |||
| hsd_generations[0] | |||
| if hsd_newest == 1 | |||
| else hsd_generations[1] if hsd_newest == 2 else 0 | |||
| ), | |||
| "sfd_selected_generation": ( | |||
| sfd_generations[0] | |||
| if sfd_newest == 1 | |||
| else sfd_generations[1] if sfd_newest == 2 else 0 | |||
| ), | |||
| "hsd_save_count": get_u32(latest, 20), | |||
| "sfd_save_count": get_u32(latest, 22), | |||
| "hsd_crc32": get_u32(latest, 24), | |||
| "sfd_crc32": get_u32(latest, 26), | |||
| } | |||
| raise RuntimeError(f"持久化诊断快照连续不一致:{latest}") | |||
| def wait_persistence_clean(client: RtuClient, timeout: float = 4.0) -> dict[str, int]: | |||
| deadline = time.monotonic() + timeout | |||
| latest: dict[str, int] | None = None | |||
| while time.monotonic() < deadline: | |||
| latest = read_persistence(client) | |||
| if not latest["flags"] & 1 and latest["hsd_valid_mask"] != 0: | |||
| return latest | |||
| raise RuntimeError(f"HSD 检查点未在期限内完成:{latest}") | |||
| def send_call( | |||
| client: RtuClient, sequence: int, axis: int, operation: int | |||
| ) -> list[int]: | |||
| request = [0] * 16 | |||
| put_u32(request, 0, sequence) | |||
| request[2] = 0 | |||
| put_u32(request, 3, S0_BASES[axis]) | |||
| request[5] = 0 | |||
| put_u32(request, 6, S1_BASES[axis]) | |||
| request[8] = 0 | |||
| put_u32(request, 10, 1) | |||
| request[12] = axis | |||
| request[13] = 0 | |||
| request[14] = operation | |||
| client.write_multiple(CALL_REQUEST, request) | |||
| return wait_response(client, CALL_RESPONSE, 12, sequence) | |||
| def start_long_motion(client: RtuClient, sequence: int) -> int: | |||
| s0 = [0] * 20 | |||
| put_u32(s0, 0, 1) | |||
| put_u32(s0, 10, 100_000) | |||
| put_u32(s0, 12, 50_000_000) | |||
| client.write_multiple(S0_BASES[0], s0) | |||
| client.write_multiple(S1_BASES[0], [0] * 4) | |||
| response = send_call(client, sequence, 0, CALL_COMMIT) | |||
| if response[3] != RESULT_OK or response[11] != 1: | |||
| raise RuntimeError(f"长任务 COMMIT 失败:{response}") | |||
| sequence += 1 | |||
| response = send_call(client, sequence, 0, CALL_START) | |||
| if response[3] != RESULT_QUEUED: | |||
| raise RuntimeError(f"长任务 START 失败:{response}") | |||
| deadline = time.monotonic() + 5.0 | |||
| latest: dict[str, int] | None = None | |||
| while time.monotonic() < deadline: | |||
| latest = read_axis_status(client, 0) | |||
| if ( | |||
| latest["last_sequence"] == sequence | |||
| and latest["state"] in {STATE_ACCEL, STATE_RUN, STATE_DECEL} | |||
| and latest["flags"] & (1 << 1) | |||
| ): | |||
| return sequence + 1 | |||
| raise RuntimeError(f"长任务未进入真实输出态:{latest}") | |||
| def load_state(path: Path) -> dict[str, object]: | |||
| if not path.exists(): | |||
| raise RuntimeError(f"阶段状态文件不存在:{path}") | |||
| return json.loads(path.read_text(encoding="utf-8")) | |||
| def save_state(path: Path, state: dict[str, object]) -> None: | |||
| path.parent.mkdir(parents=True, exist_ok=True) | |||
| path.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8") | |||
| def print_diagnostics(diag: dict[str, int]) -> None: | |||
| print( | |||
| "持久化诊断:" | |||
| f"HSD valid=0x{diag['hsd_valid_mask']:02X}, " | |||
| f"gen={diag['hsd_selected_generation']}, crc=0x{diag['hsd_crc32']:08X}, " | |||
| f"boot saves={diag['hsd_save_count']}; " | |||
| f"SFD valid=0x{diag['sfd_valid_mask']:02X}, " | |||
| f"gen={diag['sfd_selected_generation']}, crc=0x{diag['sfd_crc32']:08X}, " | |||
| f"boot saves={diag['sfd_save_count']}; flags=0x{diag['flags']:04X}" | |||
| ) | |||
| def phase_hsd_prepare(client: RtuClient, state_path: Path) -> None: | |||
| sequence = next_sequence() | |||
| for axis, position in enumerate(KNOWN_POSITIONS): | |||
| response = send_command(client, sequence, axis, CMD_SET_POSITION, position) | |||
| if response[4] != RESULT_QUEUED: | |||
| raise RuntimeError(f"轴{axis} SET_POSITION 未排队:{response}") | |||
| status = wait_command_applied(client, axis, sequence) | |||
| if status["last_result"] != RESULT_OK or status["logical_position"] != position: | |||
| raise RuntimeError(f"轴{axis}位置检查失败:{status}") | |||
| sequence += 1 | |||
| diag = wait_persistence_clean(client) | |||
| if diag["last_hsd_save"] != PERSISTENCE_OK: | |||
| raise RuntimeError(f"HSD 保存结果不是 OK:{diag}") | |||
| save_state(state_path, {"positions": list(KNOWN_POSITIONS)}) | |||
| print_diagnostics(diag) | |||
| print("HSD prepare PASS。现在关闭主电源(保持 VBAT),再执行 --phase hsd-verify。") | |||
| def phase_hsd_verify(client: RtuClient, state_path: Path) -> None: | |||
| expected = [int(value) for value in load_state(state_path)["positions"]] | |||
| for axis, position in enumerate(expected): | |||
| status = read_axis_status(client, axis) | |||
| if status["logical_position"] != position or not status["flags"] & (1 << 5): | |||
| raise RuntimeError(f"轴{axis} HSD 恢复失败:期望{position},实测{status}") | |||
| if status["flags"] & (1 << 1): | |||
| raise RuntimeError(f"轴{axis} 上电后意外输出:{status}") | |||
| diag = read_persistence(client) | |||
| if diag["last_hsd_load"] != PERSISTENCE_OK: | |||
| raise RuntimeError(f"HSD 上电加载结果不是 OK:{diag}") | |||
| if not diag["flags"] & (1 << 2) or diag["flags"] & (1 << 3): | |||
| raise RuntimeError(f"HSD restored flags 不符合正常停机:{diag}") | |||
| print_diagnostics(diag) | |||
| print("HSD VBAT 掉主电恢复 PASS。") | |||
| def phase_busy_prepare(client: RtuClient) -> None: | |||
| sequence = start_long_motion(client, next_sequence()) | |||
| diag = wait_persistence_clean(client) | |||
| print_diagnostics(diag) | |||
| print( | |||
| f"运行中掉电已就绪(下一序号{sequence})。现在直接关闭主电源或硬复位," | |||
| "不要先发 STOP;重启后执行 --phase busy-verify。" | |||
| ) | |||
| def phase_busy_verify(client: RtuClient) -> None: | |||
| for axis in range(4): | |||
| status = read_axis_status(client, axis) | |||
| if status["flags"] & (1 << 1): | |||
| raise RuntimeError(f"轴{axis} 重启后仍有输出:{status}") | |||
| if status["flags"] & (1 << 5): | |||
| raise RuntimeError(f"轴{axis} 运行中复位后 position_valid 未清除:{status}") | |||
| if status["state"] != STATE_IDLE: | |||
| raise RuntimeError(f"轴{axis} 重启后不是 IDLE:{status}") | |||
| diag = read_persistence(client) | |||
| if not diag["flags"] & (1 << 3) or diag["flags"] & (1 << 2): | |||
| raise RuntimeError(f"busy-reset restored flags 错误:{diag}") | |||
| print_diagnostics(diag) | |||
| print("运行中掉电/复位 PASS:未自动续跑,所有轴 position_valid=0。") | |||
| def phase_sfd_save(client: RtuClient, state_path: Path) -> None: | |||
| before = read_persistence(client) | |||
| sequence = next_sequence() | |||
| response = send_command(client, sequence, 0, CMD_SAVE_CONFIG) | |||
| if response[4] != RESULT_QUEUED: | |||
| raise RuntimeError(f"SAVE_CONFIG 未排队:{response}") | |||
| status = wait_command_applied(client, 0, sequence, timeout=10.0) | |||
| if status["last_result"] != RESULT_OK: | |||
| raise RuntimeError(f"SAVE_CONFIG 内核执行失败:{status}") | |||
| time.sleep(0.1) | |||
| after = read_persistence(client) | |||
| was_dirty = bool(before["flags"] & (1 << 1)) | |||
| if was_dirty: | |||
| if after["sfd_save_count"] != before["sfd_save_count"] + 1: | |||
| raise RuntimeError(f"SFD 实际保存次数不正确:before={before}, after={after}") | |||
| if after["last_sfd_save"] != PERSISTENCE_OK: | |||
| raise RuntimeError(f"SFD 保存结果不是 OK:{after}") | |||
| if after["sfd_selected_generation"] == before["sfd_selected_generation"]: | |||
| raise RuntimeError(f"SFD generation 未前进:before={before}, after={after}") | |||
| elif after["sfd_save_count"] != before["sfd_save_count"]: | |||
| raise RuntimeError("SFD clean no-op 不应擦写 Flash") | |||
| if after["sfd_valid_mask"] == 0: | |||
| raise RuntimeError(f"SFD 没有可验证的有效槽:{after}") | |||
| state = load_state(state_path) if state_path.exists() else {} | |||
| state["sfd_generation"] = after["sfd_selected_generation"] | |||
| state["sfd_crc32"] = after["sfd_crc32"] | |||
| save_state(state_path, state) | |||
| print_diagnostics(after) | |||
| if not was_dirty: | |||
| print("SFD 当前为 clean,本次 SAVE 正确地没有重复擦写;验证现有提交记录。") | |||
| print("SFD save PASS。现在硬复位/掉电重启,再执行 --phase sfd-verify。") | |||
| def phase_sfd_verify(client: RtuClient, state_path: Path) -> None: | |||
| state = load_state(state_path) | |||
| expected_generation = int(state["sfd_generation"]) | |||
| expected_crc = int(state["sfd_crc32"]) | |||
| diag = read_persistence(client) | |||
| if diag["last_sfd_load"] != PERSISTENCE_OK: | |||
| raise RuntimeError(f"SFD 上电加载结果不是 OK:{diag}") | |||
| if ( | |||
| diag["sfd_selected_generation"] != expected_generation | |||
| or diag["sfd_crc32"] != expected_crc | |||
| ): | |||
| raise RuntimeError( | |||
| "SFD 上电记录不一致:" | |||
| f"期望 gen={expected_generation}, crc=0x{expected_crc:08X};实测={diag}" | |||
| ) | |||
| print_diagnostics(diag) | |||
| print("SFD A/B + CRC 上电加载 PASS。") | |||
| def phase_motion_save_busy(client: RtuClient) -> None: | |||
| sequence = start_long_motion(client, next_sequence()) | |||
| before = read_persistence(client) | |||
| response = send_command(client, sequence, 0, CMD_SAVE_CONFIG) | |||
| if response[4] != RESULT_QUEUED: | |||
| raise RuntimeError(f"运行中 SAVE_CONFIG 未进入命令队列:{response}") | |||
| status = wait_command_applied(client, 0, sequence) | |||
| if status["last_result"] != RESULT_BUSY: | |||
| raise RuntimeError(f"运行中 SAVE_CONFIG 未返回 BUSY:{status}") | |||
| after = read_persistence(client) | |||
| if after["sfd_save_count"] != before["sfd_save_count"]: | |||
| raise RuntimeError("运行中 SAVE_CONFIG 意外擦写了 Flash") | |||
| sequence += 1 | |||
| response = send_command(client, sequence, 0, CMD_STOP_IMMEDIATE) | |||
| if response[4] != RESULT_QUEUED: | |||
| raise RuntimeError(f"清理 STOP_IMMEDIATE 未排队:{response}") | |||
| status = wait_command_applied(client, 0, sequence) | |||
| if status["last_result"] != RESULT_OK or status["state"] != STATE_STOPPED: | |||
| raise RuntimeError(f"清理停止失败:{status}") | |||
| print_diagnostics(after) | |||
| print("运行中禁止 SFD Flash 擦写 PASS:SAVE_CONFIG=BUSY,save_count 未增加。") | |||
| def phase_invalidate( | |||
| client: RtuClient, target: str, allow: bool, confirmation: str | None | |||
| ) -> None: | |||
| if not allow or confirmation != DESTRUCTIVE_CONFIRM: | |||
| raise RuntimeError( | |||
| "破坏性诊断未授权;必须同时使用 --allow-destructive " | |||
| f"--confirm {DESTRUCTIVE_CONFIRM}" | |||
| ) | |||
| before = read_persistence(client) | |||
| if not before["flags"] & (1 << 4): | |||
| raise RuntimeError("固件未显式启用 PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG") | |||
| mask_key = "hsd_valid_mask" if target == "hsd" else "sfd_valid_mask" | |||
| if before[mask_key] != 3: | |||
| raise RuntimeError(f"必须先有两个有效槽,当前诊断={before}") | |||
| sequence = next_sequence() | |||
| inverse = (~sequence) & 0xFFFFFFFF | |||
| request = [DESTRUCTIVE_MAGIC_A, DESTRUCTIVE_MAGIC_B] | |||
| request += signed_dword_words(sequence) | |||
| request += signed_dword_words(inverse) | |||
| request += [1 if target == "hsd" else 2, DESTRUCTIVE_ARM] | |||
| client.write_multiple(PERSISTENCE_REQUEST, request) | |||
| response = wait_response(client, PERSISTENCE_RESPONSE, 8, sequence) | |||
| if response[3] == RESULT_NOT_SUPPORTED: | |||
| raise RuntimeError("破坏性诊断被固件拒绝(normal build)") | |||
| if response[3] != RESULT_OK: | |||
| raise RuntimeError(f"失效 newest {target.upper()} 槽失败:{response}") | |||
| after = read_persistence(client) | |||
| if after[mask_key] not in {1, 2}: | |||
| raise RuntimeError(f"失效后应只剩一个有效槽:{after}") | |||
| print_diagnostics(after) | |||
| print(f"受控失效 newest {target.upper()} 槽 PASS;请立即执行对应 LOAD/重启回退验证。") | |||
| def main() -> int: | |||
| parser = argparse.ArgumentParser(description="PLSR HSD/SFD 真机掉电与 Flash 验收") | |||
| parser.add_argument( | |||
| "--phase", | |||
| required=True, | |||
| choices=( | |||
| "diagnostics", | |||
| "hsd-prepare", | |||
| "hsd-verify", | |||
| "busy-prepare", | |||
| "busy-verify", | |||
| "sfd-save", | |||
| "sfd-verify", | |||
| "motion-save-busy", | |||
| "invalidate-hsd", | |||
| "invalidate-sfd", | |||
| ), | |||
| ) | |||
| parser.add_argument("--port", default="COM5") | |||
| parser.add_argument("--baud", type=int, default=9600) | |||
| parser.add_argument("--slave", type=int, default=1) | |||
| parser.add_argument( | |||
| "--state-file", | |||
| type=Path, | |||
| default=Path(tempfile.gettempdir()) / "plsr_persistence_board_state.json", | |||
| ) | |||
| parser.add_argument("--allow-destructive", action="store_true") | |||
| parser.add_argument("--confirm") | |||
| args = parser.parse_args() | |||
| with serial.Serial( | |||
| port=choose_port(args.port), | |||
| baudrate=args.baud, | |||
| bytesize=serial.EIGHTBITS, | |||
| parity=serial.PARITY_EVEN, | |||
| stopbits=serial.STOPBITS_ONE, | |||
| timeout=1.0, | |||
| write_timeout=1.0, | |||
| ) as uart: | |||
| client = RtuClient(uart, args.slave) | |||
| header = client.read_holding(CONTROL_BASE, 8) | |||
| if header[:5] != [0x504C, 0x5352, 0x0100, CONTROL_WINDOW_WORDS, 0x0007]: | |||
| raise RuntimeError(f"控制窗口未就绪或固件过旧:{header}") | |||
| if header[7] != PERFORMANCE_VERSION: | |||
| raise RuntimeError(f"需要诊断版本 V{PERFORMANCE_VERSION},当前 V{header[7]}") | |||
| print(f"控制窗口 D1200~D1537 就绪;阶段状态文件:{args.state_file}") | |||
| if args.phase == "diagnostics": | |||
| print_diagnostics(read_persistence(client)) | |||
| elif args.phase == "hsd-prepare": | |||
| phase_hsd_prepare(client, args.state_file) | |||
| elif args.phase == "hsd-verify": | |||
| phase_hsd_verify(client, args.state_file) | |||
| elif args.phase == "busy-prepare": | |||
| phase_busy_prepare(client) | |||
| elif args.phase == "busy-verify": | |||
| phase_busy_verify(client) | |||
| elif args.phase == "sfd-save": | |||
| phase_sfd_save(client, args.state_file) | |||
| elif args.phase == "sfd-verify": | |||
| phase_sfd_verify(client, args.state_file) | |||
| elif args.phase == "motion-save-busy": | |||
| phase_motion_save_busy(client) | |||
| elif args.phase == "invalidate-hsd": | |||
| phase_invalidate(client, "hsd", args.allow_destructive, args.confirm) | |||
| else: | |||
| phase_invalidate(client, "sfd", args.allow_destructive, args.confirm) | |||
| return 0 | |||
| if __name__ == "__main__": | |||
| try: | |||
| raise SystemExit(main()) | |||
| except (RuntimeError, serial.SerialException, KeyError, ValueError) as error: | |||
| print(f"测试失败:{error}") | |||
| raise SystemExit(1) | |||
| @@ -16,9 +16,22 @@ typedef enum | |||
| MODBUS_DATA_DEVICE_FD | |||
| } MODBUS_DATA_DEVICE; | |||
| /* Logical PLC bit-device spaces. Standard Modbus coils expose M; X is | |||
| * written by the input-image producer and HM is available to retained logic. | |||
| * All consumers, including PLSR, read the same packed images here. */ | |||
| typedef enum | |||
| { | |||
| MODBUS_BIT_DEVICE_X = 0, | |||
| MODBUS_BIT_DEVICE_M, | |||
| MODBUS_BIT_DEVICE_HM | |||
| } MODBUS_BIT_DEVICE; | |||
| #define MODBUS_DATA_D_WORD_COUNT (10000UL) | |||
| #define MODBUS_DATA_HD_WORD_COUNT (10000UL) | |||
| #define MODBUS_DATA_FD_WORD_COUNT (10000UL) | |||
| #define MODBUS_DATA_X_BIT_COUNT (10000UL) | |||
| #define MODBUS_DATA_M_BIT_COUNT (10000UL) | |||
| #define MODBUS_DATA_HM_BIT_COUNT (10000UL) | |||
| uint8_t ModbusDataValidateWords(MODBUS_DATA_DEVICE device, | |||
| uint32_t firstAddress, | |||
| @@ -44,6 +57,16 @@ uint8_t ModbusDataReadLinear(uint32_t address, uint16_t *value); | |||
| /* Even means stable; odd means a multi-register write is in progress. */ | |||
| uint32_t ModbusDataGetWriteSequence(void); | |||
| uint8_t ModbusDataValidateBits(MODBUS_BIT_DEVICE device, | |||
| uint32_t firstAddress, | |||
| uint32_t bitCount); | |||
| uint8_t ModbusDataReadBit(MODBUS_BIT_DEVICE device, | |||
| uint32_t address, | |||
| uint8_t *value); | |||
| uint8_t ModbusDataWriteBit(MODBUS_BIT_DEVICE device, | |||
| uint32_t address, | |||
| uint8_t value); | |||
| #ifdef __cplusplus | |||
| } | |||
| #endif | |||
| @@ -20,6 +20,21 @@ extern "C" | |||
| #define MODBUS_SLAVE_DEFAULT_ADDRESS (1U) | |||
| #define MODBUS_CONNECTION_TIMEOUT_MS (1000U) | |||
| /* Read-only vendor diagnostics. The request uses the same start/quantity | |||
| * layout as function 0x03; diagnostic words are described below. */ | |||
| #define MODBUS_RUNTIME_DIAGNOSTICS_FUNCTION (0x47U) | |||
| #define MODBUS_RUNTIME_DIAGNOSTICS_SIGNATURE (0x4D42U) | |||
| #define MODBUS_RUNTIME_DIAGNOSTICS_VERSION (1U) | |||
| #define MODBUS_RUNTIME_DIAGNOSTICS_WORDS (40U) | |||
| #define MODBUS_RUNTIME_FLAG_INITIALIZED (1U << 0U) | |||
| #define MODBUS_RUNTIME_FLAG_VALID_FRAME_SEEN (1U << 1U) | |||
| #define MODBUS_RUNTIME_FLAG_CONNECTED (1U << 2U) | |||
| #define MODBUS_RUNTIME_FLAG_TX_BUSY (1U << 3U) | |||
| #define MODBUS_RUNTIME_FLAG_RX_FRAME_READY (1U << 4U) | |||
| #define MODBUS_RUNTIME_FLAG_RX_ASSEMBLY_INVALID (1U << 5U) | |||
| #define MODBUS_RUNTIME_FLAG_RX_RESTART_OK (1U << 6U) | |||
| /** | |||
| * @brief 与 TouchWin 触摸屏联调使用的演示地址 | |||
| * | |||
| @@ -66,6 +81,34 @@ typedef struct | |||
| uint32_t droppedFrameCount; ///< 接收槽占用或长度错误导致的丢帧数 | |||
| uint32_t uartErrorCount; ///< HAL 串口错误计数 | |||
| } MODBUS_SLAVE_STATS; | |||
| /** | |||
| * @brief Read-only snapshot of the Modbus RTU runtime state. | |||
| * | |||
| * Every aligned 32-bit-or-smaller field is read without tearing on Cortex-M4. | |||
| * The getter does not mask motion or UART interrupts, so the complete structure | |||
| * may intentionally span adjacent UART events and is not a transaction-wide | |||
| * atomic snapshot. | |||
| */ | |||
| typedef struct | |||
| { | |||
| MODBUS_SLAVE_STATS statistics; | |||
| uint32_t currentTick; | |||
| uint32_t lastValidFrameTick; | |||
| uint32_t lastInterFrameGapCycles; | |||
| uint32_t receiveRestartAttemptCount; | |||
| uint32_t receiveRestartFailureCount; | |||
| uint32_t lastUartErrorCode; | |||
| uint16_t rxAssemblyLength; | |||
| uint16_t rxFrameLength; | |||
| uint8_t lastReceiveStartStatus; | |||
| uint8_t initialized; | |||
| uint8_t hasReceivedValidFrame; | |||
| uint8_t connected; | |||
| uint8_t txBusy; | |||
| uint8_t rxFrameReady; | |||
| uint8_t rxAssemblyInvalid; | |||
| } MODBUS_SLAVE_RUNTIME_DIAGNOSTICS; | |||
| /** @brief Modbus 从站通信统计数据 */ | |||
| extern volatile MODBUS_SLAVE_STATS ModbusSlaveStatistics; | |||
| /** | |||
| @@ -140,6 +183,17 @@ uint8_t ModbusSlaveGetCoil(uint16_t address, uint8_t *state); | |||
| * @retval 0 尚未收到有效请求或连接已经超时 | |||
| */ | |||
| uint8_t ModbusSlaveIsConnected(uint32_t timeoutMs); | |||
| /** | |||
| * @brief Copy all RTU diagnostics with tear-free individual scalar fields. | |||
| * @param[in] timeoutMs Age used to calculate the connected flag. | |||
| * @param[out] diagnostics Destination for the complete snapshot. | |||
| * @retval 1 Snapshot returned. | |||
| * @retval 0 diagnostics is NULL. | |||
| */ | |||
| uint8_t ModbusSlaveGetRuntimeDiagnostics( | |||
| uint32_t timeoutMs, | |||
| MODBUS_SLAVE_RUNTIME_DIAGNOSTICS *diagnostics); | |||
| void ModbusRetainedRegistersLoad(void); | |||
| void ModbusRetainedRegistersPoll(void); | |||
| @@ -12,6 +12,11 @@ | |||
| #define MODBUS_DATA_CCM_WORD_COUNT (29999UL) | |||
| #define MODBUS_DATA_HD_SRAM_OFFSET (10000UL) | |||
| #define MODBUS_DATA_LINEAR_CCM_BASE (40000UL) | |||
| #if (MODBUS_DATA_X_BIT_COUNT != MODBUS_DATA_M_BIT_COUNT) \ | |||
| || (MODBUS_DATA_X_BIT_COUNT != MODBUS_DATA_HM_BIT_COUNT) | |||
| #error "Packed X/M/HM images require equal configured capacities" | |||
| #endif | |||
| #define MODBUS_DATA_BIT_BYTES ((MODBUS_DATA_X_BIT_COUNT + 7UL) / 8UL) | |||
| static uint16_t ModbusDataSram[MODBUS_DATA_SRAM_WORD_COUNT]; | |||
| @@ -22,6 +27,8 @@ __root | |||
| #endif | |||
| static uint16_t ModbusDataCcm[MODBUS_DATA_CCM_WORD_COUNT]; | |||
| static uint8_t ModbusBitImages[3U][MODBUS_DATA_BIT_BYTES]; | |||
| static volatile uint32_t ModbusDataWriteSequence; | |||
| static volatile uint32_t ModbusDataWriteFirstAddress; | |||
| static volatile uint32_t ModbusDataWriteWordCount; | |||
| @@ -347,3 +354,83 @@ uint32_t ModbusDataGetWriteSequence(void) | |||
| { | |||
| return ModbusDataWriteSequence; | |||
| } | |||
| static uint32_t ModbusDataBitCapacity(MODBUS_BIT_DEVICE device) | |||
| { | |||
| switch (device) | |||
| { | |||
| case MODBUS_BIT_DEVICE_X: | |||
| return MODBUS_DATA_X_BIT_COUNT; | |||
| case MODBUS_BIT_DEVICE_M: | |||
| return MODBUS_DATA_M_BIT_COUNT; | |||
| case MODBUS_BIT_DEVICE_HM: | |||
| return MODBUS_DATA_HM_BIT_COUNT; | |||
| default: | |||
| return 0UL; | |||
| } | |||
| } | |||
| uint8_t ModbusDataValidateBits(MODBUS_BIT_DEVICE device, | |||
| uint32_t firstAddress, | |||
| uint32_t bitCount) | |||
| { | |||
| uint32_t capacity = ModbusDataBitCapacity(device); | |||
| if ((capacity == 0UL) || (bitCount == 0UL) | |||
| || (firstAddress >= capacity)) | |||
| { | |||
| return 0U; | |||
| } | |||
| return (bitCount <= (capacity - firstAddress)) ? 1U : 0U; | |||
| } | |||
| uint8_t ModbusDataReadBit(MODBUS_BIT_DEVICE device, | |||
| uint32_t address, | |||
| uint8_t *value) | |||
| { | |||
| uint8_t mask; | |||
| if ((value == NULL) | |||
| || (ModbusDataValidateBits(device, address, 1UL) == 0U)) | |||
| { | |||
| return 0U; | |||
| } | |||
| mask = (uint8_t)(1U << (address & 7UL)); | |||
| MODBUS_DATA_BARRIER(); | |||
| *value = ((ModbusBitImages[(uint32_t)device][address >> 3U] & mask) != 0U) | |||
| ? 1U | |||
| : 0U; | |||
| MODBUS_DATA_BARRIER(); | |||
| return 1U; | |||
| } | |||
| uint8_t ModbusDataWriteBit(MODBUS_BIT_DEVICE device, | |||
| uint32_t address, | |||
| uint8_t value) | |||
| { | |||
| uint8_t *byte; | |||
| uint8_t mask; | |||
| uint32_t interruptState; | |||
| if (ModbusDataValidateBits(device, address, 1UL) == 0U) | |||
| { | |||
| return 0U; | |||
| } | |||
| byte = &ModbusBitImages[(uint32_t)device][address >> 3U]; | |||
| mask = (uint8_t)(1U << (address & 7UL)); | |||
| interruptState = ModbusDataEnterShortCritical(); | |||
| if (value != 0U) | |||
| { | |||
| *byte |= mask; | |||
| } | |||
| else | |||
| { | |||
| *byte &= (uint8_t)(~mask); | |||
| } | |||
| MODBUS_DATA_BARRIER(); | |||
| ModbusDataExitShortCritical(interruptState); | |||
| return 1U; | |||
| } | |||
| @@ -51,12 +51,16 @@ static uint32_t ModbusRtuT35Cycles; // T3.5对应的CPU周期数 | |||
| static uint32_t ModbusRtuCharCycles; // 一个UART字符对应的CPU周期数 | |||
| static volatile uint32_t ModbusLastValidFrameTick; | |||
| static volatile uint8_t ModbusHasReceivedValidFrame; | |||
| static volatile uint32_t ModbusReceiveRestartAttemptCount; | |||
| static volatile uint32_t ModbusReceiveRestartFailureCount; | |||
| static volatile uint32_t ModbusNextReceiveRetryTick; | |||
| static volatile uint32_t ModbusLastUartErrorCode; | |||
| static volatile uint8_t ModbusLastReceiveStartStatus = (uint8_t)HAL_ERROR; | |||
| static volatile MODBUS_BACKUP_DATA *ModbusBackupData = | |||
| (volatile MODBUS_BACKUP_DATA *)BKPSRAM_BASE; | |||
| /* Word data is owned by modbus_data_store.c. This file keeps only the | |||
| * protocol-facing coil space and RTU buffers. */ | |||
| static uint8_t ModbusCoils[(MODBUS_MAP_ITEM_COUNT + 7U) / 8U]; | |||
| /* Word and bit images are owned by modbus_data_store.c. Standard Modbus | |||
| * coils are the protocol view of the PLC M device space. */ | |||
| volatile MODBUS_SLAVE_STATS ModbusSlaveStatistics; | |||
| @@ -136,9 +140,10 @@ static uint8_t ModbusAddressRangeIsValid(uint16_t start, uint16_t quantity) | |||
| */ | |||
| static uint8_t ModbusCoilGetUnchecked(uint16_t address) | |||
| { | |||
| uint8_t mask = (uint8_t)(1U << (address & 0x0007U)); | |||
| uint8_t value = 0U; | |||
| return ((ModbusCoils[address >> 3U] & mask) != 0U) ? 1U : 0U; | |||
| (void)ModbusDataReadBit(MODBUS_BIT_DEVICE_M, address, &value); | |||
| return value; | |||
| } | |||
| /** | |||
| @@ -148,16 +153,7 @@ static uint8_t ModbusCoilGetUnchecked(uint16_t address) | |||
| */ | |||
| static void ModbusCoilSetUnchecked(uint16_t address, uint8_t state) | |||
| { | |||
| uint8_t mask = (uint8_t)(1U << (address & 0x0007U)); | |||
| if (state != 0U) | |||
| { | |||
| ModbusCoils[address >> 3U] |= mask; | |||
| } | |||
| else | |||
| { | |||
| ModbusCoils[address >> 3U] &= (uint8_t)(~mask); | |||
| } | |||
| (void)ModbusDataWriteBit(MODBUS_BIT_DEVICE_M, address, state); | |||
| } | |||
| /** | |||
| @@ -256,9 +252,10 @@ static void ModbusTryFinalizeReceive(void) | |||
| /* 静默达到T3.5,当前RTU帧结束,发送响应前停止接收DMA */ | |||
| (void)HAL_UART_AbortReceive(ModbusUart); | |||
| __disable_irq(); | |||
| /* HAL_UART_AbortReceive has already stopped DMA/IDLE callbacks. The | |||
| * finalize routine may copy a full RTU ADU, so it must not globally mask | |||
| * the 100 kHz motion interrupts around that memcpy. */ | |||
| ModbusRxAssemblyFinalize(); | |||
| __enable_irq(); | |||
| /* 无效帧被丢弃后,重新启动DMA接收。 */ | |||
| if (ModbusRxFrameReady == 0U) | |||
| { | |||
| @@ -278,14 +275,37 @@ static HAL_StatusTypeDef ModbusStartReceive(void) | |||
| { | |||
| HAL_StatusTypeDef status; | |||
| ModbusReceiveRestartAttemptCount++; | |||
| if (ModbusUart == NULL) | |||
| { | |||
| status = HAL_ERROR; | |||
| ModbusReceiveRestartFailureCount++; | |||
| ModbusLastReceiveStartStatus = (uint8_t)status; | |||
| return status; | |||
| } | |||
| if (ModbusTxBusy != 0U) | |||
| { | |||
| return HAL_BUSY; | |||
| status = HAL_BUSY; | |||
| ModbusReceiveRestartFailureCount++; | |||
| ModbusLastReceiveStartStatus = (uint8_t)status; | |||
| return status; | |||
| } | |||
| status = HAL_UARTEx_ReceiveToIdle_DMA(ModbusUart, ModbusRxDmaBuffer, | |||
| sizeof(ModbusRxDmaBuffer)); | |||
| /* A duplicate start request can race with an already healthy DMA receiver. | |||
| * Treat that specific HAL_BUSY case as operational, not as a recovery | |||
| * failure; all other HAL_BUSY/HAL_ERROR results remain visible. */ | |||
| if ((status == HAL_BUSY) | |||
| && (ModbusUart->RxState == HAL_UART_STATE_BUSY_RX) | |||
| && ((ModbusUart->Instance->CR3 & USART_CR3_DMAR) != 0U)) | |||
| { | |||
| status = HAL_OK; | |||
| } | |||
| if ((status == HAL_OK) && (ModbusUart->hdmarx != NULL)) | |||
| { | |||
| /* | |||
| @@ -295,6 +315,18 @@ static HAL_StatusTypeDef ModbusStartReceive(void) | |||
| __HAL_DMA_DISABLE_IT(ModbusUart->hdmarx, DMA_IT_HT); | |||
| } | |||
| if (status != HAL_OK) | |||
| { | |||
| ModbusReceiveRestartFailureCount++; | |||
| ModbusLastUartErrorCode = ModbusUart->ErrorCode; | |||
| ModbusNextReceiveRetryTick = HAL_GetTick() + 10UL; | |||
| } | |||
| else | |||
| { | |||
| ModbusNextReceiveRetryTick = 0UL; | |||
| } | |||
| ModbusLastReceiveStartStatus = (uint8_t)status; | |||
| return status; | |||
| } | |||
| @@ -329,13 +361,133 @@ static uint16_t ModbusBuildException(uint8_t function, uint8_t exception) | |||
| } | |||
| /** | |||
| * @brief 处理读线圈功能码 0x01 | |||
| * @param[in] request RTU 请求帧 | |||
| * @param[in] requestLength 请求帧长度 | |||
| * @return 待发送响应长度,异常请求返回异常响应长度 | |||
| * @brief Build a bounded, read-only runtime diagnostic response (function 0x47). | |||
| * | |||
| * The request is: function, start word (BE), quantity (BE). Multi-word | |||
| * 32-bit values use low word first, matching the PLSR diagnostic window. | |||
| */ | |||
| static uint16_t ModbusProcessReadCoils(const uint8_t *request, | |||
| uint16_t requestLength) | |||
| static uint16_t ModbusProcessRuntimeDiagnostics(const uint8_t *request, | |||
| uint16_t requestLength) | |||
| { | |||
| MODBUS_SLAVE_RUNTIME_DIAGNOSTICS diagnostics; | |||
| uint16_t words[MODBUS_RUNTIME_DIAGNOSTICS_WORDS]; | |||
| uint32_t flags = 0U; | |||
| uint32_t stats[10]; | |||
| uint16_t start; | |||
| uint16_t quantity; | |||
| uint16_t index; | |||
| uint16_t responseLength; | |||
| if (requestLength != 8U) | |||
| { | |||
| ModbusSlaveStatistics.illegalValueCount++; | |||
| return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); | |||
| } | |||
| start = ModbusGetU16Be(&request[2]); | |||
| quantity = ModbusGetU16Be(&request[4]); | |||
| if ((quantity == 0U) | |||
| || (start >= MODBUS_RUNTIME_DIAGNOSTICS_WORDS) | |||
| || (quantity > (MODBUS_RUNTIME_DIAGNOSTICS_WORDS - start))) | |||
| { | |||
| ModbusSlaveStatistics.illegalAddressCount++; | |||
| return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); | |||
| } | |||
| (void)ModbusSlaveGetRuntimeDiagnostics(MODBUS_CONNECTION_TIMEOUT_MS, | |||
| &diagnostics); | |||
| if (diagnostics.initialized != 0U) | |||
| { | |||
| flags |= MODBUS_RUNTIME_FLAG_INITIALIZED; | |||
| } | |||
| if (diagnostics.hasReceivedValidFrame != 0U) | |||
| { | |||
| flags |= MODBUS_RUNTIME_FLAG_VALID_FRAME_SEEN; | |||
| } | |||
| if (diagnostics.connected != 0U) | |||
| { | |||
| flags |= MODBUS_RUNTIME_FLAG_CONNECTED; | |||
| } | |||
| if (diagnostics.txBusy != 0U) | |||
| { | |||
| flags |= MODBUS_RUNTIME_FLAG_TX_BUSY; | |||
| } | |||
| if (diagnostics.rxFrameReady != 0U) | |||
| { | |||
| flags |= MODBUS_RUNTIME_FLAG_RX_FRAME_READY; | |||
| } | |||
| if (diagnostics.rxAssemblyInvalid != 0U) | |||
| { | |||
| flags |= MODBUS_RUNTIME_FLAG_RX_ASSEMBLY_INVALID; | |||
| } | |||
| if (diagnostics.lastReceiveStartStatus == (uint8_t)HAL_OK) | |||
| { | |||
| flags |= MODBUS_RUNTIME_FLAG_RX_RESTART_OK; | |||
| } | |||
| words[0] = MODBUS_RUNTIME_DIAGNOSTICS_SIGNATURE; | |||
| words[1] = MODBUS_RUNTIME_DIAGNOSTICS_VERSION; | |||
| words[2] = MODBUS_RUNTIME_DIAGNOSTICS_WORDS; | |||
| words[3] = (uint16_t)flags; | |||
| words[4] = (uint16_t)diagnostics.currentTick; | |||
| words[5] = (uint16_t)(diagnostics.currentTick >> 16U); | |||
| words[6] = (uint16_t)diagnostics.lastValidFrameTick; | |||
| words[7] = (uint16_t)(diagnostics.lastValidFrameTick >> 16U); | |||
| words[8] = (uint16_t)diagnostics.lastInterFrameGapCycles; | |||
| words[9] = (uint16_t)(diagnostics.lastInterFrameGapCycles >> 16U); | |||
| words[10] = (uint16_t)diagnostics.receiveRestartAttemptCount; | |||
| words[11] = (uint16_t)(diagnostics.receiveRestartAttemptCount >> 16U); | |||
| words[12] = (uint16_t)diagnostics.receiveRestartFailureCount; | |||
| words[13] = (uint16_t)(diagnostics.receiveRestartFailureCount >> 16U); | |||
| words[14] = (uint16_t)diagnostics.lastUartErrorCode; | |||
| words[15] = (uint16_t)(diagnostics.lastUartErrorCode >> 16U); | |||
| words[16] = diagnostics.lastReceiveStartStatus; | |||
| words[17] = diagnostics.rxAssemblyLength; | |||
| words[18] = diagnostics.rxFrameLength; | |||
| words[19] = MODBUS_CONNECTION_TIMEOUT_MS; | |||
| stats[0] = diagnostics.statistics.rxEventCount; | |||
| stats[1] = diagnostics.statistics.validFrameCount; | |||
| stats[2] = diagnostics.statistics.txFrameCount; | |||
| stats[3] = diagnostics.statistics.crcErrorCount; | |||
| stats[4] = diagnostics.statistics.ignoredAddressCount; | |||
| stats[5] = diagnostics.statistics.illegalFunctionCount; | |||
| stats[6] = diagnostics.statistics.illegalAddressCount; | |||
| stats[7] = diagnostics.statistics.illegalValueCount; | |||
| stats[8] = diagnostics.statistics.droppedFrameCount; | |||
| stats[9] = diagnostics.statistics.uartErrorCount; | |||
| for (index = 0U; index < 10U; index++) | |||
| { | |||
| words[20U + (index * 2U)] = (uint16_t)stats[index]; | |||
| words[21U + (index * 2U)] = (uint16_t)(stats[index] >> 16U); | |||
| } | |||
| ModbusTxFrame[0] = ModbusSlaveAddress; | |||
| ModbusTxFrame[1] = MODBUS_RUNTIME_DIAGNOSTICS_FUNCTION; | |||
| ModbusTxFrame[2] = (uint8_t)(quantity * 2U); | |||
| for (index = 0U; index < quantity; index++) | |||
| { | |||
| uint16_t value = words[start + index]; | |||
| ModbusTxFrame[3U + (index * 2U)] = (uint8_t)(value >> 8U); | |||
| ModbusTxFrame[4U + (index * 2U)] = (uint8_t)value; | |||
| } | |||
| responseLength = (uint16_t)(3U + (quantity * 2U)); | |||
| ModbusAppendCrc(ModbusTxFrame, responseLength); | |||
| return (uint16_t)(responseLength + 2U); | |||
| } | |||
| /** | |||
| * @brief Process read coils (0x01) or discrete inputs (0x02). | |||
| * @param[in] request RTU request frame. | |||
| * @param[in] requestLength Request frame length. | |||
| * @param[in] functionCode Function code echoed in the response. | |||
| * @param[in] device Backing PLC bit-device image. | |||
| * @return Response ADU length, including an exception response when invalid. | |||
| */ | |||
| static uint16_t ModbusProcessReadBits(const uint8_t *request, | |||
| uint16_t requestLength, | |||
| uint8_t functionCode, | |||
| MODBUS_BIT_DEVICE device) | |||
| { | |||
| uint16_t start; | |||
| @@ -367,13 +519,18 @@ static uint16_t ModbusProcessReadCoils(const uint8_t *request, | |||
| byteCount = (uint8_t)((quantity + 7U) / 8U); | |||
| ModbusTxFrame[0] = ModbusSlaveAddress; | |||
| ModbusTxFrame[1] = 0X01; | |||
| ModbusTxFrame[1] = functionCode; | |||
| ModbusTxFrame[2] = byteCount; | |||
| (void)memset(&ModbusTxFrame[3], 0, byteCount); | |||
| for (index = 0U; index < quantity; index++) | |||
| { | |||
| if (ModbusCoilGetUnchecked((uint16_t)(start + index)) != 0U) | |||
| uint8_t value = 0U; | |||
| if ((ModbusDataReadBit(device, | |||
| (uint32_t)start + index, | |||
| &value) != 0U) | |||
| && (value != 0U)) | |||
| { | |||
| ModbusTxFrame[3U + (index >> 3U)] |= | |||
| (uint8_t)(1U << (index & 0x0007U)); | |||
| @@ -811,7 +968,18 @@ static uint16_t ModbusProcessRequest(const uint8_t *request, | |||
| // 广播请求不允许读取,从站不作响应 | |||
| return (isBroadcast != 0U) | |||
| ? 0U | |||
| : ModbusProcessReadCoils(request, requestLength); | |||
| : ModbusProcessReadBits(request, | |||
| requestLength, | |||
| 0X01U, | |||
| MODBUS_BIT_DEVICE_M); | |||
| case 0X02U: /* Read discrete inputs: protocol view of PLC X. */ | |||
| return (isBroadcast != 0U) | |||
| ? 0U | |||
| : ModbusProcessReadBits(request, | |||
| requestLength, | |||
| 0X02U, | |||
| MODBUS_BIT_DEVICE_X); | |||
| case 0X03U: // 读保持寄存器 | |||
| return (isBroadcast != 0U) | |||
| @@ -833,6 +1001,12 @@ static uint16_t ModbusProcessRequest(const uint8_t *request, | |||
| return ModbusProcessWriteMultipleRegisters(request, requestLength, | |||
| isBroadcast); | |||
| case MODBUS_RUNTIME_DIAGNOSTICS_FUNCTION: | |||
| return (isBroadcast != 0U) | |||
| ? 0U | |||
| : ModbusProcessRuntimeDiagnostics(request, | |||
| requestLength); | |||
| case 0x48U: // 读大地址保持寄存器 | |||
| return (isBroadcast != 0U) | |||
| ? 0U | |||
| @@ -851,6 +1025,10 @@ HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart, | |||
| uint8_t slaveAddress) | |||
| { | |||
| if ((huart == NULL) || (slaveAddress == 0U) || (slaveAddress > 247U)) | |||
| { | |||
| return HAL_ERROR; | |||
| } | |||
| ModbusUart = huart; | |||
| ModbusSlaveAddress = slaveAddress; | |||
| @@ -859,6 +1037,7 @@ HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart, | |||
| ModbusRxAssemblyInvalid = 0U; | |||
| ModbusRxFrameReady = 0U; | |||
| ModbusTxBusy = 0U; | |||
| ModbusNextReceiveRetryTick = 0UL; | |||
| ModbusRtuTimingInit(huart->Init.BaudRate); | |||
| return ModbusStartReceive(); | |||
| @@ -868,6 +1047,19 @@ void ModbusSlavePoll(void) | |||
| { | |||
| uint16_t responseLength; | |||
| /* Recover from a failed DMA rearm even when no later UART callback occurs. | |||
| * Limit retries to 100 Hz so a persistent HAL fault cannot monopolize the | |||
| * 1 ms application task. */ | |||
| if ((ModbusUart != NULL) && (ModbusTxBusy == 0U) | |||
| && (ModbusRxFrameReady == 0U) | |||
| && ((ModbusUart->RxState != HAL_UART_STATE_BUSY_RX) | |||
| || ((ModbusUart->Instance->CR3 & USART_CR3_DMAR) == 0U)) | |||
| && ((ModbusNextReceiveRetryTick == 0UL) | |||
| || ((int32_t)(HAL_GetTick() - ModbusNextReceiveRetryTick) >= 0))) | |||
| { | |||
| (void)ModbusStartReceive(); | |||
| } | |||
| /* 检查末字节后的静默时间是否已经达到T3.5 */ | |||
| ModbusTryFinalizeReceive(); | |||
| if ((ModbusRxFrameReady == 0U) || (ModbusTxBusy != 0U)) | |||
| @@ -889,6 +1081,7 @@ void ModbusSlavePoll(void) | |||
| { | |||
| ModbusTxBusy = 0U; | |||
| ModbusSlaveStatistics.uartErrorCount++; | |||
| ModbusLastUartErrorCode = ModbusUart->ErrorCode; | |||
| (void)ModbusStartReceive(); // 重启DMA接收 | |||
| } | |||
| } | |||
| @@ -1000,6 +1193,7 @@ void ModbusSlaveOnUartError(UART_HandleTypeDef *huart) | |||
| } | |||
| ModbusSlaveStatistics.uartErrorCount++; | |||
| ModbusLastUartErrorCode = huart->ErrorCode; | |||
| ModbusTxBusy = 0U; | |||
| ModbusRxAssemblyLength = 0U; | |||
| ModbusRxAssemblyInvalid = 0U; | |||
| @@ -1059,6 +1253,65 @@ uint8_t ModbusSlaveIsConnected(uint32_t timeoutMs) | |||
| return ((HAL_GetTick() - ModbusLastValidFrameTick) <= timeoutMs) ? 1U : 0U; | |||
| } | |||
| uint8_t ModbusSlaveGetRuntimeDiagnostics( | |||
| uint32_t timeoutMs, | |||
| MODBUS_SLAVE_RUNTIME_DIAGNOSTICS *diagnostics) | |||
| { | |||
| if (diagnostics == NULL) | |||
| { | |||
| return 0U; | |||
| } | |||
| /* Every source field is naturally aligned and 32-bit-or-smaller on the | |||
| * Cortex-M4. A diagnostic snapshot may span adjacent frame events, but | |||
| * must never mask the 100 kHz motion/AB fast-gate interrupts. */ | |||
| __DMB(); | |||
| diagnostics->statistics.rxEventCount = | |||
| ModbusSlaveStatistics.rxEventCount; | |||
| diagnostics->statistics.validFrameCount = | |||
| ModbusSlaveStatistics.validFrameCount; | |||
| diagnostics->statistics.txFrameCount = | |||
| ModbusSlaveStatistics.txFrameCount; | |||
| diagnostics->statistics.crcErrorCount = | |||
| ModbusSlaveStatistics.crcErrorCount; | |||
| diagnostics->statistics.ignoredAddressCount = | |||
| ModbusSlaveStatistics.ignoredAddressCount; | |||
| diagnostics->statistics.illegalFunctionCount = | |||
| ModbusSlaveStatistics.illegalFunctionCount; | |||
| diagnostics->statistics.illegalAddressCount = | |||
| ModbusSlaveStatistics.illegalAddressCount; | |||
| diagnostics->statistics.illegalValueCount = | |||
| ModbusSlaveStatistics.illegalValueCount; | |||
| diagnostics->statistics.droppedFrameCount = | |||
| ModbusSlaveStatistics.droppedFrameCount; | |||
| diagnostics->statistics.uartErrorCount = | |||
| ModbusSlaveStatistics.uartErrorCount; | |||
| diagnostics->currentTick = HAL_GetTick(); | |||
| diagnostics->lastValidFrameTick = ModbusLastValidFrameTick; | |||
| diagnostics->lastInterFrameGapCycles = ModbusLastInterFrameGapCycles; | |||
| diagnostics->receiveRestartAttemptCount = | |||
| ModbusReceiveRestartAttemptCount; | |||
| diagnostics->receiveRestartFailureCount = | |||
| ModbusReceiveRestartFailureCount; | |||
| diagnostics->lastUartErrorCode = ModbusLastUartErrorCode; | |||
| diagnostics->rxAssemblyLength = ModbusRxAssemblyLength; | |||
| diagnostics->rxFrameLength = ModbusRxFrameLength; | |||
| diagnostics->lastReceiveStartStatus = ModbusLastReceiveStartStatus; | |||
| diagnostics->initialized = (ModbusUart != NULL) ? 1U : 0U; | |||
| diagnostics->hasReceivedValidFrame = ModbusHasReceivedValidFrame; | |||
| diagnostics->connected = | |||
| ((ModbusHasReceivedValidFrame != 0U) | |||
| && ((diagnostics->currentTick - ModbusLastValidFrameTick) | |||
| <= timeoutMs)) | |||
| ? 1U | |||
| : 0U; | |||
| diagnostics->txBusy = ModbusTxBusy; | |||
| diagnostics->rxFrameReady = ModbusRxFrameReady; | |||
| diagnostics->rxAssemblyInvalid = ModbusRxAssemblyInvalid; | |||
| __DMB(); | |||
| return 1U; | |||
| } | |||
| // 上电恢复函数 | |||
| void ModbusRetainedRegistersLoad(void) | |||
| { | |||
| @@ -0,0 +1,37 @@ | |||
| #ifndef PLSR_BUILD_CONFIG_H | |||
| #define PLSR_BUILD_CONFIG_H | |||
| /* Validation is the repository default while the remaining board acceptance | |||
| * suite is being executed. A production image must override this symbol to | |||
| * zero in the IAR configuration; the Modbus control plane remains available. */ | |||
| #ifndef PLSR_ENABLE_BOARD_SELF_TEST | |||
| #define PLSR_ENABLE_BOARD_SELF_TEST (1U) | |||
| #endif | |||
| #ifndef PLSR_ENABLE_HW_TRACE | |||
| #define PLSR_ENABLE_HW_TRACE PLSR_ENABLE_BOARD_SELF_TEST | |||
| #endif | |||
| #ifndef PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG | |||
| #define PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG (0U) | |||
| #endif | |||
| #ifndef APP_ENABLE_USB_CDC | |||
| #define APP_ENABLE_USB_CDC (1U) | |||
| #endif | |||
| #define PLSR_BOARD_TEST_NONE (0U) | |||
| #define PLSR_BOARD_TEST_CW_CCW (9U) | |||
| #define PLSR_BOARD_TEST_FAST_REFRESH (10U) | |||
| #define PLSR_BOARD_TEST_DYNAMIC_FREQ (11U) | |||
| #define PLSR_BOARD_TEST_MODBUS_DATA (12U) | |||
| #define PLSR_BOARD_TEST_MODBUS_CONTROL (13U) | |||
| #define PLSR_BOARD_TEST_HW_COUNTER (14U) | |||
| #define PLSR_BOARD_TEST_DUAL_AB (17U) | |||
| #define PLSR_BOARD_TEST_LONG_STRESS (18U) | |||
| #ifndef PLSR_BOARD_TEST_SELECT | |||
| #define PLSR_BOARD_TEST_SELECT PLSR_BOARD_TEST_DUAL_AB | |||
| #endif | |||
| #endif | |||
| @@ -9,11 +9,26 @@ | |||
| extern "C" { | |||
| #endif | |||
| #define PLSR_PROCESS_STAGE_COUNT (6U) | |||
| typedef enum | |||
| { | |||
| PLSR_PROCESS_STAGE_ACCOUNT_PROTECTION = 0, | |||
| PLSR_PROCESS_STAGE_CRITICAL_EVENTS, | |||
| PLSR_PROCESS_STAGE_COMMANDS, | |||
| PLSR_PROCESS_STAGE_NORMAL_EVENTS, | |||
| PLSR_PROCESS_STAGE_TICK_PATH_PROFILE, | |||
| PLSR_PROCESS_STAGE_HSD_CHECKPOINT | |||
| } PLSR_PROCESS_STAGE; | |||
| PLSR_RESULT PlsrInit(void); | |||
| void PlsrTask(void *argument); | |||
| void PlsrProcess(void); | |||
| void PlsrControlTick100us(void); | |||
| void PlsrSetControlTickHook(void (*hook)(void)); | |||
| uint32_t PlsrGetMaxProcessCycles(void); | |||
| uint32_t PlsrGetMaxProcessResponseCycles(void); | |||
| uint32_t PlsrGetMaxProcessStageCycles(uint8_t stage); | |||
| PLSR_RESULT PlsrPostCall(const PLSR_CALL *call); | |||
| /* Side-effect-free COMMIT validation. It parses the complete S0/S1/S2/D | |||
| @@ -49,6 +49,18 @@ uint32_t PlsrHwGetTimerClockHz(uint8_t axis); | |||
| uint32_t PlsrHwGetCurrentFrequencyHz(uint8_t axis); | |||
| int64_t PlsrHwGetEmittedPulses(uint8_t axis); | |||
| uint8_t PlsrHwIsAbStartupPriming(uint8_t axis); | |||
| uint8_t PlsrHwUsesHardwareCounter(uint8_t axis); | |||
| uint32_t PlsrHwGetMaxOutputIsrCycles(void); | |||
| uint32_t PlsrHwGetMaxCounterIsrCycles(void); | |||
| uint32_t PlsrHwGetMaxControlIsrCycles(void); | |||
| /* Fast gate from the final AB 00 boundary; budget is one 100kHz quarter | |||
| * period (420 cycles at 168MHz). */ | |||
| uint32_t PlsrHwGetMaxAbGateCycles(void); | |||
| /* Atomically sample the free-running CPU counter and the accumulated time | |||
| * spent inside PLSR timer ISRs. PlsrProcess uses the pair to distinguish its | |||
| * own CPU cost from wall-clock response time under high-rate preemption. */ | |||
| void PlsrHwGetCycleSnapshot(uint32_t *cycleCount, | |||
| uint64_t *plsrIsrCycles); | |||
| /* 每 1ms tick 推进 HAL 状态机(DIR 延时等)。 */ | |||
| void PlsrHwTick(uint8_t axis); | |||
| @@ -73,8 +85,12 @@ uint8_t PlsrHwTestGetAbPhaseA(uint8_t axis); | |||
| uint8_t PlsrHwTestGetAbPhaseB(uint8_t axis); | |||
| uint8_t PlsrHwTestGetAbQuarter(uint8_t axis); | |||
| void PlsrHwTestTriggerUpdate(uint8_t axis); | |||
| void PlsrHwTestTriggerUpdateAndCompare(uint8_t axis); | |||
| void PlsrHwTestTriggerCompare(uint8_t axis); | |||
| void PlsrHwTestAdvanceAbQuarter(uint8_t axis); | |||
| uint32_t PlsrHwTestGetAbFullGateCount(void); | |||
| void PlsrHwTestSignalDualAbFinalBoundary(uint8_t firstAxis); | |||
| void PlsrHwTestSignalDualAbStaggeredFinalBoundary(uint8_t firstAxis); | |||
| #endif | |||
| #ifdef __cplusplus | |||
| @@ -9,18 +9,35 @@ extern "C" { | |||
| #endif | |||
| #define PLSR_MODBUS_PROTOCOL_VERSION (0x0100U) | |||
| #define PLSR_MODBUS_WINDOW_WORDS (256UL) | |||
| #define PLSR_MODBUS_WINDOW_WORDS (338UL) | |||
| #define PLSR_MODBUS_CALL_REQUEST_OFFSET (8UL) | |||
| #define PLSR_MODBUS_CALL_RESPONSE_OFFSET (24UL) | |||
| #define PLSR_MODBUS_COMMAND_REQUEST_OFFSET (40UL) | |||
| #define PLSR_MODBUS_COMMAND_RESPONSE_OFFSET (48UL) | |||
| #define PLSR_MODBUS_PERFORMANCE_OFFSET (56UL) | |||
| #define PLSR_MODBUS_PERFORMANCE_WORDS (8UL) | |||
| #define PLSR_MODBUS_AXIS_STATUS_OFFSET (64UL) | |||
| #define PLSR_MODBUS_AXIS_STATUS_WORDS (48UL) | |||
| #define PLSR_MODBUS_STAGE_PERFORMANCE_OFFSET (256UL) | |||
| #define PLSR_MODBUS_STAGE_PERFORMANCE_WORDS (12UL) | |||
| #define PLSR_MODBUS_AB_GATE_PERFORMANCE_OFFSET (268UL) | |||
| #define PLSR_MODBUS_AB_GATE_PERFORMANCE_WORDS (2UL) | |||
| #define PLSR_MODBUS_PERSISTENCE_OFFSET (270UL) | |||
| #define PLSR_MODBUS_PERSISTENCE_WORDS (30UL) | |||
| #define PLSR_MODBUS_PERSISTENCE_REQUEST_OFFSET (300UL) | |||
| #define PLSR_MODBUS_PERSISTENCE_REQUEST_WORDS (8UL) | |||
| #define PLSR_MODBUS_PERSISTENCE_RESPONSE_OFFSET (308UL) | |||
| #define PLSR_MODBUS_PERSISTENCE_RESPONSE_WORDS (8UL) | |||
| #define PLSR_MODBUS_USB_DIAGNOSTICS_OFFSET (316UL) | |||
| #define PLSR_MODBUS_USB_DIAGNOSTICS_WORDS (22UL) | |||
| #define PLSR_MODBUS_CALL_NONE (0U) | |||
| #define PLSR_MODBUS_CALL_COMMIT (1U) | |||
| #define PLSR_MODBUS_CALL_START (2U) | |||
| #define PLSR_MODBUS_PERFORMANCE_VERSION (7U) | |||
| #define PLSR_MODBUS_PERSISTENCE_VERSION (1U) | |||
| #define PLSR_MODBUS_USB_DIAGNOSTICS_VERSION (1U) | |||
| PLSR_RESULT PlsrModbusControlInit(uint16_t baseAddress); | |||
| void PlsrModbusControlPoll(void); | |||
| @@ -7,8 +7,9 @@ | |||
| extern "C" { | |||
| #endif | |||
| /* Build a PLSR data source backed by the Modbus register store. D, HD and FD | |||
| * are separate logical spaces; standard Modbus holding registers expose D. */ | |||
| /* Build a PLSR data source backed by the shared PLC data store. D/HD/FD and | |||
| * X/M/HM are separate logical spaces; standard Modbus holding registers expose | |||
| * D and standard Modbus coils expose M. */ | |||
| void PlsrModbusDataSourceInit(PLSR_DATA_SOURCE *source); | |||
| #ifdef __cplusplus | |||
| @@ -36,6 +36,35 @@ typedef enum | |||
| PLSR_PERSISTENCE_PROGRAM_FAILED | |||
| } PLSR_PERSISTENCE_RESULT; | |||
| /* Cached persistence health snapshot. Slot CRCs are evaluated only by | |||
| * load/save/erase/diagnostic operations; reading this structure never scans | |||
| * Backup SRAM or Flash and is therefore safe in the 1 ms Modbus poll path. */ | |||
| typedef struct | |||
| { | |||
| uint8_t hsdValidMask; | |||
| uint8_t sfdValidMask; | |||
| uint8_t hsdNewestMask; | |||
| uint8_t sfdNewestMask; | |||
| uint8_t destructiveDiagnosticEnabled; | |||
| PLSR_PERSISTENCE_RESULT lastHsdLoadResult; | |||
| PLSR_PERSISTENCE_RESULT lastSfdLoadResult; | |||
| PLSR_PERSISTENCE_RESULT lastHsdSaveResult; | |||
| PLSR_PERSISTENCE_RESULT lastSfdSaveResult; | |||
| PLSR_PERSISTENCE_RESULT lastSfdEraseResult; | |||
| uint32_t hsdGeneration[2]; | |||
| uint32_t sfdGeneration[2]; | |||
| uint32_t hsdSaveCount; | |||
| uint32_t sfdSaveCount; | |||
| uint32_t selectedHsdCrc32; | |||
| uint32_t selectedSfdCrc32; | |||
| } PLSR_PERSISTENCE_DIAGNOSTICS; | |||
| typedef enum | |||
| { | |||
| PLSR_PERSISTENCE_DIAG_TARGET_HSD = 1, | |||
| PLSR_PERSISTENCE_DIAG_TARGET_SFD = 2 | |||
| } PLSR_PERSISTENCE_DIAG_TARGET; | |||
| PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadHsd(PLSR_HSD_DATA *data); | |||
| PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveHsd(const PLSR_HSD_DATA *data); | |||
| void PlsrPersistenceResetHsd(void); | |||
| @@ -44,6 +73,16 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadSfd(PLSR_SFD_DATA *data); | |||
| PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data); | |||
| PLSR_PERSISTENCE_RESULT PlsrPersistenceEraseSfd(void); | |||
| void PlsrPersistenceGetDiagnostics(PLSR_PERSISTENCE_DIAGNOSTICS *diagnostics); | |||
| /* Deliberately unavailable in normal builds. A validation build may enable | |||
| * it with PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG=1; the Modbus control layer | |||
| * additionally requires a double-magic, sequence/inverse handshake and all | |||
| * motion axes idle. This function only invalidates the newest committed slot | |||
| * and never accepts a memory address. */ | |||
| PLSR_PERSISTENCE_RESULT PlsrPersistenceDiagnosticInvalidateNewest( | |||
| PLSR_PERSISTENCE_DIAG_TARGET target); | |||
| #ifdef PLSR_HOST_TEST | |||
| typedef enum | |||
| { | |||
| @@ -58,6 +97,8 @@ void PlsrPersistenceTestResetStorage(void); | |||
| void PlsrPersistenceTestCorruptNewestHsd(void); | |||
| void PlsrPersistenceTestCorruptNewestSfd(void); | |||
| void PlsrPersistenceTestSetSfdFault(PLSR_TEST_SFD_FAULT fault); | |||
| uint32_t PlsrPersistenceTestGetHsdSaveCount(void); | |||
| uint32_t PlsrPersistenceTestCrc32(const uint8_t *data, uint32_t length); | |||
| #endif | |||
| #ifdef __cplusplus | |||
| @@ -38,6 +38,10 @@ PLSR_RESULT PlsrDynamicFrequencySelfTestQueue(void); | |||
| PLSR_RESULT PlsrModbusDataSelfTestQueue(void); | |||
| /* P13 prepares deterministic SFD K1 data; motion is commanded via Modbus. */ | |||
| PLSR_RESULT PlsrModbusControlSelfTestPrepare(void); | |||
| /* P14/P15/P17: P/D counter, soft-limit and dual-AB Modbus stress setup. */ | |||
| PLSR_RESULT PlsrHardwareCounterSelfTestPrepare(void); | |||
| /* P18: K4 four-axis 100kHz P/D long-stress setup with soft limits disabled. */ | |||
| PLSR_RESULT PlsrLongStressSelfTestPrepare(void); | |||
| void PlsrSelfTestControlTick100us(void); | |||
| /* P11 board-test control. Write this signed dword from IAR Watch while the | |||
| @@ -179,6 +179,7 @@ typedef struct | |||
| uint8_t wait; | |||
| uint8_t directionPositive; | |||
| uint8_t highResourceMask; | |||
| uint8_t hardwareCounter; | |||
| uint8_t directionPoint; | |||
| uint8_t positionValid; | |||
| uint8_t jobValid; | |||
| @@ -85,6 +85,12 @@ static uint8_t PlsrInitialized; | |||
| static PLSR_JOB_SNAPSHOT PlsrJobScratch; | |||
| static PLSR_JOB_SNAPSHOT PlsrValidationScratch; | |||
| static void (* volatile PlsrControlTickHook)(void); | |||
| static volatile uint32_t PlsrMaxProcessCycles; | |||
| static volatile uint32_t PlsrMaxProcessResponseCycles; | |||
| static volatile uint32_t | |||
| PlsrMaxProcessStageCycles[PLSR_PROCESS_STAGE_COUNT]; | |||
| static uint8_t PlsrDeferHsdCheckpoint; | |||
| static uint8_t PlsrHsdCheckpointPending; | |||
| static void PlsrStopSegmentHardware(uint8_t axis, PLSR_AXIS *axisObject); | |||
| static PLSR_RESULT PlsrStartSegmentHardware(uint8_t axis, | |||
| @@ -94,6 +100,63 @@ static void PlsrAccountHardwarePulses(uint8_t axis, | |||
| static void PlsrPublishSegmentEvent(uint8_t axis, | |||
| PLSR_AXIS *axisObject, | |||
| PLSR_STOP_REASON reason); | |||
| static uint8_t PlsrAnyAxisBusy(void); | |||
| static uint8_t PlsrAllAxesPositionValid(void); | |||
| #ifndef PLSR_HOST_TEST | |||
| static void PlsrUpdateProcessStageMax(uint8_t stage, | |||
| uint32_t started, | |||
| uint64_t startedIsrCycles, | |||
| uint32_t finished, | |||
| uint64_t finishedIsrCycles) | |||
| { | |||
| uint32_t responseCycles = finished - started; | |||
| uint64_t preemptedCycles = finishedIsrCycles - startedIsrCycles; | |||
| uint32_t processCycles = | |||
| (preemptedCycles < (uint64_t)responseCycles) | |||
| ? responseCycles - (uint32_t)preemptedCycles | |||
| : 0UL; | |||
| if ((stage < PLSR_PROCESS_STAGE_COUNT) | |||
| && (processCycles > PlsrMaxProcessStageCycles[stage])) | |||
| { | |||
| PlsrMaxProcessStageCycles[stage] = processCycles; | |||
| } | |||
| } | |||
| #endif | |||
| static void PlsrFlushHsdCheckpoint(void) | |||
| { | |||
| if (PlsrHsdCheckpointPending != 0U) | |||
| { | |||
| (void)PlcDeviceSetHsdCheckpointMeta( | |||
| PlsrAllAxesPositionValid(), | |||
| PlsrAnyAxisBusy()); | |||
| if (PlcDeviceCheckpointHsd() == PLC_DEVICE_OK) | |||
| { | |||
| PlsrHsdCheckpointPending = 0U; | |||
| } | |||
| } | |||
| } | |||
| static void PlsrCheckpointHsd(void) | |||
| { | |||
| PlsrHsdCheckpointPending = 1U; | |||
| if (PlsrDeferHsdCheckpoint == 0U) | |||
| { | |||
| PlsrFlushHsdCheckpoint(); | |||
| } | |||
| } | |||
| static void PlsrCheckpointHsdImmediate(void) | |||
| { | |||
| uint8_t deferHsdCheckpoint = PlsrDeferHsdCheckpoint; | |||
| PlsrHsdCheckpointPending = 1U; | |||
| PlsrDeferHsdCheckpoint = 0U; | |||
| PlsrFlushHsdCheckpoint(); | |||
| PlsrDeferHsdCheckpoint = deferHsdCheckpoint; | |||
| } | |||
| static uint32_t PlsrCoreEnterCritical(void) | |||
| { | |||
| @@ -485,11 +548,15 @@ static PLSR_RESULT PlsrReadLimitInput(const PLSR_JOB_SNAPSHOT *job, | |||
| return PLSR_RESULT_OK; | |||
| } | |||
| static int64_t PlsrGetBrakingDistance(const PLSR_AXIS *axisObject) | |||
| static int64_t PlsrGetBrakingDistance(uint8_t axis, | |||
| const PLSR_AXIS *axisObject) | |||
| { | |||
| uint64_t frequencyHz; | |||
| uint64_t hardwareFrequencyHz; | |||
| uint64_t denominator; | |||
| uint64_t numerator; | |||
| uint64_t brakingPulses; | |||
| uint64_t samplingPulses; | |||
| uint64_t frequencyQ32; | |||
| uint32_t decelSlopeHzPerMs; | |||
| uint32_t interruptState; | |||
| @@ -508,9 +575,30 @@ static int64_t PlsrGetBrakingDistance(const PLSR_AXIS *axisObject) | |||
| return 0; | |||
| } | |||
| frequencyHz = frequencyQ32 >> 32U; | |||
| hardwareFrequencyHz = PlsrHwGetCurrentFrequencyHz(axis); | |||
| if (hardwareFrequencyHz > frequencyHz) | |||
| { | |||
| /* Protection must follow the frequency already present at the output, | |||
| * not an earlier/lower profile value waiting behind timer preload. */ | |||
| frequencyHz = hardwareFrequencyHz; | |||
| } | |||
| numerator = frequencyHz * frequencyHz; | |||
| denominator = UINT64_C(2000) * decelSlopeHzPerMs; | |||
| return (int64_t)((numerator + denominator - 1UL) / denominator); | |||
| brakingPulses = (numerator + denominator - 1UL) / denominator; | |||
| /* The 1ms protection task can observe several new pulses per pass. The | |||
| * inclusive position comparison already covers one of them; reserve only | |||
| * the additional pulses so <=1kHz behavior is unchanged while higher | |||
| * frequencies cannot cross the limit by a complete sampling window. */ | |||
| samplingPulses = (frequencyHz + UINT64_C(999)) / UINT64_C(1000); | |||
| if (samplingPulses > 0UL) | |||
| { | |||
| samplingPulses--; | |||
| } | |||
| if (brakingPulses > (uint64_t)INT64_MAX - samplingPulses) | |||
| { | |||
| return INT64_MAX; | |||
| } | |||
| return (int64_t)(brakingPulses + samplingPulses); | |||
| } | |||
| static PLSR_RESULT PlsrUpdateLimitState(PLSR_AXIS *axisObject, | |||
| @@ -541,7 +629,8 @@ static PLSR_RESULT PlsrUpdateLimitState(PLSR_AXIS *axisObject, | |||
| { | |||
| if (includeBrakingDistance != 0U) | |||
| { | |||
| brakingDistance = PlsrGetBrakingDistance(axisObject); | |||
| brakingDistance = PlsrGetBrakingDistance(job->dAxis, | |||
| axisObject); | |||
| } | |||
| if (axisObject->logicalPosition | |||
| >= job->limits.positiveSoftLimitPulses) | |||
| @@ -800,9 +889,8 @@ PLSR_RESULT PlsrStateTransition(uint8_t axis, | |||
| PlsrPathTerminate(&axisObject->path); | |||
| PlsrStopSegmentHardware(axis, axisObject); | |||
| PlsrResourceRelease(&axisObject->lease); | |||
| /* 运动已结束:记录 lastBusy=0,保证掉电后恢复时位置仍可信。 */ | |||
| (void)PlcDeviceSetHsdCheckpointMeta(axisObject->positionValid, 0U); | |||
| (void)PlcDeviceCheckpointHsd(); | |||
| /* 检查点写入前按全部轴的最终状态统一计算 lastBusy。 */ | |||
| PlsrCheckpointHsd(); | |||
| } | |||
| PlsrPublishAxis(axis); | |||
| return PLSR_RESULT_OK; | |||
| @@ -1108,11 +1196,27 @@ static uint8_t PlsrAnyAxisBusy(void) | |||
| return 0U; | |||
| } | |||
| static uint8_t PlsrAllAxesPositionValid(void) | |||
| { | |||
| uint8_t axis; | |||
| for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) | |||
| { | |||
| if ((PlsrAxes[axis].positionValid == 0U) | |||
| || (PlsrAxes[axis].positionOverflow != 0U)) | |||
| { | |||
| return 0U; | |||
| } | |||
| } | |||
| return 1U; | |||
| } | |||
| static PLSR_RESULT PlsrStartAxis(PLSR_AXIS *axisObject, | |||
| const PLSR_START_REQUEST *start) | |||
| { | |||
| PLSR_RESOURCE_REQUEST resourceRequest; | |||
| PLSR_RESULT result; | |||
| uint8_t axisWasBusy = PlsrAnyAxisBusy(); | |||
| if ((axisObject->state != PLSR_STATE_IDLE) | |||
| && (axisObject->state != PLSR_STATE_COMPLETED) | |||
| @@ -1165,9 +1269,12 @@ static PLSR_RESULT PlsrStartAxis(PLSR_AXIS *axisObject, | |||
| } | |||
| else | |||
| { | |||
| /* 运动开始:掉电恢复时据此判定"断电时在运动中"。 */ | |||
| (void)PlcDeviceSetHsdCheckpointMeta(axisObject->positionValid, 1U); | |||
| (void)PlcDeviceCheckpointHsd(); | |||
| /* 全局空闲到运行的边沿立即落盘,保持原有掉电安全窗口;后续 | |||
| * 并发轴已由同一个全局 lastBusy=1 检查点覆盖。 */ | |||
| if (axisWasBusy == 0U) | |||
| { | |||
| PlsrCheckpointHsdImmediate(); | |||
| } | |||
| } | |||
| return result; | |||
| } | |||
| @@ -1178,6 +1285,7 @@ static PLSR_RESULT PlsrStartCall(PLSR_AXIS *axisObject, | |||
| PLSR_RESOURCE_REQUEST resourceRequest; | |||
| PLSR_PARSE_CONTEXT parseContext; | |||
| PLSR_RESULT result; | |||
| uint8_t axisWasBusy = PlsrAnyAxisBusy(); | |||
| if ((axisObject->state != PLSR_STATE_IDLE) | |||
| && (axisObject->state != PLSR_STATE_COMPLETED) | |||
| @@ -1308,6 +1416,12 @@ static PLSR_RESULT PlsrStartCall(PLSR_AXIS *axisObject, | |||
| } | |||
| else | |||
| { | |||
| /* 先持久化全局 0->1 busy 边沿,再允许硬件输出启动;后续并发 | |||
| * 轴由同一个 lastBusy=1 检查点覆盖。 */ | |||
| if (axisWasBusy == 0U) | |||
| { | |||
| PlsrCheckpointHsdImmediate(); | |||
| } | |||
| /* 启动当前段硬件输出与速度曲线。 */ | |||
| /* 零脉冲跳转链耗尽本轮预算时没有实际运动段,等待下一次 | |||
| * PlsrPathTick 找到非零段后再启动硬件。 */ | |||
| @@ -1346,9 +1460,6 @@ static PLSR_RESULT PlsrStartCall(PLSR_AXIS *axisObject, | |||
| } | |||
| return result; | |||
| } | |||
| /* 运动开始:掉电恢复时据此判定"断电时在运动中"。 */ | |||
| (void)PlcDeviceSetHsdCheckpointMeta(axisObject->positionValid, 1U); | |||
| (void)PlcDeviceCheckpointHsd(); | |||
| } | |||
| return result; | |||
| } | |||
| @@ -1618,8 +1729,7 @@ static PLSR_RESULT PlsrExecuteCommand(const PLSR_COMMAND_SLOT *slot) | |||
| axisObject->positionValid = 1U; | |||
| axisObject->positionOverflow = 0U; | |||
| PlsrPublishPosition(slot->command.axis, axisObject); | |||
| (void)PlcDeviceSetHsdCheckpointMeta(1U, 0U); | |||
| (void)PlcDeviceCheckpointHsd(); | |||
| PlsrCheckpointHsd(); | |||
| result = PLSR_RESULT_OK; | |||
| } | |||
| break; | |||
| @@ -1635,8 +1745,7 @@ static PLSR_RESULT PlsrExecuteCommand(const PLSR_COMMAND_SLOT *slot) | |||
| axisObject->positionValid = 1U; | |||
| axisObject->positionOverflow = 0U; | |||
| PlsrPublishPosition(slot->command.axis, axisObject); | |||
| (void)PlcDeviceSetHsdCheckpointMeta(1U, 0U); | |||
| (void)PlcDeviceCheckpointHsd(); | |||
| PlsrCheckpointHsd(); | |||
| result = PLSR_RESULT_OK; | |||
| } | |||
| break; | |||
| @@ -1649,10 +1758,7 @@ static PLSR_RESULT PlsrExecuteCommand(const PLSR_COMMAND_SLOT *slot) | |||
| else | |||
| { | |||
| axisObject->totalPulses = 0; | |||
| (void)PlcDeviceSetHsdCheckpointMeta( | |||
| axisObject->positionValid, | |||
| 0U); | |||
| (void)PlcDeviceCheckpointHsd(); | |||
| PlsrCheckpointHsd(); | |||
| result = PLSR_RESULT_OK; | |||
| } | |||
| break; | |||
| @@ -2336,6 +2442,13 @@ PLSR_RESULT PlsrInit(void) | |||
| (void)memset(PlsrAxes, 0, sizeof(PlsrAxes)); | |||
| (void)memset(PlsrCommandQueue, 0, sizeof(PlsrCommandQueue)); | |||
| PlsrNextTicket = 0UL; | |||
| PlsrMaxProcessCycles = 0UL; | |||
| PlsrMaxProcessResponseCycles = 0UL; | |||
| (void)memset((void *)PlsrMaxProcessStageCycles, | |||
| 0, | |||
| sizeof(PlsrMaxProcessStageCycles)); | |||
| PlsrDeferHsdCheckpoint = 0U; | |||
| PlsrHsdCheckpointPending = 0U; | |||
| PlsrResourceInit(); | |||
| (void)PlsrHwInit(); | |||
| PlsrControlTickHook = NULL; | |||
| @@ -2526,6 +2639,12 @@ void PlsrProcess(void) | |||
| { | |||
| PLSR_COMMAND_SLOT slot; | |||
| uint32_t events; | |||
| #ifndef PLSR_HOST_TEST | |||
| uint32_t started; | |||
| uint64_t startedIsrCycles; | |||
| uint32_t stageStarted; | |||
| uint64_t stageStartedIsrCycles; | |||
| #endif | |||
| uint8_t processedCommands = 0U; | |||
| uint8_t criticalAxes = 0U; | |||
| uint8_t axis; | |||
| @@ -2534,6 +2653,12 @@ void PlsrProcess(void) | |||
| { | |||
| return; | |||
| } | |||
| #ifndef PLSR_HOST_TEST | |||
| PlsrHwGetCycleSnapshot(&started, &startedIsrCycles); | |||
| stageStarted = started; | |||
| stageStartedIsrCycles = startedIsrCycles; | |||
| #endif | |||
| PlsrDeferHsdCheckpoint = 1U; | |||
| /* 先合并 ISR 已完成的实际脉冲,确保段完成、STOP或新命令不会在 | |||
| * HAL 计数清零前丢失最后一批位置增量。 */ | |||
| @@ -2542,6 +2667,21 @@ void PlsrProcess(void) | |||
| PlsrAccountHardwarePulses(axis, &PlsrAxes[axis]); | |||
| PlsrMonitorAxisProtection(axis); | |||
| } | |||
| #ifndef PLSR_HOST_TEST | |||
| { | |||
| uint32_t finished; | |||
| uint64_t finishedIsrCycles; | |||
| PlsrHwGetCycleSnapshot(&finished, &finishedIsrCycles); | |||
| PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_ACCOUNT_PROTECTION, | |||
| stageStarted, | |||
| stageStartedIsrCycles, | |||
| finished, | |||
| finishedIsrCycles); | |||
| stageStarted = finished; | |||
| stageStartedIsrCycles = finishedIsrCycles; | |||
| } | |||
| #endif | |||
| for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) | |||
| { | |||
| @@ -2552,6 +2692,21 @@ void PlsrProcess(void) | |||
| PlsrProcessCriticalEvents(axis, events); | |||
| } | |||
| } | |||
| #ifndef PLSR_HOST_TEST | |||
| { | |||
| uint32_t finished; | |||
| uint64_t finishedIsrCycles; | |||
| PlsrHwGetCycleSnapshot(&finished, &finishedIsrCycles); | |||
| PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_CRITICAL_EVENTS, | |||
| stageStarted, | |||
| stageStartedIsrCycles, | |||
| finished, | |||
| finishedIsrCycles); | |||
| stageStarted = finished; | |||
| stageStartedIsrCycles = finishedIsrCycles; | |||
| } | |||
| #endif | |||
| /* Apply related multi-axis DIR changes after all commands and segment | |||
| * events, keeping cross-port GPIO writes in one short commit window. */ | |||
| @@ -2575,6 +2730,21 @@ void PlsrProcess(void) | |||
| } | |||
| processedCommands++; | |||
| } | |||
| #ifndef PLSR_HOST_TEST | |||
| { | |||
| uint32_t finished; | |||
| uint64_t finishedIsrCycles; | |||
| PlsrHwGetCycleSnapshot(&finished, &finishedIsrCycles); | |||
| PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_COMMANDS, | |||
| stageStarted, | |||
| stageStartedIsrCycles, | |||
| finished, | |||
| finishedIsrCycles); | |||
| stageStarted = finished; | |||
| stageStartedIsrCycles = finishedIsrCycles; | |||
| } | |||
| #endif | |||
| for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) | |||
| { | |||
| @@ -2588,6 +2758,21 @@ void PlsrProcess(void) | |||
| } | |||
| PlsrHwEndDirectionBatch(); | |||
| #ifndef PLSR_HOST_TEST | |||
| { | |||
| uint32_t finished; | |||
| uint64_t finishedIsrCycles; | |||
| PlsrHwGetCycleSnapshot(&finished, &finishedIsrCycles); | |||
| PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_NORMAL_EVENTS, | |||
| stageStarted, | |||
| stageStartedIsrCycles, | |||
| finished, | |||
| finishedIsrCycles); | |||
| stageStarted = finished; | |||
| stageStartedIsrCycles = finishedIsrCycles; | |||
| } | |||
| #endif | |||
| /* 1ms tick:路径执行器推进(WAIT/ACT 计时、信号/EXT 轮询、跳转链) | |||
| * + 速度曲线推进(P2) + HAL 状态机(DIR 延时)。 */ | |||
| @@ -2597,7 +2782,11 @@ void PlsrProcess(void) | |||
| PLSR_PATH_ACTION action; | |||
| PlsrHwTick(axis); | |||
| PlsrAccountHardwarePulses(axis, axisObject); | |||
| /* Pulses were merged at the beginning of this pass. Merging again | |||
| * here republishes HSD/SD runtime data for the few pulses emitted | |||
| * while PlsrProcess itself was running and nearly doubles the | |||
| * four-axis cost. Those pulses are safely merged at the beginning | |||
| * of the next pass or by the terminal event path. */ | |||
| /* 首次 AB 内部预热周期不属于用户运动,速度曲线也必须冻结; | |||
| * 否则低速起步时会在隐藏周期内提前爬升十余个刷新步。 */ | |||
| if (PlsrHwIsAbStartupPriming(axis) != 0U) | |||
| @@ -2623,6 +2812,68 @@ void PlsrProcess(void) | |||
| PlsrStepProfileAxis(axis); | |||
| } | |||
| } | |||
| #ifndef PLSR_HOST_TEST | |||
| { | |||
| uint32_t tickFinished; | |||
| uint32_t finished; | |||
| uint32_t responseCycles; | |||
| uint32_t processCycles; | |||
| uint64_t tickFinishedIsrCycles; | |||
| uint64_t finishedIsrCycles; | |||
| uint64_t preemptedCycles; | |||
| PlsrHwGetCycleSnapshot(&tickFinished, &tickFinishedIsrCycles); | |||
| PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_TICK_PATH_PROFILE, | |||
| stageStarted, | |||
| stageStartedIsrCycles, | |||
| tickFinished, | |||
| tickFinishedIsrCycles); | |||
| PlsrDeferHsdCheckpoint = 0U; | |||
| PlsrFlushHsdCheckpoint(); | |||
| PlsrHwGetCycleSnapshot(&finished, &finishedIsrCycles); | |||
| PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_HSD_CHECKPOINT, | |||
| tickFinished, | |||
| tickFinishedIsrCycles, | |||
| finished, | |||
| finishedIsrCycles); | |||
| responseCycles = finished - started; | |||
| preemptedCycles = finishedIsrCycles - startedIsrCycles; | |||
| processCycles = (preemptedCycles < (uint64_t)responseCycles) | |||
| ? responseCycles - (uint32_t)preemptedCycles | |||
| : 0UL; | |||
| if (processCycles > PlsrMaxProcessCycles) | |||
| { | |||
| PlsrMaxProcessCycles = processCycles; | |||
| } | |||
| if (responseCycles > PlsrMaxProcessResponseCycles) | |||
| { | |||
| PlsrMaxProcessResponseCycles = responseCycles; | |||
| } | |||
| } | |||
| #else | |||
| PlsrDeferHsdCheckpoint = 0U; | |||
| PlsrFlushHsdCheckpoint(); | |||
| #endif | |||
| } | |||
| uint32_t PlsrGetMaxProcessCycles(void) | |||
| { | |||
| return PlsrMaxProcessCycles; | |||
| } | |||
| uint32_t PlsrGetMaxProcessResponseCycles(void) | |||
| { | |||
| return PlsrMaxProcessResponseCycles; | |||
| } | |||
| uint32_t PlsrGetMaxProcessStageCycles(uint8_t stage) | |||
| { | |||
| if (stage >= PLSR_PROCESS_STAGE_COUNT) | |||
| { | |||
| return 0UL; | |||
| } | |||
| return PlsrMaxProcessStageCycles[stage]; | |||
| } | |||
| void PlsrTask(void *argument) | |||
| @@ -2673,6 +2924,7 @@ PLSR_RESULT PlsrGetStatus(uint8_t axis, PLSR_STATUS *status) | |||
| status->wait = (axisObject->state == PLSR_STATE_WAIT) ? 1U : 0U; | |||
| status->directionPositive = axisObject->directionPositive; | |||
| status->highResourceMask = axisObject->lease.highMask; | |||
| status->hardwareCounter = PlsrHwUsesHardwareCounter(axis); | |||
| status->directionPoint = (axisObject->lease.valid != 0U) | |||
| ? axisObject->lease.directionPoint | |||
| : PLSR_DIRECTION_POINT_NONE; | |||
| @@ -1,12 +1,20 @@ | |||
| #include "plsr_modbus_control.h" | |||
| #include "modbus_data_store.h" | |||
| #include "plc_device.h" | |||
| #include "plsr_address_map.h" | |||
| #include "plsr_core.h" | |||
| #include "plsr_hal_f407.h" | |||
| #include "plsr_job.h" | |||
| #include "plsr_modbus_data.h" | |||
| #include "plsr_persistence.h" | |||
| #include <stddef.h> | |||
| #include <string.h> | |||
| #ifndef PLSR_HOST_TEST | |||
| #include "stm32f4xx.h" | |||
| #include "usbd_cdc_if.h" | |||
| #endif | |||
| #define PLSR_MODBUS_MAGIC_LOW (0x504CU) | |||
| #define PLSR_MODBUS_MAGIC_HIGH (0x5352U) | |||
| #define PLSR_MODBUS_CAPABILITIES (0x0007U) | |||
| @@ -19,6 +27,11 @@ | |||
| #define PLSR_MODBUS_S1_WORDS (4UL) | |||
| #define PLSR_MODBUS_HASH_OFFSET (2166136261UL) | |||
| #define PLSR_MODBUS_HASH_PRIME (16777619UL) | |||
| #define PLSR_MODBUS_PERSIST_MAGIC_A (0xDA7AU) | |||
| #define PLSR_MODBUS_PERSIST_MAGIC_B (0x51F0U) | |||
| #define PLSR_MODBUS_PERSIST_ARM (0xA55AU) | |||
| #define PLSR_MODBUS_PERSIST_INVALIDATE_HSD (1U) | |||
| #define PLSR_MODBUS_PERSIST_INVALIDATE_SFD (2U) | |||
| typedef struct | |||
| { | |||
| @@ -32,6 +45,9 @@ static uint8_t PlsrModbusEnabled; | |||
| static uint32_t PlsrModbusLastCallRequestSequence; | |||
| static uint32_t PlsrModbusLastCommandRequestSequence; | |||
| static uint32_t PlsrModbusStatusGeneration[PLSR_AXIS_COUNT]; | |||
| static uint32_t PlsrModbusPersistenceGeneration; | |||
| static uint32_t PlsrModbusUsbDiagnosticsGeneration; | |||
| static uint32_t PlsrModbusLastPersistenceRequestSequence; | |||
| static PLSR_MODBUS_COMMITTED_CALL PlsrModbusCommitted[PLSR_AXIS_COUNT]; | |||
| static uint16_t PlsrModbusStatusWords[PLSR_AXIS_COUNT] | |||
| [PLSR_MODBUS_AXIS_STATUS_WORDS]; | |||
| @@ -436,6 +452,164 @@ static void PlsrModbusHandleCommandRequest(void) | |||
| result); | |||
| } | |||
| static uint8_t PlsrModbusPersistenceAllAxesIdle(void) | |||
| { | |||
| PLSR_STATUS status; | |||
| uint8_t axis; | |||
| for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) | |||
| { | |||
| if ((PlsrGetStatus(axis, &status) != PLSR_RESULT_OK) | |||
| || (status.busy != 0U) | |||
| || (status.pulseActive != 0U)) | |||
| { | |||
| return 0U; | |||
| } | |||
| } | |||
| return 1U; | |||
| } | |||
| static uint32_t PlsrModbusEnterPersistenceDiagnosticCritical(void) | |||
| { | |||
| #ifdef PLSR_HOST_TEST | |||
| return 0UL; | |||
| #else | |||
| uint32_t interruptState = __get_PRIMASK(); | |||
| __disable_irq(); | |||
| __DMB(); | |||
| return interruptState; | |||
| #endif | |||
| } | |||
| static void PlsrModbusExitPersistenceDiagnosticCritical( | |||
| uint32_t interruptState) | |||
| { | |||
| #ifdef PLSR_HOST_TEST | |||
| (void)interruptState; | |||
| #else | |||
| __DMB(); | |||
| if (interruptState == 0UL) | |||
| { | |||
| __enable_irq(); | |||
| } | |||
| #endif | |||
| } | |||
| static PLSR_RESULT PlsrModbusMapPersistenceResult( | |||
| PLSR_PERSISTENCE_RESULT persistenceResult) | |||
| { | |||
| if (persistenceResult == PLSR_PERSISTENCE_OK) | |||
| { | |||
| return PLSR_RESULT_OK; | |||
| } | |||
| if (persistenceResult == PLSR_PERSISTENCE_NOT_IMPLEMENTED) | |||
| { | |||
| return PLSR_RESULT_NOT_SUPPORTED; | |||
| } | |||
| if (persistenceResult == PLSR_PERSISTENCE_INVALID_ARGUMENT) | |||
| { | |||
| return PLSR_RESULT_INVALID_ARGUMENT; | |||
| } | |||
| return PLSR_RESULT_PERSISTENCE_ERROR; | |||
| } | |||
| static void PlsrModbusPublishPersistenceResponse(uint32_t sequence, | |||
| uint16_t operation, | |||
| PLSR_RESULT result) | |||
| { | |||
| PLSR_PERSISTENCE_DIAGNOSTICS diagnostics; | |||
| uint16_t response[PLSR_MODBUS_PERSISTENCE_RESPONSE_WORDS] = {0U}; | |||
| PlsrPersistenceGetDiagnostics(&diagnostics); | |||
| PlsrModbusPutU32(response, 0UL, sequence); | |||
| response[2UL] = operation; | |||
| response[3UL] = (uint16_t)result; | |||
| response[4UL] = (uint16_t)diagnostics.hsdValidMask | |||
| | ((uint16_t)diagnostics.sfdValidMask << 8U); | |||
| response[5UL] = (uint16_t)diagnostics.hsdNewestMask | |||
| | ((uint16_t)diagnostics.sfdNewestMask << 8U); | |||
| response[6UL] = diagnostics.destructiveDiagnosticEnabled; | |||
| (void)ModbusDataWriteWords( | |||
| MODBUS_DATA_DEVICE_D, | |||
| (uint32_t)PlsrModbusBaseAddress | |||
| + PLSR_MODBUS_PERSISTENCE_RESPONSE_OFFSET, | |||
| response, | |||
| PLSR_MODBUS_PERSISTENCE_RESPONSE_WORDS); | |||
| } | |||
| static void PlsrModbusHandlePersistenceRequest(void) | |||
| { | |||
| uint16_t request[PLSR_MODBUS_PERSISTENCE_REQUEST_WORDS]; | |||
| const uint16_t clearRequest[PLSR_MODBUS_PERSISTENCE_REQUEST_WORDS] = | |||
| {0U}; | |||
| PLSR_PERSISTENCE_RESULT persistenceResult; | |||
| PLSR_PERSISTENCE_DIAG_TARGET target; | |||
| PLSR_RESULT result; | |||
| uint32_t sequence; | |||
| uint32_t inverseSequence; | |||
| uint32_t interruptState; | |||
| uint16_t operation; | |||
| if (PlsrModbusReadWords(PLSR_MODBUS_PERSISTENCE_REQUEST_OFFSET, | |||
| request, | |||
| PLSR_MODBUS_PERSISTENCE_REQUEST_WORDS) == 0U) | |||
| { | |||
| return; | |||
| } | |||
| if ((request[0UL] != PLSR_MODBUS_PERSIST_MAGIC_A) | |||
| || (request[1UL] != PLSR_MODBUS_PERSIST_MAGIC_B)) | |||
| { | |||
| return; | |||
| } | |||
| sequence = PlsrModbusGetU32(request, 2UL); | |||
| inverseSequence = PlsrModbusGetU32(request, 4UL); | |||
| operation = request[6UL]; | |||
| if ((sequence != 0UL) | |||
| && (sequence == PlsrModbusLastPersistenceRequestSequence)) | |||
| { | |||
| return; | |||
| } | |||
| if (sequence != 0UL) | |||
| { | |||
| PlsrModbusLastPersistenceRequestSequence = sequence; | |||
| } | |||
| result = PLSR_RESULT_INVALID_ARGUMENT; | |||
| if ((sequence != 0UL) | |||
| && (inverseSequence == ~sequence) | |||
| && (request[7UL] == PLSR_MODBUS_PERSIST_ARM) | |||
| && ((operation == PLSR_MODBUS_PERSIST_INVALIDATE_HSD) | |||
| || (operation == PLSR_MODBUS_PERSIST_INVALIDATE_SFD))) | |||
| { | |||
| /* Keep the idle check and the optional one-word invalidation in one | |||
| * scheduling exclusion window. This diagnostic is disabled in | |||
| * normal builds; when enabled, no START can race the Flash write. */ | |||
| interruptState = PlsrModbusEnterPersistenceDiagnosticCritical(); | |||
| if (PlsrModbusPersistenceAllAxesIdle() == 0U) | |||
| { | |||
| result = PLSR_RESULT_BUSY; | |||
| } | |||
| else | |||
| { | |||
| target = (operation == PLSR_MODBUS_PERSIST_INVALIDATE_HSD) | |||
| ? PLSR_PERSISTENCE_DIAG_TARGET_HSD | |||
| : PLSR_PERSISTENCE_DIAG_TARGET_SFD; | |||
| persistenceResult = | |||
| PlsrPersistenceDiagnosticInvalidateNewest(target); | |||
| result = PlsrModbusMapPersistenceResult(persistenceResult); | |||
| } | |||
| PlsrModbusExitPersistenceDiagnosticCritical(interruptState); | |||
| } | |||
| PlsrModbusPublishPersistenceResponse(sequence, operation, result); | |||
| (void)ModbusDataWriteWords( | |||
| MODBUS_DATA_DEVICE_D, | |||
| (uint32_t)PlsrModbusBaseAddress | |||
| + PLSR_MODBUS_PERSISTENCE_REQUEST_OFFSET, | |||
| clearRequest, | |||
| PLSR_MODBUS_PERSISTENCE_REQUEST_WORDS); | |||
| } | |||
| static void PlsrModbusPublishAxisStatus(uint8_t axis) | |||
| { | |||
| PLSR_STATUS status; | |||
| @@ -488,6 +662,7 @@ static void PlsrModbusPublishAxisStatus(uint8_t axis) | |||
| words[34UL] = status.currentSegment; | |||
| words[35UL] = status.directionPoint; | |||
| words[36UL] = status.highResourceMask; | |||
| words[37UL] = status.hardwareCounter; | |||
| PlsrModbusPutU32(words, 38UL, status.currentFrequencyHz); | |||
| PlsrModbusPutU32(words, 40UL, status.targetFrequencyHz); | |||
| PlsrModbusPutU32(words, 42UL, status.liveFrequencyRejectCount); | |||
| @@ -501,6 +676,144 @@ static void PlsrModbusPublishAxisStatus(uint8_t axis) | |||
| PLSR_MODBUS_AXIS_STATUS_WORDS); | |||
| } | |||
| static void PlsrModbusPublishPerformance(void) | |||
| { | |||
| uint16_t words[PLSR_MODBUS_PERFORMANCE_WORDS]; | |||
| uint16_t stageWords[PLSR_MODBUS_STAGE_PERFORMANCE_WORDS]; | |||
| uint16_t abGateWords[PLSR_MODBUS_AB_GATE_PERFORMANCE_WORDS]; | |||
| uint32_t outputCycles = PlsrHwGetMaxOutputIsrCycles(); | |||
| uint32_t counterCycles = PlsrHwGetMaxCounterIsrCycles(); | |||
| uint8_t stage; | |||
| PlsrModbusPutU32(words, 0UL, PlsrGetMaxProcessCycles()); | |||
| PlsrModbusPutU32(words, 2UL, PlsrGetMaxProcessResponseCycles()); | |||
| PlsrModbusPutU32(words, 4UL, PlsrHwGetMaxControlIsrCycles()); | |||
| words[6UL] = (uint16_t)((outputCycles > UINT16_MAX) | |||
| ? UINT16_MAX | |||
| : outputCycles); | |||
| words[7UL] = (uint16_t)((counterCycles > UINT16_MAX) | |||
| ? UINT16_MAX | |||
| : counterCycles); | |||
| (void)ModbusDataWriteWords( | |||
| MODBUS_DATA_DEVICE_D, | |||
| (uint32_t)PlsrModbusBaseAddress + PLSR_MODBUS_PERFORMANCE_OFFSET, | |||
| words, | |||
| PLSR_MODBUS_PERFORMANCE_WORDS); | |||
| for (stage = 0U; stage < PLSR_PROCESS_STAGE_COUNT; stage++) | |||
| { | |||
| PlsrModbusPutU32(stageWords, | |||
| (uint32_t)stage * 2UL, | |||
| PlsrGetMaxProcessStageCycles(stage)); | |||
| } | |||
| (void)ModbusDataWriteWords( | |||
| MODBUS_DATA_DEVICE_D, | |||
| (uint32_t)PlsrModbusBaseAddress | |||
| + PLSR_MODBUS_STAGE_PERFORMANCE_OFFSET, | |||
| stageWords, | |||
| PLSR_MODBUS_STAGE_PERFORMANCE_WORDS); | |||
| PlsrModbusPutU32(abGateWords, 0UL, PlsrHwGetMaxAbGateCycles()); | |||
| (void)ModbusDataWriteWords( | |||
| MODBUS_DATA_DEVICE_D, | |||
| (uint32_t)PlsrModbusBaseAddress | |||
| + PLSR_MODBUS_AB_GATE_PERFORMANCE_OFFSET, | |||
| abGateWords, | |||
| PLSR_MODBUS_AB_GATE_PERFORMANCE_WORDS); | |||
| } | |||
| static void PlsrModbusPublishPersistence(void) | |||
| { | |||
| PLSR_PERSISTENCE_DIAGNOSTICS diagnostics; | |||
| uint16_t words[PLSR_MODBUS_PERSISTENCE_WORDS] = {0U}; | |||
| uint32_t flags = 0UL; | |||
| uint32_t generation = PlsrModbusPersistenceGeneration + 2UL; | |||
| if (generation == 0UL) | |||
| { | |||
| generation = 2UL; | |||
| } | |||
| PlsrModbusPersistenceGeneration = generation; | |||
| PlsrPersistenceGetDiagnostics(&diagnostics); | |||
| if (PlcDeviceIsHsdDirty() != 0U) flags |= (1UL << 0U); | |||
| if (PlcDeviceIsSfdDirty() != 0U) flags |= (1UL << 1U); | |||
| if (PlcDeviceGetRestoredHsdPositionValid() != 0U) | |||
| { | |||
| flags |= (1UL << 2U); | |||
| } | |||
| if (PlcDeviceGetRestoredHsdLastBusy() != 0U) | |||
| { | |||
| flags |= (1UL << 3U); | |||
| } | |||
| if (diagnostics.destructiveDiagnosticEnabled != 0U) | |||
| { | |||
| flags |= (1UL << 4U); | |||
| } | |||
| PlsrModbusPutU32(words, 0UL, generation); | |||
| words[2UL] = PLSR_MODBUS_PERSISTENCE_VERSION; | |||
| words[3UL] = (uint16_t)diagnostics.hsdValidMask | |||
| | ((uint16_t)diagnostics.sfdValidMask << 8U); | |||
| words[4UL] = (uint16_t)diagnostics.hsdNewestMask | |||
| | ((uint16_t)diagnostics.sfdNewestMask << 8U); | |||
| words[5UL] = (uint16_t)flags; | |||
| words[6UL] = (uint16_t)diagnostics.lastHsdLoadResult; | |||
| words[7UL] = (uint16_t)diagnostics.lastSfdLoadResult; | |||
| words[8UL] = (uint16_t)diagnostics.lastHsdSaveResult; | |||
| words[9UL] = (uint16_t)diagnostics.lastSfdSaveResult; | |||
| words[10UL] = (uint16_t)diagnostics.lastSfdEraseResult; | |||
| PlsrModbusPutU32(words, 12UL, diagnostics.hsdGeneration[0]); | |||
| PlsrModbusPutU32(words, 14UL, diagnostics.hsdGeneration[1]); | |||
| PlsrModbusPutU32(words, 16UL, diagnostics.sfdGeneration[0]); | |||
| PlsrModbusPutU32(words, 18UL, diagnostics.sfdGeneration[1]); | |||
| PlsrModbusPutU32(words, 20UL, diagnostics.hsdSaveCount); | |||
| PlsrModbusPutU32(words, 22UL, diagnostics.sfdSaveCount); | |||
| PlsrModbusPutU32(words, 24UL, diagnostics.selectedHsdCrc32); | |||
| PlsrModbusPutU32(words, 26UL, diagnostics.selectedSfdCrc32); | |||
| PlsrModbusPutU32(words, 28UL, generation); | |||
| (void)ModbusDataWriteWords( | |||
| MODBUS_DATA_DEVICE_D, | |||
| (uint32_t)PlsrModbusBaseAddress + PLSR_MODBUS_PERSISTENCE_OFFSET, | |||
| words, | |||
| PLSR_MODBUS_PERSISTENCE_WORDS); | |||
| } | |||
| static void PlsrModbusPublishUsbDiagnostics(void) | |||
| { | |||
| uint16_t words[PLSR_MODBUS_USB_DIAGNOSTICS_WORDS] = {0U}; | |||
| uint32_t generation = PlsrModbusUsbDiagnosticsGeneration + 2UL; | |||
| #ifndef PLSR_HOST_TEST | |||
| USB_CDC_RUNTIME_DIAGNOSTICS diagnostics; | |||
| (void)memset(&diagnostics, 0, sizeof(diagnostics)); | |||
| (void)CDC_GetRuntimeDiagnostics(&diagnostics); | |||
| #endif | |||
| if (generation == 0UL) | |||
| { | |||
| generation = 2UL; | |||
| } | |||
| PlsrModbusUsbDiagnosticsGeneration = generation; | |||
| PlsrModbusPutU32(words, 0UL, generation); | |||
| words[2UL] = PLSR_MODBUS_USB_DIAGNOSTICS_VERSION; | |||
| #ifndef PLSR_HOST_TEST | |||
| words[3UL] = diagnostics.initialized; | |||
| PlsrModbusPutU32(words, 4UL, diagnostics.rxPacketCount); | |||
| PlsrModbusPutU32(words, 6UL, diagnostics.rxByteCount); | |||
| PlsrModbusPutU32(words, 8UL, diagnostics.rxRearmFailureCount); | |||
| PlsrModbusPutU32(words, 10UL, diagnostics.txRequestCount); | |||
| PlsrModbusPutU32(words, 12UL, diagnostics.txByteCount); | |||
| PlsrModbusPutU32(words, 14UL, diagnostics.txBusyCount); | |||
| PlsrModbusPutU32(words, 16UL, diagnostics.txFailureCount); | |||
| PlsrModbusPutU32(words, 18UL, diagnostics.txCompleteCount); | |||
| #endif | |||
| PlsrModbusPutU32(words, 20UL, generation); | |||
| (void)ModbusDataWriteWords( | |||
| MODBUS_DATA_DEVICE_D, | |||
| (uint32_t)PlsrModbusBaseAddress | |||
| + PLSR_MODBUS_USB_DIAGNOSTICS_OFFSET, | |||
| words, | |||
| PLSR_MODBUS_USB_DIAGNOSTICS_WORDS); | |||
| } | |||
| PLSR_RESULT PlsrModbusControlInit(uint16_t baseAddress) | |||
| { | |||
| uint16_t header[8] = {0U}; | |||
| @@ -515,6 +828,9 @@ PLSR_RESULT PlsrModbusControlInit(uint16_t baseAddress) | |||
| PlsrModbusEnabled = 0U; | |||
| PlsrModbusLastCallRequestSequence = 0UL; | |||
| PlsrModbusLastCommandRequestSequence = 0UL; | |||
| PlsrModbusLastPersistenceRequestSequence = 0UL; | |||
| PlsrModbusPersistenceGeneration = 0UL; | |||
| PlsrModbusUsbDiagnosticsGeneration = 0UL; | |||
| (void)memset(PlsrModbusCommitted, 0, sizeof(PlsrModbusCommitted)); | |||
| (void)memset(PlsrModbusStatusGeneration, | |||
| 0, | |||
| @@ -531,6 +847,8 @@ PLSR_RESULT PlsrModbusControlInit(uint16_t baseAddress) | |||
| header[2UL] = PLSR_MODBUS_PROTOCOL_VERSION; | |||
| header[3UL] = (uint16_t)PLSR_MODBUS_WINDOW_WORDS; | |||
| header[4UL] = PLSR_MODBUS_CAPABILITIES; | |||
| PlsrModbusPutU32(header, 5UL, PlsrHwGetTimerClockHz(0U)); | |||
| header[7UL] = PLSR_MODBUS_PERFORMANCE_VERSION; | |||
| if (ModbusDataWriteWords(MODBUS_DATA_DEVICE_D, | |||
| baseAddress, | |||
| header, | |||
| @@ -553,10 +871,14 @@ void PlsrModbusControlPoll(void) | |||
| } | |||
| PlsrModbusHandleCallRequest(); | |||
| PlsrModbusHandleCommandRequest(); | |||
| PlsrModbusHandlePersistenceRequest(); | |||
| for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) | |||
| { | |||
| PlsrModbusPublishAxisStatus(axis); | |||
| } | |||
| PlsrModbusPublishPerformance(); | |||
| PlsrModbusPublishPersistence(); | |||
| PlsrModbusPublishUsbDiagnostics(); | |||
| } | |||
| uint8_t PlsrModbusControlIsEnabled(void) | |||
| @@ -58,6 +58,33 @@ static uint8_t PlsrModbusReadDword(void *context, | |||
| return ModbusDataReadDword(modbusDevice, address, value); | |||
| } | |||
| static uint8_t PlsrModbusBitDevice(PLSR_DEVICE_TYPE device, | |||
| MODBUS_BIT_DEVICE *modbusDevice) | |||
| { | |||
| if ((modbusDevice == NULL) || (device < PLSR_DEVICE_X) | |||
| || (device > PLSR_DEVICE_HM)) | |||
| { | |||
| return 0U; | |||
| } | |||
| *modbusDevice = (MODBUS_BIT_DEVICE)(device - PLSR_DEVICE_X); | |||
| return 1U; | |||
| } | |||
| static uint8_t PlsrModbusReadBit(void *context, | |||
| PLSR_DEVICE_TYPE device, | |||
| uint32_t address, | |||
| uint8_t *value) | |||
| { | |||
| MODBUS_BIT_DEVICE modbusDevice; | |||
| (void)context; | |||
| if (PlsrModbusBitDevice(device, &modbusDevice) == 0U) | |||
| { | |||
| return 0U; | |||
| } | |||
| return ModbusDataReadBit(modbusDevice, address, value); | |||
| } | |||
| void PlsrModbusDataSourceInit(PLSR_DATA_SOURCE *source) | |||
| { | |||
| if (source == NULL) | |||
| @@ -68,5 +95,5 @@ void PlsrModbusDataSourceInit(PLSR_DATA_SOURCE *source) | |||
| source->validateWords = PlsrModbusValidateWords; | |||
| source->readWord = PlsrModbusReadWord; | |||
| source->readDword = PlsrModbusReadDword; | |||
| source->readBit = NULL; | |||
| source->readBit = PlsrModbusReadBit; | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| #include "plsr_persistence.h" | |||
| #include "plsr_build_config.h" | |||
| #include <stddef.h> | |||
| #include <string.h> | |||
| @@ -23,6 +24,14 @@ | |||
| #define PLSR_HSD_PARAMETER_SET_STRIDE (20U) | |||
| #define PLSR_HSD_DEFAULT_BASE (460U) | |||
| static const uint32_t PlsrPersistenceCrc32Nibble[16] = | |||
| { | |||
| 0x00000000UL, 0x1DB71064UL, 0x3B6E20C8UL, 0x26D930ACUL, | |||
| 0x76DC4190UL, 0x6B6B51F4UL, 0x4DB26158UL, 0x5005713CUL, | |||
| 0xEDB88320UL, 0xF00F9344UL, 0xD6D6A3E8UL, 0xCB61B38CUL, | |||
| 0x9B64C2B0UL, 0x86D3D2D4UL, 0xA00AE278UL, 0xBDBDF21CUL | |||
| }; | |||
| typedef struct | |||
| { | |||
| uint32_t magic; | |||
| @@ -55,33 +64,53 @@ typedef struct | |||
| static PLSR_HSD_BACKUP_RECORD PlsrHostBackupSlots[2]; | |||
| static PLSR_SFD_FLASH_RECORD PlsrHostSfdSlots[2]; | |||
| static PLSR_TEST_SFD_FAULT PlsrHostSfdFault; | |||
| static uint32_t PlsrHostHsdSaveCount; | |||
| #else | |||
| #include "stm32f4xx.h" | |||
| #include "stm32f4xx_hal.h" | |||
| #include "stm32f4xx_hal_flash_ex.h" | |||
| #endif | |||
| static PLSR_PERSISTENCE_DIAGNOSTICS PlsrPersistenceDiagnostics; | |||
| static uint8_t PlsrPersistenceDiagnosticsInitialized; | |||
| static void PlsrPersistenceEnsureDiagnostics(void) | |||
| { | |||
| if (PlsrPersistenceDiagnosticsInitialized == 0U) | |||
| { | |||
| (void)memset(&PlsrPersistenceDiagnostics, | |||
| 0, | |||
| sizeof(PlsrPersistenceDiagnostics)); | |||
| PlsrPersistenceDiagnostics.lastHsdLoadResult = | |||
| PLSR_PERSISTENCE_NOT_IMPLEMENTED; | |||
| PlsrPersistenceDiagnostics.lastSfdLoadResult = | |||
| PLSR_PERSISTENCE_NOT_IMPLEMENTED; | |||
| PlsrPersistenceDiagnostics.lastHsdSaveResult = | |||
| PLSR_PERSISTENCE_NOT_IMPLEMENTED; | |||
| PlsrPersistenceDiagnostics.lastSfdSaveResult = | |||
| PLSR_PERSISTENCE_NOT_IMPLEMENTED; | |||
| PlsrPersistenceDiagnostics.lastSfdEraseResult = | |||
| PLSR_PERSISTENCE_NOT_IMPLEMENTED; | |||
| #if PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG != 0U | |||
| PlsrPersistenceDiagnostics.destructiveDiagnosticEnabled = 1U; | |||
| #endif | |||
| PlsrPersistenceDiagnosticsInitialized = 1U; | |||
| } | |||
| } | |||
| static uint32_t PlsrPersistenceCrc32Update(uint32_t crc, | |||
| const volatile uint8_t *data, | |||
| uint32_t length) | |||
| { | |||
| uint32_t index; | |||
| uint8_t bit; | |||
| for (index = 0U; index < length; index++) | |||
| { | |||
| crc ^= data[index]; | |||
| for (bit = 0U; bit < 8U; bit++) | |||
| { | |||
| if ((crc & 1UL) != 0UL) | |||
| { | |||
| crc = (crc >> 1U) ^ 0xEDB88320UL; | |||
| } | |||
| else | |||
| { | |||
| crc >>= 1U; | |||
| } | |||
| } | |||
| crc = (crc >> 4U) | |||
| ^ PlsrPersistenceCrc32Nibble[crc & 0x0FUL]; | |||
| crc = (crc >> 4U) | |||
| ^ PlsrPersistenceCrc32Nibble[crc & 0x0FUL]; | |||
| } | |||
| return crc; | |||
| @@ -161,6 +190,106 @@ static uint8_t PlsrPersistenceSfdRecordIsValid( | |||
| return (expectedCrc == record->crc32) ? 1U : 0U; | |||
| } | |||
| static uint8_t PlsrPersistenceNewestMask(uint8_t validA, | |||
| uint8_t validB, | |||
| uint32_t generationA, | |||
| uint32_t generationB) | |||
| { | |||
| if ((validA != 0U) && (validB != 0U)) | |||
| { | |||
| return (PlsrPersistenceGenerationIsNewer(generationB, generationA) | |||
| != 0U) | |||
| ? 2U | |||
| : 1U; | |||
| } | |||
| if (validA != 0U) | |||
| { | |||
| return 1U; | |||
| } | |||
| return (validB != 0U) ? 2U : 0U; | |||
| } | |||
| static void PlsrPersistenceUpdateHsdDiagnostics( | |||
| const volatile PLSR_HSD_BACKUP_RECORD *slotA, | |||
| const volatile PLSR_HSD_BACKUP_RECORD *slotB, | |||
| uint8_t validA, | |||
| uint8_t validB) | |||
| { | |||
| uint8_t newestMask; | |||
| PlsrPersistenceEnsureDiagnostics(); | |||
| PlsrPersistenceDiagnostics.hsdValidMask = | |||
| (uint8_t)((validA != 0U ? 1U : 0U) | (validB != 0U ? 2U : 0U)); | |||
| PlsrPersistenceDiagnostics.hsdGeneration[0] = | |||
| (validA != 0U) ? slotA->generation : 0UL; | |||
| PlsrPersistenceDiagnostics.hsdGeneration[1] = | |||
| (validB != 0U) ? slotB->generation : 0UL; | |||
| newestMask = PlsrPersistenceNewestMask( | |||
| validA, | |||
| validB, | |||
| PlsrPersistenceDiagnostics.hsdGeneration[0], | |||
| PlsrPersistenceDiagnostics.hsdGeneration[1]); | |||
| PlsrPersistenceDiagnostics.hsdNewestMask = newestMask; | |||
| PlsrPersistenceDiagnostics.selectedHsdCrc32 = | |||
| (newestMask == 1U) ? slotA->crc32 | |||
| : (newestMask == 2U) ? slotB->crc32 | |||
| : 0UL; | |||
| } | |||
| static void PlsrPersistenceUpdateSfdDiagnostics( | |||
| const volatile PLSR_SFD_FLASH_RECORD *slotA, | |||
| const volatile PLSR_SFD_FLASH_RECORD *slotB, | |||
| uint8_t validA, | |||
| uint8_t validB) | |||
| { | |||
| uint8_t newestMask; | |||
| PlsrPersistenceEnsureDiagnostics(); | |||
| PlsrPersistenceDiagnostics.sfdValidMask = | |||
| (uint8_t)((validA != 0U ? 1U : 0U) | (validB != 0U ? 2U : 0U)); | |||
| PlsrPersistenceDiagnostics.sfdGeneration[0] = | |||
| (validA != 0U) ? slotA->generation : 0UL; | |||
| PlsrPersistenceDiagnostics.sfdGeneration[1] = | |||
| (validB != 0U) ? slotB->generation : 0UL; | |||
| newestMask = PlsrPersistenceNewestMask( | |||
| validA, | |||
| validB, | |||
| PlsrPersistenceDiagnostics.sfdGeneration[0], | |||
| PlsrPersistenceDiagnostics.sfdGeneration[1]); | |||
| PlsrPersistenceDiagnostics.sfdNewestMask = newestMask; | |||
| PlsrPersistenceDiagnostics.selectedSfdCrc32 = | |||
| (newestMask == 1U) ? slotA->crc32 | |||
| : (newestMask == 2U) ? slotB->crc32 | |||
| : 0UL; | |||
| } | |||
| static void PlsrPersistenceRefreshSfdDiagnostics(void) | |||
| { | |||
| volatile PLSR_SFD_FLASH_RECORD *slotA = | |||
| PlsrPersistenceGetSfdSlot(0U); | |||
| volatile PLSR_SFD_FLASH_RECORD *slotB = | |||
| PlsrPersistenceGetSfdSlot(1U); | |||
| PlsrPersistenceUpdateSfdDiagnostics( | |||
| slotA, | |||
| slotB, | |||
| PlsrPersistenceSfdRecordIsValid(slotA), | |||
| PlsrPersistenceSfdRecordIsValid(slotB)); | |||
| } | |||
| static PLSR_PERSISTENCE_RESULT PlsrPersistenceCompleteSfdSave( | |||
| PLSR_PERSISTENCE_RESULT result) | |||
| { | |||
| PlsrPersistenceEnsureDiagnostics(); | |||
| PlsrPersistenceDiagnostics.lastSfdSaveResult = result; | |||
| PlsrPersistenceRefreshSfdDiagnostics(); | |||
| if (result == PLSR_PERSISTENCE_OK) | |||
| { | |||
| PlsrPersistenceDiagnostics.sfdSaveCount++; | |||
| } | |||
| return result; | |||
| } | |||
| static void PlsrPersistenceCopySfdFromVolatile( | |||
| PLSR_SFD_DATA *destination, | |||
| const volatile PLSR_SFD_DATA *source) | |||
| @@ -443,8 +572,11 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadHsd(PLSR_HSD_DATA *data) | |||
| uint8_t validA; | |||
| uint8_t validB; | |||
| PlsrPersistenceEnsureDiagnostics(); | |||
| if (data == NULL) | |||
| { | |||
| PlsrPersistenceDiagnostics.lastHsdLoadResult = | |||
| PLSR_PERSISTENCE_INVALID_ARGUMENT; | |||
| return PLSR_PERSISTENCE_INVALID_ARGUMENT; | |||
| } | |||
| @@ -452,10 +584,13 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadHsd(PLSR_HSD_DATA *data) | |||
| slotB = PlsrPersistenceGetHsdSlot(1U); | |||
| validA = PlsrPersistenceHsdRecordIsValid(slotA); | |||
| validB = PlsrPersistenceHsdRecordIsValid(slotB); | |||
| PlsrPersistenceUpdateHsdDiagnostics(slotA, slotB, validA, validB); | |||
| if ((validA == 0U) && (validB == 0U)) | |||
| { | |||
| PlsrPersistenceApplyHsdDefaults(data); | |||
| PlsrPersistenceDiagnostics.lastHsdLoadResult = | |||
| PLSR_PERSISTENCE_DEFAULTED; | |||
| return PLSR_PERSISTENCE_DEFAULTED; | |||
| } | |||
| @@ -475,6 +610,7 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadHsd(PLSR_HSD_DATA *data) | |||
| } | |||
| PlsrPersistenceCopyHsdFromVolatile(data, &selected->data); | |||
| PlsrPersistenceDiagnostics.lastHsdLoadResult = PLSR_PERSISTENCE_OK; | |||
| return PLSR_PERSISTENCE_OK; | |||
| } | |||
| @@ -489,13 +625,23 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveHsd(const PLSR_HSD_DATA *data) | |||
| uint32_t generationB; | |||
| uint8_t validA; | |||
| uint8_t validB; | |||
| uint8_t targetSlot; | |||
| uint8_t targetValid; | |||
| PlsrPersistenceEnsureDiagnostics(); | |||
| if (data == NULL) | |||
| { | |||
| PlsrPersistenceDiagnostics.lastHsdSaveResult = | |||
| PLSR_PERSISTENCE_INVALID_ARGUMENT; | |||
| return PLSR_PERSISTENCE_INVALID_ARGUMENT; | |||
| } | |||
| #ifdef PLSR_HOST_TEST | |||
| PlsrHostHsdSaveCount++; | |||
| #endif | |||
| if (sizeof(PLSR_HSD_BACKUP_RECORD) > PLSR_BACKUP_SLOT_STRIDE) | |||
| { | |||
| PlsrPersistenceDiagnostics.lastHsdSaveResult = | |||
| PLSR_PERSISTENCE_VERIFY_FAILED; | |||
| return PLSR_PERSISTENCE_VERIFY_FAILED; | |||
| } | |||
| @@ -514,26 +660,31 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveHsd(const PLSR_HSD_DATA *data) | |||
| { | |||
| newestGeneration = generationB; | |||
| target = slotA; | |||
| targetSlot = 0U; | |||
| } | |||
| else | |||
| { | |||
| newestGeneration = generationA; | |||
| target = slotB; | |||
| targetSlot = 1U; | |||
| } | |||
| } | |||
| else if (validA != 0U) | |||
| { | |||
| newestGeneration = slotA->generation; | |||
| target = slotB; | |||
| targetSlot = 1U; | |||
| } | |||
| else if (validB != 0U) | |||
| { | |||
| newestGeneration = slotB->generation; | |||
| target = slotA; | |||
| targetSlot = 0U; | |||
| } | |||
| else | |||
| { | |||
| target = slotA; | |||
| targetSlot = 0U; | |||
| } | |||
| (void)memset(&record, 0, sizeof(record)); | |||
| @@ -547,18 +698,39 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveHsd(const PLSR_HSD_DATA *data) | |||
| (uint32_t)offsetof(PLSR_HSD_BACKUP_RECORD, crc32)); | |||
| PlsrPersistenceWriteRecord(target, &record); | |||
| return (PlsrPersistenceHsdRecordIsValid(target) != 0U) | |||
| ? PLSR_PERSISTENCE_OK | |||
| : PLSR_PERSISTENCE_VERIFY_FAILED; | |||
| targetValid = PlsrPersistenceHsdRecordIsValid(target); | |||
| if (targetSlot == 0U) | |||
| { | |||
| validA = targetValid; | |||
| } | |||
| else | |||
| { | |||
| validB = targetValid; | |||
| } | |||
| PlsrPersistenceUpdateHsdDiagnostics(slotA, slotB, validA, validB); | |||
| PlsrPersistenceDiagnostics.lastHsdSaveResult = | |||
| (targetValid != 0U) ? PLSR_PERSISTENCE_OK | |||
| : PLSR_PERSISTENCE_VERIFY_FAILED; | |||
| if (targetValid != 0U) | |||
| { | |||
| PlsrPersistenceDiagnostics.hsdSaveCount++; | |||
| } | |||
| return PlsrPersistenceDiagnostics.lastHsdSaveResult; | |||
| } | |||
| void PlsrPersistenceResetHsd(void) | |||
| { | |||
| PlsrPersistenceGetHsdSlot(0U)->magic = 0UL; | |||
| PlsrPersistenceGetHsdSlot(1U)->magic = 0UL; | |||
| volatile PLSR_HSD_BACKUP_RECORD *slotA = | |||
| PlsrPersistenceGetHsdSlot(0U); | |||
| volatile PLSR_HSD_BACKUP_RECORD *slotB = | |||
| PlsrPersistenceGetHsdSlot(1U); | |||
| slotA->magic = 0UL; | |||
| slotB->magic = 0UL; | |||
| #ifndef PLSR_HOST_TEST | |||
| __DMB(); | |||
| #endif | |||
| PlsrPersistenceUpdateHsdDiagnostics(slotA, slotB, 0U, 0U); | |||
| } | |||
| PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadSfd(PLSR_SFD_DATA *data) | |||
| @@ -571,8 +743,11 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadSfd(PLSR_SFD_DATA *data) | |||
| uint8_t validA; | |||
| uint8_t validB; | |||
| PlsrPersistenceEnsureDiagnostics(); | |||
| if (data == NULL) | |||
| { | |||
| PlsrPersistenceDiagnostics.lastSfdLoadResult = | |||
| PLSR_PERSISTENCE_INVALID_ARGUMENT; | |||
| return PLSR_PERSISTENCE_INVALID_ARGUMENT; | |||
| } | |||
| @@ -580,10 +755,13 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadSfd(PLSR_SFD_DATA *data) | |||
| slotB = PlsrPersistenceGetSfdSlot(1U); | |||
| validA = PlsrPersistenceSfdRecordIsValid(slotA); | |||
| validB = PlsrPersistenceSfdRecordIsValid(slotB); | |||
| PlsrPersistenceUpdateSfdDiagnostics(slotA, slotB, validA, validB); | |||
| if ((validA == 0U) && (validB == 0U)) | |||
| { | |||
| PlsrPersistenceApplySfdDefaults(data); | |||
| PlsrPersistenceDiagnostics.lastSfdLoadResult = | |||
| PLSR_PERSISTENCE_DEFAULTED; | |||
| return PLSR_PERSISTENCE_DEFAULTED; | |||
| } | |||
| @@ -603,6 +781,7 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadSfd(PLSR_SFD_DATA *data) | |||
| } | |||
| PlsrPersistenceCopySfdFromVolatile(data, &selected->data); | |||
| PlsrPersistenceDiagnostics.lastSfdLoadResult = PLSR_PERSISTENCE_OK; | |||
| return PLSR_PERSISTENCE_OK; | |||
| } | |||
| @@ -628,12 +807,17 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) | |||
| uint8_t validA; | |||
| uint8_t validB; | |||
| PlsrPersistenceEnsureDiagnostics(); | |||
| if (data == NULL) | |||
| { | |||
| PlsrPersistenceDiagnostics.lastSfdSaveResult = | |||
| PLSR_PERSISTENCE_INVALID_ARGUMENT; | |||
| return PLSR_PERSISTENCE_INVALID_ARGUMENT; | |||
| } | |||
| if (sizeof(PLSR_SFD_FLASH_RECORD) > PLSR_SFD_FLASH_SECTOR_SIZE) | |||
| { | |||
| PlsrPersistenceDiagnostics.lastSfdSaveResult = | |||
| PLSR_PERSISTENCE_VERIFY_FAILED; | |||
| return PLSR_PERSISTENCE_VERIFY_FAILED; | |||
| } | |||
| @@ -702,13 +886,14 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) | |||
| result = PlsrPersistenceBeginSfdOperation(); | |||
| if (result != PLSR_PERSISTENCE_OK) | |||
| { | |||
| return result; | |||
| return PlsrPersistenceCompleteSfdSave(result); | |||
| } | |||
| result = PlsrPersistenceEraseSfdSlot(targetSlot); | |||
| if (result != PLSR_PERSISTENCE_OK) | |||
| { | |||
| return PlsrPersistenceEndSfdOperation(result); | |||
| result = PlsrPersistenceEndSfdOperation(result); | |||
| return PlsrPersistenceCompleteSfdSave(result); | |||
| } | |||
| /* The valid magic is committed last so an interrupted write stays invalid. */ | |||
| @@ -718,7 +903,8 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) | |||
| headerWords[index]); | |||
| if (result != PLSR_PERSISTENCE_OK) | |||
| { | |||
| return PlsrPersistenceEndSfdOperation(result); | |||
| result = PlsrPersistenceEndSfdOperation(result); | |||
| return PlsrPersistenceCompleteSfdSave(result); | |||
| } | |||
| } | |||
| @@ -729,7 +915,8 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) | |||
| dataWords[index]); | |||
| if (result != PLSR_PERSISTENCE_OK) | |||
| { | |||
| return PlsrPersistenceEndSfdOperation(result); | |||
| result = PlsrPersistenceEndSfdOperation(result); | |||
| return PlsrPersistenceCompleteSfdSave(result); | |||
| } | |||
| } | |||
| @@ -738,7 +925,8 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) | |||
| crc32); | |||
| if (result != PLSR_PERSISTENCE_OK) | |||
| { | |||
| return PlsrPersistenceEndSfdOperation(result); | |||
| result = PlsrPersistenceEndSfdOperation(result); | |||
| return PlsrPersistenceCompleteSfdSave(result); | |||
| } | |||
| #ifdef PLSR_HOST_TEST | |||
| @@ -754,38 +942,44 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) | |||
| crc32) | |||
| == 0U) | |||
| { | |||
| return PlsrPersistenceEndSfdOperation( | |||
| result = PlsrPersistenceEndSfdOperation( | |||
| PLSR_PERSISTENCE_VERIFY_FAILED); | |||
| return PlsrPersistenceCompleteSfdSave(result); | |||
| } | |||
| #ifdef PLSR_HOST_TEST | |||
| if (PlsrHostSfdFault == PLSR_TEST_SFD_FAULT_BEFORE_COMMIT) | |||
| { | |||
| PlsrHostSfdFault = PLSR_TEST_SFD_FAULT_NONE; | |||
| return PlsrPersistenceEndSfdOperation( | |||
| result = PlsrPersistenceEndSfdOperation( | |||
| PLSR_PERSISTENCE_PROGRAM_FAILED); | |||
| return PlsrPersistenceCompleteSfdSave(result); | |||
| } | |||
| #endif | |||
| result = PlsrPersistenceProgramSfdWord(&targetWords[0], headerWords[0]); | |||
| if (result != PLSR_PERSISTENCE_OK) | |||
| { | |||
| return PlsrPersistenceEndSfdOperation(result); | |||
| result = PlsrPersistenceEndSfdOperation(result); | |||
| return PlsrPersistenceCompleteSfdSave(result); | |||
| } | |||
| result = (PlsrPersistenceSfdRecordIsValid(target) != 0U) | |||
| ? PLSR_PERSISTENCE_OK | |||
| : PLSR_PERSISTENCE_VERIFY_FAILED; | |||
| return PlsrPersistenceEndSfdOperation(result); | |||
| result = PlsrPersistenceEndSfdOperation(result); | |||
| return PlsrPersistenceCompleteSfdSave(result); | |||
| } | |||
| PLSR_PERSISTENCE_RESULT PlsrPersistenceEraseSfd(void) | |||
| { | |||
| PLSR_PERSISTENCE_RESULT result; | |||
| PlsrPersistenceEnsureDiagnostics(); | |||
| result = PlsrPersistenceBeginSfdOperation(); | |||
| if (result != PLSR_PERSISTENCE_OK) | |||
| { | |||
| PlsrPersistenceDiagnostics.lastSfdEraseResult = result; | |||
| return result; | |||
| } | |||
| @@ -795,7 +989,103 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceEraseSfd(void) | |||
| result = PlsrPersistenceEraseSfdSlot(1U); | |||
| } | |||
| return PlsrPersistenceEndSfdOperation(result); | |||
| result = PlsrPersistenceEndSfdOperation(result); | |||
| PlsrPersistenceDiagnostics.lastSfdEraseResult = result; | |||
| PlsrPersistenceRefreshSfdDiagnostics(); | |||
| return result; | |||
| } | |||
| void PlsrPersistenceGetDiagnostics( | |||
| PLSR_PERSISTENCE_DIAGNOSTICS *diagnostics) | |||
| { | |||
| if (diagnostics == NULL) | |||
| { | |||
| return; | |||
| } | |||
| PlsrPersistenceEnsureDiagnostics(); | |||
| *diagnostics = PlsrPersistenceDiagnostics; | |||
| } | |||
| PLSR_PERSISTENCE_RESULT PlsrPersistenceDiagnosticInvalidateNewest( | |||
| PLSR_PERSISTENCE_DIAG_TARGET target) | |||
| { | |||
| #if PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG != 0U | |||
| uint32_t generationA; | |||
| uint32_t generationB; | |||
| uint8_t validA; | |||
| uint8_t validB; | |||
| PlsrPersistenceEnsureDiagnostics(); | |||
| if (target == PLSR_PERSISTENCE_DIAG_TARGET_HSD) | |||
| { | |||
| volatile PLSR_HSD_BACKUP_RECORD *slotA = | |||
| PlsrPersistenceGetHsdSlot(0U); | |||
| volatile PLSR_HSD_BACKUP_RECORD *slotB = | |||
| PlsrPersistenceGetHsdSlot(1U); | |||
| volatile PLSR_HSD_BACKUP_RECORD *newest; | |||
| validA = PlsrPersistenceHsdRecordIsValid(slotA); | |||
| validB = PlsrPersistenceHsdRecordIsValid(slotB); | |||
| PlsrPersistenceUpdateHsdDiagnostics(slotA, slotB, validA, validB); | |||
| if ((validA == 0U) || (validB == 0U)) | |||
| { | |||
| return PLSR_PERSISTENCE_VERIFY_FAILED; | |||
| } | |||
| generationA = slotA->generation; | |||
| generationB = slotB->generation; | |||
| newest = (PlsrPersistenceGenerationIsNewer(generationB, generationA) | |||
| != 0U) | |||
| ? slotB | |||
| : slotA; | |||
| newest->magic = 0UL; | |||
| #ifndef PLSR_HOST_TEST | |||
| __DMB(); | |||
| #endif | |||
| PlsrPersistenceUpdateHsdDiagnostics( | |||
| slotA, | |||
| slotB, | |||
| (newest == slotA) ? 0U : 1U, | |||
| (newest == slotB) ? 0U : 1U); | |||
| return PLSR_PERSISTENCE_OK; | |||
| } | |||
| if (target == PLSR_PERSISTENCE_DIAG_TARGET_SFD) | |||
| { | |||
| volatile PLSR_SFD_FLASH_RECORD *slotA = | |||
| PlsrPersistenceGetSfdSlot(0U); | |||
| volatile PLSR_SFD_FLASH_RECORD *slotB = | |||
| PlsrPersistenceGetSfdSlot(1U); | |||
| volatile PLSR_SFD_FLASH_RECORD *newest; | |||
| PLSR_PERSISTENCE_RESULT result; | |||
| validA = PlsrPersistenceSfdRecordIsValid(slotA); | |||
| validB = PlsrPersistenceSfdRecordIsValid(slotB); | |||
| PlsrPersistenceUpdateSfdDiagnostics(slotA, slotB, validA, validB); | |||
| if ((validA == 0U) || (validB == 0U)) | |||
| { | |||
| return PLSR_PERSISTENCE_VERIFY_FAILED; | |||
| } | |||
| generationA = slotA->generation; | |||
| generationB = slotB->generation; | |||
| newest = (PlsrPersistenceGenerationIsNewer(generationB, generationA) | |||
| != 0U) | |||
| ? slotB | |||
| : slotA; | |||
| result = PlsrPersistenceBeginSfdOperation(); | |||
| if (result == PLSR_PERSISTENCE_OK) | |||
| { | |||
| result = PlsrPersistenceProgramSfdWord( | |||
| (volatile uint32_t *)&newest->magic, | |||
| 0UL); | |||
| result = PlsrPersistenceEndSfdOperation(result); | |||
| } | |||
| PlsrPersistenceRefreshSfdDiagnostics(); | |||
| return result; | |||
| } | |||
| return PLSR_PERSISTENCE_INVALID_ARGUMENT; | |||
| #else | |||
| (void)target; | |||
| return PLSR_PERSISTENCE_NOT_IMPLEMENTED; | |||
| #endif | |||
| } | |||
| #ifdef PLSR_HOST_TEST | |||
| @@ -804,6 +1094,29 @@ void PlsrPersistenceTestResetStorage(void) | |||
| (void)memset(PlsrHostBackupSlots, 0, sizeof(PlsrHostBackupSlots)); | |||
| (void)memset(PlsrHostSfdSlots, 0xFF, sizeof(PlsrHostSfdSlots)); | |||
| PlsrHostSfdFault = PLSR_TEST_SFD_FAULT_NONE; | |||
| PlsrHostHsdSaveCount = 0UL; | |||
| PlsrPersistenceDiagnosticsInitialized = 0U; | |||
| PlsrPersistenceEnsureDiagnostics(); | |||
| PlsrPersistenceUpdateHsdDiagnostics( | |||
| PlsrPersistenceGetHsdSlot(0U), | |||
| PlsrPersistenceGetHsdSlot(1U), | |||
| 0U, | |||
| 0U); | |||
| PlsrPersistenceRefreshSfdDiagnostics(); | |||
| } | |||
| uint32_t PlsrPersistenceTestGetHsdSaveCount(void) | |||
| { | |||
| return PlsrHostHsdSaveCount; | |||
| } | |||
| uint32_t PlsrPersistenceTestCrc32(const uint8_t *data, uint32_t length) | |||
| { | |||
| if ((data == NULL) && (length != 0UL)) | |||
| { | |||
| return 0UL; | |||
| } | |||
| return PlsrPersistenceCrc32(data, length); | |||
| } | |||
| void PlsrPersistenceTestCorruptNewestHsd(void) | |||
| @@ -1,4 +1,5 @@ | |||
| #include "plsr_self_test.h" | |||
| #include "plsr_build_config.h" | |||
| #include "plc_device.h" | |||
| #include "modbus_data_store.h" | |||
| #include "plsr_core.h" | |||
| @@ -6,6 +7,8 @@ | |||
| #include "plsr_modbus_data.h" | |||
| #include <string.h> | |||
| #if PLSR_ENABLE_BOARD_SELF_TEST != 0U | |||
| /* 上电自测(验证后可删除): | |||
| * - AB 模式使用 Q0(A)/Q1(B),其余用出厂默认参数(K1) | |||
| * - 任务:3 段完整 AB 周期(H00 完成,顺序衔接): | |||
| @@ -964,3 +967,143 @@ PLSR_RESULT PlsrModbusControlSelfTestPrepare(void) | |||
| SelfTestWriteSfdDword((uint16_t)(setBase + 18U), 200UL); | |||
| return PLSR_RESULT_OK; | |||
| } | |||
| PLSR_RESULT PlsrHardwareCounterSelfTestPrepare(void) | |||
| { | |||
| uint16_t commonBase; | |||
| uint16_t setBase; | |||
| uint16_t limitSetBase; | |||
| uint16_t abSetBase; | |||
| uint32_t limitFrequencyHz; | |||
| uint8_t axis; | |||
| for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) | |||
| { | |||
| commonBase = (uint16_t)(900U | |||
| + (uint16_t)axis | |||
| * SELF_TEST_SFD_AXIS_STRIDE); | |||
| setBase = (uint16_t)(commonBase + SELF_TEST_SFD_SET_OFFSET); | |||
| limitSetBase = (uint16_t)(setBase + 20U); | |||
| abSetBase = (uint16_t)(setBase + 40U); | |||
| /* PULSE/DIR, pulse-unit soft limits, one direction point per axis. */ | |||
| (void)PlcDeviceWriteSfd(commonBase, (1U << 2U)); | |||
| SelfTestWriteSfdDword((uint16_t)(commonBase + 2U), 1UL); | |||
| SelfTestWriteSfdDword((uint16_t)(commonBase + 4U), 1UL); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 6U), | |||
| (uint16_t)(SELF_TEST_DIR_POINT + axis)); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 7U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 8U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 9U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 12U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 15U), 0xFFFFU); | |||
| SelfTestWriteSfdDword((uint16_t)(commonBase + 30U), 1000000UL); | |||
| SelfTestWriteSfdDword((uint16_t)(commonBase + 32U), | |||
| (uint32_t)(int32_t)-1000000); | |||
| /* Exact 100kHz plateau. 200000-pulse S0 jobs cross the 16-bit | |||
| * counter boundary three times while avoiding profile ramp effects. */ | |||
| SelfTestWriteSfdDword(setBase, 100000UL); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(setBase + 2U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(setBase + 3U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(setBase + 4U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(setBase + 5U), 0U); | |||
| SelfTestWriteSfdDword((uint16_t)(setBase + 6U), 100000UL); | |||
| SelfTestWriteSfdDword((uint16_t)(setBase + 8U), 100000UL); | |||
| SelfTestWriteSfdDword((uint16_t)(setBase + 10U), 0UL); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(setBase + 12U), 50U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(setBase + 13U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(setBase + 14U), 0U); | |||
| SelfTestWriteSfdDword((uint16_t)(setBase + 16U), 2000UL); | |||
| SelfTestWriteSfdDword((uint16_t)(setBase + 18U), 200UL); | |||
| /* K2: P15 limit matrix. Even axes use 500Hz, odd axes 2000Hz; | |||
| * start at target speed and decelerate for 100ms at a soft limit. */ | |||
| limitFrequencyHz = ((axis & 1U) == 0U) ? 500UL : 2000UL; | |||
| SelfTestWriteSfdDword(limitSetBase, limitFrequencyHz); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 2U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 3U), 100U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 4U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 5U), 0U); | |||
| SelfTestWriteSfdDword((uint16_t)(limitSetBase + 6U), | |||
| limitFrequencyHz); | |||
| SelfTestWriteSfdDword((uint16_t)(limitSetBase + 8U), | |||
| limitFrequencyHz); | |||
| SelfTestWriteSfdDword((uint16_t)(limitSetBase + 10U), 0UL); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 12U), 50U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 13U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 14U), 0U); | |||
| SelfTestWriteSfdDword((uint16_t)(limitSetBase + 16U), 2000UL); | |||
| SelfTestWriteSfdDword((uint16_t)(limitSetBase + 18U), 200UL); | |||
| /* K3: dual-AB 100kHz plateau. TIM9/TIM12 count complete-cycle | |||
| * source events so the output timers only interrupt for rephase and | |||
| * the guarded final 00 boundary. */ | |||
| SelfTestWriteSfdDword(abSetBase, 100000UL); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 2U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 3U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 4U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 5U), 0U); | |||
| SelfTestWriteSfdDword((uint16_t)(abSetBase + 6U), 100000UL); | |||
| SelfTestWriteSfdDword((uint16_t)(abSetBase + 8U), 100000UL); | |||
| SelfTestWriteSfdDword((uint16_t)(abSetBase + 10U), 0UL); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 12U), 50U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 13U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 14U), 0U); | |||
| SelfTestWriteSfdDword((uint16_t)(abSetBase + 16U), 2000UL); | |||
| SelfTestWriteSfdDword((uint16_t)(abSetBase + 18U), 200UL); | |||
| } | |||
| return PLSR_RESULT_OK; | |||
| } | |||
| PLSR_RESULT PlsrLongStressSelfTestPrepare(void) | |||
| { | |||
| uint16_t commonBase; | |||
| uint16_t longSetBase; | |||
| uint8_t axis; | |||
| for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) | |||
| { | |||
| commonBase = (uint16_t)(900U | |||
| + (uint16_t)axis | |||
| * SELF_TEST_SFD_AXIS_STRIDE); | |||
| longSetBase = (uint16_t)(commonBase | |||
| + SELF_TEST_SFD_SET_OFFSET | |||
| + 60U); /* K4 occupies the final 20 words. */ | |||
| /* P18 is a bench-only PULSE/DIR endurance fixture. Keep the proven | |||
| * Q4..Q7 direction mapping and disable both hard-input assignments and | |||
| * soft limits so the deliberately long positive jobs cannot stop at | |||
| * the P14/P15 +/-1000000-pulse validation boundary. */ | |||
| (void)PlcDeviceWriteSfd(commonBase, 0U); | |||
| SelfTestWriteSfdDword((uint16_t)(commonBase + 2U), 1UL); | |||
| SelfTestWriteSfdDword((uint16_t)(commonBase + 4U), 1UL); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 6U), | |||
| (uint16_t)(SELF_TEST_DIR_POINT + axis)); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 7U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 8U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 9U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 12U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 15U), 0xFFFFU); | |||
| SelfTestWriteSfdDword((uint16_t)(commonBase + 30U), 0UL); | |||
| SelfTestWriteSfdDword((uint16_t)(commonBase + 32U), 0UL); | |||
| /* K4: exact 100kHz plateau, no acceleration/deceleration ramp. */ | |||
| SelfTestWriteSfdDword(longSetBase, 100000UL); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 2U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 3U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 4U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 5U), 0U); | |||
| SelfTestWriteSfdDword((uint16_t)(longSetBase + 6U), 100000UL); | |||
| SelfTestWriteSfdDword((uint16_t)(longSetBase + 8U), 100000UL); | |||
| SelfTestWriteSfdDword((uint16_t)(longSetBase + 10U), 0UL); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 12U), 50U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 13U), 0U); | |||
| (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 14U), 0U); | |||
| SelfTestWriteSfdDword((uint16_t)(longSetBase + 16U), 2000UL); | |||
| SelfTestWriteSfdDword((uint16_t)(longSetBase + 18U), 200UL); | |||
| } | |||
| return PLSR_RESULT_OK; | |||
| } | |||
| #endif | |||
| @@ -57,11 +57,18 @@ static void TestAddressMap(void) | |||
| static void TestHsdAndPersistence(void) | |||
| { | |||
| static const uint8_t crcVector[] = "123456789"; | |||
| uint16_t value16; | |||
| int32_t value32; | |||
| const int32_t firstDword = INT32_C(0x12345678); | |||
| const int32_t secondDword = -INT32_C(0x1020304); | |||
| TEST_CHECK(PlsrPersistenceTestCrc32(crcVector, | |||
| sizeof(crcVector) - 1UL) | |||
| == 0xCBF43926UL); | |||
| TEST_CHECK(PlsrPersistenceTestCrc32(NULL, 0UL) == 0UL); | |||
| TEST_CHECK(PlsrPersistenceTestCrc32(NULL, 1UL) == 0UL); | |||
| PlsrPersistenceTestResetStorage(); | |||
| TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); | |||
| TEST_CHECK(PlcDeviceGetLastHsdLoadResult() == PLSR_PERSISTENCE_DEFAULTED); | |||
| @@ -349,12 +349,29 @@ static void TestPositionCheckpointing(void) | |||
| PLSR_STATUS status; | |||
| int32_t hsdPosition; | |||
| int32_t hsdEquivalent; | |||
| uint8_t axis; | |||
| /* 1. SET_POSITION 后立即写入 HSD 检查点。 */ | |||
| /* 1. 全局position-valid必须保守聚合:只建立轴0坐标时,重启 | |||
| * 后不得把其他三条未校准轴一起判为可信。 */ | |||
| TestReset(); | |||
| TEST_CHECK(TestPostCommand(1U, 0U, PLSR_CMD_SET_POSITION, 500) | |||
| == PLSR_RESULT_QUEUED); | |||
| PlsrProcess(); | |||
| TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); | |||
| TEST_CHECK(PlcDeviceGetRestoredHsdPositionValid() == 0U); | |||
| /* 2. 同一轮的四轴 SET_POSITION 合并为一个完整 HSD 检查点。 */ | |||
| TestReset(); | |||
| for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) | |||
| { | |||
| TEST_CHECK(TestPostCommand((uint32_t)axis + 1UL, | |||
| axis, | |||
| PLSR_CMD_SET_POSITION, | |||
| (axis == 0U) ? 500 : 0) | |||
| == PLSR_RESULT_QUEUED); | |||
| } | |||
| PlsrProcess(); | |||
| TEST_CHECK(PlsrPersistenceTestGetHsdSaveCount() == 1UL); | |||
| status = TestGetStatus(0U); | |||
| TEST_CHECK(status.lastCommandResult == PLSR_RESULT_OK); | |||
| TEST_CHECK(status.positionValid != 0U); | |||
| @@ -364,14 +381,14 @@ static void TestPositionCheckpointing(void) | |||
| TEST_CHECK(PlcDeviceReadHsdDword(2U, &hsdEquivalent) == PLC_DEVICE_OK); | |||
| TEST_CHECK(hsdEquivalent == 500); | |||
| /* 2. 模拟重启:位置与 positionValid 应恢复到上次正常停机的检查点。 */ | |||
| /* 3. 模拟重启:位置与 positionValid 应恢复到上次正常停机的检查点。 */ | |||
| TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); | |||
| TEST_CHECK(PlsrInit() == PLSR_RESULT_OK); | |||
| status = TestGetStatus(0U); | |||
| TEST_CHECK(status.positionValid != 0U); | |||
| TEST_CHECK(status.logicalPosition == 500); | |||
| /* 3. 运动中掉电(lastBusy=1):恢复后位置不可信。 */ | |||
| /* 4. 运动中掉电(lastBusy=1):恢复后位置不可信。 */ | |||
| TEST_CHECK(PlcDeviceSetHsdCheckpointMeta(1U, 1U) == PLC_DEVICE_OK); | |||
| TEST_CHECK(PlcDeviceCheckpointHsd() == PLC_DEVICE_OK); | |||
| TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); | |||
| @@ -380,8 +397,18 @@ static void TestPositionCheckpointing(void) | |||
| TEST_CHECK(status.positionValid == 0U); | |||
| TEST_CHECK(status.logicalPosition == 500); | |||
| /* 4. 64位位置超出 INT32 范围:保留内部值、HSD保持最近合法值。 */ | |||
| TEST_CHECK(TestPostCommand(2U, | |||
| /* 5. 先重新建立四轴有效坐标,再验证64位位置超出INT32范围: | |||
| * 保留内部值、HSD保持最近合法值,但持久化有效位必须清除。 */ | |||
| for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) | |||
| { | |||
| TEST_CHECK(TestPostCommand(10UL + (uint32_t)axis, | |||
| axis, | |||
| PLSR_CMD_SET_POSITION, | |||
| (axis == 0U) ? 500 : 0) | |||
| == PLSR_RESULT_QUEUED); | |||
| } | |||
| PlsrProcess(); | |||
| TEST_CHECK(TestPostCommand(20U, | |||
| 0U, | |||
| PLSR_CMD_SET_POSITION, | |||
| INT64_C(0x100000000)) | |||
| @@ -396,8 +423,17 @@ static void TestPositionCheckpointing(void) | |||
| TEST_CHECK(PlcDeviceReadHsdDword(2U, &hsdEquivalent) == PLC_DEVICE_OK); | |||
| TEST_CHECK(hsdEquivalent == 500); | |||
| /* 5. 清零位置重新建立有效坐标,并解除兼容发布溢出锁存。 */ | |||
| TEST_CHECK(TestPostCommand(3U, 0U, PLSR_CMD_CLEAR_POSITION, 0) | |||
| /* HSD只能保存32位位置;发生发布溢出后,旧的合法HSD值不得在 | |||
| * 重启时冒充当前可信坐标。 */ | |||
| TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); | |||
| TEST_CHECK(PlcDeviceGetRestoredHsdPositionValid() == 0U); | |||
| TEST_CHECK(PlsrInit() == PLSR_RESULT_OK); | |||
| status = TestGetStatus(0U); | |||
| TEST_CHECK(status.positionValid == 0U); | |||
| TEST_CHECK(status.logicalPosition == 500); | |||
| /* 6. 清零位置重新建立有效坐标,并解除兼容发布溢出锁存。 */ | |||
| TEST_CHECK(TestPostCommand(21U, 0U, PLSR_CMD_CLEAR_POSITION, 0) | |||
| == PLSR_RESULT_QUEUED); | |||
| PlsrProcess(); | |||
| status = TestGetStatus(0U); | |||
| @@ -410,6 +446,28 @@ static void TestPositionCheckpointing(void) | |||
| TEST_CHECK(hsdEquivalent == 0); | |||
| } | |||
| static void TestMultiAxisBusyCheckpoint(void) | |||
| { | |||
| /* 轴0结束时轴1仍在运动,持久化lastBusy必须保持1。 */ | |||
| TestReset(); | |||
| TEST_CHECK(TestPostStart(100U, | |||
| 0U, | |||
| PLSR_OUTPUT_PULSE_DIR, | |||
| 4U, | |||
| 1U) == PLSR_RESULT_QUEUED); | |||
| TEST_CHECK(TestPostStart(101U, | |||
| 1U, | |||
| PLSR_OUTPUT_PULSE_DIR, | |||
| 5U, | |||
| 1U) == PLSR_RESULT_QUEUED); | |||
| PlsrProcess(); | |||
| TEST_CHECK(PlsrPostEvent(0U, PLSR_EVENT_JOB_COMPLETE) | |||
| == PLSR_RESULT_OK); | |||
| PlsrProcess(); | |||
| TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); | |||
| TEST_CHECK(PlcDeviceGetRestoredHsdLastBusy() != 0U); | |||
| } | |||
| int main(void) | |||
| { | |||
| TestResourceManager(); | |||
| @@ -417,6 +475,7 @@ int main(void) | |||
| TestLimitWaitAndPairs(); | |||
| TestQueueAndInputValidation(); | |||
| TestPositionCheckpointing(); | |||
| TestMultiAxisBusyCheckpoint(); | |||
| (void)printf("PASS: %u PLSR core checks\n", TestCount); | |||
| return EXIT_SUCCESS; | |||
| @@ -123,20 +123,6 @@ static void TestResetEnvironment(void) | |||
| CHECK(PlsrInit() == PLSR_RESULT_OK); | |||
| } | |||
| static void TestCompleteFirstAbPrime(uint8_t axis) | |||
| { | |||
| int quarter; | |||
| CHECK(PlsrHwIsAbStartupPriming(axis) != 0U); | |||
| for (quarter = 0; quarter < 4; quarter++) | |||
| { | |||
| PlsrHwTestAdvanceAbQuarter(axis); | |||
| } | |||
| CHECK(PlsrHwIsAbStartupPriming(axis) == 0U); | |||
| CHECK(PlsrHwGetEmittedPulses(axis) == 0); | |||
| CHECK(PlsrHwTestGetAbQuarter(axis) == 0U); | |||
| } | |||
| static PLSR_CALL TestMakeCall(TEST_MEMORY *memory) | |||
| { | |||
| PLSR_CALL call; | |||
| @@ -300,6 +286,55 @@ static void TestDirectionBatch(void) | |||
| CHECK(PlsrHwTestGetDirLevel(1U) == 1U); | |||
| } | |||
| static void TestHardwareCounterLeases(void) | |||
| { | |||
| PLSR_HW_START_PARAMS params; | |||
| (void)PlsrHwInit(); | |||
| (void)memset(¶ms, 0, sizeof(params)); | |||
| params.frequencyHz = 100000UL; | |||
| params.targetPulses = 10; | |||
| params.outputMode = PLSR_OUTPUT_PULSE_DIR; | |||
| params.directionPoint = 4U; | |||
| params.directionPositive = 1U; | |||
| CHECK(PlsrHwStartPulse(0U, ¶ms) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwUsesHardwareCounter(0U) == 1U); | |||
| /* Replacing a prepared segment must release and reacquire the same lease | |||
| * instead of orphaning TIM9 under the old preparation. */ | |||
| CHECK(PlsrHwStartPulse(0U, ¶ms) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwUsesHardwareCounter(0U) == 1U); | |||
| CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); | |||
| /* A hardware-counted PWM starts in the inactive half-cycle. TIM9/TIM12 | |||
| * must see a low ITR level when external-clock mode is armed, otherwise | |||
| * the startup OCREF level is counted before a terminal pulse exists. */ | |||
| CHECK(PlsrHwTestGetCnt(0U) == PlsrHwTestGetCcr(0U)); | |||
| params.directionPoint = 5U; | |||
| CHECK(PlsrHwStartPulse(2U, ¶ms) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwUsesHardwareCounter(2U) == 0U); | |||
| CHECK(PlsrHwSetFrequency(2U, 100000UL) == PLSR_RESULT_OK); | |||
| /* PWM compare and update flags coexist at the period boundary. A | |||
| * software fallback axis must consume both in one ISR and count once. */ | |||
| PlsrHwTestTriggerUpdateAndCompare(2U); | |||
| CHECK(PlsrHwGetEmittedPulses(2U) == 1); | |||
| PlsrHwTestTriggerCompare(2U); | |||
| CHECK(PlsrHwGetEmittedPulses(2U) == 1); | |||
| CHECK(PlsrHwStopPulse(0U) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwStopPulse(2U) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwUsesHardwareCounter(0U) == 0U); | |||
| params.directionPoint = 5U; | |||
| CHECK(PlsrHwStartPulse(2U, ¶ms) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwUsesHardwareCounter(2U) == 1U); | |||
| CHECK(PlsrHwSetFrequency(2U, 100000UL) == PLSR_RESULT_OK); | |||
| while (PlsrHwGetState(2U) != PLSR_HW_STATE_DONE) | |||
| { | |||
| PlsrHwTestTriggerUpdate(2U); | |||
| } | |||
| CHECK(PlsrHwGetEmittedPulses(2U) == 10); | |||
| CHECK(PlsrHwUsesHardwareCounter(2U) == 0U); | |||
| } | |||
| static void TestCwCcwSequence(void) | |||
| { | |||
| PLSR_HW_START_PARAMS params; | |||
| @@ -622,9 +657,9 @@ static void TestAbPhaseAndCounting(void) | |||
| == (PlsrHwTestGetArr(0U) + 1UL) / 2UL); | |||
| /* 两相从精确 00 边界起步;CC1IF 会在开中断前再次清除。 */ | |||
| CHECK(PlsrHwTestGetCnt(0U) | |||
| == ((PlsrHwTestGetArr(0U) + 1UL) * 3UL) / 4UL); | |||
| CHECK(PlsrHwTestGetCnt(1U) == PlsrHwTestGetCcr(1U)); | |||
| TestCompleteFirstAbPrime(0U); | |||
| == ((PlsrHwTestGetArr(0U) + 1UL) * 3UL) / 4UL + 1UL); | |||
| CHECK(PlsrHwTestGetCnt(1U) == PlsrHwTestGetCcr(1U) + 1UL); | |||
| CHECK(PlsrHwIsAbStartupPriming(0U) == 0U); | |||
| /* 任一物理 timer update 不能直接计作完整 AB 周期。 */ | |||
| PlsrHwTestTriggerUpdate(0U); | |||
| @@ -680,8 +715,8 @@ static void TestAbPhaseAndCounting(void) | |||
| == 2UL * (PlsrHwTestGetPsc(1U) + 1UL)); | |||
| CHECK(PlsrHwTestGetArr(0U) == PlsrHwTestGetArr(1U)); | |||
| newPeriod = PlsrHwTestGetArr(0U) + 1UL; | |||
| CHECK(PlsrHwTestGetCnt(0U) == (newPeriod * 3UL) / 4UL); | |||
| CHECK(PlsrHwTestGetCnt(1U) == newPeriod / 2UL); | |||
| CHECK(PlsrHwTestGetCnt(0U) == (newPeriod * 3UL) / 4UL + 1UL); | |||
| CHECK(PlsrHwTestGetCnt(1U) == newPeriod / 2UL + 1UL); | |||
| for (quarter = 0; quarter < 4; quarter++) | |||
| { | |||
| @@ -689,6 +724,7 @@ static void TestAbPhaseAndCounting(void) | |||
| } | |||
| CHECK(PlsrHwGetEmittedPulses(0U) == 4); | |||
| CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); | |||
| PlsrHwTick(0U); | |||
| CHECK(PlsrHwTestGetPwmEnabled(0U) == 0U); | |||
| CHECK(PlsrHwTestGetPwmEnabled(1U) == 0U); | |||
| CHECK(PlsrHwTestGetAbPhaseA(0U) == 0U); | |||
| @@ -707,6 +743,7 @@ static void TestAbPhaseAndCounting(void) | |||
| } | |||
| CHECK(PlsrHwGetEmittedPulses(0U) == 1); | |||
| CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); | |||
| PlsrHwTick(0U); | |||
| /* 紧急停止即使发生在周期中间,也必须回到安全 00。 */ | |||
| params.targetPulses = 10; | |||
| @@ -742,14 +779,12 @@ static void TestTwoAbAxesIndependent(void) | |||
| CHECK(PlsrHwTestGetPwmEnabled(2U) != 0U); | |||
| CHECK(PlsrHwTestGetPwmEnabled(3U) != 0U); | |||
| TestCompleteFirstAbPrime(0U); | |||
| TestCompleteFirstAbPrime(2U); | |||
| for (quarter = 0; quarter < 4; quarter++) | |||
| { | |||
| PlsrHwTestAdvanceAbQuarter(0U); | |||
| } | |||
| CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); | |||
| PlsrHwTick(0U); | |||
| CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_RUNNING); | |||
| CHECK(PlsrHwGetEmittedPulses(2U) == 0); | |||
| CHECK(PlsrHwTestGetPwmEnabled(0U) == 0U); | |||
| @@ -763,11 +798,164 @@ static void TestTwoAbAxesIndependent(void) | |||
| } | |||
| CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_DONE); | |||
| CHECK(PlsrHwGetEmittedPulses(2U) == 1); | |||
| PlsrHwTick(2U); | |||
| } | |||
| static void TestDualAbHardwareCountersAndResume(void) | |||
| { | |||
| PLSR_HW_START_PARAMS params; | |||
| int quarter; | |||
| int cycle; | |||
| (void)PlsrHwInit(); | |||
| (void)memset(¶ms, 0, sizeof(params)); | |||
| params.frequencyHz = 100000UL; | |||
| params.targetPulses = 6; | |||
| params.outputMode = PLSR_OUTPUT_AB; | |||
| params.directionPoint = PLSR_HW_DIR_POINT_NONE; | |||
| params.directionPositive = 1U; | |||
| CHECK(PlsrHwStartPulse(0U, ¶ms) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwStartPulse(2U, ¶ms) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwUsesHardwareCounter(0U) == 1U); | |||
| CHECK(PlsrHwUsesHardwareCounter(2U) == 1U); | |||
| CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwSetFrequency(2U, 100000UL) == PLSR_RESULT_OK); | |||
| for (cycle = 0; cycle < 2; cycle++) | |||
| { | |||
| for (quarter = 0; quarter < 4; quarter++) | |||
| { | |||
| PlsrHwTestAdvanceAbQuarter(0U); | |||
| } | |||
| } | |||
| for (quarter = 0; quarter < 4; quarter++) | |||
| { | |||
| PlsrHwTestAdvanceAbQuarter(2U); | |||
| } | |||
| CHECK(PlsrHwGetEmittedPulses(0U) == 2); | |||
| CHECK(PlsrHwGetEmittedPulses(2U) == 1); | |||
| /* Request PAUSE after the source edge but before the following 00. The | |||
| * pair must finish this already-started cycle instead of forcing GPIO 00 | |||
| * and re-emitting its source edge after RESUME. */ | |||
| PlsrHwTestAdvanceAbQuarter(0U); | |||
| CHECK(PlsrHwGetEmittedPulses(0U) == 2); | |||
| CHECK(PlsrHwSetFrequency(0U, 0UL) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwTestGetAbQuarter(0U) == 1U); | |||
| CHECK((PlsrHwTestGetCr1(0U) & 1UL) != 0UL); | |||
| CHECK((PlsrHwTestGetCr1(1U) & 1UL) != 0UL); | |||
| for (quarter = 1; quarter < 4; quarter++) | |||
| { | |||
| PlsrHwTestAdvanceAbQuarter(0U); | |||
| } | |||
| CHECK(PlsrHwGetEmittedPulses(0U) == 3); | |||
| CHECK((PlsrHwTestGetCr1(0U) & 1UL) == 0UL); | |||
| CHECK((PlsrHwTestGetCr1(1U) & 1UL) == 0UL); | |||
| CHECK(PlsrHwResumePulse(0U) == PLSR_RESULT_INVALID_STATE); | |||
| PlsrHwTick(0U); | |||
| CHECK(PlsrHwGetEmittedPulses(0U) == 3); | |||
| CHECK(PlsrHwResumePulse(0U) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); | |||
| for (cycle = 0; cycle < 3; cycle++) | |||
| { | |||
| for (quarter = 0; quarter < 4; quarter++) | |||
| { | |||
| PlsrHwTestAdvanceAbQuarter(0U); | |||
| } | |||
| } | |||
| CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); | |||
| CHECK(PlsrHwGetEmittedPulses(0U) == 6); | |||
| PlsrHwTick(0U); | |||
| CHECK(PlsrHwUsesHardwareCounter(0U) == 0U); | |||
| CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_RUNNING); | |||
| for (cycle = 1; cycle < 6; cycle++) | |||
| { | |||
| for (quarter = 0; quarter < 4; quarter++) | |||
| { | |||
| PlsrHwTestAdvanceAbQuarter(2U); | |||
| } | |||
| } | |||
| CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_DONE); | |||
| CHECK(PlsrHwGetEmittedPulses(2U) == 6); | |||
| PlsrHwTick(2U); | |||
| CHECK(PlsrHwUsesHardwareCounter(2U) == 0U); | |||
| } | |||
| static void TestDualAbSimultaneousFastGate(void) | |||
| { | |||
| PLSR_HW_START_PARAMS params; | |||
| (void)PlsrHwInit(); | |||
| (void)memset(¶ms, 0, sizeof(params)); | |||
| params.frequencyHz = 100000UL; | |||
| params.targetPulses = 200000; | |||
| params.outputMode = PLSR_OUTPUT_AB; | |||
| params.directionPoint = PLSR_HW_DIR_POINT_NONE; | |||
| params.directionPositive = 1U; | |||
| CHECK(PlsrHwStartPulse(0U, ¶ms) == PLSR_RESULT_OK); | |||
| params.directionPositive = 0U; | |||
| CHECK(PlsrHwStartPulse(2U, ¶ms) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwSetFrequency(2U, 100000UL) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwUsesHardwareCounter(0U) == 1U); | |||
| CHECK(PlsrHwUsesHardwareCounter(2U) == 1U); | |||
| /* Both lag flags become pending before the first equal-priority handler. | |||
| * Its entry scan must freeze all four timers before publishing axis 0. */ | |||
| PlsrHwTestSignalDualAbFinalBoundary(0U); | |||
| CHECK(PlsrHwTestGetAbFullGateCount() == 1UL); | |||
| CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); | |||
| CHECK(PlsrHwGetEmittedPulses(0U) == 200000); | |||
| CHECK((PlsrHwTestGetCr1(2U) & 1UL) == 0UL); | |||
| CHECK((PlsrHwTestGetCr1(3U) & 1UL) == 0UL); | |||
| CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_RUNNING); | |||
| /* The lag axis for negative Q2/Q3 is Q2. */ | |||
| PlsrHwTestTriggerCompare(2U); | |||
| CHECK(PlsrHwTestGetAbFullGateCount() == 2UL); | |||
| CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_DONE); | |||
| CHECK(PlsrHwGetEmittedPulses(2U) == 200000); | |||
| PlsrHwTick(0U); | |||
| PlsrHwTick(2U); | |||
| CHECK(PlsrHwUsesHardwareCounter(0U) == 0U); | |||
| CHECK(PlsrHwUsesHardwareCounter(2U) == 0U); | |||
| /* Repeat with pair 2's flag injected after the first entry scan. The | |||
| * second scan must gate it before pair 0 performs deferred GPIO/counter | |||
| * cleanup, and both pairs must still be frozen at a real 00 boundary. */ | |||
| (void)PlsrHwInit(); | |||
| params.directionPositive = 1U; | |||
| CHECK(PlsrHwStartPulse(0U, ¶ms) == PLSR_RESULT_OK); | |||
| params.directionPositive = 0U; | |||
| CHECK(PlsrHwStartPulse(2U, ¶ms) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwSetFrequency(2U, 100000UL) == PLSR_RESULT_OK); | |||
| PlsrHwTestSignalDualAbStaggeredFinalBoundary(0U); | |||
| CHECK(PlsrHwTestGetAbFullGateCount() == 1UL); | |||
| CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); | |||
| CHECK(PlsrHwGetEmittedPulses(0U) == 200000); | |||
| CHECK((PlsrHwTestGetCr1(2U) & 1UL) == 0UL); | |||
| CHECK((PlsrHwTestGetCr1(3U) & 1UL) == 0UL); | |||
| CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_RUNNING); | |||
| PlsrHwTestTriggerCompare(2U); | |||
| CHECK(PlsrHwTestGetAbFullGateCount() == 2UL); | |||
| CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_DONE); | |||
| CHECK(PlsrHwGetEmittedPulses(2U) == 200000); | |||
| PlsrHwTick(0U); | |||
| PlsrHwTick(2U); | |||
| CHECK(PlsrHwUsesHardwareCounter(0U) == 0U); | |||
| CHECK(PlsrHwUsesHardwareCounter(2U) == 0U); | |||
| } | |||
| static void TestAbFrequencyLimits(void) | |||
| { | |||
| PLSR_HW_START_PARAMS params; | |||
| uint32_t oldArr; | |||
| int quarter; | |||
| (void)PlsrHwInit(); | |||
| @@ -785,13 +973,17 @@ static void TestAbFrequencyLimits(void) | |||
| == 2UL * (PlsrHwTestGetPsc(1U) + 1UL)); | |||
| CHECK(PlsrHwTestGetArr(0U) <= 65535UL); | |||
| TestCompleteFirstAbPrime(0U); | |||
| oldArr = PlsrHwTestGetArr(0U); | |||
| CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); | |||
| CHECK(PlsrHwTestGetArr(0U) == oldArr); | |||
| for (quarter = 0; quarter < 4; quarter++) | |||
| { | |||
| PlsrHwTestAdvanceAbQuarter(0U); | |||
| } | |||
| /* This request is 99 cycles away from target-1: it must be applied by | |||
| * the frequency one-shot CCIE, not accidentally by the final guard. */ | |||
| CHECK(PlsrHwTestGetArr(0U) != oldArr); | |||
| CHECK(PlsrHwTestGetArr(0U) == 839UL); | |||
| CHECK(PlsrHwTestGetArr(0U) == PlsrHwTestGetArr(1U)); | |||
| CHECK((PlsrHwTestGetPsc(0U) + 1UL) | |||
| == 2UL * (PlsrHwTestGetPsc(1U) + 1UL)); | |||
| @@ -969,8 +1161,6 @@ static void TestEndToEndAbSegment(void) | |||
| CHECK(PlsrHwTestGetPwmEnabled(0U) != 0U); | |||
| CHECK(PlsrHwTestGetPwmEnabled(1U) != 0U); | |||
| TestCompleteFirstAbPrime(0U); | |||
| /* 负脉冲选择反向相序,完整两个周期后由同一事件链结束任务。 */ | |||
| PlsrHwTestAdvanceAbQuarter(0U); | |||
| CHECK(PlsrHwTestGetAbPhaseA(0U) == 0U); | |||
| @@ -982,6 +1172,7 @@ static void TestEndToEndAbSegment(void) | |||
| CHECK(PlsrHwGetEmittedPulses(0U) == 2); | |||
| CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); | |||
| PlsrProcess(); | |||
| PlsrProcess(); | |||
| status = TestGetStatus(); | |||
| CHECK(status.state == PLSR_STATE_COMPLETED); | |||
| CHECK(status.done != 0U); | |||
| @@ -1211,6 +1402,7 @@ static void TestSoftLimitAndSegmentEvent(void) | |||
| int32_t errorCode; | |||
| int pulse; | |||
| int tick; | |||
| uint32_t pulsePhase = 0UL; | |||
| TestResetEnvironment(); | |||
| CHECK(PlcDeviceWriteSfd(900U, (1U << 2U)) == PLC_DEVICE_OK); | |||
| @@ -1303,6 +1495,97 @@ static void TestSoftLimitAndSegmentEvent(void) | |||
| CHECK(PlcDeviceReadEvent(6000U, &eventRecord) == PLC_DEVICE_OK); | |||
| CHECK(eventRecord.count == 1UL); | |||
| CHECK(eventRecord.lastReason == PLSR_STOP_REASON_LIMIT_POSITIVE); | |||
| /* Board-equivalent time simulation for the +500 precision case. Each | |||
| * loop represents 1 ms and emits complete hardware periods according to | |||
| * the frequency that was active during that interval. */ | |||
| TestResetEnvironment(); | |||
| CHECK(PlcDeviceWriteSfd(900U, (1U << 2U)) == PLC_DEVICE_OK); | |||
| CHECK(PlcDeviceWriteSfd(907U, 0U) == PLC_DEVICE_OK); | |||
| TestWriteSfdDword(930U, 500UL); | |||
| TestWriteSfdDword(932U, (uint32_t)(int32_t)-500); | |||
| TestWriteSfdDword(950U, 1000UL); | |||
| CHECK(PlcDeviceWriteSfd(952U, 100U) == PLC_DEVICE_OK); | |||
| CHECK(PlcDeviceWriteSfd(953U, 100U) == PLC_DEVICE_OK); | |||
| TestWriteSfdDword(958U, 1000UL); | |||
| TestWriteSfdDword(960U, 0UL); | |||
| command.sequence = 45UL; | |||
| command.opcode = PLSR_CMD_SET_POSITION; | |||
| command.argument = 0; | |||
| CHECK(PlsrPostCommand(&command) == PLSR_RESULT_QUEUED); | |||
| PlsrProcess(); | |||
| (void)memset(&memory, 0, sizeof(memory)); | |||
| TestWriteDword(&memory, PLSR_DEVICE_D, TEST_S0_BASE, 1); | |||
| TestSetSegment(&memory, 1U, 1000U, 10000); | |||
| call = TestMakeCall(&memory); | |||
| call.sequence = 46UL; | |||
| CHECK(PlsrPostCall(&call) == PLSR_RESULT_QUEUED); | |||
| PlsrProcess(); | |||
| for (tick = 0; tick < 1000; tick++) | |||
| { | |||
| pulsePhase += PlsrHwGetCurrentFrequencyHz(0U); | |||
| while ((pulsePhase >= 1000UL) | |||
| && (PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING)) | |||
| { | |||
| pulsePhase -= 1000UL; | |||
| PlsrHwTestTriggerUpdate(0U); | |||
| } | |||
| PlsrProcess(); | |||
| status = TestGetStatus(); | |||
| if (status.state == PLSR_STATE_STOPPED) | |||
| { | |||
| break; | |||
| } | |||
| } | |||
| CHECK(status.state == PLSR_STATE_STOPPED); | |||
| CHECK(status.stopReason == PLSR_STOP_REASON_LIMIT_POSITIVE); | |||
| CHECK(status.logicalPosition >= 499); | |||
| CHECK(status.logicalPosition <= 501); | |||
| /* Negative/high-speed companion case for the P15 board matrix. */ | |||
| TestResetEnvironment(); | |||
| pulsePhase = 0UL; | |||
| CHECK(PlcDeviceWriteSfd(900U, (1U << 2U)) == PLC_DEVICE_OK); | |||
| CHECK(PlcDeviceWriteSfd(907U, 0U) == PLC_DEVICE_OK); | |||
| TestWriteSfdDword(930U, 1000UL); | |||
| TestWriteSfdDword(932U, (uint32_t)(int32_t)-1000); | |||
| TestWriteSfdDword(950U, 2000UL); | |||
| CHECK(PlcDeviceWriteSfd(952U, 100U) == PLC_DEVICE_OK); | |||
| CHECK(PlcDeviceWriteSfd(953U, 100U) == PLC_DEVICE_OK); | |||
| TestWriteSfdDword(958U, 2000UL); | |||
| TestWriteSfdDword(960U, 0UL); | |||
| command.sequence = 47UL; | |||
| command.opcode = PLSR_CMD_SET_POSITION; | |||
| command.argument = 0; | |||
| CHECK(PlsrPostCommand(&command) == PLSR_RESULT_QUEUED); | |||
| PlsrProcess(); | |||
| (void)memset(&memory, 0, sizeof(memory)); | |||
| TestWriteDword(&memory, PLSR_DEVICE_D, TEST_S0_BASE, 1); | |||
| TestSetSegment(&memory, 1U, 2000U, -10000); | |||
| call = TestMakeCall(&memory); | |||
| call.sequence = 48UL; | |||
| CHECK(PlsrPostCall(&call) == PLSR_RESULT_QUEUED); | |||
| PlsrProcess(); | |||
| for (tick = 0; tick < 1000; tick++) | |||
| { | |||
| pulsePhase += PlsrHwGetCurrentFrequencyHz(0U); | |||
| while ((pulsePhase >= 1000UL) | |||
| && (PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING)) | |||
| { | |||
| pulsePhase -= 1000UL; | |||
| PlsrHwTestTriggerUpdate(0U); | |||
| } | |||
| PlsrProcess(); | |||
| status = TestGetStatus(); | |||
| if (status.state == PLSR_STATE_STOPPED) | |||
| { | |||
| break; | |||
| } | |||
| } | |||
| CHECK(status.state == PLSR_STATE_STOPPED); | |||
| CHECK(status.stopReason == PLSR_STOP_REASON_LIMIT_NEGATIVE); | |||
| CHECK(status.logicalPosition >= -1001); | |||
| CHECK(status.logicalPosition <= -999); | |||
| } | |||
| static void TestHardLimitAndEmergencyLatch(void) | |||
| @@ -1543,6 +1826,7 @@ static void TestFourAxisSelfTest(void) | |||
| CHECK(PlsrHwGetState(axis) == PLSR_HW_STATE_RUNNING); | |||
| CHECK(PlsrHwGetCurrentFrequencyHz(axis) | |||
| == expectedFrequency[axis]); | |||
| CHECK(status.hardwareCounter == ((axis < 2U) ? 1U : 0U)); | |||
| } | |||
| /* A 0.25ms base slot produces 1/2/3/4kHz update ratios while all four | |||
| @@ -2019,6 +2303,10 @@ static void TestModbusControlProtocol(void) | |||
| uint16_t callResponse[12]; | |||
| uint16_t commandRequest[8] = {0U}; | |||
| uint16_t commandResponse[8]; | |||
| uint16_t controlHeader[8]; | |||
| uint16_t performanceWords[8]; | |||
| uint16_t stagePerformanceWords[PLSR_MODBUS_STAGE_PERFORMANCE_WORDS]; | |||
| uint16_t usbDiagnosticsWords[PLSR_MODBUS_USB_DIAGNOSTICS_WORDS]; | |||
| uint16_t axisStatus[48]; | |||
| uint16_t pulseWords[2]; | |||
| uint32_t generationBegin; | |||
| @@ -2032,6 +2320,54 @@ static void TestModbusControlProtocol(void) | |||
| CHECK(PlsrModbusControlInit((uint16_t)controlBase) == PLSR_RESULT_OK); | |||
| CHECK(PlsrModbusControlIsEnabled() == 1U); | |||
| CHECK(PlsrModbusControlGetBaseAddress() == controlBase); | |||
| for (tick = 0; tick < 8; tick++) | |||
| { | |||
| CHECK(ModbusDataReadWord(MODBUS_DATA_DEVICE_D, | |||
| controlBase + (uint32_t)tick, | |||
| &controlHeader[tick]) == 1U); | |||
| CHECK(ModbusDataReadWord( | |||
| MODBUS_DATA_DEVICE_D, | |||
| controlBase + PLSR_MODBUS_PERFORMANCE_OFFSET | |||
| + (uint32_t)tick, | |||
| &performanceWords[tick]) == 1U); | |||
| } | |||
| CHECK(controlHeader[5] == (uint16_t)(168000000UL & 0xFFFFUL)); | |||
| CHECK(controlHeader[6] == (uint16_t)(168000000UL >> 16U)); | |||
| CHECK(controlHeader[7] == PLSR_MODBUS_PERFORMANCE_VERSION); | |||
| CHECK(controlHeader[3] == (uint16_t)PLSR_MODBUS_WINDOW_WORDS); | |||
| for (tick = 0; tick < 8; tick++) | |||
| { | |||
| CHECK(performanceWords[tick] == 0U); | |||
| } | |||
| for (tick = 0; tick < (int)PLSR_MODBUS_STAGE_PERFORMANCE_WORDS; tick++) | |||
| { | |||
| CHECK(ModbusDataReadWord( | |||
| MODBUS_DATA_DEVICE_D, | |||
| controlBase + PLSR_MODBUS_STAGE_PERFORMANCE_OFFSET | |||
| + (uint32_t)tick, | |||
| &stagePerformanceWords[tick]) == 1U); | |||
| CHECK(stagePerformanceWords[tick] == 0U); | |||
| } | |||
| for (tick = 0; tick < (int)PLSR_MODBUS_USB_DIAGNOSTICS_WORDS; tick++) | |||
| { | |||
| CHECK(ModbusDataReadWord( | |||
| MODBUS_DATA_DEVICE_D, | |||
| controlBase + PLSR_MODBUS_USB_DIAGNOSTICS_OFFSET | |||
| + (uint32_t)tick, | |||
| &usbDiagnosticsWords[tick]) == 1U); | |||
| } | |||
| generationBegin = (uint32_t)usbDiagnosticsWords[0] | |||
| | ((uint32_t)usbDiagnosticsWords[1] << 16U); | |||
| generationEnd = (uint32_t)usbDiagnosticsWords[20] | |||
| | ((uint32_t)usbDiagnosticsWords[21] << 16U); | |||
| CHECK(generationBegin == generationEnd); | |||
| CHECK((generationBegin & 1UL) == 0UL); | |||
| CHECK(usbDiagnosticsWords[2] == | |||
| PLSR_MODBUS_USB_DIAGNOSTICS_VERSION); | |||
| for (tick = 3; tick < 20; tick++) | |||
| { | |||
| CHECK(usbDiagnosticsWords[tick] == 0U); | |||
| } | |||
| s0Words[0] = 1U; | |||
| s0Words[10] = 2000U; | |||
| @@ -2152,6 +2488,7 @@ static void TestModbusControlProtocol(void) | |||
| CHECK(axisStatus[10] == 4U); | |||
| CHECK(axisStatus[11] == 0U); | |||
| CHECK(axisStatus[8] == PLSR_RESULT_OK); | |||
| CHECK(axisStatus[37] == 1U); | |||
| commandRequest[0] = 10U; | |||
| commandRequest[2] = PLSR_CMD_PAUSE; | |||
| @@ -2281,6 +2618,7 @@ int main(void) | |||
| TestMapping(); | |||
| TestDirDelaySequence(); | |||
| TestDirectionBatch(); | |||
| TestHardwareCounterLeases(); | |||
| TestCwCcwSequence(); | |||
| TestFastRefreshControlTick(); | |||
| TestDynamicFrequencyRetarget(); | |||
| @@ -2288,6 +2626,8 @@ int main(void) | |||
| TestPulseCounting(); | |||
| TestAbPhaseAndCounting(); | |||
| TestTwoAbAxesIndependent(); | |||
| TestDualAbHardwareCountersAndResume(); | |||
| TestDualAbSimultaneousFastGate(); | |||
| TestAbFrequencyLimits(); | |||
| TestStopAndInvalidArgs(); | |||
| TestEndToEndTwoSegments(); | |||
| @@ -21,6 +21,7 @@ int main(void) | |||
| PLSR_DATA_SOURCE source; | |||
| uint16_t words[4] = {0x5678U, 0x1234U, 0xFFFEU, 0xFFFFU}; | |||
| uint16_t word; | |||
| uint8_t bit; | |||
| int32_t dword; | |||
| uint32_t sequence; | |||
| @@ -57,12 +58,32 @@ int main(void) | |||
| CHECK(ModbusDataReadLinear(39999UL, &word) == 0U); | |||
| CHECK(ModbusDataReadLinear(69999UL, &word) == 0U); | |||
| CHECK(ModbusDataValidateBits(MODBUS_BIT_DEVICE_X, 9999UL, 1UL) == 1U); | |||
| CHECK(ModbusDataValidateBits(MODBUS_BIT_DEVICE_M, 9999UL, 2UL) == 0U); | |||
| CHECK(ModbusDataValidateBits(MODBUS_BIT_DEVICE_HM, 0UL, 0UL) == 0U); | |||
| CHECK(ModbusDataValidateBits((MODBUS_BIT_DEVICE)3, 0UL, 1UL) == 0U); | |||
| CHECK(ModbusDataWriteBit(MODBUS_BIT_DEVICE_X, 17UL, 1U) == 1U); | |||
| CHECK(ModbusDataWriteBit(MODBUS_BIT_DEVICE_M, 17UL, 0U) == 1U); | |||
| CHECK(ModbusDataWriteBit(MODBUS_BIT_DEVICE_HM, 17UL, 1U) == 1U); | |||
| CHECK(ModbusDataWriteBit(MODBUS_BIT_DEVICE_X, 18UL, 1U) == 1U); | |||
| CHECK(ModbusDataWriteBit(MODBUS_BIT_DEVICE_X, 17UL, 0U) == 1U); | |||
| CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_X, 17UL, &bit) == 1U); | |||
| CHECK(bit == 0U); | |||
| CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_X, 18UL, &bit) == 1U); | |||
| CHECK(bit == 1U); | |||
| CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_M, 17UL, &bit) == 1U); | |||
| CHECK(bit == 0U); | |||
| CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_HM, 17UL, &bit) == 1U); | |||
| CHECK(bit == 1U); | |||
| CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_X, 10000UL, &bit) == 0U); | |||
| CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_X, 0UL, NULL) == 0U); | |||
| PlsrModbusDataSourceInit(&source); | |||
| CHECK(source.context == NULL); | |||
| CHECK(source.validateWords != NULL); | |||
| CHECK(source.readWord != NULL); | |||
| CHECK(source.readDword != NULL); | |||
| CHECK(source.readBit == NULL); | |||
| CHECK(source.readBit != NULL); | |||
| CHECK(source.validateWords(source.context, PLSR_DEVICE_D, 1000UL, 4UL) | |||
| == 1U); | |||
| CHECK(source.validateWords(source.context, PLSR_DEVICE_X, 0UL, 1UL) | |||
| @@ -77,6 +98,14 @@ int main(void) | |||
| CHECK(source.readWord(source.context, PLSR_DEVICE_FD, 5UL, &word) == 1U); | |||
| CHECK(word == 0x5AA5U); | |||
| CHECK(source.readWord(source.context, PLSR_DEVICE_M, 5UL, &word) == 0U); | |||
| CHECK(source.readBit(source.context, PLSR_DEVICE_X, 17UL, &bit) == 1U); | |||
| CHECK(bit == 0U); | |||
| CHECK(source.readBit(source.context, PLSR_DEVICE_M, 17UL, &bit) == 1U); | |||
| CHECK(bit == 0U); | |||
| CHECK(source.readBit(source.context, PLSR_DEVICE_HM, 17UL, &bit) == 1U); | |||
| CHECK(bit == 1U); | |||
| CHECK(source.readBit(source.context, PLSR_DEVICE_D, 17UL, &bit) == 0U); | |||
| CHECK(source.readBit(source.context, PLSR_DEVICE_X, 10000UL, &bit) == 0U); | |||
| PlsrModbusDataSourceInit(NULL); | |||
| (void)printf("PASS: %u Modbus data-source checks\n", TestChecks); | |||
| @@ -95,6 +95,8 @@ uint8_t UserTxBufferFS[APP_TX_DATA_SIZE]; | |||
| /* USER CODE BEGIN PRIVATE_VARIABLES */ | |||
| static volatile USB_CDC_RUNTIME_DIAGNOSTICS CdcRuntimeDiagnostics; | |||
| /* USER CODE END PRIVATE_VARIABLES */ | |||
| /** | |||
| @@ -155,6 +157,7 @@ static int8_t CDC_Init_FS(void) | |||
| /* Set Application Buffers */ | |||
| USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0); | |||
| USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS); | |||
| CdcRuntimeDiagnostics.initialized = 1U; | |||
| return (USBD_OK); | |||
| /* USER CODE END 3 */ | |||
| } | |||
| @@ -166,6 +169,7 @@ static int8_t CDC_Init_FS(void) | |||
| static int8_t CDC_DeInit_FS(void) | |||
| { | |||
| /* USER CODE BEGIN 4 */ | |||
| CdcRuntimeDiagnostics.initialized = 0U; | |||
| return (USBD_OK); | |||
| /* USER CODE END 4 */ | |||
| } | |||
| @@ -261,9 +265,25 @@ static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) | |||
| static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) | |||
| { | |||
| /* USER CODE BEGIN 6 */ | |||
| USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); | |||
| USBD_CDC_ReceivePacket(&hUsbDeviceFS); | |||
| return (USBD_OK); | |||
| uint8_t result; | |||
| if ((Buf == NULL) || (Len == NULL)) | |||
| { | |||
| CdcRuntimeDiagnostics.rxRearmFailureCount++; | |||
| return (USBD_FAIL); | |||
| } | |||
| CdcRuntimeDiagnostics.rxPacketCount++; | |||
| CdcRuntimeDiagnostics.rxByteCount += *Len; | |||
| result = USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); | |||
| if (result == USBD_OK) | |||
| { | |||
| result = USBD_CDC_ReceivePacket(&hUsbDeviceFS); | |||
| } | |||
| if (result != USBD_OK) | |||
| { | |||
| CdcRuntimeDiagnostics.rxRearmFailureCount++; | |||
| } | |||
| return (int8_t)result; | |||
| /* USER CODE END 6 */ | |||
| } | |||
| @@ -283,11 +303,33 @@ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len) | |||
| uint8_t result = USBD_OK; | |||
| /* USER CODE BEGIN 7 */ | |||
| USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData; | |||
| if (hcdc->TxState != 0){ | |||
| CdcRuntimeDiagnostics.txRequestCount++; | |||
| if ((Buf == NULL) || (Len == 0U) || (hcdc == NULL)) | |||
| { | |||
| CdcRuntimeDiagnostics.txFailureCount++; | |||
| return USBD_FAIL; | |||
| } | |||
| if (hcdc->TxState != 0U){ | |||
| CdcRuntimeDiagnostics.txBusyCount++; | |||
| return USBD_BUSY; | |||
| } | |||
| USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len); | |||
| result = USBD_CDC_TransmitPacket(&hUsbDeviceFS); | |||
| result = USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len); | |||
| if (result == USBD_OK) | |||
| { | |||
| result = USBD_CDC_TransmitPacket(&hUsbDeviceFS); | |||
| } | |||
| if (result == USBD_OK) | |||
| { | |||
| CdcRuntimeDiagnostics.txByteCount += Len; | |||
| } | |||
| else if (result == USBD_BUSY) | |||
| { | |||
| CdcRuntimeDiagnostics.txBusyCount++; | |||
| } | |||
| else | |||
| { | |||
| CdcRuntimeDiagnostics.txFailureCount++; | |||
| } | |||
| /* USER CODE END 7 */ | |||
| return result; | |||
| } | |||
| @@ -311,12 +353,37 @@ static int8_t CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len, uint8_t epnum) | |||
| UNUSED(Buf); | |||
| UNUSED(Len); | |||
| UNUSED(epnum); | |||
| CdcRuntimeDiagnostics.txCompleteCount++; | |||
| /* USER CODE END 13 */ | |||
| return result; | |||
| } | |||
| /* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */ | |||
| uint8_t CDC_GetRuntimeDiagnostics(USB_CDC_RUNTIME_DIAGNOSTICS *diagnostics) | |||
| { | |||
| if (diagnostics == NULL) | |||
| { | |||
| return 0U; | |||
| } | |||
| /* Aligned word reads are atomic on Cortex-M4. Do not mask the motion | |||
| * interrupts merely to make unrelated USB counters mutually consistent. */ | |||
| __DMB(); | |||
| diagnostics->rxPacketCount = CdcRuntimeDiagnostics.rxPacketCount; | |||
| diagnostics->rxByteCount = CdcRuntimeDiagnostics.rxByteCount; | |||
| diagnostics->rxRearmFailureCount = | |||
| CdcRuntimeDiagnostics.rxRearmFailureCount; | |||
| diagnostics->txRequestCount = CdcRuntimeDiagnostics.txRequestCount; | |||
| diagnostics->txByteCount = CdcRuntimeDiagnostics.txByteCount; | |||
| diagnostics->txBusyCount = CdcRuntimeDiagnostics.txBusyCount; | |||
| diagnostics->txFailureCount = CdcRuntimeDiagnostics.txFailureCount; | |||
| diagnostics->txCompleteCount = CdcRuntimeDiagnostics.txCompleteCount; | |||
| diagnostics->initialized = CdcRuntimeDiagnostics.initialized; | |||
| __DMB(); | |||
| return 1U; | |||
| } | |||
| /* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION */ | |||
| /** | |||
| @@ -32,6 +32,8 @@ | |||
| /* USER CODE BEGIN INCLUDE */ | |||
| #include <stdint.h> | |||
| /* USER CODE END INCLUDE */ | |||
| /** @addtogroup STM32_USB_OTG_DEVICE_LIBRARY | |||
| @@ -66,6 +68,20 @@ | |||
| /* USER CODE BEGIN EXPORTED_TYPES */ | |||
| /** Read-only USB CDC activity snapshot used by production diagnostics. */ | |||
| typedef struct | |||
| { | |||
| uint32_t rxPacketCount; | |||
| uint32_t rxByteCount; | |||
| uint32_t rxRearmFailureCount; | |||
| uint32_t txRequestCount; | |||
| uint32_t txByteCount; | |||
| uint32_t txBusyCount; | |||
| uint32_t txFailureCount; | |||
| uint32_t txCompleteCount; | |||
| uint8_t initialized; | |||
| } USB_CDC_RUNTIME_DIAGNOSTICS; | |||
| /* USER CODE END EXPORTED_TYPES */ | |||
| /** | |||
| @@ -110,6 +126,8 @@ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len); | |||
| /* USER CODE BEGIN EXPORTED_FUNCTIONS */ | |||
| uint8_t CDC_GetRuntimeDiagnostics(USB_CDC_RUNTIME_DIAGNOSTICS *diagnostics); | |||
| /* USER CODE END EXPORTED_FUNCTIONS */ | |||
| /** | |||
| @@ -91,7 +91,10 @@ void HAL_PCD_MspInit(PCD_HandleTypeDef* pcdHandle) | |||
| __HAL_RCC_USB_OTG_FS_CLK_ENABLE(); | |||
| /* Peripheral interrupt init */ | |||
| HAL_NVIC_SetPriority(OTG_FS_IRQn, 0, 0); | |||
| /* Motion timers run at priorities 1/2. USB is intentionally lower so a | |||
| burst of CDC traffic cannot delay pulse generation or the 10 kHz | |||
| control ISR; it remains above the USART/DMA communication IRQs (5). */ | |||
| HAL_NVIC_SetPriority(OTG_FS_IRQn, 4, 0); | |||
| HAL_NVIC_EnableIRQ(OTG_FS_IRQn); | |||
| /* USER CODE BEGIN USB_OTG_FS_MspInit 1 */ | |||