mbed OS5 の Callback

mbed OS2 までは、引数に関数を受け取る場合、FunctionPointerArg1<戻り値, 引数> を使って抽象化していた。mbed OS 5 では Callback を使う。

mbed OS 5.1 からこの Callback 型を使わず、既存のAPIで関数を引数として渡すと非推奨の warning が出る。

Callback

mbed のレポジトリで

git grep Callback\<|grep Callback.h

すると定義が山ほど出てくるけど、よく使うのは

Callback<戻り値型(呼び出す関数の引数)> cb = callback(関数ポインタ);
Callback<戻り値型(呼び出す関数の引数)> cb = callback(オブジェクトへのポインタ、そのオブジェクトの関数ポインタ);

あたり。

例えば数値の2乗を返す関数を Callback 型にしたいときは、callback() をつかう。

#include <math.h>
uint16_t pow2(uint8_t n) {
    return pow(n, 2);
}

Callback<uint16_t(uint8_t)> callbackPow2 = callback(&pow2);
callbackPow2(10);
callbackPow2.call(10);

Callback 型は ()演算子オーバーロードしてるので、関数呼び出しのように書くこともできるし、明示的に call() で呼び出すことも出来る。

また、もちろんオブジェクトとそのオブジェクトが持つ関数のバインドにもよく使う。

Callback<R(A0)> cb = callback(&objFoo, &Foo::func);

mbed で Serial の CTS/RTS を普通のGPIOとして利用する

現状バグっていて出来ない。

DEVICE_SERIAL_FC を定義すればうまくいきそうなものの、こちらもうまく動かない。(リンカエラーになるので、cpp のコンパイル時にうまく定義できてない?)

そのため、↑のエントリーで書かれているように

  • SerialBase::set_flow_control を時前で定義する
  • 各ハードウェアにあわせて無効化する

NRF なら

    NRF_UART0->PSELRTS = 0xFFFFFFFFUL;
    NRF_UART0->PSELCTS = 0xFFFFFFFFUL;

これでGPIOとして使えるようになる。

HX711 の準備完了時間、mbed OS5 での thread

HX711 では、データ出力準備が整ったら、DOUTが HIGH -> LOW になる。この出力時間の待ち受け時間を計ったところ、約77msかかる。メインループで待ち受けすると77msは割と支配的でUIのレスポンスに悪化に繋がる。

AVR の Arduino なら Timer のハードウェア割り込みを使って、メインループとは別のところで処理しつつ、適当な volatile 変数に値を入れるような実装を書くと思う。

mbed OS 5 の場合

mbed は指定時間ごとに関数を呼び出せる Ticker クラスが用意されているが、割り込みハンドラは全部合わせて1つなので、Ticker に重い処理を登録すると遅延が発生する。

mbed OS5 では rtos になったので、この辺の優先度付けが出来るのかと思ったら現状特に出来ないので、Thread を使ってメインループ以外で実行し続けることで解決してみた。

例えばこんな感じ。

#include <mbed.h>
#include "HX711.hpp"

class ScaleGauge {
    HX711 *_p_scale;
    Thread _thread;
    volatile float _gram;

    void threadLoop() {
        while(1) {
            _gram = _p_scale->readGram(1);
        }
    }
public:
    ScaleGauge(HX711 *hx711) :
        _p_scale(hx711), _thread(osPriorityLow) {
    }

    void start() {
        _thread.start(callback(this, &ScaleGauge::threadLoop));
    }

    float getGram() {
        return _gram;
    }
};

あとは main で

int main() {
    ScaleGauge sg(&hx711);
    sg.start();

    while(1) { wait_ms(100) };
}

とすれば、ずっと裏側で値をとり続ける。

最初

int main() {
    ScaleGauge sg(&hx711);
    sg.start();

    while(1);
}

としててうまく動かない (threadLoop がよばれない) けど、理由は Thread の優先度をosPriorityLow にしていたからで、その場合 while(1) が延々と呼び出されてしまう。osPriorityNormal の標準プライオリティにするか、わざと割り込みできるように wait_ms() 等を入れるか、yield で処理を戻すか等々をする必要がある。

あわせてよみたい:

追記:

 Thread::wait(osWaitForever);

で待ち受ければ良いぽい。

安い電子パーツを ebay や aliexpress (アリババ) で買う

時間がかかっても安く買いたい、そんなときは ebay もしくは ali で買う。

ebay は "Free international shipping" な物が国際送料無料。基本送料込みで ebay はソートしてくれるので買う。ebay に無い場合は ali で探す。ali にしか出ていないショップもある。

買うの難しいの?

普通のECなので特に難しくない。特に ebay は決済を paypal でできるので、paypal 民にとってはかなり楽に購入できる。

どれぐらい安いの?

大体日本国内で、輸入されたと思われる同等製品を買う場合、5倍ぐらいの値段がついている。また、中国からは何故か国際郵便がすごく安いらしく、100円の品物も送料無料で買えたりする。

Amazon.co.jp で売り手が複数いる安価な中国製品を見つけた場合、大体 ebay で探すと1/5ぐらいの値段で売っている。またこのような商品は購入後中国から船便で送られてくるため、Amazon プライムで即納でも無い限り、ebay 等から買った方が良い。

