Sunday, March 15, 2026

ST-Robotics R12 arm - more commands, and storing joint positions

I have a few things I want to try to figure out.  One is the possibility of doing direct memory assignments in order to construct a ROUTE, given that my controller doesn't appear to provide the $JL command, and I'm not getting any response from ST-Robotics support.

One thing I'm doing to figure this out is to do a run-through of the manuals again.  Somewhere, I recall having seen a memory layout, so that might help narrow down where the route point storage is in memory.

primer7.pdf 

Along the way, I started reading primer7.pdf.  I don't think I'd read that before.  Notes are below.

UNTWIST
Rotates the wrist back to its zero count position.
(Same as TELL WRIST 0 MOVETO)

That suggests that the wrist position (in joint 5) could be measured as the hand position (TELL L-HAND n MOVE) changes.  Hand is also referred to as pitch.

The SPEED variable range
Range: 2-65000

The ACCEL variable range
Range 100-5000

Teach pad: "tick" or checkmark is "learn".  X is delete.
in Cartesian coordinates, PITCH and W are usable for hand rotation (pitch) and wrist (roll), but in Robwin they are expressed in degrees.

The TOOLSET command appears to be one of those that the stock Python Shell would not allow through, because it's one that prompts the user for additional information after the command is issued.  It looks like it says

TOOLSET

then asks for WRIST PITCH, WRITE ROLL,  TOOL-LENGTH, ALIGN, and EXECUTE.

It also says that beforehand, you can use the command READY to "get the robot down into a safe working position." 

After READY, the Cartesian movement operators are x y z MOVETO or MOVE, and TOOLSET followed by responses.

TOOLSET tests.

> CALIBRATE

> HOME

> TELL ELBOW E-RATIO MOVE

> TELL SHOULDER S-RATIO 2 / MOVE

> TOOLSET

WRIST PITCH      45.0 ?

> 900

900

WRIST ROLL     0.0 ?

> 450

450

TOOL LENGTH      0.0?

>

 

ALIGN ROLL?

> <---- here you can just hit Enter, or enter Y or N.  Y invokes ALIGN mode

 

EXECUTE?

> Y

Y

OK

Now hand is pointing straight down.  Wrist is not obviously positioned differently, but it's hard to tell.  It's not clear what the appropriate responses to TOOL LENGTH and ALIGN ROLL should be.

Important:  the doc says, "EXECUTE? Y to execute the above changes immediately.
Or press enter to defer the changes. They will take place the next time you use a MOVE
or MOVETO command."

So that's the secret sauce for moving the joints and hand and wrist all in one go.  If you were to have a route position stored with a bunch of joint locations, you record their CARTESIAN position.  Then, issue the TOOLSET command to get the hand and wrist ready, but don't execute the change.  Then you do an X Y Z MOVE or MOVETO command to complete the move.

The tutorial says that there are ROUTEs and PLACEs.  However, I think the system doesn't actually support a command or variable called PLACE.  Instead, you end up with numbered places in a route, and you can use the GOTO command to go to one of the places.

An Approach position appears to be some Place that is associated to an existing, numbered PLACE within a route.

Can there be multiple Approach points for a given Place?

Can an Approach place have its own Approach place?

To make a route run in a continuous way, you say routeName CONTINUOUS ADJUST and then follow up with RUN.

R12 manual.pdf

A few more interesting finds here.  

Joint steps (per 90 degrees) 

Where previously I thought I'd gleaned magic information from the github repo about the joint-to-angle ratios, they appear to be provided in these constants:

WAIST    B-RATIO
SHOULDER S-RATIO 
ELBOW    E-RATIO
L-HAND   W-RATIO
WRIST    T-RATIO

So, one quick experiment is to just . those values and see what appears.

> B-RATIO . 

B-RATIO . 3640 OK

> S-RATIO .

S-RATIO . 8400 OK

> E-RATIO .

E-RATIO . 6000 OK

> W-RATIO .

W-RATIO . 4000 OK

> T-RATIO .

T-RATIO . 4500 OK

The 90-degree relationship is implied later on, too, in the calibration step (page 22) where it says you can execute

> TELL ELBOW E-RATIO MOVE

Memory and data dump

The parameters appear to be stored at address A200, length 200.  They mention these parameters: 

bank 0 

start address A200 

length 200

Presumably, those are responses to a "Save binary" command that's only in Robwin, and the values are hexadecimal, not decimal.

Test to perform: 

1. run Save binary on other segments of memory and inspect for Point coordinates

2. run Save binary and see if there are limits on the start address and length

Memory dump, flash backup, and original flash RAM 

Supposedly there's a backup file supplied with the robot, e.g., R12C123.RAM, that can be loaded to re-flash the memory to a good, known state.

Do I have one of those?  Yes, maybe.  In the original disk, the following were provided:

Backups/R12B725.SIG.RAM 512

Backups/R12VA161.RAM 24576

Backups/R12VA6161.ram 24576

There are also R17 and Nexus files in there.

The  R12VA161.RAM has clear strings of FORTH commands within it, as does R12VA6161.ram

One can hope that the SIG.RAM file is some kind of digital signature than can be used to roughly verify that the contents of the other files are legit.  It's only 512 bytes long. 

The two *161 files differ starting at byte 683, and the VA6161.ram file has a 13-DEC-2016 timestamp, whereas the VA161.RAM file is dated 19-JUL-2016. 

If I back up the existing RAM to a file, how does it compare to either of the VA files?

Warning: do not run PSAVE as that could corrupt the FORTH image

More commands

DATUM appears to be a command to seek out a calibration sensor.  The example shown is

> TELL WRIST DATUM

USAVE appears to save "user memory" (not "program memory" as with PSAVE?) to flash.

LIMITS and MAXON may be interesting to explore later.  Also, other calibration commands CAL1, CAL2, CAL3, CAL4, and SETLIMITS

HERALD and WRU ("who are you?") provide version and serial number information that inform the file naming convention for the RAM files above.  There is a suggestion to save a file of the form "serial number.SIG.RAM" after calibration.

> HERALD

HERALD ROBOFORTH II V17.4 for R12, Forth vA (C) D.N.Sands

> WRU

WRU R12B725 OK

That matches up with the filename I'm seeing under Backups.

Page 25 suggests pin 16 of the arm's Dsub25 male port provides +24v to LEDs, but does not mention current limits.

It may be necessary to quote the WRU serial number when requesting support.  (See page 26)

Help Sheets / HELP17 Downloading coordinates.pdf

Until now, I hadn't bothered to look in the Help Sheets folder for any information!

There is useful information in here.

Apparently, you can transmit route coordinates to the arm using the RX command, followed by (0D) (is that a carriage-return character?) and then send x, y, z, pitch, and roll positions (x, y, z in decimeters, pitch and roll in decidegrees).

The first line of GETLINE says LINE# !.  I take that to mean that GETLINE is given a line number, and it's stashed in the pre-defined variable LINE# upon entry.

What does E! do?  Google says it stores a floating point value in Extended Memory.  E@ is the opposite, fetching the value.

What does MOVES do?  It's not clear, but it seems like MOVES is a pre-defined value that represents the count of spots stored for route.  The code starts with 0 stored in Extended Memory in the MOVES location (specific to the route).  It then loops by adding 1 to MOVES and then calling GETLINE with that value, so GETLINE starts at line 1 and goes up from there.  As each 

What does GETLINE do?  GETLINE is a function defined in the sample code.  It reads a line of input from the user, parses out five values from it, and stores the values in memory relative to the start point of a ROUTE.

Each line is stored relative to a ROUTE's line #0 offset.  Each line is a set of 8 16-bit quantities representing joint positions (motor steps) for WAIST, SHOULDER, ELBOW; angular positions for L-HAND and WRIST; and extra flags for relative or absolute position or function ID, object ID occupying a space, and perhaps a flag for approach vs. not.  There's a doc on that somewhere in the set.

