# 技能创作

{% hint style="info" %}
最简单的创作新技能并生成技能数组的方法是使用[**技能创作坊**](https://docs.petoi.com/v/chinese/zhuo-mian-ying-yong/ji-neng-chuang-zuo-fang)。 关闭电源并重新启动后新技能仍可以保留，但是，通过技能编辑器导出的技能只是暂时地存储在EEPROM中，会被后来导出的新技能覆盖，所以您无法在一个程序中方便地调用两个新技能。 本章重点说明 OpenCat 技能相关的代码和数据结构，以便您可以将任意数量的新技能集成到源代码中。
{% endhint %}

## 准备工作

熟悉上传程序固件（参考[Nybble](https://nybble.petoi.com/v/chinese-1/3-pei-zhi-NyBoard)/[Bittle](https://bittle.petoi.com/v/zhong-wen/3-pei-zhi-NyBoard)用户手册第3章），组装流程（参考[Nybble](https://nybble.petoi.com/v/chinese-1/4-zu-zhuang-gu-jia)/[Bittle](https://bittle.petoi.com/v/zhong-wen/4-zu-zhuang)用户手册第4章），校准舵机（参考[Nybble](https://nybble.petoi.com/v/chinese-1/6-xiao-zhun)/[Bittle](https://bittle.petoi.com/v/zhong-wen/6-jiao-zhun)用户手册第6章）。 使用红外遥控器验证以下函数是否按预期工作。

* 按下[红外遥控器](https://docs.petoi.com/v/chinese/hong-wai-yao-kong/yao-kong-qi)第 2 行第 2 列的按钮（如下图所示）。 稍后我们将使用 (2, 2) 作为索引。 您也可以连接USB适配器，通过[串口监视器](https://docs.petoi.com/v/chinese/arduino-ide/chuan-kou-jian-shi-qi)输入串口命令 `kbalance`。 机器人站立起来。
* 按下[红外遥控器](https://docs.petoi.com/v/chinese/hong-wai-yao-kong/yao-kong-qi)第 7 行第 3 列的按钮（如下图所示）。 稍后我们将使用 (7, 3) 作为索引。 您也可以连接USB适配器，通过[串口监视器](https://docs.petoi.com/v/chinese/arduino-ide/chuan-kou-jian-shi-qi)输入串口命令 `kzero`。 机器人所有舵机都转动到0度位置，这是程序中的“零”技能（如下图所示）。

<div align="center"><figure><img src="/files/j8TT61Kr6idpyJAk6CTX" alt=""><figcaption></figcaption></figure> <figure><img src="/files/PGG2ae3V7KcdY0Pzepal" alt=""><figcaption></figcaption></figure></div>

## 理解程序代码   &#x20;

机器人的所有技能数组定义在`Instinct***.h`文件中。

* **Nybble**: `InstinctNybble.h`
* **Bittle**: `InstinctBittle.h`

以下是一个`Instinct***.h`文件的缩略示例：

```cpp
//a short version of Instinct***.h as example
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,
};

const char* skillNameWithType[] = {"crFI", "puI", "restI", "zeroN",};
#if !defined(MAIN_SKETCH) || !defined(I2C_EEPROM)
const char* progmemPointer[] = {crF, pu, rest, zero, };
#else
const char* progmemPointer[] = {zero};
#endif
```

### 数据结构

技能数组的数据结构含义如下图所示：

<figure><img src="/files/N0DHeLlFaIIFkvQKsCq4" alt=""><figcaption><p>数据结构示意图</p></figcaption></figure>

#### 总帧数（Total **# of Frames** ）

技能数组中第1个元素表示技能所包含所有动作帧的总数量，数值前如果有负号（**-**）表示此技能是一种[行为](#hang-wei-ji-neng-shu-zu)。

上述数据结构示意图中，休息（**rest**） 是一种[姿势](#zi-shi-ji-neng-shu-zu)。它只有1个动作帧（1行数据）。

向前爬行（**crF**） 是一种[步态](#bu-tai-ji-neng-shu-zu)。 它包含 36 个动作帧（36行数据）。

#### 身体方向期望值（Expected Body Orientation）

技能数组中第2个元素和第3个元素表示身体方向期望值，即机器人在执行技能动作时身体保持的倾斜角度，分别对应身体的横滚角（**Roll**）和俯仰角（**Pitch**）。当机器人在执行技能动作时，如果身体倾斜度偏离了期望值，平衡算法将对相关腿部舵机的角度值做一些调整，以使身体倾斜度尽量保持在期望值附近。

<figure><img src="/files/S5ePACI7TMAfm3m4JJTd" alt=""><figcaption></figcaption></figure>

上图所示机器人的俯仰角（Pitch），横滚角（Roll）都处于0度位置。左图中，机器人身体从0度位置绕中心点逆时针旋转，俯仰角为正值；顺时针旋转，俯仰角为负值。右图中，机器人身体从0度位置绕中心点逆时针旋转，横滚角为正值；顺时针旋转，横滚角为负值。

请看以下示例代码，站立：

```cpp
const char balance[] PROGMEM = { 
1, 0, 0, 1,
    0,   0,   0,   0,   0,   0,   0,   0,  30,  30,  30,  30,  30,  30,  30,  30,};
```

坐：

```cpp
const char sit[] PROGMEM = { 
1, 0, -30, 1,
    0,   0, -45,   0,  -5,  -5,  20,  20,  45,  45, 105, 105,  45,  45, -45, -45,};
```

![](/files/IYdpMGnnFQBNg0N7xdbm)

数组中第 2 个元素和第 3 个元素表示身体方向期望值（对应身体的横滚角和俯仰角），单位是度。 &#x20;

激活陀螺仪后，轻微旋转 Bittle身体，当机身发生倾斜偏离了身体方向期望值时，平衡算法会以使机身尽量保持该姿势。

#### 舵机序号及其角度值（Indexed Joint Angles）

舵机序号如下图所示：

<div><figure><img src="/files/sd5pTYY3LOvu3nRGzEwR" alt=""><figcaption><p>Nybble</p></figcaption></figure> <figure><img src="/files/8IXY8tljLN2eaJ2xyqUX" alt=""><figcaption><p>Bittle</p></figcaption></figure></div>

关节舵机旋转的角度范围请控制在\[-125\~125]之间，对于腿部舵机，在机器人的左侧观测，腿部从0度位置绕关节中心点（螺钉固定位置）逆时针旋转，角度为正值；顺时针旋转，角度为负值；在机器人的右侧观测，腿部旋转角度与左侧成镜像对称（从0度位置绕关节中心点顺时针旋转，角度为正值；逆时针旋转，角度为负值）。对于机器人颈部舵机，从机器人头顶向下俯视，颈部从0度位置绕关节中心点（螺钉固定位置）逆时针旋转，角度为正值；顺时针旋转，角度为负值。

{% hint style="info" %}
对于Nybble头部舵机（1号舵机）在机器人的右侧观测，头部从0度位置绕关节中心点（螺钉固定位置）逆时针旋转，角度为正值；顺时针旋转，角度为负值。

对于Nybble尾部舵机（2号舵机）面对尾部向下俯视，尾部从0度位置绕中心点（螺钉固定位置）逆时针旋转，角度为正值；顺时针旋转，角度为负值。
{% endhint %}

技能数组中的每个动作帧都包含多个舵机的旋转角度值。

上述数据结构示意图中，休息（**rest）**&#x52A8;作帧中其中包含有16 个关节舵机角度值，对应舵机序号从0开始从小到大依次排列。

向前爬行（**crF** ）中每个动作帧包含 8 个关节舵机角度值，对应舵机序号从8开始从小到大依次排列。

#### 角度比率（**Angle ratio**）

技能数组中第4个元素表示角度比率。当需要存储超出 -128 到 127 范围内的角度值时，可以增大角度比率值。比如将角度比率设置为 2，技能数组中所有关节舵机的角度值乘以2才是舵机的实际旋转角度。

请看以下示例技能数组 **rc**（四脚朝天后恢复站立）：

```cpp
const char rc[] PROGMEM = { 
-3, 0, 0, 2,
 0, 0, 0, 
    0,   0,   0,   0,   0,   0,   0,   0, -88, -43,  67,  87,  42, -35,  42,  42,	15, 0, 0, 0,
    0,   0,   0,   0,   0,   0,   0,   0, -83, -88,  87,  42,  42,  42,  42, -40,	15, 0, 0, 0,
   -8, -20, -11,   0,  -1,  -1,   0,   0,  18,  18,  18,  18, -14, -14, -14, -14,	10, 0, 0, 0,
};
```

数组中第 4 个元素（**2**）表示角度比率。 这意味着所有关节转动的实际角度值都等于数组中每一个关节角度值（从第**8**个元素开始）乘以此角度比率。

### 姿势技能数组

姿势技能数组只包含一个动作帧。&#x4EE5;***Bittle***&#x4E3A;例，在`InstinctBittle.h`中找到 **zero**数组：

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

修改其中一些关节舵机角度值：

```cpp
const char zero[] PROGMEM = { 
1, 0, 0, 1,
70, 0, 0, 0, 0, 0, 0, 0, -60, 0, 0, 0, 60, 0, 0, 0,};
```

![修改前后数值对比](/files/xQkMScY4XfByUkiunpKB)

保存修改，将主功能程序OpenCat.ino上传到Bittle主板上。上传完成后，点击红外遥控器上的按钮 (7, 3)  即可看到修改过的零技能，新的姿势如下图所示：

![](/files/6ni4POqJkkVSNxA0gosI)

数组中第一个元素（1）表示该技能的总帧数，1表示是姿势技能。

数组中第 4 个元素 (1) 表示角度比。 这意味着以下所有索引关节角度都是实际角度（因为它们中的每一个都乘以 1）。

数组中从第 5 到第 20 个元素分别代表 16 个关节在当前帧中各自对应的角度值。

对于 Bittle，数组中第 5 个元素（70，对应舵机序号0）表示 Bittle 颈部的舵机逆时针旋转 70度。 Bittle 的头转向机身左侧。

数组中第 13 个元素（-60，对应舵机序号8）表示 Bittle 的左前大腿围绕关节中心点顺时针旋转 60度。

数组中第 17 个元素（60，对应舵机序号12）表示 Bittle 的左前小腿绕关节中心点逆时针旋转 60度。

其他关节角度保持 0度不变。

### 步态技能数组

步态技能数组包含多个连贯的动作帧，而且这些动作帧按照顺序循环不断重复执行，除非机器人收到新的技能命令，才会停止执行。例如后退（**bk**）步态技能数组定义如下所示：

```cpp
const char bk[] PROGMEM = {
  35, 0, 0, 1,
  46,  54,  46,  54,  -5, -23,  -5, -23,
  43,  58,  43,  58,  -5, -24,  -5, -24,
  ......
  52,  43,  52,  43,  -5, -21,  -5, -21,
  50,  48,  50,  48,  -5, -22,  -5, -22,
  47,  53,  47,  53,  -5, -23,  -5, -23,
};
```

数组中第一个元素 (35) 表示此技能有 35 个动作帧。 从第二行数据开始，每行是一个动作帧，其中包含了8 个关节舵机的角度值，对应舵机序号从8开始从小到大依次排列（共 35 行）。

{% hint style="info" %}
对于步态技能，未来每个动作帧可能包含 12 个关节舵机角度值，对应舵机序号将从4开始从小到大依次排列，这取决于参与运动的腿部舵机数量。
{% endhint %}

### 行为技能数组

行为技能数组同样包含了多个连贯的动作帧，而且所有动作帧按照顺序只执行一轮，但其中一部分连续动作帧可以循环执行多次。例如俯卧撑（**pu**）行为技能数组定义如下所示：

```c
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,
};
```

此数据结构包含了比姿势和步态更多的信息：

第1行的4个元素含义如前所述，其中第一个元素（总帧数）为负数，表明此技能是一个**行为**。&#x20;

第2行的3个元素表示这个行为中包含的循环结构：开始帧，结束帧，循环次数。

示例中的 **6,  7,  3** 表示这个行为从第 7 帧到第 8 帧循环执行 3 次（帧序号从 0 开始）。 整个行为的动作序列只执行一轮，而不会像步态那样不断循环执行。

每个动作帧包含20个元素。前 16 个元素如前所述表示关节舵机的角度，对应舵机序号从0开始从小到大依次排列。后 4 个元素含义分别如下：

1. 第一个表示速度因子。默认的速度因子是 4，它可以更改为从 1（慢）到 127（快）的整数。 单位是每步的度数。 如果设置为 0，舵机将以最大速度（约 0.07 秒/60 度）旋转到目标角度。 除非您了解风险，否则不建议使用大于 10 的值。
2. 第二个表示延迟时间。默认延迟为 0。可设置范围为 0 到 127，单位为 50 毫秒（如设置为2，则实际延迟 100 毫秒）。
3. 第三个表示触发轴。用来设置机器人触发下一帧动作时的身体旋转方向，有以下5个设置选项
   * 0 表示无触发轴，解发角条件设置
   * 1 表示正俯仰，机器人身体向前俯下方向旋转
   * -1 表示负俯仰，机器人身体向后仰起方向旋转
   * 2 表示正横滚，机器人身体向自身左侧翻滚
   * -2 表示负横滚，机器人身体向自身右侧翻滚
4. 第四个表示是触发角度。角度值正负含义与[身体方向期望值](#shen-ti-fang-xiang-qi-wang-zhi-expected-body-orientation)相同。 触发角设置范围：-125 \~ 125 之间的整数值。

{% hint style="info" %}
注意：

* 触发轴与触发角度需搭配使用：只有当机器人在完成此帧动作后，并按照设置的身体旋转方向旋转越过触发角度时，才会触发下一帧动作。如果触发轴设置为0时，触发角度大小则无意义。所以触发轴设置为0时，触发角度一般也随之设置为0。
* 如果动作帧中还设置了延迟时间，那么机器人在运行此动作帧时，不但要满足触发轴，触发角设置的触发条件，还要达到延时设置时长，才会触发执行下一帧动作。
  {% endhint %}

### 技能存储类型

EEPROM 的擦写次数有限（1,000,000次），为了尽量减少写入操作，定义两种技能：**本能**（**Instinct）**&#x548C; **新技**（**Newbility**）。它们的索引地址都作为查找表保存在芯片（ATmega328P）内置的EEPROM（1KB）里，但主体的数据却存在不同的存储单元：

* I2C EEPROM (8KB) 存储**本能**（Instinct）\
  **本能**是指固定技能（或偶尔需要微调），可以把它们理解成“肌肉记忆”。
* Flash (与Arduino程序代码分享32KB存储空间) 存储**新技**（Newbility）\
  **新技**是指用户自定义技能（可能修改增删）。它们并不会被写入静态的EERPOM，而是与Arduino程序代码一起上传至闪存（Flash）中。它们的索引地址由代码执行时实时分配，只要技能总数（包括所有本能和新技能）不变，该值很少改变。

具体示例代码如下：

```cpp
const char* skillNameWithType[] = {"crFI", "puI", "restI", "zeroN",};
#if !defined(MAIN_SKETCH) || !defined(I2C_EEPROM)
const char* progmemPointer[] = {crF, pu, rest, zero, };
#else
const char* progmemPointer[] = {zero};
#endif
```

在字符指针数组**skillNameWithType**中给每个技能数组名称加了一个后缀，“**N**”表示是**新技**，“**I**”表示是**本能**。

`const char* progmemPointer[] = {crF, pu, rest, zero, };`这部分代码在上传主板配置程序时处于激活动状态。 它包含所有技能的数据和指针。

`const char* progmemPointer[] = {zero};`这部分代码在上传主要功能程序时处于激活状态。 由于**本能**已经保存在外部I2C EEPROM 中，因此此处省略了它们的数据以节省空间。 如果只是对已有新技能（比如`zero`）做动作调整，就无需重新上传主板配置程序了。

## 创作新技能

### 调试技能动作

在创作或调试技能动作时可以采用以下两种方法：

* 通过运行Python脚本控制机器人做动作，详情请参考[serialMaster使用指南](https://docs.petoi.com/v/chinese/api/serialmaster-shi-yong-zhi-nan)。
* 使用桌面应用程序中的[技能创作坊](https://docs.petoi.com/v/chinese/zhuo-mian-ying-yong/ji-neng-chuang-zuo-fang)控制机器人做动作，然后利用“[导出动作](https://docs.petoi.com/v/chinese/zhuo-mian-ying-yong/ji-neng-chuang-zuo-fang#dao-chu-dong-zuo)”功能，可以把调试好技能数组内容拷贝，粘贴到在`Instinct***.h`文件使用。具体格式请参考[数据结构](https://app.gitbook.com/o/-M-_eWZUjFA4usjshHcZ/s/-MQ6a951Q6Jn1Zzt5Ajr-3369173170/~/changes/9JBP0HuTS05kf4gScIb1/ying-yong-shi-li/ji-neng-chuang-zuo#shu-ju-jie-gou)。

{% hint style="info" %}
如果你想创作一个定制的步态，这个 [git 代码仓](https://github.com/ger01d/kinematic-model-opencat) 是一个很好的起点。 如果想进行一些反向运动学计算，可以使用以下关键尺寸来构建Bittle模型:

&#x20;

<img src="/files/mSIOBUl9LZo8Ik61F4H4" alt="" data-size="original">
{% endhint %}

{% hint style="info" %}
Github中的[SkillLibrary文件夹](https://github.com/PetoiCamp/OpenCat/tree/main/SkillLibrary)是OpenCat 机器人可以执行的新技能的集合，可以供您参考，使用。也欢迎您通过向此文件夹发送合并请求来分享您的新技能。
{% endhint %}

{% hint style="info" %}
另一个方向是为Bittle建立模型及模拟环境，然后在现实中测试模型。您可以使用Bittle的[统一机器人描述格式（URDF）文件](https://github.com/AIWintermuteAI/Bittle_URDF)并设置NVIDIA Omniverse进行模拟训练。

{% embed url="<https://youtu.be/phTnbmXM06g>" %}
{% endhint %}

### 新增技能数组

#### 新增本能

新增以 **I** 结尾的新技（**testI**）数组，需要在示例代码宏判断第1个分支中加入技能数组变量名（test），具体代码修改如下所示：

```cpp
const char* skillNameWithType[] = {"crFI", "puI", "restI", "zeroN", "testI"};
#if !defined(MAIN_SKETCH) || !defined(I2C_EEPROM)
const char* progmemPointer[] = {crF, pu, rest, zero, test};
#else
const char* progmemPointer[] = {zero};
#endif
```

#### 新增新技

新增以 **N** 结尾的新技（**testN**）数组，需要在示例代码宏判断两个分支中都加入技能数组变量名（**test**），具体代码修改如下所示：

```cpp
const char* skillNameWithType[] = {"crFI", "puI", "restI", "zeroN", "testN"};
#if !defined(MAIN_SKETCH) || !defined(I2C_EEPROM)
const char* progmemPointer[] = {crF, pu, rest, zero, test};
#else
const char* progmemPointer[] = {zero, test};
#endif
```

## 激活新技能

完成程序代码修改后，请将程序上传到主板上进行验证：

* NyBoard: <https://docs.petoi.com/v/chinese/arduino-ide/wei-nyboard-shang-chuan-cheng-xu>
* BiBoard: <https://docs.petoi.com/v/chinese/arduino-ide/wei-biboard-shang-chuan-cheng-xu>

使用Arduino IDE上传主板配置程序后，打开串口监视器，当串口提示以下内容时：

```cpp
Reset joint offsets? (Y/n):
```

输入‘**Y**’ 或者 ‘**n**’ 后，所有本能数组将一次性存储在I2C EEPROM里，同时所有技能的索引地址也会被生成并存储在芯片内置的EERPOM里，之后再上传主要功能程序。具体操作流程请参考**为NyBoard上传程序**中[相关章节](https://docs.petoi.com/v/chinese/arduino-ide/wei-nyboard-shang-chuan-cheng-xu#shang-chuan-cheng-xu-gu-jian)。

验证技能动作时，可以打开[串口监视器](https://docs.petoi.com/v/chinese/arduino-ide/chuan-kou-jian-shi-qi)，通过串口指令 ‘**k**’ 口令召唤。比如，`ksit`可以Bittle变换到坐的姿势。

## 智能化

通过添加一些[扩展模块](https://docs.petoi.com/v/chinese/kuo-zhan-mo-kuai/kuo-zhan-mo-kuai-gai-yao-shuo-ming)（比如手势传感器），可以帮助机器人更好地感知环境甚至做出决策。 通过积累这些自动的行为，并设计决策树最终实现机器人的全自动智能运行！


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.petoi.com/chinese/ying-yong-shi-li/ji-neng-chuang-zuo.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