到着までの日にちは

大体2週間~4週間。買ったことを忘れた頃に届く。また店の場所が香港の場合、早く届く可能性が高い(感覚として1~2週間で届く、深センとあまり場所変わらないのにね)ので、ちょっと高くても香港の店で出店してる場合、そちらで買うことが多い。

トラブルはあるの?

今のところ30回ぐらい買って、DMでやりとりしないと届かなかったのが2回。うち1回は未だにやりとりを続けているので、それなりにあると言える。ただ単価が安いので、まぁ良いかと思ってる。

また ebaypaypal などで購入した場合、eBay買い手保護保障制度 があるので安心ぽい(使ったことないけど)。

mbed SDK での C++ コーディングルール

にコーディングルールが書かれていた。

今まで C++ でちゃんとしたコーディングルール上で開発をしたことがなかったので読んでみた。

ルール

  • The mbed SDK code follows K&R style (Reference: K&R style) with at least 2 exceptions which can be found in the list below the code snippet:
static const PinMap PinMap_ADC[] = {
    {PTC2, ADC0_SE4b, 0},
    {NC , NC , 0}
};

uint32_t adc_function(analogin_t *obj, uint32_t options)
{
    uint32_t instance = obj->adc >> ADC_INSTANCE_SHIFT;
    switch (options) {
        case 1:
            timeout = 6;
            break;
        default:
            timeout = 10;
            break;
    }

    while (!adc_hal_is_conversion_completed(instance, 0)) {
        if (timeout == 0) {
            break;
        } else {
            timeout--;
        }
    }

    if (obj->adc == ADC_CHANNEL0) {
        adc_measure_channel(instance);
        adc_stop_channel(instance);
    } else {
        error("channel not available");
    }

#if DEBUG
    for (uint32_t i = 0; i < 10; i++) {
        printf("Loop : %d", i);
    }
#endif
    return adc_hal_get_conversion_value(instance, 0);
}
  • Indentation - 4 spaces. Please do not use tabs!
    • インデントは4スペース。タブは使わないで!
  • Braces - K&R, except for functions where the opening brace is on the new line.
    • {} は K&R。ただし新しい行出だしに { がある関数は除く。(意図がわからなかった…)
  • 1 TBS - use braces for statements if, else, while, for (exception from K&R) Reference: 1TBS).
    • if, else, while, for 式の場合、一行に {} を書く。 (K&Rからの例外)
  • One line per statement.
    • 式は一行に一つ
  • Preprocessor macro starts at the beginning of a new line, the code inside is indented accordingly the code above it.
    • プリプロセッサーのマクロは新しい行のはじめから書く。コードの内部にある時はインデントにあわせて書く。
  • Cases within switch are indented (exception from K&R).
    • switch 内側の case 文はインデントする。 (K&Rからの例外)
  • Space after statements if, while, for, switch, same applies to binary and ternary operators.
  • Each line has preferably at most 120 characters.
    • できれば1行は120文字以内におさめる。
  • For pointers, * is adjacent to a name (analogin_t *obj).
  • Don't leave trailing spaces at the end of lines.
    • 行の最後のスペースの削除を忘れずに
  • Empty lines should have no trailing spaces.
    • 空行にスペースを入れるべきでない
  • Unix line endings are default option for files.
    • ファイル標準の改行コードは Unix (\n) で
  • Use capital letters for macros.
    • マクロは大文字で
  • A file should have an empty line at the end. and:
    • ファイルの最後は空白行で終わるべき。(and: ←これなんだろう)
  • We are not using C++11 yet so do not write code compliant to this standard.
    • まだ C++11 を使えないので、C++11準拠のコードを書かないで。
    • (Kail と GCC_ARM は対応してるけど、IAR が対応してないからかな…)
  • We are not using libraries like BOOST so please so not include any BOOST headers to your code.
    • BOOSTのようなライブラリを使わない、BOOST のヘッダを含めないで
  • C++ & templates: please take under consideration templates are not fully supported by cross-compilers. You may have difficulties compiling template code few cross-compilers so make sure your template code compilers for more than one compiler.
    • 複数のコンパイラでサポートされるかどうかを考慮の上、テンプレートを使って。複数のクロスコンパイラで確認すれば良いよ。
    • (これは mbed が Kail, IAR, GCC_ARM のコンパイラをサポートしてるから)

命名規則

Classes: クラス名

  • Begins with a capital letter, and each word in it begins also with a capital letter (AnalogIn, BusInOut).
    • 大文字で始まり、また単語頭も大文字に (AnalogIn, BusInOut)
  • Methods contain small letters, distinct words separated by underscore.
    • メソッドは小文字で、異なる単語は _ で区切る
  • Private members starts with an underscore.
    • プライベートなメンバ変数は _ で始める。

User defined types (typedef):

  • Structures - suffix _t - to denote it is user defined type
    • 構造体は _t を末尾につける
  • Enumeration - the type name and values name - same naming convention as classes (e.g MyNewEnum)
    • Enum はタイプと値の名前で、クラスメイト同じ規則

