Slide 1

Slide 1 text

市販E-Readerを乗っ取れ 〜Embedded Swiftで電子ペーパーガジェットを制御する〜 iOSDC Japan 2026 / trickart

Slide 2

Slide 2 text

自己紹介 trickart • エセソフトウェアエンジニア • 計算機科学卒ではないため • 仕事では勘でiOSアプリを作っている • 趣味で電子工作などをしている • 今回初めてまともに計算機科学に向 き合った

Slide 3

Slide 3 text

このE-Readerで 動いている ファームウェア ほとんどSwiftで書きました

Slide 4

Slide 4 text

今日のゴール: 電源投入から「最初の1ピクセル」まで 普段のiOSアプリ開発ではなかなか見ないところを深堀りしていきます • ランタイム依存を自前で用意する • 電源投入からmain()まで • 最初の1ピクセル • MMIOを使いSPIでE-Inkパネルを駆動する →なんか絵が出るまでめっちゃ長い!!地味!!

Slide 5

Slide 5 text

今日話さないこと E-Ink表示とは関係ないところはバッサリカットします • FATファイルシステム • PNG/JPEGデコーダ • フォント描画

Slide 6

Slide 6 text

前提: 何を何の上で動かすのか • Embedded Swift製自作ファームウェアを • Xteink X4の上で

Slide 7

Slide 7 text

きっかけ Xteink X4というガジェット • にわかに流行っているらしいことを 知る • カスタムファームウェアが存在して いるらしいことを知る • チップ次第でSwiftで動かせるので は…?

Slide 8

Slide 8 text

とりあえずスペックを調べる スライド書いたあとに公式サイトからページが消えてた • 4.3インチ • 200ppi • 77g • WiFi/Bluetooth • Type-C充電ポート 細かいシステムの詳細はわからず…

Slide 9

Slide 9 text

分解画像を見ると: ESP32-C3が載っていた 出典: https://min.news/en/digital/3d78670f66f36741698fd086320d6488.html

Slide 10

Slide 10 text

ESP32-C3とは Espressif Systems社製SoC • RISC-V 32bitシングルコア 160MHz • RISC-V: オープンソースなCPUアーキテクチャ • 400KB SRAM

Slide 11

Slide 11 text

いけるのでは…? Embedded Swiftは現状以下のプラットフォームで動く • Arm(ざっくり。動かないのもある) • x86/x86̲64 • Web Assembly • RISC-V ← これ!!

Slide 12

Slide 12 text

Embedded Swiftとは まさにESP32-C3向け • マイクロコントローラ向けに機能制限されたSwift • AppleもSecure Enclaveプロセッサで使っている

Slide 13

Slide 13 text

Embedded Swiftで使えないもの キツいようなキツくないような…? • Foundation • Date • URLSession • Codable • Mirror

Slide 14

Slide 14 text

Embedded Swiftで使えるもの • struct • class • enum • Optional 基本的な言語機能は使える!

Slide 15

Slide 15 text

ただしXcode同梱のSwiftでは使えない SwiftlyでOSSツールチェーンを入れる • 組み込み向けの諸々がオミットされている • swift.orgで配布されているツールチェーンが必要 • Swiftlyでインストール可能 • $ swiftly install 6.3.0 • 使うツールチェーンは ̀TOOLCHAINS̀ 環境変数で切り替え可能 • $ TOOLCHAINS=org.swift.630202603201a swift build

Slide 16

Slide 16 text

開発環境: テキストエディタ+ターミナル ̀swift̀ コマンドが実行できれば何でも良い • テキストエディタは好きなものを • VSCode, Vim, Emacs, etc… • もちろんXcodeを使っても良い(というか使っていた) • ビルドボタンとかは使えないけど補完とかはよく効く

Slide 17

Slide 17 text

Package.swift: Embedded向けターゲット構成 .enableExperimentalFeature(“Embedded”) が重要 // swift-tools-version: 6.3 import PackageDescription let package = Package( name: "InkedFeather", products: [ .executable(name: "Application", targets: ["Application"]), ], targets: [ .executableTarget( name: "Application", swiftSettings: [ .enableExperimentalFeature("Embedded"), // ←これ!! ) ] ), ]

Slide 18

Slide 18 text

ビルドパイプライン: swift buildから実機まで esptoolは使わず、イメージ変換も書き込みもSwiftスクリプト • swift build → ELF(リンカスクリプトで配置) • elf2image.swift → ESPイメージ(.bin)へ変換 • write- ash.swift → USBシリアル経由でFlashへ • ̀make ash̀ 一発で実機起動 macOS 側 ELF elf2image.swift write-flash.swift + toolset.json -Osize -wmo -nostdlib esp32c3.ld で配置 ESPイメージ .bin へ変換 シリアル経由で書き込み fl swift build fl Package.swift 実機で起動

