From 6c6168261746ce36a8d7ae0c051fd910cbbf4e31 Mon Sep 17 00:00:00 2001
From: ywh <2227158009@qq.com>
Date: Sun, 9 Aug 2026 15:08:38 +0800
Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E7=A8=8B=E5=8D=95=E4=BD=8D/=E8=84=89?=
=?UTF-8?q?=E5=86=B2=E5=BD=93=E9=87=8F=E6=8D=A2=E7=AE=97=E8=90=BD=E5=9C=B0?=
=?UTF-8?q?=EF=BC=88=E4=BD=99=E6=95=B0=E7=B4=AF=E8=AE=A1=E7=9C=9F=E6=9C=BA?=
=?UTF-8?q?=E9=AA=8C=E8=AF=81=EF=BC=89+=20=E4=BD=8D=E7=BD=AE=E9=93=BE?=
=?UTF-8?q?=E8=B7=AF=E9=97=AD=E7=8E=AF=20+=20AB=20=E5=90=AF=E5=8A=A8?=
=?UTF-8?q?=E9=A2=84=E7=83=AD=20=E6=9C=AC=E6=89=B9=E5=AE=8C=E6=88=90?=
=?UTF-8?q?=E5=86=85=E5=AE=B9=EF=BC=9A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
工程单位换算(新增 plsr_position 模块):
- 单位代码校验、脉冲数/转与移动量/转等效配置校验。
- 工程单位 ↔ 脉冲双向换算(相对/绝对模式),带溢出检查。
- 段目标按等效配置换算为脉冲,换算余数跨段累计,
避免每段独立取整造成累计位置误差。
任务解析(plsr_job):
- S2 等效配置(单位代码/脉冲每转/移动量每转)解析与校验。
- 段目标支持工程单位形式(相对/绝对),换算失败返回
PLSR_RESULT_POSITION_OVERFLOW 并给出解析块定位。
位置链路(plsr_core):
- logicalPosition/taskPulses/totalPulses 随运动实时累加,
带 int64 溢出检查(溢出发 COUNTER_FAULT)。
- HSD 掉电检查点:运动开始/完成/停止时保存位置与有效标志。
- 软急停、正/负限位 I 事件处理接入状态机。
HAL(AB 启动预热):
- 首次启动保持"吞周期"流程建立 00 相位;完成预热后
段间/调频重定相直接从已验证的 00 边界交还 AF,
不再吞掉用户周期。(待 AB 上板验证)
host 测试扩展:
- 单位换算(含余数累计)、等效配置解析、位置累加/溢出、
AB 预热状态机断言,全量测试通过。
上板验证(逻辑分析仪实测):
- 两段各 1001 工程单位 → 段1 输出 1501 脉冲、段2 输出 1502
脉冲(余数累计正确),总计 3003 脉冲。
- 稳定频率 1500Hz,无毛刺,起步/收尾平滑,时长约 2.2s。
---
Core/Src/main.c | 3 +-
EWARM/Modbus.ewp | 6 +
PLSR/Inc/plc_device.h | 4 +
PLSR/Inc/plsr_address_map.h | 8 +
PLSR/Inc/plsr_hal_f407.h | 2 +
PLSR/Inc/plsr_job.h | 4 +
PLSR/Inc/plsr_position.h | 56 +++++
PLSR/Inc/plsr_self_test.h | 4 +
PLSR/Inc/plsr_types.h | 1 +
PLSR/Src/plc_device.c | 54 +++++
PLSR/Src/plsr_core.c | 379 +++++++++++++++++++++++++++++++--
PLSR/Src/plsr_hal_f407.c | 47 +++-
PLSR/Src/plsr_job.c | 179 ++++++++++++++--
PLSR/Src/plsr_path.c | 7 +-
PLSR/Src/plsr_position.c | 227 ++++++++++++++++++++
PLSR/Src/plsr_self_test.c | 49 +++++
PLSR/Test/run_host_tests.ps1 | 13 ++
PLSR/Test/test_plc_device.c | 10 +
PLSR/Test/test_plsr_core.c | 21 +-
PLSR/Test/test_plsr_hal.c | 331 ++++++++++++++++++++++++++++
PLSR/Test/test_plsr_job.c | 48 +++++
PLSR/Test/test_plsr_position.c | 93 ++++++++
tmp/pdfs/xinjie_page_30.png | Bin 0 -> 336416 bytes
tmp/pdfs/xinjie_page_31.png | Bin 0 -> 288347 bytes
24 files changed, 1510 insertions(+), 36 deletions(-)
create mode 100644 PLSR/Inc/plsr_position.h
create mode 100644 PLSR/Src/plsr_position.c
create mode 100644 PLSR/Test/test_plsr_position.c
create mode 100644 tmp/pdfs/xinjie_page_30.png
create mode 100644 tmp/pdfs/xinjie_page_31.png
diff --git a/Core/Src/main.c b/Core/Src/main.c
index 07b5ff5..16c8ae1 100644
--- a/Core/Src/main.c
+++ b/Core/Src/main.c
@@ -202,7 +202,8 @@ int main(void)
/* 上电自测:延时 1s 后由 Q0/Q1 输出三段 AB 正交周期(验证后关闭)。 */
HAL_Delay(1000U);
- (void)PlsrSelfTestQueue();
+ // (void)PlsrSelfTestQueue();
+ (void)PlsrEquivalentSelfTestQueue();
OSStart();
/* USER CODE END 2 */
diff --git a/EWARM/Modbus.ewp b/EWARM/Modbus.ewp
index 870b464..9c07e0e 100644
--- a/EWARM/Modbus.ewp
+++ b/EWARM/Modbus.ewp
@@ -1290,6 +1290,9 @@
$PROJ_DIR$\..\PLSR\Inc\plsr_profile.h
+
+ $PROJ_DIR$\..\PLSR\Inc\plsr_position.h
+
$PROJ_DIR$\..\PLSR\Inc\plsr_hal_f407.h
@@ -1314,6 +1317,9 @@
$PROJ_DIR$\..\PLSR\Src\plsr_profile.c
+
+ $PROJ_DIR$\..\PLSR\Src\plsr_position.c
+
$PROJ_DIR$\..\PLSR\Src\plsr_hal_f407.c
diff --git a/PLSR/Inc/plc_device.h b/PLSR/Inc/plc_device.h
index 13a0485..8bfd422 100644
--- a/PLSR/Inc/plc_device.h
+++ b/PLSR/Inc/plc_device.h
@@ -49,9 +49,13 @@ PLC_DEVICE_RESULT PlcDevicePublishSm(uint8_t axis,
PLC_DEVICE_RESULT PlcDeviceReadSd(uint16_t address, int32_t *value);
PLC_DEVICE_RESULT PlcDeviceWriteSd(uint16_t address, int32_t value);
+PLC_DEVICE_RESULT PlcDeviceReadSdDword(uint16_t lowAddress, int32_t *value);
PLC_DEVICE_RESULT PlcDevicePublishSd(uint8_t axis,
uint8_t item,
int32_t value);
+PLC_DEVICE_RESULT PlcDevicePublishSdDword(uint8_t axis,
+ uint8_t lowItem,
+ int32_t value);
PLC_DEVICE_RESULT PlcDeviceGetEventAddress(uint8_t axis,
uint16_t segmentNumber,
diff --git a/PLSR/Inc/plsr_address_map.h b/PLSR/Inc/plsr_address_map.h
index 5580be3..8d23252 100644
--- a/PLSR/Inc/plsr_address_map.h
+++ b/PLSR/Inc/plsr_address_map.h
@@ -22,6 +22,14 @@ extern "C" {
#define PLSR_SD_AXIS_ITEM_COUNT (12U)
#define PLSR_EVENT_AXIS_ITEM_COUNT (100U)
+#define PLSR_SD_ITEM_SEGMENT (0U)
+#define PLSR_SD_ITEM_SEGMENT_PULSES (2U)
+#define PLSR_SD_ITEM_SEGMENT_EQUIV (4U)
+#define PLSR_SD_ITEM_FREQUENCY (6U)
+#define PLSR_SD_ITEM_SPEED (8U)
+#define PLSR_SD_ITEM_ERROR_CODE (10U)
+#define PLSR_SD_ITEM_ERROR_BLOCK (11U)
+
typedef struct
{
uint16_t hsdRuntimeBase;
diff --git a/PLSR/Inc/plsr_hal_f407.h b/PLSR/Inc/plsr_hal_f407.h
index b01e5ed..86fc4c3 100644
--- a/PLSR/Inc/plsr_hal_f407.h
+++ b/PLSR/Inc/plsr_hal_f407.h
@@ -38,7 +38,9 @@ PLSR_RESULT PlsrHwStopPulse(uint8_t axis);
uint8_t PlsrHwIsPulseActive(uint8_t axis);
PLSR_HW_STATE PlsrHwGetState(uint8_t axis);
uint32_t PlsrHwGetTimerClockHz(uint8_t axis);
+uint32_t PlsrHwGetCurrentFrequencyHz(uint8_t axis);
int64_t PlsrHwGetEmittedPulses(uint8_t axis);
+uint8_t PlsrHwIsAbStartupPriming(uint8_t axis);
/* 每 1ms tick 推进 HAL 状态机(DIR 延时等)。 */
void PlsrHwTick(uint8_t axis);
diff --git a/PLSR/Inc/plsr_job.h b/PLSR/Inc/plsr_job.h
index 685f6a1..379e7f4 100644
--- a/PLSR/Inc/plsr_job.h
+++ b/PLSR/Inc/plsr_job.h
@@ -1,6 +1,7 @@
#ifndef PLSR_JOB_H
#define PLSR_JOB_H
+#include "plsr_position.h"
#include "plsr_types.h"
#include
@@ -137,7 +138,10 @@ typedef struct
PLSR_DATA_REF s0;
PLSR_DATA_REF s1;
PLSR_S2_SNAPSHOT s2;
+ PLSR_EQUIVALENT_CONFIG equivalent;
PLSR_SEGMENT_SNAPSHOT segments[PLSR_MAX_SEGMENTS];
+ uint32_t inputDefaultSpeed;
+ uint32_t inputMaximumSpeed;
uint32_t timerClockHz;
uint32_t pairedTimerClockHz;
uint16_t segmentCount;
diff --git a/PLSR/Inc/plsr_position.h b/PLSR/Inc/plsr_position.h
new file mode 100644
index 0000000..410f8e8
--- /dev/null
+++ b/PLSR/Inc/plsr_position.h
@@ -0,0 +1,56 @@
+#ifndef PLSR_POSITION_H
+#define PLSR_POSITION_H
+
+#include "plsr_types.h"
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct
+{
+ uint32_t pulsesPerRevolution;
+ uint32_t movementPerRevolution;
+ uint8_t unitCode;
+} PLSR_EQUIVALENT_CONFIG;
+
+uint8_t PlsrPositionUnitCodeIsValid(uint8_t unitCode);
+uint8_t PlsrPositionUsesEquivalent(const PLSR_EQUIVALENT_CONFIG *config);
+PLSR_RESULT PlsrPositionValidateEquivalent(
+ const PLSR_EQUIVALENT_CONFIG *config);
+
+/* 带余数的相对距离换算。remainder/newRemainder 的分母恒为
+ * movementPerRevolution,允许负值以保证正反向运动可相互抵消。 */
+PLSR_RESULT PlsrPositionUnitsToPulses(
+ const PLSR_EQUIVALENT_CONFIG *config,
+ int32_t units,
+ int64_t remainder,
+ int64_t *pulses,
+ int64_t *newRemainder);
+
+PLSR_RESULT PlsrPositionAbsoluteUnitsToPulses(
+ const PLSR_EQUIVALENT_CONFIG *config,
+ int32_t units,
+ int64_t *pulses);
+
+PLSR_RESULT PlsrPositionPulsesToUnits(
+ const PLSR_EQUIVALENT_CONFIG *config,
+ int64_t pulses,
+ int64_t *units);
+
+PLSR_RESULT PlsrPositionSpeedToPulseFrequency(
+ const PLSR_EQUIVALENT_CONFIG *config,
+ uint32_t speed,
+ uint32_t *frequencyHz);
+
+PLSR_RESULT PlsrPositionPulseFrequencyToSpeed(
+ const PLSR_EQUIVALENT_CONFIG *config,
+ uint32_t frequencyHz,
+ uint32_t *speed);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PLSR_POSITION_H */
diff --git a/PLSR/Inc/plsr_self_test.h b/PLSR/Inc/plsr_self_test.h
index 2781265..00b2870 100644
--- a/PLSR/Inc/plsr_self_test.h
+++ b/PLSR/Inc/plsr_self_test.h
@@ -11,6 +11,10 @@ extern "C" {
* 用于 P3b-2 上板验证,验证完成后应关闭。 */
PLSR_RESULT PlsrSelfTestQueue(void);
+/* P4-2 当量换算上板自测:Q0=PULSE、Y4=DIR,两段各1工程单位;
+ * 3脉冲/2单位比例下应依次输出1、2个脉冲。 */
+PLSR_RESULT PlsrEquivalentSelfTestQueue(void);
+
#ifdef __cplusplus
}
#endif
diff --git a/PLSR/Inc/plsr_types.h b/PLSR/Inc/plsr_types.h
index 838f038..759da73 100644
--- a/PLSR/Inc/plsr_types.h
+++ b/PLSR/Inc/plsr_types.h
@@ -166,6 +166,7 @@ typedef struct
uint32_t illegalTransitionCount;
uint32_t pendingEvents;
int64_t logicalPosition;
+ int64_t taskPulses;
int64_t totalPulses;
uint8_t busy;
uint8_t pulseActive;
diff --git a/PLSR/Src/plc_device.c b/PLSR/Src/plc_device.c
index 5f2c844..5f37c9a 100644
--- a/PLSR/Src/plc_device.c
+++ b/PLSR/Src/plc_device.c
@@ -549,6 +549,7 @@ PLC_DEVICE_RESULT PlcDevicePublishSm(uint8_t axis,
PLC_DEVICE_RESULT PlcDeviceReadSd(uint16_t address, int32_t *value)
{
int32_t *source;
+ uint32_t interruptState;
if (value == NULL)
{
@@ -560,7 +561,9 @@ PLC_DEVICE_RESULT PlcDeviceReadSd(uint16_t address, int32_t *value)
return PLC_DEVICE_INVALID_ADDRESS;
}
+ interruptState = PlcDeviceEnterCritical();
*value = *source;
+ PlcDeviceExitCritical(interruptState);
return PLC_DEVICE_OK;
}
@@ -576,12 +579,63 @@ PLC_DEVICE_RESULT PlcDevicePublishSd(uint8_t axis,
uint8_t item,
int32_t value)
{
+ uint32_t interruptState;
+
if ((axis >= PLSR_AXIS_COUNT) || (item >= PLSR_SD_AXIS_ITEM_COUNT))
{
return PLC_DEVICE_INVALID_ARGUMENT;
}
+ interruptState = PlcDeviceEnterCritical();
PlcSdRuntime[axis][item] = value;
+ PlcDeviceExitCritical(interruptState);
+ return PLC_DEVICE_OK;
+}
+
+PLC_DEVICE_RESULT PlcDeviceReadSdDword(uint16_t lowAddress, int32_t *value)
+{
+ int32_t *source;
+ uint32_t rawValue;
+ uint32_t interruptState;
+ uint8_t axis;
+ uint8_t item;
+
+ if (value == NULL)
+ {
+ return PLC_DEVICE_NULL_POINTER;
+ }
+ source = PlcDeviceResolveSd(lowAddress, &axis, &item);
+ if ((source == NULL) || ((item & 1U) != 0U)
+ || (item > PLSR_SD_ITEM_SPEED))
+ {
+ return PLC_DEVICE_INVALID_ADDRESS;
+ }
+ interruptState = PlcDeviceEnterCritical();
+ rawValue = (uint32_t)PlcSdRuntime[axis][item] & 0xFFFFUL;
+ rawValue |= ((uint32_t)PlcSdRuntime[axis][item + 1U] & 0xFFFFUL)
+ << 16U;
+ PlcDeviceExitCritical(interruptState);
+ *value = (int32_t)rawValue;
+ return PLC_DEVICE_OK;
+}
+
+PLC_DEVICE_RESULT PlcDevicePublishSdDword(uint8_t axis,
+ uint8_t lowItem,
+ int32_t value)
+{
+ uint32_t rawValue;
+ uint32_t interruptState;
+
+ if ((axis >= PLSR_AXIS_COUNT) || ((lowItem & 1U) != 0U)
+ || (lowItem > PLSR_SD_ITEM_SPEED))
+ {
+ return PLC_DEVICE_INVALID_ARGUMENT;
+ }
+ rawValue = (uint32_t)value;
+ interruptState = PlcDeviceEnterCritical();
+ PlcSdRuntime[axis][lowItem] = (int32_t)(rawValue & 0xFFFFUL);
+ PlcSdRuntime[axis][lowItem + 1U] = (int32_t)(rawValue >> 16U);
+ PlcDeviceExitCritical(interruptState);
return PLC_DEVICE_OK;
}
diff --git a/PLSR/Src/plsr_core.c b/PLSR/Src/plsr_core.c
index ce7b748..00cd3bc 100644
--- a/PLSR/Src/plsr_core.c
+++ b/PLSR/Src/plsr_core.c
@@ -7,6 +7,8 @@
#include "plsr_resource.h"
#include
+#define PLSR_CORE_SFD_AXIS_STRIDE (130U)
+
#ifndef PLSR_HOST_TEST
#include "stm32f4xx.h"
#include "ucos_ii.h"
@@ -17,6 +19,8 @@ typedef struct
PLSR_STATE state;
PLSR_OUTPUT_MODE outputMode;
PLSR_ERROR error;
+ uint16_t compatibleErrorCode;
+ uint16_t compatibleErrorBlock;
PLSR_STOP_REASON stopReason;
PLSR_STATE pendingTerminal;
PLSR_RESOURCE_LEASE lease;
@@ -27,7 +31,11 @@ typedef struct
PLSR_RESULT lastCommandResult;
uint32_t illegalTransitionCount;
int64_t logicalPosition;
+ int64_t taskPulses;
int64_t totalPulses;
+ int64_t segmentAccountedPulses;
+ int64_t equivalentCommandRemainder;
+ PLSR_EQUIVALENT_CONFIG equivalent;
PLSR_PATH_CONTEXT path;
PLSR_PROFILE_STATE profile;
uint8_t profileActive;
@@ -39,6 +47,7 @@ typedef struct
uint8_t positionValid;
uint8_t jobValid;
uint8_t positionOverflow;
+ uint8_t segmentAccountingActive;
} PLSR_AXIS;
typedef struct
@@ -61,6 +70,8 @@ static PLSR_JOB_SNAPSHOT PlsrJobScratch;
static void PlsrStopSegmentHardware(uint8_t axis, PLSR_AXIS *axisObject);
static PLSR_RESULT PlsrStartSegmentHardware(uint8_t axis,
PLSR_AXIS *axisObject);
+static void PlsrAccountHardwarePulses(uint8_t axis,
+ PLSR_AXIS *axisObject);
static uint32_t PlsrCoreEnterCritical(void)
{
@@ -104,6 +115,161 @@ static uint8_t PlsrStateIsPulseActive(PLSR_STATE state)
: 0U;
}
+static void PlsrLoadAxisEquivalentConfig(uint8_t axis,
+ PLSR_EQUIVALENT_CONFIG *config)
+{
+ uint16_t base = (uint16_t)(PLSR_SFD_CONFIG_START
+ + (uint16_t)axis
+ * PLSR_CORE_SFD_AXIS_STRIDE);
+ uint16_t word;
+ uint16_t lowWord;
+ uint16_t highWord;
+
+ config->unitCode = 0U;
+ config->pulsesPerRevolution = 1UL;
+ config->movementPerRevolution = 1UL;
+ if (PlcDeviceReadSfd(base, &word) != PLC_DEVICE_OK)
+ {
+ return;
+ }
+ config->unitCode = (uint8_t)((word >> 8U) & 0x07U);
+ if ((PlcDeviceReadSfd((uint16_t)(base + 2U), &lowWord)
+ != PLC_DEVICE_OK)
+ || (PlcDeviceReadSfd((uint16_t)(base + 3U), &highWord)
+ != PLC_DEVICE_OK))
+ {
+ config->unitCode = 0U;
+ return;
+ }
+ config->pulsesPerRevolution = (uint32_t)lowWord
+ | ((uint32_t)highWord << 16U);
+ if ((PlcDeviceReadSfd((uint16_t)(base + 4U), &lowWord)
+ != PLC_DEVICE_OK)
+ || (PlcDeviceReadSfd((uint16_t)(base + 5U), &highWord)
+ != PLC_DEVICE_OK))
+ {
+ config->unitCode = 0U;
+ return;
+ }
+ config->movementPerRevolution = (uint32_t)lowWord
+ | ((uint32_t)highWord << 16U);
+ if (PlsrPositionValidateEquivalent(config) != PLSR_RESULT_OK)
+ {
+ config->unitCode = 0U;
+ config->pulsesPerRevolution = 1UL;
+ config->movementPerRevolution = 1UL;
+ }
+}
+
+static void PlsrPublishSdDword(uint8_t axis,
+ uint8_t lowItem,
+ int32_t value)
+{
+ (void)PlcDevicePublishSdDword(axis, lowItem, value);
+}
+
+static int32_t PlsrGetCompatibleSegmentPulses(const PLSR_AXIS *axisObject)
+{
+ int64_t signedPulses = (axisObject->directionPositive != 0U)
+ ? axisObject->segmentAccountedPulses
+ : -axisObject->segmentAccountedPulses;
+
+ if (signedPulses > INT32_MAX)
+ {
+ return INT32_MAX;
+ }
+ if (signedPulses < INT32_MIN)
+ {
+ return INT32_MIN;
+ }
+ return (int32_t)signedPulses;
+}
+
+static int32_t PlsrClampCompatibleInt32(int64_t value)
+{
+ if (value > INT32_MAX) return INT32_MAX;
+ if (value < INT32_MIN) return INT32_MIN;
+ return (int32_t)value;
+}
+
+static void PlsrPublishRuntime(uint8_t axis)
+{
+ PLSR_AXIS *axisObject = &PlsrAxes[axis];
+ uint16_t currentSegment = (axisObject->jobValid != 0U)
+ ? PlsrPathGetCurrentSegment(
+ &axisObject->path)
+ : 0U;
+ int32_t segmentPulses = PlsrGetCompatibleSegmentPulses(axisObject);
+ int32_t frequencyHz = (int32_t)PlsrHwGetCurrentFrequencyHz(axis);
+ int64_t segmentEquivalent64 = segmentPulses;
+ uint32_t speed = (uint32_t)frequencyHz;
+ uint16_t publishedError;
+ uint16_t publishedBlock;
+
+ if (PlsrPositionPulsesToUnits(&axisObject->equivalent,
+ segmentPulses,
+ &segmentEquivalent64) != PLSR_RESULT_OK)
+ {
+ axisObject->positionOverflow = 1U;
+ segmentEquivalent64 = (segmentPulses < 0) ? INT32_MIN : INT32_MAX;
+ }
+ if (PlsrPositionPulseFrequencyToSpeed(&axisObject->equivalent,
+ (uint32_t)frequencyHz,
+ &speed) != PLSR_RESULT_OK)
+ {
+ speed = UINT32_MAX;
+ }
+
+ PlsrPublishSdDword(axis,
+ PLSR_SD_ITEM_SEGMENT,
+ (int32_t)currentSegment);
+ PlsrPublishSdDword(axis,
+ PLSR_SD_ITEM_SEGMENT_PULSES,
+ segmentPulses);
+ PlsrPublishSdDword(axis,
+ PLSR_SD_ITEM_SEGMENT_EQUIV,
+ PlsrClampCompatibleInt32(segmentEquivalent64));
+ PlsrPublishSdDword(axis, PLSR_SD_ITEM_FREQUENCY, frequencyHz);
+ PlsrPublishSdDword(axis,
+ PLSR_SD_ITEM_SPEED,
+ (speed > (uint32_t)INT32_MAX) ? INT32_MAX
+ : (int32_t)speed);
+ publishedError = (axisObject->compatibleErrorCode != 0U)
+ ? axisObject->compatibleErrorCode
+ : (uint16_t)axisObject->error;
+ publishedBlock = (axisObject->compatibleErrorCode != 0U)
+ ? axisObject->compatibleErrorBlock
+ : (((axisObject->error != PLSR_ERROR_NONE)
+ && (axisObject->jobValid != 0U))
+ ? currentSegment
+ : 0U);
+ (void)PlcDevicePublishSd(axis,
+ PLSR_SD_ITEM_ERROR_CODE,
+ (int32_t)publishedError);
+ (void)PlcDevicePublishSd(axis,
+ PLSR_SD_ITEM_ERROR_BLOCK,
+ (int32_t)publishedBlock);
+}
+
+static void PlsrSetCompatibleEquivalentError(uint8_t axis,
+ PLSR_AXIS *axisObject)
+{
+ uint16_t commonBase = (uint16_t)(PLSR_SFD_CONFIG_START
+ + (uint16_t)axis
+ * PLSR_CORE_SFD_AXIS_STRIDE);
+
+ if ((axisObject->parseDetail.result == PLSR_RESULT_INVALID_S2)
+ && (axisObject->parseDetail.block == PLSR_PARSE_BLOCK_S2)
+ && ((axisObject->parseDetail.address
+ == (uint32_t)(commonBase + 2U))
+ || (axisObject->parseDetail.address
+ == (uint32_t)(commonBase + 4U))))
+ {
+ axisObject->compatibleErrorCode = 2U;
+ axisObject->compatibleErrorBlock = 0U;
+ }
+}
+
static void PlsrPublishAxis(uint8_t axis)
{
PLSR_AXIS *axisObject = &PlsrAxes[axis];
@@ -114,6 +280,7 @@ static void PlsrPublishAxis(uint8_t axis)
(void)PlcDevicePublishSm(axis,
compatibleRun,
axisObject->directionPositive);
+ PlsrPublishRuntime(axis);
}
static void PlsrSetStopReason(PLSR_AXIS *axis,
@@ -125,21 +292,102 @@ static void PlsrSetStopReason(PLSR_AXIS *axis,
}
}
-/* 将 64 位逻辑位置发布为 HSD 的 32 位兼容值:
- * 超出 INT32 范围时回绕(与信捷 32 位寄存器一致)并置溢出诊断标志。 */
+/* 将 64 位逻辑位置发布为 HSD 的 32 位兼容值。
+ * 超出范围时保持最近一次合法值,只锁存诊断,禁止回绕或静默截断。 */
static void PlsrPublishPosition(uint8_t axis, PLSR_AXIS *axisObject)
{
int32_t compatValue;
+ int64_t equivalentPosition;
+ uint16_t base = (uint16_t)((uint16_t)axis
+ * PLSR_HSD_RUNTIME_AXIS_COUNT);
if ((axisObject->logicalPosition > INT32_MAX)
|| (axisObject->logicalPosition < INT32_MIN))
{
axisObject->positionOverflow = 1U;
+ return;
+ }
+ compatValue = (int32_t)axisObject->logicalPosition;
+ (void)PlcDevicePublishHsdDword(base, compatValue);
+ if ((PlsrPositionPulsesToUnits(&axisObject->equivalent,
+ axisObject->logicalPosition,
+ &equivalentPosition) != PLSR_RESULT_OK)
+ || (equivalentPosition > INT32_MAX)
+ || (equivalentPosition < INT32_MIN))
+ {
+ axisObject->positionOverflow = 1U;
+ return;
+ }
+ (void)PlcDevicePublishHsdDword((uint16_t)(base + 2U),
+ (int32_t)equivalentPosition);
+}
+
+static uint8_t PlsrAddInt64Checked(int64_t left,
+ int64_t right,
+ int64_t *result)
+{
+ if ((result == NULL)
+ || ((right > 0) && (left > INT64_MAX - right))
+ || ((right < 0) && (left < INT64_MIN - right)))
+ {
+ return 0U;
+ }
+ *result = left + right;
+ return 1U;
+}
+
+/* 将 HAL 实际完成的完整脉冲/AB周期合并到64位位置。
+ * emittedPulses 每段从0开始,因此用 segmentAccountedPulses 做差量去重。 */
+static void PlsrAccountHardwarePulses(uint8_t axis,
+ PLSR_AXIS *axisObject)
+{
+ int64_t emittedPulses;
+ int64_t delta;
+ int64_t signedDelta;
+ int64_t newLogicalPosition;
+ int64_t newTaskPulses;
+ int64_t newTotalPulses;
+
+ if (axisObject->segmentAccountingActive == 0U)
+ {
+ return;
+ }
+ emittedPulses = PlsrHwGetEmittedPulses(axis);
+ if ((emittedPulses < 0)
+ || (emittedPulses < axisObject->segmentAccountedPulses))
+ {
+ axisObject->positionOverflow = 1U;
+ (void)PlsrPostEvent(axis, PLSR_EVENT_COUNTER_FAULT);
+ return;
+ }
+ delta = emittedPulses - axisObject->segmentAccountedPulses;
+ if (delta == 0)
+ {
+ return;
+ }
+ signedDelta = (axisObject->directionPositive != 0U) ? delta : -delta;
+ if ((PlsrAddInt64Checked(axisObject->logicalPosition,
+ signedDelta,
+ &newLogicalPosition) == 0U)
+ || (PlsrAddInt64Checked(axisObject->taskPulses,
+ signedDelta,
+ &newTaskPulses) == 0U)
+ || (PlsrAddInt64Checked(axisObject->totalPulses,
+ delta,
+ &newTotalPulses) == 0U))
+ {
+ axisObject->positionOverflow = 1U;
+ axisObject->segmentAccountedPulses = emittedPulses;
+ (void)PlsrPostEvent(axis, PLSR_EVENT_COUNTER_FAULT);
+ return;
}
- compatValue = (int32_t)(uint32_t)axisObject->logicalPosition;
- (void)PlcDevicePublishHsdDword(
- (uint16_t)((uint16_t)axis * PLSR_HSD_RUNTIME_AXIS_COUNT),
- compatValue);
+
+ axisObject->logicalPosition = newLogicalPosition;
+ axisObject->taskPulses = newTaskPulses;
+ axisObject->totalPulses = newTotalPulses;
+ axisObject->segmentAccountedPulses = emittedPulses;
+ PlsrPublishPosition(axis, axisObject);
+ PlsrPublishRuntime(axis);
}
static uint8_t PlsrTransitionIsAllowed(PLSR_STATE current,
@@ -542,11 +790,16 @@ static PLSR_RESULT PlsrStartAxis(PLSR_AXIS *axisObject,
axisObject->outputMode = start->outputMode;
axisObject->directionPositive = (start->directionPositive != 0U) ? 1U : 0U;
axisObject->error = PLSR_ERROR_NONE;
+ axisObject->compatibleErrorCode = 0U;
+ axisObject->compatibleErrorBlock = 0U;
axisObject->stopReason = PLSR_STOP_REASON_NONE;
axisObject->pendingTerminal = PLSR_STATE_UNINITIALIZED;
axisObject->immediateStopPending = 0U;
axisObject->done = 0U;
axisObject->jobValid = 0U;
+ axisObject->taskPulses = 0;
+ axisObject->segmentAccountedPulses = 0;
+ axisObject->segmentAccountingActive = 0U;
result = PlsrStateTransition(start->axis,
PLSR_STATE_ACCEL,
PLSR_TRANSITION_START);
@@ -577,6 +830,9 @@ static PLSR_RESULT PlsrStartCall(PLSR_AXIS *axisObject,
return PLSR_RESULT_INVALID_STATE;
}
+ axisObject->compatibleErrorCode = 0U;
+ axisObject->compatibleErrorBlock = 0U;
+
parseContext.logicalPosition = axisObject->logicalPosition;
parseContext.positionValid = axisObject->positionValid;
result = PlsrBuildJobSnapshot(call,
@@ -585,6 +841,7 @@ static PLSR_RESULT PlsrStartCall(PLSR_AXIS *axisObject,
&axisObject->parseDetail);
if (result != PLSR_RESULT_OK)
{
+ PlsrSetCompatibleEquivalentError(call->dAxis, axisObject);
return result;
}
@@ -606,13 +863,28 @@ static PLSR_RESULT PlsrStartCall(PLSR_AXIS *axisObject,
axisObject->job = PlsrJobScratch;
axisObject->jobValid = 1U;
+ if ((axisObject->equivalent.unitCode
+ != axisObject->job.equivalent.unitCode)
+ || (axisObject->equivalent.pulsesPerRevolution
+ != axisObject->job.equivalent.pulsesPerRevolution)
+ || (axisObject->equivalent.movementPerRevolution
+ != axisObject->job.equivalent.movementPerRevolution))
+ {
+ axisObject->equivalentCommandRemainder = 0;
+ }
+ axisObject->equivalent = axisObject->job.equivalent;
axisObject->outputMode = (PLSR_OUTPUT_MODE)axisObject->job.outputMode;
axisObject->directionPositive = axisObject->job.initialDirectionPositive;
axisObject->error = PLSR_ERROR_NONE;
+ axisObject->compatibleErrorCode = 0U;
+ axisObject->compatibleErrorBlock = 0U;
axisObject->stopReason = PLSR_STOP_REASON_NONE;
axisObject->pendingTerminal = PLSR_STATE_UNINITIALIZED;
axisObject->immediateStopPending = 0U;
axisObject->done = 0U;
+ axisObject->taskPulses = 0;
+ axisObject->segmentAccountedPulses = 0;
+ axisObject->segmentAccountingActive = 0U;
PlsrPathBegin(&axisObject->path,
&axisObject->job,
axisObject->logicalPosition);
@@ -864,6 +1136,10 @@ static PLSR_RESULT PlsrExecuteCommand(const PLSR_COMMAND_SLOT *slot)
else
{
axisObject->totalPulses = 0;
+ (void)PlcDeviceSetHsdCheckpointMeta(
+ axisObject->positionValid,
+ 0U);
+ (void)PlcDeviceCheckpointHsd();
result = PLSR_RESULT_OK;
}
break;
@@ -904,6 +1180,8 @@ static PLSR_RESULT PlsrExecuteCommand(const PLSR_COMMAND_SLOT *slot)
else
{
axisObject->error = PLSR_ERROR_NONE;
+ axisObject->compatibleErrorCode = 0U;
+ axisObject->compatibleErrorBlock = 0U;
axisObject->stopReason = PLSR_STOP_REASON_NONE;
axisObject->done = 0U;
result = PlsrStateTransition(slot->command.axis,
@@ -1034,25 +1312,75 @@ static PLSR_RESULT PlsrStartSegmentHardware(uint8_t axis,
PLSR_HW_START_PARAMS params;
PLSR_RESULT result;
int64_t pulses;
+ int64_t signedPulses;
+ int64_t targetPosition;
+ int64_t nextEquivalentRemainder =
+ axisObject->equivalentCommandRemainder;
uint8_t positive;
if (job->positioningMode == 0U)
{
- pulses = (segment->pulseOrTarget < 0)
- ? -(int64_t)segment->pulseOrTarget
- : (int64_t)segment->pulseOrTarget;
- positive = (segment->pulseOrTarget >= 0) ? 1U : 0U;
+ result = PlsrPositionUnitsToPulses(
+ &axisObject->equivalent,
+ segment->pulseOrTarget,
+ axisObject->equivalentCommandRemainder,
+ &signedPulses,
+ &nextEquivalentRemainder);
+ if (result != PLSR_RESULT_OK)
+ {
+ return result;
+ }
+ if (signedPulses == INT64_MIN)
+ {
+ return PLSR_RESULT_POSITION_OVERFLOW;
+ }
+ pulses = (signedPulses < 0) ? -signedPulses : signedPulses;
+ positive = (signedPulses >= 0) ? 1U : 0U;
}
else
{
- /* 绝对模式:P4 位置闭环后完善;P3a 按当前位置计算。 */
- int64_t delta = (int64_t)segment->pulseOrTarget
- - axisObject->logicalPosition;
+ /* 绝对模式使用实际硬件脉冲闭环更新后的逻辑位置计算位移。 */
+ int64_t delta;
+
+ result = PlsrPositionAbsoluteUnitsToPulses(
+ &axisObject->equivalent,
+ segment->pulseOrTarget,
+ &targetPosition);
+ if (result != PLSR_RESULT_OK)
+ {
+ return result;
+ }
+ if (((axisObject->logicalPosition > 0)
+ && (targetPosition
+ < INT64_MIN + axisObject->logicalPosition))
+ || ((axisObject->logicalPosition < 0)
+ && (targetPosition
+ > INT64_MAX + axisObject->logicalPosition)))
+ {
+ return PLSR_RESULT_POSITION_OVERFLOW;
+ }
+ delta = targetPosition - axisObject->logicalPosition;
+ if (delta == INT64_MIN)
+ {
+ return PLSR_RESULT_POSITION_OVERFLOW;
+ }
pulses = (delta < 0) ? -delta : delta;
positive = (delta >= 0) ? 1U : 0U;
}
+ if (pulses == 0)
+ {
+ /* 当量小于一个物理脉冲时保存余数并按零脉冲段推进;不启动PWM。 */
+ axisObject->equivalentCommandRemainder = nextEquivalentRemainder;
+ axisObject->directionPositive = positive;
+ axisObject->segmentAccountedPulses = 0;
+ axisObject->segmentAccountingActive = 0U;
+ PlsrPublishAxis(axis);
+ (void)PlsrPostEvent(axis, PLSR_EVENT_SEGMENT_COMPLETE);
+ return PLSR_RESULT_OK;
+ }
+
(void)memset(&profileRequest, 0, sizeof(profileRequest));
profileRequest.targetFrequencyHz = segment->targetFrequency;
profileRequest.startFrequencyHz = job->s2.startSpeed;
@@ -1090,18 +1418,27 @@ static PLSR_RESULT PlsrStartSegmentHardware(uint8_t axis,
axisObject->profileWasAccel = 0U;
return result;
}
+ axisObject->equivalentCommandRemainder = nextEquivalentRemainder;
+ axisObject->directionPositive = positive;
+ axisObject->segmentAccountedPulses = 0;
+ axisObject->segmentAccountingActive = 1U;
axisObject->profileActive = 1U;
axisObject->profileWasAccel =
(axisObject->profile.phase == PLSR_PROFILE_PHASE_ACCEL) ? 1U : 0U;
+ PlsrPublishAxis(axis);
return PLSR_RESULT_OK;
}
/* 停止当前段的硬件输出与速度曲线。 */
static void PlsrStopSegmentHardware(uint8_t axis, PLSR_AXIS *axisObject)
{
+ PlsrAccountHardwarePulses(axis, axisObject);
axisObject->profileActive = 0U;
axisObject->profileWasAccel = 0U;
(void)PlsrHwStopPulse(axis);
+ PlsrAccountHardwarePulses(axis, axisObject);
+ axisObject->segmentAccountingActive = 0U;
+ PlsrPublishRuntime(axis);
}
/* 应用路径执行器的动作:段间推进、进入等待、结束、让出、错误。 */
@@ -1293,6 +1630,7 @@ PLSR_RESULT PlsrInit(void)
PlsrAxes[axis].state = PLSR_STATE_UNINITIALIZED;
PlsrAxes[axis].pendingTerminal = PLSR_STATE_UNINITIALIZED;
PlsrAxes[axis].lease.directionPoint = PLSR_DIRECTION_POINT_NONE;
+ PlsrLoadAxisEquivalentConfig(axis, &PlsrAxes[axis].equivalent);
if (PlcDeviceReadHsdDword(
(uint16_t)(axis * PLSR_HSD_RUNTIME_AXIS_COUNT),
&restoredPosition) == PLC_DEVICE_OK)
@@ -1328,6 +1666,13 @@ void PlsrProcess(void)
return;
}
+ /* 先合并 ISR 已完成的实际脉冲,确保段完成、STOP或新命令不会在
+ * HAL 计数清零前丢失最后一批位置增量。 */
+ for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++)
+ {
+ PlsrAccountHardwarePulses(axis, &PlsrAxes[axis]);
+ }
+
for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++)
{
events = PlsrTakeEvents(axis, PLSR_EVENT_CRITICAL_MASK);
@@ -1369,6 +1714,13 @@ void PlsrProcess(void)
uint8_t wasAccel;
PlsrHwTick(axis);
+ PlsrAccountHardwarePulses(axis, axisObject);
+ /* 首次 AB 内部预热周期不属于用户运动,速度曲线也必须冻结;
+ * 否则低速起步时会在隐藏周期内提前爬升十余个刷新步。 */
+ if (PlsrHwIsAbStartupPriming(axis) != 0U)
+ {
+ continue;
+ }
if (PlsrStateIsBusy(axisObject->state) == 0U)
{
continue;
@@ -1475,6 +1827,7 @@ PLSR_RESULT PlsrGetStatus(uint8_t axis, PLSR_STATUS *status)
status->illegalTransitionCount = axisObject->illegalTransitionCount;
status->pendingEvents = axisObject->pendingEvents;
status->logicalPosition = axisObject->logicalPosition;
+ status->taskPulses = axisObject->taskPulses;
status->totalPulses = axisObject->totalPulses;
status->busy = PlsrStateIsBusy(axisObject->state);
status->pulseActive = PlsrStateIsPulseActive(axisObject->state);
diff --git a/PLSR/Src/plsr_hal_f407.c b/PLSR/Src/plsr_hal_f407.c
index 970cd28..5f4cced 100644
--- a/PLSR/Src/plsr_hal_f407.c
+++ b/PLSR/Src/plsr_hal_f407.c
@@ -148,6 +148,8 @@ typedef struct
uint8_t directionPositive;
uint8_t abQuarter;
uint8_t abCountAxis;
+ uint8_t abStartupPriming;
+ uint8_t abOutputPrimed;
uint16_t abActiveBasePsc;
uint16_t abActivePairPsc;
uint16_t abActiveArr;
@@ -688,6 +690,7 @@ static void PlsrHwBeginAbOutput(uint8_t axis, uint8_t debugReason)
lagStart = periodTicks / 2UL;
state->abCountAxis = lagAxis;
state->abQuarter = 0U;
+ state->abStartupPriming = (state->abOutputPrimed == 0U) ? 1U : 0U;
#ifndef PLSR_HOST_TEST
interruptState = __get_PRIMASK();
@@ -733,14 +736,17 @@ static void PlsrHwBeginAbOutput(uint8_t axis, uint8_t debugReason)
PlsrHwTimerSetCc1e(pairAxis, 1UL);
PlsrHwTimerSetPwmMode1(axis);
PlsrHwTimerSetPwmMode1(pairAxis);
- /* OCREF 已在低电平位置稳定后再把物理引脚交还定时器。 */
PlsrHwTimerClearCc1if(axis);
PlsrHwTimerClearCc1if(pairAxis);
- PlsrHwReleasePulsePin(axis);
- PlsrHwReleasePulsePin(pairAxis);
+ if (state->abStartupPriming == 0U)
+ {
+ /* 完成过首次预热后,段间/调频重定相均从已验证的 00 边界
+ * 直接交还 AF,不额外吞掉用户周期。 */
+ PlsrHwReleasePulsePin(axis);
+ PlsrHwReleasePulsePin(pairAxis);
+ }
PlsrHwTimerSetCen(axis, 1UL);
PlsrHwTimerSetCen(pairAxis, 1UL);
- /* 最后才允许落后相计数中断。 */
PlsrHwTimerClearCc1if(axis);
PlsrHwTimerClearCc1if(pairAxis);
PlsrHwTimerSetCc1ie(lagAxis, 1UL);
@@ -797,6 +803,7 @@ static void PlsrHwStopActiveOutput(uint8_t axis,
#endif
PlsrHwStopPwmTimer(axis);
PlsrHwStopPwmTimer(pairAxis);
+ PlsrHwAxes[axis].abStartupPriming = 0U;
#ifndef PLSR_HOST_TEST
__DMB();
if (interruptState == 0UL)
@@ -1062,6 +1069,16 @@ uint32_t PlsrHwGetTimerClockHz(uint8_t axis)
return PlsrHwAxisMap[axis].timerClockHz;
}
+uint32_t PlsrHwGetCurrentFrequencyHz(uint8_t axis)
+{
+ if ((axis >= PLSR_HW_AXIS_COUNT)
+ || (PlsrHwAxes[axis].state != PLSR_HW_STATE_RUNNING))
+ {
+ return 0UL;
+ }
+ return PlsrHwAxes[axis].currentFrequencyHz;
+}
+
/* 硬件已发出的脉冲数(profile 虚拟计数校准用,中断内递增)。 */
int64_t PlsrHwGetEmittedPulses(uint8_t axis)
{
@@ -1090,6 +1107,15 @@ int64_t PlsrHwGetEmittedPulses(uint8_t axis)
return emittedPulses;
}
+uint8_t PlsrHwIsAbStartupPriming(uint8_t axis)
+{
+ if ((axis >= PLSR_HW_AXIS_COUNT) || (PlsrHwIsAbBaseAxis(axis) == 0U))
+ {
+ return 0U;
+ }
+ return PlsrHwAxes[axis].abStartupPriming;
+}
+
void PlsrHwTick(uint8_t axis)
{
PLSR_HW_AXIS_STATE *state;
@@ -1163,6 +1189,19 @@ void PlsrHwOnTimerUpdate(uint8_t axis)
{
return;
}
+ if (state->abStartupPriming != 0U)
+ {
+ /* F407 首次切换 OC 模式时 OC1REF 初态不可直接作为物理AB相。
+ * GPIO 保持 00 隐藏首个内部周期;落后相下降沿是真实 00
+ * 边界,此时再交还 AF,且该隐藏周期绝不能计入 emitted。 */
+ state->abStartupPriming = 0U;
+ state->abOutputPrimed = 1U;
+#ifndef PLSR_HOST_TEST
+ PlsrHwReleasePulsePin(ownerAxis);
+ PlsrHwReleasePulsePin(PlsrHwGetPairedAxis(ownerAxis));
+#endif
+ return;
+ }
state->emittedPulses++;
if (state->emittedPulses >= state->targetPulses)
{
diff --git a/PLSR/Src/plsr_job.c b/PLSR/Src/plsr_job.c
index bb97b22..45e17e3 100644
--- a/PLSR/Src/plsr_job.c
+++ b/PLSR/Src/plsr_job.c
@@ -482,6 +482,43 @@ static PLSR_RESULT PlsrLoadS2(const PLSR_CALL *call,
result = PlsrReadFixedWord(0U, commonBase, &word, detail);
if (result != PLSR_RESULT_OK) return result;
snapshot->directionActiveHigh = ((word & (1U << 1U)) != 0U) ? 1U : 0U;
+ snapshot->equivalent.unitCode = (uint8_t)((word >> 8U) & 0x07U);
+ if (PlsrPositionUnitCodeIsValid(snapshot->equivalent.unitCode) == 0U)
+ {
+ PlsrSetDetail(detail,
+ PLSR_RESULT_INVALID_S2,
+ PLSR_PARSE_BLOCK_S2,
+ commonBase,
+ snapshot->equivalent.unitCode,
+ 0U);
+ return PLSR_RESULT_INVALID_S2;
+ }
+ result = PlsrReadFixedDword(0U,
+ (uint16_t)(commonBase + 2U),
+ &snapshot->equivalent.pulsesPerRevolution,
+ detail);
+ if (result != PLSR_RESULT_OK) return result;
+ result = PlsrReadFixedDword(0U,
+ (uint16_t)(commonBase + 4U),
+ &snapshot->equivalent.movementPerRevolution,
+ detail);
+ if (result != PLSR_RESULT_OK) return result;
+ if (PlsrPositionValidateEquivalent(&snapshot->equivalent)
+ != PLSR_RESULT_OK)
+ {
+ uint16_t invalidAddress =
+ (snapshot->equivalent.pulsesPerRevolution == 0UL)
+ ? (uint16_t)(commonBase + 2U)
+ : (uint16_t)(commonBase + 4U);
+
+ PlsrSetDetail(detail,
+ PLSR_RESULT_INVALID_S2,
+ PLSR_PARSE_BLOCK_S2,
+ invalidAddress,
+ 0,
+ 0U);
+ return PLSR_RESULT_INVALID_S2;
+ }
if (call->outputModeOverride == PLSR_OUTPUT_MODE_FROM_SFD)
{
snapshot->outputMode = ((word & (1U << 13U)) != 0U)
@@ -550,6 +587,8 @@ static PLSR_RESULT PlsrLoadS2(const PLSR_CALL *call,
snapshot->s2.stopSpeed = snapshot->s2.maximumSpeed;
snapshot->speedClamped = 1U;
}
+ snapshot->inputDefaultSpeed = snapshot->s2.defaultSpeed;
+ snapshot->inputMaximumSpeed = snapshot->s2.maximumSpeed;
return PLSR_RESULT_OK;
}
@@ -835,9 +874,12 @@ static PLSR_RESULT PlsrCheckInitialPosition(
{
uint8_t visited[PLSR_MAX_SEGMENTS];
int64_t position = context->logicalPosition;
- int32_t movement;
+ int64_t movement;
+ int64_t targetPosition;
+ int64_t remainder = 0;
int32_t next;
uint16_t segment = snapshot->startSegment;
+ PLSR_RESULT result;
if ((snapshot->positioningMode != 0U) && (context->positionValid == 0U))
{
@@ -858,7 +900,24 @@ static PLSR_RESULT PlsrCheckInitialPosition(
visited[segment - 1U] = 1U;
if (snapshot->positioningMode == 0U)
{
- movement = segmentData->pulseOrTarget;
+ result = PlsrPositionUnitsToPulses(&snapshot->equivalent,
+ segmentData->pulseOrTarget,
+ remainder,
+ &movement,
+ &remainder);
+ if (result != PLSR_RESULT_OK)
+ {
+ PlsrSetDetail(detail,
+ PLSR_RESULT_POSITION_OVERFLOW,
+ PLSR_PARSE_BLOCK_POSITION,
+ snapshot->s0.address
+ + (uint32_t)segment
+ * PLSR_S0_SEGMENT_WORDS
+ + 2UL,
+ segmentData->pulseOrTarget,
+ segment);
+ return PLSR_RESULT_POSITION_OVERFLOW;
+ }
if (((movement > 0) && (position > INT64_MAX - movement))
|| ((movement < 0) && (position < INT64_MIN - movement)))
{
@@ -877,10 +936,18 @@ static PLSR_RESULT PlsrCheckInitialPosition(
}
else
{
- movement = (segmentData->pulseOrTarget > position)
+ result = PlsrPositionAbsoluteUnitsToPulses(
+ &snapshot->equivalent,
+ segmentData->pulseOrTarget,
+ &targetPosition);
+ if (result != PLSR_RESULT_OK)
+ {
+ return PLSR_RESULT_POSITION_OVERFLOW;
+ }
+ movement = (targetPosition > position)
? 1
- : ((segmentData->pulseOrTarget < position) ? -1 : 0);
- position = segmentData->pulseOrTarget;
+ : ((targetPosition < position) ? -1 : 0);
+ position = targetPosition;
}
if ((movement != 0) && (snapshot->initialDirectionPositive == 0U))
{
@@ -910,6 +977,84 @@ static PLSR_RESULT PlsrCheckInitialPosition(
return PLSR_RESULT_OK;
}
+static PLSR_RESULT PlsrConvertSnapshotSpeeds(
+ PLSR_JOB_SNAPSHOT *snapshot,
+ PLSR_PARSE_DETAIL *detail)
+{
+ uint16_t segment;
+ PLSR_RESULT result;
+
+ result = PlsrPositionSpeedToPulseFrequency(&snapshot->equivalent,
+ snapshot->s2.defaultSpeed,
+ &snapshot->s2.defaultSpeed);
+ if (result != PLSR_RESULT_OK) return result;
+ result = PlsrPositionSpeedToPulseFrequency(&snapshot->equivalent,
+ snapshot->s2.maximumSpeed,
+ &snapshot->s2.maximumSpeed);
+ if (result != PLSR_RESULT_OK) return result;
+ result = PlsrPositionSpeedToPulseFrequency(&snapshot->equivalent,
+ snapshot->s2.startSpeed,
+ &snapshot->s2.startSpeed);
+ if (result != PLSR_RESULT_OK) return result;
+ result = PlsrPositionSpeedToPulseFrequency(&snapshot->equivalent,
+ snapshot->s2.stopSpeed,
+ &snapshot->s2.stopSpeed);
+ if (result != PLSR_RESULT_OK) return result;
+ result = PlsrPositionSpeedToPulseFrequency(&snapshot->equivalent,
+ snapshot->s2.zrnHighSpeed,
+ &snapshot->s2.zrnHighSpeed);
+ if (result != PLSR_RESULT_OK) return result;
+ result = PlsrPositionSpeedToPulseFrequency(&snapshot->equivalent,
+ snapshot->s2.zrnCrawlSpeed,
+ &snapshot->s2.zrnCrawlSpeed);
+ if (result != PLSR_RESULT_OK) return result;
+
+ if ((snapshot->s2.maximumSpeed < PLSR_FREQUENCY_MIN_HZ)
+ || (snapshot->s2.maximumSpeed > PLSR_FREQUENCY_MAX_HZ))
+ {
+ PlsrSetDetail(detail,
+ PLSR_RESULT_INVALID_FREQUENCY,
+ PLSR_PARSE_BLOCK_S2,
+ 0UL,
+ (int32_t)snapshot->s2.maximumSpeed,
+ 0U);
+ return PLSR_RESULT_INVALID_FREQUENCY;
+ }
+ for (segment = 1U; segment <= snapshot->segmentCount; segment++)
+ {
+ PLSR_SEGMENT_SNAPSHOT *segmentData =
+ &snapshot->segments[segment - 1U];
+
+ if (segmentData->targetFrequency == 0UL)
+ {
+ continue;
+ }
+ result = PlsrPositionSpeedToPulseFrequency(
+ &snapshot->equivalent,
+ segmentData->targetFrequency,
+ &segmentData->targetFrequency);
+ if ((result != PLSR_RESULT_OK)
+ || (segmentData->targetFrequency < PLSR_FREQUENCY_MIN_HZ)
+ || (segmentData->targetFrequency > PLSR_FREQUENCY_MAX_HZ))
+ {
+ PlsrSetDetail(detail,
+ PLSR_RESULT_INVALID_FREQUENCY,
+ PLSR_PARSE_BLOCK_S0,
+ snapshot->s0.address
+ + (uint32_t)segment * PLSR_S0_SEGMENT_WORDS,
+ (int32_t)segmentData->targetFrequency,
+ segment);
+ return PLSR_RESULT_INVALID_FREQUENCY;
+ }
+ result = PlsrValidateFrequencyDivider(snapshot,
+ segmentData->targetFrequency,
+ detail,
+ segment);
+ if (result != PLSR_RESULT_OK) return result;
+ }
+ return PLSR_RESULT_OK;
+}
+
PLSR_RESULT PlsrBuildJobSnapshot(const PLSR_CALL *call,
const PLSR_PARSE_CONTEXT *context,
PLSR_JOB_SNAPSHOT *snapshot,
@@ -1186,14 +1331,6 @@ PLSR_RESULT PlsrBuildJobSnapshot(const PLSR_CALL *call,
segmentData->flags |= PLSR_SEGMENT_FLAG_SPEED_CLAMPED;
snapshot->speedClamped = 1U;
}
- if (segmentData->targetFrequency != 0UL)
- {
- result = PlsrValidateFrequencyDivider(snapshot,
- segmentData->targetFrequency,
- detail,
- segment);
- if (result != PLSR_RESULT_OK) return result;
- }
result = PlsrValidateWait(call, segmentData, segment, detail);
if (result != PLSR_RESULT_OK) return result;
result = PlsrValidateJump(call,
@@ -1205,6 +1342,8 @@ PLSR_RESULT PlsrBuildJobSnapshot(const PLSR_CALL *call,
if (result != PLSR_RESULT_OK) return result;
}
+ result = PlsrConvertSnapshotSpeeds(snapshot, detail);
+ if (result != PLSR_RESULT_OK) return result;
result = PlsrCheckConstantPath(snapshot, detail);
if (result != PLSR_RESULT_OK) return result;
result = PlsrCheckInitialPosition(snapshot, context, detail);
@@ -1246,14 +1385,22 @@ PLSR_RESULT PlsrResolveLiveFrequency(const PLSR_JOB_SNAPSHOT *snapshot,
NULL);
if (result != PLSR_RESULT_OK) return result;
if (rawFrequency < 0) return PLSR_RESULT_INVALID_FREQUENCY;
- *frequency = (rawFrequency == 0) ? snapshot->s2.defaultSpeed
+ *frequency = (rawFrequency == 0) ? snapshot->inputDefaultSpeed
: (uint32_t)rawFrequency;
*clamped = 0U;
- if (*frequency > snapshot->s2.maximumSpeed)
+ if (*frequency > snapshot->inputMaximumSpeed)
{
- *frequency = snapshot->s2.maximumSpeed;
+ *frequency = snapshot->inputMaximumSpeed;
*clamped = 1U;
}
if (*frequency == 0UL) return PLSR_RESULT_INVALID_FREQUENCY;
+ result = PlsrPositionSpeedToPulseFrequency(&snapshot->equivalent,
+ *frequency,
+ frequency);
+ if ((result != PLSR_RESULT_OK) || (*frequency == 0UL)
+ || (*frequency > PLSR_FREQUENCY_MAX_HZ))
+ {
+ return PLSR_RESULT_INVALID_FREQUENCY;
+ }
return PlsrValidateFrequencyDivider(snapshot, *frequency, NULL, segment);
}
diff --git a/PLSR/Src/plsr_path.c b/PLSR/Src/plsr_path.c
index 39d832f..cbb3015 100644
--- a/PLSR/Src/plsr_path.c
+++ b/PLSR/Src/plsr_path.c
@@ -88,13 +88,18 @@ static uint8_t PlsrPathSegmentIsZeroPulse(const PLSR_JOB_SNAPSHOT *job,
{
const PLSR_SEGMENT_SNAPSHOT *segmentSnapshot =
&job->segments[segment - 1U];
+ int64_t targetPosition;
if ((segmentSnapshot->flags & PLSR_SEGMENT_FLAG_ZERO_PULSE) != 0U)
{
return 1U;
}
if ((job->positioningMode == 1U)
- && (segmentSnapshot->pulseOrTarget == (int32_t)logicalPosition))
+ && (PlsrPositionAbsoluteUnitsToPulses(
+ &job->equivalent,
+ segmentSnapshot->pulseOrTarget,
+ &targetPosition) == PLSR_RESULT_OK)
+ && (targetPosition == logicalPosition))
{
return 1U;
}
diff --git a/PLSR/Src/plsr_position.c b/PLSR/Src/plsr_position.c
new file mode 100644
index 0000000..0f50faf
--- /dev/null
+++ b/PLSR/Src/plsr_position.c
@@ -0,0 +1,227 @@
+#include "plsr_position.h"
+#include
+#include
+
+uint8_t PlsrPositionUnitCodeIsValid(uint8_t unitCode)
+{
+ return ((unitCode == 0U) || (unitCode == 1U) || (unitCode == 3U)
+ || (unitCode == 5U) || (unitCode == 7U))
+ ? 1U
+ : 0U;
+}
+
+uint8_t PlsrPositionUsesEquivalent(const PLSR_EQUIVALENT_CONFIG *config)
+{
+ return ((config != NULL) && (config->unitCode != 0U)) ? 1U : 0U;
+}
+
+PLSR_RESULT PlsrPositionValidateEquivalent(
+ const PLSR_EQUIVALENT_CONFIG *config)
+{
+ if ((config == NULL)
+ || (PlsrPositionUnitCodeIsValid(config->unitCode) == 0U))
+ {
+ return PLSR_RESULT_INVALID_S2;
+ }
+ if ((config->unitCode != 0U)
+ && ((config->pulsesPerRevolution == 0UL)
+ || (config->movementPerRevolution == 0UL)))
+ {
+ return PLSR_RESULT_INVALID_S2;
+ }
+ return PLSR_RESULT_OK;
+}
+
+PLSR_RESULT PlsrPositionUnitsToPulses(
+ const PLSR_EQUIVALENT_CONFIG *config,
+ int32_t units,
+ int64_t remainder,
+ int64_t *pulses,
+ int64_t *newRemainder)
+{
+ int64_t numerator;
+ int64_t pulseValue;
+ uint32_t denominator;
+
+ if ((pulses == NULL) || (newRemainder == NULL)
+ || (PlsrPositionValidateEquivalent(config) != PLSR_RESULT_OK))
+ {
+ return PLSR_RESULT_INVALID_ARGUMENT;
+ }
+ if (config->unitCode == 0U)
+ {
+ *pulses = units;
+ *newRemainder = 0;
+ return PLSR_RESULT_OK;
+ }
+ denominator = config->movementPerRevolution;
+ if ((remainder <= -(int64_t)denominator)
+ || (remainder >= (int64_t)denominator))
+ {
+ return PLSR_RESULT_INVALID_ARGUMENT;
+ }
+
+ /* int32 * uint32 本身落在 int64 范围内;仅余数相加需要边界检查。 */
+ numerator = (int64_t)units
+ * (int64_t)config->pulsesPerRevolution;
+ if (((remainder > 0) && (numerator > INT64_MAX - remainder))
+ || ((remainder < 0) && (numerator < INT64_MIN - remainder)))
+ {
+ return PLSR_RESULT_POSITION_OVERFLOW;
+ }
+ numerator += remainder;
+ pulseValue = numerator / (int64_t)denominator;
+ *newRemainder = numerator % (int64_t)denominator;
+ *pulses = pulseValue;
+ return PLSR_RESULT_OK;
+}
+
+PLSR_RESULT PlsrPositionAbsoluteUnitsToPulses(
+ const PLSR_EQUIVALENT_CONFIG *config,
+ int32_t units,
+ int64_t *pulses)
+{
+ int64_t ignoredRemainder;
+
+ return PlsrPositionUnitsToPulses(config,
+ units,
+ 0,
+ pulses,
+ &ignoredRemainder);
+}
+
+static uint64_t PlsrPositionMagnitude(int64_t value)
+{
+ return (value < 0) ? (uint64_t)(-(value + 1)) + 1ULL
+ : (uint64_t)value;
+}
+
+PLSR_RESULT PlsrPositionPulsesToUnits(
+ const PLSR_EQUIVALENT_CONFIG *config,
+ int64_t pulses,
+ int64_t *units)
+{
+ uint64_t magnitude;
+ uint64_t quotient;
+ uint64_t remainder;
+ uint64_t whole;
+ uint64_t fraction;
+ uint64_t scaled;
+ uint64_t numerator;
+ uint64_t denominator;
+
+ if ((units == NULL)
+ || (PlsrPositionValidateEquivalent(config) != PLSR_RESULT_OK))
+ {
+ return PLSR_RESULT_INVALID_ARGUMENT;
+ }
+ if (config->unitCode == 0U)
+ {
+ *units = pulses;
+ return PLSR_RESULT_OK;
+ }
+
+ magnitude = PlsrPositionMagnitude(pulses);
+ numerator = config->movementPerRevolution;
+ denominator = config->pulsesPerRevolution;
+ quotient = magnitude / denominator;
+ remainder = magnitude % denominator;
+ if ((quotient != 0ULL) && (numerator > UINT64_MAX / quotient))
+ {
+ return PLSR_RESULT_POSITION_OVERFLOW;
+ }
+ whole = quotient * numerator;
+ /* remainder 与 numerator 均为 uint32 范围,乘积不会超过 uint64。 */
+ fraction = (remainder * numerator) / denominator;
+ if (whole > UINT64_MAX - fraction)
+ {
+ return PLSR_RESULT_POSITION_OVERFLOW;
+ }
+ scaled = whole + fraction;
+ if ((pulses >= 0) && (scaled > (uint64_t)INT64_MAX))
+ {
+ return PLSR_RESULT_POSITION_OVERFLOW;
+ }
+ if ((pulses < 0) && (scaled > (UINT64_C(1) << 63U)))
+ {
+ return PLSR_RESULT_POSITION_OVERFLOW;
+ }
+ if ((pulses < 0) && (scaled == (UINT64_C(1) << 63U)))
+ {
+ *units = INT64_MIN;
+ }
+ else
+ {
+ *units = (pulses < 0) ? -(int64_t)scaled : (int64_t)scaled;
+ }
+ return PLSR_RESULT_OK;
+}
+
+static PLSR_RESULT PlsrPositionScaleUnsigned(uint32_t value,
+ uint32_t numerator,
+ uint32_t denominator,
+ uint32_t *result)
+{
+ uint64_t product;
+ uint64_t scaled;
+
+ if ((result == NULL) || (denominator == 0UL))
+ {
+ return PLSR_RESULT_INVALID_ARGUMENT;
+ }
+ product = (uint64_t)value * (uint64_t)numerator;
+ /* 速度采用四舍五入;累计位置仍采用截断并由绝对累计避免漂移。 */
+ scaled = product / denominator;
+ if ((product % denominator) >= ((uint64_t)denominator + 1ULL) / 2ULL)
+ {
+ scaled++;
+ }
+ if (scaled > UINT32_MAX)
+ {
+ return PLSR_RESULT_POSITION_OVERFLOW;
+ }
+ *result = (uint32_t)scaled;
+ return PLSR_RESULT_OK;
+}
+
+PLSR_RESULT PlsrPositionSpeedToPulseFrequency(
+ const PLSR_EQUIVALENT_CONFIG *config,
+ uint32_t speed,
+ uint32_t *frequencyHz)
+{
+ if ((frequencyHz == NULL)
+ || (PlsrPositionValidateEquivalent(config) != PLSR_RESULT_OK))
+ {
+ return PLSR_RESULT_INVALID_ARGUMENT;
+ }
+ if (config->unitCode == 0U)
+ {
+ *frequencyHz = speed;
+ return PLSR_RESULT_OK;
+ }
+ return PlsrPositionScaleUnsigned(speed,
+ config->pulsesPerRevolution,
+ config->movementPerRevolution,
+ frequencyHz);
+}
+
+PLSR_RESULT PlsrPositionPulseFrequencyToSpeed(
+ const PLSR_EQUIVALENT_CONFIG *config,
+ uint32_t frequencyHz,
+ uint32_t *speed)
+{
+ if ((speed == NULL)
+ || (PlsrPositionValidateEquivalent(config) != PLSR_RESULT_OK))
+ {
+ return PLSR_RESULT_INVALID_ARGUMENT;
+ }
+ if (config->unitCode == 0U)
+ {
+ *speed = frequencyHz;
+ return PLSR_RESULT_OK;
+ }
+ return PlsrPositionScaleUnsigned(frequencyHz,
+ config->movementPerRevolution,
+ config->pulsesPerRevolution,
+ speed);
+}
diff --git a/PLSR/Src/plsr_self_test.c b/PLSR/Src/plsr_self_test.c
index 3544009..2403142 100644
--- a/PLSR/Src/plsr_self_test.c
+++ b/PLSR/Src/plsr_self_test.c
@@ -70,6 +70,13 @@ static void SelfTestWriteDword(PLSR_DEVICE_TYPE device,
SelfTestWords[device][address + 1UL] = (uint16_t)(value >> 16U);
}
+static void SelfTestWriteSfdDword(uint16_t address, uint32_t value)
+{
+ (void)PlcDeviceWriteSfd(address, (uint16_t)(value & 0xFFFFUL));
+ (void)PlcDeviceWriteSfd((uint16_t)(address + 1U),
+ (uint16_t)(value >> 16U));
+}
+
PLSR_RESULT PlsrSelfTestQueue(void)
{
PLSR_CALL call;
@@ -109,3 +116,45 @@ PLSR_RESULT PlsrSelfTestQueue(void)
call.outputModeOverride = PLSR_OUTPUT_AB;
return PlsrPostCall(&call);
}
+
+PLSR_RESULT PlsrEquivalentSelfTestQueue(void)
+{
+ PLSR_CALL call;
+
+ (void)memset(SelfTestWords, 0, sizeof(SelfTestWords));
+
+ /* SFD900 Bit10~8=001(1um当量);3脉冲/2单位。 */
+ (void)PlcDeviceWriteSfd(900U, (1U << 8U));
+ SelfTestWriteSfdDword(902U, 3UL);
+ SelfTestWriteSfdDword(904U, 2UL);
+ (void)PlcDeviceWriteSfd(906U, SELF_TEST_DIR_POINT);
+ (void)PlcDeviceWriteSfd(907U, 10U);
+ /* 当量换算后物理最高速度=90000Hz,不超过硬件100kHz。 */
+ SelfTestWriteSfdDword(956U, 60000UL);
+
+ /* 板端可观察版本:两段各1001工程单位。
+ * 3脉冲/2单位带余数换算后分别输出1501、1502脉冲,累计3003脉冲;
+ * 1500Hz附近持续约2s,避免原3脉冲自检在逻辑分析仪启动前已经结束。 */
+ SelfTestWriteDword(PLSR_DEVICE_D, SELF_TEST_S0_BASE, 2U);
+ SelfTestWriteDword(PLSR_DEVICE_D, SELF_TEST_S0_BASE + 10U, 1000UL);
+ SelfTestWriteDword(PLSR_DEVICE_D, SELF_TEST_S0_BASE + 12U, 1001UL);
+ SelfTestWriteDword(PLSR_DEVICE_D, SELF_TEST_S0_BASE + 20U, 1000UL);
+ SelfTestWriteDword(PLSR_DEVICE_D, SELF_TEST_S0_BASE + 22U, 1001UL);
+ SelfTestWriteDword(PLSR_DEVICE_D, SELF_TEST_S1_BASE, 0U);
+
+ (void)memset(&call, 0, sizeof(call));
+ call.sequence = 0xA5A6UL;
+ call.source.context = NULL;
+ call.source.validateWords = SelfTestValidateWords;
+ call.source.readWord = SelfTestReadWord;
+ call.source.readBit = SelfTestReadBit;
+ call.s0.device = PLSR_DEVICE_D;
+ call.s0.address = SELF_TEST_S0_BASE;
+ call.s1.device = PLSR_DEVICE_D;
+ call.s1.address = SELF_TEST_S1_BASE;
+ call.s2.type = PLSR_OPERAND_CONSTANT;
+ call.s2.constant = 1;
+ call.dAxis = 0U;
+ call.outputModeOverride = PLSR_OUTPUT_PULSE_DIR;
+ return PlsrPostCall(&call);
+}
diff --git a/PLSR/Test/run_host_tests.ps1 b/PLSR/Test/run_host_tests.ps1
index 571a63c..95ec94c 100644
--- a/PLSR/Test/run_host_tests.ps1
+++ b/PLSR/Test/run_host_tests.ps1
@@ -14,9 +14,17 @@ if (-not (Test-Path -LiteralPath $outputDirectory))
}
$tests = @(
+ @{
+ Name = 'test_plsr_position'
+ Sources = @(
+ "$workspacePath\PLSR\Src\plsr_position.c"
+ "$workspacePath\PLSR\Test\test_plsr_position.c"
+ )
+ },
@{
Name = 'test_plc_device'
Sources = @(
+ "$workspacePath\PLSR\Src\plsr_position.c"
"$workspacePath\PLSR\Src\plc_device.c"
"$workspacePath\PLSR\Src\plsr_persistence.c"
"$workspacePath\PLSR\Test\test_plc_device.c"
@@ -25,6 +33,7 @@ $tests = @(
@{
Name = 'test_plsr_core'
Sources = @(
+ "$workspacePath\PLSR\Src\plsr_position.c"
"$workspacePath\PLSR\Src\plc_device.c"
"$workspacePath\PLSR\Src\plsr_persistence.c"
"$workspacePath\PLSR\Src\plsr_resource.c"
@@ -39,6 +48,7 @@ $tests = @(
@{
Name = 'test_plsr_job'
Sources = @(
+ "$workspacePath\PLSR\Src\plsr_position.c"
"$workspacePath\PLSR\Src\plc_device.c"
"$workspacePath\PLSR\Src\plsr_persistence.c"
"$workspacePath\PLSR\Src\plsr_resource.c"
@@ -53,6 +63,7 @@ $tests = @(
@{
Name = 'test_plsr_path'
Sources = @(
+ "$workspacePath\PLSR\Src\plsr_position.c"
"$workspacePath\PLSR\Src\plc_device.c"
"$workspacePath\PLSR\Src\plsr_persistence.c"
"$workspacePath\PLSR\Src\plsr_resource.c"
@@ -67,6 +78,7 @@ $tests = @(
@{
Name = 'test_plsr_profile'
Sources = @(
+ "$workspacePath\PLSR\Src\plsr_position.c"
"$workspacePath\PLSR\Src\plsr_profile.c"
"$workspacePath\PLSR\Test\test_plsr_profile.c"
)
@@ -74,6 +86,7 @@ $tests = @(
@{
Name = 'test_plsr_hal'
Sources = @(
+ "$workspacePath\PLSR\Src\plsr_position.c"
"$workspacePath\PLSR\Src\plc_device.c"
"$workspacePath\PLSR\Src\plsr_persistence.c"
"$workspacePath\PLSR\Src\plsr_resource.c"
diff --git a/PLSR/Test/test_plc_device.c b/PLSR/Test/test_plc_device.c
index 83c443c..304ed84 100644
--- a/PLSR/Test/test_plc_device.c
+++ b/PLSR/Test/test_plc_device.c
@@ -269,6 +269,16 @@ static void TestSmAndSd(void)
TEST_CHECK(PlcDeviceWriteSd(1071U, 0) == PLC_DEVICE_READ_ONLY);
TEST_CHECK(PlcDeviceReadSd(1012U, &value) == PLC_DEVICE_INVALID_ADDRESS);
TEST_CHECK(PlcDevicePublishSd(4U, 0U, 0) == PLC_DEVICE_INVALID_ARGUMENT);
+ TEST_CHECK(PlcDevicePublishSdDword(2U, 2U, -123456)
+ == PLC_DEVICE_OK);
+ TEST_CHECK(PlcDeviceReadSdDword(1042U, &value) == PLC_DEVICE_OK);
+ TEST_CHECK(value == -123456);
+ TEST_CHECK(PlcDeviceReadSdDword(1043U, &value)
+ == PLC_DEVICE_INVALID_ADDRESS);
+ TEST_CHECK(PlcDevicePublishSdDword(2U, 9U, 0)
+ == PLC_DEVICE_INVALID_ARGUMENT);
+ TEST_CHECK(PlcDeviceReadSdDword(1070U, &value)
+ == PLC_DEVICE_INVALID_ADDRESS);
TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK);
TEST_CHECK(PlcDeviceReadSm(1040U, &state) == PLC_DEVICE_OK);
diff --git a/PLSR/Test/test_plsr_core.c b/PLSR/Test/test_plsr_core.c
index a2084d0..89c92ef 100644
--- a/PLSR/Test/test_plsr_core.c
+++ b/PLSR/Test/test_plsr_core.c
@@ -348,6 +348,7 @@ static void TestPositionCheckpointing(void)
{
PLSR_STATUS status;
int32_t hsdPosition;
+ int32_t hsdEquivalent;
/* 1. SET_POSITION 后立即写入 HSD 检查点。 */
TestReset();
@@ -360,6 +361,8 @@ static void TestPositionCheckpointing(void)
TEST_CHECK(PlcDeviceIsHsdDirty() == 0U);
TEST_CHECK(PlcDeviceReadHsdDword(0U, &hsdPosition) == PLC_DEVICE_OK);
TEST_CHECK(hsdPosition == 500);
+ TEST_CHECK(PlcDeviceReadHsdDword(2U, &hsdEquivalent) == PLC_DEVICE_OK);
+ TEST_CHECK(hsdEquivalent == 500);
/* 2. 模拟重启:位置与 positionValid 应恢复到上次正常停机的检查点。 */
TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK);
@@ -377,7 +380,7 @@ static void TestPositionCheckpointing(void)
TEST_CHECK(status.positionValid == 0U);
TEST_CHECK(status.logicalPosition == 500);
- /* 4. 64位位置超出 INT32 范围:对外回绕并置溢出诊断。 */
+ /* 4. 64位位置超出 INT32 范围:保留内部值、HSD保持最近合法值。 */
TEST_CHECK(TestPostCommand(2U,
0U,
PLSR_CMD_SET_POSITION,
@@ -387,8 +390,24 @@ static void TestPositionCheckpointing(void)
status = TestGetStatus(0U);
TEST_CHECK(status.lastCommandResult == PLSR_RESULT_OK);
TEST_CHECK(status.positionOverflow != 0U);
+ TEST_CHECK(status.logicalPosition == INT64_C(0x100000000));
+ TEST_CHECK(PlcDeviceReadHsdDword(0U, &hsdPosition) == PLC_DEVICE_OK);
+ TEST_CHECK(hsdPosition == 500);
+ TEST_CHECK(PlcDeviceReadHsdDword(2U, &hsdEquivalent) == PLC_DEVICE_OK);
+ TEST_CHECK(hsdEquivalent == 500);
+
+ /* 5. 清零位置重新建立有效坐标,并解除兼容发布溢出锁存。 */
+ TEST_CHECK(TestPostCommand(3U, 0U, PLSR_CMD_CLEAR_POSITION, 0)
+ == PLSR_RESULT_QUEUED);
+ PlsrProcess();
+ status = TestGetStatus(0U);
+ TEST_CHECK(status.logicalPosition == 0);
+ TEST_CHECK(status.positionValid != 0U);
+ TEST_CHECK(status.positionOverflow == 0U);
TEST_CHECK(PlcDeviceReadHsdDword(0U, &hsdPosition) == PLC_DEVICE_OK);
TEST_CHECK(hsdPosition == 0);
+ TEST_CHECK(PlcDeviceReadHsdDword(2U, &hsdEquivalent) == PLC_DEVICE_OK);
+ TEST_CHECK(hsdEquivalent == 0);
}
int main(void)
diff --git a/PLSR/Test/test_plsr_hal.c b/PLSR/Test/test_plsr_hal.c
index f644a69..f40fec1 100644
--- a/PLSR/Test/test_plsr_hal.c
+++ b/PLSR/Test/test_plsr_hal.c
@@ -81,6 +81,14 @@ static void TestWriteDword(TEST_MEMORY *memory,
memory->words[device][address + 1UL] = (uint16_t)(raw >> 16U);
}
+static void TestWriteSfdDword(uint16_t address, uint32_t value)
+{
+ CHECK(PlcDeviceWriteSfd(address, (uint16_t)(value & 0xFFFFUL))
+ == PLC_DEVICE_OK);
+ CHECK(PlcDeviceWriteSfd((uint16_t)(address + 1U),
+ (uint16_t)(value >> 16U)) == PLC_DEVICE_OK);
+}
+
static void TestSetSegment(TEST_MEMORY *memory,
uint16_t number,
uint32_t frequency,
@@ -104,6 +112,20 @@ 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;
@@ -134,6 +156,20 @@ static PLSR_STATUS TestGetStatus(void)
return status;
}
+static int32_t TestReadSdDword(uint16_t lowAddress)
+{
+ int32_t lowWord = 0;
+ int32_t highWord = 0;
+ uint32_t rawValue;
+
+ CHECK(PlcDeviceReadSd(lowAddress, &lowWord) == PLC_DEVICE_OK);
+ CHECK(PlcDeviceReadSd((uint16_t)(lowAddress + 1U), &highWord)
+ == PLC_DEVICE_OK);
+ rawValue = ((uint32_t)lowWord & 0xFFFFUL)
+ | (((uint32_t)highWord & 0xFFFFUL) << 16U);
+ return (int32_t)rawValue;
+}
+
/* ---- HAL 单测 ---- */
static void TestMapping(void)
@@ -316,6 +352,7 @@ static void TestAbPhaseAndCounting(void)
CHECK(PlsrHwTestGetCnt(0U)
== ((PlsrHwTestGetArr(0U) + 1UL) * 3UL) / 4UL);
CHECK(PlsrHwTestGetCnt(1U) == PlsrHwTestGetCcr(1U));
+ TestCompleteFirstAbPrime(0U);
/* 任一物理 timer update 不能直接计作完整 AB 周期。 */
PlsrHwTestTriggerUpdate(0U);
@@ -433,6 +470,9 @@ static void TestTwoAbAxesIndependent(void)
CHECK(PlsrHwTestGetPwmEnabled(2U) != 0U);
CHECK(PlsrHwTestGetPwmEnabled(3U) != 0U);
+ TestCompleteFirstAbPrime(0U);
+ TestCompleteFirstAbPrime(2U);
+
for (quarter = 0; quarter < 4; quarter++)
{
PlsrHwTestAdvanceAbQuarter(0U);
@@ -473,6 +513,8 @@ static void TestAbFrequencyLimits(void)
== 2UL * (PlsrHwTestGetPsc(1U) + 1UL));
CHECK(PlsrHwTestGetArr(0U) <= 65535UL);
+ TestCompleteFirstAbPrime(0U);
+
CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK);
for (quarter = 0; quarter < 4; quarter++)
{
@@ -580,6 +622,11 @@ static void TestEndToEndTwoSegments(void)
status = TestGetStatus();
CHECK(status.state == PLSR_STATE_ACCEL);
CHECK(status.currentSegment == 2U);
+ CHECK(status.logicalPosition == 100);
+ CHECK(status.taskPulses == 100);
+ CHECK(status.totalPulses == 100);
+ CHECK(TestReadSdDword(1000U) == 2);
+ CHECK(TestReadSdDword(1002U) == 0);
/* 段2:DIR 延时 → PWM → 加速 → RUN。 */
for (ticks = 0; ticks < 600; ticks++)
@@ -603,6 +650,23 @@ static void TestEndToEndTwoSegments(void)
status = TestGetStatus();
CHECK(status.state == PLSR_STATE_COMPLETED);
CHECK(status.done != 0U);
+ CHECK(status.logicalPosition == 300);
+ CHECK(status.taskPulses == 300);
+ CHECK(status.totalPulses == 300);
+ CHECK(TestReadSdDword(1000U) == 2);
+ CHECK(TestReadSdDword(1002U) == 200);
+ CHECK(TestReadSdDword(1004U) == 200);
+ CHECK(TestReadSdDword(1006U) == 0);
+ {
+ int32_t hsdPulses;
+ int32_t hsdEquivalent;
+
+ CHECK(PlcDeviceReadHsdDword(0U, &hsdPulses) == PLC_DEVICE_OK);
+ CHECK(hsdPulses == 300);
+ CHECK(PlcDeviceReadHsdDword(2U, &hsdEquivalent)
+ == PLC_DEVICE_OK);
+ CHECK(hsdEquivalent == 300);
+ }
/* 终态转换后 HAL 回 IDLE(允许重新启动),脉冲已停止。 */
CHECK(PlsrHwIsPulseActive(0U) == 0U);
CHECK(PlsrHwTestGetPwmEnabled(0U) == 0U);
@@ -633,6 +697,8 @@ static void TestEndToEndAbSegment(void)
CHECK(PlsrHwTestGetPwmEnabled(0U) != 0U);
CHECK(PlsrHwTestGetPwmEnabled(1U) != 0U);
+ TestCompleteFirstAbPrime(0U);
+
/* 负脉冲选择反向相序,完整两个周期后由同一事件链结束任务。 */
PlsrHwTestAdvanceAbQuarter(0U);
CHECK(PlsrHwTestGetAbPhaseA(0U) == 0U);
@@ -647,11 +713,222 @@ static void TestEndToEndAbSegment(void)
status = TestGetStatus();
CHECK(status.state == PLSR_STATE_COMPLETED);
CHECK(status.done != 0U);
+ CHECK(status.directionPositive == 0U);
+ CHECK(status.logicalPosition == -2);
+ CHECK(status.taskPulses == -2);
+ CHECK(status.totalPulses == 2);
+ CHECK(TestReadSdDword(1002U) == -2);
+ CHECK(TestReadSdDword(1004U) == -2);
CHECK(status.highResourceMask == 0U);
CHECK(PlsrHwTestGetPwmEnabled(0U) == 0U);
CHECK(PlsrHwTestGetPwmEnabled(1U) == 0U);
}
+static void TestPositionOnImmediateStop(void)
+{
+ TEST_MEMORY memory;
+ PLSR_CALL call;
+ PLSR_COMMAND command;
+ PLSR_STATUS status;
+ int pulse;
+ int tick;
+
+ TestResetEnvironment();
+ (void)memset(&memory, 0, sizeof(memory));
+ TestWriteDword(&memory, PLSR_DEVICE_D, TEST_S0_BASE, 1);
+ TestSetSegment(&memory, 1U, 1000U, 100);
+ call = TestMakeCall(&memory);
+ call.sequence = 30UL;
+ CHECK(PlsrPostCall(&call) == PLSR_RESULT_QUEUED);
+ PlsrProcess();
+ for (tick = 0; tick < 10; tick++)
+ {
+ PlsrProcess();
+ }
+ CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING);
+
+ for (pulse = 0; pulse < 37; pulse++)
+ {
+ PlsrHwTestTriggerUpdate(0U);
+ }
+ PlsrProcess();
+ status = TestGetStatus();
+ CHECK(status.logicalPosition == 37);
+ CHECK(status.taskPulses == 37);
+ CHECK(status.totalPulses == 37);
+
+ command.sequence = 31UL;
+ command.axis = 0U;
+ command.opcode = PLSR_CMD_STOP_IMMEDIATE;
+ command.argument = 0;
+ CHECK(PlsrPostCommand(&command) == PLSR_RESULT_QUEUED);
+ PlsrProcess();
+ CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_IDLE);
+ CHECK(PlsrPostEvent(0U, PLSR_EVENT_STOP_IMMEDIATE_DONE)
+ == PLSR_RESULT_OK);
+ PlsrProcess();
+ status = TestGetStatus();
+ CHECK(status.state == PLSR_STATE_STOPPED);
+ CHECK(status.logicalPosition == 37);
+ CHECK(status.taskPulses == 37);
+ CHECK(status.totalPulses == 37);
+ CHECK(TestReadSdDword(1002U) == 37);
+
+ command.sequence = 32UL;
+ command.opcode = PLSR_CMD_CLEAR_TOTAL;
+ CHECK(PlsrPostCommand(&command) == PLSR_RESULT_QUEUED);
+ PlsrProcess();
+ status = TestGetStatus();
+ CHECK(status.logicalPosition == 37);
+ CHECK(status.totalPulses == 0);
+}
+
+static void TestAbsolutePositionAccounting(void)
+{
+ TEST_MEMORY memory;
+ PLSR_CALL call;
+ PLSR_COMMAND command;
+ PLSR_STATUS status;
+ int32_t hsdPosition;
+ int pulse;
+ int tick;
+
+ TestResetEnvironment();
+ command.sequence = 40UL;
+ command.axis = 0U;
+ command.opcode = PLSR_CMD_SET_POSITION;
+ command.argument = 100;
+ 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, 130);
+ TestWriteDword(&memory, PLSR_DEVICE_D, TEST_S1_BASE, 1);
+ call = TestMakeCall(&memory);
+ call.sequence = 41UL;
+ CHECK(PlsrPostCall(&call) == PLSR_RESULT_QUEUED);
+ PlsrProcess();
+ for (tick = 0; tick < 10; tick++)
+ {
+ PlsrProcess();
+ }
+ CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING);
+ for (pulse = 0; pulse < 30; pulse++)
+ {
+ PlsrHwTestTriggerUpdate(0U);
+ }
+ PlsrProcess();
+ status = TestGetStatus();
+ CHECK(status.state == PLSR_STATE_COMPLETED);
+ CHECK(status.logicalPosition == 130);
+ CHECK(status.taskPulses == 30);
+ CHECK(status.totalPulses == 30);
+ CHECK(status.positionValid != 0U);
+ CHECK(PlcDeviceReadHsdDword(0U, &hsdPosition) == PLC_DEVICE_OK);
+ CHECK(hsdPosition == 130);
+}
+
+static void TestEquivalentRemainderAccounting(void)
+{
+ TEST_MEMORY memory;
+ PLSR_CALL call;
+ PLSR_STATUS status;
+ int32_t hsdPulses;
+ int32_t hsdEquivalent;
+
+ TestResetEnvironment();
+ CHECK(PlcDeviceWriteSfd(900U, (1U << 8U)) == PLC_DEVICE_OK);
+ TestWriteSfdDword(902U, 3UL);
+ TestWriteSfdDword(904U, 2UL);
+ TestWriteSfdDword(956U, 60000UL);
+ CHECK(PlcDeviceWriteSfd(907U, 0U) == PLC_DEVICE_OK);
+
+ (void)memset(&memory, 0, sizeof(memory));
+ TestWriteDword(&memory, PLSR_DEVICE_D, TEST_S0_BASE, 1);
+ TestSetSegment(&memory, 1U, 1000U, 1);
+ call = TestMakeCall(&memory);
+ call.sequence = 30UL;
+ call.outputModeOverride = PLSR_OUTPUT_PULSE_DIR;
+
+ /* 3脉冲/2单位:第一次1单位只输出1脉冲并保存1/2余数。 */
+ CHECK(PlsrPostCall(&call) == PLSR_RESULT_QUEUED);
+ PlsrProcess();
+ CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING);
+ PlsrHwTestTriggerUpdate(0U);
+ PlsrProcess();
+ status = TestGetStatus();
+ CHECK(status.state == PLSR_STATE_COMPLETED);
+ CHECK(status.logicalPosition == 1);
+ CHECK(status.taskPulses == 1);
+ CHECK(status.totalPulses == 1);
+ CHECK(PlcDeviceReadHsdDword(0U, &hsdPulses) == PLC_DEVICE_OK);
+ CHECK(PlcDeviceReadHsdDword(2U, &hsdEquivalent) == PLC_DEVICE_OK);
+ CHECK(hsdPulses == 1);
+ CHECK(hsdEquivalent == 0);
+
+ /* 第二次1单位合并余数后输出2脉冲;两次合计精确为3脉冲/2单位。 */
+ call.sequence = 31UL;
+ CHECK(PlsrPostCall(&call) == PLSR_RESULT_QUEUED);
+ PlsrProcess();
+ PlsrHwTestTriggerUpdate(0U);
+ CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING);
+ PlsrHwTestTriggerUpdate(0U);
+ PlsrProcess();
+ status = TestGetStatus();
+ CHECK(status.state == PLSR_STATE_COMPLETED);
+ CHECK(status.logicalPosition == 3);
+ CHECK(status.taskPulses == 2);
+ CHECK(status.totalPulses == 3);
+ CHECK(PlcDeviceReadHsdDword(0U, &hsdPulses) == PLC_DEVICE_OK);
+ CHECK(PlcDeviceReadHsdDword(2U, &hsdEquivalent) == PLC_DEVICE_OK);
+ CHECK(hsdPulses == 3);
+ CHECK(hsdEquivalent == 2);
+ CHECK(TestReadSdDword(1002U) == 2);
+ CHECK(TestReadSdDword(1004U) == 1);
+}
+
+static void TestEquivalentCompatibleError(void)
+{
+ TEST_MEMORY memory;
+ PLSR_CALL call;
+ PLSR_STATUS status;
+ int32_t errorCode = -1;
+ int32_t errorBlock = -1;
+
+ TestResetEnvironment();
+ CHECK(PlcDeviceWriteSfd(900U, (1U << 8U)) == PLC_DEVICE_OK);
+ TestWriteSfdDword(902U, 0UL);
+ TestWriteSfdDword(904U, 2UL);
+ TestWriteSfdDword(956U, 60000UL);
+
+ (void)memset(&memory, 0, sizeof(memory));
+ TestWriteDword(&memory, PLSR_DEVICE_D, TEST_S0_BASE, 1);
+ TestSetSegment(&memory, 1U, 1000U, 1);
+ call = TestMakeCall(&memory);
+ call.sequence = 32UL;
+ call.outputModeOverride = PLSR_OUTPUT_PULSE_DIR;
+
+ CHECK(PlsrPostCall(&call) == PLSR_RESULT_QUEUED);
+ PlsrProcess();
+ status = TestGetStatus();
+ CHECK(status.lastCommandResult == PLSR_RESULT_INVALID_S2);
+ CHECK(status.state == PLSR_STATE_IDLE);
+ CHECK(PlcDeviceReadSd(1010U, &errorCode) == PLC_DEVICE_OK);
+ CHECK(PlcDeviceReadSd(1011U, &errorBlock) == PLC_DEVICE_OK);
+ CHECK(errorCode == 2);
+ CHECK(errorBlock == 0);
+
+ /* A valid retry clears the compatible parameter error. */
+ TestWriteSfdDword(902U, 3UL);
+ call.sequence = 33UL;
+ CHECK(PlsrPostCall(&call) == PLSR_RESULT_QUEUED);
+ PlsrProcess();
+ CHECK(PlcDeviceReadSd(1010U, &errorCode) == PLC_DEVICE_OK);
+ CHECK(errorCode == 0);
+ CHECK(PlsrHwStopPulse(0U) == PLSR_RESULT_OK);
+}
+
static void TestProductionSelfTestStartsAb(void)
{
PLSR_STATUS status;
@@ -670,6 +947,55 @@ static void TestProductionSelfTestStartsAb(void)
CHECK(PlsrHwStopPulse(0U) == PLSR_RESULT_OK);
}
+static void TestEquivalentSelfTest(void)
+{
+ PLSR_STATUS status;
+ int32_t hsdPulses;
+ int32_t hsdEquivalent;
+ int ticks;
+
+ TestResetEnvironment();
+ CHECK(PlsrEquivalentSelfTestQueue() == PLSR_RESULT_QUEUED);
+ PlsrProcess();
+
+ /* SFD907=10ms. Advance the simulated hardware delay before segment 1. */
+ for (ticks = 0; ticks < 10; ticks++)
+ {
+ PlsrProcess();
+ }
+ CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING);
+
+ /* 3 pulses / 2 units: 1001 units emit 1501 pulses and keep 1/2 remainder. */
+ for (ticks = 0; ticks < 1501; ticks++)
+ {
+ PlsrHwTestTriggerUpdate(0U);
+ }
+ PlsrProcess();
+ CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING);
+
+ /* Segment 2 consumes the remainder and therefore emits 1502 pulses. */
+ for (ticks = 0; ticks < 1502; ticks++)
+ {
+ PlsrHwTestTriggerUpdate(0U);
+ }
+ PlsrProcess();
+
+ status = TestGetStatus();
+ CHECK(status.lastCommandResult == PLSR_RESULT_OK);
+ CHECK(status.outputMode == PLSR_OUTPUT_PULSE_DIR);
+ CHECK(status.state == PLSR_STATE_COMPLETED);
+ CHECK(status.currentSegment == 2U);
+ CHECK(status.logicalPosition == 3003);
+ CHECK(status.taskPulses == 3003);
+ CHECK(status.totalPulses == 3003);
+ CHECK(PlcDeviceReadHsdDword(0U, &hsdPulses) == PLC_DEVICE_OK);
+ CHECK(PlcDeviceReadHsdDword(2U, &hsdEquivalent) == PLC_DEVICE_OK);
+ CHECK(hsdPulses == 3003);
+ CHECK(hsdEquivalent == 2002);
+ CHECK(TestReadSdDword(1002U) == 1502);
+ CHECK(TestReadSdDword(1004U) == 1001);
+}
+
static void TestStopStopsHardware(void)
{
TEST_MEMORY memory;
@@ -722,7 +1048,12 @@ int main(void)
TestStopAndInvalidArgs();
TestEndToEndTwoSegments();
TestEndToEndAbSegment();
+ TestPositionOnImmediateStop();
+ TestAbsolutePositionAccounting();
+ TestEquivalentRemainderAccounting();
+ TestEquivalentCompatibleError();
TestProductionSelfTestStartsAb();
+ TestEquivalentSelfTest();
TestStopStopsHardware();
if (TestFailures != 0)
diff --git a/PLSR/Test/test_plsr_job.c b/PLSR/Test/test_plsr_job.c
index 545f80b..ad6876c 100644
--- a/PLSR/Test/test_plsr_job.c
+++ b/PLSR/Test/test_plsr_job.c
@@ -362,6 +362,53 @@ static void TestOutputAndDivider(void)
CHECK(PlcDeviceWriteSfd(900U, 0) == PLC_DEVICE_OK);
}
+static void TestEquivalentSnapshot(void)
+{
+ TEST_MEMORY memory;
+ PLSR_CALL call;
+ PLSR_PARSE_CONTEXT context = {0, 1U};
+ PLSR_JOB_SNAPSHOT snapshot;
+ PLSR_PARSE_DETAIL detail;
+ uint32_t liveFrequency;
+ uint8_t clamped;
+
+ TestBuildValidBlocks(&memory);
+ TestWriteDword(&memory, PLSR_DEVICE_D, 110U, 40000);
+ call = TestMakeCall(&memory);
+ CHECK(PlcDeviceWriteSfd(900U, (1U << 8U)) == PLC_DEVICE_OK);
+ TestWriteSfdDword(902U, 3UL);
+ TestWriteSfdDword(904U, 2UL);
+ TestWriteSfdDword(956U, 60000UL);
+ TestWriteSfdDword(958U, 0UL);
+
+ CHECK(PlsrBuildJobSnapshot(&call, &context, &snapshot, &detail)
+ == PLSR_RESULT_OK);
+ CHECK(snapshot.equivalent.unitCode == 1U);
+ CHECK(snapshot.equivalent.pulsesPerRevolution == 3UL);
+ CHECK(snapshot.equivalent.movementPerRevolution == 2UL);
+ CHECK(snapshot.inputDefaultSpeed == 1000UL);
+ CHECK(snapshot.inputMaximumSpeed == 60000UL);
+ CHECK(snapshot.s2.defaultSpeed == 1500UL);
+ CHECK(snapshot.s2.maximumSpeed == 90000UL);
+ CHECK(snapshot.segments[0].targetFrequency == 60000UL);
+ CHECK(snapshot.segments[1].targetFrequency == 1500UL);
+
+ TestWriteDword(&memory, PLSR_DEVICE_D, 110U, 20000);
+ CHECK(PlsrResolveLiveFrequency(&snapshot,
+ 1U,
+ &liveFrequency,
+ &clamped) == PLSR_RESULT_OK);
+ CHECK(liveFrequency == 30000UL);
+ CHECK(clamped == 0U);
+
+ TestWriteSfdDword(902U, 0UL);
+ CHECK(PlsrBuildJobSnapshot(&call, &context, &snapshot, &detail)
+ == PLSR_RESULT_INVALID_S2);
+ CHECK(detail.address == 902U);
+
+ TestConfigureAxis0K1();
+}
+
static void TestCoreSubmission(void)
{
TEST_MEMORY memory;
@@ -443,6 +490,7 @@ int main(void)
TestValidationFailures();
TestDynamicReferences();
TestOutputAndDivider();
+ TestEquivalentSnapshot();
TestDefaultSfdStartable();
TestCoreSubmission();
diff --git a/PLSR/Test/test_plsr_position.c b/PLSR/Test/test_plsr_position.c
new file mode 100644
index 0000000..c208086
--- /dev/null
+++ b/PLSR/Test/test_plsr_position.c
@@ -0,0 +1,93 @@
+#include "plsr_position.h"
+#include
+#include
+
+static int TestChecks;
+static int TestFailures;
+
+#define CHECK(condition) \
+ do \
+ { \
+ TestChecks++; \
+ if (!(condition)) \
+ { \
+ TestFailures++; \
+ (void)printf("FAIL line %d: %s\n", __LINE__, #condition); \
+ } \
+ } while (0)
+
+int main(void)
+{
+ PLSR_EQUIVALENT_CONFIG config = {3UL, 2UL, 1U};
+ PLSR_EQUIVALENT_CONFIG pulseConfig = {0UL, 0UL, 0U};
+ int64_t pulses;
+ int64_t units;
+ int64_t remainder;
+ uint32_t speed;
+
+ CHECK(PlsrPositionUnitCodeIsValid(0U) != 0U);
+ CHECK(PlsrPositionUnitCodeIsValid(1U) != 0U);
+ CHECK(PlsrPositionUnitCodeIsValid(3U) != 0U);
+ CHECK(PlsrPositionUnitCodeIsValid(5U) != 0U);
+ CHECK(PlsrPositionUnitCodeIsValid(7U) != 0U);
+ CHECK(PlsrPositionUnitCodeIsValid(2U) == 0U);
+ CHECK(PlsrPositionValidateEquivalent(&config) == PLSR_RESULT_OK);
+
+ CHECK(PlsrPositionUnitsToPulses(&config, 1, 0, &pulses, &remainder)
+ == PLSR_RESULT_OK);
+ CHECK(pulses == 1);
+ CHECK(remainder == 1);
+ CHECK(PlsrPositionUnitsToPulses(&config,
+ 1,
+ remainder,
+ &pulses,
+ &remainder) == PLSR_RESULT_OK);
+ CHECK(pulses == 2);
+ CHECK(remainder == 0);
+
+ CHECK(PlsrPositionUnitsToPulses(&config, -1, 0, &pulses, &remainder)
+ == PLSR_RESULT_OK);
+ CHECK(pulses == -1);
+ CHECK(remainder == -1);
+ CHECK(PlsrPositionUnitsToPulses(&config,
+ -1,
+ remainder,
+ &pulses,
+ &remainder) == PLSR_RESULT_OK);
+ CHECK(pulses == -2);
+ CHECK(remainder == 0);
+
+ CHECK(PlsrPositionAbsoluteUnitsToPulses(&config, 10, &pulses)
+ == PLSR_RESULT_OK);
+ CHECK(pulses == 15);
+ CHECK(PlsrPositionPulsesToUnits(&config, 3, &units) == PLSR_RESULT_OK);
+ CHECK(units == 2);
+ CHECK(PlsrPositionPulsesToUnits(&config, -3, &units) == PLSR_RESULT_OK);
+ CHECK(units == -2);
+ CHECK(PlsrPositionSpeedToPulseFrequency(&config, 200U, &speed)
+ == PLSR_RESULT_OK);
+ CHECK(speed == 300U);
+ CHECK(PlsrPositionPulseFrequencyToSpeed(&config, 300U, &speed)
+ == PLSR_RESULT_OK);
+ CHECK(speed == 200U);
+
+ CHECK(PlsrPositionUnitsToPulses(&pulseConfig,
+ INT32_MIN,
+ 99,
+ &pulses,
+ &remainder) == PLSR_RESULT_OK);
+ CHECK(pulses == INT32_MIN);
+ CHECK(remainder == 0);
+ config.pulsesPerRevolution = 0UL;
+ CHECK(PlsrPositionValidateEquivalent(&config) == PLSR_RESULT_INVALID_S2);
+
+ if (TestFailures != 0)
+ {
+ (void)printf("FAIL: %d of %d PLSR position checks failed\n",
+ TestFailures,
+ TestChecks);
+ return 1;
+ }
+ (void)printf("PASS: %d PLSR position checks\n", TestChecks);
+ return 0;
+}
diff --git a/tmp/pdfs/xinjie_page_30.png b/tmp/pdfs/xinjie_page_30.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a6ee1fdba27aa72d5d9d58c08efebefa77f5a60
GIT binary patch
literal 336416
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