APIs, concepts, guides, and more
Gantry

Control a single linear axis with two motors.

The term “gantry” defines a system with 2 motors controlling a single linear axis. Each motor/bearing system is separated by a finite distance orthogonal to the direction of the axis. Small rotational motion about the center of the gantry (due to differential motion of the 2 motor/bearing systems) is usually undesirable and will lead to mechanical binding if the rotational motion is excessive.

Image

This rotational motion is treated as a separate axis for one of the gantry configurations described here. Alternatively, it may be set to zero within the gantry algorithm for linear motion. Frequently, gantries also incorporate a transverse axis, which allows X-Y placement of the supported payload. Consequently, a transverse axis may result in a dynamic center of mass for the gantry axis. In this situation, the gantry axis motion at high acceleration rates induces significant moments (twisting) about the yaw axis. These moments depend on the linear acceleration rate and the transverse position of the cross-axis mass. A gantry configuration with 2 motors and a minimum of 2 encoders has the inherent advantage of providing 2 control loops that can actively resist moments (twisting) about the yaw axis.

The most common mechanical systems use either linear motors or rotary motors with ballscrews (or belts). These provide motion on either an air-bearing or linear slide-supported stage. In addition, the gantry may use either a single encoder or 2 encoders for position information of the 2 motor/bearing systems. Typically, linear encoders are co-located with each motor/bearing system.

Note
Gantry feature is applicable when running drives in Torque mode (not common because most EtherCAT drives run in Position mode).

🔹 Standard Gantry Configuration

The term “Standard Gantry” applies to a control scheme where separate PIDs directly control each motor. The Standard Gantry has the convenient feature of providing a separate yaw axis for gantry alignment, and it also allows observing yaw position error during linear motion.

The standard gantry configuration results in a control loop that controls each motor the same as if they were separate (non-gantry) motors. The standard gantry configuration provides the convenience of commanding the motion in linear and yaw components, thereby simplifying programming and lab testing

Figure 1: Standard Gantry with Yaw Axis

Figure 2: Standard Gantry without Yaw Axis

Figure 3: XMP Configuration

A goal of the gantry system is to move both separate motor systems together so that negligible “crabbing” occurs (wherein one motor leads or lags the other). Since each side of the gantry receives the same linear command position (with zero yaw command), a lagging motor/encoder provides a greater error signal to that respective control loop. Due to the mechanical coupling of the gantry beam (or transverse axis), motion from one motor/bearing system can appear as a disturbance on the other motor/bearing system (particularly within the same bandwidth, since they have identical or similar control elements). This coupling of disturbances can reduce system performance.

Standard Gantry with Yaw Axis

Refer to Figure 1. The same linear command position (X) is issued to each motor’s PID control. The feedback from each motor is compared directly with the sum of the linear command (X) and yaw command positions. Note that for the first motor, the junction of the linear and yaw commands uses a positive yaw contribution, while for the second motor, the yaw contribution is subtracted from the linear command position.

Standard Gantry without Yaw Axis

Refer to Figure 2. The same linear command position (X) is issued to each motor’s PID control and the feedback from each motor is compared directly with that command position.

Implementation

The synthesis of the linear and yaw actual positions is shown in Figure 3, which reveals the architectural implementation of the standard gantry. Linear actual position (X axis) is one-half of the sum of the positions of encoders 0 and 1. Yaw actual position is one-half of the difference of encoders 0 and 1. These actual positions are visible through the axis object, as are the command positions.

The axes use these synthesized actual positions to calculate the position errors and feed them to the respective filters, which control the two motors.

From this diagram, you can see that the output of PID/PIV0 is unaffected by the actual position value of encoder 1 and that conversely, the output of PID/PIV1 is unaffected but the actual position value of encoder 0. This results from the fact that 0.5 (-ActPos1 + ActPos1) enters Filter[0], and 0.5(-ActPos0 + ActPos0) enters Filter [1]. Using this observation, it is clear that this implementation is equivalent to the Figure 1 representation of the gantry configurations.

Figure 3’s implementation of the gantry configuration can be summarized by the following equations:

X (Linear) Axis Actual Position = Motor X0Encoder Value + Motor X1 Encoder Value

Yaw (Rotary) Axis Actual Position = Motor X0 Encoder Value - Motor X1 Encoder Value

X (Linear) Axis Position Error = X Axis Command Position - X Axis Actual Position

