STEPPER MOTOR CONTROLLER
To top of [MainTopics]
[NextTopic] [MainTopics]
[Requirements] [Hardware] [ControlHardware] [Firmware] [ControllerFpgaInterface] [Software]
MOTOR DRIVERS
Each motor driver comprises a relatively sophisticated circuit, such as two (one for each winding) Ericsson (or Rifa, SGS-Thompson, and others) PBL3717, which translates the two power bits into actual power levels and the two phase bits into the four phase patterns.
The power bits are called I0 and I1. They select the power level as follows:
I1/I0 = 00 => High power.
I1/I0 = 01 => Medium power.
I1/I0 = 10 => Low power.
I1/I0 = 11 => Off.
The phase control bits are called PH0 and PH1. Each controls the direction of current flow in one of the motor's windings. The output drivers are full bridges, whose motor terminals are arbitrarily called A and B. When the phase bit is 0, positive current flows from B to A; when 1, from A to B. Although there are only two phase signals and the motors are called two-phase steppers, there are four step phases. Motors are connected to the drive terminals such that the controls operate as follows (for full-step, two-phase-on motor drive):
For clockwise (CW) rotation:
PH1/PH0 = 00 (0)
PH1/PH0 = 01 (1)
PH1/PH0 = 11 (3)
PH1/PH0 = 10 (2)
For counter-clockwise (CCW) rotation:
PH1/PH0 = 10 (2)
PH1/PH0 = 11 (3)
PH1/PH0 = 01 (1)
PH1/PH0 = 00 (0)
MOTOR DRIVER JACKS
Each motor drive circuit connects to a 5-pin SIP .1" On-Center jack with pin 3 (center) removed to serve as a key. Pins 1 and 2 connect to BOUT and AOUT, respectively, of PHASE 0. Pins 4 and 5 connect to BOUT and AOUT, respectively, of PHASE 1. The key is very important. If the motor connector is shifted over by one pin, one or both drivers will be destroyed.
OUTPUT SCAN CHAIN
The motor drivers are connected to a serial scan chain of the controlling IML unit. This scan chain is similar to the I/O scan chain used by most IML units but is dedicated to controlling motors and cannot be used for any other purpose. In fact, the IML CPU can't directly access the scan chain, which is controlled by an FPGA-based coprocessor.
A pair of the driver ICs drives each motor. The current control inputs, I1 and I0 are connected in parallel, so each pair requires only four scan chain outputs. The drivers are connected to the scan chain in nibbles (groups of four) as follows:
bit 0 => PH0
bit 1 => PH1
bit 2 => I0
bit 3 => I1
Each motor's control nibble is located in the scan chain at the motor number times four. For example, the control bits for motor 9 are: 36 = PH0, 37 = PH1, 38 = I0, 39 = I1. The 20 motors require 80 bits but the scan chain actually comprises 11 bytes because one extra byte is provided for checking the integrity of the scan chain. The FPGA contains all 11 bytes but the external chain may be smaller. For example, if only 10 motors are used, the external chain need only contain five bytes without the integrity check or six bytes with it. It should be noted that the integrity check doesn't require an external register if the total length of external output and input chains is no more than the length of the output chain internal to the FPGA.
MOTOR FLAGS
For each motor there are two input flags. These are in no way physically connected to or functionally dedicated to the associated motor and may be used for any purpose. However, the flag jacks are physically located next to the associated motor jack for wire bundling convenience and it is expected that they will be used for "home" detection. A motor's "home" can be any detectable position and not just at one end of travel. A motor can have two detectable positions, using its own flags, or more by stealing flags from its neighbors.
MOTOR FLAG JACKS
Each motor flag comprises a 4-pin SIP .1" On-Center jack. It is assumed that this is connected to a 3-pin or 4-pin opto-interruptor. The 3-pin device includes an integral LED current limit resistor while the 4-pin provides direct LED and phototransistor contacts. Jack pin 1 connects to 5V. In the opto-interruptor it connects to the anode of the LED. Pin 2 connects to 10K pull-up to 5V. This is the sense line. In the opto-interruptor it connects to the open collector of the NPN phototransistor. Pin 3 connects to digital ground. In the opto-interruptor it connects to the phototransistor's emitter. In the 3-pin device, this is also the LED's cathode. Pin 4 connects to 5V through a 470-Ohm resistor. It is not used for the 3-pin device but connects to the LED's cathode in the 4-pin opto-interruptor. The pin arrangement of any particular opto-interruptor may not match the jack, in which case cables are not straight-through. This should not pose a burden to manufacturing because these cables are fabricated from discrete wires in any case.
INPUT SCAN CHAIN
The motor flag jacks are connected to a standard I/O scan chain of the motor control IML unit. Unlike the motor control output scan chain, the CPU can directly access the input scan chain. The motor flags have no unique characteristics and could be used for any purpose.
When connected to an opto-interruptor, a flag input is logical 1 when the device is blocked because this turns off the pull-down phototransistor. The input is logical 0 when the device is open. Therefore, if the mechanical flag is a tab, logical 1 indicates home; if the flag is a notch, home is indicated by logical 0. Any device that can pull the 10K sense resistor below .8V may also drive the inputs.
As there are only two inputs but four outputs associated with each of the 20 motors, the input scan chain may be half as long as the output. This presents no fundamental architectural problem, but the FPGA that implements the scan chains may make the two chains of equal length for internal reasons. In this case, there will be unused bits in the input scan chain internal to the FPGA. Depending on the FPGA design, the unused bits may lie in a block outside of the range of real inputs, which lie in a contiguous space, or they may interleave with the real inputs, creating a sparse space. However, the latter is not recommended unless the "external" inputs are actually integral to the FPGA because the unused inputs have to be matched by external register bits.
The original MSM FPGA design illustrates the complexity that results from making the output and input chains of equal length in the FPGA. The FPGA's internal output and input chains each comprise 11 bytes (10 bytes to support 20 motors plus one for the integrity check register). Inputs are assigned starting at the last byte in order to create a monotonic bit chain. Therefore, the first bit of the external input scan chain is located at address 87. However, the FPGA skips odd addresses for the first set of four pairs of motor flags, which are implemented via the FPGA's own pins. Thus, motor 0 flag 1is located at bit address 86 and flag 2 at address 84. M1F1, M1F2, M2F1, M2F2, M3F1, and M3F2 are located at 82, 80, 78, 76, 74, and 72, respectively. The rest of the external inputs are really external, beginning at the RPMD (Right Panel Motor Driver) and ending at the Loader board. These inputs have contiguous addresses because every external scan chain input register bit is attached to a real input flag. The RPMD's 12 flags, M4F1 through M9F2 are assigned addresses 60 through 71. The remaining 10 pairs of flags are assigned addresses 40 through 59. Bit addresses 8 through 39 are implemented in the FPGA's internal input scan chain but serve no function. Bits 0 through 7 comprise byte 0, which is reserved for the integrity check input.
Clearly, forcing the input chain length to equal that of the output aggravates the complexity of input addressing. However, the complexity does not proliferate beyond the analyzer configuration file (analyz.ini). The hardware system designer need only establish correct device name bit address assignments in this file for both interactive and in-script commands to properly access the flags.
The same FPGA that implements the motor control output and flag input scan chains also functions as a motor controller. The complete task of controlling motors is divided between the CPU and the FPGA. The CPU's main motor control task is to convert motor move commands into the timed phase control patterns that cause motors to move the specified number of steps at the rates specified by each motor's ramp trajectory. The CPU does not directly modify the motor control outputs because it can't effect the required timing precision. Instead, it builds what is essentially a microcode script that the FPGA then executes. It should be emphasized that the FPGA doesn't contribute any intelligence to the task of motor control, rather only serving as a precisely timed proxy of the CPU.
To divorce the CPU as much as is feasible from timing constraints, it does not feed instructions one at a time to the FPGA but instead prepares a page of instructions that encompasses a relatively long time period. The CPU and FPGA ping-pong between two pages. While the CPU prepares one, the FPGA executes the other. As soon as the FPGA finishes executing a page, it interrupts the CPU to indicate that the page is free for the CPU to write the next block of instructions. At the same time, the FPGA begins to execute the other page. If the CPU fails to prepare a page before the FPGA finishes the other, motor control is compromised.
The motor control scan chain comprises 88 bits. The chain shifts at 4 MHz. After each 88-bit shift, the serial output registers are shifted in parallel to the output drivers, i.e. all motor control bits update simultaneously. The scan chain controller pauses for 10 usec before engaging the next shift period. Thus, the output is refreshed every 32 usec.
A microcode page describes 256 32-usec time slots and, therefore, an 8.192 msec time period. In each time slot, the microcode tells which, if any, of the 20 motors take a step. Each step is indicated by a scan chain bit address and the value, 0 or 1, to assign to that address. As illustrated by the Motor Driver description above, both CW and CCW rotation are effected by single-bit (gray-code) changes. At the end of each 32 usec scan, the FPGA loads the scan chain from the patterns indicated by the next time slot on the current microcode page. Only those scan chain addresses indicated in the microcode are changed. The others retain their previous states.
Both motor power and phases are controlled through the scan chain. As with the phase patterns, the CPU does not write power patterns directly to the scan chain but rather to the microcode page, which the FPGA subsequently executes. Power and phase control require quite different CPU attention. Power settings are relatively static. Except for power changes, the CPU will not write power control bits. In contrast, for a moving motor, unless it is moving very slowly, the phase control bits will change on every page and possibly multiple times per page. At each page toggle interrupt, the CPU must examine each motor's situation to determine whether and how many steps it has on the next page. Because of the gray code, the CPU doesn't toggle control bits, but only moves forward through the CW or CCW pattern, changing one bit at a time.
If the CPU fails to prepare a microcode page in time for the FPGA, the FPGA will set a page overrun flag for the CPU to read but execute the unprepared page anyway. The page will contain the steps from the previous period, potentially causing motors to skip steps or even step backwards.
There are two microcode pages, one being written by the microprocessor while the FPGA executes the other. Each page describes 256 32-usec periods in one 8.192 msec period. The microcode is divided into two parts, a fixed 256-byte event count array and a variable event array. Each element of the event count array tells how many motor events occur in the corresponding time slice. All of the events that occur in one time slice will occur simultaneously one scan chain cycle (32 usecs) after the event count is interpreted. After processing the 256 event count elements of one page, the FPGA immediately continues at the beginning of the other page.
An event count value of 0 indicates that there is no step activity in the 32 usec. time slice. The interpreter performs a read-modify-write on each event count byte, latching its current value and replacing it with 0. The purpose of this is to clear the program memory for reuse. The microprocessor could do this but it can be done more efficiently by the FPGA. Thus, the microprocessor only needs to write active event count elements, i.e. time slices in which one or more motors experience a step (phase change).
When the event count is 0, it is not necessary to clock out the scan chain, unless the microprocessor has written to any of the power control bits. Not clocking the scan chain reduces power and electrical noise, but the logic to determine whether to do it may be excessive for the rather minor benefit. The motor drive system itself is unaffected by redundant updating.
When the event count is not 0, the microcode program interpreter (motor control state machine) reads the event or events from the event array. Each element (byte) of the event array tells the logic value of one phase or power control bit. Bits 0 through 6 select a scan chain bit address. Bit 7 tells the value for the selected bit.
Like the event count array, the event array is ordered by time, but it does not exhibit the same strict association of time slice to array element. The interpreter maintains two pointers, one that iterates through the event count array at a constant 31.25 kHz (1/32usec.) rate and one that steps through the event array only as directed by the event counts. For example, assume that the beginning of the event count array is:
0 |
0 |
1 |
0 |
3 |
0 |
0 |
0 |
0 |
2 |
0 |
0 |
0 |
0 |
4 |
0 |
0 |
0 |
1 |
0 |
Call the event count pointer (implemented in the FPGA state machine) ecp and the event pointer ep. Assume that the event count array begins at 0x100 and the event array at 0x200. Further assume that both ecp and ep point to the next element and are incremented after use (post-increment). At each 32-usec period the motor control state machine operates as follows:
The main job of the state machine, done once in each 32-usec period, is straightforward:
In many periods, the FPGA needs to access the microcode memory only one time, to read a 0 (although the read-modify-write could be considered a 1.5 access). If one motor steps in a period, two accesses are needed, one to the event count and one to the event. In the worst case, all 20 motors experience a step and a power change in the same period, for which the FPGA must access the event memory 60 times (20 for all of the steps and 40 for all of the power pairs).
The two event count pages, each comprising 256 bytes, are implemented in the FPGA itself, which is mapped into the CPU's normal address space. The FPGA accords the CPU access only to the event count page that is not currently being executed, but the CPU has random access to all 256 elements in that page. The FPGA is selected by the CPU's CS3, which may be mapped to any address but which has been mapped to 0x00500000 through 0x005FFFFF (1 Mbyte) by BIOS startup code.
The two event pages are implemented in the motor control (MSM) unit's main RAM, which the CPU and FPGA, acting as a bus master, share. If all 20 motors stepped at the maximum possible rate, 32,000 steps per second, each page would have to contain 5,120 bytes (20 * 256). The FPGA provides a 13-bit event pointer for each page, which, therefore, comprises 8096 bytes. Even if every motor stepped at its maximum rate, there would still be enough event memory to support 1,488 power changes on one page, i.e. in 8 msec. Static RAM is used and the access cycle time is 20 nsec. The FPGA reads all event bytes for a 32-usec period in one burst, which could be as long as 1.2 usec (60 * 20 nsec) if all motors step and change power in the period. At the lowest level, the system obviously meets even unrealistically high demands. Any processing bandwidth failure will be the page overrun, which the FPGA will detect and flag.
System address bits A1 through A18 are connected to each of two 256K * 16 RAMs. The CPU selects RAM memory via CS1. Its A19 is decoded to select one of the two chips and its A0 to select even/odd byte. The FPGA directly controls the RAM chip selects, output enable, and even/odd byte strobes when the CPU grants it control of the bus. It contains a 13-bit address register to provide A0 though A12 for accessing the event memory of the current microcode page and two 7-bit base address registers to provide A13 through A19. The CPU writes to these base registers, affording it the ability to map the pages to any 8K location in RAM. Each page of event memory must be located on an 8K boundary because the 13-bit address register and the 7-bit base registers do not overlap.
Some of the flip-flops in the FPGA are assembled into control and data registers that the microprocessor can directly read and write. The motor scan chain input register is one of these. Only the FPGA can write directly to the motor output scan chain. Other registers are as follows (not all registers are listed):
Basically, the operation relative to these registers is as follows:
Software is the program executed by the microprocessor. Its job is to translate motor move commands and ramp definitions into the step event microcode. As long as a motor is active, the CPU must continually perform this translation, writing a new microcode page every 8 msec. As previously explained, the FPGA motor controller only serves to time-synchronize the microprogram.
At each interrupt from the FPGA, the program reads the FPGA's Interrupt Status Register to determine whether the interrupt has been issued in response to an error (scan chain integrity failure or page overrun) or because the current microprogram has been exhausted and its memory buffer can now be reused. If the latter then the microprocessor performs the core motor control task.
CORE TASK
The core task of the CPU's ISR is to determine the position (in time and, therefore, in the event count array) of every step of each motor in the 8-msec period encompassed by the microprogram under construction. The simplest way to do this would be to iterate through the 256 elements of the event count array, examining each motor's condition and requirements to determine whether it should step at that 32-usec time slice. If so, then that event count would be incremented and the appropriate event written to the current event address. The CPU would handle the event count and event pointers similarly to the FPGA, except that the data at the pointers would be written instead of being read. In one time slice, the event count and the event pointer are both incremented for each motor that steps at that time.
The simple approach is inefficient because it requires the next step time of each motor to be repeatedly computed as the algorithm visits each time slice and it requires that every time slice be visited. Alternatively, after computing the next step time of all motors, the program could immediately jump to the event count element of the next earliest step, skipping all intervening do nothing time slices (these have already been reset to 0 by the FPGA's read-modify-write cycle). This approach requires a more complicated program but can significantly improve the execution time for average cases. For the worst case of every motor stepping at every time slice, the greater complexity yields somewhat slower execution.
STEPS
The fundamental measure of a step is its duration in terms of the number of 32-usec periods. For example, for a motor to operate at 250 steps per second, each step would have a duration of 125 counts. The average 8-msec microcode page would contain slightly more than two steps for this motor. The number of residual counts for the last step of one page is carried over into the computation of the first step for that motor in the next page, thereby providing continuous stepping for an unbounded amount of time even though the two pages together encompass only 16 msec.
Steppers are not normally operated at just a constant rate but with a ramp up to that rate (which we call the slew rate even though this is technically correct only if it is the maximum speed that the motor can attain) and a ramp down to the final position. An up ramp is simply a list of decreasing step durations while a down ramp is a list of increasing step durations. All motors are controlled by ramp tables that define at least the up ramp, slew rate, and down ramp. Each up/down ramp entry tells the duration of one step, while the single constant speed entry tells the duration of every step in the slew segment of the trajectory. At each step, the position record of the motor (after it makes that step) is incremented for CW rotation and decremented for CCW rotation.
To top of [SystemDesign]
[NextTopic] [MainTopics]
STEPPER CONTROLLER SOFTWARE MODULES {}
All stepper control functions are located in a single module, stpmtr.c, of the IML application program (e.g. MSMAPP). Nothing in this module is of any use to an IML unit that does not have the FPGA motor coprocessor hardware. For these units, the module should be excluded from the build.
The program's stepper motor support capability is set in two files, analyz.h and the program definition file, e.g. apuapp.mac. To include stepper control, HAS_STEPPER is defined in analyz.h and the number of motors to support is defined as STEPPER_COUNT (see explanation under the topic Motor Descriptor). The supported number of motors primarily determines the amount of memory consumed by the program but some motor control functions execute slightly slower with more supported motors. The count only sets the upper limit. Fewer motors may be physically attached and the motor space may be sparsely populated, e.g. M0, M1, M5, M9. If HAS_STEPPER is undefined, STEPPER_COUNT is ignored.
analyz.h
/* #define HAS_STEPPER */
#define STEPPER_COUNT 20
The definitions of HAS_STEPPER and STEPPER_COUNT tell the compiler how to build modules related to stepper control but they don't automatically exclude the stpmtr module, which consumes a significant amount of memory. To link without stpmtr, the statement STPMTR=stpmtr in the program's definition file is commented out.
apudef.mac
# To build without one or more of the following modules, comment out the
# definition and undef the corresponding switch in analyz.h.
...
#STPMTR=stpmtr # Define with HAS_STEPPER.
All IML units support stepper commands, including move, stop, position, wait, and ramp, even if they don't support stepper control. The analyzer configuration file (analyz.ini) will not associate any steppers with such units. The motors will be owned by other units that do provide local stepper control. The script compiler automatically generates messages that identify the actual owner of an accessed device. When motor commands are being submitted interactively (by a user of the script debugger) the resulting messages will be sent to the appropriate motor controller. When the commands are in a script being executed by a unit that doesn't control motors itself, the interpreter will send the messages to the actual controller. This is not unique to motor commands. Any command that access a device is executed locally only if the script execution unit owns the device; otherwise the command message is sent to a slave that owns the device. If the device owner is not a slave of the script execution unit, the compiler will reject the statement.
MOTOR CONTROL STATE MACHINE {}
STRUCTURES AND DEFINITIONS
DRIVER POWER
A motor's power is set by assigning values to the two bits that control the drivers' (two per motor) I1 and I0. This is done through the same event page system as stepping because the CPU has no direct access to the motor control output scan chain. This means that power cannot be instantaneously changed but must instead be scheduled as an event in the next page. Although this complicates the program somewhat, it is not a functional problem. The requirement is for power to change only at the beginning of a ramp segment, which is necessarily a scheduled event. There is little reason for changing power asynchronously.
Four power bit base patterns are defined as follows:
UC3717 stepper driver power control patterns are:
OFF: I1/I0 = 11. LOW: I1/I0 = 10. MED: I1/I0 = 01. HIGH: I1/I0 = 00. Assume
that power is written as one USHORT comprising I0 (MSB) followed by I1 (LSB)
in the event array.
#define USP_OFF 0x8080 /* I0/I1 = 11 */
#define USP_LOW 0x0080 /* I0/I1 = 01 */
#define USP_MED 0x8000 /* I0/I1 = 10 */
#define USP_HIGH 0x0000 /* I0/I1 = 00 */
These patterns are USHORTs that comprise the two bytes that, when combined with appropriate scan chain bit addresses, are written into event memory. Note that each byte either sets or clears b7, the event state bit. The remaining bits 0 through 6 tell the address. These patterns are defined as USHORTs instead of separate bytes not only to increase structural definition but also to support the possibility of writing both into event memory in one word operation instead of two byte operations.
Each motor is controlled through a nibble (four bits) of the output scan chain, but the arrangement of these bits might vary. To remove literal dependencies on the arrangement, the usage of the bits is defined in one enum, as follows:
enum { MTR_PABIT = 0, MTR_PBBIT = 1, MTR_I0BIT = 2, MTR_I1BIT = 3 };
The USHORT power setting word (two event bytes) for any given motor and power level is defined by the following macro:
#define MPWR( M, P ) ( P + (M * 4 + MTR_I0BIT) * 256 + M * 4 + MTR_I1BIT )
M is the motor number, 0 through 19. P is one of the USP_ power definitions. For example, MPWR( 3, USP_MED) generates a USHORT with MSB equal to 0x80 + 3 * 4 + 2 = 0x8E and LSB equal to 0x00 + 3 * 4 + 3 = 0x0F. When the FPGA writes these two events to the scan chain, it will be setting bit 14 (M3 I0) and clearing bit 15 (M3 I1).
Each motor rotates through a set of four phase patterns in one direction to move clockwise and in the other direction to move counter-clockwise. Only one phase bit changes at each step change. The address of this bit and its 1 or 0 value are defined by the motor's position in the scan chain and its current state. These relationships are captured by the PHASES macro. Initial motor phase pattern is 11. From there, PLUS rotation is 10, 00, 01, 11. MINUS is 01, 00, 10, 11. After its first move, a motor may be in any phase state. This is recorded in Stepper.stepIdx, which is a 2-bit (mod 4) counter. plusRotate and minusRotate arrays contain the motor's step event words. These are unique to each motor, being based on the motor's scan chain position.
#define PHASES(M) \ { /* PLUS Phase patterns. Initial PHA = 1, PHB = 1 */ \ M*4 + MTR_PBBIT, /* PHB->0: PHA = 1, PHB = 0 */ \ M*4 + MTR_PABIT, /* PHA->0: PHA = 0, PHB = 0 */ \ M*4 + MTR_PBBIT + 0x80, /* PHB->1: PHA = 0, PHB = 1 */ \ M*4 + MTR_PABIT + 0x80, /* PHA->1: PHA = 1, PHB = 1 */ \ /* MINUS Phase patterns Initial PHA = 1, PHB = 1 */ \ M*4 + MTR_PABIT, /* PHA->0: PHA = 0, PHB = 1 */ \ M*4 + MTR_PBBIT, /* PHB->0: PHA = 0, PHB = 0 */ \ M*4 + MTR_PABIT + 0x80, /* PHA->1: PHA = 1, PHB = 0 */ \ M*4 + MTR_PBBIT + 0x80 /* PHB->1: PHA = 1, PHB = 1 */ }
For each motor, one instance of the PHASES macro creates both a Plus rotation array and a Minus rotation array. Arbitrarily, Plus rotation is considered synonymous with clockwise and Minus with counter-clockwise. A motor may be connected in such a way that this relationship is reversed but, in the absence of any compelling reason to do so, it is better to maintain a consistent relationship.
While motor phases describe a motor's rotational state, Motor States tell which trajectory segment the motor is in. Required segments are UP, SLEW, and DOWN. Optional segments are RECOIL and HOLD. Several additional states are defined for the motor control state machine embodied in the Interrupt Service Routine.
Motor states are defined in motor.h for export to the remote motor wait process, wherein a master script waits for a slave-controlled motor. As already noted, all IML units can wait for a motor even if they do not themselves control motors. Consequently, the remote motor wait process must be located in a module other than stpmtr. The few shared definitions are placed in motor.h.
motor.h
enum { /* Stepper.state. For detailed explanation see stpmtr.c */ /* Moving (or about to move) states that are not also indices. */ MS_START = -10, MS_RECOIL2, /* Moving and non-moving states (at end) that also serve as perPow, perNext * indices (perRamp uses RIDX_): */ MS_UPRAMP = 0, /* This must be 0. */ MS_SLEW, MS_DOWNRAMP, MS_RECOIL, MS_HOLD, MS_IDLE, /* Transition to idle */ /* Non-moving, non-indexing states. */ MS_IDLING, /* Motor has reached idle. */ MS_OFF, MS_STOPHARD, MS_STOPOFF };
The ordinal values of these states are divided into overlapping ranges to optimize run-time checking under several different circumstances. All of the states below MS_IDLING indicate that the motor is in a timed state, i.e. that it is moving, about to move, in HOLD, or transitioning to idle. Note the distinction between MS_IDLE, which means that the motor is changing to the idle state, and MS_IDLING, which means that it is already idling. All of the states above MS_RECOIL indicate that the motor is not moving. The states between MS_UPRAMP and MS_IDLE, inclusive, are used as array (perPow, perEnd, and perNext) indices by the motor control state machine. The states below MS_UPRAMP (negative ordinals) indicate a timed state but, unlike the range from MS_UPRAMP to MS_IDLE, the state is not used as an index. An indication of the utility of this organization are two macros (conceptually class functions) defined in motor.h:
#define IsMotorIdle(S) (S >= MS_IDLING)
#define IsMotorMoving(S) (S < MS_HOLD )
The state transition function can be paraphrased as follows.
Each motor state from UPRAMP through MS_IDLE can have a power of OFF, LOW, MED, or HIGH. MS_HOLD means the motor is not moving but is being held by a
strong magnetic detent. MS_IDLE provides a weak (LOW) or very weak (OFF) detent.
The UP, DOWN, and RECOIL trajectory segments are defined by arrays of step sizes (duration of the step in count of 32-usec periods). These arrays are stored in the "ramp heap" (memory) for use by all motors. Each motor's descriptor (struct) includes an array of three ramp reference structs, one for each of UP, DOWN, and RECOIL ramps that may be assigned to the motor. The reference struct is:
typedef struct { USHORT cnt; /* Max steps in ramp. Working count may be less for small moves. */ USHORT *src; /* Ramp source pointer. Note for dynamically stored ramps this points to RampDesc.steps, not RampDesc. */ USHORT *rmp; /* Incrementing ramp pointer. Manager reloads from src. */ } RampRef;
The cnt and src elements are assigned when the ramp is assigned to the motor and do not change as the ramp is used. The motor control state machine steps the rmp element through the ramp array as the motor traverses the ramp.
The motor control state machine operates on each motor through a complex structure that contains permanent elements related to the motor's position in the scan chain; semi-permanent elements that are assigned to the motor, such as ramp references; elements evaluated for each move; and micro-state variables used by the controller. The motor descriptor struct is defined as follows:
typedef struct { /* Permanent values for this motor. */ UCHAR rotate[8]; /* 4 PLUS followed by 4 MINUS rotation patterns. */ USHORT offPow; /* Motor off power pattern. */ USHORT hardStopPow; /* Programmable values. */ USHORT perPow[6]; /* Power pattern for UPRAMP, SLEW, DOWNRAMP, RECOIL, HOLD, IDLE. Use MPWR( M, P ) for assignment. */ USHORT perEnd[4]; /* End position for UPRAMP, SLEW, DOWNRAMP, RECOIL. */ SCHAR perNext[4]; /* Next state after UPRAMP, SLEW, DOWNRAMP, RECOIL. */ RampRef perRamp[3]; /* up, down, recoil. Indexed by RXIDX_, not MS_ */ USHORT slewStep; USHORT holdStep; UCHAR hasRamp; /* TRUE if ramps (from heap, not embedded defaults) ever assigned to this motor. */ UCHAR hasMoved; /* TRUE if ever moved. */ /* Initialized elements. */ USHORT seqId; /* Script (or interactive) who last (or currently) commanded. */ USHORT cmdNum; /* Commanding script command number. */ USHORT position; /* Current position. */ USHORT incr; /* Position increment. 1 for PLUS, 0xFFFF for MINUS. */ SCHAR state; /* Current state. */ UCHAR stepIdx; /* Rotation index. PLUS = 0-3, MINUS = 4-7. */ /* Scratchpad. */ USHORT nextStep; USHORT pow; /* Current power pattern (to determine if change needed). */ } Stepper;
PERMANENT VALUES
PROGRAMMABLE VALUES
Several elements in this section are arrays whose names begin with "per", which is a reference to the fact that they contain some value for each (per) trajectory state of the motor. These arrays are indexed by the current state of the motor when the state is within the indexing range, MS_UPRAMP through MS_IDLE, as described above.
INITIALIZED ELEMENTS
This group contains two types of items, those that the motor control state machine uses and those that describe the operation of the motor. Both types are initialized outside of the state machine just before the commencement of a move.
SCRATCHPAD ELEMENTS
These are items controlled entirely by the motor control state machine. They are variables that must be static per motor; i.e. they must retain their values between activations of the ISR.
Several characteristics of the motor descriptor are specifically designed to reduce control flow in the motor control state machine. For example, instead of using a flag to indicate whether a move is Plus or Minus, the two direction dependent elements, incr and stepIdx, are initialized outside of the state machine with values that enforce the direction automatically. When incr is 1, adding it to position or multiplying a number of steps by it automatically produces a Plus position change. When it is 0xFFFF, the identical operations produce a Minus position change. Initializing stepIdx to 0 causes it to access the Plus direction half of the rotate array while 3 causes it to access the Minus half. The increment and rollover code takes advantage of modulo arithmetic to maintain the initial range without having to test the direction:
if( ( mp->stepIdx & 3 ) == 3 ) mp->stepIdx &= ~3; else mp->stepIdx++;
For each motor, a descriptor (typedef struct Stepper) is instantiated. The permanent group values are all known at compile time and most of the other elements have useful or essential initial values. The initial values are either identical for all motors or depend only on the motor number. Consequently, the individual structure initializations can be automated by a macro that takes only the motor number as a formal parameter. The macro STEPPER_INIT has been defined for this purpose. Using this macro significantly reduces the effort required to instantiate 20 motor descriptors.
#define STEPPER_INIT(M) \ { PHASES(M), /* rotate */ \ MPWR(M,USP_OFF), /* offPow */ \ MPWR(M,USP_HIGH), /* hardStopPow */ \ MPWR(M,USP_LOW), /* perPow[ MS_UPRAMP ] */ \ MPWR(M,USP_LOW), /* perPow[ MS_SLEW ] */ \ MPWR(M,USP_LOW), /* perPow[ MS_DOWNRAMP ] */ \ MPWR(M,USP_LOW), /* perPow[ MS_RECOIL ] */ \ MPWR(M,USP_LOW), /* perPow[ MS_HOLD ] */ \ MPWR(M,USP_OFF), /* perPow[ MS_IDLE ] */ \ 0, /* perEnd[ MS_UPRAMP ] */ \ 0, /* perEnd[ MS_SLEW ] */ \ 0, /* perEnd[ MS_DOWNRAMP ] */ \ 0, /* perEnd[ MS_RECOIL ] */ \ MS_SLEW, /* perNext[ MS_UPRAMP ] */ \ MS_DOWNRAMP, /* perNext[ MS_SLEW ] */ \ MS_HOLD, /* perNext[ MS_DOWNRAMP ] */ \ MS_HOLD, /* perNext[ MS_RECOIL ] */ \ DIM( defUpRamp ), defUpRamp, 0, /* perRamp[up].cnt/src/rmp */ \ DIM( defDownRamp ), defDownRamp, 0, /* perRamp[down] */ \ DIM( defRecoilRamp ), defRecoilRamp, 0, /* perRamp[recoil] */ \ DEFAULT_SLEW_STEP, /* slewStep */ \ DEFAULT_HOLD_STEP, /* holdStep */ \ 0, /* hasRamp */ \ 0, /* hasMoved */ \ 0, /* seqId */ \ 0, /* cmdNum */ \ 0, /* Position */ \ 1, /* incr */ \ MS_OFF, /* state */ \ 0, /* stepIdx */ \ 256, /* nextStep */ \ MPWR(M, USP_OFF), /* pow */ \ } Stepper steppers[] = { STEPPER_INIT(0), STEPPER_INIT(1), STEPPER_INIT(2), STEPPER_INIT(3), STEPPER_INIT(4), STEPPER_INIT(5), STEPPER_INIT(6), STEPPER_INIT(7), STEPPER_INIT(8), STEPPER_INIT(9), STEPPER_INIT(10), STEPPER_INIT(11), STEPPER_INIT(12), STEPPER_INIT(13), STEPPER_INIT(14), STEPPER_INIT(15), STEPPER_INIT(16), STEPPER_INIT(17), STEPPER_INIT(18), STEPPER_INIT( STEPPER_COUNT - 1 ) }; /* This array should be reduced by editing to match STEPPER_COUNT in analyz.h. */
The motor descriptors array, steppers, hard-wires the fact that 20 motors are supported, in violation of the purported use of STEPPER_COUNT in analyz.h, as explained under the topic Software Modules. In fact, STEPPER_COUNT is only used to calculate the amount of RAM to allocate for the two motor event pages. The C language preprocessor does not afford an iterative definition facility as would be required to instantiate a programmable number of motors. If STEPPER_COUNT is reduced, the initialization list for steppers should be pruned by hand to avoid wasting memory.
Except for motor descriptor initialization at boot time and to prepare for a move, the motor control state machine is embodied entirely in the ISR, which is activated by the FPGA usually at the event page toggle. Unless the ISR finds an error flag set, its only job is to prepare the next motor (event and event count) page. The FPGA clears the event count page before granting the CPU access to it, so the CPU only needs to write active events. This entails writing the count of events in the appropriate time slot of the event count array and adding the events themselves to the event array (which is in temporal order but not time-coordinated like the event count array).
Basically, the page is written by repeatedly scanning the list of active motors to find the next earliest event slot. After this scan the ISR could write the earliest event except that more than one motor may have an event in this slot. Therefore, a second scan is required to find all of the motors that have an event in this slot. This time the ISR can write the events into the event page. Then another pair of scans would be needed to write the next earliest event. For faster execution, the second scan of each pair is merged with the first scan of the next pair, i.e. most scans are writing out the events that occur at the same time as the earliest event found in the previous scan while scanning for the next earliest event. Only the first and last scans are not merged. To avoid complicating the main scan loop by having to accommodate a non-merged scan, the first scan is done separately as a pre-scan. The last scan is, in effect, merged because it prepares the motors' page carry-over counts while writing out those that have an event in the last slot that occurs in the current page. All scanning is complete when the next earliest event lies beyond the current page. The complete ISR is as follows:
* ........................... notes ....................................... * - Count page carry over by subtracting 256 (one page of 32 usec. slots) from * this motor's next step time. If the result is less than 256 then the motor * has at least one step in this period. Note initial nextStep of 0x0100 to * cause the motor to take its first step at slot 0 of the next page flip * after the page in which the power is set (MS_START). *..........................................................................*/ void interrupt stpmtrIsr() { /* Generic AddPowerEvent is safe without alignment but the generated code is * really bad if P is complex, e.g. array element. AddPowerEventW code is good * but can only be used if word-aligned. */ #define AddPowerEventW(P) *((USHORT*)evPtr)++ = P, cnt += 2 #define AddPowerEvent(P) *evPtr++ = HIBYTE(P), *evPtr++ = LOBYTE(P), cnt += 2 UCHAR *evPtr; /* motor event page pointer. */ Stepper *mp; USHORT cnt; USHORT pageStep; USHORT nextPageStep; USHORT usv; UCHAR isr; UCHAR event; isr = MtrScanControl->isr; if( isr & SISR_MTROVR ) { systemError |= ERR_MTRPAGE; MtrScanControl->isr = 0; } evPtr = ( isr & SISR_MTRPAGE ) ? page0Events : page1Events; #ifdef COUNT_ISR if( ++isrCount == 63 ) /* 1 = reset, 2 = power, 3 = step. */ event = 1; /* Anything to make code for breakpoint. isrCount is only for debugging. */ #endif switch( mtrCommand ) { case MTRCMD_SUSPEND : return; case MTRCMD_RESET : /* Set every bit in the motor MOSI scan chain high by filling the event page with x80, x81, x82...xCF and setting four event counts for each motor. */ for( event = 0x80 ; event <= 0x80 + STEPPER_COUNT * 4 - 1 ; event++ ) *evPtr++ = event; for( cnt = 0 ; cnt < STEPPER_COUNT ; cnt++ ) MtrPageCounts[ cnt ] = 4; mtrCommand = MTRCMD_NOTHING; return; } if( hiMotor < loMotor ) return; /* MOTOR STATE SCAN LOOP * For each motor in the active range do: * - If it is OFF then leave it alone. Its nextStep will be > 255, which tells * the event iterator that it has nothing to do on this page. * - If the the motor has just been enabled (MS_START) set its power. * - Otherwise, decrement the motor's nextStep page count. Also search for the * earliest step in this page, preparing pageStep for the first iteration of * the step event loop. */ cnt = 0; /* AddPowerEventW modifies this. */ pageStep = 0xFFFF; for( mp = loMotor ; mp <= hiMotor ; mp++ ) { switch( mp->state ) { case MS_START: mp->state = MS_UPRAMP; usv = mp->perPow[ MS_UPRAMP ]; setpow: if( mp->pow != usv ) { AddPowerEventW( usv ); /* Will be 2 events in slot 0 */ mp->pow = usv; } break; case MS_STOPHARD: mp->state = MS_HOLD; usv = mp->hardStopPow; goto setpow; case MS_STOPOFF: mp->state = MS_OFF; usv = mp->offPow; goto setpow; case MS_IDLING: case MS_OFF: break; default: if(( mp->nextStep -= 256 ) < 256 ) { /* This motor has a step on this page. Is it the earliest? */ if( mp->nextStep < pageStep ) pageStep = mp->nextStep; } } } /* * Write the power event count into slot 0. If the first scan through the motor * list revealed no steps on this page then return; if there were any power * events, the FPGA will process them based on the indicated count. If there is * at least one step event in slot 0 (pageStep = 0) then the count written into * MtrPageCounts will be overwritten by the total count, including the power * events. If pageStep indicates that there is no step event in slot 0, the * FPGA will use the recorded power event count and the event counter should be * reinitialized to 0 before the motor list is scanned again (to record step * events and find the next page step. */ MtrPageCounts[0] = cnt; if( pageStep == 0xFFFF ) /* No step events */ return; if( pageStep != 0 && cnt > 0 ) cnt = 0; /* PAGE STEP SCAN LOOP * Scan the the active motor range repeatedly, calculating each active * motor's next step until all active motors' next step lies beyond this page. * Each scan determines the earliest step of all motors. The next scan programs * the step event for each motor that occurs at this position and calculates * that motor's next step while looking for the next earliest page step. * Begin scan with cnt = 0 or the number of power control events (2 per power * setting). Initial nextPageStep is 0x0100 so that on the first motor to step, * its updated next step can just be compared to nextPageStep. A larger value * would also suffice but then the nextPageStep would be reassigned even if the * first motor's next Step lies in a future page, which just wastes time. */ while( 1 ) /* Do until all motors' next step lies beyond this page. */ { nextPageStep = 0x0100; /* Initial value allows any motor with a step on this page to define the next page step unless replaced by an earlier one. */ for( mp = loMotor ; mp <= hiMotor ; mp++ ) { if( mp->nextStep > 255 ) continue; /* Not active on this page (this includes OFF). */ if( mp->nextStep == pageStep ) { /* This motor has an event at the earliest step determined by previous scan. */ dostate: switch( mp->state ) { case MS_UPRAMP : if( mp->position == mp->perEnd[ MS_UPRAMP ]) { mp->state = mp->perNext[ MS_UPRAMP ]; changestate: usv = mp->perPow[ mp->state ]; if( mp->pow != usv ) { AddPowerEvent( usv ); mp->pow = usv; } goto dostate; } usv = *mp->perRamp[ RIDX_UP ].rmp++; break; case MS_DOWNRAMP : if( mp->position == mp->perEnd[ MS_DOWNRAMP ]) { mp->state = mp->perNext[ MS_DOWNRAMP ]; goto changestate; } usv = *mp->perRamp[ RIDX_DOWN ].rmp++; break; case MS_RECOIL : mp->incr = 0 - mp->incr; /* Negate step increment. 1 vs. -1 */ mp->stepIdx += mp->stepIdx < 4 ? 4 : -4; mp->state = MS_RECOIL2; /* Fall through. */ case MS_RECOIL2: if( mp->position == mp->perEnd[ MS_RECOIL ]) { mp->state = mp->perNext[ MS_RECOIL ]; goto changestate; } usv = *mp->perRamp[ RIDX_RECOIL ].rmp++; break; case MS_SLEW : if( mp->position == mp->perEnd[ MS_SLEW ] && mp->position != 0xFFFF ) /* FFFF = forever. */ { mp->state = mp->perNext[ MS_SLEW ]; goto changestate; } usv = mp->slewStep; break; case MS_HOLD : mp->nextStep += mp->holdStep; mp->state = MS_IDLE; goto checkevent; /* Skip rotation but not whether the end of hold period is the next page event. */ case MS_IDLE : mp->state = MS_IDLING; mp->nextStep = 256; /* Mark inactive. */ if( mp->pow != mp->perPow[ MS_IDLE ] ) { mp->pow = mp->perPow[ MS_IDLE ]; AddPowerEvent( mp->pow ); } continue; } mp->nextStep += usv; mp->position += mp->incr; /* Advance position by +1 if CW, -1 if CCW. */ *evPtr++ = mp->rotate[ mp->stepIdx ]; /* Write this event. */ cnt++; /* count it */ /* Advance rotation pattern index. 0-3 if PLUS or 4-7 if MINUS. */ if( ( mp->stepIdx & 3 ) == 3 ) mp->stepIdx &= ~3; else mp->stepIdx++; } /* This motor has an event in earliest active slot */ checkevent: /* Whether the motor stepped or not, see if its next event is the next page * event (by being earlier than all others seen so far in this iteration). */ if( mp->nextStep < nextPageStep ) nextPageStep = mp->nextStep; } /* Iterate through active motor list range. */ if( cnt != 0 ) MtrPageCounts[ pageStep ] = cnt; /* Tell number of events at this slot. */ /* If all motors were stepping, 0 cnt would mean that there are no more * events on this page. However, the state machine also transitions to idle and * hold states, which may break the loop without generating events if the power * setting doesn't change. Therefore, we can't break just because cnt is 0 but * must check nextPageStep to determine whether there might be further * activity. */ if( nextPageStep > 255 ) break; /* No more motors step in this page. */ /* Go through the motor list again. */ pageStep = nextPageStep; /* Carry earliest next page step over to next iteration. */ cnt = 0; } }
The ISR first reads the FPGA's Interrupt State Register, clearing the page toggle flag but not error flags. It then checks the page overrun flag. If this error has occurred, the ISR sets a global system error flag to tell the main program loop so that it can respond. The ISR executes at too low a level to be able to do any more than flagging the error. It also writes a 0 to the Interrupt Status Register to clear the flag in the FPGA. The Interrupt Status Register flags are defined in sys.h as follows:
msmapp- sys.h
enum { /* xxScanControl.isr bit masks. SISR_MTRxx only in MtrScanControl. */ SISR_MTROVR = 4, /* Bit 2 = Motor page overrun (CPU missed a page). */ SISR_MTRPAGE = 8, /* Bit 3 tells which page the FPGA motor controller is currently using. CPU writes only to the other. */ SISR_DIAG = 0x10, /* Bit 4 = Diagnostic (feedback) error interrupt. */ SISR_MTRTOGGLE = 0x20, /* Bit 5 = Motor page toggle. */ SISR_ENIRQ = 0x80, /* Bit 7 = general interrupt status. */ };
Before checking any motors, the ISR checks the general-purpose mtrCommand, which tells whether the motor system has been suspended or is to be reset. Resetting entails setting every output scan chain bit to 1, which puts each motor's PHASE bits into known states and turns off the drivers' power (I1 = 1, I0 = 1 is OFF).
The principal job of the pre-scan, labeled MOTOR STATE SCAN LOOP, is to find the earliest event in order to prepare for the main merged scan loop. Some things that only need to be checked once per motor are also hoisted out of the main loop into the pre-scan. If the motor has just been started then its UPRAMP state power events are scheduled. If it has just been stopped then its STOPHARD or STOPOFF power events are scheduled. If the motor is currently moving, 256 is subtracted from its nextStep, whose value is the motor's next step position relative to the beginning of the previous page. Subtracting 256 shifts the slot time reference to the beginning of the current page. If the result is 256 or greater, the next step lies beyond this page. Otherwise, if the result is the lowest such result seen so far in the pre-scan, it is remembered as the earliest event, in preparation for the first merged scan.
If the pre-scan revealed power changes, the count of power events is written to MtrPageCounts[0], which is event 0 in the FPGA's event array. If no step events were found on the current page then the ISR is done; when the FPGA processes the page it will process only the indicated power events. In all cases, power events are scheduled to occur in slot 0. If the earliest step happens to appear in slot 0 then it (or they) will share the slot with the power events.
The main motor scan loop scan all active motors repeatedly until none of them have any steps in the current page. As each motor is visited, if its next step occurs in the same time slot as the previous scan set determined to be the earliest, its step event is added to the event page and it is counted. Additionally, several things are done to manage the motor state, including:
The primary goal of the design of the stepper control program is to hoist as much of the processing as possible out of the ISR. While the ISR is the embodiment of the control state machine, the starting state of the machine, established by the startStepper function, is a critical component of its operation. This function reduces the ISR's work by pruning the motor list, initializing segment endpoints so that the ISR doesn't have to count steps in addition to the position of each motor, truncating ramps as needed to accommodate short moves, and initializing the position incrementer and rotation index.
Every time startStepper is called it trims idle motors off the ends of the active motor list identified by loMotor and hiMotor in order to reduce the ISR's search space.
while( loMotor < hiMotor && IsMotorIdle( loMotor->state )) loMotor++; while( hiMotor >= loMotor && IsMotorIdle( hiMotor->state )) hiMotor--;
If the motor's state is MS_IDLING the move can be granted without worrying about the ISR's interference. If the motor's state is MS_HOLD or MS_IDLE, it is not moving and, therefore, a new move is granted. However, the ISR is still actively managing a motor in either of these states, so the state is changed to MS_IDLING to tell the ISR to leave it alone. If the motor is already moving, the move request is denied.
switch( mp->state ) { /* MS_HOLD and MS_IDLE are substates of HOLD. MS_HOLD is transition to hold * while MS_IDLE is transition out of hold. In either case, it is OK to start * another move, but first change state to MS_IDLING to prevent the ISR from * doing anything to this motor while we are setting up the next move. */ case MS_HOLD: case MS_IDLE: mp->state = MS_IDLING; break; default: if( !IsMotorIdle( mp->state )) return MM_BUSY; }
StartStepper needs to know both the distance of a move in order to determine whether the UP and DOWN ramps have to be truncated because they contain more steps than the move. It also needs to know the final position of the move because it will set up position parameters for the ISR instead of asking it to count both steps and position. StartStepper is given position in an absolute move and distance in a relative one; it calculates the missing parameter so that in either case it has both parameters. It also initializes the motor descriptor's incr to 1 for Plus and 0xFFFF for Minus moves (whether absolute or relative). If the motor's direction doesn't change, it can keep its current rotation pattern index dirIdx. If the direction changes, dirIdx has to be changed to the other rotational array but at its existing phase pattern. For example, if dirIdx is 2, the motor is moving in the Plus direction and the next phase pattern to apply to continue moving would be the motor's rotate[2]. Its current pattern is rotate[1], which is PhaseA = 0, PhaseB = 0. This is the same as rotate[5] in the Minus range. Therefore, if the direction changes to Minus, dirIdx should change to 6 so that the next pattern applied is the one after rotate[5]. The quickest means of transforming dirIdx from Plus to Minus and Minus to Plus is with a table. The changeDir table is used for this.
static UCHAR changeDir[] = { 4, 7, 6, 5, 0, 3, 2, 1 }; /* dirIdx change to
change direction without losing phasing. rotate[0] <-> rotate[4], etc. */
Note that changeDir[2] is 6 and the reverse transformation, changeDir[6] is 2. To understand each transformation, it helps to examine the PHASES macro, which instantiates each motor's rotate array.
/* Assign byto value to the characteristic known by the move type. If the * move is relative then byto is the distance, if absolute then the end * position. The other characteristic must be calculated based on the motor's * current position. */ if( relAbs == MOVE_ABS ) { if( byto == mp->position ) return MM_NOMOVE; position = byto; } else { if( byto == 0 ) return MM_NOMOVE; distance = byto; } /* Calculate the derived characteristic, distance or position, and the * direction parameters, incr (position change per step: 1 if PLUS, -1 if * MINUS) and dirIdx (phase pattern index: 0 if PLUS, 4 if MINUS ). */ dirIdx = mp->stepIdx; if( relAbs == MOVE_PLUS || relAbs == MOVE_ABS && byto > mp->position ) { /* PLUS */ if( dirIdx > 3 ) mp->stepIdx = dirIdx = changeDir[ dirIdx ]; mp->incr = 1; if( relAbs == MOVE_ABS ) distance = byto - mp->position; else position = mp->position + byto; } else { /* MINUS */ if( dirIdx < 4 ) mp->stepIdx = dirIdx = changeDir[ dirIdx ]; mp->incr = 0xFFFF; /* -1 */ if( relAbs == MOVE_ABS ) distance = mp->position - byto; else position = mp->position - byto; }
If the distance (step count) of the move is only 1, the motor's slew and down ramp are eliminated by setting its perNext[ MS_UPRAMP ] equal to its perNext[ MS_DOWNRAMP ], i.e. at the end of its up ramp, which contains only one step, the motor immediately transitions to whatever state was supposed to follow the down ramp. This may be MS_RECOIL, MS_HOLD, or MS_IDLE. Note that if a recoil segment has been assigned to the motor, it will not be truncated by a very short move. If the distance is greater than one but less than the total number of steps in the up and down ramps, the slew segment is skipped by assigning MS_DOWNRAMP to perNext[ MS_UPRAMP]. Only the up ramp's perNext is changed due to ramp truncation. The other segments keep whatever perNext they have due to ramp assignments. This is convenient, because the up ramp's perNext is not affected by ramp assignments, as it is always MS_SLEW unless ramp truncation is required. Thus, ramp assignments don't require a backup copy for restoration after ramp truncation. We only need to restore the up ramp's perNext to MS_SLEW.
If the move distance exactly equals the number of steps in the up and down ramps then simply short-circuiting the slew is sufficient. However, it is more likely that the distance is less than the ramp total and the ramps need truncation. This is done as symmetrically as possible, given the likelihood that the up and down ramps are not symmetrical. Most likely, the down ramp contains fewer steps because a motor can decelerate at a steeper gradient than it can accelerate. A simple approach is taken. The down ramp is allowed to have as much as half the distance and the up ramp gets the remainder. Of course the steps are removed from the end of the up ramp and from the beginning of the down ramp so that the last step of the up ramp and the first step of the down ramp are close together in size.
/* Determine whether the full up and down ramps fit into the move distance. * If not then SLEW is eliminted and ramps possibly truncated. If distances is * 1, DOWNRAMP is also eliminated. Note that RUN_FOREVER, which is indicated by * byto = 0xFFFF, is used with relAbs = MOVE_PLUS or MOVE_MINUS so up ramp is never * truncated. */ if( distance == 1 ) { mp->perNext[ MS_UPRAMP ] = mp->perNext[ MS_DOWNRAMP ]; mp->perEnd[ MS_UPRAMP ] = position; } else { mp->perEnd[ MS_DOWNRAMP ] = position; rampTotal = ( upCnt = mp->perRamp[ RIDX_UP ].cnt ) + ( downCnt = mp->perRamp[ RIDX_DOWN ].cnt ); if( distance <= rampTotal ) { mp->perNext[ MS_UPRAMP ] = MS_DOWNRAMP; /* No SLEW at all */ if( distance < rampTotal ) { /* Ramps have to be truncated. See note above. */ if( downCnt > distance / 2 ) downCnt = distance / 2; upCnt = distance - downCnt; mp->perRamp[ RIDX_DOWN ].rmp = mp->perRamp[ RIDX_DOWN ].src + mp->perRamp[ RIDX_DOWN ].cnt - downCnt; goto setends; } } else mp->perNext[ MS_UPRAMP ] = MS_SLEW; /* Restore in case of previous truncation. */ mp->perRamp[ RIDX_DOWN ].rmp = mp->perRamp[ RIDX_DOWN ].src;
Knowing the final position and calculated up and down ramp counts, StartStepper is able to compute and assign the up ramp and slew end positions. The downramp's end position is, of course, the final position. If there is a recoil segment, its end position is calculated as the final position minus the number of steps in the recoil ramp. Although a recoil segment leaves the motor at a position other than the one specified in the command, the motor control system still knows the motor's location. When recoil is used for fluid shear control, there will be no confusion due to small moves because small moves are not used. When recoil is used to compensate for backlash, the fact that the motor doesn't end up exactly where it was told to go is the intended behavior.
if( dirIdx < 4 ) /* PLUS */ { mp->perEnd[ MS_UPRAMP ] = mp->position + upCnt; mp->perEnd[ MS_SLEW ] = position - downCnt; if( mp->perNext[ MS_DOWNRAMP ] == MS_RECOIL ) mp->perEnd[ MS_RECOIL ] = position - mp->perRamp[ RIDX_RECOIL ].cnt; } else /* MINUS */ { mp->perEnd[ MS_UPRAMP ] = mp->position - upCnt; mp->perEnd[ MS_SLEW ] = position + downCnt; if( mp->perNext[ MS_DOWNRAMP ] == MS_RECOIL ) mp->perEnd[ MS_RECOIL ] = position + mp->perRamp[ RIDX_RECOIL ].cnt; } if( byto == RUN_FOREVER ) mp->perEnd[ MS_SLEW ] = 0xFFFF;
StartStepper initializes the motor's nextStep to 256, so that when the ISR first subtracts 256 from it, the result is 0, placing the first step in slot 0 of the event page. Thus, the motor will begin moving as soon as possible. The maximum delay would be one full page time, approximately 8 msec.
StartStepper is an internal utility. The move command interpreter is moveStepper. StartStepper does all of the hard work. MoveStepper primarily extracts parameters from the MoveCmd message structure and passes them to StartStepper. MoveCmd and supporting elements are defined in fsqapi.h as:
enum { MOVE_ABS = 1, MOVE_MINUS, MOVE_PLUS, MOVE_CCW, MOVE_CENTER, MOVE_CW, MOVE_STOP, /* Stop motor at the next controllable point. */ MOVE_STOP_HARD, /* Immediate active stop, e.g. high power hold if stepper, dynamic break if bridge-drive DC, etc. */ MOVE_STOP_OFF /* Stop motor immediately by removing power. */ }; #define MOVE_VAR 0x80 /* Or'd with MOVE_ABS/MINUS/PLUS. */ #define FRAC_SCALER 256 /* Fractional position scaler */ #define MOVE_ALL 0xFE /* MoveCmd.motor if not just one. */ typedef PACKED struct { UCHAR motor; USHORT position; UCHAR moveType; /* MOVE_ABS, ... */ } MoveCmd; /* CM_MOVE */
The MOVE_ enum and MOVE_VAR flag are combined into MoveCmd.moveType. MOVE_CCW, MOVE_CENTER, and MOVE_CW do not apply to steppers. FRAC_SCALER applies only to shear valves. The MOVE_VAR flag tells whether the MoveCmd.position parameter is a VAR reference or a literal. To produce a testable move type, moveStepper strips off the flag:
moveType = cmd->arg.move.moveType & ~MOVE_VAR;
If the position is a VAR reference, the VAR's value is retrieved and used for the working position parameter:
/* If the moveType indicates that the position is given in a VAR, which may * be global (in wordVars) or local to this proc, then substitute the VAR's * contents for the move position. The GET_WORDVAR macro determines from the * var's number whether it is global or local and provides the appropriate * access. The ABS, MINUS, and PLUS components of the moveType field apply to * VAR just as they do to a literal position argument. */ if( cmd->arg.move.moveType & MOVE_VAR ) position = GET_WORDVAR( cmd->arg.move.position ); else position = cmd->arg.move.position;
If the moveType is one of the three STOP types, moveStepper calls stopStepper. Otherwise, it calls startStepper. If startStepper returns OK, the motor's seqId and cmdNum are assigned values identifying the script that has launched the move. If the move command is interactive, seqId and cmdNum are each assigned 0xFFFF, indicating that no local script is responsible. This information will be used in an error report if another move is attempted before the completion of this one.
If startStepper returns BUSY, moveStepper generates a run-time error message explaining that the motor is already moving and identifying the script ID and command message number recorded when the prior move was successfully launched.
StopStepper is a simple function but the rationale for the three types of stops is complicated. The stopType is MOVE_STOP, MOVE_STOP_HARD, MOVE_STOP_OFF. Only MOVE_STOP is (nearly) guaranteed to retain the motor's position, because it uses the normal down ramp to stop. The position may be valid after a HARD stop, if the motor and its load have little inertial. The position is almost certainly lost on an OFF type. In case of MOVE_STOP, if the motor is in up or slew segment, the state machine is short-circuited by switching to the down ramp. First, interrupts are disabled to prevent the ISR from passing any more step commands to the FPGA. While not strictly required, the interrupts are disabled before testing the state to avoid the situation where the motor just enters the downramp after we have tested and the ISR has set up the first page of the downramp but then we would cause it to revert back to the beginning of the ramp again. This would probably not be serious but it would be wrong. Once the motor ISR is blocked, the motor's position accurately reflects the real position, even if not all events have been processed because we can't stop the FPGA and the motor's recorded position is the last one already programmed into motor event memory. Thus, even if the motor hasn't yet reached this position, this is effectively its real position. We can add (or subtract, according to mp->incr) to this the count of steps in the down ramp to get the end position of the fastest controlled (i.e. by down ramp) stop.
Wait for stepper not moving, idle, position greater, and position less are all supported both locally (e.g. MSM script) and remotely (e.g. APU script). Condition testing, done in isStepperCondition, is very simple. However, the overall functionality is considerably more complicated than this. Local motor wait is complicated by the optional maximum time argument and remote by this and by the various communication mechanisms needed.
fsqapi.h
enum { WAITMTR_STOP = 'A', WAITMTR_IDLE, WAITMTR_GREATER, WAITMTR_LESS }; msmapp- motor.h #define IsMotorIdle(S) (S >= MS_IDLING) #define IsMotorMoving(S) (S < MS_HOLD ) msmapp- stpmtr.c typedef packed struct { UCHAR motor; UCHAR condition; /* WAITMTR_STOP, etc. */ USHORT position; /* If condition = GREATER or LESS */ UCHAR reqUnit; /* Unit ID of requester for done message routing. */ } StepperWait; /* The motor, condition, and position elements of StepperWait are packed * to allow the group to be compared as a ULONG. They must not change in size * and the structure must be packed. */ BOOL isStepperCondition( StepperWait *swp ) { Stepper *mp; mp = steppers + swp->motor; switch( swp->condition ) { case WAITMTR_STOP: return !IsMotorMoving( mp->state ); case WAITMTR_IDLE: return IsMotorIdle( mp->state ); case WAITMTR_GREATER: return mp->position > swp->position; case WAITMTR_LESS: return mp->position < swp->position; } }
The waitStepper command message is processed by procWaitStepper, which begins by calling isStepperCondition to determine whether the condition exists. Ignoring for the moment remote motor wait, indicated by pd = PROC_SLAVE, if the condition already, the function sets the script's timeout flag false and immediately returns CMD_DONE, telling the script dispatcher that the wait is over. A subsequent test for timeout, i.e. if timeout, will be false. If the condition doesn't exist then if there is no timeout (waitStepper.ticks is 0) or if there is a timeout but the eventTimeout function says that the time limit hasn't been reached then CMD_REDO is returned to the script dispatcher, telling it to call back again later. When eventTimeout is called for the first time for this message, it sets the scripts timeout flag false and starts a timer. If the time limit has been reached at a subsequent invocation, eventTimeout sets the timeout flag true and returns EVT_TIMEOUT, directing the caller, procWaitStepper, to return CMD_DONE. Unlike the case when the condition has been met, if procWaitStepper returns CMD_DONE due to timeout, a subsequent if timeout predicate will be true.
fsqapi.h
typedef PACKED struct { UCHAR motor; USHORT ticks; /* Time limit. 0 = forever. Max @ 5 msec = 327 seconds. */ USHORT position; /* If condition = GREATER or LESS */ UCHAR condition; /* WAITMTR_STOP, etc. */ UCHAR reqUnit; /* Unit ID of requester for done message routing. */ } WaitStepperCmd; /* CM_WAITSTEPPER */
msmapp- stpmtr.c
int procWaitStepper( FsqMsg *cmd, ProcDef *pd ) { Stepper *mp; StepperWait *wp; StepperWait sw; BOOL already; UCHAR reqUnit; /* Copy command elements to StepperWait to align for quick searching and for passing to shared condition checker. */ sw.motor = cmd->arg.waitStepper.motor; sw.condition = cmd->arg.waitStepper.condition; sw.position = cmd->arg.waitStepper.position; already = isStepperCondition( &sw ); if( pd == PROC_SLAVE ) { reqUnit = cmd->arg.waitStepper.reqUnit; if( already ) { sw.reqUnit = reqUnit; return sendStepperDone( &sw ); } if( remWaitCnt == 0 ) addPeriodic( remWaitStepper, 0, DO_ALWAYS ); else { /* Search the active remWaits to see if this is a duplicate from * either the same master or from a higher (lower ID) one. Do nothing if same * master but change the requester ID if higher to ensure that the position * message gets sent to all requesters. */ for( wp = remWaits ; wp < remWaits + remWaitCnt ; wp++ ) if( *(ULONG*)wp == *(ULONG*)&sw ) { if( reqUnit < wp->reqUnit ) wp->reqUnit = reqUnit; return CMD_DONE; } if( remWaitCnt == DIM( remWaits )) return sendScriptFailMsg( PROC_SLAVE, FAIL_NOTREADY, FLINE, "No more remote stepper waits" ); } remWaits[ remWaitCnt ] = sw; remWaits[ remWaitCnt ].reqUnit = reqUnit; remWaitCnt++; return CMD_DONE; } /* Local motor wait */ if( already ) pd->timeout = FALSE; else if( cmd->arg.waitStepper.ticks == 0 || eventTimeout( pd, cmd->arg.waitStepper.ticks ) != EVT_TIMEOUT ) return CMD_REDO; /* No time limit or timeout not yet reached. */ return CMD_DONE; /* Condition matched or timeout. */ }
The process is much more complicated for remote motor waits. A remote motor wait command message is identical to the local message except that the unit listed in the message doesn't match the unit executing the message. The master sends this message unchanged to the slave. The only indication that procWaitStepper has that it is performing a service on behalf of a master is that the ProcDef argument is PROC_SLAVE.
With local motor wait, procWaitStepper is passed a ProcDef that has the built-in capacity to wait. There is no equivalent capacity when performing a service for a master, but it is essential because something has to keep checking the motor's condition. To provide this capability, procWaitStepper creates a process. Since the task is very specific, it would be wasteful to create a full-fledged Proc. Instead, a lightweight specialized process is created by taking advantage of the main loop's callback capability. The statement addPeriodic( remWaitStepper, 0, DO_ALWAYS ) tells the main loop to call remWaitStepper once in each iteration of the loop. The callback itself is cheap but there is no reason to make it permanent because installing and removing it are also cheap. Therefore, procWaitStepper installs it only when it processes a remote motor wait message. If more of these messages arrive before the first wait is done, they all share the callback. When the last wait is done, the callback removes itself (indirectly).
ProcWaitStepper needs a means to tell remWaitStepper what motor and condition to wait for. ProcWaitStepper could call remWaitStepper, passing this information, but this would be clumsy an inefficient, given that remWaitStepper is normally called by the main loop, which has no idea what the function is. A better approach is intertwined with the core function of the callback. RemWaitStepper needs some way to remember each motor and condition that it is waiting for. Its core task is to periodically check the conditions. There is no reason to make it set up the motor-condition list. ProcWaitStepper can add to the list without involving remWaitStepper and remWaitStepper can simply process whatever is in the list.
The motor-condition list is an array of StepperWait structures. The program arbitrarily contains 16 of these, which limits the motor controller to providing no more than 16 simultaneous motor waits on behalf of masters. In addition to the obvious condition elements, StepperWait contains the master's ID. This is needed because a message has to be sent to the master when the motor reaches the specified condition. Every time it is called, remWaitStepper checks all active (remWaitCnt) elements in remWaits. When a motor-condition is met, sendStepperDone is called to send the message to the awaiting master. RemWaitStepper also calls deleteRemWait, which removes the element from remWaits and compacts the array. If remWaitCnt goes to 0, deleteRemWait removes remWaitStepper from the main loop's periodic callback. In this case, after deleteRemWait returns to sendStepperDone, which returns to remWaitStepper, remWaitStepper returns to the main loop and is not called again until a new remote motor wait arrives.
Before adding a StepperWait to remWaits, procStepperWait checks to see if the request is identical to one already being serviced. If it is exactly the same, it is ignored. If the request is the same except for a different master unit, it means that more than one master is waiting for the same motor-condition. In this case, we want to send the done message to the master with the lowest ID to ensure that the message passes through all of the potentially waiting masters. Each master along the way can check to see if this message satisfies one of its waiting scripts.
There is no time limit for the slave's portion of remote motor waits. If the condition is feasible, we assume that it will eventually be met and the StepperWait removed. If the condition is not feasible, the developer should quickly realize the error and correct it during development. If the condition is not met due to system failure, the consumed StepperWait will be recovered when the system is reset. Consequently, there is no apparent reason to burden remWaitStepper with the additionally responsibility of checking timeout for each waiting motor. However, this would be easy to add.
msmapp- stpmtr.c
typedef packed struct { UCHAR motor; UCHAR condition; /* WAITMTR_STOP, etc. */ USHORT position; /* If condition = GREATER or LESS */ UCHAR reqUnit; /* Unit ID of requester for done message routing. */ } StepperWait; /* The motor, condition, and position elements of StepperWait are packed * to allow the group to be compared as a ULONG. They must not change in size * and the structure must be packed. */ StepperWait remWaits[16]; short remWaitCnt; /**************************************************************************** * Function: remWaitStepper * Description: Periodic function called back whenever there is at least one * active remote (on behalf of master) motor wait. This scans the remote waits * array for any whose motor has met the specified condition. For any that do, * a StepperDone message is sent to the master and the remote wait is deleted, * compacting the array. * .............. notes ................................................... * - This could be made more efficient for NotMoving and Idle conditions by * eliminating polling. Each motor that is the object of such a remote wait * could be flagged so that when it reaches the specified condition, it will * increment a count of all such motors, i.e. one count tells how many motors * have reach their specified condition. RemWaitStepper could keep track of how * many of the remWaits are positional, which require polling, vs. endpoint, * and truncate (perhaps to nothing) the search when it is obvious that no * specified remWait's conditions are met. *..........................................................................*/ void remWaitStepper( ULONG arg ) /* type PeriodicFunc */ { StepperWait *wp; for( wp = remWaits ; wp < remWaits + remWaitCnt ; ) if( isStepperCondition( wp )) { if( sendStepperDone( wp ) == CMD_REDO ) break; /* Don't even bother checking others if no room in TxQ. */ deleteRemWait( wp ); } else wp++; } static UCHAR mtrDoneMsg[ DmPackSize( SIZE_STEPPERDONEARG ) ] = { SIZE_STEPPERDONEARG + 1, DM_STEPPERDONE }; #define doneMsg ((Dmsg*)&mtrDoneMsg) int sendStepperDone( StepperWait *wp ) { Stepper *mp; doneMsg->arg.stepperDone.motor = wp->motor; doneMsg->arg.stepperDone.reqUnit = wp->reqUnit; mp = steppers + wp->motor; doneMsg->arg.stepperDone.state = mp->state; doneMsg->arg.stepperDone.position = mp->position; return putMasterMsg( mtrDoneMsg, MSG_HIGH_PRIORITY ); } /**************************************************************************** * Function: deleteRemWait * Description: Delete the given element of the remWait array, compacting the * array over the element if it is not the last one in the array. Decrement the * total count and if it goes to 0 then remove the periodic function from the * main loop's callback list. *..........................................................................*/ void deleteRemWait( StepperWait *wp ) { if( wp < remWaits + remWaitCnt - 1 ) { memmove( wp, wp + 1, (UCHAR *)( remWaits + remWaitCnt ) - (UCHAR*)( wp + 1 )); } if( --remWaitCnt == 0 ) removePeriodic( remWaitStepper, 0, DO_ALWAYS ); }
When a master unit encounters a remote stepper wait message in a script, it must do more than simply relay the message to the motor controller. It also must set up a timeout similar to the script wait for a local motor wait and provide the means by which the slave's stepperDone message terminates the script's wait. The latter is complicated by the fact that input messages are processed asynchronously to the script process, necessitating data sharing between independent processes. In the discussion that follows, APU is considered the master and MSM the slave motor controller. However a unit that has local motor control, such as MSM, may also serve as a master to another motor controller.
RemStepperWait structures tell the status of a motors controlled by slaves. The current implementation of the IML program assumes that there are no more than 20 remote steppers, numbered 0 through 19. The remSteppers array, which contains 20 RemStepperWaits, is synchronized to the motor numbers. For example remSteppers[4] always relates to stepper 4. The function checkSlaveRx examines all messages received from slaves. If the message is DM_STEPPERDONE, parseSlaveStepperMsg is called to process it. Regardless of who might be waiting for the given motor, parseSlaveStepperMsg transfers the motor's state and position information to the appropriate element of remSteppers. If the target unit of the message is a higher master (lower ID) the message is relayed upstream.
apuapp- fsqman.c
typedef struct { UCHAR waiters; /* Count of procs waiting for this motor. */ UCHAR state; /* Motor state, MS_UPRAMP, etc, defined in motor.h. */ USHORT position; } RemStepperState; RemStepperState remSteppers[ 20 ]; #define MS_UNKNOWN 0x7F /* Stepper pseudo-state. */ /**************************************************************************** * Function: parseSlaveStepperMsg * Description: Process DM_STEPPERDONE/StepperDoneMsg from slave. * Returns: * - MSG_PUT if the message's reqUnit indicates that a master to us wants to * see this message. * - MSG_NONE to not relay the message toward master. * Arguments: Dmsg *dm points to the message from the slave. int parseSlaveStepperMsg( Dmsg *dm ) { RemStepperState *rsp; rsp = remSteppers + dm->arg.stepperDone.motor; rsp->state = dm->arg.stepperDone.state; rsp->position = dm->arg.stepperDone.position; return dm->arg.stepperDone.reqUnit < thisUnit ? MSG_PUT : MSG_NONE; } apuapp- com.c void checkSlaveRx( void ) { ... switch( SLAVEINMSG->msgType ) { ... case DM_STEPPERDONE: slaveRxState = parseSlaveStepperMsg( SLAVEINMSG ); break;
When the script process controller, doProcs, encounters any message whose unit ID doesn't match the IML unit number, it sends the message to procParent for processing. ProcParent checks for special cases before relaying the message (putSlaveMsg( (UCHAR*)cmd )) downstream (toward the slave). If the message type is CM_WAITSTEPPER, procParent starts a timeout process for the script; the first call (with a fresh command) to eventTimeout starts the timer.
The RemStepperState waiters element is a semaphore that counts the number of scripts waiting for the motor. If it is 0 when the waitStepper message is processed, the status of the motor is set to UNKNOWN because we don't know what the motor has been doing since the last time (if ever) that we waited for it because a stepperDone message is sent only when a master is waiting for the motor.
If more than one script waits simultaneously for the same motor, a waitStepper message is sent in each case. As already explained, the slave ignores such duplications. However, it would be possible to eliminate duplicates at the source. The wait condition could be recorded in a motor's remSteppers element. When procParent sees that the motor's RemStepperState.waiters is not 0, it could compare the existing wait condition to the new request and only send the message if the two differ. The message can't be skipped unconditionally because, if the two differ, the condition of the first might be met before the second; having received only the first message, the slave would not send a done message relative to the second wait condition, which would, therefore, never appear to be met. Such an approach to avoiding duplication is more complicated than it at first appears. If three scripts might wait for different conditions of the same motor, it would be necessary to record both the first and the second condition in order to know whether to send the third waitStepper message. While the only real limit is the number of Procs, which is 16, one could argue that rarely would so many scripts wait for the same motor. But an equally persuasive argument is that rarely would more than one script wait for the same motor; avoiding the occasional message duplication isn't worth the effort. That argument won.
ProcParent is repeatedly called as the command dispatcher is told to "redo" the waitStepper message. The message is relayed to the slave only the first time. After that, procParent just watches for timeout or the specified condition to appear in the motor's RemStepperState (element of remSteppers).
apuapp- fsqman.c
int procParent( FsqMsg *cmd, ProcDef *pd ) { ... RemStepperState *rsp; switch( cmd->msgType ) { ... case CM_WAITSTEPPER: rsp = remSteppers + ( mtr = cmd->arg.waitStepper.motor ); switch( eventTimeout( pd, cmd->arg.waitStepper.ticks )) { case EVT_FRESH: if( mtr > DIM( remSteppers ) - 1 ) return sendScriptFailMsg( pd, FAIL_BADPARM, FLINE, NONMTR ); if( putSlaveMsg( (UCHAR*)cmd ) == CMD_REDO ) { pd->cmdState = CMS_FRESH; return CMD_REDO; } if( ++rsp->waiters == 1 ) { /* This is the first proc to wait for this motor at this time. The motor's * state cannot be known since we haven't yet asked for it, so set it to * UNKNOWN. This is more reliable than having the last waiter set the state to * UNKNOWN because it guards against message coincidences that can leave the * done message state in the remStepper even after all waiters have finished. * Master-slave communication could be reduced by recording the details of each * waiter and sending the command only for the first one or subsequent ones * that are waiting for some point in the motor's movement that no previous * script is waiting for. However, the extra code complexity doesn't seem worth * while for the few cases of multiple waiters for one motor. */ rsp->state = MS_UNKNOWN; return CMD_REDO; } /* Fall through to test for already done if this isn't the first proc to wait * for this motor. */ case EVT_WAITING: if( rsp->state == MS_UNKNOWN || ( cnd = cmd->arg.waitStepper.condition ) == WAITMTR_STOP && IsMotorMoving( rsp->state ) || cnd == WAITMTR_IDLE && !IsMotorIdle( rsp->state ) || cnd == WAITMTR_GREATER && rsp->position <= cmd->arg.waitStepper.position || cnd == WAITMTR_LESS && rsp->position >= cmd->arg.waitStepper.position ) return CMD_REDO; /* Fall through if motor meets done criteria. */ case EVT_TIMEOUT: --remSteppers[ mtr ].waiters; return CMD_DONE; /* Motor meets done criteria or timeout. */ }
Both remote and local wait for all conditions of a stepper are tested in the script file mtrwait.f. Two identical scripts test local and remote stepper wait, MotorWaitTest, compiled for the MSM, and RemMotorWait, compiled for the APU. Waits for motor not moving and idle report the success of the test. Where the timeout is deliberately set too short, success means timeout. Where it is set at least as long as needed, success means no timeout. Thus, success vs. failure can be detected and reported by the script itself. The success of non-timeout cases of wait for position can only be confirmed by observation. The script should report the position somewhere in mid-travel. The test for deliberate timeout waiting for position is self-documenting.
mtrwait.f
begin MotorWaitTest unit MSM echo "Begin MotorWaitTest" ramp M2 up 50 to 250 linear 20% slew 250 down 250 to 50 linear 30% hold 0.5 // Test timeout waiting for motor not moving. move M2 +200 wait M2 max 0.5 if timeout echo "Correct: timeout waiting 0.5 second for M2" else echo "Error: M2 done under 0.5 second" endif wait M2 // Test not timeout waiting for motor not moving. move M2 +200 wait M2 max 1 if timeout echo "Error: timeout waiting 1 second for M2" else echo "Correct: M2 done under 1 second" endif wait M2 // Test timeout waiting for motor idle. move M2 +200 wait M2 idle max 1 if timeout echo "Correct: timeout waiting 1 second for M2 idle" else echo "Error: M2 idle under 1 second" endif wait M2 // Test not timeout waiting for motor idle. move M2 +200 wait M2 idle max 1.5 if timeout echo "Error: timeout waiting 1.5 second for M2 idle" else echo "Correct: M2 idle under 1.5 second" endif wait for 1 second // Test wait for motor position >. position M2 1000 move M2 to 1400 wait M2 > 1200 echo "M2 > 1200" wait M2 wait for 1 second // Test wait for motor position <. move M2 to 1000 wait M2 < 1100 echo "M2 < 1100" wait M2 wait for 1 second // Test wait for true motor position > in time. move M2 to 1300 wait for M2 > 1200 max 2 seconds if timeout echo "Error: M2 not > 1200 in 2 seconds" else echo "Correct: M2 > 1200 under 2 seconds" endif // Test wait for false motor position < in time. wait for M2 < 1200 max 2 seconds if timeout echo "Correct: M2 not < 1200 in 2 seconds" else echo "Error: M2 < 1200 under 2 seconds" endif end begin RemMotorWait unit APU echo "Begin RemMotorWait" ramp M2 up 50 to 250 linear 20% slew 250 down 250 to 50 linear 30% hold 0.5 // Test timeout waiting for motor not moving. move M2 +200 wait M2 max 0.5 if timeout echo "Correct: timeout waiting 0.5 second for M2" else echo "Error: M2 done under 0.5 second" endif wait M2 // Test not timeout waiting for motor not moving. move M2 +200 wait M2 max 1 if timeout echo "Error: timeout waiting 1 second for M2" else echo "Correct: M2 done under 1 second" endif wait M2 // Test timeout waiting for motor idle. move M2 +200 wait M2 idle max 1 if timeout echo "Correct: timeout waiting 1 second for M2 idle" else echo "Error: M2 idle under 1 second" endif wait M2 // Test not timeout waiting for motor idle. move M2 +200 wait M2 idle max 1.5 if timeout echo "Error: timeout waiting 1.5 second for M2 idle" else echo "Correct: M2 idle under 1.5 second" endif wait for 1 second // Test wait for motor position >. position M2 1000 move M2 to 1400 wait M2 > 1200 echo "M2 > 1200" wait M2 wait for 1 second // Test wait for motor position <. move M2 to 1000 wait M2 < 1100 echo "M2 < 1100" wait M2 wait for 1 second // Test wait for true motor position > in time. move M2 to 1300 wait for M2 > 1200 max 2 seconds if timeout echo "Error: M2 not > 1200 in 2 seconds" else echo "Correct: M2 > 1200 under 2 seconds" endif // Test wait for false motor position < in time. wait for M2 < 1200 max 2 seconds if timeout echo "Correct: M2 not < 1200 in 2 seconds" else echo "Error: M2 < 1200 under 2 seconds" endif end
Scripts normally execute synchronously, that is through invocation by other scripts or from a top-level master, such as the script debugger. However, after a system fault, such as motor page overrun, one or more scripts should run automatically to establish safe conditions for shutdown, recovery, and/or diagnostics. In some cases, the response should be immediate and not dependent upon the top-level master or other scripts, but the system-level program itself doesn't know enough about the instrument configuration to do very much. This is the motivation for event-triggered scripts, which the system can start.
It would violate the basic principal of separating the system from the application if the system were to know what script to invoke in response to a particular event. Instead, scripts register themselves to handle one or more faults. The system and script domains must intersect in the definition of these events or the system would have no means of relating a script to an event. The script and analyzer configuration languages already have a means of defining and reporting faults. A script can send a fault to the data station using the report command [cdsref- Report]. The principle argument to report is the name of a fault defined in the configuration file. Since the purpose of this mechanism is to report faults, it is naturally related to the possibility of invoking a script in response to a fault. Most of the fault types represent a means of linking the script domain to the data station domain and do not involve the analyzer system. However, the first 39 fault ID numbers are reserved for system faults. The script domain, via the configuration file, and the analyzer system domain, in analyz.h, must agree on the meaning of these numbers. Currently, only one system fault has been defined for this mechanism, the motor page overrun.
analyz.ini
FAULTS ... ; Embedded System Faults FAULT_MOTORPAGE = 1 ;......................................................................... ; Faults Reported by FAULT command MIX_VERT_INACTIVE = 40 ;% LEVEL = RESET ;% REF = "mix head stuck in angle position"
msmapp- analyz.h
/******************* SYSTEM FAULTS ******************************************/
enum { FAULT_MOTORPAGE = 1 };
The script writer determines what fault or faults a script will be registered to handle. This is specified in a fault handler declaration in the script's begin statement, as explained in the script language reference [cdsref- BeginCommand]. A single fault or range can be specified by names or numbers, for example begin MotorFault handle FAULT_MOTORPAGE or begin DefaultHandler handle 1 through 9999.
A fault handler script is a normal script in nearly all regards and may be invoked synchronously, but its main purpose is to handle a situation created by an asynchronous fault. Since a fault handler may be invoked for a range of faults, to distinguish the cause, the script is started with its local VAR0 initialized with the ID number of the precipitating event. Local variables VAR1 and VAR2 may contain additional information for specific faults (currently none do, as the only fault, motor page, does not provide additional information). Normally, the script compiler does not allow a script to read the value of a local VAR before assigning it a value, as this would have to be an error under normal circumstances. However, if the script is a fault handler (its begin statement includes a fault handler clause) the compiler allows it to read any of the first three vars without assigning them values.
To determine whether a local VAR has been assigned a value, the script compiler uses a "register coloring" technique with an array of eight BOOLs, locVarHasVal. Whenever the compiler encounters a statement that assigns a value to a particular VAR, it sets the corresponding locVarHasVal true. At each statement that reads a VAR, the compiler checks to see whether the corresponding locVarHasVal is true. To avoid a compiler error when a fault handling script reads VAR0 through VAR2, the compiler colors their locVarHasVal elements while translating the begin statement.
fcomp- futil.h
#define PADCHAR '~' #define PADWORD (257 * '~') fcomp- fsqyac.y void initCmdState( void ) { ... memset( &fmsg.arg, PADCHAR, 20 ); flowseq => begin ... begin => T_BEGIN T_SYM { ... initCmdState(); } seqDefOptionList { ... memset( locVarHasVal, 0, sizeof( locVarHasVal )); /* All local VARS have unknown value when a script begins. */ if( fmsg.arg.begin.evLow != PADWORD ) { locVarHasVal[0] = TRUE; /* Event passed via VAR0 */ locVarHasVal[1] = TRUE; /* Two other args may also be passed. */ locVarHasVal[2] = TRUE; } else fmsg.arg.begin.evLow = 0xFFFF; /* HANDLER_NO_FAULTS */ } endline ; seqDefOptionList => seqDefOptionList seqDefOption ... seqDefOption : T_HANDLE faultType { if( yyvsp[0].ui == HANDLER_ALL_FAULTS ) { /* set range to 1 to HANDLER_ALL_FAULTS */ fmsg.arg.begin.evLow = 0x0100; fmsg.arg.begin.evHigh = swapUS( HANDLER_ALL_FAULTS ); } else { /* evLow = evHigh if handles one fault. */ fmsg.arg.begin.evLow = swapUS( yyvsp[0].ui ); fmsg.arg.begin.evHigh = swapUS( yyvsp[0].ui ); } } | T_HANDLE faultType T_THROUGH faultType { if( yyvsp[-2].ui > yyvsp[0].ui ) fatalError( -1, "The lower event is higher than the upper.\n" ); fmsg.arg.begin.evLow = swapUS( yyvsp[-2].ui ); fmsg.arg.begin.evHigh = swapUS( yyvsp[0].ui ); } faultType : T_UINT { yyval.ui = yyvsp[0].ui; } | T_FAULT_HANDLER { yyval.ui = yyvsp[0].ui; } | T_SYM { yyval.ui = getFaultNumber(); } void checkLocVar( UINT varid ) { if( varid < MINGVAR && !locVarHasVal[ varid ]) showWarning( "Local VAR%d used for value before being assigned a value.\n", varid );
When the compiler translates a begin statement that contains a fault handler range, it assigns the ID of the low fault to the begin message's evLow and the ID of the high fault to evHigh. If only one fault is given, its ID is assigned to both evLow and evHigh. If the statement includes no fault handler clause, evLow is assigned 0xFFFF; evHigh is don't care. When a script is downloaded to an IML unit, evLow and evHigh from the begin message are copied to the corresponding elements of the script's descriptor (FsqDef).
fsqapi.h
#define HANDLER_NO_FAULTS 0xFFFF #define HANDLER_ALL_FAULTS 0xFF00 /* BeginCmd.evLow pseudo-id values. */ typedef PACKED struct { ... USHORT evLow; /* Lowest event that this fsq can handle.*/ USHORT evHigh; /* Highest handled event. */ ... } BeginCmd; /* CM_BEGIN */
msmapp- fsqman.h
typedef struct { ... USHORT evLow; /* Lowest event that this fsq can handle.*/ USHORT evHigh; /* Highest handled event. */ ... } FsqDef;
The FsqDef's evLow and evHigh play no role until a system fault occurs or a report command is executed. In both of these situations, triggerEventFsq is called. Whoever calls this function, passes an array of USHORTs, the first of which tells the ID of the fault. The remainder of the array comprises a list of arguments that may be passed to the handler in local VARs. The first element of this list is the count of arguments, which may only be 0, 1, or 2. The arguments themselves follow this count.
TriggerEventFsq searches the list of loaded scripts (fsqTab) to find the best handler, which is the script that most-specifically handles the event. If it finds a script that handles only this fault then it immediately tries to start that script. The script is likely to start because only loaded scripts are listed. However, if the script doesn't start, triggerEventFsq continues searching. While looking for a specific handler, triggerEventFsq is also searching for a script that handles a range of faults that includes this event. If one is found, it is not immediately started. If its range is narrower than any other found so far, it is recorded. If the entire list contains no specific handler that will start, triggerEventFsq tries to start the best range handler if one was found. This is similar to C++ and Java exceptions, where, in the absence of a specific handler, a general fault handler is invoked.
msmapp- fsqman.c
/**************************************************************************** * Function: triggerEventFsq * Description: Start the script registered to handle either this specific * event or one whose range contains it (if more than one then the one with * the narrowest range, i.e. most specific). * * Returns: EVENT_HANDLED, EVENT_HANDLER_FAILED, or EVENT_NO_HANDLER. * Arguments: USHORT *args is [0] = event, [1] = additional arg count, * [2..] = arguments to pass to VAR1.. in the triggered script (event is passed * in VAR0. * .............. notes ................................................... * - For begin scriptName handler allFaults, the script compiler assigns a * range of 1 to FF00, indicating that the script handles all faults without * requiring special command message syntax or unique testing in this function. * Here, simply looking for the narrowest range handler that includes the given * event will find either a tighter fit or the AllFaults handler. ...........................................................................*/ int triggerEventFsq( USHORT *args ) { FsqDef *fdp; FsqDef *rangeHandler; /* Most-specific (narrowest range) handler whose range includes this event. */ int bestRange; /* rangeHandler's range. */ int range; int triedAndFailed; int idx; USHORT event; event = args[0]; rangeHandler = 0; bestRange = 0xFFFF; /* Anything is better. */ triedAndFailed = FALSE; /* Search simultaneously for specific and range handler. */ for( fdp = fsqTab ; fdp - fsqTab < nextFsqSlot ; fdp++ ) { if( fdp->evLow != HANDLER_NO_FAULTS ) { if( event == fdp->evLow && event == fdp->evHigh ) { if( startEvFsq( fdp, args ) == TRUE ) return EVENT_HANDLED; else triedAndFailed = TRUE; } else if( event >= fdp->evLow && event <= fdp->evHigh && ( range = fdp->evHigh - fdp->evLow ) < bestRange ) { rangeHandler = fdp; bestRange = range; } } } /* * We arrive here only if there were no handlers for this specific event that * could be successfully started. If there is a handler whose range includes * this fault then try it. */ if( rangeHandler != 0 ) { if( startEvFsq( rangeHandler, args ) == TRUE ) return EVENT_HANDLED; return EVENT_HANDLER_FAILED; } return triedAndFailed == TRUE ? EVENT_HANDLER_FAILED : EVENT_NO_HANDLER; } /**************************************************************************** * Function: startEvFsq * Authors: David McCracken * Description: Helper for triggerEventFsq. Try to start the given script. If * it does start then transfer the event (args[0] into the proc's VAR0 and any * additional args into VAR1... * Returns: TRUE if the given script (FsqDef) can be started else FALSE. * Arguments: FsqDef *fdp is the script to start. See triggerEventFsq for * USHORT *args description. * Globals: newProc * .............. notes ................................................... * - Because arguments are passed to event-triggered script in their local * VARs, the script compiler turns off warnings on use of local VARs before * they have been initialized for any script that is declared (in the begin * statement) as event handlers. *..........................................................................*/ BOOL startEvFsq( FsqDef *fdp, USHORT *args ) { USHORT idx; if( startFsq( fdp, PARENT_EVENT, PROC_NOPROC, NO_VAR ) == CMD_DONE ) { newProc->locVars[0] = args[0]; for( idx = 1 ; idx <= args[1] ; idx++ ) newProc->locVars[ idx ] = args[ idx + 1 ]; return TRUE; } return FALSE; }
A motor page overrun occurs when the motor controller fails to service the motor interrupt before it occurs again. Missing an interrupt is not necessarily catastrophic. The CPU remains synchronized with the FPGA because the ISR selects the event page based only on the FPGA's page flag (evPtr = ( isr & SISR_MTRPAGE ) ? page0Events : page1Events). If the page preceding the missed one contained no events, the FPGA will do nothing when it inadvertently executes the page. If there were events in the next page, they would only be delayed, not skipped. However, it's just as likely that all pages contain events and executing the wrong one and then the correct one at the wrong time could cause catastrophic positioning failures.
Between the extremes of no error and catastrophic failure lie the worst kinds of errors-- small positioning faults that could go undetected but still cause a loss of precision. To avoid this, any page overrun is treated as a catastrophic fault that probably requires system reinitialization. The reason that the required response is not a certainty is that the fault means different things under different conditions. It should never occur and should always be logged but its occurrence, for example, during a diagnostic routine is less severe than during sample processing. If it occurs during rinsing following a sample, it might be associated with that sample but is not cause to invalidate the results. The only way to properly take instrument conditions into account while still providing a means to respond rapidly is to use an event-triggered script.
The FPGA's page overrun flag can be checked at any time, but checking it in the ISR eliminates the need to periodically check it. Checking it only in the ISR is the most efficient approach but means that it isn't checked at all if the interrupt fails completely. This is not a problem, because steppers don't move if the ISR isn't serviced; such a major failure would be obvious both to scripts awaiting events and to human operators.
As shown earlier, the stepper motor ISR sets the ERR_MTRPAGE flag in the global variable systemError when the FPGA reports a page overrun. The ISR can't itself call triggerEventFsq, which cannot execute in an interrupt process. The main loop periodically tests systemError for any fault. Given that there is currently only one system fault, it might appear unnecessarily complicated for the motor ISR to set a flag for periodic sampling by the main loop, which could just as easily sample the FPGA's isr register directly. However, this "simpler" approach has two flaws. One is that additional system faults will be implemented in the future and it will be cheaper for the main loop to test for any of them by reading the value of a single variable. The normal case will be 0, indicating no faults, and the main loop will probe deeper only in the rare case of some fault. The other flaw is that it would unnecessarily expose the main loop to details of the motor control mechanism.
The main loop calls triggerEventFsq, passing it only the fault ID, FAULT_MOTORPAGE. If triggerEventFsq returns saying that no script was found (or started) to handle the fault, the main loop reports the error to the data station itself.
msmapp- proc.h
enum { ERR_MTRPAGE = 1 }; /* systemError flags. */ USHORT systemError; msmapp- appsys.c int main( UCHAR underDebugger, UCHAR unit, UCHAR comType ) { ... systemError = 0; ... while( 1 ) /* .............. main loop ............... */ { ... /* Bit-mapped systemError supports up to 16, but there is currently only * one, ERR_MTRPAGE, and it can occur only in a system that has native stepper * control. If more system error handlers are created, they should be in an * array sync'd to systemError map. */ if( systemError ) { eventArgs[0] = FAULT_MOTORPAGE; eventArgs[1] = 0; /* No arguments to pass to handler other than event in VAR0. */ if( triggerEventFsq( eventArgs ) != EVENT_HANDLED ) postMsg( DM_SYSERR, SYSERR_GENERAL, "Motor page overrun" ); systemError = 0; }
One of the primary purposes of the testSystem script command is to force errors to occur in order to test the IML unit's response. Many errors can only be forced by intrusively manipulating the system. A good example is the motor page fault. We have done everything we can to avoid the fault, making testing difficult. One way to force it would be to modify the motor ISR to consume too much time or to temporarily disable the IRQ itself. In fact, the ability to enable and disable an IRQ at will is a useful addition to the arsenal testSystem uses to attack the system. With this capability, a script can verify correct response to the motor page fault by disabling the interrupt briefly and then reenabling it.
TestSystem IRQ was added to the native test system functions. On the script compiler's side, this is very easy to do because very little syntax checking is performed on testSystem statements. The cardinal TS_IRQ is added to the nativeTestSystemFuncs in fsqapi.h. The compiler and interpreter share this. The name "Irq" is added to the compiler's testSystemFuncs superstring, which the script compiler searches for the first word appearing after the testSystem command word. With these additions, the compiler will accept the statement testSystem Irq followed by any number of integers or True and False. What is expected is testSystem Irq irqNumber true/false. For example testSystem Irq 5 False to disable IRQ 5 and testSystem Irq 5 True to enable it. By design, the compiler affords little assistance with this or any other testSystem function.
fsqapi.h
enum nativeTestSystemFuncs { TS_TRIMOTOR = 'A', TS_BUSFAULT, ...TS_IRQ, ... }; /* Native testSystem functions (SysCmd.op). The order of these must match * the compiler's testSystemFuncs list (in names.h) but the two lists don't have * to begin at the same item. */ typedef PACKED struct { UCHAR op; USHORT args[1]; } SysCmd; /* TestSystem/CM_SYSTEM */
fcomp- names.h
/* Native testSystem function names. The order of these must match that of * the nativeTestSystemFuncs enum defined in fsqapi. However, that list begins * with cardinals that don't match any in this list, for example, TS_TRIMOTOR. * The unmatched ones are generated by the parser in response to named types. * e.g. testSystem ShearValve causes the compiler to use TS_TRIMOTOR for * SysCmd.op. */ char testSystemFuncs[] = "BusFault\0\ ... Irq\0\
The script interpreter (any IML unit program, e.g. msmapp) processes testSystem messages in the procSys function. When sys.op is TS_IRQ, it converts the first USHORT arg, which is presumed to be the IRQ number, to an external interrupt selector mask. If the second USHORT arg is 0 the interrupt is disabled by reconfiguring its Port B pin as simple input instead of IRQ. If the arg is any value other than 0 the pin is configured for interrupt.
With our current hardware, this is effective only for external interrupts 7, 6, 5, and 3 because these are the only ones used. Motorola claims that the 68340's level 7 interrupt is non-maskable because it can't be disabled through the CPU's Status Register, but the external IRQ obviously can be disabled by configuring the pin to not be an interrupt. There is no general means of controlling internal interrupts, for example from the UARTs or timers, because these are configured through special registers in each on-board peripheral. The CPU's Status Register affords a means of general control but it is too general-- all interrupts below a selected level are disabled, not specific ones.
msmapp- systest.c
int procSys( FsqMsg *cmd, ProcDef *pd ) { ... switch( cmd->arg.sys.op ) { ... case TS_IRQ: irq = cmd->arg.sys.args[ 0 ]; if( irq < 7 ) { irq = 1 << irq; if( cmd->arg.sys.args[1] == 0 ) Sim->pparb &= ~irq; else Sim->pparb |= irq; }
SYSTEM FAULT TESTING
The motor page overrun fault, testSystem Irq function, and event-trigger scripts are tested together using one set of scripts. Mtrpage.f contains three scripts that can handle the motor page fault. MotorHandler handles only the specific fault; RangeHandler handles faults 1 through 20, which includes the motor fault; AllHandler handles all faults. If MotorHandler is resident, it should be triggered when the fault occurs. Otherwise, if RangeHandler is resident, it should be triggered. Otherwise, AllHandler should be triggered.
The MotorPageFault script forces a page overrun using testSystem Irq to disable IRQ 6 (the motor interrupt) and reenable it after a brief delay. TestMotorPageFault forks MotorPageFault. All of the handlers are initially present, so MotorHandler is triggered. But then TestMotorPageFault deletes MotorHandler and again forks MotorPageFault. This time RangeHandler is triggered. RangeHandler is then deleted and MotorPageFault forked. This time AllHandler is triggered.
mtrpage.f
unit MSM begin AllHandler handle allFaults echo "I'm the handler for all faults" end begin RangeHandler handle 1 through 20 echo "I'm the handler for faults 1 through 20" end begin MotorHandler handle FAULT_MOTORPAGE echo "I'm here to handle motor page overrun" echo "VAR0 is \VAR0" end begin MotorPageFault testSystem Irq 6 FALSE wait for 0.020 testSystem Irq 6 TRUE wait for 0.020 end begin TestMotorPageFault echo "Begin TestMotorPageFault" echo "Observe specific handler" fork MotorPageFault wait for 0.5 delete MotorHandler echo "Observe range handler" fork MotorPageFault wait for 0.5 delete RangeHandler echo "Observe all faults handler" fork MotorPageFault wait done MotorPageFault echo "End TestMotorPageFault" end
The resulting (decompiled) echo log is as follows:
26DM_ECHO 2Begin TestMotorPageFault 26DM_ECHO 2Observe specific handler 39DM_ECHO 2I'm here to handle motor page overrun 11DM_ECHO 2VAR0 is 1 23DM_ECHO 2Observe range handler 41DM_ECHO 2I'm the handler for faults 1 through 20 28DM_ECHO 2Observe all faults handler 32DM_ECHO 2I'm the handler for all faults 24DM_ECHO 2End TestMotorPageFault
{} To top of [FirmwareDesign]
[NextTopic] [MainTopics]
The motor control structure and ISR treat each of the five (up, slew, down, recoil, hold) segments of a motor trajectory as independent. The slew and hold segments require only single values, the duration (number of 32 usec periods) respectively of each slew step and of the total hold time. These values are stored in the motor control structure and are not shared at the control level. The up, down, and recoil segments require multiple values. Each motor structure contains pointers to arrays for these segments' values. The arrays are independent of individual motors and may be shared.
At the script language (source) level, multi-value ramp segments (henceforth, called simply ramps) can be independent or associated with a complete, including all five segments, or partial trajectory. The trajectories are an invention for the convenience of the script developer. The compiler decomposes trajectories into individual ramps.
Trajectories and ramps can be named for source level reuse by multiple motors or defined in association with a particular motor, in which case they are not named. Each trajectory is independent even if identical to another.
RAMPDEF command messages define ramps. They include the ramp's ID number (assigned by the compiler) and the step duration array. When the analyzer interprets one of these messages (interactively or in a script) it stores the ramp as an independent entity not associated with any motor.
RAMP command messages assign a full or partial trajectory to a particular motor. The RAMP message flags each of the five segments, allowing any set, including just one, to be changed. Thus, a motor's trajectory can be built by overlaying one or more partial trajectories. This capability is called superposition. Generally, trajectories are composed of dependent segments and superposition has to be used cautiously. The capability finds most use during system development. For example, the speed of a continuously running motor (e.g. a peristaltic pump) can be changed with a RAMP message that defines only the SLEW segment, or the UP ramp slope may be varied without changing the SLEW rate in order to test the step loss envelope.
RAMPDEF statements (i.e. script source) define permanent, named ramps. RAMPDEF messages generated from RAMPDEF statements include a ramp ID in a range that identifies the ramp as permanent, meaning that it cannot be removed by garbage collection and is, therefore, always available for assignment to a motor. The compiler stores the ramp's name and ID in a ramp dictionary (rampdict.dat) so that it may be referenced from any script file once defined. RAMPDEF is a normal script statement, which can be executed at any time, but it must either be interactive (submitted through the debugger) or reside in a script compiled for the motor controller (MSM). Further, it must be executed before any script refers to it. Consequently, it is typical for all RAMPDEF statements to reside in one file, which is downloaded, executed, and deleted (to save script memory) during system initialization.
RAMPDEF statements are the only source of permanent ramps but they are not the only source of RAMPDEF command messages. RAMP statements also may also cause the compiler to generate RAMPDEF messages. If a RAMP statement contains only SLEW, HOLD, and/or ramps defined by RAMPDEF statements then the compiler only generates the one RAMP message. However, the RAMP statement can define ramps itself, in which case the compiler generates as many RAMPDEF messages as needed followed by the RAMP message, which refers to the ramp IDs used in the preceding RAMPDEF messages. These IDs are in a range considered temporary. The motor controller stores such ramps and keeps them only as long as they are referenced by one or more motors. If the controller runs out of ramp heap memory, it will perform garbage collection, removing all temporary ramps, to free up memory for new ramps. Temporary ramps are unnamed and are not added to the ramp dictionary.
The script debugger supports interactive as well as automated (e.g. for Safe Operating Area testing) trajectory development. The ability to vary ramps without having to stop the analyzer and reload all of them is based on the fact that RAMP and RAMPDEF are normal script commands, which, like most commands, can be executed at any time. Normally, RAMPDEF statements are not used for interactive development because their use is clumsy and they will rapidly consume the entire ramp heap. Creating and assigning ramps in RAMP commands is not only easier but also insures that ramp memory will never be exhausted.
RAMP and RAMPDEF are fully described in the script language reference [cdsref- Ramp] [cdsref- Rampdef].
The motor controller needs event delays (step width and hold time) in terms of number of 32 usec (nominal) time slots to count. This is obviously inconvenient for instrument developers, for whom steps per second is more appropriate. Actually, SPS represents the lowest level that should be presented to developers. In the future we may want to support more application-oriented units, such as revolutions and linear inches of travel (accounting for motor step angle and lead screw pitch) per second.
The 32-usec time slot is by design and may not be realized precisely in practice. For example, the MSM's period has been measured as 30.67 usec. This should not matter to the script developer working at the level of steps per second or higher. Since we want the analyzer to do as little work as possible, the responsibility for conversion, taking into account the true slot period, falls on the script compiler. The true slot period can be defined in the analyzer definition file (analyz.ini). To avoid floating point numbers in the configuration language, which has a very primitive parser, this is inverted to rate; e.g. for the MSM, the step rate is 32,605 slots per second. Internally, the compiler has no trouble with floating point operations, which are needed for the SPS to slot count conversion.
A steps per second value is converted to slot count simply by dividing the 32,605 slot rate by the step rate. For example 50 SPS becomes 652; each step lasts for 652 slots. Note that, with only 256 slots in each 8.3 msec page, at this rate the motor would have one step for every 2.5 pages.
The MSM's motor step rate is defined in analyz.ini by:
STEPPER_MOTORS UNIT = MSM STEP_RATE = 32605 ; Stepper driver fundamental rate (1/step period = steps/second).
The script compiler's analyzer configuration language parser picks this up and stores it in the unit descriptor. The current implementation supports only one step rate per unit, assuming that no unit will have multiple, incompatible motor control subsystems.
fcomp- cfgsys.c
int loadAnalyzerCfg( void ) { ... switch( scanState ) { ... case TOK_STEPPER_MOTORS: if( !isUnitDef( NEEDS2 )) if( stricmp( tok1, "STEP_RATE" ) == 0 ) { if( tok2val < 10000 || tok2val > 60000 ) cfgError( -1, "Out of range motor step rate %u.\n", tok2val ); unitDes[ unitNum ].stepRate = tok2val;
The checkMotorController function in the script language parser verifies that the given unit is a stepper controller by virtue of its having a defined stepRate. It coincidentally assigns the step rate to the global motorStepRate for use in the rampdef generator. When the command statement is RAMPDEF, the script being compiled, seqUnit, is passed as the unit to checkMotorController. This means that rampdef must reside in a script compiled for the motor controller itself; RAMPDEF cannot be executed as a remote command (i.e. passed by a master to a slave). Also, for RAMPDEF, the argument NOTFOUND_OK is passed to checkMotorController in order to give the parser the opportunity to generate a clear message as to this restriction. When checkMotorController is called for a RAMP statement, the unit known to control the motor is passed in addition to the argument NOTFOUND_FATAL. Thus, RAMP can be executed by the motor controller or any of its masters, and a missing step rate can only mean that analyz.ini is incomplete.
fcomp- fsqyac.y
BOOL checkMotorController( UINT unit, int ifNotFound ) { ... if(( motorStepRate = unitDes[ unit ].stepRate ) == 0 ) { if( ifNotFound == NOTFOUND_FATAL ) fatalError( -1, "%s missing STEP_RATE definition.\n", getUnitName( unit )); command => T_RAMPDEF { ... if( !checkMotorController( seqUnit, NOTFOUND_OK )) fatalError( -1, "%s missing STEP_RATE definition. \ Rampdef can only be executed by a direct motor controller.\n", getUnitName( seqUnit )); command => T_RAMP T_SYM /* Motor name */ { ... checkMotorController( namedev.mtr->unit, NOTFOUND_FATAL );
The global variable motorStepRate is only a means of transferring the step rate declared in analyz.ini to ramp generator functions. They are aware that it is a USHORT, which affords insufficient resolution for calculating step duration, and that it is the inverse of duration. As shown here for the linear ramp generator, the value is converted to double and is divided by step rate in seconds to produce step duration in 32-usec counts.
fcomp- motor.c
UCHAR emitLinearRamp( USHORT start, USHORT end, USHORT grade, USHORT incflag ) { ... double step; stepRate = motorStepRate; // Convert USHORT to double. step = stepRate / (double)first; // First step width. endStep = stepRate / (double)last; // Ideal last step width.
Each ramp requires an ID as a means for ramp messages to identify the associated up, down, and recoil segments. It would be reasonable to limit the number of different ramps to 256, suggesting that a single byte would be sufficient for the ID. However, this affords an insufficient range for supporting both permanent and temporary ramps. Further, the temporary range is divided into two non-overlapping ranges, one for ramps defined in ramp statements in scripts and the other for interactive ramp definitions. Any range limit represents a compromise because the script compiler will wrap around the end of each range to its beginning. Any earlier ramps still in use risk being replaced by new ramps reusing the same IDs. In production scripts, this possibility is avoided by using only permanent ramps and by using the ClearRamps pragma to clear the ramp dictionary at the beginning of the file that contains the RAMPDEF statements.
The permanent and both types of temporary ramp ranges are defined in fsqapi.h.
fsqapi.h
#define RAMP_PERM 0xE000 /* Ramp IDs E000-FFFF (8K) reserved for permanent. */ #define RAMP_DEBUG 0x4000 /* 4000-DFFF (40K) reserved for interactive debug. */ /* Ramp IDs 1-3FFF (16K) reserved for unnamed ramps in scripts. */
In scripts and to the script debugger user, ramp definitions embedded in ramp statements look like rampdef definitions without a name, for example ramp M2 up 50 to 250 linear 20%. However, a syntactic variation has been defined specifically to support interactive ramp development. An automatic ramp's ID can be explicitly stated, for example ramp M2 #20000 up 50 to 250 linear 20%. This is not intended for the script writer, except for testing the script system itself. Its intent is to afford the debugger a means of distinguishing interactive ramps from temporary ramps defined in scripts.
fcomp- fsqyac.y
rampSeg : segType /* UP, SLEW, DOWN, RECOIL, HOLD, or = rampName */ ... optIdNum /* Forced ramp ID to bypass dictionary during development. */ segDescriptor { ... default: /* T_UP, T_DOWN, or non-0 T_RECOIL. */ *pRampId = writeRampdef( idNum ); /* Write rampdef message * into fsq file and store ID (returned by writeRampdef in MOTO form) into ramp * command in case this segment definition is a phrase in a ramp statememt * instead of in a rampdef. */ optIdNum : /* The intended use is to assign a literal number to something for debugging and development. */ '#' T_UINT { if( yyvsp[0].ui == 0 ) fatalError( -1, "0 is reserved for unassigned ID. Use 1 to 65534.\n" ); idNum = yyvsp[0].ui; } | /* */ { idNum = 0; }
When a ramp statement forces the ID, the writeRampdef function assigns this ID to the ramp in the rampdef message. Otherwise, if the command is ramp, an ID is taken from the temporary range reserved for scripts, 1 through 0x3FFF; or else an ID is taken from the permanent range reserved for named ramps and the ramp is recorded in the script dictionary. If the ID is not forced, writeRampdef increments the selected range ID, wrapping around at the range limit.
As currently defined, 16,383 IDs are reserved for unnamed ramps in scripts, 40,960 are reserved for unnamed interactive ramps, and 8,192 are reserved for permanent ramps. Each range has a unique means of propagating IDs but they all have a slight risk of assigning the ID of a script in use to a new script. The unnamed ramps in scripts and the permanent ramps depend on the script dictionary for avoiding collisions. With a fresh dictionary, both ranges are reset. As each permanent script is stored in the dictionary, its ID is stored and the range ID is incremented. As each temporary (in-script) ramp is created, the incremented range ID is stored in the dictionary even though the individual ramps are not. Persistence is needed for both ranges to avoid having to recompile every script file containing a ramp or rampdef statement whenever one file changes. Without ID persistence, each invocation of the compiler would reset the ranges' IDs, causing multiple collisions immediately.
fcomp- motor.c
USHORT tempRampId = 0xFFFF; // Initial value indicates dictionary hasn't been consulted. USHORT writeRampdef( USHORT forceId ) { ... USHORT id; if( forceId != 0 ) id = forceId; else { if( cmdType == T_RAMPDEF ) { if( ++permRampId < RAMP_PERM ) // Overflow. permRampId = RAMP_PERM; // Wrap around. id = permRampId; newPermRamps = TRUE; } else { if( tempRampId > RAMP_DEBUG ) { if(( fp = fopen( RAMP_DICT_FILE, "rb" )) != 0 ) { fread( &tempRampId, 2, 1, fp ); fclose( fp ); } else tempRampId = 0; } if( ++tempRampId >= RAMP_DEBUG ) // Overflow tempRampId = 1; // Wrap around. id = tempRampId; newTempRamps = TRUE;
Clearing the ramp dictionary, either by deleting rampdict.dat or by compiling a file containing the statement pragma ClearRamps, prevents the permanent and in-script temporary range IDs from incrementing until they wrap around and risk overwriting a ramp still in use. Permanent ramps are the most likely to remain in use but they are also the ones least likely to be discarded, so their range ID will advance very slowly, perhaps never reaching the limit. Temporary in-script ramps may be created rapidly, but this is an indication that they are being rapidly discarded.
fcomp- fsqyac.y
seqList => ... decList flowseq decList => decList decl decl => ... defineStatement statement => ... defineStatement defineStatement => ... T_PRAGMA onOffOpt T_SYM { setPragma( onOffOption != OO_OFF ); /* On and Empty = TRUE. Only Off = FALSE. */
fcomp- fsqsup.c
void setPragma( BOOL onOff ) { switch( indexSuperString( yytext, pragNames, CASE_INSENSITIVE )) { ... case PRAG_CLEAR_RAMPS: clearRampDict();
fcomp- motor.c
void clearRampDict( void ) { ... if( fpRampDict != 0 ) fclose( fpRampDict ); remove( RAMP_DICT_FILE ); tempRampId = 0; permRampId = RAMP_PERM;
The clearRampDict function, invoked by pragma ClearRamp, resets tempRampId to 0 and permRampId to RAMP_PERM as well as deleting the ramp dictionary file. Therefore, the first temporary ID will be 1 and the first permanent ID will be RAMP_PERM + 1.
The script compiler does not manage the IDs for interactively developed ramps because the debugger generates IDs, which it forces on the compiler. When the user (or automatic ramp test function) submits a ramp command, the debugger passes the text of the statement to the txScriptCommand function. This function wraps the statement in a script that it submits to the compiler. TxScriptCommand doesn't modify most statements, but for ramp statements that don't already contain forced ID clauses (#number) it inserts a forced ID for each individual embedded ramp definition clause. RampId is initialized to RAMP_DEBUG when the debugger starts and is incremented at each use. After 40,960 interactive ramps have been defined, the ID wraps around back to RAMP_DEBUG. Such a large range was selected for this use, not because any user will submit so many ramps in one debug session but because an automatic ramp test function might do so.
cdxdbg- scrcmd.cpp
* Function: txScriptCommand * Description: Compile a single script command and download the command * messages generated by the compiler to the specified unit for immediate * (interactive) execution. ... * - This filters ramp statements in order to help the user to interactively * develop ramps without affecting the compiler's ramp dictionary. The RAMP * statement allows ramps to be explicitly numbered using the construct #nn, * for example RAMP M5 UP #100 10,20,30 DOWN #101 30,20,10. When the compiler * sees this, it uses the given number as the ramp's ID in the rampdef message * and neither looks for a match nor enters the ramp into the dictionary. The * user can include these clauses but, since they serve no normal purpose, the * syntax will be unfamiliar. If we see that the user has included an ID * clause, we won't change the statement, even if the statement includes * multiple ramps and only one has an ID number (which would be a bad idea). * Otherwise, each ramp segment (not SLEW or HOLD) is given the next ID number * in the range RAMP_DEBUG to RAMP_PERM - 1. * Although it would be possible to reuse a number for any given segment of * any given motor, this is more trouble than it's worth, as it would afford * some benefit only when a ramp has exactly the same number of step changes as * its predecessor, which is unlikely. In any case, when the target's ramp heap * gets full, it will prune the heap of all ramps no longer attached to a * motor. *..........................................................................*/ int txScriptCommand( HWND owner, UCHAR *cmd, char *unitName, int targetIdx, void ( *scanMsg )( UCHAR *msg )) { ... static USHORT rampId = RAMP_DEBUG; ... fprintf( fp, "begin _SCRIPTCOMMAND" ); if( unitName != 0 && tellUnit ) fprintf( fp, " unit = %s", getUnitAlias( unitName )); fputc( '\n', fp ); sscanf( (const char*)cmd, "%s", buf ); // Get first word = command name. if( stricmp( (char *)buf, "RAMP" ) == 0 && !strchr( (const char*)cmd, '#' )) { for( cp = cmd ; ( ival = strSpanTok( (char *)cp, "UP\0DOWN\0RECOIL", &len )) < INT_MAX ; ) { ival += len; // Add token (UP/DOWN/RECOIL) length to include it in output. fwrite( cp, 1, ival, fp ); fprintf( fp, " #%u ", rampId ); if( ++rampId == RAMP_PERM ) rampId = RAMP_DEBUG; cp += ival; } fputs( (const char *)cp, fp );
To help the txScriptCommand function parse the user's ramp statement, two general-purpose utilities, stritok and strSpanTok, were developed. strSpanTok searches for the first occurrence of a word from a set in a string. TxScriptCommand repeatedly calls it to find each occurrence of "UP", "DOWN", or "RECOIL" in the statement, inserting an explicit ID clause after each instance.
The search needs to be case-insensitive and to not match embedded strings, for example the "up" in "myUpRamp". The standard library function strtok is called to get delimited tokens, i.e. essentially whole words. Strtok has a nasty side effect of modifying the string being searched (by replacing the delimiter after a match with 0). To avoid this, stritok creates a temporary copy of the source string and passes this to strtok. For each token parsed by strtok, stritok calls stricmp for a case-insensitive comparison to the one word from the set passed to it in each call from strSpanTok.
Except for the delimiter list embedded in strSpanTok, these two functions are fairly general. However, the process is not very efficient. The work required is nearly Onm, where O is the search effort, n is the number of instances, and m is the number of words in the match set. A parallel case-insensitive search for all of the key words could accomplish the same result in one pass through the source statement. However, in this situation, speed is not important and the parallel search would be a significantly more complicated program.
cdxdbg-utils.cpp
* Function: stritok * Description: Search a string for case-insensitive token match to a given * string. Token is determined by given delimiter set. Unlike strtok, which is * related (and called here) this does not modify the string being searched. * * Returns: Span (number of characters) to the match or -1 if no match. * Arguments: * - char *str is the string to search for a matching token. * - char *find is the token string to be matched. * - char *delims is the delimiter set. int stritok( char *str, char *find, char *delims ) { char *src; char *tok; src = strdup( str ); tok = strtok( src, delims ); while( tok != 0 && stricmp( tok, find ) != 0 ) tok = strtok( 0, delims ); free( src ); return tok == 0 ? -1 : tok - src; } /**************************************************************************** * Function: strSpanTok * Description: Find the earliest occurance of any one of a set of token words * in a string. This looks for whole words as delimited by space, tab, comma, * plus, and minus. It is case-insensitive. * * Returns: Span (character count) to the first matching word or INT_MAX * if none of the token words occurs in the string. * Indirectly returns the length of the token match. * * Arguments: * - char *str is the string to search. * - char *ssp is the superstring list of token words, e.g. "UP\0DOWN\0". * - int *toklen points to indirect return token match length. *..........................................................................*/ int strSpanTok( char *str, char *ssp, int *toklen ) { static char delims[] = " \t,+-"; int earliest; int idx; earliest = INT_MAX; do { if(( idx = stritok( str, ssp, delims )) >= 0 && idx < earliest ) { earliest = idx; *toklen = strlen( ssp ); } } while(( ssp = getNextSsElem( ssp )) != 0 ); return earliest; }
The analyzer needs to know the ID, length, and values in a ramp. A ramp's name, if it has one, is used only at the script source level. Its association with motors is indicated by ramp messages independently of the ramp's definition. Consequently, the rampdef message only needs to contain the ID, count of elements in the ramp, and the array of 16-bit ramp values (each representing a slot count).
It makes things much easier if we require ramp definitions to fit into a single rampdef message. This means that the element count can be a single byte. With the ID and count consuming three bytes of the maximum 240 byte payload (MAX_CMDAT in fsqapi.h) the maximum number of elements in a ramp is 118. A very slow ramp containing 81 elements was defined to test and demonstrate the new MSM but, considering that a typical ramp contains fewer than 30 step changes, the 118-step limit is reasonable.
fsqapi.h
typedef PACKED struct { UCHAR cnt; USHORT id; /* Ramp ID */ USHORT steps[1]; } RampdefCmd; /* CM_RAMPDEF */
The ramp message assigns from one to five trajectory segments to one motor. The up, down, and recoil segments are indicated by two-byte ramp IDs, the slew rate by a two-byte step width, and the hold by a two-byte delay count. This constitutes such a small amount of data that it would serve little purpose to make the message extensible. Instead, each segment value is one element in the message structure.
To support superposition of trajectory segments, the RampdefCmd structure includes a bit-mapped segment assignment element, assign. If the bit corresponding to a segment is 1, the segment is assigned; if 0, the motor's corresponding segment is unchanged.
fsqapi.h
enum { IDX_UP, IDX_DOWN, IDX_RECOIL, IDX_SLEW, IDX_HOLD, IDX_IDLE }; enum { RAMP_UP = 1, RAMP_DOWN = 2, RAMP_RECOIL = 4, RAMP_SLEW = 8, RAMP_HOLD = 0x10, RAMP_IDLE = 0x20 }; /* RampCmd.assign and PowerCmd.assign * bit masks. These are in the same order as the corresponding elements in * RampCmd and PowerCmd. */ typedef PACKED struct { UCHAR motor; UCHAR assign; /* Bit-mapped segment assignment. */ UCHAR pad; USHORT up; /* Ramp ID. */ USHORT down; /* Ramp ID. */ USHORT recoil; /* Ramp ID */ USHORT slew; /* Slew count. */ USHORT hold; /* Hold count. */ } RampCmd; /* CM_RAMP */
The power command is closely related to the ramp command. As explained in detail in the script language reference [cdsref- Power] this command sets the power, high, medium, low, or off, to any or all of the motor's trajectory segments. The PowerCmd structure conveys information similar to RampCmd, but it uses an array of UCHAR power levels indexed by the segment indices, where RampCmd contains a named USHORT for each segment's ID or value.
fsqapi.h
enum { PWR_CHAR = '0', PWR_HIGH = '0', PWR_MEDIUM, PWR_LOW, PWR_OFF }; /* These can be used as abstract enum or as 3717 motor driver bit patterns with val - PWR_CHAR. */ typedef PACKED struct { UCHAR motor; UCHAR assign; /* Bit-mapped segment assignment. */ UCHAR pow[6]; /* UP, DOWN, RECOIL, SLEW, HOLD, IDLE = PWR_HIGH, PWR_MEDIUM, PWR_LOW, or PWR_OFF. */ } PowerCmd; /* CM_POWER */
The rampdef and ramp statements have such similar syntax and complicated syntax-directed translation that we would like the parser to handle the two as one grammar. Further, to help the script developer understand various context-sensitive syntax errors, we don't want to just report generic "syntax" errors but to explain the problem. These goals are incompatible with the parser generated by BISON/YACC. The compiler's parser is intended for LALR1 context-free languages, which the script language is not. To avoid excessively complicating the parser, the scanner is partially context-sensitive as described in Software Implementation Report 15 topic Configuration Memory Read And Write subtopic Script Compiler [Reports-ScanContext].
It would be possible to use context-sensitive scanning for the ramp and rampdef grammar by having the scanner (specifically the classifySymbol function) look for otherwise undefined symbols in the ramp dictionary, just as it does for commands that manipulate devices defined in the analyzer configuration file. However, this would expose the ramp dictionary to both the scanner and parser, which we would prefer to avoid.
Alternatively, the ideal minimal syntax can be slightly modified to reduce ambiguity in the syntax preceding application-defined symbols. That is, instead of making the scanner deliver unambiguous symbols to the parser, we can design a grammar in which generic symbols' syntatic purposes are defined by the context. This only has to be done in two places. One is in the rampdef syntax. Minimally, a rampdef statement that defines only one ramp (segment) should not have to specify the segment type as up, down, or recoil, because the usage (in either another rampdef or a ramp statement) would normally have to specify this. It would seem reasonable to allow this field to be blank. However, rampdef and ramp share the segment sentential form, and, for the ramp syntax, allowing ramp type to be blank introduces reduce-reduce conflicts.
The other syntax adjustment is in the simple assignment of a pre-defined ramp or trajectory (complete or partial) to a motor. Minimally, we should be able to say, for example, ramp M4 MyRamp. However, to provide friendly error reporting, we want the ramp type to derive a generic symbol as well as any of the correct types, up, slew, down, recoil, and hold. To do this, the parser NFA would go to the state of motorName rampName | rampType, which could be resolved programmatically except that the token after rampName is end (empty production) while the token after rampType can't be end. This can't be resolved even with context-sensitive scanning, but it is easily resolved by requiring some disambiguating element between motor and rampName. An equal sign is a good choice. Thus, the grammar recognizes ramp M4 = MyRamp or ramp M4 up MyRamp but not ramp M4 MyRamp. Although this disambiguating syntax is needed to direct the parsing state machine in the error-free case, if it is left out, we can still generate an error that is reasonably friendly. The following trajectory segment name translator shows in comment how we would like to report a bad segment name. Anything other than UP, DOWN, RECOIL, SLEW, HOLD, or = name, falls into the category of error, which can be built into the parser. We would use this, except that the parser is unable to resume parsing from this state for some unknown reason (recover is possible in other places where this construct is used). Normally, we wouldn't care about recovery because compilation halts on the first error. However, for testing the compiler's handling of syntax errors, we tell it to continue parsing even after fatal errors (pragma off failFatalErrors). To patch around this problem, before the parser accepts the supposed ramp token from the scanner, it assigns PE_RAMPSEG to the global enum parseError. The error production is not defined (it is commented out but left in place to explain what is going on). Consequently, the parser falls out to a more general error, statement => error, where parseError is examined to determine whether a special error message is warranted. If parseError is PE_RAMPSEG at that point, a specific ramp segment error message is generated. This tells the names of segments that are legal, including the special case of = name to assign a trajectory defined by a rampdef statement.
fcomp- fsqyac.y
segType : { parseError = PE_RAMPSEG; } T_UP ... | T_DOWN ... | T_RECOIL ... | T_SLEW ... | T_HOLD ... | '=' T_SYM ... /* | error { fatalError( -1, "\"%s\" illegal. \ * Ramp segments are UP, SLEW, DOWN, RECOIL, and HOLD.\n", yytext ); } This * works correctly, treating any incorrect word, including reserve ones, as a * bad segment name. However, the parser seems unable to recover, which screws * up the syntax.f validation test. This problem doesn't appear with the * badStateName, which also uses embedded error token. The workaround is to let * general error catch the problem but report according to parseError. */ enum { PE_NOTHING, PE_RAMPSEG, PE_WAITSTEPPER } parseError = PE_NOTHING; statement => ...error { /* Syntax (parse) error in one statement. */ ... switch( parseError ) { case PE_RAMPSEG: fatalError( -1, "\"%s\" is not a ramp segment type. \ Types are UP, SLEW, DOWN, RECOIL, HOLD, and = rampname.\n", yytext );
Rampdef command messages are generated from rampdef and may be generated from ramp statements but only indirectly. No command message is generated directly from a rampdef statement while a ramp message is generated from a ramp statement. Rampdef messages can't be generated directly from the command statements because the statements may contain multiple ramp definitions, each requiring a separate rampdef message. The top-level BNF productions for rampdef and ramp are (from fsqyac.y):
command => T_RAMPDEF T_SYM /* ramp name */ rampList
command => T_RAMP T_SYM /* Motor name */ rampList
For both types of statements, a rampdef message is generated for each UP, DOWN, or RECOIL ramp defined in the rampList. The rest of the relevant grammar is:
rampList => rampList rampSeg | rampSeg rampSeg : segType /* UP, SLEW, DOWN, RECOIL, HOLD, or = rampName */ { stepPtr = fmsg.arg.rampdef.steps; } optIdNum /* Forced ramp ID to bypass dictionary during development. */ segDescriptor { switch( rampSegment ) { case '=' : /* Inherit segment(s) from rampdef */ ... case T_SLEW : ... case T_HOLD : ... case T_RECOIL : if( noRecoil ) ... // else fall through to process normal recoil rampdef. default: /* T_UP, T_DOWN, or non-0 T_RECOIL. */ *pRampId = writeRampdef( idNum ); /* Write rampdef message * into fsq file and store ID (returned by writeRampdef in MOTO form) into ramp * command in case this segment definition is a phrase in a ramp statememt * instead of in a rampdef. */ } } ; segType => T_UP | T_DOWN | T_RECOIL | T_SLEW | T_HOLD | '=' T_SYM segDescriptor => segComponentList | /* empty is error */ segComponentList : segComponentList '+' segComponent | segComponent; segComponent : T_UINT optExclusive T_TO T_UINT optExclusive slopeType | stepList /* Ramp list, slew, or hold */ optExclusive : T_EXCLUSIVE { yyval.ui = 0; } | /* */ { yyval.ui = 1; } ; slopeType : T_LINEAR T_UINT optPercent | T_IN T_UINT optSteps stepList : fnum /* First or only step. */ | stepList optComma fnum /* Subsequent step. */
In the code triggered by rampSeg => segType segDescriptor, if the segType is T_UP, T_DOWN, or T_RECOIL, writeRampdef is called to generate a rampdef message based on the ramp segment's description. Whether parsing a rampdef or a ramp statement, this part of the translation process is the same. As explained above, writeRampdef is aware of the differing ID range and script dictionary implications of the two commands, but it distinguishes the two by inspecting the global variable cmdType [RampdefCmdType].
The productions
segDescriptor => segComponentList
segComponentList => segComponentList '+' segComponent | segComponent
make it possible to create a single trajectory segment from multiple components. For example ramp LyseSyringe up 20,25,30,40 + 50 to 300 linear 20% might be used to create a very soft start to avoid fluid splashing at the start of an otherwise linear acceleration. Three kinds of segment components are supported, linear, log, and enumerated. The production segComponent => T_UINT optExclusive T_TO T_UINT optExclusive slopeType handles linear and log ramp components while segComponent => stepList handles the enumerated ramp type as well as the single valued slew and hold segments. Note that the log ramp is generated from the phrase in count steps, for example, ramp M2 up 50 to 250 in 10 steps.
Since each type of ramp component requires a different means of generation, each component in a multi-component segDescriptor is generated as soon as the parser has picked up its complete definition. One way to do this would be to generate each component separately and then splice them all together. That would require some kind of memory allocation for each segment as well as code and time to put the pieces together. A much simpler and more efficient approach is to generate each segment in place in the rampdef message. The key to this is simply writing the steps through a pointer instead of directly into the message. In the syntax-directed translation of rampSeg, after seeing segType (UP, DOWN, etc.) the parser points stepPtr to fmsg.arg.rampdef.steps, the beginning of the step array of the rampdef command message under construction. The first ramp component generator, through stepPtr, write its steps to the beginning of the array. Then it changes stepPtr to point to the next step after the last one that it just wrote. The next generator, also writing through stepPtr, will effectively concatenate its steps to those of the previous component.
ENUMERATED RAMP COMPONENT GENERATOR {}
An enumerated ramp component requires the simplest generator. The script specifies each step. The generator just converts the steps to native step period counts and stores them. Therefore, the generator is effected entirely by syntax-directed translation. The complete grammar for this is:
stepList :
fnum /* First or only step. */ { double count; if( rampSegment == T_HOLD ) { if(( count = motorStepRate * yyvsp[0].dval ) > 0xFFFF ) fatalError( -1, "Hold time %f exceeds %f maximum.\n", yyvsp[0].dval, (double)0xFFFF / (double)motorStepRate ); rampCmd.hold = swapUS( (USHORT)count ); } goto addStep; } | stepList optComma fnum /* Subsequent step. */ { checkNotSlewHold(); /* > 1 step illegal for SLEW or HOLD. */ addStep: if( yyvsp[0].dval == 0.0 ) switch( rampSegment ) { case T_RECOIL : noRecoil = TRUE; break; /* Go record this step even though it makes any * other steps superfluous. The purpose is to count the steps so that a higher * level production can issue a warning if a recoil ramp is defined with a 0 * and more than one step. */ case T_HOLD : break; /* Record for multi-step detection. */ default : reportStep0(); /* Only recoil and hold can have a 0 step. */ } checkStepCount( stepPtr ); *stepPtr++ = swapUS((USHORT)((double)motorStepRate / yyvsp[0].dval + 0.5)); } ;
The first step of any list is picked up by stepList => fnum. SLEW and HOLD are translated as stepList composed of one step. In the first step translation, if the segment type is HOLD, the duration is checked. The maximum hold time, approximately two seconds, is dictated by the fact that steps (including HOLD) are USHORT, which can't exceed 0xFFFF. Placing stepList to the left of fnum in the production stepList => stepList optComma fnum ensures that stepList => fnum is reduced only for the first step of a list. This relegates testing for HOLD value to one production and testing for illegal multiple steps when HOLD or SLEW (checkNotSlewHold) to the other production. Subsequent steps, as well as the first, are all stored through stepPtr by the last statement of the second production. Before storing each step, checkStepCount is called to verify that the 118-step limit would not be violated.
LINEAR AND LOG RAMP GENERATORS {}
Linear and log ramps are generated from parameters in the script statement by, respectively, emitLinearRamp and emitLogRamp. Like the enumerated ramp generator, these functions store the steps, through stepPtr, into the rampdef message under construction.
EmitLogRamp calculates the step change ("delta") by dividing the difference between the start and end steps by the number of steps specified in the statement. Each step duration is the previous step minus (if up ramp) or plus (if down ramp) the delta. This very simple formula makes log ramps popular for systems in which the ramp is generated at run-time, but its performance is not as good as a linear ramp.
Generating a linear ramp requires considerably more effort. In a linear ramp, step deltas are proportional to the step size. Thus, deltas decrease as the motor accelerates, compensating for the loss of torque with increasing speed. While the ramp is linear, acceleration is anti-logarithmic and deceleration is logarithmic. If we simply apply the given gradient to each step in turn, the last calculated step will equal the requested last step only by very unlikely coincidence. To make them equal, we can change the gradient slightly so that the acceleration curve includes the begin and end steps. This could be done deterministically using calculus. However, since the begin and end steps are not points, only an approximation is possible. Further, while the formula might be deterministic, the only way the program can perform calculus is by non-deterministic successive approximation. Thus, there is no truly deterministic, exact approach.
EmitLinearRamp uses a simple approach that doesn't pretend to be deterministic or exact. Starting with the first step, it calculates each next step based on the gradient. For example, if a down ramp step is 250 steps per second (i.e. its duration matches that of steps at 250 SPS) and the gradient is 20%, the next step is 200 steps per second. It continues this way until the calculated step lies beyond the end step. It then compares the error between the last calculated step and the specified end step to the error of the penultimate calculated step. If the penultimate step is closer to the specified step, the gradient will be increased to move it to the specified step. If the last calculated step is closer, the gradient will be decreased to move that step to the specified step. The error term of the closer step is divided by the number of steps in the ramp (which we found by creating each one from the previous-- have no idea how many steps this will be when we start). The result of this calculation is added to or subtracted from the specified gradient, yielding a new gradient which will produce a curve whose first and last steps are identical to the given parameters.
The basic formula used to produce a linear ramp does not produce symmetrical up and down ramps if it is applied from begin to end step, that is from largest to smallest in an up ramp and from smallest to largest in a down ramp. This actually has negligible real effect but it is disconcerting. We would expect, for example, the ramp 50 to 250 linear 20% to be the mirror image of 250 to 50 linear 20%. Another problem with this approach is that gradient limits are not symmetrical. For example, at a 100% gradient, the next step after the largest (i.e. first step of an up ramp) would be 0. In contrast, a 100% gradient starting with the smallest step could produce a reasonable ramp; e.g. 250 to 50 linear 100% would be 250, 125, 62. To avoid these discrepancies, both up and down ramps are generated from smallest to largest step, i.e. both as down ramps. Since the up ramp is generated backwards, we either need to generate it into a temporary array which is then reverse copied into the rampdef array or somehow know the step count before generation and create it in reverse in the rampdef. The latter is easy because we need generating passes anyway. As already described, the first pass calculates each step using the given gradient. There is no reason to save these steps as they serve only as a means to find the last step. After the first pass, we not only have the information we need to adjust the gradient but we also know how many steps will be in the ramp and can use this to calculate the address of the last step. In the second pass, we can then write the up ramp steps starting at the last one and working backwards toward the first.
fcomp- motor.c
* Arguments: * - USHORT start is the starting steps per second. Must not be 0. * - USHORT end is the ending steps per second. Must not be 0. * - USHORT grade is the percentage gradient of the ramp. e.g. 10 = 10% grade. * This is the ratio of change to step and may be larger than 100% because the * algorithm always starts at the ramp end with the smaller steps, i.e. at the * end of an up ramp and at the beginning of a down ramp. The minimum is 1%, * i.e. the change is 1/10 of the step width. The maximum is 1000, i.e. the * change is 100 times the step width. These limits exist only to prevent the * user from defining completely useless ramps. The mechanically useful range * is 5% to 100% * - USHORT incflag tells how to include the start and end rates. bit 1 = 1 * tells to include start, bit 0 = 1 tells to include end. * Globals: stepPtr * - motorStepRate is the control unit's STEP_RATE listed in analyz.ini. The * caller is responsible for selecting the control unit and verifying that * has a defined STEP_RATE. * Side effects: * - emit the ramp as an array of step width (MOTO USHORT) in the motor control * unit's native step rate (nominally 32 usec.) into fmsg.arg.rampdef.cnts * array. This is not written into the fsq file. * * ........................... notes ....................................... * - start and end are in steps per second. For up ramp, start < end, i.e. from * fewer to greater steps per second. For down ramp, start > end. These * relationships are reversed for step (iterator), endStep sentinel and emitted * steps, which are period (nominally 32 usec) counts per step. * * - Each step width is calculated as the previous times a factor, which is * nominally the gradient. To avoid error accumulation, floats are used even * though the output is USHORTs. * To avoid a potentially large error in the last step, the requested * gradient is adjusted slightly to fit exactly (with less than 1% error) * between the two requested endpoints. The step count and delta factor * adjustment are determined in one loop over the range and then used to build * the ramp in another loop. In the first phase, the program steps through the * range, accumulating the step width until this value passes the sentinel If * the overshoot is smaller than the undershoot of the previous, the extra step * is added and the factor is reduced. Otherwise, the previous step count is * taken and the factor is increased. In either case, the factor is adjusted to * spread the over or undershoot evenly accross the entire range. Ideally, the * compensation would be spread proportionately but evenly is cheaper and * yields less than 1% error. * * - Both up and down ramp are created in the same direction for symmetry, for * example so that up 50 to 250 linear 20% yields a ramp that is the mirror * image of down 250 to 50 linear 20%. This would not be the case if the two * ramps were created in the direction that they will execute (larger to * smaller steps in an up ramp but smaller to larger in a down) because the * step change is relative to the current step. Calculating both ramps in the * same direction affords the additional benefit of making the gradient range * symmetrical. For example, a 100% gradient would make the second step of 0 * width if up ramps were calculated from larger to smaller steps, while it * would create a reasonable ramp if we started at the end with smaller steps. * The ramp is calculated starting at the end with the smaller steps, because * this affords the extra benefit of matching the slew rate simply by not * including the first step width in the slope/endpoint adjustment. * *..........................................................................*/ UCHAR emitLinearRamp( USHORT start, USHORT end, USHORT grade, USHORT incflag ) { USHORT *stepDp; int stepDpInc; UCHAR cnt; UCHAR nsteps; USHORT stepUs; double step; double step2; double prevStep; double endStep; double factor; USHORT first; USHORT last; if( grade == 0 || grade > 1000 ) return fatalError( -1, "Illegal ramp gradient. Range is 1 to 1000%.\n" ); stepRate = motorStepRate; // Convert USHORT to double. if( start < end ) // Slower to faster rate is ramp up. { first = end; last = start; } else // Faster to slower rate = down ramp. { first = start; last = end; } factor = 1.0 + (double)grade / 100.0; // Increasing step width. step = stepRate / (double)first; // First step width. endStep = stepRate / (double)last; // Ideal last step width. if(( incflag & 2 ) == 0 ) // Exclude start. if( start < end ) // Up ramp is backward. endStep /= factor; else // Down ramp. step *= factor; if(( incflag & 1 ) == 0 ) // Exclude end. if( start < end ) // Up ramp is backward. step *= factor; else // Down ramp. endStep /= factor; step2 = step; // Save first step width for second loop. prevStep = step; // Don't be confused by silly ramp specification. for( cnt = 0 ; ; cnt++, step *= factor ) { if( step > endStep ) { /* Next step is beyond (larger than) end. */ if( step - endStep < endStep - prevStep ) { /* If overstep is closer to end than previous step then go over and decrease factor to compensate. */ ++cnt; factor *= 1.0 - ( step - endStep ) / ( endStep * cnt ); } else /* Previous step is closer to endStep so use it but increase factor. */ factor *= 1.0 + ( endStep - prevStep ) / ( endStep * cnt ); break; } prevStep = step; } if( start < end ) // Up ramp is stored backward into step array. { stepDp = stepPtr + cnt - 1; stepDpInc = -1; } else // Down ramp is stored forward into step array. { stepDp = stepPtr; stepDpInc = 1; } checkStepCount( stepPtr + cnt - 1 ); stepPtr += cnt; // For subsequent use by writeRampdef. nsteps = cnt; while( cnt-- > 0 ) { stepUs = step2 + 0.5; *stepDp = swapUS( stepUs ); stepDp += stepDpInc; step2 *= factor; } return nsteps; }
RAMP AND RAMPDEF INSPECTION {}
Parametric ramp generation relieves the script writer of having to know the specific steps comprising a ramp. However, there are two situations where this information is useful. One is when the motor is not behaving as expected, for example due to resonance somewhere in mid-ramp. In this case, it may be fruitful to recreate a portion of or the entire ramp in enumerated form and then modify parts of it to see the effect on the motor's behavior. The other situation is when testing and validating the script compiler.
For program validation, we want to see both the definition of ramps in rampdef commands and ramp segment assignments in ramp commands. When the compiler is invoked with the command line option -R, the global variable wantRampList is assigned TRUE.
fcomp- fsqmain.c
int fsqMain(int argc, char **argv) { ... case 'R': // Produce ramp list file. wantRampList = TRUE;
When rampdef and ramp are parsed, before a ramp is processed, prepRampList is called to, among other things, make sure that the ramp list file "RAMP.LST" is open if wantRampList is TRUE and print either "Rampdef" or "Motor" and the ramp or motor name into the file.
fcomp- fsqyac.y
command => T_RAMPDEF ... prepRampList( "Rampdef" ); /* Clear rampCmd and write rampdef name if listing. Rampdef shares rampCmd with ramp command. */ command => T_RAMP ... prepRampList( "Motor" ); /* Clear rampCmd and write motor name if listing. */ void prepRampList( char *thing ) { memset( &rampCmd, 0, sizeof( rampCmd )); if( wantRampList ) { if( rampListFile == 0 && ( rampListFile = fopen( "RAMP.LST", "wt" )) == 0 ) fatalError( -1, "Unable to open RAMP.LST file.\n" ); fprintf( rampListFile, "--- %s %s ---\n", thing, yytext ); }
For a ramp statement, if wantRampList is TRUE, showRampCmd is called to print into the ramp list file a map of the selected segments and the ID number of each segment. The ID is 0 for each unselected segment because prepRampList initialized rampCmd with 0s. For each ramp defined in both ramp and rampdef statements, writeRampdef, whose main task is to write the rampdef message out to the compiled script (FSQ) file, prints the ramp into the list file if that file is open (i.e. wantRampList is TRUE). The duration of each step and the total time consumed by the ramp are listed.
fcomp- motor.c
void showRampCmd( FILE *outf, RampCmd *rcmd ) { fprintf( outf, "Segment map is x%X. \ Up = x%X, Down = x%X, Recoil = x%X, Slew = %u, Hold = %u\n", rcmd->assign, swapUS( rcmd->up ), swapUS( rcmd->down ), swapUS( rcmd->recoil ), swapUS( rcmd->slew ), swapUS( rcmd->hold )); } USHORT writeRampdef( USHORT forceId ) { ... if( rampListFile != 0 ) { duration = 0; for( sp = fmsg.arg.rampdef.steps ; sp < stepPtr ; sp++ ) { step = swapUS( *sp ); duration += step; fprintf( rampListFile, "%u, ", step ); } fprintf( rampListFile, "time=%f.\n", (double)duration / stepRate );
The main purpose of the ramp dictionary is to provide ramp and rampdef command translation with the definition of segments in a ramp referenced by name, for example ramp Motor5 = MyRamp or rampdef HisRamp = MyRamp hold 1.0. In both cases, the referenced ramp (MyRamp) has been defined by a rampdef statement. The dictionary stores only ramps defined by rampdef. For hold and slew segments, the value is stored along with the ramp's name. For up, down, and recoil segments, the ramp ID is stored. As previously described, each permanent up, down, or recoil ramp is assigned an incrementing ID in the range reserved for permanent ramps. This ID is both included in the rampdef message that defines the ramp and saved in the dictionary with the overall ramp name. For example, rampdef MyRamp up 10,15,20 generates a rampdef message containing the three steps plus an ID. The ID is assigned to this segment only, and this segment has no name. The ramp named MyRamp is stored in the dictionary along with the up ramp's ID. If MyRamp is subsequently assigned to a motor or to another ramp (rampdef command) the up ramp segment of that motor or ramp will inherit MyRamp's up ramp ID.
The description of a ramp defined by rampdef comprises its name and its segment definitions, which is the content of a ramp message. Rather than invent a new structure, the RampCmd message structure is used for this purpose in the dictionary. This also simplifies script translation by allowing ramp and rampdef to share code to build a ramp message. For a ramp statement, the message is written to the FSQ file. For rampdef, the RampCmd structure is combined with the ramp name and stored in the dictionary.
The dictionary must be stored in a file, rampdict.dat, so that the information persists through multiple invocations of the compiler. Searching the file for ramp names would be too slow, so an image of the dictionary is kept in memory. Only the names are needed for searching. To save memory, each name is stored in memory with a pointer to the file position of the RampCmd stored with the ramp's name instead of the RampCmd structure itself. Once the name is found, we don't mind accessing the file to retrieve the segment description.
fcomp- motor.c
/* * Ramp dictionary resides in file and in-mem. A hybrid Pascal-C version of the * ramp name resides in both. The file element also contains the RampCmd of the * rampdef, while the in-mem element contains a pointer to the RampCmd in the * file. In-mem dictionary is fpos,lenName0;fpos,lenName0... In-file * dictionary is nextTempId;rampDict;lenName0,RampCmd;lenName0,RampCmd... * In-mem iterator is +sizeof(fpos)+sizeof(lenName0). In-file iterator is fpos * (of RampCmd) + sizeof(RampCmd). */ #pragma pack(1) typedef struct { long fpos; // File position of the RampCmd attached to hybrid Pascal-C name. UCHAR name[1]; // Hybrid Pascal-C name in mem as well as in file. } RampRec; // In-mem dictionary element. struct { USHORT dictSize; USHORT cnt; } rampDict; #pragma pack()
The in-memory ramp dictionary is simply a linear list of RampRec structures. There shouldn't be many permanent ramps and the dictionary should be small if the dictionary is routinely pruned by pragma ClearRamps. Therefore, searching the linear list should not prove much of a time burden. If usage is not as predicted and the dictionary grows larger than expected, we should probably store only a hashed name dictionary in memory and organize the file by hash collision groups. This simultaneously supports a much larger dictionary while affording faster name look up (except for the disk access-- but at least it's bounded).
The name element of each RampRec structure is extended to accommodate the actual length of the ramp's name. The dictionary search iterator needs to take this into account. The run-time burden is not significant but a lot of casting is needed to coerce the C compiler. This is hidden in the macro IterRampRec.
#define IncrPtr( P,I,T ) (P = (T*)((UCHAR*)P + I )) #define IterRampRec(P) IncrPtr( P, sizeof( RampRec ) + (P)->name[0] + 1, RampRec )
typedef union { UCHAR *ucp; RampRec *rp; } PrampRec; PrampRec rampRecs = {0}; // Ramp dictionary in memory begin pointer. PrampRec newRampRec; // Next in-mem dictionary address for writing.
The dictionary is not opened or created until the script compiler encounters a need for it. At that time, if the file exists, it is opened and its in-memory counterpart is initialized, allocating as much memory as the file says is needed (for the names and file pointers) plus extra for any new ramps. RAMPDICT_SIZE_INCR defines this extra. If more ramps are added than this extra can accommodate, the memory is reallocated with enough memory for the ramp that would overflow the dictionary plus, again, the RAMPDICT_SIZE_INCR extra.
size_t rampdefMem = 0; #define RAMPDICT_SIZE_INCR 500
As ramps are defined, they are simultaneously written to the dictionary file and to the in-memory portion of the dictionary. When a script file containing statements that define ramps is successfully compiled, the function saveRampDict is called. This function doesn't save the ramp definitions themselves but rather last temporary ramp ID, as explained earlier, and the rampDict structure, which tells the number of bytes in the in-memory dictionary and the number of ramps in the dictionary. If compilation fails, this function is not called; any ramps that the script file defined are not physically deleted from the dictionary file but they are effectively removed by the fact that the ramp count is not updated.
fcomp- motor.c
void saveRampDict( void ) { if( newTempRamps == TRUE ) { rewind( fpRampDict ); fwrite( &tempRampId, 2, 1, fpRampDict ); } if( newPermRamps == TRUE ) { fseek( fpRampDict, FPOS_RAMPDICT, SEEK_SET ); fwrite( &rampDict, sizeof( rampDict ), 1, fpRampDict ); } }
StartRampdef is called to begin the process of adding a ramp definition to the dictionary, i.e. at the start of translating a rampdef statement. AssignRampDef is called to assign a ramp defined by rampdef to another ramp or to a motor. Both call openRampDict to verify that the dictionary is open and ready for reading and writing. If the dictionary is not open, openRampDict opens it, but the function will not create a new dictionary, instead returning FALSE if the file doesn't exist. This gives the caller a chance to decide how to proceed. In fact, startRampdef will create a new dictionary while assignRampDef treats the lack of a dictionary as a fatal error.
Similarly, both startRampdef and assignRampDef call inRampDict to determine if the supposed ramp name in the statement exists in the dictionary. For startRampdef, a pre-existing name means that the ramp is to be redefined while not finding the name means that a new ramp is being defined. In contrast, for assignRampDef, not finding the name is a fatal error.
After finding the referenced ramp, assignRampDef copies its RampCmd structure (minus motor ID) to the ramp command message under construction and is done with the dictionary. StartRampdef is just the beginning of the involvement of the dictionary in processing the source statement. After the ramp is fully defined, generated, and written to the FSQ file, endRampDef is called to finishing recording the ramp definition by writing its RampCmd structure into the dictionary file.
fcomp- motor.c
* Function: openRampDict * Description: Make sure that the ramp dictionary is open. If already open * just return TRUE. Otherwise try to open it and, if successful, allocate * the initial dictionary memory based on the dictionary byte count stored in * the file (rampDict.dictSize) and initialize the in-mem dictionary by copying * data from the file (with fpos values initialized using ftell). * Returns: TRUE if the dictionary is open, which means that the file is * open and the in-mem dictionary is initialized. * ........................... notes ....................................... * - After processing the last ramp in the file, this seeks to the next, which * positions the file at the next point where a new ramp could be added. An * ftell would provide this value for the next new ramp. Note, however, that * there may be intervening reads, so we can't expect the file itself to remain * at this position. * * - This function reads the file only if it opens the file itself. Therefore, * a preflush in case of prior write is not needed. Whether it reads the file * or not, it doesn't flush. The caller should not assume that either read or * write is safe without a preflush. If we return TRUE because the file was * already open, the previous operation may have been read or write. If we * return TRUE because we opened the file, the last operation will be read. If * we return FALSE, the file isn't open at all. *..........................................................................*/ BOOL openRampDict( void ) { USHORT cnt; RampRec *rp; if( fpRampDict != 0 ) return TRUE; if(( fpRampDict = fopen( RAMP_DICT_FILE, "r+b" )) == 0 ) return FALSE; fread( &tempRampId, 2, 1, fpRampDict ); fread( &rampDict, sizeof( rampDict ), 1, fpRampDict ); permRampId = RAMP_PERM + rampDict.cnt; if( permRampId < RAMP_PERM ) fatalError( -1, "Ramp dictionary overflow. Delete and rebuild %s.\n", RAMP_DICT_FILE ); rampRecs.ucp = malloc( rampdefMem = rampDict.dictSize + RAMPDICT_SIZE_INCR ); checkRampDictMem(); for( cnt = 0, rp = rampRecs.rp ; cnt < rampDict.cnt ; cnt++ ) { rp->name[0] = fgetc( fpRampDict ); fread( rp->name + 1, rp->name[0] + 1, 1, fpRampDict ); rp->fpos = ftell( fpRampDict ); fseek( fpRampDict, sizeof( RampCmd ), SEEK_CUR ); IterRampRec( rp ); } newRampRec.rp = rp; return TRUE; } /**************************************************************************** * Function: inRampDict * Description: Search the ramp dictionary in memory for a match to the given * ramp name. * Returns: Pointer to the matching RampRec if found else NULL. * Arguments: char *name is the rampdef name to be matched. * Side effects: * - If the name is matched, the dictionary file is positioned at the rampdef's * RampCmd structure. If the name is not matched the file is positioned at the * next location where a new rampdef can be written. Either way, the file can * be safely read or written, as it has been flushed by a seek. *..........................................................................*/ RampRec *inRampDict( char *name ) { RampRec *rp; RampRec *lastrp; long fpos; for( rp = rampRecs.rp ; rp < newRampRec.rp ; IterRampRec( rp )) { if( stricmp( name, (char*)(rp + 1 )) == 0 ) { fseek( fpRampDict, rp->fpos, SEEK_SET ); return rp; } lastrp = rp; } /* Leave file at next record in case we are being called from rampdef create * Since this is not necessarily the end of the file, it saves some time to set * up this situation now. If the caller is a rampdef reference, we've failed * anyway so the extra time is immaterial. This also affords the benefit of * having flushed the file through all return paths, so subsequent read or * write are both safe. */ fpos = lastrp->fpos + sizeof( RampCmd ); fseek( fpRampDict, fpos, SEEK_SET ); return 0; } /**************************************************************************** * Function: startRampdef * Description: Add rampdef name to dictionary and prepare to write the rest * of the description after completely translating it. * Returns: Nothing * Arguments: char *name (usually yytext) is the rampdef name. *..........................................................................*/ void startRampdef( char *name ) { int nlen; size_t sizeRec; size_t cnt; RampRec *rp; nlen = strlen( name ); sizeRec = sizeof( RampRec ) + nlen + 1; if( openRampDict()) { rp = inRampDict( name ); if( rp != 0 ) { rampdefFpos = rp->fpos; return; /* All set up for redefinition of existing rampdef. Note that rampDict.cnt is not incremented. */ } } else if(( fpRampDict = fopen( RAMP_DICT_FILE, "w+b" )) != 0 ) { // Initialize rampDict data and the file's copy of it. tempRampId = 0; rampDict.dictSize = 0; rampDict.cnt = 0; fwrite( &tempRampId, 2, 1, fpRampDict ); fwrite( &rampDict, sizeof( rampDict ), 1, fpRampDict ); } else fatalError( -1, "Unable to open or create RAMPDICT.DAT.\n" ); /* New dictionary or existing dictionary that doesn't contain the rampdef * name. In either case, we are adding a new rampdef to the dictionary. If * existing dictionary, inRampDict will have left the dictionary file at the * position of the next record. This is not necessarily the end of the file. If * new dictionary, the position is end of file. inRampDict flushes the file * after reading so write is OK now. */ fputc( nlen, fpRampDict ); fwrite( name, nlen + 1, 1, fpRampDict ); if( rampRecs.ucp == 0 ) { rampRecs.ucp = malloc( RAMPDICT_SIZE_INCR ); rampdefMem = RAMPDICT_SIZE_INCR; newRampRec = rampRecs; } else if( rampRecs.ucp + rampdefMem - newRampRec.ucp < sizeRec ) { cnt = newRampRec.ucp - rampRecs.ucp; rampdefMem += RAMPDICT_SIZE_INCR; rampRecs.ucp = realloc( rampRecs.ucp, rampdefMem ); newRampRec.ucp = rampRecs.ucp + cnt; } checkRampDictMem(); newRampRec.rp->fpos = rampdefFpos = ftell( fpRampDict ); newRampRec.rp->name[0] = nlen; memcpy( newRampRec.rp->name + 1, name, nlen + 1 ); IncrPtr( newRampRec.rp, sizeRec, RampRec ); rampDict.dictSize += sizeRec; rampDict.cnt++; } /**************************************************************************** * Function: endRampdef * Description: Finish adding a rampdef to the dictionary by writing the * rampCmd created by translation. Any ramps (up, down, or recoil) created in * translation were emmitted to the script's FSQ file. All we put in the * dictionary are the segment selector bitmap and the selected segment IDs. * This is essentially a RampCmd minus motor but, for convenience, we write the * complete rampCmd structure. * ........................... notes ....................................... * - Seek to rampdefFpos because constituent rampdefs may move the file * position away from the one under development. * * - Counting the rampdef, by ++rampDict.cnt, is done here instead of in * startRampdef to help error reporting in the special case of rampdef reference * in first rampdef into a new dictionary. It is better, in this case, to * report "no dictionary" than can't find the ramp. * *..........................................................................*/ void endRampdef( void ) { fseek( fpRampDict, rampdefFpos, SEEK_SET ); fwrite( &rampCmd, sizeof( RampCmd ), 1, fpRampDict ); } /**************************************************************************** * Function: assignRampdef * Description: Look up given ramp name in dictionary and assign its active * segments to the rampCmd being built on behalf of either a ramp or rampdef * command (both ramp motor and rampdef can inherit segment definitions from an * existing rampdef). In neither case is a RampCmd message generated that is * directly derived from the RampCmd associated with the referenced rampdef. * However, in the case of ramp statement, a RampCmd message is eventually * generated. It is derived directly from the global rampCmd, which includes * elements from the referenced rampdef unless all of its segments are * overridden (legal but nonsensical). * Arguments: char *rampname is the name of the referenced rampdef. Usually * yytext is passed. * .............. notes .................................................... * - If the first rampdef with a new dictionary contains a rampdef reference, * e.g. rampdef NewRamp = OldRamp hold 1.0, this function will report Rampdef * not in dictionary instead of the more appropriate "no dictionary", because * this first addition to the dictionary is already counted. New rampdefs are * counted in startRampdef instead of endRampdef because it is the former's * responsibility to see if a rampdef overwrites an existing one, in which * case the new definition is not counted. Ramp command in the same situation, * e.g. ramp M2 = OldRamp, will be reported as "no dictionary" because no one * will have started a new dictionary when assignRampdef is called. *..........................................................................*/ void assignRampdef( char *rampname ) { RampCmd rcmd; RampRec *rp; openRampDict(); if( rampDict.cnt == 0 ) fatalError( -1, "No ramp dictionary.\n" ); rp = inRampDict( rampname ); if( rp == 0 ) fatalError( -1, "Rampdef %s not in dictionary.\n", rampname ); fread( &rcmd, sizeof( RampCmd ), 1, fpRampDict ); if( rcmd.assign & RAMP_UP ) rampCmd.up = rcmd.up; ...
TESTING THE RAMP DICTIONARY
To perform these tests, two scripts were written. Ramp1.f begins with pragma ClearRamps. Whenever this script is compiled, any existing dictionary is cleared. The second script, ramp2.f, contains references to permanent scripts defined in ramp1. By compiling both, first ramp1 and then ramp2, with the command line option -R, we can examine the resulting list files to determine whether the compiler meets the requirements.
// ************************************************************************** // Flow Sequence Source File: RAMP1.F // Description: // Script for testing compiler rampdef definition and inheritance. This is // not for execution. Compile with -R and examine ramp.lst. After compiling // this file, compile ramp2.f to test dictionary rampdef and permanent ID // inheritance. pragma ClearRamps // Wipes out dictionary so next permanent ramp ID is E001. begin Ramps unit MSM // Unique rampdef rampdef Ramp1 up 10 to 50 linear 50% \ // up = E001 slew 50 \ down 50 to 10 linear 50% \ // down = E002 hold 0.2 // hold = 6521 // Derived rampdef with one superposed ramp. rampdef Ramp2 = Ramp1 up 10 to 50 linear 20% // up = E003 // Inherit down = E002, hold = 6521 // Derived rampdef from derived rampdef with superposed non-ramp segment rampdef Ramp3 = Ramp2 hold 0.1 // hold = 3260 // Inherit up = E003, down = E002 // Triple-derived rampdef with additional (not superposed) ramp segment rampdef Ramp4 = Ramp3 recoil 10,10 // recoil = E004 // Inherit up = E003, down = E002, hold = 3260 ramp M1 = Ramp1 // up = E001, down = E002, hold = 6521 ramp M2 = Ramp2 // up = E003, down = E002, hold = 6521 ramp M3 = Ramp3 // up = E003, down = E002, hold = 3260 ramp M4 = Ramp4 // up = E003, down = E002, recoil = E004, hold = 3260 ramp M5 = Ramp4 up 50 to 250 linear 30% // up = 1, rest same as Ramp4. end
Compiling ramp1.f, we see first that rampdef Ramp1defines an up segment with ID xE001, a down segment with ID xE002, and slew and hold segments as specified in the statement, confirming requirement 2. If Ramp1 is compiled a second time, we see the same results, confirming that the permanent ramp ID is reset by pragma ClearRamps.
The compilation of rampdef Ramp2 shows that a new up ramp is defined with ID xE003, verifying that the permanent ramp ID continues to increment. The segment map shows that, while Ramp2 inherits Ramp1's slew, down, and hold, its own up ramp replaces that of Ramp1, verifying both inheritance and superposition (of a ramp segment) from one ramp definition to another.
Rampdef Ramp3 serves two purposes, to test indirect inheritance and superposition of a non-ramp segment. The listing shows that Ramp3 inherits Ramp1's down ramp (xE002) and slew and Ramp2's up ramp (xE003). It has its own hold, verifying superposition of a non-ramp segment.
Ramp4 is derived from Ramp3, demonstrating triple inheritance of Ramp1's down and slew. Additionally, Ramp4 defines a new recoil ramp, who's assigned ID is xE004 as expected. This shows that adding a non-superposed recoil ramp doesn't affect the inheritance of other segments.
Motors M1 through M4 are assigned ramps Ramp1 through Ramp4. The listing shows that each motor's segment map exactly matches that of the assigned ramp, verifying that ramp inheritance works for ramp statements.
Motor M5 is assigned Ramp4 and its own up ramp. The listing shows the same trajectory as for M4 but the up ramp is superposed. An up ramp is defined, but its ID is 1, not a next one expected after xE004. This verifies that a temporary ramp is assigned an ID from the temporary non-debug range. Repeated compilation of ramp1.f yields the same listing. However, this alone doesn't confirm that pragma ClearRamps resets the temporary ID because only one of these exists in this script. From it alone, we don't know whether the ID is always 1 or it has been reset. Ramp2.f contains several temporary ramps, which do receive incrementing IDs. After compiling ramp2.f, if ramp1.f is recompiled, the ID is again 1, verifying that the pragma is operating correctly.
ramp.lst from compiling ramp1.f
--- Rampdef RAMP1 --- UP: 3268, 2184, 1460, 976, 652, time=0.261923. DOWN: 652, 976, 1460, 2184, 3268, time=0.261923. Segment map is x1B. Up = xE001, Down = xE002, Recoil = x0, Slew = 652, Hold = 6521 --- Rampdef RAMP2 --- UP: 3269, 2733, 2285, 1910, 1597, 1335, 1116, 933, 780, 652, time=0.509431. Segment map is x1B. Up = xE003, Down = xE002, Recoil = x0, Slew = 652, Hold = 6521 --- Rampdef RAMP3 --- Segment map is x1B. Up = xE003, Down = xE002, Recoil = x0, Slew = 652, Hold = 3260 --- Rampdef RAMP4 --- RECOIL: 3261, 3261, time=0.200031. Segment map is x1F. Up = xE003, Down = xE002, Recoil = xE004, Slew = 652, Hold = 3260 --- Motor M1 --- Segment map is x1B. Up = xE001, Down = xE002, Recoil = x0, Slew = 652, Hold = 6521 --- Motor M2 --- Segment map is x1B. Up = xE003, Down = xE002, Recoil = x0, Slew = 652, Hold = 6521 --- Motor M3 --- Segment map is x1B. Up = xE003, Down = xE002, Recoil = x0, Slew = 652, Hold = 3260 --- Motor M4 --- Segment map is x1F. Up = xE003, Down = xE002, Recoil = xE004, Slew = 652, Hold = 3260 --- Motor M5 --- UP: 648, 496, 380, 291, 223, 170, 130, time=0.071707. Segment map is x1F. Up = x1, Down = xE002, Recoil = xE004, Slew = 652, Hold = 3260
Compiling ramp2.f after compiling ramp1.f confirms persistence of both permanent ramps and the temporary ramp ID across multiple invocations of the compiler.
// ************************************************************************** // Flow Sequence Source File: RAMP2.F // // Description: // Script for testing compiler rampdef definition and inheritance. This is // not for execution. Compile with -R and examine ramp.lst // This does not clear the ramp dictionary and should, therefore, inherit both // previously defined ramps and the next permanent ramp ID. Ramp1.f should be // compiled first followed by this file. // // If this is compiled repeatedly, the permanent ramp IDs won't change but // the temporaries will steadily increase. i.e. See 2 and 3 the first time, // then 3 and 4 the second, etc. // ************************************************************************** begin Ramps unit MSM // Verify that dictionary is opened for temporary ramp ID. Temporary ramp ID // 1 was created when ramp1.f was compiled. ramp M6 = Ramp4 up 50 to 250 linear 30% // up = 2 // Inherit the rest of Ramp4: down = E002, recoil = E004, hold = 3260 // Unique rampdef rampdef Ramp5 up 10 to 50 linear 10% \ // up = E005 slew 50 \ down 50 to 10 linear 20% // down = E006 ramp M4 = Ramp4 // up = E003, down = E002, recoil = E004, hold = 3260 ramp M5 = Ramp5 // up = E005, down = E006, recoil = 0, hold = 0 // Verify second temporary ramp in the same file. ramp M6 = Ramp4 down 250 to 50 linear 30% // down = 3 // Inherit the rest of Ramp4: up = E003, recoil = E004, hold = 3260 end
ramp.lst from compiling ramp2.f
--- Motor M6 --- UP: 648, 496, 380, 291, 223, 170, 130, time=0.071707. Segment map is x1F. Up = x2, Down = xE002, Recoil = xE004, Slew = 652, Hold = 3260 --- Rampdef RAMP5 --- UP: 3262, 2967, 2699, 2455, 2234, 2032, 1848, 1681, 1529, 1391, 1265, 1151, 1047, 952, 866, 788, 717, 652, time=0.905873. DOWN: 652, 780, 933, 1116, 1335, 1597, 1910, 2285, 2733, 3269, time=0.509431. Segment map is xB. Up = xE005, Down = xE006, Recoil = x0, Slew = 652, Hold = 0 --- Motor M4 --- Segment map is x1F. Up = xE003, Down = xE002, Recoil = xE004, Slew = 652, Hold = 3260 --- Motor M5 --- Segment map is xB. Up = xE005, Down = xE006, Recoil = x0, Slew = 652, Hold = 0 --- Motor M6 --- DOWN: 130, 170, 223, 291, 380, 496, 648, time=0.071707. Segment map is x1F. Up = xE003, Down = x3, Recoil = xE004, Slew = 652, Hold = 3260
PERMANENT RAMP SCRIPT SELF-DELETION {}
A rampdef command message can be relatively large, as it contains a USHORT for each step in the ramp. When the motor controller executes one of these messages, it copies the steps into the ramp heap. At that point, there are two copies of the ramp in memory, one in the script memory and the other in the ramp heap. Since there is no need to execute a rampdef a second time unless the motor controller is reinitialized, there is no need to keep a script in memory if it contains only rampdefs.
That scripts can delete themselves to free up memory is confirmed elsewhere. To verify the specific case that a ramp-defining script can delete itself without affecting the ramps that it put into the ramp heap, the following script was tested. After compiling, the script was downloaded and executed. When I tried again to execute it (without downloading it again) the MSM reported that the script didn't exist, confirming that it had deleted itself. I then submitted the command RAMP M2 = RAMP50 followed by MOVE M2 + 300. M2 moved according to the trajectory defined by Ramp50 in the script.
ramps.f
pragma ClearRamps begin Ramps unit MSM rampdef Ramp50 up 10 to 50 linear 50% slew 50 \ down 50 to 10 linear 50% hold 0.2 rampdef Ramp60 up 10 to 60 linear 50% slew 60 \ down 60 to 10 linear 50% hold 0.2 ... delete Ramps end
The ramp heap is a static UCHAR array, rampMem. It typically comprises 8000 bytes, which may be decreased for testing or increased to support more resident ramps. The motor controller (IML unit MSM) stores ramps in the heap in the form of RampDesc structures, which contain the ramp's ID, step count, and an extensible array of USHORT steps. RampDesc also contains a pointer to the next RampDesc in order to form a linked list within the heap.
msmapp- stpmtr.c
typedef struct RampDesc { struct RampDesc *next; USHORT id; /* 0 = dead. 1 to RAMP_DEBUG - 1 = script temp. RAMP_DEBUG to RAMP_PERM - 1 = interactive temp. RAMP_PERM to FFFF = permanent. */ UCHAR stepCnt; USHORT steps[1]; } RampDesc; RampDesc *lastRamp; /* = 0 in prepSteppers. */ UCHAR rampMem[ 8000 ]; /* This could also be allocated as needed. */
The basic task of procRampdef, the rampdef message processor, is to store the data from the message in a RampDesc in the heap. Usually this entails adding a new RampDesc to the end of the heap (within the limits of rampMem). However, if the ramp's ID and step count match a ramp already in the heap then the existing RampDesc is reused, overwriting the previous ramp. If the ramp's ID matches that of a ramp already in the heap but their counts don't match, a new RampDesc is appended to the heap and the existing ramp is "killed", which marks it for disposal. As explained earlier, motor descriptors contains RampRefs, which contain a pointer to a ramp [RampRef]. All motors' RampRefs that refer to a replaced ramp (except one replaced in place) must be updated to point to the replacement.
ProcRampdef determines whether there is enough room in the heap by calculating what would be the address of the next ramp after the ramp to be stored. This is simply the current store address (lastRamp->next) plus the size of the extended RampDesc, which is the size of RampDesc structure plus the steps, i.e. six bytes plus two for each step. If this next address lies beyond rampMem, there isn't enough room, in which case compactHeap is called and the calculation is repeated using the new store address.
msmapp- stpmtr.c
/**************************************************************************** * Function: procRampdef * Description: CM_RAMPDEF command interpreter. Store a ramp (UP, DOWN, or * RECOIL) in the ramp heap. * ........................... notes ....................................... * - Duplicate ID. In most cases, if the new ramp's ID matches that of a ramp * that is already stored and they have the same length, they are identical. * However, rather than verifying this, it is cheaper to simply write the new * ramp's step array over the old one's and this takes care of the rare cases * where the ramp has changed but not its step count. * When a new ramp replaces an old one (i.e. same ID) of a different size * (step count), the new one is stored in the next available position in the * ramp heap and all motor references to the old one are redirected to its * replacement. The replacement process has to wait until the new ramp has been * stored because the ramp heap may be full, in which case, it is compacted, * resulting in an unpredicatable address for the new ramp. Once this address * is known, the old references can be redirected. The old ramp is then killed * (id = 0) even if it is "permanent". * * - Ramp heap compaction is done only when there isn't enough room to store * an incoming ramp. Waiting until the last possible moment may reduce or * eliminate the need for compaction and affords an opportunity for increasing * efficiency by moving each ramp fewer times. *..........................................................................*/ int procRampdef( FsqMsg *cmd, ProcDef *pd ) { Stepper *mp; RampDesc *rp; RampDesc *oldRamp; UCHAR *next; USHORT stepMemCnt; USHORT id; stepMemCnt = cmd->arg.rampdef.cnt * 2; id = cmd->arg.rampdef.id; if( lastRamp == 0 ) { rp = (RampDesc *)rampMem; oldRamp = 0; } else { /* If a ramp with this ID is already stored then if it is the same length * then replace its step array. Otherwise, put this new ramp at the end, * redirect all references from the old to the new, and mark the new as dead * (ID = 0). Put off ramp heap compaction until we run out of space. */ if(( oldRamp = findRamp( id )) != 0 ) { if( oldRamp->stepCnt == cmd->arg.rampdef.cnt ) { memcpy( oldRamp->steps, cmd->arg.rampdef.steps, stepMemCnt ); return CMD_DONE; } /* Redirect old ramp references only after storing the replacement. We can't * do this immediately because the ramp heap may have to be compacted, making * the new ramp's location uncertain at this point. However, the old ramp can * be killed now. If the ramp heap is compacted, this old ramp will be wiped * out. However, the oldRamp address and the address of its elements will still * be valid, making it possible to scan the steppers for references to its * embedded steps array. */ oldRamp->id = 0; /* Kill it. */ } rp = lastRamp->next; } next = (UCHAR*)rp + stepMemCnt + sizeof( RampDesc ) - 2; if( next > BEYOND( rampMem )) { rp = compactRampHeap(); next = (UCHAR*)rp + stepMemCnt + sizeof( RampDesc ) - 2; if( next > BEYOND( rampMem )) return sendScriptFailMsg( pd, FAIL_NOTREADY, FLINE, "Insufficient ramp memory" ); } lastRamp = rp; rp->next = (RampDesc *)next; rp->id = id; rp->stepCnt = cmd->arg.rampdef.cnt; memcpy( rp->steps, cmd->arg.rampdef.steps, stepMemCnt ); /* Now that the new ramp's location is known, if it replaces an old ramp, we * can redirect the old ramp references. If ramp heap was compacted, the old * ramp no longer exists, but its address and element addresses are still * valid, which is all that redirectRamp requires. */ if( oldRamp != 0 ) findRampRefs( oldRamp, rp ); /* Redirect oldRamp references to rp. */ return CMD_DONE; } /**************************************************************************** * Function: findRamp * Description: Search ramp heap for the requested ramp. * Returns: Pointer to RampDesc or 0. * Arguments: USHORT id is the requested ramp's ID. *..........................................................................*/ RampDesc *findRamp( USHORT id ) { RampDesc *rp; for( rp = (RampDesc*)rampMem ; rp <= lastRamp ; rp = rp->next ) if( rp->id == id ) return rp; return 0; } /**************************************************************************** * Function: findRampRefs * Description: This function serves two different purposes. For both, it * searches the stepper list, looking for references to the oldRamp argument. * In one use, which is requested by a non-0 newRamp argument, every reference * to the oldRamp is replaced with the newRamp. This is used when a ramp * replaces one that has the same ID. In the other use, which is requested by * newRamp = 0, we just want to know whether any motors refer to oldRamp. In * this case, the number of references is counted and returned. This is used * when oldRamp's attachment to a motor is severed by a new ramp attachment. * Then oldRamp is killed if it is not attached to any motor. Returning the * count of references affords the caller the option of whether to sever the * connection before or after calling this function. If after then count = 1 * means that the only connection is the one about to be severed. * * Returns: Count of motor references to oldRamp. This is intended for * use only when the caller just wants to know if oldRamp is referenced * although a caller that wants to redirect oldRamp references to newRamp * might be interested in whether there were any such references. * * Arguments: * - RampDesc *oldRamp is the ramp that we want to find references to (in motor * descriptors). * - RampDesc *newRamp is the replacement ramp or NULL if we only want to count * the references to oldRamp. * * ........................... notes ....................................... * - Only addresses of oldRamp can be used. Derefencing is unreliable because * the old ramp may no longer exist. Since we are only interested in whether * the RampRef points to the old ramp's embedded steps array, we can safely * check references even if the old ramp has been removed by heap compaction. * * - The speed of the replacement process could be improved by recording a * reference count in each RampDesc. Then this function could decrement * oldRamp->refCnt at each match and stop when the count reaches 0. Each time * the ramp is assigned to a motor, its reference count is increased. This is * complicated by reassignment to the same ramp (identical or replacement), * which can occur under several scenarios. The only general solution would be * to record the ramp ID in each of the motor's RampRefs (up, down, and recoil) * and increment the ramps refCnt only for change. Since ramp replacement is * rare, the added complexity isn't worth the speedup gain. * * - oldRamp and newRamp may have the same value when this is called to * redirect references. In this case, newRamp has replaced oldRamp in the heap * (newRamp may or may not be a replacemnt for oldRamp) and the only thing that * might be changed in the reference is the step count. *..........................................................................*/ USHORT findRampRefs( RampDesc *oldRamp, RampDesc *newRamp ) { Stepper *mp; RampRef *rrp; USHORT count; for( count = 0, mp = &steppers[0] ; mp < BEYOND( steppers ) ; mp++ ) { if( mp->hasRamp != 0 ) for( rrp = mp->perRamp ; rrp < BEYOND( mp->perRamp ) ; rrp++ ) if( rrp->src == oldRamp->steps ) { count++; if( newRamp != 0 ) { rrp->src = newRamp->steps; rrp->cnt = newRamp->stepCnt; } } } return count; } /**************************************************************************** * Function: compactRampHeap * Description: Compact the ramp heap by removing dead ramps. * Returns: The next ramp position after the lastRamp (new lastRamp if * compaction occurred) if any ramps remain in the heap. If all ramps were * dead and therefore removed, the address of the beginning of the ramp heap * is returned. * - lastRamp is changed if one or more ramps are delete. If all are deleted, * lastRamp is changed to 0. * - perRamp.src of various steppers may be redirected. * ........................... notes ....................................... * - This removes only ramps that have already been killed. It can't remove * ramps that are simply unreferenced even if they are temporary and, * therefore, can't be referenced because they may be a new ones defined by * rampdefs associated with a ramp message; they will be referenced as soon as * we finish storing the ramps associated with that ramp message and then * process the ramp message. To avoid filling the heap with unused ramps, * attachRamp kills ramps that are referenced by only one motor when that * motor's ramp reference changes. This is the only way that a temporary ramp * can become unused so there won't be any leakage. * * - This removes dead ramps from memory by iterating one ramp pointer, * oldRamp, over all ramps. Meanwhile, a second pointer, newRamp, tells the * destination for moving each live ramp to squeeze out dead ones. When oldRamp * points to a dead ramp, newRamp is not advanced even while oldRamp does. * When oldRamp points to a live ramp, the ramp is copied to newRamp, wiping * out any earlier dead ramps (because newRamp wasn't advancing). newRamp * advances to the next position after each live one is moved so the live ones * are not subsequently wiped out. *..........................................................................*/ RampDesc *compactRampHeap( void ) { RampDesc *newLastRamp; RampDesc *newRamp; RampDesc *oldRamp; USHORT rdCnt; newLastRamp = 0; newRamp = oldRamp = (RampDesc*)rampMem; for( ; oldRamp <= lastRamp ; oldRamp = oldRamp->next ) if( oldRamp->id != 0 ) { /* This ramp is not dead. */ if( newRamp != oldRamp ) /* Don't waste effort if no dead ones yet. */ { findRampRefs( oldRamp, newRamp ); /* Redirect oldRamp references to newRamp. */ rdCnt = sizeof( RampDesc ) - 2 + oldRamp->stepCnt * 2; memmove( newRamp, oldRamp, rdCnt ); newRamp->next = (RampDesc*)( (UCHAR*)newRamp + rdCnt ); } newLastRamp = newRamp; newRamp = newRamp->next; /* Step over the ramp to protect it. */ } lastRamp = newLastRamp; /* 0 or moved ramp. */ return newLastRamp == 0 ? (RampDesc*)rampMem : newLastRamp->next; } /**************************************************************************** * Function: attachRamp * Description: Attach a ramp (stored in ramp heap) to a motor's ramp * reference, i.e. to its perRamp[ RIDX_UP, RIDX_DOWN, or RIDX_RECOIL ]. If * the ramp reference is already attached to a ramp that is not "permanent" (ID * >= RAMP_PERM) and that is not attached to any other ramp reference (from * either this same motor, which is unlikely, or from another motor) then kill * that ramp by changing its ID to 0. At the next heap compaction that ramp * will be deleted. * * Returns: TRUE if the attachment is successful, including a superfluous * reattachment of the same ramp (which causes an immediate return). FALSE if * the requested ramp doesn't exist. * * Arguments: * - RampRef *rrp is one of a motor's three (MS_UPRAMP, MS_DOWNRAMP, MS_RECOIL) * ramp references. * - USHORT id is the requested ramp ID. * - ProcDef *pd is used only to report an error if the ramp doesn't exist. * ............. notes ..................................................... * - RampRef.src does not point to the RampDesc but to RampDesc.steps. This is * to allow it to reference static ramps, which are just embedded USHORT * arrays, as well as the dynamic ramps, which are stored as RampDesc structs * in the ramp heap. * * - This guards against superfluous reattachment of the same ramp, which * wastes time and risks having that ramp killed for having only one reference * which is being replaced. We might expect the caller to be smarter than this * but when a ramp is replaced (i.e. new definition with same ID) all of the * old ramp's references are redirected to the new ramp. The reassignment has * thus already been done if the new ramp is then assigned to the same motor. * The rampdef function can't predict that this is going to happen, although it * usually does, so all old references have to be redirected; and the ramp * (assignment) function also can't know about the replacement without * complicated intra-message communication. However, it is simple to detect and * block superfluous reattachment in attachRamp. * * - Permanent ramps (ID >= RAMP_PERM) are not killed when they are not * referenced. They can only be killed (and deleted) when they are replaced by * a different ramp definition of the same ID. *..........................................................................*/ BOOL attachRamp( RampRef *rrp, USHORT id, ProcDef *pd ) { RampDesc *rdp; RampDesc *oldRamp; rdp = findRamp( id ); if( rdp == 0 ) { sendScriptFailMsg( pd, FAIL_NOTREADY, FLINE, "Reference to nonexistent ramp %u", id ); return FALSE; } if( rrp->src == rdp->steps ) return TRUE; /* Superfluous reattachment of the same ramp. */ /* If this RampRef already is attached to a ramp that is not permanent then * search for any other references to that ramp. If none then kill the old * ramp. Unlike some of the callers to findRampRefs, here oldRamp is known to * point to a valid RampDesc because all heap manipulation is done by the time * procRamp calls this function. */ if( (void*)rrp->src >= rampMem && rrp->src <= lastRamp->steps ) { oldRamp = (RampDesc*)((UCHAR*)rrp->src - OFFSETOF( RampDesc, steps )); if( oldRamp->id < RAMP_PERM && findRampRefs( oldRamp, 0 ) == 1 ) oldRamp->id = 0; /* Kill it */ } rrp->cnt = rdp->stepCnt; rrp->src = rdp->steps; return TRUE; } /**************************************************************************** * Function: procRamp * Description: CM_RAMP command interpreter. Assign one or more ramp segments * to a motor. SLEW and HOLD segments are embedded in the motor. UP, DOWN, and * RECOIL are referred to ramps stored in the ramp heap by rampdef commands. * ........................... notes ....................................... * - HOLD = 0 removes HOLD from after downramp and recoil. Since after recoil * can only be HOLD or IDLE, we just set it to IDLE. For downramp, first check * for after downramp being HOLD before changing. If it is RECOIL, it isn't * changed; if IDLE then changing to IDLE would be superfluous. * * - If RECOIL segment and the ramp ID is 0 it means to remove RECOIL from * after downramp. If after downramp is not RECOIL then do nothing, i.e. let * after downramp remain HOLD or IDLE. Otherwise, change RECOIL to IDLE. This * doesn't necessarily mean that any ramp statement that removes recoil forces * after downramp to IDLE, because a HOLD segment in the statement will change * after downramp (as well as after recoil) to HOLD, which is not changed by * null RECOIL. *..........................................................................*/ int procRamp( FsqMsg *cmd, ProcDef *pd ) { Stepper *mp; UCHAR assign; USHORT *ramp; mp = &steppers[ cmd->arg.ramp.motor ]; mp->hasRamp = TRUE; assign = cmd->arg.ramp.assign; if( assign & RAMP_SLEW ) mp->slewStep = cmd->arg.ramp.slew; if( assign & RAMP_HOLD ) { if( cmd->arg.ramp.hold == 0 ) { mp->perNext[ MS_RECOIL ] = MS_IDLE; if( mp->perNext[ MS_DOWNRAMP ] == MS_HOLD ) mp->perNext[ MS_DOWNRAMP ] = MS_IDLE; } else { mp->holdStep = cmd->arg.ramp.hold; mp->perNext[ MS_DOWNRAMP ] = MS_HOLD; mp->perNext[ MS_RECOIL ] = MS_HOLD; } } if( assign & RAMP_UP ) attachRamp( mp->perRamp + RIDX_UP, cmd->arg.ramp.up, pd ); if( assign & RAMP_DOWN ) attachRamp( mp->perRamp + RIDX_DOWN, cmd->arg.ramp.down, pd ); if( assign & RAMP_RECOIL ) { if( cmd->arg.ramp.recoil == 0 ) { if( mp->perNext[ MS_DOWNRAMP ] == MS_RECOIL ) mp->perNext[ MS_DOWNRAMP ] = MS_IDLE; } else { if( attachRamp( mp->perRamp + RIDX_RECOIL, cmd->arg.ramp.recoil, pd )) mp->perNext[ MS_DOWNRAMP ] = MS_RECOIL; } } return CMD_DONE; }
TESTING RAMP HEAP GARBAGE COLLECTION
The trickiest part of managing the heap is garbage collection. We not only have to remove killed ramps and move live ones to compact the space but also must preserve motor-ramp relationships. Two tests were devised. In both of these, the size of rampMem is reduced in order to force garbage collection to engage sooner than normal. This speeds up the test, which is helpful if it is being traced with a debugger (68340 BDM).
Tramp.f test garbage collection with a mix of permanent and temporary ramps. Two permanent ramps, the up and down ramps associated with MyRamp, defined by rampdef, are loaded as well as three temporary ramps. The two permanent ramps consume 172 bytes. The temporary ramps consume 62, 66, and 52 bytes. Total consumption is 330 bytes after the second temporary, so there isn't enough memory for the third temporary. Garbage collection is engaged. Tracing the program with the BDM debugger reveals that the two permanent ramps are not removed. The first temporary is removed but not the second. The reason for this is that procRampef calls compactRampHeap to free memory for storing the third temporary ramp. The second ramp remains attached to the motor until the ramp command is processed, but this can't happen until the heap has been compacted and the third ramp stored. Deleting the first temporary ramp frees up 92 bytes so, instead of 382 total bytes, the heap now uses 290 bytes. The next temporary ramp requires only 44, so it is stored without further garbage collection.
After executing the script, the command statement MOVE M2 + 200 was submitted. The 10% gradient ramp, which was the last temporary, was observed. RAMP M2 = MYRAMP followed by MOVE M2 + 200 were submitted to verify that the permanent ramps were still resident. M2 used MYRAMP. The script was executed again and MOVE M2 + 200 submitted. The motor used the temporary 10% up ramp but the slew and down ramp from MYRAMP, verifying correct functioning of superposition in the presence of garbage collection.
The commented out statement assigning a temporary down ramp to M2 was uncommented and the script compiled. The script executed once without problem but the MSM report insufficient heap memory on a second invocation. This is as expected because the down ramp remained attached to M2, as only the up ramp is replaced by the preceding ramp statements.
tramp.f
/* Test ramp heap garbage collection with mix of permanent and temporary ramps. Recompile msmapp- stpmtr.c with rampMem[ 350 ] instead of standard 8000. */ begin tramp unit MSM // Permanent up ramp consumes 92+8=100 (x64), down 64+8=72 (x48). Total = 172. rampdef MyRamp UP 50 TO 300 linear 4% SLEW 300 DOWN 300 TO 50 linear 6% ramp M2 up 50 to 250 linear 4% // size 84+8=92 (x5C). Total = 264. ramp M2 up 50 to 250 linear 6% // size 58+8=66 (x42). Total = 330. ramp M2 up 50 to 250 linear 8% // size 44+8=52 (x34). Total = 382 - 92. /* Garbage collection engaged. Ramps E002 (MyRamp UP) and E003 (MyRamp DOWN) are skipped (not removed). Killed temporary ramps have ID = 0. They are squeezed out of the heap. */ ramp M2 up 50 to 250 linear 10% // size 36+8=44 (x2C). //ramp M2 slew 250 down 250 to 50 linear 10% end
The second script tests the replacement of a temporary script. Several different versions of a temporary up ramp are stored. Each is given the same forced ID. The size of rampMem was set to 200. Execution of the script was traced with the BDM debugger. 200 bytes affords only enough room for one ramp at a time yet, unlike the previous script, each ramp was able to replace the immediately preceding one. The reason for this lies in the behavior of procRampdef. If the ID of a new ramp matches an existing one and the two are of unequal length, the existing one is immediately killed (by setting its ID equal to 0). When procRampdef calls compactRampHeap, the killed ramp is removed, freeing up enough memory for the new ramp.
begin tramp1 unit MSM ramp M5 up #10 50 to 250 linear 2% ramp M5 up #10 50 to 250 linear 4% ramp M5 up #10 50 to 250 linear 6% ramp M5 up #10 50 to 250 linear 8% ramp M5 up #10 50 to 250 linear 10% ramp M5 slew 250 down 250 to 50 linear 10% end
The RSH demo exposed an error in the MSM's ramp replacement means. In the demo script, one rack (carrier) is picked up from a tray, cycled through the mixer, and returned to the tray. This is done twice. When the script was repeated, motor control was lost in the repeat cycle.
The RSH demo script is unusual in that it doesn't simply assign a ramp to each motor but repeatedly changes several motors' ramps. The X-axis motor (TransX) has four different ramps in order to coordinate moves with Theta; the Mixer has three ramps; and the RackVertical motor has two. Two ways are provided for defining ramps. A ramp can be defined and assigned to a motor in a single ramp command, e.g. ramp TransX up 100 to 200 linear 10% slew 200 down 200 to 100 linear 10%, or the ramp may be defined by a rampdef command and subsequently assigned to any motor, e.g. rampdef TransXramp up 200 to 500 linear 5% slew 500 down 500 to 200 linear 5% followed by ramp TransX = TransXramp. As explained in the Script Language Reference [cdsref- Rampdef] rampdef is preferred because it results in greater run-time efficiency. Using ramp definition plus assignment creates temporary ramps that the MSM must repeatedly remove from the ramp heap in order to make room for replacement. Compacting the ramp heap is very expensive in terms of CPU time. The definition plus assignment facility is intended for ramp development and diagnostics rather than normal operation. However, because it is more convenient to use than separate definition and assignment, we have tended to use it. In fact all of the scripts currently on the EP1 instruments use ramp definition plus assignment. In those scripts, as well as for most of the ramps used in the RSH demo, the ramps are actually defined as macros. In the EP1 instruments, all of the ramps are defined in a DEFINE section of analyz.ini, for example:
DEFINE
Ramp50 = up 10 to 50 linear 50% slew 50 down 50 to 10 linear 50% hold 0.2
In the RSH demo, the ramps were defined in define statements, for example:
define TransXramp up 200 to 500 linear 5% slew 500 down 500 to 200 linear 5%
Macro ramp definitions afford a source-level convenience. When they are assigned to a motor, the resulting command messages are identical to any other ramp definition plus assignment and suffer the same run-time penalty. We should be using rampdef commands instead of macro ramp definitions. Nevertheless, the RSH demo script should not have caused the loss of motor control. If the problem were that the MSM had been unable to rebuild the ramp heap quickly enough to keep up with the script then it should have at least reported an error. However, this was not the problem. The problem actually was a simple error in the heap compaction code.
The stepper controller does not discard ramps or compact the ramp heap until it finds that the heap doesn't contain enough free space to hold a new ramp definition. At that point it calls the compactRampHeap function. The RSH demo reached this point in the second cycle of the second execution of the script. CompactRampHeap steps through the heap, which is a linked list, looking for temporary ramps that are no longer assigned to any motor. It removes each one of these by moving the subsequent heap contents over it. This changes the addresses of all of the subsequent ramps. The motor controller locates the ramps for a motor by addresses stored in the motor descriptor. To update these references, compactRampHeap calls findRampRefs. It was doing this before moving the heap. Consequently, the new ramp address passed to findRampRefs pointed to the ramp about be discarded instead of the one that would eventually replace it. The address would be correct for the new ramp but the contents at that address described the ramp to be discarded. This would not have been a problem if findRampRefs only served compactRampHeap, which only requires updating a motor's ramp pointers. However, this function is also called to assign a new ramp to a motor, in which case the number of steps in the ramp is copied from the ramp descriptor into the motor's descriptor for faster ISR execution. When compactRampHeap called findRampRefs, the number of steps in the ramp to be discarded was copied instead of the number of steps in the ramp being moved. This was corrected by performing the move before calling findRampRefs.
The RSH demo was repeatedly executed to verify this correction. Then the ramp macros were changed to rampdefs, in keeping with the recommended practice.
msmapp- stpmtr.c
original
RampDesc *compactRampHeap( void ) { ... newRamp = oldRamp = (RampDesc*)rampMem; for( ; oldRamp <= lastRamp ; oldRamp = oldRamp->next ) if( oldRamp->id != 0 ) { /* This ramp is not dead. */ if( newRamp != oldRamp ) /* Don't waste effort if no dead ones yet. */ { findRampRefs( oldRamp, newRamp ); /* Redirect oldRamp references to newRamp. */ rdCnt = sizeof( RampDesc ) - 2 + oldRamp->stepCnt * 2; memmove( newRamp, oldRamp, rdCnt ); newRamp->next = (RampDesc*)( (UCHAR*)newRamp + rdCnt ); USHORT findRampRefs( RampDesc *oldRamp, RampDesc *newRamp ) { ... for( count = 0, mp = &steppers[0] ; mp < BEYOND( steppers ) ; mp++ ) { if( mp->hasRamp != 0 ) for( rrp = mp->perRamp ; rrp < BEYOND( mp->perRamp ) ; rrp++ ) if( rrp->src == oldRamp->steps ) { count++; if( newRamp != 0 ) { rrp->src = newRamp->steps; rrp->cnt = newRamp->stepCnt;
Corrected
RampDesc *compactRampHeap( void ) { ... if( oldRamp->id != 0 ) { /* This ramp is not dead. */ if( newRamp != oldRamp ) /* Don't waste effort if no dead ones yet. */ { rdCnt = sizeof( RampDesc ) - 2 + oldRamp->stepCnt * 2; memmove( newRamp, oldRamp, rdCnt ); newRamp->next = (RampDesc*)( (UCHAR*)newRamp + rdCnt ); findRampRefs( oldRamp, newRamp ); /* Redirect oldRamp references to newRamp.
DESIGN
As explained in the Script Language Reference [cdsref- Acceleration] a linear slope generally affords the best stepper motor ramp. In fact, there is rarely any reason to use a different slope. During ramp development, we typically interactively (through the debugger's command field) try many different ramps with a particular motor to find the best one, for example ramp theta up 50 to 600 linear 15%. Since linear is nearly always the slope, it becomes tedious to have to repeatedly type it. A shorthand ramp description we frequently use in documenting our experiments lists just the starting and ending steps per second and the slope, for example 50-600@15%. The script language has been changed to accept @ as an alias of linear in this context, for example ramp theta up 50 to 600 @ 15%. This also prepares the language to accept exponential ramps, which are described later in this report.
IMPLEMENTATION
fcomp- fsqyac.y
slopeType : linearSlope fnum optPercent { rampSlopeType = RAMP_LINEAR; yyval.dval = yyvsp[-1].dval; } linearSlope : T_LINEAR | '@' ;
DESIGN
The ramp statement was designed to accept a linear slope gradient of 1% to 1000%. Reasonable gradients were expected to lie between 5% and 50%. In practice, we have not found any need for gradients above 50% but testing of the RSH X-axis has revealed a need for fractional gradients. At 1%, each step changes only 1% from the previous step. This would seem to be a reasonable practical limit and has been for other motors. However, the new RSH X-axis motor is basically under-powered and needs an extremely gentle acceleration near the end of its up ramp. Whether we should be using an under-powered motor is debatable. However, the script language should support as gentle an acceleration as we care to define (within the maximum step count limit, which is now 118). The language was changed to accept a fractional slope in linear ramp statements. The ramp generator function continues to reject unreasonable slopes, which are now defined as less than .01% or greater than 1000%.
Generally, fractional slopes will not be useful in simple linear ramps, because they are only needed at the high end, which can't be reached with such slow acceleration due to our ramp step limit. In any case, we wouldn't want such a ramp because the motor would grind horribly at the low end. Where fractional slopes can be useful is in exponential ramps, described later in this report.
IMPLEMENTATION
fcomp- fsqyac.y
Previously, the linear and log ramp generator functions had identical prototypes and the parser called them together through a conditional. Now, emitLinearRamp takes a double gradient where emitLogRamp takes an unsigned int (UINT) step count and the conditional must be replaced by control flow.
segComponent : T_UINT optExclusive T_TO T_UINT optExclusive slopeType { ... else if( rampSlopeType == RAMP_LINEAR ) emitLinearRamp( yyvsp[ -5 ].ui, // start step rate. yyvsp[ -2 ].ui, // end step rate. yyvsp[ 0 ].dval, // gradient. yyvsp[ -4 ].ui * 2 + yyvsp[ -1 ].ui ); /* incflag: 00 = exclude both ends, 10 = include start only, 01 = include end only, 11 = include both. */ else emitLogRamp( yyvsp[ -5 ].ui, // start step rate. yyvsp[ -2 ].ui, // end step rate. */ yyvsp[ 0 ].ui, // step count. yyvsp[ -4 ].ui * 2 + yyvsp[ -1 ].ui ); // incflag. slopeType : linearSlope fnum optPercent fnum : /* Float required but integer accepted for user convenience. */ T_UREAL { yyval.dval = yyvsp[0].dval; } | T_UINT { yyval.dval = yyvsp[0].ui; }
fcomp- motor.c
UCHAR emitLinearRamp( USHORT start, USHORT end, double grade, USHORT incflag ) { ... if( grade < 0.01 || grade > 1000 ) return fatalError( -1, "Illegal ramp gradient. Range is 0.01% to 1000%.\n" );
REQUIREMENT AND DESIGN
During testing of the RSH X-axis it became apparent that the motor could not reach its highest possible speeds (as allowed by the current design of the MSM, in which step resolution is 32 usec-- see System Design Report 32 [cdxsys- RampResolution]) without using a fractional ramp gradient near the top end of the up ramp. However, fractional gradients at the beginning of the ramp were noisy and not needed. A better ramp in this case would accelerate even more quickly at the beginning and more slowly at the end than the single-slope linear ramp (which does accelerate more quickly at lower speeds but not enough in this case).
The ideal theoretical ramp for acceleration is exponential (velocity vs. time) and for deceleration reverse exponential. The ideal cannot be achieved in practice because an exponential curve ends at infinity while a reverse exponential begins at negative infinity. The single-slope linear ramp is a fairly crude approximation of a true exponential ramp. A more accurate piece-wise approximation can be achieved by a series of linear ramps of decreasing slope.
For any given motorized system it is difficult to predict the best approximate exponential acceleration and deceleration functions. With the single-slope linear ramp we have seen that the best ramp can be found quickly by experiment when the development system allows intuitive ramp specification and rapid replacement. The language for specifying a (near) exponential ramp could be based on a mathematical formula but the real situation points to a more intuitive construct. For RSH X-axis acceleration, it was obvious that we wanted faster acceleration at the beginning, slower at the end, and presumably a continuous transition in-between. We should be able to specify the beginning and ending slopes and let the compiler compute all of the intermediate slopes. The syntax for such a specification is, for example, ramp X up 200 to 1600 @ 20% to 0.5%. While this syntax was motivated by the need to intuitively specify an exponential ramp, it is not limited to this. Most obviously, a reverse exponential could be specified, for example, by ramp X down 1600 to 200 @ 0.5% to 20%. Further, the syntax does not insist that exponential be associated with and up ramp and reverse exponential with down. A reverse exponential up ramp might be used for a very gentle start. Additionally, as with the other ramp types, the script compiler accepts ramp statements comprising multiple segments, for example ramp theta up 50 to 100 @ 0.5% to 20% + 100 to 600 @ 20%.
Because ramp tables are generated by the script compiler instead of on the fly by the MSM, formulas of any complexity can be used to compute the intermediate slopes. The basic approach is similar to the single-slope linear ramp computation. Each step is derived from the previous step. For single-slope, each step is computed as the previous step duration plus (down ramp) or minus (up ramp) the slope times the step. The only difference with a continuously varying slope is in the method of computing the step change. There are two general methods for doing this. Both methods use linear interpolation of some characteristic related to slope against the current step duration (or rate). The simpler method finds the instantaneous slope at each step by interpolation over the slope range against the step duration range. The more complex method finds the instantaneous value of something related to the slope range but then applies a non-linear function to that value to compute the step change at that point. Two functions that may be used are sine and log. For this method, instead of interpolating over the slope range, we interpolate over the inverse function, angle for sin and exp (exponent) for log. Thus, the angle range is arcsine of the starting slope to arcsine of the end slope; the exp range is exp of the starting slope to exp of the end slope. Note that in the case of sine, the ramp slope is not related to the angle used for calculating step size; sine is simply an arbitrary non-linear function just like log. The sin or log function is applied to the value found by linear interpolation to compute the step change relative to the first step change.
All three formulas for ramp generation-- direct slope interpolation, sine, and log-- were developed and tested with the RSH X-axis. Each formula required a unique combination of starting and ending gradients to produce the fastest motor rate. The resulting fastest ramps were numerically similar but not identical. All produced a quieter start than the fastest single-slope linear ramp and a faster slew. The log and sin generators produced ramps that appeared to deliver identical motor performance with a slew rate 18% higher than the single-slope ramp. The direct slope interpolator yield a slew 23% higher than the single-slope ramp. This difference might be a coincidental effect of the poor step resolution, but since this method is simpler and more intuitively matches the source syntax it was selected as the only generator means.
As discussed in System Design Report 32 [cdxsys- RampResolution] the 32-usec step resolution seriously degrades the high end of these ramps. For example, the following statement was compiled with SHOW_LINLIN defined in motor.c to display the ramp that emitLinLinRamp generates in theory and with the -r script compiler command line option to create the ramp list file.
ramp m7 up 200 to 1200 @ 20% to 0.1%
As it computed the ramp, the function printed the following theoretical ramp table (of steps in steps-per-second):
200, 250, 295, 336, 375, 411, 446, 479, 510, 539, \ 568, 595, 620, 645, 668, 691, 713, 733, 753, 772, \ 790, 808, 825, 841, 856, 871, 885, 899, 912, 924, \ 936, 948, 959, 970, 980, 990, 999, 1008, 1017, 1025, \ 1033, 1041, 1049, 1056, 1063, 1069, 1075, 1082, 1087, 1093, \ 1098, 1103, 1108, 1113, 1118, 1122, 1126, 1131, 1134, 1138, \ 1142, 1145, 1149, 1152, 1155, 1158, 1161, 1163, 1166, 1169, \ 1171, 1173, 1176, 1178, 1180, 1182, 1184, 1186, 1187, 1189, \ 1191, 1192, 1194, 1195, 1197, 1198, 1199, Step count = 87
The function also computed the tick count of each step based on the MSM's motor step rate defined in analyze.ini as:
STEP_RATE = 32605 ; Stepper driver fundamental rate (1/step period = steps/second).
The resulting tick count table, which is conveyed to the MSM in a rampdef message, was subsequently output to the ramp.lst file, which showed the following:
--- Motor M7 --- UP: [step size] 163, 130, 111, 97, 87, 79, 73, 68, 64, 60, 57, 55, 53, 51, 49, 47, 46, 44, 43, 42, 41, 40, 40, 39, 38, 37, 37, 36, 36, 35, 35, 34, 34, 34, 33, 33, 33, 32, 32, 32, 32, 31, 31, 31, 31, 30, 30, 30, 30, 30, 30, 30, 29, 29, 29, 29, 29, 29, 29, 29, 29, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 27, 27, 27, 27, 27, 27, 27, 27, 27, count = 87, time=0.106609.
Note, for example, that the monotonically increasing theoretical steps 1189 through 1199 are all listed as 27 real ticks.
IMPLEMENTATION
fcomp- fsqyac.y
The slopeType non-terminal determines what type of slope the statement specifies based on its syntax. If the phrase x% to y% is present, the global enum rampSlopeType is assigned RAMP_LINLIN unless the specified gradients are nearly identical, in which case the compiler issues a warning and identifies the ramp as RAMP_LINEAR.
segComponent : T_UINT optExclusive T_TO T_UINT optExclusive slopeType { ... switch( rampSlopeType ) { ... case RAMP_LINLIN : emitLinLinRamp( yyvsp[ -5 ].ui, // start step rate. yyvsp[ -2 ].ui, // end step rate. yyvsp[ 0 ].dval, // Begin gradient. rampEndGrade ); // End gradient. slopeType : ... | linearSlope fnum optPercent T_TO fnum optPercent { double diff; yyval.dval = yyvsp[-4].dval; rampEndGrade = yyvsp[-1].dval; diff = rampEndGrade - yyval.dval; if( diff > -0.2 && diff < 0.2 ) { showWarning( "Start and end gradients too similar.\n" ); rampSlopeType = RAMP_LINEAR; } else rampSlopeType = RAMP_LINLIN;
fcomp- motor.c
The ramp is always computed as up. If a down ramp is specified (start rate greater than end rate) the step duration and slope range endpoints are swapped and the steps array, populated with the calculated tick count of each step, is copied backwards into the rampdef message.
int emitLinLinRamp( USHORT startRate, USHORT endRate, double startGrade, double endGrade ) { ... if( startRate < endRate ) { start = 1.0 / startRate; end = 1.0 / endRate; startSlope = startGrade / 100.0; endSlope = endGrade / 100.0; } else { startSlope = endGrade / 100.0; endSlope = startGrade / 100.0; start = 1.0 / endRate; end = 1.0 / startRate; } for( cnt = 0, step = start ; cnt < 999 && step >= end ; ) { if( cnt < MAX_STEPS ) steps[ cnt ] = motorStepRate * step + 0.5; cnt++; ... slope = endSlope - ((end - step) / (end - start)) * (endSlope - startSlope ); step -= step * slope; }
FSQ FILES AND RAMP DICTIONARY {}
While working on the script compiler's ramp generators I noticed that the global UINT motorStepRate was being used in many places and always being converted to double. I changed the global to double with the conversion being done only once. To verify that this wouldn't affect any ramps, I compiled rsh.f and xramp.f before making the change and saved the resulting FSQ files. I then changed motorStepRate, recompiled the script compiler, and then recompiled the two script files. Xramp.fsq was the same under either version of the script compiler but rsh.fsq was not. Further investigation revealed that rsh.fsq changed every time rsh.f was compiled, regardless of the script compiler.
What the simple file comparison didn't show was that the actual ramp tables rsh.fsq (in rampdef messages) were the same in either case. The files differed only in ramp IDs, which are assigned in the rampdef messages and referenced in subsequent ramp messages. As explained in the Script Language Reference [cdsref- Rampdef] a rampdef statement produces a permanent ramp, which, although functionally identical to a ramp simultaneously defined and assigned to a motor by a ramp statement, can be more efficient for the MSM. All of the ramps in xramp.f are defined (and assigned) by ramp statements. In rsh.f the ramps used for normal operation are all defined by rampdef statements, but during homing ramp statements are used to create and assign temporary ramps.
As explained in Software Implementation Report 62 topic Ramps And Trajectories- Ramp Identification [reports- RampIdentification] the compiler assigns every ramp an ID. Ramps defined by ramp statements are assigned IDs from a range of values reserved for temporary ramps. Those defined by rampdef are assigned IDs from a range reserved for permanent ramps and their names and IDs are stored in the ramp dictionary, rampdict.def. Any reference to a named (permanent) ramp is translated to the ramp's ID in rampdict.def. References to temporary ramps are hidden in ramp statement translation. Recompiling a script that contains rampdef statements does not change the IDs of any of those ramps already in the dictionary. Recompiling a script that contains references to permanent ramps does not change the IDs of the references. Temporary ramps can theoretically be assigned any ID in the proper range but, as explained in Report 62, the assignment of temporary ramp IDs may begin either at the first reserved number or at the last number used in a previous session, which is recorded in the dictionary. The inconsistency is a side effect of accommodating several different development situations. If a script file contains no permanent ramp statements or references, like xramp.f, the dictionary is not opened and temporary ramp IDs are assigned starting at the first reserved number. Consequently, recompiling such a file does not change temporary ramp IDs. Script files that do contain permanent ramp statements or references cause the compiler to open the dictionary. When it does this, it reads the last temporary ramp ID from the dictionary and restarts the temporary range at this value. If a script file contains only permanent ramp statements and references, repeatedly compiling it will also produce the same FSQ file. If it contains a mix, like rsh.f, the FSQ output will change every time the file is compiled.
Functionally, it means nothing if the IDs assigned temporary ramps change. However, it makes FSQ file comparison nearly useless. In the particular case of regression comparison, there is a simple solution, which is to delete rampdict.dat each time before compiling. Alternatively, the first (or only) script file can contain the statement pragma ClearRamps (near the beginning of the file). Inconsistent FSQ files also present a problem for instrument version control. We often compare run-time files in use by an instrument to a reference set. As recommended in Report 62, if all permanent ramps are defined in one file that also contains pragma ClearRamps then temporary ramp IDs will be consistent through recompilations if all script files are compiled in a consistent order. The problem is that we don't necessarily do this. There is no programmatic solution because no single approach works for all circumstances. We may want to develop a strict procedure for compiling and deploying script files that contain ramps in order to ensure consistent FSQ files.
REQUIREMENT
As described in the Script Language Reference [cdsref- Ramp] a recoil ramp reverses a motor's direction and briefly moves it at the end of its travel in the desired direction. This may be used to improve shear control in fluid transfer or position control in mechanisms that have backlash. We have not used recoil ramps in any application but it has not cost us anything to have the capability. This situation may change with a new motor controller under consideration, in which the FPGA actively generates step patterns instead of passively playing the events passed to it by the CPU. In this design, just the capability costs significant FPGA resources. However, unless and until we decide not to support recoil, we want it to work correctly.
In developing the new motor controller, considerable effort was consumed trying to determine how the recoil ramp could work, given that its direction reversal mechanism was different from that used normally, e.g. a plus move followed by a minus move. Eventually, it was determined that the recoil ramp means was functioning incorrectly for full steppers, always losing a step.
DESIGN AND IMPLEMENTATION
The full step motor's gray code pattern is selected by an index that is incremented after each step so that it is pointing to the pattern needed to take the next step in the same direction. To reverse the direction, the index must be changed not by decrementing by 1 but by 2. However, there are only four elements in the array, so rather than testing for the two cases that might under- or overflow, it is more efficient to map the current index to the reverse step using a table, as explained in Report 62 topic Moving Motors [reports- MotorReversal]. This was the approach taken in startStepper function. However, the following code was used by stpmotorIsr to reverse direction for a recoil ramp:
void interrupt stpmtrIsr() { ... case MS_RECOIL : mp->incr = 0 - mp->incr; /* Negate step increment. 1 vs. -1 */ mp->stepIdx += mp->stepIdx < 4 ? 4 : -4;
This transformed the PLUS indices 0 through 3 to the MINUS indices 4 through 7 and vice versa. But such a transformation would select the same phase pattern for the first step of the recoil ramp as would already be applied to it. The motor would not move on this step but the motor controller would record a one-step position change.
To correct this, the index reversal table that was in startStepper was made global and used for the recoil reversal as well as for normal motor direction change.
msmapp- stpmtr.c
#define PHASES(M) \ { /* PLUS Phase patterns. Initial PHA = 1, PHB = 1 */ \ M*4 + MTR_PBBIT, /* PHB->0: PHA = 1, PHB = 0 */ \ M*4 + MTR_PABIT, /* PHA->0: PHA = 0, PHB = 0 */ \ M*4 + MTR_PBBIT + 0x80, /* PHB->1: PHA = 0, PHB = 1 */ \ M*4 + MTR_PABIT + 0x80, /* PHA->1: PHA = 1, PHB = 1 */ \ /* MINUS Phase patterns Initial PHA = 1, PHB = 1 */ \ M*4 + MTR_PABIT, /* PHA->0: PHA = 0, PHB = 1 */ \ M*4 + MTR_PBBIT, /* PHB->0: PHA = 0, PHB = 0 */ \ M*4 + MTR_PABIT + 0x80, /* PHA->1: PHA = 1, PHB = 0 */ \ M*4 + MTR_PBBIT + 0x80 /* PHB->1: PHA = 1, PHB = 1 */ } UCHAR changeDir[] = { 4, 7, 6, 5, 0, 3, 2, 1 }; /* dirIdx change to change direction without losing phasing. rotate[0] <-> rotate[4], etc. This assumes that dirIdx is pre-incremented in the original direction, i.e. it would select the next step in that direction. */ void interrupt stpmtrIsr() { ... case MS_RECOIL : mp->incr = 0 - mp->incr; /* Negate step increment. 1 vs. -1 */ mp->stepIdx = changeDir[ mp->stepIdx ];
To top of [Ramps And Trajectories]
ADDITIONAL SOFTWARE/FIRMARE DESIGN ISSUES {}
[NextTopic] [MainTopics]
REQUIREMENT
The power command sets the power for each segment of a stepper's trajectory. One power statement can set the power for any number of segments. For example, power Theta up high slew medium down low hold high idle low sets the power for all segments of Theta's trajectory; power Theta idle low sets Theta's idle power while not affecting the other segments.
The stpMtrIsr function (in msmapp- stpmtr.c) sets the power for a motor at the beginning of each segment. There is no requirement or provision for changing the power of an active segment. Generally, there would be no reason to want to do this for most segments. However, the idle state represents a special case for two reasons. One is that, unlike the other segments, a motor may remain in the idle state essentially forever, particularly during debugging. The other is that, because a static motor presents no back-EMF, the current through an idle motor under power is much higher than when the motor is moving and can damage the drivers and/or motor.
There are several situations in which the ability to change the power of an idling motor would be useful. Most of these occur during script development and debugging. For example, while developing a homing script for the Theta and vertical axes of the RSH "robot", I wanted to frequently move the picker head to certain locations to verify the recovery ability of the script. Both axes normally require some idle power to prevent drifting, but the vertical axis is difficult to move by hand even with only low power applied. Without immediate idle power change, i.e. after the idle segment has already begun, removing power requires two commands, power followed by move. In practice, the difference between having to enter one vs. two commands is more inconvenient than it sounds. The script debugger's command recall capability supports convenient toggling between two commands, such as to execute the homing script and to change the motor's idle power, but cycling through three commands is clumsy.
Another debugging situation in which immediate idle power change is useful is testing the effect of idle power by applying a load (perhaps the developers own hand) while changing the power. Another use is to reduce thermal stress on the motor drivers. If a motor remains in idle only for short periods during normal operation, a relatively high idle power may not stress the drivers; but if we pause in this state during debugging, the current may be too high. The ability to reduce the power without moving the motor can be used to reduce the stress without affecting the state of the script being tested.
Being able to change idle power without moving a motor could also be useful in a normal script. Typically, the hold segment of a trajectory is used to prevent momentum from pulling a motor past its programmed stop position. In this use, the two-second limit (64K 32-usec periods) is normally acceptable. For rare cases in which a longer hold is needed, idle power can be initially high or medium and reduced after a script-based delay. Idle power might also be increased temporarily at arbitrary times to resist movement caused by events other than the motor's own activity. For example, if the RSH robot's vertical axis drifts only when Theta moves, thermal stress on the vertical motor's drivers could be reduced by setting their idle power off when Theta is not moving.
IMPLEMENTATION
Due to the design of the motor control FPGA, it is not possible to change power settings through any random access means. Instead, the power command interpreter sets the power in a motor's descriptor (Stepper.perPow[] array of power settings for each segment of the motor's trajectory) which subsequently guides the stepper control ISR. The process of power change occurs in three stages. First the power command interpreter changes one or more perPow array values. At the next motor page interrupt in which the motor transitions to a new state, the ISR transfers the new segment's power setting to the event page. When the FPGA finishes interpreting the previous page, it starts on the new page and at this time the actual power is changed (in fact, this final stage entails several stages within the FPGA-- the power doesn't change until the FPGA scans out the new setting into the motor control MOSI chain).
The ISR changes a motor's state from down ramp or hold to idle, which is a transitory state indicating that the motor is essentially idling but its idle power has not yet been established. The ISR changes a motor's state from idle to idling when it sets the idle power. One approach to providing immediate idle power change would be for the power command interpreter to change an idling motor's state back to idle after setting the appropriate Stepper.perPow value. This approach theoretically enables a solution that doesn't require adding anything to the ISR, which is desirable, as we want the ISR to be as simple and fast as possible. However, the ISR is organized in a way that argues against this solution. The ISR is divided into two major loops. The first, motor state scan loop, effects motor starting, random stops, and support functions. The second and much more complicated, page step scan loop, programs the event page. One problem is that we cannot arbitrarily start a motor in the idle state without adding a little complexity to the page step loop. This complexity has a minimal impact on performance but the code is already uncomfortably difficult to follow. More damaging is that doing the idle power change at all in this loop is less efficient than doing it in the motor state loop because of the extra overhead inherent in the page step loop. Consequently, it is more efficient to add another case to the motor state loop to handle this function.
msmapp- motor.h
enum { /* Stepper.state. For detailed explanation see stpmtr.c */ ... MS_STOPHARD, /* Powered stop. */ MS_STOPOFF, /* No-power stop, i.e. coast to stop. */ MS_IDLEPOWER, /* Idle power change. */
msmapp- stpmtr.c
void interrupt stpmtrIsr() { ... if( hiMotor < loMotor ) return; /* MOTOR STATE SCAN LOOP ... for( mp = loMotor ; mp <= hiMotor ; mp++ ) { switch( mp->state ) { case MS_START: mp->state = MS_UPRAMP; usv = mp->perPow[ MS_UPRAMP ]; setpow: if( mp->pow != usv ) { AddPowerEventW( usv ); /* Will be 2 events in slot 0 */ mp->pow = usv; } break; ... case MS_IDLEPOWER: mp->state = MS_IDLE; usv = mp->perPow[ MS_IDLE ]; goto setpow;
The power command interpreter, procPower was simply transferring the power for each segment identified in the PowerCmd command structure to the motor's perPow array with the appropriate pattern transformation. To eliminate unnecessary activity in the ISR, the motor's state is changed to MS_IDLEPOWER only if the motor's idle power setting is actually changed, not if the same value is reassigned. Also, obviously, the motor must be idling.
The ISR determines whether a particular motor needs attention based only on its state. However, to reduce the ISR's motor scan time, the range of currently active motors is restricted by loMotor and hiMotor. If the motor whose idle power is changing lies between loMotor and hiMotor, the ISR will service it. Otherwise, the range must be extended to include this motor. The range is periodically pruned in the start motor function.
int procPower( FsqMsg *cmd, ProcDef *pd ) { ... Stepper *mp; UCHAR assign; USHORT mtr; UINT idx; USHORT pow; assign = cmd->arg.power.assign; mp = &steppers[ mtr = cmd->arg.power.motor ]; for( idx = IDX_UP ; idx <= IDX_IDLE ; idx++, assign >>= 1 ) if( assign & 1 ) { mp->perPow[ segStates[ idx ]] = pow = MPWR( mtr, powerPat[ cmd->arg.power.pow[ idx ] - PWR_CHAR ]); /* If changing idle power on a motor that is currently idling, engage a * special state to effect the change immediately. Otherwise, the power won't * change until the motor is moved. This is primarily intended to help during * debugging but it could also be used to provide an extended hold period * lasting longer than 2 seconds. */ if( idx == IDX_IDLE && mp->state == MS_IDLING && mp->pow != pow ) { mp->state = MS_IDLEPOWER; if( mp < loMotor ) loMotor = mp; if( mp > hiMotor ) /* If this is the only motor it is both lo and hi. */ hiMotor = mp; } }
REQUIREMENT
One form of the wait command affords waiting for a motor condition, including not moving (i.e. hold or idle), idle, > position, and < position. Generally, if a script needs a motor in a particular condition, it cannot continue unless and until the motor achieves that condition. The wait motor command includes an optional time limit that allows a script to detect and respond to motor/mechanical failure rather than wait forever for a condition that can never be reached. Consequently, for most situations there is no need for a wait-less motor condition test.
There are a few situations in which it is at least convenient if not essential to test a motor's condition without waiting. For example, often the most straight-forward way to search for a motor's home is to command the motor to move a distance that may be further than expected to reach home and then watch an input for the home condition (flag state). If the home condition doesn't occur before the motor reaches the end of its move, depending on the situation, either there is some kind of failure or the script simply needs to reverse direction and repeat the hunt. The fastest homing with minimal "head-banging" frequently results from a process that includes several such reversals. While it would be possible to calculate the duration of each travel and loop for the equivalent time period while watching the home flag, a much simpler script results from looping while testing the motor for not moving and the flag for home.
The wait for motor command can test a triMotor's (shear valve) condition as well as a stepper's. As with testing a stepper's condition without waiting, testing a TriMotor's condition without waiting is most likely to find use in special testing, diagnostic, and debugging situations.
DESIGN
The most obvious candidate command for testing a stepper or triMotor without waiting is if. The conditions to be tested are basically the same as those available in the wait motor command. In the great majority of cases, the wait motor condition is not moving, so making this a default signified by empty is reasonable. For example, a script can say simply wait SyringeA instead of wait SyringeA not moving. Because if motor is used in unusual circumstances, a default is more likely to result in confusion. Also, rather than making not moving a condition, the overall flexibility can be increased by making moving a condition and allowing not to be applied to all conditions. For example:
wait SyringeA moving wait SyringeA not moving wait Theta > 1050 wait Theta not > 1050.
It could be argued that not should be provided to wait motor as well as to if motor. However, it would probably find little use in the contexts in which the wait command appears.
IMPLEMENTATION
For efficiency of the run-time interpreter, each type of if statement translates to a specific command message type. Nothing dictates that this be an if command message. In fact, it might be reasonable to fold if motor condition into the wait motor command (CM_WAITSTEPPER or CM_WAITMOTOR) since these have many characteristics in common, such as motor ID and condition. However, wait and if command interpreters have unrelated generic tasks, wait in managing the waiting mechanism and if in controlling the if true and false levels for the Proc executing the script. Further, the wait motor conditions are slightly different from the if motor conditions and wait motor currently doesn't support not condition. Also, the wait motor condition test mechanism is embodied in a function optimized for use by the callback function that is part of the wait interpreter. Thus, although merging the wait motor and if motor interpreters would afford some code sharing it could do so only by adding complexity that would reduce performance and increase the cost of code maintenance. Consequently, the if motor condition statement is translated to an if motor (CM_IFMOTOR) command message, which has a unique interpreter.
As with wait and other if condition commands, if motor condition should execute remotely as well as locally. Without this, for example, the APU could not test a stepper for condition even though it could wait for the same motor to achieve the condition. Remote if is handled generically by the command dispatcher. The dispatcher automatically resends any command message whose unit ID doesn't match the executing unit. However, for remote if commands it is necessary to also set up a mechanism to receive the result of the test and to delay the script until this arrives. The dispatcher does this only for command message types that lie in a particular range. Thus, the CM_IFMOTOR must be added to this range. To shield the command dispatcher from the specific commands comprising this range, the command message header, fsqapi.h, defines constants MIN_CM_IF and MAX_CM_IF and the function-like macro IsIfCmd.
fsqapi.h
/* The If and InterUnit groups overlap. Not all of the If types are * InterUnit but all Ifs must lie in one range for MIN_CM_IF - MAX_CM_IF range * test. The InterUnit group must be last for simple in-range test by msgType * >= MIN_CM_INTERUNIT. Note that CM_BEGIN and CM_END are in this group. */ /* If group */ /* MIN_CM_IF = CM_IFBITTIME */ CM_IFBITTIME, /* IfBitTimeCmd = single bit predicate in time. */ ... /* InterUnit group. */ /* MIN_CM_INTERUNIT = CM_IFCFG */ CM_IFCFG, /* WriteIfCfgCmd */ ... CM_IFMOTOR, /* IfMotorCmd */ /* MAX_CM_IF = CM_IFMOTOR */ #define MIN_CM_IF CM_IFBITTIME ... #define MAX_CM_IF CM_IFMOTOR #define isIfCmd(T) ((T) >= MIN_CM_IF && (T) <= MAX_CM_IF ) enum { IFMTR_MOVING = 'A', IFMTR_IDLE, IFMTR_GREATER, IFMTR_LESS, IFMTR_CW, IFMTR_CCW, IFMTR_CENTER }; typedef PACKED struct { UCHAR motor; USHORT position; /* If condition = GREATER or LESS */ UCHAR condition; /* IFMTR_MOVING, etc. */ UCHAR truePred; /* TRUE or FALSE predicate. */ } IfMotorCmd; /* CM_IFMOTOR. */
Mirroring the situation of the run-time command interpreter, the minor differences between wait motor and if motor statement translations preclude convenient code sharing. Therefore, the if motor parser is implemented independently of the wait motor. They do share the scan words IDLE (for steppers) and CW, CCW, and CENTER (for triMotors).
fcomp- fsqyac.y
command => T_IF ... unitSelectOpt predicate predicate => ... | ifMotor ; ifMotor : oneMotor ifMotorCondition { switch( namedev.mtr->mtype ) { case MT_STEPPER : if( yyvsp[0].ui != 1 ) fatalError( -1, "Illegal condition for stepper %s.\n", namedev.mtr->name ); break; case MT_TRIMOTOR : if( yyvsp[0].ui != 0 ) fatalError( -1, "Illegal condition for TriMotor %s.\n", namedev.mtr->name ); break; default : fatalError( -1, "%s is a motor type whose condition can't be tested.\n", namedev.mtr->name ); } fmsg.msgType = CM_IFMOTOR; fmsg.arg.ifMotor.motor = mtrIdx; fmsg.arg.ifMotor.truePred = trueCondition == TRUE; writeFsqMsg( sizeof( IfMotorCmd )); } ; ifMotorCondition : lessOrGreater T_UINT { fmsg.arg.ifMotor.condition = yyvsp[-1].ui == '<' ? IFMTR_LESS : IFMTR_GREATER; usVal = yyvsp[0].ui; fmsg.arg.ifMotor.position = swapUS( usVal ); yyval.ui = 1; /* Indicate stepper condition for shared check. */ } | T_IDLE { fmsg.arg.ifMotor.condition = IFMTR_IDLE; yyval.ui = 1; /* Indicate stepper condition for shared check. */ } | T_MOVING { fmsg.arg.ifMotor.condition = IFMTR_MOVING; yyval.ui = 1; /* Indicate stepper condition for shared check. */ } | T_MMOVE { switch( yylval.ui ) { case MOVE_CW : fmsg.arg.ifMotor.condition = IFMTR_CW; break; case MOVE_CCW : fmsg.arg.ifMotor.condition = IFMTR_CCW; break; case MOVE_CENTER : fmsg.arg.ifMotor.condition = IFMTR_CENTER; break; } yyval.ui = 0; /* Indicate TriMotor condition for shared check. */ } ;
To shield the step motor and triMotor modules, stpmtr.c and trimtr.c, from if level control details, a general if motor command function, procIfMotor located in cmdmotor.c, is used as a front end. This is the reason that there is only one general CM_IFMOTOR message instead of distinct stepper and other motor message forms. ProcIfMotor adjusts the proc's ifLevel and then calls either ifStepperCondition or ifTriMotorCondition, as appropriate. These functions return true or false, which procIfMotor uses to determine whether to adjust the proc's falseLevel.
msm/apuapp- cmdmotor.c
int procIfMotor( FsqMsg *cmd, ProcDef *pd ) { UCHAR motor; BOOL stupidMicrotec; motor = cmd->arg.ifMotor.motor; pd->ifLevel++ ; if( motor < firstSpecialMotor ) { #ifdef HAS_STEPPER if( !ifStepperCondition( cmd )) pd->falseLevel = pd->ifLevel; return CMD_DONE; #else stupidMicrotec = TRUE; /* Do nothing. Fall through to error message. */ #endif } else /* motor >= firstSpecialMotor */ { #ifdef HAS_TRIMOTOR if( !ifTriMotorCondition( cmd )) pd->falseLevel = pd->ifLevel; return CMD_DONE; #else stupidMicrotec = TRUE; /* Do nothing. Fall through to error message. */ #endif } return sendScriptFailMsg( pd, FAIL_NOTREADY, FLINE, BADMTRCOMM ); }
msmapp- stpmtr.c
BOOL ifStepperCondition( FsqMsg *cmd ) { BOOL condition; Stepper *mp; mp = steppers + cmd->arg.ifMotor.motor; switch( cmd->arg.ifMotor.condition ) { case IFMTR_LESS : condition = mp->position < cmd->arg.ifMotor.position; break; case IFMTR_GREATER : condition = mp->position > cmd->arg.ifMotor.position; break; case IFMTR_IDLE : condition = IsMotorIdle( mp->state ); break; default : /* IFMTR_MOVING */ condition = IsMotorMoving( mp->state ); break; } return condition ^ cmd->arg.ifMotor.truePred == 0; /* TRUE if * condition is met and true predicate or if condition is not met and false * predicate. */ }
Only execution units that locally control steppers and/or triMotors contain the code for interpreting CM_IFMOTOR. For example, the APU contains the ifTriMotorCondition function but not ifStepperCondition while the MSM contains ifStepperCondition but not ifTriMotorCondition. However, all units contain code for interpreting remote CM_IFMOTOR. Other than the shared generic remote if mechanism, this entails only the addition of CM_IFMOTOR to the case statement in the procParent function. The CM_IFMOTOR message requires an entry in every unit's command dispatch function table. If the unit supports no local interpreter for the message, the table would point to a dummy function (both the APU and MSM support local interpretation, the APU for triMotors and the MSM for steppers).
msm/apu- fsqman.c
packed struct { CmdFunc proc; UCHAR id; /* Use to detect out-of-order table. */ } cmdTable[] = { ... /* InterUnit group. */ /* MIN_CM_INTERUNIT = CM_IFCFG */ { procWriteIfCfg /**/, CM_IFCFG }, ... { procIfMotor, CM_IFMOTOR }, /* MAX_CM_IF = CM_IFMOTOR */ int procParent( FsqMsg *cmd, ProcDef *pd ) { ... switch( cmd->msgType ) { case CM_IFCFG: ... case CM_IFMOTOR: switch( pd->cmdState ) { case CMS_FRESH: if( remQueryProc != NULL ) return CMD_REDO; /* Somebody else (script or master) has a * pending query. When they release remQueryProc (see parseSlaveResponseMsg), * we can submit this one. */ pd->forTime = setAlarmTime( 3 * TICKS_PER_SECOND ); retv = putSlaveMsg( (UCHAR *)cmd ); ...
TESTING
syntax.f
// .............. Miscellaneous IF commands ..................... if M0 Center // Error: Illegal condition for stepper M0.
ifmotor.f
define Motor M2 /**************************************************************************** * Script: IfMotor * Description: Local IF motor condition test. This script is executed by the * stepper motor controller so the motor condition test commands are executed * locally. * The expected echo log is (note all echos from unit 2): * 15DM_ECHO 2Begin IfMotor * 8DM_ECHO 2Moving * 8DM_ECHO 2Moving * 8DM_ECHO 2Moving * 12DM_ECHO 2Not moving * 10DM_ECHO 2Not idle * 10DM_ECHO 2Not idle * 10DM_ECHO 2Not idle * 10DM_ECHO 2Not idle * 10DM_ECHO 2Not idle * 6DM_ECHO 2Idle * 18DM_ECHO 2Motor not > 1050 * 18DM_ECHO 2Motor not > 1050 * 14DM_ECHO 2Motor > 1050 * 18DM_ECHO 2Motor not < 1050 * 18DM_ECHO 2Motor not < 1050 * 14DM_ECHO 2Motor < 1050 *..........................................................................*/ begin IfMotor unit msm echo "Begin IfMotor" ramp Motor hold 0.5 move Motor +100 loop for 3 seconds if Motor not moving echo "Not moving" break endif if Motor moving echo "Moving" endif wait for 0.25 seconds endloop move Motor -100 loop for 3 seconds if Motor idle echo "Idle" break endif if Motor not idle echo "Not idle" endif wait for 0.25 seconds endloop position Motor 1000 move motor to 1100 loop for 3 seconds if Motor > 1050 echo "Motor > 1050" break endif if not Motor > 1050 echo "Motor not > 1050" endif wait for 0.25 seconds endloop wait Motor position Motor 1100 move motor to 1000 loop for 3 seconds if Motor < 1050 echo "Motor < 1050" break endif if not Motor < 1050 echo "Motor not < 1050" endif wait for 0.25 seconds endloop wait Motor end /**************************************************************************** * Script: IfMotorRem * Description: Remote IF motor condition test. This script is executed by a * master of the stepper motor controller so the motor condition test commands * are executed remotely. * The expected echo log is (note all echos are from unit 1): * 18DM_ECHO 1Begin IfMotorRem * 8DM_ECHO 1Moving ... begin IfMotorRem unit apu echo "Begin IfMotorRem"
All of our systems' steppers require reference to an electromechanical flag for absolute position determination. This is typically referred to as a home flag although "home" has no specific meaning. In some cases, a motor's home flag may be located near one end of its travel. In others, the flag may be closer to the middle. The flag itself is typically an opto-interrupter. The RSH "robot" has, among other position sensors, a Theta home flag and a vertical home flag. The vertical home flag is a typical interrupter with a mechanical flag that passes through it. The motor is "home" when the flag blocks light in the interrupter gap. It is out of home if the flag is above or below the interrupter. The Theta home is less typical. The interrupter itself is similar but the flag consists of a metal disk. The radius of one half of the disk is greater than that of the other half. The half with the smaller radius does not block the interrupter, while the other half does. Thus, there are only two home positions, in the larger half of the disk or in the smaller. The transition point between the two could be considered "home".
Unless a motor has an absolute position encoder (none of ours do and this is a very uncommon feature because it is expensive) or its position is irrelevant, such as for a peristaltic pump or fan, it can't be used for normal application purposes without first undergoing a homing process. When the home position is found, it is assigned a numeric value (using the position command) after which absolute moves are relative to this value. In most cases, even if scripts make only relative moves, the motor still has to start in a known position in order to function properly in the mechanical system. This is particularly true of the RHS robot, which picks up racks from trays on the loader and deposits them on a belt located under the analyzer's mix head and sample probe. Syringes represent the opposite extreme, because the dispensed fluid volume depends only on the relationship between the start and stop positions. Uncertainty of a few steps is irrelevant as long as both positions are off from ideal by the same amount.
When no absolute uncertainty can be tolerated, homing has to be done carefully. The flag must always approach the home position from the same direction and with enough steps to ensure that the motor is in a controlled state. For full-stepping, the minimum number of steps in the approach is four. The stepping rate must be slow enough to be sure that momentum doesn't carry the motor past the flag position and that motor control paging uncertainty doesn't introduce position uncertainty. Scan chain latency (32 usec. for the MSM's motor flags) and script command uncertainty (typically 100 usec during homing) are relatively unimportant compared to the 8 msec. motor paging uncertainty, which limits accurate homing to no faster than 260 (preferably less than 130) steps per second. At this slow rate, finding home from an entirely unknown position may take an excessively long time and many motors are quite noisy at slow rates, leading to negative customer perception. The solution to this conundrum is to initially perform fast coarse homing, and then move the motor to a roughly known position to perform the precise but slow homing.
When hunting for home from an unknown position, the motor may encounter mechanical stops at any point, at which it bangs it head against the wall until the requested number of steps have been attempted. There is no way to know when this situation exists. To reduce the incidence of head banging, a script should attempt to search for home intelligently. For example, instead of simply trying to step full travel in one direction and then full travel in the other, an increasing circle can be used, starting at 1/4 travel in one direction and then trying 1/2 in the opposite, followed by 3/4 in the original direction and finally full travel in the reverse. This results in no more than 1/2 full travel head-banging in the worst case and substantially less in most cases. The normal shutdown position is selected to produce the fastest (and certainty head-bang-free) coarse homing, so that the instrument normally initializes cleanly and quietly.
Coarse homing should be carried out under the lowest power settings feasible to reduce the noise if head banging does occur. However, there is a tradeoff against speed. The lower the power, the slower the maximum speed. Fast homing not only reduces instrument startup time, but also leaves the customer with a better impression of the intelligence of the instrument. The subtle but inescapable impression of a long homing process is that the instrument is stupid.
One final desirable feature of homing is that it be able to function even in less than ideal circumstances. For example, we can tell users not to start an instrument with racks in trays in the loader but the impression of the RSH robot fighting with an engaged rack during initialization is still a bad one. A much better impression is left if the robot, finding that it can't home Theta, lowers the picker enough to disengage the rack and tries again. The instrument can then move the robot, confirm optically that racks are loaded and warn the operator.
The RSH was originally designed for a chemistry analyzer. Its adaptation to CDNext is not finished but pieces of the loader are currently being tested to determine whether our much cheaper control system is adequate to the task of controlling the loader. In one test situation, the "robot", which comprises the Theta and vertical movements, a rack is picked up from a tray and deposited on a stationary rail located approximately where the belt will be located. The robot itself is also stationary. In the real system it will be moved left and right by a motor and belt system attached to the loader base. The robot arrived with a dysfunctional circuit board assembly, which has been replaced by just the opto-interrupters for Theta and vertical home. A modified RPMD (Right Panel Motor Driver) board is used to drive the two motors and its motor home flag inputs are used for the MSM to read the state of the opto-interrupters.
The test comprises both a homing script and a pick and place script. Normally when more than one motor axis is involved, a script should first home an unrestricted axis and then position it to provide the next axis an unrestricted path. The vertical and Theta axes in the robot are mutually restrictive. If Theta is rotated toward the belt position, downward movement is blocked. If the picker is moved down, Theta is blocked in both directions. However, while Theta's full travel range is restricted in this situation, the home mark (transition between larger and smaller disk radii) can, nevertheless, be reached. Therefore, Theta is homed first. With Theta in the home position, vertical axis travel is unrestricted.
For both axes, homing is accomplished in two phases, a fast coarse positioning followed by a slow precise one. In both phases, the script stops the motor hard, i.e. under the hold power setting, as soon as it sees the home indication. Theta's coarse homing is simplified by the fact that its home flag's two states, Inward and Outward (defined in analyz.ini) correspond to only two mechanical states. If, for example, Theta's home flag indicates Outward, we know that moving the motor inward will eventually achieve the Inward state. The only complication is that Theta's movement may be restricted by the picker being engaged to a rack or the tray. If inward movement doesn't result in the Inward home state, the script moves the picker down a distance equal to the vertical length of the rack engagement tongue. If the picker fully engages a rack, as long as the rack is resting on the bottom of a tray, this will release the picker. Medium vertical power is used for this short move to be sure that the move actually executes. Then the coarse homing sequence is tried again. If this too fails, the picker may be stuck on the front lip of the tray. Assuming this, the script moves the picker up a short distance and tries homing again. If this fails, the script gives up and reports a failure. The script could try harder. For example, if the picker is initially lifting a rack off the tray a small distance, this procedure will not disengage the rack and picker. Rather than giving up, the script could move the head upward at least enough to be sure that the rack clears the sides of the tray and can swing. Theta power would be increased to enable it to swing the rack, and homing would be tried again. This is only a test and the mechanical configuration is not final. Production scripts should not give up homing until they have exhausting all safe means available.
Theta coarse homing initially tests whether the home flag indicates that the picker is inward or outward. If inward, the motor is simply moved outward toward the home transition. If outward, the motor is moved inward. In this case, the various rack and tray disengagement strategies may be needed. Theta can't be stuck due to a particular tray and rack arrangement if it is already inward. However, it could be stuck due to the picker having been driven behind the robot's housing. The script does not address this possibility, as would a production script.
Homing Theta from an initial Outward position illustrates the use of a VAR for implementing a simple state machine within a script. To reduce code duplication (bad in any language) a loop is used. If homing fails (note if motor not moving statement) when VAR0 is 0, the rack disengagement strategy is tried; when VAR0 is 1, tray disengagement is tried; when VAR0 > 1, the script gives up and breaks the loop.
Coarse vertical homing uses most of the same script mechanisms as Theta. However, instead of the state machine keeping track of disengagement strategies, it is used to select first 1/4 full travel in one direction followed by 1/2 travel in the reverse then 3/4 travel in the original direction and finally full travel in the reverse. The script immediately breaks the loop at any point that it sees the home condition. As with Theta, the VAR-based state machine significantly reduces code duplication. Since each script has eight local VARs, VAR0 through VAR7, and any script can access the remaining global VARs, VAR8 through VAR99, a script could implement numerous simultaneous state machines if that were appropriate. With global VARs a state machine in one script could influence the behavior of another script (this, in fact, does occur in sample handling).
Temporary motor ramps are used for homing. As described in the script language reference manual [cdsref#Ramp] a temporary ramp can be created and assigned to one motor at any time. The ramp definition persists only as long as the ramp is assigned to the motor. Assigning a different ramp to the same motor causes the temporary ramp to be discarded. This is desirable for ramps used in homing, as they are usually inappropriate for normal operation. Theta has only one homing ramp because it performs similarly whether moving outward or inward. However, testing reveals that vertical movement up and down differ significantly and using ramps optimized for the direction of travel enhances performance. Rather than repeatedly redefining the vertical ramps as first the down ramp and then the up ramp is assigned to the motor, the two ramps are defined. Defining ramps in this way does not make them permanent. The define command only affords a means to reduce script source repetition (good in any language).
After homing, it makes little difference whether a permanent or temporary ramp is assigned to Theta because the assignment doesn't change. There would be a difference only if some other motor were to share this ramp definition. In contrast, the vertical motor will be alternately assigned a ramp optimized for up movement and one optimized for down in normal operation as well as for homing. In this case there is a significant difference in run-time performance. As explained in the script reference manual [cdsref.doc#Rampdef] the execution unit (MSM) interprets a permanent ramp definition only once whereas it must interpret a temporary ramp definition every time it is assigned to a motor. Consequently, permanent ramps are defined (using rampdef statements) for the vertical motor. These statements can appear anywhere prior to assignment of the ramps. In this test, they are located at the end of the homing script since, like homing, they only need to execute one time before normal motor operation.
analyz.ini
SENSORS UNIT = MSM SPACE = MSM_MOTORFLAGS ... ThetaHome = 60 0 = Inward 1 = Outward ; AKA M0F1 RackVhome = 61 1 = Home 0 = NotHome ; AKA M0F2 STEPPER_MOTORS UNIT = MSM ... Theta = 4 ; Out = -, In = +. Full travel = 105. RackVertical = 5 ; Up = -, Down = +. Full travel = 1050. Rack tab height = 80.
rsh.f
define upFastSearchRamp up 50 to 200 linear 25% slew 200 down 200 to 50 linear 25% define downFastSearchRamp up 50 to 350 linear 25% slew 350 down 350 to 50 linear 25% /**************************************************************************** * Script: HomeRackPicker * Description: Home the Theta and RackVertical RSH robot axes. * ........................... notes ....................................... * - Moderate speed ramps with reduced power are used for searching for home * in order to reduce head-banging noise. RackVertical is initially higher * power in case it needs to move to try to clear a stuck Theta. After Theta is * homed, RackVertical power is reduced for its home search. The initial search * for home is fast enough to incur some uncertainty due to the latency of the * motor flag scan chain and possibility of motor coasting. After the home is * roughly found, the mechanism is moved to a position just outside of the * flag area and then slowly stepped into the home to yield a precise and * repeatable home position. * * - Each axis' idle power should be set to low after or before finding home in * order to prevent it from drifting out of the known position. However, to * assist testing, the RackVertical idle power is off. Even low power makes * repositioning the mechanism difficult. This should not be a problem even for * normal operation, because vertical drift only occurs when Theta moves. To * prevent this, we only need to set RackVertical's idle power to low before * driving Theta, e.g. at the beginning of the PickAndPlace script. * * - The permanent RackVertical ramps are defined at the end of this script * just for convenience. They could also be defined in a separate script or * in the PickAndPlace script, although this would not be very efficient since * PickAndPlace may be called repeatedly. *..........................................................................*/ begin HomeRackPicker echo "Begin HomeRackPicker" power Theta up low slew low down low idle low ramp Theta up 50 to 70 linear 25% slew 70 down 100 to 70 linear 25% power RackVertical up medium slew medium down medium ; In case needed of Theta assist. // Do high-speed coarse homing of Theta VAR0 = 0 if ThetaHome Outward move Theta +105 ;Inward full travel loop 0 if ThetaHome Inward stop Theta hard VAR0 = 0 ; If we succeed on repeat clear failure indicator. break endif if Theta not moving if VAR0 > 1 break ; Allow two repeats. endif if VAR0 = 0 ; Theta may be constrained by a rack. Try moving down by one tab length to free it. ramp RackVertical downFastSearchRamp move RackVertical +80 else ; VAR is already 1 so we tried going down. Maybe picker stuck in tray. ramp RackVertical upFastSearchRamp move RackVertical -40 ; Try pulling up out of tray. endif VAR0 +1 ; Failure/retry count. wait RackVertical move Theta +105 endif endloop else ; Theta is inward. move Theta -105 ;Outward full travel loop 0 if ThetaHome Outward stop Theta hard break endif if Theta not moving VAR0 = 1 ; Fail flag. break endif endloop endif // If success in coarse homing, do fine homing from roughly known position. if VAR0 != 0 echo "Theta homing failure" else // Do fine homing from coarse known position. move Theta -8 ; Move outward to set up for fine homing. wait Theta ramp Theta up 10 slew 10 down 10 move Theta +20 loop for 5 seconds if ThetaHome Inward stop Theta hard break endif endloop // Set normal run-time ramp and power for Theta ramp Theta up 30 to 100 linear 10% slew 100 down 100 to 30 linear 20% hold 0.5 power Theta up medium slew medium down medium hold medium idle low position Theta 1000 endif // ................. Home Vertical ................................ power RackVertical up low slew low down low // Do fast, coarse search for vertical home using widening search pattern. if RackVhome Home ramp RackVertical DownFastSearchRamp move RackVertical +155 ; The process requires starting not in home position. wait RackVertical endif VAR0 = 0 loop 4 if VAR0 = 0 ramp RackVertical UpFastSearchRamp move RackVertical -260 ; Up 1/4 full travel else if VAR0 = 1 ramp RackVertical DownFastSearchRamp move RackVertical +520 ; Down 1/2 full travel else if VAR0 = 2 ramp RackVertical UpFastSearchRamp move RackVertical -780 ; Up 3/4 full travel else ramp RackVertical DownFastSearchRamp move RackVertical +1050 ; Down full travel endif endif endif loop 0 wait for 0.1 second if RackVhome Home stop RackVertical hard VAR0 & 1 ; Reduce 0, 1, 2, 3 to even vs. odd. Even is up, odd down. if VAR0 = 0 ; Even (0 or 2). move RackVertical -155 ; Move up through the flag plus margin. else move RackVertical -60 ; Move up a little to provide margin. endif wait RackVertical VAR0 = 0 ; Indicate home found. break endif if RackVertical not moving VAR0 +1 break endif endloop if VAR0 = 0 break endif endloop // If successful coarse homing, do fine search from roughly known position. if VAR0 = 0 echo "Successful coarse vertical homing" ramp RackVertical up 10 slew 10 down 10 move RackVertical +400 loop 0 ;for 5 seconds if RackVhome Home stop RackVertical hard break endif endloop position RackVertical 1000 else echo "Rack vertical homing failure" endif // Define permanent RackVertical ramps. rampdef UpRackRamp up 50 to 700 linear 50% slew 700 \ down 700 to 50 linear 50% hold 0.5 rampdef DownRackRamp up 50 to 900 linear 50% slew 900 \ down 900 to 50 linear 50% hold 0.5 end ; HomeRackPicker /**************************************************************************** * Script: PickAndPlace * Description: RSH rack handler test/demo. Pick up rack out of tray and * deposit it on belt. *..........................................................................*/ begin PickAndPlace power RackVertical up medium slew medium down medium hold medium idle low ramp RackVertical = DownRackRamp move RackVertical to 1000 move Theta to 1000 wait RackVertical wait Theta move RackVertical to 1415 wait RackVertical move Theta to 987 wait Theta ; Pick up the rack and move vertical to belt level. ramp RackVertical = UpRackRamp move RackVertical to 520 wait RackVertical move Theta to 1037 wait Theta ; Move down to "release" the rack move RackVertical to 650 wait RackVertical wait for 0.5 second ; Go back up and pick up the rack off the belt. move RackVertical to 520 wait RackVertical ; Swing the rack to tray deposit position and lower it back into the tray move Theta to 987 wait Theta ramp RackVertical = DownRackRamp move RackVertical to 1415 wait RackVertical move Theta to 1000 wait Theta move RackVertical to 1000 wait RackVertical end
{} To top of [AdditionalSwFwIssues]
[NextTopic] [MainTopics]
REQUIREMENT
The RSH loader robot described in Software Implementation Report 64 topic Precise Motor Homing [reports- RshRobotHoming] was originally designed under the assumption that the motors would be driven with microstepping, which increases the native step resolution of the motors. The theta axis is particularly ill suited to full stepping. The motor provides direct drive to the swinging rack picker arm. The end of the rack is located 8 inches from the axis of rotation. With the 1.8 degree/step motor, each full step effects .25 inch linear movement of the end of the rack. Not only is this much movement very coarse but it also causes very jerky movement. The momentum of the relatively heavy rack causes it to jerk at each step, initially lagging behind the motor and then trying to pull ahead of the motor. Larger steps exacerbate this problem.
Micro-stepping reduces the effective step size of a motor by whatever divisor we choose. However, it produces only 60% of the torque of full-stepping, requires additional hardware, and increases the work required of the motor controller software. If, for example, a motor's full-step travel comprises 20 steps, the controller writes 20 events into event memory (see Software Implementation Report 62 topic Interrupt Service Routine [reports- StepperIsr]); but to travel the same distance with divide-by-8 micro-stepping, the controller must write 160 events. Consequently, we want to use micro-stepping only where it is actually needed. In many cases, simply choosing a motor with finer native step resolution solves positioning and movement problems. However, there are cases where micro-stepping is the best solution, as is the case with the RSH robot's theta axis.
DESIGN
For full-stepping, the two windings of a two-phase motor are driven at maximum selectable current (the PBL3717 chip provides low, medium, and high) in a rotating 4-state pattern. The stepper control ISR generates this pattern, with each change occurring at a time determined by the desired step rate. Micro-stepping is achieved by varying the current in the windings between phase changes.
Although it would be possible for the ISR to generate the current settings for each winding in addition to the phase control signals, such an approach would require significant changes in our motor control system. Instead of consuming four scan chain bits, each micro-stepping motor would consume 10 bits for divide-by-8 and the ISR would be considerably more complicated.
An alternative is to simplify the software-based controller, generating only direction and step clock and letting hardware translate these signals to the appropriate voltages and phase patterns. A simple circuit, comprising mainly a binary counter for voltage generation followed by a two-bit gray counter for phase control, is sufficient. Software sets the direction bit at the beginning of a move and then just toggles the step control bit to advance the translator. Relying on hardware for translation allows other kinds of motors to be supported without changing the control software. Three-phase synchronous motors, for example, have similar control requirements but provide greater and more uniform torque than micro-stepping.
IMPLEMENTATION
The motor control scan chain is unchanged. Each motor is provided four control signals: phase A, phase B, I0, and I1. For both full-step and micro-step (synchronous) motors, I0 and I1 set the motor's current at off, low, medium, or high. For full-steppers, phase A and phase B set the direction of current flow in the motor's two windings. For micro-steppers (arbitrarily) phase A provides the step clock while phase B indicates direction of rotation. The translator circuit controls the actual phase A and B (phase inputs of each of two PBL3717s or equivalent circuit). The translator converts both rising and falling edges of the step clock ("phase A") into state machine clocks so that software only needs to change the state of A to effect a step. The program doesn't have to produce a pulse, which would require twice as much work.
The motor control ISR needs to know whether a motor is a full-stepper or micro-stepper in order to know how to manipulate its control signals. For a micro-stepper, when the motor begins a move, the direction bit ("phase B") can be set or cleared and subsequently ignored. A motor begins its move in the MS_START state, which is the most obvious point for the ISR to write the direction bit into the event page. There is a subtle disadvantage to this approach. The ISR processes the motor's START state in the MOTOR STATE SCAN LOOP, which iterates once through the motor list. The power is set for each motor that is starting. Setting a motor's power requires two additions to the event page, one for its I1 and one for its I0. Since no single events precede this, the program has been able to write each power event pair as a word, which is twice as efficient as writing two bytes. However, the possibility of single-bit direction events for micro-steppers removes the certainty that the event page location is word-aligned, forcing the program to write start power as two bytes.
There are several ways to avoid setting micro-steppers' direction bits in the MOTOR STATE SCAN LOOP. One would be to set them at the on each motor's first step in the PAGE STEP SCAN LOOP. This would be a poor approach, as the ISR would have to check each step to determine whether it was the motor's first one. A better alternative would be another loop through all motors after MOTOR STATE SCAN LOOP and before PAGE STEP SCAN LOOP. This would have to repeat the iteration through the entire active motor list but it could be skipped if the MOTOR STATE SCAN LOOP determined that no micro-steppers were starting.
The simplest approach is to set micro-steppers' direction bits in the MOTOR STATE SCAN LOOP, taking advantage of the existing loop and state switch. Since the inefficient power bit programming occurs only when motors start, which is relatively infrequently, the simple approach has been implemented.
The ISR has to know whether a motor full-steps or micro-steps. An mtype element has been added to the Stepper structure. It is assigned either MTR_FULLSTEP or MTR_MICROSTEP. Other than the means of making this assignment, the motor control functions, such as move and stop, don't need to know the type of a motor. The move function assigns stepIdx a value between 0 and 3 for PLUS moves and between 4 and 7 for MINUS moves of full-steppers. The ISR can clear a micro-stepper's direction bit ("phase B") if stepIdx is less than 4 and set it otherwise. In the PAGE STEP SCAN LOOP, when the ISR determines that a motor is to take a step, it must write the phase pattern for a full-stepper or toggle the step bit for a micro-stepper. Events are scheduled by writing a bit address (b0-6) plus state (b7) into the event page. The most efficient way to toggle a micro-stepper's step bit is to keep a byte comprising the most recent bit address plus state in the motor's descriptor (Stepper structure). While it might be possible to use one of the rotate pattern bytes for this purpose, it is less complicated to simply add another byte to the descriptor. This is the purpose of the pulse element. For each motor, pulse is initialized with the bit address of the motor's phase A bit. At each scheduled step, b7 of pulse is toggled and the resulting value is written into the event page.
msmapp- stpmtr.c
enum { MTR_FULLSTEP, MTR_MICROSTEP }; /* Stepper.mtype. Only the ISR and * initMotor function are aware of this. The move, stop, etc. functions are the * same whether the motor is a full-stepper or micro-stepper (or other * synchronous motor). */ typedef struct { /* Permanent values for this motor. */ UCHAR rotate[8]; /* 4 PLUS followed by 4 MINUS rotation patterns. */ USHORT offPow; /* Motor off power pattern. */ USHORT hardStopPow; /* Programmable values. */ UCHAR mtype; /* MTR_FULLSTEP, MTR_MICROSTEP */ UCHAR pulse; /* Pulse bit pattern if MTR_MICROSTEP. */ #define STEPPER_INIT(M) \ { PHASES(M), /* rotate */ \ MPWR(M,USP_OFF), /* offPow */ \ MPWR(M,USP_HIGH), /* hardStopPow */ \ MTR_FULLSTEP, /* mtype */ \ M*4 + MTR_PABIT + 0x80, /* pulse (if MTR_MICROSTEP) */ \ * Function: stpmtrIsr * Description: Stepper control state machine. Interrupt Service Routine * activated by the FPGA normally at page toggle (it can also be on page * overrun or scan chain integrity test failure). ... void interrupt stpmtrIsr() { #define AddPowerEvent(P) *evPtr++ = HIBYTE(P), *evPtr++ = LOBYTE(P), cnt += 2 UCHAR *evPtr; /* motor event page pointer. */ Stepper *mp; ... evPtr = ( isr & SISR_MTRPAGE ) ? page0Events : page1Events; ... /* MOTOR STATE SCAN LOOP ... for( mp = loMotor ; mp <= hiMotor ; mp++ ) { switch( mp->state ) { case MS_START: if( mp->mtype == MTR_MICROSTEP ) { /* Set direction bit according to initial stepIdx passed by * move command processor. Using stepIdx in this way hides whether the motor is * microstepper or full stepper from the move command processor. Use this * motor's rotate[0] pattern, which normally clears PHB, to get the bit address * with state bit clear. */ *evPtr++ = mp->stepIdx < 4 ? mp->rotate[ 0 ] : mp->rotate[0] + 0x80; cnt++; } ... /* PAGE STEP SCAN LOOP ... for( mp = loMotor ; mp <= hiMotor ; mp++ ) { ... if( mp->nextStep == pageStep ) { /* This motor has an event at the earliest step determined by previous scan. */ ... /* If the motor is a full-stepper then write the next gray code pattern bit. * If it is a micro-stepper then toggle the step bit. In both cases only one * bit at a time changes. With a micro-stepper it is always the PHA bit, which * is used as the step strobe. With a full-stepper, it alternates between PHA * and PHB. Micro-steppers use PHB for direction. */ if( mp->mtype == MTR_FULLSTEP ) { *evPtr++ = mp->rotate[ mp->stepIdx ]; /* Write this event. */ /* Advance rotation pattern index. 0-3 if PLUS or 4-7 if MINUS. */ if( ( mp->stepIdx & 3 ) == 3 ) mp->stepIdx &= ~3; else mp->stepIdx++; } else *evPtr++ = mp->pulse ^= 0x80; cnt++; /* Count this step event. */ } /* This motor has an event in earliest active slot */
Whether a motor is a full-stepper or micro-stepper (or other kind of synchronous type) is determined by the hardware interface. As with all other application-defined system features, the types of motors should not be embedded in the motor control program. This information should be entered by instrument hardware designers into the configuration file (analyz.ini).
Some means is needed for transferring motor types from the configuration file to the motor controller. In all cases, the file is scanned when scripts are compiled. The information must be transferred into either each motor move command (other commands, such as move and position, don't require the controller to know the motor type) or into some kind of initialization command that the controller executes prior to any moves of micro-steppers (MTR_FULLSTEP is the default assignment of mtype). The former has the advantage that the controller is inherently ready for moves of either type of motor. The latter has the advantage of efficiency due to the motor type interpretation and assignment occurring only once rather than on every move command.
The initialize command approach has been chosen. That an initialize command must be executed is a relatively minor drawback. Other motors, specifically TriMotors (e.g. shear valve) require initialization. If other purposes arise for an initialize synchronous motors command, the infrastructure will already be in place.
TriMotors are individually initialized because each one has a substantial unique definition. For steppers, the only unique initialization characteristic is whether a motor is a full-stepper or micro-stepper, a detail that we would prefer to hide from scripts. Consequently, the source level command should be to initialize the entire stepper control system rather than individual motors. An obvious syntax for this is initialize steppers. The compiler could translate this command into one message for each motor, but a list of micro-steppers can easily fit into one message. In the unlikely event that substantially more initialization information becomes necessary, we can easily change to one message per motor. The important points are:
Scripts are exposed only to the fact that the stepper control system in general needs to be initialized.
The identity of a motor as full- or micro-stepper is established before any motor function commands are executed.
To identify a motor as full- or micro-stepper in the configuration file, we could add an attribute keyword to the basic motor declaration, such as:
Theta = 4 SYNCHRONOUS; Out = -, In = +. Full travel = 105.However, each real motor may appear in more than one declaration, for example M4 = 4 as well as Theta = 4. To avoid duplicating the motor type declaration or having one motor alias declared as SYNCHRONOUS and not other aliases of the same motor, we can use motor type declarations within a STEPPER_MOTORS section. The syntax is:
SYNCHRONOUS = list
where list is one or more numbers in a comma-delimited list of motor numbers. The first stepper is numbered 0. The MSM unit supports up to 20 steppers. There may be multiple SYNCHRONOUS declarations and they may appear anywhere in a STEPPER_MOTORS section. For example, the declaration SYNCHRONOUS = 9,12,19 is equivalent to the three declarations SYNCHRONOUS = 9, SYNCHRONOUS = 12, and SYNCHRONOUS = 19.
analyz.ini
STEPPER_MOTORS UNIT = MSM STEP_RATE = 32605 ; Stepper driver fundamental rate (1/step period = steps/second). SYNCHRONOUS = 9 M0 = 0 ; MSM J17 output, J15 flag 1, J16 flag 2.
To simplify supporting multiple SYCHRONOUS declarations, the script compiler has one global array, syncMotors, that tells the type of the each motor. For example, if stepper number 9 is synchronous, syncMotors[9] is TRUE (non-0). SyncMotors contains MAX_STEPPER + 1 elements. MAX_STEPPER is defined as 29 to support up to 30 steppers in one system. This could be increased without penalty other than increasing the static memory consumed by syncMotors.
fcomp- cfgsys.h
#define MAX_STEPPER 29 UCHAR syncMotors[ MAX_STEPPER + 1 ]; /* Non-0 identifies the corresponding stepper as synchronous. */
The configuration file parser calls procStepper for each association pair it finds in a STEPPER_MOTORS section. If the association pair is a SYNCHRONOUS declaration, procStepper assigns 1 to each element in syncMotors listed in the declaration. Nothing more is done with syncMotors until a script is compiled. Although intended to support compiling an initialize steppers statement, syncMotors contains sufficient information for generating a move command with embedded motor type identification should we change the decision regarding how to convey this information to the motor controller.
fcomp- cfgsys.c
int loadAnalyzerCfg( void ) { ... while(( tokCnt = getAssocToken( fp, tok1, tok2 )) > 0 ) ... switch( scanState ) ... case TOK_STEPPER_MOTORS: procStepper(); break; /**************************************************************************** * Function: procStepper * Description: Process one stepper motor declaration or unit, step_rate, or * synchronous declaration in STEPPER_MOTORS block. ... void procStepper( void ) { static char stepperItems[] = "STEP_RATE\0SYNCHRONOUS\0"; enum { STEP_RATE, SYNCHRONOUS }; char *cp; int idx; if( !isUnitDef( NEEDS2 )) switch( indexSuperString( tok1, stepperItems, CASE_INSENSITIVE )) { ... case SYNCHRONOUS: for( cp = strtok( tok2, "," ) ; cp ; cp = strtok( NULL, "," )) { idx = atoi( cp ); if( idx > 0 && idx <= MAX_STEPPER ) syncMotors[ idx ] = 1; } break; default: newMtr = addMotorAlias( MT_STEPPER ); if( newMtr->motorNumber >= MAX_STEPPER ) cfgError( -1, "Illegal stepper motor number %d. Max is %d.\n", newMtr->motorNumber, MAX_STEPPER ); break; } }
As explained in Software Implementation Report 59 topic Strobed Fluid Sensors- Implementation- Script Compiler [reports- InitializeStatement] the initialize statement has been made very general to support a variety of devices. The keyword "steppers" is added by string comparison in isCmdParm. It is not added to the lexical state machine and is, therefore, not reserved.
The various initialize statements compile to unique command messages because the requirements of the different supported devices vary widely. For initialize steppers, whether to implement a message specific to steppers or applicable to all motors is a fairly arbitrary choice. Initialize TriMotors already exists as a unique message type and can't be merged with a general motor control system initialization message because each TriMotor is individually initialized. Steppers are the only other motors that need initialization. Nevertheless, to provide a framework supporting other types whose control system may require initialization, a general message format has been implemented. The penalties for this approach are increased syntactic complexity and the need for motor type identification by the interpreter.
fsqapi.h
enum { /* Command messages. */ ... /* Motor group (except for data transmitting test commands). */ ... CM_INITMOTOR, /* InitMotorCmd */ CM_WAITMOTOR, /* WaitMotorCmd */ CM_INITTRIMTR, /* InitTriMtrCmd */ enum { INITMOTOR_STEPPERS = 'A' }; /* InitMotorCmd.mtype */ typedef PACKED struct { UCHAR mtype; union { PACKED struct { UCHAR cnt; UCHAR syncMotors[1]; } steppers; } parms; } InitMotorCmd; /* CM_INITMOTOR */ typedef union { ... InitMotorCmd initMotor; ... } FsqMsgArg;
The extensible array "syncMotors" in the InitMotorCmd structure lists each synchronous motor by number. This differs from the compiler's syncMotors global array, which tells, for each motor whether it is synchronous. The rationale for this difference is that the fixed array in the compiler enables separate SYNCHRONOUS declarations to merge into a single object, whereas the extensible array in the message structure reduces the size of the message while increasing its ability to handle a larger range of motor numbers. To increase the number of supported steppers, the compiler's syncMotors array needs to be enlarged. The function writeInitSteppers converts the contents of the compiler's array into the message's extensible array.
fcomp- fsqsup.c
int isCmdParm( void ) { NamedDevice dp; if( cmdType == T_INITIALIZE ) { if( stricmp( yytext, "STEPPERS" ) == 0 ) writeInitSteppers(); void writeInitSteppers( void ) { UCHAR idx; UCHAR *ucp; ucp = fmsg.arg.initMotor.parms.steppers.syncMotors; for( idx = 0 ; idx <= MAX_STEPPER ; idx ++ ) if( syncMotors[ idx ] != 0 ) *ucp++ = idx; if(( fmsg.arg.initMotor.parms.steppers.cnt = ucp - fmsg.arg.initMotor.parms.steppers.syncMotors ) != 0 ) { fmsg.msgType = CM_INITMOTOR; fmsg.arg.initMotor.mtype = INITMOTOR_STEPPERS; writeFsqMsg( ucp - (UCHAR *)&fmsg.arg ); } }
CM_INITMOTOR is inserted at the end of the stepper command group in the command message types enum to simplify the dispatch table in the interpreter's fsqman.c module a bit. The interpreter, procInitMotor, is placed in the stepper control module stpmtr.c. If the InitMotor message is extended to support other motor type systems, procInitMotor will be moved to cmdmotor.c and its dispatch entry pulled out from under the control of HAS_STEPPER.
msmapp- fsqman.c
packed struct { CmdFunc proc; UCHAR id; /* Use to detect out-of-order table. */ } cmdTable[] = { ... /* Motor group (except for data transmitting test commands). */ #ifdef HAS_STEPPER { procPosition, CM_POSITION }, { procRampdef, CM_RAMPDEF }, { procRamp, CM_RAMP }, { procPower, CM_POWER }, { procWaitStepper, CM_WAITSTEPPER }, { procInitMotor, CM_INITMOTOR }, /* Currently only steppers */ #else { dumProc, CM_POSITION }, { dumProc, CM_RAMPDEF }, { dumProc, CM_RAMP }, { dumProc, CM_POWER }, { dumProc, CM_WAITSTEPPER }, { dumProc, CM_INITMOTOR }, #endif
msmapp- stpmtr.c
/**************************************************************************** * Function: procInitMotor * Description: Identify the given motor as a micro-stepper or other type of * synchronous motor, such as 3-phase synchronous, which depends on hardware- * based direction + step translator. ... * ........................... notes ....................................... * - Currently, this command applies only to steppers. However, in the future * the generic InitMotor command may apply to other types, in which case, this * stepper function will be renamed and the procInitMotor converted to a front- * end for the specific types. *..........................................................................*/ int procInitMotor( FsqMsg *cmd, ProcDef *pd ) { int idx; UCHAR mtr; for( idx = 0 ; idx < cmd->arg.initMotor.parms.steppers.cnt ; idx++ ) { mtr = cmd->arg.initMotor.parms.steppers.syncMotors[ idx ]; if( mtr > DIM( steppers )) return sendScriptFailMsg( pd, FAIL_BADPARM, FLINE, "Overrange stepper ID" ); steppers[ mtr ].mtype = MTR_MICROSTEP; } return CMD_DONE; }
TESTING
Initialize Stepper In Script
rsh.f
/***************************************************************************** * MICRO-STEPPING THETA *****************************************************************************/ define Theta M9 begin HomeRackPickerM echo "Begin HomeRackPickerM" initialize steppers // Tells controller that M9 is synchronous. Test ProcInitMotor Overrange Motor ID analyz.ini STEPPER_MOTORS UNIT = MSM STEP_RATE = 32605 SYNCHRONOUS = 9,29 Enter interactive command INITIALIZE STEPPERS MSM echo response is: <8=~~~~~~EfUnit2,Master,Overrange stepper ID,stpmtr.c@1629
{} To top of [MicroStepping]
[NextTopic] [MainTopics]
As explained in implementation report 65 topic Microstepping Motors [InitializeSteppers] a stepper motor controller must execute the command initialize steppers before it can control any synchronous (micro-stepping) motors. It's easy to remember to include this in a homing script but harder to remember to invoke the command interactively when developing hardware or a new script. If the controller has not executed the initialize command, it will manage a synchronous motor as if it were a full-stepper.
Adding to the confusion during interactive development was that simply entering the command initialize steppers usually failed. The script compiler was not generating a remote version of this command. Since the APU is normally the first IML unit to log on to the debugger, it is the default unit for interactive commands. The MSM can be selected from the Target menu but we usually don't bother to do this to send commands to the MSM because most interactive commands involve manipulating uniquely owned devices, for which the compiler generates a message directed toward the owner regardless of the default target. However, steppers is a generic term that doesn't necessarily associate with a specific owner. If a system contains only one stepper controller, then the compiler could surmise that the command must be directed toward that unit but otherwise it has to assume that the script execution unit (the "default" unit for interactive commands) must execute the command.
One way to improve the interactive situation would be to support a unit name in the statement, for example initialize MSM steppers, which the compiler was not doing, under the assumption that the statement would only appear at the beginning of a homing script, which should not be executed remotely anyway. More convenient for the user of the debugger would be for the compiler to automatically direct the command to the stepper controller if there is only one. Then initialize steppers would always work as an interactive command. However, the compiler should also accept a unit name in the statement for debugging a system with multiple stepper controllers. These two changes serve only to simplify interactive debugging; in scripts, the statement should appear in a local homing sequence, and interactively the target can be selected via the Target menu.
IMPLEMENTATION
Context-sensitive scanning of execution unit name was previously implemented for other commands. The mechanism was reused for initialize steppers to accept a unit name. Doing this revealed a design error in the implementation of the special command context, EXPECT_COMMAND, which was described in report 15 topic Configuration Memory Read And Write \ Design \ Script Compiler [ExpectCommand]. In the classifySymbol function, testing for EXPECT_COMMAND preceded and hid all other contexts. This precludes naming a unit for a special command, which is acceptable for all uses of the command context developed thus far. However, this is supposed to be a general-purpose mechanism and should not be restricted only to commands in which a unit can't be named for reasons other than a compiler limitation. Context testing was rearranged to prevent EXPECT_COMMAND from hiding EXPECT_UNIT.
fcomp- fsqyac.y
command => T_INITIALIZE { cmdType = T_INITIALIZE; expect = EXPECT_COMMAND | EXPECT_UNIT; } unitSelectOpt T_DUMMY /* Scanner outputs command message (see fsqsup.c-isCmdParm) */ unitSelectOpt : T_UNITNAME { cmdUnit = selectedUnit = yyvsp[0].ui; /* Output generators can check selectedUnit to determine whether to override default. If not UNIT_NONE (80) then the statement contains explicit unit. */ } | /* Empty */ ;
fcomp- fsql.c
int classifySymbol( void ) { ... if( expect ) { if( expect & EXPECT_UNIT && ( yylval.ui = indexSuperString( yytext, unitNames, CASE_CHEAPEST )) != -1 ) return T_UNITNAME; if( expect & EXPECT_COMMAND ) return isCmdParm();
To provide single-stepper-controller initialize steppers default unit, the configuration file parser was modified to identify a default unit in a new global variable "stepperUnit". StepperUnit is initially STEPPER_UNIT_NONE, which indicates that no stepper controllers have been declared. At the first STEPPERS declaration, stepperUnit is assigned the control unit's number. At any subsequent STEPPERS declaration, unless the unit is the same as the first, stepperUnit is assigned STEPPER_UNIT_MULT, indicating that more than one unit controls steppers.
When the initialize steppers command message is generated, if no unit is named in the statement and stepperUnit contains a valid unit number, the message is directed toward that unit. Any unit number less than UNIT_NONE is valid.
fcomp- cfgsys.h
#define UNIT_NONE 80 // Any unreasonable number. #define STEPPER_UNIT_NONE 0xFF #define STEPPER_UNIT_MULT 0xFE ... UCHAR stepperUnit = STEPPER_UNIT_NONE;
fcomp- cfgsys.c
void procStepper( void ) { ... if( stepperUnit == STEPPER_UNIT_NONE ) stepperUnit = unitNum; // Default stepper controller. else if( stepperUnit != unitNum ) stepperUnit = STEPPER_UNIT_MULT; // No default controller because more than one.
fcomp- fsqsup.c
void writeInitSteppers( void ) { ... if( selectedUnit == UNIT_NONE && stepperUnit < UNIT_NONE ) cmdUnit = stepperUnit; /* If statement doesn't specify a unit and * there is a default controller (i.e. only one unit owns steppers) then send * message to that unit. */
During development of the RSH control scripts, it has frequently been necessary to stop scripts before they finish, often leaving some of the motors in powered idle states. Some of the mechanisms, such as the robot vertical, are quite difficult to move by hand when the motor is powered. We frequently want to do this at script breakpoints in order to reset the mechanism or to examine clearances. As explained in Report 64 [Stepper Idle Power Change] a motor's idle power can be immediately changed when it is idling. However, the command is fairly complicated and inconvenient, especially if it has to be repeated for multiple motors. A single command to turn off the power to all motors would be helpful.
DESIGN
Since the purpose of this requirement is to remove idle power, which should normally be safe if applied indefinitely, motor and circuit safety is not an issue as it was in the case of stopping the motor controller as described in Report 71 [Hot Motor Drivers At Breakpoint]. Instead, the only purpose is to allow the mechanisms to be moved easily by hand, which means that we cannot continue executing the script because the motor position will be lost. If we intended to continue after changing a mechanism's position, we would have to use interactive move commands. Therefore, there is no need to have an independent command simply to remove idle power if the function could be folded into a more general motor function.
The command initialize steppers was developed to provide a means of telling a motor controller which motors to control by step plus direction vs. gray code. In general, the former have microstepping drivers while the latter have full step drivers. However, it is reasonable to use this command for other motor status control, such as to reset all motors to their default state, which includes that no power is applied. In addition to serving the purpose of making mechanisms easier to move by hand, it also makes most mechanisms movable by command because the default ramp (assigned to all motors) is slow and gentle.
IMPLEMENTATION
As described in Report 65 [Microstepping Motors] the initialize steppers command interpreter procInitMotor originally took a list of motor IDs from the command message and set the type of each to SYNCHRONOUS. To add full motor system initialization, we only need to call prepSteppers first. However, prepSteppers also allocates event memory in addition to initializing the motor structures. To cause memory to be allocated only at full program initialization, the reinit argument has been added. Because INITIALIZE vs. REINITIALIZE may have more general utility, they have been defined as an enum in the general header proc.h. The MSM and APU share proc.h. They also share motor.h to provide support for remote motor queries (from APU to MSM) although many of the definitions in motor.h are used only by the direct motor controller.
msmapp- stpmtr.c
msm/apuapp- proc.h
enum { INITIALIZE, REINITIALIZE }; msm/apuapp- motor.h void prepSteppers( int reinit ); /* stpmtr.c */ void prepSteppers( int reinit ) { ... /* ..... Reserve RAM for stepper event pages ............*/ if( reinit == INITIALIZE ) { page0Events = (UCHAR *)getMem( STEPPER_COUNT * 256, 0x2000 ); page1Events = (UCHAR *)getMem( STEPPER_COUNT * 256, 0x2000 ); } /* ..... Initialize semi-static stepper elements ........*/ int procInitMotor( FsqMsg *cmd, ProcDef *pd ) { ... prepSteppers( REINITIALIZE );
DOCUMENTATION
cdx\doc\fsq\cdsref.doc MISCELLANEOUS GROUP - INITIALIZE- Initialize Steppers [cdsref- InitializeSteppers].
When the controller executes initialize steppers, in addition to configuring support for synchronous motors, it establishes default power and ramp settings for all motors. These are identical to the settings established when the motor control unit is reset. Typically, this statement is the first one in a homing script and serves only to define the synchronous motors. However, during instrument development, the secondary effect of resetting the motor control system without resetting the unit can be useful. For example, if a script stops at a breakpoint leaving a motor in an idle state with power on, the motor may be difficult to move by hand. An interactive initialize steppers command turns all idle power settings to off.
The initialize steppers command is applied to the entire stepper control system. Each motor individually requires script-based homing and setting of position prior to effective use. After this, the command should not appear in normal scripts, as its execution will wipe out all application motor settings.
{} To top of [InitializeSteppers]
[NextTopic] [MainTopics]
The recently demonstrated RSH (Random Sample Handler) script includes homing of all motors. When the X-axis motor was homed it noticeably and unexpectedly banged into the end of the frame on the move to initially find the home flag. The script was as follows:
begin HomeX power TransX TransXpower ramp TransX = TransXramp move TransX +2500 loop for 10 seconds if TransXhome Home break endif endloop stop TransX
A review of the stop command description in cdsref.doc [cdsref- StopMotor] reveals why the motor continued past the flag. Stop motor causes a soft stop by engaging the motor's down ramp. This ensures against step loss. Stop motor hard causes an immediate stop by freezing the current winding excitation and applying the motor's hold power. Often the motor cannot stop instantly, resulting in step loss. Consequently, we would not want to use this form of stop in normal operation.
The TransXramp used for this initial home search is defined as follows:
rampdef TransXramp up 200 to 500 linear 5% slew 500 down 500 to 200 linear 5%
If the script file (rsh.f) is compiled by the command line fcomp rsh.f -r, the script compiler generates a ramp list file, ramp.lst, which lists each step of TransXramp as follows:
--- Rampdef TRANSXRAMP --- UP: 163, 155, 148, 141, 134, 128, 122, 116, 111, 106, 101, 96, 91, 87, 83, 79, 75, 72, 68, 65, time=0.065665. DOWN: 65, 68, 72, 75, 79, 83, 87, 91, 96, 101, 106, 111, 116, 122, 128, 134, 141, 148, 155, 163, time=0.065665.
The down ramp segment contains 20 steps but there are only 13 steps between the flag (at initial detection when moving from right to left) and the frame.
At this speed TransX probably has enough momentum to travel a few steps even after stop TransX hard but it will not lose as many as 13 steps. Since both stop types probably lose steps, the only advantage of the hard stop is that it avoids crashing the robot into the frame. In either case, the homing process succeeds because the fast initial flag search is followed by a much slower local search. Crashing the robot into the frame doesn't really hurt anything but it looks bad and misleadingly suggests that the home flag is not used.
The script was changed to use a hard stop. Additionally, the stop command was moved into the loop to cause it to execute slightly sooner, although this makes very little real difference.
loop for 10 seconds if TransXhome Home stop TransX hard break endif endloop
{} To top of [MotorHomeStop]
HOT MOTOR DRIVERS AT BREAKPOINT {}
[NextTopic] [MainTopics]
In diagnosing the ramp replacement bug described in the preceding topic, the BDM debugger was used to trace the ramp heap management functions. Breakpoints were set at various places in the code. When they were hit, the CPU stopped servicing the motor ISR, function stpmtrIsr. This frequently left the motor drivers in a high power static state, causing them to get hot. We have several types of drivers. Those with FET transistors should survive the heat fairly well but those with bipolar transistors may fail due to secondary breakdown (where the heat produces minority carriers, which increase conduction).
Unless the ISR is running, we have no means of changing the contents of the motor scan chain and reducing the power levels. The current version of the MSM does provide random access to the scan chain but the access means is not very convenient and we have indicated that the facility could be deleted to save FPGA resources for more useful features. Even if we were to use the random access capability, we would not want to have to modify the scan chain by hand at every breakpoint. Therefore, no matter what means is used to reduce the power levels, we would want to invoke a function to do it and then break. The setIdle function was developed for this purpose.
msmapp- stpmtr.c
void bomb( void ) {} /**************************************************************************** Function: setIdle Description: Set all motor's to their IDLE power. This is not use for normal operation but is called by temporary debugging code used to trigger a breakpoint in the BDM debugger. If we simply break, motor drivers may be in a high power state and if they stay that way for very long they may be damaged. This function sets each motor's state to MS_IDLEPOWER, which causes it to set the idle power at the next motor page interrupt, which is the quickest way we can do it. To make sure that the ISR has a chance to make the change, this function delays for more than the 8 msec. page time and then calls bomb. We can set a breakpoint on bomb and then trace back to the caller. void setIdle( void ) { Stepper *mp; ULONG timer; for( mp = loMotor ; mp <= hiMotor ; mp++ ) mp->state = MS_IDLEPOWER; timer = setAlarmTime( 3 ); /* 10-15 msec. */ while( !isAlarmTime( timer )) ; bomb(); /* Set breakpoint on bomb. */ }
{} To top of [HotMotorDriversAtBreakpoint]
SHORT MOTOR MOVE WITH ASYMMETRICAL TRAJECTORY {}
[NextTopic] [MainTopics]
REQUIREMENT
Rathna has reported that the old blood she is getting is too fragile to be mixed by any of our standard mechanical mixers, so she has been mixing it by hand. This repetitive motion may present a health hazard and human-introduced variations may influence results. To combat these problems, a programmable single-tube mixer has been made from a CD3200 mix motor. The MSM controls the mixer through one of its spare stepper interfaces.
Mixing is accomplished by a script, mix1.f, that Rathna can modify to control the speed, angles, and cycle count. The initial version of this script includes a very asymmetrical motor trajectory, defined by the statement:
RAMP M3 UP 80 TO 120 LINEAR 25% SLEW 120 DOWN 120 TO 80 LINEAR 1%
By compiling mix1.f with the command line fcomp mix1.f -r, a ramp list was generated, which showed that the up ramp contained 3 steps and the down 45. This is unusual. More typically, the up ramp is longer than the down ramp. The reason for this is that ramps are usually lengthened to reduce acceleration in order to avoid step loss, which is more likely to occur during acceleration than deceleration. In the case of mix1, slow deceleration was needed to avoid jostling the tube. This was not motivated by any theory but by the simple observation that the tube was mechanically stable at relatively rapid acceleration but bounced at all but the gentlest declarations.
The script moved the motor 60 steps in each direction, rocking the tube through a 108-degree arc. To demonstrate a gentler mix, the move was reduced to 10 steps. Motor control failed. Testing revealed that any move less than 45 steps failed. The motor controller is supposed to produce reasonably good trajectories for moves less than the total number of steps in a motor's assigned ramps.
DESIGN
The motor controller was designed to truncate the up and down ramps symmetrically when a move contains fewer steps than the two ramps combined, as explained in Report 62 [reports- MovingMotors]. A review of that implementation reveals why short moves failed in this case but not previously. According to that scheme, if the down ramp contains more steps than half of the move, it is truncated to half of the move steps. The up ramp is given the remainder. As long as the up ramp is longer than the down ramp, which is usually true, both truncated ramps comprise valid subsets of the normal ramps. However, if the down ramp contains more steps than half of the move and the up ramp contains fewer than half, the "truncated" up ramp is assigned a step count greater than the number of steps in the real ramp itself, instructing the motor controller to treat random data as steps.
The problem that led to complete motor control failure is easily corrected by the following algorithm:
The new algorithm avoids the outright failure possible in the old approach but also does a poor job with significantly asymmetrical trajectories. It tries the balance the number of steps in the up and down ramps. Any asymmetry between the original ramps shows up as an unexpected difference between the step rate at the end of the up ramp and the beginning of the down ramp. This would be most problematic with a shallow up ramp and a steep down ramp, as this requires the motor to accelerate in the transition.
The only way to determine the number of steps in each of the truncated ramps that provides the closest match between the last up step and the first down step is to search the two ramp lists from both ends of the trajectory (i.e. forward from the beginning of the up ramp and backward from the end of the down ramp) toward the nearest matching steps. The basic algorithm is:
The matching step search always yields the best possible ramp truncation, but it incurs significantly greater run-time cost than the symmetrical truncation formula. Consequently, we would rather engage it only when its superior results may actually be important, that is when the trajectory is significantly asymmetrical. A reasonable threshold could be when either ramp contains 50% (or more) steps than the other. Since this determination would have to be made every time the motor controller is preparing a short move, the run-time cost can be reduced by moving the actual asymmetry calculation to a less frequently traversed area of the program. A good time is whenever an up or down ramp is assigned to a motor. At that time, a flag in the motor's descriptor can be set if the trajectory is determined to be sufficiently asymmetrical to require the more expensive ramp truncation procedure for small moves.
Further run-time cost reduction might be afforded by caching the results of the search so that if a particular motor has only one short move, the up and down step counts for that move will only have to be determined one time. However, if the motor has more than one short move (unless all of its short moves have the same number of steps) the cache will never be usable yet the cost of caching will be borne by every short move. Consequently, it was determined that this was not worthwhile.
IMPLEMENTATION
msmapp- stpmtr.c
typedef struct { ... /* Initialized elements. */ UCHAR asymTraj; /* Asymetrical trajectory. Used to select truncation type for short moves. */ ... } Stepper; int procRamp( FsqMsg *cmd, ProcDef *pd ) { ... if( assign & RAMP_UP + RAMP_DOWN ) { upCnt = mp->perRamp[ RIDX_UP ].cnt; downCnt = mp->perRamp[ RIDX_DOWN ].cnt; mp->asymTraj = upCnt > downCnt + downCnt / 2 || downCnt > upCnt + upCnt / 2; } short startStepper( Stepper *mp, USHORT relAbs, USHORT byto ) { ... if( distance < rampTotal ) { ... if( mp->asymTraj ) { idx = 0; idx2 = downCnt - 1; for( us2 = 0 ; ; us2++ ) { /* It is possible for this loop to end with upCnt or downCnt = 0. The ISR will not be confused by this. */ if( us2 == distance ) { upCnt = idx; downCnt = distance - upCnt; break; } if( mp->perRamp[ RIDX_UP ].src[ idx ] >= mp->perRamp[ RIDX_DOWN ].src[ idx2 ]) { /* Take the up ramp step because it is longer. */ if( ++idx == upCnt ) { downCnt = distance - upCnt; break; /* End of up ramp. It will be used in its entirety. */ } } else { /* Take the down ramp step becuase it is longer. */ if( idx2-- == 0 ) { upCnt = distance - downCnt; break; /* End of down ramp. It will be used in its entirety. */ } } } } else /* Symmetrical trajectory. Simple formula is OK. */ { us2 = distance / 2; if( downCnt < us2 ) upCnt = distance - downCnt; else if( upCnt < us2 ) downCnt = distance - upCnt; else { downCnt = distance / 2; upCnt = distance - downCnt;
{} To top of [ShortMotorMoveWithAssymetricTrajectory]
[NextTopic] [MainTopics]
REQUIREMENT
System Design Report 32 topic Optimal Stepper Control- Review and Suggestions- Review discusses MSM CPU bandwidth requirements [cdxsys- MsmCpuBandwidthQuestion]. Several alternative architectures are presented, among them continuing on the present course where the single MSM board controls all steppers, including those in the RSH. It was suggested that it would be difficult to determine the worst-case CPU bandwidth consumption of the motor ISR without testing scripts representative of the application.
Report 32 does not directly state how the consumption would be measure but it implies that we would simply see whether the scripts ran without problems. This is not really adequate. If the MSM CPU is running close to its limit, the uncertainties inherent in the application could cause intermittent problems. As explained in Software Implementation Report 62 [Motor Page Overrun Fault] the MSM program detects and reports when the motor interrupt has not been serviced in time. Since this interrupt has higher priority than all other CPU activities (other than system failures such as bus faults) it is the least likely failure point. Communication or script interpretation problems are more likely. If these were to occur randomly and infrequently, it could be very difficult to determine whether they were caused by CPU bandwidth constraints.
Whatever means is used to determine possible CPU bandwidth problems must have two features. It must be predictive; i.e. it should provide some measure of CPU utilization. This could be done with a series of increasingly demanding scripts to determine at which point the CPU can no longer service the motor. Properly designed, this experiment would be effective. However, it's difficult to say how the CPU load should be increased in a way that is representative of the application. We could cause a failure simply by demanding a few motors to step at 32,000 steps-per-second (the limit). A more effective approach would be to not try to cause a failure but to execute a more representative script and measure the bandwidth consumption of the motor ISR. This would afford a means of predicting the amount of other work could be expected of the CPU. The second desirable feature is a means of quickly estimating the reliability of any given configuration. For example, what impact would adding a particular motor to the loader have on the MSM's CPU?
The most effective way of providing the required features is to build into the MSM program a means of measuring the CPU bandwidth consumption of the motor ISR. Use in combination with aggressive but realistic scripts, this can tell us the likelihood of bandwidth problems. For example, if 75% or more of the CPU bandwidth might be consumed by the ISR under certain circumstances, other functions will suffer at these times. This would not necessarily mean that the MSM would be unable to perform all of its duties but it would be advisable to arrange script activity to avoid such "hot spots". If the consumption monitor is always active, test scripts would not be essential as they would be if the only means of determining CPU utilization were to increase the load until a motor interrupt failure occurred. The effect of a system change could be determined simply by running the instrument and checking the monitor.
DESIGN
Since the motor ISR executes repeatedly and entails significant processing, it is the best indicator of CPU utilization. All of the other motor functions, while complicated and demanding, execute infrequently and have little bearing on whether the MSM will be able to handle what we require of it. The FPGA asserts the motor interrupt after playing each page of 256 32-usec events. As explained in Software Implementation Report 62 topic Ramps And Trajectories [reports- StepAndDelayWidths] the MSM's FPGA actually generates a 30.67-usec event rate. This is reflected in the analyz.ini declaration STEP_RATE = 32605. Other reports, such as motor.doc topic Architecture- Motor Control Hardware [motor- MotorPageTime] state that the page period is 8.192 msec. This is based on the nominal 32-usec event timing. The MSM's actual page period, and therefore interrupt period, is 7.851msec. Since this period is fixed, the ISR bandwidth consumption is simply the execution time of one service period divided by 7.851msec.
While the average bandwidth consumption could be useful in predicting overall performance, it is the worst-case consumption of a single interrupt that predicts reliability. Further, the average can be reasonably determined from a set of worst-case samples. Finally, average is very hard to calculate on the fly because of diminishing precision as the number of samples increases. Consequently, only the worst-case will be monitored. The ISR must determine its own execution time. However, there is no need for it to perform the conversion to bandwidth consumption. To start a monitoring period, a longest time variable, isrExeTime, is cleared. Every time the ISR executes, it determines its own execution time and, if this is greater than isrExeTime, replaces isrExeTime. Subsequently, in response to a query, isrExeTime is divided by the page period to tell the bandwidth consumption.
To determine its own execution time, the ISR needs to refer to some kind of hardware-based timer. If the ISR owned the timer it could either start the timer at entry and read the count at exit or it could read a free-running timer at both points and subtract the two values to determine the execution time. The CPU has five timers: watchdog, periodic, timer 1 and timer 2. As explained in Software Implementation Report 57 [IML Clock] both the MSM and APU may use timer 2 as the IML clock when the PC communication link is not HSL. The ISR cannot interfere with the watchdog or periodic timers but it might read them. As illustrated in the MC68340 User's Manual chapter 4 (pages 4.24-4.27) the watchdog timer is not readable. The periodic timer can be read but it comprises only seven bits, which provide insufficient resolution. The only feasible approach is to use timer 1.
Because the cost of reading the timer is only slightly higher than starting it, dedicating timer 1 to the motor ISR is not warranted. Instead, it can be configured for continuous operation, much like the periodic timer but with more useful range and resolution. Then it may be used wherever we would like to precisely measure execution time, whether for performance monitoring or for generating short delays of a minimum guaranteed duration (maximum can be considerably longer due to interrupts).
To derive the most general benefit from timer 1, we want the greatest resolution and range that it can afford. As illustrated in the MC68340 User's Manual chapter 8 (pages 8.18-8.23) the timer (1 and 2 are identical) counter (CNTR register) comprises 16 bits but the total count can be extended to 24-bits by including the prescaler (SR registers). When the timer is configured to produce a symmetrical square wave, both the prescaler and counter count down, the prescaler from 0xFF and the counter from the PREL1 value. For maximum range and resolution, PREL1 is 0xFFFF. There are only two clock choices, external (which doesn't exist in our circuits) or the CPU clock (25MHz or 16MHz) divided by two. At the MSM's 25MHz clock rate, the count is in 80 nsec increments. This is greater resolution than required but not significantly so. The maximum time that can be measured is 80 nsec * 2^24 = 1.34 seconds, which is considerably longer than required. This tempts us to not incorporate the prescaler into the count, which complicates reading the timer, but the resulting 5.2-msec time would cover only 66% of the 7.851-msec page period. Additionally, to extend the timing facility to other situations, it is better to provide the greatest feasible range.
The first thing the ISR does is to read and save the timer value. There are several exit points in the ISR function, stpmtrIsr. Except for the last one, these all appear before the time-consuming step generator loop. If no motor is stepping or changing power (which also passes through the loop) one of earlier exits is taken. We don't care about these. Even if an interrupt process making an early exit happens to be the longest one, it will hold this title only briefly and the time it consumes is insignificant. Thus, there is no need to coerce all of the exits into one for the purpose of measuring every interrupt. It is sufficient to monitor just the last exit. At this point the timer is read again and this value subtracted from the value saved at the beginning of the ISR. The subtrahend and minuend are reversed from what might be expected because the timer counts down. If the result is larger than the current value of isrExeTime, it replaces isrExeTime.
We want to be able to read the worst-case motor ISR execution time at any time and not just under special instrumentation, such as a BDM debugger. Therefore a script command is needed. The general read memory command could be used to directly access isrExeTime but this would be deficient in many ways. For one thing, we would have to consult the link map (msmapp.map) to determine the address. Additionally, the conversion of the tick count to CPU bandwidth requires knowing the tick rate and motor page period. We know that for the current version of the MSM hardware and software these are 80 nsec and 7.851 msec, respectively, but they might be different in another version. Since these details are intrinsic in the MSM design it would be reasonable for the MSM program itself to perform the conversion to bandwidth as long as that can be done without placing a significant burden on the MSM. Primarily, this means foregoing floating-point operations, which the basic IML program specifically avoids in order to reduce program size and increase performance (by substituting scaled integer operations), and doing the conversion only upon request.
The script language's testSystem command affords an appropriate means of requesting the motor ISR's CPU utilization. As explained in the Script Language Reference [cdsref- TestSystem] one purpose of this command is to read resource utilization monitors in order to determine how much headroom exists, for example the maximum number of simultaneous processes or the maximum depth to which the stack has descended. The testSystem command requires at least one argument, which tells what to do. This argument is ultimately just a number and each IML unit may define its own unique tests. However, a number of standard tests are defined in the script language. Obviously, not all IML units are going to support the motor CPU utilization test, but it costs nothing to define the test as a standard. This only means that it is known to the compiler and presented to the user as an available command upon request. Any IML unit is free to reject a standard testSystem command that it chooses not to support. Since having the motor CPU utilization test defined as a standard is safer (for program implementation) and friendlier (for script developers) this will be done. The test will be called MotorCpu. The APU will reject it but the MSM will interpret it by calculating the worst-case bandwidth consumption and returning a type DM_SYS sub-type SYS_RESRC message to the PC. These messages are minimally structured. The significant information is presented as text to be read on-line (in the debugger's From Analyzer window) or in the echo log.
Most resource monitors are not changed by being read but are reset only when the program is reset. Thus, they always report the cumulative worst case. This is appropriate for monitoring program facilities, which are changed only by rebuilding the program. The motor CPU usage monitor is not in this category. We may find, for example, in writing a new script that we would like to know immediately the performance effect of moving multiple motors simultaneously. It should not be necessary to reboot to find this out. Therefore we would like to have a means of resetting the isrExeTime. The easiest way to do this is to assign it 0 in the testSystem MotorCpu interpreter.
IMPLEMENTATION
fsqapi.h (shared by fcomp and all app programs)
enum nativeTestSystemFuncs { TS_TRIMOTOR = 'A',
...
TS_REPORTDONES, TS_DEBUGFLAG, TS_MOTORCPU
}; /* Native testSystem functions (SysCmd.op). The order of these must match
* the compiler's testSystemFuncs list (in names.h) but the two lists don't have
* to begin at the same item. */
fcomp- names.h
The name MotorCpu is added to the script compiler's testSystemFuncs so that the compiler will display it in the "Native TestSystem Function" list, which is included in the "Words" display (debugger Edit menu Words item).
char testSystemFuncs[] =
"BusFault\0\
...
MotorCpu\0";
Both the MSM and APU configure timer 1 for high-resolution timing. Only the MSM's motor ISR will be using it now but it will be available for code execution (or other purpose) timing in any other situations in either unit, as using it does not alter its operation.
msm/apuapp- io.c
void prepPeriodicTimers( BOOL wantWatchdog ) { ... /* Configure timer 1 for program use as short timer or profiler. */ Timer1->cr = 0; /* Reset prior to configuring. */ /* PREL1 is loaded with 0xFFFF and prescaling is selected. The clock is 1/2 * system clock of 25MHz or 16MHz, which is 80 nsec. or 125 nsec. Interrupt * period is 80 or 125 nsec. * 65535 * 256 = 1.342 sec or 2.097 sec. Prescaler * and counter count down from 0xFFFFFF to 0. Read as 24-bit word at * Timer1->cntr by reading as ULONG and dividing by 8. */ Timer1->prel1 = 0xFFFF; Timer1->cr = 0x8608; /* Timer1 Control Register: * b15 SWR <- 1 = unreset. * b14-b12 IE2-1 <- 000 = no interrupt. * b11 TGE <- 0 = not gated by TGATE2. * b10 PCLK <- 1 = clock source is prescaled. * b9 CPE <- 1 = enable counter. * b8 CLK <- 0 = selected clock is system clock / 2. * b7-5 POT2-0 <- 000 = prescale tap = / 256. * b4-2 MODE <- 010 = symmetrical square wave. * b1-0 OC <- 00 = No output. */ /* testCodeTimer(); */
After configuring timer 1, prepPeriodicTimers may optionally call testCodeTimer to partially verify the timer's operation. Now that this has been done, it should not be necessary to test the timer again. The test code has been left in but uninstantiated should the timer's validity be questioned. Several things that we want to know about the timer are whether its prescaler is counting as expected, whether the overall timing is accurate, and whether the means of reading it is correct.
Normal reading and testing the timer are not efficient in C. One thing revealed by testCodeTimer is that CNTR and SR cannot be read as a ULONG, despite Motorola's claim. However, even if the two registers could be read together, the result would be unusable because SR appears at the lower address making it the more significant word (Motorola is big-endian). But the prescaler is the least significant byte of the count. We couldn't even read the ULONG and swap the two WORDS in the CPU register because the prescaler occupies the low byte of SR and the upper byte is unrelated to the count. The most efficient way to read the full 24-bit count is to read CNTR as WORD; shift it as LONG into the middle two bytes of the CPU register; and then read (or add) the low byte of SR (@SR+1) as BYTE. TestCodeTimer serves two purposes, to verify that this process yields a reasonable result and that the prescaler is down-counting at roughly the expected rate. The function performs these two tasks simply by reading the timer twice, first into D0 and then into D1. Since the timer will have just started when testCodeTimer begins, the CNTR value should be 0xFFFF in both readings. However, the prescaler value should be higher in the first reading than in the second by the number of 80 nano-seconds in the time it takes the CPU to take the second reading. We might expect to be able to precisely calculate the execution time of so few assembly language instructions but no arrangement of the grab bag of numbers provided by Motorola matches the prescaler count. This is due to the fact that instruction overlapping and pipelining in modern CPUs (in this regard the venerable 68340 is modern) makes such calculations nearly impossible. TestCodeTimer does show the prescaler counting somewhere near the expected rate relative to the CPU's execution rate.
To determine the overall timing accuracy of timer 1, it is tested relative to the 5-msec periodic timer. Our purpose here is only to determine that timer 1 has been properly configured but, in any case, the periodic timer's accuracy has been previously verified using external instrumentation. There is no callable function for this test. Instead, the periodic timer ISR, tickIsr, is instrumented to capture the timer 1 count at two successive interrupts.
Neither of these tests provides any means of reading the results other than by setting breakpoints using the BDM debugger. The modifications to tickIsr have been done in such a way that it continues to function normally. However, they take CPU time and should be instantiated only for the test and then commented out. TestCodeTimer only executes when called and doesn't consume much memory but it too is commented out except for testing. The test code was added only to the MSM's appasm module and not to the APU's because testing timer 1 once is sufficient and because the APU's tickIsr is already complicated without the additional selectively instantiated code. Executing the two tests has verified that timer 1 operates as designed.
msmapp- appasm.h (apuapp's declares only readCodeTimer)
extern void testCodeTimer( void ); /* In appasm.asm define TEST_CODETIMER */ extern ULONG readCodeTimer( void );
msmapp- appasm.asm
; TEST_CODETIMER EQU 1 ; Comment out to remove code timer test facilities. ; When test facilities are enabled, two tests are instantiated. One verifies ; timer ticking and the other the overall timing accuracy relative to the ; 5 msec. timer, which is independently verified by external instruments. ; The C code calls testCodeTimer after enabling the timer (could be any time ; after this). ; TestCodeTimer just reads the timer into D0 and then into D1 as fast as ; possible. At 25MHz. the count difference is only 13 = 1.04 usec. This ; verifies that the prescaler is working and that the formula used to read the ; counter seems OK. ; The other test is automatic when enabled. The 5msec. timer tick reads the ; code timer and alternately stores the value in time0 or time1. To check the ; results, break at checkCodeTimer and dump memory at time0. At 25MHz the code ; timer difference is 62,464 = 4,997 usec. ; ....................................................................... IFDEF TEST_CODETIMER ; -------- Use 5 msec. ticker to test code timer. timeTog DC.B 0 time0 DC.L 0 time1 DC.L 0 XDEF checkCodeTimer XDEF _testCodeTimer _testCodeTimer ; Break here (because visible) and then at RTS CLR.L D0 CLR.L D1 MOVE.W ASIM+TIMER1+TMRCNTR,D0 LSL.L #8,D0 MOVE.B ASIM+TIMER1+TMRSR+1,D0 MOVE.W ASIM+TIMER1+TMRCNTR,D1 LSL.L #8,D1 MOVE.B ASIM+TIMER1+TMRSR+1,D1 RTS ; Subtract D1 from D0 to read prescaler count. checkCodeTimer: ; Break here and subtract time1 from time0 to get count of ; 80ns (25MHz) or 125ns (16MHz) code timer clocks. RTS _tickIsr ADD.L #1,_tick5ms MOVE.L D0,-(SP) CLR.L D0 MOVE.W ASIM+TIMER1+TMRCNTR,D0 LSL.L #8,D0 MOVE.B ASIM+TIMER1+TMRSR+1,D0 TST.B timeTog BEQ recordTime0 MOVE.L D0,time1 BSR checkCodeTimer tickx: EORI.B #1,timeTog MOVE.L (SP)+,D0 RTE recordTime0: MOVE.L D0,time0 BRA tickx ELSEC ;---------------------- Normal ------------------------- _tickIsr ADD.L #1,_tick5ms RTE ENDC
Both the MSM and APU define the function readCodeTimer. This is an adaptation of the read process described above. As a 24-bit number instead of a full 32-bit ULONG, the normal rules of unsigned arithmetic don't apply. In particular, if the timer rolls over between the first and second reading (to time a process) the simple subtraction will yield an incorrect answer due to the "hole" in the upper byte. One way around this would be to perform the subtraction only when the first reading is larger than the second and otherwise calculate the time by adding the portion before the rollover to the portion after. A more efficient approach is to multiply every reading by 256, i.e. left-shift by 8 bits. This produces true ULONGs (scaled by 256) that yield correct subtraction results regardless of rollover. There is no need reverse the scaling until the number is reported, at which time the division by 256 costs nothing, as it is folded into other constant factors.
msm/apuapp- appasm.asm
XDEF _readCodeTimer ; ULONG readCodeTimer( void ); ; Read the full 24-bit timer 1 comprising counter plus prescaler and shift it ; by 8, i.e. read 256 times the actual count. Multiplying by 256 converts the ; result to a full ULONG so that unsigned subtraction of end time from begin ; time (the counter counts down) yields a correct result even when the counter ; rolls over between the two times. The program that uses this should be aware ; that the actual count is multiplied by 256 but it doesn't have to consider ; rollover. ; At 25MHz, each tick is 80nsec and the longest time that can be measured ; is 1.342 seconds. Each count of the code timer is .3125nsec (3.125 * 10^-10). ; At 16MHz, each tick is 125nsec and the longest time is 2.097 seconds. Each ; count of the code timer is .4883nsec (4.83 * 10^-10). ; _readCodeTimer CLR.L D0 MOVE.W ASIM+TIMER1+TMRCNTR,D0 LSL.L #8,D0 MOVE.B ASIM+TIMER1+TMRSR+1,D0 LSL.L #8,D0 RTS
The motor ISR's task in monitoring is simple and cheap enough to leave in place so that we can always find out the bandwidth consumption without having to compile a special version of the program.
msmapp- stpmtr.c
ULONG isrExeTime = 0;
void interrupt stpmtrIsr()
{ ...
ULONG begTime;
ULONG exeTime;
begTime = readCodeTimer();
...
exeTime = begTime - readCodeTimer();
if( exeTime > isrExeTime )
isrExeTime = exeTime;
}
The information needed to convert isrExeTime to bandwidth consumption is localized to the one function getMotorCpuUse. The conversion folds inverse scaling (/256) of the timer value, the 80 nsec timer rate, the 7.851 msec page period, and the percentage factor (*100) into a single division of isrExeTime by a constant.
ULONG isrExeTime = 0; void interrupt stpmtrIsr() { ... ULONG begTime; ULONG exeTime; begTime = readCodeTimer(); ... exeTime = begTime - readCodeTimer(); if( exeTime > isrExeTime ) isrExeTime = exeTime; }
msmapp- sysfunc.h
USHORT getMotorCpuUse( void );
msmapp- systest.c
int procSys( FsqMsg *cmd, ProcDef *pd )
{ ...
switch( cmd->arg.sys.op )
{
/* ................... Resource Usage Reports .................*/
#ifdef HAS_STEPPER
case TS_MOTORCPU: /* testSystem MotorCpu */
return postMsg( DM_SYS, SYS_RESRC, "Max motor CPU use %d%%.",
getMotorCpuUse());
#endif
TESTING
The basic functionality of the code timing facility is tested as described above. Executing motor moves and checking the resulting worst-case bandwidth consumption indicates that the facility works as intended. Any particular motor group move yields consistent bandwidth reports and increasing the load increases the reported consumption by what appear to be reasonable amounts. Assuming that we can trust these reports, what we really want to know is how close will the MSM be to failing under the proposed load of both the analyzer and the RSH. The sequential sample loader has such miniscule motor requirements that it isn't considered.
As we develop the fully integrated instrument we will want to periodically check the CPU loading (this may be useful for the APU as well as the MSM). For now, we primarily want to see how much bandwidth remains for tasks other than the motor step generator. A demanding but realistic test is to assign real ramps taken from the application but move all motors simultaneously. While this is unlikely to occur in practice, if we can be reasonably sure that the CPU can handle this situation then scripts can be designed without considering how motor activity might load the CPU.
The MtrCpu script, in mtrcpu.f, was developed for this purpose. It controls all of the motors currently used by the analyzer and those of the RSH under development. It assigns analyzer motors ramps taken from lab instrument CDX2 and the RSH motors ramps taken from the demonstration script, except the X-axis motor, which is assigned the fastest ramp we have been able to produce in isolated testing. The ramps for RetDyeMtr, WbcLyseMtr, FillLineMtr, SampInjMtr, HgbLyseMtr, HgbLyseMtr, RbcDilMtr, OpenProbeMtr are the fastest of several assigned at various times in newcnr3.f. The ramp for OpenProbeMtr is assigned "ramp250" in many flow scripts. Each motor is assigned a power change at the up-slew and slew-down transitions because these also require work by the ISR.
All motors are moved at once. There is a possibility of some starting to move in one motor page and the others in the next page if the motor interrupt occurs in the middle of the block of move commands. However, the maximum ISR load will not occur at the beginning of ramps, where the steps are long, anyway. The greatest load that any motor move presents to the ISR occurs when the step rate is fastest but not in the slew period, because less work is required to read the slew step than a ramp step. Thus the true worst case load occurs only in some page in which all motors are near the end of their up ramps. No attempt has been made to enforce this degree of synchronization. We can speculate that the true worst case might be as much as 5% worse than indicated by the test as it is.
After setting the motors' ramps and power, three sets of motor moves are tested: first both the analyzer and the RSH groups, then just the analyzer group, and finally just the RSH group. After each group move a testSystem MotorCpu command is issued when the motors stop. The bandwidth consumption is reported as 34%, 9%, and 14% for the three sets. With many reruns, occasionally any one of the results may be 1% higher. These results are generally reasonable although it is not clear why the two motor groups together demand significantly more bandwidth than the sum of the two individually (34% vs. 23%). If anything, we would expect the opposite, since preparing the steps for two sets together allows them to share some overhead. In any case, the 34% is still better than expected for a test deliberately more demanding than what we currently expect of the application. The CPU could probably do everything else required of it even if this level of utilization were constant.
mtrcpu.f
// The simulated motors are:
// - PeriPump1 (aka FillLineMotor, M2) rate is 450 ("ramp450").
// - PeriPump2 (aka RetDyeMtr, M3) rate is 50 ("ramp50").
// - SampInjMtr (aka SyringeA, M4) rate is 450 ("ramp450").
// - HgbLyseMtr (aka SyringeB, M5) rate is 350 ("ramp350").
// - WbcLyseMtr (aka SyringeC, M6) rate is 400 ("ramp400).
// - RbcDilMtr (aka SyringeD, M7) rate is 200 ("ramp200").
// - OpenProbeMtr (M8) rate is 250 ("ramp250").
// - TransX (M7) is the LIN Nema 17 motor driving the RSH X axis. At 36V the
// maximum rate is 1550. At 24V, it is 1400. It will probably be used in the
// application at 24V and 1200 steps/second. For this test, 1500 steps/second
// will be used.
// - Theta (M9) is the RSH Theta motor, which is micro-stepped. CPU usage is
// not significantly different for micro- vs. whole-stepping. The maximum rate
// in the application is 500 steps/second.
// - RackVertical (M5) is the RSH robot vertical motor. It moves faster going
// down than up. The maximum rate is 900 steps/second.
// - Belt (M4) is the RSH belt motor. Its fastest rate is 500 steps/second.
// - Mixer (M8) is the RSH Mixer motor. It moves the fastest when going down
// toward the rack pickup/deposit point. This is 800 steps/second.
//
// RSH and current instrument motor numbers conflict because they are actually
// using the same motor slots. This will be corrected when RSH motor drivers
// become available. For the purpose of this test, M numbers will be used for
// the conflicting RSH motors. M9 remains Theta.
// TransX = M10
// RackVertical = M11
// Belt = M12
// Mixer = M13
//
// Most of the motors' normal ramps have hold times. These consume a small
// amount of ISR time but not a significant amount compared to other phases.
// They have been removed so that the script can read the CPU utilization
// without having to wait after the motors have stopped. This is only to avoid
// confusion about how the script is working.
// Power changes also consume a little ISR change. Since they may occur at any
// trajectory segment change, each motor is assigned a different power setting
// at each segment.
// **************************************************************************
unit MSM
define TransX M10
define RackVertical M11
define Belt M12
define Mixer M13
begin MtrCpu
testSystem MotorCpu // 0 the ISR max timer.
wait for 0.5
ramp PeriPump1 up 50 to 450 @ 25% slew 450 down 450 to 50 @ 30%
ramp PeriPump2 up 10 to 50 @ 50% slew 50 down 50 to 10 @ 50%
ramp SampInjMtr up 50 to 450 @ 25% slew 450 down 450 to 50 @ 30%
ramp HgbLyseMtr up 50 to 350 @ 25% slew 350 down 350 to 50 @ 30%
ramp WbcLyseMtr up 50 to 400 @ 25% slew 400 down 400 to 50 @ 30%
ramp RbcDilMtr up 50 to 200 @ 25% slew 200 down 200 to 50 @ 30%
ramp OpenProbeMtr up 50 to 250 @ 25% slew 250 down 250 to 50 @ 30%
ramp TransX up 200 to 1500 @ 20% to 0.1% slew 1500 down 1500 to 200 @ 2% to 10%
ramp Theta up 100 to 500 @ 10% slew 500 down 500 to 100 @ 10%
ramp RackVertical up 200 to 900 @ 50% slew 900 down 900 to 200 @ 50%
ramp Belt up 200 to 500 @ 10% slew 500 down 500 to 200 @ 20%
ramp Mixer up 200 to 800 @ 20% slew 800 down 800 to 200 @ 20%
power PeriPump1 up high slew medium down low
...
// Test both main analyzer and RSH virtual motors.
move PeriPump1 +450
move PeriPump2 +50
move SampInjMtr +450
move HgbLyseMtr +350
move WbcLyseMtr +400
move RbcDilMtr +200
move OpenProbeMtr +250
move TransX +1500
move Theta +500
move RackVertical +900
move Belt +500
move Mixer +800
wait PeriPump1
...
wait for 1.0 // If 0.5 seconds then next usage says 1%. If no wait 2%. No explanation.
testSystem MotorCpu
// Test just the main analyzer motors
move PeriPump1 +450
...
testSystem MotorCpu
// Test just the RSH motors
move TransX +1500
...
testSystem MotorCpu
end
DOCUMENTATION
The function MotorCpu was added to the Script Language Reference [cdsref- TestSystem]. Previously, the general description of the resource reporting functions did not mention whether reading a resource monitor modified its value. Since MotorCpu uniquely (for now) resets the monitor, both this and the fact that reading the others does not is now mentioned. As with many other uncommon functions, MotorCpu is only briefly described. This document would overwhelm its intended audience if every obscure capability were fully described.
nativeSystemTestFunction is ...
HeapStack, MotorCpu, Scripts, WipeNonVol, TextReply, EchoTime,
...
FsqText, Fsqs, Procs, PowerDowns, TqFreeSpace, HeapStack, and MotorCpu all cause a DM_SYS subtype SYS_RESRC message to be sent to the system master. Each message contains text which names an analyzer system resource and tells how much of that resource was free at the busiest time, i.e. how much margin the system affords for that resource. Executing testSystem MotorCpu resets this resource monitor. All of the others are unchanged by reading and, therefore, always show the cumulative worst case (maximum utilization of the resource). The different texts are:
...
MotorCpu: "Max motor CPU use bandwidth%.", where bandwidth tells what percentage of the total CPU bandwidth was consumed by the stepper motor controller in the worst-case motor period. Only direct stepper motor controllers can execute this command.
To top of [MotorCpuBandwidth]
[NextTopic] [MainTopics]
The following is the stepper motor tutorial taken from the Script Debugger User's Guide.
This lesson requires the MSM. The MSM contains all of the intelligence for controlling 20 steppers. It additionally contains on-board drivers for four steppers. It controls the remaining motors through the motor scan chain, which is similar to the I/O scan chain, except that it contains additional wires for a motor winding test and you can't write directly to the output. You can, however, treat the input exactly like the I/O scan chain input, as was demonstrated in lesson 14 [cdxdbg- Lesson14].
The MSM controls six motors through the RPMD (Right Panel Motor Driver). It controls the remaining 10 through its Loader Motor jack, which may be connected to the Loader board directly or indirectly through another motor driver board similar to the RPMD.
Much of this lesson is based on experiments with a peristaltic pump, chosen because we can't hurt it even with extreme parameters. The MSM's third on-board stepper interface normally drives a pump. The lesson assumes this configuration. If this doesn't match your test system, you only need to change the motor ID in the beginning instructions.
Unlike space-based devices, stepper motors cannot be accessed by number. Therefore, you must define in the ini file any steppers that you want to access. As with I/O, you can give a stepper multiple aliases, for example a numeric-like name such as M2 and a functional name like PeriPump. You also have to tell the step resolution (maximum step rate) of the motor controller. The script compiler uses this information to generate ramp tables. Open mycfg.ini and enter the following stepper definition section. You can omit the comments to reduce typing. Save the file.
STEPPER_MOTORS
UNIT = MSM
STEP_RATE = 32605 ; Stepper driver fundamental rate
M2 = 2 ; MSM J23 output, J21 flag 1, J22 flag 2.
PeriPump = 2
Submit (through the command edit) the command MOVE M2 + 200. It doesn't matter whether the APU or MSM is selected, because the compiler will generate a command message for the direct controller, MSM, in either case. The space between + and 200 is optional. To see a description of the MOVE command, highlight the word MOVE and click the right mouse button in the command edit. Select Language from the floating menu. This will direct Word to display the Language Reference [cdsref- Move]. The peristaltic pump should rotate clockwise several revolutions when the MSM executes this command.
Submit the command MOVE M2 - 200 and observe the motor rotating counter-clockwise.
The MSM will not move a stepper without the motor having a complete ramp definition. See the Language Reference description of Ramp [cdsref- Ramp]. You were able to move M2 because all steppers have a default ramp. This default is the equivalent of the RAMP command ramp M2 Up 50 to 200 linear 15% slew 200 down 200 to 50 linear 20% recoil 0 hold 0.5. Any motor moved with this ramp will accelerate from 50 to 200 steps per second at a 15% gradient; run at a constant rate of 200 steps per second; decelerate to 50 steps per second at a 20% gradient and hold the final position (with a magnetic detent) for one-half second. The default power setting [cdsref- Power] for all ramp segments is LOW. After the hold period, power drops to the IDLE default, which is OFF.
You can change a motor's trajectory (complete ramp) or any segment of its trajectory even while it is running. Submit the command MOVE M2 + FOREVER to cause the peristaltic pump to rotate clockwise. Because of the default slew rate, it is rotating at 200 steps per second. Submit the command RAMP M2 SLEW 220. Change the slew to 230, then 240, then 150, and back to 200.
The size of speed change that the motor will tolerate without losing steps depends on the motor and its load. Change the slew to 50 and then to 240. Try jumping around from as low as 10 to as high as 250. If the peristaltic pump is not engaged, the motor can tolerate severe changes. Of course we don't know whether it is losing steps during the transitions. Starting at 250, increase the rate 50 steps at a time until the motor stops rotating and just vibrates. At this point the only way to regain control is to stop the motor. If the pump is not engaged, you should find that the motor can get up to about 550 steps per second. Change its slew rate to 150 and start it going forever again with the two commands RAMP M2 SLEW 150 and MOVE M2 + FOREVER. Now try changing the slew rate to the maximum that you found the motor could do when you stepped up in increments of 50. You should find that the motor vibrates instead of rotating. If the motor is able to make this jump, experiment until you find a rate that can be achieved only when stepped up to. If necessary, decrease the step size and/or increase the jump size. Technically speaking, the slew rate of a motor is its maximum step rate under load. The slew segment of the trajectory is the period when the motor is driven fastest but this rate should be much less than the actual slew rate of the motor.
You can determine how much margin a motor has by comparing the maximum rate at which it is used to the slew rate limit determined by a very gentle ramp up to the speed just below where the motor fails to rotate. For a continuously running motor with no absolute positions, this margin is, in many cases, the only significant factor determining whether the motor can start reliably.
Submit the command RAMP M2 UP 250 SLEW 250 DOWN 250. The trajectory retains the previously defined segments RECOIL 0 and HOLD 0.5. Submit RUN M2 - FOREVER. Obviously, a ramp is not needed for this motor, load, and speed combination. This is not usually the case. More typically, the motor does not run continuously but between positions, where the loss of even a single step is intolerable. One of the benefits of steppers is their high torque at low speeds, which enables them to move substantial loads. However, they can't move loads reliably to predictable positions without ramps. To a certain extent, an oversized motor can compensate for a bad ramp but this is not a reliable general solution because an under-loaded motor is more likely to experience resonance, causing it to miss steps. Of course you can design a ramp to avoid this problem but you may as well just design a good ramp for a motor properly sized for the load.
Submit the command RAMP M2 UP 50 TO 250 LINEAR 10% DOWN 250 TO 50 LINEAR 25%. The trajectory retains the other previously defined segments. Submit the command MOVE M2 + 300. Listen to the up ramp. Resubmit both commands, changing the 10% to 2%. You can hear that the up ramp is much slower than previously. Direction is unrelated to ramp. Try MOVE M2 - 300 alternately with MOVE M2 + 300. You should hear no difference.
The up segment is the most critical in avoiding step loss, particularly at the high end, where steppers lose torque and are, therefore, less able to overcome the inertia of the load. The linear ramp compensates for this torque loss by decreasing the step change as the steps get quicker. However, a linear ramp is not always the best approach, so several alternatives are offered.
As explained in the Language Reference [cdsref.doc- Ramp] there are three ramp types, linear, log, and arbitrary steps (enumerated). The log ramp uses a constant step change and for most situations affords inferior performance to either of the other two types. It is in common use only because it is easy to generate with inferior software or by hand. With arbitrary steps, you can create any ramp you like, but the exercise is extremely tedious.
When developing a ramp, you should first try a simple linear. For reliability, you should increase the gradient until you encounter step loss. As a rule of thumb, a gradient of half this rate will be very reliable. If step loss occurs at your preferred gradient and your slew rate is not the problem and the motor is adequately sized, the most likely problem is resonance. This is where the other ramp types become useful. Resonance usually occurs only at certain points (often only one) in the ramp. You can splice a log or enumerated ramp into this section to eliminate the resonance. For example, try the following ramp: RAMP M2 UP 10 TO 120 LINEAR 4% + 121,122,124,126 + 128 TO 250 LINEAR 4%. Test this with MOVE M2 + 300. For the same problem, you might use a log ramp splice, for example RAMP M2 UP 10 TO 120 LINEAR 4% + 121 TO 126 IN 4 STEPS + 128 TO 250 LINEAR 4%.
Another common use of a composite ramp is to provide an extremely gentle start or stop. Sometimes a sine ramp is used for this purpose, but an arbitrary composite affords more precise control. Try, for example, RAMP M2 UP 5,10,15,20,30 + 40 TO 250 LINEAR 20%. Test with MOVE M2 + 300. Try RAMP M2 UP 5 TO 10 LINEAR 10% + 10 EXCLUSIVE TO 20 LINEAR 15% + 20 EXCLUSIVE TO 250 LINEAR 20%. This use of the word exclusive is explained in the Language Reference [cdsref.doc- Exclusive].
The recoil segment provides a direction reversal at the end of travel. This is intended for two purposes: to enhance and shape fluid shear when an aliquot is delivered by a pump or syringe; and to eliminate the effect of lead screw (or gear train) backlash by recoiling in one direction only. The effect of recoil for fluid shearing is fairly obvious. The anti-backlash effect requires some explanation. To utilize this effect, you have to add the recoil segment when moving in one direction and remove it when moving in the other. It doesn't matter which direction has the recoil. The important point is that the final position is always determined with the drive mechanism pushing one way. The CDNext stepper system allows you to repeatedly change all or a portion of a motor's trajectory, even as the motor runs, so adding and removing the recoil is trivial.
A recoil segment normally comprises only a few small steps, which are most easily defined by enumeration. For the first example, you will define an unrealistically large recoil in order to see the effect. Submit the command RAMP M2 UP 50 TO 250 LINEAR 15% SLEW 250 DOWN 250 TO 50 LINEAR 25% RECOIL 50 TO 80 IN 10 STEPS HOLD 0. Use MOVE M2 + 300 to test the ramp. Observe the recoil. Change the recoil to a more subtle version with the command RAMP M2 RECOIL 50,55,60,65,70. Test this. If you watch closely, you should still have no trouble seeing the recoil. Try an even more subtle version, RAMP M2 RECOIL 50,55. You may not be able to see this.
Up, slew, and down ramp segments are required. Recoil and hold are optional. As you have seen, trajectories can be built by superposition, i.e. you can add or change one segment without affecting the others. To remove a recoil or hold segment, set it to 0. To see this capability in use, you are going to write a script that demonstrates the anti-backlash use of a recoil segment. This script contains a time delay. You haven't used any delays yet because your analyzer configuration file, mycfg.ini, needs to tell the compiler the "native tick" period of the unit before any time delay commands can be compiled. Add the following section after the units list (UNITS APU MSM) to mycfg.ini and save the file.
TIMERS ;-------------------------------------------------------------------
UNIT = APU
PERIOD = 5 SIZE = 2 ; 5msec. 2-byte command parameter.
UNIT = MSM
PERIOD = 5 SIZE = 2
From the File menu, select New. Type the following script ('\' allows a statement to continue on the next line [cdsref- LineContinuation]) and save it as test3.f. Press Ctrl + D to compile and download it. Click in the debug bar next to begin to execute the script. Since this script runs forever, submit the command !2H to stop it.
begin Test3 unit MSM
ramp M2 up 50 to 250 linear 15% \
slew 250 \
down 250 to 50 linear 25% \
recoil 0 hold 0
loop 0
ramp M2 recoil 50 to 80 in 10 steps
move M2 + 300
wait M2
wait for 0.5
ramp M2 recoil 0
move M2 - 300
wait M2
wait for 0.5
endloop
end
The power used to drive a stepper plays an important role in avoiding step loss. If the power is too low, the motor can't pull the load. If it is too high, the motor will experience resonance. The power-- high, medium, low, off-- can be set for each ramp segment individually. Power settings can be changed at any time but a segment that has already started will not be affected. You have been operating M2 with its default low power for all segments. Start the script from step 15. While the motor is slewing, submit the command POWER M2 SLEW MEDIUM. Note that there is no change until the motor stops and then reverses direction. Try POWER M2 SLEW HIGH and back to LOW. Notice how rough the motor sounds under all but low power. This is due to its being lightly loaded. While the script continues to execute and slew power is low, grab the peristaltic pump wheel to increase the load. Note how little effort is required to cause step loss. Change the slew power to high and grab the wheel again.
Motor power also plays a role when the motor is not moving. Steppers have two stationary states, hold and idle. These two states differ in that hold lasts for a specified time, which is limited to about 2 seconds, while idle lasts indefinitely. This difference is immaterial if both states have the same power setting but important if they don't. Idle power should be Off or Low in nearly all circumstances. Applying medium or high power to a motor for an extended period will cause both the driver and the motor to overheat. You can safely apply medium and high power in the hold period because of its limited duration. The purpose of applying any power to a stationary motor is to provide a magnetic detent, a force that tries to hold the motor in its current position. This might be used to resist the pull of gravity or a spring or to force an oscillatory mechanical system to settle into the desired position. To experience the magnetic detent effect, set M2's ramp with the command RAMP M2 UP 50 TO 250 LINEAR 15% SLEW 250 DOWN 250 TO 50 LINEAR 25% RECOIL 0 HOLD 1.0. If you are continuing from the previous step, you only need to set the recoil and hold using RAMP M2 RECOIL 0 HOLD 1.0. This provides a 1-second hold period. To have this mean something, submit the command POWER M2 UP HIGH SLEW HIGH HOLD HIGH. Idle power is OFF by default. Power for the up, slew, and down segments are set HIGH along with the hold because you are going to grab the pump wheel in order to feel the detent during the hold and we don't want weird motor behavior confusing the experience. Hold the wheel and resist its movement enough to feel its pull but not enough to cause step loss. Submit the command MOVE M2 + 300. When the motor stops, continue applying pressure to try to make it move. The motor resists for one second, after which it relaxes, indicating the end of the hold period. Change the idle power from Off to Low with POWER M2 IDLE LOW. Repeat the test. Try to feel the change from the high power hold to the low power idle. Change the idle power back to Off.
Hold can be used to create a very subtle fluid shearing effect-- essentially a microscopic version of the recoil segment. The momentum of the load plays against the power of the motor. In all cases, after each step, the load's momentum causes it to overshoot the magnetic center of the step. Unless there is step loss, before the load can pull the motor to the next step position, the magnetic force pulls it back. The weaker the magnetic force, the further the load can travel from the detent position. We can design a down ramp, operating at Low or Medium power, that doesn't lose steps but allows the load's momentum to carry it fairly far past the last step's magnetic center. If the hold period suddenly switches to high power, the last step position doesn't change but the magnetic force increases, accelerating the load's return to the detent. The sudden, sharp direction reversal (much like cracking a whip) can enhance the fluid shear, depending on the mechanical step size, fluid velocity, fluid surface tension, and many other factors. It is easy to experiment with such an arrangement. For example, just changing the power settings for M2 to POWER M2 UP LOW SLEW LOW DOWN LOW HOLD HIGH creates the basic condition. However, to effectively use this capability you will have to experiment with different down ramps and the actual fluid-mechanical system.
The script compiler will not allow you to make a ramp segment that contains more than 118 steps. Try, for example, submitting the command RAMP M2 UP 5 TO 250 LINEAR 2%. The compiler informs you that this would require 194 steps, which is considerably more than the 118 maximum. Try changing the gradient to 4%. The compiler accepts this. Keep in mind that this limit applies only to a ramp segment. It doesn't limit the total travel at all. Most ramp segments contain fewer than 20 steps.
If you move a motor a shorter distance (number of steps) than there are steps in its up and down ramps combined, the slew period is eliminated entirely and the up and down ramps will be symmetrically truncated. The motor control software does this itself, so it works even if the motor is moved a distance determined at run time (VAR move parameter). To observe this effect, change M2's trajectory using the command RAMP M2 UP 5 TO 250 LINEAR 4% SLEW 250 DOWN 250 TO 5 LINEAR 8% HOLD 0. Submit the command MOVE M2 + 400. Try 300. Note that both of these moves contain full up and down ramps plus a slewing period. Try MOVE M2 + 200. The slew period is now missing but the up and down ramps are intact. Try moving the motor 100 steps. Notice that the segments are truncated. Try a move of 30 steps, then 2, and finally 1.
Generally, after commanding a motor to move, you should not interfere with it until the move is done. If you issue another move command before the previous has finished, the motor controller will refuse the command and explain the problem in a text message. Set M2's trajectory with the command RAMP M2 UP 50 TO 250 LINEAR 15% SLEW 250 DOWN 250 TO 50 LINEAR 25% RECOIL 0 HOLD 0. Submit the command MOVE M2 + 600. Before this move completes, repeat the command. Open the File menu and select New. Type the following script (you can copy the ramp statement from the command edit to reduce typing) and save the file as TEST4.F.
begin Test4 unit MSM
RAMP M2 UP 50 TO 250 LINEAR 15% SLEW 250 DOWN 250 TO 50 LINEAR 25% RECOIL 0 HOLD 0
move M2 + 500
move M2 + 500
end
Press Ctrl + D to switch to debug mode and compile and download the script. Click in the debug bar next to begin to execute the script. Switch back to Edit mode and change the begin statement to begin Test4 unit APU. Switch back to Debug mode and execute this. The MSM's response to each of these three tests can be seen in the From Analyzer window in figure 86 [MoveOnMove]. The responses to the first and third test are identical. Note how they differ from the response to the second test. All three messages begin with some binary values for communication between units. Next, the MSM identifies itself as Unit 2. Then it identifies the source of the erroneous command, which is the Master in the first and third cases (the MSM doesn't distinguish between the debugger/data station and the APU) but script TEST4 at command message number 5 (not source line 5) in the second case. The next field in the message explains that Stepper 2 is already moving due to an interactive command (indicated by illegal script and message numbers 65535) in the first and third cases but command message number 4 in local script number 3 in the second case. The last field in the message tells the source file name and line number where this error was detected in the MSM's application program. As a script developer, this last message is not relevant to you, but it can help a system programmer to determine why you have received the error or to track down a program bug. In this case, the error is obviously legitimate.
One way to avoid trying to move a motor before it has stopped is to delay for a time long enough to ensure that the motor has either finished its previous move or failed. Scripts often wait after a motor finishes its move for reasons unrelated to avoiding a move collision. However, sometimes scripts need to know that the move has completed before they do something else, even if that is not just to move the motor again. In any case, the cost of verifying that a motor is not moving is very low. The wait command allows waiting for a motor as well as for a time period. As explained in the Language Reference [cdsref- Wait] you can wait for a motor to stop moving (i.e. in hold or idle state), until it is idle, or until it has reached a specified position (you will explore this possibility after first learning about stepper position and absolute moves). The motor controller issues a move collision error only if it receives a new move command while the motor is moving. A new move while the motor is in hold is not only legal but quite common. The simple wait motor statement waits only until the motor has stopped moving. Wait motor idle is used much less often because the hold period normally serves only to keep the motor from drifting out of its final position. If we intend to move the motor anyway, this action is meaningless. To test these two wait types, change TEST4.F to the following and switch to Debug to save, compile and download it.
begin Test4 unit MSM
RAMP M2 UP 50 TO 250 LINEAR 15% \
SLEW 250 DOWN 250 TO 50 LINEAR 25% RECOIL 0 HOLD 1.5
move M2 + 300
wait M2
move M2 - 300
wait M2 idle
move M2 + 300
end
When this script executes, note how the motor starts its second move (-300) as soon as the first move stops. It does not wait for the hold period to finish. It starts its third move only after the 1.5 second hold at the end of its second move.
Rarely do you want a script to wait forever for a motor to finish its move. Doing this means that some other script or the user will have to intervene to wake up the script in case of motor failure. It is normally much better for the script waiting for the motor to realize that there is a problem and do something about it. The wait command takes an optional maximum time parameter. As explained in the Language Reference [cdsref- WaitTimeout] the command interpreter will wait no longer than this for the motor to reach its specified condition. Rewrite Test4 as follows. Compile, download, and execute it. The first wait passes because the motor finishes moving in less than two seconds. The second wait times out because waiting for idle adds 1.5 seconds (the hold time specified in the ramp) to the time the motor requires to reach the specified condition.
begin Test4 unit MSM
RAMP M2 UP 50 TO 250 LINEAR 15% SLEW 250 \
DOWN 250 TO 50 LINEAR 25% RECOIL 0 HOLD 1.5
move M2 + 300
wait M2 max 2.0 seconds
if timeout
echo "Motor failed first move"
terminate
endif
echo "Motor passed first move"
move M2 - 300
wait M2 idle max 2.0 seconds
if timeout
echo "Motor failed second move"
else
move M2 + 300
endif
end
Although you normally want a motor to finish a move, there are times when you need to stop it using the Stop command. An obvious candidate is a motor that has been commanded to move Forever, like the peristaltic pump. A script might want to stop a motor before reaching its destination in response to a detected error. For example, a script would want to immediately stop a syringe motor upon detecting overpressure in the fluid line. Stop is also useful during development. You might want to freeze a motor at a particular point in a script in order to see its position. You might want to interactively stop a motor that you've accidentally given a terrible move command. As explained in the Language Reference [cdsref- StopMotor] there are three ways to stop a stepper. The default Stop effects a controlled stop by immediately engaging the motor's down ramp (only if the motor is in the up ramp or slew state). This is the only type guaranteed to not lose track of the motor's position, but it may move the motor further than you would like. Stop hard stops the motor much more quickly by freezing its current phase pattern and setting power to high. If the magnetic force is strong enough the motor will stop immediately and the controller will not lose the position. Otherwise, the recorded position will not match the motor's physical position. Stop off turns off all power to the motor and lets it coast. You can always resume moving the motor after a default stop and you may be able to after a hard stop, but you must always reinitialize the motor after an off stop.
In the following experiments, you are asked to hold the pump wheel while submitting commands. The easiest way to do this is to submit the commands first, in order to record them in the command history, and then use the up and down arrows to recall them.
Submit the commands RAMP M2 UP 50 TO 250 LINEAR 15% SLEW 250 DOWN 250 TO 50 LINEAR 5% RECOIL 0 HOLD 1.5 and POWER M2 HOLD LOW. Submit the command MOVE M2 + 5000. Before the motor reaches its destination (you have plenty of time) stop it with STOP M2. Note that the down ramp engages immediately but, because it is so long, the motor travels quite a bit before stopping.
Repeat the move and stop commands, this time lightly holding the pump wheel so that you can feel the 1.5 second hold at the end of the down ramp. Repeat the move command but replace the stop with STOP M2 HARD. Note the rapid stop. Repeat again, this time lightly holding the wheel. Feel the high power 1.5 second hold. In a hard stop, the hold power is always high regardless of the normal hold power. However, the hard stop hold time is based on the last (or current) non-0 hold time of the motor. Submit the command RAMP M2 HOLD 0. Submit the command MOVE M2 + 300 while lightly holding the wheel so that you can feel that there is no hold at the end of this move. Submit the commands MOVE M2 + 5000 and STOP M2 HARD while holding the wheel. Feel the 1.5 second hold.
Submit the commands MOVE M2 + 5000 and STOP M2 OFF. There is little momentum in this assembly and, therefore, the motor's light permanent magnet detent effect alone is sufficient to stop it rather quickly but probably not without losing the position. If the load is heavy but with little momentum, such as a lead-screw driven syringe, you may find that stop off doesn't lose the position.
In all of your experiments so far you have been using relative moves, in which the motor is commanded to move a specified number of steps in a direction that is either positive or negative, the meaning of which depends on the electro-mechanical system. Equally or more common are absolute moves, in which the motor is commanded to move to a particular position [cdsref- Move]. The concept of position is very flexible. The stepper controller assumes nothing about a motor's position but allows a position to be associated with any motor. You can assign a position value between 1 and 65534 to a motor at any time, after which all absolute moves will be made relative to this position. Although the peristaltic pump has no use for a position, we can still use it for this exercise. Set M2's trajectory with the command RAMP M2 UP 50 TO 250 LINEAR 15% SLEW 250 DOWN 250 TO 50 LINEAR 25% RECOIL 0 HOLD 0. Put a mark (e.g. tape or whiteout) on the periphery of the pump wheel. Repeating the command MOVE M2 + 1 at least three times, move the wheel until the mark aligns with some stationary feature, such as the edge of one of the tubing holder tabs. You can't just move the motor by hand because this can leave it in any one of four phases, which the controller cannot detect. The controller can move the motor from any phase, but as many as three steps may be required before the controller and motor agree on the motor's position. Submit the command POSITION M2 1000. See Language Reference [cdsref- Position]. Submit the command MOVE M2 TO 1050 followed by MOVE M2 TO 1000. Try MOVE M2 TO 950, MOVE M2 TO 900, MOVE M2 + 10, MOVE M2 TO 100. After both sequences, the motor should end up at the original position. Submit the command POSITION M2 to find out what the controller thinks is the motor's position. Submit MOVE M2 - 20 followed by POSITION M2.
The position command is usually used in conjunction with some means of finding a mechanical home position. Typically this position is indicated by an electro-mechanical flag, such as an opto-interruptor. However, any measurable phenomenon can be used. For example, the stepper might be driving a light source aperture (iris) with the home position indicated by a particular light intensity. In any case, the numeric value that you assign to home depends entirely on what is appropriate for the mechanical system. If the home position represents the middle of the system's range, the home position value should be approximately 32000. One thing that you should avoid is making the value so close to a limit that a slight overrun wraps around. For example, if you call the home position 1 and use a hunting process to verify it, the position could change from 2 to 65534 in just five steps. If this occurs, the controller will take 65532 steps to get back to the absolute position 2. The motor's home position is just a mechanical reference. Once this position has been found, the motor might never return to it except as part of an error recovery process.
The motor control system itself does not recognize home or provide a home finding mechanism for any motor. This is done in scripts. There are two basic methods for finding home: by moving the motor long enough to be sure that it has reached a mechanical barrier or by moving the motor while watching a home indicator input device, such as the motor flag demonstrated in lesson 14 steps 14.11 through 16 [cdxdbg- MotorFlag]. The former method affords the advantage of not requiring a home sensor but at the expense of making a horrible noise while the motor bangs its head against the wall. This problem can be reduced by using the lowest power possible and a process that yields the shortest worst-case head-banging time. This process entails first moving the motor one-half of its full travel distance away from the wall and then moving it the full travel distance toward the wall. In the worst case, the head banging continues for one-half the full travel time. The following script illustrates this process for a homeless syringe. If the analyzer system that you are using has this or a similar syringe, you can test this script, which you can find in the file MTR.F. You may want to change the debugger's ini file to analyz.ini for its motor and sensor definitions and you may need to change some of the names in this script. Your system may have a file that contains a similar but more appropriate (for the hardware) script.
begin HomeSyringe1
ramp Syringe1 \
up 50 to 400 linear 40% \
slew 400 \
down 400 to 50 linear 60% \
recoil 0 \
hold 0
power Syringe1 up = medium, slew = medium, down = medium, idle = off
move Syringe1 +900 // Move 1/2 full travel away from home
wait Syringe1
move Syringe1 -1800 // Move full travel toward home.
wait Syringe1
move Syringe1 +80 // Pull away from hard stop
wait Syringe1
position Syringe1 Sy1Home
// No more head-banging so increase power to avoid step loss.
power Syringe1 up = high, slew = high, down = medium, idle = off
end
If the assembly has a home flag (again, this is nothing but a physical reference means) the homing script similarly sends the motor on a long move, but it then watches the flag to determine when to stop the motor. For example:
begin HomeSyringe2
ramp Syringe2 \
up 50 to 400 linear 40% \
slew 400 \
down 400 to 50 linear 60% \
recoil 0 \
hold 0.3 // Temporary for homing
power Syringe2 up = high, slew = high, down = medium, \
hold = high, idle = off
move Syringe2 -50 // In case it is already beyond home.
wait for Syringe2
move Syringe2 +2500
loop 0
if Syringe2Home = Home
stop Syringe2 hard // Uses hold to freeze the motor
break
endif
endloop
wait for 0.3 // Give hard stop plenty of time.
ramp Syringe2 hold 0 // Don't need hold for normal operation.
position Syringe2 Sy2Home
end
Note the Position command in both of these scripts. It assigns a numeric value to each motor's home. Sy1Home in the first script and Sy2Home in the second are aliases for the actual numbers, which are assigned to them by the following statements earlier in the script file:
define Sy2Home 10000
define Sy1Home 10
It is convenient to use a motor's normal ramp and power settings when finding its home, but often this does not work. In the head-banging script, slew power is reduced to the minimum (in this case medium) needed to move the assembly in order to minimize the apparent stupidity of the system (in fact, it is stupid, but we don't want to be so obvious). In the second script, a hold time is temporarily defined for the motor for the use of the hard stop command.
To find home reliably, an unusual trajectory may be needed. For example, the rocking sample mixer's ideal home position is very close to a hard mechanical stop; its magnetic reed switch home sensor has a broad and somewhat unpredictable activation range; and the sensor activates and deactivates slowly. Experiments revealed that the most reliable ramp for this situation was one in which the motor essentially stopped at each step. For finding home, the motor uses the following ramp:
ramp Mixer up 20 slew 20 down 20 recoil 0 hold 0.5
Compare this to the ramp used for for mixing:
ramp Mixer up 20 to 100 linear 40% slew 100 down 100 to 20 linear 60% recoil 0 hold 1.0
The move command accepts an integer expression for absolute position and relative move step count. An integer expression is an arithmetic formula with integer (rather than floating point) numbers, for example 1600 / 2 + 10. All of the numbers must be known at compile time so that the compiler can fold them into a single value. The result is the same as you would get using a single integer, but the expression enables you to avoid hard-coded values or unnecessary definitions. For example, the "head-banging" home finder script shown above hard-codes the full travel and half-travel step counts as 1800 and 900, exposing the script to more knowledge of the motor than it really needs. These values could be defined in the ini file, for example as Sy1FullTravel and Sy1HalfTravel. Since the half travel value is actually derived from the full travel, its definition is redundant. You could define just Sy1FullTravel and use Sy1FullTravel / 2 as needed in the script. This difference is subtle, but everything you do to reduce redundancy improves the maintainability of your scripts.
Move the peristaltic pump one or more times to align your reference mark. Submit the command MOVE M2 +48 (or whatever value is exactly one revolution). Submit MOVE M2 + 96 / 2. Observe the motor make one revolution. Try MOVE M2 - 24 * 2 and MOVE M2 + (55 - 5) * 3 - 102. Submit POSITION M2 1000 followed by MOVE M2 TO 1000 + 48 then MOVE M2 TO 1000 + 48 * 2.
In some circumstances, some mid-travel position of a stepper-driven assembly is as important or more so than its final destination. For example, you may need the assembly to move to a particular position, but at the halfway point it has moved enough to clear the way for another assembly to move. If you start moving the second assembly at this point instead of waiting for the first one to finish moving, you may improve throughput. You may improve safety by moving a barrier into position at the earliest possible moment. Sometimes this can be achieved with a time delay, but this is an indirect approach that fails unless you remember to update the delay with every change in the timing of the first motor. This is a particularly difficult task if the script that needs to wait for the first motor is not the one that moves it. It may be nearly impossible to hard-wire the timing of the second assembly's move because the first one's move depends on run-time variables. Regardless of whether it is even possible to correctly predict a motor's position at a certain point in time, it is easier and more reliable to wait for it to reach the position. You can do this with the Wait command, which you studied in steps 22 and 23 of this lesson. As explained in the Language Reference [cdsref- Wait] you can only wait for the motor's position to be greater than or less than a particular value, emphasizing the fact that there is some lag time between when the motor reaches a position and the command interpreter realizes it.
Create the following script in a new file TEST5.F. Save, compile, download, and execute Test5.
begin Test5 unit MSM
RAMP M2 UP 50 TO 250 LINEAR 15% SLEW 250 DOWN 250 TO 50 LINEAR 25% RECOIL 0 HOLD 0
move M2 + 10
wait M2
position M2 1000
move M2 to 3000
wait M2 > 1500
echo "M2 > 1500"
wait M2 > 2500
echo "M2 > 2500"
wait M2 > 3500 max 1.5 seconds
if timeout
echo "M2 fails > 3500"
else
echo "M2 > 3500"
endif
wait M2 idle
move M2 to 1000
wait M2 < 2000
echo "M2 < 2000"
end
As shown in the From Analyzer window in figure 87 [WaitPosition] the MSM sends echo messages as the motor passes though the 1500 and 2500 positions but it times out waiting for the motor to reach the 3500 position. Going back the other way, the MSM sends a message as the motor passes the 2000 position. Watch the timing of these messages on your system. They should appear when you expect them relative to the motor's movement.
Change the begin statement to name the APU as the execution unit. Compile, download, and run the script. The results should appear to you to be exactly the same as when the MSM executes the script except that the echo messages are identified as coming from unit 1 instead of unit 2. However, there are unseen differences. As you might guess, the APU has to send all stepper commands to the MSM, which controls the motors. The overhead and control latency are of little consequence. However, when the MSM detects that the motor has reached the position specified in a wait command that it is executing on behalf of the APU, it has to send a message to the APU. The script executing on the APU won't finish its wait until this message is processed. During this communication period, which doesn't occur if the MSM executes the script, the motor continues to move. Consequently, the APU will wait slightly longer than the MSM would if it were executing the script. Actually, there is quite a bit of position uncertainty even when the script executes in the MSM. You should keep this in mind when you write scripts that contain wait for position commands.
In all of your experiments so far, whether using a relative or absolute move, the distance or position argument has been a literal number, which means that its value must be known when the script is compiled. There are times when this information is difficult or impossible to know until the script is downloaded or running. To substitute a value as the script is being downloaded, you can use a replaceable parameter. To substitute a run-time value, you can use a VAR. Replaceable parameters and VARs are described in detail in other lessons. You also can read about them in the Language Reference [cdsref- ReplaceableParameters] [cdsref-Vars] [cdsref- VarGroup]. The Language Reference describes their use in the move command [cdsref- StepCountArgument]. Using replaceable parameters involves too many extraneous details for this lesson, so you will not experiment with them at this time. VARS, on the other hand, are easy to use even if you don't know everything about them. In these experiments you will assign literal values to VARS that are subsequently used in motor move commands. Obviously, this serves no real purpose; if you know the value then you don't need a VAR. More realistically, for example, the bottom of a tube might be detected optically; this value could be transformed into a motor step position and stored in a VAR, which could then be used in a move command.
Submit the command RAMP M2 UP 50 TO 250 LINEAR 15% SLEW 250 DOWN 250 TO 50 LINEAR 25% RECOIL 0 HOLD 0 unless M2 already has this trajectory from preceding steps. Submit MOVE M2 + 1 repeatedly until the mark on the pump wheel aligns with a stationary object. The peristaltic pump motor has a 7.5-degree step angle. This means that one full revolution contains exactly 48 steps. Submit the command MOVE M2 + 48 several times to confirm that the wheel always returns to the same position. If it doesn't, you have a motor with a different step angle; experiment to determine how many steps are in a full revolution. You need to know this because you will use the reference point to determine whether the position determined by VAR is correct. Submit the command VAR40 = 48 (or whatever value produces exactly one revolution). Submit the command MOVE M2 + VAR40 several times. Try MOVE M2 - VAR40. M2 should always return to the reference point. Submit the command VAR40 * 4 followed by MOVE M2 + VAR40. The motor executes four revolutions and stops at the reference point. Submit the command VAR40 / 8 followed by MOVE M2 + VAR40. Note that the motor turns exactly one-half revolution. Confirm this by repeating the move, taking the motor back to the reference point.
Submit the commands POSITION M2 1000, VAR55 = 1048, and MOVE M2 TO VAR55. Note that the motor turns exactly one revolution. Repeat the move command. The motor doesn't move at all because it is already at the specified position. Submit the command MOVE M2 + 20 followed by MOVE M2 TO VAR55. Note that the motor returns to the reference position. Try MOVE M2 - 20 followed by MOVE M2 TO VAR55.
Figure 86: Various attempts to move a stepper that is already moving.
Figure 87: Wait for stepper motor position.
{} To top of [MotorScriptTutorial]
[NextTopic] [MainTopics]
REQUIREMENT
The development of a stepper motor overcurrent circuit breaker is motivated by reported driver failures. The TEA3717 stepper motor drivers used in existing instruments experience a relatively high number of failures. This is due at least in part to the fact that each instrument uses quite a few of these parts. Because of the large number of these components used, even a modest decrease in per-component failure may yield significant overall reliability improvements.
Additional motivation comes from reported failure of drivers on the MSM in the EP1.5 instrument under development. Two pairs of motor drivers have failed catastrophically when moving the open probe. The motor is Oriental Motor Vexta PK243-03AA. This is the same motor as used for the CD3200 mixer.
FAILURE ANALYSIS
The motor's windings have 38.5 ohms resistance. At 24V, the largest DC current that can be passed through this resistance is 623 mA. The TEA3717 manufacturer recommends a maximum motor current of 800 mA. At this current, the device generates 4W, which the manufacturer suggests can be removed by soldering the four ground pins into a 20 sq. cm. ground plane. It is not clear whether they expect this ground plane to be on the top surface of the board and uninterrupted by components. In the worst case, with no such ground plane, the device package can dissipate 1W, which occurs at a motor current of 300 mA.
The MSM's motor driver current sense resistors are .675 ohm (4 @ 2.7), setting the low, medium, and high current levels to 118 mA, 370 mA, and 622 mA. The MSM's motor drivers are soldered to a very large ground plane but it is a buried layer and there are surface components around the chips. Consequently, dissipation is better than no ground plane but we are not sure by how much. Nevertheless, it is likely that the 370 mA medium level current can be tolerated and certainly the 118 mA low level is acceptable. This is for the IDLE power setting. For all but very slow step rates, a high power setting is acceptable because the current rises slowly due to winding inductance and is soon reversed by the next step.
For all of our motor drivers, we recommend medium or high power for stepping and low or off for idle. In separate testing, when any of the MSM's motor circuits drives the PK243-03AA motor they experience only a slight temperature rise and no failure using these power levels. As a test, the idle power was changed to medium. The drivers got considerably warmer but not hot enough to fail even over time (although we would prefer to keep them cooler if possible). It is evident that the problem in the EP1.5 instrument is unique to that system, either in the hardware or the flow scripts, and is not characteristic of the MSM driving this motor.
The TEA3717 protects itself against over-temperature by shutting down. It has integrated diodes to protect the output transistors from high voltages induced by collapsing motor winding fields. The manufacturer does not specify the parameters of these diodes but we can assume that they are designed to match the transistor drivers. If a load is acceptable to the drivers, the diodes can probably withstand its inductive kick. PWM-based current limiting establishes the motor winding current level to off, low, medium, or high, but it does not protect the IC from overcurrent caused by shorting to ground because only the lower transistors are turned off. If either motor lead is grounded, excessive current will flow immediately through the upper transistors, causing them to fail. Built-in current limiting may also be unable to protect the transistors when the motor leads are shorted together. The PWM off time is deliberately relatively short. Large current pulses during the on time may heat the transistors, which, because they are bipolar, can fail due to secondary breakdown (thermal runaway caused by self-generated minority carriers). This type of failure may occur even without shorting. For example, if the idle current is set too high, the IC may be close to its thermal limit and susceptible to thermal runaway caused by a fast current spike. Once a bipolar transistor enters thermal runaway, neither the PWM nor thermal shutdown can prevent its self-destruction.
The two most likely causes of driver failure are direct short to ground or between motor leads (i.e. shorted winding) and heat-induced vulnerability to over-current breakdown. The latter would occur when the driver temperature is elevated close to but below the shutoff level and a momentary high motor current creates a localized hot spot (in the transistor) where secondary breakdown occurs. Both of these kinds of failures can be prevented by a high-side overcurrent circuit breaker.
Secondary breakdown failures can also be avoided by using drivers that have MOSFET bridges. Two such drivers that we have experimented with are the A3977 microstepping driver from Allegro and the L6207 from STMicrodevices. Both of these contain two full-H bridges so that one part replaces two 3717/18 devices. However, they are still more expensive than a 3717/18 pair plus circuit breakers. Circuit breakers should also be used with the A3977 (one on each bridge) because it can still fail due to output shorting. The L6207, which is the most expensive solution, has integrated high-side current limiting and is, thus, protected from all common causes of failure.
The A3977 can deliver more motor current (2.5A max, 2A recommended) than the TEA3718 (1.5A max, 1.2A recommended) and provides divide-by-2/4/8 microstepping. However, in our test circuit the A3977 seems to not function completely correctly when microstepping. It is only marginally more expensive than a pair of 3718s but is only available from one source. It is unlikely that the problem in the EP1.5 instrument would be corrected by replacing the 3717s with an A3977 (full-step mode functions correctly). While the A3977 might run cooler, tests show that the MSM's 3717s can drive the motor with insignificant temperature rise. If flow scripts are setting the idle power to high, the A3977 might be more likely to survive, but this setting is not required and might damage the motor.
We do not know the primary failure mechanisms of the 3717/18s in the field. If heat-induced adolescent failure is one, changing to the A3977 would reduce the failure rate. This could be attractive even if we don't (or can't) use the part for its microstepping ability. While a high-side overcurrent circuit breaker can prevent secondary breakdown failure in a 3717/18, it cannot make the driver perform correctly in this case but only prevent catastrophic failure. An A3977 in the same circuit may perform reliably due to its cooler operation (not because it doesn't experience secondary breakdown).
Both the TEA3717/18 and L6207 can be used at 36V. The A3977 cannot be safely used at this voltage. For the most part, this is not significant, as 24V is used for most motors. However, the RSH X-axis motor requires significantly more torque than any of the others. This is achieved by selecting a high-inductance motor, which performs better (by as much as 25%) when driven at 36V. The only IC that we have investigated that can adequately drive one of these motors (current and voltage) is the L6207. Because of its relatively high price, we would prefer not to use the L6207 for all motors. Regardless of whether we use TEA3718 or A3977 for the other motors, a high-side overcurrent circuit breaker will reduce failures.
DESIGN
GENERAL THEORY OF OPERATION
The current from the driver's motor power supply is converted to voltage by a small value (e.g. .1 to .5 ohm) sense resistor. This voltage is compared to a reference. When the current exceeds the maximum threshold, the power to the driver is cut off by switching off a high-side transistor (load switch) and simultaneously triggering a one-shot that prevents the transistor from being turned back on until after a relatively long delay. The long timeout yields a very low duty cycle under shorted conditions so that the load switch and motor driver experience little or no heating.
COMPONENTS
The entire circuit is referenced to the motor supply high voltage, +24V. Except during turn on/off transitions, the load switch operates fully on or off, dissipating only the power dictated by its efficiency when fully on. When off, it sees 24V; when on nearly 0. A P-channel MOSFET is appropriate for this. The Fairchild FDC5614P has been selected. Its maximum voltage and current are 60V and 3A while its power dissipation is 1.6W when properly mounted on a heat spreading pad (the part itself is a very tiny "SuperSOT") and .8W in air. Its Rdson is 0.1 ohm at Vgs = -10V and 0.135 ohm at Vgs = -4.5V. At 2.5A, its power dissipation is .625W (with Vgs >= -10V). This would be the cutoff level for an A3977 at a 2A maximum set by PWM. Thus, even with no heat spreader, the transistor can handle our maximum current requirement.
Except for the load switch, the rest of the circuit does not need to see ground. We want at least -10V relative to +24 for Vgs. Since we already have +12 power, this might serve as the low voltage (i.e. ground) of the circuit. However, because of competing uses for node voltages in the circuit, some margins are rather small and it would be better not to depend on the main 12V power to provide a consistent voltage relative to 24V. To achieve the greatest possible consistency, the 12V should be referenced not to ground but to the 24V power itself. This can be done with a -12V three-terminal linear regulator such as LM7912 or LM320-12. An unusual connection is used: +24 is connected to the regulator's GND input and analog/power ground to the regulator's IN input. The regulator's OUT terminal sources -12V relative to the +24V rail.
As a MOSFET, the load switch gate presents essentially no DC load and can potentially be driven directly by some op amps. However, there are two problems. One is that the FDC5614P begins to turn on at Vgs = -1V (worst case minimum). When turned off, no current flows through Rsense so the transistor's Source will be at 24V. An op amp driving the gate must pull up to less than 1V from the positive rail to prevent current flow during cutoff. The other problem is that the op amp must source and sink a sizeable current to quickly charge and discharge the large 760 pF gate capacitance to drive the transistor rapidly through its linear range while turning on and off. Additionally, to keep the sense resistor's power dissipation low, the sense voltage is .5V. Since this feeds directly to one input of an op amp, the op amp must work with inputs near the positive rail. To reduce cost, size, and components it is better to use one device for both the overcurrent voltage comparator and the MOSFET driver, although different devices could be used if necessary. This circuit comprises two comparators. Actual comparators could be used but none meet the combination of rail-to-rail input and output, high current output, and supply voltage greater than 12V. An inexpensive ($.60) quad op amp TLV2374 (Texas Instruments) provides the following characteristics:
16.5V Vdd max.
-.2V to Vcc + .2V max input range.
output current max +/- 100mA.
@15V Vdd Vout = 14.9/.1V @Iout = +/- 1mA. 14.74/.5V @Iout = +/- 5mA.
unity-gain slew rate typical = 2.7 V/us @ Vdd = 15V.
The 2374 does not meet all of these maxima under all conditions. For example, the output voltages are much further from the rails while sourcing or sinking 100mA. However, the Vout-high doesn't need to be near the rail (+24V) until the transistor is already nearly turned off. It means nothing that the output voltage pulls away from the rail while the op amp is sucking 100mA off the gate. The slew rate is relatively slow by today's standards but this is completely swamped by the gate charging time anyway.
Each of the two motor windings requires a circuit breaker. Since the TLV2374 contains four amplifiers and each circuit requires only two, one TLV2374 could support one motor. However, one possible failure situation is the shorting of both windings simultaneously, for example as might occur if a motor became very hot and fried both its windings. In this situation, both load switches might have to be turned off and on simultaneously (depending on coincidental synchronization of the two one-shots). The 2374 can't source/sink 100mA from two outputs simultaneously. Since it is very unlikely that two motors will simultaneously short, two motor drivers can share two TLV2374s, with one amplifier from each driving the load switch for one motor's winding and another amplifier driving the load switch for a winding of the other motor.
CIRCUIT OPERATION
Schematic [breaker.pdf]
Amplifiers A1 and A2 detect overcurrent in each of the two drivers (one for each winding) of one motor. The trigger current is set by a fixed 23.5V reference on each amplifier's non-inverting input compared to a Vsense caused by the drop across an Rsense. For example, if Rsense is .25 ohm, the trigger point is 2A. When the load current is less than the trigger, the amplifier's output is low (12V). When the current is above the trigger point, the output goes high. The outputs of A1 and A2 are wired-OR using diodes. If either amplifier output goes high, the summing point goes to 23.3V. If both are low, the point is at 12V (by resistor).
Amplifiers A5 and A6 each drive the gate of one of the load switches. They perform identically and synchronously. Two amplifiers are used instead of one only to increase the gate charging current. When the amplifier output is high, the load switch is turned off.
If either A1 or A2 registers an overcurrent both drivers will be rapidly turned off. After a delay, both drivers will be turned back on to try again. The cycle will repeat as long as either driver experiences overcurrent. The only reason for not making the two overcurrent circuit breakers entirely independent is to share the timing circuit.
Voltages
All nodes except the overcurrent comparators' OR node have more than enough design margin in all states. The OR node serves both to trigger A3 (out high) and to force A5 and A6 out high in the overcurrent condition. The difficult condition is when there isn't overcurrent so that A1 and A2 outputs are both low. In this state they contribute nothing to the node voltage. This condition occurs at two times: the normal steady state and during the off time initially triggered by overcurrent. In the steady state condition, A3 is the problem. Its In+ voltage must be lower than the 12.7V of the fully discharged (by A3 out through the discharge diode) timing capacitor so that A3 out will remain low. In the timed off state, A5 and A6 are the problem. Their outputs must remain high even though the overcurrent no longer exists. They are held high by In+ being higher than In-, which is driven by A4 out low. Since A4 out can't be lower than 12V, In+ must be greater than 12V.
The OR node low voltage is determined by a resistor that pulls up to 24V, one that pulls down to 12V and A3 out through the feedback and In+ resistors. In the steady state case, A3 out is low. In the temporary off state, A3 is high when the OR node goes low but then goes low when the timing capacitor reaches the A3 In+ voltage (which due to positive feedback is relatively high even when the node voltage returns low). Thus, for A3 the node low voltage only needs to be designed for the case of A3 out low but for A5/6 In+ it must be designed for the both A3 high and low. Here, A3 low is the problem case, as we are trying to keep A5/6 In+ higher than In-. Since A3 low is the only case of interest for A3 In+ as well as the worst case for A5/6 In+, we can design for it.
A3 Rf and In+ are 47K and 100K. Or node V12R is 10K. V24R is selected as 178K for V24R. The Thevenin equivalents of V12R and V24R are 9.47K and 12.64V. With A3 out low, A3 In+ is at 12.19V giving a margin of .51V (relative to .7V); A5/6 In+ is at 12.6V giving a margin of .5 (if A4 out pulls down to .1V). These margins should be adequate. They could be increased by adding another diode in series with the discharge diode (or using an LED, which has 1.4Vf) increasing the A3 In- minimum voltage, which In+ low must match.
A3 out and Vcap are summed at A4 In- and A4 In+ voltage is established so that A4 out goes high only when both A3 out is low and the timing capacitor is fully discharged. As already explained it is important that Vcap not go so low that A3 In+ low can't go lower. The discharge diode ensures a minimum voltage of .7V. To prevent A3 out low from reducing Vcap lower than this through the summing resistors, the Vcap summing resistor net contains a diode. Vcap would never reach 0V even without this diode because of the timing resistor to 24V but we don't want to reduce the margin between A3 In- low and A3 In+ low by any amount.
The off time is established by the time it takes Vcap to charge up to the A3 In+ voltage with A3 out high plus the time required to discharge back down to .7V. When the overcurrent exists, the OR node voltage will be 23.3V and A3 In+ 15.6V. This causes A3 out to go high, raising A3 In+ to 23.78V. Assuming that Vcap rises much more slowly than the power switches are shut off, OR node voltage will drop back to 20.59V resulting from A3 out 24V through 47K and 12.64V (Thevenin equivalent of V12R and V24R) through 109.47K (100K plus Thevenin R). Therefore, the charging time will be determined by this threshold. Because A3 out is 24V, the summing diode is reverse biased so there is no current flow other than from the timing resistor into the capacitor. The 20.59V threshold is nearly 85% of 24V, which is reached in time = 2*RC. A long power off time is used to reduce the duty cycle of the power elements to a negligible level. However, if the resistor's value is too high, its effectiveness in the A4 In- summing junction is reduced. Therefore, R is 100K and C is .1uF, yielding a charge time of 20 msec. With overload, we should measure approximately 50Hz at A3 out. A test circuit measured a frequency of 70Hz.
A3 out and Vcap are summed for A4 In-. As already explained, the Vcap leg contains a diode to maintain a minimum A3 In- of .7V. Only the A3 out leg has a resistor, 100K even though this is a summing node. The reason that no resistor is need in the Vcap leg is that the only voltage source, 24V, is already supplied through the timing resistor. When A3 out is high, both the discharge and the summing diodes are reverse biased and A3 out high alone keeps A4 out low. When Vcap reaches the 20.59V threshold, switching A3 out low, Vcap directly plus 24V through the timing resistor keep A4 In- high enough to keep A4 low. In this state, the current flows into A3 out through the 100K summing resistor. This is the only time that the current needs to be restricted. Without this resistor, A3 out low would overcome Vcap and prematurely make A4 out low. We don't want this to occur until the timing capacitor is fully discharged. When A3 out is high, A4 In- is 24V. At the top of the charge cycle, A3 out is 12V and Vcap through the summing diode is 19.89V (20.59 - .7). At that instant, A3 In- will be 19.89V. A3 out low quickly discharges the capacitor through the discharge diode. The lowest point will be 12.7V. At this point, A4 In- will be very nearly 12V. To provide some margin, A4 In+ is fixed at 12.4V. This reference can be shared by all circuits.
As previously explained, A4 out low is lower than the OR node low so that even when the overcurrent is removed (by shutting off the load switches) A5/6 out will still be high. When both A3 out and Vcap are back down to nearly 12V, A4 out switches high. This voltage must be higher than the OR node low of 12.19V in order to allow A5/6 out to go low, turning the load switches back on. However, it must be lower than the 23.3V OR node voltage at overcurrent so that the load switches can be immediately turned off. A 50% voltage divider of 47K and 47K to 12V provides A5/6 In- of 18V when A4 out is high and 12V when A4 out is low. To recap:
A3 In+
@OR node low
@A3 out low: 12.19V
@A3 out high: 20.59V
@OR node high
@A3 out low: 15.6V
@A3 out high: 23.78V
A5/6 In+
@OR node low
@A3 out low: 12.6V
@A3 out high: 12.73V
@OR node high: 23.3V (regardless of A3 out).
A5/6 In-
@A4 out low: 12V
@A4 out high: 18V
Achieving Normal Steady State On
Assume that the circuit comes up with one or both A5 and A6 out high, turning off one or both load switches. Assume that neither driver has an overcurrent condition. A1 and A2 outputs are both low and the summing node is low.
Assume that A3 out is low (12V) and Vcap (timing capacitor) is 12.7V. A3 In+ will be 12.19V. A3 out stays low because In+ is lower than In- (with a .51V margin). With A3 out = 12V and Vcap = 12.7V, A4 In- will be 12V, which is less than A4 In+ (with a .4V margin). Therefore A4 out goes high. A5 and A6 In- see 18V. A5/6 In+ is 12.19V so A5/6 out go low (if not already) turning on the load switches.
Assume, instead, that A3 out is initially high (24V). A3 In+ will be 20.59V. A3 can't remain in this state forever because the timing capacitor will charge toward 24V when the discharge diode is reverse biased. However, while A3 out remains high, A4 out is low, presenting 12V to A5 and A6 In-. Since their In+ is 18V, their outputs go high, turning off the load switches. This doesn't change the overcurrent circuit state, because it was assumed that there wasn't overcurrent and, therefore, both A1 and A2 outputs were already low. The circuit would stick in this state except that when Vcap reaches 20.59V, A3 out will go low. This changes A3 In+ to 12.19V, latching A3 in this state, since the discharge diode doesn't permit A3 out low to pull Vcap lower than 12.7V. A4 out remains low even when A3 out goes low because A4 In- is the sum of Vcap and A3 out. When A3 out low discharges the timing capacitor down to approximately 12.7, A4 out goes high (24V). With A5 and A6 In+ now at 18V, their outputs go low, turning on the load switches. At this point, A3 out is low and Vcap is 12.7V, the same condition as previously assumed. This is the steady state no overcurrent condition.
Overcurrent Condition
If either A1 or A2 detects overcurrent, the OR node voltage goes to 23.3V. A3 output had been low (12V) and A3 In+ 12.19V. A3 In+ instantaneously goes to 15.6V, causing A3 out to go high, raising In+ still further to 23.78V. With A3 out high, A4 out goes low. A5/6 In- is now 12V while In+ is 23.3V, causing A5 and A6 out to go high, turning off the load switches. As soon as the load current of the overloaded driver reduces to below the trigger point, the overcurrent comparator OR node returns low. This reduces A3 In+ to 20.59V but A3 out still remains high until Vcap reaches this level. Meanwhile, the load switches remain off. When Vcap reaches 20.59V, A3 out goes low and begins discharging Vcap through the discharge diode. The instantaneous A4 In- voltage at this time is 19.89V and A4 out remains low. A3 out low quickly discharges the timing capacitor to 12.7V whereupon A4 In- becomes 12V and A4 out goes high. This makes A5/6 In- 18V and A5/6 out go low, turning the load switches back on. Since A3 out is low and Vcap is 12.7, this is the steady state condition.
Itrip |
Rs (ohm, W) |
parallel |
For |
1.0A |
.5, .5 |
2*1ohm |
TEA3717 |
1.5A |
.33, .75 |
3*1ohm |
TEA3718 |
2.0A |
.25, 1 |
4*1ohm |
A3977 |
2.5A |
.2, 1.25 |
5*1ohm |
A3977 |
{} To top of [CircuitBreaker]
{} END > To top of [MainTopics]