What does ETX mean?  The term ETX represents a ctrl-C character and the digital value 3.  The user runs the RX command, provides sets of 5 values for coordinates on each line, and finally sends a ctrl+C on an empty line to stop transmission.  The names TX and RX here are used as transmit-receive terms, viewed from the perspective of the controller.  When RX is running, it's receiving data, and the computer sends an end "transmission" code when done.  The code isn't really clean in that it uses ETX both as a stop signal and as a comparator, allowing two other states (0 and 1) to be returned in FLAG1 from within GETVAL.  This code is from the days before #define constants in C, or similar in other languages.

The sample code in HELP17 defines an RX command, where earlier code examples in the page suggest RX is built-in.e The RX command does not exist natively.

It defines GETVAL which parses a "value", presumably, out of FLAG1 (?) until a 0D or 03 or delimiter is seen.  It does not process ASCII characters <= 0x2f, meaning some symbols, including dots and dashes, are excluded.

GETLINE calls GETVAL five times, hoping to find non-negative numerical input.  As values are seen, it maintains a total value, shifting values.  It's odd in that it multiplies the result by 10 with each new digit, but then ANDs the new character with 0xf without checking for values in the ASCII range 3A..3F range.  It really should ensure the new character is a digit.  In its current form, one could enter the characters of this set -- :;<=>? -- or even higher values, and still get data stored.

As each numerical value is parsed (including part-way through a line), the value is stored in memory. The ones provided by the user are stored at memory offsets 0, 2, 4, 6, and 8 relative to the line start.  Then, the values 0, 2, and 0 are stored in relative positions 10, 12, and 14.  (The code is inefficient in that the final three values are stored every time a new number is parsed.)

The line that stores a data entry value looks like this: 

LINE# @ LINE I 2* + E!

Since LINE# represents the value passed in from the outside, and starts at 1, then "LINE# @" would be 1 initially.

LINE is meant "give back the relative address in Extended Memory where this line is located", so it gives back values like 16, 32, or 48 for the first route defined.

"I" is the iterator of the DO loop, which is called with limit 5, index 0.  That gets multiplied by 2, ending up yielding the values 0, 2, 4, 6, and 8.

So in the end, this line says, "value = getval(); addr = LINE(LINE#) + 2*i; poke(addr, value);", where LINE() already knows its memory offset based on the selected ROUTE. 

What does this mean: 0 LINE# @ LINE 0A + E! ( 6th value

The comment "6th" value refers to the sixth 16-bit quantity stored for that line.  Since we're storing relative to position 0, the first five values are at positions 0, 2, 4, 6, and 8.  The 0A value shown represents hex value 10, which is an offset pointer to the 6th datum for the line.  The seventh and eighth values are also assigned using offsets 0C and 0E in subsequent lines.

Test

> connect

> ROBOFORTH

> START

> CALIBRATE

> TELL WAIST B-RATIO 2 / MOVE

> TELL ELBOW E-RATIO MOVE 

> CARTESIAN

> COMPUTE

> WHERE

X Y Z PITCH W(ROLL) LEN. OBJECT

176.7 176.7 250.0 0.0 0.0 0.0

> ROUTE TMI

> 10 RESERVE

> LEARN

> L.

WAIST SHOULDER ELBOW L-HAND WRIST OBJECT 

01 176.7 176.7 250.0 0.0 0.0 0.0

> JOINT

> WHERE

WAIST SHOULDER ELBOW L-HAND WRIST OBJECT 

1820 0 6000 0 0

(and other output)

> LINE# @ .

LINE# @ . -1 OK <--- this shows that the variable LINE# already exists in the system.

> 1 LINE . 

. 32 OK <--- this shows that the memory offset to line #1 for this route is 32.

> 2 LINE .

. 48 OK

In HEX, the WHERE output in JOINT mode looks like this now

> HEX

> WHERE

WAIST SHOULDER ELBOW L-HAND WRIST OBJECT

01 6E.7 6E.7 9C.4 0.0 0.0 0.0

> DECIMAL

WAIST SHOULDER ELBOW L-HAND WRIST OBJECT

01 1820 0 6000 0 0 

> CARTESIAN

X Y Z PITCH W(ROLL) LEN. OBJECT

176.7 176.7 250.0 0.0 0.0 0.0

> JOINT

> HEX

> WHERE

 

WAIST SHOULDER ELBOW L-HAND WRIST OBJECT

71C 0 1770 0 0

What about E@?  That appears to be presenting values in decimal and 16-bit quantities.

> 0 E@ . 10 OK

> 1 E@ . -7000 OK

> 3 E@ . 469 OK

Wider ranging data dump

OK, so I wrote this:

> : DISP

64 0 DO

LINE# @ LINE I 2* + E@ .

LOOP

;

and then ran

> DISP

and got back

DISP 10 6990 4 0 0 0 8 -1 C0  -57F6 4 1 0 -1 8 0 6E7 6E7 9C4 0 0 0 2 0 -1 -1 -1 ... -1 

So the data storage is visible at the 16th value (or 32 offset, given these are 16-bit quantities) for line 1.

> 10 E@ .

C0

> 12 E@ .

-57F6

> HEX

> 20 E@ .

6E7

> JOINT 

> TELL WAIST 200 MOVE

> WHERE

WAIST SHOULDER ELBOW L-HAND WRIST OBJECT

91C 0 1770 0 0

> LEARN

LEARN OK

> L.

WAIST SHOULDER ELBOW L-HAND WRIST OBJECT

01

02 91C 0 1770 0 0 0.0 (not displayed is that this was a relative move?)

and sure enough those values are now shown by re-running DISP and doing

> 30 E@ . 91C OK

The next thing I don't quite get, though, is: what if there are multiple ROUTEs?  Maybe I don't really need to know that, given I want to transfer more of the control to the Python layer.

<soapbox> 

And really, that's what gets me about this system.  I liken it to what I saw in the Tandy and Panasonic dot-matrix printers of olde.  Back in the day, they would put more and more features into the firmware layer of the printer device.  They would add character sets, and fonts, and such.  But eventually, everything became just a pixel.  It became more important to have fonts defined in the software, and have the printer drivers just send graphics.

Similar has happened to the FDM 3d printer world.  There were basic, and good, and really good 3d printers out there, but eventually they figured out that the computations in an external computer (be it a Raspberry Pi or a PC), rather than updating firmware with limited computational horsepower within the printer.

So with this robot arm, you have to ask: maybe it is a better to circumvent the whole FORTH and ROBOFORTH layers and operate directly against the Gecko motor controllers?  The physical build of the robot arm is really good, including its calibration sensors.  We already have ROS and similar doing kinematics computations, so why can't we just let ROS do that work, and have a more basic system whose responsibility is motor control?

</soapbox>

The 6th, 7th, and 8th values in a LINE

Referring back to tutorial5-axis.pdf page 37, a memory layout per line is described, but you have to tie that together to HELP17 to really understand how to get to that data.

The line is laid out as

Addr Purpose
0    Wrist
2    Shoulder
4    Elbow
6    L-Hand
8    Wrist
10   Motor 6, if applicable
12   EXAD
14   OBAD

EXAD lookup

0 ABSOLUTE JOINT

1 RELATIVE JOINT

2 ABSOLUTE CARTESIAN

3 RELATIVE CARTESIAN

Any "much larger" value means it's the CFA of a function to perform.  This can be tested by finding the CFA of GRIP (using FIND GRIP) and storing the term GRIP in the route.  The doc doesn't make it clear where a function parameter value is stored in a given LINE.

Note that there's a separate section about PLACES that might also store location information.

There's a command shown, "VIEW LINE 5", that reports an individual line of a route.

NEAR can be used to move to a location 50mm "higher" than position i.

i INTO can be used via the i NEAR point and into the target position i.