Yaw (Rotary) Axis Actual Position = Yaw Axis Command Position - Yaw Axis Actual Position

Filter[0] Position Error Input = X Axis Command Position + Yaw Axis Position Error

Filter[1] Position Error Input = X Axis Command Position - Yaw Axis Position Error

The following examples illustrate the use of these equations by the gantry configuration:

🔹 Examples

Rotary Error Only

Assuming only rotary position error with the following command positions and encoder values (defined as encoder counts):

Yaw (Rotary) Axis Command Position: 0

X (Linear) Axis Command Position: 0

Motor X0 (left) Encoder Value: +50

Motor X1 (right) Encoder Value: -50

The Gantry configuration will report to the following actual positions and generate the following position errors for the filters:

X (Linear) Axis Actual Position = (50 + 50) / 2 = 0

Yaw (Rotary) Axis Actual Position = (+50 - (-50)) / 2= 50

X (Linear) Axis Position Error = 0 - 0 = 0

Yaw (Rotary) Axis Position Error = 0 - 50 = -50

As a result, the PID or PIV filters in the filter objects will receive the following error signals after they have been summed:

Filter[0] Position Error Input = 0 + (-50) = -50

Filter[1] Position Error Input = 0 - (-50) = +50

Given these filter error inputs, Filter ) will command a Motor X0 (left) DAC output that will move the left motor from +50 back to 0, and Filter 1 will command a Motor X1 (right) DAC output that will move the rotary axis to its command position of 0.

Linear and Rotary Error

Assuming both linear and rotary position error with the following command positions and encoder values (defined as encoder counts):

Yaw (Rotary) Axis Command Position: 0

X (Linear) Axis Command Position: 0

Motor X0 (left) Encoder Value: +150

Motor X1 (right) Encoder Value: +50

The Gantry configuration will report the following actual positions and generate the following position errors for the filters:

X (Linear) Axis Actual Position = (150 + 50) / 2 = 100

Yaw (Rotary) Axis Actual Position = (150 - 50) / 2 = 50

X (Linear) Axis Position Error = 0 - 100 = -100

Yaw (Rotary) Axis Position Error = 0 - 50 = -50

As a result, the PID or PIV filters in the filter objects will receive the following error signals after they have been summed:

Filter[0] Position Error Input = -100 + (-50) = -150

Filter[1] Position Error Input = -100 - (-50) = -50

Given these filter error inputs, Filter 0 will command a Motor X0 (left) DAC output that will move the left motor from 150 back to 0, and Filter 1 will command a Motor X1 (right) DAC output that will move the right motor from 50 back to 0. The effect is to reduce both the linear and rotary axis position errors and move the linear and rotary axes to their command positions of 0. Note that both the X (linear) and Yaw (rotary) position errors are observable at -100 and -50 counts, respectively.

🔹 Simple, Single Encoder Gantry

The Single Encoder gantry configuration is similar to the Standard Gantry configuration, except that in a Single Encoder gantry, there is only one closed-loop control loop surrounding the “Master” motor/bearing system, while the second “Slave” motor/bearing system operates in open-loop mode (no feedback). There are 2 types of Single Encoder gantries:

Figure 4: Single Encoder Gantry: Single Drive Configuration

Figure 5: Single Encoder Gantry: Dual Drive Configuration

Note that for both configurations, the mechanical gantry must be very stiff to resist yaw rotation, and a separate yaw axis is not visible to the control system.

The single encoder gantry configuration is very similar to a single motor/encoder control loop which provides linear motion to a gantry that is completely restrained in yaw by mechanical means. Generally, this scheme is best applied to the conveyor belt and other low-performance applications.

Single Encoder Gantry with 1 Drive

In the single drive configuration, the PID output of the “Master” drives a single amplifier, which powers both motors. This configuration reduces costs because a single, higher power drive is typically less expensive than 2 lower power drives.

Single Encoder Gantry with 2 Drives

In a dual-drive configuration, the PID output of the “Master” drives 2 amplifiers, each of which powers a motor.

