Skip to content

Restructuring DS402 support. Added MCS2ChanParameters ST and additional FBs to support cleaner and extra operations aside from FB_motionStage & ST_MotionStage#249

Open
yannS2016 wants to merge 5 commits intopcdshub:masterfrom
yannS2016:ECS-9134
Open

Restructuring DS402 support. Added MCS2ChanParameters ST and additional FBs to support cleaner and extra operations aside from FB_motionStage & ST_MotionStage#249
yannS2016 wants to merge 5 commits intopcdshub:masterfrom
yannS2016:ECS-9134

Conversation

@yannS2016
Copy link
Contributor

@yannS2016 yannS2016 commented Jan 20, 2026

Description

https://jira.slac.stanford.edu/browse/ECS-9134

  • Restructuring DS402 support. Added MCS2ChanParameters ST and additional FBs to support cleaner and extra operations aside from FB_motionStage & ST_MotionStage
  • Splitting SDO(channel state status, open loop parameter configurations) DS402 MCS2 support into smaller functional blocks

Motivation and Context

DREAM slit and Sample Paddle Support.

How Has This Been Tested?

Tested in the Maker space context with SmartAct MCS2 EtherCAT drive and BSD PLC

Where Has This Been Documented?

Code contains appropriate documentation

Pre-merge checklist

  • Code works interactively
  • Test suite passes locally
  • Code contains descriptive comments
  • Libraries are set to Always Newest version (Library, *)
  • Committed with pre-commit or ran pre-commit run --all-files

@yannS2016 yannS2016 requested review from a team as code owners January 20, 2026 18:32
@yannS2016 yannS2016 requested review from NSLentz and removed request for a team January 20, 2026 18:32
…al Splits

- Ensured stage position is set only on the very first homing request for a stage, preventing cumulative motor/stage offset errors without the need for explicit offset values on the motor record.
- Added `pytmc` pragma for MCS2 channelstate raw status.
- Removed unused variable/error checks for improved code clarity.
- Split SDO (channel state status, open-loop parameter configurations) and DS402 MCS2 support into more modular, smaller functional blocks.
- Removed duplicate MCS2 structures and cleaned up legacy mode selection methods.
- Restructured DS402 support logic:
    - Introduced `MCS2ChanParameters` structure.
    - Added new FBs for modular channel support (in addition to FB_motionStage & ST_MotionStage).
- Improved support for reading safety flags and module parameter updates; refactored SDO operations for clarity and maintainability.
- Added support for calibrations; split and renamed SDO parameter read & update methods for maintainable operation.
NSLentz
NSLentz previously approved these changes Feb 24, 2026
Copy link
Contributor

@NSLentz NSLentz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really too long to properly review but looking through I don't notice any red flags. The PV name lengths could be reduced to help with our max pv length restrictions.

@yannS2016
Copy link
Contributor Author

yannS2016 commented Feb 25, 2026

This is really too long to properly review but looking through I don't notice any red flags. The PV name lengths could be reduced to help with our max pv length restrictions.

yeah, something I've suggested myself, just removed the PLC prefix on properties.

…ge, for ease of testing. moved error reporting to a dedicated FB
<Released>false</Released>
<Title>lcls-twincat-motion</Title>
<ProjectVersion>0.0.0</ProjectVersion>
<ProjectVersion>1.0.0</ProjectVersion>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<ProjectVersion>1.0.0</ProjectVersion>
<ProjectVersion>0.0.0</ProjectVersion>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be. will handle the version in the release stage

Comment on lines -160 to -162
<Compile Include="DUTs\ST_MotionEpicsInterface.TcDUT">
<SubType>Code</SubType>
</Compile>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this being removed? I don't see any changes to the file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, before 4.6.0, this was not needed anymore as we decided to create a separate lib for motion abstraction.

Comment on lines +9 to +20
Unknown := 0, // Default/uncategorized
MoveDone, // Absolute move completed
StepMoveDone, // Step/JOG move completed
HomeDone, // Homing completed
CalibrationDone, // Calibration completed
ResetDone, // Reset command completed
HaltDone, // Manual halt completed
AbortedHome, // Home aborted
AbortedStepMove, // Step move aborted
AbortedCalibration, // Calibration aborted
Referenced, // Axis became referenced
Calibrated // Axis became calibrated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be capitalized?

