8 👨‍🏫 Teach Bittle New Skills

"Give a cat a fish and you feed it for a day. Teach a cat to fish and you feed it for a lifetime." 🎣

8.1. Understand skills in InstinctBittle.h.

EEPROM has limited (1,000,000) write cycles. So I want to minimize the write operations on it.

There are two kinds of skills: Instincts and Newbility. The addresses of both are written to the onboard EEPROM(1KB) as a lookup table, but the actual data is stored at different memory locations:

  • I2C EEPROM (8KB) stores Instincts.

The Instincts are already fine-tuned/fixed skills. You can compare them to “muscle memory”. Multiple Instincts are linearly written to the I2C EEPROM only once with WriteInstinct.ino. Their addresses are generated and saved to the lookup table in onboard EEPROM during the runtime of WriteInstinct.ino.

  • PROGMEM (sharing the 32KB flash with the sketch) stores Newbility.

A Newbility is any new experimental skill that requires a lot of tests. It's not written to the I2C nor onboard EEPROM, but the flash memory in the format of PROGMEM. It has to be uploaded as one part of the Arduino sketch. Its address is also assigned during the runtime of the code, though the value rarely changes if the total number of skills (including all Instincts and Newbilities) is unchanged.

8.2. Example InstinctBittle.h

//a short version of InstinctBittle.h as example
​
#define BITTLE
#define NUM_SKILLS 4
#define I2C_EEPROM
​
const char rest[] PROGMEM = {
1, 0, 0, 1,
-30, -80, -45, 0, -3, -3, 3, 3, 75, 75, 75, 75, -55, -55, -55, -55,};
const char zero[] PROGMEM = {
1, 0, 0, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,};
​
const char crF[] PROGMEM = {
36, 0, -3, 1,
61, 68, 54, 61, -26, -39, -13, -26,
66, 61, 58, 55, -26, -39, -13, -26,
...
51, 81, 45, 72, -25, -37, -12, -25,
55, 76, 49, 68, -26, -38, -13, -26,
60, 70, 53, 62, -26, -39, -13, -26,
};
​
const char pu[] PROGMEM = {
-8, 0, -15, 1,
6, 7, 3,
0, 0, 0, 0, 0, 0, 0, 0, 30, 30, 30, 30, 30, 30, 30, 30, 5, 0,
15, 0, 0, 0, 0, 0, 0, 0, 30, 35, 40, 29, 50, 15, 15, 15, 5, 0,
30, 0, 0, 0, 0, 0, 0, 0, 27, 35, 40, 60, 50, 15, 20, 45, 5, 0,
15, 0, 0, 0, 0, 0, 0, 0, 45, 35, 40, 60, 25, 20, 20, 60, 5, 0,
0, 0, 0, 0, 0, 0, 0, 0, 50, 35, 75, 60, 20, 30, 20, 60, 6, 0,
-15, 0, 0, 0, 0, 0, 0, 0, 60, 60, 70, 70, 15, 15, 60, 60, 6, 0,
0, 0, 0, 0, 0, 0, 0, 0, 30, 30, 95, 95, 60, 60, 60, 60, 6, 1,
30, 0, 0, 0, 0, 0, 0, 0, 75, 70, 80, 80, -50, -50, 60, 60, 8, 0,
};
​
#if !defined(MAIN_SKETCH) || !defined(I2C_EEPROM)
const char* skillNameWithType[] =
{"crI", "puI", "restI", "zeroN",};
const char* progmemPointer[] =
{cr, pu, rest, zero, };
#else
const char* progmemPointer[] = {zero};
#endif

8.2.1. Defined constants

define WalkingDOF 8

defines the number of DoF (Degree of Freedom) for walking is 8 on Bittle.

define NUM_SKILLS 4

defines the total number of skills is 4. It should be the same as the number of items in the list const char* skillNameWithType[].

define I2C_EEPROM

Means there’s an I2C EEPROM on NyBoard to save Instincts.

If you are building your own circuit board that doesn’t have it, comment out this line. Then both kinds of skills will be saved to the flash as PROGMEM. Obviously, it will reduce the available flash space for functional codes. If there were too many skills, it may even exceed the size limit for uploading the sketch.

8.2.2. Data structure of skill array

One frame of joint angles defines a static posture, while a series of frames defines sequential postures, such as a gait or a behavior. Observe the following two examples:

const char rest[] PROGMEM = {
1, 0, 0, 1,
-30, -80, -45, 0, -3, -3, 3, 3, 75, 75, 75, 75, -55, -55, -55, -55,};
​
const char crF[] PROGMEM = {
36, 0, -3, 1,
61, 68, 54, 61, -26, -39, -13, -26,
66, 61, 58, 55, -26, -39, -13, -26,
...
51, 81, 45, 72, -25, -37, -12, -25,
55, 76, 49, 68, -26, -38, -13, -26,
60, 70, 53, 62, -26, -39, -13, -26,
};
​

They are formatted as:

rest is a static posture, it has only one frame of 16 joint angles. crF is the abbreviation for "crawl forward". It has 36 frames of 8 (or 12, depending on the number of walking DOF) joint angles that form a repetitive gait. Expected body orientation defines the body angle when the robot is conducting the skill. If the body is tilted from the expected angles, the balancing algorithm will calculate some adjustments. Angle ratio is used when you want to store angles larger than the range of -128 to 127. Change the ratio to 2 so that you can save those large angles by dividing 2.