Functions: 関数

  • Contain lower case letters (as methods within classes)
    • 小文字で(クラス内のメソッドっぽく)
  • Distinct words separated by underscore (wait_ms, read_u16)
    • 異なる単語は _ で区切る (wait_ms, read_u16)
  • Please make sure that in your module all functions have unique prefix so when your module is compiled with other modules function names (and e.g. extern global variable names) are not in naming conflict.
    • 他のモジュールから参照される(例えば extern でグローバル変数名をつけた)とき、あなたのモジュール名の関数にユニークな接頭語をつけてください。コンフリクトしないように。

サンプルコード

#define ADC_INSTANCE_SHIFT 8

class AnalogIn {
public:
    /** Create an AnalogIn, connected to the specified pin
    *
    * @param pin AnalogIn pin to connect to
    * @param name (optional) A string to identify the object
    */
    AnalogIn(PinName pin) {
        analogin_init(&_adc, pin);
    }

    /** Read the input voltage, represented as a float in the range [0.0, 1.0]
    *
    * @returns
    * A floating-point value representing the current input voltage, measured as a percentage
    */
    uint32_t read() {
        return analogin_read(&_adc, operation);
    }

protected:
    analogin_t _adc;
};

typedef enum {
    ADC0_SE0 = (0 << ADC_INSTANCE_SHIFT) | 0,
} ADCName;

struct analogin_s {
    ADCName adc;
};

typedef struct analogin_s analogin_t;

mbed で Callback 関数を扱う

mbed のコアAPI、 Ticker などでも使われている、FunctionPointer を使う。

typedef FunctionPointerArg1<void, void> FunctionPointer;
typedef FunctionPointerArg1<void, int> event_callback_t;

の定義の通り、引数なしなら FunctionPointer を、int を引数に渡したいなら event_callback_t を使う。FunctionPointerArg1 を使って自分で定義するのももちろん良い。ちなみに FunctionPointerArg2 は無い。

FunctionPointer を使うと、いわゆる bind 的なことも

void attach(T *object, R (T::*member)(void)) { // bind
void attach(R (*function)(void)) {

でしてくれるのでちょっと便利。


そもそも C++11 の <std::function> 等々が使えれば良いんじゃ無い、と思うけど、mbed OS では

We are not using C++11 yet so do not write code compliant to this standard.

というポリシーなので、このような事が必要。

BLE Nano V1.5 を mbed 上で 32K RAM で扱う

BLE Nano は V1.0 では 16KB の RAM が、V1.5 から 32K RAM がのっている。が、mbed 上では現在 1.0 ベースしかなく、折角の 32K RAM が使えない。ので使えるようにする方法メモ。

mbed OS 5 の場合

rtos の INITIAL_SP でスタックポインタのアドレスをマッピングしている模様。

#elif defined(TARGET_MCU_NORDIC_32K)
  #define INITIAL_SP            (0x20008000UL)
#elif defined(TARGET_MCU_NORDIC_16K)
  #define INITIAL_SP            (0x20004000UL)

コンパイル時のマクロの追加は mbed-os/hal/targets.json を見て追加されるので、

みたいに変更すればOK。この環境で BLEサンプルのHeartRate が問題なく nano 32K RAM 環境で動いた。

最初 RBLAB_BLENANO_32K な環境を作ってちゃんと pull req 出そうと思ったけど、V1.0 を考慮しつつも V1.5 の環境を追加する場合、スマートな書き方がうまく出来ず、思った以上に追加 & 書き換え項目多くて、報告するだけしておいた…。まだ mbed OS5 は出たて感があるから、ボード追加より開発の方が盛んっぽいしね。

なお targets.json に関しては以下が参考になった。

mbed OS 2 (Classic) の場合

オンラインコンパイラでは現状無理で、手元に落としてリンカの RAM 領域を書き換える。

GCC_ARM 環境を使う場合、手元にエクスポートしたデータ(もしくは mbed-cli から import で classic 用のプロジェクトを持ってくる)のリンカ

mbed/#{hash}/TARGET_RBLAB_BLENANO/TOOLCHAIN_GCC_ARM/NRF51822.ld

MEMORY
{
  FLASH (rx) : ORIGIN = 0x0001C000, LENGTH = 0x24000
  RAM (rwx) :  ORIGIN = 0x20002800, LENGTH = 0x1800
}

から

MEMORY
{
  FLASH (rx) : ORIGIN = 0x0001B000, LENGTH = 0x25000
  RAM (rwx) :  ORIGIN = 0x20002ef8, LENGTH = 0x5108
}

へと書き換える。0x20002ef8 + 0x5108 = 0x20008000 が開始時のスタックポインタとなる。

以前の 16KB 用のリンカだと、プログラム開始(+ちょっと初期化後) のスタックポインタは 0x20003fa8 だったが、32KBに書き換え後のリンカでは、0x20007fa8 となり、0x20008000 から始まっていることが解る。

mbed でスタックポインタを調べる

NRF51なら、GET_SP() というマクロが定義されており、そこから現在のスタックポインタが解る。