Keyball(RP2040)のファームウェアをRustで作るログ
以前Keyball用のファームウェアをRustで書くことを試みたが
やはりいろいろ辛そうということでAliExpressからRP2040が載ったProMicro互換基盤をゲットした。
これを使ってファームウェアをRustで作りたいという話
使用技術
- Rust: 言わずと知れた世界最高言語
-
embassy: Rustの組み込みでasyncができるすごいやつ
- embassyではRP2040はかなり手厚くサポートされており、USBもライブラリを入れるだけですぐ
参考資料
購入したもの
-
こちらのRP2040搭載ProMicro互換ボードx2
- 一番上のピンを無視すればProMicroにピン互換性がある
- USB-Cケーブル
- Keyballで使われてる背が低いピンヘッダ(PSS-410153-12)x4
ハードの準備
と言ってもボードにピンヘッダを半田付けするだけ。一番上(USB側)のピンは無視することに注意
Embassyプロジェクトのセットアップ
-
cargo-generateでテンプレートから作成
cargo generate --git https://github.com/lulf/embassy-template.git
-
elf2uf2-rs
のインストール
公式ではprobe-rs
を使っているがデバッグプローブは持っていないのでuf2で書きこめるようにこちらを使う。cargo install elf2uf2-rs
(基板上のbootボタンを押しながらUSBに差すとUSB Mass Storageとして認識される。そこにプログラムをコピーするだけで実行できるが、その時必要なのが
uf2
という形式 -
.cargo/config.toml
の編集.cargo/config.tomlrunner = "elf2uf2-rs -d"
これでbootselモードでUSBに差しcargo run
すれば実行できるはず
BOOTボタン
RP2040版ProMicroでKeyball向けに開発するとき、BOOTボタンが背面に隠れてしまうため、いちいち外してBOOTボタンを押さなければいけない。
これは非常に面倒。
ということでもっと簡単にBOOTSELを起動する方法はないか探してみた
Rustでkeyballのファームを作った先人の方のブログを見てみると、どうやらSWDというのを使えば裏から色々弄れるみたい。
が、このボードは正規のProMicroではないため、どうやらそのようなピンは生えていない。
RESETダブルクリックによるBOOTSELの起動
そこでさらに調べてみると、Cのpico-sdkではRESETダブルクリックによるBOOTSEL起動ができるということが分かった。
RESETしてもメモリがリセットされない領域があり、そこに一定時間適当な数字を書き込んでおくことで、2連続RESETされたことを検知する、という仕組みのようだ。
また、Keyballではないが、Rustでキーボードのファームを実装している方によるRust実装も見つけた
ので有り難くこれを使わせてもらう
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_rp::init(Default::default());
unsafe { double_reset::check_double_tap_bootloader(500).await };
...
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);
}
RESETするのに時間がかかるのであまり速くダブルクリックするとうまくリセットできない
「カチッ、カチッ」ぐらいの速度感が良い
OLEDの表示
keyballで使われているOLEDのコントローラはSSD1306というやつで、それ用のRustクレートもある
ただ、残念ながら非同期には対応してないみたい
まあそんなに頻繁に書き換えるものじゃないし良いでしょう
embedded_graphics
ssd1306はembedded_graphicsクレートに対応している。
embedded_graphicsを使えば組み込み機器で文字などを簡単に表示できる
OLEDとはi2cで通信する。
I2C_1の方を使うみたい。
先述の通りノンブロッキングは対応してないのでblockingなi2cを使う
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);
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();
}
}
これでテキストを描画するメソッドができたのであとは使うだけ
let mut display = ssd1306::Ssd1306Display::new(i2c);
display.draw_text("Hello from rust");
これでOLEDにHello from rustという文字が表示されるはず
panic時にOLEDを表示したい
ので、displayをグローバルに置く必要があるが、mutでなければならないので、同期プリミティブを使う必要がある。
// 定義
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ハンドラ内での使い方
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
DISPLAY
.try_lock()
.unwrap()
.as_mut()
.unwrap()
.draw_text("Panic!");
loop {}
}
panicハンドラ内でpanicした時のことはひとまず考えないことにする
キーの読み取り
まず、キーを読み取る部分を作っていく
Keyballの特徴として、Duplex Matrixという方法が採用されている。
そもそもマトリクススキャンのことも知らなかったので上の記事が非常に役に立った。
要するにCOLを順番にHighにしてHighになったROWがどれかがわかれば押されたキーがわかる。
で、Duplex MatrixではROWをHighにしてCOLを調べることもすることで少ない端子で沢山のキーを扱えるということみたい。
できたのがこちら
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;
}
};