UP moves away from a target position to its approach position.  (It's not clear how the distinction between approach and non-approach is represented.) 

i GOTO goes to position i directly, apparently without consideration for a "NEAR" point.

ENCTEST apparently lets you see encoder positions repeatedly while de-energized, so that would potentially be part of "lead by the nose" training. 

INTVEC can be used for writing custom interrupt-handling code.  Examples of interrupt coding and timer interrupts are around page 54 of tutorial5-axis.pdf.

?TERMINAL indicates whether an Escape key press occurred on the terminal, but it's likely that that doesn't work with the Python shell.  Similar for CTRL-C.

Next experiments

Relative moves and LEARN operations

Approach settings and memory.  tutorial5-axis.pdf says that an approach position is marked with an 'R' in the coordinates.  Something in the way it's described suggests there's an Approach position per route, not per point.  As such, maybe having an Approach position works only with a route where all positions are ABSOLUTE CARTESIAN except for the one relative position?  Nah, that'd be nutty.

L-HAND and WRIST value representation

How are negative relative moves handled?  Given  S-RATIO is the largest value of the ratios at 8400 for a 90 degree move, then a 180-degree move would be 16800.  Since that's the case, then that's within range if the joint movement is recorded as a 16-bit signed int, having a max positive (32767) or max negative (-32768) value. 

HELP20 data logging.pdf 

This document describes where user RAM is stored, and how to get to it.

It shows a little routine that starts at 0x2000 and stores a value from the stack to RAM, 2 bytes at a time, until it reaches 0x2fa0.

It also suggests using Robwin and its Save Binary function to pull the data from "bank 0", starting at address 0x2000, and of length 0x1000 or less (rounding length up in 0x100 increments) to a file.  Presumably, similar could be done using custom code.

It also uses the term CONSTANT for setting up the value of LRAM.

It also uses a function FILL that appears to be of the forms byteVal startAddr endAddr FILL and that appears to operate in bytes, not 16-bit values.

It's not clear if that kind of storage / logging would be useful for ROS purposes.  For ROS, it'd be more useful to have a timed trigger that's regularly capturing the encoder positions (ENCASSUME) and storing those in a known RAM location, perhaps with a RAMBUSY flag to avoid partial data being returned. Ideally, a caller could get the data set (including the RAMBUSY flag) in one go, and if it's valid (RAMBUSY == 0), use it to capture joint positions, while the arm is moving.

 

Thursday, March 12, 2026

ST-Robotics R12 arm - IO ports and expansion ports

IO ports 

Official doc

After doing all the analysis below, I found that much of the information below, plus further info about the PB port, is tucked away in the Help Sheets/HELP11 How to use the I-O.pdf file.  Still, there are some things that I have here that aren't shown in the help file, including actual pin positions.  I guess in my stubborn way of looking at things, I have come to think of "Help" files differently from "Manuals" so I just wasn't expecting to find much there.  But there is quite a bit of information in those files.

Output and Input Ports (PA and PB)

The controller comes with OUTPUT and INPUT ports.  These correspond to the terms PA and PB in the documentation.  OUTPUT (PA) is a Dsub 15 pin female port on the back, and INPUT (PB) is a Dsub 9 male port.  Both ports are in a horizontal orientation.

Note that in standard Dsub 15 pin numbering, the female pins are read right-to-left, and the male port pins are read left-to-right.

OUTPUT (PA) port pins

  8   7   6   5   4   3   2   1

  o   o   o   o   o   o   o   o

    o   o   o   o   o   o   o

    15  14  13  12  11  10  9

    +   +   +   +   +   -   - 

 

INPUT (PB) port pins

     1   2   3   4   5

  .   .   .   .   .

    .   .   .   .

    6   7   8   9


Internally, the first row of PA pins connect to two ULN2068B devices.  They are socketed, which is nice from a maintenance standpoint.  The "u" notch is positioned pointing towards the connector header on the board, which then goes to the exterior port on a ribbon cable.

The ULN2068B is a quad-Darlington pair device.  Each Darlington pair has K, C, and B pins.  C is the collector, and B is the base.

The base pin of each Darlington pair goes through a resistor array and back to the controller somewhere.

Each collector (C) pin is connected to one of the PA port input pins.  The physical positioning of each device allows easy connection of pins 8, 2, 16, and 10 to one set of header pins, and in the same order, 8, 2, 16, and 10 to the other set of header pins.  As such, the first device's four collectors go to pins 1, 2, 3, and 4, and the other device's four go to pins 5, 6, 7, and 8. 

The device's B connections aren't obvious, but I was able to test connectivity by removing the chips from their sockets and doing a continuity test between the socket points and the resistor array points. 

The output port's lower pins are set up as V+ (pins 15, 14, 13, 12, 11), and ground (GND) (pins 10, 9).  As expected, the associated wires are colored red and black on the corresponding internal cable.

Measuring the voltage output across pins 9 and 14, for example, yields +12VDC.

For testing, I initially used a step-down buck converter to bring the +12VDC down to +5VDC, and then sent the 5v through 300 ohms to an LED.  But a simpler solution is just to use a larger resistor, and skip the buck converter.

The test circuit on each pin is:

For each pin i in range (1..8)

+12v (9) -> 675 ohms -> LED+ -> LED- -> PA pin i

All eight pins share the same +12v output from one of the V+ pins, but each has its own path to ground.

The LED that I used has a voltage drop of 1.2V, so the current per LED is (12 - 1.2) / 675 == 0.016 A (16 mA). 

NB: the GND pins on the PA port appear to connect to case ground.

Software activation of PA pins 

To activate an output pin (i.e., to allow the flow of current through a collector), there are convenience functions in ROBOFORTH called PA n ON or PA n OFF.  Note that the n value here is in the range 0..7.

Using the above test circuit, you can say "PA 1 ON" or "PA 3 ON", and one of the LEDs will be lit. 

These functions for PA n ON/OFF provide control of individual pins, one at a time.  Later for the MultiIOcard, we'll see that QA, QB, and QC pin references are done in a different, binary way, where all pins can be set in one operation. 

The R12 manual (page 6) has an odd warning, saying, "The gripper drive module uses PA 0 and PA 1 so PA 1 should not be used for any other purpose".  However, it does not associate PA 0 nor PA 1 with physical pins.  One page 14, it only shows connections between pins 8 (black) and 11 (red).  We now see that what they're describing is a device the gripper is provided +12v on pin 11, and it drains back to pin 8 when the corresponding ULN2068B base pin is activated by saying "PA 0 ON".

I think that if you don't have a standard gripper end-effector, and don't use the built-in GRIP and UNGRIP functions, then the restriction on PA 0 and PA 1 usage isn't applicable.

With those, we find these pin mappings

ON = Darlington base enabled
OFF = Darlington base not enabled
 
PA ON/OFF OUTPUT port pin
   0        1
   1        2
   2        3
   3        4
   4        5
   5        6
   6        7 
   7        8 
V+ (12V)    11, 12, 13, 14, 15
GND         9, 10 

GRIP, UNGRIP, and TON

As mentioned earlier, GRIP and UNGRIP operate using PA 0 and PA 1.  With the LED test circuit in place, we can now observe that.  Issuing GRIP causes PA 0's LED to turn on and stay on.  UNGRIP lights PA 1 for a short period, and then both PA 0 and PA 1 are turned off.  There isn't a visible dimness to the light, but it may be that not be very noticeable.

The R12 manual, page 17, says, "During closing PA 0 is switched on and off at approx 500Hz.  Once closed PA is left full on."   So I think basically they're saying that they're using PWM to apply a 50% duty cycle (they don't specifically say whether it's on and off evenly during closure) to the PA 0 pin, and thus the grip motor closes more gently.  Upon reaching closure (how is that defined?) it holds at a 100% duty cycle.

Then, it says that "UNGRIP asserts PA 0 off and PA 1 on again at 500 Hz until the gripper is open then asserts PA 1 off".  So I take that to mean that PA 1 is driving a reverse motor gently while opening, and upon having the gripper open (how is that defined?) it turns PA 1 off again. 

The GRIPCHECK command appears to be tied to PB6, so that's reserved, too, in a way. 

In my testing, I had nothing connected to PB6.  Presumably, that's the pin that's being used to know whether the motor is closed or open in some way.  With PB6 disconnected in my case, the GRIP and UNGRIP operations still worked.

Later, on page 17, it introduces a few more variables.  TGRIP is the "time allowed for GRIP and UNGRIP".  It doesn't say what happens if the operation fails to grip or ungrip in the allowed time.

It also says that TON (meaning "time on") allows you to change the 500 Hz timing (by issuing n TON !).  A higher value causes a faster grip.  I suspect this means that TON is changing the PWM duty cycle.  The document does not describe min or max values for TON.

ROBOFORTH does not present a corresponding TOFF variable.

It's not clear if there's any other way to control PWM on any other PA pins than PA 0. 

The tutorial5-axis.pdf document also says that the commands GRIPPER ON and GRIPPER OFF can be used to activate the "pneumatic (not electric) gripper without any delay".

Further, the tutorial5-axis.pdf document says that the "Normal value for TON is 1000 so power is on for 1000 and off for 1000 uSecs".  This is a very odd way to describe PWM behavior, but it suggests that the max value for TON should be 2000 and the duty cycle is TON / 2000.  But, I can only really understand the behavior by analyzing it with an oscilloscope.  The description that the "power is on" for 1000 uSecs and off for 1000 uSecs sounds like it goes on for one second, and then off for one second.  It'd be nice if ST-Robotics could provide a graph of the waveform (ideally screen-captured from an oscilloscope) so we can get a better understanding of what's going on.

Still, all that is really only relevant if you have either of the electric or pneumatic gripper modules, and I do not.  I may, though, try to build one of my own, once I figure out how these actually behave. 

Actual GRIP, TON, and TGRIP PWM behavior

I hooked up a Tektronix 465 scope and took a bunch of video captures of signals, varying the TON value.  It turns out that it's a pretty simple concept that has a simpler explanation.

It appears that the GRIP function is made active at a PWM power mode for a total time of TGRIP (in milliseconds, I think).  The default value for TGRIP is 500, so it's given a half second to grab slowly, and then it goes to full grab power.

Within the TGRIP amount of time, it does PWM, cycling the circuit for TON microseconds enabled (allows current flow through the collector) and then 1000 microseconds disabled.  Here are some duty cycle examples

TON setting  Active      Inactive     Duty cycle
500          500 usecs   1000 usecs   33%
1000        1000 usecs   1000 usecs   50%
2000        2000 usecs   1000 usecs   66%
7000        7000 usecs   1000 usecs   87.5% (7/8ths) 

Clearly, the formula is: duty cycle = TON / (TON + 1000)

This applies to PA 0, pin 1 of the OUTPUT port (Dsub 15).

At power-up, my controller had TON set to 7000.

To set a TON value, use the command

n TON ! ( e.g., 4000 TON ! )1

To inspect the TON value, use the command

> TON @ .

I would guess that the maximum value you could assign to TON is 65535 since it appears to be dealing with a 16-bit quantity.  It's not clear if the value would be flipped negative at the 32768 point (i.e., it's not clear if it was properly implemented as an unsigned int16 as opposed to a signed int16).  Practically speaking, it shouldn't matter, given its purpose. 