Slide 19

Slide 19 text

Chapter1: ランタイム依存を自前で用意する

Slide 20

Slide 20 text

iOSでは: Appleが全部やってくれている main()を書いてビルドボタンを押すだけ • @main を書けばアプリが起動する • ランタイム関数は当然のようにlibSystemにいる • 起動前の準備(dyld、ランタイム初期化)は見たことすらない • 「動く前提」で全部揃っている

Slide 21

Slide 21 text

ベアメタルでは: 誰もやってくれない OSもlibcもない。あるのはCPUとメモリと仕様書だけ iOSアプリ このプロジェクト アプリの Swift コード アプリの Swift コード UIKit / Foundation 自作ドライバ(SPI / GPIO / e-ink / SD) Swiftランタイム / libswiftCore 自作ランタイム (malloc / memcpy / SoftFloat) dyld / libSystem(malloc など) ここから下は OSが用意 してくれる 自作ブートローダ(Swift) XNU カーネル ROMブートローダ(マスクROM・固定) ハードウェア ハードウェア(ESP32-C3) 層の数はほぼ同じ。違いは「誰が書いたか」だけ 自分で書く 必要あり

Slide 22

Slide 22 text

実はそもそもビルドが通らない! TOOLCHAINS=org.swift.630202603201a swift build --triple riscv32-none-none-eabi --product Application [1/1] Planning build Building for debugging... error: link command failed with exit code 1 (use -v to see invocation) clang: warning: argument unused during compilation: '-F/Applications/Xcode.app/ Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks' [Wunused-command-line-argument] ld.lld: error: cannot open crt0.o: No such file or directory ld.lld: error: cannot open /Users/trick/Library/Developer/Toolchains/swift-6.3RELEASE.xctoolchain/usr/lib/clang/21/lib/riscv32-none-none-eabi/ libclang_rt.builtins.a: No such file or directory ld.lld: error: unable to find library -lc clang: error: linker command failed with exit code 1 (use -v to see invocation) [10/11] Linking Application make: *** [build] Error 1 • crt0.oとかlibclang̲rt.biltins.aがないらしい • ̀-nostdlib̀ をつけるとそれ無しでビルドをしてくれるらしい

Slide 23

Slide 23 text

toolset.json: 細かいフラグをまとめて渡せる $ swift build --toolset toolset.json { "schemaVersion": "1.0", "swiftCompiler": { "extraCLIOptions": [ "-Xfrontend", "-disable-stack-protector", "-use-ld=lld", "-nostartfiles", "-Xlinker", "-nostdlib", "-Xclang-linker", "-nostdlib", "-Xlinker", "-static", "-wmo", "-Osize", "-Xlinker", "-e", "-Xlinker", "_start" ] }, "linker": { "extraCLIOptions": [ "-T", "linker/esp32c3.ld", "--gc-sections" ] } }

Slide 24

Slide 24 text

̀-nostdlib̀ でリンクするとそれはそれで怒られる 君たちは一体…? TOOLCHAINS=org.swift.630202603201a swift build --triple riscv32-none-none-eabi --toolset toolset.json --product Application // 略 ld.lld: error: undefined symbol: memset // 略 ld.lld: error: undefined symbol: memcpy

Slide 25

Slide 25 text

未定義シンボル: memset/memcpy これがランタイム依存 • メモリ関係の関数 • 本来であればlibcとかに含まれている • ̀-nostdlib̀ とかしたせい • メモリを使うなら必要不可欠

Slide 26

Slide 26 text

