4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-08-11

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

Appleにより、Embedded Swiftを使ってRaspberry Pi Picoなどのマイコンボードを動かすサンプルが公開されています
ということで、SwiftからTinyUSBを呼んで、Raspberry Pi Picoでキーボードを作ってみました

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

必要パーツ

部品名 購入リンク
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でも同様に動作するはずです

SwiftからTinyUSBを呼んでRaspberry Pi Picoをキーボードにする実装

Dockerの準備

環境をイケてる感じにしたいので、Dockerでビルドします

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

Raspberry Pi Picoのスタートガイドを参考に必要なツールをインストールします
Swiftをビルドするために追加でNinjaとPython3が必要でした

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をベースにTinyUSBを使えるように諸々追加します

BridgingHeader

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

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からTinyUSBの設定系ファイルをコピペして持ってきます
tusb_config.husb_descriptors.cusb_descriptors.hの3つのファイルです
キーボード以外にも、マウスやゲームパッドの機能も含まれているので、キーボード以外は削除しました

CMake

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

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などのイベント処理をループ実行するようになっています

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します

// 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をつけています
が、今回は特に何もしていません

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を切り替えます

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

(wokwiで作成しました)
(配線図はRaspbery Pi Picoになっています)

動作確認

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

image.png

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

キーマトリクスを検出する

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

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

パーツリスト

部品名 購入リンク
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

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

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)

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

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アプリのエンジニアさんなどいらっしゃいましたら、ぜひチャレンジして頂けると嬉しいです

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?