Naming conventions

As it turns out, there is a bit of a hidden naming convention in the IO pins.  "A" is for output, and "B" is for input.  There's also "C" that has half input and half output, as we'll see later.

PB wiring

I have not yet tested these inputs.

MultiIOcard ports

The MultiIO card, once wired up, gives you access to potentially three more Dsub25 "digital" male ports, plus an additional Dsub25 Analog female port.  Mine only came partially configured, so there are only two of the Dsub25 "digital" male ports.

The MultiIO card (see MultiIOcard.pdf)  is similar to what is depicted in MultiIOcard.pdf, page 2, but in my controller, the layout is a bit different.  The Gecko drives are positioned underneath a stack of cards that share a bus, and the boards are rotated 90 degrees clockwise compared to the picture.

Still, the junctions are as depicted.  J4 connects to the port labeled QQ, and J5 connects to QR.  I do not have the large IC shown in the upper part of the board, and I do not have a connector or Dsub25 represented as RR.

The analog Dsub25 female, AN, is connected to junction points J2 and J3.

QQ and QR were unlabeled on the outside of my controller box.

Naming conventions

As it turns out, the names QQ, QR, and RR are associated to halves of the Dsub25 for different purposes.  QQ (J4) handles the QB and QC pins.  QR (J5) is meant to be shared between QA pins and RB pins, though for my controller, the "R" bank isn't working, presumably because that IC is missing from the board.  RR would then represent the RA and RC pins.

This mapping of QA,QB,QC and RA,RB,RC can be found on page 4 of the MultiIOcard.pdf document.

As mentioned earlier, "A" means output, "B" means input, and "C" provides 4 pins for input, and 4 for output.

The physical-to-logical layout of the pins is described on page 4 as well.  But, the indication of "which pins on QC are for what purpose" isn't described.

Testing the QQ and RR ports

The MultIOcard.pdf document page 3 shows a sequence of commands used to test the proper output activity on QQ and RR, but doesn't really describe what's happening behind the scenes, nor does it describe how to test the input pins.  You first run a "PROGPIA" command.  It's not clear what that does, but it appears to activate the MultiIOcard.  Then, you run the "HEX" command which enables dictionary words 00..FF.  With that, you can use the command hh QA OUT to send a hex byte hh to the port.  To read the value back in, you use QA IN X. (declare QA as the port you want, IN pulls the data byte from it, and X. prints the value in hexadecimal).

If the value written matches the value pulled back in, then the chip is working.

Even with that test defined, you may want to test for a valid connection from there through the HE headers to the actual output Dsub25 pins. 

Software activation of QA pins 

Unlike the PA port, where the port activation opens up a collector to ground, the QA output ports actually emit +5v when activated, and 0v when not activated.  

The pin table shown on page 4 of MultiIOcard.pdf does not show the association of QA bits to corresponding QA0..QA7 references. 

To work with the QA output pins, you can use the n QA OUT command.  This can be done in decimal values, but to me it's more easily "visualized" using hex values.  So, I prefer to say something like

> PROGPIA

> HEX

> AA QA OUT

> 55 QA OUT

> 1C QA OUT

Similar applies for the QC port, but only the highest 4 bits are used for output (0x80, 0x40, 0x20, and 0x10 bits, combined with OR as desired).

> DECIMAL

Software input on QB pins

The input ports can be read as an entire byte, or using individual pin references.

To read a full byte, you can use the IN and X. commands as shown earlier:

> QB IN ( get a value from QB onto the stack )

> QB IN X. ( get the value from QB and print it in hexadecimal format )

To read individual bits, you can AND the values with a bit mask

> DECIMAL

> QB IN 16 AND .

allowing you to test an individual bit or a set of bits at once. 

The document tutorial5-axis.pdf, page 50, also shows that the BIT? operation can be used for individual bit tests.

> QB 5 BIT? . 32 OK 

You can also do a WAIT operation to loop until you see a transition from 0 to 1, or 1 to 0, e.g.,

> QB 5 1 WAIT ( wait for bit 5 to transition from 0 to 1 )

I have not tested the WAIT operation yet. 

Note that for both the BIT? and WAIT operations, the bit number (e.g., 5 above) is in the 0..7 numeric range, not 1..8.  Also, the WAIT operation has a built-in STOP check.

Danger

The pins for each of the Dsub25 connections does not appear to go through a socketed device, as was done for PA.  It also does not appear to have any optoisolation as might be found on an IO1 card (see IO1 card.pdf).  One safety approach that might be worth pursuing is to create an opto-isolation board for QB and QC inputs (12 of the pins of QQ/J4).

The headers for Dsub25 for each of QQ, QR, and RR, are physically the same.  However, their purposes are quite different.

Comparing the "first" 8 pins of each Dsub25 (pins 1, 14, 2, 15, 3, 16, 4, 17): 

QQ (J4) pins are "B" inputs QB0..QB7

QR (J5) pins are "A" outputs QA0..QA7

RR (J6) pins are "C" ins and outs RC0..RC7

Because of this, be very careful in what you attach to the pins.  A given test circuit for one has the potential to blow up another.

Electrical characteristics 

The pin limits are not described.  For example, it's not clear what the max current draw is for the QA output pins.  I've made the assumption that they're about as limited as an Arduino, so my test circuits have LEDs with 330 ohm resistors in series, and that has worked so far.

As with the input pins, it may be a good idea to opto-isolate external circuitry from the QA pins.  Since I can light up a visible LED, it's doable to light up the internal LED of an optoisolator. 

Input pins test circuit

For the input pins on QB and QC, I set up a test circuit:

Input pin ---+--> 1k resistor -> +5v
             |
             +------ pin header  gap  pin header ----> GND

