从特定角度看,SDK 上手仍然有一定难度,比如只是想实现一个极为简单的点亮 LED 功能, 需要安装开发环境、工具链,需要懂 C 语言,还需要阅读教程、查阅外设源代码。 与之对比,NodeMCUMicroPythonEspruino 等系统使用了比 C 语言友好的 Lua、Python、JavaScript,不需要安装昂贵的开发工具, 对于点亮 LED 这种简单功能实现了开箱即用。

现在,我们为 ING9188 开发板推出了 Playground 系统, 即使没有任何开发经验,不懂英语,也能体验桃芯科技。请先参照 文档 准备好环境, 然后一起在这个游乐园里游历半日。

灯,灯,灯

点亮一盏灯可称得上是嵌入式开发里的“Hello World”。Playground 里的开灯程序如下:

开灯

点击更新按钮,稍等片刻,开发板上整排 LED 全部点亮。这个不起眼的程序还有另外一种写法:

$(开灯)

为了最大限度地降低上手难度,Playground 支持用自然语言编程。把能想到的、开发板上可能实现的动作直接写到 $(...) 里试试看。比如,开灯,等待 1000 毫秒,然后关灯:

$(开灯);
$(等待 1000 毫秒);
$(关灯);

假如想让灯亮 1 秒,灭 1 秒,循环往复,显然不能不停地重复$(开灯)$(关灯)$(开灯)$(关灯)…… 重复性的工作应该由芯片里的处理器替我们完成:

重复(
    $(开灯);
    $(等待 1000 毫秒);
    $(关灯);
    $(再等 1000 毫秒);
)

单个灯也可以用自然语言控制。比如我们要让 0 号灯和 1 号灯交替亮灭,思路如下:

  1. 开 0 号灯,关 1 号灯
  2. 延后 500 毫秒
  3. 把 1 号灯打开,关掉 0 号灯
  4. 再等会儿
  5. 重复执行以上步骤

于是代码就有了:

重复(
    $(开 0 号灯);
    $(关 1 号灯);
    $(延后 500 毫秒);
    $(把 1 号灯打开);
    $(关掉 0 号灯);
    $(再等会儿);
)

蜂鸣器

(粗略地,)蜂鸣器可以发出某个指定频率的声音,同样可以用自然语言控制它,比如:

$(发出 400 Hz 的声音);
$( 500 毫秒);
$(停止发声);

这段代码的功能显然就是发出半秒的单调声音。如果不嫌弃它的单调音质,那么可以让它演奏“最简单”的简谱:

$(演奏 1155665-);

彩灯

开发板上的彩灯可以以 RGB (红、绿、蓝三原色)、 HSL (度、和度、度)两种模式控制, RGB 的每个分量范围为 0 ~ 255,HSL 的每个分量范围为 0 ~ 1.0。

设置红绿蓝(100, 0, 0);
$(等候 1000 毫秒);
设置色饱亮(0.7, 0.1, 0.1);

“炫彩”电压

芯片内的模拟数字转换器(ADC)可以测量电压。调节开发板上的可调电阻可以产生一个大致在 0V 到 2.5V 之间变化的电压供 ADC 测量。 开发板将芯片上的两个 ADC 连接到了这个电压信号上,开发板上印制了这两个 ADC 的序号,一个是 1,另一个是 4。 何不用色度来表示电压?先读取电压,除以 2.6,得到一个小于 1.0 的数值,设置为彩灯的色度。重复执行这个动作, 就能通过彩灯实时地看到电压的变化了。至于饱和度和亮度,可以随意选择数值,观察效果。代码如下:

重复(设置色饱亮(读取电压(1) / 2.6, 0.8, 0.5);)

将程序下载到开发板后,用小螺丝刀慢慢转动可调电阻,可以观察到颜色的变化。

响应按键

现在我们想用某个按键(比如 1 号按键)控制灯的亮灭,按下时开灯弹起时关灯,下面的代码显然是在实现这个功能:

按键[1].按下时([]() { 开灯; });
按键[1].弹起时([]() { 关灯; });

还可以合并起来考虑,当这个按键的状态改变时如果按下了开灯否则关灯。 下面的代码与我们的思路一般无二:

按键[1].的状态改变时([](变量 按下了) {
    如果 (按下了)
        开灯;
    否则
        关灯;
});

再设计一个程序用按键调节彩灯亮度,当按键按下时,增加一点亮度,如果亮度超过 1.0,就把亮度回 0。 为了实现这个功能,需要有一个东西(静态 变量)来保存亮度,亮度会变化,所以是变量; 又由于某些原因(不妨理解为为了防止我们的第一个变量被动态丢弃),需要说它是静态的。 下面的代码又是跟我们的设想一一对应:

静态 变量 亮度 = 0.0;
按键[1].按下时([&]() {
    亮度 = 亮度 + 0.01;
    如果 (亮度 > 1.0) 亮度 = 0.0;
    设置色饱亮(0.2, 0.8, 亮度);
});

气象站

开发板上的环境传感器可以感知温度、湿度和气压。Playground 当然可以向串口 输出 信息。两者结合, 就可以开发一个气象站小程序:

重复(
    输出(现在的温度);
    输出(现在的湿度);
    输出(现在的气压);
    $(休息 1 );
)

用串口工具连接到开发板串口,就可以看到气象数据。串口参数:波特率 115200,8 比特数据位,1 比特停止位,无校验。

注意:用串口工具连接到开发板串口之后,即使正常关闭了串口工具,更新开发板时仍可能出现错误。 重新插拔开发板可解决。

定时执行

从另一个角度考虑气象站小程序,我们是想每秒输出一次信息,是要定时执行一个动作。比如每 1000 毫秒闪一下 0 号灯。

定时执行([]() {
    [0].闪一下;
}, 1000);

流水灯

现在设计一个流水灯:让 8 个灯按顺序亮起,仿佛一个亮点在开发板上流动。每个时刻(比如 50 毫秒)只有一个要亮的灯。 到下一个时刻:如果要亮的灯小于 7,就把要亮的灯加 1 切换到下一个,否则说明已经是最后一个灯了,于是回到第 0 号灯。 根据要亮的灯控制各灯:序号正好是要亮的灯的那个要点亮,其它的熄灭。重新排版这段话,就得到如下代码:

静态 变量 要亮的灯 = 0;
定时执行([&]() {
    如果 (要亮的灯 < 7)
        要亮的灯 = 要亮的灯 + 1;
    否则
        要亮的灯 = 0;
    控制各灯([=](变量 灯序号) {
        如果 (灯序号 等于 要亮的灯)
            返回 ::;
        否则
            返回 ::;
    });
}, 50);

多任务/多线程

Playground 里可以创建新线程

创建新线程([]() {
    一直(
        设置色饱亮(读取电压(1) / 2.6, 0.8, 0.5);
        $(暂停 30 毫秒);
    )
});
创建新线程([]() {
    一直(
        [0].闪一下;
        $(暂停 30 毫秒);
    )
});

霹雳游侠流水灯

上面的流水灯走到末尾时就跳回 0 号灯重新开始,是单向流动。现在加大难度:当流动到末尾时,掉转船头,反向流动。 于是,除了要亮的灯,还需要记录流动方向。下面的代码在一个新的线程里实现了这种“霹雳游侠流水灯”:

创建新线程([]() {
    变量 要亮的灯 = 0;
    变量 流动方向 = 1;
    重复(
        要亮的灯 += 流动方向;
        如果 ((要亮的灯 < 0) 或者 (要亮的灯 > 7))
        {
            流动方向 *= -1;
            要亮的灯 += 流动方向;
        }

        控制各灯([=](变量 灯序号) { 返回 灯序号 等于 要亮的灯; });
        $(等待 50 毫秒);
    )
});

请注意,这里控制各灯的写法有所简化。

高度指示灯

现在来开发一个具有一定实际功能的例子,高度指示灯:用亮灯数目表示开发板被举起的高度。 通常情况下(20 摄氏度、1 个标准大气压),空气密度取$1.205kg/m^3$,也就是说,在较小范围内, 每升高 10cm,气压下降大约 1 帕斯卡。几十厘米的升降很容易实现,所以可以每下降 1 帕斯卡就多点亮 1 个灯。 程序的思路如下:

  1. 开机后,记录现在的气压,做为基准气压
  2. 重复一下步骤:
    1. 计算基准气压现在的气压之间的气压差;
    2. 点亮从 0 号开始的一串灯,这串灯的灯序号小于等于气压差

转换为程序:

$(稍等,传感器现在还不稳定);
变量 基准气压 = 现在的气压;
重复(
    变量 气压差 = 基准气压 - 现在的气压;
    控制各灯([=](变量 灯序号) { 返回 灯序号 小于等于 气压差; });
)

更新到开发板之后,缓缓举起开发板,看看预想的功能是否得以实现。

保持稳定

