Open20

Keyball(RP2040)のファームウェアをRustで作るログ

nazo6nazo6

購入したもの

  • こちらのRP2040搭載ProMicro互換ボードx2
    • 一番上のピンを無視すればProMicroにピン互換性がある
  • USB-Cケーブル
  • Keyballで使われてる背が低いピンヘッダ(PSS-410153-12)x4
nazo6nazo6

ハードの準備

と言ってもボードにピンヘッダを半田付けするだけ。一番上(USB側)のピンは無視することに注意

nazo6nazo6

Embassyプロジェクトのセットアップ

  1. cargo-generateでテンプレートから作成

    cargo generate --git https://github.com/lulf/embassy-template.git
    
  2. elf2uf2-rsのインストール
    公式ではprobe-rsを使っているがデバッグプローブは持っていないのでuf2で書きこめるようにこちらを使う。

    cargo install elf2uf2-rs
    

    (基板上のbootボタンを押しながらUSBに差すとUSB Mass Storageとして認識される。そこにプログラムをコピーするだけで実行できるが、その時必要なのがuf2という形式

  3. .cargo/config.tomlの編集

    .cargo/config.toml
    runner = "elf2uf2-rs -d"
    

これでbootselモードでUSBに差しcargo runすれば実行できるはず

nazo6nazo6

BOOTボタン

RP2040版ProMicroでKeyball向けに開発するとき、BOOTボタンが背面に隠れてしまうため、いちいち外してBOOTボタンを押さなければいけない。
これは非常に面倒。
ということでもっと簡単にBOOTSELを起動する方法はないか探してみた

https://hikalium.hatenablog.jp/entry/2021/12/31/150738

Rustでkeyballのファームを作った先人の方のブログを見てみると、どうやらSWDというのを使えば裏から色々弄れるみたい。

が、このボードは正規のProMicroではないため、どうやらそのようなピンは生えていない。

nazo6nazo6

RESETダブルクリックによるBOOTSELの起動

そこでさらに調べてみると、Cのpico-sdkではRESETダブルクリックによるBOOTSEL起動ができるということが分かった。

https://qiita.com/fude-t/items/dee413c2d78765ce268f

https://github.com/raspberrypi/pico-sdk/blob/6a7db34ff63345a7badec79ebea3aaef1712f374/src/rp2_common/pico_bootsel_via_double_reset/pico_bootsel_via_double_reset.c

RESETしてもメモリがリセットされない領域があり、そこに一定時間適当な数字を書き込んでおくことで、2連続RESETされたことを検知する、という仕組みのようだ。

また、Keyballではないが、Rustでキーボードのファームを実装している方によるRust実装も見つけた

https://github.com/Univa/rumcake/blob/4b7ac442b872abc60f7e92dac138773abb7123a4/rumcake/src/hw/mod.rs#L135-L150

ので有り難くこれを使わせてもらう

nazo6nazo6
main.rs
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_rp::init(Default::default());

    unsafe { double_reset::check_double_tap_bootloader(500).await };

    ...
double_reset.rs
use core::{
    cell::UnsafeCell,
    mem::MaybeUninit,
    ptr::{read_volatile, write_volatile},
};

use embassy_time::Timer;

const BOOTLOADER_MAGIC: u32 = 0xDEADBEEF;

#[link_section = ".uninit.FLAG"]
static mut FLAG: UnsafeCell<MaybeUninit<u32>> = UnsafeCell::new(MaybeUninit::uninit());

pub async unsafe fn check_double_tap_bootloader(timeout: u64) {
    if read_volatile(FLAG.get().cast::<u32>()) == BOOTLOADER_MAGIC {
        write_volatile(FLAG.get().cast(), 0);

        embassy_rp::rom_data::reset_to_usb_boot(0, 0);
    }

    write_volatile(FLAG.get().cast(), BOOTLOADER_MAGIC);

    Timer::after_millis(timeout).await;

    write_volatile(FLAG.get().cast(), 0);
}
nazo6nazo6

RESETするのに時間がかかるのであまり速くダブルクリックするとうまくリセットできない
「カチッ、カチッ」ぐらいの速度感が良い

nazo6nazo6

OLEDの表示

keyballで使われているOLEDのコントローラはSSD1306というやつで、それ用のRustクレートもある

https://docs.rs/ssd1306/latest/ssd1306/

ただ、残念ながら非同期には対応してないみたい

https://github.com/jamwaffles/ssd1306/issues/146

まあそんなに頻繁に書き換えるものじゃないし良いでしょう

embedded_graphics

ssd1306はembedded_graphicsクレートに対応している。
embedded_graphicsを使えば組み込み機器で文字などを簡単に表示できる

nazo6nazo6

OLEDとはi2cで通信する。
I2C_1の方を使うみたい。
先述の通りノンブロッキングは対応してないのでblockingなi2cを使う

main.rs
    let mut i2c_config = embassy_rp::i2c::Config::default();
    i2c_config.frequency = 400_000;

    let i2c = embassy_rp::i2c::I2c::new_blocking(p.I2C1, p.PIN_3, p.PIN_2, i2c_config);
nazo6nazo6
ssd1306.rs
use embassy_rp::{
    i2c::{Blocking, I2c},
    peripherals::I2C1,
};
use embedded_graphics::{
    mono_font::{ascii::FONT_6X10, MonoTextStyleBuilder},
    pixelcolor::BinaryColor,
    prelude::*,
    text::{Baseline, Text},
};
use ssd1306::{mode::BufferedGraphicsMode, prelude::*, I2CDisplayInterface, Ssd1306};

pub struct Ssd1306Display<'a> {
    display: Ssd1306<
        I2CInterface<I2c<'a, I2C1, Blocking>>,
        DisplaySize128x32,
        BufferedGraphicsMode<DisplaySize128x32>,
    >,
}