Each pin is pulled high to 5v, and if a jumper is added across the two header pins, the value goes low.  As such, this is inverse logic.  By default, QB pin values, using this circuit, yields a 1 bit, and adding the jumper yields a 0 bit.

These pins were set up on a board matching the QB pins: 1, 14, 2, 15, 3, 16, 4, 17.

Test: jumper on pin 1

> QB IN X. 00FE OK

Test: jumper on pin 2

> QB IN X. 00FB OK

Test: jumper on pin 7

> QB IN X. 007F OK

Output pins test circuit

For the eight QA pins and the upper four QC pins, the output test circuit is similar to what was used for PA, except that we don't need the step-down converter, because the voltage supplied on the output pin is already at +5v.

QR port (J5), for each PIN_i (1, 14, 2, 15, 3, 16, 4, 17, aka QA 1..8):

PIN_i -> 330 ohms -> LED+ -> LED- -> GND (pin 18)

The ground line is shared across all LED cathodes.

QQ port (J4), for each PIN_i (8, 21, 9, 22, aka QC 5..8): 

PIN_i -> 330 ohms -> LED+ -> LED- -> GND (pin 18)

Turning a QA or QC pin on turns the corresponding LED light on.

See mapping below for relationship of QA and QC pins to Dsub25 pins. 

QA port and pin associations

NB: QR is the Dsub25 that correlates to J5.  The first set of pins is for QA, and the latter set is for RB, but my MultiIO card was not configured to support the R pins, or its corresponding IC was removed for some reason. 

NB: Examples of "n QA OUT" shown below must be preceded by the commands PROGPIA and HEX.  You can, of course, use decimal equivalent values instead and use the command DECIMAL instead of HEX. 

QR (Dsub25) pin  QA bit  Set pin to +5v
                         > PROGPIA
                         > HEX
 1               0x01    1 QA OUT
14               0x02    2 QA OUT
 2               0x04    4 QA OUT
15               0x08    8 QA OUT
 3               0x10    10 QA OUT
16               0x20    20 QA OUT
 4               0x40    40 QA OUT
17               0x80    80 QA OUT

NB: +5v is on pin 5, GND is on pin 18 (as with all QQ, QR, RR ports, if wired properly).

QB port and pin associations

NB: QQ is the Dsub25 that correlates to J4 

                                          (Decimal)       Both BIT? and AND
QQ (Dsub25) pin  QB bit  BIT? equivalent  AND equivalent  result value
 1               0x01    QB 0 BIT?        QB IN 1 AND     1 or 0
14               0x02    QB 1 BIT?        QB IN 2 AND     2 or 0
 2               0x04    QB 2 BIT?        QB IN 4 AND     4 or 0
15               0x08    QB 3 BIT?        QB IN 8 AND     ...
 3               0x10    QB 4 BIT?        QB IN 16 AND
16               0x20    QB 5 BIT?        QB IN 32 AND
 4               0x40    QB 6 BIT?        QB IN 64 AND
17               0x80    QB 7 BIT?        QB IN 128 AND   128 or 0
 

QC port and pin associations

QC (Dsub25) input pins behave similarly to QA and QB, but the "first" four pins are inputs, and the "second" four are outputs.
NB: there's a skip in the pin numbering here relative to the end of QB, because 5 is +5V and 18 is GND
NB: QQ is the Dsub25 that correlates to J4 
NB: for testing QC, I used the +5v on pin 10, and the GND line on pin 23 instead of similar pins 5 and 18, so that 5 and 18 could be used for QB testing. Refer to MultiIOcard.pdf (aka Multi-function board 11-48A) page 4 for pin QQ/QR/RR pin names. 
                                          (Decimal)       Both BIT? and AND
QQ (Dsub25) pin  QC bit  BIT? equivalent  AND equivalent  result value
 6               0x01    QC 0 BIT?        QC IN 1 AND     1 or 0
19               0x02    QC 1 BIT?        QC IN 2 AND     2 or 0
 7               0x04    QC 2 BIT?        QC IN 4 AND     4 or 0
20               0x08    QC 3 BIT?        QC IN 8 AND     8 or 0

The remaining QC pins are outputs similar to how J
 
QQ (Dsub25) pin  QC bit  Set pin to +5v
                         > PROGPIA
                         > HEX
 8               0x10    10 QC OUT
21               0x20    20 QC OUT
 9               0x40    40 QC OUT
22               0x80    80 QC OUT

Analog port

The analog port is a Dsub25 female port, oriented vertically, and positioned near the OUTPUT (PA) and INPUT (PB) ports.  

It appears to be connected to the J2 and J3 junctions on the MultiIO card.  Their function is not well described.  

The tutorial5-axis.pdf (page 50) says:

When fitted there are 4 input channels to the ADC 0-3 and 2 DAC output channels A and
B. To read an input
0 ADC reads channel 0 and leaves the value on the stack.
0 DACA sends 0 volts out of DAC channel A
 

and then MultiIOcard.pdf has this brief blurb for testing:

Quick test of the analog input-output
Connect AN connector pins 2 and 8 (DACA to ADC channel 0)
Enter
0 DACA 0 ADC . - answer should be roughly zero
1000 DACA 0 ADC . – answer should be roughly 1000
etc.
 

Based on the descriptions, it seems that the ROBOFORTH variable name patterns are

DACA

DACB

0 ADC

1 ADC

2 ADC

3 ADC

 Near J2 and J3, there are three socketed devices.  All three are labeled 44AE0RG3 / LF / 412CN.

The closest I can find is a Texas Instruments device called LF412CN, "Low Offset, Low Drift Dual JFET Input Operational Amplifier".  Its Applications suggested list includes "Fast D/A Converters".

Each LF412CN has two pairs of inputs (one inverting, one not) and two outputs.  Thus it makes sense that having three of these would cover the four ADC plus two DAC functions above.

NOTE: Digikey says some versions of the LF412CN are obsolete. Presumably, the one I saw was obsoleted due to Pb content, given the name of this suggested substitute: https://www.digikey.com/en/products/detail/texas-instruments/LF412CN-NOPB/8893

The device pins are

1 OUTPUT A

2 INVERTING INPUT A

3 NON-INVERTING INPUT A

4 V-

5 NON-INVERTING INPUT B

6 INVERTING INPUT B

7 OUTPUT B

8 V+ 

Dsub 25 pin connections to op amps (LF412CN) 

Since these are socketed, I removed the devices and found the following with just continuity testing.  (ASCII depiction of Dsub 25 female port pins, port labeled AN.)

                    o  13 NC?

            25  o  

                    o  12 NC?

            24  o  

                    o  11 NC?

            23  o  

                    o  10 GND

            22  o  

                    o  9  GND

            21  o  

                    o  8  U3_7 (OUTPUT B), and R1 to ?

            20  o  

                    o  7  U3_1 (OUTPUT A), and R2 to ?

            19  o  

                    o  6  GND

            18  o  

                    o  5  U5_5 (NON-INV INPUT B), and R8 (1M) to GND

            17  o  

                    o  4  U5_3 (NON-INV INPUT A), and R7 (1M) to GND

            16  o  

                    o  3  U4_5 (NON-INV INPUT A), and R6 (1M) to GND

            15  o  

                    o  2  U4_3 (NON-INV INPUT A), and R5 (1M) to GND

            14  o

                    o  1  GND

All pins 14..25 tone to GND

All GND lines are common, connecting to case GND.

R5..R8 are 1 M ohm.

Resistances for R1 and R2 are not yet measured, and it's not known what else they connect to, but it's not GND.

LF412CN and HE and AN pin associations 

U4 and U5 are the LF412CN devices for the J2 socket.  

 Socket numbering, my way, J2 and J3 connector -- note 'V' key position.  J2 corresponds to U3 and U4.  J3 corresponds to U5.

                 +-------+ 
                 |       |
       +---------+       +---V-----+
       |   5    4    3    2    1   |
       |   o    o    o    o    o   |
       |                           |
       |   o    o    o    o    o   |
       |  10    9    8    7    6   |
       +---------------------------+ 

The associations between J2 pins and AN pins is