📜 Sample Code

  • C++

    #include "rsi.h" // Import our RapidCode Library.
    #include "SampleAppsHelper.h" // Import our SampleApp helper functions.
    using namespace RSI::RapidCode; // Import the RapidCode namespace
    #pragma region GantryDeclares
    Axis *linearAxis;
    Axis *yawAxis;
    int linearAxisNumber = 0;
    int yawAxisNumber = 1;
    int defaultEncoderNumerator = 0; // 0 disables
    int defaultEncoderDenominator = 0;
    int gantryEncoderNumerator = 1; // we want 1/2
    int gantryEncoderDenominator = 2;
    double x1PrimaryCoeff = 1.0;
    double x2PrimaryCoeff = 1.0;
    double x1SecondaryCoeff = 1.0;
    double x2SecondaryCoeff = -1.0;
    double defaultPrimaryCoeff = 1.0;
    double defaultSecondaryCoeff = 0.0;
    uint64_t x1EncoderAddress;
    uint64_t x2EncoderAddress;
    uint64_t x1FilterPrimaryPointerAddress;
    uint64_t x1FilterSecondaryPointerAddress;
    uint64_t x2FilterPrimaryPointerAddress;
    uint64_t x2FilterSecondaryPointerAddress;
    uint64_t x1FilterPrimaryCoefficientAddress;
    uint64_t x1FilterSecondaryCoefficientAddress;
    uint64_t x2FilterPrimaryCoefficientAddress;
    uint64_t x2FilterSecondaryCoefficientAddress;
    uint64_t x1AxisLinkAddress;
    uint64_t x2AxisLinkAddress;
    #pragma endregion
    #pragma region GantryMethods
    void ReadAddressesFromMotionController()
    {
    x1EncoderAddress = linearAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeENCODER_PRIMARY);
    x2EncoderAddress = yawAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeENCODER_PRIMARY);
    x1FilterPrimaryPointerAddress = linearAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeFILTER_PRIMARY_POINTER);
    x1FilterSecondaryPointerAddress = linearAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeFILTER_SECONDARY_POINTER);
    x2FilterPrimaryPointerAddress = yawAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeFILTER_PRIMARY_POINTER);
    x2FilterSecondaryPointerAddress = yawAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeFILTER_SECONDARY_POINTER);
    x1FilterPrimaryCoefficientAddress = linearAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeFILTER_PRIMARY_COEFF);
    x1FilterSecondaryCoefficientAddress = linearAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeFILTER_SECONDARY_COEFF);
    x2FilterPrimaryCoefficientAddress = yawAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeFILTER_PRIMARY_COEFF);
    x2FilterSecondaryCoefficientAddress = yawAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeFILTER_SECONDARY_COEFF);
    x1AxisLinkAddress = linearAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeAXIS_LINK);
    x2AxisLinkAddress = yawAxis->AddressGet(RSIAxisAddressType::RSIAxisAddressTypeAXIS_LINK);
    }
    void SetupEncoderMixing(bool enableGantry)
    {
    if (enableGantry)
    {
    // first scale encoders by half
    linearAxis->EncoderRatioSet(RSIMotorFeedback::RSIMotorFeedbackPRIMARY, gantryEncoderNumerator, gantryEncoderDenominator);
    yawAxis->EncoderRatioSet(RSIMotorFeedback::RSIMotorFeedbackPRIMARY, gantryEncoderNumerator, gantryEncoderDenominator);
    mc->OS->Sleep(10);
    // mix encoders (add on linear, subtract for yaw)
    linearAxis->FeedbackPointerSet(RSIAxisPositionInput::RSIAxisPositionInputFIRST, x1EncoderAddress);
    linearAxis->FeedbackPointerSet(RSIAxisPositionInput::RSIAxisPositionInputSECOND, x2EncoderAddress);
    linearAxis->GantryTypeSet(RSIAxisGantryType::RSIAxisGantryTypeADD);
    yawAxis->FeedbackPointerSet(RSIAxisPositionInput::RSIAxisPositionInputFIRST, x1EncoderAddress);
    yawAxis->FeedbackPointerSet(RSIAxisPositionInput::RSIAxisPositionInputSECOND, x2EncoderAddress);
    yawAxis->GantryTypeSet(RSIAxisGantryType::RSIAxisGantryTypeSUBTRACT);
    }
    else
    {
    linearAxis->EncoderRatioSet(RSIMotorFeedback::RSIMotorFeedbackPRIMARY, defaultEncoderNumerator, defaultEncoderDenominator);
    yawAxis->EncoderRatioSet(RSIMotorFeedback::RSIMotorFeedbackPRIMARY, defaultEncoderNumerator, defaultEncoderDenominator);
    mc->OS->Sleep(10);
    linearAxis->FeedbackPointerSet(RSIAxisPositionInput::RSIAxisPositionInputFIRST, x1EncoderAddress);
    linearAxis->FeedbackPointerSet(RSIAxisPositionInput::RSIAxisPositionInputSECOND, x1EncoderAddress);
    linearAxis->GantryTypeSet(RSIAxisGantryType::RSIAxisGantryTypeNONE);
    yawAxis->FeedbackPointerSet(RSIAxisPositionInput::RSIAxisPositionInputFIRST, x2EncoderAddress);
    yawAxis->FeedbackPointerSet(RSIAxisPositionInput::RSIAxisPositionInputSECOND, x2EncoderAddress);
    yawAxis->GantryTypeSet(RSIAxisGantryType::RSIAxisGantryTypeNONE);
    }
    }
    void SetupFilterMixing(bool enableGantry)
    {
    if (enableGantry)
    {
    // mix X1 filter
    mc->MemorySet(x1FilterPrimaryPointerAddress, mc->FirmwareAddressGet(x1AxisLinkAddress));
    mc->MemorySet(x1FilterSecondaryPointerAddress, mc->FirmwareAddressGet(x2AxisLinkAddress));
    // mix X2 filter
    mc->MemorySet(x2FilterPrimaryPointerAddress, mc->FirmwareAddressGet(x1AxisLinkAddress));
    mc->MemorySet(x2FilterSecondaryPointerAddress, mc->FirmwareAddressGet(x2AxisLinkAddress));
    // setup X1 filter mixing coefficients
    mc->MemoryDoubleSet(x1FilterPrimaryCoefficientAddress, x1PrimaryCoeff);
    mc->MemoryDoubleSet(x1FilterSecondaryCoefficientAddress, x1SecondaryCoeff);
    // setup X2 filter mixing coefficients
    mc->MemoryDoubleSet(x2FilterPrimaryCoefficientAddress, x2PrimaryCoeff);
    mc->MemoryDoubleSet(x2FilterSecondaryCoefficientAddress, x2SecondaryCoeff);
    }
    else
    {
    // unmix X1 filter
    mc->MemorySet(x1FilterPrimaryPointerAddress, mc->FirmwareAddressGet(x1AxisLinkAddress));
    mc->MemorySet(x1FilterSecondaryPointerAddress, mc->FirmwareAddressGet(x1AxisLinkAddress));
    // unmix X2 filter
    mc->MemorySet(x2FilterPrimaryPointerAddress, mc->FirmwareAddressGet(x2AxisLinkAddress));
    mc->MemorySet(x2FilterSecondaryPointerAddress, mc->FirmwareAddressGet(x2AxisLinkAddress));
    // setup X1 filter defult coefficients
    mc->MemoryDoubleSet(x1FilterPrimaryCoefficientAddress, defaultPrimaryCoeff);
    mc->MemoryDoubleSet(x1FilterSecondaryCoefficientAddress, defaultSecondaryCoeff);
    // setup X2 filter default coefficients
    mc->MemoryDoubleSet(x2FilterPrimaryCoefficientAddress, defaultPrimaryCoeff);
    mc->MemoryDoubleSet(x2FilterSecondaryCoefficientAddress, defaultSecondaryCoeff);
    }
    }
    void GantryEnable(bool enable)
    {
    ReadAddressesFromMotionController();
    linearAxis->Abort();
    yawAxis->Abort();
    SetupEncoderMixing(enable);
    SetupFilterMixing(enable);
    linearAxis->ClearFaults();
    linearAxis->AmpEnableSet(true);
    yawAxis->ClearFaults();
    yawAxis->AmpEnableSet(true);
    }
    #pragma endregion
    void gantryMain()
    {
    // Create and initialize MotionController
    mc = MotionController::CreateFromSoftware();
    // Get Axis X0 and X1 respectively.
    linearAxis = mc->AxisGet(linearAxisNumber);
    yawAxis = mc->AxisGet(yawAxisNumber);
    //Only need once
    ReadAddressesFromMotionController();
    //Enable when desired.
    GantryEnable(true);
    //Disable when finished.
    GantryEnable(false);
    }
    Represents a single axis of motion control. This class provides an interface for commanding motion,...
    Definition rsi.h:5643
    Represents the RMP soft motion controller. This class provides an interface to general controller con...
    Definition rsi.h:796
    static void CheckErrors(RapidCodeObject *rsiObject)
    Checks for errors in the given RapidCodeObject and throws an exception if any non-warning errors are ...