Embedded SwiftとRaspberry Pi Picoでキーボードを作る

# Embedded SwiftとRaspberry Pi Picoでキーボードを作る

Appleにより、[Embedded Swiftを使ってRaspberry Pi Picoなどのマイコンボードを動かすサンプル](https://github.com/apple/swift-embedded-examples)が公開されています
ということで、Swiftから[TinyUSB](https://github.com/hathach/tinyusb)を呼んで、Raspberry Pi Picoでキーボードを作ってみました

https://github.com/fuziki/swift-raspberry-pi-pico-examples

ボタンを押下すると、「command + B」を発行します
これにより、一発でXcodeのビルドを実行できます

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">SwiftでRaspberry Pi Picoをキーボードにして、Command + Bのショートカットキー作った！<br>これでXcodeのビルドも一発！ <a href="https://t.co/ITcjDDJMN8">pic.twitter.com/ITcjDDJMN8</a></p>&mdash; ふじき (@fzkqi) <a href="https://twitter.com/fzkqi/status/1822780398876967051?ref_src=twsrc%5Etfw">August 11, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 必要パーツ

| 部品名 | 購入リンク |
| :-- | :-- |
| Raspberry Pi Pico W | https://akizukidenshi.com/catalog/g/g117947/ |
| ブレッドボード 6穴版 | https://akizukidenshi.com/catalog/g/g112366/ |
| ジャンパーワイヤ | https://akizukidenshi.com/catalog/g/g100288/ |
| 抵抗内蔵5mmLED | https://akizukidenshi.com/catalog/g/g117554/ |
| タクトスイッチ| https://akizukidenshi.com/catalog/g/g109827/ |

* 手元で動作確認したパーツの同等品を含みます
* おそらく、[Raspberry Pi Pico](https://akizukidenshi.com/catalog/g/g116132/)でも同様に動作するはずです

# SwiftからTinyUSBを呼んでRaspberry Pi Picoをキーボードにする実装
## Dockerの準備
環境をイケてる感じにしたいので、Dockerでビルドします

```
$ docker build --platform linux/amd64 -t swift-build-env-amd64 .
```

[Raspberry Pi Picoのスタートガイド](https://datasheets.raspberrypi.com/pico/getting-started-with-pico-JP.pdf)を参考に必要なツールをインストールします
Swiftをビルドするために追加でNinjaとPython3が必要でした

```Dockerfile:Dockerfile
FROM swiftlang/swift:nightly-main-jammy

RUN apt-get update && apt-get install -y \
    gcc \
    cmake \
    gcc-arm-none-eabi \
    libnewlib-arm-none-eabi \
    build-essential \
    git \
    ninja-build \
    python3 \
    && apt-get clean

RUN git clone -b master https://github.com/raspberrypi/pico-sdk.git \
    && cd pico-sdk \
    && git submodule update --init

RUN git clone -b master https://github.com/raspberrypi/pico-examples.git

ENV PICO_SDK_PATH=/pico-sdk
ENV PICO_TOOLCHAIN_PATH=/usr/bin/arm-none-eabi-gcc

WORKDIR /workspace

ENTRYPOINT ["/bin/bash"]
```

## TinyUSBを使えるようにする

[swift-embedded-examples](https://github.com/apple/swift-embedded-examples)をベースにTinyUSBを使えるように諸々追加します

## BridgingHeader
`BridgingHeader.h`に必要なヘッダを追加します

```c:BridgingHeader.h
#pragma once

#ifndef CFG_TUSB_MCU
#define CFG_TUSB_MCU OPT_MCU_RP2040
#endif

// for malloc
#include <stdlib.h>

#include "pico/stdlib.h"

// For HID
#include "bsp/board.h"
#include "usb_descriptors.h"

// board_millis
#include "pico/time.h"
static inline uint32_t get_board_millis(void)
{
  return to_ms_since_boot(get_absolute_time());
}
```

## TinyUSBの設定系

[pico-examplesのdev_hid_composite](https://github.com/raspberrypi/pico-examples/tree/eca13acf57916a0bd5961028314006983894fc84/usb/device/dev_hid_composite)からTinyUSBの設定系ファイルをコピペして持ってきます
`tusb_config.h`、`usb_descriptors.c`、`usb_descriptors.h`の3つのファイルです
キーボード以外にも、マウスやゲームパッドの機能も含まれているので、キーボード以外は削除しました

https://github.com/fuziki/swift-raspberry-pi-pico-examples/tree/main/ex01-hid-keyboard

## CMake
コピペしてきた`usb_descriptors.c`もビルドされ、`tusb_config.h`、`usb_descriptors.h`が見つかるように諸々設定します
また、`pico_unique_id tinyusb_device tinyusb_board`などの依存ライブラリを追加しました

```cmake
cmake_minimum_required(VERSION 3.13)
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)

project(swift-app C CXX ASM)
pico_sdk_init()

if(APPLE)
execute_process(COMMAND xcrun -f swiftc OUTPUT_VARIABLE SWIFTC OUTPUT_STRIP_TRAILING_WHITESPACE)
else()
execute_process(COMMAND which swiftc OUTPUT_VARIABLE SWIFTC OUTPUT_STRIP_TRAILING_WHITESPACE)
endif()

add_executable(swift-app)
add_custom_command(
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o
    COMMAND
        ${SWIFTC}
        -target armv6m-none-none-eabi -Xcc -mfloat-abi=soft -Xcc -fshort-enums
        -Xfrontend -function-sections -enable-experimental-feature Embedded -wmo -parse-as-library
        $$\( echo '$<TARGET_PROPERTY:swift-app,INCLUDE_DIRECTORIES>' | tr '\;' '\\n' | sed -e 's/\\\(.*\\\)/-Xcc -I\\1/g' \)
        $$\( echo '${CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES}'             | tr ' '  '\\n' | sed -e 's/\\\(.*\\\)/-Xcc -I\\1/g' \)
        -Xcc -I${CMAKE_CURRENT_LIST_DIR}/usb_descriptors.c
        -import-bridging-header ${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h
        ${CMAKE_CURRENT_LIST_DIR}/Main.swift
        -c -o ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o
    DEPENDS
        ${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h
        ${CMAKE_CURRENT_LIST_DIR}/Main.swift
        ${CMAKE_CURRENT_LIST_DIR}/usb_descriptors.c
)
add_custom_target(swift-app-swiftcode DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o)

target_sources(swift-app PUBLIC ${CMAKE_CURRENT_LIST_DIR}/usb_descriptors.c)

# For search tusb_config.h
target_include_directories(swift-app PUBLIC ${CMAKE_CURRENT_LIST_DIR})

target_link_libraries(swift-app
    pico_stdlib pico_unique_id tinyusb_device tinyusb_board
    ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o
)
add_dependencies(swift-app swift-app-swiftcode)
pico_add_extra_outputs(swift-app)
```

## Swiftの実装
### Mainプログラム
入力ボタンとステータス表示用のLEDの入出力とTinyUSB系を設定し、TinyUSBなどのイベント処理をループ実行するようになっています

```swift
enum Blink: UInt32 {
    case notMounted = 250
    case mounted = 1000
    case suspended = 2500
}
var blink_interval_ms: UInt32 = Blink.notMounted.rawValue

let button_cmd_b_pin: UInt32 = 16
let led_pin: UInt32 = 15

@main
struct Main {
    static func main() {
        board_init() // ボードの初期化
        tusb_init() // TinyUSBの初期化

        gpio_init(button_cmd_b_pin) // 入力ボタンの設定
        gpio_set_dir(button_cmd_b_pin, false)
        gpio_pull_up(button_cmd_b_pin)

        gpio_init(led_pin) // ステータス表示用のLEDの設定
        gpio_set_dir(led_pin, true)

        while true {
            tud_task() // TinyUSBの処理

            hid_task() // キーボードのイベント送信(後述)

            led_blinking_task() // ステータス表示用の更新(後述)
        }
    }
}
```

### USBデバイスのコールバック
USBデバイスのコールバックを受け取ります
シンボル名を指定して呼び出されるようなので、@._cdeclで引っかかるようにしておきます
USBのイベントを受け取るとステータスを更新します
led_blinking_taskでblink_interval_msの間隔でLEDをON/OFFします

```swift
// Invoked when device is mounted
@_cdecl("tud_mount_cb")
func tud_mount_cb() {
    blink_interval_ms = Blink.mounted.rawValue
}

// Invoked when device is unmounted
@_cdecl("tud_umount_cb")
func tud_umount_cb() {
    blink_interval_ms = Blink.notMounted.rawValue
}

// Invoked when usb bus is suspended
// remote_wakeup_en : if host allow us  to perform remote wakeup
// Within 7ms, device must draw an average of current less than 2.5 mA from bus
@_cdecl("tud_suspend_cb")
func tud_suspend_cb(_ remote_wakeup_en: Bool) {
    blink_interval_ms = Blink.suspended.rawValue
}

// Invoked when usb bus is resumed
@_cdecl("tud_resume_cb")
func tud_resume_cb() {
    blink_interval_ms = Blink.mounted.rawValue
}
```

### キーボードの処理
10ミリ秒ごとにキーボード処理を実行します
ボタンが押下されているのを検出すると、`tud_hid_keyboard_report`でキーボードイベントを発行します
Embedded Swiftでは配列を利用できなかったため、mallocでメモリを確保して諸々しています
KEYBOARD_MODIFIER_LEFTGUIで、Commandの装飾をつけています
キーボード系のイベントのコールバックを受け取れるように、こちらでもコールバック系の関数に@._cdeclをつけています
が、今回は特に何もしていません

```swift
var start_ms_for_hid_task: UInt32 = 0
// use to avoid send multiple consecutive zero report for keyboard
var has_keyboard_key: Bool = false

// Every 10ms, we will sent 1 report for each HID profile (keyboard, mouse etc ..)
// tud_hid_report_complete_cb() is used to send the next report after previous one is complete
func hid_task() {
    // Poll every 10ms
    let interval_ms: UInt32 = 10

    if get_board_millis() - start_ms_for_hid_task < interval_ms { return } // not enough time
    start_ms_for_hid_task += interval_ms

    let button_cmd_b: Bool = !gpio_get(button_cmd_b_pin)

    // Remote wakeup
    if tud_suspended() && button_cmd_b {
        // Wake up host if we are in suspend mode
        // and REMOTE_WAKEUP feature is enabled by host
        tud_remote_wakeup()
    } else {
        // Send the 1st of report chain, the rest will be sent by tud_hid_report_complete_cb()

        // skip if hid is not ready yet
        if !tud_hid_ready() { return }

        if button_cmd_b {
            let keycode: UnsafeMutablePointer<UInt8> = malloc(6 * MemoryLayout<UInt8>.size)!
                .assumingMemoryBound(to: UInt8.self)
            memset(keycode, 0, 6 * MemoryLayout<UInt8>.size)

            keycode.advanced(by: 0).pointee = UInt8(HID_KEY_B)
            tud_hid_keyboard_report(UInt8(REPORT_ID_KEYBOARD), UInt8(KEYBOARD_MODIFIER_LEFTGUI.rawValue), keycode)
            free(keycode)

            has_keyboard_key = true
        } else {
            // send empty key report if previously has key pressed
            if has_keyboard_key {
                tud_hid_keyboard_report(UInt8(REPORT_ID_KEYBOARD), 0, nil)
            }
            has_keyboard_key = false
        }
    }
}

// Invoked when sent REPORT successfully to host
// Application can use this to send the next report
// Note: For composite reports, report[0] is report ID
@_cdecl("tud_hid_report_complete_cb")
func tud_hid_report_complete_cb(
    _ instance: UInt8,
    _ report: UnsafeMutablePointer<UInt8>,
    _ len: UInt16
) {}

// Invoked when received GET_REPORT control request
// Application must fill buffer report's content and return its length.
// Return zero will cause the stack to STALL request
@_cdecl("tud_hid_get_report_cb")
func tud_hid_get_report_cb(
    _ instance: UInt8,
    _ report_id: UInt8,
    _ report_type: hid_report_type_t,
    _ buffer: UnsafeMutablePointer<UInt8>,
    _ reqlen: UInt16
) -> UInt16  { 0 }

// Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 0 )
@_cdecl("tud_hid_set_report_cb")
func tud_hid_set_report_cb(
    _ instance: UInt8,
    _ report_id: Int8,
    _ report_type: hid_report_type_t,
    _ buffer: UnsafeMutablePointer<UInt8>,
    _ bufsize: UInt16
) {}
```

### LEDのON/OFF処理
デバイスのステータスに応じて、LEDのON/OFFを切り替えます

```swift
func led_blinking_task() {
    // blink is disabled
    if blink_interval_ms == 0 { return }

    // Blink every interval ms
    if get_board_millis() - start_ms_for_led_blinking < blink_interval_ms { return } // not enough time
    start_ms_for_led_blinking += blink_interval_ms

    gpio_put(led_pin, led_state)
    led_state.toggle()
}
```

# ビルド
ビルドします
ビルドに成功すると、build/swift-app.uf2が作成されています

```
$ export PICO_BOARD=pico
$ export PICO_SDK_PATH='/pico-sdk'
$ export PICO_TOOLCHAIN_PATH='/usr/bin/arm-none-eabi-gcc'

$ cmake -B build -G Ninja .
$ cmake --build build
```

# 配線
GPIOの16番をボタンに、15番をLEDに接続します
お手軽に使うために、抵抗入りLEDを利用しています
普通のLEDを利用する場合は、適切な制限抵抗を入れてください

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/1ed641e5-783b-9b91-bf53-6297730125d6.png)

([wokwi](https://wokwi.com/)で作成しました)
(配線図はRaspbery Pi Picoになっています)

# 動作確認
Raspberry Pi Pico上のBOOTSEL(多分Boot Select)ボタンを押下しながら、PCと接続します
RPI-RP2というUSBデバイスが接続されるので、作成したuf2ファイルをドラッグ&ドロップします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/8efab6cf-e659-c88a-38dc-c75f919ff217.png)

Xcodeを開いて、ブレッドボード上のボタンを押下して、ビルドのショートカットが実行されていれば成功です！

# キーマトリクスを検出する
ブレッドボード上の1スイッチだと少し寂しかったので、マイクロパッドも作成してみました
基板はpro micro用のマイクロパッドです
Raspberry Pi Picoと同じRP2040を採用し、自作キーボードで有名なpro microと互換pinのKB2040を利用しています

GitHubでコード全文公開してます

https://github.com/fuziki/swift-raspberry-pi-pico-examples/tree/main/ex03-kb2040


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Embedded SwiftとKB2040(RP2040のpro micro互換ピンのやつ)でキーボードできた！ <a href="https://t.co/Ezt0jQjh8v">pic.twitter.com/Ezt0jQjh8v</a></p>&mdash; ふじき (@fzkqi) <a href="https://twitter.com/fzkqi/status/1826446614854336739?ref_src=twsrc%5Etfw">August 22, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## パーツリスト

| 部品名 | 購入リンク |
| :-- | :-- |
| KB2040 | https://akizukidenshi.com/catalog/g/g117312/ |
| コンスルー x 2 | https://shop.yushakobo.jp/products/31?variant=37665714405537 |
| 基板 | https://toriten51517765.booth.pm/items/5509485 |
| ダイオード x 6 | https://akizukidenshi.com/catalog/g/g100941/ |
| キースイッチ x 6 | https://shop.yushakobo.jp/collections/all-switches |
| キーキャップ x 6 | https://shop.yushakobo.jp/collections/keycaps |

## キーマトリクスの検出
使った基板はキーマトリクスと呼ばれる配線になっています
2 + 3 = 5個の入出力で 2 x 3 = 6個のスイッチを検出できます (入出力1個分お得！)
これはスイッチ数が多くなればなるほど有利で、例えば9 + 9 = 18個の入出力で 9 x 9 = 81個のスイッチを検出できます

![Scheme-it-export-New-Project-2024-08-28-00-34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/57989660-1c2f-0ff6-d34c-9e7859702652.png)

利用するピンの入出力を設定します

```swift
let left_pin: UInt32 = 8
let center_pin: UInt32 = 7
let right_pin: UInt32 = 6
let upper_pin: UInt32 = 2
let bottom_pin: UInt32 = 3

func setInput(pin: UInt32) {
    gpio_init(pin)
    gpio_set_dir(pin, false)
    gpio_pull_up(pin)
}

func setOutput(pin: UInt32) {
    gpio_init(pin)
    gpio_set_dir(pin, true)
}

setInput(pin: left_pin)
setInput(pin: center_pin)
setInput(pin: right_pin)

setOutput(pin: upper_pin)
setOutput(pin: bottom_pin)
```

同時押しには対応せず、上段の左端から押下されているか順番にチェックして、最初にヒットしたスイッチと対応するキーを返しています

```swift
func getPressKey() -> UInt8? {
    // 上段の検出
    gpio_put(upper_pin, false)
    gpio_put(bottom_pin, true)
    sleep_us(30)
    if !gpio_get(left_pin) { return UInt8(HID_KEY_S) }
    else if !gpio_get(center_pin) { return UInt8(HID_KEY_W) }
    else if !gpio_get(right_pin) { return UInt8(HID_KEY_I) }

    // 下段の検出
    gpio_put(upper_pin, true)
    gpio_put(bottom_pin, false)
    sleep_us(30)
    if !gpio_get(left_pin) { return UInt8(HID_KEY_F) }
    else if !gpio_get(center_pin) { return UInt8(HID_KEY_T) }
    else if !gpio_get(right_pin) { return UInt8(HID_KEY_SPACE) }

    return nil
}
```


# まとめ
Embedded SwiftとRaspberry Pi Picoでキーボードを作ってみました
自作キーボードをプログラムから作成したなと思っていた、iOSアプリのエンジニアさんなどいらっしゃいましたら、ぜひチャレンジして頂けると嬉しいです