J2  AN
 5   1 GND
 1  14 GND
 4   2 U4_3 + pulldown
 9  15 GND
 3   3 U4_5 + pulldown
 8  16 GND
 2   4 U5_3 + pulldown
 7  17 GND
 1   5 U5_5 + pulldown
 6  18 GND

 The associations between J3 pins and AN pins is

J3  AN
 5  NC GND
 1  19 GND
 4   7 U3_1 + resistor + ?
 9  20 GND
 3   8 U3_7 + resistor + ?
 8  21 GND
 2   9 GND
 7  22 GND
 1  10 GND
 6  23 GND 

More internal devices

On the J2/U4/U5 side of the board, there is a device labeled ADS7824P.  It has a "BB" logo, which correlates to an online data sheet that says "Burr-Brown (TI)".  That's a 4-channel, A/D converter.  

On the J3/U3 side of the boards, there is a device labeled AD7237 JNZ / #1513 / AL83868, a "Dual 12-bit complete DAC with double buffered byte loading".

Analysis 

The documentation says, "there are 4 input channels to the ADC 0-3".  Given we have four pins on J2/U4/U5 connected to AN 2, 3, 4, and 5, and given the physical positioning of the ADS7824P, it looks like those are the Analog Input pins.

Similarly, the documentation says that there are "2 DAC output channels A and B", and we see two pins connected to J3/U3, and physically proximate AD7237.  As such, it seems AN 8 and 7 are the Analog Output pins.

Then, recall the test above:

Quick test of the analog input-output
Connect AN connector pins 2 and 8 (DACA to ADC channel 0)
 

Enter
0 DACA 0 ADC . - answer should be roughly zero
1000 DACA 0 ADC . – answer should be roughly 1000
etc.
  

What they're really saying is:

AN 2 = ADC channel 0

AN 3 probably is ADC channel 1

AN 4 probably is ADC channel 2

AN 5 probably is ADC channel 3

AN 8 is DACA

AN 7 is probably DACB