impl<'a> Ssd1306Display<'a> {
    pub fn new(i2c: I2c<'a, I2C1, Blocking>) -> Self {
        let interface = I2CDisplayInterface::new(i2c);
        let mut display = Ssd1306::new(interface, DisplaySize128x32, DisplayRotation::Rotate0)
            .into_buffered_graphics_mode();
        display.init().unwrap();

        Self { display }
    }

    pub fn draw_text(&mut self, text: &str) {
        self.display.clear_buffer();

        let text_style = MonoTextStyleBuilder::new()
            .font(&FONT_6X10)
            .text_color(BinaryColor::On)
            .build();

        Text::with_baseline(text, Point::zero(), text_style, Baseline::Top)
            .draw(&mut self.display)
            .unwrap();

        self.display.flush().unwrap();
    }
}
nazo6nazo6

これでテキストを描画するメソッドができたのであとは使うだけ

main.rs
    let mut display = ssd1306::Ssd1306Display::new(i2c);
    display.draw_text("Hello from rust");

これでOLEDにHello from rustという文字が表示されるはず

nazo6nazo6

panic時にOLEDを表示したい

ので、displayをグローバルに置く必要があるが、mutでなければならないので、同期プリミティブを使う必要がある。

https://embassy.dev/book/dev/sharing_peripherals.html

main.rs
// 定義
type DisplayType = Mutex<ThreadModeRawMutex, Option<Ssd1306Display<'static>>>;
static DISPLAY: DisplayType = Mutex::new(None);

// 使用側
DISPLAY
    .lock()
    .await
    .as_mut()
    .unwrap()
    .draw_text("Hello from rust");

panicハンドラ内での使い方

main.rs
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    DISPLAY
        .try_lock()
        .unwrap()
        .as_mut()
        .unwrap()
        .draw_text("Panic!");

    loop {}
}

panicハンドラ内でpanicした時のことはひとまず考えないことにする

nazo6nazo6

キーの読み取り

まず、キーを読み取る部分を作っていく

Keyballの特徴として、Duplex Matrixという方法が採用されている。

https://voyage4bliss.com/what-is-duplex-matrix/

そもそもマトリクススキャンのことも知らなかったので上の記事が非常に役に立った。
要するにCOLを順番にHighにしてHighになったROWがどれかがわかれば押されたキーがわかる。
で、Duplex MatrixではROWをHighにしてCOLを調べることもすることで少ない端子で沢山のキーを扱えるということみたい。

nazo6nazo6

できたのがこちら

main.rs
    let in_fut = async {
        loop {
            let mut str = heapless::String::<100>::new();

            str.push_str("RC:").unwrap();

            for row in rows.iter_mut() {
                row.set_as_output();
                row.set_low();
            }
            for col in cols.iter_mut() {
                col.set_as_input();
                col.set_pull(Pull::Down);
            }

            for (i, row) in rows.iter_mut().enumerate() {
                row.set_high();
                row.wait_for_high().await;

                for (j, col) in cols.iter_mut().enumerate() {
                    if col.is_high() {
                        write!(&mut str, "{},{}+", i, j).unwrap();
                    }
                }

                row.set_low();
                row.wait_for_low().await;
            }

            str.push_str("\nCR:").unwrap();

            for row in rows.iter_mut() {
                row.set_as_input();
                row.set_pull(Pull::Down);
            }
            for col in cols.iter_mut() {
                col.set_as_output();
                col.set_low();
            }

            for (j, col) in cols.iter_mut().enumerate() {
                col.set_high();
                col.wait_for_high().await;

                for (i, row) in rows.iter_mut().enumerate() {
                    if row.is_high() {
                        write!(&mut str, "{},{}+", i, j).unwrap();
                    }
                }

                col.set_low();
                col.wait_for_low().await;
            }

            // DISPLAY.lock().await.as_mut().unwrap().draw_text(&str);

            Timer::after_millis(50).await;
        }
    };
nazo6nazo6
  • 最初OutputInputを使っていたがなぜか1つキーを押すと2つ反応してしまうというよくわからないバグを作ってしまった
    • Flexを使ってプリミティブな感じで書き直すとうまくいった
  • 雑に50ms待っているがこれでいいんだろうか?
  • 5つ以上のキーを同時押しするとバグる気がする。これはハードの制限?