Thursday, March 12, 2026

ST-Robotics R12 arm - IO ports and expansion ports

IO ports 

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 pin B connections aren't as visible, but I was able to test connectivity by removing the chips from their sockets and doing a continuity test between the pins 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 used a step-down buck converter to bring the +12VDC down to +5VDC.  Then, I set up a test circuit on each pin as:

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

+12v (9) -> step-down +5v -> 330 ohms -> LED+ -> LED- -> PA pin i

GND (14) -> step-down GND 

Note that on my buck converter, the input and output ground lines are common, and I do not connect the output GND to anything.

All eight pins share the same +5v output, but have individual output paths.

Also, 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.

TBD: I think the pins are in reverse order, too.  That is, PA 0 ON turns on Dsub15 pin 8, and PA 7 ON turns on Dsub15 pin 1. 

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 are a shorthand for individual pin access.  Later for the MultiIOcard, we'll see that QA, QB, and QC pin references are done in a different, binary or bit-aware way. 

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.


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 the input  

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 
 
                                          (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.  3076 is around 5v.  4095 is around +10v.

If n exceeds 4095, it loops back around to the negative values, so I think it's paying attention to the lowest 12 bits of a 16-bit value.

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.

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.



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.

 

 

ST-Robotics R12 robot arm with K11R controller

 We've had the good fortune of coming into possession of a 5-axis robotic arm.  It's a K12, made by ST-Robotics in 2018.

The machine is mostly as depicted in other web pages.  The arm looks as you'd expect a red and white robotic arm to look like.  It does not have working LED lights.  

The controller is a K11R.  It is a rather industrial-looking thing, boxy with the appearance of being rack-mountable.  There is a control pad for "teaching" the robot.

Power

The first challenge with the robot controller is that it did not come with an obvious power plug.  In most other pages that show an R12, you see a typical 110V female port on the back.  (I supposed a European model might have a 220V plug.)  Instead, for this one, there was a circular plug, and a locking connector with two pins connecting to external red and black wires.  There was no documentation about intended voltage or current draw.

These days, for any device these days that has some kind of power plug, you will normally see a label, declaring the power type -- voltage, AC vs DC, polarity, and amperage.

I disassembled the controller to determine the voltage.  The inner workings of the controller are pretty spaciously housed, but not clearly labeled.  There was no transformer.

After getting pretty deep into the controller, I found that the red line went through a fuse, and then branched outward from terminal blocks.  Some of the branches connected to the stepper motor power port of Gecko motor drivers.

I then took a peek under the labels that covered the original labels of the arm motors.  After some searching, I found that the typical voltage to drive them would be 24 VDC.  So, I was pretty confident that it would work if I used 24 VDC on red, and 0 VDC on black. 

The documentation (page 9, see below under Documentation and software) states that the power connector has a "6.5A anti-surge" fuse for 110v power and says there's an equivalent 3.5A anti-surge for 220-240v power.   

After adding a bench power supply, pre-set to and tested at 24 V, I hooked up the controller and arm, and got a good "motor powered" single knock sound from the arm.

Subsequently, I've changed things to use a spare 24v, 10A dedicated power supply.  I'm pretty sure I pulled that from a Cube Pro Duo hefty 3d printer.  

Documentation and software

The arm came with a Mini CD.  On it, there is one directory, called manuals.  In that folder you can find many things, including PDF manuals, software, and drivers.

Manuals

There are lots of files provided in the manuals/manualsPDF folder on the CD.   Several documents on the CD are relevant for my set-up, but most are not.  Some describe other arms (e.g., R17) or expansion cards or other hardware that I do not have (e.g., teaching pads, end-effectors). 

Many of the manuals on the CD are not posted online.

Some manuals exist online.  The R12 manual is most commonly found, e.g., https://docs.rs-online.com/d934/0900766b8152ed18.pdf 

Some information I found online was not on the CD.  An important one is the useful tutorial7.pdf.  On subsequent review, it looks like this is a revision of the tutorial5-axis.pdf document in the Manuals/PDF manuals folder on the CD.

The ST-Robotics github page refers to documentation, but the links point to a now-defunct section of the ST-Robotics web site, and thus yield a 404 error.

Software 

Robwin provides the main UI for interfacing with the arm.  There are other command-line ways to do so, too, but unfortunately not all Robwin functions are described in command-line equivalents. 

The github repo provides Python code and ROS (v1)-related code for interfacing to MoveIt and Rviz.  The "python shell" provided at the git repo originally came from the Adam Heins implementation at https://github.com/adamheins/r12

Videos 

There are also several videos available on Youtube from ST-Robotics.  It appears that they are hosted by David N. Sands himself.  

Some of the same content is found in .mp4 form on the CD.

ST-Robotics had one more video posted to Youtube, showing usage of an R12 with ROS (v1). 

Recommendations

I have found these resources to be the most helpful:

1. The R12 manual

2. The tutorial7.pdf pages linked from superdroidrobots (https://www.superdroidrobots.com/blog-a/manuals/tutorial7.pdf)  This may be the same as or an earlier revision of tutorial5-axis.pdf.

3. The MultiIOcard.pdf manual.  This is only on the CD and not posted anywhere.  It describes the expansion ports and card and connections.  This is only relevant if your K11R is configured to include this card.

4. The source code and utilities at the Github repo (https://github.com/st-robotics)

5. The D.N.Sands Youtube videos.

Physical connections

The controller connects to the arm at two connection points, each of which is clearly labeled on the back of the controller.  One of these is referred to in the doc (section 12, Connections, page 25) as "25-way D-type, female", or what I'd call a D Sub 25 female port.  The other is a circular connector with lots of holes and a locking connector.

A front-side RS-232 serial port, labeled COMPUTER, connects to my laptop using a USB-to-Serial connector.  An FTDI driver disk was provided with the machine, but my Windows 7 laptop already had the drivers installed.  

A 9-pin female port labeled TEACH on the front connects the Teach pad.  The protocol for communication between the Teach pad and the controller is not described.  

Most ports are labeled clearly, and really don't give me any opportunity to mess up the connections.

OUTPUT (PA) and INPUT (PB) ports 

The OUTPUT port on the back is a 15-pin D Sub female.  Below it is an INPUT port, which is a 9-pin male port.  It turns out that these are ports PA (OUTPUT) and PB (INPUT).  Connections to these are optional.

On my controller, these were only labeled OUTPUT and INPUT.  The relationship to PA and PB could only be determined by shape, inspection, and testing. 

Expansion ports 

There are three more 25-pin ports on the back, and with a modified panel plate, there would be room for one more.  After much analysis, I've found that the ones I have connect to the J4 and J5 points on a MultiIOcard inside, and the other one appears to be for analog input/output.  As with PA and PB, connections to these are optional.

STOP jack 

Finally, while there is a STOP button on the front control panel, there is also a 1/4" jack on the back of the box for a second STOP switch.  Mine has a 1/4" plug that has two normally closed, temporary switches wire in series between the barrel and tip. 

The simple presence of an empty 1/4" plug in the jack does not allow for normal operation.

Without this STOP switch/plug in place and in a closed circuit state, the controller will power on, but will not respond to arm movements (e.g., will not honor a CALIBRATE command). 

Robwin software set-up (Windows)

The initial foray into working with the robot is to use the provided Robwin.exe program.  That runs on Windows, and provides a graphical user interface.  Among its features, the UI allows you to type and send FORTH commands, kind of like you have a terminal emulator for FORTH.

There is a .msi installer provided on the disk for installing Robwin.

For modern computers, connection to the RS232 serial port is done by a USB-to-Serial connector.  If necessary, an FTDI driver must be installed.  (It's available on a disk provided with the machine, and also can be found online.)   

Even with the driver installed, Robwin will not recognize the controller if it is connected to a COM port numbered higher than 9.  It's not clear why Robwin faces this limitation.  It turns out that upon connection, my laptop-to-controller connection was on COM16.

As it turns out, there are ways to remap the COM ports on Windows to bring a given COM port down to the desired range, as long as all those aren't already taken.  For me, they were already taken.  I disabled other software that was hogging the lower COM ports.  Between that and the remapping, I was able to get Robwin to see the controller.

Initial operation 

Much of the robot operation is as described in the demonstration videos and documentation.

The initial discussion of the machine centers around its use of FORTH as a base language.  I think I read somewhere that it's running a Z80 processor.

The main drivers are Geckos, one for each joint.  

The system is described as having an outer interpreter, and an inner interpreter.  I don't really care which is which.  The thing is, when you connect to the machine, you're using a plain FORTH interpreter.  Then, within that you issue the command ROBOFORTH, and that starts up another FORTH interpreter that has knowledge of all the special commands that let you control the robot.

You connect to the controller, run ROBOFORTH, and then say START.  At that point, if everything has gone well and all the connection checks are ok, the joint positions are assumed to be at 0, 0, 0, 0, 0, wherever the arm is currently positioned.  But, normally (and especially when first learning), you'll want that home position to be a known location, so that's why you run a calibration sequence.

To calibrate, you do this:

  • De-energize the motors (run DE-ENERGISE or DE-ENERGIZE)
  • Manually rotate the waist and arm joints roughly to a position where it's pointing straight up
  • Restore power (ENERGISE or ENERGIZE)
  • Run the CALIBRATE command.  

The CALIBRATE command moves the joints slowly to known points.  As each joint's calibration sensor point is found, it moves on to the next joint.  When all joints are calibrated, it rapidly moves all joints, leaving the arm in a fully up position.

If the CALIBRATE operation moves a joint a certain number of stepper motor steps (or perhaps a pre-defined amount of time), and does not hit its expected calibration sensor, the CALIBRATION operation stops and gives you an error message.  This may happen if a joint wasn't close enough to the sensor position to start with.  In that case, you might just want to run CALIBRATE again right away, and let it continue to the desired point.  However, if a joint is moving significantly away from the upright position, you might be better off de-energizing, manually moving the arm more upright, and trying anew.

The bottom of the arm doesn't really indicate where the "front" is.  You kind of have to guess and experiment, and be ready to hit the STOP button during movements until you're comfortable with its position.

Upon successful calibration, the arm joint positions will be at 0, 0, 0, 0, 0, which is its knowledge of "home".  You can see the step position by running the WHERE command.  If you were in Cartesian mode, you may need to run JOINT and then WHERE.  If you operate in Cartesian coordinates, you will see that the home position is X=0, Y=0, Z=5000 (i.e., 500.0 mm). 

The documentation provides other ways to modify the zero calibration points so that the arm isn't sticking straight up initially.  This can be important for environments that have limited vertical clearance, for example. 

Joint movement

For initial operation of the robot arm, you can enter either Joint teaching mode or Cartesian teaching movement in Robwin.

Clicking the T icon starts movement in joint mode.  Clicking the L-shaped axes icon starts Cartesian teaching mode.  The teaching pad becomes active, and you can move the robot arm joints by clicking on a joint button first, and then + or - to move.  

When in Joint mode, a joint moves continuously as long as the + or - button remains pressed.  The speed button, followed by + or -, changes the rate of joint movement.

In Cartesian modem, movement is stepwise, and the meaning of "speed" changes to mean an axis movement distance.  Lower step sizes might be 1 mm, whereas higher step sizes might be 10mm, or more.

What is joint movement?

The joint movement done by the teaching pad is a reflection of the relative MOVE operations underlying the system.  The controller moves the joint stepper motors by a number of steps, managing acceleration and jerk to reach the destination smoothly and with precision.  (And when I say "smooth movement", that's different from the smoothing function that the controller provides for route travel, explained later on.)

The waist joint rotates the arm around its vertical axis.

When the shoulder moves, everything further down -- the elbow, hand, and wrist -- comes along for the ride.  Similar applies to elbow movement.

Focusing on the elbow, it's important to realize that when it moves, the hand orientation relative to the elbow remains the same.  For example, suppose you calibrate, and the arm is pointing straight up and the hand is also pointing straight up.  If you rotate the elbow 45 degrees, the elbow-to-hand orientation is still straight.  But relative to ground, the hand is now 45 degrees.

The hand and wrist are a bit odd.  When the hand joint moves, the wrist does, too.  That's a result of how the gearing is done to control the wrist.  

What is Cartesian movement?

The controller provides a mechanism for doing "Cartesian" movement.  You can think of this as moving the hand-wrist in an X, Y, Z coordinate space.  At the calibrated home, you're at (x,y,z) = (0,0,5000), where 5000 means "500.0 mm".

When the Teach box is used to move in the Cartesian system, the top, left 3 buttons are for controlling X, Y, and Z motion.

Suppose you want to move the hand/wrist outward along the positive X axis.  You click the X button on the pad, and then hit +.   (The box's "speed" setting controls how far it moves along the axis.)  By moving out along X, the robot cannot possibly extend itself to maintain the original Z height.  So, the software computes new Y and Z locations that will accommodate the intended X position.  It moves all joints as needed to achieve the new X position goal.

Cartesian movements only cover X, Y, and Z positional changes.  As such, to reach a new location in one axis, the controller only has to figure out a "bend" in the other two.  The teach pad still lets you move the hand and wrist joints.  When in Cartesian mode, those are reported back as angular positions instead of motor step counts. 

More operations and details

There are mechanisms for setting up and running smoothed routes.  This is really the main function of the R12 robot.  It was built with the intent that users would move the arm to particular locations, save them as points in a route, and then run the route repeatedly.  
 
The smoothing of a route makes it such that as you move the joints from one point to another, it doesn't do a straight vector movements.  For example, suppose you have a route with four points 1, 2, 3, and 4.  You could have the arm move from 1 to 2, pause, 2 to 3, pause, and then 3 to 4.  That's what a route run looks like without smoothing.  With a smoothed route, it moves from 1 to 4, curving the motions around points 2 and 3.  It's not clear if the kinematics computations attempt to hit points 2 and 3, and maintain momentum, or if it moves near 2 and 3 as it eventually gets to the ultimate goal, point 4.
 

Addenda 

tutorial7 highlights

Some of the important things about the tutorial7 document

  • It seems to have come from ST Robotics originally.
  • Page 12 explains flag bytes that store motor enable and direction for each joint, e.g., 1 MEP C! 0 MDP C! 1000 MOVE for moving the waist 1000 steps.  MEP is preceded by a bitwise value (1 = waist, 2 = shoulder, 4 = elbow, etc.).  MDP is preceded by 0 for positive movement, or 2 for reverse movement.  (Does the existence of a per-joint MEP operation mean that you can selectively de-energize?)
  •  Page 15 shows how the command "TELL joint" is a separate operation from the MOVE or position MOVETO commands.  So, you can say TELL WAIST as one command, and then 1000 MOVE and -1000 MOVE as subsequent commands for relative movement.
  •  Page 19 shows how you can use the POINT name command to store a current location in a variable called name.  Later, you can use name GOTO to move the arm back to that point.  This is different from learned route points, but can be helpful in setting up a route.
  • Page 23 mentions the decimeters PLUNGE command.
  • Page 27 starts talking about "objects" and their presence markers at route locations.
  • Page 28 shows delay operations, e.g., MSECS or n USECS
  • Page 32 discusses some of the route location learning operations that can be performed.  However, it only goes into what the Teach pad is revealing.  What isn't shown are the route location reservation commands:
    • ROUTE name
    • n RESERVE
    • (use MOVE or MOVETO or GOTO commands to move arm)
    • LEARN
    • L.
  • Page 36 shows some commands that are not explained: LINE, EXAD, AXES, TRANSFORM.  It also discusses the CRUN and DSPASSUME commands.  I have not been able to try those yet, and don't really understand their purpose.
  • Page 36 also mentions the COMPUTE command.  This one is important, because it's pretty easy to move the arm using JOINT operations (number of motor steps a given joint moves), but in order to save those, you have to COMPUTE (get the controller to convert joint location information to Cartesian) before you can save the information as a point (using POINT name).
  • Page 39 shows how the L. command can show relative lines.  But, it doesn't really explain how a line is marked as being a relative movement.  This also appears on page 41 when discussing the "set approach" or "Set Apprch" button.
  • Page 50 discusses ENCASSUME which allows the system to pull the encoder locations as the true location.  (It's not clear what the "assume" word means here.  It could be a FORTHism, or it could mean "assume that the encoder locations are representative of a real location and are relatively accurate".  ENCASSUME is referenced later in a description of how you can do "lead by the nose" teaching, wherein you de-energize the motors, move the arm around, and then record a new position using ENCASSUME.  If you don't run ENCASSUME upon re-energizing, it will find a significant difference between the encoder positions and the last known instructed position, and will think that the arm crashed. It won't work well from then on.
  • Page 52 discusses the I/O ports PA and PB, but doesn't go into enough detail.  I'll have a separate page that discusses ports, pins, and bit offsets.  But, it does provide commands such as
    • PB IN .
    • PB 5 BIT? .
    • PB 5 1 WAIT
  • Page 52 also touches on usage of the ADC and DAC pins.  But, it does not really describe which pins of the 25-pin connector are used for what purpose.
 

Tuesday, February 17, 2026

Spinning fan with lights

 Another fun find while thrifting.  This time, it's a little battery-operated fan that shows messages with lights.





 So I got it, of course, mainly to see what makes it tick.

Disassembly 

 The back came off with a bit of prying.  Three AAA batteries power a DC motor that spins the fan.

The front has a dome over the plastic fan blade, and then the fan blade holder holds the PCB.  


The PCB has a little ribbon cable with 7 LEDs built into it.  That ribbon slots into the fan blade.


The dome is press-fit over the fan blades and onto the fan blade holder, which contains the PCB.  Then, that whole assembly is press-fit onto the motor shaft.  The trick is that there's a spring coming up from base, and the spring is connected to the battery.  As such, the DC power to the PCB is coming from the shaft and the spring.

PCB front


 PCB back


There is a little slot in the PCB back where power gets interrupted now and then.  

I think what's happening is that the DC power maintains a reasonable voltage in a capacitor somewhere and that fills up as the motor spins.  Periodically, there's a pulse generated that might kill circuit power, were it not for the cap.  The pulse that occurs is also input to the circuitry to know when the fan has hit a particular rotational point, so it has positional feedback.

The 24C08

The people who made this left me two good things.  

First, the EPROM chip's label was left visible. In other toys, they sometimes bury these chips in a potting substance, or grind the identifying information off of the top layer of the chip.


That's a HK 24C08 EPROM.  It can hold up to 8Kbits of memory, or 1 Kbyte.  It's addressable by I2C.  The pins on it are power, ground, SDA, SCL, Write Protect, and one meaningful Address line.  There are two NC pins.  

The address line allows you to have two of these EPROMs on the same I2C bus, if you want.  In this case, the pin is connected to GND, so that makes it address 0.

Note that there appears to be a little capacitor stuck between pins 7 and 8 on the device.  Pin 8 is Vcc, and pin 7 is /WC (Write Control).  One 24C08 datasheet says, "Write operations are disabled to the entire memory array when write control (/WC) is driven high.  Write operations are enabled when write control (/WC) is either driven low or left floating."

In this case, since there's a cap between Vcc and /WC, it would seem that it's "hard wired" as write-protected. 

I2C SDA/SCL connection

The next nice thing they did was that they built the device with a usable Micro USB port, whose data lines connect directly to the SDA and SCL lines of the 24C08.

While the device had a Micro USB port, you can't plug it directly into a computer USB port and expect it to do anything.  The USB interface doesn't work that way, i.e., it doesn't do direct I2C communications.

I took a Micro USB cable, chopped it, and separated the wires. 

 


Once the wire ends were stripped, I just toned out the connections with a DMM, checking for a connection from each wire to a pin on the 24C08.  In my case, the black and red connected properly to GND/Vss (4) and Vcc (8).  The green wire turned out to be my SDA (data) line (5), and the white one connected to SCL (clock) (6).

Arduino 

I fired up an old Kosmoduino board, which is an Arduino Nano, pre-wired to various controls and devices.  

My initial interest was to use the Kosmos buttons. I found that pin 7 would give me the Button 1 signal, and pin 8 would be Button 2.  I could check regularly in the main loop() function, and if either would go LOW, I could have the sketch do something in response, like read or write EPROM data.


 The green board shown here is just an easy way for me to connect pins.  There are screw-down terminals on the opposite side, so I didn't have to solder the USB cord's wires.

 

For the Arduino Nano, native I2C communications are done on pins A4 (SDA, yellow here connecting to green on the other end) and A5 (SCL, white here).

Arduino has an at24c08 library.  I'm using the one from Stefan Stromberg.  (This is one of many at24cxx libraries for eproms of different storage sizes.)  Its basic examples yielded quick results.

You start by managing the sketch libraries and installing the AT24C library.

Then, in the sketch, you have to include the library

 #include <at24c08.h>

and then you declare the eprom object and its address as a global.

 AT24C08 eprom(AT24C08_ADDRESS_0);

 From that point, you can simply call eprom.get() to read a byte from the device.  This is much easier than trying to figure out the addressing patterns manually.  If you read the datasheet for the 24C08, there's a little dance you have to do, sending a START, pretend-writing to and address, then sending another START, then reading a byte, then STOP.  All of that rigamarole is handled for you in the eprom AT24C library.

Read operations 

I made the main loop check button 1 for a LOW signal, and when that was seen, it would read.

Initially, I tried just reading a few specific bytes, but I progressed quickly to the point of having it report a page (256 bytes) of memory at a time in binary form (highest bit, mask 0x80, down to lowest bit, mask 0x01).  I then adjusted the output to be more legible, using "#" for 1 values, and "." for 0 values.

After a few test iterations, a pattern emerged.  As it turns out, if you read the values in descending address order, you can see the letters' pixels.  Example shown here is the "REDUCE USE 5-8PM" message:

104     ####.### F7
103     ...#.... 10
102     ...#.... 10
101     ...#.... 10
100     ####.### F7
99     ####.### F7
98     #..#...# 91
97     #..#...# 91
96     #..#...# 91
95     #......# 81
94     ####.### F7
93     #....... 80
92     #....... 80
91     #....... 80
90     #....... 80
89     ####.### F7
88     ...#...# 11
87     ...#...# 11
86     ...#...# 11
85     .....##. 06 <- above this, message "SMUD CAN HELP", 13 chars
84     ...#...# 11 <- rendering code
83     .#.....# 41 <- line count

82     ######## FF
81     ...#...# 11
80     ..##...# 31
79     .#.#...# 51
78     #...###. 8E
77     ######## FF
76     #..#...# 91
75     #..#...# 91
74     #..#...# 91
73     #......# 81
72     ######## FF
71     #......# 81
70     #......# 81
69     .#....#. 42
68     ..####.. 3C
67     .####### 7F
66     #....... 80
65     #....... 80
64     #....... 80
63     .####### 7F
62     .######. 7E
61     #......# 81
60     #......# 81
59     #......# 81
58     .#....#. 42
57     ######## FF
56     #..#...# 91
55     #..#...# 91
54     #..#...# 91
53     #......# 81
52     ........ 00
51     ........ 00
50     ........ 00
49     ........ 00
48     ........ 00
47     .####### 7F
46     #....... 80
45     #....... 80
44     #....... 80
43     .####### 7F
42     #...###. 8E
41     #..#...# 91
40     #..#...# 91
39     #..#...# 91
38     .##....# 61
37     ######## FF
36     #..#...# 91
35     #..#...# 91
34     #..#...# 91
33     #......# 81
32     ........ 00
31     ........ 00
30     ........ 00
29     ........ 00
28     ........ 00
27     #..##### 9F
26     #..#...# 91
25     #..#...# 91
24     #..#...# 91
23     .##....# 61
22     ...#.... 10
21     ...#.... 10
20     ...#.... 10
19     ...#.... 10
18     ...#.... 10
17     .##.###. 6E
16     #..#...# 91
15     #..#...# 91
14     #..#...# 91
13     .##.###. 6E
12     ######## FF
11     ...#...# 11
10     ...#...# 11
9     ...#...# 11
8     ....###. 0E
7     ######## FF
6     ......#. 02
5     ...###.. 1C
4     ......#. 02 
3     ######## FF <- above this, message "REDUCE USE 5-8PM", 16 chars
2     ...#...# 11 <- rendering code
1     .#.#.... 50 <- column count (80)

0     ......#. 02

 

Write operations

I made the sketch look for a button 2 LOW signal.  When seen, it would to trigger a Write operation.   I fully expected this to fail, because of the capacitor tying Vcc to /WC.  But, for whatever reason, I was able to write to the EPROM without error!  Maybe the cap has failed, or it wasn't installed correctly.  I thought I would have to de-solder it or otherwise kill it, but I was able to write!

So, I started writing values to various lines to change the message and do other experiments.  

Changing pixels was pretty easy.

Preamble byte 1: column count

Preceding each message phrase, there was additional information.  For each, you would see two bytes.  And, there was also a value in address 0 that didn't fit the pattern.

It turns out that for each message, the first byte is a "column count".  Each letter is rendered as 5 columns of 7 pixels.  So for a 16-char message, there are 80 "columns".  Thus, for the message "REDUCE USE 5-8PM" in addresses 3..82 (80 bytes), the count in address 1 is 0x50 (80 decimal).

There's an upper limit to this value.  You can't render more than 0x5a columns (90 decimal columns, 18 characters).  If you try to set this value to something that is too large, or doesn't divide by 5 properly, the message rendering will fail overall.

Preamble byte 2: rendering code 

The second byte is a rendering code.  For example, Address 2 contained 00010001.  In fact, for the fan I got, it used values 00010001 for all six of the message rendering codes.  The default behavior was to draw the message gradually, left-to-right, wait, and then erase gradually, right-to-left.

With some experimentation, I found that the nibble codes for these rendering code bytes act as follows:

       Draw  Clear
bits   Hi    Lo
0000   LR    RL (same behavior as 0001)
0001   LR    RL
0010   RL    LR
0011   OI    IO
0100   IO    OI
0101   C-LR  LR-C
0110   R-LC  C-LR
0111   Inst  Inst

LR = left to right

RL = right to left

IO = inside to outside

OI = outside to inside

C-LR = Center to Left and Right

LR-C = Left and Right to Center

Inst = show the whole message in one instant without slowly drawing

A rendering code byte has two nibbles (high and low).  As an example, if the high nibble is 0110, and the low nibble is 0011, it will draw the message from the Right and Left edges in to the Center, and then erase from the inner ring to the outer ring.

Message count 

The only thing left to figure out was byte 0.  It was set to 6 initially, and that simply meant "there are 6 messages to display".

7 bits, not 8

The fan has only 7 LEDs going around, not 8.   That didn't really make sense, given the letter patterns in the EPROM made use of all eight bits.  After some experimentation with bit values, I found that the bit at position 0x08 actually is ignored during rendering.  For example, the value 0b11110111 renders the same as 0b11111111.

Below are examples where I've changed the word HELP to other values, just to test.



Easier control

I modified my Arduino sketch to use the Serial port as an input mechanism, rather than using the buttons.

The sketch reads text from Serial into a character buffer.  As I do command text processing, I read from that buffer instead of reading directly from Serial.

There are two main operations: R and W.  The commands are as follows:

Command: R0

Read page 0 (255..0) and report EPROM byte contents in the form address, binary value, hex value.

Command: R1

Same as R0, but page 1 (511..256)

The device does not appear to use pages 2 or 3 of the data for messages.  Those pages can be read, and I'm not sure if they're important (e.g., operational code, or other configuration settings I don't know about).

Command: W decimalAddr binary7

This command lets you write a byte to a decimal address.  If the binary value only has 7 digits, it's assumed to be part of a letter, and split accordingly, inserting a 0 in the 0x08 bit position.  For example, if you say W 24 1010111, it will turn the binary value into 1010 0111 (0xA7) and write that to address 24.  This 7-bit format makes is easier to visualize the character bits that will actually appear. 

Command: W decimalAddr binary8

In the case where you have to write a full 8-bit value (e.g., rendering control codes, or column count information), you enter the binary value with all 8 bits, and it use them as provided without inserting the 0x08 bit.

For example, W 24 10100111 is equivalent to the 7-bit variant W 24 1010111.

Coding trickiness

A few notes on coding.

Debouncing

Since I started by using buttons for input, I added delay() calls as a cheap way to prevent re-reading the button too soon after I'd seen a LOW signal the first time.

Datatype: byte vs int

In early revs, I was calling eprom.put(addr,val) with an int datatype for val.  This would write two bytes, not one.  And, because I was writing bytes in reverse address order, it caused an overwrite of 0 in certain positions.

For example, if you use an int type and do:

eprom.put(80,1);
eprom.put(79,1);
eprom.put(78,1); 

then it would do something odd, like put 0 into positions 80 and 79, and 1 into 78, because it was actually writing two bytes each time.

The way to prevent the 0-overwrite behavior is to use the right data type, or do an explicit case, like this:

eprom.put(80,(byte)1);
eprom.put(79,(byte)1);
eprom.put(78,(byte)1); 

Serial read buffering

As with any character buffer management, be careful with buffer overflow possibilities.  Far too many examples are provided out there, where someone in Arduino land provides an example of char buf[32] and then lets people write off the end of it.  That will wreck up your sketch behavior, and it also teaches bad security coding practices to young programmers.

While none of my commands will expect more than 32 bytes, I keep a running count of characters read per line, and prevent buffering if the count goes too high. 

Next Steps

  • Come up with some clever messages of my own.
  • Communicate with the Arduino Serial interface using something other than Serial Monitor, and pipe a whole set of messages (including message count at Addr 0, line counts, rendering codes, and message pixel columns) as a compiled bundle. 
  • Find more of these devices, because they're fun! 

Final notes

 As it turns out, I didn't really need to do any disassembly of the device.  The Micro USB port is externally available, underneath the fan blade that doesn't have the LEDs.  At this point, I can connect the fan to the Arduino (hacked USB cable), run Write and Read commands using Arduino's Serial Monitor, do Read operations to verify that the data got written, unplug, and test.

If, however, the /WC line were preventing me from proceeding, I would have had to de-solder the capacitor between pins 7 and 8.  (For some devices, I may have also had to bodge wire pin 7 to GND/Vss pin 4 in order to write.

I have not found ways to do any of the following:

  • Change the rendering speed
  • Render around the entire circle
  • Avoid the whitespace breaks between characters
  • Render any other nice things found in other spinners (e.g., the date/time/temperature).