PLEN:bit のベース部分が、micro:bit とどういう通信をしているか気になりました。どのポートでどの仕組みで何を通信しているのか。このへん、記載されている資料が見つけられなかったので、拡張機能のソースから追ってみようと思います。
確認時のバージョンは Ver.0.0.3 です。その後、バージョンアップされていますので、以下とは内容が変わってるかもしれないです。
micro:bit への PLEN:bit 機能追加は、以下のプログラムです。
plenprojectcompany/pxt-PLENbit
https://github.com/plenprojectcompany/pxt-PLENbit
プログラム本体は pxt-PLENbit/plenbit.ts ですね。TypeScriptで記述してあります。
このあたりのドキュメントも参考にしつつ、見ていきます。
Documentation
https://makecode.microbit.org/docs
最初は、この表示の定義です。
//% weight=100 color=#00A654 icon="\uf085" block="PLEN:bit"
//%の行は、MakeCodeで解釈されるっぽいですね。
namespace plenbit {
namespace は plenbit
最初に、export enum で、モーションなどの定数を定義してます。
export enum StdMotions {
//% block="Walk Forward"
WalkForward = 0x46,
//% block="Walk Left Turn"
WalkLTurn = 0x47,
ブロックの表示が “Walk Forward”、対応する値が 0x46 ということですね。
日本語表示時の表示内容は、こちらのファイルで定義されています。
https://github.com/plenprojectcompany/pxt-PLENbit/tree/master/_locales/ja
"plenbit.StdMotions.WalkForward|block":"前に進む",
プログラムの中で、一番重要となるのは、micro:bit と外部ハードウェアの通信部分。 write8 関数に描かれています。
function write8(addr: number, d: number) {
let cmd = pins.createBuffer(2);
cmd[0] = addr;
cmd[1] = d;
pins.i2cWriteBuffer(0x6A, cmd, false);
}
名称から、通信にはI2Cを使ってるみたいです。pins …とは?
pinsで入出力関連の命令が使えるらしい。
i2c Write Buffer
function i2cWriteBuffer(address: int32, buf: Buffer, repeat?: boolean): int32;
https://makecode.microbit.org/reference/pins/i2c-write-buffer
Create Bufferで、出力用のバッファを作成して、それを i2c Write Buffer で指定したアドレスに出力。
パラメータは
address: the 7-bit I2C address to read the data from.
buffer: a buffer that contains the data to write to the device at the I2C address.
repeated: if true, don’t send a stop condition after the write. Otherwise, a stop condition is sent when false (the default).
アドレスと、送るバッファと、連続して送るかどうか。
アドレスは 0x6A 固定ですね。これがPLEN:bit 本体の i2c アドレスになるのかな。
送信するコマンドバッファは、関数のパラメータで付けられたアドレス(アドレス同じ名称でややこしい)と、値。
i2c の通信は、すべてこれを通すと思うので、あとはパラメータのアドレスが何を表すのかがわかれば!
それでは、write8 がわかったところで、改めて処理を上から見ていきます。
export function secretIncantation() {
write8(0xFE, 0x85);//PRE_SCALE
write8(0xFA, 0x00);//ALL_LED_ON_L
write8(0xFB, 0x00);//ALL_LED_ON_H
write8(0xFC, 0x66);//ALL_LED_OFF_L
write8(0xFD, 0x00);//ALL_LED_OFF_H
write8(0x00, 0x01);
}
初期化の処理です。「secretIncantation」=「秘密のおまじない」でしょうか…。これはまあ、何かしら初期化されてる、ぐらいの認識でいいでしょう。
次の行から、ブロックの定義になっています。
//% blockId=PLEN:bit_Sensor
//% block="read sensor %num"
export function sensorLR(num: LedLr) {
let neko = 0;
if (num == 16) {
neko = AnalogPin.P2;
} else {
neko = AnalogPin.P0;
}
return pins.analogReadPin(neko);
}
センサーの値取得です。LedLr の値は、以下が定義されています。
export enum LedLr {
//% block="A button side"
AButtonSide = 8,
//% block="B button side"
BButtonSide = 16
}
Aボタン側が 8、Bボタン側が 16です。
Aボタン側が引数で指定されたら、AnalogPin.P2の値を返し、Bボタン側が指定されたら、AnalogPin.P0の値を返します。
ピンの位置はこちら。
micro:bit pins
https://makecode.microbit.org/device/pins
AnalogPin.P0 / AnalogPin.P1 / AnalogPin.P2 は、大きい端子でした。前部のセンサーはここに繋がってるんですね。
センサーの3本のピンは、向かって左から順に 信号 / 3.3V / GND に接続されてました。
向いている方角の角度を取得。
//% block
export function direction() {
return Math.atan2(input.magneticForce(Dimension.X), input.magneticForce(Dimension.Z)) * 180 / 3.14 + 180
}
地磁気センサーの値を元に、コンパスの角度を取得します。
個別のサーボ制御。
//% blockId=PLEN:bit_servo
//% block="servo motor %num|number %degrees|degrees"
//% num.min=0 num.max=11
//% degrees.min=0 degrees.max=180
export function servoWrite(num: number, degrees: number) {
if (initPCA9865 == false) {
secretIncantation();
initPCA9865 = true;
}
let highByte = false;
let pwmVal = degrees * 100 * 226 / 10000;
pwmVal = Math.round(pwmVal) + 0x66;
if (pwmVal > 0xFF) {
highByte = true;
}
write8(servoNum + num * 4, pwmVal);
if (highByte) {
write8(servoNum + num * 4 + 1, 0x01);
} else {
write8(servoNum + num * 4 + 1, 0x00);
}
}
サーボ番号と、その角度を指定します。initPCA9865 は初期化フラグです。初期化されてなければ初期化処理を実行。
各サーボによって、write8 で書き込むアドレスが決まっているようです。
let servoNum = 0x08;
で定義されている 0x08 がサーボ0、そこから、各サーボ 4 バイトずつでアドレス確保されているみたいです。
サーボ0だと、0x08 + 4 = 0x0c になりますね。
実際に書き込む値は、指定された角度から計算しています。
let pwmVal = degrees * 100 * 226 / 10000;
角度が 0 ~ 180 なので、書き込む値は 0~406…?でいいのかな?なんか中途半端ですけれど。
16進で言えば 0x00~0x196 になります。
(servoNum + num * 4) に、16進の下2桁の値を、(servoNum + num * 4 + 1) に、16進の上1桁の値を書き込んでいます。上一桁は、 0x00~0x196 なので、0x00 か 0x01 どっちかになりますね。
0x196 の場合は、(servoNum + num * 4) に 0x96 を書き込み、(servoNum + num * 4 + 1) に 0x01 を書き込んでいます。
基本 / サッカー / ダンス モーション動作
//% blockId=PLEN:bit_motion_std
//% block="play std motion %fileName"
export function stdMotion(fileName: StdMotions) {
motion(fileName);
}
//% blockId=PLEN:bit_motion_Soc
//% block="play soccer motion %fileName"
export function soccerMotion(fileName: SocMotions) {
motion(fileName);
}
// blockId=PLEN:bit_motion_box
// block="play box motion %fileName"
export function boxMotion(fileName: BoxMotions) {
motion(fileName);
}
//% blockId=PLEN:bit_motion_dan
//% block="play dance motion %fileName"
export function danceMotion(fileName: DanceMotions) {
motion(fileName);
}
// blockId=PLEN:bit_motion_m
// block="play move motion %fileName"
export function moveMotion(fileName: MoveMotions) {
motion(fileName);
}
全部同じですね。与えられた値で、motion関数を呼んでいるだけです。
数が合わないですね。BoxMotions と MoveMotions に該当するブロックが画面にありません。
BoxMotions には、export enum BoxMotions で、ブロック名が付けられてないので、画面には出てこないっぽいです。
MoveMotions には一つもモーションが登録されてないので、これも画面には出てこないです。
モーション番号指定でのモーション動作
これが、PLEN:bit として一番重要な関数じゃないかと思います。上の「基本 / サッカー / ダンス モーション動作」も、これを読んで動作しています。以下、関数です。長いです!
//% blockId=PLEN:bit_motion
//% block="play motion number %fileName"
//% fileName.min=0 fileName.max=73
export function motion(fileName: number) {
let data = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let command = ">";//0x3e
let listLen = 43;
let readAdr = 0x32 + 860 * fileName;
//serial.writeNumber(fileName)
//serial.writeString(",fileName")
//serial.writeNumber(readAdr)
//serial.writeString(",adr")
let error = 0;
while (1) {
if (error == 1) {
break
}
let mBuf = reep(readAdr, listLen);
readAdr += listLen;
if (mBuf[0] == 0xff) {
break
}
let mf = ""; //=null ?
for (let i = 0; i < listLen; i++) {
let num = mBuf.getNumber(NumberFormat.Int8LE, i);
mf += numToHex(num);
}
//serial.writeString(",Nonull")
let listNum = 0;
while (listLen > listNum) {
if (command != mf[listNum]) {
listNum += 1;
continue
} //serial.writeString(",>OK")
listNum += 1; // >
//serial.writeString(mf[listNum]);
//serial.writeString(mf[listNum] + 1);
if ("mf" != (mf[listNum] + mf[listNum + 1])) {
//if (0x4d != (mf[listNum])) {
listNum += 2;
continue
} //serial.writeString(",mfOK")
listNum += 2; // MF
//if (fileName != int((_mf[listNum] + _mf[listNum + 1]), 16)) {
if (fileName != parseIntM(mf[listNum] + mf[listNum + 1])) {
error = 1;
break
}
//serial.writeString(",fileOK")
listNum += 4;// slot,flame
let times = (mf[listNum] + mf[listNum + 1] + mf[listNum + 2] + mf[listNum + 3])
let time = (parseIntM(times));
listNum += 4;
let val = 0;
while (1) {
if ((listLen < (listNum + 4)) || (command == mf[listNum]) || (24 < val)) {
setAngle(data, time);
break
}
let num = (mf[listNum] + mf[listNum + 1] + mf[listNum + 2] + mf[listNum + 3]);
let numHex = (parseIntM(num));
if (numHex >= 0x7fff) {
numHex = numHex - 0x10000;
} else {
numHex = numHex & 0xffff;
}
data[val] = numHex;
//serial.writeNumber(data[val]);
//serial.writeString(",")
val = val + 1;
listNum += 4;
}
}
}
}
長かった…!
引数で与えられたモーションのデータを EEPROM から読んで、そのデータに従って、各サーボを制御しています。
そう、モーションデータは、PLEN:bit 本体の EEPROM に入ってるみたいなんですよ。てっきり、micro:bit 側のデータとして保持しているのかと思った。これ、今後のモーション追加は、できないってことなのかな…。EEPROMを書き換える方法があるのだろうか。
→ Ver.0.04で、「function weep(eepAdr: number, num: number)」が追加されてました。これを使うと EEPROM 書き込みができると思われます。
PLEN / PLEN2 には、Motion Editor というのが用意されてて、それでモーションの作成や更新が出来たっぽいんだけど、ハードウェア構成も違ってるから、そのままでは使えなさそうですね。
let listLen = 43; let readAdr = 0x32 + 860 * fileName;
これで読出し先のアドレスを決定。”fileName”変数には、モーション番号が入っています。
let mBuf = reep(readAdr, listLen);
これで、EEPROM の指定アドレスから、データの読み込み。43 バイトずつ読み込んでいます。
モーションの処理は、長くなりそうなので、もうちょっと確認してから、別エントリで書こうと思います。
ROMからのデータ読出し reep
// blockId=PLEN:bit_reep
// block="readEEPROM %eepAdr| byte%num"
// eepAdr.min=910 eepAdr.max=2000
// num.min=0 num.max=43
export function reep(eepAdr: number, num: number) {
let data = pins.createBuffer(2);
data[0] = eepAdr >> 8;
data[1] = eepAdr & 0xFF;
// need adr change code
pins.i2cWriteBuffer(romAdr1, data)
let value = (pins.i2cReadBuffer(romAdr1, num, false));
return value
}
blockId 書いてあるので、ブロックになってたっけ?と思ったら、’//%’ではなかったので、単なるコメントでした。開発時にはブロックにしてたのかな?
i2c Read Buffer
https://makecode.microbit.org/reference/pins/i2c-read-bufferaddress: the 7-bit I2C address to read the data from.
size: the number of bytes to read into the buffer from the device.
repeated: if true, don’t send a stop condition after the read. Otherwise, a stop condition is sent when false (the default).
読み込み元のI2Cアドレスと、読み込みサイズ、連続してアクセスするかどうか。
let romAdr1 = 0x56;
読み込み元I2Cアドレスは 0x56です。
読み込み元ROMアドレスを data 配列で指定。data[0] に上位バイト、data[1]に下位バイトを入れます。
pins.i2cWriteBuffer(romAdr1, data)
で読み込み元ROMアドレスを指定して、
let value = (pins.i2cReadBuffer(romAdr1, num, false));
で num バイトを読み込み。num の MAX 値は 43 なのかな?ハードウェア的な制限かも?
BLEでスマホから接続。
これは、スペシャルキットだけのやつだと思います。
頭部パーツに、BLEユニットが付いてて、BLEシリアルでスマホと接続できる機能が付いているみたいです。
この頭部パーツだけ、部品で販売してくれないかなー…。
サーボモータ初期値設定
これ実行すると、PLEN:bit が素立ちに戻ります。重要命令!
サーボの初期値はこう定義されています。
let servoSetInit = [1000, 630, 300, 600, 240, 600, 1000, 720];
角度が10倍の値で入ってるみたいです。けっこう細かい値になってますね。微調整の結果かな…。
servoWriteで、各サーボをその値に設定。
//% block="servo motor initial"
export function servoInitialSet() {
//setAngle([0, 0, 0, 0, 0, 0, 0, 0], 1);//motionSpeed//num=1000
let sNum = 0;
servoWrite(sNum, servoSetInit[sNum] / 10);
sNum++;
servoWrite(sNum, servoSetInit[sNum] / 10);
sNum++;
servoWrite(sNum, servoSetInit[sNum] / 10);
sNum++;
servoWrite(sNum, servoSetInit[sNum] / 10);
sNum++;
servoWrite(sNum, servoSetInit[sNum] / 10);
sNum++;
servoWrite(sNum, servoSetInit[sNum] / 10);
sNum++;
servoWrite(sNum, servoSetInit[sNum] / 10);
sNum++;
servoWrite(sNum, servoSetInit[sNum] / 10);
}
(なんで for 使わないのかな…とかちょっと思った。)
サーボの力を抜きます。
//% block
export function servoFree() {
//Power Free!
write8(0xFA, 0x00);
write8(0xFB, 0x00);
write8(0xFC, 0x00);
write8(0xFD, 0x00);
write8(0x00, 0x01);
//write8(0x00, 0x80);
initPCA9865 == false
}
これも、おまじない的ですね。初期化フラグもOFFされます。
目のLED制御
//% block="eye led is %onoff"
export function eyeLed(ledOnOff: LedOnOff) {
//if (led_lr == 8) {
pins.digitalWritePin(DigitalPin.P8, ledOnOff);
//}
//if (led_lr == 16) {
pins.digitalWritePin(DigitalPin.P16, ledOnOff);
//23 or 15
//}
}
ON / OFF の定義はこちら。
export enum LedOnOff {
//% block="on"
On = 0,
//% block="off"
Off = 1
}
OFFの方が1なので気を付けて!
DigitalPin.P8 と、 DigitalPin.P16 の2つに信号送ってますね。これ、もしかして、右目と左目では?と思って、片方だけにセットしてみたけど、そうではなかったです。DigitalPin.P16 側だけが有効っぽいです。
うーん?なんで2つセットしてるんだろう?
見てみたら、他の用途で使われていないデジタルの空き、P8とP16だけですね。どっち使ってもいいように、両方に送ってるってことかな。
…と、少々の謎を残しつつ、ひとまずはソース読み切りました!