足りないなら作ればいいじゃない: Swiftで実装 案外シンプルな実装 @c(memset) @inline(never) public func memset(_ dest: UnsafeMutableRawPointer, _ value: Int32, _ count: Int) -> UnsafeMutableRawPointer { let byte = UInt8(truncatingIfNeeded: value) let ptr = dest.assumingMemoryBound(to: UInt8.self) for i in 0.. UnsafeMutableRawPointer { let d = dest.assumingMemoryBound(to: UInt8.self) let s = src.assumingMemoryBound(to: UInt8.self) for i in 0..

Slide 27

Slide 27 text

今度こそと思いきや: posix̲memalign/free 君たちも一体…? ld.lld: error: undefined symbol: posix_memalign ld.lld: error: undefined symbol: free

Slide 28

Slide 28 text

なぜSwiftがposix̲memalignを要求するのか posix̲memalign malloc(ヒープ確保) • シンプルにstructをつかうだけならスタックで完結する、が… • ヒープ必須の機能がある • class / closure / Array / String / indirect enum • ARCで数えるのもヒープ上の参照の数 • freeはアロケートしたメモリを解放する関数 final class Foo {} let foo = Foo() // この1行の裏で swift_allocObject → posix_memalign

Slide 29

Slide 29 text

mallocは自作する: free-listアロケータ 教科書(Knuth著)に出てくるシンプルなメモリアロケータ • 方式はfree-list + rst- t (約200行) • free時は隣の空きブロックと即結合(boundary tag) 使用中ブロック 空きブロック ヘッダ 4B:size|使用中=1 ヘッダ 4B:size|使用中=0 nextFree 4B (フリーリストの次の空きへ) ペイロード (malloc が返す領域) (未使用領域) フッタ 4B:size のコピー フッタ 4B:size のコピー fi fi size を先頭(ヘッダ)と末尾(フッタ)の両方に持つ = boundary tag。 最小ブロックは ヘッダ4 + nextFree4 + パディング4 + フッタ4 = 16B 次の空きブロックへ

Slide 30

Slide 30 text

400KB SRAMの使い道 低アドレスにヒープ、高アドレスにスタック。教科書通りの配置 ICache 16KB(アプリからは使えない) 0x3FC8_0000 .data(初期値付きグローバル) .bss(ゼロ初期化グローバル) 400KB SRAM • .data / .bss: グローバル変数 • heap: 残り全部(約350KB) • stack: 16KB、DRAM上端から下に伸びる • 最上端14KBはマスクROMが使うので不可侵 • フレームバッファ48KBはheapから確保 ヒープ(_heap_start 〜 _heap_end) 約353KB フレームバッファ 48KB (800 × 480 × 1bit) ← malloc で確保 スタック 16KB(_start で sp を設定・低アドレスへ 伸びる) 0x3FCD_8700 0x3FCD_C700 ROM予約 14KB(ROMブートスタック+ROMデータ) 0x3FCE_0000 ※ .data / .bss の実サイズはビルドで変動(縮尺は概念図)。スタックは慣習どおり高ア ドレス側

Slide 31

Slide 31 text

これでビルドが…通った! ̀-nostdlib̀ で外した物をSwiftで書いただけともいう TOOLCHAINS=org.swift.630202603201a swift build --triple riscv32-none-none-eabi --toolset toolset.json --product Application [1/1] Planning build Building for debugging... [9/9] Linking Application Build of product 'Application' complete! (18.41s)

Slide 32

Slide 32 text

実は公式ドキュメントに全部書いてあった その名も「External dependencies」 • これまで作ったものは公式で「用意してね」と言っているものだった • posix̲memalign/free • memset/memcpy • ̲̲stack̲chk̲* → ̀-disable-stack-protector̀ で回避 • ↑toolset.jsonに書いた謎フラグの正体 • 最初に読んでいれば…!

Slide 33

Slide 33 text

…と思ったら書いてなかったもの: ̲̲ashldi3 compiler-rt(libclang̲rt.builtins.a)の中身も自前で用意する • 64bitのシフト演算はRV32では1命令で出来ない • コンパイラは ̲̲ashldi3 という補助関数の呼び出しに変換する • 本来はlibclang̲rt.builtins.aにいる(最初のエラーの「ない」やつ) • UInt64をシフトした瞬間にリンクエラー ld.lld: error: undefined symbol: __ashldi3 @c(__ashldi3) @inline(never) public func ashldi3(_ a: UInt64, _ b: Int32) -> UInt64 { let (lo, hi) = split64(a) if b >= 32 { return make64(lo: 0, hi: lo << (b &- 32)) } else if b == 0 { return a } else { return make64(lo: lo << b, hi: (hi << b) | (lo >> (32 &- b))) } }

Slide 34

Slide 34 text

浮動小数点も同じ: FPUがない Floatを使うと ̲̲addsf3 / ̲̲mulsf3 / ̲̲ oatsisf … がまとめて要求される • ESP32-C3(RV32IMC)には浮動小数点ユニットがない • 1.0 + 2.0 も全部ソフトウェアの関数呼び出しになる • IEEE 754の加減乗除・比較・変換をSwiftで実装(約1500行) • JPEGデコーダ用に用意したが固定小数点に書き直し、今は使っていない // SoftFloat/SoftFloat.swift @c(__addsf3) @inline(never) public func addsf3(_ a: Float, _ b: Float) -> Float { /* 符号・指数・仮数を分解して足す */ } @c(__mulsf3) @inline(never) public func mulsf3(_ a: Float, _ b: Float) -> Float { /* 仮数の積は UInt64 で 48bit */ } fl // SoftFloat/SoftDouble.swift: Double版も一式

Slide 35

Slide 35 text

Chapter2: 電源投入からmain()まで

Slide 36

Slide 36 text

ESP32-C3のブートは3段構成 2nd Stage Bootloaderというやつがいる ① ROM Bootloader マスクROM内蔵 書き換え不可 ロードして実行 ② 2nd Stage Bootloader flash 0x0 ロードして jump ③ Application アプリパーティション

Slide 37

Slide 37 text

2nd Stage Bootloader 最初はESP-IDFのバイナリを借りていた • Espressif IoT Development Framework(ESP-IDF)にすでにブートローダー のコードがある • C言語製 • アプリケーションはEmbedded Swiftで書けるようになった → 2nd Stage BootloaderもSwiftで書けるのでは? →やってみよう!!

Slide 38

Slide 38 text

自作ブートローダーの全体像 アプリを動かすために色々な準備をする 1. チップを起こす clearBSS() disableWatchdogs() 4つのWDTを止める configurePLL() CPU 160MHz へ 2. Flash を読めるようにする 3. ロードして跳ぶ configureFlashSPI() loadRAMSegments() DIO 読み出し設定 IRAM / DRAM へコピー findAppPartition() setupFlashMMU() 0x8000 のテーブルを読む IROM / DROM をマップ readImageHeader() entry() magic 0xE9 を確認 アプリへ jump

Slide 39

Slide 39 text

BSS初期化 BSS: ゼロで初期化されてる変数を置く領域 • 起動時はゴミ値だったりするので全部ゼロで初期化する • 初期化しないと意味不明な挙動になったりする(1敗) @_extern(c, "_sbss") nonisolated(unsafe) var _sbss: UInt8 @_extern(c, "_ebss") nonisolated(unsafe) var _ebss: UInt8 func clearBSS() { let start = linkerSymbolAddress(&_sbss) let end = linkerSymbolAddress(&_ebss) guard let ptr = UnsafeMutablePointer(bitPattern: start) else { return } var i = 0 while start &+ UInt(i) < end { ptr[i] = 0 // ループで全部ゼロを代入 i &+= 1 } }

Slide 40

Slide 40 text

BSSクリアがmemsetになって無限再帰する LLVMは「ゼロ埋めループ」を見つけると memset 呼び出しに置き換える • ホストOSなら嬉しい最適化。libcのmemsetは速いので • 自作memsetの中のforループもmemset呼び出しに変換されうる • memsetがmemsetを呼んでスタックオーバーフロー • 対策: MemoryPrimitivesターゲットだけ変換を禁止 // Package.swift (MemoryPrimitives ターゲット) swiftSettings: [ .enableExperimentalFeature("Embedded"), .enableExperimentalFeature("Extern"), .unsafeFlags(["-Xllvm", "-disable-loop-idiom-memcpy"]), ]

Slide 41

Slide 41 text

PLL設定: 40MHz → 480MHz PLL: クロックを逓倍して高い周波数を作る回路 • リセット直後はXTALの40MHzのまま(CPUのスペックの1/4) • マスクROM内蔵の関数で設定 XTAL 40MHz ×12 ÷3 CPU 160MHz ÷6 APB(ペリフェラルバス) 80MHz BBPLL 480MHz

Slide 42

Slide 42 text

マスクROMの関数をSwiftから呼ぶ • ROMには工場出荷時から関数が焼き込まれている(アドレス固定) • ESP-IDFの ̀esp32c3.rom.ld̀ にアドレス一覧がある • リンカスクリプトで名前とアドレスを紐づけ、 ̀@̲extern(c)̀ で呼ぶ /* linker/esp32c3.ld */ PROVIDE ( rom_i2c_readReg = 0x4000195c ); @_extern(c, "rom_i2c_writeReg") @discardableResult func romI2CWrite(_ block: UInt32, _ hostid: UInt32, _ regAddr: UInt32, _ data: UInt32) -> UInt32 romI2CWrite(I2C_BBPLL, 0, 4, 0x6B) // PLL を 480MHz モードに

Slide 43

Slide 43 text

ウォッチドッグタイマー(WDT)を黙らせる WDT: ハング時に自動リセットさせる保険のタイマー • リセット直後から勝手に動いている→何とかする必要あり • 今回は簡単のため4つとも無効化 • 本当はfeedするようにしたほうがよいが… • 設定には保護解除の合言葉 ̀0x50D83AA1̀ が必要 • 暴走したプログラムに誤って無効化されないようロックがかかっている regStore(base + 0x64, 0x50D8_3AA1) // 合言葉を書いて保護解除 // ここで WDT を無効化 regStore(base + 0x64, 0) // 別の値を書いて再ロック

Slide 44

Slide 44 text

パーティションテーブルを読む パーティションテーブル=目次 0x0 • 場所は ̀0x8000̀ 固定 • 1エントリ32バイトの配列 0x8000 パーティションテーブル 0x9000 nvs 20KB 0xE000 otadata 8KB 0x10000 • magicバイト/種別/o set/サイズ/ラベル • ここからapp0を探す 2nd stage bootloader(Swift製・自作) app0 6.25MB ← 自作ファームウェア本体はここ 0x650000 app1 6.25MB(OTA予備・未使用) • app0の先頭24バイトがイメージヘッダ 0xC90000 spiffs 3.375MB • magicバイト/セグメント数/エントリー ポイント 0xFF0000 coredump 64KB ff 16MB SPI Flash(縮尺は非等倍) ① テーブルを読む ② app0 を見つけてロード

Slide 45

Slide 45 text

セグメントをIRAM/DRAMへロードする I=インストラクション(命令), D=データ アプリイメージ(flash 0x10000〜) SRAM(実体をコピー) イメージヘッダ 24B magic 0xE9 / セグメント数 / entry DRAM:.data の実体 コピー .data セグメント (宛先 0x3FC8_xxxx) IRAM:trap_handler コピー .trap_handler (宛先 0x4037_Cxxx) Flashに置いたまま(コピーしない) .text (宛先 0x4200_xxxx) MMUでマップするだけ .text → IROM にマップ 0x4200_0020 .rodata (宛先 0x3C40_xxxx) MMUでマップするだけ .rodata → DROM にマップ 0x3C40_0020 宛先アドレスを見て、SRAM宛なら実体コピー、Flash領域宛ならMMUでマップ。この分岐がブートローダの仕事

Slide 46

Slide 46 text

Flash MMU: Flashを ̀0x42000000̀にマップする SRAMに置かないけどCPUから見えるようにする SPI Flash 16MB(物理) CPU仮想アドレス空間 bootloader(0x0) パーティションテーブル (0x8000) DROM(マップ先) 0x3C40_0020 .rodata が見える app0(自作FW / 0x10000) .text(コード) Flash MMU 128エントリ 1エントリ = 64KBページ テーブル自体もMMIO (0x600C_5000) IROM(マップ先) 0x4200_0020 .text が見える .rodata(定数・文字列) app1 / spiffs / … SRAM / ペリフェラル … CPUはマップされた領域を普通のアドレスとして読むだけ。実体はFlashにあり、ICache / DCache 経由で読まれる

Slide 47

Slide 47 text

IRAMとDRAMは同じSRAMの別名 物理的なSRAMは400KBが1つだけ。入口が2つある • IRAM(0x4037̲C000〜) = 命令フェッチ用バス経由 • DRAM(0x3FC8̲0000〜) = データ読み書き用バス経由 • 同じバイトが2つのアドレスから見える(ハードリンクのようなもの) • 容量は共有: IRAMに置いた分だけDRAMが減る IRAM 0x4037_C000 I-bus(命令フェッチ) トラップハンドラを配置 同じ場所を指す 物理SRAM 400KB 実体は1つ 同じ場所を指す DRAM 0x3FC8_0000 D-bus(データ読み書き) .data / .bss / stack / heap 同じバイトが2つのアドレスから見える。CPUがどちらのバスでアクセスするかの違いだけ。 リンカスクリプトでは iram / dram を別 MEMORY として書くが、置き場所は同じ 400KB を分け合っている

Slide 48

Slide 48 text

リンカスクリプトで配置を決める • 「何をどのアドレスに置くか」の指示書 • ここまで出てきたIRAMとかが勢揃い MEMORY { iram (RWX) : ORIGIN = 0x4037C000, LENGTH = 0x60000 dram (RW) : ORIGIN = 0x3FC80000, LENGTH = 0x60000 irom (RX) : ORIGIN = 0x42000020, LENGTH = 0x400000 - 0x20 /* Flashをマップ */ drom (R) : ORIGIN = 0x3C400020, LENGTH = 0x400000 - 0x20 /* Flashをマップ */ } SECTIONS { .trap_handler : { ... } > iram } /* 物理SRAM(I-bus) */ .text : { ... } > irom .rodata .data / .bss : { ... } > drom : { ... } > dram /* 同じSRAM(D-bus) */ /* RAMで実行したいコード */ /* コードはFlashに置いたまま */

Slide 49

Slide 49 text

アプリのエントリーポイントへjumpする イメージヘッダーから読み取ったエントリーポイントのアドレスを入れてアプリの処理を呼び出す let entry = unsafeBitCast( UInt(header.entryPoint), to: (@convention(c) () -> Void).self ) entry()

Slide 50

Slide 50 text

jump先にいるのは ̲start: 3行のアセンブラ toolset.jsonの ̀-e ̲start̀ の正体。crt0.oの手書き代替 • 本来crt0.oが提供するエントリーポイントを自前で書く • スタックポインタを自分の領域に向けてmainを呼ぶだけ • スタックがない状態で動く=Swiftでは書けない .section .text._start, "ax" .globl _start _start: la sp, _stack_top /* スタックを自分の領域に向ける */ 1: call main /* Swift の main() へ */ j /* main は帰ってこない */ 1b

Slide 51

Slide 51 text

Chapter2まとめ: main()に到達した (長すぎない…?大丈夫これ?) • ブートローダー = とにかく色々準備するやつ • ようやく画面を描く処理に到達…!

Slide 52

Slide 52 text

Chapter3: 最初の1ピクセル

Slide 53

Slide 53 text

メモリに絵を描いても画面には映らない メモリはただのメモリ • 絵のデータをE-Inkパネルまで送り届ける仕組みが必要 • どうにかしてSwiftのコードからハードウェアに触る必要がある • 今のところメモリの読み書きしか出来ない • じゃあどうやって…?

Slide 54

Slide 54 text

MMIO(Memory Mapped I/O)とは 特定アドレスへの読み書きが周辺機器への操作になる CPUアドレス空間 0x3C40_0000 ペリフェラルの一部 DROM UART0 0x3FC8_0000 DRAM 0x4200_0000 IROM 0x6000_0000〜 ペリフェラル(MMIO) 0x6000_0000 GPIO 0x6000_4000 IO_MUX 0x6000_9000 SPI2 0x6002_4000 GPIO の出力レジスタに 1 を書き込む(ただの store) ピンが HIGH になる Flash MMUテーブル 0x600C_5000 特定アドレスへの load / store が、そのまま周辺機器の操作になる。OSもドライバAPIもない

Slide 55

Slide 55 text

素朴な書き方: UnsafeMutablePointer 直叩き やってることの根本の根本はこれだけ • 一応これで動く • が、2つ問題がある // GPIO4 を HIGH にする(GPIO_OUT_W1TS レジスタ = 0x6000_4008) UnsafeMutablePointer(bitPattern: 0x6000_4008)!.pointee = 1 << 4

Slide 56

Slide 56 text

直叩きの2つの問題 意味不明な上に処理が実行されないかもしれない(!?!?) • ̀0x6000̲4008̀ に ̀1 << 4̀ をストア…意味不明! • 全部のMMIOでアドレスを覚えてミスなく書く必要がある • あとから読んだときも意味不明!! • 理論上可能だけど人間には無理!! • コンパイラには「ただのメモリ」に見えるので最適化で消えうる • 「おっ、ここ無駄に2回も書き込んでるな。1回にしよ」→動かなくなる

Slide 57

Slide 57 text

swift-mmioで解決!…と思いきや 巨大SVDを食わせるとビルドが激重に… • Apple公式のMMIOを簡単にするライブラリ • APIと機能はよく出来ていたがビルドがめちゃ遅くなる • 自作ツールで同じようなAPIのコードを生成するように方針転換

Slide 58

Slide 58 text

Before: 直叩き / After: 自作ツール わかりやすくなった…よね?(少なくとも確実に実行される) // GPIO4 を HIGH にする // GPIO4 を HIGH にする UnsafeMutablePointer( bitPattern: 0x6000_4008 )!.pointee = 1 << 4 gpio.out_w1ts.write { $0.raw.storage = 1 << 4 } • アドレスがただの数字 • アドレスを手打ちしなくて済む • 最適化で読み書きが消えうる • 中身はVolatileMappedRegister • 最適化で消えない!

Slide 59

Slide 59 text

SPIでE-Inkを制御する SPI: チップ同士をつなぐシリアル通信 • クロックに合わせて1bitずつ送るだけの超シンプルな規格 • SCLK/MOSI/MISOの3本は全デバイスで共有、CSだけ相手ごとに1本 • CSをLOWにしたデバイスだけ応答する SCLK / MOSI / MISO 共有バス3本 ESP32-C3 (マスタ) e-ink (SSD1677) CS(e-ink用) CS(SD用) SDカード

Slide 60

Slide 60 text

SPI2をマスタとして初期化する やることはレジスタ設定の積み重ね: クロック供給→ピン配線→モード→速度 • 周辺機器にもクロックを供給しないと動かない • GPIO Matrixで「SPI2の信号をどのピンに出すか」を配線する • 通信速度はAPB 80MHzを8分周して10MHz • ハードウェアCSは全部オフ→誰と話すかはGPIOで自分で決める system.perip_clk_en0.modify { $0.raw.storage = $0.raw.storage | (1 << 6) } sclkPin.setFunction(output: 63) // GPIO Matrix: FSPICLK_OUT → GPIO8 mosiPin.setFunction(output: 65) // GPIO Matrix: FSPID_OUT → GPIO10 spi2.clock.write { $0.raw.storage = ... } // 80MHz ÷ 8 = 10MHz spi2.misc.write { $0.raw.storage = 0x3F } // HW CS 全無効(CSはGPIOで手動) // SPI2 にクロック供給

Slide 61

Slide 61 text

パネルを起こす: リセットと初期化コマンド SSD1677: Xteink X4のE-Ink Display Driver • 電源投入直後のSSD1677は状態が不明→まず既知の状態に戻す • RSTピンをLOW→ソフトリセットコマンド→BUSY待ち • データシート通りに設定コマンドを流し込む • 温度センサ設定/ブースター設定/ドライバ出力制御/RAMウィンドウと書き 込み位置 • 正直よくわからんがデータシートやサンプルコードを信じて書く

Slide 62

Slide 62 text

SSD1677と話す: DCピンとBUSYピン SSD1677にはSPI以外のピンがある • SPIは「バイトを送る」ことしか出来ない→DCピンでコマンド/データを区別 • E-Inkは書き換えに時間が秒単位でかかる→完了はBUSYピンを監視 DC = LOW ならコマンド、HIGH ならデータ / 完了は BUSY が LOW に戻るまで待つ -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CS DC MOSI 0x24 フレームバッファ 0x20 BUSY CPU コマンド データ送信 コマンド BUSY 待ち CS・DC は GPIO で手動制御 / BUSY は HIGH = 駆動中。5 秒タイムアウト付き 次へ 16

Slide 63

Slide 63 text

フレームバッファを用意する フレームバッファ: 画面の表示内容を保持するメモリ領域 • パネルは本来横長800x480だ がXteink X4は縦で使っている パネルネイティブ 800 × 480 1行 = 800ピクセル = 100バイト 1行 • 座標を転置して書き込む • 48KB(SRAMの約12%) b7 • 舐めてたら普通にヒープ使い きった(1敗) b1 b0 1バイト = 横8ピクセル。MSB(b7)が左端 白、0 = 黒(0xFF で塗りつぶし = 白背景) / 100バイト × 480行 = 48,000バイト = 48KB(400KB SRAM の約12%) 階調なし1bit。だからこそ 400KB SRAM に収まる b6 b5 b4 b3 b2 1=

Slide 64

Slide 64 text

処理の流れ: 転送→Display Update Sequence→表示 48KBのビット列を64Byteずつ750回に分けて送る ESP32-C3 SSD1677 0x24 Write RAM(フレームバッファ 48KB) 0x22 Display Update Control 2 0x20 Master Activation BUSY = HIGH(パネル駆動中) BUSY ピンを待つ 画面が書き換わる BUSY = LOW ESP32-C3 SSD1677

Slide 65

Slide 65 text

デバッグ手段: print()をUSBに繋ぐ ESP32-C3はUSB Serial/JTAGを内蔵。USB-Cを挿すだけでシリアルポートが生える • Xcodeのコンソールもデバッガもない。まず「どこまで動いたか」が欲しい • 公式ドキュメント: print()を使うなら putchar を用意してね • USBコントローラの送信FIFOに1バイト書く putchar をSwiftで実装 @c(putchar) @discardableResult public func putchar(_ c: Int32) -> Int32 { if c == 0x0A { _ = usbFifoWrite(0x0D); _ = usbFifoWrite(0x0A); usbFlush() } else { _ = usbFifoWrite(UInt8(truncatingIfNeeded: c)) // USB送信FIFOへ1バイト } return c } print("boot OK”) // いつもの print がそのまま動く

Slide 66

Slide 66 text

実装できた!→全く映らず… どういうことだってばよ… • 初期化コマンドを送っても画面は公 式FWの表示のまま無反応 • SPIの設定もコマンド列も合ってる はず…なぜ??? • 一発で正しい画像が出なくてもせめ て画面が変わってくれないと何もわ からん… 画像はあとから撮ったイメージです

Slide 67

Slide 67 text

原因判明: ピンがJTAG割り当てされてた JTAG: デバッグ用のインターフェース • GPIO4~7は起動直後、JTAG機能に割り当てられている • E-Inkの制御線3本がすべてこれに割り当てられていた • DC = GPIO4 / RST = GPIO5 / BUSY = GPIO 6 • IO̲MUXで ̀MCU̲SEL = 1̀ にするまで操作が全く無視される io_mux.gpio[number].modify { $0.raw.storage = ($0.raw.storage & ~(0x7 << 12)) | (1 << 12) } // MCU_SEL = 1

Slide 68

Slide 68 text

修正、そして… この瞬間が一番気持ちいい…

Slide 69

Slide 69 text

Demo

Slide 70

Slide 70 text

まとめ: Embedded Swiftで書けたこと Xteink X4の自作ファームウェアの9割以上 • ランタイム依存 • bootloader • アプリケーションのほとんど • E-Inkドライバ • ボタン監視 • etc…

Slide 71

Slide 71 text

Embedded Swiftで書けなかったこと アセンブラが2ファイルだけある ̲̀start̀ (sp設定) スタックがない状態でSwift関数は動かせない ベクタテーブル 4バイト境界にジャンプ命令を並べる生の命令列 割り込みハンドラ レジスタの退避/復元と ̀mret̀ 命令 CSRアクセサ ̀csrr̀ / ̀csrẁ 命令を直接出せない Embedded Swiftが進化すればあるいは…?

Slide 72

Slide 72 text

なぜCではなくSwiftなのか Swiftしか書けな…そこにSwiftがあったから • Embedded Swiftでどこまでできるか挑戦してみたかった • 結果としてはアセンブラがどうしても消せなかったがだいたい満足 • Embedded Swiftに興味があった • もともと電子工作とかする人間なので

Slide 73

Slide 73 text

iOSアプリ開発者にとって何が変わったか? まぁぶっちゃけあまり役立たないかも • OS/システムに委ねている処理の理解度が上がる • 計算機科学が身近に感じられる • 役には立たない…?でも楽しい!! ← これ重要

Slide 74

Slide 74 text

Thank You!! URL等 • trickart/InkedFeather • Embedded Swift language restrictions • Running Swift without an OS • Managing external dependencies • kishikawakatsumi/tryswift2026 • Getting Started with Embedded Swift Programming from ¥0

Slide 75

Slide 75 text

宣伝: 11月にSaitama.swift #1を開催します 所沢市民文化センター ミューズで僕と握手! • 埼玉県に縁があったりなかったりするiOS アプリ開発者の勉強会 • 2026/11/21(土)14:00~18:00 • 所沢市民文化センター ミューズ • 西武新宿線 航空公園駅 徒歩10分 • 参加者募集中! • Embedded SwiftでGBAのゲームを作る ワークショップやります

Slide 76

Slide 76 text

Appendix

Slide 77

Slide 77 text

free-listヒープの設計: boundary tag Knuthのアルゴリズム 使用中ブロック 空きブロック ヘッダ 4B:size|使用中=1 ヘッダ 4B:size|使用中=0 nextFree 4B (フリーリストの次の空きへ) ペイロード (malloc が返す領域) (未使用領域) フッタ 4B:size のコピー フッタ 4B:size のコピー size を先頭(ヘッダ)と末尾(フッタ)の両方に持つ = boundary tag。 最小ブロックは ヘッダ4 + nextFree4 + パディング4 + フッタ4 = 16B 次の空きブロックへ

Slide 78

Slide 78 text

隣接ブロックの結合(コアレッシング) freeしたとき空きブロックを結合する(デフラグ対策) free(B) の瞬間 空きブロック A free() されたブロック B Bの直前4Bを読む = Aのフッタ → Aのsizeが分かる → Aの先頭に O(1) で戻れる 空きブロック C Bの直後4Bを読む = Cのヘッダ → Cが空きだと分かる → そのまま後ろにも結合 フリーリストを走査せず、隣の4Bを読むだけで前後と結合できる 結合後 空きブロック A + B + C(1つに結合)

Slide 79

Slide 79 text

リンカシンボルをSwiftで読み取る ̲̀heap̲start̀ / ̲̀heap̲end̀ • ヒープの範囲を決めているのはリンカスクリプト(Swiftのコードではない) • どうにかしてSwift側で読み取る必要がある • ̀@̲extern(c)̀ で宣言してアドレスだけ取る(型はUInt8だが何でもよい) @_extern(c, "_heap_start") nonisolated(unsafe) var _heap_start: UInt8 @_extern(c, "_heap_end") nonisolated(unsafe) var _heap_end: UInt8 func linkerSymbolAddress(_ symbol: inout UInt8) -> UInt { withUnsafePointer(to: &symbol) { UInt(bitPattern: $0) } } heapStart = linkerSymbolAddress(&_heap_start) heapEnd = linkerSymbolAddress(&_heap_end) // 0x3FC800F8 // 0x3FCD8700 ≒ 353KB

Slide 80

Slide 80 text

E-InkとSDカードが同じバスを共有している問題 SPIは同時に一つのデバイスしか話せない • Xteink X4では一つのSPIインターフェースにE-InkパネルとSDカードの2つの デバイスが接続されている • フレームバッファ転送中はSDは読めないし、逆もまた然り • 間違えて両方のCSをLOWにするとおかしなことになる

Slide 81

Slide 81 text

電池が一日で切れてしまう 公式FWはそんなことなかった • バッテリーは650mAh • 最初はディープスリープのみで電源を切る機能がなかった • スリープ/レジュームから電源断/再起動に変更 • 電池が持つ公式FWもこうなっているはず

Slide 82

Slide 82 text

SDカードに状態を書き込んで復元する SDカード以外はすべて消える世界 • 電源断でSRAMのデータは飛ぶのでSDカードに状態を書き込んでおく • どの画面か • 読書画面ならどこまで読んでいるか