Comment on lines +161 to +162
{attribute 'hide'}
nChanFErrWinIdx : WORD;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these hidden?

Comment on lines +205 to +206
// Temporary workaround: Avoid declaring mc_power externally from FB_MotionStage
bOverrideDirEnable : BOOL := TRUE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this workaround still needed? I don't see it used anywhere

HOME := 10,
GEAR := 30
GEAR := 30,
CALIBRATE:=40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah. check the motion stage

@KaushikMalapati
Copy link
Contributor

Generally speaking, I think it'd be nice if all the MCS2 function blocks were coupled together somehow so you wouldn't need to instantiate and handle every one manually for every axis in a project.

{attribute 'hide'}
ftStepUpdateDone : F_TRIG;
tonStartUp : TON;
nJogParamStep : INT := 0; // 0=idle/start, 1=steps, 2=amp, 3=freq, 10=done, -1=error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an enum

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah. i'll turn it into one

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also do it for the other FB_Write***Parameters?

Comment on lines +65 to +69
<ST><![CDATA[IF NOT __ISVALIDREF(stDS402Drive)
OR NOT __ISVALIDREF(stMotionStage)
OR NOT __ISVALIDREF(stMCS2ChanParameter) THEN
RETURN;
END_IF
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to automatically perform this check for all function blocks? I see you add it manually often

Comment on lines +228 to +230
nScalededSteps:=LIMIT(-1000, stMCS2ChanParameter.nChanStep, 1000);
nScalededStepAmp:=REAL_TO_UINT(LIMIT(50, stMCS2ChanParameter.nChanStepAmp, 100) * 655.35);
nScaledStepFreq:=REAL_TO_UINT(LIMIT(500, stMCS2ChanParameter.nChanStepFreq, 1000.0));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do these values have to be limited and should the limits be configurable?

Comment on lines +72 to +75
tonStartUp(
IN := bEnable AND NOT bStepModeParamsSet,
PT := T#4S
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this timer be called outside the if bEnable block? If it's inside bEnable=FALSE will never reset it because nScalededSteps won't be called

ftStepUpdateDone : F_TRIG;
tonStartUp : TON;
nJogParamStep : INT := 0; // 0=idle/start, 1=steps, 2=amp, 3=freq, 10=done, -1=error
bForceSequentialSDOs : BOOL := FALSE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this only for debugging? It's never written to

Comment on lines +44 to +45
// Set bits 0-3 for calibration enable
stDS402Drive.nDriveControl.3 := TRUE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the comment as "set bits 0 to 3" but you only set the fourth bit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the comment as "set bits 0 to 3" but you only set the fourth bit?

legacy comment, will be reworded

END_IF

E_MoveState.IN_PROGRESS:
IF bHalt OR bReset THEN
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bHalt and bReset are used in the exact same way so one of them is redundant

stMotionStage.bError := TRUE;
eStepState := E_MoveState.ERROR;
ELSE
IF tonSettlingTime.Q AND stDS402Drive.stDriveStatus.TargetReached
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need tonSettlingTime if we can always check stDS402Drive.stDriveStatus.TargetReached and why is the settling time constant? Wouldn't different stages have different settling times

Copy link
Contributor

@KaushikMalapati KaushikMalapati Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should function block this be removed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is almost exactly identical to FB_MCS2Calibrate. What is the intended difference?

Comment on lines +43 to +53
IF ftErrorReadDone.Q AND bEnable THEN
IF NOT fbErrorRead.bError AND nPiezoErrorCode <> 0 THEN
stMotionStage.nErrorId := nPiezoErrorCode;
END_IF
stMotionStage.sErrorMessage := F_MotionErrorCodeLookup(nErrorId:=stMotionStage.nErrorId);
fbLogError( stMotionStage:=stMotionStage, bEnable:=stMotionStage.bError);
ELSE
// just return the NC error
stMotionStage.sErrorMessage := F_MotionErrorCodeLookup(nErrorId:=stMotionStage.nErrorId);
fbLogError( stMotionStage:=stMotionStage, bEnable:=stMotionStage.bError);
END_IF
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be written a little simpler by taking the two error lines out of the if block and not having an else case

Comment on lines +32 to +40
fbErrorRead(
sNetId:= THIS^.stMotionStage.stAxisParameters.sAmsNetId,
nSlaveAddr:=stDS402Drive.nSlaveAddr,
nIndex:=stMCS2ChanParameter.nChanErrorCodeIdx,
nSubIndex :=0,
pDstBuf:= ADR(nPiezoErrorCode),
cbBufLen:=SIZEOF(nPiezoErrorCode),
bExecute:= stMotionStage.bError AND bEnable
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the timeout input be used so we can't wait indefinitely for this to return?

…eManager.TcPOU


fix doc string syntax

Co-authored-by: KaushikMalapati <80156796+KaushikMalapati@users.noreply.github.com>
@yannS2016
Copy link
Contributor Author

@KaushikMalapati are we missing anything for the screens?

bReferenced := TRUE;
eStepState := E_MoveState.DONE;
ELSE
bReferenced := FALSE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we only used homed or only referenced instead of using them interchangeably

Comment on lines +64 to +68
E_MoveState.STARTED:
IF NOT stDS402Drive.stDriveStatus.TargetReached
AND NOT stDS402Drive.stDriveStatus.Bit13_OpModeSpecific THEN
eStepState := E_MoveState.IN_PROGRESS;
END_IF
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can skip this and just go from INIT to IN_PROGRESS

Comment on lines +84 to +89
1: // RESET FAULT
stDS402Drive.nDriveControl := 16#80;
tonStartupStep(IN := TRUE);
IF tonStartupStep.Q THEN
tonStartupStep(IN := FALSE);
eStartupState := 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should have made a method to set and wait nDriveControl instead of repeating this pattern

Comment on lines +122 to +155
IF bOperational THEN
CASE stDS402Drive.nModeOfOperationDisplay OF
E_DS402OpMode.CSP:
bCSPModeEnabled := TRUE;
bHomeModeEnabled := FALSE;
bStepModeEnabled := FALSE;
bCalibrationModeEnabled := FALSE;
E_DS402OpMode.HOME:
bCSPModeEnabled := FALSE;
bHomeModeEnabled := TRUE;
bStepModeEnabled := FALSE;
bCalibrationModeEnabled := FALSE;
E_DS402OpMode.MCS2_OL_STEP_MODE:
bCSPModeEnabled := FALSE;
bHomeModeEnabled := FALSE;
bStepModeEnabled := TRUE;
bCalibrationModeEnabled := FALSE;
E_DS402OpMode.MCS2_CALIBRATION:
bCSPModeEnabled := FALSE;
bHomeModeEnabled := FALSE;
bStepModeEnabled := FALSE;
bCalibrationModeEnabled := TRUE;
ELSE
bCSPModeEnabled := FALSE;
bHomeModeEnabled := FALSE;
bStepModeEnabled := FALSE;
bCalibrationModeEnabled := FALSE;
END_CASE
ELSE
bCSPModeEnabled := FALSE;
bHomeModeEnabled := FALSE;
bStepModeEnabled := FALSE;
bCalibrationModeEnabled := FALSE;
END_IF
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be simplified by setting all four booleans false and having a switch where each case just sets a single variable true

END_IF

// Mode switch/request evaluator
IF bUserExec AND (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be set to false in the case?

@KaushikMalapati
Copy link
Contributor

@KaushikMalapati are we missing anything for the screens?

I believe so, but install your latest branch of this library onto the dream motion and rebuild/restart the ioc so I can double check?

Comment on lines +60 to +63
tonStartUp(
IN := bEnable AND NOT bCalibrationsParamsSet,
PT := T#4S
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there something we can poll for instead of waiting for a fixed period of time?

Comment on lines +271 to +273
IF ftFErrWinSetDone.Q OR ftSoftLimMinSetDone.Q OR ftSoftLimMaxSetDone.Q OR
ftMaxCloseLoopFreqSetDone.Q OR ftSensorPowerModeSetDone.Q OR
ftLogicalScaleOffsetSetDone.Q OR ftLogicalScaleInvSetDone.Q THEN
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think every falling trigger might need its own if statement. It doens't make sense that you would set the error based on fbSetFErrorWin if it's not done but any of the other ones are.

END_VAR
]]></Declaration>
<Implementation>
<ST><![CDATA[nScaledFErrWin:=LREAL_TO_DINT(( stMotionStage.stAxisParameters.fCtrlPosDiffMax / MAX( stMotionStage.stAxisParameters.fEncScaleFactorInternal, stMCS2ChanParameter.fScalingFactor)));
Copy link
Contributor

@KaushikMalapati KaushikMalapati Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind explaining why we take the max here and how these two values could be different? In any case, I would store it in variable instead of recalculating for the soft limits

END_VAR

VAR PERSISTENT
nCommandLocal : INT;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is nCommandLocal being saved?

Comment on lines +1078 to +1079
<ST><![CDATA[(*Do not change a moving axis parameter*)
fbMcWriteParameter(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to check for a null stMotionStage reference here?

<Method Name="WriteParameterNC" Id="{a04e1bd8-55ae-495c-bca4-be27f11603b4}">
<Declaration><![CDATA[METHOD WriteParameterNC
VAR_INPUT
Execute:BOOL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you call this method with execute false?

Comment on lines +1019 to +1023
//AND stMCS2ChanParameter.bOverrideDirEnable;
stMotionStage.bAllBackwardEnable:= stMotionStage.bLimitBackwardEnable
AND ( stMotionStage.bGantryBackwardEnable OR NOT stMotionStage.bGantryAxis)
AND stMotionStage.stEPSBackwardEnable.bEPS_OK;
//AND stMCS2ChanParameter.bOverrideDirEnable;
Copy link
Contributor

@KaushikMalapati KaushikMalapati Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do the bOverrideDirEnable lines need to be removed or uncommented with OR?

END_VAR
]]></Declaration>
<Implementation>
<ST><![CDATA[(* SmartACT: The encoder count from the Piezo drive is not a raw count from the embeded actuator device
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<ST><![CDATA[(* SmartACT: The encoder count from the Piezo drive is not a raw count from the embeded actuator device
<ST><![CDATA[(* SmartACT: The encoder count from the Piezo drive is not a raw count from the embedded actuator device

Comment on lines +986 to +990
IF stMotionStage.nRawEncoderDINT <> 0 THEN
stMotionStage.nEncoderCount:=DINT_TO_UDINT(ABS( stMotionStage.nRawEncoderDINT));
ELSE
stMotionStage.nEncoderCount:=0;
END_IF
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IF check is unnecessary

ELSE
fMeasuredPos:=DINT_TO_REAL( stMotionStage.nRawEncoderDINT) * MAX( stMotionStage.stAxisParameters.fEncScaleFactorInternal, stMCS2ChanParameter.fScalingFactor);
stMotionStage.fPosDiff:= stMotionStage.fPosition - fMeasuredPos;
// NB: Not actual in open loop
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does NB mean?

fMeasuredAcc := stMotionStage.Axis.NcToPlc.ActAcc;
stMotionStage.fPosDiff:= stMotionStage.Axis.NcToPlc.PosDiff;
ELSE
fMeasuredPos:=DINT_TO_REAL( stMotionStage.nRawEncoderDINT) * MAX( stMotionStage.stAxisParameters.fEncScaleFactorInternal, stMCS2ChanParameter.fScalingFactor);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are two possible different scaling factors and why do use the larger one?

Comment on lines +604 to +605
(*Mandatory must be unique for each MCS2 axis *)
eModule : E_Module;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this only needs to be unique for each MCS2 axis, why let the user set it? Can't you have a GVL with an atomic counter and have each FB_MotionStageMCS2 use its value as its eModule and then increment it or some other automatic way?

(* Execute on rising edge*)
IF rtMoveCmdShortcut.Q AND NOT stMotionStage.bExecute THEN
stMotionStage.bExecute := TRUE;
stMotionStage.nCommand := E_EpicsMotorCmd.MOVE_ABSOLUTE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you forget to set nCommandLocal here?

bExecHome:=bLocalExec AND NOT bExecMove;

(* When we start, set the busy/done appropriately
NB: CLose loop control using NC *)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does NB mean?

@yannS2016
Copy link
Contributor Author

Thanks @KaushikMalapati for the second pass, I'll go through the comments and suggestions whenever possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants