rustでmbedからBLEビーコンを送るソフトを作成してみた

October 03, 2021

最近rustというプログラミング言語を勉強しているのですが、この言語はC言語のように組み込み開発にも使用できるということ知り、早速以前購入したmbed HRM1017(旧バージョン)向けにBLEのビーコンを送るソフトを実装しましたのでメモします。HRM1017はCortex-M0を搭載したNordic社のnRF51822チップを搭載しているBLEモジュールです。

今回はC言語ではなく、Rustを使用するため、mbedの開発環境(WEBアプリ)は使えないので、mbedにソフトを書き込んだりデバッグするための実機デバッグ環境と、プログラムを開発するためのコーディング環境をそれぞれmacとWindows PC上に構築しました。

まず、macのほうですが、こちらのページを参考に、macにopenocdというツールをインストールして、mbed HRM1017をUSBポートにつなぎ、さらにgithubからopenocdのソースをクローンしてきてその中に含まれている、HRM1017と同じチップを積んだ評価基板であるNordicのNRF51822_MKIT用の設定ファイルを指定してopenocdを起動してgdbサーバを立ち上げました。このサーバにコーディング環境のgdbクライアントからリモートで接続すれば、mbedのフラッシュにソフトを書き込んだり、デバッグすることが可能になります。openocdは以下のようにオプションをつけて実行してます。起動に成功すると、mbed HRM1017の緑のLEDが高速に点滅し続けるようになります。なお、指定しているcfgファイルは、前述したopencdのソースのtcl/boardフォルダの中にあります。

openocd -f board/nordic_nrf51822_mkit.cfg -c "bindto 0.0.0.0"

ちなみに、cfgをのぞいてみてわかったのですが、swdというデバッグ用の規格(プロトコル)を使っているようです。

次にコーディング環境の構築です。VSCODEで新規にプロジェクトを作成し、remote containerのプロジェクトとしてコンテナで起動します。このとき、プロジェクトの使用言語としてrustを指定しました。そして、rustの組み込み開発を行うための各種インストールを行うため、.devcontainer/Dockerfileを以下のように書き換えました。いろいろ調べながら編集したので余計なものまで入っているかもしれません。

# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.194.0/containers/rust/.devcontainer/base.Dockerfile

FROM mcr.microsoft.com/vscode/devcontainers/rust:0-1

# [Optional] Uncomment this section to install additional packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
#     && apt-get -y install --no-install-recommends <your-package-list-here>

RUN apt -y update && apt install -y binutils-arm-none-eabi gdb-multiarch
RUN apt install -y cmake libssl-dev libssh-dev pkg-config
USER vscode

RUN cargo install cargo-binutils
RUN rustup component add llvm-tools-preview
RUN rustup target add thumbv6m-none-eabi 

rustの組み込み開発について調査したところ、Nordic社のnRF51シリーズ向けの開発を行うクレートとしてnrf51-halがあることがわかりましたので、このクレートを使ってみることにしました。このクレートのソースをgithubからとってきて、同梱されているサンプルプロジェクト(blinky-button-demo)をテンプレートとして使用しました。このデモプログラムは、ボタンを押すとLEDが点灯する、という単純なものになっています。念のため、期待通り動くか実際にmbedにLEDとボタンをつないで後述する方法でビルド、イメージのフラッシュ書き込み、実行すると、問題なく動作しました。結線はいつものようにブレッドボード上で行いました。

最初、このクレートを使えばBLEが使えるようになるのかと期待したのですが、残念ながらBLEのプロトコルスタックまでは含まれていないので、別途追加しないといけないことがわかりました。こちらのQ and Aによると、Softdeviceというバイナリファイルをあらかじめフラッシュに書き込んで、開発したアプリから使う方法と、rubbleというプロトコルスタックを実装したクレートを使う方法がある、とわかりました。前者の方法はRustから使用するためにbindingするためのクレートが必要なのですが、唯一見つかったnrf-softdeviceというクレートがnrf51に対応していないようでしたので、今回はrubbleの方を使用することにしました。ちなみにこのクレートは動作保証外とのことで、使用される場合は要注意です。

githubからrubbleのソースに含まれているnrf52-beaconというビーコンを飛ばすサンプルプログラムを参考に、先ほどのblinky-button-demoのプログラムをボタンを押すとBLEのビーコンを送るように改造してみました。ソースコードは以下の通りです。

#![no_main]
#![no_std]
#![warn(rust_2018_idioms)]

use nrf51_hal as hal;
use nrf51_hal::{gpio::Level,prelude::ConfigurablePpi,prelude::Ppi};
use rubble_nrf5x::utils::get_device_address;
use rtt_target::{rprintln, rtt_init_print};
use rubble::beacon::Beacon;
use rubble::link::{ad_structure::AdStructure, MIN_PDU_BUF};
use rubble_nrf5x::radio::{BleRadio, PacketBuffer};

#[panic_handler] // panicking behavior
fn panic(_: &core::panic::PanicInfo<'_>) -> ! {
    loop {
        cortex_m::asm::bkpt();
    }
}

#[rtic::app(device = crate::hal::pac, peripherals = true)]
const APP: () = {
    struct Resources {
        #[init([0; MIN_PDU_BUF])]
        ble_tx_buf: PacketBuffer,
        #[init([0; MIN_PDU_BUF])]
        ble_rx_buf: PacketBuffer,
        radio: BleRadio,
        beacon: Beacon,
        gpiote: hal::gpiote::Gpiote,
    }

    #[init(resources = [ble_tx_buf, ble_rx_buf])]
    fn init(ctx: init::Context) -> init::LateResources {
        rtt_init_print!();

        // ボタンとLEDの設定
        rtic::pend(hal::pac::Interrupt::GPIOTE);
        let port0 = hal::gpio::p0::Parts::new(ctx.device.GPIO);
        let button = port0.p0_13.into_pullup_input().degrade();
        let led = port0.p0_17.into_push_pull_output(Level::Low).degrade();
        let gpiote = hal::gpiote::Gpiote::new(ctx.device.GPIOTE);

        // Set btn1 to generate event on channel 0 and enable interrupt
        gpiote
            .channel0()
            .input_pin(&button)
            .hi_to_lo()
            .enable_interrupt();

        gpiote
            .channel1()
            .output_pin(led)
            .task_out_polarity(hal::gpiote::TaskOutPolarity::Toggle)
            .init_low();
        let ppi_channels = hal::ppi::Parts::new(ctx.device.PPI);
        let mut ppi0 = ppi_channels.ppi0;
        ppi0.set_task_endpoint(gpiote.channel1().task_out());
        ppi0.set_event_endpoint(gpiote.channel0().event());
        ppi0.enable();

        // On reset, the internal high frequency clock is already used, but we
        // also need to switch to the external HF oscillator. This is needed
        // for Bluetooth to work.
        let _clocks = hal::clocks::Clocks::new(ctx.device.CLOCK).enable_ext_hfosc();

        // Determine device address
        let device_address = get_device_address();

        // Rubble currently requires an RX buffer even though the radio is only used as a TX-only beacon.
        let radio = BleRadio::new(
            ctx.device.RADIO,
            &ctx.device.FICR,
            ctx.resources.ble_tx_buf,
            ctx.resources.ble_rx_buf,
        );

        let beacon = Beacon::new(
            device_address,
            &[AdStructure::CompleteLocalName("Rusty Beacon (nRF51)")],
        )
        .unwrap();

        rprintln!("Blinky button demo starting");

        init::LateResources { radio, beacon, gpiote }
    }

    #[task(binds = GPIOTE, resources = [radio, beacon, gpiote])]
    fn gpiote(ctx: gpiote::Context) {
        rprintln!("sending beacon...");
        ctx.resources.beacon.broadcast(ctx.resources.radio);
        ctx.resources.gpiote.reset_events();
    }
};

nrf52-beaconはrticという割り込みをトリガーとした並列処理を実装できるクレートを使用しているのですが、timer-queueという機能を利用しています。この機能はnrf51では使用できない(Cortex-M0がDWTをサポートしていないため)ので、timer-queueを使わないようにして、ボタンを押すことでビーコンを送るように改造しています。また、ボタンを押したときに割り込みがかかるようにするため、gpioteという仕組みを使っています。こちらはrticのソースにサンプルソースがありましたので、それを参考に実装してます。ちなみに実際に割り込みがかかっているかどうかをチェックできるようにするため、ボタンを押すとLEDがトグルするようにしてます(gpioteのtaskを使用)。

続いて実機でデバッグする手順を書きます。ビルドは普通にcargo buildで行います。そして、macでopenocdを起動しておき、Windows PCのVSCODEのターミナルで以下のコマンドを打ち込みます。

gdb-multiarch target/thumbv6m-none-eabi/debug/blinky-button-demo

gdb-multiarchはarmのプログラムもデバッグ可能なgdbです。gdbが起動したら、macのgdbサーバにつないでビルドしたイメージを書き込むため、以下のようにコマンドを入力します。

(gdb) target remote server:3333
(gdb) monitor reset halt
(gdb) load
(gdb) continue

これでWindows PCからイメージがmacに送られて、mbedのフラッシュに書き込まれ、アプリが起動します(mac側の操作は不要)。実際は、serverはmacのIPアドレスです。ブレークをかけたい場合は、例えば事前にtbreak fooのように入力してからcontinueすれば、fooの関数に処理がうつるとブレークすると思います(まだ試していません)。

最後にRTTについて。RTTはCortex-M0にも搭載されている高速転送技術で、JTAGまたはSWDでデバッグしている状態であれば、ホスト側にデバッグプリントを出力できます。rustの組み込みアプリからこの機能を使うため、rtt-targetというクレートを使用しています。使い方としては、rtt_init_print!()を初期化時に呼んでおいて、デバッグプリントを出したいときにrprintln!(string)を呼ぶだけです。

openocdからRTTによって出力されたプリント文をリモートに送るためには、openocdのコマンドを実行して新たにRTTのサーバを立ち上げる必要がありますが、このページが非常に参考になりました。このページではopenocdのソースにパッチをあててビルドしてますが、現在はbrewでインストールした状態のままで使えました。また、rtt setupコマンドで指定するアドレスは、nmコマンドにビルドイメージのパスを指定すれば、シンボルとアドレスのリストが出るので、RTTでgrepすれば調べることができます。RTTのサーバを起動したら、リモートからtelnetで接続すれば、rprintln!で出力したデバッグプリントが送られてきます。私の場合はこのページにあるようにgdb上でrttのコマンドを打ち込んで、下記のようにRTTサーバを起動するようにしています。

(gdb) monitor rtt setup 0x200000000 512 SEGGER\ RTT
(gdb) monitor rtt start
(gdb) monitor rtt channels
(gdb) monitor rtt server start 7777 0

ビーコンの動作確認はAndroidアプリのBLE Scannerを使用しました。ボタンを押すと、「Rusty Beacon(nRF51)」という名前でリストに表示されましたので、期待通り動いているようです。