开关板上带有一个 3 轴加速度传感器,可以感知如图所示 X/Y/Z 三个轴向上的加速度,其中 Z 轴为垂直向下。将开发板平放, Z 轴上的加速度分量大小等于重力加速度。在机体坐标系里,绕 X、Y、Z 三个轴的旋转角度分别称为横滚角(roll)、俯仰角(pitch)、偏航角(yaw)。 保持开发板静止,根据重力加速度在三个轴上的分量可以推算出横滚角和俯仰角。

现在使用俯仰角开发一个体感小游戏,目标:保持开发板水平静止。实现思路如下:

  1. 现在的俯仰角转换为亮灯个数
  2. 如果亮灯个数为 0,说明为水平状态,彩灯亮红色鼓励;否则熄灭彩灯;
  3. 如果角度为正数,说明机头上仰,从第 3 个开始向左逐个点亮 LED 灯;
  4. 如果角度为负数,说明机头下沉,从第 4 个开始向右逐个点亮 LED 灯;
  5. 每 50 毫秒重复执行上述步骤。

使用灵敏度参数控制俯仰角向亮灯个数的转换:灵敏度越低,对角度越不敏感,游戏难度越低;反之越难。完整代码如下:

定时执行([]() {
    变量 灵敏度 = 0.5;
    变量 亮灯个数 = (整数)(现在的俯仰角 * 灵敏度);
    如果 (亮灯个数 == 0) {
        设置红绿蓝(50, 0, 0);
        关灯;
    } 否则 {
        设置红绿蓝(0, 0, 0);
        控制各灯([=](变量 序号) {
            如果 (亮灯个数 > 0)
                返回 (4 - 亮灯个数 <= 序号) 并且 (序号 <= 3);
            否则
                返回 (4 <= 序号) 并且 (序号 <= 3 - 亮灯个数);
        });
    }
}, 50);

灵敏度取 0.5,表示大概每偏 2 度多亮 1 个灯,难度适中。

蓝牙

当然,一款蓝牙芯片肯定可以设置蓝牙名称

设置蓝牙名称("我的蓝牙");

更新开发板之后,使用蓝牙工具(ING BLE、LightBlue、nRF Connect 等)可以看到名为“我的蓝牙”的设备。

蓝牙服务

蓝牙 Profile 完整描述了蓝牙设备“是什么东西”。一个 Profile 包含若干个服务,一个服务包含若干个特征。 无论服务还是特征,都有一个编号。蓝牙组织定义了一些通用的编号,根据编号按图索骥就知道了设备的功能, 比如 AAAA 表示体温计,BBBB 表示人机接口设备(键盘、鼠标等)。也可以自定义编号, 当然,这些编号所对应的功能“外人”是不知道的。

下面我们自定义一个编号为“111…”的服务,里面包含一个编号为“222…”特征。 每当这个特征被写入数据时,就去开关灯。

设置蓝牙名称("我的蓝牙");
定义服务("11111111");
定义服务特征("22222222", [](变量, 变量) {
    $(开关灯);
});

更新开发板之后,用蓝牙工具(ING BLE、LightBlue、nRF Connect 等)连接到“我的蓝牙”这个设备,向 “222…”这个特征多次写入任意数值,可观察到灯在亮灭交替。

可以响应写入的数值,比如下面的例子:

设置蓝牙名称("我的蓝牙");
定义服务("11111111");
定义服务特征("22222222", [](变量 指令, 变量 长度) {
    执行(指令, 长度);
});

这时我们只能使用 nRF Connect 工具(感谢友商),向“222..”写入中文句子,看看会发生什么。

cmd

一个完整的自定义编号长度为 16 字节,从图中看出后面的数字不是我们指定的,这是有意为之:只要前面的几个数字容易辨认、区分即可, 其它数字无所谓。

下面的例子演示了如何从外部读取特征的数值。我们把数据的长度定为 2,第 1 个数值为摄氏温度的整数部分,第 2 个数值为随机数。

设置蓝牙名称("我的蓝牙");
    定义服务("11111111");
    定义服务特征("33333333",
        []() { 返回 2; },
        [](变量 数据, 变量 长度) {
            数据[0] = (整数)现在的温度;
            数据[1] = 随机数;
        }
    );

使用蓝牙工具连接到“我的蓝牙”这个设备,多次读取“333…”这个特征,可观察到这两个值:

图中的温度值“15”为 16 进制,等于十进制的 21。

使用英语

上面的演示代码都是用中文编写,可能略感“违和”。Playground 里同样可以使用英语编程,demo/en.md 为以上演示代码的英语版本。