A posture has only one frame, and a gait has more than one frames and will be looped over.

The following example is a behavior:

const char pu[] PROGMEM = {
-8, 0, -15, 1,
6, 7, 3,
0, 0, 0, 0, 0, 0, 0, 0, 30, 30, 30, 30, 30, 30, 30, 30, 5, 0, 0, 0,
15, 0, 0, 0, 0, 0, 0, 0, 30, 35, 40, 29, 50, 15, 15, 15, 5, 0, 0, 0,
30, 0, 0, 0, 0, 0, 0, 0, 27, 35, 40, 60, 50, 15, 20, 45, 5, 0, 0, 0,
15, 0, 0, 0, 0, 0, 0, 0, 45, 35, 40, 60, 25, 20, 20, 60, 5, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 50, 35, 75, 60, 20, 30, 20, 60, 6, 0, 0, 0,
-15, 0, 0, 0, 0, 0, 0, 0, 60, 60, 70, 70, 15, 15, 60, 60, 6, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 30, 30, 95, 95, 60, 60, 60, 60, 6, 1, 0, 0,
30, 0, 0, 0, 0, 0, 0, 0, 75, 70, 80, 80, -50, -50, 60, 60, 8, 0, 0, 0,
};

pu is short for "push up", and is a "behavior". Its data structure contains more information than posture and gait.

The first four elements are defined the same as before, except that the number of frames is saved as a negative value to indicate that it's a behavior. The next three elements define the repeating frames in the sequence: starting frame, ending frame, looping cycles. So the 6, 7, 3 in the example means the behavior should loop from the 7th to the 8th frame for 3 times (the index starts from 0). The whole behavior array will be executed only once, rather than looping over like the gait.

Each frame contains 16 joint angles, and the last 4 elements define the speed of the transition, and the delay condition after each transition:

  1. The default speed factor is 4, it can be changed to an integer from 1 (slow) to 127 (fast). The unit is degree per step. If it's set to 0, the servo will rotate to the target angle by its maximal speed (about 0.07sec/60 degrees). It's not recommended to use a value larger than 10 unless you understand the risks.

  2. The default delay is 0. It can be set from 0 to 127, the unit is 50 ms.

  3. The 3rd number is the trigger axis. If it's not 0, the previous delay time will be ignored. The trigger of the next frame will depend on the body angle on the corresponding axis. 1 for the pitch axis, and 2 for the roll axis. The sign of the number defines the direction of the threshold, i.e. if the current angle is smaller or larger than the trigger angle.

  4. The 4th number is the trigger angle. It can be -128 to 127 degrees.

8.2.3. Suffix for indicating Instinct and Newbility

You must upload WriteInstinct.ino to have the skills written to EEPROM for the first time. The following information will be used:

const char* skillNameWithType[] =
{"crI", "puI", "restI", "zeroN",};
const char* progmemPointer[] =
{cr, pu, rest, zero, };

Notice the suffix I or N in the skill name strings. They tell the program where to store skill data and when to assign their addresses.

Later, if the uploaded sketch is the main sketch OpenCat.ino, and if you are using NyBoard that has an I2C EEPROM, the program will only need the pointer to the Newbility list

const char* progmemPointer[] = {zero};

to extract the full knowledge of pre-defined skills.

8.3. Define new skills and behaviors

8.3.1 Modify the existing skill template

There’s already a skill called “zeroN” in InstinctBittle.h. It’s a posture at the zero state waiting for your new definition.

You can first use the command mIndex Offset to move an individual joint to your target position, then replace the joint angles (bold fonts) in array at once:

const char zero[] PROGMEM = {
1, 0, 0, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,};

Because it’s declared as a Newbility and doesn’t require writing to I2C EEPROM, you can simply upload OpenCat.ino everytime you change the array (without uploading WriteInstinct.ino). You can trigger the new posture by pressing 7> on the IR remote, or type kzero in the serial monitor.

You can rename this skill, but remember to update the keymap of the IR remote.

8.3.2. Add more skills in InstinctBittle.h

You can add more skills in InstinctBittle.h. Remember to increase the skill number at the beginning of the file and add the corresponding skill name and pointer in the skill list array.

A skill can be called from the serial monitor with the token 'k' command. For example, ksit will move Bittle to posture "sit".

You can also tune new skills by sending posture frames through the serial port, using the m, i, l tokens without uploading a new sketch. After fine-tuning the skill, you can save it in the instinctBittle.h and upload it to the board as a newbility or instinct.

Always check the actual code for the available skill names. We may alter the skill set as we iterate the software.

If you are going to do some inverse kinematics calculation, you may use the following key dimensions to build your model:

8.3.3. Automation

So far Bittle is controlled by the infrared remote. You make decisions for Bittle's behavior.

You can connect Bittle with your computer or smartphone, and let them send out instructions automatically. Bittle will try its best to follow those instructions.

By adding some sensors (like a touch sensor), or some communication modules (like a voice control module), you can bring new perception and decision abilities to Bittle. You can accumulate those automatic behaviors and eventually make Bittle live by itself!