If I got that right, then I think the documentation makes things a little confusing, only because the order of ADC vs DAC is reversed (they say pins 2 and 8 (DACA to ADC channel 0) where I think it would be better written as pins 8 and 2 (DACA to ADC channel 0).

Reference ground is available on a number of pins, but the proximate ones are:

ADC (J2, U4, U5): AN 2,3,4,5 have ground nearby (and represented in the connector) as  AN 14, 15, 16, 17, 18 (nearest pins on the wide side, adjacent to 2, 3, 4, 5).  But, you could also use AN 1 or AN 6 on the narrower side of the D sub 25.

DAC (J3, U3): AN 7,8 have ground nearby (and represented in the connector) as AN 19, 20, 21.  But, you could also use AN 9 or AN 10 on the narrower side of the D sub 25.

What is the purpose of the three LF412CN devices?

It seems the LF412CN op amp U3 is there for the purpose of amplifying DAC output to the pins to a particular voltage level.  Device pins 1 and 7 (OUTPUT A and OUTPUT B) find their way out to AN port pins.  

It seems, also, that the LF412CN op amps U4 and U5 are there to reverse-amplify the ADC signals before they're provided to the ADS7824P.  I suppose they also thereby provide some level of safety to the ADS7824P.  If anything's going to blow up, it's the socketed U4 or U5 chip, not the ADS7824P.

Existing AN wired-up connector

The controller came with a Dsub25 male connector that had three wires attached.  Orange = pin 2, unlabeled.  Two gray wires = pin 17 and pin 18, which we know now are GND.

As such, that connector could have been used for ADC channel 0 testing, but safe voltage is not yet certain. 

I re-wired it in order to be able to test against pins 2, 3, 4, 5 (input ADC), and 7, 8 (output DAC). 

Voltage testing (DAC outputs)

Wire AN 7 and 8 for testing V+ output.  Wire to correlated ground pins AN 19, 20, or 21.  Shrink-wrap for safe insulation.

Connect to multimeter.  Insulate/protect any unused wires for safety.

Issue values for the range 0..1000 DACA or 0..1000 DACB and report voltage results.

Test 1: AN 8 and GND

startup: 10v!

0 DACA = -10v

2048 DACA = 0V

4095 DACA = 10V

4096 DACA = -10V AGAIN!

3076 DACA = 5.03V

value is not affected by DE-ENERGIZE

Value is not properly controlled in a non-START condition.

> connect

> ROBOFORTH

> 3076 DACA = -9V

> START

> 3076 DACA = 5V

Connected to pin 7, DACB has similar controls, but what I'm seeing is a significant delay as the DAC output transitions.

First, on pin 7, at startup the pin is giving me around 4.37V, whereas DACA initialized at 10V.

> connect

> ROBOFORTH
> START

still at 4.37v

2048 DACB

0v

4095 DACB

10v

But then the next thing I'm observing is that the transition between values causes the DAC to dip to 0v before going to the new value.  

3076 DACB

Multimeter shows 0v for a little bit, and then 5v.

4095 DACA

4095 DACB

So... in the end, good news and bad news (maybe?) on the DAC front.

Good news

  • I'm pretty sure of my pin analysis.  DACA is AN 8, and DACB is AN 7.
  • Voltage can swing from -10V to 10V, PWM, presumably.
  • Both DACA and DACB lines appear to be working, though the initial voltage is odd.  (Would a 1 Meg or 100K resistor to GND help to bias it to 0v?  It seems like a solid voltage, like it's not floating, like maybe the controller just hasn't initialized a value.  But it is an amplified signal, so maybe it is floating behind the op amp?) 

Bad news

  • Initial voltage is strange and inconsistent.  Anything you connect should be prepared to receive +5 to +10 VDC at startup, prior to connection and until the first DACA or DACB command is issued.
  • Voltage can swing negative, so any circuit attached to the lines should have appropriate rectifying / guards. 
Maybe the negative voltage swing is actually a good thing.  If it's safe to drive a small DC motor with that pin, it could allow bidirectional operation with speed managed by PWM.  Typically, though, it'd be safer to have a separate motor controller that can do that work. 

ADC inputs

Now that I know what DACA and DACB can emit, I can feel comfortable tying DACA to ADC 0, 1, 2, or 3. 

To test the inputs, I'll connect DACA (pin AN 8) to one of the inputs, starting with ADC 0 (AN2) and going up to ADC 3 (AN5).

Given the negative voltage that I'm not really expecting, and the initial value of 10v, I'll do this:

> connect
> ROBOFORTH

> START

> 2048 DACA

> 0 ADC .

 and then vary the values to DACA

Indeed, all of that worked.  ADC 0 reported back values that were typically 3 lower than what was sent in from DACA, even when ADC 0 was set to a 0 (-10v).

Repeat with ADC 1 (AN 3).  Same.

Repeat with ADC 2 (AN 4).  Same.

Repeat with ADC 3 (AN 5).  Same.

Analog - end result and findings

The Analog port AN is a Dsub25 female port.  A diagram of the pin numbers is shown earlier in this document.

It is connected through HE connectors to ports J2 and J3 on the MultiIOcard.  (That is an optional card, so this information may not be pertinent for all K11R controller owners.) 

DAC (output PWM signal) 

The DAC output ports are available on AN pins  8 (DACA) and 7 (DACB) with corresponding ground on neighboring pins AN 19, 20, or 21.  (Other ground pins on the Dsub 25 exist, but these seem like good choices due to proximity and header connections.)

DACA initializes at 10v

DACB initializes at around 4.4V

Neither gets a new voltage unless you issue the n DACA or n DACB command.

The value of n DACA or n DACB must be in the range 0..4095.  0 yields approximately -10v.  2048 is around 0v.  3072 is around 5v.  4095 is around +10v.

If n exceeds 4095, it loops back around to the negative values, so the controller is only paying attention to the lowest 12 bits of the 16-bit value in DACA or DACB.

Observed on an oscilloscope, the signal from DACB was a steady positive or negative voltage, not a PWM signal.  It's safe to assume DACA behaves the same way. 

Input ADC pins 

ADC ports are available at AN pins 2, 3, 4, 5.  The ADC command, given a channel number parameter, reads the DAC value from the device and puts it on the FORTH stack.  The pin and ADC channel print command relationships are:

AN 2 = 0 ADC .

AN 3 = 1 ADC .

AN 4 = 2 ADC .

AN 5 = 3 ADC .

Recommended corresponding ADC GND pins are  AN 14, 15, 16, 17, or 18.  (Other ground pins on the Dsub 25 exist, but these seem like good choices due to proximity and header connections.)

All four ADC pins had no problem reading back values in the 0..4095 from DACA.  As such, the pins are capable of receiving -10v to +10v, relative to GND.  It's not clear what would happen if an externally generated voltage outside of the -10v to +10v range were to be sent in to an ADC pin.  Hopefully, something like +12v would not generate a number "larger" than 4095, and thus loop around numerically (e.g., generate 4915 or hex 0x1333, whose low 12 bits would be 0x0333 instead of maxing out at 0x07ff).

DAC and ADC

Behind the scenes, the DAC and ADC pins connect to an op amp LF412CN before going to an ADC device (ADS7824P) or DAC device (AD7237).  

If the LF412CN chips blow up, they are socketed and are therefore replaceable.  Replacements currently cost about $2, ignoring shipping/handling.

Recommendations

If the AN pins are going to be used for any active mechanism (e.g., an end-effector), we should add a little bit more safety, because the AN pins are not set up with a consistent default voltage at power-up.

It might good to include an "enable" mechanism in the end-effector circuit.  That then would be controlled by a QA or QC output pin.  The startup sequence would be PROGPIA, 0 QA OUT, 0 QC OUT, 2048 DACA, 2048 DACB to get things in proper order.  After that's been established, the program can enable the circuit using whichever QA or QC pin was chosen, and then set DACA or DACB values as desired.

I'm not sure what to do about the voltage drop problem which I observed on DACB.  If, for example, a grip mechanism were to be holding an object with DACB at 4095, and then if we were to lessen the grip to DACB at 4000, we don't want the gripper to release completely.  But, it looks like that might happen, given the momentary voltage drop to 0v.  It may be a good idea to use a capacitor on the DAC output to avoid sudden changes.  It probably would be a good idea to try to capture that voltage drop on an oscilloscope to understand it better (particularly, measure its duration). 

Next steps

Consider looking at AN output pins on an oscilloscope both to understand voltage and PWM, but also to record and measure the perceived voltage drop.

Wednesday, March 11, 2026

ST-Robotics R12 arm - Gecko drives and max current draw

 I got a little more insight into the current draw for the motors.

In the CD of documents provided with the robot, there is a sub-manual for the Gecko drives.  It's in Other / G203V-REV-7-MANUAL.pdf (Rev 7, January 7, 2010).

That manual says...

The G203V will accommodate motor winding currents from 0 to 7A. Use the following equation to calculate the value, (in kilo-Ohms) of the current set resistor: 

R (in kilo-ohms) = 47 * I / (7 – I) 

Use the nearest standard value 5% tolerance, 1/4W resistor for this setting.

Here are the current set resistor values for motor current in .5A increments. Round the appropriate answer to the nearest 5% resistor value.
a. 1A – 7.8K
b. 1.5A – 12.8K
c. 2A – 18.8K
d. 2.5A – 26.1K
e. 3A – 35.25K
f. 3.5A – 47K
g. 4A – 62.67K
h. 4.5A – 84.6K
i. 5A – 117.5K
j. 5.5A – 172.33K
k. 6A – 282K
l. 6.5A – 611K
m. 7A – OPEN

 Separately, there is a table on page 20 of the R12 manual (PDF manuals / R12 manual.pdf) that lays out the resistor settings and amperage for the joints.

1 WAIST 3.0 36K
2 SHOULDER 3.5 47K 
3 ELBOW 3.5 47K
4 L-HAND 2.0 18K
5 WRIST or R-HAND 1.0 8.2K
6 WRIST 5 1.2 12K

Most of these amp values match that Gecko documentation table.

Does that mean the overall current draw is the sum of the amperages of the motors, plus some for the controller?  Maybe.

Even when a stepper motor is idle, it draws current as long as the windings are powered.  The Gecko specifications also say "Auto Current Reduction: 71% of set current, 1 second after last Step Pulse", but if all motors are pulsing in a given move, it seems like it could be drawing max current during that time and until one second beyond.

If that's the case, then even for my robotic arm that is lacking a 6th joint, I'll up needing a power supply rated for 13+ Amps (!).


ST-Robotics R12 robot arm and controller - ROS and Python

One path I'm exploring with the K12 arm and its K11R controller is whether I can make the robot operate with ROS.

I'm completely new to the ROS world of Robotics.  This puts me at least 8 years behind the learning curve.  And as such, I might not explain things really accurately here.

ROS 

The initial idea is that ROS is a bunch of middleware software that provides a common way for things to talk to each other.

There are "nodes", which conceptually could be things that have information.  In our world, a joint could be a node.  A set of configuration information could also be a node. 

Then, you have publish-subscribe mechanisms.  As an example, the robot arm could have joint location information.  You could have something that publishes the arm's joint location on a regular basis.  Then, you could have a visualizer that reads the joint information and renders it graphically.

There are also ways in which you can send actions, and so on.

This all came about with ROS way back in history.  Since 2018, ROS has moved on to ROS2, and the underlying APIs for coding nodes and actions, and the ways in which things are launched, has changed.

ST-Robotics has published a git repo at https://github.com/st-robotics that provides software for ROS, but it's for ROS v1 and hasn't been updated to V2 yet.  In fact, the last commit I could find was from September, 2018, and there hasn't been anything there since.

Nonetheless, the repository provides some insight into their intent.

ROS environment

In order to run ROS, I installed Ubuntu 24.04.4_LTS.  I could grab the ros package as part of Ubuntu, but found that the ST-Robotics code was built based on the ROS melodic version.

To keep things orderly, I set up a Docker image, and then set up ROS melodic within that.

The next thing I ran into is that ROS provides a user interface in MoveIt and Rviz, but you have to do some extra steps in order for the outer system's X display to be accessible from the inner docker instance.  

Then, I ran into issues with Python.  Part of the implementation in the ST-Robotics code is using a version of python and its dependencies that are quite outdated.  These days, too, it's recommended to run a virtual environment in python so as to avoid perturbing the global python libraries that everyone shares.  This is done for proper security reasons.

Overall, I have gotten to the point where I have ROS set up, but I don't really know how to launch the right pieces in order for a configured arm to appear in Moveit and Rviz.

However, I found some useful bits in the python shell and in the r12_hardware_interface code.

The python shell

Within the git repo, there is a section call r12_python_shell.

This originated as a shell built by Adam Heins.

The way it works is that you can run the r12_python_shell/r12/r12-shell script as an executable within Linux.  This depends on its shell.py, and arm.py.

shell.py is a subclass of the cmd.Cmd object, which basically provides you with terminal emulator behavior.  Text comes in to its reader, and the words get compared against known values in self.commands.  The parent object (cmd.Cmd) looks at strings that match commands, and redirects to functions of the form do_name() as they're found.

If a match for a string is not found in the list of do_name methods of the object, the default() method is called.

In this way, shell.py defines its own special set of commands (e.g., "connect"), and passes through other commands directly to the controller.  (BTW, from a naming standpoint, I think "arm.py" could be "controller.py" so as to distinguish between a K11R controller, and the device it's controlling.)

Before it can do any work with the controller, though, the system has to connect via the USB-to-Serial interface.  This is handled in arm.py in its search_for_port() method.  It scans the known USB devices for one that is from vendor 0x0403 and has the product ID 0x6001.

In Linux, you can see this kind of information by using the lsusb command and other similar commands.  However, you may need to be provided access in order for that to work.  First, the outer shell has to start docker in a particular way in order for the docker instance to see the usb devices.  For me, they're found at /dev/ttyusb0.  Second, even if the docker instance can see the usb device, the user that you're logged in as, when in the docker instance, has to belong to the right permission group.

Once set up, a typical sequence that I run is this:

1.  Start a terminal window in Linux

2. Start a docker instance, allowing the created docker instance access to the outer X display, and access to the usb devices.

$ cd

$ cd r12_python_shell

$ cd r12

$ ./r12-shell

This provides me with the cmd.Cmd prompt and awaits instructions.  The first thing to do is connect to the controller

> connect

And then, typically, I'll start ROBOFORTH, get the system fired up, and get the arm calibrated.

> ROBOFORTH

> START

> CALIBRATE

Modifications to the shell

I haven't diffed the original Adam Heins code against the version in the r12_python_shell area.  But I did try various operations and ran into strange delays at points.

One of the main problems was in getting the Teach pad to work.  After calibrating, I would run

> TEACH

and then it would sit there for a long time, and come back with an odd "Speed? <n>" message.

If I would run

> TEACH

again, then it would turn the lights on on the Teach pad, and I would be able to move the arm in joint mode.

The cause of the delay is that the basic python shell sets up a 30 second serial read timeout.  This is defined in arm.py.  It's there because they wanted to allow the arm enough time to run a calibration sequence without timing out.

The reason this gets in the way of the TEACH operation is that TEACH actually immediately responds with a request for a motor movement speed setting.  Normally, if you run this, the controller will respond as follows:

> TEACH

Speed? <16>

and then it waits for more input.  But the response isn't of a form that the shell was expecting, so it sits there waiting for more.  I think the second TEACH command I was sending was not being interpreted as an actual command, but as a response to the "Speed?" question.

The way I've gotten around the delay is to make the timeout amount much smaller (2 seconds), generally.  If the command is CALIBRATE, it adjusts the timeout (for that call only) to the longer 30 seconds.

The next problem is that normally you should be able to run TEACH, get the Speed? <value> response, and just hit Enter to accept the default value.  This is the reason that some of the documentation says "Type TEACH and hit Enter twice".  What they're really saying is "Enter the TEACH command, and then accept the default speed setting."

The problem in the Python shell is that the default cmd.Cmd code treats an empty line as "repeat the last operation".  Not wanting that, they overrode the command in shell.py's emptyline() method to just "pass", meaning it will not do anything.

Instead, we want an empty line to pass the empty line down to the controller.  I changed the emptyline() method to just call self.default("").

That leads to a third problem.  In its current form, if the default() method is given an empty string, it will not pass it along to the controller.  There are a few lines at the start that check for all uppercase in the string.  If it fails the check, it returns immediately.  It is a faulty assumption that everything going to the controller is uppercase.  In the case of TEACH, the response to the command is a numeric speed, or an empty line.  So, I've disabled the uppercase "Unrecognized command" check.

With that in place, it now works to enter Teach mode (equivalent of the T icon in Robwin), move the arm around in Joint mode, and hit the check mark on the Teach pad (or in British English, the "tick") to exit teach mode.  Then, I can run WHERE, or COMPUTE and POINT name, to capture the joint positions.

An interesting side-effect of this change is that lowercase commands are flowing through to the controller now.  But, they get uppercased somewhere in the path, so they still work.  "energise" is treated as "ENERGISE", and so on.  The only thing that doesn't make it through to default() is a command that matches the special python shell set of commands, like "connect" or "exit".  I've also found, interestingly, that uppercase "EXIT" behaves differently than lowercase "exit".

Cartesian mode with the Teach pad

I have not yet figured out how to get Cartesian movements of the Teach pad to work in the python shell.  They always end up giving me an error.  I think I have to observe what's going on with Robwin so I can know what has to be entered in the python shell.

Python shell vs. r12_hardware_interface

With just enough knowledge of the python shell's shell.py and arm.py code, it became clear that the same code is being used (with slight changes, probably) in the r12_hardware_interface arm.py code.

The r12_hardware_interface is a "ROS package providing a FollowJointTrajectoryAction interface to the ST Robotics r12 arm. Requires r12_moveit_config and my_r12_description packages. Based on Adam Heins' python shell for the r12." 

It has two ROS things going on.  

One is the joint_trajectory_action_server.py.  I don't fully understand it yet, but I'm starting to get it.

The _execute_cb() is a callback that is provided as a parameter to the SimpleActionServer constructor at startup.  Presumably, someone calls this to say "you, arm, go here!".  Within the _execute_cb(), you can see that it sends FORTH commands like "FIND TRAJECTORY" to find the route named TRAJECTORY.  It forgets it if it already existed (releases the variable and reserved memory).  Then, it creates it anew by saying "ROUTE TRAJECTORY" (create a route of the name TRAJECTORY), and follows that up with "500 RESERVE" (allocates space for 500 movement positions in the route).

Then, it converts the provided set of coordinates to joint positions, sends them to the controller using a special $JL command, and moves the arm along the route by issuing a special $RUN command.

After the route execution has begun, it loops and calls _update_feedback().  That in turn calls arm.read_pos().  That calls another special ROBOFORTH command, $, which gets the 5 joint positions back as an array.  (The serial response buffer may also have 'SPEED = nnnn' in the way, generated by the $RUN command, so the code has to ignore that on every arm.read_pos() call.)  

NB: I think the code that parses the joint positions could be improved with a regular expression parser.

The action server code continues to loop until the arm position indicates that it's close enough to the intended movement goal.

Separately, the arm position is also being published via the joint_state_remapper.py().  I think this allows some other ROS node to subscribe to the arm position and update its understanding of the arm's position.  I think this allows other subscribers (e.g., a UI visualizer like RViz) to see what the arm is doing. 

$JL, $RUN, and $ commands 

One big roadblock I face is that my controller does not understand the $, $JL, or $RUN commands.  Without these, the current ROS code won't work, and I can't consider upgrading the ROS v1 code to be based on ROS2.  It's like a fundamental dependency isn't being met.

The $JL command lets this external system (MoveIt, or any other caller from Python land) construct a set of ROUTE points without actually moving the robot arm.  Recall that in the originally intended operation from ST-Robotics, a user would move the joints with the Teach pad, or lead the arm "by the nose", and capture joint locations.  Those locations would be stored as positions within a ROUTE.

As it stands, it is not clear where ROUTE memory is stored.  It's also not obvious if there's some other way to hack values into a ROUTE without actually moving the arm.  The only way I know to add a point to a route is by using the LEARN operation, and that pulls the joint positions from the encoders.  $JL is like a "poke" or "store" operation that allows programmatic assignment of the route position without reading the encoders.

The $RUN command appears to be similar to a normal RUN operation.  But, perhaps it's non-blocking.  I'm not sure.  I think ideally it returns immediately (whereas I think RUN is a blocking call) and that allows the rest of the system to continue, checking for positional changes until complete or interrupted.

The $ command is terse, and provides a way to get arm joint position information while the $RUN is in process.  Its output differs from WHERE in few ways.  First, it seems to always report position information in joint form, i.e., as motor step positions.  It does not report data in Cartesian form.  Second, it's terse.  The command is a single character, reducing transmission speed, and the response is just a set of five integers.  By comparison, the WHERE command returns a header line and three lines of data, and it may return Cartesian values if you don't ensure you're in JOINT mode first.

The github repository states that the $JL and $RUN commands are required, and if you can't run them, you should contact ST-Robotics support.  I've tried that, and have not heard back.

Other findings

I'm a little confused why the python shell (and subsequently the arm.py code in r12_hardware_interface) establishes serial communication with two stop bits.   Other parts of the documentation mention how you can change the baud rate, and I believe it says to use one stop bit.   The existing shell code works with two bits, so I'm not wholly inclined to try to change that to one.  But it may be an opportunity for a very small speed-up.  I think it may be doable to improve overall communication speed by just bumping up the baud rate.  That might not work in high (electrical) noise environments.

One neat thing I found in r12_hardware_interface  / joint_state_remapper.py is conversion ratios.  There is this chunk of code:

RATIOS = [3640, 8400, 6000, 4000, 4500]  #Joint motor counts corresponding to 90 degree rotation

This defines the number of steps required on each axis in order to move 90 degrees.  While it's not clearly commented in the code, the order of values is waist, shoulder, elbow, hand, and wrist.

So, you can say

> connect

> ROBOFORTH

> START

> CALIBRATE

> TELL WAIST

> 1820 MOVE

> TELL ELBOW

> 6000 MOVE

and you should have the waist rotated 45 degrees (half of 3640) and the elbow-to-hand section level to the floor.

These values will be important if I'm to do any of my own route computations.